cawdev-cli 1.0.11-beta → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,4 +10,14 @@ https://github.com/4dali/cawdev-cli
10
10
 
11
11
  `jira/import.mjs` brings a Jira Cloud project's issues, epics, sprints and
12
12
  comments into a cawdev project, once and again without duplicates (R337). Run from a checkout; see
13
- "Leaving Jira" in GUIDE.md. Its tests: `node --test "tools/jira/*.test.mjs"`.
13
+ "Leaving Jira" in GUIDE.md. `--team <team id>` (R489) names the team the sprints open in, when the project is
14
+ shared with several writer teams. Its tests: `node --test "tools/jira/*.test.mjs"`.
15
+
16
+ ## The `Sprint:` line
17
+
18
+ A card in a sprint carries one `Sprint:` line in `ROADMAP.md` and `ISSUES.md`
19
+ (`lib/roadmap-format.mjs`): `Sprint: Core team · S7 Notifications` since sprints
20
+ became a team's (R489). Files written before that, `Sprint: S7 Notifications`,
21
+ still parse (with no team). `roadmap/import.mjs` sends the words
22
+ (`Core team · S7`) for the first form and the number for the second, so a file
23
+ naming a sprint the target does not have is refused with the API's own sentence.
@@ -332,14 +332,24 @@ export function parseRoadmap(text) {
332
332
  entry.origin = origin[1].trim();
333
333
  continue;
334
334
  }
335
- const sprint = /^Sprint:\s*S(\d+)\s*(.*)$/.exec(line.trim());
335
+ const sprint = /^Sprint:\s*(?:(?!S\d+(?:\s|$))(.+?)\s*·\s*)?S(\d+)\s*(.*)$/.exec(line.trim());
336
336
  if (sprint) {
337
- // R257: `Sprint: S1 Notifications` — the sprint's number and its name
338
- // as the file says it. The same shape the API's `sprint` has, so a
339
- // re-render from either is byte-identical; the importer sends only
340
- // the number, since a sprint's name is the database's to say.
341
- const number = Number(sprint[1]);
342
- entry.sprint = { number, ref: `S${number}`, name: sprint[2].trim() };
337
+ // R257, R489: `Sprint: Core team · S1 Notifications` — a team's
338
+ // sprint, its number and its name as the file says it. A file written
339
+ // before sprints were teams' says `Sprint: S1 Notifications`; that
340
+ // still parses, with no team. The same shape the API's `sprint` has
341
+ // (`label`, `ref`, `number`, `name`), so a re-render from either is
342
+ // byte-identical; the importer sends the team's words and the number,
343
+ // since a sprint's name is the database's to say.
344
+ const number = Number(sprint[2]);
345
+ const team = sprint[1]?.trim() || null;
346
+ entry.sprint = {
347
+ team,
348
+ number,
349
+ ref: `S${number}`,
350
+ label: team ? `${team} · S${number}` : `S${number}`,
351
+ name: sprint[3].trim(),
352
+ };
343
353
  continue;
344
354
  }
345
355
  bodyLines.push(line);
@@ -619,7 +629,10 @@ function renderEntry(entry) {
619
629
  // API's `sprint` and the parser's are the same shape, which is what makes
620
630
  // render → parse → render a fixed point. State is not written — the file
621
631
  // says which sprint, the database says whether it is still open.
622
- lines.push(`Sprint: ${entry.sprint.ref ?? `S${entry.sprint.number}`} ${entry.sprint.name}`.trimEnd());
632
+ // R489: the label (`Core team · S7`) is the only spelling that means
633
+ // anything outside the team; `ref` alone is the pre-teams file's.
634
+ const label = entry.sprint.label ?? entry.sprint.ref ?? `S${entry.sprint.number}`;
635
+ lines.push(`Sprint: ${label} ${entry.sprint.name ?? ''}`.trimEnd());
623
636
  }
624
637
 
625
638
  if (entry.body?.trim()) {
@@ -75,6 +75,8 @@ export const CAWDEV_READS = new Set([
75
75
  'quality_get',
76
76
  'roadmap_statuses',
77
77
  'roadmap_list',
78
+ // R489. The project's sprints, read from the endpoint a card's picker uses.
79
+ 'sprint_list',
78
80
  'roadmap_get',
79
81
  'issue_list',
80
82
  'changelog_list',
package/mcp/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  Plain Node, zero dependencies, stdio JSON-RPC — `server.mjs`, plus the shared
4
4
  rule matcher in `tools/lib/tool-rules.mjs` that the runner uses too. It gives a coding
5
5
  agent a project's roadmap and changelog — the same verbs a person gets in the
6
- console, because the API was built to mirror these tools one-for-one.
6
+ console, because the API was built to mirror these tools one-for-one. This README says
7
+ **card** for the roadmap thing and **run** for the work on it, as the console does; the API and
8
+ the tool names say *entry* (`roadmap_list`, `propose_entry`, `EntryRef`) and keep doing so.
7
9
 
8
10
  ## Setting it up in a repository
9
11
 
@@ -59,23 +61,23 @@ four being different from what you assumed.
59
61
 
60
62
  The sequence is fixed, and the second step is the one people skip:
61
63
 
62
- 1. Branch off an up-to-date `main`, named after the entry — `r4-roadmap-entries`.
63
- 2. **Move the entry to `CODING` naming the branch, before your first commit**
64
+ 1. Branch off an up-to-date `main`, named after the card — `r4-roadmap-entries`.
65
+ 2. **Move the card to `IN_DEVELOPMENT` naming the branch, before your first commit**
64
66
  (`roadmap_set_status`). Not after. The roadmap should be able to answer
65
67
  "what is being worked on right now" without asking anyone.
66
- 3. Build to the entry's "Done when" list.
68
+ 3. Build to the card's "Done when" list.
67
69
  4. Finish with a PR. Never push to `main`. As your session ends the platform
68
70
  moves the *branch* on the development board, not the card: to `REVIEW` where
69
71
  the project runs `require_review` and somebody is going to read it, and to
70
72
  `DONE` where it does not — a finish nobody was asked about (R132), because
71
73
  there is no reviewer coming.
72
- 5. When the PR merges, move the entry to `MERGED` naming it. The branch may then
73
- be deleted — `CODING` is for work somebody is doing, not work that is done.
74
+ 5. When the PR merges, move the card to `MERGED` naming it. The branch may then
75
+ be deleted — `IN_DEVELOPMENT` is for work somebody is doing, not work that is done.
74
76
 
75
77
  ### What a status must carry
76
78
 
77
79
  `roadmap_statuses` will tell you, and it is worth reading rather than guessing:
78
- `CODING` needs a branch, `MERGED` needs the merge — a pull request, a merge
80
+ `IN_DEVELOPMENT` needs a branch, `MERGED` needs the merge — a pull request, a merge
79
81
  commit, or a sha — `SHIPPED` needs a version that is a real git tag, and
80
82
  `DECLINED` needs a reason. `REVIEW` needs nothing, deliberately: the pull
81
83
  request it would otherwise carry belongs to the run, and a project that reviews
@@ -85,19 +87,29 @@ without opening pull requests has none to give it.
85
87
  when you name no status is the project's rule, per door, and the default for a
86
88
  card or issue an agent files is the backlog: waiting until a person or a sprint
87
89
  brings it in, in no sprint and on no board column. Its origin names your
88
- session and the card your run was on. A plan session's card lands where the console's
90
+ session and the card your run was on. A plan run's card lands where the console's
89
91
  do (R380). Name a status (`CONSIDERING`, `NEW`) when
90
92
  you mean the pipeline. Giving a backlog card a sprint moves it to the first
91
93
  status of its kind; moving a card to `BACKLOG` takes it out of its sprint.
92
94
 
93
- **Any status may move to any other.** There is no transition diagram — an entry
95
+ **Sprints are a team's** (R474, R489). The `sprint` of `roadmap_create`,
96
+ `roadmap_update` and the filter of `roadmap_list` take an integer or a string:
97
+ `7` or `"S7"` when the project's teams have one S7, `"Core team · S7"` (or
98
+ `"Core team / S7"`) when two do — the API refuses an ambiguous `S7` naming both
99
+ spellings, so read the sentence and send the long one — a sprint id, and `0` or
100
+ `""` to take a card out. A completed sprint refuses a card that is not already
101
+ in it, and only a person can reopen one; do not retry. `sprint_list` says which
102
+ sprints there are. A card prints its sprint as `Core team · S7 Name (active)`.
103
+ Opening, starting, completing and renaming a sprint stay a person's.
104
+
105
+ **Any status may move to any other.** There is no transition diagram — a card
94
106
  really can go from `CONSIDERING` straight to `SHIPPED` if that is what happened.
95
107
  The rules are about what a status *carries*, not the path it took.
96
108
 
97
109
  ### The body is what we want; the plan is how; the comments are the argument
98
110
 
99
111
  `roadmap_get` gives you all three, and `task_current` gives you them for the
100
- entry you are working on.
112
+ card you are working on.
101
113
 
102
114
  **The plan is the one to read before doing any of the work.** It was written by
103
115
  a plan phase and agreed by somebody, and it names the files — R124. If you are
@@ -110,8 +122,8 @@ either. The first is the platform's — it stores what a plan phase reports when
110
122
  the phase ends, so that a session which must not write is not handed a writer to
111
123
  do its own bookkeeping with. The second is refused outright: *an agent cannot
112
124
  start another agent*, which is `RunAccess.requireCanStart`'s rule and not an
113
- omission here. **Read the discussion before you propose anything about an
114
- entry.** It is where an objection was answered and where an obvious-looking
125
+ omission here. **Read the discussion before you propose anything about a
126
+ card.** It is where an objection was answered and where an obvious-looking
115
127
  approach was ruled out with a reason — and re-proposing what was talked out
116
128
  three months ago is precisely what it exists to stop.
117
129
 
@@ -123,7 +135,7 @@ that the next reader does not have to reconstruct it from the thread.
123
135
  A comment you write is attributed to your run as well as to the account that
124
136
  minted your token, so a person can tell an agent's reading from a colleague's.
125
137
 
126
- ### Read what the earlier sessions did before repeating it
138
+ ### Read what the earlier runs did before repeating it
127
139
 
128
140
  `task_current` lists **every run this card has already had** — how each ended,
129
141
  on what branch, with what model, whether the work was pushed, and the commits it
@@ -133,21 +145,21 @@ to know what the first tried.
133
145
 
134
146
  No transcripts come with it, deliberately: the terminal log is on the run's own
135
147
  page in the console, and nine of them would fill the context this call exists to
136
- orient. If you need the detail, the discussion under the entry is where a
137
- previous session should have written down what it learned — and where yours
148
+ orient. If you need the detail, the discussion under the card is where a
149
+ previous run should have written down what it learned — and where yours
138
150
  should.
139
151
 
140
152
  ### There is no delete
141
153
 
142
154
  Not in these tools, not in the API. `roadmap_decline` with a reason is the only
143
- exit an entry has, and the reason is the point: it is what stops the same idea
155
+ exit a card has, and the reason is the point: it is what stops the same idea
144
156
  being proposed again in six months. Same for changelog entries — correct the
145
157
  text, do not remove the record. Same for comments: nothing removes one, and the
146
158
  most you can do to your own is correct its wording.
147
159
 
148
160
  ### Ids are permanent, and a card is named by its ref
149
161
 
150
- An entry's number never changes and is never reused. Commit messages and code
162
+ A card's number never changes and is never reused. Commit messages and code
151
163
  comments point at it. Since R221 a roadmap card and an issue each count on
152
164
  their own sequence, so `R91` and `i91` are two cards: wherever a tool takes
153
165
  `number`, `related`, `after` or `entryNumber`, pass the ref — `"i91"` for an
@@ -169,13 +181,14 @@ what you are allowed to do.
169
181
  |---|---|
170
182
  | `roadmap_where` | Which platform, which token, read from where, and who you are. Start here. |
171
183
  | `roadmap_statuses` | The eight statuses, what they mean, what each requires. |
172
- | `roadmap_list` | Entries, optionally by status. `brief` omits bodies — use it to survey. |
173
- | `roadmap_get` | One entry in full: its body, **the plan agreed for it**, and the discussion on it. |
174
- | `roadmap_create` | Create an entry; the platform allocates its permanent number. |
175
- | `roadmap_update` | Title, body, section, related ids, the cards it starts coding after (`after` — R181), when it is due and how big it is (`due` — R351: an ISO date, `""` clears; `estimate`: a positive number, `0` clears), the version it is meant for (`targetVersion` — R358, `""` clears; since R362 it must be one the project defined under Project settings → Versions, and any other name is refused), the versions an issue was found in (`affectedVersions` — R378, issues only: names the project defined or shipped in, `[]` clears), and who it is assigned to (`assignee` — R331: a username, `""` clears; `roadmap_list`/`issue_list` take `assignee` too, and `me` means the token's owner). |
176
- | `roadmap_comment` | Say something *beside* an entry rather than inside it. |
184
+ | `roadmap_list` | Cards, optionally by status or `sprint` (any spelling below). `brief` omits bodies — use it to survey. |
185
+ | `sprint_list` | The project's sprints with how far each has got (R489): `Core team · S7 Notifications — active — 4 of 9 landed`. ACTIVE and UPCOMING unless `state` says otherwise. A read: PLAN and VERIFY have it. |
186
+ | `roadmap_get` | One card in full: its body, **the plan agreed for it**, and the discussion on it. |
187
+ | `roadmap_create` | Create a card; the platform allocates its permanent number. |
188
+ | `roadmap_update` | Title, body, section, sprint (R489: `7`, `S7`, `"Core team · S7"`, a sprint id; `0` or `""` takes the card out — see below), related ids, the cards it starts coding after (`after` — R181), when it is due and how big it is (`due` — R351: an ISO date, `""` clears; `estimate`: a positive number, `0` clears), the version it is meant for (`targetVersion` — R358, `""` clears; since R362 it must be one the project defined under Project settings → Versions, and any other name is refused), the versions an issue was found in (`affectedVersions` — R378, issues only: names the project defined or shipped in, `[]` clears), and who it is assigned to (`assignee` — R331: a username, `""` clears; `roadmap_list`/`issue_list` take `assignee` too, and `me` means the token's owner). |
189
+ | `roadmap_comment` | Say something *beside* a card rather than inside it. |
177
190
  | `done_when_tick` | Tick (or untick) one numbered item of the card's Done-when checklist as built, with one line of evidence (R368). `task_current` lists the items. Defaults to the run's own card and refuses another; the reviewer's column is a person's and no token can set it. |
178
- | `roadmap_set_status` | Move an entry, carrying whatever the status requires. |
191
+ | `roadmap_set_status` | Move a card, carrying whatever the status requires. |
179
192
  | `roadmap_decline` | Decline with a reason. The only exit. |
180
193
  | `backlog_file` | File **feedback**, not a card — something a person should decide about. A WRITER accepts it as a card or an issue, or refuses it (R199). Tagged `agent` as a source, plus any `tags` you pass — `customer` when you relay what a user said (R372). |
181
194
  | `changelog_list` | The changelog, grouped by release, newest first. |
@@ -192,8 +205,8 @@ token gets a refusal saying so.
192
205
 
193
206
  | Tool | What it does |
194
207
  |---|---|
195
- | `task_current` | The entry you are working on, its branch, everything already said and asked on this run, **and every earlier run on the same card**. **Call it first**, and again whenever you are unsure where you are. |
196
- | `report` | `progress` as often as useful; `done` when finished, naming the branch and any PR; `blocked` when a person must resolve something. `done` and `blocked` end the run; on a coding run `done` is refused until everything is committed and pushed (R345). |
208
+ | `task_current` | The card you are working on, its branch, everything already said and asked on this run, **and every earlier run on the same card**. **Call it first**, and again whenever you are unsure where you are. |
209
+ | `report` | `progress` as often as useful; `done` when finished, naming the branch and any PR; `blocked` when a person must resolve something. `done` and `blocked` end the run; on a review run the `done` body is filed as the branch's review (R529), so write it for the person reading the thread; on a coding run `done` is refused until everything is committed and pushed (R345). |
197
210
  | `ask_user` | Ask the person who started the run, and wait. Blocks up to ten minutes, then hands back a `question_id`. |
198
211
  | `await_answer` | Resume waiting for a question `ask_user` handed back. |
199
212
  | `ask_group` | Ask a **round** — up to twelve questions that belong together, under one title. Blocks until every one of them has been answered, then hands back a `group_id`. |
@@ -202,7 +215,7 @@ token gets a refusal saying so.
202
215
  | `approve` | **Not yours to call.** Claude Code calls it itself, as `--permission-prompt-tool`, when no rule covers a tool call. |
203
216
 
204
217
  **`ask_user` is for decisions that are genuinely theirs** — an architectural
205
- choice, a trade-off with no right answer, something the entry does not settle.
218
+ choice, a trade-off with no right answer, something the card does not settle.
206
219
  Not for checking work you can check yourself. Every question stops the run and
207
220
  costs somebody's attention.
208
221
 
@@ -213,7 +226,7 @@ asked because the decision was not yours. Call `await_answer`, or `report`
213
226
  **`ask_group` is for when you have several questions at once** — R96. One
214
227
  question at a time is right for a decision you hit halfway through a piece of
215
228
  work; it is wrong when you are trying to understand something, because forty
216
- separate inbox items arriving over an afternoon is not a conversation, it is an
229
+ separate Waiting on you items arriving over an afternoon is not a conversation, it is an
217
230
  interruption repeated forty times. A round arrives as one form under one title,
218
231
  the person answers it in one sitting, and you are woken once when all of it is
219
232
  answered.
@@ -259,9 +272,9 @@ Two smoke tests, both driving the server over real stdio.
259
272
  CAWDEV_URL=http://localhost:4200 CAWDEV_TOKEN=cawd_… node tools/mcp/smoke.mjs scratch-project
260
273
  ```
261
274
 
262
- **Name a scratch project.** The smoke test creates a roadmap entry and declines
263
- it, and entries cannot be deleted — so it refuses to guess which project you
264
- meant. It left two declined entries in cawdev's own roadmap before it did.
275
+ **Name a scratch project.** The smoke test creates a roadmap card and declines
276
+ it, and cards cannot be deleted — so it refuses to guess which project you
277
+ meant. It left two declined cards in cawdev's own roadmap before it did.
265
278
 
266
279
  It drives the server the way a client does — spawn, write lines to stdin, read
267
280
  lines from stdout — rather than importing its functions, because the parts most
package/mcp/server.mjs CHANGED
@@ -375,6 +375,41 @@ const TOOLS = [
375
375
  },
376
376
  },
377
377
 
378
+ {
379
+ name: 'sprint_list',
380
+ description:
381
+ 'The sprints a project\'s cards can be filed into, with how far each has got — R489. Sprints ' +
382
+ 'are a team\'s: written `S7` inside the team and `Core team · S7` outside it. Read this before ' +
383
+ 'filing a card into one with roadmap_create / roadmap_update `sprint`. State narrows it ' +
384
+ '(ACTIVE, UPCOMING, COMPLETED; comma-separated); the default is ACTIVE and UPCOMING.',
385
+ inputSchema: {
386
+ type: 'object',
387
+ properties: {
388
+ ...PROJECT_ARGUMENT,
389
+ state: {
390
+ type: 'string',
391
+ description: 'ACTIVE, UPCOMING, COMPLETED — one or several, comma-separated. Default ACTIVE,UPCOMING.',
392
+ },
393
+ },
394
+ },
395
+ handler: async (config, args) => {
396
+ const slug = await resolveProject(config, args.project);
397
+ const state = args.state || 'ACTIVE,UPCOMING';
398
+ const sprints = await api(config, `/api/projects/${slug}/sprints?state=${encodeURIComponent(state)}`);
399
+ if (!sprints.length) return `${slug} has no sprints in ${state}.`;
400
+ return sprints
401
+ .map((sprint) => {
402
+ const counts = sprint.counts ?? {};
403
+ return (
404
+ `${sprint.label ?? sprint.ref}${sprint.name ? ` ${sprint.name}` : ''} — ` +
405
+ `${String(sprint.state).toLowerCase()} — ${counts.landed ?? 0} of ${counts.cards ?? 0} landed` +
406
+ (sprint.goal ? `\n goal: ${sprint.goal}` : '')
407
+ );
408
+ })
409
+ .join('\n');
410
+ },
411
+ },
412
+
378
413
  {
379
414
  name: 'roadmap_statuses',
380
415
  description:
@@ -396,18 +431,17 @@ const TOOLS = [
396
431
  {
397
432
  name: 'roadmap_list',
398
433
  description:
399
- 'A project\'s roadmap entries, optionally filtered by status or by sprint. Use brief=true ' +
400
- 'to survey without pulling every entry\'s body into context.',
434
+ 'A project\'s roadmap cards, optionally filtered by status or by sprint. Use brief=true ' +
435
+ 'to survey without pulling every card\'s body into context.',
401
436
  inputSchema: {
402
437
  type: 'object',
403
438
  properties: {
404
439
  ...PROJECT_ARGUMENT,
405
440
  sprint: {
406
- type: 'integer',
407
- minimum: 1,
441
+ type: ['integer', 'string'],
408
442
  description:
409
- 'Only cards in this sprint, by number — 1 for S1. A number the project has no ' +
410
- 'sprint for is refused naming the ones it has.',
443
+ 'Only cards in this sprint: `S7` (or 7), "Core team · S7" when two teams have an S7, or a ' +
444
+ 'sprint id. A sprint the project has none for is refused naming the ones it has.',
411
445
  },
412
446
  status: {
413
447
  type: 'string',
@@ -439,7 +473,9 @@ const TOOLS = [
439
473
  if (args.status) query.set('status', args.status);
440
474
  // R257. The API refuses a number with no sprint behind it, naming the
441
475
  // sprints there are; that sentence is passed through as the tool error.
442
- if (args.sprint) query.set('sprint', String(args.sprint));
476
+ if (args.sprint !== undefined && args.sprint !== '' && args.sprint !== 0) {
477
+ query.set('sprint', String(args.sprint));
478
+ }
443
479
  if (args.assignee) query.set('assignee', args.assignee);
444
480
  query.set('brief', String(brief));
445
481
 
@@ -454,7 +490,7 @@ const TOOLS = [
454
490
  {
455
491
  name: 'roadmap_get',
456
492
  description:
457
- 'One roadmap entry in full: its body, THE PLAN somebody agreed for it, and the ' +
493
+ 'One roadmap card in full: its body, THE PLAN somebody agreed for it, and the ' +
458
494
  'discussion on it. Read the plan before doing any of the work — it is what was decided, ' +
459
495
  'and it names the files. Read the comments before proposing anything: they are where an ' +
460
496
  'objection was answered, and re-proposing what was talked out three months ago is the ' +
@@ -594,7 +630,7 @@ const TOOLS = [
594
630
  {
595
631
  name: 'roadmap_comment',
596
632
  description:
597
- 'Say something about an entry, beside the entry rather than inside it. Use it for the ' +
633
+ 'Say something about a card, beside the card rather than inside it. Use it for the ' +
598
634
  'argument: an objection, a measurement, why an obvious approach was not taken. The body ' +
599
635
  'is where a settled conclusion is written down — edit that when the discussion reaches ' +
600
636
  'one. Comments cannot be deleted, by you or by anyone.',
@@ -680,12 +716,12 @@ const TOOLS = [
680
716
  {
681
717
  name: 'roadmap_create',
682
718
  description:
683
- 'Create a roadmap entry. The platform allocates its permanent number — written R91 on ' +
684
- 'a roadmap card and i91 on an issue, one sequence across both — and the ' +
719
+ 'Create a roadmap card. The platform allocates its permanent number — written R91 on ' +
720
+ 'a roadmap card and i91 on an issue, two sequences, one per kind — and the ' +
685
721
  'project decides where a card you file starts — the BACKLOG by default for a card an ' +
686
722
  'agent files, waiting until a person or a sprint brings it in — unless you say ' +
687
723
  'otherwise; a status that requires something must be given it. A card in the BACKLOG ' +
688
- 'holds no sprint: giving it one moves it to CONSIDERING. A plan session\'s card lands ' +
724
+ 'holds no sprint: giving it one moves it to CONSIDERING. A plan run\'s card lands ' +
689
725
  'where the console\'s do (R380).',
690
726
  inputSchema: {
691
727
  type: 'object',
@@ -714,12 +750,12 @@ const TOOLS = [
714
750
  reason: { type: 'string' },
715
751
  section: { type: 'string', description: 'Which part of the roadmap, e.g. "Phase 2 — …".' },
716
752
  sprint: {
717
- type: 'integer',
718
- minimum: 0,
753
+ type: ['integer', 'string'],
719
754
  description:
720
- 'The sprint to file this card into, by number — 1 for S1. 0 takes it out of its ' +
721
- 'sprint; leave it out to leave it alone. A CLOSED sprint refuses a card that is ' +
722
- 'not already in it, and only a person can reopen one — do not retry.',
755
+ 'The sprint to file this card into: `S7` (or 7) when the project\'s teams have one S7, ' +
756
+ '"Core team · S7" when two teams do, a sprint id, or 0 / "" to take it out of its sprint; ' +
757
+ 'leave it out to leave it alone. A COMPLETED sprint refuses a card that is not already in it, ' +
758
+ 'and only a person can reopen one — do not retry. Sprint_list shows what there is.',
723
759
  },
724
760
  related: {
725
761
  type: 'array',
@@ -929,7 +965,7 @@ const TOOLS = [
929
965
  {
930
966
  name: 'roadmap_update',
931
967
  description:
932
- 'Edit an entry\'s title, body, section, sprint, tags, parent, related refs or the cards it ' +
968
+ 'Edit a card\'s title, body, section, sprint, tags, parent, related refs or the cards it ' +
933
969
  'starts after. Use roadmap_set_status to move it.',
934
970
  inputSchema: {
935
971
  type: 'object',
@@ -943,12 +979,12 @@ const TOOLS = [
943
979
  body: { type: 'string' },
944
980
  section: { type: 'string' },
945
981
  sprint: {
946
- type: 'integer',
947
- minimum: 0,
982
+ type: ['integer', 'string'],
948
983
  description:
949
- 'The sprint to file this card into, by number — 1 for S1. 0 takes it out of its ' +
950
- 'sprint; leave it out to leave it alone. A CLOSED sprint refuses a card that is ' +
951
- 'not already in it, and only a person can reopen one — do not retry.',
984
+ 'The sprint to file this card into: `S7` (or 7) when the project\'s teams have one S7, ' +
985
+ '"Core team · S7" when two teams do, a sprint id, or 0 / "" to take it out of its sprint; ' +
986
+ 'leave it out to leave it alone. A COMPLETED sprint refuses a card that is not already in it, ' +
987
+ 'and only a person can reopen one — do not retry. Sprint_list shows what there is.',
952
988
  },
953
989
  related: {
954
990
  type: 'array',
@@ -1027,7 +1063,7 @@ const TOOLS = [
1027
1063
  {
1028
1064
  name: 'roadmap_set_status',
1029
1065
  description:
1030
- 'Move an entry to a status. Any status may move to any other — the rules are about what ' +
1066
+ 'Move a card to a status. Any status may move to any other — the rules are about what ' +
1031
1067
  'a status must carry, not a permitted path. IN_DEVELOPMENT needs a branch, MERGED the ' +
1032
1068
  'merge — the PR, the merge commit or the sha — and SHIPPED a version. Move a card to ' +
1033
1069
  'MERGED when its pull request lands; the branch may then be deleted. ' +
@@ -1076,7 +1112,7 @@ const TOOLS = [
1076
1112
  {
1077
1113
  name: 'roadmap_decline',
1078
1114
  description:
1079
- 'Decline an entry, with a reason. This is the only exit an entry has — there is no delete. ' +
1115
+ 'Decline a card, with a reason. This is the only exit a card has — there is no delete. ' +
1080
1116
  'The reason is the point: it stops the idea being proposed again.',
1081
1117
  inputSchema: {
1082
1118
  type: 'object',
@@ -1153,10 +1189,10 @@ const TOOLS = [
1153
1189
  entryNumber: {
1154
1190
  type: ['string', 'integer'],
1155
1191
  description:
1156
- 'The roadmap card or issue this entry describes — R156 — by ref: R91, i91, or a '
1192
+ 'The roadmap card or issue this changelog entry describes — R156 — by ref: R91, i91, or a '
1157
1193
  + 'bare number for a roadmap card (R221). Set it. Until R156 the only link was the '
1158
1194
  + 'ref you type into the prose, which cannot be queried, so a release could not '
1159
- + 'tell whether a card already had an entry and would write a second one beside '
1195
+ + 'tell whether a card already had a changelog entry and would write a second one beside '
1160
1196
  + 'the first. Put the ref in the text as well; that is what a reader sees.',
1161
1197
  },
1162
1198
  },
@@ -1189,8 +1225,8 @@ const TOOLS = [
1189
1225
  entryNumber: {
1190
1226
  type: ['string', 'integer'],
1191
1227
  description:
1192
- 'The roadmap card or issue this entry describes — R156 — by ref (R91, i91; a bare '
1193
- + 'number is a roadmap card). Set it on an older entry that has never been linked; '
1228
+ 'The roadmap card or issue this changelog entry describes — R156 — by ref (R91, i91; a bare '
1229
+ + 'number is a roadmap card). Set it on an older changelog entry that has never been linked; '
1194
1230
  + 'omitting it leaves whatever link is already there.',
1195
1231
  },
1196
1232
  },
@@ -1211,9 +1247,9 @@ const TOOLS = [
1211
1247
  {
1212
1248
  name: 'task_current',
1213
1249
  description:
1214
- 'What you are working on: the roadmap entry, THE PLAN agreed for it, its branch, and ' +
1250
+ 'What you are working on: the roadmap card, THE PLAN agreed for it, its branch, and ' +
1215
1251
  'everything already said and asked on this run. Call it first, and again whenever you ' +
1216
- 'are unsure where you are — a resumed or confused session re-orients from this alone.',
1252
+ 'are unsure where you are — a resumed or confused agent session re-orients from this alone.',
1217
1253
  inputSchema: { type: 'object', properties: {} },
1218
1254
  handler: async (config) => {
1219
1255
  const { runId, project } = await requireRun(config);
@@ -1435,20 +1471,20 @@ const TOOLS = [
1435
1471
  {
1436
1472
  name: 'propose_entry',
1437
1473
  description:
1438
- 'Record something an audit found, or a card a scoping session cut, as a proposed entry. ' +
1474
+ 'Record something an audit found, or a card a scoping run cut, as a proposed card. ' +
1439
1475
  '`kind` is your opinion: "issue" ' +
1440
1476
  '(broken, unsafe, or loses data — with a severity) or "roadmap" (it should also do this, ' +
1441
1477
  'or do it better — no severity). A PERSON decides, and may file it the other way — you ' +
1442
- 'are not creating an entry, you are suggesting it. Default "issue", so an audit written ' +
1478
+ 'are not creating a card, you are suggesting it. Default "issue", so an audit written ' +
1443
1479
  'before there was a choice files what it always did. Severity is "critical" (broken, ' +
1444
1480
  'unsafe, or loses data), "medium" (it will hurt, but not today) or "minor" (worth doing, ' +
1445
- 'nobody is bleeding). `section` is where you think it belongs; a scoping session names ' +
1481
+ 'nobody is bleeding). `section` is where you think it belongs; a scoping run names ' +
1446
1482
  'ONE for every card of the idea. A roadmap card MUST say what it starts after — `after`: ' +
1447
1483
  '`[]` for a card that starts after none, `"#2"` for the second card you proposed in this ' +
1448
1484
  'run (the number this tool answered with), `"R12"` for a card already on the roadmap. ' +
1449
1485
  'You can only name what has already been proposed, so file in build order. Write each ' +
1450
- 'one as an entry would be written, and say where in the code you saw it. Only an audit ' +
1451
- 'or a scoping session may use this.',
1486
+ 'one as a card would be written, and say where in the code you saw it. Only an audit ' +
1487
+ 'or a scoping run may use this.',
1452
1488
  inputSchema: {
1453
1489
  type: 'object',
1454
1490
  properties: {
@@ -1473,7 +1509,7 @@ const TOOLS = [
1473
1509
  type: 'string',
1474
1510
  description:
1475
1511
  'The roadmap section this belongs under, in your opinion. Optional. The person ' +
1476
- 'accepting sees it as the default and may file it elsewhere. A scoping session ' +
1512
+ 'accepting sees it as the default and may file it elsewhere. A scoping run ' +
1477
1513
  'uses the same section on every card it cuts from one idea.',
1478
1514
  },
1479
1515
  after: {
@@ -1485,7 +1521,7 @@ const TOOLS = [
1485
1521
  'card you proposed in THIS run, by the number this tool answered with; it must ' +
1486
1522
  'already have been proposed, so propose what a card depends on before the card. ' +
1487
1523
  '`"R12"` or `"i7"` names a card already on the roadmap. A person adds the cards ' +
1488
- 'in this order, and a coding session on one waits until what it starts after has ' +
1524
+ 'in this order, and a coding run on one waits until what it starts after has ' +
1489
1525
  'landed.',
1490
1526
  },
1491
1527
  },
@@ -1565,7 +1601,7 @@ const TOOLS = [
1565
1601
  name: 'ask_group',
1566
1602
  description:
1567
1603
  'Ask a ROUND of questions at once, and wait for all of them. Use it when you have several ' +
1568
- 'things to settle that belong together — they arrive in the person\'s inbox under one ' +
1604
+ 'things to settle that belong together — they arrive in the person\'s Waiting on you page under one ' +
1569
1605
  'title, on one form, instead of interrupting them once per question. At most 12 in a ' +
1570
1606
  'round; ask the rest in the next one. Blocks for up to ten minutes; if the round is ' +
1571
1607
  'unfinished by then it returns a group_id for await_group.',
@@ -1575,7 +1611,7 @@ const TOOLS = [
1575
1611
  title: {
1576
1612
  type: 'string',
1577
1613
  description:
1578
- 'What this round is called in their inbox. A CTO Interview uses "CTO Interview".',
1614
+ 'What this round is called in Waiting on you. A CTO Interview uses "CTO Interview".',
1579
1615
  },
1580
1616
  intro: {
1581
1617
  type: 'string',
@@ -1705,7 +1741,7 @@ const TOOLS = [
1705
1741
  name: 'await_answer',
1706
1742
  description:
1707
1743
  'Resume waiting for a question ask_user handed back. Between the two you experience one ' +
1708
- 'natural blocking ask; the person experiences an inbox item.',
1744
+ 'natural blocking ask; the person experiences a Waiting on you item.',
1709
1745
  inputSchema: {
1710
1746
  type: 'object',
1711
1747
  properties: { question_id: { type: 'string' } },
@@ -2065,11 +2101,23 @@ function approvalTimeoutSeconds() {
2065
2101
  // The same goes for comments — nothing here removes one, and nothing there
2066
2102
  // does either.
2067
2103
  //
2068
- // Nor is there a sprint_open, sprint_close or sprint_rename — R257. Opening,
2069
- // closing and renaming a sprint are a person's acts on the page, and the API
2070
- // says so structurally (POST/PATCH /sprints take a session, not a token). An
2071
- // agent's whole reach here is reading a card's sprint and filing a card into
2072
- // one with roadmap_create / roadmap_update `sprint`.
2104
+ // Nor is there a sprint_open, sprint_start, sprint_complete or sprint_rename —
2105
+ // R257, and a team's since R474. Opening, starting, completing and renaming a
2106
+ // sprint are a person's acts on the page, and the API says so structurally
2107
+ // (the /api/teams/{id}/sprints writes take a session, not a token). An
2108
+ // agent's whole reach here is reading the sprints (sprint_list) and a card's
2109
+ // sprint, and filing a card into one with roadmap_create / roadmap_update
2110
+ // `sprint`.
2111
+
2112
+ /**
2113
+ * R489. A card's sprint as a line: `Core team · S7 Name (active)`. The label
2114
+ * is the server's one spelling; `S7` alone means nothing outside its team.
2115
+ */
2116
+ function sprintLine(sprint) {
2117
+ const label = sprint.label ?? sprint.ref ?? `S${sprint.number}`;
2118
+ const state = String(sprint.state ?? '').toLowerCase();
2119
+ return `${label}${sprint.name ? ` ${sprint.name}` : ''}${state ? ` (${state})` : ''}`;
2120
+ }
2073
2121
 
2074
2122
  function pick(source, keys) {
2075
2123
  const out = {};
@@ -2361,7 +2409,7 @@ function formatEntry(entry, { brief, comments, plan }) {
2361
2409
  // depend on which tool printed the card; a closed sprint says so.
2362
2410
  if (entry.sprint) {
2363
2411
  lines.push(
2364
- ` sprint: ${entry.sprint.ref} ${entry.sprint.name}${entry.sprint.state === 'CLOSED' ? ' (closed)' : ''}`,
2412
+ ` sprint: ${sprintLine(entry.sprint)}`,
2365
2413
  );
2366
2414
  }
2367
2415
  if (entry.related?.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cawdev-cli",
3
- "version": "1.0.11-beta",
3
+ "version": "1.1.0",
4
4
  "description": "cawdev's terminal client and runner daemon — zero dependencies, on purpose",
5
5
  "type": "module",
6
6
  "bin": {
package/runner/README.md CHANGED
@@ -158,8 +158,9 @@ served like any added project, and the rest of the console's setup walk —
158
158
  rules, members, the CTO Interview — is linked at
159
159
  `<url>/projects/<slug>/setup`.
160
160
 
161
- **What takes effect when** (R294). Three settings a running daemon takes
162
- without a restart — `agentCommands`, `grantable`, `acceptsRulesFromConsole`,
161
+ **What takes effect when** (R294). Four settings a running daemon takes
162
+ without a restart — `agentCommands`, `grantable`, `acceptsRulesFromConsole`
163
+ and `autoWorkspaces`,
163
164
  the `HOT_SWAPPABLE` list in `tools/lib/runner-config.mjs`, spelled once —
164
165
  because the daemon reads them fresh on every use. Everything else in the file
165
166
  (`projects` and their workspaces, `url`, `name`, the token, `browser`, the
@@ -197,7 +198,7 @@ and its copy all work exactly as they always have, and scrolling up reaches the
197
198
  start of the session. The only thing pinned is the footer at the bottom, and it
198
199
  never scrolls away: which cawdev, who you are signed in as, this machine's
199
200
  runner, the per-project session counts against their checkouts and the machine's
200
- total against `maxSessions` — plus the run you are watching and the keys that
201
+ total — plus the run you are watching and the keys that
201
202
  work right now. Anything *stopping* a session sits above all of that, because it
202
203
  is the only thing in there waiting on a person.
203
204
 
@@ -283,7 +284,7 @@ the shape of the decision, and the whole value of asking a person is that they
283
284
  can say the thing that was not on the list — so getting there costs one key, and
284
285
  `esc` from it comes back to the list rather than abandoning the answer. A chosen
285
286
  option and a typed sentence resolve the same question the same way, through the
286
- same endpoint the inbox and the run page post to: one record, the same
287
+ same endpoint the Waiting on you page and the run page post to: one record, the same
287
288
  `answeredBy`, and the badge clears at once.
288
289
 
289
290
  A permission request is the same widget — R60's lengths of yes as rows, with the
@@ -694,7 +695,7 @@ it ends.
694
695
  A bare path means one workspace, which is what every config meant before this
695
696
  existed — nothing changes for a machine that serves one checkout per project.
696
697
 
697
- How many coding runs go at once is `min(workspaces, maxSessions)`. A run that
698
+ How many coding runs go at once is the number of workspaces. A run that
698
699
  waits now says **"no free workspace in cawdev (2 here, all busy)"** instead of
699
700
  "that project already has a run here", which was a proxy for it.
700
701
 
@@ -717,18 +718,16 @@ says about knowing it.
717
718
 
718
719
  **This number caps coding and nothing else** (R70). `ASK`, `ROADMAP`, `AUDIT`
719
720
  and `SCOPE` runs take no workspace, so a project whose checkouts are all busy
720
- still starts a question, an entry-writing session, an audit and a scoping
721
- session at once. `INTERVIEW`, `MERGE`
721
+ still starts a question, a card-writing run, an audit and a scoping
722
+ run at once. `INTERVIEW`, `MERGE`
722
723
  and `RELEASE` do take one — each commits something — and count against this
723
724
  number; what each may *write* in it is far narrower than a coding run's and is
724
725
  decided by the profile, not by the prompt: an interview writes `docs/brief/`,
725
726
  a merge writes the files git put conflict markers in, and a release (R187)
726
727
  writes exactly the version files the platform names on the claim, with `git`,
727
728
  `gh pr create`, `gh pr view` and `node` for the exports — no build tools, no
728
- permission prompt, no `gh pr merge`. What holds the rest back
729
- is `maxSessions`, which counts every profile — **"at 4 sessions on this machine
730
- (every profile counts)"** is the other thing a waiting run can say, and the log
731
- always says which of the two it was.
729
+ permission prompt, no `gh pr merge`. What holds a coding run back
730
+ is a free workspace, and the log says so (R109 removed `maxSessions`).
732
731
 
733
732
  **One thing this gate cannot express, and does not try to.** It serialises on a
734
733
  checkout, so two runs on *the same branch* in two checkouts are, to this daemon,
@@ -797,8 +796,8 @@ whether the browser is allowed. Those five answer nearly every "why did that
797
796
  not happen", and they used to be spread across a config file and a shrug.
798
797
 
799
798
  Attaching adds a second bar row: the URL, each project as **coding sessions over
800
- checkouts** (`cawdev 1/2`), and the machine's total against `maxSessions`.
801
- Both gates, on screen, counting what each actually bounds — a question is in the
799
+ checkouts** (`cawdev 1/2`), and the machine's total.
800
+ The gate, on screen, counting what it actually bounds — a question is in the
802
801
  total on the right and not in any project's figure, because it took no checkout.
803
802
  A run that is waiting is explained by the bar
804
803
  above it rather than by reading the source. A narrow terminal drops projects
@@ -849,11 +848,11 @@ than twenty-six. A machine that wants it unattended puts that string in its own
849
848
  ### What a person can ask of a checkout
850
849
 
851
850
  R57. The daemon polls `workspace-requests/claim` every few seconds and does one
852
- of **ten** things to a checkout it serves. It said six for two entries, and
851
+ of **eleven** things to a checkout it serves. It said six for two entries, and
853
852
  `HANDOFF` had been missing from the list since R148 — a table that quietly does
854
853
  not describe one of the things a person can ask for is worse than no table. So
855
854
  count the rows below against that number before you trust either. `CUT_BRANCH`
856
- is the ninth, R260's; `OPEN_PR` the tenth, R261's.
855
+ is the ninth, R260's; `OPEN_PR` the tenth, R261's, `CREATE_BRANCH` the eleventh, R379's.
857
856
 
858
857
  | | |
859
858
  |---|---|
@@ -867,6 +866,7 @@ is the ninth, R260's; `OPEN_PR` the tenth, R261's.
867
866
  | `TAG` | R156's release: `git ls-remote --tags origin refs/tags/<version>` for the version in `message`. **The remote, not this clone** — a local tag nobody pushed is exactly the state a release is trying to rule out, and `git tag --list` cannot tell the two apart. A read; it fetches nothing and moves no ref. The sha is reported on the result's first line, the way `MERGE` reports its URL, because the platform will not confirm a release on `ok: true` alone. **A tag that is not there yet is `ok: false` with a sentence, and that is a normal answer** — the release procedure pushes the tag last, so the first check correctly finds nothing. Addressed to a **release** rather than to a directory alone, which is why it is asked for from the roadmap board and refused on this channel. |
868
867
  | `CUT_BRANCH` | R260's sprint branch: `git push origin origin/<default>:refs/heads/<name>` for the branch in `message`, after a fetch — so the sprint's cards are cut from what everyone else has, not from whatever this checkout was left on. The second kind after `MERGE` whose effect is **not** in the checkout: the push names two refs and this clone's HEAD is not one of them, so any live checkout of the project will do and nothing local moves. **A branch already on origin is `ok: true`** ("already on origin at <sha>") — the sprint wanted one to exist, and one does. The sha is reported on the result's first line, the way `TAG` reports its. Addressed to a **sprint** and asked for from the sprint page by an OWNER, for `MERGE`'s reason — it reaches the remote — and refused on this channel. A `MERGE` of a card's pull request into a sprint branch still runs `--squash --delete-branch`: the card's branch goes, the sprint's stays. Landing the sprint branch itself is `OPEN_PR` then `MERGE` below. |
869
868
  | `OPEN_PR` | R261's sprint merge, first half: `gh pr create --head <branch> --base <base> --title <title>` for the branch in `message` — the sprint's — to `baseBranch ?? defaultBranch`, titled as the claim's `title` (*S1 Notifications*). **An existing pull request is `ok: true` with its URL** — the same `gh pr view` lookup the `auto_pr` rule makes, and the reason the platform can ask this every time: it holds no git-host credential and cannot know whether one exists. The URL is reported on the result's first line, and the platform queues the `MERGE` to this same checkout when it reads one. Nothing is pushed: the branch is on origin already, cut there by `CUT_BRANCH` and merged into by every card. Addressed to a **work item** — the sprint's — and asked for by Merge on the development board or *Merge sprint* on the sprint page, by an OWNER; refused on this channel for `MERGE`'s reason. |
869
+ | `CREATE_BRANCH` | R379's New branch from the console: pushes `origin/<fromBranch>` to `refs/heads/<name>` for the name in `message`. Unlike `CUT_BRANCH` a name already on origin is a **refusal**, because the person asked for a new branch and the existing one may be somebody else's work. Touches no working tree; a daemon that knows the kind says `createBranch: true` in its hello. |
870
870
 
871
871
  **A `MERGE` that fails also says which KIND of failure it was** — R155, in one
872
872
  of five words beside `gh`'s own unchanged text:
@@ -1257,12 +1257,9 @@ rest queue.
1257
1257
 
1258
1258
  **Questions are not serialised.** An `ASK` run prepares nothing and writes
1259
1259
  nothing, so it runs alongside whatever else is going on — you can ask about a
1260
- project while an agent is working in it. `maxSessions` (4 by default) bounds how
1261
- many agent processes this machine will host at once, and it is the **only**
1262
- thing bounding a question, a roadmap session or an audit (R70). It counts runs
1263
- this daemon has claimed, not children it has spawned: a claim takes a second or
1264
- two to become a process, and counting processes let one pass of the loop claim
1265
- the whole queue.
1260
+ project while an agent is working in it. Nothing bounds a question, a roadmap session or an audit but the platform's
1261
+ own queue: `maxSessions` was removed in R109, and the workspace gate above is
1262
+ the only one.
1266
1263
 
1267
1264
  ## If the daemon dies mid-run
1268
1265
 
@@ -1391,7 +1388,7 @@ permission stops forever.
1391
1388
 
1392
1389
  - `--permission-mode acceptEdits` lets it write files.
1393
1390
  - `--allowedTools mcp__cawdev__…` lets it use the cawdev tools. **Without this
1394
- the entire loop is unreachable** — no reading the task, no moving the entry,
1391
+ the entire loop is unreachable** — no reading the task, no moving the card,
1395
1392
  no reporting, no asking. A real session found this by being denied
1396
1393
  `task_current` and stopping rather than guessing, which was the right call
1397
1394
  and a good sign for the prompt.
@@ -1417,7 +1414,7 @@ never asks you anything — an unlisted tool is refused, not raised (i138).
1417
1414
 
1418
1415
  **Anything else stops and asks you** (R51). Rather than being denied in
1419
1416
  silence, a session that needs `mvn` or `npm` posts a request that appears in
1420
- your inbox and on the run, and waits for an answer. Allow it once, or allow it
1417
+ your Waiting on you page and on the run, and waits for an answer. Allow it once, or allow it
1421
1418
  always here — which writes a rule on the project that every runner serving it
1422
1419
  inherits.
1423
1420
 
@@ -1548,8 +1545,8 @@ after `--mcp-config` fails with `ENAMETOOLONG`, which names neither the flag nor
1548
1545
  the prompt.
1549
1546
 
1550
1547
  The prompt deliberately teaches the **method**, not the task. The task is in the
1551
- roadmap entry, which the agent reads for itself with `task_current` — putting it
1552
- in the prompt too would be a second copy that can disagree with the entry.
1548
+ roadmap card, which the agent reads for itself with `task_current` — putting it
1549
+ in the prompt too would be a second copy that can disagree with the card.
1553
1550
 
1554
1551
  ### Codex CLI
1555
1552
 
package/runner/runner.mjs CHANGED
@@ -189,6 +189,7 @@ const DEFAULTS = {
189
189
  'mcp__cawdev__quality_get',
190
190
  'mcp__cawdev__roadmap_statuses',
191
191
  'mcp__cawdev__roadmap_list',
192
+ 'mcp__cawdev__sprint_list',
192
193
  'mcp__cawdev__roadmap_get',
193
194
  // The MCP server has served this since R37, its README documents it, and
194
195
  // the REVIEW prompt below TELLS a session to file its findings with it —
@@ -9643,6 +9644,17 @@ async function main() {
9643
9644
  // right way.
9644
9645
  quiet = attaching;
9645
9646
 
9647
+ // A SIGHUP that arrives while booting — `cawdev config` run a moment after
9648
+ // the daemon started — is held, not fatal: Node's default for an unhandled
9649
+ // SIGHUP is to exit, and the real handler is only installed once
9650
+ // `rereadConfig` exists, well after the first lines are logged. It is
9651
+ // applied as soon as that handler is in place.
9652
+ let hupWhileBooting = false;
9653
+ const holdHup = () => {
9654
+ hupWhileBooting = true;
9655
+ };
9656
+ process.on('SIGHUP', holdHup);
9657
+
9646
9658
  const config = await readConfig();
9647
9659
  // The checkouts made on demand by an earlier run of this daemon, found
9648
9660
  // again by their name — nothing about them is in the config.
@@ -9869,7 +9881,11 @@ async function main() {
9869
9881
  // event type it does not know.
9870
9882
  control?.publish({ type: 'runner', runner: advertised });
9871
9883
  };
9884
+ process.off('SIGHUP', holdHup);
9872
9885
  process.on('SIGHUP', () => rereadConfig('SIGHUP'));
9886
+ if (hupWhileBooting) {
9887
+ void rereadConfig('SIGHUP (sent while booting)');
9888
+ }
9873
9889
 
9874
9890
  // The heartbeat's half of R294: the file's mtime, read once per beat, and a
9875
9891
  // re-read when it moved. `stat` and nothing more when it did not, which is
package/runner/split.mjs CHANGED
@@ -963,7 +963,7 @@ export function cardLines(card, ink, url = null) {
963
963
  field('status', [card.statusDisplay ?? card.status, kind].filter(Boolean).join(' · '));
964
964
  field('section', card.section);
965
965
  field('tags', card.tags?.length ? card.tags.join(', ') : null);
966
- field('sprint', card.sprint ? `${card.sprint.ref ?? card.sprint.number} ${card.sprint.name ?? ''}`.trim() : null);
966
+ field('sprint', card.sprint ? `${card.sprint.label ?? card.sprint.ref ?? card.sprint.number} ${card.sprint.name ?? ''}`.trim() : null);
967
967
  const item = card.workItem;
968
968
  field('branch', card.branch ?? item?.branch);
969
969
  field('work', item ? [item.statusDisplay ?? item.status, item.runnerName && `on ${item.runnerName}`]