cawdev-cli 1.0.9-beta → 1.0.11-beta

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.
@@ -17,6 +17,15 @@
17
17
  * - `BashOutput` (or `TaskOutput`) reads one by `bash_id`/`shell_id`/`task_id`;
18
18
  * the command is over only when a read says a status other than running;
19
19
  * - `KillShell`/`KillBash`/`TaskStop` ends one.
20
+ * And a delegation started in the background: `Agent` (or `Task`) with
21
+ * `run_in_background: true`, or whose result says "Async agent launched" —
22
+ * its id is the result's `agentId: <id>`. It is over when a `<task-notification>`
23
+ * naming that id arrives with a status other than running, when a
24
+ * `TaskOutput` read says so, or when `TaskStop` ends it. R6 on saasng is why:
25
+ * IMPLEMENT handed the whole card to two background agents, said "both agents
26
+ * are still working in the background" and ended its turn; the walk marched on
27
+ * and the run FINISHED with nothing built.
28
+ *
20
29
  * A command whose id cannot be read off its result stays unfinished: failing
21
30
  * loudly and re-running a stage costs a turn, calling unproven work proven
22
31
  * costs somebody's afternoon.
@@ -26,6 +35,7 @@
26
35
  */
27
36
 
28
37
  const READS = new Set(['BashOutput', 'TaskOutput']);
38
+ const DELEGATES = new Set(['Agent', 'Task']);
29
39
  const KILLS = new Set(['KillShell', 'KillBash', 'TaskStop']);
30
40
 
31
41
  /** The text of a tool_result's content, whichever of its two shapes it came in. */
@@ -44,15 +54,19 @@ function shellIdOf(input) {
44
54
 
45
55
  export class BackgroundShells {
46
56
  constructor() {
47
- /** tool_use id of each background `Bash` → { command, shellId, done }. */
57
+ /** tool_use id of each background `Bash` or `Agent` → { command, shellId, done }. */
48
58
  this.started = new Map();
59
+ /** tool_use id of each `Agent` call not yet known to be in the background → its label and input. */
60
+ this.delegations = new Map();
49
61
  /** tool_use id of each read → the shell id it read. */
50
62
  this.reads = new Map();
51
63
  }
52
64
 
53
65
  /** One stream-json event. Anything that is not about a shell is ignored. */
54
66
  observe(event) {
55
- const parts = event?.message?.content;
67
+ const content = event?.message?.content;
68
+ if (event?.type === 'user') this.notified(textOf(content));
69
+ const parts = content;
56
70
  if (!Array.isArray(parts)) return;
57
71
  if (event.type === 'assistant') {
58
72
  for (const part of parts) {
@@ -63,6 +77,9 @@ export class BackgroundShells {
63
77
  shellId: null,
64
78
  done: false,
65
79
  });
80
+ } else if (DELEGATES.has(part.name)) {
81
+ const label = `Agent: ${String(part.input?.description ?? part.input?.subagent_type ?? 'a delegation').trim()}`;
82
+ this.delegations.set(part.id, { label, background: part.input?.run_in_background === true });
66
83
  } else if (READS.has(part.name)) {
67
84
  const id = shellIdOf(part.input);
68
85
  if (id) this.reads.set(part.id, id);
@@ -76,6 +93,17 @@ export class BackgroundShells {
76
93
  for (const part of parts) {
77
94
  if (part?.type !== 'tool_result') continue;
78
95
  const text = textOf(part.content);
96
+ const delegation = this.delegations.get(part.tool_use_id);
97
+ if (delegation) {
98
+ this.delegations.delete(part.tool_use_id);
99
+ // A foreground delegation's result IS its end; only one that came
100
+ // back as "launched" is still out there.
101
+ if (delegation.background || /async agent launched/i.test(text)) {
102
+ const id = /\bagentId:?\s*([A-Za-z0-9_-]+)/i.exec(text);
103
+ this.started.set(part.tool_use_id, { command: delegation.label, shellId: id ? id[1] : null, done: false });
104
+ }
105
+ continue;
106
+ }
79
107
  const launched = this.started.get(part.tool_use_id);
80
108
  if (launched && !launched.shellId) {
81
109
  const id = /\bID:?\s*([A-Za-z0-9_-]+)/.exec(text);
@@ -94,6 +122,20 @@ export class BackgroundShells {
94
122
  }
95
123
  }
96
124
 
125
+ /**
126
+ * The CLI tells a session that a background task ended with a
127
+ * `<task-notification>` in a user turn — `<task-id>` and `<status>`. Any
128
+ * status but running is over: a failed or killed agent is not one to wait for.
129
+ */
130
+ notified(text) {
131
+ if (!text || !text.includes('<task-notification>')) return;
132
+ for (const block of text.split('<task-notification>').slice(1)) {
133
+ const id = /<task-id>\s*([^<\s]+)\s*<\/task-id>/.exec(block);
134
+ const status = /<status>\s*([^<\s]+)\s*<\/status>/.exec(block);
135
+ if (id && status && status[1].toLowerCase() !== 'running') this.finish(id[1]);
136
+ }
137
+ }
138
+
97
139
  finish(shellId) {
98
140
  if (!shellId) return;
99
141
  for (const each of this.started.values()) {
@@ -101,7 +143,7 @@ export class BackgroundShells {
101
143
  }
102
144
  }
103
145
 
104
- /** The commands started in the background whose end this session never saw. */
146
+ /** The commands and delegations started in the background whose end this session never saw. */
105
147
  unfinished() {
106
148
  return [...this.started.values()].filter((each) => !each.done).map((each) => each.command);
107
149
  }
@@ -56,11 +56,13 @@ export function statusToDisplay(status) {
56
56
  * The file has always grouped these two this way, and deriving them means a
57
57
  * declined entry cannot end up filed under a phase it is no longer part of.
58
58
  *
59
- * Two, and REVIEW is deliberately not a third: work waiting to be read is still
59
+ * Three since R371 — BACKLOG joined — and REVIEW is deliberately not a fourth: work waiting to be read is still
60
60
  * part of the phase it belongs to, and moving it to a section of its own would
61
61
  * take it out of its phase for a few days and put it back afterwards.
62
62
  */
63
63
  export const STATUS_SECTIONS = {
64
+ // R371: waiting to be brought in, before what is being considered.
65
+ BACKLOG: 'Backlog — waiting to be brought in',
64
66
  CONSIDERING: 'Considering — wanted, not settled',
65
67
  DECLINED: 'Declined — decided against, with the reason',
66
68
  };
@@ -77,6 +79,8 @@ export const STATUS_SECTIONS = {
77
79
  export const ISSUE_SECTIONS = {
78
80
  NEW: 'New — filed, not yet triaged',
79
81
  CONFIRMED: 'Confirmed — real, waiting for somebody',
82
+ // R371: an issue waiting to be brought in — after what needs somebody now.
83
+ BACKLOG: 'Backlog — waiting to be brought in',
80
84
  IN_DEVELOPMENT: 'In development — somebody is fixing it',
81
85
  MERGED: 'Resolved — the fix landed',
82
86
  SHIPPED: 'Released — the fix shipped',
@@ -249,6 +253,12 @@ export function parseRoadmap(text) {
249
253
  entry.targetVersion = target[1].trim();
250
254
  continue;
251
255
  }
256
+ // R378. The versions an issue was found in, comma separated.
257
+ const affects = /^Affects:\s*(.+)$/.exec(line.trim());
258
+ if (affects) {
259
+ entry.affectedVersions = affects[1].split(',').map((v) => v.trim()).filter(Boolean);
260
+ continue;
261
+ }
252
262
  const estimate = /^Estimate:\s*(.+)$/.exec(line.trim());
253
263
  if (estimate) {
254
264
  const n = Number(estimate[1].trim());
@@ -530,6 +540,10 @@ function renderEntry(entry) {
530
540
  if (entry.targetVersion && !entry.version) {
531
541
  lines.push(`Target: ${entry.targetVersion}`);
532
542
  }
543
+ // R378. Where an issue was found; history, so it stays once the issue ships.
544
+ if (entry.affectedVersions?.length) {
545
+ lines.push(`Affects: ${entry.affectedVersions.join(', ')}`);
546
+ }
533
547
 
534
548
  // Each status writes only what it currently claims. A MERGED entry keeps its
535
549
  // branch in the database as history, but the file must not print it: the
@@ -76,7 +76,7 @@ function canonical(value) {
76
76
  if (value === undefined) return 'undefined';
77
77
  return JSON.stringify(value, (_, inner) => (
78
78
  inner && typeof inner === 'object' && !Array.isArray(inner)
79
- ? Object.fromEntries(Object.keys(inner).sort().map((key) => [key, inner[key]]))
79
+ ? Object.fromEntries(Object.keys(inner).sort((a, b) => a.localeCompare(b)).map((key) => [key, inner[key]]))
80
80
  : inner
81
81
  ));
82
82
  }
package/mcp/README.md CHANGED
@@ -81,6 +81,15 @@ commit, or a sha — `SHIPPED` needs a version that is a real git tag, and
81
81
  request it would otherwise carry belongs to the run, and a project that reviews
82
82
  without opening pull requests has none to give it.
83
83
 
84
+ **A card you file may land in the `BACKLOG`** (R371). Where a new card goes
85
+ when you name no status is the project's rule, per door, and the default for a
86
+ card or issue an agent files is the backlog: waiting until a person or a sprint
87
+ 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
89
+ do (R380). Name a status (`CONSIDERING`, `NEW`) when
90
+ you mean the pipeline. Giving a backlog card a sprint moves it to the first
91
+ status of its kind; moving a card to `BACKLOG` takes it out of its sprint.
92
+
84
93
  **Any status may move to any other.** There is no transition diagram — an entry
85
94
  really can go from `CONSIDERING` straight to `SHIPPED` if that is what happened.
86
95
  The rules are about what a status *carries*, not the path it took.
@@ -163,11 +172,12 @@ what you are allowed to do.
163
172
  | `roadmap_list` | Entries, optionally by status. `brief` omits bodies — use it to survey. |
164
173
  | `roadmap_get` | One entry in full: its body, **the plan agreed for it**, and the discussion on it. |
165
174
  | `roadmap_create` | Create an entry; the platform allocates its permanent number. |
166
- | `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), 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). |
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). |
167
176
  | `roadmap_comment` | Say something *beside* an entry rather than inside it. |
177
+ | `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. |
168
178
  | `roadmap_set_status` | Move an entry, carrying whatever the status requires. |
169
179
  | `roadmap_decline` | Decline with a reason. The only exit. |
170
- | `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). |
180
+ | `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). |
171
181
  | `changelog_list` | The changelog, grouped by release, newest first. |
172
182
  | `changelog_get` | One entry. |
173
183
  | `changelog_add` | Add an entry; no version means `Unreleased`. |
package/mcp/server.mjs CHANGED
@@ -412,6 +412,7 @@ const TOOLS = [
412
412
  status: {
413
413
  type: 'string',
414
414
  enum: [
415
+ 'BACKLOG',
415
416
  'CONSIDERING',
416
417
  'PLANNED',
417
418
  'NEW',
@@ -621,13 +622,71 @@ const TOOLS = [
621
622
  },
622
623
  },
623
624
 
625
+ {
626
+ name: 'done_when_tick',
627
+ description:
628
+ "Tick (or untick) one item of the card's Done-when checklist as built. Items are " +
629
+ "numbered from 1 as task_current lists them. Call it when you have BUILT the item and " +
630
+ 'CHECKED it, with one line of evidence — a test name, a command, a file:line. Do not ' +
631
+ 'tick what you did not check: the reviewer ticks their own column separately and sees ' +
632
+ 'yours. Defaults to the card this run is working on, and a run cannot tick another.',
633
+ inputSchema: {
634
+ type: 'object',
635
+ properties: {
636
+ ...PROJECT_ARGUMENT,
637
+ number: {
638
+ type: ['string', 'integer'],
639
+ description:
640
+ 'The card by ref — R91, i91. Optional inside a run: defaults to the run\'s own card.',
641
+ },
642
+ item: { type: 'integer', minimum: 1, description: "The item's number, from 1." },
643
+ done: { type: 'boolean', description: 'false takes the tick back. Default true.' },
644
+ note: {
645
+ type: 'string',
646
+ description: 'One line of evidence: a test name, a command, a file:line.',
647
+ },
648
+ },
649
+ required: ['item'],
650
+ },
651
+ handler: async (config, args) => {
652
+ let slug;
653
+ let ref = args.number;
654
+ if (ref === undefined || ref === null || ref === '') {
655
+ const { runId, project } = await requireRun(config);
656
+ const run = await api(config, `/api/projects/${project}/runs/${runId}`);
657
+ ref = run.entryRef ?? run.entryNumber;
658
+ slug = project;
659
+ } else {
660
+ slug = await resolveProject(config, args.project);
661
+ }
662
+ if (ref === undefined || ref === null || ref === '') {
663
+ throw new CawdevError('This run has no card, so there is no Done-when list to tick.');
664
+ }
665
+ if (!Number.isInteger(args.item) || args.item < 1) {
666
+ throw new CawdevError('item must be a whole number from 1, as task_current lists them.');
667
+ }
668
+ const done = args.done !== false;
669
+ const row = await api(config, `/api/projects/${slug}/roadmap/${encodeURIComponent(String(ref))}/done-when/${args.item}/built`, {
670
+ method: 'PUT',
671
+ body: { done, note: args.note },
672
+ });
673
+ return (
674
+ `${done ? 'Ticked' : 'Unticked'} item ${row.position} of ${ref} in ${slug} as built: ` +
675
+ row.text
676
+ );
677
+ },
678
+ },
679
+
624
680
  {
625
681
  name: 'roadmap_create',
626
682
  description:
627
683
  'Create a roadmap entry. The platform allocates its permanent number — written R91 on ' +
628
684
  'a roadmap card and i91 on an issue, one sequence across both — and the ' +
629
- 'platform decides where a card starts — CONSIDERING — unless you say otherwise; ' +
630
- 'a status that requires something must be given it.',
685
+ 'project decides where a card you file starts — the BACKLOG by default for a card an ' +
686
+ 'agent files, waiting until a person or a sprint brings it in — unless you say ' +
687
+ '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 ' +
689
+ 'where the console\'s do (R380).',
631
690
  inputSchema: {
632
691
  type: 'object',
633
692
  properties: {
@@ -640,6 +699,7 @@ const TOOLS = [
640
699
  status: {
641
700
  type: 'string',
642
701
  enum: [
702
+ 'BACKLOG',
643
703
  'CONSIDERING',
644
704
  'PLANNED',
645
705
  'IN_DEVELOPMENT',
@@ -733,7 +793,7 @@ const TOOLS = [
733
793
  description:
734
794
  'What is broken in a project, and how badly — R85. The same numbering as the roadmap, ' +
735
795
  'written with an i: card 91 filed as an issue is i91, and there is no R91 as well. ' +
736
- 'Statuses are NEW, CONFIRMED, IN_DEVELOPMENT, MERGED (drawn as ' +
796
+ 'Statuses are BACKLOG, NEW, CONFIRMED, IN_DEVELOPMENT, MERGED (drawn as ' +
737
797
  'Resolved) and DECLINED (Won\'t fix).',
738
798
  inputSchema: {
739
799
  type: 'object',
@@ -741,7 +801,7 @@ const TOOLS = [
741
801
  ...PROJECT_ARGUMENT,
742
802
  status: {
743
803
  type: 'string',
744
- enum: ['NEW', 'CONFIRMED', 'IN_DEVELOPMENT', 'MERGED', 'SHIPPED', 'DECLINED'],
804
+ enum: ['BACKLOG', 'NEW', 'CONFIRMED', 'IN_DEVELOPMENT', 'MERGED', 'SHIPPED', 'DECLINED'],
745
805
  },
746
806
  assignee: {
747
807
  type: 'string',
@@ -768,8 +828,10 @@ const TOOLS = [
768
828
  {
769
829
  name: 'issue_file',
770
830
  description:
771
- 'File an issue: something that is broken, with how badly. Lands at NEW — filed and not ' +
772
- 'yet triaged — unless you say CONFIRMED, which claims you have already checked it. The ' +
831
+ 'File an issue: something that is broken, with how badly. Lands where the project says ' +
832
+ 'for an issue an agent files — the BACKLOG by default, waiting to be brought in, or NEW ' +
833
+ '(filed and not yet triaged) — unless you say NEW, or CONFIRMED, which claims you have ' +
834
+ 'already checked it. The ' +
773
835
  'severity is required: it is how the board is ordered. Use `related` to name the card it ' +
774
836
  'was found on, by ref (R84). Issues count on their own sequence — R221 — and are ' +
775
837
  'written with an i: i91 is the ninety-first issue, and R91 is another card.',
@@ -780,7 +842,7 @@ const TOOLS = [
780
842
  title: { type: 'string' },
781
843
  body: { type: 'string', description: 'Markdown. What happens, and what should instead.' },
782
844
  severity: { type: 'string', enum: ['CRITICAL', 'MEDIUM', 'MINOR'] },
783
- status: { type: 'string', enum: ['NEW', 'CONFIRMED'] },
845
+ status: { type: 'string', enum: ['BACKLOG', 'NEW', 'CONFIRMED'] },
784
846
  related: {
785
847
  type: 'array',
786
848
  items: { type: ['string', 'integer'] },
@@ -805,6 +867,12 @@ const TOOLS = [
805
867
  description:
806
868
  'How much it matters; it does not reorder the board. Leave out for MEDIUM.',
807
869
  },
870
+ affectedVersions: {
871
+ type: 'array',
872
+ items: { type: 'string' },
873
+ description:
874
+ 'Versions the project defined or shipped in where this issue was found — R378. Any other name is refused.',
875
+ },
808
876
  },
809
877
  required: ['title', 'severity'],
810
878
  },
@@ -812,7 +880,7 @@ const TOOLS = [
812
880
  const slug = await resolveProject(config, args.project);
813
881
  const issue = await api(config, `/api/projects/${slug}/issues`, {
814
882
  method: 'POST',
815
- body: pick(args, ['title', 'body', 'severity', 'status', 'related', 'tags', 'assignee', 'priority']),
883
+ body: pick(args, ['title', 'body', 'severity', 'status', 'related', 'tags', 'assignee', 'priority', 'affectedVersions']),
816
884
  });
817
885
  return `Filed ${refOf(issue)} in ${slug}.\n\n${formatEntry(issue, {})}`;
818
886
  },
@@ -833,14 +901,25 @@ const TOOLS = [
833
901
  kind: { type: 'string', enum: ['ISSUE', 'FEATURE'] },
834
902
  title: { type: 'string' },
835
903
  body: { type: 'string', description: 'Markdown. What you noticed, and what should happen instead.' },
904
+ tags: {
905
+ type: 'array',
906
+ items: { type: 'string' },
907
+ description:
908
+ 'Who it came from, beyond you — e.g. "customer" when you are passing on what a user ' +
909
+ 'said. "agent" is always added: you filed it. Same word rule as card tags.',
910
+ },
836
911
  },
837
912
  required: ['kind', 'title', 'body'],
838
913
  },
839
914
  handler: async (config, args) => {
840
915
  const slug = await resolveProject(config, args.project);
916
+ // R372: feedback says who it came from, and an agent filing it is one
917
+ // of the sources. Added here, at the agent's own door, not by the API:
918
+ // an application's token files through the same endpoint and is not an agent.
919
+ const tags = [...new Set([...(Array.isArray(args.tags) ? args.tags : []), 'agent'])];
841
920
  const item = await api(config, `/api/projects/${slug}/backlog`, {
842
921
  method: 'POST',
843
- body: pick(args, ['kind', 'title', 'body']),
922
+ body: { ...pick(args, ['kind', 'title', 'body']), tags },
844
923
  });
845
924
  return `Filed in ${slug}'s backlog as pending: "${item.title}". Somebody will accept it ` +
846
925
  'as a card or an issue, or refuse it.';
@@ -921,6 +1000,13 @@ const TOOLS = [
921
1000
  'The card this one sits under (an epic), e.g. "R300". "" takes the card out of its ' +
922
1001
  'parent; leave out to leave it alone. Same rules as roadmap_create.',
923
1002
  },
1003
+ affectedVersions: {
1004
+ type: 'array',
1005
+ items: { type: 'string' },
1006
+ description:
1007
+ 'Issues only (R378): the versions the project defined or shipped in where it was found. ' +
1008
+ 'Leave out to leave them alone; [] clears them. A roadmap card refuses a non-empty list.',
1009
+ },
924
1010
  },
925
1011
  required: ['number'],
926
1012
  },
@@ -931,7 +1017,7 @@ const TOOLS = [
931
1017
  // pick keeps a present 0, which is how a card leaves its sprint.
932
1018
  body: pick(args, [
933
1019
  'title', 'body', 'section', 'related', 'after', 'sprint', 'tags', 'assignee', 'priority',
934
- 'due', 'estimate', 'targetVersion', 'parent',
1020
+ 'due', 'estimate', 'targetVersion', 'parent', 'affectedVersions',
935
1021
  ]),
936
1022
  });
937
1023
  return `Updated ${refOf(entry)}.\n\n${formatEntry(entry, {})}`;
@@ -959,6 +1045,7 @@ const TOOLS = [
959
1045
  status: {
960
1046
  type: 'string',
961
1047
  enum: [
1048
+ 'BACKLOG',
962
1049
  'CONSIDERING',
963
1050
  'PLANNED',
964
1051
  'NEW',
@@ -1170,6 +1257,12 @@ const TOOLS = [
1170
1257
  config,
1171
1258
  `/api/projects/${project}/roadmap/${cardRef}/plans`,
1172
1259
  ).catch(() => []);
1260
+ // R368. The plan's Done-when list, numbered, with the ticks so far. Its
1261
+ // own try, like the plan above: a platform without it answers 404.
1262
+ const criteria = await api(
1263
+ config,
1264
+ `/api/projects/${project}/roadmap/${cardRef}/done-when`,
1265
+ ).catch(() => []);
1173
1266
 
1174
1267
  const lines = [];
1175
1268
 
@@ -1212,6 +1305,9 @@ const TOOLS = [
1212
1305
  }
1213
1306
  }
1214
1307
  lines.push(formatEntry(entry, { comments, plan: plans?.[0] ?? null }));
1308
+ if (criteria?.length) {
1309
+ lines.push('', formatDoneWhen(criteria));
1310
+ }
1215
1311
 
1216
1312
  // What happened the other times. A session is told the entry and the
1217
1313
  // branch but not that two previous runs on this card failed, which is
@@ -2259,6 +2355,8 @@ function formatEntry(entry, { brief, comments, plan }) {
2259
2355
  if (entry.estimate) lines.push(` estimate: ${entry.estimate}`);
2260
2356
  // R358. Printed until the card ships; `version` says where it did.
2261
2357
  if (entry.targetVersion && !entry.version) lines.push(` target: ${entry.targetVersion}`);
2358
+ // R378. History, not plan: still printed once the issue has shipped.
2359
+ if (entry.affectedVersions?.length) lines.push(` affects: ${entry.affectedVersions.join(', ')}`);
2262
2360
  // R257. Always printed when the card has one, so a reader's shape does not
2263
2361
  // depend on which tool printed the card; a closed sprint says so.
2264
2362
  if (entry.sprint) {
@@ -2340,6 +2438,22 @@ function formatPlan(plan) {
2340
2438
  return [`--- the plan (${written}) ---`, plan.body].join('\n');
2341
2439
  }
2342
2440
 
2441
+ /**
2442
+ * R368. The Done-when list as an agent reads it: numbered as done_when_tick
2443
+ * takes them, with each tick and its evidence. A reviewer's confirmation is
2444
+ * shown too — it is the other column, and the agent should see it.
2445
+ */
2446
+ export function formatDoneWhen(criteria) {
2447
+ const lines = ['--- done when (tick each with done_when_tick once you have checked it) ---'];
2448
+ for (const item of criteria) {
2449
+ const mark = item.built ? '[built]' : '[ ]';
2450
+ const note = item.built && item.builtNote ? ` — ${item.builtNote}` : '';
2451
+ const confirmed = item.confirmed ? ' (confirmed by reviewer)' : '';
2452
+ lines.push(`${String(item.position).padStart(2)}. ${mark} ${item.text}${note}${confirmed}`);
2453
+ }
2454
+ return lines.join('\n');
2455
+ }
2456
+
2343
2457
  /**
2344
2458
  * One earlier attempt at this card — R38.
2345
2459
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cawdev-cli",
3
- "version": "1.0.9-beta",
3
+ "version": "1.0.11-beta",
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
@@ -469,7 +469,7 @@ this machine serves — unless the command names it with `@project`, or
469
469
  advanced mode is narrowed to ONE project (`/projects`), which is a choice
470
470
  already made rather than a guess. Never guessed from the focused window, nor
471
471
  from a machine serving just one, since a session on the wrong project is a
472
- card planned or a branch cut in the wrong repository. `/new dev` alone and `d` list the planned cards of every project,
472
+ card planned or a branch cut in the wrong repository. `/new dev` alone and `d` list the planned cards and the confirmed issues of every project,
473
473
  each with its own, so they ask nothing more. A usage window over this machine's threshold is refused in a
474
474
  sentence; `--anyway` starts it regardless, the same decision the console's
475
475
  panel asks for. All four commands need you signed in (`/login`), and they work
@@ -510,6 +510,22 @@ then ↑.
510
510
  **`/view`** opens the focused session's page on cawdev — transcript, stages,
511
511
  card — in the browser, on the console this terminal signed in to.
512
512
 
513
+ **On a session waiting for you** — a question, or a permission such as a stage
514
+ gate whose summary is the whole plan — the prompt only has room for the start
515
+ of it, and says so: `/view reads it all · /open answers it on cawdev`.
516
+ **`/view`** then opens the READER: one window over the whole screen with the
517
+ session's output and, under a rule, the request in full (the question and its
518
+ options; the permission's summary, its input and its keys). It opens on the
519
+ request; `↑`/`↓` (and the wheel), `PgUp`/`PgDn`, `g`/`G` scroll it, `esc`
520
+ closes it, and any other key closes it and does what it always does — `a`
521
+ answers, `y`/`s`/`n` decide. Outside advanced mode `/view` prints the request
522
+ whole into the scrollback. When the question has options its list opens by
523
+ itself and hides the banner; the list's own key line says `/view reads it all`,
524
+ and `/` there leaves the list for the command line (`a` brings it back).
525
+ **`/open`** opens the session's run page on cawdev,
526
+ where the question panel and the decision are. `readerFrame` in split.mjs is
527
+ the window, pure.
528
+
513
529
  **`/view R29`** prints a card here instead: its status, section, tags, branch,
514
530
  work item and pull request, who is coding or reviewing it, what it is related
515
531
  to, its page on the web, and the first thirty lines of its body (`cardLines`,
@@ -559,7 +575,12 @@ serves as a small board — Considering, Planned and In development side by
559
575
  side, one line per card, `●` on a card with a session running now; Merged,
560
576
  Shipped and Declined — work that is over, and only grows — are counts beside
561
577
  the project's name (`1 merged · 2 shipped`), as the console's board folds its
562
- own last columns. As tall as the fullest column, never more than a
578
+ own last columns. The Backlog is a count there too, first (`3 backlog`): since
579
+ R371 it is where a new issue lands, and it would be the first column to go. **Issues are on it too** (R85's other board, read from
580
+ `/issues` beside `/roadmap`): a NEW issue sits in Considering and a CONFIRMED
581
+ one in Planned — their counterparts — first in the column and most severe first,
582
+ so a `+N more` never hides a critical defect, with the ref (`i12`) in the colour
583
+ of its severity. As tall as the fullest column, never more than a
563
584
  third of the screen, and gone before the windows are on a short terminal.
564
585
  When the room runs out it is SAID: a column ends in `+N more`, empty columns
565
586
  go first when the width does, then whole columns (Considering, then Planned —
@@ -607,6 +628,21 @@ the whole of removing it. A clone and not a `git worktree`, on purpose: the
607
628
  daemon's reset checks the default branch out, which git refuses in a second
608
629
  worktree. Changed without a restart, like `agentCommands`.
609
630
 
631
+ ### Where a session with no branch reads: `<first>-read`
632
+
633
+ A plan, a verify or an ask cuts no branch and takes no checkout, so it used to
634
+ run in the project's first checkout **as that checkout happened to be** — on
635
+ whatever branch the last run left there. A cancelled CTO Interview left
636
+ `cto-interview` checked out, hundreds of commits behind, and every plan after
637
+ it read that code. Such a session now reads `<first>-read`, beside the first
638
+ checkout: a **detached `git worktree`** of it, brought to `origin/<default
639
+ branch>` at the start of a session whenever no other session is reading there.
640
+ The checkout itself is never moved, because a coding run may be working in it.
641
+ One per project rather than one per run, because the CLI keeps a resumable
642
+ conversation by its folder. Detached, so the default branch is never checked
643
+ out twice. If it cannot be made — no remote, not a repository — the session
644
+ reads the first checkout as before, and the log says why.
645
+
610
646
  ### `/reload`
611
647
 
612
648
  Quitting and starting again, in one word — for when the client's code changed