cadet-agent 0.53.0 → 0.56.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 +18 -4
- package/package.json +1 -1
- package/src/cli.mjs +698 -17
- package/src/harness/acceptance-form.mjs +281 -0
- package/src/harness/architecture.mjs +201 -0
- package/src/harness/commands.mjs +60 -0
- package/src/harness/context-protocol.mjs +470 -0
- package/src/harness/design-review.mjs +162 -0
- package/src/harness/gates.mjs +364 -0
- package/src/harness/hosts.mjs +349 -0
- package/src/harness/index.mjs +42 -3
- package/src/harness/policy.mjs +283 -10
- package/src/harness/routing.mjs +14 -2
- package/src/harness/state.mjs +185 -15
- package/src/harness/status.mjs +132 -0
- package/src/harness/verification.mjs +51 -28
- package/src/install.mjs +129 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate builder registry — the single source of truth for what may produce each
|
|
3
|
+
* gate's evidence.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: `GATES` declares the gate *names* and `TRANSITIONS` declares
|
|
6
|
+
* where each name is required, but nothing declared which command is allowed to
|
|
7
|
+
* produce the evidence. So `harness verify` treated any caller-supplied
|
|
8
|
+
* `--command` as an automated pass, and the gate name implied an evidence
|
|
9
|
+
* meaning that nothing checked. Reproduced against 0.55.0 on 2026-09-30:
|
|
10
|
+
*
|
|
11
|
+
* node bin/cli.mjs harness verify --gate codeReviewCompleted \
|
|
12
|
+
* --command "node -e \"process.exit(0)\"" --files src/a.mjs
|
|
13
|
+
*
|
|
14
|
+
* exited 0, wrote `source: "automated"` evidence, and flipped an agent-owned
|
|
15
|
+
* gate to true. An unrelated exit-zero command satisfied a semantic claim, and a
|
|
16
|
+
* made-up gate name was accepted the same way.
|
|
17
|
+
*
|
|
18
|
+
* This registry inverts that: the contract is a property of the gate, resolved
|
|
19
|
+
* before anything runs, and `harness verify` refuses a project command for every
|
|
20
|
+
* gate that does not declare one.
|
|
21
|
+
*
|
|
22
|
+
* Field notes — what is deliberately NOT declared here:
|
|
23
|
+
*
|
|
24
|
+
* - `phase` is not declared. `TRANSITIONS` already states where a gate is
|
|
25
|
+
* required, and a second copy would be free to drift from it. The registry
|
|
26
|
+
* test asserts instead that every registered gate is required somewhere, so
|
|
27
|
+
* a new name cannot arrive with no transition to use it.
|
|
28
|
+
* - `exceptionEligible` is not declared. `EXCEPTION_CATEGORIES` in policy.mjs
|
|
29
|
+
* owns that taxonomy, including per-category expiry.
|
|
30
|
+
*
|
|
31
|
+
* `manual` records whether a human assertion is a meaningful record for the gate
|
|
32
|
+
* — the fact the owner needs in order to tighten §5.5 of the product plan. PR 1
|
|
33
|
+
* does not enforce it: `harness confirm` behaves exactly as it did, because a
|
|
34
|
+
* consumer already holds manual records for `codeReviewCompleted` and
|
|
35
|
+
* `securityReviewPassed`, and refusing them here would break a closure that is
|
|
36
|
+
* otherwise valid. Enforcement belongs with the manual-evidence policy in the
|
|
37
|
+
* next change, and the registry is where that change reads the intent from.
|
|
38
|
+
* Note that `MANUAL_ONLY_GATES` in policy.mjs comments that an agent-owned gate
|
|
39
|
+
* "can never be satisfied by a human assertion" — that is asserted nowhere in
|
|
40
|
+
* the code, `harness confirm` accepts `codeReviewCompleted`, and the registry
|
|
41
|
+
* records the enforced behaviour, not the comment.
|
|
42
|
+
*
|
|
43
|
+
* Contract: docs/core/HarnessContract.md C14.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { GATES, REACHABILITY_GATE, MANUAL_ONLY_GATES } from './policy.mjs';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Who owns the evidence for a gate.
|
|
50
|
+
*
|
|
51
|
+
* `automated` — a command produces it.
|
|
52
|
+
* `agent` — a reviewer records a judgement (a review, a reconciliation). No
|
|
53
|
+
* command can produce it, and either an agent or a human may make
|
|
54
|
+
* the record: the framework does not force a judgement onto an
|
|
55
|
+
* agent. This is the class `MANUAL_ONLY_GATES` lists in policy.mjs.
|
|
56
|
+
* `human` — a person accepts the work, and no reviewer's record can stand in
|
|
57
|
+
* for them. No gate is human-owned yet; the class exists because
|
|
58
|
+
* `humanAcceptanceConfirmed` is planned as its first member.
|
|
59
|
+
*/
|
|
60
|
+
export const GATE_OWNERS = Object.freeze(['automated', 'agent', 'human']);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every gate's evidence contract, keyed by gate name.
|
|
64
|
+
*
|
|
65
|
+
* `contract` is a version for the gate's evidence RULES, not for the gate's
|
|
66
|
+
* meaning. Bump it when what the builder accepts or binds changes, so evidence
|
|
67
|
+
* recorded against the earlier rules is refused as stale rather than silently
|
|
68
|
+
* re-read as if it still meant the same thing (see `evidenceFreshness`).
|
|
69
|
+
*
|
|
70
|
+
* `projectCommand` is the load-bearing field: it decides whether a caller may
|
|
71
|
+
* supply `--command` at all. It is true only for the gates whose evidence IS a
|
|
72
|
+
* project command run — the repository owns its test, compile, analyzer and
|
|
73
|
+
* tracking scripts, so Cadet cannot know the right command and the project must
|
|
74
|
+
* be able to name it.
|
|
75
|
+
*/
|
|
76
|
+
export const GATE_BUILDERS = Object.freeze({
|
|
77
|
+
testsPassed: {
|
|
78
|
+
owner: 'automated',
|
|
79
|
+
contract: 1,
|
|
80
|
+
automatedPath: 'harness verify --gate testsPassed',
|
|
81
|
+
command: { policyKey: 'testCommand', fallback: 'npm test', tool: 'test' },
|
|
82
|
+
projectCommand: true,
|
|
83
|
+
manual: false,
|
|
84
|
+
binds: 'files',
|
|
85
|
+
attests: 'the project test suite ran on the bound files and passed',
|
|
86
|
+
},
|
|
87
|
+
compileCheckConfirmed: {
|
|
88
|
+
owner: 'automated',
|
|
89
|
+
contract: 1,
|
|
90
|
+
automatedPath: 'harness verify --gate compileCheckConfirmed',
|
|
91
|
+
command: { policyKey: 'compileCommand', fallback: null, tool: 'unity-build', unityBuild: true },
|
|
92
|
+
projectCommand: true,
|
|
93
|
+
manual: true,
|
|
94
|
+
binds: 'files',
|
|
95
|
+
attests: 'the project compiled from the bound files',
|
|
96
|
+
},
|
|
97
|
+
unityAnalyzerClean: {
|
|
98
|
+
owner: 'automated',
|
|
99
|
+
contract: 1,
|
|
100
|
+
automatedPath: 'harness verify --gate unityAnalyzerClean',
|
|
101
|
+
command: { policyKey: 'analyzerCommand', fallback: null, tool: 'unity-analyzer' },
|
|
102
|
+
projectCommand: true,
|
|
103
|
+
manual: true,
|
|
104
|
+
binds: 'files',
|
|
105
|
+
attests: 'the declared analyzer ran over the bound files and reported no finding',
|
|
106
|
+
},
|
|
107
|
+
storyTrackingUpdated: {
|
|
108
|
+
owner: 'automated',
|
|
109
|
+
contract: 1,
|
|
110
|
+
automatedPath: 'harness verify --gate storyTrackingUpdated',
|
|
111
|
+
// No default command: the tracking check reads the repository's own story
|
|
112
|
+
// and epic markdown, so only the repository can name it. `--command` is the
|
|
113
|
+
// documented path, which is why this gate accepts a project command while
|
|
114
|
+
// having no fallback of its own.
|
|
115
|
+
command: { policyKey: null, fallback: null, tool: 'tracking' },
|
|
116
|
+
projectCommand: true,
|
|
117
|
+
manual: true,
|
|
118
|
+
binds: 'files',
|
|
119
|
+
attests: 'the story and epic markdown agree with the work item state',
|
|
120
|
+
},
|
|
121
|
+
acceptanceCriteriaValidated: {
|
|
122
|
+
owner: 'automated',
|
|
123
|
+
contract: 1,
|
|
124
|
+
automatedPath: 'harness verify-acs --story <path>',
|
|
125
|
+
command: null,
|
|
126
|
+
projectCommand: false,
|
|
127
|
+
manual: false,
|
|
128
|
+
binds: 'story',
|
|
129
|
+
attests: 'every declared acceptance criterion names a test that ran in the passing inventory',
|
|
130
|
+
},
|
|
131
|
+
reachabilityAddressed: {
|
|
132
|
+
owner: 'automated',
|
|
133
|
+
contract: 1,
|
|
134
|
+
automatedPath: `harness verify-reachability --story <path>`,
|
|
135
|
+
command: null,
|
|
136
|
+
projectCommand: false,
|
|
137
|
+
manual: false,
|
|
138
|
+
binds: 'story',
|
|
139
|
+
attests: 'the story declares how its deliverable is reached, and the declaration holds',
|
|
140
|
+
},
|
|
141
|
+
architectureFitnessPassed: {
|
|
142
|
+
owner: 'automated',
|
|
143
|
+
contract: 1,
|
|
144
|
+
// The command is the project's own declared check, read out of `architectureFitness`
|
|
145
|
+
// in the policy — never `--command`. That is the difference between this gate and
|
|
146
|
+
// `testsPassed`: a project may substitute its test runner, but it may not substitute
|
|
147
|
+
// the *shape* of an architecture constraint with an unrelated exit-zero command.
|
|
148
|
+
automatedPath: 'cadet-agent harness verify-architecture',
|
|
149
|
+
command: null,
|
|
150
|
+
projectCommand: false,
|
|
151
|
+
manual: false,
|
|
152
|
+
binds: 'files',
|
|
153
|
+
attests: 'every required architecture check that governs the changed files ran, and none of them reported a violation',
|
|
154
|
+
},
|
|
155
|
+
designReviewCompleted: {
|
|
156
|
+
owner: 'automated',
|
|
157
|
+
contract: 1,
|
|
158
|
+
automatedPath: 'harness verify-design-review --artifact <path> --files <inputs>',
|
|
159
|
+
// No default command and no project override: the check reads the review
|
|
160
|
+
// artifact against the design inputs, which an arbitrary exit-zero command
|
|
161
|
+
// cannot attest. Like the AC gate, its evidence is an artifact plus the
|
|
162
|
+
// structure the artifact must have.
|
|
163
|
+
command: null,
|
|
164
|
+
projectCommand: false,
|
|
165
|
+
// A review is a judgement, and a judgement belongs to whoever made it — an
|
|
166
|
+
// agent or a person. The command is the stronger route (it checks the artifact
|
|
167
|
+
// and binds the inputs it names), and it is not the only one: a human who read
|
|
168
|
+
// the design records it with `harness confirm`, which still requires a reason,
|
|
169
|
+
// a scope, an environment and a short expiry under strict closure.
|
|
170
|
+
manual: true,
|
|
171
|
+
binds: 'files',
|
|
172
|
+
attests: 'a reviewer challenged the design, recorded findings with dispositions, and left no contested finding unresolved',
|
|
173
|
+
},
|
|
174
|
+
humanAcceptanceConfirmed: {
|
|
175
|
+
owner: 'human',
|
|
176
|
+
contract: 1,
|
|
177
|
+
// No automated path at all, and no project override: the gate asks whether a
|
|
178
|
+
// PERSON accepted the work. A command cannot answer it, and neither can a
|
|
179
|
+
// reviewer's record — that is the difference between this gate and
|
|
180
|
+
// `codeReviewCompleted`, which is why the registry distinguishes `agent` from
|
|
181
|
+
// `human` rather than lumping both under "manual".
|
|
182
|
+
automatedPath: null,
|
|
183
|
+
command: null,
|
|
184
|
+
projectCommand: false,
|
|
185
|
+
manual: true,
|
|
186
|
+
binds: 'files',
|
|
187
|
+
attests: 'a named person accepted the delivered work against a stated witness, with the limitations they accepted',
|
|
188
|
+
},
|
|
189
|
+
codeReviewCompleted: {
|
|
190
|
+
owner: 'agent',
|
|
191
|
+
contract: 1,
|
|
192
|
+
automatedPath: null,
|
|
193
|
+
command: null,
|
|
194
|
+
projectCommand: false,
|
|
195
|
+
manual: true,
|
|
196
|
+
binds: 'files',
|
|
197
|
+
attests: 'a reviewer read the change and recorded findings and dispositions',
|
|
198
|
+
},
|
|
199
|
+
securityReviewPassed: {
|
|
200
|
+
owner: 'agent',
|
|
201
|
+
contract: 1,
|
|
202
|
+
automatedPath: null,
|
|
203
|
+
command: null,
|
|
204
|
+
projectCommand: false,
|
|
205
|
+
manual: true,
|
|
206
|
+
binds: 'files',
|
|
207
|
+
attests: 'the security review ran and its findings were dispositioned',
|
|
208
|
+
},
|
|
209
|
+
designArtifactSyncConfirmed: {
|
|
210
|
+
owner: 'agent',
|
|
211
|
+
contract: 1,
|
|
212
|
+
automatedPath: null,
|
|
213
|
+
// Read-only, and it records no gate: `harness reconcile` reports the
|
|
214
|
+
// mechanical findings, and the reconciliation skill makes the semantic pass
|
|
215
|
+
// and records the result. Named here so the refusal can point at it.
|
|
216
|
+
supportingCommand: 'harness reconcile --format json',
|
|
217
|
+
command: null,
|
|
218
|
+
projectCommand: false,
|
|
219
|
+
manual: true,
|
|
220
|
+
binds: 'files',
|
|
221
|
+
attests: 'the planning artifacts still agree with each other and with the work item state',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/** The contract entry for a gate, or undefined when the gate is not registered. */
|
|
226
|
+
export function gateBuilder(gate) {
|
|
227
|
+
return Object.prototype.hasOwnProperty.call(GATE_BUILDERS, gate) ? GATE_BUILDERS[gate] : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The identity of a gate's current evidence contract, e.g. `testsPassed@1`.
|
|
232
|
+
*
|
|
233
|
+
* Stamped into automated evidence and compared at transition time, so a change
|
|
234
|
+
* to the builder's rules refuses the records written under the old rules instead
|
|
235
|
+
* of reading them as if nothing had changed.
|
|
236
|
+
*/
|
|
237
|
+
export function gateContractId(gate) {
|
|
238
|
+
const builder = gateBuilder(gate);
|
|
239
|
+
return builder ? `${gate}@${builder.contract}` : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Gates a caller may satisfy with its own `--command`. */
|
|
243
|
+
export function gatesAcceptingProjectCommand() {
|
|
244
|
+
return GATES.filter((gate) => gateBuilder(gate)?.projectCommand === true);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Gates whose evidence is a record instead of a command — a reviewer's judgement or
|
|
249
|
+
* a person's acceptance. Named for that fact, because the two are different gates
|
|
250
|
+
* with the same route: see `MANUAL_ONLY_GATES` in policy.mjs.
|
|
251
|
+
*/
|
|
252
|
+
export function manualOnlyGateNames() {
|
|
253
|
+
return GATES.filter((gate) => gateBuilder(gate)?.owner !== 'automated');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @deprecated Use `manualOnlyGateNames`; kept only long enough to notice a straggler. */
|
|
257
|
+
export const agentOwnedGateNames = manualOnlyGateNames;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* One sentence explaining why a gate refuses a caller-supplied command, naming
|
|
261
|
+
* the path that does work. Shared by `harness verify` (which refuses) and the
|
|
262
|
+
* registry test (which asserts the sentence matches the contract), so the two
|
|
263
|
+
* cannot disagree about what a gate will accept.
|
|
264
|
+
*/
|
|
265
|
+
export function describeGateRefusal(gate) {
|
|
266
|
+
const builder = gateBuilder(gate);
|
|
267
|
+
if (!builder) {
|
|
268
|
+
return `unknown gate "${gate}". Valid gates: ${GATES.join(', ')}`;
|
|
269
|
+
}
|
|
270
|
+
if (!GATE_OWNERS.includes(builder.owner)) {
|
|
271
|
+
return `gate "${gate}" declares an invalid owner "${builder.owner}".`;
|
|
272
|
+
}
|
|
273
|
+
if (builder.owner === 'human') {
|
|
274
|
+
return `gate "${gate}" is human-owned: a person accepts this work, and no command can do it in their place.`;
|
|
275
|
+
}
|
|
276
|
+
const head = builder.owner === 'agent'
|
|
277
|
+
? `gate "${gate}" is agent-owned: an arbitrary command cannot attest ${builder.attests}`
|
|
278
|
+
: `gate "${gate}" has a fixed evidence contract: an arbitrary command cannot attest ${builder.attests}`;
|
|
279
|
+
const path = builder.automatedPath
|
|
280
|
+
? ` Run "${builder.automatedPath}" instead.`
|
|
281
|
+
: (builder.supportingCommand ? ` Its mechanical backing is "${builder.supportingCommand}".` : '');
|
|
282
|
+
return `${head}.${path}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Explain why a gate that DOES accept a project command has none to run yet.
|
|
287
|
+
* Separate from `describeGateRefusal`, which forbids a command: this one asks for
|
|
288
|
+
* the missing input. A gate with no default (the tracking check) needs it, and
|
|
289
|
+
* the old message called that gate "agent-owned", which said the opposite of the
|
|
290
|
+
* truth and pointed at no remedy.
|
|
291
|
+
*/
|
|
292
|
+
export function describeMissingCommand(gate) {
|
|
293
|
+
const builder = gateBuilder(gate);
|
|
294
|
+
if (!builder) return `unknown gate "${gate}". Valid gates: ${GATES.join(', ')}`;
|
|
295
|
+
const path = builder.automatedPath ? ` Example: "${builder.automatedPath} --command <your command>".` : '';
|
|
296
|
+
return `gate "${gate}" has no default command — its evidence is the repository's own command — so pass --command <command>.${path}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Assert the registry against the frozen gate list and the policy constants.
|
|
301
|
+
* Returns a list of problems (empty when the registry is coherent).
|
|
302
|
+
*
|
|
303
|
+
* Called by the registry test, and by nothing else in production: a check that
|
|
304
|
+
* runs at import time would turn a bad edit into a CLI that cannot start its own
|
|
305
|
+
* diagnostics.
|
|
306
|
+
*/
|
|
307
|
+
export function auditGateRegistry() {
|
|
308
|
+
const problems = [];
|
|
309
|
+
for (const gate of GATES) {
|
|
310
|
+
const builder = gateBuilder(gate);
|
|
311
|
+
if (!builder) {
|
|
312
|
+
problems.push(`gate "${gate}" has no builder contract`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (!GATE_OWNERS.includes(builder.owner)) {
|
|
316
|
+
problems.push(`gate "${gate}" declares unknown owner "${builder.owner}"`);
|
|
317
|
+
}
|
|
318
|
+
if (!Number.isInteger(builder.contract) || builder.contract < 1) {
|
|
319
|
+
problems.push(`gate "${gate}" declares contract "${builder.contract}"; expected a positive integer`);
|
|
320
|
+
}
|
|
321
|
+
if (typeof builder.projectCommand !== 'boolean') {
|
|
322
|
+
problems.push(`gate "${gate}" must declare projectCommand as a boolean`);
|
|
323
|
+
}
|
|
324
|
+
if (builder.projectCommand && !builder.command) {
|
|
325
|
+
problems.push(`gate "${gate}" accepts a project command but declares no command contract`);
|
|
326
|
+
}
|
|
327
|
+
if (typeof builder.manual !== 'boolean') {
|
|
328
|
+
problems.push(`gate "${gate}" must declare manual as a boolean`);
|
|
329
|
+
}
|
|
330
|
+
if (!['files', 'story'].includes(builder.binds)) {
|
|
331
|
+
problems.push(`gate "${gate}" declares binds "${builder.binds}"; expected "files" or "story"`);
|
|
332
|
+
}
|
|
333
|
+
if (typeof builder.attests !== 'string' || builder.attests.trim() === '') {
|
|
334
|
+
problems.push(`gate "${gate}" declares no attests sentence`);
|
|
335
|
+
}
|
|
336
|
+
if (builder.owner === 'agent' && builder.projectCommand) {
|
|
337
|
+
problems.push(`gate "${gate}" is agent-owned and must not accept a project command`);
|
|
338
|
+
}
|
|
339
|
+
if (builder.owner === 'human' && (builder.projectCommand || builder.automatedPath)) {
|
|
340
|
+
problems.push(`gate "${gate}" is human-owned and must not declare an automated path`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const name of Object.keys(GATE_BUILDERS)) {
|
|
344
|
+
if (!GATES.includes(name)) problems.push(`registry declares "${name}", which is not a known gate`);
|
|
345
|
+
}
|
|
346
|
+
// Both directions, because the two declarations must be one fact written twice:
|
|
347
|
+
// policy.mjs owns the `disallowManualFor` refusal, the registry owns the owner,
|
|
348
|
+
// and a drift would let one of them forbid a route the other needs.
|
|
349
|
+
for (const gate of GATES) {
|
|
350
|
+
const builder = gateBuilder(gate);
|
|
351
|
+
if (!builder) continue;
|
|
352
|
+
const listed = MANUAL_ONLY_GATES.includes(gate);
|
|
353
|
+
if (listed !== (builder.owner !== 'automated')) {
|
|
354
|
+
problems.push(
|
|
355
|
+
`gate "${gate}" is${listed ? '' : ' not'} listed in MANUAL_ONLY_GATES, but the registry declares owner "${builder.owner}"; `
|
|
356
|
+
+ 'the two must agree',
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (gateBuilder(REACHABILITY_GATE)?.projectCommand !== false) {
|
|
361
|
+
problems.push(`gate "${REACHABILITY_GATE}" must not accept a project command`);
|
|
362
|
+
}
|
|
363
|
+
return problems;
|
|
364
|
+
}
|