simt-dev 0.2.1 → 0.3.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/CHANGELOG.md +239 -0
- package/bin/simt-dev.mjs +21 -2
- 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/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
- package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
- package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
- package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
- package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
- package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
- package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
- package/package.json +2 -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/agenthooks/decide.mjs +123 -0
- package/src/agenthooks/policy.mjs +78 -0
- package/src/agenthooks/render.mjs +240 -0
- package/src/argv.mjs +55 -1
- package/src/bench/ablate.mjs +630 -0
- package/src/bench/adapters.json +149 -0
- package/src/bench/grade.mjs +229 -0
- package/src/bench/mine.mjs +811 -0
- package/src/bench/probes.mjs +1350 -0
- package/src/bench/report.mjs +899 -0
- package/src/bench/run.mjs +1002 -0
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +359 -0
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +45 -21
- package/src/commands/harness.mjs +301 -0
- package/src/commands/help.mjs +73 -2
- package/src/commands/index.mjs +67 -25
- package/src/commands/init.mjs +12 -8
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/plan.mjs +99 -0
- 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 +169 -0
- package/src/compare/row.mjs +73 -2
- package/src/decisions/index.mjs +23 -5
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/dispatcher.mjs +35 -3
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/mcp/resources.mjs +157 -0
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +228 -20
- package/src/mcp/tools.mjs +372 -1
- package/src/measure/context.mjs +542 -0
- package/src/measure/loaderView.mjs +435 -0
- package/src/measure/session.mjs +267 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +78 -2
- package/src/packs.mjs +182 -0
- package/src/plan/index.mjs +650 -0
- package/src/plan/template.mjs +331 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/manifest.mjs +114 -3
- package/src/projection/project.mjs +140 -9
- package/src/projection/rules.mjs +365 -22
- package/src/projection/surfaces.mjs +155 -23
- package/src/projection/targets.mjs +37 -0
- package/src/toolwords.mjs +69 -0
- package/templates/plan/tracker.json +36 -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
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.mjs';
|
|
2
|
+
import { formatCounts, planDone, planNext, planStatus } from '../plan/index.mjs';
|
|
3
|
+
import { CLI } from '../name.mjs';
|
|
4
|
+
|
|
5
|
+
// Preview unless --yes, and --dry-run wins even then. A tracker row marked done
|
|
6
|
+
// for a commit that was never written is a claim the next reader will believe.
|
|
7
|
+
|
|
8
|
+
export async function planCommand(sub, args, flags) {
|
|
9
|
+
const root = flags.root ?? process.cwd();
|
|
10
|
+
if (sub === 'status') return runStatus(root, flags);
|
|
11
|
+
if (sub === 'next') return runNext(root, args[0] ?? null, flags);
|
|
12
|
+
if (sub === 'done') {
|
|
13
|
+
if (!args[0]) return fail('plan done needs a slice id', EXIT.USAGE, `run: ${CLI} plan next`);
|
|
14
|
+
return runDone(root, args[0], args[1] ?? null, flags);
|
|
15
|
+
}
|
|
16
|
+
if (!sub) {
|
|
17
|
+
return fail('plan needs a subcommand', EXIT.USAGE, `run: ${CLI} plan status | ${CLI} plan next | ${CLI} plan done <id>`);
|
|
18
|
+
}
|
|
19
|
+
return fail(`unknown plan subcommand ${sub}`, EXIT.USAGE, `run: ${CLI} plan status`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runStatus(root, flags) {
|
|
23
|
+
const result = await planStatus(root);
|
|
24
|
+
if (result.error) return fail(result.error, EXIT.USAGE, result.remedy);
|
|
25
|
+
if (isJson(flags)) {
|
|
26
|
+
emitJson({ ok: true, command: 'plan status', ...result });
|
|
27
|
+
return EXIT.OK;
|
|
28
|
+
}
|
|
29
|
+
const lines = [`repo ${root}`, `template ${result.template}`, `dirs ${result.directories.join(', ')}`];
|
|
30
|
+
if (result.documents.length === 0) lines.push(`(no tracker in ${result.directories.join(' or ')})`);
|
|
31
|
+
for (const doc of result.documents) {
|
|
32
|
+
lines.push(`${doc.name} slices=${doc.slices} ${formatCounts(doc.counts, result.stateOrder)}`);
|
|
33
|
+
}
|
|
34
|
+
lines.push('', `total ${formatCounts(result.totals, result.stateOrder) || '-'}`);
|
|
35
|
+
lines.push(`ready ${result.ready}`);
|
|
36
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
37
|
+
return EXIT.OK;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runNext(root, id, flags) {
|
|
41
|
+
const result = await planNext(root, id);
|
|
42
|
+
if (result.error) return fail(result.error, EXIT.USAGE, result.remedy);
|
|
43
|
+
if (isJson(flags)) {
|
|
44
|
+
emitJson({ ok: true, command: 'plan next', ...result });
|
|
45
|
+
return EXIT.OK;
|
|
46
|
+
}
|
|
47
|
+
if (!result.next) {
|
|
48
|
+
const lines = [`repo ${root}`, result.reason];
|
|
49
|
+
if (result.blocked?.length) {
|
|
50
|
+
lines.push('', 'blocked:');
|
|
51
|
+
for (const row of result.blocked) {
|
|
52
|
+
const unknown = row.unknownDependencies.length ? ` unknown: ${row.unknownDependencies.join(', ')}` : '';
|
|
53
|
+
lines.push(` ${row.id} ${row.doc} depends on: ${row.dependsOn || '(none)'}${unknown}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
57
|
+
return EXIT.OK;
|
|
58
|
+
}
|
|
59
|
+
const next = result.next;
|
|
60
|
+
const head = [
|
|
61
|
+
`repo ${root}`,
|
|
62
|
+
`slice ${next.id} ${next.doc}`,
|
|
63
|
+
`state ${next.state}`,
|
|
64
|
+
`depends ${next.dependsOn || '(none)'}`,
|
|
65
|
+
`bytes ${next.sectionBytes}`,
|
|
66
|
+
'',
|
|
67
|
+
next.section || '(no section for this id)',
|
|
68
|
+
];
|
|
69
|
+
emit(head.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
70
|
+
return EXIT.OK;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function runDone(root, id, sha, flags) {
|
|
74
|
+
const mode = writeMode(flags);
|
|
75
|
+
const result = await planDone(root, id, {
|
|
76
|
+
sha,
|
|
77
|
+
test: flags.test ?? null,
|
|
78
|
+
state: flags.state ?? null,
|
|
79
|
+
dryRun: !mode.apply,
|
|
80
|
+
});
|
|
81
|
+
if (result.error) return fail(result.error, EXIT.USAGE, result.remedy);
|
|
82
|
+
if (isJson(flags)) {
|
|
83
|
+
emitJson({ ok: true, command: 'plan done', ...result });
|
|
84
|
+
return EXIT.OK;
|
|
85
|
+
}
|
|
86
|
+
const lines = [`repo ${root}`];
|
|
87
|
+
if (result.dryRun) {
|
|
88
|
+
lines.push(`dry-run ${result.id} ${result.doc} line ${result.line}`);
|
|
89
|
+
lines.push(result.wouldChange ? `would ${result.from} -> ${result.to}` : `state ${result.from}`);
|
|
90
|
+
if (result.remedy) lines.push(`apply ${result.remedy}`);
|
|
91
|
+
} else {
|
|
92
|
+
lines.push(`changed ${result.changed ? 'yes' : 'no'} ${result.id} ${result.doc} line ${result.line}`);
|
|
93
|
+
lines.push(result.from === result.to ? `state ${result.to}` : `state ${result.from} -> ${result.to}`);
|
|
94
|
+
}
|
|
95
|
+
if (result.evidenceRecorded) lines.push(`evidence ${result.sha}${result.test ? ` test: ${result.test}` : ''}`);
|
|
96
|
+
else if (result.evidenceNote) lines.push(`note ${result.evidenceNote}`);
|
|
97
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
98
|
+
return EXIT.OK;
|
|
99
|
+
}
|
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
|
|