@sublang/playbook 17.4.0 → 17.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "17.4.0",
3
+ "version": "17.4.1",
4
4
  "type": "module",
5
5
  "description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, PR, and INSPECT workflows driven by GEARS specs.",
6
6
  "license": "Apache-2.0",
@@ -272,7 +272,7 @@
272
272
  "provenance": true
273
273
  },
274
274
  "dependencies": {
275
- "@sublang/cligent": "^0.33.1",
275
+ "@sublang/cligent": "^0.33.2",
276
276
  "@sublang/spex": "^3.0.0",
277
277
  "p-queue": "^9.3.1",
278
278
  "xstate": "^5.19.4",
@@ -12,7 +12,7 @@ The caller supplies a coding request together with any relevant context.
12
12
 
13
13
  A new coding intent follows either one direct implementation phase or an IR sequence consisting of a new-IR phase followed by one phase for each IR task.
14
14
  An existing IR follows one IR-task phase for each remaining unfinished task, starting with its next unfinished task.
15
- Each phase ends with one new Coder commit (owned by `code`).
15
+ Each completed phase ends with one new Coder commit (owned by `code`).
16
16
  After each `code`-owned commit, Captain shall call the `review` playbook and wait until it passes with no unsettled findings.
17
17
  Playbook `review` owns every review round and every review-fix commit.
18
18
  Do not rewrite any existing commit.
@@ -40,22 +40,37 @@ Plan affected spec updates before, with, or after their corresponding code chang
40
40
  For an existing IR, read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.
41
41
  Do not implement a later task in this phase.
42
42
  Mark the IR's progress and deliverables when relevant.
43
+ After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.
44
+ Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.
45
+ Report only what the IR and completed work establish, in ordinary prose without a required marker.
43
46
  If the IR will be finished after this phase, double-check that all acceptance criteria are met.
44
47
 
45
48
  Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.
46
49
  ```
47
50
 
48
- At the start of every later IR-task phase, Captain shall relay to Coder the original caller input, the IR identity, and any relevant run results in quotes (`>`), along with the following instruction:
51
+ On each successful nested REVIEW, Captain shall freeze the previous phase review as one record from the canonical child result and the phase's accepted receipt-owned scope: exact phase kind (direct, new intent or IR task), phase outcome, CODE scope commit, REVIEW evaluated revision, and IR/task identity when present.
52
+ CODE scope commit and evaluated revision remain distinct because REVIEW may create fix commits.
53
+ No caller option, player prose or repository status text supplies this record.
54
+ The record survives permitted restoration and the current task's Boss-answer continuation, and a fresh run or explicit Boss interruption clears it.
55
+
56
+ At the start of every later IR-task phase, Captain shall relay to Coder the original caller input, the IR identity, any relevant run results, and that previous phase review as quoted evidence in quotes (`>`), along with the following instruction; when absent, the record reads "No accepted prior-phase review is available.":
49
57
 
50
58
  > Original request: <caller-input>
51
59
  > IR number: <ir-number>
52
60
  > Run results: <run-results>
61
+ > Previous phase review: <previous-phase-review>
53
62
 
54
63
  ```markdown
55
64
  Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.
56
65
  Do not implement a later task in this phase.
57
66
  Mark the IR's progress and deliverables when relevant.
67
+ After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.
68
+ Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.
69
+ Report only what the IR and completed work establish, in ordinary prose without a required marker.
58
70
  If the IR will be finished after this phase, double-check that all acceptance criteria are met.
71
+ Use the quoted previous-phase review only for its exact recorded scope; committed pending-review text may predate that canonical result.
72
+ Before treating it as current review evidence, verify clean current HEAD equals its evaluatedRevision; a mismatch is not approval.
73
+ It does not replace independent review of this task or any new owner or release decision.
59
74
  ```
60
75
 
61
76
  At the start of *every* phase, Captain shall append the following instruction:
@@ -63,7 +78,10 @@ At the start of *every* phase, Captain shall append the following instruction:
63
78
  ```markdown
64
79
  Keep to the original intent and follow what it asks.
65
80
  Do not re-run tests or builds whose inputs have not changed since any previous reported run.
66
- Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
81
+ If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.
82
+ Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.
83
+ After the answer allows completion, make exactly one phase-owned commit before REVIEW.
84
+ When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
67
85
  Make the commit message explain concisely what changed and why, including relevant verification.
68
86
  Identify every new commit you make.
69
87
  Credit every AI that contributed to this commit: Coder <coder-llm>.
@@ -73,9 +91,11 @@ Every new-intent phase has two semantic outcomes: direct implementation and new
73
91
  Every IR-task phase, including the first phase for an existing IR, identifies the IR and the implemented task, and has two semantic outcomes: more tasks and final task.
74
92
  The first phase therefore has four semantic outcomes — direct implementation, new IR, more tasks, and final task — plus the Boss question when the continued IR is ambiguous.
75
93
  Each semantic outcome requires affirmative support in Coder's result, but no phase transition shall depend on a fixed presentation format of Coder's reply.
94
+ The Boss-question outcome means that, at the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome.
95
+ A commit statement alone does not establish semantic completion, and contradictory or insufficient evidence supports no forced commit outcome.
76
96
  Captain shall use the repository-effect receipt as the authoritative identity of the phase's new commit.
77
97
 
78
- At the end of every phase, Captain shall call playbook `review` and input the following in quotes (`>`):
98
+ At the end of every completed phase, Captain shall call playbook `review` and input the following in quotes (`>`):
79
99
 
80
100
  > Original intent: \<caller-input\>
81
101
  > Review scope: the commit \<code-commit\> from this coding phase and its resulting repository state.
@@ -1161,12 +1161,18 @@ export function createCaptainSessionStore(options = {}) {
1161
1161
  try {
1162
1162
  await assertPrivateDirectory(assetsDirectory, fs);
1163
1163
  const names = await fs.readdir(assetsDirectory);
1164
- for (const name of names) {
1165
- if (!/^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name)) throw new Error('unknown session asset entry prevents deletion');
1166
- await assertPrivateRegularPath(join(assetsDirectory, name), 0o600, fs, 'session asset');
1164
+ const owned = names.filter((name) => /^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name));
1165
+ for (const name of owned) await assertPrivateRegularPath(join(assetsDirectory, name), 0o600, fs, 'session asset');
1166
+ for (const name of owned) await fs.unlink(join(assetsDirectory, name));
1167
+ // Foreign regular files (Finder or editor files) go with the session; a
1168
+ // remaining subdirectory or link keeps the directory, not the session.
1169
+ for (const name of names.filter((entry) => !owned.includes(entry))) {
1170
+ const path = join(assetsDirectory, name);
1171
+ try { const stat = await fs.lstat(path); if (stat.isFile() && !stat.isSymbolicLink()) await fs.unlink(path); }
1172
+ catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
1167
1173
  }
1168
- for (const name of names) await fs.unlink(join(assetsDirectory, name));
1169
- await fs.rmdir(assetsDirectory);
1174
+ try { await fs.rmdir(assetsDirectory); }
1175
+ catch (cause) { if (cause?.code !== 'ENOTEMPTY' && cause?.code !== 'EEXIST') throw cause; }
1170
1176
  await syncDirectory(sessionsDir, fs);
1171
1177
  } catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
1172
1178
  for (const suffix of ['.records.jsonl', '.hints.json', '.spex.json', '.json']) {
@@ -51,6 +51,8 @@ export interface IrTaskPhasePlayerInput extends PlayerInputBase {
51
51
  readonly sourceItem: 'CODE-3';
52
52
  /** `<ir-number>`: the IR whose next unfinished task this phase implements. */
53
53
  readonly irNumber?: string;
54
+ /** Source-owned relay; historical canonical evidence, not present approval. */
55
+ readonly previousPhaseReview: string;
54
56
  }
55
57
  export type PlayerInput = FirstPhasePlayerInput | IrTaskPhasePlayerInput;
56
58
  export type PlayerOutput = {
@@ -125,6 +127,14 @@ export interface CodeInput {
125
127
  /** Optional seed for `<run-results>`; `START_CODE` may supply its own. */
126
128
  readonly runResults?: string;
127
129
  }
130
+ export interface PreviousPhaseReview {
131
+ readonly phaseKind: 'direct' | 'new-intent' | 'ir-task';
132
+ readonly phaseOutcome: PhaseOutcome;
133
+ readonly scopeCommit: string;
134
+ readonly evaluatedRevision: string;
135
+ readonly irNumber?: string;
136
+ readonly irTask?: string;
137
+ }
128
138
  export interface CodeContext {
129
139
  /** `<run-results>`: relevant run results relayed to every phase. */
130
140
  readonly runResults: string;
@@ -142,6 +152,8 @@ export interface CodeContext {
142
152
  readonly phaseOutcome?: PhaseOutcome;
143
153
  /** Revision evaluated by the latest passing review. */
144
154
  readonly evaluatedRevision?: string;
155
+ /** Frozen only by an accepted canonical clean child REVIEW. */
156
+ readonly previousPhaseReview?: PreviousPhaseReview;
145
157
  /** Compact failure CODE reports when review did not pass a phase. */
146
158
  readonly reviewError?: CompactError;
147
159
  /** Sanitized evidence of the review result that ended CODE. */
@@ -25,13 +25,19 @@ const FIRST_PHASE_PROMPT = [
25
25
  'For an existing IR, read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
26
26
  'Do not implement a later task in this phase.',
27
27
  "Mark the IR's progress and deliverables when relevant.",
28
+ "After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
29
+ "Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
30
+ "Report only what the IR and completed work establish, in ordinary prose without a required marker.",
28
31
  'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
29
32
  '',
30
33
  'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
31
34
  '',
32
35
  'Keep to the original intent and follow what it asks.',
33
36
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
34
- "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
37
+ "If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
38
+ "Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
39
+ "After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
40
+ "When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
35
41
  'Make the commit message explain concisely what changed and why, including relevant verification.',
36
42
  'Identify every new commit you make.',
37
43
  'Credit every AI that contributed to this commit: Coder <coder-llm>.',
@@ -43,11 +49,20 @@ const IR_TASK_PHASE_PROMPT = [
43
49
  'Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
44
50
  'Do not implement a later task in this phase.',
45
51
  "Mark the IR's progress and deliverables when relevant.",
52
+ "After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
53
+ "Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
54
+ "Report only what the IR and completed work establish, in ordinary prose without a required marker.",
46
55
  'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
56
+ 'Use the quoted previous-phase review only for its exact recorded scope; committed pending-review text may predate that canonical result.',
57
+ 'Before treating it as current review evidence, verify clean current HEAD equals its evaluatedRevision; a mismatch is not approval.',
58
+ 'It does not replace independent review of this task or any new owner or release decision.',
47
59
  '',
48
60
  'Keep to the original intent and follow what it asks.',
49
61
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
50
- "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
62
+ "If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
63
+ "Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
64
+ "After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
65
+ "When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
51
66
  'Make the commit message explain concisely what changed and why, including relevant verification.',
52
67
  'Identify every new commit you make.',
53
68
  'Credit every AI that contributed to this commit: Coder <coder-llm>.',
@@ -55,6 +70,7 @@ const IR_TASK_PHASE_PROMPT = [
55
70
  '> Original request: <caller-input>',
56
71
  '> IR number: <ir-number>',
57
72
  '> Run results: <run-results>',
73
+ '> Previous phase review: <previous-phase-review>',
58
74
  ].join('\n');
59
75
  const REVIEW_NEW_INTENT_PHASE_TEMPLATE = [
60
76
  '> Original intent: <caller-input>',
@@ -68,7 +84,7 @@ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
68
84
  '',
69
85
  '> Current IR task: <ir-task>',
70
86
  ].join('\n');
71
- const NEEDS_BOSS_REPLY_DESCRIPTION = "The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
87
+ const NEEDS_BOSS_REPLY_DESCRIPTION = "At the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
72
88
  const FIRST_PHASE_RESULT = {
73
89
  directCommit: "Coder completed the new coding intent as one direct implementation phase and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
74
90
  irCommit: "Coder decomposed the new coding intent into a new IR, implemented no IR task, and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, and `irNumber` identifying the created IR.",
@@ -467,6 +483,7 @@ export const codeMachine = setup({
467
483
  irTask: undefined,
468
484
  phaseOutcome: undefined,
469
485
  evaluatedRevision: undefined,
486
+ previousPhaseReview: undefined,
470
487
  reviewError: undefined,
471
488
  reviewEvidence: undefined,
472
489
  lastError: undefined,
@@ -477,6 +494,7 @@ export const codeMachine = setup({
477
494
  resetForInterrupt: assign({
478
495
  phaseOutcome: undefined,
479
496
  evaluatedRevision: undefined,
497
+ previousPhaseReview: undefined,
480
498
  reviewError: undefined,
481
499
  reviewEvidence: undefined,
482
500
  lastError: undefined,
@@ -516,11 +534,24 @@ export const codeMachine = setup({
516
534
  pendingBossQuestion: undefined,
517
535
  bossReply: undefined,
518
536
  }),
519
- rememberReviewPass: assign(({ event }) => {
537
+ rememberReviewPass: assign(({ context, event }) => {
520
538
  const pass = reviewPassOf(event);
521
- return pass === undefined
539
+ return pass === undefined || context.codeCommit === undefined || context.phaseOutcome === undefined
522
540
  ? {}
523
- : { evaluatedRevision: pass.evaluatedRevision, lastError: undefined };
541
+ : {
542
+ evaluatedRevision: pass.evaluatedRevision,
543
+ previousPhaseReview: {
544
+ phaseKind: context.phaseOutcome === 'directCommit'
545
+ ? 'direct' : context.phaseOutcome === 'irCommit'
546
+ ? 'new-intent' : 'ir-task',
547
+ phaseOutcome: context.phaseOutcome,
548
+ scopeCommit: context.codeCommit,
549
+ evaluatedRevision: pass.evaluatedRevision,
550
+ ...(context.irNumber === undefined ? {} : { irNumber: context.irNumber }),
551
+ ...(context.irTask === undefined ? {} : { irTask: context.irTask }),
552
+ },
553
+ lastError: undefined,
554
+ };
524
555
  }),
525
556
  rememberReviewNotPassed: assign(({ event }) => {
526
557
  const output = doneOutputOf(event);
@@ -703,6 +734,9 @@ export const codeMachine = setup({
703
734
  ? { irNumber: context.irNumber }
704
735
  : {}),
705
736
  runResults: context.runResults,
737
+ previousPhaseReview: context.previousPhaseReview === undefined
738
+ ? 'No accepted prior-phase review is available.'
739
+ : JSON.stringify(context.previousPhaseReview),
706
740
  ...bossReplyFields(context),
707
741
  }),
708
742
  onDone: [
@@ -95,6 +95,8 @@ export interface IrTaskPhasePlayerInput extends PlayerInputBase {
95
95
  readonly sourceItem: 'CODE-3';
96
96
  /** `<ir-number>`: the IR whose next unfinished task this phase implements. */
97
97
  readonly irNumber?: string;
98
+ /** Source-owned relay; historical canonical evidence, not present approval. */
99
+ readonly previousPhaseReview: string;
98
100
  }
99
101
 
100
102
  export type PlayerInput = FirstPhasePlayerInput | IrTaskPhasePlayerInput;
@@ -194,6 +196,15 @@ export interface CodeInput {
194
196
  readonly runResults?: string;
195
197
  }
196
198
 
199
+ export interface PreviousPhaseReview {
200
+ readonly phaseKind: 'direct' | 'new-intent' | 'ir-task';
201
+ readonly phaseOutcome: PhaseOutcome;
202
+ readonly scopeCommit: string;
203
+ readonly evaluatedRevision: string;
204
+ readonly irNumber?: string;
205
+ readonly irTask?: string;
206
+ }
207
+
197
208
  export interface CodeContext {
198
209
  /** `<run-results>`: relevant run results relayed to every phase. */
199
210
  readonly runResults: string;
@@ -211,6 +222,8 @@ export interface CodeContext {
211
222
  readonly phaseOutcome?: PhaseOutcome;
212
223
  /** Revision evaluated by the latest passing review. */
213
224
  readonly evaluatedRevision?: string;
225
+ /** Frozen only by an accepted canonical clean child REVIEW. */
226
+ readonly previousPhaseReview?: PreviousPhaseReview;
214
227
  /** Compact failure CODE reports when review did not pass a phase. */
215
228
  readonly reviewError?: CompactError;
216
229
  /** Sanitized evidence of the review result that ended CODE. */
@@ -249,13 +262,19 @@ const FIRST_PHASE_PROMPT = [
249
262
  'For an existing IR, read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
250
263
  'Do not implement a later task in this phase.',
251
264
  "Mark the IR's progress and deliverables when relevant.",
265
+ "After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
266
+ "Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
267
+ "Report only what the IR and completed work establish, in ordinary prose without a required marker.",
252
268
  'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
253
269
  '',
254
270
  'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
255
271
  '',
256
272
  'Keep to the original intent and follow what it asks.',
257
273
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
258
- "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
274
+ "If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
275
+ "Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
276
+ "After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
277
+ "When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
259
278
  'Make the commit message explain concisely what changed and why, including relevant verification.',
260
279
  'Identify every new commit you make.',
261
280
  'Credit every AI that contributed to this commit: Coder <coder-llm>.',
@@ -268,11 +287,20 @@ const IR_TASK_PHASE_PROMPT = [
268
287
  'Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
269
288
  'Do not implement a later task in this phase.',
270
289
  "Mark the IR's progress and deliverables when relevant.",
290
+ "After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
291
+ "Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
292
+ "Report only what the IR and completed work establish, in ordinary prose without a required marker.",
271
293
  'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
294
+ 'Use the quoted previous-phase review only for its exact recorded scope; committed pending-review text may predate that canonical result.',
295
+ 'Before treating it as current review evidence, verify clean current HEAD equals its evaluatedRevision; a mismatch is not approval.',
296
+ 'It does not replace independent review of this task or any new owner or release decision.',
272
297
  '',
273
298
  'Keep to the original intent and follow what it asks.',
274
299
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
275
- "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
300
+ "If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
301
+ "Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
302
+ "After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
303
+ "When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
276
304
  'Make the commit message explain concisely what changed and why, including relevant verification.',
277
305
  'Identify every new commit you make.',
278
306
  'Credit every AI that contributed to this commit: Coder <coder-llm>.',
@@ -280,6 +308,7 @@ const IR_TASK_PHASE_PROMPT = [
280
308
  '> Original request: <caller-input>',
281
309
  '> IR number: <ir-number>',
282
310
  '> Run results: <run-results>',
311
+ '> Previous phase review: <previous-phase-review>',
283
312
  ].join('\n');
284
313
 
285
314
  const REVIEW_NEW_INTENT_PHASE_TEMPLATE = [
@@ -297,7 +326,7 @@ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
297
326
  ].join('\n');
298
327
 
299
328
  const NEEDS_BOSS_REPLY_DESCRIPTION =
300
- "The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
329
+ "At the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
301
330
 
302
331
  const FIRST_PHASE_RESULT = {
303
332
  directCommit:
@@ -823,6 +852,7 @@ export const codeMachine = setup({
823
852
  irTask: undefined,
824
853
  phaseOutcome: undefined,
825
854
  evaluatedRevision: undefined,
855
+ previousPhaseReview: undefined,
826
856
  reviewError: undefined,
827
857
  reviewEvidence: undefined,
828
858
  lastError: undefined,
@@ -834,6 +864,7 @@ export const codeMachine = setup({
834
864
  resetForInterrupt: assign({
835
865
  phaseOutcome: undefined,
836
866
  evaluatedRevision: undefined,
867
+ previousPhaseReview: undefined,
837
868
  reviewError: undefined,
838
869
  reviewEvidence: undefined,
839
870
  lastError: undefined,
@@ -876,11 +907,24 @@ export const codeMachine = setup({
876
907
  pendingBossQuestion: undefined,
877
908
  bossReply: undefined,
878
909
  }),
879
- rememberReviewPass: assign(({ event }) => {
910
+ rememberReviewPass: assign(({ context, event }) => {
880
911
  const pass = reviewPassOf(event);
881
- return pass === undefined
912
+ return pass === undefined || context.codeCommit === undefined || context.phaseOutcome === undefined
882
913
  ? {}
883
- : { evaluatedRevision: pass.evaluatedRevision, lastError: undefined };
914
+ : {
915
+ evaluatedRevision: pass.evaluatedRevision,
916
+ previousPhaseReview: {
917
+ phaseKind: context.phaseOutcome === 'directCommit'
918
+ ? 'direct' as const : context.phaseOutcome === 'irCommit'
919
+ ? 'new-intent' as const : 'ir-task' as const,
920
+ phaseOutcome: context.phaseOutcome,
921
+ scopeCommit: context.codeCommit,
922
+ evaluatedRevision: pass.evaluatedRevision,
923
+ ...(context.irNumber === undefined ? {} : { irNumber: context.irNumber }),
924
+ ...(context.irTask === undefined ? {} : { irTask: context.irTask }),
925
+ },
926
+ lastError: undefined,
927
+ };
884
928
  }),
885
929
  rememberReviewNotPassed: assign(({ event }) => {
886
930
  const output = doneOutputOf(event);
@@ -1086,6 +1130,9 @@ export const codeMachine = setup({
1086
1130
  ? { irNumber: context.irNumber }
1087
1131
  : {}),
1088
1132
  runResults: context.runResults,
1133
+ previousPhaseReview: context.previousPhaseReview === undefined
1134
+ ? 'No accepted prior-phase review is available.'
1135
+ : JSON.stringify(context.previousPhaseReview),
1089
1136
  ...bossReplyFields(context),
1090
1137
  }),
1091
1138
  onDone: [
@@ -11,7 +11,7 @@ Roles:
11
11
 
12
12
  ### CODE-1
13
13
 
14
- Each phase ends with exactly one new Coder commit owned by `code`, and no existing commit is rewritten.
14
+ Each completed phase ends with exactly one new Coder commit owned by `code`, and no existing commit is rewritten.
15
15
  Captain uses the repository-effect receipt as the authoritative identity of the phase's new commit.
16
16
  When the request may continue an existing IR that it does not identify unambiguously, Coder asks Boss before changing files, and Boss's answer resumes this same phase.
17
17
 
@@ -28,13 +28,19 @@ When the first coding phase begins for the caller's coding request and any relev
28
28
  > For an existing IR, read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.
29
29
  > Do not implement a later task in this phase.
30
30
  > Mark the IR's progress and deliverables when relevant.
31
+ > After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.
32
+ > Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.
33
+ > Report only what the IR and completed work establish, in ordinary prose without a required marker.
31
34
  > If the IR will be finished after this phase, double-check that all acceptance criteria are met.
32
35
  >
33
36
  > Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.
34
37
  >
35
38
  > Keep to the original intent and follow what it asks.
36
39
  > Do not re-run tests or builds whose inputs have not changed since any previous reported run.
37
- > Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
40
+ > If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.
41
+ > Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.
42
+ > After the answer allows completion, make exactly one phase-owned commit before REVIEW.
43
+ > When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
38
44
  > Make the commit message explain concisely what changed and why, including relevant verification.
39
45
  > Identify every new commit you make.
40
46
  > Credit every AI that contributed to this commit: Coder <coder-llm>.
@@ -47,6 +53,7 @@ Results:
47
53
  - `irCommit`: Coder decomposed the new coding intent into a new IR, implemented no IR task, and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, and `irNumber` identifying the created IR.
48
54
  - `moreTasks`: Coder continued an existing IR, implemented exactly its next unfinished task, made its one new commit, and at least one IR task remains unfinished; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.
49
55
  - `finalTask`: Coder continued an existing IR, implemented its final unfinished task, and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.
56
+ - `needsBossReply`: At the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome. Output shall include `question: <verbatim question text from the acting agent's prose>`.
50
57
 
51
58
  ### CODE-2
52
59
 
@@ -59,6 +66,10 @@ When a direct implementation or new-IR phase ends with its one new `code`-owned
59
66
  > > Coder output: <coder-output>
60
67
 
61
68
  A nested `review` passes the phase only when its result applies to that supplied review scope, returns the exact evaluated repository revision, and affirmatively establishes that no unsettled findings remain.
69
+ On each successful nested REVIEW, Captain shall freeze the previous phase review as one record from the canonical child result and the phase's accepted receipt-owned scope: exact phase kind (direct, new intent or IR task), phase outcome, CODE scope commit, REVIEW evaluated revision, and IR/task identity when present.
70
+ CODE scope commit and evaluated revision remain distinct because REVIEW may create fix commits.
71
+ No caller option, player prose or repository status text supplies this record.
72
+ The record survives permitted restoration and the current task's Boss-answer continuation, and a fresh run or explicit Boss interruption clears it.
62
73
  When `review` passes a direct implementation phase, `code` is complete and returns to its caller the exact last `code`-owned commit, the exact final evaluated repository revision, and the fact that every phase's review passed with no unsettled findings.
63
74
  When `review` passes a new-IR phase, Captain continues with the next unfinished IR-task phase.
64
75
  When `review` returns an authored abort or failure, or a terminal result that does not establish that the supplied scope was evaluated with no unsettled findings, `code` starts no further phase and reports the failure and the last `code`-owned commit to its caller.
@@ -66,7 +77,9 @@ When the nested `review` call fails outside that authored result contract, `code
66
77
 
67
78
  ### CODE-3
68
79
 
69
- Each phase ends with exactly one new Coder commit owned by `code`, and no existing commit is rewritten.
80
+ When absent, the previous phase review reads "No accepted prior-phase review is available.".
81
+
82
+ Each completed phase ends with exactly one new Coder commit owned by `code`, and no existing commit is rewritten.
70
83
  Captain uses the repository-effect receipt as the authoritative identity of the phase's new commit.
71
84
 
72
85
  When a later IR-task phase begins, Captain shall prompt Coder:
@@ -74,11 +87,20 @@ When a later IR-task phase begins, Captain shall prompt Coder:
74
87
  > Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.
75
88
  > Do not implement a later task in this phase.
76
89
  > Mark the IR's progress and deliverables when relevant.
90
+ > After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.
91
+ > Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.
92
+ > Report only what the IR and completed work establish, in ordinary prose without a required marker.
77
93
  > If the IR will be finished after this phase, double-check that all acceptance criteria are met.
94
+ > Use the quoted previous-phase review only for its exact recorded scope; committed pending-review text may predate that canonical result.
95
+ > Before treating it as current review evidence, verify clean current HEAD equals its evaluatedRevision; a mismatch is not approval.
96
+ > It does not replace independent review of this task or any new owner or release decision.
78
97
  >
79
98
  > Keep to the original intent and follow what it asks.
80
99
  > Do not re-run tests or builds whose inputs have not changed since any previous reported run.
81
- > Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
100
+ > If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.
101
+ > Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.
102
+ > After the answer allows completion, make exactly one phase-owned commit before REVIEW.
103
+ > When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
82
104
  > Make the commit message explain concisely what changed and why, including relevant verification.
83
105
  > Identify every new commit you make.
84
106
  > Credit every AI that contributed to this commit: Coder <coder-llm>.
@@ -86,10 +108,12 @@ When a later IR-task phase begins, Captain shall prompt Coder:
86
108
  > > Original request: <caller-input>
87
109
  > > IR number: <ir-number>
88
110
  > > Run results: <run-results>
111
+ > > Previous phase review: <previous-phase-review>
89
112
 
90
113
  Results:
91
114
  - `moreTasks`: Coder implemented exactly the IR's next unfinished task, made its one new commit, and at least one IR task remains unfinished; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the IR, and `irTask` naming the implemented task.
92
115
  - `finalTask`: Coder implemented the IR's final unfinished task and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the IR, and `irTask` naming the implemented task.
116
+ - `needsBossReply`: At the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome. Output shall include `question: <verbatim question text from the acting agent's prose>`.
93
117
 
94
118
  ### CODE-4
95
119
 
@@ -104,6 +128,10 @@ When an IR-task phase, including the first phase for an existing IR, ends with i
104
128
  > > Current IR task: <ir-task>
105
129
 
106
130
  A nested `review` passes the phase only when its result applies to that supplied review scope, returns the exact evaluated repository revision, and affirmatively establishes that no unsettled findings remain.
131
+ On each successful nested REVIEW, Captain shall freeze the previous phase review as one record from the canonical child result and the phase's accepted receipt-owned scope: exact phase kind (direct, new intent or IR task), phase outcome, CODE scope commit, REVIEW evaluated revision, and IR/task identity when present.
132
+ CODE scope commit and evaluated revision remain distinct because REVIEW may create fix commits.
133
+ No caller option, player prose or repository status text supplies this record.
134
+ The record survives permitted restoration and the current task's Boss-answer continuation, and a fresh run or explicit Boss interruption clears it.
107
135
  When `review` passes a nonfinal IR-task phase, Captain continues with the next unfinished IR-task phase.
108
136
  When `review` passes the final IR-task phase, `code` is complete and returns to its caller the exact last `code`-owned commit, the exact final evaluated repository revision, and the fact that every phase's review passed with no unsettled findings.
109
137
  When `review` returns an authored abort or failure, or a terminal result that does not establish that the supplied scope was evaluated with no unsettled findings, `code` starts no further phase and reports the failure and the last `code`-owned commit to its caller.
@@ -24,6 +24,15 @@ function retainedEffectLedgerCanRebase(checkpoint, current) {
24
24
  .slice(checkpoint.boundaries.length)
25
25
  .every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged');
26
26
  }
27
+ function missingPackagedReview(enablementById, playbookId) {
28
+ const selected = enablementById.get(playbookId);
29
+ if (selected === undefined ||
30
+ enablementById.has('review') ||
31
+ !['code', 'decide'].includes(playbookId) ||
32
+ selected.configuredFrom !== `@sublang/playbook/${playbookId}/registry`)
33
+ return undefined;
34
+ return `/${selected.command} requires enabled playbook "review"; enable REVIEW before starting this packaged workflow`;
35
+ }
27
36
  function createRuntimeForEnablement(enablement, hostCapabilitiesById) {
28
37
  const hostCapabilities = hostCapabilitiesById.get(enablement.entry.id);
29
38
  if (hostCapabilities === undefined) {
@@ -2363,6 +2372,7 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
2363
2372
  hostCapabilitiesById.set(entry.id, hostCapability);
2364
2373
  enablementById.set(entry.id, {
2365
2374
  entry,
2375
+ configuredFrom: from,
2366
2376
  artifactSchema,
2367
2377
  command,
2368
2378
  options: validatedOptions,
@@ -3842,13 +3852,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
3842
3852
  const sessionId = allocateSessionId();
3843
3853
  const playerBindings = makePlayerBindings(enablement);
3844
3854
  const runtime = createRuntimeForEnablement(enablement, hostCapabilitiesById);
3855
+ // A nested child inherits only its parent's selection, never the turn's
3856
+ // unselected references; only a root reads the turn's selection.
3857
+ const inherited = parent ? parent.frame.attachments : (activeTurn?.selectedAttachments ?? activeTurn?.attachments);
3845
3858
  return {
3846
3859
  entry,
3847
3860
  enablement,
3848
3861
  runtime,
3849
3862
  sessionId,
3850
3863
  rootSessionId: parent?.frame.rootSessionId ?? sessionId,
3851
- ...((parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn?.attachments)?.length ? { attachments: [...(parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn.attachments)] } : {}),
3864
+ ...(inherited?.length ? { attachments: [...inherited] } : {}),
3852
3865
  depth: parent ? parent.frame.depth + 1 : 0,
3853
3866
  playerBindings,
3854
3867
  ...(parent ? { parent } : {}),
@@ -4152,8 +4165,6 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4152
4165
  finally {
4153
4166
  if (leafFrame() === frame) {
4154
4167
  frames.pop();
4155
- if (!frame.parent)
4156
- pendingAttachments = [];
4157
4168
  if (frame.parent) {
4158
4169
  pendingChildParents.delete(frame.parent.frame);
4159
4170
  }
@@ -4541,6 +4552,24 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4541
4552
  return;
4542
4553
  }
4543
4554
  assertRetainableResult(frame, result);
4555
+ // A deferred checkpoint refusal changes the runtime's control overlay
4556
+ // without publishing an FSM transition. Refresh its derived question
4557
+ // mirror from an exact safe snapshot before reporting or saving the stop.
4558
+ if (leafFrame() === frame && !frame.disposing &&
4559
+ retainedEffectReconciliation === undefined) {
4560
+ try {
4561
+ const exported = frame.runtime.exportSnapshot?.();
4562
+ if (exported !== undefined) {
4563
+ const snapshot = assertPlaybookRuntimeSnapshot(exported, frame.entry.id);
4564
+ if (isDeepStrictEqual(snapshot.state, result.state) &&
4565
+ isDeepStrictEqual(snapshot.effectLedger, currentEffectLedger())) {
4566
+ pendingBossQuestions = snapshot.pendingBossQuestions.length === 0 && pendingBossQuestions === undefined
4567
+ ? undefined : mirroredBossQuestions(snapshot.pendingBossQuestions);
4568
+ }
4569
+ }
4570
+ }
4571
+ catch { /* An inconsistent runtime must still fail exact capture. */ }
4572
+ }
4544
4573
  if (result.outcome === 'aborted' && runFailureFacts) {
4545
4574
  runFailureFacts.push(`${frameLabel(frame)} was aborted before its outcome could be confirmed; it was not repeated automatically.`);
4546
4575
  }
@@ -4610,6 +4639,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4610
4639
  request.playbookId,
4611
4640
  ].join(' -> ')}`);
4612
4641
  }
4642
+ const reviewRefusal = missingPackagedReview(enablementById, entry.id);
4643
+ if (reviewRefusal !== undefined)
4644
+ throw new Error(reviewRefusal);
4613
4645
  pendingChildParents.add(parent);
4614
4646
  let child;
4615
4647
  try {
@@ -6245,7 +6277,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
6245
6277
  // selections freeze after their work and before reporting begins.
6246
6278
  if (selection.action === 'respond')
6247
6279
  freezeControllerEvidence();
6248
- let settlement = await executeSelection(selection, signal, automaticRecovery);
6280
+ signal.throwIfAborted();
6281
+ const reviewRefusal = selection.action === 'start' || selection.action === 'switch'
6282
+ ? missingPackagedReview(enablementById, selection.playbookId)
6283
+ : undefined;
6284
+ let settlement = reviewRefusal === undefined
6285
+ ? await executeSelection(selection, signal, automaticRecovery)
6286
+ : await rejectSelection(selection, reviewRefusal);
6249
6287
  // One controller action can include bounded prerequisite recovery.
6250
6288
  // Neither the model nor a retry manufactures an unadvertised transition.
6251
6289
  if (turn && abortPreparation && settlement.status !== 'rejected' && !['respond', 'dismiss', 'recover'].includes(selection.action)) {
@@ -6723,6 +6761,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
6723
6761
  const nextAttachments = mergeAttachments(leaf.attachments ?? [], selected);
6724
6762
  if (nextAttachments.length)
6725
6763
  leaf.attachments = nextAttachments;
6764
+ // Delivered references now belong to the leaf; the rest stay pending.
6765
+ const delivered = new Set(selected.map((reference) => reference.assetId));
6766
+ pendingAttachments = pendingAttachments.filter((reference) => !delivered.has(reference.assetId));
6726
6767
  // CAPTAIN-8: delivery carries text only, and the shell is authoritative
6727
6768
  // for that text — any text carried on the selection is ignored.
6728
6769
  turn.settled = true;
@@ -446,12 +446,27 @@ export interface PlaybookCaptainShell extends Captain {
446
446
  // normalized `captain.options.playbooks.<id>` role map.
447
447
  interface Enablement {
448
448
  entry: PlaybookCaptainRegistryEntry;
449
+ readonly configuredFrom: string;
449
450
  artifactSchema: 3;
450
451
  command: string;
451
452
  options: JsonValue;
452
453
  roleBindings: ReadonlyMap<string, EffectivePlayerBinding>;
453
454
  }
454
455
 
456
+ function missingPackagedReview(
457
+ enablementById: ReadonlyMap<string, Enablement>,
458
+ playbookId: string,
459
+ ): string | undefined {
460
+ const selected = enablementById.get(playbookId);
461
+ if (
462
+ selected === undefined ||
463
+ enablementById.has('review') ||
464
+ !['code', 'decide'].includes(playbookId) ||
465
+ selected.configuredFrom !== `@sublang/playbook/${playbookId}/registry`
466
+ ) return undefined;
467
+ return `/${selected.command} requires enabled playbook "review"; enable REVIEW before starting this packaged workflow`;
468
+ }
469
+
455
470
  function createRuntimeForEnablement(
456
471
  enablement: Enablement,
457
472
  hostCapabilitiesById: ReadonlyMap<
@@ -4181,6 +4196,7 @@ async function buildEnablements(
4181
4196
  hostCapabilitiesById.set(entry.id, hostCapability);
4182
4197
  enablementById.set(entry.id, {
4183
4198
  entry,
4199
+ configuredFrom: from,
4184
4200
  artifactSchema,
4185
4201
  command,
4186
4202
  options: validatedOptions,
@@ -6142,13 +6158,16 @@ export function createPlaybookCaptainShell(
6142
6158
  enablement,
6143
6159
  hostCapabilitiesById,
6144
6160
  );
6161
+ // A nested child inherits only its parent's selection, never the turn's
6162
+ // unselected references; only a root reads the turn's selection.
6163
+ const inherited = parent ? parent.frame.attachments : (activeTurn?.selectedAttachments ?? activeTurn?.attachments);
6145
6164
  return {
6146
6165
  entry,
6147
6166
  enablement,
6148
6167
  runtime,
6149
6168
  sessionId,
6150
6169
  rootSessionId: parent?.frame.rootSessionId ?? sessionId,
6151
- ...((parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn?.attachments)?.length ? { attachments: [...(parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn!.attachments)] } : {}),
6170
+ ...(inherited?.length ? { attachments: [...inherited] } : {}),
6152
6171
  depth: parent ? parent.frame.depth + 1 : 0,
6153
6172
  playerBindings,
6154
6173
  ...(parent ? { parent } : {}),
@@ -6486,7 +6505,6 @@ export function createPlaybookCaptainShell(
6486
6505
  } finally {
6487
6506
  if (leafFrame() === frame) {
6488
6507
  frames.pop();
6489
- if (!frame.parent) pendingAttachments = [];
6490
6508
  if (frame.parent) {
6491
6509
  pendingChildParents.delete(frame.parent.frame);
6492
6510
  }
@@ -6930,6 +6948,23 @@ export function createPlaybookCaptainShell(
6930
6948
  return;
6931
6949
  }
6932
6950
  assertRetainableResult(frame, result);
6951
+ // A deferred checkpoint refusal changes the runtime's control overlay
6952
+ // without publishing an FSM transition. Refresh its derived question
6953
+ // mirror from an exact safe snapshot before reporting or saving the stop.
6954
+ if (leafFrame() === frame && !frame.disposing &&
6955
+ retainedEffectReconciliation === undefined) {
6956
+ try {
6957
+ const exported = frame.runtime.exportSnapshot?.();
6958
+ if (exported !== undefined) {
6959
+ const snapshot = assertPlaybookRuntimeSnapshot(exported, frame.entry.id);
6960
+ if (isDeepStrictEqual(snapshot.state, result.state) &&
6961
+ isDeepStrictEqual(snapshot.effectLedger, currentEffectLedger())) {
6962
+ pendingBossQuestions = snapshot.pendingBossQuestions.length === 0 && pendingBossQuestions === undefined
6963
+ ? undefined : mirroredBossQuestions(snapshot.pendingBossQuestions);
6964
+ }
6965
+ }
6966
+ } catch { /* An inconsistent runtime must still fail exact capture. */ }
6967
+ }
6933
6968
  if (result.outcome === 'aborted' && runFailureFacts) {
6934
6969
  runFailureFacts.push(
6935
6970
  `${frameLabel(frame)} was aborted before its outcome could be confirmed; it was not repeated automatically.`,
@@ -7011,6 +7046,9 @@ export function createPlaybookCaptainShell(
7011
7046
  );
7012
7047
  }
7013
7048
 
7049
+ const reviewRefusal = missingPackagedReview(enablementById, entry.id);
7050
+ if (reviewRefusal !== undefined) throw new Error(reviewRefusal);
7051
+
7014
7052
  pendingChildParents.add(parent);
7015
7053
  let child: EngagementFrame;
7016
7054
  try {
@@ -9004,7 +9042,13 @@ export function createPlaybookCaptainShell(
9004
9042
  // its decision-call prose crosses the presentation boundary. Acting
9005
9043
  // selections freeze after their work and before reporting begins.
9006
9044
  if (selection.action === 'respond') freezeControllerEvidence();
9007
- let settlement = await executeSelection(selection, signal, automaticRecovery);
9045
+ signal.throwIfAborted();
9046
+ const reviewRefusal = selection.action === 'start' || selection.action === 'switch'
9047
+ ? missingPackagedReview(enablementById, selection.playbookId)
9048
+ : undefined;
9049
+ let settlement = reviewRefusal === undefined
9050
+ ? await executeSelection(selection, signal, automaticRecovery)
9051
+ : await rejectSelection(selection, reviewRefusal);
9008
9052
  // One controller action can include bounded prerequisite recovery.
9009
9053
  // Neither the model nor a retry manufactures an unadvertised transition.
9010
9054
  if (turn && abortPreparation && settlement.status !== 'rejected' && !['respond', 'dismiss', 'recover'].includes(selection.action)) {
@@ -9548,6 +9592,9 @@ export function createPlaybookCaptainShell(
9548
9592
  const selected = selection.attachmentIds === undefined ? turn.attachments : selection.attachmentIds.map((id) => available.get(id as SessionAssetRef['assetId'])!);
9549
9593
  const nextAttachments = mergeAttachments(leaf.attachments ?? [], selected);
9550
9594
  if (nextAttachments.length) leaf.attachments = nextAttachments;
9595
+ // Delivered references now belong to the leaf; the rest stay pending.
9596
+ const delivered = new Set(selected.map((reference) => reference.assetId));
9597
+ pendingAttachments = pendingAttachments.filter((reference) => !delivered.has(reference.assetId));
9551
9598
  // CAPTAIN-8: delivery carries text only, and the shell is authoritative
9552
9599
  // for that text — any text carried on the selection is ignored.
9553
9600
  turn.settled = true;
@@ -102,9 +102,11 @@ export function createAssetStore(options) {
102
102
  try {
103
103
  const opened = await directoryHandle.stat();
104
104
  if (opened.dev !== before.dev || opened.ino !== before.ino) throw new Error('asset owner directory changed during preparation');
105
- if (process.platform !== 'win32') await directoryHandle.chmod(0o700);
105
+ // fchmod always moves ctime, which verified readers pin: change only modes that differ.
106
+ if (process.platform !== 'win32' && (opened.mode & 0o777) !== 0o700) await directoryHandle.chmod(0o700);
106
107
  for (const name of await readdir(directory)) {
107
- if (!/^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name)) throw new Error('unknown asset owner entry');
108
+ // Foreign names (Finder or editor files) are neither assets nor ours to modify.
109
+ if (!/^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name)) continue;
108
110
  const path = join(directory, name);
109
111
  const stat = await lstat(path);
110
112
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) throw new Error('asset entry must be a single-link regular file');
@@ -114,7 +116,7 @@ export function createAssetStore(options) {
114
116
  try {
115
117
  const pinned = await handle.stat();
116
118
  if (!sameFile(stat, pinned)) throw new Error('asset entry changed during preparation');
117
- if (process.platform !== 'win32') await handle.chmod(0o600);
119
+ if (process.platform !== 'win32' && (pinned.mode & 0o777) !== 0o600) await handle.chmod(0o600);
118
120
  assertFile(await handle.stat(), 'asset entry');
119
121
  const final = await lstat(path);
120
122
  if (final.dev !== pinned.dev || final.ino !== pinned.ino || final.nlink !== 1) throw new Error('asset entry changed during preparation');
@@ -286,9 +288,8 @@ export function createAssetStore(options) {
286
288
  const entries = await readdir(directory);
287
289
  const result = [];
288
290
  for (const name of entries.sort()) {
289
- if (/^\.import-[0-9a-f-]+\.tmp$/.test(name)) continue;
290
- if (/^[0-9a-f]{64}\.json$/.test(name)) continue;
291
- if (!/^[0-9a-f]{64}$/.test(name)) throw new Error(`unknown asset owner entry ${JSON.stringify(basename(name))}`);
291
+ // Descriptors, import temporaries and foreign files are not enumerated.
292
+ if (!/^[0-9a-f]{64}$/.test(name)) continue;
292
293
  if (!entries.includes(`${name}.json`)) { const orphan = await checkedOpen(name, 'unreferenced asset content'); await orphan.handle.close(); continue; }
293
294
  const reader = await openAsset(`sha256:${name}`); await reader.close();
294
295
  result.push(Object.freeze({ assetId: reader.reference.assetId, path: name, metadataPath: `${name}.json`, byteLength: reader.reference.byteLength }));
package/slc/gears2fsm.md CHANGED
@@ -602,6 +602,9 @@ safety beyond the guarded direct transition.
602
602
  ## Context and prompts
603
603
 
604
604
  Context fields used to drive guards or compose prompts shall be **typed and named**.
605
+ Where Source owns array-valued retained discussion whose initial state is no history, initialize that context field to `[]` and reset it to `[]` only at its Source lifecycle boundaries.
606
+ Carry its declared array shape through ordinary and continuation `invoke.input`; do not leave the empty structural history absent so a prompt-contract probe mistakes it for a scalar.
607
+ This empty history is not a default caller intent, prerequisite report, domain evidence, approval, or prior-stage result; those remain unavailable until actually established.
605
608
  The compiler shall not branch on untyped properties of `lastResult`; persistent routing decisions belong in typed context fields. (`lastResult` is for inspection only.)
606
609
  Where Source declares a finite ordered plan, represent it as a typed readonly
607
610
  JSON-safe array and validate that shape on the actor-output transition; an
@@ -835,8 +838,8 @@ Only
835
838
  The canonical storage paths are `context.pendingBossQuestion` and `context.bossReply` for the scalar form, or `context.pendingBossQuestions[stateId]` and `context.bossReplies[stateId]` for the keyed form.
836
839
  A private wrapper such as `context.continuation` shall not replace these fields directly on machine context.
837
840
 
838
- A machine with at most one active Captain or player task may use the scalar
839
- form:
841
+ A machine that declares no root parallel group shall use the scalar form;
842
+ keyed storage alone is not visible to the flat shared factory:
840
843
 
841
844
  - An `awaitBossReply` state with stable `id: 'awaitBossReply'`, tag
842
845
  `playbook.parked`, and description
@@ -847,7 +850,9 @@ form:
847
850
  - `resumableStates(ids)`, `setPendingBossQuestion`, and
848
851
  `clearBossReplyContext` helpers with the existing single-question behavior.
849
852
 
850
- A machine with parallel delegated-player tasks shall use the keyed form:
853
+ A machine that declares a root parallel group shall use the keyed form
854
+ throughout the machine, including any sequential working leaves and their
855
+ root reply wait:
851
856
 
852
857
  - One local waiting leaf per branch, tagged `playbook.parked`.
853
858
  - A `BOSS_REPLY` event carrying `{ questionId: string; answer: string }`.
@@ -884,6 +889,15 @@ pending question and reply selected for that working leaf as singular
884
889
  `pendingBossQuestion` and `bossReply` fields, regardless of the scalar or keyed
885
890
  context representation, so prompt composition has one stable contract.
886
891
  When Source requires earlier discussion or constraints on a later invocation, the compiler shall persist that source-owned history in serializable machine context before replacing or clearing the pending Q/A fields.
892
+ Where that Source-owned value is the complete received Boss reply, retain it
893
+ directly from the current working leaf's canonical reply context at the
894
+ Source-authored accepted-outcome boundary, before clearing that leaf's Q/A.
895
+ Preserve its exact string, including surrounding whitespace, CRLF/LF separators,
896
+ blank lines and trailing separators; do not require a model-produced echo or
897
+ replace it with one. Acceptance remains the acting agent's semantic judgment,
898
+ guarded by the current matching question's state, Source-item and asker binding
899
+ and actual receipt of a nonempty reply. Neither stale question/reply context
900
+ nor a different actor-supplied value establishes that binding or approval.
887
901
  The later `invoke.input` shall relay that persisted context on both resumed and fresh calls that Source says need it, preserving Source-owned relevance and format without imposing all-history semantics on sources that do not require it.
888
902
  Shared Boss-reply continuation carries only the latest Q/A pair, and a backend continuation token is not durable Source history.
889
903
  When both fields are present, the linked runtime shall compose the continuation preamble and labelled Q&A blocks per [link.md "Player prompt composition"](link.md#player-prompt-composition).
package/slc/text2gears.md CHANGED
@@ -189,6 +189,13 @@ between the Source and the catalog: leave the Target unwritten and report it
189
189
  as an incompatible compiler input rather than rename or invent.
190
190
  A Source outside those ids is named from its own words as above.
191
191
 
192
+ A value Source assigns to the received Boss input or reply is already runtime
193
+ input, not a value the acting agent produces. Where Source requires that
194
+ complete value retained for later calls, preserve the exact input's retention
195
+ and the Source's acceptance condition in non-acting GEARS prose; do not declare
196
+ a Judge-extracted echo property for it. The semantic approval or refusal stays
197
+ in the acting result contract.
198
+
192
199
  A produced value consumed later shall have a declared producer: where any
193
200
  later item's blockquote reads a value through a `<placeholder>`, the item
194
201
  whose behavior produces that value shall declare the `Results:` contract
@@ -866,12 +866,16 @@ export function defaultExtractRequiredFields(description) {
866
866
  fields.push(m[1]);
867
867
  return fields;
868
868
  }
869
+ const CURRENT_CALL_ADJUDICATION_GUIDANCE = "Classify the invoked role's current call by the result it reports at call end, using the full output and declared outcome meanings. " +
870
+ 'An earlier interim question is not outstanding when later prose explicitly resolves or supersedes it for this call; a prerequisite only for a later workflow stage does not block an affirmatively completed current call. ' +
871
+ 'A genuinely unanswered question needed to finish the current call remains outstanding. A commit statement alone does not establish semantic completion; do not force contradictory or insufficient evidence into a completed outcome.';
869
872
  /** Default delegated-player adjudicator prompt. */
870
873
  export function defaultBuildJudgePrompt(input, finalText) {
871
874
  const lines = [];
872
875
  lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
873
876
  'seek external evidence. Decide only from the supplied player output ' +
874
877
  'and outcome descriptions. Reply with exactly one JSON object and no prose.');
878
+ lines.push(CURRENT_CALL_ADJUDICATION_GUIDANCE);
875
879
  lines.push('');
876
880
  lines.push(`The ${input.role} role just produced this output:`);
877
881
  lines.push('');
@@ -957,6 +961,7 @@ function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
957
961
  const lines = [
958
962
  'This is hidden control work. Do not call tools, inspect files, or seek external evidence.',
959
963
  'Decide only from the supplied player output and declared outcomes.',
964
+ CURRENT_CALL_ADJUDICATION_GUIDANCE,
960
965
  'Reply with exactly one JSON object and no prose.',
961
966
  '',
962
967
  `The ${input.role} role just produced this output:`,
@@ -1706,6 +1706,11 @@ export function defaultExtractRequiredFields(description: string): string[] {
1706
1706
  return fields;
1707
1707
  }
1708
1708
 
1709
+ const CURRENT_CALL_ADJUDICATION_GUIDANCE =
1710
+ "Classify the invoked role's current call by the result it reports at call end, using the full output and declared outcome meanings. " +
1711
+ 'An earlier interim question is not outstanding when later prose explicitly resolves or supersedes it for this call; a prerequisite only for a later workflow stage does not block an affirmatively completed current call. ' +
1712
+ 'A genuinely unanswered question needed to finish the current call remains outstanding. A commit statement alone does not establish semantic completion; do not force contradictory or insufficient evidence into a completed outcome.';
1713
+
1709
1714
  /** Default delegated-player adjudicator prompt. */
1710
1715
  export function defaultBuildJudgePrompt(
1711
1716
  input: PlaybookPlayerInput,
@@ -1717,6 +1722,7 @@ export function defaultBuildJudgePrompt(
1717
1722
  'seek external evidence. Decide only from the supplied player output ' +
1718
1723
  'and outcome descriptions. Reply with exactly one JSON object and no prose.',
1719
1724
  );
1725
+ lines.push(CURRENT_CALL_ADJUDICATION_GUIDANCE);
1720
1726
  lines.push('');
1721
1727
  lines.push(`The ${input.role} role just produced this output:`);
1722
1728
  lines.push('');
@@ -1822,6 +1828,7 @@ function buildGovernedJudgePrompt(
1822
1828
  const lines = [
1823
1829
  'This is hidden control work. Do not call tools, inspect files, or seek external evidence.',
1824
1830
  'Decide only from the supplied player output and declared outcomes.',
1831
+ CURRENT_CALL_ADJUDICATION_GUIDANCE,
1825
1832
  'Reply with exactly one JSON object and no prose.',
1826
1833
  '',
1827
1834
  `The ${input.role} role just produced this output:`,