simt-dev 0.7.0 → 0.8.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 +191 -870
- package/README.md +188 -213
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +12 -2
- package/rules/core.md +7 -0
- package/rules/mdc/intake-questions.mdc +8 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agenthooks/shim.mjs +1 -1
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +31 -0
- package/src/argv.mjs +48 -3
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +163 -105
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +33 -73
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +121 -475
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +79 -1064
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +120 -962
- package/src/bench/run.mjs +185 -802
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +48 -15
- package/src/ci/dependabot.mjs +25 -55
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +4 -13
- package/src/commands/agenthook.mjs +70 -40
- package/src/commands/bench.mjs +191 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +13 -14
- package/src/commands/compare.mjs +42 -42
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +4 -4
- package/src/commands/distill.mjs +4 -3
- package/src/commands/docs.mjs +88 -0
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +6 -9
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +26 -25
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +6 -5
- package/src/commands/help.mjs +168 -19
- package/src/commands/hooks.mjs +119 -49
- package/src/commands/ignore.mjs +13 -8
- package/src/commands/impact.mjs +8 -15
- package/src/commands/index.mjs +318 -193
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +124 -105
- package/src/commands/lease.mjs +73 -25
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +24 -14
- package/src/commands/mcp.mjs +2 -18
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +2 -2
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +12 -11
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +37 -16
- package/src/commands/shared.mjs +14 -19
- package/src/commands/skills.mjs +33 -32
- package/src/commands/skillscompile.mjs +7 -8
- package/src/commands/skillsinstall.mjs +7 -6
- package/src/commands/slice.mjs +220 -74
- package/src/commands/spec.mjs +167 -0
- package/src/commands/status.mjs +27 -18
- package/src/commands/sync.mjs +27 -25
- package/src/commands/tests.mjs +18 -21
- package/src/commands/tokens.mjs +21 -26
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +15 -7
- package/src/commands/why.mjs +15 -21
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +3 -2
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +6 -2
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +7 -5
- package/src/hooks/dispatcher.mjs +209 -60
- package/src/hooks/gates.mjs +10 -43
- package/src/hooks/resolve.mjs +4 -14
- package/src/hooks/shim.mjs +5 -2
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +31 -12
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +94 -20
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +150 -14
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +119 -25
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -967
- package/src/measure/corpus.mjs +150 -0
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/session.mjs +3 -2
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +209 -10
- package/src/packs/manage.mjs +32 -12
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/lock.mjs +11 -1
- package/src/projection/manifest.mjs +29 -468
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +20 -550
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +31 -0
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +19 -22
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +10 -13
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/sys/proc.mjs +151 -0
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +3 -0
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +58 -11
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/docs/decisions/README.md +0 -600
- package/src/bench/tasks.mjs +0 -36
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The command spec: one row per path this binary dispatches (review 0003 Q60).
|
|
2
|
+
//
|
|
3
|
+
// The list of commands existed in four places that disagreed - the dispatch map, a
|
|
4
|
+
// hand-written `--help` template, the completions table and the README - so a command
|
|
5
|
+
// could work and be undiscoverable, or be documented and absent. A row here is the one
|
|
6
|
+
// place a command is declared; `src/commands/index.mjs` derives its dispatchable paths
|
|
7
|
+
// and group defaults from it, and `test/spec.test.mjs` fails when the registry and the
|
|
8
|
+
// spec stop agreeing in either direction.
|
|
9
|
+
//
|
|
10
|
+
// The fields that only some consumers need (`tier`, `writes`, `args`, `flags`) arrive
|
|
11
|
+
// with the slices that read them: help (Q61), validation (Q62), MCP (Q66). A field no
|
|
12
|
+
// code reads is a field that drifts, and a wrong `writes` claim is worse than none.
|
|
13
|
+
//
|
|
14
|
+
// `defaultFor` marks the subcommand a bare group resolves to. It sits on the row that
|
|
15
|
+
// IS the default rather than on a separate map, so the reason for a default lives beside
|
|
16
|
+
// the command it names - `skills` alone indexes because the read is the safe default,
|
|
17
|
+
// `slice` alone starts because an id-less `start` fails before it writes anything.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} SpecRow
|
|
21
|
+
* @property {string} path the dispatch key: `status`, or `group subcommand`
|
|
22
|
+
* @property {string} summary one phrase, printed by help and by the generated docs
|
|
23
|
+
* @property {string} [defaultFor] the group whose bare form resolves to this row
|
|
24
|
+
* @property {string[]} [aliases] spellings that resolve to this row, never listed in help
|
|
25
|
+
* @property {string} [mcp] the MCP tool name that answers this row, when one does
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** @type {SpecRow[]} */
|
|
29
|
+
export const SPEC = [
|
|
30
|
+
{ path: 'agent-hook', usage: '<event> --harness', summary: 'the harness entry point: decide one tool call, or deliver the prompt packet', aliases: ['harness hook'], flags: ["diff","harness"], },
|
|
31
|
+
{ path: 'agent-hooks install', usage: '--yes', summary: "write this repository's policies into each harness hooks file", writes: 'repo', aliases: ['harness install'], flags: ["yes","diff","harness"], },
|
|
32
|
+
{ path: 'agent-hooks status', summary: 'what each harness will run, and what is not trusted yet', defaultFor: 'agent-hooks', flags: ["diff","harness"], },
|
|
33
|
+
{ path: 'agents install', usage: '--yes', summary: 'write the Simt-Agent into each harness agents file', writes: 'repo', flags: ["yes","diff","harness"], },
|
|
34
|
+
// A group like `agent-hooks`: `install` writes the agent file into each harness's own
|
|
35
|
+
// directory and `status` reports what a harness carries. The default is the read.
|
|
36
|
+
{ path: 'agents status', summary: 'what each harness carries, and what is not installed yet', defaultFor: 'agents', flags: ["diff","harness"], },
|
|
37
|
+
{ path: 'bench ablate', usage: '--yes', summary: 'measure one arm of the brief against its ablation', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
38
|
+
{ path: 'bench critic', summary: 'score the review critic against the labelled bench set', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
39
|
+
{ path: 'bench export', usage: '--yes', summary: 'write the sealed bench corpus as one signed document', writes: 'repo', flags: ["yes","all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
40
|
+
{ path: 'bench import', summary: 'verify a sealed corpus and load it into the vault', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
41
|
+
{ path: 'bench mine', usage: '--yes', summary: 'mine tasks out of a repository history', writes: 'repo', flags: ["yes","all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
42
|
+
{ path: 'bench probes', usage: '--yes', summary: 'run the deterministic probes against a workspace', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
43
|
+
{ path: 'bench report', usage: '[--compare a b]', summary: 'summarise the runs recorded in the vault', defaultFor: 'bench', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
44
|
+
{ path: 'bench run', usage: '--yes', summary: 'run agents against the mined tasks', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
45
|
+
{ path: 'bench variants', usage: '--yes', summary: 'run one task across its variant prompts', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
46
|
+
{ path: 'brief', usage: '"<task>"', mcp: { name: 'brief', flags: ['budget', 'paths', 'task'] }, summary: 'the packet for a task: rules, skills and the files it names, within a budget', flags: ["budget","explain","paths","task"], },
|
|
47
|
+
{ path: 'check', mcp: { name: 'check', flags: [] }, summary: 'report stale or missing generated regions and every check finding', flags: ["check","diff","dry-run"], },
|
|
48
|
+
{ path: 'ci', summary: "refresh simt-dev's own .github workflow and Dependabot files", flags: ["check","diff"], },
|
|
49
|
+
{ path: 'compare', mcp: { name: 'compare', flags: [] }, summary: 'the adoption and drift matrix across every sibling repository', flags: [], },
|
|
50
|
+
{ path: 'completions', usage: '<bash|zsh|fish>', summary: 'print a bash, zsh or fish completion script generated from this spec', flags: [], },
|
|
51
|
+
{ path: 'decide', usage: '"<title>" --yes', summary: 'scaffold a numbered decision record with its frontmatter', writes: 'repo', flags: ["yes","check","diff","dry-run","paths","supersedes","task"], },
|
|
52
|
+
{ path: 'distill', usage: '<skill|doc>', summary: 'turn a repository into a skill or rule proposal', flags: [], },
|
|
53
|
+
{ path: 'docs readme', summary: 'render the README command list from the spec', writes: 'repo', defaultFor: 'docs', flags: ["yes","diff"], },
|
|
54
|
+
{ path: 'doctor', usage: '[--fix]', mcp: { name: 'doctor', flags: [] }, summary: 'report what is wrong with this installation', writes: 'repo', flags: ["yes","diff","fix"], },
|
|
55
|
+
{ path: 'evidence', summary: 'the runs recorded against the tree on disk', flags: [], },
|
|
56
|
+
{ path: 'evidence check', usage: '<msg-file>', summary: 'a commit message claiming a run this tree does not have', flags: [], },
|
|
57
|
+
{ path: 'explain', usage: '<CODE>', summary: 'what one check finding code means, and how to fix it', flags: [], },
|
|
58
|
+
{ path: 'gate', usage: '--changed', summary: 'the tests a change reaches, and only those', flags: ["changed"], },
|
|
59
|
+
{ path: 'graph lint', usage: '[--spec agentskills]', summary: 'check the skill registry against the skills on disk', defaultFor: 'graph', aliases: ['skills lint'], flags: ["spec"], },
|
|
60
|
+
{ path: 'harness detect', usage: '[--user]', summary: 'which harness is installed, and what it carries', defaultFor: 'harness', flags: ["at","reader","table","user"], },
|
|
61
|
+
{ path: 'harness loaders', usage: '[--table]', summary: 'the instruction files each harness loads, and what they cost', flags: ["at","reader","table","user"], },
|
|
62
|
+
{ path: 'hooks install', usage: '--yes', summary: 'install the shared dispatcher and report what it would run', writes: 'repo', flags: ["yes","diff","dry-run"], },
|
|
63
|
+
{ path: 'hooks run', usage: '<event>', summary: "run one git event's gates now, as git would", writes: 'repo', flags: ["yes","diff","dry-run"], },
|
|
64
|
+
{ path: 'hooks status', summary: 'what each git event routes to, and whether it is wired', defaultFor: 'hooks', flags: ["diff","dry-run"], },
|
|
65
|
+
{ path: 'ignore', usage: '[--yes] [--diff]', summary: 'render the one ignore list into every harness ignore file', flags: ["diff"], },
|
|
66
|
+
{ path: 'impact', usage: '<file|symbol>', summary: 'who a change reaches: importers, their tests and the contract', flags: [], },
|
|
67
|
+
{ path: 'index', usage: '[--embed]', summary: 'rebuild the local file index from git blob hashes', flags: ["allow-remote","embed"], },
|
|
68
|
+
{ path: 'init', usage: '[path]', summary: 'adopt this repository: manifest, region, hooks, agents and agent-hooks', writes: 'repo', flags: ["yes","accept-conflicts","backup","diff","harness","agents","surfaces"], },
|
|
69
|
+
{ path: 'lease', usage: '[<paths>] --session', summary: 'claim files for one session, or list the claims', flags: ["harness","session","ttl"], },
|
|
70
|
+
{ path: 'lessons capture', summary: 'scan the ledger and history for new lesson candidates', flags: ["diff","expires","paths","task"], },
|
|
71
|
+
{ path: 'lessons list', summary: 'deterministic lesson candidates and the promoted lessons', defaultFor: 'lessons', flags: ["diff","expires","paths","task"], },
|
|
72
|
+
{ path: 'lessons review', usage: '<id> --yes', summary: 'promote one lesson candidate into docs/lessons', flags: ["diff","expires","paths","task"], },
|
|
73
|
+
{ path: 'local doctor', usage: '[--allow-remote]', summary: 'the local model endpoints, and whether they answer', defaultFor: 'local', flags: ["allow-remote"], },
|
|
74
|
+
{ path: 'map', usage: '--task "<task>"', summary: 'the files a task is most likely to touch, within a byte budget', flags: ["allow-remote","budget","embed","level","recall","task"], values: { level: ['outline', 'names', 'signatures'] }, },
|
|
75
|
+
{ path: 'mcp', usage: '[--list]', summary: 'serve the read-only MCP surface over stdio', flags: ["list"], },
|
|
76
|
+
// Plan 0003 R45. The default is the read: `pack` alone lints the working directory,
|
|
77
|
+
// the way `skills` alone indexes and `graph` alone lints.
|
|
78
|
+
{ path: 'pack lint', usage: '[dir] [--require-measured]', summary: 'lint a rule pack in the working directory', defaultFor: 'pack', flags: ["diff","require-measured","source"], },
|
|
79
|
+
{ path: 'pack new', usage: '<name> --source <spec>', summary: 'scaffold a rule pack', flags: ["diff","require-measured","source"], },
|
|
80
|
+
{ path: 'pack publish', usage: '[dir] --yes', summary: 'publish a rule pack', flags: ["diff","require-measured","source"], },
|
|
81
|
+
{ path: 'plan done', usage: '<id> [<sha>] --yes', mcp: { name: 'plan_done', flags: ['state', 'test'] }, summary: 'mark one tracker row complete, with its SHA and its evidence', flags: ["diff","state","test"], },
|
|
82
|
+
{ path: 'plan next', usage: '[<id>]', mcp: { name: 'plan_next', flags: [] }, summary: "one ready slice's section, or the one named", flags: ["diff","state","test"], },
|
|
83
|
+
{ path: 'plan status', summary: 'count the tracker rows', defaultFor: 'plan', flags: ["diff","state","test"], },
|
|
84
|
+
{ path: 'plugin', usage: '<claude|gemini|cursor|opencode>', summary: 'generate a harness package from the same tables the hooks use', aliases: ['harness plugin'], flags: ["diff","out"], },
|
|
85
|
+
{ path: 'pr-body', usage: '[--base <ref>]', mcp: { name: 'pr_body', flags: ['base'] }, summary: 'the pull request body: evidence, decisions, gate state and residual risk', flags: ["base"], },
|
|
86
|
+
{ path: 'review', usage: '[--local]', summary: 'review the staged diff against the estate rules', flags: [], },
|
|
87
|
+
{ path: 'rules decisions', summary: 'every known decision, and which layer each came from', writes: 'repo', flags: ["yes","all"], },
|
|
88
|
+
{ path: 'rules list', usage: '[--root <path>]', mcp: { name: 'rules_list', flags: [] }, summary: 'the rule catalogue, marked with what applies to this repository', defaultFor: 'rules', flags: ["all"], },
|
|
89
|
+
{ path: 'rules show', usage: '<id>', summary: 'one rule body', flags: ["all"], },
|
|
90
|
+
{ path: 'rules why', usage: '<id|ADR-NNN> [--all]', mcp: { name: 'rules_why', flags: [] }, summary: 'the decision that settled a rule, and whether it is still in force', flags: ["all"], },
|
|
91
|
+
{ path: 'shared install', usage: '--yes', summary: 'install the shared rules into a reader home directory', writes: 'repo', flags: ["yes","diff"], },
|
|
92
|
+
// ADR-084. `shared` is the one group whose default writes nothing, and it is `plan`
|
|
93
|
+
// for the same reason `skills`'s is `index`: the read answers "what would this do, and
|
|
94
|
+
// where", and `install` - the subcommand that touches a reader's home directory - is
|
|
95
|
+
// the one that needs `--yes` before it does anything.
|
|
96
|
+
{ path: 'shared plan', summary: 'what the shared install would write, and where', writes: 'repo', defaultFor: 'shared', flags: ["yes","diff"], },
|
|
97
|
+
{ path: 'skills brief', usage: '--task "<task>"', mcp: { name: 'skills_brief', flags: ['all', 'task'] }, summary: 'the packet for a task, read from the skill registry', aliases: ['skills select'], flags: ["all", "plan", "task"], },
|
|
98
|
+
{ path: 'skills compile', usage: '[<name>]', summary: 'compile a skill to the Agent Skills standard', flags: [], },
|
|
99
|
+
{ path: 'skills eval', usage: '[corpus]', summary: 'score the skill registry against transcripts', flags: ["all","plan"], },
|
|
100
|
+
{ path: 'skills find', usage: '<query>', mcp: { name: 'skills_find', flags: [] }, summary: 'skills that match a query', flags: ["all","plan"], },
|
|
101
|
+
{ path: 'skills index', mcp: { name: 'skills_index', flags: [] }, summary: 'rebuild the skill index', defaultFor: 'skills', aliases: ['skills list'], flags: ["all","plan"], },
|
|
102
|
+
{ path: 'skills install', usage: '<name> --yes', summary: 'install skills into the workspace', writes: 'repo', flags: ["yes","diff","from","variant"], },
|
|
103
|
+
{ path: 'skills show', usage: '<name>', mcp: { name: 'skills_show', flags: [] }, summary: 'one skill packet', flags: ["all","plan"], },
|
|
104
|
+
{ path: 'skills why', usage: '<name>', mcp: { name: 'skills_why', flags: [] }, summary: 'which skill covers a task', flags: ["all","plan"], },
|
|
105
|
+
{ path: 'slice abandon', usage: '<id>', summary: 'remove a slice worktree, delete its branch, and return the row', aliases: ['plan abandon'], flags: ["session","test","type"], },
|
|
106
|
+
{ path: 'slice finish', usage: '<id> --yes', summary: 'run the gates, commit the slice and record its evidence', writes: 'repo', aliases: ['plan finish'], flags: ["yes","session","test","type"], },
|
|
107
|
+
// Plan 0003 R53. Both halves write, so a bare `slice` has no read to default to.
|
|
108
|
+
// `start` is the default because with the id it is missing it fails with a usage error
|
|
109
|
+
// before anything is created - the one default here that cannot mutate.
|
|
110
|
+
{ path: 'slice start', usage: '<id> --yes | -- "<text>"', summary: 'give a slice its worktree, branch and brief; -- starts one from a prompt, no row', defaultFor: 'slice', aliases: ['plan start'], flags: ["id","session","test","type"], },
|
|
111
|
+
{ path: 'status', usage: '[--line]', summary: 'the repository snapshot, or one line of the last check', flags: ["line"], },
|
|
112
|
+
{ path: 'sync', usage: '[--explain] --yes', summary: 're-render the generated regions from the rule content', writes: 'repo', flags: ["yes","check","diff","dry-run"], },
|
|
113
|
+
{ path: 'tests', usage: '<path> [--cochange]', summary: 'the tests that exercise a file', flags: ["cochange"], },
|
|
114
|
+
{ path: 'tokens', usage: '[--repos] [--session]', summary: 'count tokens per instruction file and per skill', aliases: ['harness cost'], flags: ["repos","session"], },
|
|
115
|
+
{ path: 'undo', usage: '--yes', summary: 'restore the last journaled set of writes, if untouched', writes: 'repo', flags: ["yes","diff"], },
|
|
116
|
+
{ path: 'upgrade', usage: '--yes', summary: 'replace this installation with the pinned release', writes: 'repo', flags: ["yes","all","diff","workspace"], },
|
|
117
|
+
{ path: 'why', usage: '[path]', summary: 'what governs a file: decisions, rules, skills, gates and lessons', flags: [], },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Which tier a command belongs to, decided by its group: the commands a reader reaches
|
|
123
|
+
* for while working are core and appear in `--help`; the rest are advanced and appear
|
|
124
|
+
* under `help --all`. A tier is a presentation fact, not a permission, so a command
|
|
125
|
+
* cannot be missing from both pages - `test/help.test.mjs` checks that every path is on
|
|
126
|
+
* one of them (review 0003 Q61).
|
|
127
|
+
*/
|
|
128
|
+
const CORE_GROUPS = new Set(['brief', 'check', 'gate', 'graph', 'hooks', 'impact', 'init', 'map', 'plan', 'rules', 'skills', 'slice', 'status', 'sync', 'tests', 'why']);
|
|
129
|
+
|
|
130
|
+
/** The tier a path is listed under: `core` or `advanced`. */
|
|
131
|
+
export function tierOf(path) {
|
|
132
|
+
return CORE_GROUPS.has(path.split(' ')[0]) ? 'core' : 'advanced';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The tools that answer a question no command asks.
|
|
137
|
+
*
|
|
138
|
+
* `federation_query` reads the federation config and consults a sibling repository
|
|
139
|
+
* directly (src/federation/query.mjs). There is no CLI verb for it, so giving it a spec
|
|
140
|
+
* row would advertise a command this binary cannot dispatch; leaving it undeclared is
|
|
141
|
+
* how a tool list and a command list drift apart. It is listed here with the reason
|
|
142
|
+
* instead, and test/mcp-spec.test.mjs accepts a tool without a row only when this table
|
|
143
|
+
* says why.
|
|
144
|
+
*/
|
|
145
|
+
export const MCP_ONLY = [
|
|
146
|
+
{ tool: 'federation_query', why: 'a transport-only read: no command in this binary asks a sibling repository a question' },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
/** The dispatchable paths, sorted - the answer to "what can this binary do". */
|
|
150
|
+
export const SPEC_PATHS = SPEC.map((row) => row.path).sort();
|
|
151
|
+
|
|
152
|
+
/** group -> the subcommand its bare form resolves to. */
|
|
153
|
+
export const GROUP_DEFAULTS = new Map(SPEC.filter((row) => row.defaultFor !== undefined).map((row) => [row.defaultFor, row.path.split(' ')[1]]));
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The spellings this binary answers that are not paths (review 0003 Q81).
|
|
157
|
+
*
|
|
158
|
+
* A rename costs every script and every habit a reader already has, so a new name
|
|
159
|
+
* arrives as an alias instead of a replacement: the path stays the dispatch key, help
|
|
160
|
+
* lists the path, and the alias resolves before the registry is consulted. An alias is
|
|
161
|
+
* deliberately absent from help and from the generated docs — the point of a second
|
|
162
|
+
* spelling is that the reader who already knows it can type it, not that a new reader
|
|
163
|
+
* has to choose between two names for one command.
|
|
164
|
+
*/
|
|
165
|
+
export const ALIASES = new Map(
|
|
166
|
+
SPEC.flatMap((row) => (row.aliases ?? []).map((alias) => [alias, row.path])),
|
|
167
|
+
);
|
package/src/commands/status.mjs
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
|
-
// `simt status --line`
|
|
1
|
+
// `simt status [--line]`
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `
|
|
7
|
-
//
|
|
3
|
+
// Two questions, one command. `--line` is for shell prompts, tmux and agent status lines:
|
|
4
|
+
// one line from the last `check`, read only from `.simt/check.json` plus one
|
|
5
|
+
// `git rev-parse HEAD`, so it answers in milliseconds while `check` itself takes the time
|
|
6
|
+
// projection takes. Bare `status` is the aggregate a UI's home view asks for - last
|
|
7
|
+
// check, git hooks, agent hooks, evidence for the tree on disk, active leases, tracker
|
|
8
|
+
// state, the last journal set - and it was a usage error until review 0003 Q82.
|
|
8
9
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// The line's cache lives in `status.mjs`; the aggregate lives in `status-snapshot.mjs`.
|
|
11
|
+
// Both report the gate *as of the last check* rather than measuring: a status command
|
|
12
|
+
// that ran the gate would cost what the gate costs, and a prompt that waited for it is a
|
|
13
|
+
// prompt nobody reads.
|
|
12
14
|
|
|
13
|
-
import { EXIT, emit, emitJson,
|
|
15
|
+
import { EXIT, emit, emitJson, isJson, present } from '../output/index.mjs';
|
|
14
16
|
import { CLI } from '../name.mjs';
|
|
15
17
|
import { currentHead, readCheckCache, renderStatusLine } from '../status.mjs';
|
|
18
|
+
import { renderSnapshot, statusSnapshot } from '../status-snapshot.mjs';
|
|
19
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
16
20
|
|
|
17
21
|
export async function statusCommand(args, flags) {
|
|
18
|
-
const root = flags
|
|
19
|
-
if (flags.line
|
|
20
|
-
|
|
22
|
+
const root = commandRoot(flags, args);
|
|
23
|
+
if (flags.line === true) return statusLine(root, flags);
|
|
24
|
+
|
|
25
|
+
const snapshot = await statusSnapshot(root);
|
|
26
|
+
const code = snapshot.ok ? EXIT.OK : EXIT.FAIL;
|
|
27
|
+
if (isJson(flags)) {
|
|
28
|
+
emitJson(snapshot);
|
|
29
|
+
return code;
|
|
21
30
|
}
|
|
31
|
+
emit(renderSnapshot(snapshot));
|
|
32
|
+
return code;
|
|
33
|
+
}
|
|
22
34
|
|
|
35
|
+
async function statusLine(root, flags) {
|
|
23
36
|
const record = await readCheckCache(root);
|
|
24
37
|
if (record === null) {
|
|
25
38
|
// Informational, not a failure: a repository that never ran `check` — or
|
|
26
39
|
// one a writer just invalidated — has no state to report, and a prompt
|
|
27
40
|
// that errored until the first check would train its reader to ignore it.
|
|
28
|
-
|
|
29
|
-
emitJson({ ok: true, command: 'status', root, line: null, cached: false, stale: false });
|
|
30
|
-
return EXIT.OK;
|
|
31
|
-
}
|
|
32
|
-
emit('simt no cache - run check');
|
|
41
|
+
present({ ok: true, command: 'status', root, line: null, cached: false, stale: false }, () => 'simt no cache - run check', flags);
|
|
33
42
|
return EXIT.OK;
|
|
34
43
|
}
|
|
35
44
|
|
package/src/commands/sync.mjs
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import { mkdir, readFile, stat, symlink, unlink } from 'node:fs/promises';
|
|
20
20
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
21
21
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import { repoContext } from '../context.mjs';
|
|
23
|
+
import { plan, checkTargets, checkTarget, select, pointerTargets, pointerState } from '../projection/project.mjs';
|
|
23
24
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
24
25
|
import { manifestDrift, readManifest, unrunnableGates, buildManifest, writeManifest, withGenerated, manifestText, manifestPath } from '../projection/manifest.mjs';
|
|
25
26
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
@@ -28,12 +29,14 @@ import { surfacesLine } from '../projection/surfaces.mjs';
|
|
|
28
29
|
import { CLI, LOCK_FILE, MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
29
30
|
import { assertVersionPin, packageVersion } from '../package-version.mjs';
|
|
30
31
|
import { resolvePacks } from '../packs.mjs';
|
|
31
|
-
import { commitJournal
|
|
32
|
+
import { commitJournal } from '../journal.mjs';
|
|
33
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
32
34
|
import { buildCacheRecord, hooksSummary, invalidateCheckCache, writeCheckCache } from '../status.mjs';
|
|
33
35
|
import { computeLock, readLock } from '../projection/lock.mjs';
|
|
34
36
|
import { runDeclaredChecks } from '../kernel/checks.mjs';
|
|
35
37
|
import { decisionsCutoff, decisionsDir } from '../decisions/dir.mjs';
|
|
36
38
|
import { lintDecisions } from '../memory/decide.mjs';
|
|
39
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
37
40
|
|
|
38
41
|
/** Findings `sync` reports and does not fail on.
|
|
39
42
|
*
|
|
@@ -123,7 +126,7 @@ export const CHECK_NON_BLOCKING = new Set([
|
|
|
123
126
|
]);
|
|
124
127
|
|
|
125
128
|
export async function syncCommand(positional, flags) {
|
|
126
|
-
const root = flags
|
|
129
|
+
const root = commandRoot(flags, positional);
|
|
127
130
|
const checkOnly = flags.check === true;
|
|
128
131
|
const mode = writeMode(flags);
|
|
129
132
|
|
|
@@ -324,19 +327,18 @@ export async function syncCommand(positional, flags) {
|
|
|
324
327
|
await writeManifest(root, manifestPlan.manifest, journal);
|
|
325
328
|
note(`wrote ${manifestPlan.file} (generated field(s) ${manifestPlan.stale.join(', ')})`);
|
|
326
329
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
330
|
+
// The parent is derived from the target, never from the surface table, so a future
|
|
331
|
+
// surface with a deeper path needs no change here. Three of the four rule surfaces
|
|
332
|
+
// live in directories that do not exist yet in a repository that has only ever been
|
|
333
|
+
// written to by hand, and the first `sync` after a surface was added failed with
|
|
334
|
+
// ENOENT on the second target having already written the first.
|
|
335
|
+
const targetChanges = await Promise.all(
|
|
336
|
+
changed.map(async (t) =>
|
|
337
|
+
change({ path: t.path, before: await readBefore(t.path), after: t.text, owner: 'sync', reason: 'the region is stale' }),
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
await applyPlan(root, targetChanges, { mode, journal, commit: false });
|
|
341
|
+
for (const t of changed) note(`wrote ${t.file}`);
|
|
340
342
|
await commitJournal(root, 'sync', journal);
|
|
341
343
|
// The regions moved, so the last `check` no longer describes this tree.
|
|
342
344
|
// Invalidated rather than refreshed: `sync` does not run gates, checkers
|
|
@@ -409,7 +411,7 @@ export async function syncCommand(positional, flags) {
|
|
|
409
411
|
: `would write ${changed.length + pointersAdded.length + (manifestChanged ? 1 : 0)} file(s); apply with: ${CLI} sync --yes`,
|
|
410
412
|
);
|
|
411
413
|
}
|
|
412
|
-
emit(lines.join('\n')
|
|
414
|
+
emit(lines.join('\n'));
|
|
413
415
|
// After the report: the report says what, the diff shows it (R50).
|
|
414
416
|
emitDiffs(diffs, flags);
|
|
415
417
|
}
|
|
@@ -544,8 +546,9 @@ async function gateCwdFindings(root, manifest) {
|
|
|
544
546
|
// reports 77. The command below renders these facts for terminals; the tool
|
|
545
547
|
// returns them as JSON. The CLI keeps its field names; the tool adopts them.
|
|
546
548
|
export async function checkRepo(root, opts = {}) {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
+
const ctx = opts.ctx ?? repoContext(root);
|
|
550
|
+
const findings = await checkTargets(root, { ...opts, ctx });
|
|
551
|
+
const rules = await ctx.rules();
|
|
549
552
|
|
|
550
553
|
// A rule file with no id would be projected without a traceable source, so it
|
|
551
554
|
// is a gate failure rather than a warning: `rules why` could not resolve it.
|
|
@@ -568,7 +571,7 @@ export async function checkRepo(root, opts = {}) {
|
|
|
568
571
|
// A declared gate this repo cannot run fails closed on every commit once hooks are
|
|
569
572
|
// installed. Reported here rather than left to `hooks status`, so the repo is red
|
|
570
573
|
// before anyone installs anything rather than after.
|
|
571
|
-
const { manifest, exists } = await
|
|
574
|
+
const { manifest, exists } = await ctx.manifest();
|
|
572
575
|
// The gates slot, counted — not just reported — so the line can print run/total.
|
|
573
576
|
// A cwd finding names a declared gate that will not run, so it counts with the
|
|
574
577
|
// unrunnable; an unknown-gate-cwd names no declared gate, so it does not.
|
|
@@ -603,14 +606,14 @@ export async function checkRepo(root, opts = {}) {
|
|
|
603
606
|
// hand either: the next `init` puts simt-dev's own bytes back and the formatter
|
|
604
607
|
// takes them away again. Reporting it here is what stops that being a surprise
|
|
605
608
|
// discovered at upgrade time.
|
|
606
|
-
const { stack } = await fingerprint(
|
|
607
|
-
const drift = await manifestDrift(root, stack, await
|
|
609
|
+
const { stack } = await ctx.fingerprint();
|
|
610
|
+
const drift = await manifestDrift(root, stack, await ctx.digest());
|
|
608
611
|
if (drift) findings.push(drift);
|
|
609
612
|
}
|
|
610
613
|
if (opts.frozen) findings.push(...(await frozenFindings(root, opts)));
|
|
611
614
|
// The checkers a selected rule declared (ADR-088). They read the rule source,
|
|
612
615
|
// not the folded region, so a line that left the prompt is still the line that runs.
|
|
613
|
-
const fp = await fingerprint(
|
|
616
|
+
const fp = await ctx.fingerprint();
|
|
614
617
|
const { selected } = select(rules.filter((r) => r.error === null), fp.stack ?? [], fp.facts ?? []);
|
|
615
618
|
findings.push(...(await runDeclaredChecks(root, selected, manifest ?? null)));
|
|
616
619
|
|
|
@@ -652,7 +655,7 @@ export async function checkRepo(root, opts = {}) {
|
|
|
652
655
|
}
|
|
653
656
|
|
|
654
657
|
export async function checkCommand(positional, flags) {
|
|
655
|
-
const root = flags
|
|
658
|
+
const root = commandRoot(flags, positional);
|
|
656
659
|
const { ok, findings } = await checkRepo(root, flags);
|
|
657
660
|
const payload = {
|
|
658
661
|
ok,
|
|
@@ -678,7 +681,6 @@ export async function checkCommand(positional, flags) {
|
|
|
678
681
|
return f.remedy ? [head, `${' '.repeat(11)}remedy: ${f.remedy}`] : [head];
|
|
679
682
|
})
|
|
680
683
|
.join('\n'),
|
|
681
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
682
684
|
);
|
|
683
685
|
return ok ? EXIT.OK : EXIT.FAIL;
|
|
684
686
|
}
|
package/src/commands/tests.mjs
CHANGED
|
@@ -5,18 +5,17 @@
|
|
|
5
5
|
// batch. Coverage is read only from a fixed list of paths. History is the last
|
|
6
6
|
// 200 non-merge commits, which is a map and not the whole changelog.
|
|
7
7
|
|
|
8
|
-
import { execFile } from 'node:child_process';
|
|
9
8
|
import { readFile } from 'node:fs/promises';
|
|
10
9
|
import { join } from 'node:path';
|
|
11
|
-
import { promisify } from 'node:util';
|
|
12
10
|
|
|
13
|
-
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
11
|
+
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
14
12
|
import { CLI } from '../name.mjs';
|
|
15
|
-
import { isTestPath } from '../
|
|
13
|
+
import { isTestPath } from '../testing/paths.mjs';
|
|
16
14
|
import { blobTexts, isIndexStale, langOf, readEdges, readFiles, reindex } from '../index/store.mjs';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
import { coverage } from '../index/coverage.mjs';
|
|
16
|
+
import { HISTORY, importsOf, parseCoverage, testEdges } from '../index/tests.mjs';
|
|
17
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
18
|
+
import { git } from '../sys/git.mjs';
|
|
20
19
|
|
|
21
20
|
const COVERAGE_PATHS = [
|
|
22
21
|
'coverage/lcov.info',
|
|
@@ -31,11 +30,11 @@ const COVERAGE_PATHS = [
|
|
|
31
30
|
|
|
32
31
|
export async function testsCommand(args, flags) {
|
|
33
32
|
if (args.length > 1) return fail(`unknown tests argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} tests [path]`);
|
|
34
|
-
const root = flags
|
|
33
|
+
const root = commandRoot(flags);
|
|
35
34
|
const loaded = await loadFiles(root);
|
|
36
35
|
if (loaded.error) return loaded.error;
|
|
37
36
|
const { files } = loaded;
|
|
38
|
-
const [stored, texts,
|
|
37
|
+
const [stored, texts, reports, commits] = await Promise.all([
|
|
39
38
|
readEdges(root),
|
|
40
39
|
testBlobs(root, files),
|
|
41
40
|
coverageTexts(root),
|
|
@@ -49,20 +48,23 @@ export async function testsCommand(args, flags) {
|
|
|
49
48
|
if (!text) continue;
|
|
50
49
|
imports.push(...importsOf(file.path, text, langOf(file.path), paths));
|
|
51
50
|
}
|
|
52
|
-
const edges = testEdges({ files, imports, coverage, commits });
|
|
51
|
+
const edges = testEdges({ files, imports, coverage: reports, commits });
|
|
53
52
|
const target = args[0];
|
|
54
53
|
if (!target) return summarise(root, flags, edges);
|
|
55
54
|
if (!paths.has(target)) {
|
|
56
55
|
return fail(`not in the index: ${target}`, EXIT.USAGE, `pass a path ${CLI} index knows`);
|
|
57
56
|
}
|
|
58
|
-
const tests =
|
|
57
|
+
const tests = coverage({ files, imports, coverage: reports, commits }, [target], {
|
|
58
|
+
transitive: false,
|
|
59
|
+
cochange: flags.cochange === true,
|
|
60
|
+
});
|
|
59
61
|
const payload = { ok: true, command: 'tests', root, path: target, tests };
|
|
60
62
|
if (isJson(flags)) {
|
|
61
|
-
emitJson(payload
|
|
63
|
+
emitJson(payload);
|
|
62
64
|
return EXIT.OK;
|
|
63
65
|
}
|
|
64
66
|
const lines = [`tests ${tests.length}`, ...tests];
|
|
65
|
-
emit(lines.join('\n')
|
|
67
|
+
emit(lines.join('\n'));
|
|
66
68
|
return EXIT.OK;
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -74,11 +76,7 @@ function summarise(root, flags, edges) {
|
|
|
74
76
|
else if (edge.kind === 'cochange') cochange += 1;
|
|
75
77
|
}
|
|
76
78
|
const payload = { ok: true, command: 'tests', root, test, cochange };
|
|
77
|
-
|
|
78
|
-
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
79
|
-
return EXIT.OK;
|
|
80
|
-
}
|
|
81
|
-
emit(`test ${test}\ncochange ${cochange}`, { maxBytes: flags.maxBytes ?? 0 });
|
|
79
|
+
present(payload, () => `test ${test}\ncochange ${cochange}`, flags);
|
|
82
80
|
return EXIT.OK;
|
|
83
81
|
}
|
|
84
82
|
|
|
@@ -128,10 +126,9 @@ async function coverageTexts(root) {
|
|
|
128
126
|
async function history(root) {
|
|
129
127
|
let stdout = '';
|
|
130
128
|
try {
|
|
131
|
-
|
|
132
|
-
cwd: root,
|
|
129
|
+
stdout = await git(root, ['log', '-n', String(HISTORY), '--no-merges', '--name-only', '--pretty=format:%x1e'], {
|
|
133
130
|
maxBuffer: 16 << 20,
|
|
134
|
-
})
|
|
131
|
+
});
|
|
135
132
|
} catch {
|
|
136
133
|
return [];
|
|
137
134
|
}
|
package/src/commands/tokens.mjs
CHANGED
|
@@ -35,15 +35,10 @@ import {
|
|
|
35
35
|
} from '../measure/context.mjs';
|
|
36
36
|
import { measureSession } from '../measure/session.mjs';
|
|
37
37
|
import { sharedTotals } from '../projection/shared.mjs';
|
|
38
|
-
|
|
39
|
-
/** `1234567`, never `1,234,567`. `toLocaleString` output depends on the locale, and
|
|
40
|
-
* this command exists so two machines produce two numbers a pull request can diff. */
|
|
41
|
-
function plain(n) {
|
|
42
|
-
return String(n);
|
|
43
|
-
}
|
|
38
|
+
import { plainNumber } from '../util/fs.mjs';
|
|
44
39
|
|
|
45
40
|
function files(n) {
|
|
46
|
-
return n === 1 ? '1 file' : `${
|
|
41
|
+
return n === 1 ? '1 file' : `${plainNumber(n)} files`;
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
export async function tokensCommand(positional, flags) {
|
|
@@ -68,12 +63,12 @@ export async function tokensCommand(positional, flags) {
|
|
|
68
63
|
}
|
|
69
64
|
const lines = [
|
|
70
65
|
`tokens ${session.path} (session)`,
|
|
71
|
-
`rules ${
|
|
72
|
-
`file reads ${
|
|
73
|
-
`tool output ${
|
|
74
|
-
`total ${
|
|
66
|
+
`rules ${plainNumber(session.rules.bytes)} B ~${plainNumber(session.rules.tokens)} tokens (${plainNumber(session.rules.share)}%)`,
|
|
67
|
+
`file reads ${plainNumber(session.fileReads.bytes)} B ~${plainNumber(session.fileReads.tokens)} tokens (${plainNumber(session.fileReads.share)}%)`,
|
|
68
|
+
`tool output ${plainNumber(session.toolOutput.bytes)} B ~${plainNumber(session.toolOutput.tokens)} tokens (${plainNumber(session.toolOutput.share)}%)`,
|
|
69
|
+
`total ${plainNumber(session.total.bytes)} B ~${plainNumber(session.total.tokens)} tokens`,
|
|
75
70
|
];
|
|
76
|
-
emit(lines.join('\n')
|
|
71
|
+
emit(lines.join('\n'));
|
|
77
72
|
return EXIT.OK;
|
|
78
73
|
}
|
|
79
74
|
|
|
@@ -140,27 +135,27 @@ export async function tokensCommand(positional, flags) {
|
|
|
140
135
|
|
|
141
136
|
const lines = [];
|
|
142
137
|
if (flags.repos) {
|
|
143
|
-
lines.push(`tokens ${root} ${
|
|
138
|
+
lines.push(`tokens ${root} ${plainNumber(summary.roots)} repositories (${target.source})`);
|
|
144
139
|
for (const row of rows) {
|
|
145
|
-
const budget = row.budget.over ? ` over budget by ${
|
|
140
|
+
const budget = row.budget.over ? ` over budget by ${plainNumber(row.budget.overBy)} B` : '';
|
|
146
141
|
lines.push(
|
|
147
|
-
`${row.name} generated ${
|
|
142
|
+
`${row.name} generated ${plainNumber(row.generatedBytes)} B duplicate ${plainNumber(row.redundancy.duplicateBytes)} B unique ${plainNumber(row.redundancy.uniqueBytes)} B${budget}`,
|
|
148
143
|
);
|
|
149
144
|
for (const finding of row.findings) lines.push(`note ${row.name}: ${finding}`);
|
|
150
145
|
}
|
|
151
146
|
const share =
|
|
152
147
|
summary.generatedBytes === 0 ? 0 : Math.round((summary.duplicateBytes / summary.generatedBytes) * 100);
|
|
153
148
|
lines.push(
|
|
154
|
-
`totals generated ${
|
|
149
|
+
`totals generated ${plainNumber(summary.generatedBytes)} B duplicate ${plainNumber(summary.duplicateBytes)} B (${plainNumber(share)}% of generated) unique ${plainNumber(summary.uniqueBytes)} B repeat ${plainNumber(summary.repeatBytes)} B ~${plainNumber(summary.tokens)} tokens`,
|
|
155
150
|
);
|
|
156
151
|
// Only when there is a copy to count. The line a reader sees most is the one that
|
|
157
152
|
// says what their whole estate loads, and adding a second number to it that is
|
|
158
153
|
// usually zero is how a report stops being read.
|
|
159
154
|
if (summary.sharedBytes > 0) {
|
|
160
155
|
lines.push(
|
|
161
|
-
`shared ${
|
|
156
|
+
`shared ${plainNumber(summary.sharedBytes)} B carried once in ${plainNumber(summary.sharedFiles.length)} user-level file(s) estate total ${plainNumber(summary.estateBytes)} B`,
|
|
162
157
|
);
|
|
163
|
-
for (const f of summary.sharedFiles) lines.push(` ${f.path} ${
|
|
158
|
+
for (const f of summary.sharedFiles) lines.push(` ${f.path} ${plainNumber(f.bytes)} B ${plainNumber(f.rules)} rule(s)`);
|
|
164
159
|
}
|
|
165
160
|
} else {
|
|
166
161
|
const row = rows[0];
|
|
@@ -168,31 +163,31 @@ export async function tokensCommand(positional, flags) {
|
|
|
168
163
|
lines.push(`state ${row.adoption} surfaces ${row.surfaces.length === 0 ? '(AGENTS.md only)' : row.surfaces.join(', ')}`);
|
|
169
164
|
for (const channel of row.channels) {
|
|
170
165
|
lines.push(
|
|
171
|
-
`${channel.channel.padEnd(9)}${files(channel.files)} ${
|
|
166
|
+
`${channel.channel.padEnd(9)}${files(channel.files)} ${plainNumber(channel.generatedBytes)} B generated ${plainNumber(channel.readerBytes)} B read`,
|
|
172
167
|
);
|
|
173
168
|
}
|
|
174
|
-
const verdict = row.budget.over ? `over by ${
|
|
169
|
+
const verdict = row.budget.over ? `over by ${plainNumber(row.budget.overBy)} B` : 'within budget';
|
|
175
170
|
lines.push(
|
|
176
|
-
`always-on ${
|
|
171
|
+
`always-on ${plainNumber(row.generatedBytes)} B generated ~${plainNumber(row.generatedTokens)} tokens budget ${plainNumber(row.budget.bytes)} B ${verdict}`,
|
|
177
172
|
);
|
|
178
173
|
// The same reason as the `--repos` line, and it matters more here: measuring one
|
|
179
174
|
// repository is the default, so a reader who owns the workspace copy and never saw it
|
|
180
175
|
// counted would read a saving their own session does not get.
|
|
181
176
|
if (summary.sharedBytes > 0) {
|
|
182
177
|
lines.push(
|
|
183
|
-
`shared ${
|
|
178
|
+
`shared ${plainNumber(summary.sharedBytes)} B carried once in ${plainNumber(summary.sharedFiles.length)} user-level file(s) always-on for you: ${plainNumber(summary.estateBytes)} B`,
|
|
184
179
|
);
|
|
185
|
-
for (const f of summary.sharedFiles) lines.push(` ${f.path} ${
|
|
180
|
+
for (const f of summary.sharedFiles) lines.push(` ${f.path} ${plainNumber(f.bytes)} B ${plainNumber(f.rules)} rule(s)`);
|
|
186
181
|
}
|
|
187
182
|
lines.push(
|
|
188
|
-
`redundant ${
|
|
183
|
+
`redundant ${plainNumber(row.redundancy.duplicateBytes)} B repeats an earlier repository ${plainNumber(row.redundancy.repeatBytes)} B repeats an earlier document here ${plainNumber(row.redundancy.uniqueBytes)} B unique of ${plainNumber(row.redundancy.blockBytes)} B counted in blocks`,
|
|
189
184
|
);
|
|
190
185
|
for (const finding of row.findings) lines.push(`note ${finding}`);
|
|
191
186
|
}
|
|
192
187
|
lines.push(
|
|
193
|
-
`mcp tools/list ${
|
|
188
|
+
`mcp tools/list ${plainNumber(mcp.bytes)} B of ${plainNumber(mcp.tools)} tools (the same for every repository, so it is not in any total)`,
|
|
194
189
|
);
|
|
195
190
|
|
|
196
|
-
emit(lines.join('\n')
|
|
191
|
+
emit(lines.join('\n'));
|
|
197
192
|
return EXIT.OK;
|
|
198
193
|
}
|