diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 3cadbfe20..e8899e09e 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -625,7 +625,7 @@ in an unattended run, never a warning; interactive runs follow the repair rule a Write `$_AP/gate.json` (shape: `gstack-gate --help`): one item per User Challenge (`uc`, approval; `why` = you said / both recommend / why / blind spots / cost if wrong), per taste decision (`d`, auto; `why` = principle and the alternative's -impact) and `plan` (approval: a) approve as-is (recommended) b) with overrides +impact) and `p1` (approval: a) approve as-is (recommended) b) with overrides c) interrogate d) revise e) reject). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json` and append its `decisions` output to `$_AP/decisions.jsonl`. Write the full review to `$_AP/review-record.md` and its Plan Summary alone to `$_AP/summary.md`: @@ -663,8 +663,8 @@ verbatim (URGENT block, plain summary, numbered decisions seven per page with ea auto default, review-record path; `--page N` for later pages), never the full review. Parse the reply: `~/.claude/skills/gstack/bin/gstack-gate parse $_AP/gate.json --reply "" --gate-rev `; -unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `plana` -without overrides = A; `d` overrides = B; `uc` overrides = B2; `planc`/`pland`/`plane` +unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `p1a` +without overrides = A; `d` overrides = B; `uc` overrides = B2; `p1c`/`p1d`/`p1e` = C/D/E. Under `SESSION_KIND: unattended` Read `sections/unattended.md`, end here with its `GSTACK_RESULT` line and never approve. diff --git a/autoplan/SKILL.md.tmpl b/autoplan/SKILL.md.tmpl index 842cc7610..c5f48565f 100644 --- a/autoplan/SKILL.md.tmpl +++ b/autoplan/SKILL.md.tmpl @@ -360,7 +360,7 @@ there with each incomplete item. Write `$_AP/gate.json` (shape: `gstack-gate --help`): one item per User Challenge (`uc`, approval; `why` = you said / both recommend / why / blind spots / cost if wrong), per taste decision (`d`, auto; `why` = principle and the alternative's -impact) and `plan` (approval: a) approve as-is (recommended) b) with overrides +impact) and `p1` (approval: a) approve as-is (recommended) b) with overrides c) interrogate d) revise e) reject). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json` and append its `decisions` output to `$_AP/decisions.jsonl`. Write the full review to `$_AP/review-record.md` and its Plan Summary alone to `$_AP/summary.md`: @@ -398,8 +398,8 @@ verbatim (URGENT block, plain summary, numbered decisions seven per page with ea auto default, review-record path; `--page N` for later pages), never the full review. Parse the reply: `~/.claude/skills/gstack/bin/gstack-gate parse $_AP/gate.json --reply "" --gate-rev `; -unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `plana` -without overrides = A; `d` overrides = B; `uc` overrides = B2; `planc`/`pland`/`plane` +unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `p1a` +without overrides = A; `d` overrides = B; `uc` overrides = B2; `p1c`/`p1d`/`p1e` = C/D/E. Under `SESSION_KIND: unattended` Read `sections/unattended.md`, end here with its `GSTACK_RESULT` line and never approve. diff --git a/changelog.d/autoplan-gate-accepts-plan-approval-replies.md b/changelog.d/autoplan-gate-accepts-plan-approval-replies.md new file mode 100644 index 000000000..6962d1e14 --- /dev/null +++ b/changelog.d/autoplan-gate-accepts-plan-approval-replies.md @@ -0,0 +1 @@ +- **/autoplan's final approval gate renders again, and replies to its plan-approval item are accepted.** The skill told the agent to name that gate item `plan` and to read replies such as `plana` and `planc`, but `gstack-gate` only accepts an id made of a lowercase label and a number, so `gstack-gate render` refused the whole gate and those replies never parsed (#4181). The skill now names it `p1`, with replies `p1a`, `p1c`, `p1d` and `p1e`, the id gstack's own gate builder and docs/unattended.md already use. A test now builds the gate exactly as the skill describes it and runs it through `gstack-gate render` and `parse`. Reported by @exGeni in #4181; fixes proposed by @ElNeMeR70 (#4189) and @szsunyuan (#4191). diff --git a/test/fixtures/skill-md-size-allowlist.json b/test/fixtures/skill-md-size-allowlist.json index a1cf9fd52..5bd4e25fd 100644 --- a/test/fixtures/skill-md-size-allowlist.json +++ b/test/fixtures/skill-md-size-allowlist.json @@ -2,53 +2,53 @@ "_comment": "Generated SKILL.md files over the limit, pinned at their exact character count (#3199). test/skill-md-size-guard.test.ts fails when a pinned skill grows past its pin or shrinks below it: lower the pin when a skill shrinks and delete it once the skill is within the limit. Raising a pin is an owner decision. The shared-preamble hoist (GSTA-96) moved every tier 2+ skill's shared preamble into sections/preamble.md; the pins below are what remains.", "limit": 100000, "pins": { - "agy/gstack-autoplan": 109585, + "agy/gstack-autoplan": 109575, "agy/gstack-plan-design-review": 109096, "agy/gstack-plan-devex-review": 114924, "agy/gstack-plan-eng-review": 143580, "agy/gstack-review": 104735, - "codex/gstack-autoplan": 108328, + "codex/gstack-autoplan": 108318, "codex/gstack-plan-design-review": 110453, "codex/gstack-plan-devex-review": 116432, "codex/gstack-plan-eng-review": 144745, "codex/gstack-review": 104119, - "copilot/gstack-autoplan": 109028, + "copilot/gstack-autoplan": 109018, "copilot/gstack-plan-design-review": 109362, "copilot/gstack-plan-devex-review": 114386, "copilot/gstack-plan-eng-review": 143745, "copilot/gstack-review": 104316, - "cursor/gstack-autoplan": 108260, + "cursor/gstack-autoplan": 108250, "cursor/gstack-plan-design-review": 107904, "cursor/gstack-plan-devex-review": 113656, "cursor/gstack-plan-eng-review": 142335, "cursor/gstack-review": 122788, - "factory/gstack-autoplan": 110538, + "factory/gstack-autoplan": 110528, "factory/gstack-plan-design-review": 111002, "factory/gstack-plan-devex-review": 117019, "factory/gstack-plan-eng-review": 145443, "factory/gstack-review": 126561, - "gbrain/gstack-autoplan": 108667, + "gbrain/gstack-autoplan": 108657, "gbrain/gstack-plan-eng-review": 124963, - "gemini/gstack-autoplan": 108818, + "gemini/gstack-autoplan": 108808, "gemini/gstack-plan-design-review": 109190, "gemini/gstack-plan-devex-review": 114214, "gemini/gstack-plan-eng-review": 143578, "gemini/gstack-review": 104138, - "hermes/gstack-autoplan": 108621, + "hermes/gstack-autoplan": 108611, "hermes/gstack-plan-eng-review": 124999, - "kiro/gstack-autoplan": 108034, + "kiro/gstack-autoplan": 108024, "kiro/gstack-plan-design-review": 107750, "kiro/gstack-plan-devex-review": 113508, "kiro/gstack-plan-eng-review": 142197, "kiro/gstack-review": 122612, - "openclaw/gstack-autoplan": 108490, + "openclaw/gstack-autoplan": 108480, "openclaw/gstack-plan-eng-review": 123478, - "opencode/gstack-autoplan": 109009, + "opencode/gstack-autoplan": 108999, "opencode/gstack-plan-design-review": 109218, "opencode/gstack-plan-devex-review": 114264, "opencode/gstack-plan-eng-review": 143604, "opencode/gstack-review": 123410, - "slate/gstack-autoplan": 108147, + "slate/gstack-autoplan": 108137, "slate/gstack-plan-design-review": 107827, "slate/gstack-plan-devex-review": 113582, "slate/gstack-plan-eng-review": 142266, diff --git a/test/gate-list.test.ts b/test/gate-list.test.ts index 880c377ca..13c446077 100644 --- a/test/gate-list.test.ts +++ b/test/gate-list.test.ts @@ -250,3 +250,32 @@ describe('bin/gstack-gate', () => { expect(pending.every(r => r.status === 'pending')).toBe(true); }); }); + +describe('the /autoplan Phase 4 gate as documented (#4181)', () => { + // Value: protects=the gate /autoplan tells the agent to write renders, and the documented plan-approval replies parse, through bin/gstack-gate; + // fails_when=the template names a gate item id or reply token outside the gate grammar; why_new=the grammar cases build their own lists; seam=none + test('the template\'s plan-approval item renders and each documented reply for it parses to its option', () => { + const tmpl = fs.readFileSync(path.join(ROOT, 'autoplan', 'SKILL.md.tmpl'), 'utf8').replace(/\s+/g, ' '); + const write_ = /Write `\$_AP\/gate\.json`.*?gstack-gate render/.exec(tmpl)?.[0] ?? ''; + const item = /and `([^`]+)` \(approval: (a\) .*? e\) \w+)\)/.exec(write_); + expect(item).not.toBeNull(); + const id = item![1]!; + const parts = item![2]!.split(/(?:^| )([a-e])\) /).slice(1); + const options = parts.flatMap((key, i) => (i % 2 ? [] : [{ key, text: parts[i + 1]! }])); + expect(options.map(o => o.key)).toEqual(['a', 'b', 'c', 'd', 'e']); + const gate: GateList = { schema_version: 1, run: 'autoplan-4181', gate_rev: 1, items: [ + { id: 'uc1', title: 'User challenge', options: ab, recommended: 'a', kind: 'approval' }, + { id: 'd1', title: 'Taste decision', options: ab, recommended: 'a', kind: 'auto' }, + { id, title: 'Plan approval', options, recommended: 'a', kind: 'approval' }, + ] }; + const file = write('gate.json', gate); + const parseText = /Parse the reply:.*?SESSION_KIND/.exec(tmpl)?.[0] ?? ''; + const replies = [...parseText.matchAll(/`([a-z0-9]+)`/g)].map(m => m[1]!).filter(t => t.startsWith(id) && t.length === id.length + 1); + expect(replies.map(t => t.slice(-1))).toEqual(['a', 'c', 'd', 'e']); + + const calls = [['render', file], ...replies.map(reply => ['parse', file, '--reply', reply, '--gate-rev', '1', '--json'])]; + const results = calls.map(args => { const r = run(args); return { args: args.filter(a => a !== file).join(' '), code: r.code, err: r.err, out: r.out }; }); + expect(results.map(({ args, code, err }) => ({ args, code, err }))).toEqual(results.map(({ args }) => ({ args, code: EXIT.ok, err: '' }))); + expect(results.slice(1).map(r => chosen(JSON.parse(r.out))[id])).toEqual(['a', 'c', 'd', 'e']); + }); +});