fix(autoplan): revise naming a non-pending decision refuses with AMENDMENT_UNKNOWN, not PLAN_AMEND_PENDING

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-13 01:46:07 +00:00
1 parent 5d5f41fc82
commit 0736955c85
6 files changed
+32 -2

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
- **`submit --record <file>` binds a review a subagent wrote by hand.** It accepts the canonical findings fence, one findings table (Severity and Title columns), one severity-tagged list (`- [High] title`) or a `No findings.` line, and it never guesses a severity. A record whose `INPUT:` line is present but wrong is refused. A record without one binds only with `--attest "<reason>" --attempt <id>`; it is labeled `receipt: attested`, `validate` counts it (`attested=<n>`), and it is refused if the plan was revised after that attempt was dispatched. `--attempt` is optional when exactly one attempt is open. `submit --voice outside --unavailable|--failed "<reason>"` closes a phase whose outside voice cannot run, and `next` and `resume` never redispatch it.
- **`revise --plan <v2>` reviews a revised plan in the same run.** Every phase whose reviewed Implementation-plan hash differs reopens (plus Eng) on the new snapshot; at the gate it starts the next gate revision. The revise is one recoverable step (`resume` finishes an interrupted one), and archives never overwrite each other. `next` prints `PLAN_CHANGED` and `answer` prints `PLAN_SOURCE_CHANGED` when the plan file moved since its snapshot.
- **Accepted plan changes must reach the plan.** When the owner accepts a change at the gate, `answer` prints one `APPLY:` line per change and `NEXT: gstack-autoplan revise …`. `next` refuses with `PLAN_AMEND_PENDING` until `revise --incorporates <decision ids>` binds the edit, or the owner withdraws a change with `--withdraw <id>`. A run cannot export as complete with a change pending.
- **Accepted plan changes must reach the plan.** When the owner accepts a change at the gate, `answer` prints one `APPLY:` line per change and `NEXT: gstack-autoplan revise …`. `next` refuses with `PLAN_AMEND_PENDING` until `revise --incorporates <decision ids>` binds the edit, or the owner withdraws a change with `--withdraw <id>`. A run cannot export as complete with a change pending. Naming an id that is not pending (never accepted as a plan change, or already settled) refuses with `AMENDMENT_UNKNOWN` instead of asking for the edit you just made.
- **User-challenge defaults follow agreeing voices, and say why.** When both voices flag the same finding as a change to scope, cost or direction (the same `changes` value) and both give a remedy, the item now recommends the change. Option b shows both remedies word for word, and the default is labeled `recommended: b (both voices; remedies shown)`. Anything weaker keeps your direction, labeled `(plan direction; voices disagree)`. **`all` now accepts those agreed changes**; reply `all as-is` to keep every direction in one token. p1's recommended text names the changes it would apply.
- **Reruns stop churning.** Reviewers set `user_challenge` only for a change to scope, cost or direction (`changes`). On a rerun, a challenge without one becomes the agent's own decision and does not reopen anything. Rerun prompts carry a `DECIDED AT THE GATE` block, and answers to items from phases that did not rerun carry forward instead of being asked again. A partial reply never overwrites an earlier answer, and the gate stays open until every approval is settled.
- **A plan without the two sections is scaffolded, not refused with empty output.** The first `next` explains `PLAN_SECTIONS_INVALID`; `next --scaffold` reviews a copy with the missing heading added and never edits your file.
+1
View File
@@ -176,6 +176,7 @@ gstack_result_anchor() {
DRAFT_DIRECTION_UNRESOLVED) printf '%s' 'draft-direction-unresolved' ;;
PLAN_SECTIONS_INVALID) printf '%s' 'plan-sections-invalid' ;;
PLAN_AMEND_PENDING) printf '%s' 'plan-amend-pending' ;;
AMENDMENT_UNKNOWN) printf '%s' 'amendment-unknown' ;;
GSTACK_VERSION_CHANGED) printf '%s' 'gstack-version-changed' ;;
ARTIFACT_GATE_UNSETTLED) printf '%s' 'artifact-gate-unsettled' ;;
UPGRADE_REFUSED) printf '%s' 'upgrade-refused' ;;
+12
View File
@@ -2570,6 +2570,18 @@ not change; an id you do not name stays pending. To drop an accepted change
instead, `gstack-autoplan revise --out <dir> --withdraw <decision id>` records
the withdrawal in the journal.
<a id="amendment-unknown"></a>
### `AMENDMENT_UNKNOWN`
**Meaning.** `gstack-autoplan revise --incorporates` or `--withdraw` named a
decision id that is not a pending amendment: the owner never accepted it as a
plan change at the gate (for example a gate answered before gstack recorded
amendments), or it was already incorporated or withdrawn. Nothing was changed.
**Fix.** Name only the ids the refusal lists after `pending:`. With none
pending, revise without `--incorporates`:
`gstack-autoplan revise --out <dir> --plan <file>`.
<a id="gstack-version-changed"></a>
### `GSTACK_VERSION_CHANGED`
+1 -1
View File
@@ -56,7 +56,7 @@ export function reviseRun(o: ReviseOptions): ReviseResult {
if (['complete', 'incomplete', 'refused'].includes(state.status)) throw new RunError('PHASE_NOT_AWAITING', `run is ${state.status}; revise works on a running or gated run`, 3);
const pending = pendingAmendments(state).map(a => a.decision);
for (const id of [...(o.incorporates ?? []), ...(o.withdraw ?? [])]) {
if (!pending.includes(id)) throw new RunError('PLAN_AMEND_PENDING', `${id} is not a pending amendment (pending: ${pending.join(', ') || 'none'})`, 3);
if (!pending.includes(id)) throw new RunError('AMENDMENT_UNKNOWN', `${id} is not a pending amendment (pending: ${pending.join(', ') || 'none'})`, 3);
}
const at = now();
if (!o.plan) {
+5
View File
@@ -476,6 +476,11 @@ export const RESULT_CODES = {
summary: 'the owner accepted plan changes at the gate that the plan has not absorbed yet',
fix: 'edit the plan, then `gstack-autoplan revise --out <dir> --plan <file> --incorporates <decision id,...>`; or drop one with `gstack-autoplan revise --out <dir> --withdraw <decision id>`',
},
AMENDMENT_UNKNOWN: {
anchor: 'amendment-unknown',
summary: '`revise --incorporates` or `--withdraw` named a decision that is not a pending amendment (never accepted as a plan change, or already incorporated or withdrawn)',
fix: 'name only the ids the refusal lists after `pending:`; with none pending, drop --incorporates and run `gstack-autoplan revise --out <dir> --plan <file>`',
},
GSTACK_VERSION_CHANGED: {
anchor: 'gstack-version-changed',
summary: 'the run directory was written by a different gstack version',
+12
View File
@@ -124,6 +124,18 @@ describe('revise', () => {
expect(res.status).toBe(1);
expect(res.stderr).toMatch(/revised after .*dispatched.*\(RESULT_RECEIPT_MISSING\)$/m);
});
test('naming a decision that is not a pending amendment refuses with AMENDMENT_UNKNOWN and never asks for the edit just made', () => {
const s = start();
const before = fs.readFileSync(path.join(s.out, 'run.json'), 'utf8');
for (const args of [['--plan', v2File(s.dir), '--incorporates', 'd1b'], ['--withdraw', 'd1b']]) {
const res = r.ap(['revise', '--out', s.out, ...args]);
expect(res.status).toBe(3);
expect(res.stderr).toMatch(/d1b is not a pending amendment \(pending: none\).*\(AMENDMENT_UNKNOWN\)$/m);
expect(res.stderr).not.toContain('PLAN_AMEND_PENDING');
expect(res.stderr).not.toContain('edit the plan');
}
expect(fs.readFileSync(path.join(s.out, 'run.json'), 'utf8')).toBe(before);
});
});
describe('drift hints', () => {