cawdev-cli 1.0.8-beta → 1.0.10-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.
- package/README.md +6 -1
- package/lib/roadmap-format.mjs +101 -2
- package/lib/runner-config.mjs +1 -1
- package/mcp/README.md +11 -2
- package/mcp/server.mjs +189 -13
- package/package.json +1 -1
- package/runner/README.md +31 -0
- package/runner/attach.mjs +192 -17
- package/runner/runner.mjs +214 -10
- package/runner/split.mjs +32 -0
package/README.md
CHANGED
|
@@ -5,4 +5,9 @@ and spawning processes are Node's home turf, and a file with no dependencies is
|
|
|
5
5
|
one you can read before running it against your repositories.
|
|
6
6
|
visit https://cawdev.com to learn more
|
|
7
7
|
|
|
8
|
-
https://github.com/4dali/cawdev-cli
|
|
8
|
+
https://github.com/4dali/cawdev-cli
|
|
9
|
+
## jira/
|
|
10
|
+
|
|
11
|
+
`jira/import.mjs` brings a Jira Cloud project's issues, epics, sprints and
|
|
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"`.
|
package/lib/roadmap-format.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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',
|
|
@@ -232,6 +236,32 @@ export function parseRoadmap(text) {
|
|
|
232
236
|
}
|
|
233
237
|
continue;
|
|
234
238
|
}
|
|
239
|
+
// R351. The date shape is checked here, where the line is read: a
|
|
240
|
+
// `Due:` that is not a date is a hand edit, and --live would otherwise
|
|
241
|
+
// report it as drift from a database that never held it.
|
|
242
|
+
const due = /^Due:\s*(.+)$/.exec(line.trim());
|
|
243
|
+
if (due) {
|
|
244
|
+
entry.due = due[1].trim();
|
|
245
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.due) || Number.isNaN(Date.parse(entry.due))) {
|
|
246
|
+
problems.push(`${block.heading}: Due "${due[1].trim()}" is not a date written 2026-11-01.`);
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// R358. The version the card means to ship in; a free string, as `Version:` is.
|
|
251
|
+
const target = /^Target:\s*(.+)$/.exec(line.trim());
|
|
252
|
+
if (target) {
|
|
253
|
+
entry.targetVersion = target[1].trim();
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const estimate = /^Estimate:\s*(.+)$/.exec(line.trim());
|
|
257
|
+
if (estimate) {
|
|
258
|
+
const n = Number(estimate[1].trim());
|
|
259
|
+
entry.estimate = n;
|
|
260
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
261
|
+
problems.push(`${block.heading}: Estimate "${estimate[1].trim()}" is not a positive whole number.`);
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
235
265
|
const development = /^Development:\s*(.+)$/.exec(line.trim());
|
|
236
266
|
if (development) {
|
|
237
267
|
entry.development = statusFromDisplay(development[1].trim());
|
|
@@ -262,6 +292,16 @@ export function parseRoadmap(text) {
|
|
|
262
292
|
entry.afterRefs = tokens.map(({ ref }) => ref);
|
|
263
293
|
continue;
|
|
264
294
|
}
|
|
295
|
+
const parent = /^Parent:\s*(\S+)\s*$/.exec(line.trim());
|
|
296
|
+
if (parent) {
|
|
297
|
+
// R336. One ref, always written in full (R300, i12). A token that is
|
|
298
|
+
// not a card is left as ordinary body text rather than eaten.
|
|
299
|
+
const token = parseRefToken(parent[1]);
|
|
300
|
+
if (token) {
|
|
301
|
+
entry.parentRef = token.ref;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
265
305
|
const tags = /^Tags:\s*(.+)$/.exec(line.trim());
|
|
266
306
|
if (tags) {
|
|
267
307
|
// R315. Already normalised by the server (lowercase, no `#`) by the
|
|
@@ -356,6 +396,45 @@ function* splitBlocks(text) {
|
|
|
356
396
|
if (entry) yield entry;
|
|
357
397
|
}
|
|
358
398
|
|
|
399
|
+
/** The ref of the card this one sits under, from either shape — R336. */
|
|
400
|
+
function parentRefOf(entry) {
|
|
401
|
+
const ref = entry.parent?.ref ?? entry.parentRef;
|
|
402
|
+
return ref ? (parseRefToken(ref)?.ref ?? null) : null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Files each child directly after its parent, within one section — R336.
|
|
407
|
+
*
|
|
408
|
+
* Pure. A child keeps its place among its siblings (they stay in the order
|
|
409
|
+
* given), and a child whose parent is in some other section stays exactly
|
|
410
|
+
* where it was: the file groups by section first, and an epic spread over
|
|
411
|
+
* phases is still one epic by its `Parent:` lines.
|
|
412
|
+
*/
|
|
413
|
+
export function nestUnderParents(list) {
|
|
414
|
+
const refs = new Map(list.map((entry) => [entryRef(entry), entry]));
|
|
415
|
+
const nestedUnder = (entry) => {
|
|
416
|
+
const parent = parentRefOf(entry);
|
|
417
|
+
const card = parent && refs.get(parent);
|
|
418
|
+
// A parent that is itself a child is bad data; leave the child where it is
|
|
419
|
+
// rather than lose it behind a card that is not a root.
|
|
420
|
+
return card && !parentRefOf(card) ? parent : null;
|
|
421
|
+
};
|
|
422
|
+
const children = new Map();
|
|
423
|
+
for (const entry of list) {
|
|
424
|
+
const parent = nestedUnder(entry);
|
|
425
|
+
if (parent) {
|
|
426
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
427
|
+
children.get(parent).push(entry);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const out = [];
|
|
431
|
+
for (const entry of list) {
|
|
432
|
+
if (nestedUnder(entry)) continue;
|
|
433
|
+
out.push(entry, ...(children.get(entryRef(entry)) ?? []));
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
|
|
359
438
|
/**
|
|
360
439
|
* Writes entries as ROADMAP.md's entry sections.
|
|
361
440
|
*
|
|
@@ -385,7 +464,7 @@ export function renderRoadmap(entries, { preamble, sectionOrder = [] }) {
|
|
|
385
464
|
const parts = [preamble.trimEnd(), ''];
|
|
386
465
|
for (const section of ordered) {
|
|
387
466
|
parts.push('---', '', `## ${section}`, '');
|
|
388
|
-
for (const entry of bySection.get(section)) {
|
|
467
|
+
for (const entry of nestUnderParents(bySection.get(section))) {
|
|
389
468
|
parts.push(renderEntry(entry), '');
|
|
390
469
|
}
|
|
391
470
|
}
|
|
@@ -443,6 +522,19 @@ function renderEntry(entry) {
|
|
|
443
522
|
lines.push(`Priority: ${entry.priority}`);
|
|
444
523
|
}
|
|
445
524
|
|
|
525
|
+
// R351. Two facts, written only when the card carries them, after the
|
|
526
|
+
// priority: when, then how big.
|
|
527
|
+
if (entry.due) {
|
|
528
|
+
lines.push(`Due: ${entry.due}`);
|
|
529
|
+
}
|
|
530
|
+
if (entry.estimate != null && entry.estimate > 0) {
|
|
531
|
+
lines.push(`Estimate: ${entry.estimate}`);
|
|
532
|
+
}
|
|
533
|
+
// R358. Where it means to ship; dropped once it has, because `Version:` then says.
|
|
534
|
+
if (entry.targetVersion && !entry.version) {
|
|
535
|
+
lines.push(`Target: ${entry.targetVersion}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
446
538
|
// Each status writes only what it currently claims. A MERGED entry keeps its
|
|
447
539
|
// branch in the database as history, but the file must not print it: the
|
|
448
540
|
// branch is expected to be deleted, and `--live` would then be reading a claim
|
|
@@ -492,6 +584,13 @@ function renderEntry(entry) {
|
|
|
492
584
|
.map(({ ref }) => ref);
|
|
493
585
|
lines.push(`After: ${written.join(', ')}`);
|
|
494
586
|
}
|
|
587
|
+
{
|
|
588
|
+
// R336. The API gives `parent` as {number, ref, title}; the parser gives
|
|
589
|
+
// `parentRef`. Always the full ref, and no line at all without a parent, so
|
|
590
|
+
// no existing card's text changes.
|
|
591
|
+
const parent = parentRefOf(entry);
|
|
592
|
+
if (parent) lines.push(`Parent: ${parent}`);
|
|
593
|
+
}
|
|
495
594
|
if (entry.tags?.length) {
|
|
496
595
|
// R315. Sorted, so the export does not churn on the set's own order.
|
|
497
596
|
lines.push(`Tags: ${[...entry.tags].sort().join(', ')}`);
|
package/lib/runner-config.mjs
CHANGED
|
@@ -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,14 @@ 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. Name a status (`CONSIDERING`, `NEW`) when
|
|
89
|
+
you mean the pipeline. Giving a backlog card a sprint moves it to the first
|
|
90
|
+
status of its kind; moving a card to `BACKLOG` takes it out of its sprint.
|
|
91
|
+
|
|
84
92
|
**Any status may move to any other.** There is no transition diagram — an entry
|
|
85
93
|
really can go from `CONSIDERING` straight to `SHIPPED` if that is what happened.
|
|
86
94
|
The rules are about what a status *carries*, not the path it took.
|
|
@@ -163,11 +171,12 @@ what you are allowed to do.
|
|
|
163
171
|
| `roadmap_list` | Entries, optionally by status. `brief` omits bodies — use it to survey. |
|
|
164
172
|
| `roadmap_get` | One entry in full: its body, **the plan agreed for it**, and the discussion on it. |
|
|
165
173
|
| `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), 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). |
|
|
174
|
+
| `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). |
|
|
167
175
|
| `roadmap_comment` | Say something *beside* an entry rather than inside it. |
|
|
176
|
+
| `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
177
|
| `roadmap_set_status` | Move an entry, carrying whatever the status requires. |
|
|
169
178
|
| `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). |
|
|
179
|
+
| `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
180
|
| `changelog_list` | The changelog, grouped by release, newest first. |
|
|
172
181
|
| `changelog_get` | One entry. |
|
|
173
182
|
| `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,70 @@ 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
|
-
'
|
|
630
|
-
'a
|
|
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.',
|
|
631
689
|
inputSchema: {
|
|
632
690
|
type: 'object',
|
|
633
691
|
properties: {
|
|
@@ -640,6 +698,7 @@ const TOOLS = [
|
|
|
640
698
|
status: {
|
|
641
699
|
type: 'string',
|
|
642
700
|
enum: [
|
|
701
|
+
'BACKLOG',
|
|
643
702
|
'CONSIDERING',
|
|
644
703
|
'PLANNED',
|
|
645
704
|
'IN_DEVELOPMENT',
|
|
@@ -692,6 +751,26 @@ const TOOLS = [
|
|
|
692
751
|
description:
|
|
693
752
|
'How much it matters; it does not reorder the board. Leave out for MEDIUM.',
|
|
694
753
|
},
|
|
754
|
+
due: {
|
|
755
|
+
type: 'string',
|
|
756
|
+
description: 'When it is due, written 2026-11-01. A fact on the card; it moves nothing. Leave out for none.',
|
|
757
|
+
},
|
|
758
|
+
estimate: {
|
|
759
|
+
type: 'integer',
|
|
760
|
+
minimum: 1,
|
|
761
|
+
description: "How big it is, in the team's own unit (points, days — the platform does not say). Leave out for none.",
|
|
762
|
+
},
|
|
763
|
+
targetVersion: {
|
|
764
|
+
type: 'string',
|
|
765
|
+
description: 'The version it is meant to ship in — one the project DEFINED under Project settings → Versions (R362); any other name is refused. The release corrects it if it lands elsewhere. Leave out for none.',
|
|
766
|
+
},
|
|
767
|
+
parent: {
|
|
768
|
+
type: 'string',
|
|
769
|
+
description:
|
|
770
|
+
'The card this one sits under (an epic), e.g. "R300". One level only: the parent ' +
|
|
771
|
+
'must be the same kind and have no parent itself, and this card must have no ' +
|
|
772
|
+
'children. An open card cannot be filed under a SHIPPED or DECLINED one.',
|
|
773
|
+
},
|
|
695
774
|
},
|
|
696
775
|
required: ['title'],
|
|
697
776
|
},
|
|
@@ -701,7 +780,7 @@ const TOOLS = [
|
|
|
701
780
|
method: 'POST',
|
|
702
781
|
body: pick(args, [
|
|
703
782
|
'title', 'body', 'status', 'branch', 'merge', 'version', 'reason', 'section', 'related',
|
|
704
|
-
'after', 'sprint', 'tags', 'assignee', 'priority',
|
|
783
|
+
'after', 'sprint', 'tags', 'assignee', 'priority', 'due', 'estimate', 'targetVersion', 'parent',
|
|
705
784
|
]),
|
|
706
785
|
});
|
|
707
786
|
return `Created ${refOf(entry)} in ${slug}.\n\n${formatEntry(entry, {})}`;
|
|
@@ -713,7 +792,7 @@ const TOOLS = [
|
|
|
713
792
|
description:
|
|
714
793
|
'What is broken in a project, and how badly — R85. The same numbering as the roadmap, ' +
|
|
715
794
|
'written with an i: card 91 filed as an issue is i91, and there is no R91 as well. ' +
|
|
716
|
-
'Statuses are NEW, CONFIRMED, IN_DEVELOPMENT, MERGED (drawn as ' +
|
|
795
|
+
'Statuses are BACKLOG, NEW, CONFIRMED, IN_DEVELOPMENT, MERGED (drawn as ' +
|
|
717
796
|
'Resolved) and DECLINED (Won\'t fix).',
|
|
718
797
|
inputSchema: {
|
|
719
798
|
type: 'object',
|
|
@@ -721,7 +800,7 @@ const TOOLS = [
|
|
|
721
800
|
...PROJECT_ARGUMENT,
|
|
722
801
|
status: {
|
|
723
802
|
type: 'string',
|
|
724
|
-
enum: ['NEW', 'CONFIRMED', 'IN_DEVELOPMENT', 'MERGED', 'SHIPPED', 'DECLINED'],
|
|
803
|
+
enum: ['BACKLOG', 'NEW', 'CONFIRMED', 'IN_DEVELOPMENT', 'MERGED', 'SHIPPED', 'DECLINED'],
|
|
725
804
|
},
|
|
726
805
|
assignee: {
|
|
727
806
|
type: 'string',
|
|
@@ -748,8 +827,10 @@ const TOOLS = [
|
|
|
748
827
|
{
|
|
749
828
|
name: 'issue_file',
|
|
750
829
|
description:
|
|
751
|
-
'File an issue: something that is broken, with how badly. Lands
|
|
752
|
-
'
|
|
830
|
+
'File an issue: something that is broken, with how badly. Lands where the project says ' +
|
|
831
|
+
'for an issue an agent files — the BACKLOG by default, waiting to be brought in, or NEW ' +
|
|
832
|
+
'(filed and not yet triaged) — unless you say NEW, or CONFIRMED, which claims you have ' +
|
|
833
|
+
'already checked it. The ' +
|
|
753
834
|
'severity is required: it is how the board is ordered. Use `related` to name the card it ' +
|
|
754
835
|
'was found on, by ref (R84). Issues count on their own sequence — R221 — and are ' +
|
|
755
836
|
'written with an i: i91 is the ninety-first issue, and R91 is another card.',
|
|
@@ -760,7 +841,7 @@ const TOOLS = [
|
|
|
760
841
|
title: { type: 'string' },
|
|
761
842
|
body: { type: 'string', description: 'Markdown. What happens, and what should instead.' },
|
|
762
843
|
severity: { type: 'string', enum: ['CRITICAL', 'MEDIUM', 'MINOR'] },
|
|
763
|
-
status: { type: 'string', enum: ['NEW', 'CONFIRMED'] },
|
|
844
|
+
status: { type: 'string', enum: ['BACKLOG', 'NEW', 'CONFIRMED'] },
|
|
764
845
|
related: {
|
|
765
846
|
type: 'array',
|
|
766
847
|
items: { type: ['string', 'integer'] },
|
|
@@ -813,14 +894,25 @@ const TOOLS = [
|
|
|
813
894
|
kind: { type: 'string', enum: ['ISSUE', 'FEATURE'] },
|
|
814
895
|
title: { type: 'string' },
|
|
815
896
|
body: { type: 'string', description: 'Markdown. What you noticed, and what should happen instead.' },
|
|
897
|
+
tags: {
|
|
898
|
+
type: 'array',
|
|
899
|
+
items: { type: 'string' },
|
|
900
|
+
description:
|
|
901
|
+
'Who it came from, beyond you — e.g. "customer" when you are passing on what a user ' +
|
|
902
|
+
'said. "agent" is always added: you filed it. Same word rule as card tags.',
|
|
903
|
+
},
|
|
816
904
|
},
|
|
817
905
|
required: ['kind', 'title', 'body'],
|
|
818
906
|
},
|
|
819
907
|
handler: async (config, args) => {
|
|
820
908
|
const slug = await resolveProject(config, args.project);
|
|
909
|
+
// R372: feedback says who it came from, and an agent filing it is one
|
|
910
|
+
// of the sources. Added here, at the agent's own door, not by the API:
|
|
911
|
+
// an application's token files through the same endpoint and is not an agent.
|
|
912
|
+
const tags = [...new Set([...(Array.isArray(args.tags) ? args.tags : []), 'agent'])];
|
|
821
913
|
const item = await api(config, `/api/projects/${slug}/backlog`, {
|
|
822
914
|
method: 'POST',
|
|
823
|
-
body: pick(args, ['kind', 'title', 'body']),
|
|
915
|
+
body: { ...pick(args, ['kind', 'title', 'body']), tags },
|
|
824
916
|
});
|
|
825
917
|
return `Filed in ${slug}'s backlog as pending: "${item.title}". Somebody will accept it ` +
|
|
826
918
|
'as a card or an issue, or refuse it.';
|
|
@@ -830,8 +922,8 @@ const TOOLS = [
|
|
|
830
922
|
{
|
|
831
923
|
name: 'roadmap_update',
|
|
832
924
|
description:
|
|
833
|
-
'Edit an entry\'s title, body, section, sprint, tags, related refs or the cards it
|
|
834
|
-
'after. Use roadmap_set_status to move it.',
|
|
925
|
+
'Edit an entry\'s title, body, section, sprint, tags, parent, related refs or the cards it ' +
|
|
926
|
+
'starts after. Use roadmap_set_status to move it.',
|
|
835
927
|
inputSchema: {
|
|
836
928
|
type: 'object',
|
|
837
929
|
properties: {
|
|
@@ -882,6 +974,25 @@ const TOOLS = [
|
|
|
882
974
|
description:
|
|
883
975
|
'How much it matters; it does not reorder the board. Leave out for MEDIUM (leave out to leave it alone).',
|
|
884
976
|
},
|
|
977
|
+
due: {
|
|
978
|
+
type: 'string',
|
|
979
|
+
description: 'When it is due, written 2026-11-01. Leave out to leave it alone; "" clears it.',
|
|
980
|
+
},
|
|
981
|
+
estimate: {
|
|
982
|
+
type: 'integer',
|
|
983
|
+
minimum: 0,
|
|
984
|
+
description: "How big it is, in the team's own unit. Leave out to leave it alone; 0 clears it.",
|
|
985
|
+
},
|
|
986
|
+
targetVersion: {
|
|
987
|
+
type: 'string',
|
|
988
|
+
description: 'The version it is meant to ship in — one the project defined (R362); any other name is refused. Leave out to leave it alone; "" clears it.',
|
|
989
|
+
},
|
|
990
|
+
parent: {
|
|
991
|
+
type: 'string',
|
|
992
|
+
description:
|
|
993
|
+
'The card this one sits under (an epic), e.g. "R300". "" takes the card out of its ' +
|
|
994
|
+
'parent; leave out to leave it alone. Same rules as roadmap_create.',
|
|
995
|
+
},
|
|
885
996
|
},
|
|
886
997
|
required: ['number'],
|
|
887
998
|
},
|
|
@@ -890,7 +1001,10 @@ const TOOLS = [
|
|
|
890
1001
|
const entry = await api(config, `/api/projects/${slug}/roadmap/${args.number}`, {
|
|
891
1002
|
method: 'PATCH',
|
|
892
1003
|
// pick keeps a present 0, which is how a card leaves its sprint.
|
|
893
|
-
body: pick(args, [
|
|
1004
|
+
body: pick(args, [
|
|
1005
|
+
'title', 'body', 'section', 'related', 'after', 'sprint', 'tags', 'assignee', 'priority',
|
|
1006
|
+
'due', 'estimate', 'targetVersion', 'parent',
|
|
1007
|
+
]),
|
|
894
1008
|
});
|
|
895
1009
|
return `Updated ${refOf(entry)}.\n\n${formatEntry(entry, {})}`;
|
|
896
1010
|
},
|
|
@@ -917,6 +1031,7 @@ const TOOLS = [
|
|
|
917
1031
|
status: {
|
|
918
1032
|
type: 'string',
|
|
919
1033
|
enum: [
|
|
1034
|
+
'BACKLOG',
|
|
920
1035
|
'CONSIDERING',
|
|
921
1036
|
'PLANNED',
|
|
922
1037
|
'NEW',
|
|
@@ -1128,6 +1243,12 @@ const TOOLS = [
|
|
|
1128
1243
|
config,
|
|
1129
1244
|
`/api/projects/${project}/roadmap/${cardRef}/plans`,
|
|
1130
1245
|
).catch(() => []);
|
|
1246
|
+
// R368. The plan's Done-when list, numbered, with the ticks so far. Its
|
|
1247
|
+
// own try, like the plan above: a platform without it answers 404.
|
|
1248
|
+
const criteria = await api(
|
|
1249
|
+
config,
|
|
1250
|
+
`/api/projects/${project}/roadmap/${cardRef}/done-when`,
|
|
1251
|
+
).catch(() => []);
|
|
1131
1252
|
|
|
1132
1253
|
const lines = [];
|
|
1133
1254
|
|
|
@@ -1154,8 +1275,25 @@ const TOOLS = [
|
|
|
1154
1275
|
`run: ${run.state}${run.runnerName ? ` on ${run.runnerName}` : ''}`,
|
|
1155
1276
|
`started by ${run.startedByEmail}`,
|
|
1156
1277
|
'',
|
|
1157
|
-
formatEntry(entry, { comments, plan: plans?.[0] ?? null }),
|
|
1158
1278
|
);
|
|
1279
|
+
// R336. The epic this card was cut from, so a session sees the context.
|
|
1280
|
+
// Its own try, like the history above: a parent that cannot be read (a
|
|
1281
|
+
// 404, no grant) costs the paragraph and never the call.
|
|
1282
|
+
if (entry.parent) {
|
|
1283
|
+
const epic = await api(config, `/api/projects/${project}/roadmap/${entry.parent.ref}`)
|
|
1284
|
+
.catch(() => null);
|
|
1285
|
+
if (epic) {
|
|
1286
|
+
lines.push(
|
|
1287
|
+
`This card is part of ${entry.parent.ref} — ${epic.title}:`,
|
|
1288
|
+
epicExcerpt(epic.body),
|
|
1289
|
+
'',
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
lines.push(formatEntry(entry, { comments, plan: plans?.[0] ?? null }));
|
|
1294
|
+
if (criteria?.length) {
|
|
1295
|
+
lines.push('', formatDoneWhen(criteria));
|
|
1296
|
+
}
|
|
1159
1297
|
|
|
1160
1298
|
// What happened the other times. A session is told the entry and the
|
|
1161
1299
|
// branch but not that two previous runs on this card failed, which is
|
|
@@ -2173,6 +2311,15 @@ function originWord(origin) {
|
|
|
2173
2311
|
}
|
|
2174
2312
|
}
|
|
2175
2313
|
|
|
2314
|
+
/** The first 600 characters of an epic's body, cut at a word — R336. */
|
|
2315
|
+
function epicExcerpt(body) {
|
|
2316
|
+
const text = (body ?? '').trim();
|
|
2317
|
+
if (text.length <= 600) return text;
|
|
2318
|
+
const cut = text.slice(0, 600);
|
|
2319
|
+
const space = cut.lastIndexOf(' ');
|
|
2320
|
+
return `${(space > 0 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2176
2323
|
function formatEntry(entry, { brief, comments, plan }) {
|
|
2177
2324
|
const lines = [`${refOf(entry)} — ${entry.title}`, ` status: ${entry.statusDisplay}`];
|
|
2178
2325
|
if (entry.branch) lines.push(` branch: ${entry.branch}`);
|
|
@@ -2189,6 +2336,11 @@ function formatEntry(entry, { brief, comments, plan }) {
|
|
|
2189
2336
|
if (entry.priority && entry.priority !== 'MEDIUM') {
|
|
2190
2337
|
lines.push(` priority: ${entry.priority}`);
|
|
2191
2338
|
}
|
|
2339
|
+
// R351. Both printed only when the card carries them, as the export does.
|
|
2340
|
+
if (entry.due) lines.push(` due: ${entry.due}`);
|
|
2341
|
+
if (entry.estimate) lines.push(` estimate: ${entry.estimate}`);
|
|
2342
|
+
// R358. Printed until the card ships; `version` says where it did.
|
|
2343
|
+
if (entry.targetVersion && !entry.version) lines.push(` target: ${entry.targetVersion}`);
|
|
2192
2344
|
// R257. Always printed when the card has one, so a reader's shape does not
|
|
2193
2345
|
// depend on which tool printed the card; a closed sprint says so.
|
|
2194
2346
|
if (entry.sprint) {
|
|
@@ -2214,6 +2366,14 @@ function formatEntry(entry, { brief, comments, plan }) {
|
|
|
2214
2366
|
.join(', ')}`,
|
|
2215
2367
|
);
|
|
2216
2368
|
}
|
|
2369
|
+
// R336. The epic this card sits under, and the cards under this one.
|
|
2370
|
+
if (entry.parent) lines.push(` parent: ${entry.parent.ref} — ${entry.parent.title}`);
|
|
2371
|
+
if (entry.children?.length) {
|
|
2372
|
+
const progress = entry.progress ? `${entry.progress.done} of ${entry.progress.total} done — ` : '';
|
|
2373
|
+
lines.push(
|
|
2374
|
+
` children: ${progress}${entry.children.map((c) => `${c.ref} (${c.status})`).join(', ')}`,
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2217
2377
|
// Said in a survey, where the comments themselves are not fetched: an entry
|
|
2218
2378
|
// with an argument attached should be visibly different from one without,
|
|
2219
2379
|
// even in a list. Omitted at zero rather than written as "comments: 0".
|
|
@@ -2262,6 +2422,22 @@ function formatPlan(plan) {
|
|
|
2262
2422
|
return [`--- the plan (${written}) ---`, plan.body].join('\n');
|
|
2263
2423
|
}
|
|
2264
2424
|
|
|
2425
|
+
/**
|
|
2426
|
+
* R368. The Done-when list as an agent reads it: numbered as done_when_tick
|
|
2427
|
+
* takes them, with each tick and its evidence. A reviewer's confirmation is
|
|
2428
|
+
* shown too — it is the other column, and the agent should see it.
|
|
2429
|
+
*/
|
|
2430
|
+
export function formatDoneWhen(criteria) {
|
|
2431
|
+
const lines = ['--- done when (tick each with done_when_tick once you have checked it) ---'];
|
|
2432
|
+
for (const item of criteria) {
|
|
2433
|
+
const mark = item.built ? '[built]' : '[ ]';
|
|
2434
|
+
const note = item.built && item.builtNote ? ` — ${item.builtNote}` : '';
|
|
2435
|
+
const confirmed = item.confirmed ? ' (confirmed by reviewer)' : '';
|
|
2436
|
+
lines.push(`${String(item.position).padStart(2)}. ${mark} ${item.text}${note}${confirmed}`);
|
|
2437
|
+
}
|
|
2438
|
+
return lines.join('\n');
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2265
2441
|
/**
|
|
2266
2442
|
* One earlier attempt at this card — R38.
|
|
2267
2443
|
*
|
package/package.json
CHANGED
package/runner/README.md
CHANGED
|
@@ -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`,
|
|
@@ -607,6 +623,21 @@ the whole of removing it. A clone and not a `git worktree`, on purpose: the
|
|
|
607
623
|
daemon's reset checks the default branch out, which git refuses in a second
|
|
608
624
|
worktree. Changed without a restart, like `agentCommands`.
|
|
609
625
|
|
|
626
|
+
### Where a session with no branch reads: `<first>-read`
|
|
627
|
+
|
|
628
|
+
A plan, a verify or an ask cuts no branch and takes no checkout, so it used to
|
|
629
|
+
run in the project's first checkout **as that checkout happened to be** — on
|
|
630
|
+
whatever branch the last run left there. A cancelled CTO Interview left
|
|
631
|
+
`cto-interview` checked out, hundreds of commits behind, and every plan after
|
|
632
|
+
it read that code. Such a session now reads `<first>-read`, beside the first
|
|
633
|
+
checkout: a **detached `git worktree`** of it, brought to `origin/<default
|
|
634
|
+
branch>` at the start of a session whenever no other session is reading there.
|
|
635
|
+
The checkout itself is never moved, because a coding run may be working in it.
|
|
636
|
+
One per project rather than one per run, because the CLI keeps a resumable
|
|
637
|
+
conversation by its folder. Detached, so the default branch is never checked
|
|
638
|
+
out twice. If it cannot be made — no remote, not a repository — the session
|
|
639
|
+
reads the first checkout as before, and the log says why.
|
|
640
|
+
|
|
610
641
|
### `/reload`
|
|
611
642
|
|
|
612
643
|
Quitting and starting again, in one word — for when the client's code changed
|