@detergent-software/conductor 1.3.1-dev.1 → 1.4.0-dev.10

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 (110) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +92 -19
  3. package/build/brief-cli.js +105 -23
  4. package/build/brief-cli.js.map +1 -1
  5. package/build/brief.js +107 -9
  6. package/build/brief.js.map +1 -1
  7. package/build/broker.js +40 -14
  8. package/build/broker.js.map +1 -1
  9. package/build/cli.js +56 -18
  10. package/build/cli.js.map +1 -1
  11. package/build/code-host-cli.js +101 -2
  12. package/build/code-host-cli.js.map +1 -1
  13. package/build/code-host.js +18 -5
  14. package/build/code-host.js.map +1 -1
  15. package/build/conditions.js +104 -30
  16. package/build/conditions.js.map +1 -1
  17. package/build/daemon.js +64 -16
  18. package/build/daemon.js.map +1 -1
  19. package/build/follow-ups.js +72 -0
  20. package/build/follow-ups.js.map +1 -0
  21. package/build/help.js +721 -192
  22. package/build/help.js.map +1 -1
  23. package/build/in-process.js +30 -0
  24. package/build/in-process.js.map +1 -0
  25. package/build/inbox.js +113 -11
  26. package/build/inbox.js.map +1 -1
  27. package/build/landing.js +2 -11
  28. package/build/landing.js.map +1 -1
  29. package/build/lib/providers/ado/code-host.js +8 -5
  30. package/build/lib/providers/ado/code-host.js.map +1 -1
  31. package/build/lib/providers/ado/pipelines.js +16 -10
  32. package/build/lib/providers/ado/pipelines.js.map +1 -1
  33. package/build/lib/providers/github/actions.js +11 -0
  34. package/build/lib/providers/github/actions.js.map +1 -1
  35. package/build/lib/providers/github/code-host.js +17 -0
  36. package/build/lib/providers/github/code-host.js.map +1 -1
  37. package/build/lib/providers/index.js +1 -1
  38. package/build/lib/providers/index.js.map +1 -1
  39. package/build/lib/providers/npm/stage.js +33 -2
  40. package/build/lib/providers/npm/stage.js.map +1 -1
  41. package/build/lib/providers/verdicts/pr.js +14 -12
  42. package/build/lib/providers/verdicts/pr.js.map +1 -1
  43. package/build/lib/providers/verdicts/runs.js +4 -4
  44. package/build/lib/providers/verdicts/runs.js.map +1 -1
  45. package/build/manager.js +8 -3
  46. package/build/manager.js.map +1 -1
  47. package/build/note.js +95 -14
  48. package/build/note.js.map +1 -1
  49. package/build/notes.js +104 -16
  50. package/build/notes.js.map +1 -1
  51. package/build/notify.js +2 -1
  52. package/build/notify.js.map +1 -1
  53. package/build/package-info.js +12 -0
  54. package/build/package-info.js.map +1 -0
  55. package/build/profile.js +88 -14
  56. package/build/profile.js.map +1 -1
  57. package/build/project-check-cli.js +85 -0
  58. package/build/project-check-cli.js.map +1 -0
  59. package/build/project-check.js +362 -0
  60. package/build/project-check.js.map +1 -0
  61. package/build/resolve.js +11 -3
  62. package/build/resolve.js.map +1 -1
  63. package/build/rule-actions.js +9 -0
  64. package/build/rule-actions.js.map +1 -1
  65. package/build/rules-service.js +28 -19
  66. package/build/rules-service.js.map +1 -1
  67. package/build/runs.js +208 -0
  68. package/build/runs.js.map +1 -0
  69. package/build/scheduler.js +43 -1
  70. package/build/scheduler.js.map +1 -1
  71. package/build/server.js +76 -7
  72. package/build/server.js.map +1 -1
  73. package/build/store.js +27 -4
  74. package/build/store.js.map +1 -1
  75. package/build/tracker-writes.js +74 -0
  76. package/build/tracker-writes.js.map +1 -0
  77. package/build/trackers.js +20 -0
  78. package/build/trackers.js.map +1 -1
  79. package/build/wait.js +1 -0
  80. package/build/wait.js.map +1 -1
  81. package/build/watchers/ado.js +41 -12
  82. package/build/watchers/ado.js.map +1 -1
  83. package/build/watchers/boards-writes.js +182 -34
  84. package/build/watchers/boards-writes.js.map +1 -1
  85. package/build/watchers/boards.js +7 -2
  86. package/build/watchers/boards.js.map +1 -1
  87. package/build/watchers/github.js +45 -10
  88. package/build/watchers/github.js.map +1 -1
  89. package/build/watchers/index.js +40 -5
  90. package/build/watchers/index.js.map +1 -1
  91. package/build/watchers/issues-brief.js +48 -0
  92. package/build/watchers/issues-brief.js.map +1 -0
  93. package/build/watchers/issues-queue.js +85 -0
  94. package/build/watchers/issues-queue.js.map +1 -0
  95. package/build/watchers/issues-writes.js +233 -0
  96. package/build/watchers/issues-writes.js.map +1 -0
  97. package/build/watchers/issues.js +264 -0
  98. package/build/watchers/issues.js.map +1 -0
  99. package/build/watchers/linear-writes.js +198 -0
  100. package/build/watchers/linear-writes.js.map +1 -0
  101. package/build/watchers/merge.js +53 -0
  102. package/build/watchers/merge.js.map +1 -0
  103. package/build/watchers/npm.js +7 -16
  104. package/build/watchers/npm.js.map +1 -1
  105. package/build/watchers/source.js.map +1 -1
  106. package/build/worktrees.js +1 -0
  107. package/build/worktrees.js.map +1 -1
  108. package/mod/hooks/model.ts +11 -0
  109. package/mod/hooks/register.tsx +25 -3
  110. package/package.json +1 -1
package/build/help.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { styleText } from 'node:util';
1
2
  import { ConductorError } from "./errors.js";
2
3
  // By task, in the order a reader needs them; the commands inside each are listed A–Z.
3
4
  export const GROUPS = [
@@ -11,224 +12,449 @@ export const GROUPS = [
11
12
  'Setup',
12
13
  ];
13
14
  const DEFAULT_WIDTH = 80;
15
+ // Wider than this, a paragraph is too long a line to read.
16
+ const MAX_WIDTH = 100;
14
17
  // Narrower than this, a hanging indent leaves no room for the text.
15
18
  const MIN_WIDTH = 40;
16
19
  // A longer term goes on a line of its own, with its description under it.
17
20
  const MAX_TERM = 24;
18
- const RANGE = 'A <range> is today, yesterday, a date (2026-10-10), A..B (2026-10-10..2026-10-11), or a span (24h, the default; 7d).';
19
- const WHEN = '<when> is 17:00 (the next one), tomorrow, a day such as monday, or a date and time.';
20
- const WINDOW = 'A --since or --until <when> is now, today, yesterday, a span ago (30m, 24h, 7d), a date, or a date and time. Give a <range> or --since/--until, not both.';
21
+ const LOOKS = { command: 'cyan', heading: 'bold', placeholder: 'dim' };
22
+ const ITEM = [
23
+ '--item <item>',
24
+ "the item, when neither the worktree's name nor its branch carries it",
25
+ ];
26
+ const LOST_LOGIN = [5, 'a lost provider login: run conduct wait login <provider>'];
27
+ const PR_REPO = [
28
+ '--repo <repo>',
29
+ "github:<owner>/<repo> or ado:<repository>; the profile's code host by default",
30
+ ];
31
+ const RANGE = [
32
+ '<range>',
33
+ 'today, yesterday, a date (2026-10-10), A..B (2026-10-10..2026-10-11), or a span (24h, the default; 7d)',
34
+ ];
35
+ const WHEN = [
36
+ '<when>',
37
+ '17:00 (the next one), tomorrow, a day such as monday, or a date and time',
38
+ ];
39
+ const WINDOW = [
40
+ '<when>',
41
+ 'for --since or --until: now, today, yesterday, a span ago (30m, 24h, 7d), a date, or a date and time',
42
+ ];
43
+ const WORKER = ['<worker>', 'its item (ATK-309) or its name (probe-ATK-309)'];
44
+ const REPORT_OPTIONS = [
45
+ ['--json', 'print the report as JSON'],
46
+ ['--project <name>', 'one project only'],
47
+ ['--since <when>', 'the window starts then'],
48
+ ['--until <when>', 'the window ends then'],
49
+ ];
21
50
  export const COMMANDS = [
22
51
  {
23
- details: [
24
- 'Posts "approve" on the item as you and wakes its worker. <item> is the item (ATK-312), or the inbox item\'s id as #12.',
25
- '--text adds to the comment: "approve: <text>".',
52
+ arguments: [
53
+ ['<item>', "the item (ATK-312), or the inbox item's id as '#12' (quoted: a shell reads #12 as a comment)"],
54
+ ],
55
+ description: ['Posts "approve" on the item as you and wakes its worker.'],
56
+ examples: [
57
+ ["approve ATK-312's plan", 'approve ATK-312'],
58
+ ['the same, with a word for the worker', 'approve ATK-312 --text "skip the docs"'],
26
59
  ],
27
60
  group: 'Inbox and approvals',
28
61
  name: 'approve',
62
+ options: [['--text <text>', 'add to the comment: "approve: <text>"']],
63
+ seeAlso: ['revise', 'inbox'],
29
64
  summary: 'approve a plan and wake its worker',
30
65
  usage: ['approve <item> [--text <text>]'],
31
66
  },
32
67
  {
33
- details: ['Wakes a parked worker (with no event text) and opens its session in a tab.'],
68
+ arguments: [WORKER],
69
+ description: ['Wakes a parked worker (with no event text) and opens its session in a tab.'],
70
+ examples: [["open ATK-309's worker in a tab", 'attach ATK-309']],
34
71
  group: 'Projects and workers',
35
72
  name: 'attach',
73
+ seeAlso: ['launch', 'retire', 'status'],
36
74
  summary: 'wake a parked worker and open it in a tab',
37
75
  usage: ['attach <worker>'],
38
76
  },
39
77
  {
40
- details: [
41
- "Renames the worktree's branch (worktree-ATK-454) to <type>/<key>-<slug>, such as feat/atk-454-code-host-writes, with the item from the worktree's name (or --item). <type> is a conventional commit type.",
78
+ arguments: [
79
+ ['<type>', 'a conventional commit type: feat, fix, chore, docs…'],
80
+ ['<slug>', 'a few words for the branch, joined by dashes'],
81
+ ],
82
+ description: [
83
+ "Renames the worktree's branch (worktree-ATK-454) to <type>/<key>-<slug>, such as feat/atk-454-code-host-writes, with the item from the worktree's name (or --item).",
42
84
  'Prints JSON. Runs here, with or without a conductor.',
43
85
  ],
86
+ examples: [['in the ATK-454 worktree: feat/atk-454-code-host-writes', 'branch feat code-host-writes']],
44
87
  group: 'Used by work skills',
45
88
  name: 'branch',
89
+ options: [ITEM],
90
+ seeAlso: ['checkout', 'state'],
46
91
  summary: "rename the worktree's branch after its item",
47
92
  usage: ['branch <type> <slug> [--item <item>]'],
48
93
  },
49
94
  {
50
- details: [
95
+ arguments: [['<item>', 'the item (ATK-312)']],
96
+ description: [
51
97
  "The item's context for a worker's orient step, read by the conductor from the tracker (no MCP): title, status, project, Area and labels, blockers, the items it blocks and related items, its phase notes, the approve or revise: after the newest Plan, and the description.",
52
- 'A brief is kept for 30 seconds, and anything the conductor sees happen to the item (a note, an approve or revise it posted, a wait resolved) drops it. --fresh reads the tracker again, as a check for an approval after a wake should.',
53
- "Exit 3 when no conductor gave it (none running, one without the brief API, or one that couldn't read the tracker): read the item from the tracker then.",
98
+ "Also the work skill's phase (orient, plan, awaiting-approval, implement, wait, land, done, abandoned, canceled), from the status, the notes and the PR; and the PR (its worker's, else the newest PR opened note's) as its code host has it, with its verdict.",
99
+ 'A brief is kept for 30 seconds, and anything the conductor sees happen to the item (a note, an approve or revise it posted, a wait resolved) drops it.',
100
+ "With no conductor, it is read here from this checkout's project, as its account.",
101
+ ],
102
+ examples: [
103
+ ['orient on ATK-312', 'brief ATK-312'],
104
+ ['check for an approval after a wake', 'brief ATK-312 --fresh'],
105
+ ],
106
+ exitCodes: [
107
+ [
108
+ 3,
109
+ "nobody gave it (no conductor and this isn't the item's project checkout, or the conductor couldn't read the tracker): read the item from the tracker then",
110
+ ],
54
111
  ],
55
112
  group: 'Used by work skills',
56
113
  name: 'brief',
57
- summary: "an item's title, status, links, notes and approval",
58
- usage: ['brief <item> [--json] [--fresh]'],
114
+ options: [
115
+ ['--comments', "add every comment's words"],
116
+ ['--fresh', 'read the tracker again, as a check for an approval after a wake should'],
117
+ ['--json', 'print it as JSON'],
118
+ ],
119
+ seeAlso: ['note', 'wait'],
120
+ summary: "an item's status, phase, PR, links, notes and approval",
121
+ usage: ['brief <item> [--json] [--fresh] [--comments]'],
59
122
  },
60
123
  {
61
- details: [
124
+ arguments: [
125
+ ['<owner>/<repo>', 'the GitHub repo'],
126
+ ['<type>', 'a conventional commit type for the branch'],
127
+ ['<slug>', 'a few words for the branch, joined by dashes'],
128
+ ],
129
+ description: [
62
130
  'Clones another GitHub repo into the worktree at .scratch/repos/<repo>, on <type>/<key>-<slug> from its default branch, with no upstream until the first push. Again on an existing checkout, it says where it is and changes nothing.',
63
131
  "The clone stays out of the worktree's git status, and conduct retire checks it for unpushed work.",
64
132
  'Prints JSON. Runs here.',
65
133
  ],
134
+ examples: [
135
+ ['clone the registry for a skill fix', 'checkout EmergentSoftware/agentic-toolkit-registry fix lock-path'],
136
+ ],
66
137
  group: 'Used by work skills',
67
138
  name: 'checkout',
139
+ options: [ITEM],
140
+ seeAlso: ['branch', 'state', 'pr'],
68
141
  summary: 'clone another repo into the worktree for the item',
69
142
  usage: ['checkout <owner>/<repo> <type> <slug> [--item <item>]'],
70
143
  },
71
144
  {
72
- details: [
73
- "Opens the console, the Claude session the conductor mod serves, in a Windows Terminal tab in the project's main checkout. --here runs it in this terminal.",
145
+ description: [
146
+ "Opens the console, the Claude session the conductor mod serves, in a Windows Terminal tab in the project's main checkout.",
74
147
  'The tab needs Windows Terminal, and refuses while CONDUCTOR_HOME is set (a tab would not see it): use --here then.',
75
148
  'Also how to get it back after it closed: it keeps no state of its own.',
76
- 'One at a time: it refuses while a console is open, or started in the last minute and not yet following the events; --force opens another anyway (each escalation then wakes both).',
149
+ 'One at a time: it refuses while a console is open, or started in the last minute and not yet following the events.',
150
+ ],
151
+ examples: [
152
+ ['open the console in a new tab', 'console'],
153
+ ['run it in this terminal', 'console --here'],
77
154
  ],
78
155
  group: 'Console and rules',
79
156
  name: 'console',
157
+ options: [
158
+ ['--force', 'open another console anyway; each escalation then wakes both'],
159
+ ['--here', 'run it in this terminal, not a tab'],
160
+ ['--project <name>', 'the project whose checkout it runs in; needed when several are registered'],
161
+ ],
162
+ seeAlso: ['escalations', 'inbox', 'install-mod'],
80
163
  summary: 'open the console, the session the conductor mod serves',
81
164
  usage: ['console [--project <name>] [--here] [--force]'],
82
165
  },
83
166
  {
84
- details: [
85
- 'For a physical action, or one you handled yourself. <id> is the inbox id as conduct inbox prints it (#12, or 12).',
86
- ],
167
+ arguments: [['<id>', 'the inbox id as conduct inbox prints it (#12, or 12)']],
168
+ description: ['For a physical action, or one you handled yourself.'],
169
+ examples: [['you approved the npm stage of inbox item #12 yourself', 'done 12']],
87
170
  group: 'Inbox and approvals',
88
171
  name: 'done',
172
+ seeAlso: ['inbox', 'approve'],
89
173
  summary: 'mark an inbox item done',
90
174
  usage: ['done <id>'],
91
175
  },
92
176
  {
93
- details: [
177
+ commands: [
178
+ ['escalations [--json]', "what rules escalated to the console in the last 14 days and it hasn't handled"],
94
179
  [
95
- ['escalations [--json]', "what rules escalated to the console in the last 14 days and it hasn't handled"],
96
- [
97
- 'done <id> [--ask] <text>',
98
- 'log the console\'s finding on escalation #<id>; --ask also puts it in the inbox for you (a "console" item). Every word after <id> is the text',
99
- ],
180
+ 'done <id> [--ask] <text>',
181
+ "log the console's finding on escalation #<id>. Every word after <id> is the text, quoted or not",
100
182
  ],
101
183
  ],
184
+ description: ["The console's work: what the rules escalated to it, and its findings on them."],
185
+ examples: [
186
+ ['what waits on the console', 'escalations'],
187
+ ['log a finding on #7 and put it in the inbox', 'escalations done 7 --ask the PR needs a merge of develop'],
188
+ ],
102
189
  group: 'Console and rules',
103
190
  name: 'escalations',
191
+ options: [
192
+ ['--ask', 'with done: also put the finding in the inbox for you (a "console" item)'],
193
+ ['--json', 'print the escalations as JSON'],
194
+ ],
195
+ seeAlso: ['console', 'rules', 'why'],
104
196
  summary: "what rules escalated to the console and it hasn't handled",
105
197
  usage: ['escalations [--json]', 'escalations done <id> [--ask] <text>'],
106
198
  },
107
199
  {
108
- details: [
109
- 'The event log after <id> (all of it without --since), one JSON event per line.',
110
- '--follow keeps following it (live without --since), reconnecting when the conductor restarts; the state lines conduct.connected and conduct.disconnected say so.',
111
- "--console (or CONDUCTOR_CONSOLE=1) marks the console's stream, as the conductor mod runs it: with none open, escalations raise an inbox item after 30 s.",
200
+ description: ['The event log, one JSON event per line.'],
201
+ examples: [
202
+ ['every event after 4200', 'events --since 4200'],
203
+ ['follow the log live', 'events --follow'],
112
204
  ],
113
205
  group: 'Logs and reports',
114
206
  name: 'events',
207
+ options: [
208
+ [
209
+ '--console',
210
+ "mark the stream as the console's, as the conductor mod runs it (or CONDUCTOR_CONSOLE=1): with none open, escalations raise an inbox item after 30 s",
211
+ ],
212
+ [
213
+ '--follow',
214
+ 'keep following it (live without --since), reconnecting when the conductor restarts; the state lines conduct.connected and conduct.disconnected say so',
215
+ ],
216
+ ['--since <id>', 'the events after <id>; all of them without it'],
217
+ ],
218
+ seeAlso: ['history', 'why'],
115
219
  summary: 'the event log, one JSON event per line',
116
220
  usage: ['events [--since <id>]', 'events --follow [--since <id>] [--console]'],
117
221
  },
118
222
  {
119
- details: [
120
- "--slots and --weight set the project's slots or ordering weight (-100 to 100) until <when>; with neither, its items go first (weight 100).",
121
- WHEN,
223
+ description: [
224
+ "Files a follow-up of this checkout's item (or --item) on its tracker: in Backlog, in the item's project, with its Area, the profile's triage label, and the human label too with --human. It is related to the item and to each --related, and waits on each --blocked-by.",
225
+ "Search the tracker for a duplicate first: this only files. An Area outside the group, or a key of another project, is refused before anything is written. On Linear a relation that fails once the issue exists (such as an item it doesn't have) is a warning; on ADO Boards the item and its links are one request, so a bad link refuses the whole filing. On GitHub Issues the related items are a Related: line in its body, and a dependency that fails is a warning.",
226
+ 'With a conductor, the daemon files it for the worker that owns this checkout and records it; without one it is filed here. Prints JSON with "via".',
227
+ ],
228
+ examples: [
229
+ [
230
+ 'a follow-up in apps/conductor that waits on ATK-480',
231
+ 'file --title "conductor: color the status" --body-file follow-up.md --area apps/conductor --blocked-by ATK-480',
232
+ ],
233
+ ],
234
+ exitCodes: [[1, 'refused or failed'], LOST_LOGIN],
235
+ group: 'Used by work skills',
236
+ name: 'file',
237
+ options: [
238
+ [
239
+ '--area <area>',
240
+ "one of the tracker's Areas: a label of the Area group, a path below a board's root, or the name after a GitHub Issues area prefix",
241
+ ],
242
+ ['--blocked-by <item>…', 'the items it waits on; repeated or comma-separated'],
243
+ ['--body-file <file>', 'its description'],
244
+ ['--human', 'add the human label too'],
245
+ ['--item <item>', "the item it follows up, when neither the worktree's name nor its branch carries it"],
246
+ ['--related <item>…', 'more items it is related to; repeated or comma-separated'],
247
+ ['--title <title>', 'its title'],
248
+ ],
249
+ seeAlso: ['brief', 'note'],
250
+ summary: 'file a follow-up of the item, in Backlog with its Area',
251
+ usage: [
252
+ 'file --title <title> --body-file <file> --area <area> [--related <item>…] [--blocked-by <item>…] [--human] [--item <item>]',
253
+ ],
254
+ },
255
+ {
256
+ arguments: [['<project>', 'a registered project'], WHEN],
257
+ description: [
258
+ "Sets the project's slots or ordering weight until <when>; with neither, its items go first (weight 100).",
259
+ ],
260
+ examples: [
261
+ ['work atk first until 17:00', 'focus atk --until 17:00'],
262
+ ['six slots for atk until tomorrow', 'focus atk --slots 6 --until tomorrow'],
122
263
  ],
123
264
  group: 'Scheduling',
124
265
  name: 'focus',
266
+ options: [
267
+ ['--slots <n>', "the project's slots"],
268
+ ['--until <when>', 'when the focus ends (required)'],
269
+ ['--weight <n>', 'its ordering weight, -100 to 100'],
270
+ ],
271
+ seeAlso: ['pause', 'queue'],
125
272
  summary: 'more slots or weight for a project, for a while',
126
273
  usage: ['focus <project> [--slots <n>] [--weight <n>] --until <when>'],
127
274
  },
128
275
  {
129
- details: [
276
+ arguments: [RANGE, WINDOW],
277
+ description: [
130
278
  'What a window of the log says: shipped items, time in each phase by tier, plan latency, waits by kind, stalls, failures, inbox, slot use, rules and spend.',
131
- RANGE,
132
- WINDOW,
279
+ 'Give a <range> or --since/--until, not both.',
280
+ ],
281
+ examples: [
282
+ ['the last 24 hours', 'history'],
283
+ ['a week of atk', 'history 7d --project atk'],
284
+ ['the last 30 minutes', 'history --since 30m'],
133
285
  ],
134
286
  group: 'Logs and reports',
135
287
  name: 'history',
288
+ options: REPORT_OPTIONS,
289
+ seeAlso: ['retro', 'events'],
136
290
  summary: 'what a window of the log says, from shipped items to spend',
137
291
  usage: ['history [<range>] [--since <when>] [--until <when>] [--project <name>] [--json]'],
138
292
  },
139
293
  {
140
- details: ['Each open item with its age, its commands and its links.'],
294
+ description: ['Each open item with its age, its commands and its links.'],
295
+ examples: [['what waits on you', 'inbox']],
141
296
  group: 'Inbox and approvals',
142
297
  name: 'inbox',
298
+ options: [['--json', 'print the items as JSON']],
299
+ seeAlso: ['approve', 'done', 'notify'],
143
300
  summary: 'what waits on you',
144
301
  usage: ['inbox [--json]'],
145
302
  },
146
303
  {
147
- details: [
304
+ description: [
148
305
  'Installs the conductor mod from this package in Claude Code (user scope): the inbox band, pane and commands in the console (a session started with CONDUCTOR_CONSOLE=1).',
149
- '--remove uninstalls it.',
306
+ ],
307
+ examples: [
308
+ ['install the mod', 'install-mod'],
309
+ ['uninstall it', 'install-mod --remove'],
150
310
  ],
151
311
  group: 'Setup',
152
312
  name: 'install-mod',
313
+ options: [['--remove', 'uninstall it']],
314
+ seeAlso: ['console'],
153
315
  summary: 'install the conductor mod in Claude Code',
154
316
  usage: ['install-mod', 'install-mod --remove'],
155
317
  },
156
318
  {
157
- details: [
319
+ description: [
158
320
  "Registers a Windows Task Scheduler task that runs conduct supervise at logon, which restarts the daemon if it dies. Run it from the main checkout: a worktree's build goes away.",
159
- '--remove removes that logon task.',
160
321
  ],
322
+ examples: [['start the daemon at every logon', 'install-startup']],
161
323
  group: 'Setup',
162
324
  name: 'install-startup',
325
+ options: [['--remove', 'remove that logon task']],
326
+ seeAlso: ['supervise', 'start'],
163
327
  summary: 'start the daemon at logon (Windows Task Scheduler)',
164
328
  usage: ['install-startup', 'install-startup --remove'],
165
329
  },
166
330
  {
167
- details: [
331
+ arguments: [['<pr>', 'the PR number']],
332
+ description: [
168
333
  "Sets the worker's PR up to land, then gives its first verdict. Safe to repeat. A brake already on the PR (a comment, a failed build, conflicts) is reported and nothing is armed. Otherwise it arms the host's own landing: on Azure DevOps a +10 vote (auto mode) and auto-complete with noFastForward; on GitHub, auto-merge with a merge commit (auto mode, where the repo allows it, refused with the reason where it doesn't). Then it posts the Landing note.",
169
- "--mode is held to the profile's landing rules for the PR's files and the plan's tier: auto where they allow it, else refused. Without --mode the rules pick it. The daemon takes the tier from the item's Plan note; without a conductor, pass --tier.",
170
334
  'A Landing note with auto-complete or auto-merge gone means someone cancelled it: auto-merge-cancelled, until --rearm.',
171
335
  'With a conductor, the daemon records the landing and keeps it armed while the worker waits (conduct wait pr <pr>): it queues an expired build again and restores a cleared vote.',
172
- 'Prints JSON with the verdict and "via". Exit 1 when refused; 5 for a lost login.',
336
+ 'Prints JSON with the verdict and "via".',
173
337
  ],
338
+ examples: [
339
+ ['land PR 1850 by itself', 'land 1850 --mode auto --reason "S fix, no review paths"'],
340
+ ['arm it again after a cancel', 'land 1850 --rearm --reason "Jason said go on"'],
341
+ ],
342
+ exitCodes: [[1, 'refused'], LOST_LOGIN],
174
343
  group: 'Used by work skills',
175
344
  name: 'land',
345
+ options: [
346
+ ITEM,
347
+ [
348
+ '--mode auto|review',
349
+ "held to the profile's landing rules for the PR's files and the plan's tier: auto where they allow it, else refused; without it the rules pick",
350
+ ],
351
+ ['--reason <why>', 'why this mode, for the Landing note (required)'],
352
+ ['--rearm', 'arm it again after someone cancelled it'],
353
+ PR_REPO,
354
+ ['--tier S|M|L', "the plan's tier without a conductor; the daemon takes it from the item's Plan note"],
355
+ ],
356
+ seeAlso: ['wait', 'pr', 'runs'],
176
357
  summary: "arm the worker's PR to land, and its first verdict",
177
358
  usage: ['land <pr> --reason <why> [--mode auto|review] [--rearm] [--tier S|M|L] [--repo <repo>] [--item <item>]'],
178
359
  },
179
360
  {
180
- details: [
361
+ arguments: [['<item>', 'the item to work (ATK-312)']],
362
+ description: [
181
363
  "Starts a worker on the item: its worktree, a background session running the project's work skill, and a tab.",
182
- [
183
- ['--project <name>', 'the project the item belongs to'],
184
- ['--probe', "a probe that works no issue: its session and worktree are probe-<item>, out of the fleet's way"],
185
- ['--prompt <text>', "the session's first prompt instead of the work skill (or the probe prompt)"],
186
- ['--conductor <name>', 'passed to the work skill as --conductor'],
187
- ],
364
+ ],
365
+ examples: [
366
+ ['start a worker on ATK-312', 'launch ATK-312'],
367
+ ['a probe with its own prompt', 'launch ATK-312 --probe --prompt "time the test suite"'],
188
368
  ],
189
369
  group: 'Projects and workers',
190
370
  name: 'launch',
371
+ options: [
372
+ ['--conductor <name>', 'passed to the work skill as --conductor'],
373
+ ['--probe', "a probe that works no issue: its session and worktree are probe-<item>, out of the fleet's way"],
374
+ ['--project <name>', 'the project the item belongs to'],
375
+ ['--prompt <text>', "the session's first prompt instead of the work skill (or the probe prompt)"],
376
+ ],
377
+ seeAlso: ['attach', 'retire', 'status'],
191
378
  summary: 'start a worker: its worktree, a background session, a tab',
192
379
  usage: ['launch <item> [--project <name>] [--probe] [--prompt <text>] [--conductor <name>]'],
193
380
  },
194
381
  {
195
- details: [
196
- 'acquire, release or show a lock every project shares, such as chrome for verify runs. It goes stale 30 minutes after its holder last acquired it: acquire it again to keep it.',
197
- "The owner is this checkout's worker's item unless --owner gives one. --wait polls for up to that many minutes.",
198
- 'Prints JSON. Exit 3 when no conductor answers.',
382
+ commands: [
383
+ ['acquire <name>', 'take the lock, or keep it when you hold it'],
384
+ ['release <name>', 'let it go'],
385
+ ['status <name>', 'who holds it'],
386
+ ],
387
+ description: [
388
+ 'A lock every project shares, such as chrome for verify runs. It goes stale 30 minutes after its holder last acquired it: acquire it again to keep it.',
389
+ 'Prints JSON.',
390
+ ],
391
+ examples: [
392
+ ['take the browser, waiting up to 30 minutes for it', 'lock acquire chrome --wait 30'],
393
+ ['give it back', 'lock release chrome'],
199
394
  ],
395
+ exitCodes: [[3, 'no conductor answered']],
200
396
  group: 'Used by work skills',
201
397
  name: 'lock',
398
+ options: [
399
+ ['--owner <id>', "who holds it; this checkout's worker's item by default"],
400
+ ['--wait <minutes>', 'poll for the lock for up to that many minutes'],
401
+ ],
402
+ seeAlso: ['ports', 'status'],
202
403
  summary: 'a lock every project shares, such as chrome',
203
404
  usage: ['lock acquire|release|status <name> [--owner <id>] [--wait <minutes>]'],
204
405
  },
205
406
  {
206
- details: [
207
- 'Posts the file as a phase note on the item ("## <work skill> · <Phase>") and records its phase on the worker: this checkout\'s, or --worker <worker>.',
208
- 'Exit 3 when nobody posted it (no conductor, or the tracker refused it).',
209
- 'Exit 4 when it held the note back because Jason revised an approved plan (by him or by the conductor): re-plan with his words (printed) and wait for approval.',
407
+ arguments: [
408
+ ['<item>', 'the item (ATK-312)'],
409
+ ['<file>', 'the note, its heading first'],
410
+ ],
411
+ description: [
412
+ 'Posts the file as a phase note on the item ("## <work skill> · <Phase>") and records its phase on the worker: this checkout\'s, or --worker.',
413
+ "A Started, PR opened or Landed note also moves the item, to the profile's tracker.transitions (by default In Progress and assigned, In Review, Done on Linear), and PR opened links the worker's PR (else the note's). A Started never reopens a closed item. A state the tracker won't take is a warning: the note stands.",
414
+ "With no conductor, it is posted from this checkout as its project's account, and the item moves the same way.",
415
+ ],
416
+ examples: [['post the Plan note', 'note ATK-312 plan.md']],
417
+ exitCodes: [
418
+ [3, "nobody posted it (the tracker refused it, or no conductor and this isn't the item's project checkout)"],
419
+ [
420
+ 4,
421
+ 'held back because Jason revised an approved plan (by him or by the conductor): re-plan with his words (printed) and wait for approval',
422
+ ],
210
423
  ],
211
424
  group: 'Used by work skills',
212
425
  name: 'note',
426
+ options: [['--worker <worker>', "the worker whose phase it records; this checkout's by default"]],
427
+ seeAlso: ['brief', 'file', 'wait'],
213
428
  summary: 'post a phase note on the item and record the phase',
214
429
  usage: ['note <item> <file> [--worker <worker>]'],
215
430
  },
216
431
  {
217
- details: [
432
+ commands: [
433
+ ['notify [--json]', "the notification profile, quiet hours, and each channel's state"],
218
434
  [
219
- ['notify [--json]', "the notification profile, quiet hours, and each channel's state"],
220
- [
221
- 'profile [<name>]',
222
- 'switch the profile now (built in: default, away, focus); --until 17:00 or a date and time hands back to the one before it then. Without <name>, the same as conduct notify',
223
- ],
224
- [
225
- 'test',
226
- "push a sample item (default npm-otp) on the profile's channels now; --raise puts a real item in the inbox instead, routed by the profile (quiet hours, reminders), so it takes no --channel",
227
- ],
435
+ 'profile [<name>]',
436
+ 'switch the profile now (built in: default, away, focus); without <name>, the same as conduct notify',
437
+ ],
438
+ [
439
+ 'test',
440
+ "push a sample item on the profile's channels now, or with --raise put a real item in the inbox, routed by the profile (quiet hours, reminders)",
228
441
  ],
229
442
  ],
443
+ description: ['The notification profile and its channels: see them, switch the profile, or test it.'],
444
+ examples: [
445
+ ['away for the afternoon', 'notify profile away --until 17:00'],
446
+ ['push a sample npm-otp item', 'notify test'],
447
+ ],
230
448
  group: 'Inbox and approvals',
231
449
  name: 'notify',
450
+ options: [
451
+ ['--channel remote-control|pushover', 'with test: push on that channel'],
452
+ ['--json', 'print the state as JSON'],
453
+ ['--kind <kind>', 'with test: the kind of item (npm-otp by default)'],
454
+ ['--raise', 'with test: put a real item in the inbox instead; it takes no --channel'],
455
+ ['--until <when>', 'with profile: hand back to the profile before it then (17:00, or a date and time)'],
456
+ ],
457
+ seeAlso: ['inbox'],
232
458
  summary: 'the notification profile and channels; switch or test them',
233
459
  usage: [
234
460
  'notify [--json]',
@@ -237,35 +463,74 @@ export const COMMANDS = [
237
463
  ],
238
464
  },
239
465
  {
240
- details: [
241
- 'No new launches in the project, or in every project without one, until <when> or conduct unpause.',
242
- WHEN,
466
+ arguments: [['<project>', 'a registered project; every project without it'], WHEN],
467
+ description: ['No new launches in the project, or in every project without one, until <when> or conduct unpause.'],
468
+ examples: [
469
+ ['pause every project until tomorrow', 'pause --until tomorrow'],
470
+ ['pause atk until you unpause it', 'pause atk'],
243
471
  ],
244
472
  group: 'Scheduling',
245
473
  name: 'pause',
474
+ options: [['--until <when>', 'when the pause ends']],
475
+ seeAlso: ['unpause', 'focus', 'queue'],
246
476
  summary: 'no new launches, until a time or unpause',
247
477
  usage: ['pause [<project>] [--until <when>]'],
248
478
  },
249
479
  {
250
- details: [
251
- "The checkout's dev ports from the port broker: its project's base ports plus its slot, which no other project's worktree shares (a main checkout: slot 0). --prefer asks for a slot first. Without <path>, every worktree's.",
252
- 'Exit 3 when no conductor brokered it, so the caller keeps its own ports.',
480
+ arguments: [['<path>', "the checkout; without it, every worktree's"]],
481
+ description: [
482
+ "The checkout's dev ports from the port broker: its project's base ports plus its slot, which no other project's worktree shares (a main checkout: slot 0).",
253
483
  ],
484
+ examples: [
485
+ ["this checkout's ports", 'ports .'],
486
+ ["every worktree's", 'ports'],
487
+ ],
488
+ exitCodes: [[3, 'no conductor brokered it, so the caller keeps its own ports']],
254
489
  group: 'Used by work skills',
255
490
  name: 'ports',
491
+ options: [
492
+ ['--json', 'print them as JSON'],
493
+ ['--prefer <slot>', 'ask for this slot first'],
494
+ ],
495
+ seeAlso: ['lock'],
256
496
  summary: "a checkout's dev ports, from the port broker",
257
497
  usage: ['ports [<path>] [--prefer <slot>] [--json]'],
258
498
  },
259
499
  {
260
- details: [
261
- "pr create: checks the title carries the item key, the branch carries it too, the description fits the host, no PR is open from the branch, nothing is uncommitted, the branch is ahead of its base, and commitlint passes, all before anything is pushed. Then it pushes and opens the PR into the base branch (--repo: into the other repo's default branch, from its checkout).",
262
- 'With a conductor, the daemon opens it for the worker that owns this checkout, checking the ownership again, and records the PR on the worker. Without one (or an older one) it opens it here. --dry-run stops before the push.',
263
- "pr threads: the PR's threads (on GitHub each comment and review), each with needsAnswer: a person's, open, with no note after it, and asking for something. A read, so it runs here.",
264
- "--repo takes github:<owner>/<repo> or ado:<repository>; the profile's code host by default.",
265
- 'Prints JSON with "via": "daemon" or "local". Exit 1 when refused or failed; 5 for a lost login: run conduct wait login <provider>.',
500
+ commands: [
501
+ [
502
+ 'create',
503
+ "check the title carries the item key, the branch carries it too, the description fits the host, no PR is open from the branch, nothing is uncommitted, the branch is ahead of its base, and commitlint passes, all before anything is pushed. Then push and open the PR into the base branch (--repo: into the other repo's default branch, from its checkout)",
504
+ ],
505
+ [
506
+ 'threads <pr>',
507
+ "the PR's threads (on GitHub each comment and review), each with needsAnswer: a person's, open, with no note after it, and asking for something. A read, so it runs here",
508
+ ],
509
+ ],
510
+ description: [
511
+ "Opens the worker's PR, checked and pushed, or reads its threads.",
512
+ "The description ends with the tracker's reference when it has one (AB#<id> for an ADO Boards item on a GitHub PR, so the Boards GitHub app links it), counted in its length.",
513
+ 'With a conductor, the daemon opens it for the worker that owns this checkout, checking the ownership again, and records the PR on the worker. Without one (or an older one) it opens it here.',
514
+ 'Prints JSON with "via": "daemon" or "local".',
515
+ ],
516
+ examples: [
517
+ [
518
+ 'check everything, push nothing',
519
+ 'pr create --title "feat(conductor): help sections (ATK-478)" --body-file pr.md --dry-run',
520
+ ],
521
+ ["PR 1850's threads", 'pr threads 1850'],
266
522
  ],
523
+ exitCodes: [[1, 'refused or failed'], LOST_LOGIN],
267
524
  group: 'Used by work skills',
268
525
  name: 'pr',
526
+ options: [
527
+ ['--body-file <file>', 'with create: its description'],
528
+ ['--dry-run', 'with create: stop before the push'],
529
+ ITEM,
530
+ PR_REPO,
531
+ ['--title <title>', 'with create: its title, with the item key'],
532
+ ],
533
+ seeAlso: ['land', 'reply', 'state'],
269
534
  summary: "open the worker's PR, checked and pushed; read its threads",
270
535
  usage: [
271
536
  'pr create --title <title> --body-file <file> [--repo <repo>] [--item <item>] [--dry-run]',
@@ -273,225 +538,411 @@ export const COMMANDS = [
273
538
  ],
274
539
  },
275
540
  {
276
- details: [
541
+ commands: [
542
+ ['add <path>', 'register the project whose checkout holds .claude/conductor.json; works without the daemon'],
543
+ ['list [--json]', 'the registered projects'],
277
544
  [
278
- [
279
- 'add <path>',
280
- 'register the project whose checkout holds .claude/conductor.json (again after changing it); works without the daemon',
281
- ],
282
- ['list [--json]', 'the registered projects'],
545
+ 'check <name>',
546
+ 'whether the project can be conducted, before its first launch finds out: its profile (and what differs from the stored copy), tracker, code host and base branch, release pipelines, a throwaway worktree made and removed through its hooks, its installed skills, a free port slot and its health URLs. One ok, warn, fail or skip line each, with the fix',
283
547
  ],
284
548
  ],
549
+ description: [
550
+ 'Registers a project, lists the registered ones, or checks one.',
551
+ 'After that the daemon takes the profile from disk when it starts and each time the file is saved.',
552
+ "check goes through the daemon, as its accounts, or runs here as the machine's when it isn't running. Only the worktree check writes, and it removes what it made.",
553
+ ],
554
+ examples: [
555
+ ['register the checkout you are in', 'project add .'],
556
+ ['the registered projects', 'project list'],
557
+ ['check atk before conducting it', 'project check atk'],
558
+ ['the same without the worktree (its hook runs pnpm install)', 'project check atk --no-worktree'],
559
+ ],
285
560
  group: 'Projects and workers',
286
561
  name: 'project',
287
- summary: 'register a project, or list the registered ones',
288
- usage: ['project add <path>', 'project list [--json]'],
562
+ options: [
563
+ ['--json', 'with list or check: print them as JSON'],
564
+ ['--no-worktree', 'with check: skip the throwaway worktree'],
565
+ ],
566
+ seeAlso: ['launch', 'ports', 'status'],
567
+ summary: 'register a project, list the registered ones, or check one',
568
+ usage: ['project add <path>', 'project list [--json]', 'project check <name> [--no-worktree] [--json]'],
289
569
  },
290
570
  {
291
- details: ["--fresh asks every project's tracker now, instead of using the last read."],
571
+ description: ['What the scheduler picks next, and why the rest wait.'],
572
+ examples: [
573
+ ['what launches next', 'queue'],
574
+ ['the same, after a change in the tracker', 'queue --fresh'],
575
+ ],
292
576
  group: 'Scheduling',
293
577
  name: 'queue',
578
+ options: [
579
+ ['--fresh', "ask every project's tracker now, instead of using the last read"],
580
+ ['--json', 'print it as JSON'],
581
+ ],
582
+ seeAlso: ['focus', 'pause', 'status'],
294
583
  summary: 'what the scheduler picks next, and why the rest wait',
295
584
  usage: ['queue [--json] [--fresh]'],
296
585
  },
297
586
  {
298
- details: [
587
+ arguments: [
588
+ ['<pr>', 'the PR number'],
589
+ ['<thread>', 'the Azure DevOps thread; without it, a note on the PR'],
590
+ ],
591
+ description: [
299
592
  'Answers on the worker\'s PR: in the Azure DevOps thread named, or as a note on the PR (GitHub, which has no threads, or no thread given). The text is headed "## <work skill> · Reply" unless it already starts with a note heading, so the PR reads it as the worker\'s.',
300
- "With a conductor, the daemon posts it for the worker that owns this checkout, checking the PR is the worker's. Prints JSON. Exit 1 when refused; 5 for a lost login.",
593
+ "With a conductor, the daemon posts it for the worker that owns this checkout, checking the PR is the worker's. Prints JSON.",
301
594
  ],
595
+ examples: [['answer thread 4411 on PR 1850', 'reply 1850 4411 --body-file answer.md']],
596
+ exitCodes: [[1, 'refused'], LOST_LOGIN],
302
597
  group: 'Used by work skills',
303
598
  name: 'reply',
599
+ options: [['--body-file <file>', 'the answer'], ITEM, PR_REPO],
600
+ seeAlso: ['pr', 'land'],
304
601
  summary: "answer a comment on the worker's PR",
305
602
  usage: ['reply <pr> [<thread>] --body-file <file> [--repo <repo>] [--item <item>]'],
306
603
  },
307
604
  {
308
- details: [
309
- "Stops the worker's session and removes its worktree. It refuses while the worker's turn runs or its worktree has unfinished work (uncommitted or unpushed); --force retires it anyway and throws that work away.",
605
+ arguments: [WORKER],
606
+ description: [
607
+ "Stops the worker's session and removes its worktree. It refuses while the worker's turn runs or its worktree has unfinished work (uncommitted or unpushed).",
310
608
  ],
609
+ examples: [['retire ATK-309 once it is done', 'retire ATK-309']],
311
610
  group: 'Projects and workers',
312
611
  name: 'retire',
612
+ options: [['--force', 'retire it anyway and throw that work away']],
613
+ seeAlso: ['launch', 'attach', 'status'],
313
614
  summary: 'stop a worker and remove its worktree and session',
314
615
  usage: ['retire <worker> [--force]'],
315
616
  },
316
617
  {
317
- details: ['The same report as conduct history, as Markdown.', RANGE, WINDOW],
618
+ arguments: [RANGE, WINDOW],
619
+ description: ['The same report as conduct history, as Markdown.', 'Give a <range> or --since/--until, not both.'],
620
+ examples: [
621
+ ["yesterday's retro", 'retro yesterday'],
622
+ ['a week of atk, to a file', 'retro 7d --project atk > retro.md'],
623
+ ],
318
624
  group: 'Logs and reports',
319
625
  name: 'retro',
626
+ options: REPORT_OPTIONS,
627
+ seeAlso: ['history'],
320
628
  summary: 'the history report as Markdown',
321
629
  usage: ['retro [<range>] [--since <when>] [--until <when>] [--project <name>] [--json]'],
322
630
  },
323
631
  {
324
- details: [
632
+ arguments: [
633
+ ['<item>', "the item (ATK-312), or the inbox item's id as '#12' (quoted: a shell reads #12 as a comment)"],
634
+ ['<text>', 'what to change in the plan'],
635
+ ],
636
+ description: [
325
637
  'Posts "revise: <text>" on the item as you and wakes its worker. Every word after <item> is the text, quoted or not.',
326
638
  ],
639
+ examples: [['ask for a smaller plan', 'revise ATK-312 put the web change in a PR of its own']],
327
640
  group: 'Inbox and approvals',
328
641
  name: 'revise',
642
+ seeAlso: ['approve', 'inbox'],
329
643
  summary: 'ask for a revised plan and wake its worker',
330
644
  usage: ['revise <item> <text>'],
331
645
  },
332
646
  {
333
- details: [
647
+ commands: [
648
+ [
649
+ 'rules [--json]',
650
+ 'the rules in force (project, machine, built-in defaults), what replaces what, and any rules file refused. Machine rules are ~/.conductor/rules.json',
651
+ ],
334
652
  [
335
- [
336
- 'rules [--json]',
337
- 'the rules in force (project, machine, built-in defaults), what replaces what, and any rules file refused. Machine rules are ~/.conductor/rules.json',
338
- ],
339
- [
340
- 'test <file> [--json]',
341
- "dry-run the rules on a sample JSON file (no event: a state rule's subject); acts on nothing",
342
- ],
653
+ 'test <file> [--json]',
654
+ "dry-run the rules on a sample JSON file (no event: a state rule's subject); acts on nothing",
343
655
  ],
344
- 'A sample:',
656
+ ],
657
+ description: [
658
+ 'The rules in force, and a dry run of them on a sample. A sample for rules test:',
345
659
  { pre: ['{ "event": "pr.comment", "project": "atk",', ' "fields": { "wait.kind": "merge" } }'] },
346
660
  ],
661
+ examples: [
662
+ ['the rules in force', 'rules'],
663
+ ['what the rules would do with that sample', 'rules test sample.json'],
664
+ ],
347
665
  group: 'Console and rules',
348
666
  name: 'rules',
667
+ options: [['--json', 'print it as JSON']],
668
+ seeAlso: ['why', 'escalations'],
349
669
  summary: 'the rules in force; dry-run them on a sample',
350
670
  usage: ['rules [--json]', 'rules test <file> [--json]'],
351
671
  },
352
672
  {
353
- details: ['One daemon per machine. conduct start runs it detached instead.'],
673
+ arguments: [['<sha>', 'the merge commit']],
674
+ description: [
675
+ "One look at a merge's runs: on Azure Pipelines for the project's Azure DevOps repo (the pipelines whose YAML trigger the merge fires, read in this worktree), on GitHub Actions for a GitHub repo (the runs the merge's push started). The outcome is succeeded, failed, cancelled, waiting-for-approval, missing, none (nothing ran: at once on Azure Pipelines, after 5 minutes on GitHub) or waiting (look again, or conduct wait merge).",
676
+ "Each failed run comes with its failed tasks and the end of its log (logs). For the project's repo, each release in the profile (releases, else work.releases) whose run succeeded gives what it staged (stages): package, version, stageId, the npm stage approve line, and whether npm has it (published).",
677
+ 'Prints JSON. A read, so it runs here.',
678
+ ],
679
+ examples: [
680
+ ['the runs of the merge a1b2c3d', 'runs a1b2c3d'],
681
+ ['its API deploy alone', 'runs a1b2c3d --for deploy-api'],
682
+ ],
683
+ exitCodes: [[1, 'refused or failed'], LOST_LOGIN],
684
+ group: 'Used by work skills',
685
+ name: 'runs',
686
+ options: [
687
+ [
688
+ '--for <a,b>',
689
+ 'the outcome comes from these pipelines alone (YAML file name such as deploy-api, its path, or its name); the rest are listed under others',
690
+ ],
691
+ [
692
+ '--repo <repo>',
693
+ "github:<owner>/<repo> for another repo, such as the registry; the profile's code host by default",
694
+ ],
695
+ ],
696
+ seeAlso: ['wait', 'land'],
697
+ summary: "a merge's runs, their verdict, failed logs and npm stages",
698
+ usage: ['runs <sha> [--for <pipeline>[,<pipeline>…]] [--repo <repo>]'],
699
+ },
700
+ {
701
+ description: ['One daemon per machine. conduct start runs it detached instead.'],
702
+ examples: [['run the daemon in this terminal', 'serve']],
354
703
  group: 'Daemon',
355
704
  name: 'serve',
705
+ seeAlso: ['start', 'supervise', 'stop'],
356
706
  summary: 'run the daemon in this process',
357
707
  usage: ['serve'],
358
708
  },
359
709
  {
360
- details: [
710
+ description: [
361
711
  'Starts the logon task when it runs this build, else a detached conduct supervise --fail-fast, and waits until the daemon answers. Does nothing when one runs already.',
362
712
  ],
713
+ examples: [['start the daemon', 'start']],
363
714
  group: 'Daemon',
364
715
  name: 'start',
716
+ seeAlso: ['stop', 'status', 'serve'],
365
717
  summary: 'start the daemon detached and wait until it answers',
366
718
  usage: ['start'],
367
719
  },
368
720
  {
369
- details: [
370
- "The worktree's branch, how far it is ahead of and behind its base, its uncommitted files, its branch's PR, and the same for each repo checked out under .scratch/repos. --repo shows one.",
371
- "Prints JSON. Runs here; the PR is read as the project's account. Exit 5 for a lost login.",
721
+ description: [
722
+ "The worktree's branch, how far it is ahead of and behind its base, its uncommitted files, its branch's PR, and the same for each repo checked out under .scratch/repos.",
723
+ "Prints JSON. Runs here; the PR is read as the project's account.",
724
+ ],
725
+ examples: [
726
+ ['where this worktree stands', 'state'],
727
+ ['the registry checkout alone', 'state --repo EmergentSoftware/agentic-toolkit-registry'],
372
728
  ],
729
+ exitCodes: [LOST_LOGIN],
373
730
  group: 'Used by work skills',
374
731
  name: 'state',
732
+ options: [['--repo <repo>', 'one checked-out repo only']],
733
+ seeAlso: ['brief', 'checkout', 'pr'],
375
734
  summary: "the worktree's branch, changes and PR",
376
735
  usage: ['state [--repo <repo>]'],
377
736
  },
378
737
  {
379
- details: [
380
- '--json prints it as JSON. With no daemon running, that JSON says running: false, lists the workers from the store, and exits 1.',
738
+ description: [
739
+ "The daemon's pid, port, projects, workers and held locks. With no daemon running, --json says running: false, lists the workers from the store, and exits 1.",
381
740
  ],
741
+ examples: [['is the daemon up, and what it runs', 'status']],
382
742
  group: 'Daemon',
383
743
  name: 'status',
744
+ options: [['--json', 'print it as JSON']],
745
+ seeAlso: ['start', 'queue', 'inbox'],
384
746
  summary: "the daemon's pid, port, projects, workers and held locks",
385
747
  usage: ['status [--json]'],
386
748
  },
387
749
  {
388
- details: [
750
+ description: [
389
751
  'Stops the daemon through its API. When none answers, a conduct supervise may be waiting to restart one: stop asks it to end through supervisor.stop in the conductor home and waits up to 10 s for its answer, then removes the request. A supervisor heeds only a request written after it started and in the last 30 s.',
390
752
  ],
753
+ examples: [['stop the daemon', 'stop']],
391
754
  group: 'Daemon',
392
755
  name: 'stop',
756
+ seeAlso: ['start', 'upgrade'],
393
757
  summary: 'stop the running daemon',
394
758
  usage: ['stop'],
395
759
  },
396
760
  {
397
- details: [
761
+ description: [
398
762
  'Runs conduct serve and starts it again when it dies (a crash, a kill, a start that fails), after 1 s, 5 s, 15 s, 30 s, then every minute, and every 5 minutes once it has pushed. A daemon down 2 minutes sends one Pushover push, held through quiet hours. A stop (conduct stop, upgrade, Ctrl+C), or another daemon holding the lock, ends it. The logon task runs it.',
399
- [['--fail-fast', 'end when the first serve exits before it comes up, as conduct start runs it']],
400
763
  ],
764
+ examples: [['what the logon task runs', 'supervise']],
401
765
  group: 'Daemon',
402
766
  name: 'supervise',
767
+ options: [['--fail-fast', 'end when the first serve exits before it comes up, as conduct start runs it']],
768
+ seeAlso: ['install-startup', 'start', 'serve'],
403
769
  summary: 'run the daemon and restart it when it dies',
404
770
  usage: ['supervise [--fail-fast]'],
405
771
  },
406
772
  {
407
- details: [
773
+ arguments: [['<project>', "a registered project; without it, the checkout you're in"]],
774
+ description: [
408
775
  'Several skill publishes in one merge (the profile\'s "train"). A worker marks its registry PR with a "<work skill> · Train" note ("· Train · urgent: <why>" asks for one now); the daemon asks you once enough wait.',
409
776
  "Takes each marked PR that targets the default branch, has passed its checks and shares no file with another, merges their heads into one train/<stamp> branch on GitHub's side, opens one PR and notes its link on each. Merge it with a merge commit: each member then shows as merged and its worker wakes.",
410
- "<project> is a registered project; without it, the checkout you're in. --dry-run only reads: what waits, and what a train would take and leave out.",
777
+ ],
778
+ examples: [
779
+ ['what a train would take and leave out', 'train --dry-run'],
780
+ ["merge atk's waiting publishes", 'train atk'],
411
781
  ],
412
782
  group: 'Projects and workers',
413
783
  name: 'train',
784
+ options: [['--dry-run', 'only read: what waits, and what a train would take and leave out']],
785
+ seeAlso: ['pr', 'land'],
414
786
  summary: 'merge waiting skill publishes into one registry PR',
415
787
  usage: ['train [<project>] [--dry-run]'],
416
788
  },
417
789
  {
418
- details: [
790
+ arguments: [['<project>', 'a registered project; every project without it']],
791
+ description: [
419
792
  "Ends the project's pause, or without <project> the pause of every project. Unpausing one project leaves a pause of every project in force.",
420
793
  ],
794
+ examples: [
795
+ ['launch again everywhere', 'unpause'],
796
+ ['launch again in atk', 'unpause atk'],
797
+ ],
421
798
  group: 'Scheduling',
422
799
  name: 'unpause',
800
+ seeAlso: ['pause', 'focus'],
423
801
  summary: 'launch again after a pause',
424
802
  usage: ['unpause [<project>]'],
425
803
  },
426
804
  {
427
- details: [
428
- 'Stops the daemon, installs the newest version on the tag with npm, and starts the daemon again. The tag defaults to the one the installed version came from.',
429
- "--allow-downgrade installs the tag's version even when it is older than the installed one.",
805
+ description: ['Stops the daemon, installs the newest version on the tag with npm, and starts the daemon again.'],
806
+ examples: [
807
+ ['the newest version on the same tag', 'upgrade'],
808
+ ['move to the dev tag', 'upgrade --tag dev'],
430
809
  ],
431
810
  group: 'Daemon',
432
811
  name: 'upgrade',
812
+ options: [
813
+ ['--allow-downgrade', "install the tag's version even when it is older than the installed one"],
814
+ ['--tag dev|latest', 'the npm dist-tag; the one the installed version came from by default'],
815
+ ],
816
+ seeAlso: ['stop', 'start', 'install-mod'],
433
817
  summary: 'install the newest version and restart the daemon',
434
818
  usage: ['upgrade [--tag dev|latest] [--allow-downgrade]'],
435
819
  },
436
820
  {
437
- details: [
438
- "In a worker's worktree with the daemon running, registers the wait and exits 0 at once: end the turn, and the daemon resumes the session when the condition holds. Otherwise checks it here and blocks: exit 0 when it holds, 2 at --timeout (minutes, local only; 29 by default). --local checks it here even with the daemon running.",
439
- 'Conditions:',
821
+ conditions: [
822
+ [
823
+ 'pr <n>',
824
+ "the PR's verdict once it isn't waiting: merged, build-failed, conflicts, review-comment, changes-requested, merge-failed, auto-merge-cancelled or abandoned. The daemon keeps a landing conduct land armed while it waits",
825
+ ],
826
+ [
827
+ 'pr <n> merged|checks|comment',
828
+ 'a pull request merged, its checks finished, or a new comment; --repo github:<owner>/<repo> for a GitHub repo',
829
+ ],
830
+ [
831
+ 'merge <sha>',
832
+ "the merge's runs settled, as conduct runs reads them: on Azure Pipelines for the project's repo, on GitHub Actions with --repo github:<owner>/<repo>; --for <a,b> settles on those pipelines alone. The wake names the outcome and each release's staged version; conduct runs has the logs",
833
+ ],
834
+ ['run <pipeline> <run id> [done]', 'a pipeline run finished'],
835
+ [
836
+ 'npm <version>',
837
+ "a version on npm; --tag <dist-tag> (default latest), --package <name> (default the profile's)",
838
+ ],
839
+ ['health <env>', "the environment's health answers --version <x> or --commit <sha>"],
840
+ ['approval [<item>]', "an approve or revise: comment after the item's newest Plan note"],
841
+ [
842
+ 'item <item> <state>',
843
+ 'the item reaches a state; item <item> state|label|note <value> for a state, a label or a note of more than one word',
844
+ ],
440
845
  [
441
- [
442
- 'pr <n>',
443
- "the PR's verdict once it isn't waiting: merged, build-failed, conflicts, review-comment, changes-requested, merge-failed, auto-merge-cancelled or abandoned. The daemon keeps a landing conduct land armed while it waits",
444
- ],
445
- [
446
- 'pr <n> merged|checks|comment',
447
- 'a pull request merged, its checks finished, or a new comment; --repo github:<owner>/<repo> for a GitHub repo',
448
- ],
449
- ['run <pipeline> <run id> [done]', 'a pipeline run finished'],
450
- [
451
- 'npm <version>',
452
- "a version on npm; --tag <dist-tag> (default latest), --package <name> (default the profile's)",
453
- ],
454
- ['health <env>', "the environment's health answers --version <x> or --commit <sha>"],
455
- ['approval [<item>]', "an approve or revise: comment after the item's newest Plan note"],
456
- [
457
- 'item <item> <state>',
458
- 'the item reaches a state; item <item> state|label|note <value> for a state, a label or a note of more than one word',
459
- ],
460
- [
461
- 'login ado|github|linear',
462
- "the project's login to the provider works again (projects.<name> in config.json says which account)",
463
- ],
464
- ['time <when>', 'such as 17:00 (the next one) or 2026-10-10T09:00 (local)'],
465
- ['<full form>', 'such as pr:ado:Emergent.AgenticToolkit#1850:merged'],
846
+ 'login ado|github|linear',
847
+ "the project's login to the provider works again (projects.<name> in config.json says which account)",
848
+ ],
849
+ ['time <when>', 'such as 17:00 (the next one) or 2026-10-10T09:00 (local)'],
850
+ ['<full form>', 'such as pr:ado:Emergent.AgenticToolkit#1850:merged'],
851
+ ],
852
+ description: [
853
+ "In a worker's worktree with the daemon running, registers the wait and exits 0 at once: end the turn, and the daemon resumes the session when the condition holds. Otherwise checks it here and blocks until it holds or --timeout passes.",
854
+ ],
855
+ examples: [
856
+ ['wait for PR 1850 to merge', 'wait pr 1850 merged'],
857
+ ['wait for the plan approval', 'wait approval'],
858
+ ['wait for a dev release on npm', 'wait npm 8.7.0-dev.8 --tag dev --package @detergent-software/atk'],
859
+ ],
860
+ exitCodes: [
861
+ [0, 'the condition holds, or the daemon registered the wait'],
862
+ [2, '--timeout passed (local only)'],
863
+ [
864
+ 5,
865
+ 'not from wait: worker verbs (state, pr, land, reply, runs) exit 5 when they met a lost provider login; the worker then runs conduct wait login <provider> and ends its turn',
466
866
  ],
467
- 'Exit code 5 is for worker verbs (state, pr, land, reply) that met a lost provider login: the worker runs conduct wait login <provider> and ends its turn.',
468
867
  ],
469
868
  group: 'Used by work skills',
470
869
  name: 'wait',
870
+ options: [
871
+ ['--commit <sha>', 'with health: the commit to answer'],
872
+ ['--for <a,b>', 'with merge: settle on these pipelines alone'],
873
+ ['--item <item>', "the item for the condition; this checkout's worker's by default"],
874
+ ['--local', 'check it here even with the daemon running'],
875
+ ['--package <name>', "with npm: the package; the profile's by default"],
876
+ ['--repo <repo>', 'with pr and merge: github:<owner>/<repo> for a GitHub repo'],
877
+ ['--tag <dist-tag>', 'with npm: the dist-tag; latest by default'],
878
+ ['--timeout <minutes>', 'local only: give up after this many minutes; 29 by default'],
879
+ ['--version <x>', 'with health: the version to answer'],
880
+ ['--worker <worker>', "the worker whose wait it is, by name or item; this checkout's by default"],
881
+ ],
882
+ seeAlso: ['land', 'runs', 'brief'],
471
883
  summary: 'wait for an outside event, such as a merge or an approval',
472
884
  usage: ['wait <condition> [--local] [--timeout <minutes>] [--worker <worker>] [--item <item>]'],
473
885
  },
474
886
  {
475
- details: [
476
- "With an event id, that event's rule and the events that led to it; with an item (ATK-312), the same for each of its newest actions.",
887
+ arguments: [['<event id | item>', 'an event id (4200) or an item (ATK-312)']],
888
+ description: [
889
+ "With an event id, that event's rule and the events that led to it; with an item, the same for each of its newest actions.",
890
+ ],
891
+ examples: [
892
+ ['why the conductor acted on ATK-312', 'why ATK-312'],
893
+ ['the rule behind event 4200', 'why 4200'],
477
894
  ],
478
895
  group: 'Console and rules',
479
896
  name: 'why',
897
+ options: [['--json', 'print it as JSON']],
898
+ seeAlso: ['rules', 'events', 'escalations'],
480
899
  summary: 'the rule behind an action, and the events that led to it',
481
900
  usage: ['why <event id | item> [--json]'],
482
901
  },
483
902
  ];
484
- // The rows under the groups, which share their column.
903
+ // The listing's last rows: help itself and the CLI's global options.
485
904
  const HELP_ROWS = [
486
- ['help [<command>]', "this listing, or one command's help (also <command> --help)"],
487
- ['--version', 'the installed version'],
905
+ ['help [<command>]', "this listing, or one command's help"],
906
+ ['-h, --help', 'the same: conduct --help, or conduct <command> --help'],
907
+ ['-v, --version', 'the installed version'],
908
+ ];
909
+ const FOOTER = [
910
+ 'Run conduct help <command> for its options and examples.',
911
+ 'A <worker> is its item (ATK-309) or its name (probe-ATK-309).',
912
+ 'Files live in ~/.conductor (CONDUCTOR_HOME overrides it).',
488
913
  ];
489
- // `conduct help <command>`: the summary, every form, then the details.
490
- export function commandHelp(name, width = terminalWidth()) {
914
+ // A placeholder, `conduct <command>`, or a flag: what prose styles.
915
+ const TOKENS = /(<[^<>]+>)|\b(conduct [a-z][a-z-]*)|(?<![\w-])(--?[a-z][a-z-]*)/g;
916
+ // The text as written: tests, a pipe, NO_COLOR.
917
+ export const plain = (_look, text) => text;
918
+ // `conduct help <command>`: the headline, then each section the command has.
919
+ export function commandHelp(name, width = terminalWidth(), paint = plain) {
491
920
  const command = find(name);
492
- const lines = [...hang(`conduct ${name}: `, command.summary.split(' '), width), '', ...usageLines(command, width)];
493
- for (const detail of command.details)
494
- lines.push('', ...detailLines(detail, width));
921
+ const lines = hang(`conduct ${name} — `, words(command.summary), width).map((l) => prose(l, paint));
922
+ lines.push('', paint('heading', 'USAGE'), ...formLines(command, width, paint));
923
+ for (const paragraph of command.description)
924
+ lines.push('', ...paragraphLines(paragraph, width, paint));
925
+ const table = (heading, rows) => {
926
+ if (rows?.length)
927
+ lines.push('', paint('heading', heading), ...rowLines(rows, width, paint));
928
+ };
929
+ table('COMMANDS', command.commands);
930
+ table('CONDITIONS', command.conditions);
931
+ table('ARGUMENTS', command.arguments);
932
+ table('OPTIONS', command.options);
933
+ lines.push('', paint('heading', 'EXAMPLES'));
934
+ command.examples.forEach(([comment, example], i) => {
935
+ if (i > 0)
936
+ lines.push('');
937
+ const [verb = '', ...args] = words(example);
938
+ const text = [...hang(' # ', words(comment), width), ...hang(` conduct ${verb} `, args, width)];
939
+ lines.push(...text.map((l) => prose(l, paint)));
940
+ });
941
+ table('EXIT CODES', command.exitCodes?.map(([code, text]) => [String(code), text]));
942
+ if (command.seeAlso?.length) {
943
+ const names = command.seeAlso.map((n, i, all) => `conduct ${n}${i < all.length - 1 ? ',' : ''}`);
944
+ lines.push('', paint('heading', 'SEE ALSO'), ...hang(' ', names, width).map((l) => prose(l, paint)));
945
+ }
495
946
  return lines.join('\n');
496
947
  }
497
948
  // The text after `prefix`, word by word, with every line after the first indented to the prefix's
@@ -516,46 +967,63 @@ export function hang(prefix, words, width) {
516
967
  export function isCommand(name) {
517
968
  return COMMANDS.some((c) => c.name === name);
518
969
  }
519
- // `conduct` and `conduct help`: one line per command under its group, A–Z, then the help rows.
520
- export function listing(width = terminalWidth()) {
970
+ // `conduct` and `conduct help`: what conduct is, then one line per command under its group, A–Z,
971
+ // then help and the global options.
972
+ export function listing(about, width = terminalWidth(), paint = plain) {
521
973
  const column = Math.max(...[...COMMANDS.map((c) => c.name), ...HELP_ROWS.map(([term]) => term)].map((t) => t.length)) + 4;
522
- const row = (term, text) => hang(` ${term}`.padEnd(column), text.split(' '), width);
523
- const lines = ['Usage: conduct <command> [options]'];
974
+ const header = `conduct ${about.version}${about.description ? ` · ${about.description}` : ''}`;
975
+ const lines = hang('', words(header), width).map((l) => prose(l, paint));
976
+ lines.push('', paint('heading', 'USAGE'), ` ${term('conduct <command> [options]', paint)}`);
524
977
  for (const group of GROUPS) {
525
- const names = COMMANDS.filter((c) => c.group === group).toSorted((a, b) => a.name.localeCompare(b.name));
526
- lines.push('', group, ...names.flatMap((c) => row(c.name, c.summary)));
978
+ const rows = COMMANDS.filter((c) => c.group === group)
979
+ .toSorted((a, b) => a.name.localeCompare(b.name))
980
+ .map((c) => [c.name, c.summary]);
981
+ lines.push('', paint('heading', group.toUpperCase()), ...rowLines(rows, width, paint, column));
527
982
  }
528
- lines.push('', 'Help', ...HELP_ROWS.flatMap(([term, text]) => row(term, text)));
529
- lines.push('', ...hang('', 'A <worker> is its item (ATK-309) or its name (probe-ATK-309).'.split(' '), width));
530
- lines.push(...hang('', 'Files live in ~/.conductor (CONDUCTOR_HOME overrides it).'.split(' '), width));
983
+ lines.push('', paint('heading', 'HELP'), ...rowLines(HELP_ROWS, width, paint, column));
984
+ lines.push('', ...FOOTER.flatMap((text) => hang('', words(text), width).map((l) => prose(l, paint))));
531
985
  return lines.join('\n');
532
986
  }
533
- // The width to wrap to: the terminal's, or 80 when it isn't one.
987
+ // Styled for `stream`: bold headings, cyan command names and flags, dim placeholders. With
988
+ // validateStream (always, but in a test), plain unless the stream takes color, as NO_COLOR and
989
+ // FORCE_COLOR say.
990
+ export function terminalStyle(stream, validateStream = true) {
991
+ return (look, text) => styleText(LOOKS[look], text, { stream, validateStream });
992
+ }
993
+ // The width to wrap to: the terminal's up to 100, or 80 when it isn't one (a pipe has no columns,
994
+ // and some pseudo-terminals say 0).
534
995
  export function terminalWidth(stream = process.stdout) {
535
- return Math.max(stream.columns ?? DEFAULT_WIDTH, MIN_WIDTH);
996
+ const columns = stream.columns ?? 0;
997
+ return Math.min(Math.max(columns > 0 ? columns : DEFAULT_WIDTH, MIN_WIDTH), MAX_WIDTH);
536
998
  }
537
- // What `conduct help <name>` refuses for a name that is no command.
999
+ // What `conduct help <name>` refuses for a name that is no command, with the nearest command when
1000
+ // one is a typo away.
538
1001
  export function unknownCommand(name) {
539
- return new ConductorError(`unknown command "${name}"`, ['run `conduct help` for the commands']);
1002
+ const near = nearest(name);
1003
+ return new ConductorError(`unknown command "${name}"`, [
1004
+ ...(near === undefined ? [] : [`did you mean \`conduct ${near}\`?`]),
1005
+ 'run `conduct help` for the commands',
1006
+ ]);
540
1007
  }
541
1008
  // A usage error for one command: the message, then its forms and where its help is. Never the
542
- // whole listing. The CLI prints the details indented by two.
1009
+ // whole listing, and never styled: workers read it. The CLI prints the details indented by two.
543
1010
  export function usageError(name, message = `wrong or missing arguments for conduct ${name}`) {
544
1011
  return new ConductorError(message, [
545
1012
  ...usageLines(find(name), terminalWidth(process.stderr) - 2),
546
1013
  `run \`conduct help ${name}\` for the details`,
547
1014
  ]);
548
1015
  }
549
- function detailLines(detail, width) {
550
- if (typeof detail === 'string')
551
- return hang('', detail.split(' '), width);
552
- if ('pre' in detail)
553
- return detail.pre.map((line) => ` ${line}`);
554
- const short = detail.map(([term]) => term.length).filter((n) => n <= MAX_TERM);
555
- const column = 2 + Math.max(0, ...short) + 2;
556
- return detail.flatMap(([term, text]) => term.length <= MAX_TERM
557
- ? hang(` ${term}`.padEnd(column), text.split(' '), width)
558
- : [` ${term}`, ...hang(' '.repeat(column), text.split(' '), width)]);
1016
+ // Levenshtein: the fewest single-character edits from `a` to `b`.
1017
+ function editDistance(a, b) {
1018
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
1019
+ for (const [i, ca] of [...a].entries()) {
1020
+ const current = [i + 1];
1021
+ for (const [j, cb] of [...b].entries()) {
1022
+ current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (ca === cb ? 0 : 1)));
1023
+ }
1024
+ previous = current;
1025
+ }
1026
+ return previous[b.length] ?? 0;
559
1027
  }
560
1028
  function find(name) {
561
1029
  const command = COMMANDS.find((c) => c.name === name);
@@ -563,6 +1031,59 @@ function find(name) {
563
1031
  throw new Error(`no help for ${name}`);
564
1032
  return command;
565
1033
  }
1034
+ // USAGE: each form under the heading.
1035
+ function formLines(command, width, paint) {
1036
+ return forms(command, width, () => ' ').flatMap(([first = '', ...more]) => [
1037
+ ` ${term(first.slice(2), paint)}`,
1038
+ ...more.map((l) => prose(l, paint)),
1039
+ ]);
1040
+ }
1041
+ // Each form as `<head>conduct <form>`, a long one wrapped under its first argument.
1042
+ function forms(command, width, head) {
1043
+ return command.usage.map((form, i) => {
1044
+ const [name = '', ...rest] = synopsisWords(form);
1045
+ return hang(`${head(i)}conduct ${name} `, rest, width);
1046
+ });
1047
+ }
1048
+ // The command a typo is closest to, within 2 edits and fewer edits than it has letters.
1049
+ function nearest(name) {
1050
+ let best;
1051
+ for (const candidate of [...COMMANDS.map((c) => c.name), 'help'].toSorted()) {
1052
+ const distance = editDistance(name, candidate);
1053
+ if (distance <= 2 && distance < name.length && (best === undefined || distance < best.distance))
1054
+ best = { distance, name: candidate };
1055
+ }
1056
+ return best?.name;
1057
+ }
1058
+ function paragraphLines(paragraph, width, paint) {
1059
+ if (typeof paragraph === 'string')
1060
+ return hang(' ', words(paragraph), width).map((l) => prose(l, paint));
1061
+ return paragraph.pre.map((line) => ` ${line}`);
1062
+ }
1063
+ // Text with its placeholders dim, and `conduct <command>` and flags cyan.
1064
+ function prose(text, paint) {
1065
+ return text.replace(TOKENS, (match, placeholder, command, flag) => {
1066
+ if (placeholder !== undefined)
1067
+ return paint('placeholder', placeholder);
1068
+ if (flag !== undefined)
1069
+ return paint('command', flag);
1070
+ const name = command?.slice('conduct '.length);
1071
+ return name === 'help' || isCommand(name) ? paint('command', match) : match;
1072
+ });
1073
+ }
1074
+ // Rows of a term and its description, the descriptions in one column: the listing's, else two past
1075
+ // the longest term up to MAX_TERM. A term too long for it stands on its own line, with its
1076
+ // description under it in that column.
1077
+ function rowLines(rows, width, paint, column) {
1078
+ const at = column ?? 2 + Math.max(0, ...rows.map(([t]) => t.length).filter((n) => n <= MAX_TERM)) + 2;
1079
+ return rows.flatMap(([t, text]) => {
1080
+ if (t.length > at - 4) {
1081
+ return [` ${term(t, paint)}`, ...hang(' '.repeat(at), words(text), width).map((l) => prose(l, paint))];
1082
+ }
1083
+ const [first = '', ...more] = hang(` ${t}`.padEnd(at), words(text), width);
1084
+ return [` ${term(t, paint)}${prose(first.slice(2 + t.length), paint)}`, ...more.map((l) => prose(l, paint))];
1085
+ });
1086
+ }
566
1087
  // A form split at its top-level spaces, so `[--since <when>]` stays on one line.
567
1088
  function synopsisWords(form) {
568
1089
  const words = [];
@@ -583,12 +1104,20 @@ function synopsisWords(form) {
583
1104
  }
584
1105
  return [...words, word];
585
1106
  }
586
- // `usage: conduct <form>`, one line per form, a long form wrapped under its first argument.
1107
+ // A term, such as a form or a row's: its leading words (the command and sub-command names) cyan,
1108
+ // then the rest as prose.
1109
+ function term(text, paint) {
1110
+ const parts = synopsisWords(text);
1111
+ const lead = parts.findIndex((w) => !/^[a-z]/.test(w));
1112
+ const names = parts.slice(0, lead === -1 ? parts.length : lead).join(' ');
1113
+ const rest = parts.slice(lead === -1 ? parts.length : lead).join(' ');
1114
+ return [names === '' ? '' : paint('command', names), prose(rest, paint)].filter((p) => p !== '').join(' ');
1115
+ }
1116
+ // `usage: conduct <form>`, then the other forms under it.
587
1117
  function usageLines(command, width) {
588
- return command.usage.flatMap((form, i) => {
589
- const [name = '', ...words] = synopsisWords(form);
590
- const head = `${i === 0 ? 'usage: ' : ' '}conduct ${name} `;
591
- return hang(head, words, width);
592
- });
1118
+ return forms(command, width, (i) => (i === 0 ? 'usage: ' : ' ')).flat();
1119
+ }
1120
+ function words(text) {
1121
+ return text.split(' ');
593
1122
  }
594
1123
  //# sourceMappingURL=help.js.map