@warren-dean/code-kit 0.21.0 → 0.22.1

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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +20 -0
  4. package/README.md +46 -23
  5. package/bin/code-kit.mjs +271 -21
  6. package/hooks/guard-agent.mjs +36 -0
  7. package/hooks/guard-bash.mjs +23 -11
  8. package/hooks/guard-paths.mjs +11 -0
  9. package/hooks/hooks.json +20 -0
  10. package/hooks/lane-audit.mjs +38 -1
  11. package/hooks/lib/adapters/hub/client.mjs +177 -25
  12. package/hooks/lib/config.mjs +148 -5
  13. package/hooks/lib/decisionstore.mjs +89 -0
  14. package/hooks/lib/harness.mjs +7 -0
  15. package/hooks/lib/hook.mjs +7 -0
  16. package/hooks/lib/instructions.mjs +1 -1
  17. package/hooks/lib/laneclaims.mjs +89 -0
  18. package/hooks/lib/loop-state.mjs +20 -2
  19. package/hooks/lib/next.mjs +34 -1
  20. package/hooks/lib/plan.mjs +3 -1
  21. package/hooks/lib/questions.mjs +23 -5
  22. package/hooks/lib/regression.mjs +2 -1
  23. package/hooks/lib/rules.mjs +18 -12
  24. package/hooks/lib/schedule.mjs +86 -0
  25. package/hooks/lib/sessions.mjs +223 -0
  26. package/hooks/lib/snapshot.mjs +9 -3
  27. package/hooks/mod/decisions.mjs +1 -28
  28. package/hooks/mod/loop.mjs +8 -0
  29. package/hooks/mod/mission.mjs +21 -19
  30. package/hooks/mod/register.mjs +118 -41
  31. package/hooks/mod/register.test.ts +244 -73
  32. package/hooks/mod/view.mjs +98 -47
  33. package/hooks/post-edit-check.mjs +3 -0
  34. package/hooks/session.mjs +24 -0
  35. package/hooks/stop-check.mjs +12 -0
  36. package/package.json +1 -1
  37. package/skills/decide/SKILL.md +1 -1
  38. package/skills/dispatch/SKILL.md +2 -2
  39. package/skills/init/SKILL.md +1 -1
  40. package/skills/lead/SKILL.md +28 -0
  41. package/skills/next/SKILL.md +1 -1
  42. package/skills/spec-design/SKILL.md +2 -1
  43. package/skills/team-init/SKILL.md +1 -1
  44. package/templates/claude-md.md +3 -1
  45. package/templates/plan.md +2 -1
@@ -0,0 +1,223 @@
1
+ // Sessions as members of the team (docs/specs/23-the-team.md, SESS-1 to SESS-5). Several Claude Code
2
+ // sessions can work one project at once; each is a member, the person and the session, as spec 16 makes
3
+ // each developer one. One is the lead session (the loop runs there, dispatches start from it); the rest
4
+ // are working sessions, which stay quiet and record an ad hoc task, with the paths it leases, before
5
+ // they change code. A session's id is Claude Code's: `session_id` in hooks, `$.session.id()` in the mod,
6
+ // `CLAUDE_CODE_SESSION_ID` in every command an agent runs. Kept in the main checkout, one file per session,
7
+ // so every session and worktree on the machine reads the same: this is the local provider of SESS-1.
8
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
9
+ import { userInfo } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { matchesAny } from './glob.mjs';
12
+ import { CLI, projectWorktrees } from './rules.mjs';
13
+ import { shortSession } from './laneclaims.mjs';
14
+
15
+ export { shortSession };
16
+ export const SESSIONS_DIR = '.claude/state/sessions';
17
+ /** A session seen within this is live; one not seen for longer has ended, whether or not it said so. */
18
+ export const SESSION_LIVE_MS = 15 * 60_000;
19
+ /** Hooks note that a session is still there at most this often. */
20
+ const SEEN_EVERY_MS = 60_000;
21
+ /** The uncommitted files a session wrote, kept for SESS-5: at most this many. */
22
+ const WROTE_KEPT = 500;
23
+
24
+ const main = (root) => projectWorktrees(root)[0] ?? root;
25
+ const dirOf = (root) => join(main(root), SESSIONS_DIR);
26
+ const safe = (id) => String(id).replace(/[^\w.-]/g, '_');
27
+ const fileOf = (root, id) => join(dirOf(root), `${safe(id)}.json`);
28
+
29
+ /** The person at this machine: their GitHub login if code-kit has it cached, or the OS user. */
30
+ export function personHere(root) {
31
+ try {
32
+ const cached = JSON.parse(readFileSync(join(main(root), '.claude/state/whoami.json'), 'utf8'));
33
+ if (cached?.login) return cached.login;
34
+ } catch {
35
+ // not cached: the OS user
36
+ }
37
+ try {
38
+ return userInfo().username || null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /** SESS-1: who a claim, approval, question or decision belongs to: `<person>/<short id>`. */
45
+ export const memberOf = (record) =>
46
+ record ? `${record.person ?? 'someone'}/${shortSession(record.id)}` : null;
47
+
48
+ export function readSession(root, id) {
49
+ if (!id) return null;
50
+ try {
51
+ return JSON.parse(readFileSync(fileOf(root, id), 'utf8'));
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function write(root, record) {
58
+ mkdirSync(dirOf(root), { recursive: true });
59
+ writeFileSync(fileOf(root, record.id), `${JSON.stringify(record)}\n`);
60
+ return record;
61
+ }
62
+
63
+ const isLive = (r, now) => !r.ended && now - Date.parse(r.seen ?? r.started) < SESSION_LIVE_MS;
64
+
65
+ /** Every session the project has seen: [{ ...record, live }], the newest first. */
66
+ export function sessions(root, now = Date.now()) {
67
+ let files = [];
68
+ try {
69
+ files = readdirSync(dirOf(root)).filter((f) => f.endsWith('.json'));
70
+ } catch {
71
+ return [];
72
+ }
73
+ return files
74
+ .flatMap((f) => {
75
+ try {
76
+ const r = JSON.parse(readFileSync(join(dirOf(root), f), 'utf8'));
77
+ return r?.id ? [{ ...r, live: isLive(r, now) }] : [];
78
+ } catch {
79
+ return [];
80
+ }
81
+ })
82
+ .sort((a, b) => Date.parse(b.started) - Date.parse(a.started));
83
+ }
84
+
85
+ /** SESS-2: the live lead session, or null. */
86
+ export const leadSession = (root, now = Date.now()) =>
87
+ sessions(root, now).find((s) => s.live && s.role === 'lead') ?? null;
88
+
89
+ /**
90
+ * SESS-2: a session starting (or resumed). It's the lead when no other live session is; otherwise a
91
+ * working session. A resumed session keeps the role it had, unless it was the lead and another session
92
+ * has led since.
93
+ */
94
+ export function startSession(root, id, now = Date.now()) {
95
+ const at = new Date(now).toISOString();
96
+ const had = readSession(root, id);
97
+ const lead = leadSession(root, now);
98
+ return write(root, {
99
+ id,
100
+ person: had?.person ?? personHere(root),
101
+ role: lead && lead.id !== id ? 'working' : 'lead',
102
+ started: had?.started ?? at,
103
+ seen: at,
104
+ ended: null,
105
+ task: had?.task ?? null,
106
+ wrote: had?.wrote ?? [],
107
+ });
108
+ }
109
+
110
+ /** A hook saw the session act: it's still there. A session code-kit hadn't seen starts here. */
111
+ export function seeSession(root, id, now = Date.now()) {
112
+ if (!id) return null;
113
+ const had = readSession(root, id);
114
+ if (!had || had.ended) return startSession(root, id, now);
115
+ // A lead that went quiet past SESSION_LIVE_MS, while another session took the lead, works now.
116
+ const lead = had.role === 'lead' ? leadSession(root, now) : null;
117
+ const demoted = lead && lead.id !== id;
118
+ if (!demoted && now - Date.parse(had.seen) < SEEN_EVERY_MS) return had;
119
+ return write(root, {
120
+ ...had,
121
+ seen: new Date(now).toISOString(),
122
+ ...(demoted ? { role: 'working' } : {}),
123
+ });
124
+ }
125
+
126
+ /** The session ended: it's no longer live, and leads nothing. Its task stays, stale, for SESS-4. */
127
+ export function endSession(root, id, now = Date.now()) {
128
+ const had = readSession(root, id);
129
+ if (!had) return null;
130
+ return write(root, { ...had, ended: new Date(now).toISOString() });
131
+ }
132
+
133
+ /** SESS-2: the person chose this session to lead: it does, and the session that led becomes a working one. */
134
+ export function makeLead(root, id, now = Date.now()) {
135
+ for (const s of sessions(root, now))
136
+ if (s.role === 'lead' && s.id !== id) write(root, { ...strip(s), role: 'working' });
137
+ const had = readSession(root, id) ?? startSession(root, id, now);
138
+ return write(root, { ...had, role: 'lead', seen: new Date(now).toISOString(), ended: null });
139
+ }
140
+
141
+ const strip = ({ live: _live, ...r }) => r;
142
+
143
+ /** SESS-4: the session's task: { title, paths, branch, started }, or null to end it. */
144
+ export function setTask(root, id, task, now = Date.now()) {
145
+ const had = readSession(root, id) ?? startSession(root, id, now);
146
+ return write(root, {
147
+ ...had,
148
+ task: task ? { ...task, started: had.task?.started ?? new Date(now).toISOString() } : null,
149
+ });
150
+ }
151
+
152
+ /**
153
+ * SESS-4: `taker` takes over the task of the session `from` (by its id or short id): the task, and its
154
+ * decisions with it (OWN-4), move to the session running the command. { ok, task, from } or { ok: false, why }.
155
+ */
156
+ export function takeTask(root, from, taker, now = Date.now()) {
157
+ const source = sessions(root, now).find((s) => s.id === from || shortSession(s.id) === from);
158
+ if (!source?.task) return { ok: false, why: `No session ${from} has a task.` };
159
+ if (source.id === taker) return { ok: false, why: 'That task is already this session’s.' };
160
+ if (source.live)
161
+ return {
162
+ ok: false,
163
+ why: `Session ${shortSession(source.id)} is still working on it; ask it to finish with \`code-kit task done\` first.`,
164
+ };
165
+ write(root, { ...strip(source), task: null });
166
+ const took = setTask(root, taker, source.task, now);
167
+ // OWN-4: the task's decisions come with it.
168
+ write(root, {
169
+ ...took,
170
+ took: [...new Set([...(took.took ?? []), source.id, ...(source.took ?? [])])],
171
+ });
172
+ return { ok: true, task: source.task, from: source };
173
+ }
174
+
175
+ /**
176
+ * SESS-4: why `session` may not write `rel`, because another live session's task leases it, or null.
177
+ * A stale task (its session has ended) leases nothing.
178
+ */
179
+ export function taskRefusal(root, session, rel, now = Date.now()) {
180
+ const other = sessions(root, now).find(
181
+ (s) => s.live && s.id !== session && s.task?.paths?.length && matchesAny(rel, s.task.paths),
182
+ );
183
+ if (!other) return null;
184
+ return `${rel} is leased by another session (${memberOf(other)}), working on "${other.task.title}". Leave it to that session, wait until it finishes (\`code-kit task done\` there), or work on something else.`;
185
+ }
186
+
187
+ /** SESS-5: a file a session wrote in a checkout, kept while it may still be uncommitted. */
188
+ export function noteWrite(root, id, rel, now = Date.now()) {
189
+ if (!id || !rel) return;
190
+ const had = readSession(root, id) ?? startSession(root, id, now);
191
+ const wrote = [rel, ...(had.wrote ?? []).filter((f) => f !== rel)].slice(0, WROTE_KEPT);
192
+ if (wrote.join('\n') === (had.wrote ?? []).join('\n')) return;
193
+ write(root, { ...had, wrote });
194
+ }
195
+
196
+ /** SESS-5: the sessions, other than `session`, that wrote each of `files`: { file: [record] }. */
197
+ export function writersOf(root, files, session = null, now = Date.now()) {
198
+ const all = sessions(root, now).filter((s) => s.id !== session);
199
+ return Object.fromEntries(
200
+ files
201
+ .map((f) => [f, all.filter((s) => (s.wrote ?? []).includes(f))])
202
+ .filter(([, w]) => w.length),
203
+ );
204
+ }
205
+
206
+ /**
207
+ * SESS-4, SESS-5: why this write may not happen because of the sessions working the project, or null.
208
+ * Another live session's task leases the path; or this is a working session's own main agent, which
209
+ * writes in a worktree of its own (the main checkout is every session's) and records its task first.
210
+ * code-kit's state, and paths outside the project, are never held back.
211
+ */
212
+ export function sessionWriteProblem({ project, root, rel, actor, session, now = Date.now() }) {
213
+ if (!session || !rel || rel.startsWith('..') || rel.startsWith('.claude/state/')) return null;
214
+ const leased = taskRefusal(project, session, rel, now);
215
+ if (leased) return leased;
216
+ const me = readSession(project, session);
217
+ if (actor?.kind !== 'lead' || me?.role !== 'working') return null;
218
+ if (root === main(project))
219
+ return `this is a working session (another session leads the project), and ${rel} is in the main checkout, which every session shares: your uncommitted files there would block the other sessions' git. Work in a worktree of your own: start the session with \`claude --worktree\`, or move it into one (EnterWorktree), then record your task and go on.`;
220
+ if (!me.task)
221
+ return `this is a working session (another session leads the project): record what you're doing before you change code, so the other sessions see it and leave your files alone:\n node "${CLI}" task "<what you're doing, in a line>" --touches "<globs of the paths you'll change>"`;
222
+ return null;
223
+ }
@@ -24,8 +24,10 @@ import {
24
24
  sourceOf,
25
25
  } from './standards.mjs';
26
26
 
27
- /** 2 adds the project's standards (code-kit spec 22). */
28
- export const SNAPSHOT_VERSION = 2;
27
+ import { slicesOf } from './schedule.mjs';
28
+
29
+ /** 2 adds the project's standards (code-kit spec 22); 3 the plan's slices (spec 23, SCHED-2). */
30
+ export const SNAPSHOT_VERSION = 3;
29
31
 
30
32
  /** A story's stage, as a report names it. */
31
33
  export const STAGES = [
@@ -52,6 +54,8 @@ export const SNAPSHOT_SCHEMA = {
52
54
  version: 'count',
53
55
  project: { repository: 'name?', base: 'name', codeKit: 'name', at: 'ms' },
54
56
  milestones: [{ name: 'name', target: 'date?', stories: ['id'] }],
57
+ // SCHED-2: each slice in plan order, whether it's open or done, and its stories. Added in version 3.
58
+ slices: [{ name: 'name', open: 'bool', done: 'bool', stories: ['id'] }],
55
59
  stories: [
56
60
  {
57
61
  id: 'id',
@@ -117,7 +121,7 @@ export const SNAPSHOT_SCHEMA = {
117
121
  };
118
122
 
119
123
  /** Parts a report from an older code-kit doesn't have yet: valid when missing, checked when there. */
120
- export const LATER_PARTS = ['standards'];
124
+ export const LATER_PARTS = ['standards', 'slices'];
121
125
 
122
126
  const ONE_LINE = (max) => (v) => typeof v === 'string' && v.length <= max && !/[\r\n]/.test(v);
123
127
  const KINDS = {
@@ -170,6 +174,7 @@ export function snapshotHash(snapshot, parts = null) {
170
174
  /** The parts whose change is sent at once (HUBR-3); usage alone waits for the ten minutes. */
171
175
  export const EVENT_PARTS = [
172
176
  'milestones',
177
+ 'slices',
173
178
  'stories',
174
179
  'sendBacks',
175
180
  'requirements',
@@ -363,6 +368,7 @@ export function buildSnapshot({
363
368
  at: now,
364
369
  },
365
370
  milestones,
371
+ slices: slicesOf(status.stories).map((sl) => ({ ...sl, name: line(sl.name).slice(0, 120) })),
366
372
  stories,
367
373
  sendBacks: sendBackList,
368
374
  requirements,
@@ -8,7 +8,6 @@ import { approvalLabel, askWords, commandItself } from './view.mjs';
8
8
  export const DECISION_KINDS = new Set([
9
9
  'held',
10
10
  'approvals',
11
- 'stalled',
12
11
  'proposal',
13
12
  'queue',
14
13
  'review',
@@ -20,7 +19,6 @@ export const DECISION_KINDS = new Set([
20
19
  'teamDrift',
21
20
  'question',
22
21
  'brief',
23
- 'denied',
24
22
  'report',
25
23
  'waiting',
26
24
  ]);
@@ -51,10 +49,6 @@ const EFFECT = {
51
49
  answer: 'Type your answer; the lead is told it and carries on.',
52
50
  answerBrief: "Type your answer; the lead records it in the brief's decisions and builds on it.",
53
51
  laterBrief: 'It waits; it comes back next session.',
54
- runDenied:
55
- 'Puts it in your prompt after !, so you run it yourself; press Enter to run it, then Done it.',
56
- deniedDone: 'The lead is told you ran it, or allowed it, and carries on.',
57
- deniedNo: "The lead is told you won't run it now, and carries on with other work.",
58
52
  answerLead: 'The lead is told your answer and carries on.',
59
53
  answerLeadText: 'Type your answer; the lead is told it and carries on.',
60
54
  askAgain: 'The lead is asked to take this step again now.',
@@ -93,17 +87,6 @@ function eachDecision(line) {
93
87
  text: `From your brief: ${b.question}`,
94
88
  brief: b,
95
89
  }));
96
- if (line.kind === 'denied')
97
- return (line.denials ?? [line.denial]).map((d) => ({
98
- ...line,
99
- text: `Claude Code refused ${d.who}'s ${d.what}`,
100
- denial: d,
101
- actions: [
102
- ...(d.command ? [{ id: 'runDenied', label: 'Put it in the prompt' }] : []),
103
- { id: 'deniedDone', label: 'Done it' },
104
- { id: 'deniedNo', label: 'Not now' },
105
- ],
106
- }));
107
90
  if (line.kind === 'report')
108
91
  return (line.reports ?? [line.report]).map((r) => ({ ...line, text: r.title, report: r }));
109
92
  return [line];
@@ -164,7 +147,6 @@ const TAG = {
164
147
  notice: 'notice',
165
148
  question: 'question',
166
149
  brief: 'your brief',
167
- denied: 'refused',
168
150
  report: 'bug report',
169
151
  waiting: 'the lead',
170
152
  };
@@ -211,11 +193,6 @@ function headlineOf(line, story) {
211
193
  headline: line.brief.question,
212
194
  sub: `open in ${line.brief.path}${line.brief.blocks ? ` · holds up ${line.brief.blocks}` : ''}`,
213
195
  };
214
- if (line.kind === 'denied')
215
- return {
216
- headline: `Claude Code refused ${line.denial.who}'s call`,
217
- sub: clip(line.denial.command ?? line.denial.what, 80),
218
- };
219
196
  if (line.kind === 'report')
220
197
  return { headline: line.report.title, sub: "drafted by an agent, for code-kit's GitHub" };
221
198
  if (line.kind === 'waiting' && line.waiting.question)
@@ -261,8 +238,6 @@ function whyOf(line, check) {
261
238
  );
262
239
  if (line.kind === 'brief')
263
240
  return `It's an open question in ${line.brief.path}, left for you: what gets built depends on your answer. Not sure what it means? Ask about it when you answer.`;
264
- if (line.kind === 'denied')
265
- return `Claude Code's own permission check refused it${line.denial.reason ? ` (${line.denial.reason})` : ''}, which code-kit can't approve past: only you can run it or allow it.`;
266
241
  if (line.kind === 'waiting' && line.waiting.kind === 'person')
267
242
  return "The lead's last reply waits on you, so the loop doesn't prompt it again until you answer, here or in the chat.";
268
243
  if (line.kind === 'waiting' && line.waiting.kind === 'stopped')
@@ -391,7 +366,7 @@ export function contextOf(card) {
391
366
  }
392
367
 
393
368
  /** DECIDE-4: the kinds of decision settled with a choice, asked in Claude Code's own question dialog. */
394
- export const NATIVE_KINDS = new Set(['proposal', 'stalled', 'teamStale', 'question', 'waiting']);
369
+ export const NATIVE_KINDS = new Set(['proposal', 'teamStale', 'question', 'waiting']);
395
370
 
396
371
  /** One sentence ended as a question asks. */
397
372
  const asked = (text, question) => `${String(text).replace(/[.\s]+$/, '')}. ${question}`;
@@ -461,8 +436,6 @@ const NOTIFY_TITLE = {
461
436
  question: 'the lead asks you',
462
437
  brief: 'a question in your brief',
463
438
  waiting: 'the lead waits on you',
464
- stalled: 'a lane stalled',
465
- denied: 'Claude Code refused a call',
466
439
  proposal: 'a step to approve',
467
440
  queue: 'a merge for you',
468
441
  };
@@ -218,6 +218,14 @@ export function restartPrompt({ type, id, story, minutes }) {
218
218
  return `${type} (agent ${id}) on ${story} has made no tool call for ${minutes} minutes. Stop it with TaskStop, then dispatch ${story} again with the same brief on the same branch: /code-kit:dispatch ${story}`;
219
219
  }
220
220
 
221
+ /**
222
+ * WHO-4: a lane stalled past its restarts is the lead's to sort out, not the person's: what it tried,
223
+ * and the lead's three ways on (a narrower brief, smaller stories, or the story handed back).
224
+ */
225
+ export function stalledPrompt({ type, id, lane, story, restarts, last = null }) {
226
+ return `${type} (agent ${id}) on ${story} has stalled again after ${restarts} restart${restarts === 1 ? '' : 's'}${last ? `; its last call was ${last}` : ''}. Stop it with TaskStop, read its report and what it committed on its branch, then decide: dispatch ${story} again with a brief that names what it got stuck on, split ${story} into smaller stories in the plan, or hand it back to the ${lane} lane's queue with the reason. Ask the person only if the story needs a product decision.`;
227
+ }
228
+
221
229
  /** The message a quiet agent gets (LOOP-3). */
222
230
  export const nudgeText = (minutes) =>
223
231
  `No tool call for ${minutes} minutes: report where you are, or carry on.`;
@@ -50,7 +50,8 @@ 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', 'elsewhere'].includes(loop?.state);
53
+ const away = ['elsewhere', 'unled'].includes(loop?.state);
54
+ const loopOn = Boolean(loop?.started) && !['off', 'stopped'].includes(loop?.state) && !away;
54
55
  if (!leadBusy && !working.length && !loopOn) return [];
55
56
  const rows = [];
56
57
  const goal = goalOf(state.stories);
@@ -65,28 +66,29 @@ export function missionLines({
65
66
  : 'lead idle';
66
67
  const loopWord =
67
68
  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;
69
+ ? 'the lead is in another session'
70
+ : loop?.state === 'unled'
71
+ ? 'no session leads the project'
72
+ : loop?.state === 'paused'
73
+ ? 'loop paused'
74
+ : loop?.state === 'off'
75
+ ? 'loop off'
76
+ : loop?.state === 'done'
77
+ ? 'milestone done'
78
+ : loopOn
79
+ ? `loop ${loop.autonomy === 'propose' ? 'proposing' : 'on'}${loop.last ? ` · last: ${loop.last}` : ''}`
80
+ : null;
78
81
  rows.push({
79
82
  kind: 'now',
80
83
  label: 'now',
81
84
  text: [lead, loopWord].filter(Boolean).join(' · '),
82
- actions:
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
- : [],
85
+ actions: away
86
+ ? [{ id: 'leadHere', label: 'Lead from here' }]
87
+ : loop?.state === 'paused'
88
+ ? [{ id: 'resume', label: 'Resume' }]
89
+ : loopOn && loop?.state === 'running'
90
+ ? [{ id: 'pause', label: 'Pause' }]
91
+ : [],
90
92
  });
91
93
  const next = nextText(nextStep);
92
94
  if (next && loop?.state !== 'done') rows.push({ kind: 'next', label: 'next', text: next });