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/README.md CHANGED
@@ -10,4 +10,14 @@ https://github.com/4dali/cawdev-cli
10
10
 
11
11
  `jira/import.mjs` brings a Jira Cloud project's issues, epics, sprints and
12
12
  comments into a cawdev project, once and again without duplicates (R337). Run from a checkout; see
13
- "Leaving Jira" in GUIDE.md. Its tests: `node --test "tools/jira/*.test.mjs"`.
13
+ "Leaving Jira" in GUIDE.md. `--team <team id>` (R489) names the team the sprints open in, when the project is
14
+ shared with several writer teams. Its tests: `node --test "tools/jira/*.test.mjs"`.
15
+
16
+ ## The `Sprint:` line
17
+
18
+ A card in a sprint carries one `Sprint:` line in `ROADMAP.md` and `ISSUES.md`
19
+ (`lib/roadmap-format.mjs`): `Sprint: Core team · S7 Notifications` since sprints
20
+ became a team's (R489). Files written before that, `Sprint: S7 Notifications`,
21
+ still parse (with no team). `roadmap/import.mjs` sends the words
22
+ (`Core team · S7`) for the first form and the number for the second, so a file
23
+ naming a sprint the target does not have is refused with the API's own sentence.
@@ -17,6 +17,15 @@
17
17
  * - `BashOutput` (or `TaskOutput`) reads one by `bash_id`/`shell_id`/`task_id`;
18
18
  * the command is over only when a read says a status other than running;
19
19
  * - `KillShell`/`KillBash`/`TaskStop` ends one.
20
+ * And a delegation started in the background: `Agent` (or `Task`) with
21
+ * `run_in_background: true`, or whose result says "Async agent launched" —
22
+ * its id is the result's `agentId: <id>`. It is over when a `<task-notification>`
23
+ * naming that id arrives with a status other than running, when a
24
+ * `TaskOutput` read says so, or when `TaskStop` ends it. R6 on saasng is why:
25
+ * IMPLEMENT handed the whole card to two background agents, said "both agents
26
+ * are still working in the background" and ended its turn; the walk marched on
27
+ * and the run FINISHED with nothing built.
28
+ *
20
29
  * A command whose id cannot be read off its result stays unfinished: failing
21
30
  * loudly and re-running a stage costs a turn, calling unproven work proven
22
31
  * costs somebody's afternoon.
@@ -26,6 +35,7 @@
26
35
  */
27
36
 
28
37
  const READS = new Set(['BashOutput', 'TaskOutput']);
38
+ const DELEGATES = new Set(['Agent', 'Task']);
29
39
  const KILLS = new Set(['KillShell', 'KillBash', 'TaskStop']);
30
40
 
31
41
  /** The text of a tool_result's content, whichever of its two shapes it came in. */
@@ -44,15 +54,19 @@ function shellIdOf(input) {
44
54
 
45
55
  export class BackgroundShells {
46
56
  constructor() {
47
- /** tool_use id of each background `Bash` → { command, shellId, done }. */
57
+ /** tool_use id of each background `Bash` or `Agent` → { command, shellId, done }. */
48
58
  this.started = new Map();
59
+ /** tool_use id of each `Agent` call not yet known to be in the background → its label and input. */
60
+ this.delegations = new Map();
49
61
  /** tool_use id of each read → the shell id it read. */
50
62
  this.reads = new Map();
51
63
  }
52
64
 
53
65
  /** One stream-json event. Anything that is not about a shell is ignored. */
54
66
  observe(event) {
55
- const parts = event?.message?.content;
67
+ const content = event?.message?.content;
68
+ if (event?.type === 'user') this.notified(textOf(content));
69
+ const parts = content;
56
70
  if (!Array.isArray(parts)) return;
57
71
  if (event.type === 'assistant') {
58
72
  for (const part of parts) {
@@ -63,6 +77,9 @@ export class BackgroundShells {
63
77
  shellId: null,
64
78
  done: false,
65
79
  });
80
+ } else if (DELEGATES.has(part.name)) {
81
+ const label = `Agent: ${String(part.input?.description ?? part.input?.subagent_type ?? 'a delegation').trim()}`;
82
+ this.delegations.set(part.id, { label, background: part.input?.run_in_background === true });
66
83
  } else if (READS.has(part.name)) {
67
84
  const id = shellIdOf(part.input);
68
85
  if (id) this.reads.set(part.id, id);
@@ -76,6 +93,17 @@ export class BackgroundShells {
76
93
  for (const part of parts) {
77
94
  if (part?.type !== 'tool_result') continue;
78
95
  const text = textOf(part.content);
96
+ const delegation = this.delegations.get(part.tool_use_id);
97
+ if (delegation) {
98
+ this.delegations.delete(part.tool_use_id);
99
+ // A foreground delegation's result IS its end; only one that came
100
+ // back as "launched" is still out there.
101
+ if (delegation.background || /async agent launched/i.test(text)) {
102
+ const id = /\bagentId:?\s*([A-Za-z0-9_-]+)/i.exec(text);
103
+ this.started.set(part.tool_use_id, { command: delegation.label, shellId: id ? id[1] : null, done: false });
104
+ }
105
+ continue;
106
+ }
79
107
  const launched = this.started.get(part.tool_use_id);
80
108
  if (launched && !launched.shellId) {
81
109
  const id = /\bID:?\s*([A-Za-z0-9_-]+)/.exec(text);
@@ -94,6 +122,20 @@ export class BackgroundShells {
94
122
  }
95
123
  }
96
124
 
125
+ /**
126
+ * The CLI tells a session that a background task ended with a
127
+ * `<task-notification>` in a user turn — `<task-id>` and `<status>`. Any
128
+ * status but running is over: a failed or killed agent is not one to wait for.
129
+ */
130
+ notified(text) {
131
+ if (!text || !text.includes('<task-notification>')) return;
132
+ for (const block of text.split('<task-notification>').slice(1)) {
133
+ const id = /<task-id>\s*([^<\s]+)\s*<\/task-id>/.exec(block);
134
+ const status = /<status>\s*([^<\s]+)\s*<\/status>/.exec(block);
135
+ if (id && status && status[1].toLowerCase() !== 'running') this.finish(id[1]);
136
+ }
137
+ }
138
+
97
139
  finish(shellId) {
98
140
  if (!shellId) return;
99
141
  for (const each of this.started.values()) {
@@ -101,7 +143,7 @@ export class BackgroundShells {
101
143
  }
102
144
  }
103
145
 
104
- /** The commands started in the background whose end this session never saw. */
146
+ /** The commands and delegations started in the background whose end this session never saw. */
105
147
  unfinished() {
106
148
  return [...this.started.values()].filter((each) => !each.done).map((each) => each.command);
107
149
  }
@@ -253,6 +253,12 @@ export function parseRoadmap(text) {
253
253
  entry.targetVersion = target[1].trim();
254
254
  continue;
255
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
+ }
256
262
  const estimate = /^Estimate:\s*(.+)$/.exec(line.trim());
257
263
  if (estimate) {
258
264
  const n = Number(estimate[1].trim());
@@ -326,14 +332,24 @@ export function parseRoadmap(text) {
326
332
  entry.origin = origin[1].trim();
327
333
  continue;
328
334
  }
329
- const sprint = /^Sprint:\s*S(\d+)\s*(.*)$/.exec(line.trim());
335
+ const sprint = /^Sprint:\s*(?:(?!S\d+(?:\s|$))(.+?)\s*·\s*)?S(\d+)\s*(.*)$/.exec(line.trim());
330
336
  if (sprint) {
331
- // R257: `Sprint: S1 Notifications` — the sprint's number and its name
332
- // as the file says it. The same shape the API's `sprint` has, so a
333
- // re-render from either is byte-identical; the importer sends only
334
- // the number, since a sprint's name is the database's to say.
335
- const number = Number(sprint[1]);
336
- entry.sprint = { number, ref: `S${number}`, name: sprint[2].trim() };
337
+ // R257, R489: `Sprint: Core team · S1 Notifications` — a team's
338
+ // sprint, its number and its name as the file says it. A file written
339
+ // before sprints were teams' says `Sprint: S1 Notifications`; that
340
+ // still parses, with no team. The same shape the API's `sprint` has
341
+ // (`label`, `ref`, `number`, `name`), so a re-render from either is
342
+ // byte-identical; the importer sends the team's words and the number,
343
+ // since a sprint's name is the database's to say.
344
+ const number = Number(sprint[2]);
345
+ const team = sprint[1]?.trim() || null;
346
+ entry.sprint = {
347
+ team,
348
+ number,
349
+ ref: `S${number}`,
350
+ label: team ? `${team} · S${number}` : `S${number}`,
351
+ name: sprint[3].trim(),
352
+ };
337
353
  continue;
338
354
  }
339
355
  bodyLines.push(line);
@@ -534,6 +550,10 @@ function renderEntry(entry) {
534
550
  if (entry.targetVersion && !entry.version) {
535
551
  lines.push(`Target: ${entry.targetVersion}`);
536
552
  }
553
+ // R378. Where an issue was found; history, so it stays once the issue ships.
554
+ if (entry.affectedVersions?.length) {
555
+ lines.push(`Affects: ${entry.affectedVersions.join(', ')}`);
556
+ }
537
557
 
538
558
  // Each status writes only what it currently claims. A MERGED entry keeps its
539
559
  // branch in the database as history, but the file must not print it: the
@@ -609,7 +629,10 @@ function renderEntry(entry) {
609
629
  // API's `sprint` and the parser's are the same shape, which is what makes
610
630
  // render → parse → render a fixed point. State is not written — the file
611
631
  // says which sprint, the database says whether it is still open.
612
- lines.push(`Sprint: ${entry.sprint.ref ?? `S${entry.sprint.number}`} ${entry.sprint.name}`.trimEnd());
632
+ // R489: the label (`Core team · S7`) is the only spelling that means
633
+ // anything outside the team; `ref` alone is the pre-teams file's.
634
+ const label = entry.sprint.label ?? entry.sprint.ref ?? `S${entry.sprint.number}`;
635
+ lines.push(`Sprint: ${label} ${entry.sprint.name ?? ''}`.trimEnd());
613
636
  }
614
637
 
615
638
  if (entry.body?.trim()) {
@@ -75,6 +75,8 @@ export const CAWDEV_READS = new Set([
75
75
  'quality_get',
76
76
  'roadmap_statuses',
77
77
  'roadmap_list',
78
+ // R489. The project's sprints, read from the endpoint a card's picker uses.
79
+ 'sprint_list',
78
80
  'roadmap_get',
79
81
  'issue_list',
80
82
  'changelog_list',
package/mcp/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  Plain Node, zero dependencies, stdio JSON-RPC — `server.mjs`, plus the shared
4
4
  rule matcher in `tools/lib/tool-rules.mjs` that the runner uses too. It gives a coding
5
5
  agent a project's roadmap and changelog — the same verbs a person gets in the
6
- console, because the API was built to mirror these tools one-for-one.
6
+ console, because the API was built to mirror these tools one-for-one. This README says
7
+ **card** for the roadmap thing and **run** for the work on it, as the console does; the API and
8
+ the tool names say *entry* (`roadmap_list`, `propose_entry`, `EntryRef`) and keep doing so.
7
9
 
8
10
  ## Setting it up in a repository
9
11
 
@@ -59,23 +61,23 @@ four being different from what you assumed.
59
61
 
60
62
  The sequence is fixed, and the second step is the one people skip:
61
63
 
62
- 1. Branch off an up-to-date `main`, named after the entry — `r4-roadmap-entries`.
63
- 2. **Move the entry to `CODING` naming the branch, before your first commit**
64
+ 1. Branch off an up-to-date `main`, named after the card — `r4-roadmap-entries`.
65
+ 2. **Move the card to `IN_DEVELOPMENT` naming the branch, before your first commit**
64
66
  (`roadmap_set_status`). Not after. The roadmap should be able to answer
65
67
  "what is being worked on right now" without asking anyone.
66
- 3. Build to the entry's "Done when" list.
68
+ 3. Build to the card's "Done when" list.
67
69
  4. Finish with a PR. Never push to `main`. As your session ends the platform
68
70
  moves the *branch* on the development board, not the card: to `REVIEW` where
69
71
  the project runs `require_review` and somebody is going to read it, and to
70
72
  `DONE` where it does not — a finish nobody was asked about (R132), because
71
73
  there is no reviewer coming.
72
- 5. When the PR merges, move the entry to `MERGED` naming it. The branch may then
73
- be deleted — `CODING` is for work somebody is doing, not work that is done.
74
+ 5. When the PR merges, move the card to `MERGED` naming it. The branch may then
75
+ be deleted — `IN_DEVELOPMENT` is for work somebody is doing, not work that is done.
74
76
 
75
77
  ### What a status must carry
76
78
 
77
79
  `roadmap_statuses` will tell you, and it is worth reading rather than guessing:
78
- `CODING` needs a branch, `MERGED` needs the merge — a pull request, a merge
80
+ `IN_DEVELOPMENT` needs a branch, `MERGED` needs the merge — a pull request, a merge
79
81
  commit, or a sha — `SHIPPED` needs a version that is a real git tag, and
80
82
  `DECLINED` needs a reason. `REVIEW` needs nothing, deliberately: the pull
81
83
  request it would otherwise carry belongs to the run, and a project that reviews
@@ -85,18 +87,29 @@ without opening pull requests has none to give it.
85
87
  when you name no status is the project's rule, per door, and the default for a
86
88
  card or issue an agent files is the backlog: waiting until a person or a sprint
87
89
  brings it in, in no sprint and on no board column. Its origin names your
88
- session and the card your run was on. Name a status (`CONSIDERING`, `NEW`) when
90
+ session and the card your run was on. A plan run's card lands where the console's
91
+ do (R380). Name a status (`CONSIDERING`, `NEW`) when
89
92
  you mean the pipeline. Giving a backlog card a sprint moves it to the first
90
93
  status of its kind; moving a card to `BACKLOG` takes it out of its sprint.
91
94
 
92
- **Any status may move to any other.** There is no transition diagram — an entry
95
+ **Sprints are a team's** (R474, R489). The `sprint` of `roadmap_create`,
96
+ `roadmap_update` and the filter of `roadmap_list` take an integer or a string:
97
+ `7` or `"S7"` when the project's teams have one S7, `"Core team · S7"` (or
98
+ `"Core team / S7"`) when two do — the API refuses an ambiguous `S7` naming both
99
+ spellings, so read the sentence and send the long one — a sprint id, and `0` or
100
+ `""` to take a card out. A completed sprint refuses a card that is not already
101
+ in it, and only a person can reopen one; do not retry. `sprint_list` says which
102
+ sprints there are. A card prints its sprint as `Core team · S7 Name (active)`.
103
+ Opening, starting, completing and renaming a sprint stay a person's.
104
+
105
+ **Any status may move to any other.** There is no transition diagram — a card
93
106
  really can go from `CONSIDERING` straight to `SHIPPED` if that is what happened.
94
107
  The rules are about what a status *carries*, not the path it took.
95
108
 
96
109
  ### The body is what we want; the plan is how; the comments are the argument
97
110
 
98
111
  `roadmap_get` gives you all three, and `task_current` gives you them for the
99
- entry you are working on.
112
+ card you are working on.
100
113
 
101
114
  **The plan is the one to read before doing any of the work.** It was written by
102
115
  a plan phase and agreed by somebody, and it names the files — R124. If you are
@@ -109,8 +122,8 @@ either. The first is the platform's — it stores what a plan phase reports when
109
122
  the phase ends, so that a session which must not write is not handed a writer to
110
123
  do its own bookkeeping with. The second is refused outright: *an agent cannot
111
124
  start another agent*, which is `RunAccess.requireCanStart`'s rule and not an
112
- omission here. **Read the discussion before you propose anything about an
113
- entry.** It is where an objection was answered and where an obvious-looking
125
+ omission here. **Read the discussion before you propose anything about a
126
+ card.** It is where an objection was answered and where an obvious-looking
114
127
  approach was ruled out with a reason — and re-proposing what was talked out
115
128
  three months ago is precisely what it exists to stop.
116
129
 
@@ -122,7 +135,7 @@ that the next reader does not have to reconstruct it from the thread.
122
135
  A comment you write is attributed to your run as well as to the account that
123
136
  minted your token, so a person can tell an agent's reading from a colleague's.
124
137
 
125
- ### Read what the earlier sessions did before repeating it
138
+ ### Read what the earlier runs did before repeating it
126
139
 
127
140
  `task_current` lists **every run this card has already had** — how each ended,
128
141
  on what branch, with what model, whether the work was pushed, and the commits it
@@ -132,21 +145,21 @@ to know what the first tried.
132
145
 
133
146
  No transcripts come with it, deliberately: the terminal log is on the run's own
134
147
  page in the console, and nine of them would fill the context this call exists to
135
- orient. If you need the detail, the discussion under the entry is where a
136
- previous session should have written down what it learned — and where yours
148
+ orient. If you need the detail, the discussion under the card is where a
149
+ previous run should have written down what it learned — and where yours
137
150
  should.
138
151
 
139
152
  ### There is no delete
140
153
 
141
154
  Not in these tools, not in the API. `roadmap_decline` with a reason is the only
142
- exit an entry has, and the reason is the point: it is what stops the same idea
155
+ exit a card has, and the reason is the point: it is what stops the same idea
143
156
  being proposed again in six months. Same for changelog entries — correct the
144
157
  text, do not remove the record. Same for comments: nothing removes one, and the
145
158
  most you can do to your own is correct its wording.
146
159
 
147
160
  ### Ids are permanent, and a card is named by its ref
148
161
 
149
- An entry's number never changes and is never reused. Commit messages and code
162
+ A card's number never changes and is never reused. Commit messages and code
150
163
  comments point at it. Since R221 a roadmap card and an issue each count on
151
164
  their own sequence, so `R91` and `i91` are two cards: wherever a tool takes
152
165
  `number`, `related`, `after` or `entryNumber`, pass the ref — `"i91"` for an
@@ -168,13 +181,14 @@ what you are allowed to do.
168
181
  |---|---|
169
182
  | `roadmap_where` | Which platform, which token, read from where, and who you are. Start here. |
170
183
  | `roadmap_statuses` | The eight statuses, what they mean, what each requires. |
171
- | `roadmap_list` | Entries, optionally by status. `brief` omits bodies — use it to survey. |
172
- | `roadmap_get` | One entry in full: its body, **the plan agreed for it**, and the discussion on it. |
173
- | `roadmap_create` | Create an entry; the platform allocates its permanent number. |
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). |
175
- | `roadmap_comment` | Say something *beside* an entry rather than inside it. |
184
+ | `roadmap_list` | Cards, optionally by status or `sprint` (any spelling below). `brief` omits bodies — use it to survey. |
185
+ | `sprint_list` | The project's sprints with how far each has got (R489): `Core team · S7 Notifications — active — 4 of 9 landed`. ACTIVE and UPCOMING unless `state` says otherwise. A read: PLAN and VERIFY have it. |
186
+ | `roadmap_get` | One card in full: its body, **the plan agreed for it**, and the discussion on it. |
187
+ | `roadmap_create` | Create a card; the platform allocates its permanent number. |
188
+ | `roadmap_update` | Title, body, section, sprint (R489: `7`, `S7`, `"Core team · S7"`, a sprint id; `0` or `""` takes the card out — see below), related ids, the cards it starts coding after (`after` — R181), when it is due and how big it is (`due` — R351: an ISO date, `""` clears; `estimate`: a positive number, `0` clears), the version it is meant for (`targetVersion` — R358, `""` clears; since R362 it must be one the project defined under Project settings → Versions, and any other name is refused), the versions an issue was found in (`affectedVersions` — R378, issues only: names the project defined or shipped in, `[]` clears), and who it is assigned to (`assignee` — R331: a username, `""` clears; `roadmap_list`/`issue_list` take `assignee` too, and `me` means the token's owner). |
189
+ | `roadmap_comment` | Say something *beside* a card rather than inside it. |
176
190
  | `done_when_tick` | Tick (or untick) one numbered item of the card's Done-when checklist as built, with one line of evidence (R368). `task_current` lists the items. Defaults to the run's own card and refuses another; the reviewer's column is a person's and no token can set it. |
177
- | `roadmap_set_status` | Move an entry, carrying whatever the status requires. |
191
+ | `roadmap_set_status` | Move a card, carrying whatever the status requires. |
178
192
  | `roadmap_decline` | Decline with a reason. The only exit. |
179
193
  | `backlog_file` | File **feedback**, not a card — something a person should decide about. A WRITER accepts it as a card or an issue, or refuses it (R199). Tagged `agent` as a source, plus any `tags` you pass — `customer` when you relay what a user said (R372). |
180
194
  | `changelog_list` | The changelog, grouped by release, newest first. |
@@ -191,8 +205,8 @@ token gets a refusal saying so.
191
205
 
192
206
  | Tool | What it does |
193
207
  |---|---|
194
- | `task_current` | The entry you are working on, its branch, everything already said and asked on this run, **and every earlier run on the same card**. **Call it first**, and again whenever you are unsure where you are. |
195
- | `report` | `progress` as often as useful; `done` when finished, naming the branch and any PR; `blocked` when a person must resolve something. `done` and `blocked` end the run; on a coding run `done` is refused until everything is committed and pushed (R345). |
208
+ | `task_current` | The card you are working on, its branch, everything already said and asked on this run, **and every earlier run on the same card**. **Call it first**, and again whenever you are unsure where you are. |
209
+ | `report` | `progress` as often as useful; `done` when finished, naming the branch and any PR; `blocked` when a person must resolve something. `done` and `blocked` end the run; on a review run the `done` body is filed as the branch's review (R529), so write it for the person reading the thread; on a coding run `done` is refused until everything is committed and pushed (R345). |
196
210
  | `ask_user` | Ask the person who started the run, and wait. Blocks up to ten minutes, then hands back a `question_id`. |
197
211
  | `await_answer` | Resume waiting for a question `ask_user` handed back. |
198
212
  | `ask_group` | Ask a **round** — up to twelve questions that belong together, under one title. Blocks until every one of them has been answered, then hands back a `group_id`. |
@@ -201,7 +215,7 @@ token gets a refusal saying so.
201
215
  | `approve` | **Not yours to call.** Claude Code calls it itself, as `--permission-prompt-tool`, when no rule covers a tool call. |
202
216
 
203
217
  **`ask_user` is for decisions that are genuinely theirs** — an architectural
204
- choice, a trade-off with no right answer, something the entry does not settle.
218
+ choice, a trade-off with no right answer, something the card does not settle.
205
219
  Not for checking work you can check yourself. Every question stops the run and
206
220
  costs somebody's attention.
207
221
 
@@ -212,7 +226,7 @@ asked because the decision was not yours. Call `await_answer`, or `report`
212
226
  **`ask_group` is for when you have several questions at once** — R96. One
213
227
  question at a time is right for a decision you hit halfway through a piece of
214
228
  work; it is wrong when you are trying to understand something, because forty
215
- separate inbox items arriving over an afternoon is not a conversation, it is an
229
+ separate Waiting on you items arriving over an afternoon is not a conversation, it is an
216
230
  interruption repeated forty times. A round arrives as one form under one title,
217
231
  the person answers it in one sitting, and you are woken once when all of it is
218
232
  answered.
@@ -258,9 +272,9 @@ Two smoke tests, both driving the server over real stdio.
258
272
  CAWDEV_URL=http://localhost:4200 CAWDEV_TOKEN=cawd_… node tools/mcp/smoke.mjs scratch-project
259
273
  ```
260
274
 
261
- **Name a scratch project.** The smoke test creates a roadmap entry and declines
262
- it, and entries cannot be deleted — so it refuses to guess which project you
263
- meant. It left two declined entries in cawdev's own roadmap before it did.
275
+ **Name a scratch project.** The smoke test creates a roadmap card and declines
276
+ it, and cards cannot be deleted — so it refuses to guess which project you
277
+ meant. It left two declined cards in cawdev's own roadmap before it did.
264
278
 
265
279
  It drives the server the way a client does — spawn, write lines to stdin, read
266
280
  lines from stdout — rather than importing its functions, because the parts most