Make autoplan's plan-approval gate item p1 so gstack-gate accepts it (#4181)

/autoplan Phase 4 told the agent to write a gate item with id `plan` and
to read replies `plana`/`planc`/`pland`/`plane`. gstack-gate only accepts
ids of a lowercase label plus a number, so render refused the gate and the
replies never parsed. Rename it to `p1` (the id lib/autoplan-gate.ts and
docs/unattended.md already use), lower the autoplan size pins by the 10
characters saved, and add a test that builds the gate from the template text
and runs it through bin/gstack-gate render and parse.

Co-authored-by: Eng. Mahmoud Bek ElNemer <83190847+ElNeMeR70@users.noreply.github.com>
Co-authored-by: Yuan S. <3579304+szsunyuan@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
committed 2026-11-09 14:08:15 +00:00
1 parent 6473a17de4
commit 7b97904bc8
5 files changed
+48 -18

No files matched your search

+3 -3
View File
@@ -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<n>`, approval; `why` = you said / both recommend / why / blind spots / cost if
wrong), per taste decision (`d<n>`, 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 "<text>" --gate-rev <rev>`;
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `plana`
without overrides = A; `d<n>` overrides = B; `uc<n>` overrides = B2; `planc`/`pland`/`plane`
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `p1a`
without overrides = A; `d<n>` overrides = B; `uc<n>` 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.
+3 -3
View File
@@ -360,7 +360,7 @@ there with each incomplete item.
Write `$_AP/gate.json` (shape: `gstack-gate --help`): one item per User Challenge
(`uc<n>`, approval; `why` = you said / both recommend / why / blind spots / cost if
wrong), per taste decision (`d<n>`, 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 "<text>" --gate-rev <rev>`;
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `plana`
without overrides = A; `d<n>` overrides = B; `uc<n>` overrides = B2; `planc`/`pland`/`plane`
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `p1a`
without overrides = A; `d<n>` overrides = B; `uc<n>` 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.
@@ -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).
+12 -12
View File
@@ -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,
+29
View File
@@ -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']);
});
});