simt-dev 0.2.1 → 0.2.2
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/CHANGELOG.md +202 -0
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/package.json +1 -1
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +2 -0
- package/rules/stacks/deno.md +2 -0
- package/rules/stacks/dotnet.md +2 -0
- package/rules/stacks/elixir.md +2 -0
- package/rules/stacks/go.md +2 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +2 -0
- package/rules/stacks/rust.md +2 -0
- package/rules/stacks/swift.md +2 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/argv.mjs +19 -0
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +45 -21
- package/src/commands/harness.mjs +150 -0
- package/src/commands/help.mjs +15 -1
- package/src/commands/index.mjs +43 -25
- package/src/commands/init.mjs +12 -8
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/rules.mjs +42 -57
- package/src/commands/skills.mjs +67 -92
- package/src/commands/skillsinstall.mjs +17 -2
- package/src/commands/sync.mjs +62 -6
- package/src/commands/tokens.mjs +138 -0
- package/src/compare/row.mjs +73 -2
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/dispatcher.mjs +35 -3
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +197 -20
- package/src/mcp/tools.mjs +288 -1
- package/src/measure/context.mjs +371 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +78 -2
- package/src/packs.mjs +182 -0
- package/src/projection/manifest.mjs +88 -0
- package/src/projection/project.mjs +140 -9
- package/src/projection/rules.mjs +288 -20
- package/src/projection/surfaces.mjs +133 -15
- package/src/projection/targets.mjs +37 -0
package/src/commands/init.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { promisify } from 'node:util';
|
|
|
20
20
|
import { mkdir, readFile, symlink, writeFile } from 'node:fs/promises';
|
|
21
21
|
import { dirname, join } from 'node:path';
|
|
22
22
|
|
|
23
|
-
import { EXIT, emit, fail, isJson, note } from '../output/index.mjs';
|
|
23
|
+
import { EXIT, emit, emitJson, fail, isJson, note } from '../output/index.mjs';
|
|
24
24
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
25
25
|
import { rulePaths, scanDocument, summarise } from '../projection/conflicts.mjs';
|
|
26
26
|
import { MANIFEST, buildManifest, manifestPath, readManifest, writeManifest, planGates } from '../projection/manifest.mjs';
|
|
@@ -219,7 +219,7 @@ export async function initCommand(positional, flags) {
|
|
|
219
219
|
payload.ok = false;
|
|
220
220
|
payload.blockedBy = 'surface-problem';
|
|
221
221
|
for (const p of surfaceProblems) note(`PROBLEM ${p}`);
|
|
222
|
-
if (isJson(flags))
|
|
222
|
+
if (isJson(flags)) emitJson(payload);
|
|
223
223
|
return fail(
|
|
224
224
|
`${surfaceProblems.length} surface(s) cannot be resolved (${surfaceProblems[0]})`,
|
|
225
225
|
EXIT.FAIL,
|
|
@@ -234,7 +234,7 @@ export async function initCommand(positional, flags) {
|
|
|
234
234
|
payload.ok = false;
|
|
235
235
|
payload.blockedBy = 'mdc-problem';
|
|
236
236
|
for (const p of mdcProblems) note(`PROBLEM ${p}`);
|
|
237
|
-
if (isJson(flags))
|
|
237
|
+
if (isJson(flags)) emitJson(payload);
|
|
238
238
|
return fail(
|
|
239
239
|
`${mdcProblems.length} canonical .mdc rule(s) cannot be projected (${mdcProblems[0]})`,
|
|
240
240
|
EXIT.FAIL,
|
|
@@ -249,7 +249,7 @@ export async function initCommand(positional, flags) {
|
|
|
249
249
|
payload.ok = false;
|
|
250
250
|
payload.blockedBy = 'conflict';
|
|
251
251
|
if (isJson(flags)) {
|
|
252
|
-
|
|
252
|
+
emitJson(payload);
|
|
253
253
|
} else {
|
|
254
254
|
emit(render(payload, manifest, { npm, version }));
|
|
255
255
|
note('');
|
|
@@ -271,7 +271,7 @@ export async function initCommand(positional, flags) {
|
|
|
271
271
|
if (malformed.length > 0) {
|
|
272
272
|
payload.ok = false;
|
|
273
273
|
payload.blockedBy = 'malformed-region';
|
|
274
|
-
if (isJson(flags))
|
|
274
|
+
if (isJson(flags)) emitJson(payload);
|
|
275
275
|
else emit(render(payload, manifest, { npm, version }));
|
|
276
276
|
return fail(
|
|
277
277
|
`${malformed.length} target(s) have a malformed generated region (${malformed[0].file}: ${malformed[0].reason})`,
|
|
@@ -282,7 +282,7 @@ export async function initCommand(positional, flags) {
|
|
|
282
282
|
|
|
283
283
|
// Step 4/5: report everything, then make changes only when told to.
|
|
284
284
|
if (isJson(flags)) {
|
|
285
|
-
if (!apply)
|
|
285
|
+
if (!apply) emitJson(payload);
|
|
286
286
|
} else {
|
|
287
287
|
emit(render(payload, manifest, { npm, version }));
|
|
288
288
|
}
|
|
@@ -443,7 +443,7 @@ export async function initCommand(positional, flags) {
|
|
|
443
443
|
}
|
|
444
444
|
payload.written = true;
|
|
445
445
|
|
|
446
|
-
if (isJson(flags))
|
|
446
|
+
if (isJson(flags)) emitJson(payload);
|
|
447
447
|
|
|
448
448
|
note('');
|
|
449
449
|
note(`next ${CLI} sync --yes, then ${CLI} check in CI. Commit when you are happy with the diff.`);
|
|
@@ -608,8 +608,12 @@ function render(payload, manifest, { npm = true, version } = {}) {
|
|
|
608
608
|
// on it would understate the edit the reader is approving. A baseline is the
|
|
609
609
|
// opposite case to AGENTS.md in the one way that matters here — it is placed FIRST,
|
|
610
610
|
// so the repo's own settings below it keep winning (ADR-025).
|
|
611
|
+
// A nested `apps/web/AGENTS.md` is the same kind of target as the root's, so the
|
|
612
|
+
// test is the suffix and not the exact path: a monorepo's package region holds
|
|
613
|
+
// hand-written content too, and describing it as a generated rule file would be
|
|
614
|
+
// the wrong contract for a reader who has prose above the markers.
|
|
611
615
|
const caption =
|
|
612
|
-
target.file
|
|
616
|
+
target.file.endsWith(PRIMARY_TARGET)
|
|
613
617
|
? 'hand-written content outside the markers is never modified'
|
|
614
618
|
: target.kind === 'baseline'
|
|
615
619
|
? 'a region placed first, so your own settings below it still win (ADR-025)'
|
package/src/commands/mcp.mjs
CHANGED
|
@@ -13,9 +13,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
|
|
16
|
-
import { EXIT, emit, fail, isJson } from '../output/index.mjs';
|
|
16
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
17
17
|
import { TOOLS } from '../mcp/tools.mjs';
|
|
18
|
-
import { PROTOCOL_VERSION, serve } from '../mcp/server.mjs';
|
|
18
|
+
import { PROTOCOL_VERSION, SUPPORTED_VERSIONS, serve } from '../mcp/server.mjs';
|
|
19
19
|
import { CLI } from '../name.mjs';
|
|
20
20
|
|
|
21
21
|
export async function mcpCommand(positional, flags) {
|
|
@@ -23,12 +23,27 @@ export async function mcpCommand(positional, flags) {
|
|
|
23
23
|
|
|
24
24
|
if (flags.list === true) {
|
|
25
25
|
if (isJson(flags)) {
|
|
26
|
-
|
|
26
|
+
emitJson({
|
|
27
|
+
ok: true,
|
|
28
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
29
|
+
protocolVersions: SUPPORTED_VERSIONS,
|
|
30
|
+
version,
|
|
31
|
+
// The output schema and the read-only annotation travel with the tool here too:
|
|
32
|
+
// a reader listing the surface should see the same contract a call returns,
|
|
33
|
+
// rather than discovering the shape by making one.
|
|
34
|
+
tools: TOOLS.map(({ name, description, inputSchema, outputSchema, annotations }) => ({
|
|
35
|
+
name,
|
|
36
|
+
description,
|
|
37
|
+
inputSchema,
|
|
38
|
+
...(outputSchema ? { outputSchema } : {}),
|
|
39
|
+
annotations,
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
27
42
|
return EXIT.OK;
|
|
28
43
|
}
|
|
29
44
|
emit(
|
|
30
45
|
[
|
|
31
|
-
`simt-dev ${version} - MCP surface over stdio (
|
|
46
|
+
`simt-dev ${version} - MCP surface over stdio (protocols ${SUPPORTED_VERSIONS.join(', ')})`,
|
|
32
47
|
'',
|
|
33
48
|
`${TOOLS.length} read-only tools:`,
|
|
34
49
|
...TOOLS.map((t) => ` ${t.name.padEnd(16)} ${t.description.split('.')[0]}`),
|
|
@@ -44,7 +59,7 @@ export async function mcpCommand(positional, flags) {
|
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
try {
|
|
47
|
-
return await serve(TOOLS, { version });
|
|
62
|
+
return await serve(TOOLS, { packageVersion: version });
|
|
48
63
|
} catch (error) {
|
|
49
64
|
return fail(`mcp transport failed: ${error.message}`, EXIT.FAIL, `run: ${CLI} mcp --list to check the surface loads`);
|
|
50
65
|
}
|
package/src/commands/rules.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// one, because "this rule has no decision behind it" is exactly the fact a
|
|
15
15
|
// reviewer needs and exactly the fact an empty output would hide.
|
|
16
16
|
|
|
17
|
-
import { EXIT, emit, fail, isJson } from '../output/index.mjs';
|
|
17
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
18
18
|
import { loadRules, select } from '../projection/project.mjs';
|
|
19
19
|
import { loadMdcRules } from '../projection/mdc.mjs';
|
|
20
20
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
@@ -92,30 +92,25 @@ export async function listCommand(args, flags) {
|
|
|
92
92
|
const selection = root ? await selectionFor(rules, root) : null;
|
|
93
93
|
|
|
94
94
|
if (isJson(flags)) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
},
|
|
115
|
-
null,
|
|
116
|
-
2,
|
|
117
|
-
)}\n`,
|
|
118
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
95
|
+
emitJson(
|
|
96
|
+
{
|
|
97
|
+
ok: broken.length === 0,
|
|
98
|
+
command: 'rules list',
|
|
99
|
+
root,
|
|
100
|
+
rules: sorted(rules).map((r) => ({
|
|
101
|
+
id: r.id,
|
|
102
|
+
title: r.title,
|
|
103
|
+
origin: r.origin,
|
|
104
|
+
source: r.source,
|
|
105
|
+
applies: r.applies,
|
|
106
|
+
fallback: r.fallback,
|
|
107
|
+
bytes: r.bytes,
|
|
108
|
+
error: r.error,
|
|
109
|
+
adrs: citations(r.body),
|
|
110
|
+
selected: selection ? selection.selected.has(r.id) : null,
|
|
111
|
+
})),
|
|
112
|
+
notes: selection?.notes ?? [],
|
|
113
|
+
},
|
|
119
114
|
);
|
|
120
115
|
return broken.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
121
116
|
}
|
|
@@ -188,7 +183,7 @@ export async function showCommand(args, flags) {
|
|
|
188
183
|
|
|
189
184
|
const adrs = citations(rule.body);
|
|
190
185
|
if (isJson(flags)) {
|
|
191
|
-
|
|
186
|
+
emitJson({ ok: !rule.error, ...rule, adrs }, { maxBytes: flags.maxBytes ?? 0 });
|
|
192
187
|
return rule.error ? EXIT.FAIL : EXIT.OK;
|
|
193
188
|
}
|
|
194
189
|
|
|
@@ -238,21 +233,16 @@ async function ruleDirection(id, rules, decisions, flags, broken) {
|
|
|
238
233
|
const index = byId(decisions);
|
|
239
234
|
|
|
240
235
|
if (isJson(flags)) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
},
|
|
252
|
-
null,
|
|
253
|
-
2,
|
|
254
|
-
)}\n`,
|
|
255
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
236
|
+
emitJson(
|
|
237
|
+
{
|
|
238
|
+
ok: true,
|
|
239
|
+
command: 'rules why',
|
|
240
|
+
rule: rule.id,
|
|
241
|
+
source: rule.source,
|
|
242
|
+
origin: rule.origin,
|
|
243
|
+
adrs: cited.map((c) => index.get(c) ?? { id: c, missing: true }),
|
|
244
|
+
uncited: cited.length === 0,
|
|
245
|
+
},
|
|
256
246
|
);
|
|
257
247
|
return EXIT.OK;
|
|
258
248
|
}
|
|
@@ -313,22 +303,17 @@ async function adrDirection(adr, rules, decisions, flags) {
|
|
|
313
303
|
// effective ruling is carried alongside rather than instead: the record asked about
|
|
314
304
|
// is still named, and what is in force is named separately.
|
|
315
305
|
const replacement = d.supersededBy ? decisions.find((x) => x.id === d.supersededBy) : null;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
},
|
|
328
|
-
null,
|
|
329
|
-
2,
|
|
330
|
-
)}\n`,
|
|
331
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
306
|
+
emitJson(
|
|
307
|
+
{
|
|
308
|
+
ok: true,
|
|
309
|
+
command: 'rules why',
|
|
310
|
+
adr: d,
|
|
311
|
+
supersededBy: replacement ? { id: replacement.id, title: replacement.title } : null,
|
|
312
|
+
inForce: replacement
|
|
313
|
+
? { id: replacement.id, status: replacement.status, summary: replacement.summary }
|
|
314
|
+
: { id: d.id, status: d.status, summary: d.summary },
|
|
315
|
+
rules: restates.map((r) => r.id),
|
|
316
|
+
},
|
|
332
317
|
);
|
|
333
318
|
return EXIT.OK;
|
|
334
319
|
}
|
package/src/commands/skills.mjs
CHANGED
|
@@ -97,24 +97,19 @@ export async function indexCommand(args, flags) {
|
|
|
97
97
|
const workspace = await workspaceLine(flags);
|
|
98
98
|
|
|
99
99
|
if (isJson(flags)) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
},
|
|
114
|
-
null,
|
|
115
|
-
2,
|
|
116
|
-
)}\n`,
|
|
117
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
100
|
+
emitJson(
|
|
101
|
+
{
|
|
102
|
+
ok: true,
|
|
103
|
+
workspace,
|
|
104
|
+
skills: registry.skills.map((s) => ({
|
|
105
|
+
name: s.name,
|
|
106
|
+
summary: s.summary,
|
|
107
|
+
capabilities: s.capabilities,
|
|
108
|
+
stack: s.stack,
|
|
109
|
+
bytes: s.bytes,
|
|
110
|
+
deferred: Boolean(s.deferred),
|
|
111
|
+
})),
|
|
112
|
+
},
|
|
118
113
|
);
|
|
119
114
|
return EXIT.OK;
|
|
120
115
|
}
|
|
@@ -168,20 +163,15 @@ async function emitAll(registry, repoStack, flags, warning) {
|
|
|
168
163
|
const inline = usable.filter((s) => !s.deferred);
|
|
169
164
|
|
|
170
165
|
if (isJson(flags)) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
},
|
|
181
|
-
null,
|
|
182
|
-
2,
|
|
183
|
-
)}\n`,
|
|
184
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
166
|
+
emitJson(
|
|
167
|
+
{
|
|
168
|
+
ok: true,
|
|
169
|
+
all: true,
|
|
170
|
+
repoStack,
|
|
171
|
+
inline: inline.map((s) => s.name),
|
|
172
|
+
deferred: deferred.map((s) => s.name),
|
|
173
|
+
...(warning ? { warning } : {}),
|
|
174
|
+
},
|
|
185
175
|
);
|
|
186
176
|
return EXIT.OK;
|
|
187
177
|
}
|
|
@@ -217,7 +207,10 @@ export async function findCommand(args, flags) {
|
|
|
217
207
|
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
218
208
|
|
|
219
209
|
if (isJson(flags)) {
|
|
220
|
-
|
|
210
|
+
emitJson(
|
|
211
|
+
{ ok: true, query, workspace, results: results.map((r) => ({ name: r.skill.name, score: r.score, reasons: r.reasons })) },
|
|
212
|
+
{ maxBytes: flags.maxBytes ?? 0 },
|
|
213
|
+
);
|
|
221
214
|
return EXIT.OK;
|
|
222
215
|
}
|
|
223
216
|
|
|
@@ -275,24 +268,19 @@ export async function briefCommand(args, flags) {
|
|
|
275
268
|
// is what `warning` carries. Failing here would tell a caller the tool
|
|
276
269
|
// broke when in fact it is telling them their query was.
|
|
277
270
|
if (flags.all) return emitAll(registry, repoStack, flags, warning);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
null,
|
|
292
|
-
2,
|
|
293
|
-
)}\n`,
|
|
294
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
295
|
-
);
|
|
271
|
+
emitJson(
|
|
272
|
+
{
|
|
273
|
+
ok: true,
|
|
274
|
+
repo: { root, stack: repoStack },
|
|
275
|
+
workspace,
|
|
276
|
+
task: null,
|
|
277
|
+
budget,
|
|
278
|
+
resolved: [],
|
|
279
|
+
deferred: [],
|
|
280
|
+
excluded: [],
|
|
281
|
+
warning,
|
|
282
|
+
},
|
|
283
|
+
);
|
|
296
284
|
return EXIT.OK;
|
|
297
285
|
}
|
|
298
286
|
emit(
|
|
@@ -333,22 +321,17 @@ export async function briefCommand(args, flags) {
|
|
|
333
321
|
const lines = [];
|
|
334
322
|
|
|
335
323
|
if (isJson(flags)) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
},
|
|
348
|
-
null,
|
|
349
|
-
2,
|
|
350
|
-
)}\n`,
|
|
351
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
324
|
+
emitJson(
|
|
325
|
+
{
|
|
326
|
+
ok: true,
|
|
327
|
+
repo: { root, stack: repoStack },
|
|
328
|
+
workspace,
|
|
329
|
+
task: flags.task ?? null,
|
|
330
|
+
budget,
|
|
331
|
+
resolved: picked.map((e) => ({ name: e.skill.name, bytes: e.skill.bytes, why: [...e.reasons, ...[...e.via].map((v) => `requires:${v}`)] })),
|
|
332
|
+
deferred: deferred.map((e) => ({ name: e.skill.name, bytes: e.skill.bytes })),
|
|
333
|
+
excluded: excluded.filter((e) => !closureNames.has(e.skill.name)).map((e) => ({ name: e.skill.name, reason: e.reason })),
|
|
334
|
+
},
|
|
352
335
|
);
|
|
353
336
|
return EXIT.OK;
|
|
354
337
|
}
|
|
@@ -470,10 +453,7 @@ export async function showCommand(args, flags) {
|
|
|
470
453
|
const body = await sourcePath(skill, workspaceRoot, root);
|
|
471
454
|
const bodyFrom = body === null ? 'none' : body.startsWith(`${root}/`) ? 'repo' : 'workspace';
|
|
472
455
|
if (isJson(flags)) {
|
|
473
|
-
|
|
474
|
-
`${JSON.stringify({ ok: true, ...skill, workspace, body, bodyFrom }, null, 2)}\n`,
|
|
475
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
476
|
-
);
|
|
456
|
+
emitJson({ ok: true, ...skill, workspace, body, bodyFrom }, { maxBytes: flags.maxBytes ?? 0 });
|
|
477
457
|
return EXIT.OK;
|
|
478
458
|
}
|
|
479
459
|
emit(
|
|
@@ -640,27 +620,22 @@ export async function evalCommand(positional, flags) {
|
|
|
640
620
|
const okRun = unknown.length === 0 && score.clean;
|
|
641
621
|
|
|
642
622
|
if (isJson(flags)) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
},
|
|
660
|
-
null,
|
|
661
|
-
2,
|
|
662
|
-
)}\n`,
|
|
663
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
623
|
+
emitJson(
|
|
624
|
+
{
|
|
625
|
+
ok: okRun,
|
|
626
|
+
command: 'skills eval',
|
|
627
|
+
corpus: corpusPath,
|
|
628
|
+
registry: path,
|
|
629
|
+
cases: score.cases.map((c) => ({
|
|
630
|
+
name: c.name,
|
|
631
|
+
selected: c.selected,
|
|
632
|
+
missing: c.missing,
|
|
633
|
+
unexpected: c.unexpected,
|
|
634
|
+
})),
|
|
635
|
+
unknownSkills: unknown,
|
|
636
|
+
precision: score.precision,
|
|
637
|
+
recall: score.recall,
|
|
638
|
+
},
|
|
664
639
|
);
|
|
665
640
|
return okRun ? EXIT.OK : EXIT.FAIL;
|
|
666
641
|
}
|
|
@@ -24,7 +24,7 @@ import { cp, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
|
24
24
|
import { existsSync } from 'node:fs';
|
|
25
25
|
import { dirname, join } from 'node:path';
|
|
26
26
|
|
|
27
|
-
import { EXIT, emit, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
27
|
+
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
28
28
|
import { loadRegistry, measureSkill } from '../graph/load.mjs';
|
|
29
29
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
30
30
|
import { readCapability } from '../hooks/capability.mjs';
|
|
@@ -35,6 +35,7 @@ import { nearMatches } from '../resolve/near.mjs';
|
|
|
35
35
|
import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
36
36
|
import { CLI } from '../name.mjs';
|
|
37
37
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
38
|
+
import { nameProblems } from '../graph/spec.mjs';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Where skills live in this repo. The estate already has a convention - a
|
|
@@ -112,6 +113,20 @@ export async function installCommand(args, flags) {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
const dir = skillsDir(root, await dirEntries(root));
|
|
116
|
+
// The Agent Skills specification requires `name` to equal the directory it lives in,
|
|
117
|
+
// and the directory is named after the skill. So a name that breaks the naming rules
|
|
118
|
+
// cannot be installed at all: the client resolves by name, finds a directory the name
|
|
119
|
+
// does not match, and never reads the body this command just copied. Refused here,
|
|
120
|
+
// before anything is written, with the rule broken - because the alternative is a
|
|
121
|
+
// successful install that no client can load.
|
|
122
|
+
const broken = nameProblems(name);
|
|
123
|
+
if (broken !== null) {
|
|
124
|
+
return fail(
|
|
125
|
+
`${name}: ${broken}`,
|
|
126
|
+
EXIT.REGISTRY,
|
|
127
|
+
`rename it in ${skill.source ?? 'the registry'} to lowercase letters, digits and single hyphens, then install again`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
115
130
|
const dest = join(root, dir, name);
|
|
116
131
|
const inplace = await samePath(src, dest);
|
|
117
132
|
const action = inplace ? 'in-place' : await compare(dest, src, measured.flat, name);
|
|
@@ -147,7 +162,7 @@ export async function installCommand(args, flags) {
|
|
|
147
162
|
// combination a script writes. The payload is now emitted once, at the end,
|
|
148
163
|
// after the write it describes.
|
|
149
164
|
const report = () => {
|
|
150
|
-
if (isJson(flags))
|
|
165
|
+
if (isJson(flags)) emitJson(payload);
|
|
151
166
|
};
|
|
152
167
|
const mode = writeMode(flags);
|
|
153
168
|
|
package/src/commands/sync.mjs
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// three `.mdc` rules would let a reader believe a repo is fully current when
|
|
17
17
|
// only the file they happened to open is.
|
|
18
18
|
|
|
19
|
-
import { mkdir, readFile, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
20
|
-
import { dirname, join } from 'node:path';
|
|
19
|
+
import { mkdir, readFile, stat, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
20
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
21
21
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
22
22
|
import { plan, checkTargets, checkTarget, provenanceDigest, loadRules, select, pointerTargets, pointerState } from '../projection/project.mjs';
|
|
23
23
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
@@ -27,6 +27,7 @@ import { surfacesLine } from '../projection/surfaces.mjs';
|
|
|
27
27
|
import { CLI } from '../name.mjs';
|
|
28
28
|
import { MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
29
29
|
import { assertVersionPin, packageVersion } from '../package-version.mjs';
|
|
30
|
+
import { resolvePacks } from '../packs.mjs';
|
|
30
31
|
|
|
31
32
|
/** Findings `sync` reports and does not fail on.
|
|
32
33
|
*
|
|
@@ -48,7 +49,7 @@ import { assertVersionPin, packageVersion } from '../package-version.mjs';
|
|
|
48
49
|
* people learn to ignore. A *foreign* one — a real file or a real directory where the
|
|
49
50
|
* pointer belongs — is reported loudly with what is in the way, because that one is a
|
|
50
51
|
* decision somebody has to make. */
|
|
51
|
-
export const SYNC_NON_BLOCKING = new Set(['pointer-unread', 'orphaned']);
|
|
52
|
+
export const SYNC_NON_BLOCKING = new Set(['pointer-unread', 'orphaned', 'pack-unpinned', 'budget']);
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Findings `check` reports and does not fail on.
|
|
@@ -68,7 +69,20 @@ export const SYNC_NON_BLOCKING = new Set(['pointer-unread', 'orphaned']);
|
|
|
68
69
|
*
|
|
69
70
|
* `orphaned` stays non-blocking in both. Nothing can satisfy it, in either command.
|
|
70
71
|
*/
|
|
71
|
-
|
|
72
|
+
// `pack-unpinned` is here and `pack-digest-mismatch` is not, which is the whole
|
|
73
|
+
// judgement. An unpinned pack still renders its rules correctly; what is missing
|
|
74
|
+
// is the record of which bytes were declared, and failing a gate over a field the
|
|
75
|
+
// tool does not require would fail every repository that vendored a pack and
|
|
76
|
+
// pinned it later. A pack whose bytes do not match its pin renders a region
|
|
77
|
+
// nobody can account for, so that one blocks.
|
|
78
|
+
//
|
|
79
|
+
// `budget` is here for the opposite reason, and it is a ruling rather than a patch
|
|
80
|
+
// (ADR-072). An over-budget finding says the projection is bigger than this repository
|
|
81
|
+
// said it would be, and the remedy is a decision about which rules to narrow — not a
|
|
82
|
+
// missing file. Nothing is cut: the projection is still in full mode, so a gate that
|
|
83
|
+
// failed here would be failing a repository for exceeding a ceiling it set itself,
|
|
84
|
+
// with no way to fix it except lowering the number it wrote down.
|
|
85
|
+
export const CHECK_NON_BLOCKING = new Set(['orphaned', 'pack-unpinned', 'budget']);
|
|
72
86
|
|
|
73
87
|
export async function syncCommand(positional, flags) {
|
|
74
88
|
const root = flags.root ?? positional[0] ?? process.cwd();
|
|
@@ -399,8 +413,43 @@ async function formatterIgnoresManifest(root) {
|
|
|
399
413
|
.some((line) => line !== '' && !line.startsWith('#') && (line === MANIFEST_FILE || line === MANIFEST_LEGACY));
|
|
400
414
|
}
|
|
401
415
|
|
|
402
|
-
|
|
403
|
-
|
|
416
|
+
/**
|
|
417
|
+
* `gates.cwd` entries that do not resolve to a directory inside the repository.
|
|
418
|
+
*
|
|
419
|
+
* Reported here, not only where the gate runs: the dispatcher fails closed on the same
|
|
420
|
+
* condition, so a reader finds out on their next commit — after writing a commit, which
|
|
421
|
+
* is the wrong order. The finding names the gate and the path, and says which of the two
|
|
422
|
+
* things is wrong, because "no such directory" and "outside the repository" are repaired
|
|
423
|
+
* by different edits.
|
|
424
|
+
*
|
|
425
|
+
* @returns {Promise<{file: string, status: string, gate: string, detail: string, remedy: string}[]>}
|
|
426
|
+
*/
|
|
427
|
+
async function gateCwdFindings(root, manifest) {
|
|
428
|
+
const cwds = manifest?.gates?.cwd;
|
|
429
|
+
if (!cwds || typeof cwds !== 'object' || Array.isArray(cwds)) return [];
|
|
430
|
+
const declared = [...(manifest?.gates?.preCommit ?? []), ...(manifest?.gates?.prePush ?? [])];
|
|
431
|
+
const findings = [];
|
|
432
|
+
for (const name of Object.keys(cwds).sort()) {
|
|
433
|
+
if (!declared.includes(name)) continue;
|
|
434
|
+
const dir = String(cwds[name]);
|
|
435
|
+
const resolved = resolve(root, dir);
|
|
436
|
+
const inside = resolved === root || resolved.startsWith(`${root}${sep}`);
|
|
437
|
+
const isDir = inside && ((await stat(resolved).catch(() => null))?.isDirectory() ?? false);
|
|
438
|
+
if (isDir) continue;
|
|
439
|
+
findings.push({
|
|
440
|
+
file: MANIFEST_FILE,
|
|
441
|
+
status: inside ? 'gate-cwd-missing' : 'gate-cwd-outside',
|
|
442
|
+
gate: name,
|
|
443
|
+
detail: inside
|
|
444
|
+
? `gates.cwd["${name}"] is ${dir}, which is not a directory in this repository`
|
|
445
|
+
: `gates.cwd["${name}"] is ${dir}, which resolves outside the repository`,
|
|
446
|
+
remedy: `point gates.cwd["${name}"] at a directory in this repository, or remove it to run the gate at the root`,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return findings;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function checkCommand(positional, flags) { const root = flags.root ?? positional[0] ?? process.cwd();
|
|
404
453
|
const findings = await checkTargets(root, flags);
|
|
405
454
|
const rules = await loadRules(root);
|
|
406
455
|
|
|
@@ -431,6 +480,13 @@ export async function checkCommand(positional, flags) {
|
|
|
431
480
|
findings.push({ file: MANIFEST_FILE, status: 'version-mismatch', detail: mismatch, remedy: `run: ${CLI} upgrade --yes` });
|
|
432
481
|
}
|
|
433
482
|
findings.push(...unrunnableGates(manifest, await readCapability(root)));
|
|
483
|
+
findings.push(...(await gateCwdFindings(root, manifest)));
|
|
484
|
+
// A pack the repository declares but cannot produce is reported here rather
|
|
485
|
+
// than when a rule happens to be missing, because that is the failure mode
|
|
486
|
+
// this guards: the region still renders, it renders SHORT, and `check` would
|
|
487
|
+
// pass. A reader whose rules were quietly halved is worse off than one whose
|
|
488
|
+
// gate is red, so this is a blocking finding, not a note.
|
|
489
|
+
findings.push(...(await resolvePacks(root)).findings);
|
|
434
490
|
// A manifest a formatter has rewritten is not wrong, and not editable-away by
|
|
435
491
|
// hand either: the next `init` puts simt-dev's own bytes back and the formatter
|
|
436
492
|
// takes them away again. Reporting it here is what stops that being a surprise
|