@sublang/playbook 15.1.0 → 17.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/README.md +2 -0
- package/docs/cli.md +36 -18
- package/docs/configuration.md +7 -5
- package/docs/embedding.md +28 -7
- package/package.json +4 -4
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
- package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
- package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
- package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
- package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
- package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
- package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
- package/reference/sdlc/captain.md +19 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
- package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
- package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
- package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
- package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
- package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
- package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
- package/reference/sdlc/code.playbook/bin/run.js +128 -85
- package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
- package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
- package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
- package/reference/sdlc/code.playbook/code.fsm.js +609 -649
- package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
- package/reference/sdlc/code.playbook/code.gears.md +39 -29
- package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
- package/reference/sdlc/code.playbook/code.playbook.js +166 -147
- package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
- package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.ts +6 -2
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
- package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
- package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
- package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
- package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
- package/reference/sdlc/code.playbook/session-store.js +1 -0
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
- package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
- package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
- package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
- package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +116 -134
- package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
- package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
- package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
- package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +95 -88
- package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
- package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
- package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
- package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
- package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
- package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
- package/reference/sdlc/review.md +3 -3
- package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
- package/reference/sdlc/review.playbook/review.fsm.js +479 -550
- package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
- package/reference/sdlc/review.playbook/review.gears.md +32 -29
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
- package/reference/sdlc/review.playbook/review.playbook.js +180 -155
- package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
- package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
- package/reference/sdlc/review.playbook/review.registry.js +3 -3
- package/reference/sdlc/review.playbook/review.registry.ts +9 -5
- package/slc/gears2fsm.md +77 -8
- package/slc/link.md +127 -60
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +29 -28
- package/slc/prefix.md +27 -24
- package/slc/slc.pin-inputs.json +11 -2
- package/slc/text2gears.md +71 -11
- package/src/runtime.d.ts +46 -2
- package/src/runtime.ts +42 -2
- package/src/xstate-playbook-runtime.d.ts +63 -23
- package/src/xstate-playbook-runtime.js +1637 -475
- package/src/xstate-playbook-runtime.ts +2105 -532
- package/src/xstate-runtime.d.ts +3 -4
- package/src/xstate-runtime.js +52 -0
- package/src/xstate-runtime.ts +59 -4
|
@@ -1,20 +1,56 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
//
|
|
4
|
+
// FSM object artifact compiled from ./decide.gears.md by gears2fsm.
|
|
5
|
+
// It declares only the machine, its actor contracts, and typed inputs; the
|
|
6
|
+
// linked runtime supplies the `player` and `playbook` actor implementations.
|
|
6
7
|
import { assign, fromPromise, setup } from 'xstate';
|
|
7
|
-
|
|
8
|
+
import { validatePlaybookCallResult } from '@sublang/playbook/xstate-runtime';
|
|
9
|
+
/**
|
|
10
|
+
* One role-id array per parallel group, in first-item source order; the inner
|
|
11
|
+
* array follows the `independent-proposals` item order (DECIDE-1, DECIDE-2).
|
|
12
|
+
*/
|
|
13
|
+
export const concurrentRoleSets = [
|
|
14
|
+
['coder', 'reviewer'],
|
|
15
|
+
];
|
|
16
|
+
/** The literal nested-call target of DECIDE-4. */
|
|
17
|
+
const REVIEW_PLAYBOOK_ID = 'review';
|
|
18
|
+
const RESUMABLE_STATES = {
|
|
19
|
+
askCoderProposal: { sourceItem: 'DECIDE-1', roleId: 'coder' },
|
|
20
|
+
askReviewerProposal: { sourceItem: 'DECIDE-2', roleId: 'reviewer' },
|
|
21
|
+
synthesizeCommit: { sourceItem: 'DECIDE-3', roleId: 'coder' },
|
|
22
|
+
};
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Prompts (each item's blockquote, verbatim) and local result contracts
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
8
26
|
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
|
-
|
|
10
|
-
|
|
27
|
+
/** DECIDE-1 blockquote; `<caller-topic>` is carried as `callerTopic`. */
|
|
28
|
+
const CODER_PROPOSAL_PROMPT = [
|
|
29
|
+
'Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.',
|
|
30
|
+
'Propose your design.',
|
|
31
|
+
'Keep your proposal coherent, focused, and concise.',
|
|
32
|
+
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
33
|
+
'Do not change any files.',
|
|
11
34
|
'',
|
|
35
|
+
'> Original topic: <caller-topic>',
|
|
36
|
+
].join('\n');
|
|
37
|
+
/** DECIDE-2 blockquote; `<caller-topic>` is carried as `callerTopic`. */
|
|
38
|
+
const REVIEWER_PROPOSAL_PROMPT = [
|
|
12
39
|
'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
40
|
'Propose your design.',
|
|
14
41
|
'Keep your proposal coherent, focused, and concise.',
|
|
15
42
|
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
16
43
|
'Do not change any files.',
|
|
44
|
+
'',
|
|
45
|
+
'> Original topic: <caller-topic>',
|
|
17
46
|
].join('\n');
|
|
47
|
+
/**
|
|
48
|
+
* DECIDE-3 blockquote. `<caller-topic>` is carried as `callerTopic` and
|
|
49
|
+
* `<reviewer-proposal>` as `reviewerProposal`. `<coder-llm>` and
|
|
50
|
+
* `<reviewer-llm>` are local-role prompt identities of Coder and Reviewer: the
|
|
51
|
+
* linker resolves them through the invocation-scoped `promptIdentity(roleId)`
|
|
52
|
+
* lookup, so they stay literal here and have no machine or actor-input field.
|
|
53
|
+
*/
|
|
18
54
|
const SYNTHESIZE_COMMIT_PROMPT = [
|
|
19
55
|
"Synthesize your independent proposal with Reviewer's proposal below.",
|
|
20
56
|
'Keep to the original topic below and follow what it asks.',
|
|
@@ -31,270 +67,460 @@ const SYNTHESIZE_COMMIT_PROMPT = [
|
|
|
31
67
|
'> Original topic: <caller-topic>',
|
|
32
68
|
"> Reviewer's independent proposal: <reviewer-proposal>",
|
|
33
69
|
].join('\n');
|
|
34
|
-
const
|
|
70
|
+
const CODER_PROPOSAL_RESULT = {
|
|
71
|
+
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>`.',
|
|
72
|
+
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
73
|
+
};
|
|
74
|
+
const REVIEWER_PROPOSAL_RESULT = {
|
|
75
|
+
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>`.',
|
|
76
|
+
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
77
|
+
};
|
|
78
|
+
const SYNTHESIZE_COMMIT_RESULT = {
|
|
79
|
+
committed: 'Coder synthesized the proposals into the necessary DRs and/or spec items and committed the result as one new commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
|
|
80
|
+
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
81
|
+
};
|
|
82
|
+
/** DECIDE-4 blockquote: the `review` call-text template, verbatim. */
|
|
83
|
+
const REVIEW_COMMIT_TEMPLATE = [
|
|
35
84
|
'> Original intent: <caller-topic>',
|
|
36
85
|
'> Review scope: the `decide`-owned commit <decide-commit> and its resulting repository state.',
|
|
86
|
+
"> Coder's independent proposal: <coder-proposal>",
|
|
37
87
|
'> Coder output: <coder-output>',
|
|
38
88
|
].join('\n');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// State descriptions and public metadata
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
const STATE_DESCRIPTIONS = {
|
|
93
|
+
ready: 'Waiting for the caller to give a topic to decide.',
|
|
94
|
+
independentProposals: 'Coder and Reviewer independently propose designs for the topic in parallel.',
|
|
95
|
+
coderProposalRegion: "Coder's independent proposal branch.",
|
|
96
|
+
askCoderProposal: 'Coder is independently proposing a design for the topic.',
|
|
97
|
+
awaitCoderProposalReply: "Waiting for Boss to answer Coder's question about its independent proposal.",
|
|
98
|
+
coderProposalStaged: "Coder's independent proposal is complete and staged for the join.",
|
|
99
|
+
reviewerProposalRegion: "Reviewer's independent proposal branch.",
|
|
100
|
+
askReviewerProposal: 'Reviewer is independently proposing a design for the topic.',
|
|
101
|
+
awaitReviewerProposalReply: "Waiting for Boss to answer Reviewer's question about its independent proposal.",
|
|
102
|
+
reviewerProposalStaged: "Reviewer's independent proposal is complete and staged for the join.",
|
|
103
|
+
synthesizeCommit: 'Coder synthesizes both proposals into the necessary DRs and/or spec items and commits the result as one new commit.',
|
|
104
|
+
awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
|
|
105
|
+
reviewCommit: 'The review playbook examines the decide-owned commit.',
|
|
106
|
+
failed: 'DECIDE parked after a control-plane failure and waits for Boss to restart or resume it.',
|
|
107
|
+
reportedReviewFailure: "DECIDE reported review's abort, failure, or unestablished result to its caller with the last decide-owned commit.",
|
|
108
|
+
done: 'DECIDE completed: review established no unsettled findings for the decide-owned commit at the reported evaluated revision.',
|
|
109
|
+
};
|
|
110
|
+
function playbookMeta(stateId, role) {
|
|
111
|
+
return {
|
|
112
|
+
playbook: {
|
|
113
|
+
stateId,
|
|
114
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
115
|
+
...(role === undefined ? {} : { role }),
|
|
116
|
+
},
|
|
44
117
|
};
|
|
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
|
-
});
|
|
52
118
|
}
|
|
53
|
-
|
|
54
|
-
const playbookOutputOf = (event) => event.output;
|
|
55
|
-
const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
56
|
-
const coderProposed = ({ event }) => {
|
|
57
|
-
const output = outputOf(event);
|
|
58
|
-
return output.guard === 'proposed' && isNonEmptyString(output.coderProposal);
|
|
59
|
-
};
|
|
60
|
-
const reviewerProposed = ({ event }) => {
|
|
61
|
-
const output = outputOf(event);
|
|
62
|
-
return (output.guard === 'proposed' && isNonEmptyString(output.reviewerProposal));
|
|
63
|
-
};
|
|
64
|
-
function commitOutput(event) {
|
|
65
|
-
const output = outputOf(event);
|
|
66
|
-
if (output.guard !== 'committed' ||
|
|
67
|
-
!isNonEmptyString(output.coderOutput) ||
|
|
68
|
-
!isNonEmptyString(output.latestCommit)) {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
119
|
+
function terminalMeta(stateId, terminal) {
|
|
71
120
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
121
|
+
playbook: {
|
|
122
|
+
stateId,
|
|
123
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
124
|
+
terminal,
|
|
125
|
+
},
|
|
74
126
|
};
|
|
75
127
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
85
|
-
const pendingQuestionIds = (context) => Object.keys(context.pendingBossQuestions ?? {});
|
|
86
|
-
const resolvedQuestionId = (context, event) => {
|
|
87
|
-
if (event.type !== 'BOSS_REPLY')
|
|
88
|
-
return undefined;
|
|
89
|
-
if (event.questionId)
|
|
90
|
-
return event.questionId;
|
|
91
|
-
const ids = pendingQuestionIds(context);
|
|
92
|
-
return ids.length === 1 ? ids[0] : undefined;
|
|
93
|
-
};
|
|
94
|
-
const bossReplyTargets = (stateId, requireAnswer) => ({ context, event }) => event.type === 'BOSS_REPLY' &&
|
|
95
|
-
resolvedQuestionId(context, event) === stateId &&
|
|
96
|
-
(requireAnswer
|
|
97
|
-
? event.answer.trim().length > 0
|
|
98
|
-
: event.answer.trim().length === 0);
|
|
99
|
-
const bossReplyFields = (context, stateId) => ({
|
|
100
|
-
...(context.pendingBossQuestions?.[stateId]
|
|
101
|
-
? { pendingBossQuestion: context.pendingBossQuestions[stateId] }
|
|
102
|
-
: {}),
|
|
103
|
-
...(context.bossReplies?.[stateId]
|
|
104
|
-
? { bossReply: context.bossReplies[stateId] }
|
|
105
|
-
: {}),
|
|
106
|
-
});
|
|
107
|
-
const withoutKey = (record, stateId) => {
|
|
108
|
-
if (!record || record[stateId] === undefined)
|
|
109
|
-
return record;
|
|
110
|
-
const next = { ...record };
|
|
111
|
-
delete next[stateId];
|
|
112
|
-
return Object.keys(next).length > 0 ? next : undefined;
|
|
113
|
-
};
|
|
114
|
-
const withNeedsBossReply = (result) => ({
|
|
115
|
-
...result,
|
|
116
|
-
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
117
|
-
});
|
|
118
|
-
const isPlainRecord = (value) => typeof value === 'object' &&
|
|
119
|
-
value !== null &&
|
|
120
|
-
!Array.isArray(value) &&
|
|
121
|
-
(Object.getPrototypeOf(value) === Object.prototype ||
|
|
122
|
-
Object.getPrototypeOf(value) === null);
|
|
123
|
-
const isExactKeys = (value, keys) => {
|
|
124
|
-
const ownKeys = Reflect.ownKeys(value);
|
|
125
|
-
return (ownKeys.length === keys.length &&
|
|
126
|
-
ownKeys.every((key) => typeof key === 'string' && keys.includes(key)));
|
|
127
|
-
};
|
|
128
|
-
function isStateValue(value, ancestors = new Set()) {
|
|
129
|
-
if (typeof value === 'string')
|
|
130
|
-
return true;
|
|
131
|
-
if (!isPlainRecord(value) || ancestors.has(value))
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Structural narrowing helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
function isObject(value) {
|
|
132
|
+
return typeof value === 'object' && value !== null;
|
|
133
|
+
}
|
|
134
|
+
function isPlainRecord(value) {
|
|
135
|
+
if (!isObject(value) || Array.isArray(value))
|
|
132
136
|
return false;
|
|
133
|
-
const
|
|
134
|
-
return
|
|
135
|
-
Object.prototype.propertyIsEnumerable.call(value, key) &&
|
|
136
|
-
isStateValue(value[key], next));
|
|
137
|
+
const prototype = Object.getPrototypeOf(value);
|
|
138
|
+
return prototype === Object.prototype || prototype === null;
|
|
137
139
|
}
|
|
138
|
-
function
|
|
139
|
-
return
|
|
140
|
-
Reflect.ownKeys(value).length === value.length + 1 &&
|
|
141
|
-
Reflect.ownKeys(value).every((key) => {
|
|
142
|
-
if (key === 'length')
|
|
143
|
-
return true;
|
|
144
|
-
return (typeof key === 'string' &&
|
|
145
|
-
/^(0|[1-9][0-9]*)$/.test(key) &&
|
|
146
|
-
Number(key) < value.length);
|
|
147
|
-
}) &&
|
|
148
|
-
value.every((entry) => isNonEmptyString(entry)) &&
|
|
149
|
-
new Set(value).size === value.length);
|
|
140
|
+
function isNonEmptyString(value) {
|
|
141
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
150
142
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'value',
|
|
156
|
-
'activeStateIds',
|
|
157
|
-
'tags',
|
|
158
|
-
'status',
|
|
159
|
-
'quiescent',
|
|
160
|
-
'stateId',
|
|
161
|
-
]);
|
|
162
|
-
if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
|
|
163
|
-
!Object.prototype.hasOwnProperty.call(value, 'value') ||
|
|
164
|
-
!Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
|
|
165
|
-
!Object.prototype.hasOwnProperty.call(value, 'tags') ||
|
|
166
|
-
!Object.prototype.hasOwnProperty.call(value, 'status') ||
|
|
167
|
-
!Object.prototype.hasOwnProperty.call(value, 'quiescent')) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
return (isStateValue(value.value) &&
|
|
171
|
-
isStringArray(value.activeStateIds) &&
|
|
172
|
-
isStringArray(value.tags) &&
|
|
173
|
-
(value.status === 'active' ||
|
|
174
|
-
value.status === 'done' ||
|
|
175
|
-
value.status === 'error' ||
|
|
176
|
-
value.status === 'stopped') &&
|
|
177
|
-
typeof value.quiescent === 'boolean' &&
|
|
178
|
-
(!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
|
|
179
|
-
(isNonEmptyString(value.stateId) &&
|
|
180
|
-
value.activeStateIds.length === 1 &&
|
|
181
|
-
value.activeStateIds[0] === value.stateId)));
|
|
143
|
+
function isResumableStateId(value) {
|
|
144
|
+
return (value === 'askCoderProposal' ||
|
|
145
|
+
value === 'askReviewerProposal' ||
|
|
146
|
+
value === 'synthesizeCommit');
|
|
182
147
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// no-unsettled-findings fact. Any other shape does not establish those facts.
|
|
187
|
-
function reviewSuccessFrom(event) {
|
|
188
|
-
const output = playbookOutputOf(event);
|
|
189
|
-
if (!isPlainRecord(output))
|
|
190
|
-
return undefined;
|
|
191
|
-
if (!isExactKeys(output, ['evaluatedRevision', 'noUnsettledFindings'])) {
|
|
148
|
+
/** The actor's `output`, narrowed structurally from an unknown done event. */
|
|
149
|
+
function actorOutput(event) {
|
|
150
|
+
if (!isObject(event))
|
|
192
151
|
return undefined;
|
|
152
|
+
const output = event.output;
|
|
153
|
+
return isPlainRecord(output) ? output : undefined;
|
|
154
|
+
}
|
|
155
|
+
function outputString(event, field) {
|
|
156
|
+
const value = actorOutput(event)?.[field];
|
|
157
|
+
return isNonEmptyString(value) ? value : undefined;
|
|
158
|
+
}
|
|
159
|
+
function outputGuardIs(event, guard) {
|
|
160
|
+
return actorOutput(event)?.guard === guard;
|
|
161
|
+
}
|
|
162
|
+
/** The actor's `error`, narrowed structurally from an unknown error event. */
|
|
163
|
+
function actorError(event) {
|
|
164
|
+
return isObject(event) ? event.error : undefined;
|
|
165
|
+
}
|
|
166
|
+
/** Normalizes any thrown or malformed value to a JSON-safe error record. */
|
|
167
|
+
function errorRecord(value) {
|
|
168
|
+
if (value instanceof Error) {
|
|
169
|
+
return {
|
|
170
|
+
name: isNonEmptyString(value.name) ? value.name : 'Error',
|
|
171
|
+
message: typeof value.message === 'string' ? value.message : '',
|
|
172
|
+
...(typeof value.stack === 'string' ? { stack: value.stack } : {}),
|
|
173
|
+
};
|
|
193
174
|
}
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
175
|
+
if (isObject(value) && typeof value.message === 'string') {
|
|
176
|
+
return {
|
|
177
|
+
name: isNonEmptyString(value.name) ? value.name : 'Error',
|
|
178
|
+
message: value.message,
|
|
179
|
+
};
|
|
197
180
|
}
|
|
198
181
|
return {
|
|
199
|
-
|
|
200
|
-
|
|
182
|
+
name: 'Error',
|
|
183
|
+
message: typeof value === 'string'
|
|
184
|
+
? value
|
|
185
|
+
: 'The actor failed with a non-Error value.',
|
|
201
186
|
};
|
|
202
187
|
}
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
function withoutKey(record, stateId) {
|
|
189
|
+
const next = {};
|
|
190
|
+
for (const key of Object.keys(record)) {
|
|
191
|
+
if (!isResumableStateId(key) || key === stateId)
|
|
192
|
+
continue;
|
|
193
|
+
const value = record[key];
|
|
194
|
+
if (value !== undefined)
|
|
195
|
+
next[key] = value;
|
|
196
|
+
}
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Strict JSON validation: null, booleans, finite numbers, strings, exact
|
|
201
|
+
* arrays, and plain records of enumerable own data properties. Cycle
|
|
202
|
+
* detection tracks only the active recursion path.
|
|
203
|
+
*/
|
|
204
|
+
function isJsonValue(value, path = new Set()) {
|
|
205
|
+
if (value === null)
|
|
206
|
+
return true;
|
|
207
|
+
switch (typeof value) {
|
|
208
|
+
case 'boolean':
|
|
209
|
+
case 'string':
|
|
210
|
+
return true;
|
|
211
|
+
case 'number':
|
|
212
|
+
return Number.isFinite(value);
|
|
213
|
+
case 'object':
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const container = value;
|
|
219
|
+
if (path.has(container))
|
|
220
|
+
return false;
|
|
221
|
+
path.add(container);
|
|
222
|
+
try {
|
|
223
|
+
if (Array.isArray(container)) {
|
|
224
|
+
if (Object.getPrototypeOf(container) !== Array.prototype)
|
|
225
|
+
return false;
|
|
226
|
+
const keys = Reflect.ownKeys(container);
|
|
227
|
+
if (keys.length !== container.length + 1)
|
|
228
|
+
return false;
|
|
229
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(container, 'length');
|
|
230
|
+
if (lengthDescriptor === undefined ||
|
|
231
|
+
lengthDescriptor.configurable !== false ||
|
|
232
|
+
lengthDescriptor.enumerable !== false ||
|
|
233
|
+
lengthDescriptor.value !== container.length) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
for (let index = 0; index < container.length; index += 1) {
|
|
237
|
+
const descriptor = Object.getOwnPropertyDescriptor(container, String(index));
|
|
238
|
+
if (descriptor === undefined ||
|
|
239
|
+
!descriptor.enumerable ||
|
|
240
|
+
!('value' in descriptor) ||
|
|
241
|
+
!isJsonValue(descriptor.value, path)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
const prototype = Object.getPrototypeOf(container);
|
|
248
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
249
|
+
return false;
|
|
250
|
+
for (const key of Reflect.ownKeys(container)) {
|
|
251
|
+
if (typeof key !== 'string')
|
|
252
|
+
return false;
|
|
253
|
+
const descriptor = Object.getOwnPropertyDescriptor(container, key);
|
|
254
|
+
if (descriptor === undefined ||
|
|
255
|
+
!descriptor.enumerable ||
|
|
256
|
+
!('value' in descriptor) ||
|
|
257
|
+
!isJsonValue(descriptor.value, path)) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
path.delete(container);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Actor-output narrowing (declared per-state result contracts)
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
function isCoderProposed(event) {
|
|
271
|
+
return (outputGuardIs(event, 'proposed') &&
|
|
272
|
+
outputString(event, 'coderProposal') !== undefined);
|
|
273
|
+
}
|
|
274
|
+
function isReviewerProposed(event) {
|
|
275
|
+
return (outputGuardIs(event, 'proposed') &&
|
|
276
|
+
outputString(event, 'reviewerProposal') !== undefined);
|
|
277
|
+
}
|
|
278
|
+
function isCommitted(event) {
|
|
279
|
+
return (outputGuardIs(event, 'committed') &&
|
|
280
|
+
outputString(event, 'coderOutput') !== undefined &&
|
|
281
|
+
outputString(event, 'latestCommit') !== undefined);
|
|
282
|
+
}
|
|
283
|
+
function isNeedsBossReply(event) {
|
|
284
|
+
return (outputGuardIs(event, 'needsBossReply') &&
|
|
285
|
+
outputString(event, 'question') !== undefined);
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Nested `review` results
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* Recognizes an authored rejected child result: a validated public result
|
|
292
|
+
* whose status is `aborted` or `error`, or `ok` at the child's own authored
|
|
293
|
+
* failure terminal. Anything else is a control-plane error.
|
|
294
|
+
*/
|
|
295
|
+
export function authoredChildResult(error, expectedPlaybookId) {
|
|
205
296
|
if (!(error instanceof Error))
|
|
206
297
|
return undefined;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'playbookId',
|
|
213
|
-
'childSessionId',
|
|
214
|
-
'state',
|
|
215
|
-
'error',
|
|
216
|
-
]);
|
|
217
|
-
if (Reflect.ownKeys(result).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
|
|
218
|
-
(result.status !== 'aborted' && result.status !== 'error') ||
|
|
219
|
-
result.playbookId !== 'review' ||
|
|
220
|
-
Object.prototype.hasOwnProperty.call(result, 'output')) {
|
|
221
|
-
return undefined;
|
|
298
|
+
try {
|
|
299
|
+
const result = validatePlaybookCallResult(error.result, expectedPlaybookId);
|
|
300
|
+
return result.status !== 'ok' || result.terminal?.kind === 'failure'
|
|
301
|
+
? result
|
|
302
|
+
: undefined;
|
|
222
303
|
}
|
|
223
|
-
|
|
224
|
-
!isNonEmptyString(result.childSessionId)) {
|
|
304
|
+
catch {
|
|
225
305
|
return undefined;
|
|
226
306
|
}
|
|
227
|
-
|
|
228
|
-
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* DECIDE-4's acceptance predicate on the builtin `review` public interface:
|
|
310
|
+
* the result gives the exact evaluated repository revision and affirmatively
|
|
311
|
+
* establishes that no unsettled findings remain. The shared bridge correlates
|
|
312
|
+
* the result with the supplied review scope.
|
|
313
|
+
*/
|
|
314
|
+
function reviewApprovalFrom(event) {
|
|
315
|
+
const output = actorOutput(event);
|
|
316
|
+
if (output === undefined)
|
|
317
|
+
return undefined;
|
|
318
|
+
if (output.noUnsettledFindings !== true ||
|
|
319
|
+
!isNonEmptyString(output.evaluatedRevision)) {
|
|
229
320
|
return undefined;
|
|
230
321
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
if (!isNonEmptyString(result.error.name) ||
|
|
243
|
-
typeof result.error.message !== 'string' ||
|
|
244
|
-
(Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
|
|
245
|
-
typeof result.error.stack !== 'string')) {
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
normalizedError = {
|
|
249
|
-
name: result.error.name,
|
|
250
|
-
message: result.error.message,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
else if (result.status === 'error') {
|
|
322
|
+
return {
|
|
323
|
+
noUnsettledFindings: true,
|
|
324
|
+
evaluatedRevision: output.evaluatedRevision,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/** Reported status, compact error, and evidence of an authored review failure. */
|
|
328
|
+
function authoredReviewFailureOf(event) {
|
|
329
|
+
const result = authoredChildResult(actorError(event), REVIEW_PLAYBOOK_ID);
|
|
330
|
+
if (result === undefined)
|
|
254
331
|
return undefined;
|
|
332
|
+
if (result.status === 'ok') {
|
|
333
|
+
// `ok` at review's own authored failure terminal: a review failure whose
|
|
334
|
+
// own output is relayed as evidence.
|
|
335
|
+
const description = result.terminal?.description;
|
|
336
|
+
return {
|
|
337
|
+
status: 'error',
|
|
338
|
+
error: {
|
|
339
|
+
name: 'ReviewFailed',
|
|
340
|
+
message: isNonEmptyString(description)
|
|
341
|
+
? `review ended at its authored failure terminal: ${description}`
|
|
342
|
+
: 'review ended at its authored failure terminal.',
|
|
343
|
+
},
|
|
344
|
+
evidence: {
|
|
345
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
346
|
+
status: 'ok',
|
|
347
|
+
...(result.output === undefined ? {} : { output: result.output }),
|
|
348
|
+
},
|
|
349
|
+
};
|
|
255
350
|
}
|
|
351
|
+
const error = result.error !== undefined
|
|
352
|
+
? { name: result.error.name, message: result.error.message }
|
|
353
|
+
: { name: 'AbortError', message: 'The review call was aborted.' };
|
|
256
354
|
return {
|
|
257
355
|
status: result.status,
|
|
258
|
-
|
|
259
|
-
|
|
356
|
+
error,
|
|
357
|
+
evidence: { playbookId: REVIEW_PLAYBOOK_ID, status: result.status, error },
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const REVIEW_NOT_ESTABLISHED = {
|
|
361
|
+
name: 'ReviewNotEstablished',
|
|
362
|
+
message: 'review returned a terminal result that does not establish the exact evaluated repository revision with no unsettled findings.',
|
|
363
|
+
};
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Nested call text composition (DECIDE-4 quoted relays)
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
const REVIEW_PLACEHOLDER = /<caller-topic>|<decide-commit>|<coder-proposal>|<coder-output>/g;
|
|
368
|
+
/**
|
|
369
|
+
* Composes the `review` call text from the verbatim template, one line at a
|
|
370
|
+
* time: a quoted relay line whose value is empty contributes no line, and a
|
|
371
|
+
* multi-line value is quoted line by line. The composer adds no empty quoted
|
|
372
|
+
* line of its own.
|
|
373
|
+
*/
|
|
374
|
+
function reviewCallText(context) {
|
|
375
|
+
const values = {
|
|
376
|
+
'<caller-topic>': context.callerTopic,
|
|
377
|
+
'<decide-commit>': context.decideCommit,
|
|
378
|
+
'<coder-proposal>': context.coderProposal,
|
|
379
|
+
'<coder-output>': context.coderOutput,
|
|
380
|
+
};
|
|
381
|
+
const lines = [];
|
|
382
|
+
for (const line of REVIEW_COMMIT_TEMPLATE.split('\n')) {
|
|
383
|
+
const tokens = line.match(REVIEW_PLACEHOLDER) ?? [];
|
|
384
|
+
if (tokens.some((token) => values[token] === ''))
|
|
385
|
+
continue;
|
|
386
|
+
lines.push(line.replace(REVIEW_PLACEHOLDER, (token) => (values[token] ?? token).split('\n').join('\n> ')));
|
|
387
|
+
}
|
|
388
|
+
return lines.join('\n');
|
|
389
|
+
}
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Player input mapping (pure reads of typed, entry-validated context)
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
function bossReplyFields(context, stateId) {
|
|
394
|
+
const pendingBossQuestion = context.pendingBossQuestions[stateId];
|
|
395
|
+
const bossReply = context.bossReplies[stateId];
|
|
396
|
+
return {
|
|
397
|
+
...(pendingBossQuestion === undefined ? {} : { pendingBossQuestion }),
|
|
398
|
+
...(bossReply === undefined ? {} : { bossReply }),
|
|
260
399
|
};
|
|
261
400
|
}
|
|
262
|
-
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Context snapshots
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
const INITIAL_CONTEXT = {
|
|
405
|
+
callerTopic: '',
|
|
406
|
+
stagedCoderProposal: '',
|
|
407
|
+
stagedReviewerProposal: '',
|
|
408
|
+
coderProposal: '',
|
|
409
|
+
reviewerProposal: '',
|
|
410
|
+
coderOutput: '',
|
|
411
|
+
decideCommit: '',
|
|
412
|
+
evaluatedRevision: '',
|
|
413
|
+
reviewStatus: null,
|
|
414
|
+
reviewError: null,
|
|
415
|
+
reviewEvidence: null,
|
|
416
|
+
lastError: null,
|
|
417
|
+
pendingBossQuestions: {},
|
|
418
|
+
bossReplies: {},
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* A fresh run for a topic: clears every staged and promoted proposal, the
|
|
422
|
+
* commit, review evidence, errors, and every pending Boss question and reply.
|
|
423
|
+
*/
|
|
424
|
+
function freshRun(callerTopic) {
|
|
425
|
+
return { ...INITIAL_CONTEXT, callerTopic };
|
|
426
|
+
}
|
|
427
|
+
/** Context reset shared by every exit into `failed`. */
|
|
428
|
+
const FAILURE_RESET = {
|
|
429
|
+
pendingBossQuestions: {},
|
|
430
|
+
bossReplies: {},
|
|
431
|
+
stagedCoderProposal: '',
|
|
432
|
+
stagedReviewerProposal: '',
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* A synthesis restart keeps the topic and the promoted proposals, and clears
|
|
436
|
+
* the prior commit, review evidence, error, and every pending question.
|
|
437
|
+
*/
|
|
438
|
+
const SYNTHESIS_RESTART = {
|
|
439
|
+
coderOutput: '',
|
|
440
|
+
decideCommit: '',
|
|
441
|
+
evaluatedRevision: '',
|
|
442
|
+
reviewStatus: null,
|
|
443
|
+
reviewError: null,
|
|
444
|
+
reviewEvidence: null,
|
|
445
|
+
lastError: null,
|
|
446
|
+
pendingBossQuestions: {},
|
|
447
|
+
bossReplies: {},
|
|
448
|
+
};
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// Transition helpers
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
/**
|
|
453
|
+
* Accepted-outcome marker (artifact schema 3). `target` names the public
|
|
454
|
+
* state the next snapshot shows after the transition's macrostep.
|
|
455
|
+
*/
|
|
456
|
+
function acceptedOutcome(source, target, outcome) {
|
|
457
|
+
return {
|
|
458
|
+
type: 'playbook.acceptedOutcome',
|
|
459
|
+
params: { source, target, acceptedOutcome: outcome },
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/** Root `BOSS_INTERRUPT` arms, one per jumpable state. */
|
|
263
463
|
function bossInterrupts(ids) {
|
|
264
|
-
return ids.map((
|
|
265
|
-
guard:
|
|
266
|
-
|
|
267
|
-
typeof event.bossIntent === 'string' &&
|
|
268
|
-
event.bossIntent.trim().length > 0,
|
|
269
|
-
target: `#${id}`,
|
|
464
|
+
return ids.map((targetId) => ({
|
|
465
|
+
guard: { type: 'interruptTargets', params: { targetId } },
|
|
466
|
+
target: `#${targetId}`,
|
|
270
467
|
reenter: true,
|
|
271
|
-
actions: '
|
|
468
|
+
actions: 'restartFromInterrupt',
|
|
272
469
|
}));
|
|
273
470
|
}
|
|
471
|
+
/** Keyed `BOSS_REPLY` arms of the wait that resumes the given working leaves. */
|
|
274
472
|
function resumableStates(ids) {
|
|
275
|
-
return ids.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
473
|
+
return ids.flatMap((stateId) => [
|
|
474
|
+
{
|
|
475
|
+
guard: { type: 'bossReplyIsEmpty', params: { stateId } },
|
|
476
|
+
target: '#failed',
|
|
477
|
+
actions: 'rememberEmptyBossReply',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
guard: { type: 'bossReplyResumes', params: { stateId } },
|
|
481
|
+
target: `#${stateId}`,
|
|
482
|
+
reenter: true,
|
|
483
|
+
actions: { type: 'rememberBossReply', params: { stateId } },
|
|
484
|
+
},
|
|
485
|
+
]);
|
|
283
486
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
},
|
|
293
|
-
commitCoderProposal: {
|
|
294
|
-
sourceItem: 'DECIDE-3',
|
|
295
|
-
asker: { kind: 'role', roleId: 'coder' },
|
|
296
|
-
},
|
|
487
|
+
/**
|
|
488
|
+
* At the root synthesis wait, where no other task is active, a keyed
|
|
489
|
+
* `BOSS_REPLY` that names no pending question fails the run.
|
|
490
|
+
*/
|
|
491
|
+
const unknownBossReplyArm = {
|
|
492
|
+
guard: 'bossReplyNamesNoPendingQuestion',
|
|
493
|
+
target: '#failed',
|
|
494
|
+
actions: 'rememberUnknownBossReply',
|
|
297
495
|
};
|
|
496
|
+
const startDecideArm = {
|
|
497
|
+
guard: 'validStartTopic',
|
|
498
|
+
target: '#independentProposals',
|
|
499
|
+
actions: 'startDecide',
|
|
500
|
+
};
|
|
501
|
+
function suspendArm(stateId, wait) {
|
|
502
|
+
return {
|
|
503
|
+
guard: 'needsBossReply',
|
|
504
|
+
target: `#${wait}`,
|
|
505
|
+
actions: [
|
|
506
|
+
acceptedOutcome(stateId, wait, 'needsBossReply'),
|
|
507
|
+
{ type: 'setPendingBossQuestion', params: { stateId } },
|
|
508
|
+
],
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function malformedPlayerOutputArm(sourceItem) {
|
|
512
|
+
return {
|
|
513
|
+
target: '#failed',
|
|
514
|
+
actions: { type: 'rememberMalformedPlayerOutput', params: { sourceItem } },
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const playerOnError = {
|
|
518
|
+
target: '#failed',
|
|
519
|
+
actions: 'rememberActorError',
|
|
520
|
+
};
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Machine
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
298
524
|
export const decideMachine = setup({
|
|
299
525
|
types: {
|
|
300
526
|
context: {},
|
|
@@ -310,673 +536,480 @@ export const decideMachine = setup({
|
|
|
310
536
|
throw new Error('playbook actor must be provided by the runner');
|
|
311
537
|
}),
|
|
312
538
|
},
|
|
539
|
+
guards: {
|
|
540
|
+
validStartTopic: ({ event }) => event.type === 'START_DECIDE' && isNonEmptyString(event.callerTopic),
|
|
541
|
+
// Each arm guards its target id and every typed precondition for
|
|
542
|
+
// entering that target safely.
|
|
543
|
+
interruptTargets: ({ context, event }, params) => {
|
|
544
|
+
if (event.type !== 'BOSS_INTERRUPT')
|
|
545
|
+
return false;
|
|
546
|
+
if (event.targetId !== params.targetId)
|
|
547
|
+
return false;
|
|
548
|
+
if (event.targetId === 'independentProposals') {
|
|
549
|
+
return isNonEmptyString(event.callerTopic);
|
|
550
|
+
}
|
|
551
|
+
return (isNonEmptyString(context.callerTopic) &&
|
|
552
|
+
isNonEmptyString(context.coderProposal) &&
|
|
553
|
+
isNonEmptyString(context.reviewerProposal));
|
|
554
|
+
},
|
|
555
|
+
// A branch completing after its sibling has staged its proposal also
|
|
556
|
+
// completes the join, so its accepted outcome lands on `synthesizeCommit`.
|
|
557
|
+
coderProposedJoining: ({ context, event }) => isCoderProposed(event) &&
|
|
558
|
+
isNonEmptyString(context.stagedReviewerProposal) &&
|
|
559
|
+
isNonEmptyString(context.callerTopic),
|
|
560
|
+
coderProposed: ({ event }) => isCoderProposed(event),
|
|
561
|
+
reviewerProposedJoining: ({ context, event }) => isReviewerProposed(event) &&
|
|
562
|
+
isNonEmptyString(context.stagedCoderProposal) &&
|
|
563
|
+
isNonEmptyString(context.callerTopic),
|
|
564
|
+
reviewerProposed: ({ event }) => isReviewerProposed(event),
|
|
565
|
+
committed: ({ context, event }) => isCommitted(event) &&
|
|
566
|
+
isNonEmptyString(context.callerTopic) &&
|
|
567
|
+
isNonEmptyString(context.coderProposal),
|
|
568
|
+
needsBossReply: ({ event }) => isNeedsBossReply(event),
|
|
569
|
+
reviewApproved: ({ context, event }) => isNonEmptyString(context.decideCommit) &&
|
|
570
|
+
reviewApprovalFrom(event) !== undefined,
|
|
571
|
+
authoredReviewFailure: ({ context, event }) => isNonEmptyString(context.decideCommit) &&
|
|
572
|
+
authoredReviewFailureOf(event) !== undefined,
|
|
573
|
+
bossReplyNamesNoPendingQuestion: ({ context, event }) => event.type === 'BOSS_REPLY' &&
|
|
574
|
+
(!isResumableStateId(event.questionId) ||
|
|
575
|
+
context.pendingBossQuestions[event.questionId] === undefined),
|
|
576
|
+
bossReplyIsEmpty: ({ context, event }, params) => event.type === 'BOSS_REPLY' &&
|
|
577
|
+
event.questionId === params.stateId &&
|
|
578
|
+
context.pendingBossQuestions[params.stateId] !== undefined &&
|
|
579
|
+
!isNonEmptyString(event.answer),
|
|
580
|
+
bossReplyResumes: ({ context, event }, params) => event.type === 'BOSS_REPLY' &&
|
|
581
|
+
event.questionId === params.stateId &&
|
|
582
|
+
context.pendingBossQuestions[params.stateId] !== undefined &&
|
|
583
|
+
isNonEmptyString(event.answer) &&
|
|
584
|
+
isNonEmptyString(context.callerTopic),
|
|
585
|
+
},
|
|
313
586
|
actions: {
|
|
314
587
|
'playbook.acceptedOutcome': (_args, _params) => undefined,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
lastError: undefined,
|
|
325
|
-
pendingBossQuestions: undefined,
|
|
326
|
-
bossReplies: undefined,
|
|
327
|
-
stagedCoderResult: undefined,
|
|
328
|
-
stagedReviewerResult: undefined,
|
|
329
|
-
}),
|
|
330
|
-
copyInterruptedTopic: assign({
|
|
331
|
-
callerTopic: ({ event }) => event.type === 'BOSS_INTERRUPT' ? event.bossIntent : undefined,
|
|
332
|
-
coderProposal: undefined,
|
|
333
|
-
reviewerProposal: undefined,
|
|
334
|
-
latestCommit: undefined,
|
|
335
|
-
coderOutput: undefined,
|
|
336
|
-
reviewResult: undefined,
|
|
337
|
-
reviewFailure: undefined,
|
|
338
|
-
lastResult: undefined,
|
|
339
|
-
lastError: undefined,
|
|
340
|
-
pendingBossQuestions: undefined,
|
|
341
|
-
bossReplies: undefined,
|
|
342
|
-
stagedCoderResult: undefined,
|
|
343
|
-
stagedReviewerResult: undefined,
|
|
344
|
-
}),
|
|
345
|
-
stageCoderProposal: assign({
|
|
346
|
-
stagedCoderResult: ({ event }) => outputOf(event),
|
|
347
|
-
lastResult: ({ event }) => outputOf(event),
|
|
348
|
-
lastError: undefined,
|
|
349
|
-
}),
|
|
350
|
-
stageReviewerProposal: assign({
|
|
351
|
-
stagedReviewerResult: ({ event }) => outputOf(event),
|
|
352
|
-
lastResult: ({ event }) => outputOf(event),
|
|
353
|
-
lastError: undefined,
|
|
588
|
+
startDecide: assign(({ event }) => event.type === 'START_DECIDE' ? freshRun(event.callerTopic) : {}),
|
|
589
|
+
// A new topic restarts the whole proposal pair from a fresh run; a
|
|
590
|
+
// synthesis restart keeps the topic and the promoted proposals.
|
|
591
|
+
restartFromInterrupt: assign(({ event }) => {
|
|
592
|
+
if (event.type !== 'BOSS_INTERRUPT')
|
|
593
|
+
return {};
|
|
594
|
+
return event.targetId === 'independentProposals'
|
|
595
|
+
? freshRun(event.callerTopic)
|
|
596
|
+
: SYNTHESIS_RESTART;
|
|
354
597
|
}),
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
598
|
+
// Each branch assigns only its own staged result.
|
|
599
|
+
stageCoderProposal: assign(({ context, event }) => ({
|
|
600
|
+
stagedCoderProposal: outputString(event, 'coderProposal') ?? '',
|
|
601
|
+
pendingBossQuestions: withoutKey(context.pendingBossQuestions, 'askCoderProposal'),
|
|
602
|
+
bossReplies: withoutKey(context.bossReplies, 'askCoderProposal'),
|
|
603
|
+
})),
|
|
604
|
+
stageReviewerProposal: assign(({ context, event }) => ({
|
|
605
|
+
stagedReviewerProposal: outputString(event, 'reviewerProposal') ?? '',
|
|
606
|
+
pendingBossQuestions: withoutKey(context.pendingBossQuestions, 'askReviewerProposal'),
|
|
607
|
+
bossReplies: withoutKey(context.bossReplies, 'askReviewerProposal'),
|
|
608
|
+
})),
|
|
609
|
+
// The join promotes both staged proposals atomically.
|
|
610
|
+
promoteProposals: assign(({ context }) => ({
|
|
611
|
+
coderProposal: context.stagedCoderProposal,
|
|
612
|
+
reviewerProposal: context.stagedReviewerProposal,
|
|
613
|
+
stagedCoderProposal: '',
|
|
614
|
+
stagedReviewerProposal: '',
|
|
615
|
+
pendingBossQuestions: {},
|
|
616
|
+
bossReplies: {},
|
|
617
|
+
})),
|
|
618
|
+
rememberCommit: assign(({ context, event }) => ({
|
|
619
|
+
coderOutput: outputString(event, 'coderOutput') ?? '',
|
|
620
|
+
decideCommit: outputString(event, 'latestCommit') ?? '',
|
|
621
|
+
lastError: null,
|
|
622
|
+
pendingBossQuestions: withoutKey(context.pendingBossQuestions, 'synthesizeCommit'),
|
|
623
|
+
bossReplies: withoutKey(context.bossReplies, 'synthesizeCommit'),
|
|
624
|
+
})),
|
|
625
|
+
setPendingBossQuestion: assign(({ context, event }, params) => {
|
|
626
|
+
const question = outputString(event, 'question');
|
|
627
|
+
if (question === undefined)
|
|
628
|
+
return {};
|
|
629
|
+
const { sourceItem, roleId } = RESUMABLE_STATES[params.stateId];
|
|
630
|
+
const pending = {
|
|
631
|
+
questionId: params.stateId,
|
|
632
|
+
resumeStateId: params.stateId,
|
|
633
|
+
sourceItem,
|
|
634
|
+
asker: { kind: 'role', roleId },
|
|
635
|
+
question,
|
|
636
|
+
};
|
|
637
|
+
return {
|
|
638
|
+
pendingBossQuestions: {
|
|
639
|
+
...context.pendingBossQuestions,
|
|
640
|
+
[params.stateId]: pending,
|
|
641
|
+
},
|
|
642
|
+
bossReplies: withoutKey(context.bossReplies, params.stateId),
|
|
643
|
+
};
|
|
377
644
|
}),
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
name: 'ReviewProtocolError',
|
|
384
|
-
message: 'REVIEW returned without the evaluated repository revision and a no-unsettled-findings result for the supplied scope.',
|
|
645
|
+
rememberBossReply: assign(({ context, event }, params) => event.type === 'BOSS_REPLY'
|
|
646
|
+
? {
|
|
647
|
+
bossReplies: {
|
|
648
|
+
...context.bossReplies,
|
|
649
|
+
[params.stateId]: event.answer,
|
|
385
650
|
},
|
|
651
|
+
}
|
|
652
|
+
: {}),
|
|
653
|
+
rememberEmptyBossReply: assign({
|
|
654
|
+
lastError: {
|
|
655
|
+
name: 'MalformedBossReply',
|
|
656
|
+
message: 'BOSS_REPLY carried an empty answer.',
|
|
386
657
|
},
|
|
387
|
-
|
|
658
|
+
...FAILURE_RESET,
|
|
388
659
|
}),
|
|
389
|
-
|
|
390
|
-
lastError:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
rememberMalformedBossReply: assign({
|
|
396
|
-
lastError: ({ event }) => event,
|
|
660
|
+
rememberUnknownBossReply: assign({
|
|
661
|
+
lastError: {
|
|
662
|
+
name: 'MalformedBossReply',
|
|
663
|
+
message: 'BOSS_REPLY names no pending question.',
|
|
664
|
+
},
|
|
665
|
+
...FAILURE_RESET,
|
|
397
666
|
}),
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
667
|
+
rememberMalformedPlayerOutput: assign(({ event }, params) => {
|
|
668
|
+
const output = actorOutput(event);
|
|
669
|
+
return {
|
|
670
|
+
lastError: {
|
|
671
|
+
name: 'MalformedPlayerOutput',
|
|
672
|
+
message: output?.guard === 'needsBossReply'
|
|
673
|
+
? `Player output for ${params.sourceItem} declared needsBossReply without a question.`
|
|
674
|
+
: `Player output for ${params.sourceItem} did not match an outcome declared for its state.`,
|
|
405
675
|
},
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
lastResult: ({ event }) => outputOf(event),
|
|
409
|
-
lastError: undefined,
|
|
410
|
-
}),
|
|
411
|
-
rememberBossReply: assign({
|
|
412
|
-
bossReplies: ({ context, event }) => {
|
|
413
|
-
const questionId = resolvedQuestionId(context, event);
|
|
414
|
-
return event.type === 'BOSS_REPLY' && questionId
|
|
415
|
-
? { ...context.bossReplies, [questionId]: event.answer }
|
|
416
|
-
: context.bossReplies;
|
|
417
|
-
},
|
|
418
|
-
lastError: undefined,
|
|
676
|
+
...FAILURE_RESET,
|
|
677
|
+
};
|
|
419
678
|
}),
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
679
|
+
rememberActorError: assign(({ event }) => ({
|
|
680
|
+
lastError: errorRecord(actorError(event)),
|
|
681
|
+
...FAILURE_RESET,
|
|
682
|
+
})),
|
|
683
|
+
rememberReviewApproval: assign(({ event }) => {
|
|
684
|
+
const approval = reviewApprovalFrom(event);
|
|
685
|
+
if (approval === undefined)
|
|
686
|
+
return {};
|
|
687
|
+
const output = actorOutput(event);
|
|
688
|
+
const reviewEvidence = {
|
|
689
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
690
|
+
status: 'ok',
|
|
691
|
+
...(isJsonValue(output) ? { output } : {}),
|
|
692
|
+
};
|
|
693
|
+
return {
|
|
694
|
+
evaluatedRevision: approval.evaluatedRevision,
|
|
695
|
+
reviewStatus: null,
|
|
696
|
+
reviewError: null,
|
|
697
|
+
reviewEvidence,
|
|
698
|
+
lastError: null,
|
|
699
|
+
};
|
|
423
700
|
}),
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
701
|
+
rememberUnestablishedReview: assign(({ event }) => {
|
|
702
|
+
const output = actorOutput(event);
|
|
703
|
+
const reviewEvidence = {
|
|
704
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
705
|
+
status: 'ok',
|
|
706
|
+
...(isJsonValue(output) ? { output } : {}),
|
|
707
|
+
};
|
|
708
|
+
return {
|
|
709
|
+
evaluatedRevision: '',
|
|
710
|
+
reviewStatus: 'error',
|
|
711
|
+
reviewError: REVIEW_NOT_ESTABLISHED,
|
|
712
|
+
reviewEvidence,
|
|
713
|
+
lastError: null,
|
|
714
|
+
};
|
|
429
715
|
}),
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
716
|
+
rememberAuthoredReviewFailure: assign(({ event }) => {
|
|
717
|
+
const failure = authoredReviewFailureOf(event);
|
|
718
|
+
return failure === undefined
|
|
719
|
+
? {}
|
|
720
|
+
: {
|
|
721
|
+
evaluatedRevision: '',
|
|
722
|
+
reviewStatus: failure.status,
|
|
723
|
+
reviewError: failure.error,
|
|
724
|
+
reviewEvidence: failure.evidence,
|
|
725
|
+
lastError: null,
|
|
726
|
+
};
|
|
433
727
|
}),
|
|
434
728
|
},
|
|
435
|
-
guards: {
|
|
436
|
-
coderProposed,
|
|
437
|
-
reviewerProposed,
|
|
438
|
-
committed,
|
|
439
|
-
needsBossReplyWithQuestion,
|
|
440
|
-
needsBossReplyWithoutQuestion,
|
|
441
|
-
validReviewSuccess,
|
|
442
|
-
authoredReviewFailure,
|
|
443
|
-
},
|
|
444
729
|
}).createMachine({
|
|
445
730
|
id: 'decide',
|
|
731
|
+
description: 'DECIDE: independent Coder and Reviewer proposals, a synthesized decide-owned commit, and a nested review.',
|
|
446
732
|
initial: 'ready',
|
|
447
|
-
context: () =>
|
|
733
|
+
context: () => INITIAL_CONTEXT,
|
|
448
734
|
output: ({ context }) => {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
735
|
+
const commit = context.decideCommit;
|
|
736
|
+
if (!isNonEmptyString(commit)) {
|
|
737
|
+
throw new Error('DECIDE reached a final state without its decide-owned commit');
|
|
738
|
+
}
|
|
739
|
+
if (context.reviewStatus !== null) {
|
|
453
740
|
return {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
741
|
+
lastDecideCommit: commit,
|
|
742
|
+
noUnsettledFindings: false,
|
|
743
|
+
reviewStatus: context.reviewStatus,
|
|
744
|
+
...(context.reviewError === null ? {} : { error: context.reviewError }),
|
|
457
745
|
};
|
|
458
746
|
}
|
|
459
|
-
|
|
460
|
-
|
|
747
|
+
const evaluatedRevision = context.evaluatedRevision;
|
|
748
|
+
if (!isNonEmptyString(evaluatedRevision)) {
|
|
749
|
+
throw new Error('DECIDE completed without the evaluated repository revision');
|
|
461
750
|
}
|
|
462
751
|
return {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
...(context.reviewFailure.error
|
|
467
|
-
? { error: context.reviewFailure.error }
|
|
468
|
-
: {}),
|
|
752
|
+
decideCommit: commit,
|
|
753
|
+
evaluatedRevision,
|
|
754
|
+
noUnsettledFindings: true,
|
|
469
755
|
};
|
|
470
756
|
},
|
|
471
757
|
on: {
|
|
472
|
-
BOSS_INTERRUPT: bossInterrupts([
|
|
758
|
+
BOSS_INTERRUPT: bossInterrupts([
|
|
759
|
+
'independentProposals',
|
|
760
|
+
'synthesizeCommit',
|
|
761
|
+
]),
|
|
473
762
|
},
|
|
474
763
|
states: {
|
|
475
764
|
ready: {
|
|
476
765
|
id: 'ready',
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
playbook: {
|
|
481
|
-
stateId: 'ready',
|
|
482
|
-
description: 'Waiting for a topic to decide.',
|
|
483
|
-
},
|
|
484
|
-
},
|
|
766
|
+
description: STATE_DESCRIPTIONS.ready,
|
|
767
|
+
meta: playbookMeta('ready'),
|
|
768
|
+
tags: ['playbook.parked'],
|
|
485
769
|
on: {
|
|
486
|
-
START_DECIDE:
|
|
487
|
-
guard: ({ event }) => event.callerTopic.trim().length > 0,
|
|
488
|
-
target: 'independentProposals',
|
|
489
|
-
actions: 'copyStartTopic',
|
|
490
|
-
},
|
|
770
|
+
START_DECIDE: startDecideArm,
|
|
491
771
|
},
|
|
492
772
|
},
|
|
493
773
|
independentProposals: {
|
|
494
774
|
id: 'independentProposals',
|
|
775
|
+
description: STATE_DESCRIPTIONS.independentProposals,
|
|
776
|
+
meta: playbookMeta('independentProposals'),
|
|
495
777
|
type: 'parallel',
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
stateId: 'independentProposals',
|
|
500
|
-
description: 'Coder and Reviewer prepare independent proposals.',
|
|
501
|
-
},
|
|
502
|
-
},
|
|
778
|
+
// No group-level BOSS_REPLY handler: a reply is handled only by the
|
|
779
|
+
// branch wait whose pending question it names, and a reply naming no
|
|
780
|
+
// pending branch question moves no branch.
|
|
503
781
|
states: {
|
|
504
|
-
|
|
505
|
-
|
|
782
|
+
coderProposalRegion: {
|
|
783
|
+
id: 'coderProposalRegion',
|
|
784
|
+
description: STATE_DESCRIPTIONS.coderProposalRegion,
|
|
785
|
+
meta: playbookMeta('coderProposalRegion'),
|
|
786
|
+
initial: 'askCoderProposal',
|
|
506
787
|
states: {
|
|
507
|
-
|
|
788
|
+
askCoderProposal: {
|
|
508
789
|
id: 'askCoderProposal',
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
playbook: {
|
|
513
|
-
stateId: 'askCoderProposal',
|
|
514
|
-
description: 'Coder independently proposes a spec design.',
|
|
515
|
-
role: 'coder',
|
|
516
|
-
},
|
|
517
|
-
},
|
|
790
|
+
description: STATE_DESCRIPTIONS.askCoderProposal,
|
|
791
|
+
meta: playbookMeta('askCoderProposal', 'coder'),
|
|
792
|
+
tags: ['playbook.busy'],
|
|
518
793
|
invoke: {
|
|
519
794
|
src: 'player',
|
|
520
795
|
input: ({ context }) => ({
|
|
521
796
|
stateId: 'askCoderProposal',
|
|
522
|
-
sourceItem: 'DECIDE-1',
|
|
523
797
|
role: 'coder',
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}),
|
|
798
|
+
sourceItem: 'DECIDE-1',
|
|
799
|
+
prompt: CODER_PROPOSAL_PROMPT,
|
|
800
|
+
result: CODER_PROPOSAL_RESULT,
|
|
528
801
|
callerTopic: context.callerTopic,
|
|
529
802
|
...bossReplyFields(context, 'askCoderProposal'),
|
|
530
803
|
}),
|
|
531
804
|
onDone: [
|
|
805
|
+
suspendArm('askCoderProposal', 'awaitCoderProposalReply'),
|
|
532
806
|
{
|
|
533
|
-
guard: '
|
|
534
|
-
target: '
|
|
807
|
+
guard: 'coderProposedJoining',
|
|
808
|
+
target: 'coderProposalStaged',
|
|
535
809
|
actions: [
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
params: {
|
|
539
|
-
source: 'askCoderProposal',
|
|
540
|
-
target: 'waitCoderProposalReply',
|
|
541
|
-
acceptedOutcome: 'needsBossReply',
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
type: 'setPendingBossQuestion',
|
|
546
|
-
params: {
|
|
547
|
-
...stateMetadata.askCoderProposal,
|
|
548
|
-
resumeStateId: 'askCoderProposal',
|
|
549
|
-
question: '',
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
],
|
|
553
|
-
},
|
|
554
|
-
{
|
|
555
|
-
guard: 'needsBossReplyWithoutQuestion',
|
|
556
|
-
target: '#failed',
|
|
557
|
-
actions: [
|
|
558
|
-
'rememberMalformedActorOutput',
|
|
559
|
-
'clearProposalRoundContext',
|
|
810
|
+
acceptedOutcome('askCoderProposal', 'synthesizeCommit', 'proposed'),
|
|
811
|
+
'stageCoderProposal',
|
|
560
812
|
],
|
|
561
813
|
},
|
|
562
814
|
{
|
|
563
815
|
guard: 'coderProposed',
|
|
564
|
-
target: '
|
|
816
|
+
target: 'coderProposalStaged',
|
|
565
817
|
actions: [
|
|
566
|
-
|
|
567
|
-
type: 'playbook.acceptedOutcome',
|
|
568
|
-
params: ({ context }) => ({
|
|
569
|
-
source: 'askCoderProposal',
|
|
570
|
-
target: context.stagedReviewerResult === undefined
|
|
571
|
-
? 'coderProposalComplete'
|
|
572
|
-
: 'commitCoderProposal',
|
|
573
|
-
acceptedOutcome: 'proposed',
|
|
574
|
-
}),
|
|
575
|
-
},
|
|
818
|
+
acceptedOutcome('askCoderProposal', 'coderProposalStaged', 'proposed'),
|
|
576
819
|
'stageCoderProposal',
|
|
577
|
-
{
|
|
578
|
-
type: 'clearBranchBossReplyContext',
|
|
579
|
-
params: { stateId: 'askCoderProposal' },
|
|
580
|
-
},
|
|
581
|
-
],
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
target: '#failed',
|
|
585
|
-
actions: [
|
|
586
|
-
'rememberMalformedActorOutput',
|
|
587
|
-
'clearProposalRoundContext',
|
|
588
820
|
],
|
|
589
821
|
},
|
|
822
|
+
malformedPlayerOutputArm('DECIDE-1'),
|
|
590
823
|
],
|
|
591
|
-
onError:
|
|
592
|
-
target: '#failed',
|
|
593
|
-
actions: [
|
|
594
|
-
'rememberActorError',
|
|
595
|
-
'clearProposalRoundContext',
|
|
596
|
-
],
|
|
597
|
-
},
|
|
824
|
+
onError: playerOnError,
|
|
598
825
|
},
|
|
599
826
|
},
|
|
600
|
-
|
|
601
|
-
id: '
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
playbook: {
|
|
606
|
-
stateId: 'waitCoderProposalReply',
|
|
607
|
-
description: 'Coder waits for Boss to answer a question.',
|
|
608
|
-
},
|
|
609
|
-
},
|
|
827
|
+
awaitCoderProposalReply: {
|
|
828
|
+
id: 'awaitCoderProposalReply',
|
|
829
|
+
description: STATE_DESCRIPTIONS.awaitCoderProposalReply,
|
|
830
|
+
meta: playbookMeta('awaitCoderProposalReply'),
|
|
831
|
+
tags: ['playbook.parked'],
|
|
610
832
|
on: {
|
|
611
|
-
BOSS_REPLY: [
|
|
612
|
-
{
|
|
613
|
-
guard: bossReplyTargets('askCoderProposal', false),
|
|
614
|
-
target: '#failed',
|
|
615
|
-
actions: [
|
|
616
|
-
'rememberMalformedBossReply',
|
|
617
|
-
'clearProposalRoundContext',
|
|
618
|
-
],
|
|
619
|
-
},
|
|
620
|
-
{
|
|
621
|
-
guard: bossReplyTargets('askCoderProposal', true),
|
|
622
|
-
target: 'working',
|
|
623
|
-
actions: 'rememberBossReply',
|
|
624
|
-
},
|
|
625
|
-
],
|
|
833
|
+
BOSS_REPLY: resumableStates(['askCoderProposal']),
|
|
626
834
|
},
|
|
627
835
|
},
|
|
628
|
-
|
|
836
|
+
coderProposalStaged: {
|
|
837
|
+
id: 'coderProposalStaged',
|
|
838
|
+
description: STATE_DESCRIPTIONS.coderProposalStaged,
|
|
839
|
+
meta: playbookMeta('coderProposalStaged'),
|
|
629
840
|
type: 'final',
|
|
630
|
-
description: 'Coder proposal is staged for the parallel join.',
|
|
631
|
-
meta: {
|
|
632
|
-
playbook: {
|
|
633
|
-
stateId: 'coderProposalComplete',
|
|
634
|
-
description: 'Coder proposal is staged for the parallel join.',
|
|
635
|
-
},
|
|
636
|
-
},
|
|
637
841
|
},
|
|
638
842
|
},
|
|
639
843
|
},
|
|
640
|
-
|
|
641
|
-
|
|
844
|
+
reviewerProposalRegion: {
|
|
845
|
+
id: 'reviewerProposalRegion',
|
|
846
|
+
description: STATE_DESCRIPTIONS.reviewerProposalRegion,
|
|
847
|
+
meta: playbookMeta('reviewerProposalRegion'),
|
|
848
|
+
initial: 'askReviewerProposal',
|
|
642
849
|
states: {
|
|
643
|
-
|
|
850
|
+
askReviewerProposal: {
|
|
644
851
|
id: 'askReviewerProposal',
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
playbook: {
|
|
649
|
-
stateId: 'askReviewerProposal',
|
|
650
|
-
description: 'Reviewer independently proposes a spec design.',
|
|
651
|
-
role: 'reviewer',
|
|
652
|
-
},
|
|
653
|
-
},
|
|
852
|
+
description: STATE_DESCRIPTIONS.askReviewerProposal,
|
|
853
|
+
meta: playbookMeta('askReviewerProposal', 'reviewer'),
|
|
854
|
+
tags: ['playbook.busy'],
|
|
654
855
|
invoke: {
|
|
655
856
|
src: 'player',
|
|
656
857
|
input: ({ context }) => ({
|
|
657
858
|
stateId: 'askReviewerProposal',
|
|
658
|
-
sourceItem: 'DECIDE-2',
|
|
659
859
|
role: 'reviewer',
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}),
|
|
860
|
+
sourceItem: 'DECIDE-2',
|
|
861
|
+
prompt: REVIEWER_PROPOSAL_PROMPT,
|
|
862
|
+
result: REVIEWER_PROPOSAL_RESULT,
|
|
664
863
|
callerTopic: context.callerTopic,
|
|
665
864
|
...bossReplyFields(context, 'askReviewerProposal'),
|
|
666
865
|
}),
|
|
667
866
|
onDone: [
|
|
867
|
+
suspendArm('askReviewerProposal', 'awaitReviewerProposalReply'),
|
|
668
868
|
{
|
|
669
|
-
guard: '
|
|
670
|
-
target: '
|
|
671
|
-
actions: [
|
|
672
|
-
{
|
|
673
|
-
type: 'playbook.acceptedOutcome',
|
|
674
|
-
params: {
|
|
675
|
-
source: 'askReviewerProposal',
|
|
676
|
-
target: 'waitReviewerProposalReply',
|
|
677
|
-
acceptedOutcome: 'needsBossReply',
|
|
678
|
-
},
|
|
679
|
-
},
|
|
680
|
-
{
|
|
681
|
-
type: 'setPendingBossQuestion',
|
|
682
|
-
params: {
|
|
683
|
-
...stateMetadata.askReviewerProposal,
|
|
684
|
-
resumeStateId: 'askReviewerProposal',
|
|
685
|
-
question: '',
|
|
686
|
-
},
|
|
687
|
-
},
|
|
688
|
-
],
|
|
689
|
-
},
|
|
690
|
-
{
|
|
691
|
-
guard: 'needsBossReplyWithoutQuestion',
|
|
692
|
-
target: '#failed',
|
|
869
|
+
guard: 'reviewerProposedJoining',
|
|
870
|
+
target: 'reviewerProposalStaged',
|
|
693
871
|
actions: [
|
|
694
|
-
'
|
|
695
|
-
'
|
|
872
|
+
acceptedOutcome('askReviewerProposal', 'synthesizeCommit', 'proposed'),
|
|
873
|
+
'stageReviewerProposal',
|
|
696
874
|
],
|
|
697
875
|
},
|
|
698
876
|
{
|
|
699
877
|
guard: 'reviewerProposed',
|
|
700
|
-
target: '
|
|
878
|
+
target: 'reviewerProposalStaged',
|
|
701
879
|
actions: [
|
|
702
|
-
|
|
703
|
-
type: 'playbook.acceptedOutcome',
|
|
704
|
-
params: ({ context }) => ({
|
|
705
|
-
source: 'askReviewerProposal',
|
|
706
|
-
target: context.stagedCoderResult === undefined
|
|
707
|
-
? 'reviewerProposalComplete'
|
|
708
|
-
: 'commitCoderProposal',
|
|
709
|
-
acceptedOutcome: 'proposed',
|
|
710
|
-
}),
|
|
711
|
-
},
|
|
880
|
+
acceptedOutcome('askReviewerProposal', 'reviewerProposalStaged', 'proposed'),
|
|
712
881
|
'stageReviewerProposal',
|
|
713
|
-
{
|
|
714
|
-
type: 'clearBranchBossReplyContext',
|
|
715
|
-
params: { stateId: 'askReviewerProposal' },
|
|
716
|
-
},
|
|
717
|
-
],
|
|
718
|
-
},
|
|
719
|
-
{
|
|
720
|
-
target: '#failed',
|
|
721
|
-
actions: [
|
|
722
|
-
'rememberMalformedActorOutput',
|
|
723
|
-
'clearProposalRoundContext',
|
|
724
882
|
],
|
|
725
883
|
},
|
|
884
|
+
malformedPlayerOutputArm('DECIDE-2'),
|
|
726
885
|
],
|
|
727
|
-
onError:
|
|
728
|
-
target: '#failed',
|
|
729
|
-
actions: [
|
|
730
|
-
'rememberActorError',
|
|
731
|
-
'clearProposalRoundContext',
|
|
732
|
-
],
|
|
733
|
-
},
|
|
886
|
+
onError: playerOnError,
|
|
734
887
|
},
|
|
735
888
|
},
|
|
736
|
-
|
|
737
|
-
id: '
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
playbook: {
|
|
742
|
-
stateId: 'waitReviewerProposalReply',
|
|
743
|
-
description: 'Reviewer waits for Boss to answer a question.',
|
|
744
|
-
},
|
|
745
|
-
},
|
|
889
|
+
awaitReviewerProposalReply: {
|
|
890
|
+
id: 'awaitReviewerProposalReply',
|
|
891
|
+
description: STATE_DESCRIPTIONS.awaitReviewerProposalReply,
|
|
892
|
+
meta: playbookMeta('awaitReviewerProposalReply'),
|
|
893
|
+
tags: ['playbook.parked'],
|
|
746
894
|
on: {
|
|
747
|
-
BOSS_REPLY: [
|
|
748
|
-
{
|
|
749
|
-
guard: bossReplyTargets('askReviewerProposal', false),
|
|
750
|
-
target: '#failed',
|
|
751
|
-
actions: [
|
|
752
|
-
'rememberMalformedBossReply',
|
|
753
|
-
'clearProposalRoundContext',
|
|
754
|
-
],
|
|
755
|
-
},
|
|
756
|
-
{
|
|
757
|
-
guard: bossReplyTargets('askReviewerProposal', true),
|
|
758
|
-
target: 'working',
|
|
759
|
-
actions: 'rememberBossReply',
|
|
760
|
-
},
|
|
761
|
-
],
|
|
895
|
+
BOSS_REPLY: resumableStates(['askReviewerProposal']),
|
|
762
896
|
},
|
|
763
897
|
},
|
|
764
|
-
|
|
898
|
+
reviewerProposalStaged: {
|
|
899
|
+
id: 'reviewerProposalStaged',
|
|
900
|
+
description: STATE_DESCRIPTIONS.reviewerProposalStaged,
|
|
901
|
+
meta: playbookMeta('reviewerProposalStaged'),
|
|
765
902
|
type: 'final',
|
|
766
|
-
description: 'Reviewer proposal is staged for the parallel join.',
|
|
767
|
-
meta: {
|
|
768
|
-
playbook: {
|
|
769
|
-
stateId: 'reviewerProposalComplete',
|
|
770
|
-
description: 'Reviewer proposal is staged for the parallel join.',
|
|
771
|
-
},
|
|
772
|
-
},
|
|
773
903
|
},
|
|
774
904
|
},
|
|
775
905
|
},
|
|
776
906
|
},
|
|
907
|
+
// The join: taken only after both regions reach final; it promotes both
|
|
908
|
+
// staged proposals atomically before DECIDE-3 begins.
|
|
777
909
|
onDone: {
|
|
778
|
-
target: '
|
|
910
|
+
target: 'synthesizeCommit',
|
|
779
911
|
actions: 'promoteProposals',
|
|
780
912
|
},
|
|
781
913
|
},
|
|
782
|
-
|
|
783
|
-
id: '
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
playbook: {
|
|
788
|
-
stateId: 'commitCoderProposal',
|
|
789
|
-
description: 'Coder synthesizes both proposals and commits the design.',
|
|
790
|
-
role: 'coder',
|
|
791
|
-
},
|
|
792
|
-
},
|
|
914
|
+
synthesizeCommit: {
|
|
915
|
+
id: 'synthesizeCommit',
|
|
916
|
+
description: STATE_DESCRIPTIONS.synthesizeCommit,
|
|
917
|
+
meta: playbookMeta('synthesizeCommit', 'coder'),
|
|
918
|
+
tags: ['playbook.busy'],
|
|
793
919
|
invoke: {
|
|
794
920
|
src: 'player',
|
|
795
921
|
input: ({ context }) => ({
|
|
796
|
-
stateId: '
|
|
797
|
-
sourceItem: 'DECIDE-3',
|
|
922
|
+
stateId: 'synthesizeCommit',
|
|
798
923
|
role: 'coder',
|
|
924
|
+
sourceItem: 'DECIDE-3',
|
|
799
925
|
prompt: SYNTHESIZE_COMMIT_PROMPT,
|
|
800
|
-
result:
|
|
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>`.',
|
|
802
|
-
}),
|
|
926
|
+
result: SYNTHESIZE_COMMIT_RESULT,
|
|
803
927
|
callerTopic: context.callerTopic,
|
|
804
928
|
reviewerProposal: context.reviewerProposal,
|
|
805
|
-
...bossReplyFields(context, '
|
|
929
|
+
...bossReplyFields(context, 'synthesizeCommit'),
|
|
806
930
|
}),
|
|
807
931
|
onDone: [
|
|
808
|
-
|
|
809
|
-
guard: 'needsBossReplyWithQuestion',
|
|
810
|
-
target: 'awaitBossReply',
|
|
811
|
-
actions: [
|
|
812
|
-
{
|
|
813
|
-
type: 'playbook.acceptedOutcome',
|
|
814
|
-
params: {
|
|
815
|
-
source: 'commitCoderProposal',
|
|
816
|
-
target: 'awaitBossReply',
|
|
817
|
-
acceptedOutcome: 'needsBossReply',
|
|
818
|
-
},
|
|
819
|
-
},
|
|
820
|
-
{
|
|
821
|
-
type: 'setPendingBossQuestion',
|
|
822
|
-
params: {
|
|
823
|
-
...stateMetadata.commitCoderProposal,
|
|
824
|
-
resumeStateId: 'commitCoderProposal',
|
|
825
|
-
question: '',
|
|
826
|
-
},
|
|
827
|
-
},
|
|
828
|
-
],
|
|
829
|
-
},
|
|
830
|
-
{
|
|
831
|
-
guard: 'needsBossReplyWithoutQuestion',
|
|
832
|
-
target: 'failed',
|
|
833
|
-
actions: [
|
|
834
|
-
'rememberMalformedActorOutput',
|
|
835
|
-
'clearBossReplyContext',
|
|
836
|
-
],
|
|
837
|
-
},
|
|
932
|
+
suspendArm('synthesizeCommit', 'awaitBossReply'),
|
|
838
933
|
{
|
|
839
934
|
guard: 'committed',
|
|
840
935
|
target: 'reviewCommit',
|
|
841
936
|
actions: [
|
|
842
|
-
|
|
843
|
-
type: 'playbook.acceptedOutcome',
|
|
844
|
-
params: {
|
|
845
|
-
source: 'commitCoderProposal',
|
|
846
|
-
target: 'reviewCommit',
|
|
847
|
-
acceptedOutcome: 'committed',
|
|
848
|
-
},
|
|
849
|
-
},
|
|
937
|
+
acceptedOutcome('synthesizeCommit', 'reviewCommit', 'committed'),
|
|
850
938
|
'rememberCommit',
|
|
851
|
-
{
|
|
852
|
-
type: 'clearBranchBossReplyContext',
|
|
853
|
-
params: { stateId: 'commitCoderProposal' },
|
|
854
|
-
},
|
|
855
|
-
],
|
|
856
|
-
},
|
|
857
|
-
{
|
|
858
|
-
target: 'failed',
|
|
859
|
-
actions: [
|
|
860
|
-
'rememberMalformedActorOutput',
|
|
861
|
-
'clearBossReplyContext',
|
|
862
939
|
],
|
|
863
940
|
},
|
|
941
|
+
malformedPlayerOutputArm('DECIDE-3'),
|
|
864
942
|
],
|
|
865
|
-
onError:
|
|
866
|
-
target: 'failed',
|
|
867
|
-
actions: ['rememberActorError', 'clearBossReplyContext'],
|
|
868
|
-
},
|
|
943
|
+
onError: playerOnError,
|
|
869
944
|
},
|
|
870
945
|
},
|
|
871
946
|
awaitBossReply: {
|
|
872
947
|
id: 'awaitBossReply',
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
playbook: {
|
|
877
|
-
stateId: 'awaitBossReply',
|
|
878
|
-
description: 'Waiting for Boss to answer Coder’s question.',
|
|
879
|
-
},
|
|
880
|
-
},
|
|
948
|
+
description: STATE_DESCRIPTIONS.awaitBossReply,
|
|
949
|
+
meta: playbookMeta('awaitBossReply'),
|
|
950
|
+
tags: ['playbook.parked'],
|
|
881
951
|
on: {
|
|
882
952
|
BOSS_REPLY: [
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
target: 'failed',
|
|
886
|
-
actions: [
|
|
887
|
-
'rememberMalformedBossReply',
|
|
888
|
-
'clearBossReplyContext',
|
|
889
|
-
],
|
|
890
|
-
},
|
|
891
|
-
...resumableStates(['commitCoderProposal']),
|
|
953
|
+
unknownBossReplyArm,
|
|
954
|
+
...resumableStates(['synthesizeCommit']),
|
|
892
955
|
],
|
|
956
|
+
START_DECIDE: startDecideArm,
|
|
893
957
|
},
|
|
894
958
|
},
|
|
895
959
|
reviewCommit: {
|
|
896
960
|
id: 'reviewCommit',
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
playbook: {
|
|
901
|
-
stateId: 'reviewCommit',
|
|
902
|
-
description: 'REVIEW examines the committed proposal.',
|
|
903
|
-
},
|
|
904
|
-
},
|
|
961
|
+
description: STATE_DESCRIPTIONS.reviewCommit,
|
|
962
|
+
meta: playbookMeta('reviewCommit'),
|
|
963
|
+
tags: ['playbook.suspended'],
|
|
905
964
|
invoke: {
|
|
906
965
|
src: 'playbook',
|
|
907
966
|
input: ({ context }) => ({
|
|
908
967
|
stateId: 'reviewCommit',
|
|
909
968
|
sourceItem: 'DECIDE-4',
|
|
910
|
-
playbookId:
|
|
911
|
-
text:
|
|
969
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
970
|
+
text: reviewCallText(context),
|
|
912
971
|
}),
|
|
913
972
|
onDone: [
|
|
914
973
|
{
|
|
915
|
-
guard: '
|
|
974
|
+
guard: 'reviewApproved',
|
|
916
975
|
target: 'done',
|
|
917
|
-
actions: '
|
|
976
|
+
actions: 'rememberReviewApproval',
|
|
918
977
|
},
|
|
919
978
|
{
|
|
920
979
|
target: 'reportedReviewFailure',
|
|
921
|
-
actions: '
|
|
980
|
+
actions: 'rememberUnestablishedReview',
|
|
922
981
|
},
|
|
923
982
|
],
|
|
924
983
|
onError: [
|
|
925
984
|
{
|
|
926
985
|
guard: 'authoredReviewFailure',
|
|
927
986
|
target: 'reportedReviewFailure',
|
|
928
|
-
actions: '
|
|
929
|
-
},
|
|
930
|
-
{
|
|
931
|
-
target: 'failed',
|
|
932
|
-
actions: 'rememberActorError',
|
|
987
|
+
actions: 'rememberAuthoredReviewFailure',
|
|
933
988
|
},
|
|
989
|
+
{ target: 'failed', actions: 'rememberActorError' },
|
|
934
990
|
],
|
|
935
991
|
},
|
|
936
992
|
},
|
|
937
993
|
failed: {
|
|
938
994
|
id: 'failed',
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
playbook: {
|
|
943
|
-
stateId: 'failed',
|
|
944
|
-
description: 'DECIDE failed and is waiting for a new topic.',
|
|
945
|
-
},
|
|
946
|
-
},
|
|
995
|
+
description: STATE_DESCRIPTIONS.failed,
|
|
996
|
+
meta: playbookMeta('failed'),
|
|
997
|
+
tags: ['playbook.parked'],
|
|
947
998
|
on: {
|
|
948
|
-
START_DECIDE:
|
|
949
|
-
guard: ({ event }) => event.callerTopic.trim().length > 0,
|
|
950
|
-
target: 'independentProposals',
|
|
951
|
-
actions: 'copyStartTopic',
|
|
952
|
-
},
|
|
999
|
+
START_DECIDE: startDecideArm,
|
|
953
1000
|
},
|
|
954
1001
|
},
|
|
955
1002
|
reportedReviewFailure: {
|
|
956
1003
|
id: 'reportedReviewFailure',
|
|
1004
|
+
description: STATE_DESCRIPTIONS.reportedReviewFailure,
|
|
1005
|
+
meta: terminalMeta('reportedReviewFailure', 'failure'),
|
|
957
1006
|
type: 'final',
|
|
958
|
-
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
959
|
-
meta: {
|
|
960
|
-
playbook: {
|
|
961
|
-
stateId: 'reportedReviewFailure',
|
|
962
|
-
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
963
|
-
// DR-048: this final state means the workflow failed, so a caller
|
|
964
|
-
// learns that from the machine rather than from DECIDE's output.
|
|
965
|
-
terminal: 'failure',
|
|
966
|
-
},
|
|
967
|
-
},
|
|
968
1007
|
},
|
|
969
1008
|
done: {
|
|
970
1009
|
id: 'done',
|
|
1010
|
+
description: STATE_DESCRIPTIONS.done,
|
|
1011
|
+
meta: terminalMeta('done', 'success'),
|
|
971
1012
|
type: 'final',
|
|
972
|
-
description: 'DECIDE completed with an approved commit.',
|
|
973
|
-
meta: {
|
|
974
|
-
playbook: {
|
|
975
|
-
stateId: 'done',
|
|
976
|
-
description: 'DECIDE completed with an approved commit.',
|
|
977
|
-
terminal: 'success',
|
|
978
|
-
},
|
|
979
|
-
},
|
|
980
1013
|
},
|
|
981
1014
|
},
|
|
982
1015
|
});
|