@sublang/playbook 12.3.0 → 13.1.0

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.
Files changed (53) hide show
  1. package/docs/cli.md +56 -52
  2. package/docs/configuration.md +2 -2
  3. package/docs/embedding.md +68 -32
  4. package/package.json +11 -3
  5. package/reference/sdlc/code.md +7 -0
  6. package/reference/sdlc/code.playbook/bin/interactive-session.js +1 -0
  7. package/reference/sdlc/code.playbook/bin/launch-config.js +2 -0
  8. package/reference/sdlc/code.playbook/bin/playbook.js +42 -4
  9. package/reference/sdlc/code.playbook/bin/portable-codec.js +190 -0
  10. package/reference/sdlc/code.playbook/bin/replay-observer.js +18 -2
  11. package/reference/sdlc/code.playbook/bin/run.js +62 -18
  12. package/reference/sdlc/code.playbook/bin/session-host.js +104 -0
  13. package/reference/sdlc/code.playbook/bin/session-store.js +622 -65
  14. package/reference/sdlc/code.playbook/code.fsm.js +5 -5
  15. package/reference/sdlc/code.playbook/code.fsm.ts +5 -5
  16. package/reference/sdlc/code.playbook/code.gears.md +5 -5
  17. package/reference/sdlc/code.playbook/code.playbook.d.ts +1 -1
  18. package/reference/sdlc/code.playbook/code.playbook.js +6 -16
  19. package/reference/sdlc/code.playbook/code.playbook.ts +7 -17
  20. package/reference/sdlc/code.playbook/playbook-captain.d.ts +5 -0
  21. package/reference/sdlc/code.playbook/playbook-captain.js +56 -14
  22. package/reference/sdlc/code.playbook/playbook-captain.ts +71 -21
  23. package/reference/sdlc/code.playbook/session-host.d.ts +75 -0
  24. package/reference/sdlc/code.playbook/session-host.js +18 -0
  25. package/reference/sdlc/code.playbook/session-store.d.ts +176 -1
  26. package/reference/sdlc/code.playbook/session-store.js +22 -6
  27. package/reference/sdlc/decide.md +2 -0
  28. package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
  29. package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
  30. package/reference/sdlc/decide.playbook/decide.gears.md +2 -2
  31. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -2
  32. package/reference/sdlc/decide.playbook/decide.playbook.js +8 -22
  33. package/reference/sdlc/decide.playbook/decide.playbook.ts +8 -25
  34. package/reference/sdlc/dev.md +21 -1
  35. package/reference/sdlc/dev.playbook/dev.fsm.js +10 -10
  36. package/reference/sdlc/dev.playbook/dev.fsm.ts +13 -13
  37. package/reference/sdlc/dev.playbook/dev.gears.md +15 -15
  38. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +1 -1
  39. package/reference/sdlc/dev.playbook/dev.playbook.js +7 -17
  40. package/reference/sdlc/dev.playbook/dev.playbook.ts +8 -20
  41. package/reference/sdlc/review.md +18 -0
  42. package/reference/sdlc/review.playbook/review.fsm.js +8 -8
  43. package/reference/sdlc/review.playbook/review.fsm.ts +8 -8
  44. package/reference/sdlc/review.playbook/review.gears.md +8 -8
  45. package/reference/sdlc/review.playbook/review.playbook.d.ts +1 -1
  46. package/reference/sdlc/review.playbook/review.playbook.js +5 -15
  47. package/reference/sdlc/review.playbook/review.playbook.ts +6 -16
  48. package/slc/link.md +12 -24
  49. package/src/runtime.d.ts +2 -0
  50. package/src/runtime.ts +2 -0
  51. package/src/xstate-playbook-runtime.d.ts +4 -2
  52. package/src/xstate-playbook-runtime.js +30 -26
  53. package/src/xstate-playbook-runtime.ts +40 -35
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ import type { PlaybookCaptainShell, PlaybookCaptainShellSnapshot } from './playbook-captain.js';
5
+ import type { PlaybookEffectLedger } from './host-capabilities.js';
6
+ import type { PlaybookSessionLifecycle, SessionExecutionProjection, SessionStructuralProjection, SessionFreshBoundary, SessionRecovery } from './session-store.js';
7
+
8
+ export interface SessionHost {
9
+ readonly shell: PlaybookCaptainShell;
10
+ readonly host: { runBossTurn(input: string): Promise<void>; abortActiveTurn(): void; dispose(): Promise<void>; [key: string]: any };
11
+ readonly snapshot: PlaybookCaptainShellSnapshot;
12
+ reconcileRepositoryEffects(): Promise<PlaybookEffectLedger>;
13
+ }
14
+ export interface SessionHostOptions {
15
+ readonly config: SessionExecutionProjection;
16
+ readonly sessionId?: string;
17
+ readonly cwd: string;
18
+ readonly sessionLease: PlaybookSessionLifecycle;
19
+ readonly loadModule: (specifier: string) => Promise<any>;
20
+ readonly restoreSnapshot?: PlaybookCaptainShellSnapshot;
21
+ readonly reconcileUncertainTurnReplay?: boolean;
22
+ readonly signal?: AbortSignal;
23
+ readonly [key: string]: any;
24
+ }
25
+ export declare function createCaptainSessionHost(options: SessionHostOptions): Promise<SessionHost>;
26
+ export declare function installRetainedGenerationsForLaunch(options: {
27
+ lease: PlaybookSessionLifecycle;
28
+ shell: PlaybookCaptainShell;
29
+ freshBoundary?: SessionFreshBoundary;
30
+ onFreshRecord?: (record: SessionRecovery | undefined) => void;
31
+ onLegacyRecord?: (record: any) => void | Promise<void>;
32
+ onInvalidRecord?: (record: any) => void | Promise<void>;
33
+ retainedGenerations?: Readonly<Record<string, any>>;
34
+ reconcileRepositoryEffects?: () => Promise<PlaybookEffectLedger>;
35
+ }): Promise<Readonly<Record<string, any>>>;
36
+ export declare function executionConfigFromPlan(plan: any): SessionExecutionProjection;
37
+ export declare function validateFrozenExecutionConfig(structural: SessionStructuralProjection, execution: SessionExecutionProjection, dependencies: { loadModule: (specifier: string) => Promise<any>; prepareRegistryModule?: (request: {id: string; from: string; authoredFrom: string}) => Promise<string | void> }): Promise<SessionExecutionProjection>;
38
+ export declare function driveHeadlessCaptainTurn(options: any): Promise<any>;
39
+ export declare function normalizeLaunchPlan(top: any, options?: any): Promise<any>;
40
+ export declare function loadLaunchPlan(options: any): Promise<any>;
41
+ export declare function composeGenericConfig(top: any, loadModule: (specifier: string) => Promise<any>, configPath?: string): Promise<any>;
42
+ export declare function projectTmuxConfig(plan: any): any;
43
+ export declare function resolveLaunchSessionsDir(options: any): string;
44
+
45
+ import type { SharedSessionStore, ReplayStreamStatus, ReplayStreamEntry, SessionGraph } from './session-store.js';
46
+ export interface OpenSessionHostOptions {
47
+ readonly store?: SharedSessionStore;
48
+ readonly sessionsDir?: string;
49
+ readonly sessionId?: string;
50
+ readonly mode?: 'new' | 'continue' | 'retry';
51
+ readonly cwd?: string;
52
+ readonly config?: SessionExecutionProjection;
53
+ readonly plan?: any;
54
+ readonly loadModule?: (specifier: string) => Promise<any>;
55
+ readonly prepareRegistryModule?: (request: {id: string; from: string; authoredFrom: string}) => Promise<string | void>;
56
+ readonly observers?: readonly { onRecord?(record: any): void | Promise<void> }[];
57
+ readonly onStoredRecord?: (record: ReplayStreamEntry, status: ReplayStreamStatus) => void | Promise<void>;
58
+ readonly onCheckpoint?: (record: SessionRecovery) => void | Promise<void>;
59
+ readonly onIncomplete?: () => void | Promise<void>;
60
+ readonly graphs?: readonly { playbookId: string; graph: SessionGraph | null }[];
61
+ readonly initialVisible?: readonly string[];
62
+ readonly [key: string]: any;
63
+ }
64
+ export interface SessionHostController {
65
+ readonly sessionId: string;
66
+ readonly host: SessionHost['host'];
67
+ readonly shell: PlaybookCaptainShell;
68
+ readonly lease: PlaybookSessionLifecycle;
69
+ read(): Promise<SessionRecovery | undefined>;
70
+ handleBossTurn(input: string): Promise<SessionRecovery>;
71
+ retry(): Promise<SessionRecovery>;
72
+ dispose(): Promise<void>;
73
+ }
74
+ export declare function openSessionHost(options: OpenSessionHostOptions): Promise<SessionHostController>;
75
+ export declare function discardSessionUncertain(store: SharedSessionStore, sessionId: string): Promise<SessionRecovery | undefined>;
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ export {
5
+ createCaptainSessionHost,
6
+ installRetainedGenerationsForLaunch,
7
+ executionConfigFromPlan,
8
+ validateFrozenExecutionConfig,
9
+ driveHeadlessCaptainTurn,
10
+ } from './bin/run.js';
11
+ export {
12
+ normalizeLaunchPlan,
13
+ loadLaunchPlan,
14
+ composeGenericConfig,
15
+ projectTmuxConfig,
16
+ resolveLaunchSessionsDir,
17
+ } from './bin/launch-config.js';
18
+ export { openSessionHost, discardSessionUncertain } from './bin/session-host.js';
@@ -48,7 +48,7 @@ export type ReplayStreamStatus =
48
48
  export interface PlaybookSessionSummary {
49
49
  readonly schemaVersion: number;
50
50
  readonly sessionId: string;
51
- readonly state: 'settled' | 'uncertain';
51
+ readonly state: 'settled' | 'uncertain' | 'history-only';
52
52
  readonly cwd: string;
53
53
  readonly updatedAt: string;
54
54
  }
@@ -80,3 +80,178 @@ export interface PlaybookSessionLease {
80
80
  streamStatus(): ReplayStreamStatus;
81
81
  release(): Promise<ReplayStreamStatus>;
82
82
  }
83
+
84
+ /** Recovery data is interpreted by the version-aware codec, not by live runtimes. */
85
+ export type SessionSnapshot = Readonly<Record<string, any>>;
86
+ export interface SessionEffectLedger {
87
+ readonly schemaVersion: 1;
88
+ readonly revision: number;
89
+ readonly boundaries: readonly Readonly<Record<string, any>>[];
90
+ readonly logicalOperations: readonly Readonly<Record<string, any>>[];
91
+ }
92
+ export interface SessionUnresolvedEffect {
93
+ readonly classification: 'one-descendant-commit' | 'multiple-commits' |
94
+ 'rewritten-or-non-descendant' | 'worktree-only-change' |
95
+ 'concurrent-or-foreign-change' | 'observation-ambiguous' | 'incomplete';
96
+ readonly baselineHead: string;
97
+ readonly afterHead?: string;
98
+ readonly commitOid?: string;
99
+ }
100
+ export type SessionRetentionUpdate =
101
+ | { readonly kind: 'retain'; readonly rootPlaybookId: string; readonly generation: Readonly<Record<string, any>> }
102
+ | { readonly kind: 'clear'; readonly rootPlaybookId: string };
103
+
104
+ export interface SessionExecutionProjection {
105
+ readonly schemaVersion: 2;
106
+ readonly captain: Readonly<Record<string, any>>;
107
+ readonly players: readonly Readonly<Record<string, any>>[];
108
+ readonly catalog: Readonly<Record<string, any>>;
109
+ }
110
+ export interface SessionStructuralProjection {
111
+ readonly schemaVersion: 1;
112
+ readonly captain: Readonly<Record<string, any>>;
113
+ readonly players: readonly Readonly<Record<string, any>>[];
114
+ readonly catalog: Readonly<Record<string, any>>;
115
+ }
116
+ export interface SessionRecovery {
117
+ readonly schemaVersion: 6;
118
+ readonly kind: 'captain-session';
119
+ readonly sessionId: string;
120
+ readonly cwd: string;
121
+ readonly createdAt: string;
122
+ readonly updatedAt: string;
123
+ readonly state: 'settled' | 'uncertain';
124
+ readonly structuralProjection: SessionStructuralProjection;
125
+ readonly lastAppliedExecutionProjection: SessionExecutionProjection;
126
+ readonly snapshot: SessionSnapshot;
127
+ readonly effectLedger: SessionEffectLedger;
128
+ readonly unresolvedEffects: readonly SessionUnresolvedEffect[];
129
+ readonly retainedGenerations?: Readonly<Record<string, any>>;
130
+ readonly uncertain?: {
131
+ readonly input: string;
132
+ readonly attemptId: string;
133
+ readonly attemptNumber: number;
134
+ readonly baseUpdatedAt: string | null;
135
+ readonly markedAt: string;
136
+ readonly attemptedExecutionProjection: SessionExecutionProjection;
137
+ readonly abandonment?: Readonly<Record<string, any>>;
138
+ };
139
+ }
140
+ export interface SessionReplayCheckpoint {
141
+ readonly seq: number;
142
+ readonly sha256: string;
143
+ readonly incomplete: boolean;
144
+ }
145
+ export type SessionManifest = Omit<SessionRecovery, 'schemaVersion'> & {
146
+ readonly schemaVersion: 7;
147
+ readonly replay: SessionReplayCheckpoint;
148
+ readonly contextSeq: number;
149
+ } | {
150
+ readonly schemaVersion: 7;
151
+ readonly kind: 'captain-session';
152
+ readonly state: 'history-only';
153
+ readonly sessionId: string;
154
+ readonly cwd: string;
155
+ readonly createdAt: string;
156
+ readonly updatedAt: string;
157
+ readonly replay: SessionReplayCheckpoint;
158
+ readonly contextSeq: number | null;
159
+ readonly reason: string;
160
+ };
161
+ export type StoredSessionManifest = SessionManifest | {
162
+ readonly schemaVersion: number;
163
+ readonly sessionId: string;
164
+ readonly cwd?: unknown;
165
+ readonly [key: string]: unknown;
166
+ };
167
+ export interface SessionGraph {
168
+ readonly initial: string;
169
+ readonly nodes: readonly { readonly id: string; readonly kind: 'state' | 'final'; readonly tags: readonly string[]; readonly parent?: string; readonly role?: string; readonly description?: string }[];
170
+ readonly edges: readonly { readonly id: string; readonly from: string; readonly to: string; readonly event: string }[];
171
+ }
172
+ export interface SessionContext {
173
+ readonly type: 'session_context';
174
+ readonly timestamp: number;
175
+ readonly contextVersion: 1;
176
+ readonly captainId: string;
177
+ readonly configuration: SessionExecutionProjection;
178
+ readonly graphs: readonly { readonly playbookId: string; readonly graph: SessionGraph | null }[];
179
+ readonly initialVisible: readonly string[];
180
+ }
181
+ export interface SessionHistory extends ReplayStreamReadResult {
182
+ readonly synthetic?: true;
183
+ readonly missing: boolean;
184
+ readonly incomplete: boolean;
185
+ readonly pendingTail: boolean;
186
+ readonly damage?: { readonly seq: number; readonly offset: number; readonly reason: string };
187
+ readonly digests: readonly string[];
188
+ readonly completeBytes: number;
189
+ }
190
+ export interface SessionValidation {
191
+ readonly integrityValid: boolean;
192
+ readonly sessionId: string;
193
+ readonly resumable: boolean;
194
+ readonly reasons: readonly string[];
195
+ readonly manifest: StoredSessionManifest;
196
+ readonly history: SessionHistory;
197
+ }
198
+ export interface SessionHints {
199
+ readonly players: Readonly<Record<string, string>>;
200
+ readonly captain?: { readonly kind: 'pinned'; readonly token: string } | { readonly kind: 'needsCatchUp'; readonly resume: string | false; readonly afterJournalSeq: number };
201
+ }
202
+ export interface SessionFreshBoundary {
203
+ readonly cwd: string;
204
+ readonly structuralProjection: SessionStructuralProjection;
205
+ readonly executionProjection: SessionExecutionProjection;
206
+ readonly snapshot: SessionSnapshot;
207
+ readonly context?: SessionContext;
208
+ readonly onLegacyRecord?: (record: any) => void | Promise<void>;
209
+ readonly onInvalidRecord?: (record: any) => void | Promise<void>;
210
+ }
211
+ export interface PlaybookSessionLifecycle extends PlaybookSessionLease {
212
+ read(): Promise<SessionRecovery | undefined>;
213
+ readManifest(): Promise<StoredSessionManifest>;
214
+ initializeSettledWithPredecessor(options: SessionFreshBoundary): Promise<SessionRecovery>;
215
+ abandonFreshSettled(options: { expected: SessionRecovery }): Promise<boolean>;
216
+ beginTurn(options: { input: string; attemptId: string; attemptedExecutionProjection: SessionExecutionProjection }): Promise<SessionRecovery>;
217
+ beginRetry(options: { expectedAttemptId: string; nextAttemptId: string }): Promise<SessionRecovery>;
218
+ settle(options: { attemptId: string; snapshot: SessionSnapshot; unresolvedEffects: readonly SessionUnresolvedEffect[]; retentionUpdates?: readonly SessionRetentionUpdate[] }): Promise<SessionRecovery>;
219
+ discard(options: { attemptId: string }): Promise<SessionRecovery | undefined>;
220
+ beginUnresolvedEffectAbandonment(options: any): Promise<any>;
221
+ completeUnresolvedEffectAbandonment(options: any): Promise<any>;
222
+ recoverUnresolvedEffectAbandonment(): Promise<SessionRecovery | undefined>;
223
+ writeEffectLedger(authority: any, commands: readonly Readonly<Record<string, any>>[]): Promise<SessionEffectLedger>;
224
+ assertOwner(): Promise<any>;
225
+ assertContinuable(context?: { cwd?: string; executionProjection?: SessionExecutionProjection }): Promise<SessionValidation>;
226
+ recordContext(context: SessionContext): Promise<number>;
227
+ consumeHints(): Promise<SessionHints>;
228
+ acknowledgeHint(participantId: string, token: string): void;
229
+ clearHint(participantId: string): void;
230
+ }
231
+ export interface SharedSessionStore {
232
+ readonly sessionsDir: string;
233
+ prepare(): Promise<void>;
234
+ read(sessionId: string): Promise<SessionRecovery>;
235
+ readManifest(sessionId: string): Promise<StoredSessionManifest>;
236
+ /** Observation only; mutations still require acquiring the lease. */
237
+ readLeaseState(sessionId: string): Promise<'active' | 'idle' | 'unknown'>;
238
+ readHistory(sessionId: string, options?: ReplayStreamReadOptions): Promise<SessionHistory>;
239
+ readStream(sessionId: string, options?: ReplayStreamReadOptions): Promise<ReplayStreamReadResult>;
240
+ readSummary(sessionId: string): Promise<PlaybookSessionSummary>;
241
+ listSummaries(): Promise<PlaybookSessionListResult>;
242
+ latest(options?: { preferredCwd?: string; onLegacyRecord?: (record: any) => void | Promise<void> }): Promise<SessionRecovery>;
243
+ acquire(sessionId: string): Promise<PlaybookSessionLifecycle>;
244
+ acquireManagement(sessionId: string): Promise<{ readonly sessionId: string; readonly ownerToken: string; assertOwner(): Promise<unknown>; release(): Promise<void> }>;
245
+ validate(sessionId: string, context?: { cwd?: string; executionProjection?: SessionExecutionProjection }): Promise<SessionValidation>;
246
+ delete(sessionId: string): Promise<void>;
247
+ migrateLegacyDefault(options?: { env?: Readonly<Record<string, string | undefined>>; homeDir?: string }): Promise<{ sourceDir: string; migrated: readonly string[]; skipped: readonly { sessionId: string; reason: string }[] }>;
248
+ migrate(sessionId: string, options?: { sourcePath?: string; cwd?: string; backupDir?: string }): Promise<{ manifest: SessionManifest; migrated: boolean; reasons: readonly string[] }>;
249
+ }
250
+ export declare function createSessionStore(options?: { sessionsDir?: string; env?: Readonly<Record<string, string | undefined>>; homeDir?: string; [key: string]: any }): SharedSessionStore;
251
+ export declare function validateSessionManifest(value: unknown): SessionManifest;
252
+ export declare function validateSessionContext(value: unknown): SessionContext;
253
+ export declare function projectCaptainSessionStructure(value: SessionExecutionProjection): SessionStructuralProjection;
254
+ export declare function validateCaptainSessionExecutionProjection(value: unknown): SessionExecutionProjection;
255
+ export declare function validateCaptainSessionStructuralProjection(value: unknown): SessionStructuralProjection;
256
+ export declare function assertCaptainSessionExecutionCompatible(structural: SessionStructuralProjection, execution: SessionExecutionProjection): SessionExecutionProjection;
257
+ export declare function attachSessionHints(snapshot: SessionSnapshot, hints: SessionHints): SessionSnapshot;
@@ -21,14 +21,17 @@ export function openSessionStore(sessionsDir) {
21
21
  }
22
22
 
23
23
  function wrapStore(store) {
24
+ let preparation;
25
+ const prepared = async (operation) => {
26
+ await (preparation ??= store.prepare());
27
+ return operation();
28
+ };
24
29
  return Object.freeze({
25
30
  sessionsDir: store.sessionsDir,
26
- list: async () => projectListResult(await store.listSummaries()),
27
- read: async (sessionId) =>
28
- projectSummary(await store.readSummary(sessionId)),
29
- readStream: async (sessionId, options) =>
30
- projectReadResult(await store.readStream(sessionId, options)),
31
- acquire: async (sessionId) => wrapLease(await store.acquire(sessionId)),
31
+ list: () => prepared(async () => projectListResult(await store.listSummaries())),
32
+ read: (sessionId) => prepared(async () => projectSummary(await store.readSummary(sessionId))),
33
+ readStream: (sessionId, options) => prepared(async () => projectReadResult(await store.readStream(sessionId, options))),
34
+ acquire: (sessionId) => prepared(async () => wrapLease(await store.acquire(sessionId))),
32
35
  });
33
36
  }
34
37
 
@@ -111,3 +114,16 @@ function projectStatus(status) {
111
114
  incomplete: status.incomplete,
112
115
  });
113
116
  }
117
+
118
+ export {
119
+ createCaptainSessionStore as createSessionStore,
120
+ projectCaptainSessionStructure,
121
+ validateCaptainSessionExecutionProjection,
122
+ validateCaptainSessionStructuralProjection,
123
+ assertCaptainSessionExecutionCompatible,
124
+ } from './bin/session-store.js';
125
+ export {
126
+ validateSessionManifest,
127
+ validateSessionContext,
128
+ attachSessionHints,
129
+ } from './bin/portable-codec.js';
@@ -15,6 +15,8 @@ When the caller gives a topic, Captain shall relay the complete topic in quotes
15
15
  Captain shall not wait for either proposal before requesting the other.
16
16
  Captain shall give the player bound to each role the following instruction:
17
17
 
18
+ > Original topic: <caller-topic>
19
+
18
20
  ```markdown
19
21
  Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.
20
22
  Propose your design.
@@ -7,7 +7,7 @@ import { assign, fromPromise, setup } from 'xstate';
7
7
  export const concurrentRoleSets = [['coder', 'reviewer']];
8
8
  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>`.";
9
9
  const INDEPENDENT_PROPOSAL_PROMPT = [
10
- '> <caller-topic>',
10
+ '> Original topic: <caller-topic>',
11
11
  '',
12
12
  'Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.',
13
13
  'Propose your design.',
@@ -148,7 +148,7 @@ const NEEDS_BOSS_REPLY_DESCRIPTION =
148
148
  "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>`.";
149
149
 
150
150
  const INDEPENDENT_PROPOSAL_PROMPT = [
151
- '> <caller-topic>',
151
+ '> Original topic: <caller-topic>',
152
152
  '',
153
153
  'Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.',
154
154
  'Propose your design.',
@@ -22,7 +22,7 @@ Parallel group: independent-proposals
22
22
 
23
23
  When the caller gives a topic, Captain shall relay the complete topic to Coder in quotes and prompt Coder:
24
24
 
25
- > > <caller-topic>
25
+ > > Original topic: <caller-topic>
26
26
  >
27
27
  > Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.
28
28
  > Propose your design.
@@ -39,7 +39,7 @@ Parallel group: independent-proposals
39
39
 
40
40
  When the caller gives a topic, Captain shall relay the complete topic to Reviewer in quotes and prompt Reviewer:
41
41
 
42
- > > <caller-topic>
42
+ > > Original topic: <caller-topic>
43
43
  >
44
44
  > Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.
45
45
  > Propose your design.
@@ -10,7 +10,7 @@ export interface DecidePlaybookRuntimeConstruction {
10
10
  readonly hostCapabilities: DecidePlaybookHostCapabilities;
11
11
  }
12
12
  type PromptIdentity = (roleId: RoleId) => string;
13
- declare function composePlayerPrompt(input: PlayerInput, promptIdentity: PromptIdentity): string;
13
+ declare function composePlayerPrompt(input: PlayerInput, promptIdentity: PromptIdentity, resuming?: boolean): string;
14
14
  declare function requiredFieldsFor(description: string): string[];
15
15
  declare function extractJson(raw: string): Record<string, unknown> | null;
16
16
  declare function buildClassifierPrompt(text: string, ctx: {
@@ -67,7 +67,6 @@ export declare const _internal: {
67
67
  VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string>;
68
68
  BOSS_INTERRUPT_TARGETS: readonly ["independentProposals"];
69
69
  UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string>;
70
- CONTINUATION_PREAMBLE: string;
71
70
  TELEMETRY_TOPIC: string;
72
71
  };
73
72
  export default createPlaybookRuntime;
@@ -19,7 +19,7 @@ import { randomUUID } from 'node:crypto';
19
19
  import PQueue from 'p-queue';
20
20
  import { createActor, fromPromise } from 'xstate';
21
21
  import { createAcceptedOutcomeConsumer, } from '../../../src/accepted-outcome.js';
22
- import { assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, renderGovernedOutcomeContract, snapshotJsonValue, snapshotPlaybookSession, terminalOutcomesFromMachine, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
22
+ import { assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, combineAbortSignals, composePlayerContinuation, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, renderGovernedOutcomeContract, snapshotJsonValue, snapshotPlaybookSession, terminalOutcomesFromMachine, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
23
23
  import decideMachine from './decide.fsm.js';
24
24
  function snapshotDecideRuntimeOptions(value) {
25
25
  const captured = snapshotJsonValue(value, 'DECIDE runtime options');
@@ -125,7 +125,6 @@ const TELEMETRY_TOPIC = 'playbook.fsm.state';
125
125
  const TRACE_TOPIC = 'playbook.trace';
126
126
  const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID = 'reconcile:unresolved-effect';
127
127
  const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
128
- const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
129
128
  const PLACEHOLDER_FIELDS = [
130
129
  ['<caller-topic>', 'callerTopic'],
131
130
  ['<reviewer-proposal>', 'reviewerProposal'],
@@ -135,19 +134,7 @@ const VERBATIM_PAYLOAD_FIELDS = new Set([
135
134
  'reviewerProposal',
136
135
  'coderOutput',
137
136
  ]);
138
- function composePlayerPrompt(input, promptIdentity) {
139
- const blocks = [];
140
- if (input.pendingBossQuestion && input.bossReply !== undefined) {
141
- blocks.push([
142
- CONTINUATION_PREAMBLE,
143
- '',
144
- 'Boss question:',
145
- input.pendingBossQuestion.question,
146
- '',
147
- 'Boss reply:',
148
- input.bossReply,
149
- ].join('\n'));
150
- }
137
+ function composePlayerPrompt(input, promptIdentity, resuming = false) {
151
138
  const replacements = new Map();
152
139
  for (const [placeholder, field] of PLACEHOLDER_FIELDS) {
153
140
  const value = input[field];
@@ -171,8 +158,7 @@ function composePlayerPrompt(input, promptIdentity) {
171
158
  ? value.replaceAll('\n', '\n> ')
172
159
  : value;
173
160
  });
174
- blocks.push(body);
175
- return blocks.join('\n\n');
161
+ return composePlayerContinuation(input, body, resuming);
176
162
  }
177
163
  // A `result` description names required payload fields in its
178
164
  // "Output shall include ..." sentence.
@@ -883,7 +869,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
883
869
  };
884
870
  const resolvedPlayerId = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.playerId;
885
871
  const promptIdentity = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.promptIdentity ?? roleId;
886
- const composeInvocationPrompt = (input) => {
872
+ const composeInvocationPrompt = (input, resuming = false) => {
887
873
  let active = true;
888
874
  const lookup = (roleId) => {
889
875
  if (!active) {
@@ -895,7 +881,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
895
881
  return promptIdentity(roleId);
896
882
  };
897
883
  try {
898
- return composePlayerPrompt(input, lookup);
884
+ return composePlayerPrompt(input, lookup, resuming);
899
885
  }
900
886
  finally {
901
887
  active = false;
@@ -1339,7 +1325,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
1339
1325
  const roleId = input.role;
1340
1326
  const playerId = resolvedPlayerId(roleId);
1341
1327
  const playerKey = continuationKey(roleId, playerId);
1342
- const prompt = composeInvocationPrompt(input);
1328
+ const freshPrompt = composeInvocationPrompt(input);
1343
1329
  let resume;
1344
1330
  try {
1345
1331
  signal.throwIfAborted();
@@ -1353,6 +1339,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
1353
1339
  latchControlPlaneError(error, signal);
1354
1340
  throw error;
1355
1341
  }
1342
+ const prompt = resume === false ? freshPrompt : composeInvocationPrompt(input, true);
1356
1343
  const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
1357
1344
  const identity = {
1358
1345
  stateId: input.stateId,
@@ -1384,7 +1371,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
1384
1371
  let rawResult;
1385
1372
  try {
1386
1373
  signal.throwIfAborted();
1387
- const boundary = Promise.resolve(requirePorts().callPlayer(roleId, prompt, signal, { resume }));
1374
+ const boundary = Promise.resolve(requirePorts().callPlayer(roleId, prompt, signal, { resume, ...(prompt === freshPrompt ? {} : { freshPrompt }) }));
1388
1375
  rawResult = await boundary;
1389
1376
  // An XState sibling cancellation does not cancel an arbitrary coder
1390
1377
  // promise. Re-check before a late resolution can mutate continuity or
@@ -3586,7 +3573,6 @@ export const _internal = {
3586
3573
  VERBATIM_PAYLOAD_FIELDS,
3587
3574
  BOSS_INTERRUPT_TARGETS,
3588
3575
  UNFINISHED_FINAL_STATE_IDS,
3589
- CONTINUATION_PREAMBLE,
3590
3576
  TELEMETRY_TOPIC,
3591
3577
  };
3592
3578
  export default createPlaybookRuntime;
@@ -32,6 +32,7 @@ import {
32
32
  assertPlaybookEffectLedger,
33
33
  assertPlaybookRuntimeSnapshot,
34
34
  combineAbortSignals,
35
+ composePlayerContinuation,
35
36
  createNestedPlaybookBridge,
36
37
  detachPersistedMachineSnapshot,
37
38
  normalizeError,
@@ -286,8 +287,6 @@ const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID =
286
287
  const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID =
287
288
  'abandon:unresolved-effect';
288
289
 
289
- const CONTINUATION_PREAMBLE =
290
- 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
291
290
 
292
291
  const PLACEHOLDER_FIELDS: ReadonlyArray<readonly [string, keyof PlayerInput]> =
293
292
  [
@@ -306,23 +305,8 @@ type PromptIdentity = (roleId: RoleId) => string;
306
305
  function composePlayerPrompt(
307
306
  input: PlayerInput,
308
307
  promptIdentity: PromptIdentity,
308
+ resuming = false,
309
309
  ): string {
310
- const blocks: string[] = [];
311
-
312
- if (input.pendingBossQuestion && input.bossReply !== undefined) {
313
- blocks.push(
314
- [
315
- CONTINUATION_PREAMBLE,
316
- '',
317
- 'Boss question:',
318
- input.pendingBossQuestion.question,
319
- '',
320
- 'Boss reply:',
321
- input.bossReply,
322
- ].join('\n'),
323
- );
324
- }
325
-
326
310
  const replacements = new Map<string, string>();
327
311
  for (const [placeholder, field] of PLACEHOLDER_FIELDS) {
328
312
  const value = input[field];
@@ -348,8 +332,7 @@ function composePlayerPrompt(
348
332
  },
349
333
  );
350
334
 
351
- blocks.push(body);
352
- return blocks.join('\n\n');
335
+ return composePlayerContinuation(input, body, resuming);
353
336
  }
354
337
 
355
338
  // A `result` description names required payload fields in its
@@ -1518,7 +1501,7 @@ function createDecidePlaybookRuntime(
1518
1501
  const promptIdentity = (roleId: RoleId): string =>
1519
1502
  requireSessionIdentity().roleBindings?.[roleId]?.promptIdentity ?? roleId;
1520
1503
 
1521
- const composeInvocationPrompt = (input: PlayerInput): string => {
1504
+ const composeInvocationPrompt = (input: PlayerInput, resuming = false): string => {
1522
1505
  let active = true;
1523
1506
  const lookup: PromptIdentity = (roleId) => {
1524
1507
  if (!active) {
@@ -1534,7 +1517,7 @@ function createDecidePlaybookRuntime(
1534
1517
  return promptIdentity(roleId);
1535
1518
  };
1536
1519
  try {
1537
- return composePlayerPrompt(input, lookup);
1520
+ return composePlayerPrompt(input, lookup, resuming);
1538
1521
  } finally {
1539
1522
  active = false;
1540
1523
  }
@@ -2182,7 +2165,7 @@ function createDecidePlaybookRuntime(
2182
2165
  const roleId = input.role;
2183
2166
  const playerId = resolvedPlayerId(roleId);
2184
2167
  const playerKey = continuationKey(roleId, playerId);
2185
- const prompt = composeInvocationPrompt(input);
2168
+ const freshPrompt = composeInvocationPrompt(input);
2186
2169
  let resume: PlayerCallOptions['resume'];
2187
2170
  try {
2188
2171
  signal.throwIfAborted();
@@ -2195,6 +2178,7 @@ function createDecidePlaybookRuntime(
2195
2178
  latchControlPlaneError(error, signal);
2196
2179
  throw error;
2197
2180
  }
2181
+ const prompt = resume === false ? freshPrompt : composeInvocationPrompt(input, true);
2198
2182
  const callId =
2199
2183
  continuation?.callId ?? `player-${++playerCallSequence}`;
2200
2184
  const identity = {
@@ -2249,7 +2233,7 @@ function createDecidePlaybookRuntime(
2249
2233
  try {
2250
2234
  signal.throwIfAborted();
2251
2235
  const boundary = Promise.resolve(
2252
- requirePorts().callPlayer(roleId, prompt, signal, { resume }),
2236
+ requirePorts().callPlayer(roleId, prompt, signal, { resume, ...(prompt === freshPrompt ? {} : { freshPrompt }) }),
2253
2237
  );
2254
2238
  rawResult = await boundary;
2255
2239
  // An XState sibling cancellation does not cancel an arbitrary coder
@@ -5081,7 +5065,6 @@ export const _internal = {
5081
5065
  VERBATIM_PAYLOAD_FIELDS,
5082
5066
  BOSS_INTERRUPT_TARGETS,
5083
5067
  UNFINISHED_FINAL_STATE_IDS,
5084
- CONTINUATION_PREAMBLE,
5085
5068
  TELEMETRY_TOPIC,
5086
5069
  };
5087
5070
 
@@ -15,6 +15,10 @@ It coordinates existing playbooks and owns no repository commit itself.
15
15
 
16
16
  At the start of `dev` and after each Boss reply, Captain shall relay the development request, relevant discussion context, and any relevant run results to Analyst in quotes (`>`), along with the following instruction:
17
17
 
18
+ > Original request: <development-request>
19
+ > Prior discussion: <discussion-context>
20
+ > Run results: <run-results>
21
+
18
22
  ```markdown
19
23
  Inspect the request and the relevant repository and specs only as needed to determine the smallest sound next step.
20
24
  Do not change files or commit while planning or discussing the request.
@@ -35,15 +39,31 @@ No outcome depends on a fixed presentation format of Analyst's reply.
35
39
  `dev` shall act on the accepted outcome itself and shall not return to the session Captain for another routing decision.
36
40
 
37
41
  For needs Boss reply, `dev` shall use the standard Boss-question suspension with Analyst's complete response.
38
- The session Captain shall present that response to Boss and, after Boss replies, resume `dev` with the question and answer in the same Analyst conversation.
42
+ The session Captain shall present that response to Boss and, after Boss replies, resume `dev` with the answer in the same Analyst conversation; include the previous question only when that conversation must start fresh.
39
43
 
40
44
  Discussion complete is available only after a Boss reply, when any useful analysis has already been presented through needs Boss reply.
41
45
  It completes `dev` without a child call or repository change.
42
46
 
43
47
  For code, `dev` shall directly call playbook `code` with the development request, relevant discussion context, and planning result in quotes (`>`).
44
48
 
49
+ > Original request: <development-request>
50
+ > Prior discussion: <discussion-context>
51
+ > Planning result: <planning-result>
52
+
45
53
  For decide then code, `dev` shall call playbook `decide` with the development request, relevant discussion context, and planning result in quotes (`>`).
54
+
55
+ > Original request: <development-request>
56
+ > Prior discussion: <discussion-context>
57
+ > Planning result: <planning-result>
58
+
46
59
  Only after `decide` succeeds shall `dev` call playbook `code` with the development request, relevant discussion context, planning result, `decide`-owned commit, and exact evaluated repository revision in quotes (`>`).
60
+
61
+ > Original request: <development-request>
62
+ > Prior discussion: <discussion-context>
63
+ > Planning result: <planning-result>
64
+ > DECIDE commit: <decide-commit>
65
+ > Evaluated revision: <evaluated-revision>
66
+
47
67
  `dev` shall not separately call `review` for the design scope already reviewed by `decide`.
48
68
 
49
69
  `dev` completes with the successful result of its final child call.