@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.
@@ -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
- if ((await json('loop').catch(() => null))?.paused)
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
- : `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.`,
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
- submit: async (prompt, kind, target) => {
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' ? 'off' : loop.done ? 'done' : loop.paused ? 'paused' : 'running';
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);