@warren-dean/code-kit 0.12.2 → 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.
@@ -75,7 +75,7 @@ export function leadPrompt(step, { cli = 'code-kit' } = {}) {
75
75
  // MQ-2: the head of the merge queue, where the lead merges within the delegated rules.
76
76
  if (step.step === 'merge')
77
77
  return ids.length
78
- ? `Merge ${ids[0]}, next in the merge queue: run node "${cli}" queue merge --delegated. It verifies the branch against the base as it is now, and sends it back to its lane if it conflicts or fails.`
78
+ ? `Merge ${ids[0]}, next in the merge queue: run node "${cli}" queue merge --delegated. It verifies the branch against the base as it is now, and sends it back to its lane if it conflicts or fails; a branch that moved after its review leaves the queue for you to review again.`
79
79
  : null;
80
80
  // SETUP-2: the base has no rules yet; the lead puts the approved ones there, then merges as usual.
81
81
  if (step.step === 'rules')
@@ -148,3 +148,77 @@ export function restartPrompt({ type, id, story, minutes }) {
148
148
  /** The message a quiet agent gets (LOOP-3). */
149
149
  export const nudgeText = (minutes) =>
150
150
  `No tool call for ${minutes} minutes: report where you are, or carry on.`;
151
+
152
+ /** LOOP-11: what a lead turn left the build at, as the loop reads the lead's reply. */
153
+ export const OUTCOMES = new Set(['done', 'started', 'needs-person', 'blocked']);
154
+ const ANSWER_SHOWN = 4000;
155
+
156
+ /**
157
+ * LOOP-11: the side call that reads the lead's last reply before the loop prompts it again: over the
158
+ * lead's own conversation (so it knows the whole exchange), with the reply itself for a call that
159
+ * can't fork. `prompt` is the loop's own prompt for that turn, or null when the person started it.
160
+ */
161
+ export function outcomePrompt({ prompt = null, answer = '' }) {
162
+ const reply = String(answer ?? '').trim();
163
+ const shown =
164
+ reply.length > ANSWER_SHOWN ? `${reply.slice(0, ANSWER_SHOWN)}\n[… cut short]` : reply;
165
+ return [
166
+ prompt
167
+ ? `code-kit's lead loop prompted you: "${prompt}"`
168
+ : "The person prompted you, and code-kit's lead loop is about to prompt you with the build's next step.",
169
+ `Your reply ended your turn. It read:\n"""\n${shown}\n"""`,
170
+ 'Say where that reply left things, so the loop knows whether to prompt you again. Answer with one JSON object and nothing else:',
171
+ '{"outcome": "done" | "started" | "needs-person" | "blocked", "question": "…", "options": [{"label": "…", "effect": "…"}], "reason": "…"}',
172
+ '- done: you finished what was asked, or there was nothing to do.',
173
+ '- started: you started work that carries on without you, such as lane agents.',
174
+ "- needs-person: you asked the person something, or wait on their answer, approval or feedback before you go on. question: what you wait on, as one question; options: 2 to 4 answers they could give (a label of at most five words, and what it does), or [] when it's theirs to put in words.",
175
+ "- blocked: you didn't take the step, for a reason no answer from the person settles now. reason: why, in a sentence.",
176
+ ].join('\n');
177
+ }
178
+
179
+ /**
180
+ * LOOP-11: the side call's answer: { outcome, question, options: [{ label, effect }], reason }, or
181
+ * null when it isn't one (the loop then goes on as before).
182
+ */
183
+ export function readOutcome(text) {
184
+ const raw = String(text ?? '');
185
+ const json = raw.slice(raw.indexOf('{'), raw.lastIndexOf('}') + 1);
186
+ let parsed;
187
+ try {
188
+ parsed = JSON.parse(json);
189
+ } catch {
190
+ return null;
191
+ }
192
+ if (!parsed || !OUTCOMES.has(parsed.outcome)) return null;
193
+ const words = (s, n) =>
194
+ typeof s === 'string' && s.trim() ? s.replace(/\s+/g, ' ').trim().slice(0, n) : null;
195
+ const options = (Array.isArray(parsed.options) ? parsed.options : [])
196
+ .map((o) => ({ label: words(o?.label, 40), effect: words(o?.effect, 200) }))
197
+ .filter((o) => o.label)
198
+ .slice(0, 4);
199
+ return {
200
+ outcome: parsed.outcome,
201
+ question: words(parsed.question, 400),
202
+ options: options.length >= 2 ? options : [],
203
+ reason: words(parsed.reason, 300),
204
+ };
205
+ }
206
+
207
+ /**
208
+ * LOOP-12: a design is made with the person, so the loop hands a design story to the lead once; while
209
+ * the stories `next` names have all been handed over, it leaves them to the person and the lead.
210
+ */
211
+ export const designHandedOver = (step, handed) =>
212
+ step?.step === 'design' &&
213
+ (step.args ?? '')
214
+ .split(/\s+/)
215
+ .filter(Boolean)
216
+ .every((id) => handed.has(id));
217
+
218
+ /** LOOP-11: the lead's prompt with the person's answer to what it waited on. */
219
+ export const answeredPrompt = (question, answer) => {
220
+ const said = /[.!?]$/.test(answer) ? answer : `${answer}.`;
221
+ return question
222
+ ? `The person answered what you asked ("${question}"): ${said} Carry on with it.`
223
+ : `The person says: ${said} Carry on with it.`;
224
+ };
@@ -50,7 +50,7 @@ export function missionLines({
50
50
  }) {
51
51
  if (state?.kind !== 'ok') return [];
52
52
  const working = state.lanes.filter((l) => l.name !== 'lead' && l.active && live[l.name]);
53
- const loopOn = Boolean(loop?.started) && !['off', 'stopped'].includes(loop?.state);
53
+ const loopOn = Boolean(loop?.started) && !['off', 'stopped', 'elsewhere'].includes(loop?.state);
54
54
  if (!leadBusy && !working.length && !loopOn) return [];
55
55
  const rows = [];
56
56
  const goal = goalOf(state.stories);
@@ -64,25 +64,29 @@ export function missionLines({
64
64
  ? `lead ${live.lead?.tool ? `at work: ${live.lead.tool}` : 'at work'}`
65
65
  : 'lead idle';
66
66
  const loopWord =
67
- loop?.state === 'paused'
68
- ? 'loop paused'
69
- : loop?.state === 'off'
70
- ? 'loop off'
71
- : loop?.state === 'done'
72
- ? 'milestone done'
73
- : loopOn
74
- ? `loop ${loop.autonomy === 'propose' ? 'proposing' : 'on'}${loop.last ? ` · last: ${loop.last}` : ''}`
75
- : null;
67
+ loop?.state === 'elsewhere'
68
+ ? 'loop in another session'
69
+ : loop?.state === 'paused'
70
+ ? 'loop paused'
71
+ : loop?.state === 'off'
72
+ ? 'loop off'
73
+ : loop?.state === 'done'
74
+ ? 'milestone done'
75
+ : loopOn
76
+ ? `loop ${loop.autonomy === 'propose' ? 'proposing' : 'on'}${loop.last ? ` · last: ${loop.last}` : ''}`
77
+ : null;
76
78
  rows.push({
77
79
  kind: 'now',
78
80
  label: 'now',
79
81
  text: [lead, loopWord].filter(Boolean).join(' · '),
80
82
  actions:
81
- loop?.state === 'paused'
82
- ? [{ id: 'resume', label: 'Resume' }]
83
- : loopOn && loop?.state === 'running'
84
- ? [{ id: 'pause', label: 'Pause' }]
85
- : [],
83
+ loop?.state === 'elsewhere'
84
+ ? [{ id: 'leadHere', label: 'Lead from here' }]
85
+ : loop?.state === 'paused'
86
+ ? [{ id: 'resume', label: 'Resume' }]
87
+ : loopOn && loop?.state === 'running'
88
+ ? [{ id: 'pause', label: 'Pause' }]
89
+ : [],
86
90
  });
87
91
  const next = nextText(nextStep);
88
92
  if (next && loop?.state !== 'done') rows.push({ kind: 'next', label: 'next', text: next });
@@ -1357,7 +1357,17 @@ export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
1357
1357
  paddingX: 1,
1358
1358
  rowGap: 0,
1359
1359
  children: [
1360
- text(card.headline, { bold: true, wrap: 'wrap' }),
1360
+ text(
1361
+ card.kind === 'brief'
1362
+ ? 'An open question in your brief'
1363
+ : card.kind === 'question'
1364
+ ? 'A question from the lead'
1365
+ : card.headline,
1366
+ { bold: true, wrap: 'wrap' },
1367
+ ),
1368
+ ...(card.details ?? []).map((d) =>
1369
+ row(d.label, d.value, d.label === 'Question' ? { bold: true } : {}),
1370
+ ),
1361
1371
  ...(card.asked ? [row('You asked for', card.asked)] : []),
1362
1372
  ...(card.story
1363
1373
  ? [