@sublang/playbook 11.0.0 → 12.0.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.
- package/docs/cli.md +9 -6
- package/docs/configuration.md +15 -1
- package/package.json +22 -3
- package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
- package/reference/sdlc/code.playbook/code.fsm.js +116 -52
- package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
- package/reference/sdlc/code.playbook/code.gears.md +40 -20
- package/reference/sdlc/code.playbook/code.playbook.js +23 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +18 -2
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
- package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
- package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
- package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
- package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
- package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
- package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
- package/reference/sdlc/dev.md +52 -0
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
- package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
- package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
- package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
- package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
- package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
- package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
- package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
- package/reference/sdlc/review.playbook/review.fsm.js +77 -27
- package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
- package/reference/sdlc/review.playbook/review.gears.md +52 -26
- package/reference/sdlc/review.playbook/review.playbook.js +17 -7
- package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
- package/reference/sdlc/review.playbook/review.registry.js +1 -1
- package/reference/sdlc/review.playbook/review.registry.ts +1 -1
- package/slc/link.md +12 -5
- package/slc/text2gears.md +3 -0
- package/src/xstate-playbook-runtime.js +5 -2
- package/src/xstate-playbook-runtime.ts +5 -2
- package/src/xstate-runtime.js +13 -1
- package/src/xstate-runtime.ts +13 -1
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// Role metadata: Coder→coder (canonical local role)
|
|
7
7
|
// Boss event: deterministic START_CODE entry; exact Boss text becomes
|
|
8
8
|
// callerInput; pending Coder questions retain BOSS_REPLY
|
|
9
|
-
// Adjudication: LLM judge per player state; coderOutput is carried verbatim
|
|
9
|
+
// Adjudication: LLM judge per player state; coderOutput is carried verbatim;
|
|
10
|
+
// the first phase decides among four semantic outcomes
|
|
10
11
|
// Nested call: literal review target through the shared bridge
|
|
11
12
|
// Compat: artifact schema 3 / runtime ABI 1
|
|
12
13
|
|
|
@@ -195,7 +196,7 @@ const runtimeSpec = {
|
|
|
195
196
|
runFirstPhase: {
|
|
196
197
|
role: 'coder',
|
|
197
198
|
label:
|
|
198
|
-
'Coder is
|
|
199
|
+
'Coder is running the first coding phase: a direct implementation, a new intent record, or an existing intent-record task.',
|
|
199
200
|
},
|
|
200
201
|
runIrTask: {
|
|
201
202
|
role: 'coder',
|
|
@@ -213,6 +214,23 @@ const runtimeSpec = {
|
|
|
213
214
|
repositoryDisposition: 'one-descendant-commit',
|
|
214
215
|
},
|
|
215
216
|
irCommit: {
|
|
217
|
+
fields: {
|
|
218
|
+
coderOutput: 'presentation',
|
|
219
|
+
latestCommit: 'effect',
|
|
220
|
+
irNumber: 'semantic',
|
|
221
|
+
},
|
|
222
|
+
repositoryDisposition: 'one-descendant-commit',
|
|
223
|
+
},
|
|
224
|
+
moreTasks: {
|
|
225
|
+
fields: {
|
|
226
|
+
coderOutput: 'presentation',
|
|
227
|
+
latestCommit: 'effect',
|
|
228
|
+
irNumber: 'semantic',
|
|
229
|
+
irTask: 'semantic',
|
|
230
|
+
},
|
|
231
|
+
repositoryDisposition: 'one-descendant-commit',
|
|
232
|
+
},
|
|
233
|
+
finalTask: {
|
|
216
234
|
fields: {
|
|
217
235
|
coderOutput: 'presentation',
|
|
218
236
|
latestCommit: 'effect',
|
|
@@ -231,6 +249,7 @@ const runtimeSpec = {
|
|
|
231
249
|
fields: {
|
|
232
250
|
coderOutput: 'presentation',
|
|
233
251
|
latestCommit: 'effect',
|
|
252
|
+
irNumber: 'semantic',
|
|
234
253
|
irTask: 'semantic',
|
|
235
254
|
},
|
|
236
255
|
repositoryDisposition: 'one-descendant-commit',
|
|
@@ -239,6 +258,8 @@ const runtimeSpec = {
|
|
|
239
258
|
fields: {
|
|
240
259
|
coderOutput: 'presentation',
|
|
241
260
|
latestCommit: 'effect',
|
|
261
|
+
irNumber: 'semantic',
|
|
262
|
+
irTask: 'semantic',
|
|
242
263
|
},
|
|
243
264
|
repositoryDisposition: 'one-descendant-commit',
|
|
244
265
|
},
|
|
@@ -63,13 +63,24 @@ players:
|
|
|
63
63
|
permissions:
|
|
64
64
|
mode: auto
|
|
65
65
|
|
|
66
|
+
# DEV's planner stays a distinct player: equal ids deliberately share one
|
|
67
|
+
# provider conversation, and planning context must not bleed into review
|
|
68
|
+
# conversations (DR-044).
|
|
69
|
+
dev.analyst:
|
|
70
|
+
adapter: claude
|
|
71
|
+
model: claude-opus-5
|
|
72
|
+
effort: xhigh
|
|
73
|
+
permissions:
|
|
74
|
+
mode: auto
|
|
75
|
+
|
|
66
76
|
# Enabled playbooks. Each is loaded from its explicit `from` module; the
|
|
67
77
|
# `<id>` key must equal that module's manifest id. `from`, `command`, and
|
|
68
78
|
# `roles` are launcher-owned; every other key is that playbook's option
|
|
69
79
|
# slice.
|
|
70
80
|
#
|
|
71
|
-
# Every manifest role is bound explicitly.
|
|
72
|
-
# reuse the same two stable players
|
|
81
|
+
# Every manifest role is bound explicitly. CODE, REVIEW, and DECIDE
|
|
82
|
+
# intentionally reuse the same two stable players, while DEV keeps its own
|
|
83
|
+
# Analyst; change an id to isolate its conversation.
|
|
73
84
|
playbooks:
|
|
74
85
|
code:
|
|
75
86
|
from: "@sublang/playbook/code/registry"
|
|
@@ -87,3 +98,8 @@ playbooks:
|
|
|
87
98
|
roles:
|
|
88
99
|
coder: dev.coder
|
|
89
100
|
reviewer: dev.reviewer
|
|
101
|
+
|
|
102
|
+
dev:
|
|
103
|
+
from: "@sublang/playbook/dev/registry"
|
|
104
|
+
roles:
|
|
105
|
+
analyst: dev.analyst
|
|
@@ -19,7 +19,12 @@ type PendingBossQuestions = Partial<Record<ResumableStateId, PendingBossQuestion
|
|
|
19
19
|
type BossReplies = Partial<Record<ResumableStateId, string>>;
|
|
20
20
|
type PendingBossQuestionParams = Omit<PendingBossQuestion, 'questionId'>;
|
|
21
21
|
export interface ReviewSuccessOutput {
|
|
22
|
-
|
|
22
|
+
evaluatedRevision: string;
|
|
23
|
+
noUnsettledFindings: true;
|
|
24
|
+
}
|
|
25
|
+
export interface DecideSuccessOutput {
|
|
26
|
+
decideCommit: string;
|
|
27
|
+
evaluatedRevision: string;
|
|
23
28
|
noUnsettledFindings: true;
|
|
24
29
|
}
|
|
25
30
|
export interface DecideFailureOutput {
|
|
@@ -31,7 +36,7 @@ export interface DecideFailureOutput {
|
|
|
31
36
|
message: string;
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
|
-
export type DecideOutput =
|
|
39
|
+
export type DecideOutput = DecideSuccessOutput | DecideFailureOutput;
|
|
35
40
|
interface ChildFailure {
|
|
36
41
|
status: 'aborted' | 'error';
|
|
37
42
|
playbookId: 'review';
|
|
@@ -45,6 +50,7 @@ export interface DecideContext {
|
|
|
45
50
|
coderProposal?: string;
|
|
46
51
|
reviewerProposal?: string;
|
|
47
52
|
latestCommit?: string;
|
|
53
|
+
coderOutput?: string;
|
|
48
54
|
reviewResult?: ReviewSuccessOutput;
|
|
49
55
|
reviewFailure?: ChildFailure;
|
|
50
56
|
lastResult?: PlayerOutput;
|
|
@@ -74,6 +80,7 @@ export interface PlayerInput {
|
|
|
74
80
|
result: Record<string, string>;
|
|
75
81
|
role: Role;
|
|
76
82
|
callerTopic?: string;
|
|
83
|
+
reviewerProposal?: string;
|
|
77
84
|
pendingBossQuestion?: PendingBossQuestion;
|
|
78
85
|
bossReply?: string;
|
|
79
86
|
}
|
|
@@ -139,9 +146,6 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
|
|
|
139
146
|
} | {
|
|
140
147
|
type: "rememberBossReply";
|
|
141
148
|
params: import("xstate").NonReducibleUnknown;
|
|
142
|
-
} | {
|
|
143
|
-
type: "clearBossReplyContext";
|
|
144
|
-
params: import("xstate").NonReducibleUnknown;
|
|
145
149
|
} | {
|
|
146
150
|
type: "copyInterruptedTopic";
|
|
147
151
|
params: import("xstate").NonReducibleUnknown;
|
|
@@ -186,6 +190,9 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
|
|
|
186
190
|
} | {
|
|
187
191
|
type: "clearProposalRoundContext";
|
|
188
192
|
params: import("xstate").NonReducibleUnknown;
|
|
193
|
+
} | {
|
|
194
|
+
type: "clearBossReplyContext";
|
|
195
|
+
params: import("xstate").NonReducibleUnknown;
|
|
189
196
|
}, {
|
|
190
197
|
type: "authoredReviewFailure";
|
|
191
198
|
params: unknown;
|
|
@@ -212,7 +219,7 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
|
|
|
212
219
|
coder: "complete" | "working" | "waiting";
|
|
213
220
|
reviewer: "complete" | "working" | "waiting";
|
|
214
221
|
};
|
|
215
|
-
}, string, Readonly<Record<string, never>>,
|
|
222
|
+
}, string, Readonly<Record<string, never>>, DecideSuccessOutput | DecideFailureOutput, import("xstate").EventObject, import("xstate").MetaObject, {
|
|
216
223
|
id: "decide";
|
|
217
224
|
states: {
|
|
218
225
|
readonly ready: {
|
|
@@ -15,30 +15,40 @@ const INDEPENDENT_PROPOSAL_PROMPT = [
|
|
|
15
15
|
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
16
16
|
'Do not change any files.',
|
|
17
17
|
].join('\n');
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const SYNTHESIZE_COMMIT_PROMPT = [
|
|
19
|
+
"Synthesize your independent proposal with Reviewer's proposal below.",
|
|
20
|
+
'Keep to the original topic below and follow what it asks.',
|
|
21
|
+
'Keep the best, essential parts of either proposal and reject any point that is unsound, unnecessary, or outside the topic.',
|
|
22
|
+
'Turn the resulting design into the necessary DRs and/or spec items.',
|
|
20
23
|
'Follow @specs/meta.md and update @specs/map.md when needed.',
|
|
21
|
-
|
|
22
|
-
'Do not change code or implement the proposal.',
|
|
24
|
+
'Do not change code or implement the design.',
|
|
23
25
|
'',
|
|
24
26
|
'Commit the result as one new commit, following @specs/packages/git.md.',
|
|
25
27
|
'Make the commit message explain concisely what changed and why.',
|
|
26
|
-
'
|
|
28
|
+
'Identify every new commit you make.',
|
|
29
|
+
'Coder is <coder-llm> and Reviewer is <reviewer-llm>.',
|
|
30
|
+
'',
|
|
31
|
+
'> Original topic: <caller-topic>',
|
|
32
|
+
"> Reviewer's independent proposal: <reviewer-proposal>",
|
|
27
33
|
].join('\n');
|
|
28
34
|
const REVIEW_INPUT_TEMPLATE = [
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'',
|
|
33
|
-
'Initial intent: <caller-topic>.',
|
|
34
|
-
"Coder's independent proposal: <coder-proposal>.",
|
|
35
|
+
'> Original intent: <caller-topic>',
|
|
36
|
+
'> Review scope: the `decide`-owned commit <decide-commit> and its resulting repository state.',
|
|
37
|
+
'> Coder output: <coder-output>',
|
|
35
38
|
].join('\n');
|
|
36
|
-
function composeReviewInput(callerTopic,
|
|
39
|
+
function composeReviewInput(callerTopic, decideCommit, coderOutput) {
|
|
37
40
|
const fields = {
|
|
38
41
|
'<caller-topic>': callerTopic ?? '',
|
|
39
|
-
'<
|
|
42
|
+
'<decide-commit>': decideCommit ?? '',
|
|
43
|
+
'<coder-output>': coderOutput ?? '',
|
|
40
44
|
};
|
|
41
|
-
return REVIEW_INPUT_TEMPLATE.replace(/<caller-topic>|<coder-
|
|
45
|
+
return REVIEW_INPUT_TEMPLATE.replace(/<caller-topic>|<decide-commit>|<coder-output>/g, (placeholder, offset, source) => {
|
|
46
|
+
const value = fields[placeholder];
|
|
47
|
+
const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
|
|
48
|
+
return source.slice(lineStart, offset).startsWith('> ')
|
|
49
|
+
? value.replaceAll('\n', '\n> ')
|
|
50
|
+
: value;
|
|
51
|
+
});
|
|
42
52
|
}
|
|
43
53
|
const outputOf = (event) => event.output;
|
|
44
54
|
const playbookOutputOf = (event) => event.output;
|
|
@@ -170,19 +180,23 @@ function isPlaybookState(value) {
|
|
|
170
180
|
value.activeStateIds.length === 1 &&
|
|
171
181
|
value.activeStateIds[0] === value.stateId)));
|
|
172
182
|
}
|
|
183
|
+
// DECIDE supplied `review` a scope rooted at the `decide`-owned commit, so a
|
|
184
|
+
// result applies to that scope only when it is the exact declared terminal
|
|
185
|
+
// contract: the evaluated repository revision plus the affirmative
|
|
186
|
+
// no-unsettled-findings fact. Any other shape does not establish those facts.
|
|
173
187
|
function reviewSuccessFrom(event) {
|
|
174
188
|
const output = playbookOutputOf(event);
|
|
175
189
|
if (!isPlainRecord(output))
|
|
176
190
|
return undefined;
|
|
177
|
-
if (!isExactKeys(output, ['
|
|
191
|
+
if (!isExactKeys(output, ['evaluatedRevision', 'noUnsettledFindings'])) {
|
|
178
192
|
return undefined;
|
|
179
193
|
}
|
|
180
|
-
if (output.
|
|
194
|
+
if (!isNonEmptyString(output.evaluatedRevision) ||
|
|
181
195
|
output.noUnsettledFindings !== true) {
|
|
182
196
|
return undefined;
|
|
183
197
|
}
|
|
184
198
|
return {
|
|
185
|
-
|
|
199
|
+
evaluatedRevision: output.evaluatedRevision,
|
|
186
200
|
noUnsettledFindings: true,
|
|
187
201
|
};
|
|
188
202
|
}
|
|
@@ -303,6 +317,7 @@ export const decideMachine = setup({
|
|
|
303
317
|
coderProposal: undefined,
|
|
304
318
|
reviewerProposal: undefined,
|
|
305
319
|
latestCommit: undefined,
|
|
320
|
+
coderOutput: undefined,
|
|
306
321
|
reviewResult: undefined,
|
|
307
322
|
reviewFailure: undefined,
|
|
308
323
|
lastResult: undefined,
|
|
@@ -317,6 +332,7 @@ export const decideMachine = setup({
|
|
|
317
332
|
coderProposal: undefined,
|
|
318
333
|
reviewerProposal: undefined,
|
|
319
334
|
latestCommit: undefined,
|
|
335
|
+
coderOutput: undefined,
|
|
320
336
|
reviewResult: undefined,
|
|
321
337
|
reviewFailure: undefined,
|
|
322
338
|
lastResult: undefined,
|
|
@@ -346,6 +362,7 @@ export const decideMachine = setup({
|
|
|
346
362
|
}),
|
|
347
363
|
rememberCommit: assign({
|
|
348
364
|
latestCommit: ({ event }) => outputOf(event).latestCommit,
|
|
365
|
+
coderOutput: ({ event }) => outputOf(event).coderOutput,
|
|
349
366
|
lastResult: ({ event }) => outputOf(event),
|
|
350
367
|
lastError: undefined,
|
|
351
368
|
}),
|
|
@@ -364,7 +381,7 @@ export const decideMachine = setup({
|
|
|
364
381
|
playbookId: 'review',
|
|
365
382
|
error: {
|
|
366
383
|
name: 'ReviewProtocolError',
|
|
367
|
-
message: 'REVIEW returned without
|
|
384
|
+
message: 'REVIEW returned without the evaluated repository revision and a no-unsettled-findings result for the supplied scope.',
|
|
368
385
|
},
|
|
369
386
|
},
|
|
370
387
|
lastError: undefined,
|
|
@@ -429,8 +446,16 @@ export const decideMachine = setup({
|
|
|
429
446
|
initial: 'ready',
|
|
430
447
|
context: () => ({}),
|
|
431
448
|
output: ({ context }) => {
|
|
432
|
-
if (context.reviewResult)
|
|
433
|
-
|
|
449
|
+
if (context.reviewResult) {
|
|
450
|
+
if (!context.latestCommit) {
|
|
451
|
+
throw new Error('DECIDE completed without its decide-owned commit');
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
decideCommit: context.latestCommit,
|
|
455
|
+
evaluatedRevision: context.reviewResult.evaluatedRevision,
|
|
456
|
+
noUnsettledFindings: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
434
459
|
if (!context.latestCommit || !context.reviewFailure) {
|
|
435
460
|
throw new Error('DECIDE reached a final state without a result');
|
|
436
461
|
}
|
|
@@ -498,7 +523,7 @@ export const decideMachine = setup({
|
|
|
498
523
|
role: 'coder',
|
|
499
524
|
prompt: INDEPENDENT_PROPOSAL_PROMPT,
|
|
500
525
|
result: withNeedsBossReply({
|
|
501
|
-
proposed: 'Coder
|
|
526
|
+
proposed: 'Coder affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `coderProposal: <verbatim final text>`.',
|
|
502
527
|
}),
|
|
503
528
|
callerTopic: context.callerTopic,
|
|
504
529
|
...bossReplyFields(context, 'askCoderProposal'),
|
|
@@ -634,7 +659,7 @@ export const decideMachine = setup({
|
|
|
634
659
|
role: 'reviewer',
|
|
635
660
|
prompt: INDEPENDENT_PROPOSAL_PROMPT,
|
|
636
661
|
result: withNeedsBossReply({
|
|
637
|
-
proposed: 'Reviewer
|
|
662
|
+
proposed: 'Reviewer affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `reviewerProposal: <verbatim final text>`.',
|
|
638
663
|
}),
|
|
639
664
|
callerTopic: context.callerTopic,
|
|
640
665
|
...bossReplyFields(context, 'askReviewerProposal'),
|
|
@@ -757,11 +782,11 @@ export const decideMachine = setup({
|
|
|
757
782
|
commitCoderProposal: {
|
|
758
783
|
id: 'commitCoderProposal',
|
|
759
784
|
tags: 'playbook.busy',
|
|
760
|
-
description: 'Coder
|
|
785
|
+
description: 'Coder synthesizes both proposals and commits the design.',
|
|
761
786
|
meta: {
|
|
762
787
|
playbook: {
|
|
763
788
|
stateId: 'commitCoderProposal',
|
|
764
|
-
description: 'Coder
|
|
789
|
+
description: 'Coder synthesizes both proposals and commits the design.',
|
|
765
790
|
role: 'coder',
|
|
766
791
|
},
|
|
767
792
|
},
|
|
@@ -771,10 +796,12 @@ export const decideMachine = setup({
|
|
|
771
796
|
stateId: 'commitCoderProposal',
|
|
772
797
|
sourceItem: 'DECIDE-3',
|
|
773
798
|
role: 'coder',
|
|
774
|
-
prompt:
|
|
799
|
+
prompt: SYNTHESIZE_COMMIT_PROMPT,
|
|
775
800
|
result: withNeedsBossReply({
|
|
776
|
-
committed:
|
|
801
|
+
committed: 'Coder synthesized both proposals and committed the resulting design as one new commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
|
|
777
802
|
}),
|
|
803
|
+
callerTopic: context.callerTopic,
|
|
804
|
+
reviewerProposal: context.reviewerProposal,
|
|
778
805
|
...bossReplyFields(context, 'commitCoderProposal'),
|
|
779
806
|
}),
|
|
780
807
|
onDone: [
|
|
@@ -881,7 +908,7 @@ export const decideMachine = setup({
|
|
|
881
908
|
stateId: 'reviewCommit',
|
|
882
909
|
sourceItem: 'DECIDE-4',
|
|
883
910
|
playbookId: 'review',
|
|
884
|
-
text: composeReviewInput(context.callerTopic, context.
|
|
911
|
+
text: composeReviewInput(context.callerTopic, context.latestCommit, context.coderOutput),
|
|
885
912
|
}),
|
|
886
913
|
onDone: [
|
|
887
914
|
{
|
|
@@ -43,7 +43,13 @@ type BossReplies = Partial<Record<ResumableStateId, string>>;
|
|
|
43
43
|
type PendingBossQuestionParams = Omit<PendingBossQuestion, 'questionId'>;
|
|
44
44
|
|
|
45
45
|
export interface ReviewSuccessOutput {
|
|
46
|
-
|
|
46
|
+
evaluatedRevision: string;
|
|
47
|
+
noUnsettledFindings: true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DecideSuccessOutput {
|
|
51
|
+
decideCommit: string;
|
|
52
|
+
evaluatedRevision: string;
|
|
47
53
|
noUnsettledFindings: true;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -57,7 +63,7 @@ export interface DecideFailureOutput {
|
|
|
57
63
|
};
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
export type DecideOutput =
|
|
66
|
+
export type DecideOutput = DecideSuccessOutput | DecideFailureOutput;
|
|
61
67
|
|
|
62
68
|
interface ChildFailure {
|
|
63
69
|
status: 'aborted' | 'error';
|
|
@@ -73,6 +79,7 @@ export interface DecideContext {
|
|
|
73
79
|
coderProposal?: string;
|
|
74
80
|
reviewerProposal?: string;
|
|
75
81
|
latestCommit?: string;
|
|
82
|
+
coderOutput?: string;
|
|
76
83
|
reviewResult?: ReviewSuccessOutput;
|
|
77
84
|
reviewFailure?: ChildFailure;
|
|
78
85
|
lastResult?: PlayerOutput;
|
|
@@ -105,6 +112,7 @@ export interface PlayerInput {
|
|
|
105
112
|
result: Record<string, string>;
|
|
106
113
|
role: Role;
|
|
107
114
|
callerTopic?: string;
|
|
115
|
+
reviewerProposal?: string;
|
|
108
116
|
pendingBossQuestion?: PendingBossQuestion;
|
|
109
117
|
bossReply?: string;
|
|
110
118
|
}
|
|
@@ -149,37 +157,48 @@ const INDEPENDENT_PROPOSAL_PROMPT = [
|
|
|
149
157
|
'Do not change any files.',
|
|
150
158
|
].join('\n');
|
|
151
159
|
|
|
152
|
-
const
|
|
153
|
-
|
|
160
|
+
const SYNTHESIZE_COMMIT_PROMPT = [
|
|
161
|
+
"Synthesize your independent proposal with Reviewer's proposal below.",
|
|
162
|
+
'Keep to the original topic below and follow what it asks.',
|
|
163
|
+
'Keep the best, essential parts of either proposal and reject any point that is unsound, unnecessary, or outside the topic.',
|
|
164
|
+
'Turn the resulting design into the necessary DRs and/or spec items.',
|
|
154
165
|
'Follow @specs/meta.md and update @specs/map.md when needed.',
|
|
155
|
-
|
|
156
|
-
'Do not change code or implement the proposal.',
|
|
166
|
+
'Do not change code or implement the design.',
|
|
157
167
|
'',
|
|
158
168
|
'Commit the result as one new commit, following @specs/packages/git.md.',
|
|
159
169
|
'Make the commit message explain concisely what changed and why.',
|
|
160
|
-
'
|
|
170
|
+
'Identify every new commit you make.',
|
|
171
|
+
'Coder is <coder-llm> and Reviewer is <reviewer-llm>.',
|
|
172
|
+
'',
|
|
173
|
+
'> Original topic: <caller-topic>',
|
|
174
|
+
"> Reviewer's independent proposal: <reviewer-proposal>",
|
|
161
175
|
].join('\n');
|
|
162
176
|
|
|
163
177
|
const REVIEW_INPUT_TEMPLATE = [
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'
|
|
167
|
-
'',
|
|
168
|
-
'Initial intent: <caller-topic>.',
|
|
169
|
-
"Coder's independent proposal: <coder-proposal>.",
|
|
178
|
+
'> Original intent: <caller-topic>',
|
|
179
|
+
'> Review scope: the `decide`-owned commit <decide-commit> and its resulting repository state.',
|
|
180
|
+
'> Coder output: <coder-output>',
|
|
170
181
|
].join('\n');
|
|
171
182
|
|
|
172
183
|
function composeReviewInput(
|
|
173
184
|
callerTopic: string | undefined,
|
|
174
|
-
|
|
185
|
+
decideCommit: string | undefined,
|
|
186
|
+
coderOutput: string | undefined,
|
|
175
187
|
): string {
|
|
176
188
|
const fields: Readonly<Record<string, string>> = {
|
|
177
189
|
'<caller-topic>': callerTopic ?? '',
|
|
178
|
-
'<
|
|
190
|
+
'<decide-commit>': decideCommit ?? '',
|
|
191
|
+
'<coder-output>': coderOutput ?? '',
|
|
179
192
|
};
|
|
180
193
|
return REVIEW_INPUT_TEMPLATE.replace(
|
|
181
|
-
/<caller-topic>|<coder-
|
|
182
|
-
(placeholder) =>
|
|
194
|
+
/<caller-topic>|<decide-commit>|<coder-output>/g,
|
|
195
|
+
(placeholder, offset: number, source: string) => {
|
|
196
|
+
const value = fields[placeholder];
|
|
197
|
+
const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
|
|
198
|
+
return source.slice(lineStart, offset).startsWith('> ')
|
|
199
|
+
? value.replaceAll('\n', '\n> ')
|
|
200
|
+
: value;
|
|
201
|
+
},
|
|
183
202
|
);
|
|
184
203
|
}
|
|
185
204
|
|
|
@@ -370,20 +389,24 @@ function isPlaybookState(value: unknown): boolean {
|
|
|
370
389
|
);
|
|
371
390
|
}
|
|
372
391
|
|
|
392
|
+
// DECIDE supplied `review` a scope rooted at the `decide`-owned commit, so a
|
|
393
|
+
// result applies to that scope only when it is the exact declared terminal
|
|
394
|
+
// contract: the evaluated repository revision plus the affirmative
|
|
395
|
+
// no-unsettled-findings fact. Any other shape does not establish those facts.
|
|
373
396
|
function reviewSuccessFrom(event: unknown): ReviewSuccessOutput | undefined {
|
|
374
397
|
const output = playbookOutputOf(event);
|
|
375
398
|
if (!isPlainRecord(output)) return undefined;
|
|
376
|
-
if (!isExactKeys(output, ['
|
|
399
|
+
if (!isExactKeys(output, ['evaluatedRevision', 'noUnsettledFindings'])) {
|
|
377
400
|
return undefined;
|
|
378
401
|
}
|
|
379
402
|
if (
|
|
380
|
-
output.
|
|
403
|
+
!isNonEmptyString(output.evaluatedRevision) ||
|
|
381
404
|
output.noUnsettledFindings !== true
|
|
382
405
|
) {
|
|
383
406
|
return undefined;
|
|
384
407
|
}
|
|
385
408
|
return {
|
|
386
|
-
|
|
409
|
+
evaluatedRevision: output.evaluatedRevision,
|
|
387
410
|
noUnsettledFindings: true,
|
|
388
411
|
};
|
|
389
412
|
}
|
|
@@ -537,6 +560,7 @@ export const decideMachine = setup({
|
|
|
537
560
|
coderProposal: undefined,
|
|
538
561
|
reviewerProposal: undefined,
|
|
539
562
|
latestCommit: undefined,
|
|
563
|
+
coderOutput: undefined,
|
|
540
564
|
reviewResult: undefined,
|
|
541
565
|
reviewFailure: undefined,
|
|
542
566
|
lastResult: undefined,
|
|
@@ -552,6 +576,7 @@ export const decideMachine = setup({
|
|
|
552
576
|
coderProposal: undefined,
|
|
553
577
|
reviewerProposal: undefined,
|
|
554
578
|
latestCommit: undefined,
|
|
579
|
+
coderOutput: undefined,
|
|
555
580
|
reviewResult: undefined,
|
|
556
581
|
reviewFailure: undefined,
|
|
557
582
|
lastResult: undefined,
|
|
@@ -583,6 +608,7 @@ export const decideMachine = setup({
|
|
|
583
608
|
}),
|
|
584
609
|
rememberCommit: assign({
|
|
585
610
|
latestCommit: ({ event }) => outputOf(event).latestCommit,
|
|
611
|
+
coderOutput: ({ event }) => outputOf(event).coderOutput,
|
|
586
612
|
lastResult: ({ event }) => outputOf(event),
|
|
587
613
|
lastError: undefined,
|
|
588
614
|
}),
|
|
@@ -603,7 +629,7 @@ export const decideMachine = setup({
|
|
|
603
629
|
error: {
|
|
604
630
|
name: 'ReviewProtocolError',
|
|
605
631
|
message:
|
|
606
|
-
'REVIEW returned without
|
|
632
|
+
'REVIEW returned without the evaluated repository revision and a no-unsettled-findings result for the supplied scope.',
|
|
607
633
|
},
|
|
608
634
|
},
|
|
609
635
|
lastError: undefined,
|
|
@@ -678,7 +704,16 @@ export const decideMachine = setup({
|
|
|
678
704
|
initial: 'ready',
|
|
679
705
|
context: (): DecideContext => ({}),
|
|
680
706
|
output: ({ context }): DecideOutput => {
|
|
681
|
-
if (context.reviewResult)
|
|
707
|
+
if (context.reviewResult) {
|
|
708
|
+
if (!context.latestCommit) {
|
|
709
|
+
throw new Error('DECIDE completed without its decide-owned commit');
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
decideCommit: context.latestCommit,
|
|
713
|
+
evaluatedRevision: context.reviewResult.evaluatedRevision,
|
|
714
|
+
noUnsettledFindings: true,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
682
717
|
if (!context.latestCommit || !context.reviewFailure) {
|
|
683
718
|
throw new Error('DECIDE reached a final state without a result');
|
|
684
719
|
}
|
|
@@ -747,7 +782,7 @@ export const decideMachine = setup({
|
|
|
747
782
|
prompt: INDEPENDENT_PROPOSAL_PROMPT,
|
|
748
783
|
result: withNeedsBossReply({
|
|
749
784
|
proposed:
|
|
750
|
-
'Coder
|
|
785
|
+
'Coder affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `coderProposal: <verbatim final text>`.',
|
|
751
786
|
}),
|
|
752
787
|
callerTopic: context.callerTopic,
|
|
753
788
|
...bossReplyFields(context, 'askCoderProposal'),
|
|
@@ -887,7 +922,7 @@ export const decideMachine = setup({
|
|
|
887
922
|
prompt: INDEPENDENT_PROPOSAL_PROMPT,
|
|
888
923
|
result: withNeedsBossReply({
|
|
889
924
|
proposed:
|
|
890
|
-
'Reviewer
|
|
925
|
+
'Reviewer affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `reviewerProposal: <verbatim final text>`.',
|
|
891
926
|
}),
|
|
892
927
|
callerTopic: context.callerTopic,
|
|
893
928
|
...bossReplyFields(context, 'askReviewerProposal'),
|
|
@@ -1014,11 +1049,12 @@ export const decideMachine = setup({
|
|
|
1014
1049
|
commitCoderProposal: {
|
|
1015
1050
|
id: 'commitCoderProposal',
|
|
1016
1051
|
tags: 'playbook.busy',
|
|
1017
|
-
description: 'Coder
|
|
1052
|
+
description: 'Coder synthesizes both proposals and commits the design.',
|
|
1018
1053
|
meta: {
|
|
1019
1054
|
playbook: {
|
|
1020
1055
|
stateId: 'commitCoderProposal',
|
|
1021
|
-
description:
|
|
1056
|
+
description:
|
|
1057
|
+
'Coder synthesizes both proposals and commits the design.',
|
|
1022
1058
|
role: 'coder',
|
|
1023
1059
|
},
|
|
1024
1060
|
},
|
|
@@ -1028,11 +1064,13 @@ export const decideMachine = setup({
|
|
|
1028
1064
|
stateId: 'commitCoderProposal',
|
|
1029
1065
|
sourceItem: 'DECIDE-3',
|
|
1030
1066
|
role: 'coder',
|
|
1031
|
-
prompt:
|
|
1067
|
+
prompt: SYNTHESIZE_COMMIT_PROMPT,
|
|
1032
1068
|
result: withNeedsBossReply({
|
|
1033
1069
|
committed:
|
|
1034
|
-
|
|
1070
|
+
'Coder synthesized both proposals and committed the resulting design as one new commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
|
|
1035
1071
|
}),
|
|
1072
|
+
callerTopic: context.callerTopic,
|
|
1073
|
+
reviewerProposal: context.reviewerProposal,
|
|
1036
1074
|
...bossReplyFields(context, 'commitCoderProposal'),
|
|
1037
1075
|
}),
|
|
1038
1076
|
onDone: [
|
|
@@ -1141,7 +1179,8 @@ export const decideMachine = setup({
|
|
|
1141
1179
|
playbookId: 'review',
|
|
1142
1180
|
text: composeReviewInput(
|
|
1143
1181
|
context.callerTopic,
|
|
1144
|
-
context.
|
|
1182
|
+
context.latestCommit,
|
|
1183
|
+
context.coderOutput,
|
|
1145
1184
|
),
|
|
1146
1185
|
}),
|
|
1147
1186
|
onDone: [
|