simt-dev 0.6.2 → 0.6.3
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 +48 -1
- package/README.md +44 -15
- package/bin/simt.mjs +19 -6
- package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- package/docs/decisions/README.md +44 -0
- package/package.json +1 -1
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/stacks/android.md +7 -8
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/jvm.md +1 -1
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- package/src/agenthooks/decide.mjs +101 -10
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/shell.mjs +523 -0
- package/src/argv.mjs +54 -9
- package/src/commands/agenthook.mjs +25 -3
- package/src/commands/ci.mjs +28 -3
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +14 -1
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/explain.mjs +52 -0
- package/src/commands/help.mjs +66 -17
- package/src/commands/hooks.mjs +208 -60
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/index.mjs +54 -100
- package/src/commands/init.mjs +82 -12
- package/src/commands/lease.mjs +136 -0
- package/src/commands/lessons.mjs +12 -0
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +16 -1
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +3 -0
- package/src/commands/rules.mjs +145 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +293 -205
- package/src/commands/skillsinstall.mjs +100 -30
- package/src/commands/slice.mjs +353 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +110 -15
- package/src/commands/undo.mjs +106 -0
- package/src/commands/upgrade.mjs +70 -8
- package/src/completions.mjs +191 -0
- package/src/diff.mjs +254 -0
- package/src/explain.mjs +258 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +14 -5
- package/src/hooks/dispatcher.mjs +110 -0
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +14 -0
- package/src/ignore.mjs +133 -0
- package/src/index/persist.mjs +66 -9
- package/src/journal.mjs +326 -0
- package/src/lease.mjs +113 -0
- package/src/mcp/server.mjs +15 -1
- package/src/mcp/tools.mjs +296 -256
- package/src/memory/decide.mjs +6 -1
- package/src/memory/lessons.mjs +12 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +6 -0
- package/src/plugins.mjs +27 -6
- package/src/prbody.mjs +251 -0
- package/src/projection/manifest.mjs +13 -2
- package/src/projection/shared.mjs +4 -1
- package/src/secrets.mjs +231 -0
- package/src/status.mjs +152 -0
package/src/commands/skills.mjs
CHANGED
|
@@ -51,6 +51,232 @@ export function listFlag(value) {
|
|
|
51
51
|
.filter(Boolean);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// --- cores: one computation for the CLI and the MCP tools (Q15) ----------------
|
|
55
|
+
//
|
|
56
|
+
// `skills_brief`, `skills_find`, `skills_show` and `skills_why` in the MCP
|
|
57
|
+
// surface used to rebuild these answers from the same modules with different
|
|
58
|
+
// loops, and the copies had already drifted: the budget loop inlined a deferred
|
|
59
|
+
// dependency on one surface and not the other, and the payloads disagreed on
|
|
60
|
+
// field names (`resolved`/`selected`, `reasons`/`why`, `signals`). The commands
|
|
61
|
+
// below render these facts for terminals; the tools return them as JSON. The
|
|
62
|
+
// CLI keeps its field names; the tools adopt them.
|
|
63
|
+
//
|
|
64
|
+
// A core throws when it cannot answer: the message names the problem, `remedy`
|
|
65
|
+
// names the next command, and `exitCode` is the exit the CLI leaves with. The
|
|
66
|
+
// MCP handler turns the same throw into an error result, so both surfaces
|
|
67
|
+
// report the same failure for the same cause.
|
|
68
|
+
function skillsFailure(message, remedy, exitCode) {
|
|
69
|
+
throw Object.assign(new Error(message), { remedy, exitCode });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function skillsRegistry(args) {
|
|
73
|
+
const { registry, errors, ok } = await loadRegistry(null, args);
|
|
74
|
+
if (!ok) skillsFailure(errors[0], WORKSPACE_REMEDY, EXIT.REGISTRY);
|
|
75
|
+
return registry;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findSkill(registry, name, verb) {
|
|
79
|
+
if (!name) skillsFailure(`${verb} needs a skill name`, `usage: ${CLI} skills ${verb} <name>`, EXIT.USAGE);
|
|
80
|
+
const skill = registry.skills.find((s) => s.name === name);
|
|
81
|
+
if (!skill) {
|
|
82
|
+
const near = nearMatches(name, registry.skills.map((s) => s.name));
|
|
83
|
+
skillsFailure(
|
|
84
|
+
`no skill named "${name}"${near.length ? ` (did you mean ${near.join(', ')}?)` : ''}`,
|
|
85
|
+
`run: ${CLI} skills find <query>`,
|
|
86
|
+
EXIT.USAGE,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return skill;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The JSON-safe skill facts both surfaces spread: absent lists read as empty, not missing. */
|
|
93
|
+
function skillFacts(skill) {
|
|
94
|
+
return {
|
|
95
|
+
...skill,
|
|
96
|
+
capabilities: skill.capabilities ?? [],
|
|
97
|
+
stack: skill.stack ?? [],
|
|
98
|
+
keywords: skill.keywords ?? [],
|
|
99
|
+
requires: skill.requires ?? [],
|
|
100
|
+
variants: skill.variants ?? [],
|
|
101
|
+
aliasOf: skill.aliasOf ?? null,
|
|
102
|
+
variantOf: skill.variantOf ?? null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Tier 1: the selection the task resolves to, with the text packet's display sets. */
|
|
107
|
+
export async function skillsBriefFacts(root, args = {}) {
|
|
108
|
+
const registry = await skillsRegistry(args);
|
|
109
|
+
const repo = await fingerprint(root);
|
|
110
|
+
const { note: workspace, root: workspaceRoot } = await announceWorkspace(args);
|
|
111
|
+
const repoStack = repo.signals;
|
|
112
|
+
const paths = args.paths ? parsePathList(args.paths) : await dirtyPaths(root);
|
|
113
|
+
const taskTokens = tokenize(args.task ?? '');
|
|
114
|
+
const forced = listFlag(args.skill);
|
|
115
|
+
const budget = args.budget ?? DEFAULT_BUDGET;
|
|
116
|
+
const task = args.task ?? null;
|
|
117
|
+
const repoFacts = { root, stack: repoStack, facts: repo.facts };
|
|
118
|
+
|
|
119
|
+
// A brief with no task and no paths resolves on stack alone, which produces a
|
|
120
|
+
// plausible-looking list the agent may act on. It warns rather than silently
|
|
121
|
+
// answering; under `--json` the warning is a field, not prose on stdout.
|
|
122
|
+
if (!args.task && paths.length === 0 && forced.length === 0) {
|
|
123
|
+
const warning =
|
|
124
|
+
'no --task and no staged files, so selection can only use the repo signals. ' +
|
|
125
|
+
'pass --task "<what you are doing>" or stage files so the diff drives selection, ' +
|
|
126
|
+
'or pass --all to take everything for this stack.';
|
|
127
|
+
if (args.all) {
|
|
128
|
+
const usable = registry.skills.filter((s) => stackCompatible(s, repoStack));
|
|
129
|
+
return {
|
|
130
|
+
kind: 'all',
|
|
131
|
+
repoStack,
|
|
132
|
+
inline: usable.filter((s) => !s.deferred),
|
|
133
|
+
deferred: usable.filter((s) => s.deferred),
|
|
134
|
+
warning,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
kind: 'brief',
|
|
139
|
+
repo: repoFacts,
|
|
140
|
+
workspace,
|
|
141
|
+
task,
|
|
142
|
+
budget,
|
|
143
|
+
inlineBytes: 0,
|
|
144
|
+
resolved: [],
|
|
145
|
+
deferred: [],
|
|
146
|
+
excluded: [],
|
|
147
|
+
shown: [],
|
|
148
|
+
shownDeferred: [],
|
|
149
|
+
shownUsed: 0,
|
|
150
|
+
pointerOnly: false,
|
|
151
|
+
warning,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const context = { repoStack, taskTokens, paths };
|
|
156
|
+
const { selected, excluded } = rankSkills(registry.skills, context, { forced });
|
|
157
|
+
|
|
158
|
+
// Hard dependencies are pulled in even when they did not score, and even past
|
|
159
|
+
// budget: a skill that is useless without its dependency is worse than a large
|
|
160
|
+
// context, because it silently misleads.
|
|
161
|
+
const expanded = closure(selected, registry.skills);
|
|
162
|
+
const { picked, deferred, used: inlineBytes } = selectWithinBudget(expanded, {
|
|
163
|
+
budget,
|
|
164
|
+
all: args.all === true,
|
|
165
|
+
});
|
|
166
|
+
const closureNames = new Set(expanded.map((e) => e.skill.name));
|
|
167
|
+
|
|
168
|
+
const plain = (entry) => ({
|
|
169
|
+
name: entry.skill.name,
|
|
170
|
+
bytes: entry.skill.bytes,
|
|
171
|
+
summary: entry.skill.summary,
|
|
172
|
+
why: [...entry.reasons, ...[...entry.via].map((v) => `requires:${v}`)],
|
|
173
|
+
});
|
|
174
|
+
// A body the packet cannot paste costs nothing. Charging the catalogue size
|
|
175
|
+
// is how a brief with no workspace reports "budget capped" and then prints
|
|
176
|
+
// the same remedy once per skill.
|
|
177
|
+
const readable = new Set();
|
|
178
|
+
for (const entry of expanded) {
|
|
179
|
+
if (await bodyReadable(entry.skill, workspaceRoot, root)) readable.add(entry.skill.name);
|
|
180
|
+
}
|
|
181
|
+
const pointerOnly = expanded.length > 0 && readable.size === 0;
|
|
182
|
+
|
|
183
|
+
let shown = picked;
|
|
184
|
+
let shownDeferred = deferred;
|
|
185
|
+
let shownUsed = inlineBytes;
|
|
186
|
+
if (pointerOnly) {
|
|
187
|
+
const priced = expanded.map((e) =>
|
|
188
|
+
e.skill.deferred ? e : { ...e, skill: { ...e.skill, bytes: 0 } },
|
|
189
|
+
);
|
|
190
|
+
const again = selectWithinBudget(priced, { budget, all: args.all === true });
|
|
191
|
+
shown = again.picked;
|
|
192
|
+
shownDeferred = again.deferred;
|
|
193
|
+
shownUsed = again.used;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
kind: 'brief',
|
|
198
|
+
repo: repoFacts,
|
|
199
|
+
workspace,
|
|
200
|
+
workspaceRoot,
|
|
201
|
+
task,
|
|
202
|
+
budget,
|
|
203
|
+
inlineBytes,
|
|
204
|
+
resolved: picked.map(plain),
|
|
205
|
+
deferred: deferred.map((e) => ({
|
|
206
|
+
name: e.skill.name,
|
|
207
|
+
bytes: e.skill.bytes,
|
|
208
|
+
reason: e.skill.deferred ? 'marked deferred' : 'over budget',
|
|
209
|
+
})),
|
|
210
|
+
excluded: excluded.filter((e) => !closureNames.has(e.skill.name)).map((e) => ({ name: e.skill.name, reason: e.reason })),
|
|
211
|
+
shown,
|
|
212
|
+
shownDeferred,
|
|
213
|
+
shownUsed,
|
|
214
|
+
pointerOnly,
|
|
215
|
+
excludedEntries: excluded.filter((e) => !closureNames.has(e.skill.name)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Raw search across capability, keyword and summary. */
|
|
220
|
+
export async function skillsFindFacts(query, args = {}) {
|
|
221
|
+
const q = String(query ?? '').trim();
|
|
222
|
+
if (!q) skillsFailure('find needs a query', `usage: ${CLI} skills find <query>`, EXIT.USAGE);
|
|
223
|
+
const registry = await skillsRegistry(args);
|
|
224
|
+
const tokens = tokenize(q);
|
|
225
|
+
const results = registry.skills
|
|
226
|
+
.map((skill) => ({ skill, ...scoreSkill(skill, { repoStack: [], taskTokens: tokens, paths: [] }) }))
|
|
227
|
+
.filter((r) => selectionVerdict(r.evidence, r.reasons) === null)
|
|
228
|
+
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
229
|
+
return {
|
|
230
|
+
query: q,
|
|
231
|
+
results: results.map((r) => ({ name: r.skill.name, score: r.score, summary: r.skill.summary, reasons: r.reasons })),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Tier 2: one skill body and the metadata needed to judge it. */
|
|
236
|
+
export async function skillsShowFacts(root, name, args = {}) {
|
|
237
|
+
const registry = await skillsRegistry(args);
|
|
238
|
+
const skill = findSkill(registry, name, 'show');
|
|
239
|
+
const { note: workspace, root: workspaceRoot } = await announceWorkspace(args);
|
|
240
|
+
// The repo is a location for a declared source too, so `show` reads a body the
|
|
241
|
+
// caller already has instead of reporting the body missing.
|
|
242
|
+
const body = await sourcePath(skill, workspaceRoot, root);
|
|
243
|
+
const bodyFrom = body === null ? 'none' : body.startsWith(`${root}/`) ? 'repo' : 'workspace';
|
|
244
|
+
return {
|
|
245
|
+
skill: skillFacts(skill),
|
|
246
|
+
workspace,
|
|
247
|
+
body,
|
|
248
|
+
bodyFrom,
|
|
249
|
+
bodyText: await readBody(skill, workspaceRoot, root),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The signal chain for one skill in one repository. */
|
|
254
|
+
export async function skillsWhyFacts(root, name, args = {}) {
|
|
255
|
+
const registry = await skillsRegistry(args);
|
|
256
|
+
const skill = findSkill(registry, name, 'why');
|
|
257
|
+
const repo = await fingerprint(root);
|
|
258
|
+
const paths = args.paths ? parsePathList(args.paths) : await dirtyPaths(root);
|
|
259
|
+
const tokens = tokenize(args.task ?? '');
|
|
260
|
+
const { score, reasons } = scoreSkill(skill, { repoStack: repo.signals, taskTokens: tokens, paths });
|
|
261
|
+
const compatible = stackCompatible(skill, repo.signals);
|
|
262
|
+
const ws = await resolveWorkspace(args);
|
|
263
|
+
return {
|
|
264
|
+
skill: skill.name,
|
|
265
|
+
score,
|
|
266
|
+
compatible,
|
|
267
|
+
gate: compatible ? 'pass' : 'fail',
|
|
268
|
+
repo: { root, stack: repo.stack, facts: repo.facts, evidence: repo.evidence },
|
|
269
|
+
workspace: { root: ws.root ?? null, source: ws.source, detail: ws.detail },
|
|
270
|
+
paths: paths.slice(0, 8),
|
|
271
|
+
taskTokens: tokens,
|
|
272
|
+
signals: reasons,
|
|
273
|
+
variants: (skill.variants ?? []).map((v) => ({ id: v.id, stack: v.stack, bytes: v.bytes })),
|
|
274
|
+
aliasOf: skill.aliasOf ?? null,
|
|
275
|
+
variantOf: skill.variantOf ?? null,
|
|
276
|
+
requires: skill.requires ?? [],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
54
280
|
/** Compact stack label: the tags that actually discriminate, not all of them. */
|
|
55
281
|
function stackLabel(tags = []) {
|
|
56
282
|
const DISCRIMINATING = [
|
|
@@ -142,11 +368,8 @@ function repoLine(repo) {
|
|
|
142
368
|
}
|
|
143
369
|
|
|
144
370
|
/** `--all` with no other signal: everything this stack can use, deferred excluded. */
|
|
145
|
-
async function
|
|
146
|
-
const
|
|
147
|
-
const usable = registry.skills.filter((s) => stackCompatible(s, repoStack));
|
|
148
|
-
const deferred = usable.filter((s) => s.deferred);
|
|
149
|
-
const inline = usable.filter((s) => !s.deferred);
|
|
371
|
+
async function emitAllFacts(facts, flags) {
|
|
372
|
+
const { inline, deferred, repoStack, warning } = facts;
|
|
150
373
|
|
|
151
374
|
if (isJson(flags)) {
|
|
152
375
|
emitJson(
|
|
@@ -179,22 +402,18 @@ async function emitAll(registry, repoStack, flags, warning) {
|
|
|
179
402
|
// --- find ---------------------------------------------------------------------
|
|
180
403
|
|
|
181
404
|
export async function findCommand(args, flags) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
405
|
+
let facts;
|
|
406
|
+
try {
|
|
407
|
+
facts = await skillsFindFacts(args.join(' '), flags);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
|
|
410
|
+
}
|
|
411
|
+
const { query, results } = facts;
|
|
189
412
|
const workspace = await workspaceLine(flags);
|
|
190
|
-
const results = registry.skills
|
|
191
|
-
.map((skill) => ({ skill, ...scoreSkill(skill, { repoStack: [], taskTokens: tokens, paths: [] }) }))
|
|
192
|
-
.filter((r) => selectionVerdict(r.evidence, r.reasons) === null)
|
|
193
|
-
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
194
413
|
|
|
195
414
|
if (isJson(flags)) {
|
|
196
415
|
emitJson(
|
|
197
|
-
{ ok: true, query, workspace, results
|
|
416
|
+
{ ok: true, query, workspace, results },
|
|
198
417
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
199
418
|
);
|
|
200
419
|
return EXIT.OK;
|
|
@@ -212,7 +431,7 @@ export async function findCommand(args, flags) {
|
|
|
212
431
|
'',
|
|
213
432
|
...results.map(
|
|
214
433
|
(r) =>
|
|
215
|
-
`${r.
|
|
434
|
+
`${r.name.padEnd(26)} ${kb(r.bytes).padStart(7)} ${r.reasons.join(' ')}`,
|
|
216
435
|
),
|
|
217
436
|
].join('\n'),
|
|
218
437
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
@@ -221,128 +440,44 @@ export async function findCommand(args, flags) {
|
|
|
221
440
|
}
|
|
222
441
|
|
|
223
442
|
// --- brief: tier 1 ------------------------------------------------------------
|
|
224
|
-
|
|
225
443
|
export async function briefCommand(args, flags) {
|
|
226
|
-
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
227
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
228
|
-
|
|
229
444
|
const root = flags.root ?? process.cwd();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const forced = listFlag(flags.skill);
|
|
237
|
-
const budget = flags.budget ?? DEFAULT_BUDGET;
|
|
445
|
+
let facts;
|
|
446
|
+
try {
|
|
447
|
+
facts = await skillsBriefFacts(root, flags);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
|
|
450
|
+
}
|
|
238
451
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// is what `warning` carries. Failing here would tell a caller the tool
|
|
255
|
-
// broke when in fact it is telling them their query was.
|
|
256
|
-
if (flags.all) return emitAll(registry, repoStack, flags, warning);
|
|
257
|
-
emitJson(
|
|
258
|
-
{
|
|
259
|
-
ok: true,
|
|
260
|
-
repo: { root, stack: repoStack },
|
|
261
|
-
workspace,
|
|
262
|
-
task: null,
|
|
263
|
-
budget,
|
|
264
|
-
resolved: [],
|
|
265
|
-
deferred: [],
|
|
266
|
-
excluded: [],
|
|
267
|
-
warning,
|
|
268
|
-
},
|
|
269
|
-
);
|
|
270
|
-
return EXIT.OK;
|
|
271
|
-
}
|
|
452
|
+
if (facts.kind === 'all') {
|
|
453
|
+
return emitAllFacts(facts, flags);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// `ok: true` because the command did what was asked; the answer being weak
|
|
457
|
+
// is what `warning` carries. Failing here would tell a caller the tool
|
|
458
|
+
// broke when in fact it is telling them their query was.
|
|
459
|
+
if (isJson(flags)) {
|
|
460
|
+
const { shown, shownDeferred, shownUsed, pointerOnly, excludedEntries, workspaceRoot, kind, ...doc } = facts;
|
|
461
|
+
emitJson({ ok: true, ...doc });
|
|
462
|
+
return EXIT.OK;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (facts.warning) {
|
|
466
|
+
const repo = await fingerprint(root);
|
|
272
467
|
emit(
|
|
273
468
|
[
|
|
274
469
|
`repo ${root.split('/').pop()} (${repoLine(repo)})`,
|
|
275
470
|
'',
|
|
276
|
-
warning,
|
|
471
|
+
facts.warning,
|
|
277
472
|
].join('\n'),
|
|
278
473
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
279
474
|
);
|
|
280
|
-
if (flags.all) return emitAll(registry, repoStack, flags, warning);
|
|
281
475
|
return EXIT.OK;
|
|
282
476
|
}
|
|
283
477
|
|
|
284
|
-
const
|
|
285
|
-
const { selected, excluded } = rankSkills(registry.skills, context, { forced });
|
|
286
|
-
|
|
287
|
-
// Hard dependencies are pulled in even when they did not score, and even past
|
|
288
|
-
// budget: a skill that is useless without its dependency is worse than a large
|
|
289
|
-
// context, because it silently misleads.
|
|
290
|
-
const expanded = closure(selected, registry.skills);
|
|
291
|
-
const order = new Map(expanded.map((e, i) => [e.skill.name, i]));
|
|
292
|
-
expanded.sort((a, b) => (order.get(a.skill.name) - order.get(b.skill.name)));
|
|
293
|
-
|
|
294
|
-
// Budget, by one shared function. The loop this replaced existed here and in the MCP
|
|
295
|
-
// tool, and the copies had already drifted: this one skipped a `deferred` skill before
|
|
296
|
-
// looking at the budget and the other checked it after, so a deferred dependency was
|
|
297
|
-
// inlined by one command and not the other. `selectWithinBudget` is the whole
|
|
298
|
-
// arithmetic, and it is the only place that knows a dependency does not get to exceed
|
|
299
|
-
// the budget on its own — which is what let a 2,000-byte budget resolve 13,503 bytes.
|
|
300
|
-
const { picked, deferred, used: bytesUsed } = selectWithinBudget(expanded, {
|
|
301
|
-
budget,
|
|
302
|
-
all: flags.all === true,
|
|
303
|
-
});
|
|
304
|
-
const closureNames = new Set(expanded.map((e) => e.skill.name));
|
|
305
|
-
|
|
306
|
-
const excludedNames = new Set(excluded.map((e) => e.skill.name));
|
|
478
|
+
const { shown, shownDeferred, shownUsed, pointerOnly, excludedEntries, workspace, workspaceRoot, budget } = facts;
|
|
307
479
|
const lines = [];
|
|
308
|
-
|
|
309
|
-
if (isJson(flags)) {
|
|
310
|
-
emitJson(
|
|
311
|
-
{
|
|
312
|
-
ok: true,
|
|
313
|
-
repo: { root, stack: repoStack },
|
|
314
|
-
workspace,
|
|
315
|
-
task: flags.task ?? null,
|
|
316
|
-
budget,
|
|
317
|
-
resolved: picked.map((e) => ({ name: e.skill.name, bytes: e.skill.bytes, why: [...e.reasons, ...[...e.via].map((v) => `requires:${v}`)] })),
|
|
318
|
-
deferred: deferred.map((e) => ({ name: e.skill.name, bytes: e.skill.bytes })),
|
|
319
|
-
excluded: excluded.filter((e) => !closureNames.has(e.skill.name)).map((e) => ({ name: e.skill.name, reason: e.reason })),
|
|
320
|
-
},
|
|
321
|
-
);
|
|
322
|
-
return EXIT.OK;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// A body the packet cannot paste costs nothing. Charging the catalogue size
|
|
326
|
-
// is how a brief with no workspace reports "budget capped" and then prints
|
|
327
|
-
// the same remedy once per skill (S33).
|
|
328
|
-
const readable = new Set();
|
|
329
|
-
for (const entry of expanded) {
|
|
330
|
-
if (await bodyReadable(entry.skill, workspaceRoot, root)) readable.add(entry.skill.name);
|
|
331
|
-
}
|
|
332
|
-
const pointerOnly = expanded.length > 0 && readable.size === 0;
|
|
333
|
-
|
|
334
|
-
let shown = picked;
|
|
335
|
-
let shownDeferred = deferred;
|
|
336
|
-
let shownUsed = bytesUsed;
|
|
337
|
-
if (pointerOnly) {
|
|
338
|
-
const priced = expanded.map((e) =>
|
|
339
|
-
e.skill.deferred ? e : { ...e, skill: { ...e.skill, bytes: 0 } },
|
|
340
|
-
);
|
|
341
|
-
const again = selectWithinBudget(priced, { budget, all: flags.all === true });
|
|
342
|
-
shown = again.picked;
|
|
343
|
-
shownDeferred = again.deferred;
|
|
344
|
-
shownUsed = again.used;
|
|
345
|
-
}
|
|
480
|
+
const repo = await fingerprint(root);
|
|
346
481
|
|
|
347
482
|
lines.push(
|
|
348
483
|
`RESOLVED ${shown.length} skills, ${kb(shownUsed)}${flags.all || shownUsed <= budget ? '' : ' (budget capped)'} - repo ${root.split('/').pop()} (${repoLine(repo)})`,
|
|
@@ -358,7 +493,7 @@ export async function briefCommand(args, flags) {
|
|
|
358
493
|
lines.push('', `DEFERRED ${shownDeferred.length} (marked deferred)`);
|
|
359
494
|
for (const entry of shownDeferred) lines.push(pointerLine(entry));
|
|
360
495
|
}
|
|
361
|
-
const hidden =
|
|
496
|
+
const hidden = excludedEntries;
|
|
362
497
|
if (hidden.length > 0) {
|
|
363
498
|
lines.push('', `NOT SELECTED ${hidden.length}`, '', ` notes: ${CLI} skills why <name> | ${CLI} skills show <name>`);
|
|
364
499
|
}
|
|
@@ -398,7 +533,7 @@ export async function briefCommand(args, flags) {
|
|
|
398
533
|
}
|
|
399
534
|
}
|
|
400
535
|
|
|
401
|
-
const notSelected =
|
|
536
|
+
const notSelected = excludedEntries;
|
|
402
537
|
if (notSelected.length > 0) {
|
|
403
538
|
lines.push('', 'NOT SELECTED');
|
|
404
539
|
for (const { skill, reason } of notSelected) {
|
|
@@ -415,31 +550,17 @@ export async function briefCommand(args, flags) {
|
|
|
415
550
|
// --- show: tier 2 -------------------------------------------------------------
|
|
416
551
|
|
|
417
552
|
export async function showCommand(args, flags) {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const skill = registry.skills.find((s) => s.name === name);
|
|
425
|
-
if (!skill) {
|
|
426
|
-
const near = nearMatches(name, registry.skills.map((s) => s.name));
|
|
427
|
-
return fail(
|
|
428
|
-
`no skill named "${name}"${near.length ? ` (did you mean ${near.join(', ')}?)` : ''}`,
|
|
429
|
-
EXIT.USAGE,
|
|
430
|
-
`run: ${CLI} skills find <query>`,
|
|
431
|
-
);
|
|
553
|
+
const root = flags.root ?? process.cwd();
|
|
554
|
+
let facts;
|
|
555
|
+
try {
|
|
556
|
+
facts = await skillsShowFacts(root, args[0], flags);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
|
|
432
559
|
}
|
|
433
|
-
|
|
560
|
+
const { skill, workspace, body, bodyFrom, bodyText } = facts;
|
|
434
561
|
const source = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
|
|
435
|
-
const { note: workspace, root: workspaceRoot } = await announceWorkspace(flags);
|
|
436
|
-
// The repo is a location for a declared source too, so `show` reads a body the
|
|
437
|
-
// caller already has instead of reporting the body missing (ADR-015).
|
|
438
|
-
const root = flags.root ?? process.cwd();
|
|
439
|
-
const body = await sourcePath(skill, workspaceRoot, root);
|
|
440
|
-
const bodyFrom = body === null ? 'none' : body.startsWith(`${root}/`) ? 'repo' : 'workspace';
|
|
441
562
|
if (isJson(flags)) {
|
|
442
|
-
emitJson({ ok: true, ...skill, workspace, body, bodyFrom }, { maxBytes: flags.maxBytes ?? 0 });
|
|
563
|
+
emitJson({ ok: true, ...skill, workspace, body, bodyFrom, bodyText }, { maxBytes: flags.maxBytes ?? 0 });
|
|
443
564
|
return EXIT.OK;
|
|
444
565
|
}
|
|
445
566
|
emit(
|
|
@@ -458,7 +579,7 @@ export async function showCommand(args, flags) {
|
|
|
458
579
|
`body ${body === null ? '(unavailable)' : `${bodyFrom} ${body}`}`,
|
|
459
580
|
workspace,
|
|
460
581
|
'',
|
|
461
|
-
|
|
582
|
+
bodyText,
|
|
462
583
|
]
|
|
463
584
|
.filter((l) => l !== null)
|
|
464
585
|
.join('\n'),
|
|
@@ -470,29 +591,13 @@ export async function showCommand(args, flags) {
|
|
|
470
591
|
// --- why ----------------------------------------------------------------------
|
|
471
592
|
|
|
472
593
|
export async function whyCommand(args, flags) {
|
|
473
|
-
const name = args[0];
|
|
474
|
-
if (!name) return fail('why needs a skill name', EXIT.USAGE, `usage: ${CLI} skills why <name>`);
|
|
475
|
-
|
|
476
|
-
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
477
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
478
|
-
|
|
479
|
-
const skill = registry.skills.find((s) => s.name === name);
|
|
480
|
-
if (!skill) {
|
|
481
|
-
const near = nearMatches(name, registry.skills.map((s) => s.name));
|
|
482
|
-
return fail(
|
|
483
|
-
`no skill named "${name}"${near.length ? ` (did you mean ${near.join(', ')}?)` : ''}`,
|
|
484
|
-
EXIT.USAGE,
|
|
485
|
-
`run: ${CLI} skills find <query>`,
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
594
|
const root = flags.root ?? process.cwd();
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
595
|
+
let facts;
|
|
596
|
+
try {
|
|
597
|
+
facts = await skillsWhyFacts(root, args[0], flags);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
|
|
600
|
+
}
|
|
496
601
|
|
|
497
602
|
// Every other command honours `--json`; this one built its answer as text and
|
|
498
603
|
// emitted it, so `--json` was silently ignored and a caller got prose it could
|
|
@@ -501,48 +606,31 @@ export async function whyCommand(args, flags) {
|
|
|
501
606
|
// to scrape: a skill that can never be selected in this repo is a decision, not
|
|
502
607
|
// a line of output.
|
|
503
608
|
if (isJson(flags)) {
|
|
504
|
-
|
|
505
|
-
emitJson({
|
|
506
|
-
ok: true,
|
|
507
|
-
command: 'skills why',
|
|
508
|
-
skill: skill.name,
|
|
509
|
-
score,
|
|
510
|
-
compatible,
|
|
511
|
-
gate: compatible ? 'pass' : 'fail',
|
|
512
|
-
repo: { root, stack: repo.stack, facts: repo.facts, evidence: repo.evidence },
|
|
513
|
-
workspace: { root: workspace.root ?? null, source: workspace.source, detail: workspace.detail },
|
|
514
|
-
paths: paths.slice(0, 8),
|
|
515
|
-
taskTokens: tokens,
|
|
516
|
-
signals: reasons,
|
|
517
|
-
variants: (skill.variants ?? []).map((v) => ({ id: v.id, stack: v.stack, bytes: v.bytes })),
|
|
518
|
-
aliasOf: skill.aliasOf ?? null,
|
|
519
|
-
variantOf: skill.variantOf ?? null,
|
|
520
|
-
requires: skill.requires ?? [],
|
|
521
|
-
}, { maxBytes: flags.maxBytes ?? 0 });
|
|
609
|
+
emitJson({ ok: true, command: 'skills why', ...facts }, { maxBytes: flags.maxBytes ?? 0 });
|
|
522
610
|
return EXIT.OK;
|
|
523
611
|
}
|
|
524
612
|
|
|
525
613
|
const lines = [
|
|
526
|
-
`${skill
|
|
614
|
+
`${facts.skill} - score ${facts.score}`,
|
|
527
615
|
'',
|
|
528
|
-
`repo stack ${repo.stack.join(', ') || 'undetected'}`,
|
|
529
|
-
`repo facts ${repo.facts.join(', ') || 'none'}`,
|
|
530
|
-
`repo evidence ${repo.evidence.join(' ') || 'none'}`,
|
|
616
|
+
`repo stack ${facts.repo.stack.join(', ') || 'undetected'}`,
|
|
617
|
+
`repo facts ${facts.repo.facts.join(', ') || 'none'}`,
|
|
618
|
+
`repo evidence ${facts.repo.evidence.join(' ') || 'none'}`,
|
|
531
619
|
await workspaceLine(flags),
|
|
532
|
-
`paths seen ${paths.length > 0 ? paths.
|
|
533
|
-
`task tokens ${
|
|
620
|
+
`paths seen ${facts.paths.length > 0 ? facts.paths.join(' ') : '(none - pass --paths or stage files)'}`,
|
|
621
|
+
`task tokens ${facts.taskTokens.length > 0 ? facts.taskTokens.join(' ') : '(none - pass --task)'}`,
|
|
534
622
|
'',
|
|
535
|
-
`gate ${compatible ? 'pass (stack overlaps the repo)' : 'FAIL (no stack overlap, so it can never be selected)'}`,
|
|
536
|
-
`signals ${
|
|
623
|
+
`gate ${facts.compatible ? 'pass (stack overlaps the repo)' : 'FAIL (no stack overlap, so it can never be selected)'}`,
|
|
624
|
+
`signals ${facts.signals.length > 0 ? facts.signals.join(' ') : '(none - this skill scores 0 for the current context)'}`,
|
|
537
625
|
];
|
|
538
626
|
|
|
539
|
-
if (
|
|
627
|
+
if (facts.variants?.length > 0) {
|
|
540
628
|
lines.push('', 'variants (one body per stack):');
|
|
541
|
-
for (const v of
|
|
629
|
+
for (const v of facts.variants) lines.push(` ${v.id.padEnd(8)} ${v.stack.join('+').padEnd(30)} ${kb(v.bytes)}`);
|
|
542
630
|
}
|
|
543
|
-
if (
|
|
544
|
-
if (
|
|
545
|
-
if (
|
|
631
|
+
if (facts.aliasOf) lines.push('', `alias-of ${facts.aliasOf} (retired name, resolves to that skill)`);
|
|
632
|
+
if (facts.variantOf) lines.push('', `variant-of ${facts.variantOf} (sibling group, one body per stack)`);
|
|
633
|
+
if (facts.requires?.length > 0) lines.push(`requires ${facts.requires.join(', ')}`);
|
|
546
634
|
|
|
547
635
|
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
548
636
|
return EXIT.OK;
|
|
@@ -668,7 +756,7 @@ async function bodyReadable(skill, workspaceRoot, root) {
|
|
|
668
756
|
return Boolean(full && existsSync(full));
|
|
669
757
|
}
|
|
670
758
|
|
|
671
|
-
|
|
759
|
+
async function readBody(skill, workspaceRoot, root) {
|
|
672
760
|
const full = await sourcePath(skill, workspaceRoot, root);
|
|
673
761
|
if (!full) {
|
|
674
762
|
return `(body unavailable: this installation has no workspace and ${root ?? 'this directory'} does not hold ${skill.name}; ${WORKSPACE_REMEDY})`;
|