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.
- package/lib/background-shells.mjs +45 -3
- package/lib/roadmap-format.mjs +15 -1
- package/lib/runner-config.mjs +1 -1
- package/mcp/README.md +12 -2
- package/mcp/server.mjs +124 -10
- package/package.json +1 -1
- package/runner/README.md +38 -2
- package/runner/attach.mjs +211 -25
- package/runner/git-host.mjs +263 -0
- package/runner/runner.mjs +521 -126
- package/runner/split.mjs +77 -5
- package/runner/workspace-check.mjs +78 -0
|
@@ -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
|
|
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
|
}
|
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',
|
|
@@ -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
|
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,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
|
-
'
|
|
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. 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
|
|
772
|
-
'
|
|
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
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.
|
|
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
|