@warren-dean/code-kit 0.13.0 → 0.14.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +7 -2
- package/bin/code-kit.mjs +26 -6
- package/hooks/guard-bash.mjs +5 -0
- package/hooks/lib/loop-state.mjs +49 -9
- package/hooks/mod/decisions.mjs +75 -0
- package/hooks/mod/loop.mjs +74 -0
- package/hooks/mod/mission.mjs +19 -15
- package/hooks/mod/register.mjs +283 -14
- package/hooks/mod/register.test.ts +335 -8
- package/hooks/mod/view.mjs +66 -4
- package/package.json +1 -1
package/hooks/mod/register.mjs
CHANGED
|
@@ -51,8 +51,12 @@ import {
|
|
|
51
51
|
toolLine,
|
|
52
52
|
} from './panes.mjs';
|
|
53
53
|
import {
|
|
54
|
+
answeredPrompt,
|
|
55
|
+
designHandedOver,
|
|
54
56
|
leadPrompt,
|
|
55
57
|
nudgeText,
|
|
58
|
+
outcomePrompt,
|
|
59
|
+
readOutcome,
|
|
56
60
|
restartPrompt,
|
|
57
61
|
stallDue,
|
|
58
62
|
stepKey,
|
|
@@ -61,7 +65,7 @@ import {
|
|
|
61
65
|
withoutInFlight,
|
|
62
66
|
} from './loop.mjs';
|
|
63
67
|
import { goalOf, nextText } from './mission.mjs';
|
|
64
|
-
import { contextOf, decisionCards, limitsWhy } from './decisions.mjs';
|
|
68
|
+
import { contextOf, decisionCards, limitsWhy, nativeChoice } from './decisions.mjs';
|
|
65
69
|
import { DEFAULT_TEAM_UI, TEAM_ID, teamFrame } from './teamview.mjs';
|
|
66
70
|
import { glowOf, mapStoryOf, touchOf } from './codemap.mjs';
|
|
67
71
|
import { harnessSettings } from '../lib/harness.mjs';
|
|
@@ -111,7 +115,22 @@ const loop = {
|
|
|
111
115
|
nextStep: null, // what `code-kit next` last said, for the band's next row
|
|
112
116
|
done: null, // { count } once every story is done
|
|
113
117
|
doneDismissed: false,
|
|
118
|
+
// LOOP-11: the step the lead's turn was prompted with, until that turn ends: { prompt, key, label, kind, target }
|
|
119
|
+
sent: null,
|
|
120
|
+
// LOOP-11, LOOP-12: what holds the loop: { kind: 'person' | 'stopped' | 'blocked' | 'design', key, label, prompt, question, options, reason }
|
|
121
|
+
waiting: null,
|
|
122
|
+
handedDesign: new Set(), // LOOP-12: design stories handed to the lead this session
|
|
114
123
|
};
|
|
124
|
+
// LOOP-10: whether this session runs the loop, the session that does when another does, and when this
|
|
125
|
+
// one last renewed its lease.
|
|
126
|
+
const lease = { mine: false, other: null, renewed: -Infinity };
|
|
127
|
+
const LEASE_RENEW_MS = 30000;
|
|
128
|
+
// LOOP-11: the lead's last reply, read once before the loop next prompts it: { turnId, text, aborted, sent, read }
|
|
129
|
+
let lastReply = null;
|
|
130
|
+
let turnQuestions = []; // the open questions as the lead's running turn started
|
|
131
|
+
// DECIDE-4: decisions asked in Claude Code's own question dialog this session, and whether one is open.
|
|
132
|
+
const offered = new Set();
|
|
133
|
+
let offering = false;
|
|
115
134
|
/** LOOP-8: the times in a row the lead is prompted with one step before the person is asked. */
|
|
116
135
|
const ASK_LIMIT = 2;
|
|
117
136
|
let leadTurn = null; // the lead's running turn, if any
|
|
@@ -207,7 +226,7 @@ export function register(on) {
|
|
|
207
226
|
const cli = `${$.plugin.root}/bin/code-kit.mjs`;
|
|
208
227
|
const cwd = e.cwd ?? (await $.session.cwd());
|
|
209
228
|
const session = await $.session.id();
|
|
210
|
-
where = { cli, cwd, interactive: e.isInteractive !== false };
|
|
229
|
+
where = { cli, cwd, session, interactive: e.isInteractive !== false };
|
|
211
230
|
// The held call's answer, for the `code-kit hold` it waits in.
|
|
212
231
|
const answerHeld = (id, answer) =>
|
|
213
232
|
$.process.run(['node', cli, 'hold', id, '--answer', answer], { cwd });
|
|
@@ -217,9 +236,12 @@ export function register(on) {
|
|
|
217
236
|
};
|
|
218
237
|
// What a CLI run said, in a line: why it refused, for a notice.
|
|
219
238
|
const firstLine = (ran) => (ran.stderr || ran.stdout).trim().split('\n')[0];
|
|
220
|
-
// A session starts with the loop running: a pause recorded by an earlier one no longer holds.
|
|
221
|
-
|
|
239
|
+
// A session starts with the loop running: a pause recorded by an earlier one no longer holds. A pause
|
|
240
|
+
// by a session that still runs the loop (LOOP-10) is that session's, and stays.
|
|
241
|
+
const recorded = await json('loop').catch(() => null);
|
|
242
|
+
if (recorded?.paused && (!recorded.lead || recorded.lead.session === session))
|
|
222
243
|
await $.process.run(['node', cli, 'loop', 'resume'], { cwd }).catch(() => {});
|
|
244
|
+
lease.other = recorded?.lead && recorded.lead.session !== session ? recorded.lead : null;
|
|
223
245
|
const stamp = async (path) => {
|
|
224
246
|
const s = await $.fs.stat(`${cwd}/${path}`).catch(() => null);
|
|
225
247
|
return s ? `${path}@${s.mtimeMs}` : `${path}-`;
|
|
@@ -270,6 +292,11 @@ export function register(on) {
|
|
|
270
292
|
boardSynced = at;
|
|
271
293
|
$.process.run(['node', cli, 'team', 'board'], { cwd, timeoutMs: 120000 }).catch(() => {});
|
|
272
294
|
}
|
|
295
|
+
// LOOP-10: while another session runs the loop, whether it still does (its lease lapses as it ends).
|
|
296
|
+
if (!lease.mine) {
|
|
297
|
+
const lead = (await json('loop').catch(() => null))?.lead ?? null;
|
|
298
|
+
lease.other = lead && lead.session !== session ? lead : null;
|
|
299
|
+
}
|
|
273
300
|
// The agents at work; none where the session has none bound (claude -p before it mounts).
|
|
274
301
|
const atWork = agentsAtWork(await $.agent.list().catch(() => []));
|
|
275
302
|
model = {
|
|
@@ -634,6 +661,12 @@ export function register(on) {
|
|
|
634
661
|
}
|
|
635
662
|
if (q.kind === 'question')
|
|
636
663
|
model = { ...model, questions: (model.questions ?? []).filter((x) => x.id !== q.id) };
|
|
664
|
+
// LOOP-11: answered, what the lead waited on no longer holds the loop.
|
|
665
|
+
if (
|
|
666
|
+
q.kind === 'lead' ||
|
|
667
|
+
(q.kind === 'question' && loop.waiting?.questionIds?.includes(q.id))
|
|
668
|
+
)
|
|
669
|
+
loop.waiting = null;
|
|
637
670
|
$.ui.invalidate('ui.render');
|
|
638
671
|
const said = /[.!?]$/.test(text) ? text : `${text}.`;
|
|
639
672
|
$.prompt
|
|
@@ -641,7 +674,9 @@ export function register(on) {
|
|
|
641
674
|
text:
|
|
642
675
|
q.kind === 'question'
|
|
643
676
|
? `The person answered ${q.id} (${q.question}): ${said}${q.blocks ? ` ${q.story} no longer waits on them.` : ''} Carry on with it.`
|
|
644
|
-
:
|
|
677
|
+
: q.kind === 'lead'
|
|
678
|
+
? answeredPrompt(q.question, text)
|
|
679
|
+
: `The person answered an open question in ${q.path}, "${q.question}": ${said} Record it in the brief's decision log, take it out of the open questions, and carry on with it.`,
|
|
645
680
|
})
|
|
646
681
|
.catch(() => {});
|
|
647
682
|
await act.reload();
|
|
@@ -1004,17 +1039,184 @@ export function register(on) {
|
|
|
1004
1039
|
loop.steps.push({ at: await $.clock.now(), kind, target, ...(prompt ? { prompt } : {}) });
|
|
1005
1040
|
if (loop.steps.length > 50) loop.steps.shift();
|
|
1006
1041
|
},
|
|
1007
|
-
// LOOP-1: a prompt to the lead, recorded. It isn't awaited: the turn it starts runs on.
|
|
1008
|
-
|
|
1042
|
+
// LOOP-1: a prompt to the lead, recorded. It isn't awaited: the turn it starts runs on. LOOP-11:
|
|
1043
|
+
// the step is kept until that turn ends, so the loop reads the reply to it.
|
|
1044
|
+
submit: async (
|
|
1045
|
+
prompt,
|
|
1046
|
+
kind,
|
|
1047
|
+
target,
|
|
1048
|
+
{ key = `${kind}:${target}`, label = `${kind} ${target}` } = {},
|
|
1049
|
+
) => {
|
|
1009
1050
|
loop.started = true;
|
|
1010
1051
|
loop.proposal = null;
|
|
1052
|
+
loop.sent = { prompt, key, label, kind, target };
|
|
1053
|
+
// LOOP-12: a design story is handed to the lead once; the person designs it with the lead.
|
|
1054
|
+
if (kind === 'design')
|
|
1055
|
+
for (const id of String(target).split(/\s+/).filter(Boolean)) loop.handedDesign.add(id);
|
|
1011
1056
|
await act.record(kind, target, prompt);
|
|
1012
1057
|
$.ui.invalidate('ui.render');
|
|
1013
1058
|
$.prompt.submit({ text: prompt }).catch((err) => {
|
|
1059
|
+
loop.sent = null;
|
|
1014
1060
|
notice = `The loop couldn't prompt the lead: ${err?.message ?? err}`;
|
|
1015
1061
|
$.ui.invalidate('ui.render');
|
|
1016
1062
|
});
|
|
1017
1063
|
},
|
|
1064
|
+
// LOOP-10: this session takes or renews the lease of the one session that runs the loop; another
|
|
1065
|
+
// session holding it keeps it, unless the person moves it here (`take`).
|
|
1066
|
+
lead: async ({ take = false } = {}) => {
|
|
1067
|
+
const now = await $.clock.now();
|
|
1068
|
+
if (lease.mine && !take && now - lease.renewed < LEASE_RENEW_MS) return true;
|
|
1069
|
+
const ran = await $.process
|
|
1070
|
+
.run(['node', cli, 'loop', 'lead', '--session', session, ...(take ? ['--take'] : [])], {
|
|
1071
|
+
cwd,
|
|
1072
|
+
})
|
|
1073
|
+
.catch(() => ({ exitCode: 1 }));
|
|
1074
|
+
const was = lease.mine;
|
|
1075
|
+
lease.mine = ran.exitCode === 0;
|
|
1076
|
+
lease.renewed = now;
|
|
1077
|
+
lease.other = lease.mine ? null : ((await json('loop').catch(() => null))?.lead ?? null);
|
|
1078
|
+
// Taken over by another session: this one stops prompting its lead, and offers nothing more.
|
|
1079
|
+
if (was && !lease.mine) {
|
|
1080
|
+
loop.proposal = null;
|
|
1081
|
+
loop.pending = [];
|
|
1082
|
+
loop.waiting = null;
|
|
1083
|
+
}
|
|
1084
|
+
if (was !== lease.mine || !lease.mine) $.ui.invalidate('ui.render');
|
|
1085
|
+
return lease.mine;
|
|
1086
|
+
},
|
|
1087
|
+
// LOOP-10: the person moves the loop to this session; the other stops prompting its lead.
|
|
1088
|
+
leadHere: async () => {
|
|
1089
|
+
if (!(await act.lead({ take: true }))) {
|
|
1090
|
+
notice = "This session couldn't take the loop: code-kit loop lead failed.";
|
|
1091
|
+
$.ui.invalidate('ui.render');
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
loop.lastKey = null;
|
|
1095
|
+
await act.record('lead', 'this session');
|
|
1096
|
+
$.ui.invalidate('ui.render');
|
|
1097
|
+
await act.step('resume');
|
|
1098
|
+
},
|
|
1099
|
+
// LOOP-11: the lead's last reply, read once before the loop prompts it again. A reply that waits on
|
|
1100
|
+
// the person, or a turn they stopped, holds the loop until they speak; a step the lead didn't take
|
|
1101
|
+
// holds that step while it's still next.
|
|
1102
|
+
hear: async () => {
|
|
1103
|
+
const reply = lastReply;
|
|
1104
|
+
if (!reply || reply.read) return;
|
|
1105
|
+
reply.read = true;
|
|
1106
|
+
const sent = reply.sent;
|
|
1107
|
+
const step = sent
|
|
1108
|
+
? {
|
|
1109
|
+
key: sent.key,
|
|
1110
|
+
label: sent.label,
|
|
1111
|
+
prompt: sent.prompt,
|
|
1112
|
+
stepKind: sent.kind,
|
|
1113
|
+
target: sent.target,
|
|
1114
|
+
}
|
|
1115
|
+
: {};
|
|
1116
|
+
if (reply.aborted) {
|
|
1117
|
+
loop.waiting = { kind: 'stopped', ...step };
|
|
1118
|
+
$.ui.invalidate('ui.render');
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
// It asked through code-kit: those questions are the person's decisions already.
|
|
1122
|
+
const open = ((await json('ask').catch(() => null)) ?? []).filter(
|
|
1123
|
+
(q) => q.state === 'open',
|
|
1124
|
+
);
|
|
1125
|
+
const asked = open.filter((q) => !reply.asked.includes(q.id));
|
|
1126
|
+
if (asked.length) {
|
|
1127
|
+
model = { ...model, questions: open };
|
|
1128
|
+
loop.waiting = {
|
|
1129
|
+
kind: 'person',
|
|
1130
|
+
...step,
|
|
1131
|
+
questionIds: asked.map((q) => q.id),
|
|
1132
|
+
reason: `it asked you ${asked.map((q) => q.id).join(', ')} through code-kit`,
|
|
1133
|
+
};
|
|
1134
|
+
$.ui.invalidate('ui.render');
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (!reply.text.trim()) return;
|
|
1138
|
+
const said = await act.sideCall(
|
|
1139
|
+
outcomePrompt({ prompt: sent?.prompt ?? null, answer: reply.text }),
|
|
1140
|
+
);
|
|
1141
|
+
const heard = said.isAnswered ? readOutcome(said.text) : null;
|
|
1142
|
+
// Spec 05's data: what came of the step, once known.
|
|
1143
|
+
const record = sent ? loop.steps.findLast((x) => x.prompt === sent.prompt) : null;
|
|
1144
|
+
if (record) record.outcome = heard?.outcome ?? 'unread';
|
|
1145
|
+
if (heard?.outcome === 'needs-person')
|
|
1146
|
+
loop.waiting = {
|
|
1147
|
+
kind: 'person',
|
|
1148
|
+
...step,
|
|
1149
|
+
question: heard.question,
|
|
1150
|
+
options: heard.options,
|
|
1151
|
+
reason: heard.reason,
|
|
1152
|
+
};
|
|
1153
|
+
else if (heard?.outcome === 'blocked' && sent)
|
|
1154
|
+
loop.waiting = { kind: 'blocked', ...step, reason: heard.reason };
|
|
1155
|
+
if (loop.waiting) $.ui.invalidate('ui.render');
|
|
1156
|
+
},
|
|
1157
|
+
// LOOP-11: the person's choice from the lead's options, or their own words, told to the lead.
|
|
1158
|
+
answerLead: (line) =>
|
|
1159
|
+
act.sendAnswer({ kind: 'lead', question: line.waiting.question }, line.choice),
|
|
1160
|
+
answerLeadTyped: (line, text) =>
|
|
1161
|
+
act.sendAnswer({ kind: 'lead', question: line.waiting.question }, text),
|
|
1162
|
+
answerLeadText: async (line) => {
|
|
1163
|
+
answering = {
|
|
1164
|
+
kind: 'lead',
|
|
1165
|
+
question: line.waiting.question,
|
|
1166
|
+
options: line.waiting.options ?? [],
|
|
1167
|
+
text: '',
|
|
1168
|
+
error: null,
|
|
1169
|
+
aside: { thread: [], draft: '' },
|
|
1170
|
+
};
|
|
1171
|
+
await $.ui.open({ id: ANSWER_ID, title: 'Answer', focus: true, closeOnEscape: true });
|
|
1172
|
+
$.ui.invalidate('ui.render');
|
|
1173
|
+
},
|
|
1174
|
+
// DECIDE-4: a question's answer typed in the dialog, as the person's own words.
|
|
1175
|
+
answerTyped: (line, text) => act.sendAnswer({ kind: 'question', ...line.question }, text),
|
|
1176
|
+
// LOOP-11, LOOP-12: the person asks the lead again for the step that held the loop.
|
|
1177
|
+
askAgain: async () => {
|
|
1178
|
+
const w = loop.waiting;
|
|
1179
|
+
if (!w?.prompt || leadTurn) return;
|
|
1180
|
+
loop.waiting = null;
|
|
1181
|
+
loop.asked = { key: w.key, times: 0 };
|
|
1182
|
+
await act.submit(w.prompt, w.stepKind, w.target, { key: w.key, label: w.label });
|
|
1183
|
+
},
|
|
1184
|
+
// LOOP-11: the person lets the loop go on.
|
|
1185
|
+
carryOn: async () => {
|
|
1186
|
+
loop.waiting = null;
|
|
1187
|
+
loop.lastKey = null;
|
|
1188
|
+
$.ui.invalidate('ui.render');
|
|
1189
|
+
await act.step('resume');
|
|
1190
|
+
},
|
|
1191
|
+
// DECIDE-4: the next decision the person settles with a choice, asked in Claude Code's own question
|
|
1192
|
+
// dialog while the lead is idle. Each is asked once: dismissed, it stays in the band and the
|
|
1193
|
+
// Decisions tab. Never in a session another one runs the loop for, nor over what the person types.
|
|
1194
|
+
offer: async () => {
|
|
1195
|
+
if (offering || leadTurn || loop.sent || !where.interactive) return;
|
|
1196
|
+
if (lease.other && !lease.mine) return;
|
|
1197
|
+
const now = await $.clock.now();
|
|
1198
|
+
const choice = cardsNow(now)
|
|
1199
|
+
.map((card) => ({ card, choice: nativeChoice(card) }))
|
|
1200
|
+
.find((c) => c.choice && !offered.has(c.choice.key));
|
|
1201
|
+
if (!choice) return;
|
|
1202
|
+
const box = await $.prompt.read().catch(() => null);
|
|
1203
|
+
if (box?.text?.trim()) return;
|
|
1204
|
+
offered.add(choice.choice.key);
|
|
1205
|
+
offering = true;
|
|
1206
|
+
const { question, header, options, typed } = choice.choice;
|
|
1207
|
+
const answer = await $.ui
|
|
1208
|
+
.ask(question, { options: options.map((o) => o.label), header })
|
|
1209
|
+
.catch(() => null)
|
|
1210
|
+
.finally(() => {
|
|
1211
|
+
offering = false;
|
|
1212
|
+
});
|
|
1213
|
+
if (answer == null) return;
|
|
1214
|
+
const picked = options.find((o) => o.label === answer);
|
|
1215
|
+
if (picked?.act) await act[picked.act](picked.line);
|
|
1216
|
+
else if (!picked && typed && answer.trim())
|
|
1217
|
+
await act[typed](choice.card.line, answer.trim());
|
|
1218
|
+
act.offer().catch(() => {});
|
|
1219
|
+
},
|
|
1018
1220
|
// LOOP-1, LOOP-5, LOOP-7: with the lead idle and no draft, the step `next` reports.
|
|
1019
1221
|
step: async (trigger) => {
|
|
1020
1222
|
const h = harness();
|
|
@@ -1023,6 +1225,11 @@ export function register(on) {
|
|
|
1023
1225
|
if (trigger === 'tick' && !loop.started && !loop.pending.length) return;
|
|
1024
1226
|
const box = await $.prompt.read().catch(() => null);
|
|
1025
1227
|
if (box?.text?.trim()) return;
|
|
1228
|
+
// LOOP-10: one session in the project runs the loop; in any other, the lead is the person's alone.
|
|
1229
|
+
if (!(await act.lead())) return;
|
|
1230
|
+
// LOOP-11: the lead's last reply first: one that waits on the person holds every step.
|
|
1231
|
+
await act.hear();
|
|
1232
|
+
if (loop.waiting && ['person', 'stopped'].includes(loop.waiting.kind)) return;
|
|
1026
1233
|
if (loop.pending.length) {
|
|
1027
1234
|
const { story, prompt } = loop.pending.shift();
|
|
1028
1235
|
// Until the person has chosen how hands-off the loop is, or under propose, a restart is offered
|
|
@@ -1034,6 +1241,7 @@ export function register(on) {
|
|
|
1034
1241
|
label: `Restart ${story}`,
|
|
1035
1242
|
kind: 'restart',
|
|
1036
1243
|
target: story,
|
|
1244
|
+
key: `restart:${story}`,
|
|
1037
1245
|
first: !chosenYet,
|
|
1038
1246
|
};
|
|
1039
1247
|
$.ui.invalidate('ui.render');
|
|
@@ -1080,7 +1288,19 @@ export function register(on) {
|
|
|
1080
1288
|
loop.asked = { key, times: 0 };
|
|
1081
1289
|
if (loop.proposal?.repeated) loop.proposal = null;
|
|
1082
1290
|
}
|
|
1291
|
+
// LOOP-11, LOOP-12: a step the lead didn't take, or designs left to the person and the lead, hold
|
|
1292
|
+
// that step while it's still next; any other step goes on.
|
|
1293
|
+
if (loop.waiting) {
|
|
1294
|
+
if (loop.waiting.key === key) return;
|
|
1295
|
+
loop.waiting = null;
|
|
1296
|
+
$.ui.invalidate('ui.render');
|
|
1297
|
+
}
|
|
1083
1298
|
if (!prompt) return;
|
|
1299
|
+
if (designHandedOver(step, loop.handedDesign)) {
|
|
1300
|
+
loop.waiting = { kind: 'design', key, label, prompt, stepKind: kind, target };
|
|
1301
|
+
$.ui.invalidate('ui.render');
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1084
1304
|
// Never the same step twice while the project stands still.
|
|
1085
1305
|
const print = await act.fingerprint();
|
|
1086
1306
|
if (key === loop.lastKey && print === loop.lastPrint) return;
|
|
@@ -1090,7 +1310,7 @@ export function register(on) {
|
|
|
1090
1310
|
// moving elsewhere (lanes committing) doesn't make a third prompt useful. The person decides.
|
|
1091
1311
|
if (loop.asked.times >= ASK_LIMIT) {
|
|
1092
1312
|
if (loop.proposal?.label !== label) {
|
|
1093
|
-
loop.proposal = { prompt, label, kind, target, repeated: loop.asked.times };
|
|
1313
|
+
loop.proposal = { prompt, label, kind, target, key, repeated: loop.asked.times };
|
|
1094
1314
|
$.ui.invalidate('ui.render');
|
|
1095
1315
|
}
|
|
1096
1316
|
return;
|
|
@@ -1098,12 +1318,12 @@ export function register(on) {
|
|
|
1098
1318
|
// Until the person has chosen how hands-off the loop is, its first step is offered, not taken.
|
|
1099
1319
|
const chosen = (model.check?.harnessSet ?? []).includes('autonomy');
|
|
1100
1320
|
if (h.autonomy === 'propose' || !chosen) {
|
|
1101
|
-
loop.proposal = { prompt, label, kind, target, first: !chosen };
|
|
1321
|
+
loop.proposal = { prompt, label, kind, target, key, first: !chosen };
|
|
1102
1322
|
$.ui.invalidate('ui.render');
|
|
1103
1323
|
return;
|
|
1104
1324
|
}
|
|
1105
1325
|
loop.asked.times += 1;
|
|
1106
|
-
await act.submit(prompt, kind, target);
|
|
1326
|
+
await act.submit(prompt, kind, target, { key, label });
|
|
1107
1327
|
},
|
|
1108
1328
|
// The first offer's answers: the person's choice of autonomy, recorded as theirs, then acted on.
|
|
1109
1329
|
chooseAutonomy: async (value) => {
|
|
@@ -1146,10 +1366,10 @@ export function register(on) {
|
|
|
1146
1366
|
},
|
|
1147
1367
|
go: async () => {
|
|
1148
1368
|
if (!loop.proposal || leadTurn) return;
|
|
1149
|
-
const { prompt, kind, target, repeated } = loop.proposal;
|
|
1369
|
+
const { prompt, kind, target, key, label, repeated } = loop.proposal;
|
|
1150
1370
|
// The person asked again: once more, then it's theirs to offer again if it still doesn't take.
|
|
1151
1371
|
loop.asked.times = repeated ? ASK_LIMIT : loop.asked.times + 1;
|
|
1152
|
-
await act.submit(prompt, kind, target);
|
|
1372
|
+
await act.submit(prompt, kind, target, { key, label });
|
|
1153
1373
|
},
|
|
1154
1374
|
// LOOP-6: pausing stops new steps at once; a turn in progress finishes.
|
|
1155
1375
|
pause: async () => {
|
|
@@ -1405,6 +1625,9 @@ export function register(on) {
|
|
|
1405
1625
|
if (busy) return;
|
|
1406
1626
|
busy = true;
|
|
1407
1627
|
try {
|
|
1628
|
+
// LOOP-10: the lead session renews its lease; DECIDE-4: with the lead idle, a decision waiting.
|
|
1629
|
+
if (lease.mine) await act.lead();
|
|
1630
|
+
if (!leadTurn) act.offer().catch(() => {});
|
|
1408
1631
|
await act.stalls();
|
|
1409
1632
|
await act.animate();
|
|
1410
1633
|
const now = await act.fingerprint();
|
|
@@ -1629,6 +1852,8 @@ export function register(on) {
|
|
|
1629
1852
|
|
|
1630
1853
|
on('turn.start', async ($, e, next) => {
|
|
1631
1854
|
leadTurn = e.turnId;
|
|
1855
|
+
// LOOP-11: the questions already open, so the loop knows which the lead asked in this turn.
|
|
1856
|
+
turnQuestions = (model.questions ?? []).map((q) => q.id);
|
|
1632
1857
|
$.ui.invalidate('ui.render'); // the lead's lane reads working while its turn runs
|
|
1633
1858
|
if (reviewTurn === 'next') reviewTurn = e.turnId;
|
|
1634
1859
|
return next(e);
|
|
@@ -1650,6 +1875,16 @@ export function register(on) {
|
|
|
1650
1875
|
// as the spike found it must be). The prompt it submits isn't awaited: that turn isn't this hook's.
|
|
1651
1876
|
if (!e.agentId && (!leadTurn || leadTurn === e.turnId)) {
|
|
1652
1877
|
leadTurn = null;
|
|
1878
|
+
// LOOP-11: the lead's reply, read before the loop next prompts it, with the step it answered.
|
|
1879
|
+
lastReply = {
|
|
1880
|
+
turnId: e.turnId,
|
|
1881
|
+
text: e.answer ?? '',
|
|
1882
|
+
aborted: Boolean(e.isAborted),
|
|
1883
|
+
sent: loop.sent,
|
|
1884
|
+
asked: turnQuestions,
|
|
1885
|
+
read: false,
|
|
1886
|
+
};
|
|
1887
|
+
loop.sent = null;
|
|
1653
1888
|
$.ui.invalidate('ui.render');
|
|
1654
1889
|
// What the person asked for goes first, whatever the loop's state.
|
|
1655
1890
|
if (personAsks.length) {
|
|
@@ -1658,15 +1893,47 @@ export function register(on) {
|
|
|
1658
1893
|
return res;
|
|
1659
1894
|
}
|
|
1660
1895
|
await act?.step('turn').catch(() => {});
|
|
1896
|
+
// DECIDE-4: a decision waiting, asked in Claude Code's own dialog; not awaited, the person answers.
|
|
1897
|
+
act?.offer().catch(() => {});
|
|
1661
1898
|
}
|
|
1662
1899
|
return res;
|
|
1663
1900
|
});
|
|
1664
1901
|
|
|
1902
|
+
// LOOP-11: the person speaking to the lead answers what the loop waited on, whatever they said; the
|
|
1903
|
+
// loop reads the lead's reply to them before it prompts again. Designs stay theirs (LOOP-12).
|
|
1904
|
+
on('prompt.submit', async ($, e, next) => {
|
|
1905
|
+
if (['composer', 'bridge'].includes(e.origin?.kind) && loop.waiting?.kind !== 'design') {
|
|
1906
|
+
if (loop.waiting) $.ui.invalidate('ui.render');
|
|
1907
|
+
loop.waiting = null;
|
|
1908
|
+
loop.asked = { key: null, times: 0 };
|
|
1909
|
+
}
|
|
1910
|
+
return next(e);
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// LOOP-10: the lead session ends: the next session to act takes the loop at once.
|
|
1914
|
+
on('session.end', async ($, e, next) => {
|
|
1915
|
+
if (lease.mine && where) {
|
|
1916
|
+
lease.mine = false;
|
|
1917
|
+
await $.process
|
|
1918
|
+
.run(['node', where.cli, 'loop', 'release', '--session', where.session], { cwd: where.cwd })
|
|
1919
|
+
.catch(() => {});
|
|
1920
|
+
}
|
|
1921
|
+
return next(e);
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1665
1924
|
// What the pane and band show of the loop.
|
|
1666
1925
|
const loopNow = (now) => {
|
|
1667
1926
|
const h = harness();
|
|
1668
1927
|
const state =
|
|
1669
|
-
h.autonomy === 'off'
|
|
1928
|
+
h.autonomy === 'off'
|
|
1929
|
+
? 'off'
|
|
1930
|
+
: lease.other && !lease.mine
|
|
1931
|
+
? 'elsewhere'
|
|
1932
|
+
: loop.done
|
|
1933
|
+
? 'done'
|
|
1934
|
+
: loop.paused
|
|
1935
|
+
? 'paused'
|
|
1936
|
+
: 'running';
|
|
1670
1937
|
const laneNotes = {};
|
|
1671
1938
|
for (const a of activity.values()) {
|
|
1672
1939
|
if (!a.lane) continue;
|
|
@@ -1684,6 +1951,7 @@ export function register(on) {
|
|
|
1684
1951
|
started: loop.started,
|
|
1685
1952
|
last: last ? `${last.kind} ${last.target}` : null,
|
|
1686
1953
|
proposal: loop.proposal,
|
|
1954
|
+
waiting: loop.waiting,
|
|
1687
1955
|
flagged: [...flagged.values()],
|
|
1688
1956
|
doneCount: loop.done?.count ?? 0,
|
|
1689
1957
|
steps: loop.steps,
|
|
@@ -1820,6 +2088,7 @@ export function register(on) {
|
|
|
1820
2088
|
flag: (t) => `${t} stalled again after its restarts, so it came to you.`,
|
|
1821
2089
|
pause: () => 'You paused the loop.',
|
|
1822
2090
|
resume: () => 'You resumed the loop.',
|
|
2091
|
+
lead: () => 'You moved the loop to this session.',
|
|
1823
2092
|
done: (t) => `Every story was done (${t}).`,
|
|
1824
2093
|
};
|
|
1825
2094
|
const journalNow = () => {
|
|
@@ -1829,7 +2098,7 @@ export function register(on) {
|
|
|
1829
2098
|
.map((x) => ({
|
|
1830
2099
|
at: x.at,
|
|
1831
2100
|
kind: 'loop',
|
|
1832
|
-
who: ['pause', 'resume'].includes(x.kind) ? 'person' : 'lead',
|
|
2101
|
+
who: ['pause', 'resume', 'lead'].includes(x.kind) ? 'person' : 'lead',
|
|
1833
2102
|
text: STEP_WORDS[x.kind](x.target),
|
|
1834
2103
|
}));
|
|
1835
2104
|
return [...recorded, ...steps].sort((a, b) => a.at - b.at);
|