cawdev-cli 1.0.10-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/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,13 @@ 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.',
724
+ 'holds no sprint: giving it one moves it to CONSIDERING. A plan run\'s card lands ' +
725
+ 'where the console\'s do (R380).',
689
726
  inputSchema: {
690
727
  type: 'object',
691
728
  properties: {
@@ -713,12 +750,12 @@ const TOOLS = [
713
750
  reason: { type: 'string' },
714
751
  section: { type: 'string', description: 'Which part of the roadmap, e.g. "Phase 2 — …".' },
715
752
  sprint: {
716
- type: 'integer',
717
- minimum: 0,
753
+ type: ['integer', 'string'],
718
754
  description:
719
- 'The sprint to file this card into, by number — 1 for S1. 0 takes it out of its ' +
720
- 'sprint; leave it out to leave it alone. A CLOSED sprint refuses a card that is ' +
721
- '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.',
722
759
  },
723
760
  related: {
724
761
  type: 'array',
@@ -866,6 +903,12 @@ const TOOLS = [
866
903
  description:
867
904
  'How much it matters; it does not reorder the board. Leave out for MEDIUM.',
868
905
  },
906
+ affectedVersions: {
907
+ type: 'array',
908
+ items: { type: 'string' },
909
+ description:
910
+ 'Versions the project defined or shipped in where this issue was found — R378. Any other name is refused.',
911
+ },
869
912
  },
870
913
  required: ['title', 'severity'],
871
914
  },
@@ -873,7 +916,7 @@ const TOOLS = [
873
916
  const slug = await resolveProject(config, args.project);
874
917
  const issue = await api(config, `/api/projects/${slug}/issues`, {
875
918
  method: 'POST',
876
- body: pick(args, ['title', 'body', 'severity', 'status', 'related', 'tags', 'assignee', 'priority']),
919
+ body: pick(args, ['title', 'body', 'severity', 'status', 'related', 'tags', 'assignee', 'priority', 'affectedVersions']),
877
920
  });
878
921
  return `Filed ${refOf(issue)} in ${slug}.\n\n${formatEntry(issue, {})}`;
879
922
  },
@@ -922,7 +965,7 @@ const TOOLS = [
922
965
  {
923
966
  name: 'roadmap_update',
924
967
  description:
925
- '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 ' +
926
969
  'starts after. Use roadmap_set_status to move it.',
927
970
  inputSchema: {
928
971
  type: 'object',
@@ -936,12 +979,12 @@ const TOOLS = [
936
979
  body: { type: 'string' },
937
980
  section: { type: 'string' },
938
981
  sprint: {
939
- type: 'integer',
940
- minimum: 0,
982
+ type: ['integer', 'string'],
941
983
  description:
942
- 'The sprint to file this card into, by number — 1 for S1. 0 takes it out of its ' +
943
- 'sprint; leave it out to leave it alone. A CLOSED sprint refuses a card that is ' +
944
- '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.',
945
988
  },
946
989
  related: {
947
990
  type: 'array',
@@ -993,6 +1036,13 @@ const TOOLS = [
993
1036
  'The card this one sits under (an epic), e.g. "R300". "" takes the card out of its ' +
994
1037
  'parent; leave out to leave it alone. Same rules as roadmap_create.',
995
1038
  },
1039
+ affectedVersions: {
1040
+ type: 'array',
1041
+ items: { type: 'string' },
1042
+ description:
1043
+ 'Issues only (R378): the versions the project defined or shipped in where it was found. ' +
1044
+ 'Leave out to leave them alone; [] clears them. A roadmap card refuses a non-empty list.',
1045
+ },
996
1046
  },
997
1047
  required: ['number'],
998
1048
  },
@@ -1003,7 +1053,7 @@ const TOOLS = [
1003
1053
  // pick keeps a present 0, which is how a card leaves its sprint.
1004
1054
  body: pick(args, [
1005
1055
  'title', 'body', 'section', 'related', 'after', 'sprint', 'tags', 'assignee', 'priority',
1006
- 'due', 'estimate', 'targetVersion', 'parent',
1056
+ 'due', 'estimate', 'targetVersion', 'parent', 'affectedVersions',
1007
1057
  ]),
1008
1058
  });
1009
1059
  return `Updated ${refOf(entry)}.\n\n${formatEntry(entry, {})}`;
@@ -1013,7 +1063,7 @@ const TOOLS = [
1013
1063
  {
1014
1064
  name: 'roadmap_set_status',
1015
1065
  description:
1016
- '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 ' +
1017
1067
  'a status must carry, not a permitted path. IN_DEVELOPMENT needs a branch, MERGED the ' +
1018
1068
  'merge — the PR, the merge commit or the sha — and SHIPPED a version. Move a card to ' +
1019
1069
  'MERGED when its pull request lands; the branch may then be deleted. ' +
@@ -1062,7 +1112,7 @@ const TOOLS = [
1062
1112
  {
1063
1113
  name: 'roadmap_decline',
1064
1114
  description:
1065
- '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. ' +
1066
1116
  'The reason is the point: it stops the idea being proposed again.',
1067
1117
  inputSchema: {
1068
1118
  type: 'object',
@@ -1139,10 +1189,10 @@ const TOOLS = [
1139
1189
  entryNumber: {
1140
1190
  type: ['string', 'integer'],
1141
1191
  description:
1142
- '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 '
1143
1193
  + 'bare number for a roadmap card (R221). Set it. Until R156 the only link was the '
1144
1194
  + 'ref you type into the prose, which cannot be queried, so a release could not '
1145
- + '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 '
1146
1196
  + 'the first. Put the ref in the text as well; that is what a reader sees.',
1147
1197
  },
1148
1198
  },
@@ -1175,8 +1225,8 @@ const TOOLS = [
1175
1225
  entryNumber: {
1176
1226
  type: ['string', 'integer'],
1177
1227
  description:
1178
- 'The roadmap card or issue this entry describes — R156 — by ref (R91, i91; a bare '
1179
- + '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; '
1180
1230
  + 'omitting it leaves whatever link is already there.',
1181
1231
  },
1182
1232
  },
@@ -1197,9 +1247,9 @@ const TOOLS = [
1197
1247
  {
1198
1248
  name: 'task_current',
1199
1249
  description:
1200
- '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 ' +
1201
1251
  'everything already said and asked on this run. Call it first, and again whenever you ' +
1202
- '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.',
1203
1253
  inputSchema: { type: 'object', properties: {} },
1204
1254
  handler: async (config) => {
1205
1255
  const { runId, project } = await requireRun(config);
@@ -1421,20 +1471,20 @@ const TOOLS = [
1421
1471
  {
1422
1472
  name: 'propose_entry',
1423
1473
  description:
1424
- '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. ' +
1425
1475
  '`kind` is your opinion: "issue" ' +
1426
1476
  '(broken, unsafe, or loses data — with a severity) or "roadmap" (it should also do this, ' +
1427
1477
  'or do it better — no severity). A PERSON decides, and may file it the other way — you ' +
1428
- '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 ' +
1429
1479
  'before there was a choice files what it always did. Severity is "critical" (broken, ' +
1430
1480
  'unsafe, or loses data), "medium" (it will hurt, but not today) or "minor" (worth doing, ' +
1431
- '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 ' +
1432
1482
  'ONE for every card of the idea. A roadmap card MUST say what it starts after — `after`: ' +
1433
1483
  '`[]` for a card that starts after none, `"#2"` for the second card you proposed in this ' +
1434
1484
  'run (the number this tool answered with), `"R12"` for a card already on the roadmap. ' +
1435
1485
  'You can only name what has already been proposed, so file in build order. Write each ' +
1436
- 'one as an entry would be written, and say where in the code you saw it. Only an audit ' +
1437
- '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.',
1438
1488
  inputSchema: {
1439
1489
  type: 'object',
1440
1490
  properties: {
@@ -1459,7 +1509,7 @@ const TOOLS = [
1459
1509
  type: 'string',
1460
1510
  description:
1461
1511
  'The roadmap section this belongs under, in your opinion. Optional. The person ' +
1462
- '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 ' +
1463
1513
  'uses the same section on every card it cuts from one idea.',
1464
1514
  },
1465
1515
  after: {
@@ -1471,7 +1521,7 @@ const TOOLS = [
1471
1521
  'card you proposed in THIS run, by the number this tool answered with; it must ' +
1472
1522
  'already have been proposed, so propose what a card depends on before the card. ' +
1473
1523
  '`"R12"` or `"i7"` names a card already on the roadmap. A person adds the cards ' +
1474
- '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 ' +
1475
1525
  'landed.',
1476
1526
  },
1477
1527
  },
@@ -1551,7 +1601,7 @@ const TOOLS = [
1551
1601
  name: 'ask_group',
1552
1602
  description:
1553
1603
  'Ask a ROUND of questions at once, and wait for all of them. Use it when you have several ' +
1554
- '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 ' +
1555
1605
  'title, on one form, instead of interrupting them once per question. At most 12 in a ' +
1556
1606
  'round; ask the rest in the next one. Blocks for up to ten minutes; if the round is ' +
1557
1607
  'unfinished by then it returns a group_id for await_group.',
@@ -1561,7 +1611,7 @@ const TOOLS = [
1561
1611
  title: {
1562
1612
  type: 'string',
1563
1613
  description:
1564
- '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".',
1565
1615
  },
1566
1616
  intro: {
1567
1617
  type: 'string',
@@ -1691,7 +1741,7 @@ const TOOLS = [
1691
1741
  name: 'await_answer',
1692
1742
  description:
1693
1743
  'Resume waiting for a question ask_user handed back. Between the two you experience one ' +
1694
- 'natural blocking ask; the person experiences an inbox item.',
1744
+ 'natural blocking ask; the person experiences a Waiting on you item.',
1695
1745
  inputSchema: {
1696
1746
  type: 'object',
1697
1747
  properties: { question_id: { type: 'string' } },
@@ -2051,11 +2101,23 @@ function approvalTimeoutSeconds() {
2051
2101
  // The same goes for comments — nothing here removes one, and nothing there
2052
2102
  // does either.
2053
2103
  //
2054
- // Nor is there a sprint_open, sprint_close or sprint_rename — R257. Opening,
2055
- // closing and renaming a sprint are a person's acts on the page, and the API
2056
- // says so structurally (POST/PATCH /sprints take a session, not a token). An
2057
- // agent's whole reach here is reading a card's sprint and filing a card into
2058
- // 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
+ }
2059
2121
 
2060
2122
  function pick(source, keys) {
2061
2123
  const out = {};
@@ -2341,11 +2403,13 @@ function formatEntry(entry, { brief, comments, plan }) {
2341
2403
  if (entry.estimate) lines.push(` estimate: ${entry.estimate}`);
2342
2404
  // R358. Printed until the card ships; `version` says where it did.
2343
2405
  if (entry.targetVersion && !entry.version) lines.push(` target: ${entry.targetVersion}`);
2406
+ // R378. History, not plan: still printed once the issue has shipped.
2407
+ if (entry.affectedVersions?.length) lines.push(` affects: ${entry.affectedVersions.join(', ')}`);
2344
2408
  // R257. Always printed when the card has one, so a reader's shape does not
2345
2409
  // depend on which tool printed the card; a closed sprint says so.
2346
2410
  if (entry.sprint) {
2347
2411
  lines.push(
2348
- ` sprint: ${entry.sprint.ref} ${entry.sprint.name}${entry.sprint.state === 'CLOSED' ? ' (closed)' : ''}`,
2412
+ ` sprint: ${sprintLine(entry.sprint)}`,
2349
2413
  );
2350
2414
  }
2351
2415
  if (entry.related?.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cawdev-cli",
3
- "version": "1.0.10-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
@@ -469,7 +470,7 @@ this machine serves — unless the command names it with `@project`, or
469
470
  advanced mode is narrowed to ONE project (`/projects`), which is a choice
470
471
  already made rather than a guess. Never guessed from the focused window, nor
471
472
  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,
473
+ 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
474
  each with its own, so they ask nothing more. A usage window over this machine's threshold is refused in a
474
475
  sentence; `--anyway` starts it regardless, the same decision the console's
475
476
  panel asks for. All four commands need you signed in (`/login`), and they work
@@ -575,7 +576,12 @@ serves as a small board — Considering, Planned and In development side by
575
576
  side, one line per card, `●` on a card with a session running now; Merged,
576
577
  Shipped and Declined — work that is over, and only grows — are counts beside
577
578
  the project's name (`1 merged · 2 shipped`), as the console's board folds its
578
- own last columns. As tall as the fullest column, never more than a
579
+ own last columns. The Backlog is a count there too, first (`3 backlog`): since
580
+ 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
581
+ `/issues` beside `/roadmap`): a NEW issue sits in Considering and a CONFIRMED
582
+ one in Planned — their counterparts — first in the column and most severe first,
583
+ so a `+N more` never hides a critical defect, with the ref (`i12`) in the colour
584
+ of its severity. As tall as the fullest column, never more than a
579
585
  third of the screen, and gone before the windows are on a short terminal.
580
586
  When the room runs out it is SAID: a column ends in `+N more`, empty columns
581
587
  go first when the width does, then whole columns (Considering, then Planned —
@@ -689,7 +695,7 @@ it ends.
689
695
  A bare path means one workspace, which is what every config meant before this
690
696
  existed — nothing changes for a machine that serves one checkout per project.
691
697
 
692
- 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
693
699
  waits now says **"no free workspace in cawdev (2 here, all busy)"** instead of
694
700
  "that project already has a run here", which was a proxy for it.
695
701
 
@@ -712,18 +718,16 @@ says about knowing it.
712
718
 
713
719
  **This number caps coding and nothing else** (R70). `ASK`, `ROADMAP`, `AUDIT`
714
720
  and `SCOPE` runs take no workspace, so a project whose checkouts are all busy
715
- still starts a question, an entry-writing session, an audit and a scoping
716
- 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`
717
723
  and `RELEASE` do take one — each commits something — and count against this
718
724
  number; what each may *write* in it is far narrower than a coding run's and is
719
725
  decided by the profile, not by the prompt: an interview writes `docs/brief/`,
720
726
  a merge writes the files git put conflict markers in, and a release (R187)
721
727
  writes exactly the version files the platform names on the claim, with `git`,
722
728
  `gh pr create`, `gh pr view` and `node` for the exports — no build tools, no
723
- permission prompt, no `gh pr merge`. What holds the rest back
724
- is `maxSessions`, which counts every profile — **"at 4 sessions on this machine
725
- (every profile counts)"** is the other thing a waiting run can say, and the log
726
- 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`).
727
731
 
728
732
  **One thing this gate cannot express, and does not try to.** It serialises on a
729
733
  checkout, so two runs on *the same branch* in two checkouts are, to this daemon,
@@ -792,8 +796,8 @@ whether the browser is allowed. Those five answer nearly every "why did that
792
796
  not happen", and they used to be spread across a config file and a shrug.
793
797
 
794
798
  Attaching adds a second bar row: the URL, each project as **coding sessions over
795
- checkouts** (`cawdev 1/2`), and the machine's total against `maxSessions`.
796
- 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
797
801
  total on the right and not in any project's figure, because it took no checkout.
798
802
  A run that is waiting is explained by the bar
799
803
  above it rather than by reading the source. A narrow terminal drops projects
@@ -844,11 +848,11 @@ than twenty-six. A machine that wants it unattended puts that string in its own
844
848
  ### What a person can ask of a checkout
845
849
 
846
850
  R57. The daemon polls `workspace-requests/claim` every few seconds and does one
847
- 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
848
852
  `HANDOFF` had been missing from the list since R148 — a table that quietly does
849
853
  not describe one of the things a person can ask for is worse than no table. So
850
854
  count the rows below against that number before you trust either. `CUT_BRANCH`
851
- 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.
852
856
 
853
857
  | | |
854
858
  |---|---|
@@ -862,6 +866,7 @@ is the ninth, R260's; `OPEN_PR` the tenth, R261's.
862
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. |
863
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. |
864
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. |
865
870
 
866
871
  **A `MERGE` that fails also says which KIND of failure it was** — R155, in one
867
872
  of five words beside `gh`'s own unchanged text:
@@ -1252,12 +1257,9 @@ rest queue.
1252
1257
 
1253
1258
  **Questions are not serialised.** An `ASK` run prepares nothing and writes
1254
1259
  nothing, so it runs alongside whatever else is going on — you can ask about a
1255
- project while an agent is working in it. `maxSessions` (4 by default) bounds how
1256
- many agent processes this machine will host at once, and it is the **only**
1257
- thing bounding a question, a roadmap session or an audit (R70). It counts runs
1258
- this daemon has claimed, not children it has spawned: a claim takes a second or
1259
- two to become a process, and counting processes let one pass of the loop claim
1260
- 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.
1261
1263
 
1262
1264
  ## If the daemon dies mid-run
1263
1265
 
@@ -1386,7 +1388,7 @@ permission stops forever.
1386
1388
 
1387
1389
  - `--permission-mode acceptEdits` lets it write files.
1388
1390
  - `--allowedTools mcp__cawdev__…` lets it use the cawdev tools. **Without this
1389
- 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,
1390
1392
  no reporting, no asking. A real session found this by being denied
1391
1393
  `task_current` and stopping rather than guessing, which was the right call
1392
1394
  and a good sign for the prompt.
@@ -1412,7 +1414,7 @@ never asks you anything — an unlisted tool is refused, not raised (i138).
1412
1414
 
1413
1415
  **Anything else stops and asks you** (R51). Rather than being denied in
1414
1416
  silence, a session that needs `mvn` or `npm` posts a request that appears in
1415
- 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
1416
1418
  always here — which writes a rule on the project that every runner serving it
1417
1419
  inherits.
1418
1420
 
@@ -1543,8 +1545,8 @@ after `--mcp-config` fails with `ENAMETOOLONG`, which names neither the flag nor
1543
1545
  the prompt.
1544
1546
 
1545
1547
  The prompt deliberately teaches the **method**, not the task. The task is in the
1546
- roadmap entry, which the agent reads for itself with `task_current` — putting it
1547
- 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.
1548
1550
 
1549
1551
  ### Codex CLI
1550
1552
 
package/runner/attach.mjs CHANGED
@@ -61,7 +61,7 @@ import { clip, keyList, padVisible, painter, stripAnsi, visibleWidth, wrap } fro
61
61
  import { restartNeededFor } from '../lib/runner-config.mjs';
62
62
  import {
63
63
  BOARD_COLUMNS, MIN_ZONE_HEIGHT, PROMPT_ROWS, SplitScreen, readerFrame, tabsLine, visibleSessions, actionsFor, composeFrame,
64
- carriesOnHere, builtNothing, cardLines, cardTitleFor, commandWords, wordsText, subtitleFor, agentModelFor, wordWrap, isAutoCheckout, isPullRequest, parseNew, roadmapStrip, sessionTag, splitLayout, titleFor, toneFor,
64
+ carriesOnHere, builtNothing, cardLines, columnOf, cardTitleFor, commandWords, wordsText, subtitleFor, agentModelFor, wordWrap, isAutoCheckout, isPullRequest, parseNew, roadmapStrip, sessionTag, splitLayout, titleFor, toneFor,
65
65
  windowLines,
66
66
  } from './split.mjs';
67
67
  import { KNOWN_AGENTS, agentOfCommand } from '../lib/agents.mjs';
@@ -3413,9 +3413,10 @@ export class Attached {
3413
3413
  }
3414
3414
 
3415
3415
  /**
3416
- * Every card of every project this machine serves, whatever its status —
3417
- * the strip draws them all, and `d` offers the PLANNED ones. One request
3418
- * per project, the board's own (`brief`, so no bodies). Needs a person's
3416
+ * Every card AND ISSUE of every project this machine serves, whatever its
3417
+ * status — the strip draws them all, and `d` offers the PLANNED cards and
3418
+ * the CONFIRMED issues. Two requests per project, the two boards' own
3419
+ * (`brief`, so no bodies). Needs a person's
3419
3420
  * session, like everything read off the roadmap here.
3420
3421
  *
3421
3422
  * A project that cannot be read keeps ITS last cards and no other's: this
@@ -3435,6 +3436,15 @@ export class Attached {
3435
3436
  for (const card of Array.isArray(cards) ? cards : []) {
3436
3437
  all.push({ ref: card.ref, title: card.title, slug, status: card.status, live: Boolean(card.activeRun) });
3437
3438
  }
3439
+ // The issues are another list on the platform (R85), and were never
3440
+ // read here: the strip drew a project's ideas and not one of its bugs.
3441
+ const issues = await this.session.request(`/api/projects/${slug}/issues?brief=true`);
3442
+ for (const issue of Array.isArray(issues) ? issues : []) {
3443
+ all.push({
3444
+ ref: issue.ref, title: issue.title, slug, status: issue.status,
3445
+ live: Boolean(issue.activeRun), severity: issue.severity ?? 'MINOR',
3446
+ });
3447
+ }
3438
3448
  read.push(slug);
3439
3449
  this.gone.delete(slug);
3440
3450
  } catch (failure) {
@@ -3447,7 +3457,8 @@ export class Attached {
3447
3457
  }
3448
3458
  }
3449
3459
  this.cards = all;
3450
- this.planned = all.filter((card) => card.status === 'PLANNED');
3460
+ // Ready to be developed: a planned card, or a confirmed issue.
3461
+ this.planned = all.filter((card) => columnOf(card.status) === 'PLANNED');
3451
3462
  this.dirty = true;
3452
3463
  if (!hold) this.sayGone();
3453
3464
  return { signedIn: true, read, failed };
@@ -3482,7 +3493,7 @@ export class Attached {
3482
3493
  const counts = read.filter((slug) => shown.has(slug)).map((slug) => {
3483
3494
  const mine = this.cards.filter((card) => card.slug === slug);
3484
3495
  const tally = BOARD_COLUMNS
3485
- .map(([status, label]) => `${mine.filter((card) => card.status === status).length} ${label.toLowerCase()}`)
3496
+ .map(([status, label]) => `${mine.filter((card) => columnOf(card.status) === status).length} ${label.toLowerCase()}`)
3486
3497
  .join(' · ');
3487
3498
  return `${slug}: ${tally}`;
3488
3499
  });
@@ -3498,8 +3509,8 @@ export class Attached {
3498
3509
  }
3499
3510
 
3500
3511
  /**
3501
- * `d`, or `/new dev` with nothing after it: the planned cards as a list, and
3502
- * the one picked is developed — `/new dev R3 @slug`, on this machine.
3512
+ * `d`, or `/new dev` with nothing after it: the planned cards — and the
3513
+ * confirmed issues — as a list, and the one picked is developed — `/new dev R3 @slug`, on this machine.
3503
3514
  */
3504
3515
  async pickPlanned(slug = null) {
3505
3516
  if (!this.requireSignIn('start a session')) return;