fix(autoplan): the publication no-op is 'true autoplan-published <phase>'

Measured on Claude Code 2.1.292 in a default-settings interactive session:
a bare ':' Bash command asks for approval ('This command requires
approval'), and after a background reviewer's completion notice starts a
new turn the skill's allowed tools no longer cover it, so the phase-close
no-op prompted the user. 'true' with arguments runs without approval and
prints nothing. The guard only needs a later journaled record, so the
command changes everywhere it is named.

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-07 21:02:16 +00:00
1 parent e811aaf4c0
commit 66f50f1b74
9 files changed
+13 -12

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ interface Reason { disposition: Disposition; text: (d: Detail) => string }
export const FALLBACK = 'Fallback: run /plan-ceo-review, then /plan-devex-review, then /plan-eng-review by hand; ' +
'or run /context-save, start a new Claude Code session (not --resume), run /context-restore, then /autoplan <plan path>.';
const PUBLISH_SEPARATELY = (d: Detail) => `Publish the filled Phase ${d.phase} report as your own parent assistant text in a message ` +
`whose only tool call is the Bash no-op \`: autoplan-published <phase>\`, then make this phase-entry call in a later message.`;
`whose only tool call is the Bash no-op \`true autoplan-published <phase>\`, then make this phase-entry call in a later message.`;
export const REASONS = {
// Journal ownership (lib/claude-public-transcript.ts OwnedTranscriptReason).
+1 -1
View File
@@ -651,7 +651,7 @@ function unverifiedOutput(code: string, detail: Detail, version: string | undefi
const notice = reasonText(code, detail, version);
return { systemMessage: `[autoplan] ${notice}`, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext:
`[autoplan] ${notice} This call proceeds unverified. Publish each completed phase report as your own parent ` +
'assistant text, in a message whose only tool call is the Bash no-op `: autoplan-published <phase>`, before entering the next phase.' } };
'assistant text, in a message whose only tool call is the Bash no-op `true autoplan-published <phase>`, before entering the next phase.' } };
}
function decided(decision: Decision, via: EvaluationPath, version?: string): object {
+1 -1
View File
@@ -42,7 +42,7 @@ preparation does not perform them.
6. **Publish the parent report.** After successful verification, SEND the filled
report below now as visible parent assistant text, using actual findings and voice
statuses, in its own message whose only tool call is the Bash no-op
`: autoplan-published <PHASE>` (no output). The guard counts a report only when a
`true autoplan-published <PHASE>` (no output). The guard counts a report only when a
later record follows it, so this message is the next operation before any next-phase tool call.
Use the packet's `report` fields for this phase, the actual host's reviewer names,
and N/A when either review voice is missing; confirmed counts require both voices.
+1 -1
View File
@@ -40,7 +40,7 @@ preparation does not perform them.
6. **Publish the parent report.** After successful verification, SEND the filled
report below now as visible parent assistant text, using actual findings and voice
statuses, in its own message whose only tool call is the Bash no-op
`: autoplan-published <PHASE>` (no output). The guard counts a report only when a
`true autoplan-published <PHASE>` (no output). The guard counts a report only when a
later record follows it, so this message is the next operation before any next-phase tool call.
Use the packet's `report` fields for this phase, the actual host's reviewer names,
and N/A when either review voice is missing; confirmed counts require both voices.
+4 -3
View File
@@ -38,8 +38,9 @@ to the hook. So the guard counts a phase report only when a later journaled
record follows it: a tool result, or a record from another assistant message.
The phase-close procedure therefore publishes each report in a message whose
only tool call is the Bash no-op `: autoplan-published <phase>` (it prints
nothing and needs no permission), and makes the next phase's `Read` or `Agent`
only tool call is the Bash no-op `true autoplan-published <phase>` (it prints
nothing and needs no permission; Claude Code 2.1.292 asks for approval of a bare
`:` command, so the no-op is `true`), and makes the next phase's `Read` or `Agent`
call in a later message. Sessions that put the report and the next phase's
call in one message get one [`publication_unflushed`](#publication-unflushed)
denial naming that fix.
@@ -191,7 +192,7 @@ be verified, so Claude Code is not writing it to the journal in time.
### `publication_missing`
No filled report for the previous phase appears after its close packet Read.
Publish it as your own assistant text in a message whose only tool call is
`: autoplan-published <phase>`, then enter the next phase in a later message.
`true autoplan-published <phase>`, then enter the next phase in a later message.
<a id="publication-unflushed"></a>
### `publication_unflushed`
+1 -1
View File
@@ -356,7 +356,7 @@ describe('Autoplan parent publication guard', () => {
expect(warning).toContain('code too_large');
expect(warning).toContain('docs/autoplan-guard-troubleshooting.md#too-large');
expect(warning).toContain('Phase-report enforcement was skipped');
expect(output.hookSpecificOutput.additionalContext).toContain(': autoplan-published <phase>');
expect(output.hookSpecificOutput.additionalContext).toContain('true autoplan-published <phase>');
});
test('#3050: the batch read and the narrowed parent loop report too_large, not a plain error', async () => {
+2 -2
View File
@@ -110,7 +110,7 @@ describe('Cause B: the current call is not journaled at hook time (payload path)
expect(codeOf(await f.hook(f.input('b', 'Agent', dispatch(f, { model: 'haiku' }))))).toBe('agent_key');
const next = await f.hook(f.input('c', 'Read', { file_path: section('design-phase.md') }));
expect(codeOf(next)).toBe('publication_missing');
expect(denial(next)).toContain(': autoplan-published <phase>');
expect(denial(next)).toContain('true autoplan-published <phase>');
});
});
@@ -121,7 +121,7 @@ describe('Publication flush: a report counts only once a later journaled record
const f = make(); f.report(f.nextMessage()); f.journal();
const output = await f.hook(next(f));
expect(codeOf(output)).toBe('publication_unflushed');
expect(denial(output)).toContain('`: autoplan-published <phase>`');
expect(denial(output)).toContain('`true autoplan-published <phase>`');
expect(denial(output)).not.toMatch(/\bretry\b/i);
});
+1 -1
View File
@@ -59,7 +59,7 @@ export function guardFixture(phase: Phase = 'ceo', opts: { methodologyLines?: nu
/** Phase-close step 6: the report and the no-op in their own message, then the no-op's result. */
const publish = () => {
const messageId = nextMessage();
report(messageId); use(`noop-${messageId}`, 'Bash', { command: `: autoplan-published ${phase}` }, messageId);
report(messageId); use(`noop-${messageId}`, 'Bash', { command: `true autoplan-published ${phase}` }, messageId);
result(`noop-${messageId}`, { content: '' });
};
+1 -1
View File
@@ -575,7 +575,7 @@ do not launch the downstream skill or open a browser.`,
maxSkeletonBytes: 70_000, // Phase-specific outside coverage, native fallback, and harness guard.
minUnionBytes: 85_000, // measured union 86,926
mustContain: ['6 Decision Principles', 'TASTE DECISION', 'USER CHALLENGE', 'consensus', 'Restore Point'],
maxSizeRatio: 1.1215, // Four validated outside invocations replace raw CLI calls; phases keep independent coverage. + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site, and phase-close publishing each report with the `: autoplan-published <phase>` no-op before the next phase entry in a later message (CEO-1); measured 114,320 / 101,979 = 1.1210 (2026-10-07).
maxSizeRatio: 1.1215, // Four validated outside invocations replace raw CLI calls; phases keep independent coverage. + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site, and phase-close publishing each report with the `true autoplan-published <phase>` no-op before the next phase entry in a later message (CEO-1); measured 114,320 / 101,979 = 1.1210 (2026-10-07).
},
spec: {
skill: 'spec',