Constraint-rich prompts

State scope, non-goals, compatibility limits, and acceptance commands in prompts that bound generated changes.

level intermediate time 15 min at Standard depth
version Node 24
what

A constraint-rich prompt defines the requested outcome and the boundary inside which a generated change is allowed to solve it.

trap

A feature-only prompt leaves adjacent refactors, dependency changes, compatibility choices, and proof of completion to the agent’s guesses.

fix

Name the write scope, non-goals, protected behavior, exact acceptance commands, and a stop condition for conflicts or missing evidence.

What it is and why it exists

A constraint-rich prompt is a bounded change request. It states what must become true, where edits may occur, which plausible outcomes are intentionally excluded, what existing behavior remains protected, and which evidence decides completion. The result is still a natural-language request, but its important choices are explicit enough to review.

A desired feature alone describes only one point in a large solution space. “Add a coupon limit” could lead an agent to edit validation, replace a dependency, rename a public function, migrate stored data, reformat nearby files, or weaken a test that blocks its preferred implementation. Each move may look locally reasonable even when only two files were meant to change.

The issue is not that an agent lacks initiative. Codebases contain several plausible patterns, incomplete documentation, and conflicting local examples. Unstated constraints force the model to infer product and maintenance decisions from that evidence, so fluent output can conceal a choice the requester never authorized.

Four fields do most of the bounding work:

FieldQuestion it answersConcrete form
ScopeWhat may be changed?named files, symbols, directories, generated artifacts, and a file budget
Non-goalsWhich nearby outcomes are excluded?no dependency upgrade, schema migration, broad rename, or unrelated cleanup
CompatibilityWhich existing interactions stay valid?public signature, error type, serialized shape, runtime, or CLI exit code
AcceptanceWhat evidence decides completion?exact commands, expected exit status, focused cases, and diff checks

Scope is primarily an edit boundary, not a reading boundary. An agent may need to inspect callers, configuration, tests, or instructions outside the allowed write set to understand the change. A good prompt distinguishes “you may inspect” from “you may modify” so investigation remains useful without silently widening the patch.

A non-goal is an active exclusion, not filler. It records an attractive neighboring change that could otherwise be mistaken for part of the task. “Do not replace the validation library” is useful when the repository contains both an old helper and a new library; “do not change anything unnecessary” merely repeats a judgment the agent already has to make.

Compatibility limits identify the protected portion of an API contract . They might preserve positional and keyword calls, JSON fields, exception classes, event order, or the minimum runtime. “No breaking changes” is too broad until the prompt names the consumers and observations for which a breaking change is forbidden.

Acceptance commands make the finish condition reproducible. They should be copied from repository scripts or verified locally, include any required working directory, and say what counts as success. A command’s exit code is evidence; the agent’s summary that “tests should pass” is not.

Constraint-rich does not mean long. A small private edit may need one allowed file, one protected behavior, and one test command. Cross-package work, persisted data, public APIs, authentication, payments, or migrations need more explicit boundaries because the cost of a plausible wrong assumption is higher.

These prompts are especially useful when an agent can edit autonomously, run tools, and continue across several steps. The longer the action loop, the more opportunities there are for scope drift or for an early assumption to shape later edits. The prompt gives each step a stable reference point.

A prompt is not an authorization system. Saying “do not read secrets” cannot revoke filesystem access, and “only edit src/checkout/” cannot enforce a sandbox. Use permissions, isolation, review gates, and deterministic validation for security boundaries; use prompt constraints to communicate intended work.

How it works

Start from repository evidence rather than drafting constraints from memory. Identify the active symbol, its callers, the failing behavior, local instructions, package scripts, runtime target, and current diff. This turns a generic preference such as “keep compatibility” into a claim about actual consumers.

A practical drafting sequence is:

  1. State one observable outcome and the user or caller that needs it.
  2. Name the smallest justified write surface and any generated files that must not be edited directly.
  3. Exclude adjacent work that is plausible in this repository but not required now.
  4. List protected compatibility dimensions and intentional behavior changes separately.
  5. Give exact acceptance commands plus decisive positive and negative cases.
  6. Define when the agent must stop and report instead of guessing or widening scope.

The order matters because later constraints qualify the outcome. “Return zero for an explicit page size of zero” is a behavior requirement. “Keep the existing one-argument export” protects compatibility, while “do not change callers” narrows the implementation surface. One clause cannot safely stand in for the others.

Bound the write surface

File scope is the easiest boundary to inspect in a diff. Name exact files for a surgical change, or named directories and file kinds when tests or generated snapshots may be added. If the task permits at most three changed files, state that change budget so a fourth file triggers a discussion rather than an invisible expansion.

Paths alone can be too rigid. A prompt that allows src/checkout/discount.mjs but forgets the matching test file makes proof impossible. Prefer a small required set plus a conditional rule such as “you may add one test fixture under test/checkout/; report before changing any manifest, lockfile, schema, or public type.”

Also state the semantic boundary. “Keep pricing policy in discount.mjs; do not move network calls into it” protects an architecture boundary that a path list cannot express. An edit may remain inside an allowed directory while reversing dependency direction or mixing policy with I/O.

Read access usually needs a wider radius than write access. Ask the agent to search all call sites and inspect package instructions, but constrain modifications to the named surface. If investigation proves the fix requires another owner or migration, the stop condition should require a report with evidence before any expansion.

State useful non-goals

Non-goals should be adjacent, tempting, and testable in the final diff. Common examples are upgrading a package, changing the database schema, renaming unrelated symbols, introducing a framework, reformatting untouched code, or fixing a second bug discovered nearby. Each one blocks a realistic form of scope creep.

Avoid predicting every possible unwanted act. A hundred prohibitions become hard to reconcile and easy to skim. Select the few exclusions suggested by repository evidence, then add a general stop rule for any required change outside the permitted boundary.

Negative wording needs a positive owner. “Do not edit generated files” should say which source owns them and which generation command may update them. “Do not change the API” should name the exact exported symbol and protected observations. Otherwise the agent knows where not to move but not where the legitimate change belongs.

Specify compatibility by dimension

Backward compatibility is always relative to a set of previously valid interactions. Name that set. For a JavaScript function it may include arity-sensitive reflection, default behavior when an argument is omitted, acceptance of zero, thrown error classes, returned object fields, and asynchronous timing.

Different compatibility dimensions can point in different directions:

DimensionExample constraintEvidence
Sourcekeep parsePageSize(value) callable with one argumentexisting caller tests and call-site search
Behavioralomitted input still returns 25; explicit 0 remains 0named boundary cases
Datado not add, remove, or rename stored JSON fieldsfixture round trip and schema diff
Runtimeuse APIs available in Node 24; add no polyfilldeclared toolchain and clean install
Operationalpreserve exit codes and stderr format used by automationCLI integration test

Separate intentional incompatibility from protected behavior. If invalid strings used to coerce to numbers and must now fail, say so explicitly. A prompt cannot simultaneously demand strict rejection and complete behavioral compatibility with callers that relied on coercion.

Unknown consumers make universal compatibility claims impossible. Ask for a call-site search, public-surface inventory, or characterization test, then scope the promise to evidence found. If the evidence is incomplete, require the agent to label the uncertainty rather than claim “no regressions.”

Make acceptance executable

An acceptance command should be copyable, runnable from a named directory, and capable of failing for the defect in question. Prefer node test/checkout/discount.test.mjs over “run relevant tests.” Add the repository’s type, lint, build, or formatting commands only when they apply to the changed surface.

Commands and behavioral criteria complement each other. A broad test suite may pass without checking a new zero boundary, while a focused test may miss a type or packaging failure. State the decisive cases in words and name the commands that execute them.

Include negative acceptance evidence for forbidden change. git diff --check catches whitespace errors, a changed-file inspection catches scope drift, and a manifest diff catches an accidental dependency. These checks do not prove semantics, but they close common gaps left by behavior tests.

Define what to do when a command cannot run. The agent should report the exact command, exit status, and blocker; it must not substitute “looks correct” or silently omit the check. If an unrelated baseline failure exists, require evidence that distinguishes it from a regression caused by the patch.

Run cheap, focused checks before expensive broad ones. This shortens feedback without weakening the final gate. The prompt should still say that completion requires every named command, not whichever early check happens to pass.

Resolve conflicts before editing

Constraints form one set, so contradictions must be handled together. An exact two-file scope can conflict with a requirement to regenerate a lockfile. Preserving an error class can conflict with adopting a library whose public wrapper throws another class. More detail helps only when the details can all be true.

Give the agent a precedence and escalation rule: repository instructions and safety boundaries apply first; explicit task constraints qualify the requested behavior; when two requirements cannot both hold, stop with the smallest conflicting set and one or two supported options. Do not invite the agent to pick whichever clause seems more important.

Ask for assumptions to be visible before they become code. A short pre-edit summary can list the intended files, protected behavior, expected commands, and unresolved questions. For a straightforward task, this can be a compact checklist rather than a ceremonial plan.

A reusable prompt shape

The following fields are a starting shape, not a mandatory form:

  • Task: one observable behavior change, its target symbol, and the affected caller.
  • Evidence: current behavior, failing example, relevant source locations, and repository instructions.
  • Scope: allowed files or directories, file budget, generated-file policy, and allowed reads.
  • Non-goals: nearby refactors, migrations, dependency changes, or product behavior intentionally excluded.
  • Compatibility: protected signatures, failures, data shapes, runtime versions, and consumers.
  • Acceptance: exact commands, decisive cases, diff checks, and expected success conditions.
  • Stop condition: conflicts, missing authority, unavailable evidence, or a necessary write outside scope.

Write each field in repository language. If the package calls its boundary an adapter, reuse that word and name the file. If the test script is pnpm test:checkout, copy it exactly; inventing a generic npm test weakens an otherwise precise request.

Examples

These examples turn prompt clauses into checks that can reject an attractive but unauthorized patch. The scripts are small models of repository gates, not claims that prose alone enforces an agent. Every output was produced locally with Node 24.

Rejecting edit-scope expansion

Suppose the task is to adjust checkout discounts. The prompt allows implementation and test files under two named roots, caps the patch at three files, and marks package metadata as protected. Its non-goals exclude documentation cleanup and dependency changes.

scope_guard.mjs
const contract = {
  allowedRoots: ["src/checkout/", "test/checkout/"],
  protectedFiles: new Set(["package.json", "pnpm-lock.yaml"]),
  maxChangedFiles: 3,
};

function assessChange(paths) {
  const violations = [];
  if (paths.length > contract.maxChangedFiles) {
    violations.push(`file budget exceeded: ${paths.length}`);
  }
  for (const path of paths) {
    if (contract.protectedFiles.has(path)) {
      violations.push(`protected file: ${path}`);
    } else if (!contract.allowedRoots.some((root) => path.startsWith(root))) {
      violations.push(`outside scope: ${path}`);
    }
  }
  return violations;
}

const proposals = [
  ["focused", ["src/checkout/discount.mjs", "test/checkout/discount.test.mjs"]],
  ["expanded", ["src/checkout/discount.mjs", "README.md", "package.json"]],
];

for (const [name, paths] of proposals) {
  const violations = assessChange(paths);
  console.log(`${name}: ${violations.length === 0 ? "PASS" : `FAIL (${violations.join("; ")})`}`);
}
focused: PASS
expanded: FAIL (outside scope: README.md; protected file: package.json)

The focused proposal satisfies the path rules. The expanded proposal has only three files, but two files violate more specific boundaries. A file budget is therefore not a substitute for an allowlist or protected-file rule.

The prompt should still permit investigation outside these roots. Reading package.json to discover the test command is different from changing it. If the implementation truly requires a dependency, the correct outcome is an out-of-scope report, not a hidden manifest edit.

Protecting a compatibility boundary

Now the task changes parsePageSize so invalid values fail explicitly. The prompt preserves a one-argument call, keeps undefined mapped to 25, keeps explicit 0 valid, caps the value at 100, and requires the existing RangeError message. Those clauses prevent a generated value || 25 shortcut from changing zero.

compatibility_matrix.mjs
function parsePageSize(value) {
  if (value === undefined) return 25;
  if (!Number.isSafeInteger(value) || value < 0 || value > 100) {
    throw new RangeError("pageSize must be an integer from 0 to 100");
  }
  return value;
}

const cases = [
  ["missing", undefined],
  ["zero", 0],
  ["maximum", 100],
  ["too large", 101],
];

for (const [name, value] of cases) {
  try {
    console.log(`${name}: ${parsePageSize(value)}`);
  } catch (error) {
    console.log(`${name}: ${error.name}: ${error.message}`);
  }
}
missing: 25
zero: 0
maximum: 100
too large: RangeError: pageSize must be an integer from 0 to 100

The cases distinguish omission from an explicit falsy value and place examples on the upper boundary. They also make the error channel and message observable. “Keep old behavior” would not reveal which of these interactions the requester actually protects.

An acceptance clause can name node compatibility_matrix.mjs and require exit status zero, but the printed rows remain useful review evidence. If the real repository already has a focused test command, the prompt should invoke that command rather than create a parallel ad hoc harness.

Refusing incomplete acceptance evidence

The final example models a completion gate. The prompt requires a focused behavior check, lint over the changed surface, and a diff hygiene check. An agent may report completion only when evidence contains every command with exit code zero.

acceptance_evidence.mjs
const requiredCommands = [
  "node test/checkout/discount.test.mjs",
  "pnpm eslint src/checkout test/checkout",
  "git diff --check",
];

function releaseDecision(results) {
  const evidence = new Map(results.map((result) => [result.command, result.exitCode]));
  let ready = true;

  for (const command of requiredCommands) {
    const exitCode = evidence.get(command);
    const state = exitCode === undefined ? "MISSING" : exitCode === 0 ? "PASS" : `FAIL exit=${exitCode}`;
    console.log(`${state}: ${command}`);
    if (exitCode !== 0) ready = false;
  }

  console.log(`decision: ${ready ? "READY" : "NOT READY"}`);
}

releaseDecision([
  { command: "node test/checkout/discount.test.mjs", exitCode: 0 },
  { command: "pnpm eslint src/checkout test/checkout", exitCode: 1 },
]);
PASS: node test/checkout/discount.test.mjs
FAIL exit=1: pnpm eslint src/checkout test/checkout
MISSING: git diff --check
decision: NOT READY

A passed focused test cannot cancel a failed lint command, and an omitted diff check is not a pass. The useful agent response includes the real failure and missing evidence, then either fixes the in-scope cause or stops if resolving it requires work outside the prompt.

Exact command strings also prevent evidence substitution. pnpm eslint src/checkout test/checkout and an editor’s “no visible warnings” are not equivalent checks. If the command itself is stale, changing the acceptance contract requires an explicit decision rather than quiet replacement.

Pitfalls

Adding constraints that cannot all hold

Fix: run a satisfiability pass before editing. Pair each required outcome with the files and commands it entails. If two clauses conflict, stop with the smallest conflicting set and ask the owner to relax or split one requirement.

Writing non-goals as vague restraint

Fix: name two or three repository-specific temptations: no package upgrade, no schema migration, no rename outside src/checkout/, and no cleanup of pre-existing lint findings. Verify them against the final changed-file list and diff.

Making scope either brittle or unlimited

Fix: give a narrow default scope plus conditional expansion. State which extra test or generated files are allowed, which files are protected, and when the agent must report evidence before adding another path.

Using compatibility as a slogan

Fix: list protected consumers and dimensions in a small compatibility matrix. Put an example or characterization test beside each important row, and state any behavior that is intentionally allowed to change.

Overconstraining the implementation

Fix: constrain observable behavior and architecture boundaries first. Prescribe an internal form only when it protects ownership, dependency direction, security, performance, or a stable convention, and state that reason in the prompt.

Treating named commands as executed evidence

Fix: require the command, working directory when relevant, exit code, and concise real output in the completion report. Missing or blocked commands keep the result incomplete; baseline failures need a focused comparison that shows whether the patch changed them.

Deep Constraint interaction and proof closure

Constraint interaction and proof closure

A prompt’s constraints operate as an intersection. A candidate patch must deliver the requested behavior, remain inside the edit scope, preserve every protected interaction, avoid non-goals, and produce the required evidence. Passing four of those sets does not compensate for missing the fifth.

This model changes review questions. Instead of asking only “does the feature work?”, ask whether there exists a patch that satisfies the whole set and whether the submitted patch is one of them. The first question catches contradictory prompts; the second catches scope drift and incomplete verification.

Behavior and scope are independent axes

A behaviorally correct patch can be out of scope. For example, replacing the project’s validation library may implement the new rule perfectly while creating a manifest change and migration work the task excluded. Conversely, a two-line in-scope patch can preserve the diff budget while returning the wrong error type.

Track both axes explicitly:

Review resultBehavior satisfiedScope satisfiedDecision
Intended patchyesyescontinue to compatibility and verification
Scope driftyesnostop or obtain expansion approval
Incomplete fixnoyesrevise within the boundary
Unrelated churnnonodiscard and return to evidence

This is why line count alone is a weak guard. A one-line public signature change can be more disruptive than a twenty-line focused test. File and line budgets are useful tripwires, but semantic ownership and observable behavior still decide whether the patch belongs.

Non-goals differ from negative tests

A non-goal limits the project outcome: “do not migrate existing records.” A negative acceptance case constrains behavior: “an absent country code throws TypeError and writes nothing.” Both use negative language, but they govern different objects.

Confusing them creates gaps. A test cannot prove that no unrelated file was reformatted unless the test observes the diff, and a diff allowlist cannot prove that invalid input leaves storage unchanged. Give project exclusions a change-level check and behavioral exclusions a runtime observation.

Some non-goals are temporary sequencing decisions. “Do not remove the legacy endpoint in this patch” protects a staged migration even if removal is planned later. Record the future owner or follow-up issue outside the current acceptance criteria so the agent does not implement phase two early.

Compatibility needs a protected set

Compatibility review starts by enumerating consumers. Static call sites reveal direct source dependencies; schemas, fixtures, snapshots, integration tests, and automation scripts reveal data and operational dependencies. Runtime reflection, plugins, or external clients may remain unknown and should be reported as uncertainty.

A decision table helps when several input classes and compatibility rules overlap. Rows can cover omitted, zero, maximum, malformed, and legacy forms; columns can record old result, intended result, whether change is allowed, and the evidence command. This makes an intentional change visibly different from an accidental regression.

Do not promise compatibility beyond the observed set without an external contract. If a library is public, Semantic Versioning can communicate intended impact, but a version number does not discover consumers or prove their behavior. The prompt still needs the public surface and migration boundary.

Acceptance commands close claims with evidence

Each acceptance claim should point to an evidence producer. Behavioral claims point to focused tests with independent expected values. Type and build claims point to actual tool invocations. Scope claims point to the changed-file list and diff. Dependency claims point to manifest and lockfile inspection.

The mapping can be recorded before implementation:

ClaimEvidence producerFailure action
Explicit zero remains validnamed boundary testrevise implementation
Public error class is preservedcompatibility testrevise or approve a breaking change
No dependency changedmanifest and lockfile diffremove change or request expansion
Patch passes repository checksexact test, lint, and build commandsreport exit code and fix in scope

Evidence must be independent enough to catch the likely mistake. A generated test that computes its expected value with the implementation’s formula can agree with the same bug. Use literal outcomes, boundary pairs, known fixtures, or an external schema where the contract calls for them.

The command list also needs freshness. Package scripts move, flags change, and a command copied from another directory may exercise no files. Verify commands against the current repository before putting them in the prompt, and treat “no tests found” as evidence failure even if the process exits successfully.

Stop conditions preserve ownership

A stop condition defines when autonomy ends. Useful triggers include a required write outside scope, contradictory requirements, a missing product decision, unavailable credentials, an unsafe migration, or a named acceptance command that cannot be executed. The agent should report the blocker and the smallest evidence-backed options.

Stopping is not failure when proceeding would invent authority. It preserves the requester’s ownership of scope and compatibility decisions. A good blocker report names the exact clause, repository fact, affected file or consumer, and the decision needed to continue.

Avoid a stop rule for every minor uncertainty. The prompt can authorize reversible implementation choices inside the boundary while reserving escalation for choices that change product behavior, public compatibility, security posture, data, cost, or edit scope. This keeps the agent useful without making silence look like consent.

Prompt constraints and enforcement layers

Prompt constraints guide model decisions; deterministic gates inspect outcomes; permissions restrict capabilities. These layers reinforce one another but are not interchangeable. A model can misunderstand prose, a diff check can miss runtime semantics, and a sandbox can prevent an allowed build unless configured correctly.

Use the prompt to express intent and stop conditions. Use tests, schemas, linters, builds, and diff policies to make important properties falsifiable. Use credentials, filesystem boundaries, network policy, and approval gates to prevent unauthorized effects even if the model ignores an instruction.

For high-risk work, connect the layers explicitly. A prompt may say “do not execute migrations,” the tool environment may deny production credentials, and the acceptance gate may inspect migration files. Redundancy is useful because each layer addresses a different failure mode.

Reviewing the prompt itself

Before handing off a task, review the prompt as a small engineering artifact. Check that every constraint has an owner, every compatibility claim has a consumer, every acceptance command exists, and every non-goal can be inspected. Remove decorative restrictions that do not change a decision.

Then test the prompt with two candidate solutions: one obviously too broad and one narrowly correct. The wording should reject the first for a named reason and permit the second without requiring hidden knowledge. If both candidates appear valid, the missing distinction belongs in behavior, compatibility, scope, or evidence.

A constraint-rich prompt remains revisable. Investigation can expose a caller, generated artifact, or repository rule that the requester did not know about. Revision should be explicit: record the evidence, change the affected clause, and rerun the checks derived from it.

Further reading

checkpoint

4 questions · 1 predict-the-output · 1 spot-the-bug

Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?