cawdev-cli 0.9.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 +175 -0
- package/lib/ansi.mjs +224 -0
- package/lib/cawdev.mjs +104 -0
- package/lib/code-map.mjs +164 -0
- package/lib/harness-prompt.mjs +197 -0
- package/lib/roadmap-format.mjs +453 -0
- package/lib/run-plugin.mjs +119 -0
- package/lib/secrets.mjs +290 -0
- package/lib/stage-tools.mjs +384 -0
- package/lib/tool-line.mjs +92 -0
- package/lib/tool-rules.mjs +282 -0
- package/lib/transcript-batch.mjs +88 -0
- package/lib/usage-limit.mjs +80 -0
- package/lib/usage-report.mjs +142 -0
- package/lib/usage.mjs +119 -0
- package/mcp/README.md +273 -0
- package/mcp/orchestration-smoke.mjs +267 -0
- package/mcp/server.mjs +2163 -0
- package/mcp/smoke.mjs +220 -0
- package/package.json +20 -0
- package/runner/README.md +930 -0
- package/runner/attach.mjs +2397 -0
- package/runner/banner.mjs +106 -0
- package/runner/bootstrap.mjs +501 -0
- package/runner/brand.mjs +57 -0
- package/runner/cawdev.mjs +414 -0
- package/runner/control.mjs +225 -0
- package/runner/history.mjs +91 -0
- package/runner/input.mjs +355 -0
- package/runner/macbook-laptop.json +48 -0
- package/runner/runner.mjs +7445 -0
- package/runner/scrollback.mjs +165 -0
- package/runner/select.mjs +316 -0
- package/runner/session-store.mjs +78 -0
- package/runner/sign-in.mjs +210 -0
- package/runner/stub-agent.mjs +212 -0
- package/runner/token-store.mjs +107 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each stage of a lifecycle may do — R112.
|
|
3
|
+
*
|
|
4
|
+
* In `lib/` for `run-plugin.mjs`'s and `harness-prompt.mjs`'s reason: the claim
|
|
5
|
+
* this whole entry rests on is a claim about a LIST OF STRINGS, and a list of
|
|
6
|
+
* strings should not need a daemon, a platform and a spawned agent to check.
|
|
7
|
+
*
|
|
8
|
+
* The claim: **PLAN and VERIFY have no tool that can change anything in the
|
|
9
|
+
* CHECKOUT.** Not "are asked not to" — cannot. That is the difference between
|
|
10
|
+
* enforcement and hope, and it is the difference R28's profiles were built on:
|
|
11
|
+
*
|
|
12
|
+
* > a session told not to touch the code but able to is one refusal away from
|
|
13
|
+
* > touching it; a session that cannot has nothing to decide, and nothing to be
|
|
14
|
+
* > talked out of.
|
|
15
|
+
*
|
|
16
|
+
* The one exception, and it is narrow enough to name in a sentence — R150. A
|
|
17
|
+
* plan session started with NO CARD holds exactly one platform write,
|
|
18
|
+
* `roadmap_create`, because a card is what it is planning and it has not got
|
|
19
|
+
* one. Writing the thing it is about is the first step of planning it, not a
|
|
20
|
+
* widening of what a plan stage may do: it still cannot write a file, run a
|
|
21
|
+
* command, or comment on a card. That is why it is {@link CARD_WRITE} — one
|
|
22
|
+
* named string behind one named option — rather than an entry in `CAWDEV_READS`,
|
|
23
|
+
* which would hand a write to every read-only stage of every profile and call it
|
|
24
|
+
* a read.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Reading git without being able to change it. */
|
|
28
|
+
export const GIT_READS = [
|
|
29
|
+
'Bash(git diff *)',
|
|
30
|
+
'Bash(git log *)',
|
|
31
|
+
'Bash(git show *)',
|
|
32
|
+
'Bash(git status *)',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* cawdev's OWN tools that only read — NAMED, because this is the one registry
|
|
37
|
+
* this repository knows the whole of.
|
|
38
|
+
*
|
|
39
|
+
* <p>Everything else under `mcp__cawdev__` is a writer. That inverts how the
|
|
40
|
+
* rest of this file works and it is deliberate: for a registry we do not
|
|
41
|
+
* control, shape is the only signal there is; for ours it is the WORST one
|
|
42
|
+
* available, because we can simply say. `roadmap_comment` is the proof — a
|
|
43
|
+
* write to an append-only, undeletable discussion, whose name ends in a noun,
|
|
44
|
+
* which a shape test reads as a read.
|
|
45
|
+
*
|
|
46
|
+
* <p>The direction of failure is the argument. Forget to add a new READER here
|
|
47
|
+
* and a read-only stage says out loud that it cannot do something. Forget to add
|
|
48
|
+
* a new WRITER to a list of writers and the stage quietly has it. R112's claim
|
|
49
|
+
* survives only while the silence is on the safe side.
|
|
50
|
+
*
|
|
51
|
+
* <p>`report`, `ask_user`, `await_answer` and `approve` are here in spite of
|
|
52
|
+
* writing something: they are the loop every PROFILE runs on, and a session that
|
|
53
|
+
* cannot report has no way to finish. A read-only STAGE is the exception, and
|
|
54
|
+
* {@link STAGE_READS} below is where that is said.
|
|
55
|
+
*/
|
|
56
|
+
export const CAWDEV_READS = new Set([
|
|
57
|
+
'approve',
|
|
58
|
+
'task_current',
|
|
59
|
+
'report',
|
|
60
|
+
'ask_user',
|
|
61
|
+
'await_answer',
|
|
62
|
+
// R96's rounds are `ask_user` and `await_answer` in the interview's shape:
|
|
63
|
+
// one asks a person something, the rest are waits. The regex this list
|
|
64
|
+
// replaced kept all four, and they belong with the two above rather than
|
|
65
|
+
// with the tools that change a card.
|
|
66
|
+
'ask_group',
|
|
67
|
+
'await_group',
|
|
68
|
+
'interview_rounds',
|
|
69
|
+
'await_more_rounds',
|
|
70
|
+
'roadmap_where',
|
|
71
|
+
'code_map',
|
|
72
|
+
'file_deps',
|
|
73
|
+
'roadmap_statuses',
|
|
74
|
+
'roadmap_list',
|
|
75
|
+
'roadmap_get',
|
|
76
|
+
'issue_list',
|
|
77
|
+
'changelog_list',
|
|
78
|
+
'changelog_get',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The same list minus `report`, which is what a read-only STAGE may hold.
|
|
83
|
+
*
|
|
84
|
+
* <p>The one name where a profile and a stage genuinely disagree, and it is not
|
|
85
|
+
* a subtlety: `report` kind "done" ends the **run**, not the stage. A profile
|
|
86
|
+
* needs it — that is how an ASK or a REVIEW session finishes at all. A PLAN
|
|
87
|
+
* stage calling it would finish the whole run in the middle of the walk, and
|
|
88
|
+
* every stage behind it would be skipped with "the run was reported finished
|
|
89
|
+
* during the PLAN stage".
|
|
90
|
+
*
|
|
91
|
+
* <p>Derived rather than typed out twice: the two lists differ by exactly one
|
|
92
|
+
* name, and a second literal is a second thing to keep in step.
|
|
93
|
+
*/
|
|
94
|
+
const STAGE_READS = new Set([...CAWDEV_READS].filter((name) => name !== 'report'));
|
|
95
|
+
|
|
96
|
+
/** The `mcp__cawdev__` prefix, once. */
|
|
97
|
+
const CAWDEV = 'mcp__cawdev__';
|
|
98
|
+
|
|
99
|
+
/** The one tool no STAGE may hold — see {@link toolsForStage}. */
|
|
100
|
+
const REPORT = `${CAWDEV}report`;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The one platform write a PLAN stage may hold — R150.
|
|
104
|
+
*
|
|
105
|
+
* <p>Only when the run has no card, and only on PLAN. `canChangeThings` goes on
|
|
106
|
+
* calling it a writer, which is correct and is what makes this an exception
|
|
107
|
+
* rather than a reclassification: the name is let through by the stage that was
|
|
108
|
+
* deliberately given it, and by nobody else.
|
|
109
|
+
*/
|
|
110
|
+
export const CARD_WRITE = `${CAWDEV}roadmap_create`;
|
|
111
|
+
|
|
112
|
+
/** The tool a session delegates with, and what an older CLI called it. */
|
|
113
|
+
const DELEGATES = /^(Agent|Task)\b/;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Tools a subagent may hold and still change nothing — R147.
|
|
117
|
+
*
|
|
118
|
+
* <p>A NAMED list, on `CAWDEV_READS`'s argument rather than `canChangeThings`'s:
|
|
119
|
+
* an expert's `tools:` line is somebody else's frontmatter, out of a marketplace
|
|
120
|
+
* repository, and the question is not "does this look like a writer" but "is
|
|
121
|
+
* every one of these a tool we know only reads". Forget a reader here and an
|
|
122
|
+
* expert is refused from a planning stage, which is loud; a shape test that
|
|
123
|
+
* let `WebFetch` through would have let `Bash` through the day somebody wrote
|
|
124
|
+
* it as `bash`.
|
|
125
|
+
*/
|
|
126
|
+
export const SUBAGENT_READS = new Set([
|
|
127
|
+
'Read', 'Grep', 'Glob', 'LS', 'NotebookRead', 'WebFetch', 'WebSearch', 'TodoRead',
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether an expert can change nothing — R147, and the half of R112's claim
|
|
132
|
+
* that delegation used to break.
|
|
133
|
+
*
|
|
134
|
+
* <p>`canChangeThings` calls `Agent` a writer because a subagent has its own
|
|
135
|
+
* tool list, and a PLAN stage holding it could write through a helper. True of
|
|
136
|
+
* an expert that inherits every tool, or asks for `Write` — and false of one
|
|
137
|
+
* whose frontmatter names only reads, which is what `architect`, `planner` and
|
|
138
|
+
* `code-explorer` do. Those are the experts a planning stage exists to consult,
|
|
139
|
+
* and R112 was refusing them the one tool that reaches an expert.
|
|
140
|
+
*
|
|
141
|
+
* <p>Judged from the same fields `run-plugin.mjs` writes into the file the CLI
|
|
142
|
+
* loads, so what is checked is what the subagent is actually spawned with. An
|
|
143
|
+
* expert with NO `tools:` line inherits the parent's — which in a PLAN stage is
|
|
144
|
+
* read-only anyway, but "inherits whatever the CLI decides" is not a claim this
|
|
145
|
+
* file can make, so it is not read-only here.
|
|
146
|
+
*/
|
|
147
|
+
export function readOnlyExpert(agent) {
|
|
148
|
+
const declared = String(agent?.tools ?? '').split(',').map((each) => each.trim())
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
return declared.length > 0 && declared.every((tool) => SUBAGENT_READS.has(tool));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Anything that can change something, recognised by shape rather than by name.
|
|
155
|
+
*
|
|
156
|
+
* A list of forbidden tool names would go stale the day the CLI ships a new
|
|
157
|
+
* writer — and the failure would be silent, because a stage would simply have a
|
|
158
|
+
* tool nobody thought to exclude. This asks the opposite question: does this
|
|
159
|
+
* permission let the session change anything? Everything that does is refused
|
|
160
|
+
* from a read-only stage, including tools that do not exist yet.
|
|
161
|
+
*
|
|
162
|
+
* <p>cawdev's own tools are the exception and are judged by {@link STAGE_READS}
|
|
163
|
+
* instead — see the note there for why shape is the worst available signal on
|
|
164
|
+
* the one registry we own.
|
|
165
|
+
*/
|
|
166
|
+
export function canChangeThings(tool) {
|
|
167
|
+
// Delegation is the loudest writer there is, and it does not look like one.
|
|
168
|
+
// `Agent` spawns a SUBAGENT with its own tool list — so a PLAN stage holding
|
|
169
|
+
// it could write every file it was carefully not given a tool for, through a
|
|
170
|
+
// helper. R112's load-bearing claim is that a planning stage *cannot* write,
|
|
171
|
+
// and it survives only if this says so. IMPLEMENT and TEST keep it, which is
|
|
172
|
+
// where an expert was always meant to be reached from.
|
|
173
|
+
if (/^(Agent|Task)\b/.test(tool)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
// A PREFIX, deliberately without a word boundary: `WriteMany` is a writer and
|
|
177
|
+
// `\b` would let it through, because `Write` is followed by another word
|
|
178
|
+
// character. Recognising by shape only helps if the shape is the loose one.
|
|
179
|
+
if (/^(Write|Edit|MultiEdit|NotebookEdit|Update)/.test(tool)) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (/^Bash\b/.test(tool)) {
|
|
183
|
+
// Bash is the one that has to be judged rather than named: `Bash(git *)`
|
|
184
|
+
// can commit, `Bash(git diff *)` cannot. Anything not on the read list is
|
|
185
|
+
// treated as able to change something, which is the safe direction and the
|
|
186
|
+
// one that stays right when somebody adds a pattern here.
|
|
187
|
+
return !GIT_READS.includes(tool);
|
|
188
|
+
}
|
|
189
|
+
// Our own registry is judged by the list, because we have one. Anything under
|
|
190
|
+
// this prefix that is not a known read is a writer — including a tool added
|
|
191
|
+
// tomorrow and forgotten here, which is the direction that fails loudly.
|
|
192
|
+
if (tool.startsWith(CAWDEV)) {
|
|
193
|
+
return !STAGE_READS.has(tool.slice(CAWDEV.length));
|
|
194
|
+
}
|
|
195
|
+
// Everybody else's MCP tools, where shape is all there is: one that writes
|
|
196
|
+
// usually says so in its name. A guess, and acceptable only because the
|
|
197
|
+
// alternative — treating every third-party MCP tool as a writer — would strip
|
|
198
|
+
// a read-only stage of R76's registry entirely.
|
|
199
|
+
return /(create|update|set_status|decline|add|save|report)$/.test(tool);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Anything that can RUN something, recognised by shape rather than by name.
|
|
204
|
+
*
|
|
205
|
+
* R129's mirror of `canChangeThings`, and the same question asked the other way
|
|
206
|
+
* round: not "does this let the session change anything" but "does this let the
|
|
207
|
+
* session EXECUTE anything". A TEST stage set to write the testbook may write
|
|
208
|
+
* all it likes — it has a file to write — and must not be able to run the suite
|
|
209
|
+
* it is describing.
|
|
210
|
+
*
|
|
211
|
+
* Deliberately narrower than `canChangeThings`: `Write` and `Edit` are not
|
|
212
|
+
* runners, and a stage refused them could not produce the artefact it exists
|
|
213
|
+
* for.
|
|
214
|
+
*/
|
|
215
|
+
export function canRunThings(tool) {
|
|
216
|
+
// Delegation runs anything: a subagent with its own tool list could run the
|
|
217
|
+
// very suite this stage was spawned unable to run. The same argument
|
|
218
|
+
// canChangeThings makes about writing, and it is the same tool making it.
|
|
219
|
+
if (/^(Agent|Task)\b/.test(tool)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (/^Bash\b/.test(tool)) {
|
|
223
|
+
// The git READS are kept, for the read-only stages' reason: `git diff` is
|
|
224
|
+
// how this stage finds out what the run it is describing actually changed.
|
|
225
|
+
// Anything else that reaches a shell is treated as able to run something,
|
|
226
|
+
// which is the safe direction.
|
|
227
|
+
return !GIT_READS.includes(tool);
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The tools one stage gets, narrowed from what the PROFILE already allows.
|
|
234
|
+
*
|
|
235
|
+
* <p>Narrowed, never widened: a stage cannot be handed something the profile
|
|
236
|
+
* would have refused, so an interview's IMPLEMENT stage gets an interview's
|
|
237
|
+
* tools rather than a coding run's. That ordering is what keeps R28's decision
|
|
238
|
+
* the outer one.
|
|
239
|
+
*
|
|
240
|
+
* @param stage one of PLAN, VERIFY, IMPLEMENT, TEST, MEMORY
|
|
241
|
+
* @param profileTools what the run's profile allows
|
|
242
|
+
* @param testMode R129, and only TEST reads it: `TESTBOOK` narrows the stage to
|
|
243
|
+
* what cannot run anything. Absent — an older platform that does not send it —
|
|
244
|
+
* means `RUN`, which is what TEST has always been.
|
|
245
|
+
*/
|
|
246
|
+
export function toolsForStage(stage, profileTools, testMode, options = {}) {
|
|
247
|
+
// R147. A read-only stage keeps `Agent` when EVERY expert it is handed is
|
|
248
|
+
// read-only — the caller narrows the list to those before spawning, and
|
|
249
|
+
// `readOnlyExpert` is the judge. The claim stays intact: a subagent holding
|
|
250
|
+
// only Read, Grep and Glob can no more write than the stage that called it.
|
|
251
|
+
const delegates = Boolean(options.delegatesReadOnly);
|
|
252
|
+
// `report` kind "done" ends the RUN, not the stage — so NO stage may hold it,
|
|
253
|
+
// whatever else it may do. STAGE_READS says this above and only said it to the
|
|
254
|
+
// read-only stages, because IMPLEMENT and TEST were handed the profile's list
|
|
255
|
+
// untouched. They therefore kept `report`, used it, and every stage behind
|
|
256
|
+
// them was marked SKIPPED with "the run was reported finished during the TEST
|
|
257
|
+
// stage" — which is how MEMORY came to run once in fifty runs.
|
|
258
|
+
//
|
|
259
|
+
// A stage ends by its turn ending; the walk advances on `close` and finishes
|
|
260
|
+
// the run when it runs out of stages. Nothing here needs to announce it. A run
|
|
261
|
+
// with NO lifecycle never comes through this function and keeps `report`,
|
|
262
|
+
// which is the case the tool exists for. It applies to a TESTBOOK stage too:
|
|
263
|
+
// narrowing what a stage may RUN and refusing it the tool that ends the run
|
|
264
|
+
// are two different questions, and this one is asked of every stage.
|
|
265
|
+
const allowed = (Array.isArray(profileTools) ? profileTools : [])
|
|
266
|
+
.filter((tool) => tool !== REPORT);
|
|
267
|
+
// R150. The card write, and ONLY on PLAN, and only when the run said it has
|
|
268
|
+
// no card. VERIFY shares the filter below and must not get it: by the time a
|
|
269
|
+
// VERIFY stage runs, the card exists and writing a second one is not what
|
|
270
|
+
// this permission is for. That is why the arm is split rather than the flag
|
|
271
|
+
// read once for all three.
|
|
272
|
+
const writesItsOwnCard = Boolean(options.writesItsOwnCard);
|
|
273
|
+
const readOnly = (keepsCardWrite) => [
|
|
274
|
+
...allowed.filter((tool) => !canChangeThings(tool)
|
|
275
|
+
|| (delegates && DELEGATES.test(tool))
|
|
276
|
+
|| (keepsCardWrite && tool === CARD_WRITE)),
|
|
277
|
+
...GIT_READS.filter((tool) => allowed.includes(tool) || allowed.includes('Bash(git *)')),
|
|
278
|
+
];
|
|
279
|
+
switch (stage) {
|
|
280
|
+
case 'PLAN':
|
|
281
|
+
// The load-bearing claim. Note this filters the PROFILE's list rather
|
|
282
|
+
// than naming a list of its own: a stage's tools are always a subset of
|
|
283
|
+
// what the run was already allowed, and a fresh list here would be a
|
|
284
|
+
// second place deciding what a profile may do. That subsetting is what
|
|
285
|
+
// makes the card write safe to allow here — the profile has to have
|
|
286
|
+
// handed it over first, and only a cardless plan run's does.
|
|
287
|
+
return readOnly(writesItsOwnCard);
|
|
288
|
+
case 'VERIFY':
|
|
289
|
+
case 'MEMORY':
|
|
290
|
+
return readOnly(false);
|
|
291
|
+
case 'TEST':
|
|
292
|
+
// R129. The mirror of the claim above, with the other question asked: a
|
|
293
|
+
// stage that may WRITE and cannot RUN. Same filtering of the PROFILE's
|
|
294
|
+
// own list, for the same reason — a fresh list here would be a second
|
|
295
|
+
// place deciding what a profile may do.
|
|
296
|
+
if (testMode === 'TESTBOOK') {
|
|
297
|
+
return [...allowed.filter((tool) => !canRunThings(tool)),
|
|
298
|
+
...GIT_READS.filter((tool) => allowed.includes(tool) || allowed.includes('Bash(git *)'))];
|
|
299
|
+
}
|
|
300
|
+
return allowed;
|
|
301
|
+
case 'IMPLEMENT':
|
|
302
|
+
return allowed;
|
|
303
|
+
default:
|
|
304
|
+
// A stage this version does not know is given the profile's own tools —
|
|
305
|
+
// the same as no lifecycle at all. Refusing everything would turn a
|
|
306
|
+
// rollback into a run that can do nothing and cannot say why.
|
|
307
|
+
return allowed;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Whether a TOOL line is one of the git reads a narrowed stage legitimately has.
|
|
313
|
+
*
|
|
314
|
+
* A tool line is `<name> <described input>`, and for a shell call the name is
|
|
315
|
+
* the bare `Bash` — the PATTERN it was allowed under is not in the line, only
|
|
316
|
+
* the command. So the first word alone cannot tell `git diff` from `npm test`,
|
|
317
|
+
* and a check that judged by it would fire on the very `git diff` a testbook
|
|
318
|
+
* stage is TOLD to run. A note that appears on every run makes nothing visible,
|
|
319
|
+
* which is the one thing R113's note has to do.
|
|
320
|
+
*/
|
|
321
|
+
function readsGit(toolLine) {
|
|
322
|
+
const command = String(toolLine ?? '').split(/\s+/).slice(1).join(' ');
|
|
323
|
+
return GIT_READS.some((read) => {
|
|
324
|
+
const verb = read.slice('Bash('.length, -' *)'.length);
|
|
325
|
+
return command === verb || command.startsWith(`${verb} `);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Whether a tool call contradicts the stage that claims to be running — R113.
|
|
331
|
+
*
|
|
332
|
+
* <p>Returns a sentence, or null. **It never blocks.** R112 does the blocking,
|
|
333
|
+
* by not handing over the tool in the first place; this is the cross-check that
|
|
334
|
+
* catches the case where it somehow did — a stage list that grew a tool nobody
|
|
335
|
+
* meant, a CLI that ignored `--allowedTools`, a version of this file that is
|
|
336
|
+
* wrong. A second thing that can stop a run is a second thing that can stop it
|
|
337
|
+
* wrongly, and that is a worse failure than a line in a transcript.
|
|
338
|
+
*
|
|
339
|
+
* <p>Said in the transcript rather than only in the daemon's log, because the
|
|
340
|
+
* person who needs to know is the one reading the run.
|
|
341
|
+
*/
|
|
342
|
+
export function driftedFrom(stage, toolLine, testMode, readOnlyExperts = [], options = {}) {
|
|
343
|
+
const readOnly = stage === 'PLAN' || stage === 'VERIFY' || stage === 'MEMORY';
|
|
344
|
+
// R129. A TEST stage set to write the testbook has its own thing it should
|
|
345
|
+
// not have been able to do, and the same reason for saying so out loud.
|
|
346
|
+
const testbook = stage === 'TEST' && testMode === 'TESTBOOK';
|
|
347
|
+
if (!readOnly && !testbook) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
// `linesOf` writes a TOOL line as `<name> <described input>`, so the name is
|
|
351
|
+
// the first word and the pattern is what follows it.
|
|
352
|
+
const name = String(toolLine ?? '').split(/\s/, 1)[0];
|
|
353
|
+
if (!name) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const notice = 'The work was not stopped — this is a note that what ran and what was '
|
|
357
|
+
+ 'permitted have disagreed, which should not be possible and is worth looking at.';
|
|
358
|
+
if (testbook) {
|
|
359
|
+
if (!canRunThings(name) || readsGit(toolLine)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return `cawdev: the TEST stage is set to write the testbook and called ${name}, which it `
|
|
363
|
+
+ `should not have been able to. ${notice}`;
|
|
364
|
+
}
|
|
365
|
+
if (!canChangeThings(name)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
// R147. A delegation to an expert this stage was allowed is the one call
|
|
369
|
+
// `canChangeThings` names a writer that is not one here. `describeCall`
|
|
370
|
+
// writes it as `Agent(<name>) …`, and the name is the CLI's qualified one.
|
|
371
|
+
const delegated = /^(?:Agent|Task)\(([^)]+)\)/.exec(String(toolLine ?? ''));
|
|
372
|
+
if (delegated && readOnlyExperts.includes(delegated[1])) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
// R150. The other call this stage was deliberately given and `canChangeThings`
|
|
376
|
+
// correctly calls a writer. Same shape as the delegation above, and the same
|
|
377
|
+
// reason: a note that fires on the one thing the stage was told to do is a
|
|
378
|
+
// note that teaches people to ignore it.
|
|
379
|
+
if (stage === 'PLAN' && options.writesItsOwnCard && name === CARD_WRITE) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return `cawdev: the ${stage} stage called ${name}, which it should not have been able to. `
|
|
383
|
+
+ notice;
|
|
384
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a tool call is written into a transcript — R130.
|
|
3
|
+
*
|
|
4
|
+
* <p>In `lib/` for `stage-tools.mjs`'s reason: this is a claim about a STRING,
|
|
5
|
+
* and a claim about a string should not need a daemon to check. It has no
|
|
6
|
+
* dependencies and it never throws.
|
|
7
|
+
*
|
|
8
|
+
* <p><strong>This file is one half of a parsing contract.</strong> The runner
|
|
9
|
+
* FORMATS here and the platform PARSES in
|
|
10
|
+
* `backend/src/main/java/dev/caw/cawdev/run/CapabilityUse.java`; the console
|
|
11
|
+
* classifies the same three shapes again for the transcript's gutter badge, in
|
|
12
|
+
* `frontend/src/app/runs/capability-tag.ts`. Three implementations of one rule
|
|
13
|
+
* is two too many, and the mitigation is that all three are tiny, pure, and
|
|
14
|
+
* tested against the same example table — `tool-line.test.mjs`,
|
|
15
|
+
* `CapabilityUseTest.java`, `capability-tag.spec.ts`. Change the strings below
|
|
16
|
+
* and change all three.
|
|
17
|
+
*
|
|
18
|
+
* <p>The shapes are deliberately ones an OLD daemon can never accidentally
|
|
19
|
+
* emit. A daemon that predates this writes `Agent {"description":"…`, which
|
|
20
|
+
* does not match `^(Agent|Task)\(` — so it produces no ledger row rather than a
|
|
21
|
+
* row named `{"description":"…`. Degrading to silence is required; garbage in
|
|
22
|
+
* the panel is not.
|
|
23
|
+
*
|
|
24
|
+
* <p>Everything that is not a capability is byte-identical to what was written
|
|
25
|
+
* before this entry, which is what keeps `driftedFrom` in `stage-tools.mjs`
|
|
26
|
+
* working: it reads the tool name off the front of a TOOL body.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A skill's arguments are context, not the identity of the call. */
|
|
30
|
+
const ARGS_LIMIT = 120;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A tool call as one line: what was called, and enough to know which one.
|
|
34
|
+
*
|
|
35
|
+
* <p>The three capability shapes are named rather than dumped. Before this, a
|
|
36
|
+
* `Skill` call fell through to `JSON.stringify` and an `Agent` call was mostly
|
|
37
|
+
* the prompt — so which skill ran, and which expert was delegated to, was
|
|
38
|
+
* normally lost off the end of the 200-character cut.
|
|
39
|
+
*/
|
|
40
|
+
export function describeCall(name, input) {
|
|
41
|
+
const args = input && typeof input === 'object' ? input : {};
|
|
42
|
+
if (name === 'Skill' && typeof args.skill === 'string' && args.skill) {
|
|
43
|
+
const extra = typeof args.args === 'string' ? args.args.trim() : '';
|
|
44
|
+
return `Skill(${args.skill})${extra ? ` ${extra.slice(0, ARGS_LIMIT)}` : ''}`;
|
|
45
|
+
}
|
|
46
|
+
if (name === 'Agent' || name === 'Task') {
|
|
47
|
+
// `Task` is normalised to `Agent`, matching DELEGATE in runner.mjs: an
|
|
48
|
+
// older CLI's name for the same act should not produce a second vocabulary
|
|
49
|
+
// on the panel, with one expert appearing twice under two spellings.
|
|
50
|
+
const who = typeof args.subagent_type === 'string' && args.subagent_type
|
|
51
|
+
? args.subagent_type
|
|
52
|
+
: 'unnamed';
|
|
53
|
+
const what = typeof args.description === 'string' ? args.description : '';
|
|
54
|
+
return `Agent(${who}) ${what}`.trim();
|
|
55
|
+
}
|
|
56
|
+
return `${name} ${describeInput(input)}`.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A tool's input as one short line — the arguments that identify the call. */
|
|
60
|
+
export function describeInput(input) {
|
|
61
|
+
if (!input || typeof input !== 'object') return '';
|
|
62
|
+
const interesting = ['file_path', 'path', 'command', 'pattern', 'url', 'query', 'number', 'kind'];
|
|
63
|
+
for (const key of interesting) {
|
|
64
|
+
if (typeof input[key] === 'string' || typeof input[key] === 'number') {
|
|
65
|
+
return String(input[key]).slice(0, 300);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const json = JSON.stringify(input);
|
|
69
|
+
return json.length > 200 ? `${json.slice(0, 199)}…` : json;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Which capability a TOOL line named, if it named one — R161.
|
|
74
|
+
*
|
|
75
|
+
* <p>The reader for the two shapes {@link describeCall} writes: `Skill(<key>)`
|
|
76
|
+
* and `Agent(<key>)`. The daemon asks it "has this stage used the thing the
|
|
77
|
+
* project requires", which is a question about a string, so it belongs beside
|
|
78
|
+
* the code that wrote the string and in `lib/` with it — answering it should
|
|
79
|
+
* not need a daemon, a platform and a spawned agent.
|
|
80
|
+
*
|
|
81
|
+
* <p>Third reader of the contract this file's header describes. `Task(` is not
|
|
82
|
+
* matched because `describeCall` normalises it away before anything is written:
|
|
83
|
+
* what reaches a transcript is always `Agent(`.
|
|
84
|
+
*
|
|
85
|
+
* @returns `{kind, key}`, or null for anything that is not a capability call.
|
|
86
|
+
*/
|
|
87
|
+
export function capabilityIn(line) {
|
|
88
|
+
if (typeof line !== 'string') return null;
|
|
89
|
+
const found = /^(Skill|Agent)\(([^)]+)\)/.exec(line);
|
|
90
|
+
if (!found) return null;
|
|
91
|
+
return { kind: found[1] === 'Skill' ? 'SKILL' : 'EXPERT', key: found[2] };
|
|
92
|
+
}
|