simt-dev 0.6.3 → 0.7.1
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 +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.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-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- 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/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +31 -14
- package/src/projection/manifest.mjs +89 -53
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
|
@@ -0,0 +1,151 @@
|
|
|
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} [mcp] the MCP tool name that answers this row, when one does
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** @type {SpecRow[]} */
|
|
28
|
+
export const SPEC = [
|
|
29
|
+
{ path: 'agent-hook', summary: 'the harness entry point: decide one tool call, or deliver the prompt packet', flags: ["diff","harness"], },
|
|
30
|
+
{ path: 'agent-hooks install', summary: "write this repository's policies into each harness hooks file", writes: 'repo', flags: ["yes","diff","harness"], },
|
|
31
|
+
{ path: 'agent-hooks status', summary: 'what each harness will run, and what is not trusted yet', defaultFor: 'agent-hooks', flags: ["diff","harness"], },
|
|
32
|
+
{ path: 'agents install', summary: 'write the Simt-Agent into each harness agents file', writes: 'repo', flags: ["yes","diff","harness"], },
|
|
33
|
+
// A group like `agent-hooks`: `install` writes the agent file into each harness's own
|
|
34
|
+
// directory and `status` reports what a harness carries. The default is the read.
|
|
35
|
+
{ path: 'agents status', summary: 'what each harness carries, and what is not installed yet', defaultFor: 'agents', flags: ["diff","harness"], },
|
|
36
|
+
{ path: 'bench ablate', summary: 'measure one arm of the brief against its ablation', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
37
|
+
{ 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"], },
|
|
38
|
+
{ path: 'bench export', 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"], },
|
|
39
|
+
{ 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"], },
|
|
40
|
+
{ path: 'bench mine', summary: 'mine tasks out of a repository history', writes: 'repo', defaultFor: 'bench', flags: ["yes","all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
41
|
+
{ path: 'bench probes', summary: 'run the deterministic probes against a workspace', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
42
|
+
{ path: 'bench report', summary: 'summarise the runs recorded in the vault', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
43
|
+
{ path: 'bench run', summary: 'run agents against the mined tasks', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
44
|
+
{ path: 'bench variants', summary: 'run one task across its variant prompts', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
|
|
45
|
+
{ path: 'brief', 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"], },
|
|
46
|
+
{ path: 'check', mcp: { name: 'check', flags: [] }, summary: 'report stale or missing generated regions and every check finding', flags: ["check","diff","dry-run"], },
|
|
47
|
+
{ path: 'ci', summary: "refresh simt-dev's own .github workflow and Dependabot files", flags: ["check","diff"], },
|
|
48
|
+
{ path: 'compare', mcp: { name: 'compare', flags: [] }, summary: 'the adoption and drift matrix across every sibling repository', flags: [], },
|
|
49
|
+
{ path: 'completions', summary: 'print a bash, zsh or fish completion script generated from this spec', flags: [], },
|
|
50
|
+
{ path: 'decide', summary: 'scaffold a numbered decision record with its frontmatter', writes: 'repo', flags: ["yes","check","diff","dry-run","paths","supersedes","task"], },
|
|
51
|
+
{ path: 'distill', summary: 'turn a repository into a skill or rule proposal', flags: [], },
|
|
52
|
+
{ path: 'doctor', mcp: { name: 'doctor', flags: [] }, summary: 'report what is wrong with this installation', writes: 'repo', flags: ["yes","diff","fix"], },
|
|
53
|
+
{ path: 'evidence', summary: 'the runs recorded against the tree on disk', flags: [], },
|
|
54
|
+
{ path: 'evidence check', summary: 'a commit message claiming a run this tree does not have', flags: [], },
|
|
55
|
+
{ path: 'explain', summary: 'what one check finding code means, and how to fix it', flags: [], },
|
|
56
|
+
{ path: 'gate', summary: 'the tests a change reaches, and only those', flags: ["changed"], },
|
|
57
|
+
{ path: 'graph lint', summary: 'check the skill registry against the skills on disk', defaultFor: 'graph', flags: ["spec"], },
|
|
58
|
+
{ path: 'harness detect', summary: 'which harness is installed, and what it carries', defaultFor: 'harness', flags: ["at","reader","table","user"], },
|
|
59
|
+
{ path: 'harness loaders', summary: 'the instruction files each harness loads, and what they cost', flags: ["at","reader","table","user"], },
|
|
60
|
+
{ path: 'hooks install', summary: 'install the shared dispatcher and report what it would run', writes: 'repo', flags: ["yes","diff","dry-run"], },
|
|
61
|
+
{ path: 'hooks run', summary: "run one git event's gates now, as git would", writes: 'repo', flags: ["yes","diff","dry-run"], },
|
|
62
|
+
{ path: 'hooks status', summary: 'what each git event routes to, and whether it is wired', defaultFor: 'hooks', flags: ["diff","dry-run"], },
|
|
63
|
+
{ path: 'ignore', summary: 'render the one ignore list into every harness ignore file', flags: ["diff"], },
|
|
64
|
+
{ path: 'impact', summary: 'who a change reaches: importers, their tests and the contract', flags: [], },
|
|
65
|
+
{ path: 'index', summary: 'rebuild the local file index from git blob hashes', flags: ["allow-remote","embed"], },
|
|
66
|
+
{ path: 'init', summary: 'adopt this repository: manifest, region, hooks and toolchain pins', writes: 'repo', flags: ["yes","accept-conflicts","backup","diff","surfaces"], },
|
|
67
|
+
{ path: 'lease', summary: 'claim files for one session, or list the claims', flags: ["harness","session","ttl"], },
|
|
68
|
+
{ path: 'lessons capture', summary: 'scan the ledger and history for new lesson candidates', flags: ["diff","expires","paths","task"], },
|
|
69
|
+
{ path: 'lessons list', summary: 'deterministic lesson candidates and the promoted lessons', defaultFor: 'lessons', flags: ["diff","expires","paths","task"], },
|
|
70
|
+
{ path: 'lessons review', summary: 'promote one lesson candidate into docs/lessons', flags: ["diff","expires","paths","task"], },
|
|
71
|
+
{ path: 'local doctor', summary: 'the local model endpoints, and whether they answer', defaultFor: 'local', flags: ["allow-remote"], },
|
|
72
|
+
{ path: 'map', 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'] }, },
|
|
73
|
+
{ path: 'mcp', summary: 'serve the read-only MCP surface over stdio', flags: ["list"], },
|
|
74
|
+
// Plan 0003 R45. The default is the read: `pack` alone lints the working directory,
|
|
75
|
+
// the way `skills` alone indexes and `graph` alone lints.
|
|
76
|
+
{ path: 'pack lint', summary: 'lint a rule pack in the working directory', defaultFor: 'pack', flags: ["diff","require-measured","source"], },
|
|
77
|
+
{ path: 'pack new', summary: 'scaffold a rule pack', flags: ["diff","require-measured","source"], },
|
|
78
|
+
{ path: 'pack publish', summary: 'publish a rule pack', flags: ["diff","require-measured","source"], },
|
|
79
|
+
{ path: 'plan done', mcp: { name: 'plan_done', flags: ['state', 'test'] }, summary: 'mark one tracker row complete, with its SHA and its evidence', flags: ["diff","state","test"], },
|
|
80
|
+
{ path: 'plan next', mcp: { name: 'plan_next', flags: [] }, summary: "one ready slice's section, or the one named", flags: ["diff","state","test"], },
|
|
81
|
+
{ path: 'plan status', summary: 'count the tracker rows', defaultFor: 'plan', flags: ["diff","state","test"], },
|
|
82
|
+
{ path: 'plugin', summary: 'generate a harness package from the same tables the hooks use', flags: ["diff","out"], },
|
|
83
|
+
{ path: 'pr-body', mcp: { name: 'pr_body', flags: ['base'] }, summary: 'the pull request body: evidence, decisions, gate state and residual risk', flags: ["base"], },
|
|
84
|
+
{ path: 'review', summary: 'review the staged diff against the estate rules', flags: [], },
|
|
85
|
+
{ path: 'rules decisions', summary: 'every known decision, and which layer each came from', writes: 'repo', flags: ["yes","all"], },
|
|
86
|
+
{ path: 'rules list', mcp: { name: 'rules_list', flags: [] }, summary: 'the rule catalogue, marked with what applies to this repository', defaultFor: 'rules', flags: ["all"], },
|
|
87
|
+
{ path: 'rules show', summary: 'one rule body', flags: ["all"], },
|
|
88
|
+
{ path: 'rules why', mcp: { name: 'rules_why', flags: [] }, summary: 'the decision that settled a rule, and whether it is still in force', flags: ["all"], },
|
|
89
|
+
{ path: 'shared install', summary: 'install the shared rules into a reader home directory', writes: 'repo', flags: ["yes","diff"], },
|
|
90
|
+
// ADR-084. `shared` is the one group whose default writes nothing, and it is `plan`
|
|
91
|
+
// for the same reason `skills`'s is `index`: the read answers "what would this do, and
|
|
92
|
+
// where", and `install` - the subcommand that touches a reader's home directory - is
|
|
93
|
+
// the one that needs `--yes` before it does anything.
|
|
94
|
+
{ path: 'shared plan', summary: 'what the shared install would write, and where', writes: 'repo', defaultFor: 'shared', flags: ["yes","diff"], },
|
|
95
|
+
{ path: 'skills brief', mcp: { name: 'skills_brief', flags: ['all', 'task'] }, summary: 'the packet for a task, read from the skill registry', flags: ["all", "plan", "task"], },
|
|
96
|
+
{ path: 'skills compile', summary: 'compile a skill to the Agent Skills standard', flags: [], },
|
|
97
|
+
{ path: 'skills eval', summary: 'score the skill registry against transcripts', flags: ["all","plan"], },
|
|
98
|
+
{ path: 'skills find', mcp: { name: 'skills_find', flags: [] }, summary: 'skills that match a query', flags: ["all","plan"], },
|
|
99
|
+
{ path: 'skills index', mcp: { name: 'skills_index', flags: [] }, summary: 'rebuild the skill index', defaultFor: 'skills', flags: ["all","plan"], },
|
|
100
|
+
{ path: 'skills install', summary: 'install skills into the workspace', writes: 'repo', flags: ["yes","diff","from","variant"], },
|
|
101
|
+
{ path: 'skills show', mcp: { name: 'skills_show', flags: [] }, summary: 'one skill packet', flags: ["all","plan"], },
|
|
102
|
+
{ path: 'skills why', mcp: { name: 'skills_why', flags: [] }, summary: 'which skill covers a task', flags: ["all","plan"], },
|
|
103
|
+
{ path: 'slice abandon', summary: 'remove a slice worktree, delete its branch, and return the row', flags: ["session","test","type"], },
|
|
104
|
+
{ path: 'slice finish', summary: 'run the gates, commit the slice and record its evidence', writes: 'repo', flags: ["yes","session","test","type"], },
|
|
105
|
+
// Plan 0003 R53. Both halves write, so a bare `slice` has no read to default to.
|
|
106
|
+
// `start` is the default because with the id it is missing it fails with a usage error
|
|
107
|
+
// before anything is created - the one default here that cannot mutate.
|
|
108
|
+
{ path: 'slice start', summary: 'give a slice its own worktree, branch and brief', defaultFor: 'slice', flags: ["session","test","type"], },
|
|
109
|
+
{ path: 'status', summary: 'gate state from the last check', flags: ["line"], },
|
|
110
|
+
{ path: 'sync', summary: 're-render the generated regions from the rule content', writes: 'repo', flags: ["yes","check","diff","dry-run"], },
|
|
111
|
+
{ path: 'tests', summary: 'the tests that exercise a file', flags: ["cochange"], },
|
|
112
|
+
{ path: 'tokens', summary: 'count tokens per instruction file and per skill', flags: ["repos","session"], },
|
|
113
|
+
{ path: 'undo', summary: 'restore the last journaled set of writes, if untouched', writes: 'repo', flags: ["yes","diff"], },
|
|
114
|
+
{ path: 'upgrade', summary: 'replace this installation with the pinned release', writes: 'repo', flags: ["yes","all","diff","workspace"], },
|
|
115
|
+
{ path: 'why', summary: 'what governs a file: decisions, rules, skills, gates and lessons', flags: [], },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Which tier a command belongs to, decided by its group: the commands a reader reaches
|
|
121
|
+
* for while working are core and appear in `--help`; the rest are advanced and appear
|
|
122
|
+
* under `help --all`. A tier is a presentation fact, not a permission, so a command
|
|
123
|
+
* cannot be missing from both pages - `test/help.test.mjs` checks that every path is on
|
|
124
|
+
* one of them (review 0003 Q61).
|
|
125
|
+
*/
|
|
126
|
+
const CORE_GROUPS = new Set(['brief', 'check', 'gate', 'graph', 'hooks', 'impact', 'init', 'map', 'plan', 'rules', 'skills', 'slice', 'status', 'sync', 'tests', 'why']);
|
|
127
|
+
|
|
128
|
+
/** The tier a path is listed under: `core` or `advanced`. */
|
|
129
|
+
export function tierOf(path) {
|
|
130
|
+
return CORE_GROUPS.has(path.split(' ')[0]) ? 'core' : 'advanced';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The tools that answer a question no command asks.
|
|
135
|
+
*
|
|
136
|
+
* `federation_query` reads the federation config and consults a sibling repository
|
|
137
|
+
* directly (src/federation/query.mjs). There is no CLI verb for it, so giving it a spec
|
|
138
|
+
* row would advertise a command this binary cannot dispatch; leaving it undeclared is
|
|
139
|
+
* how a tool list and a command list drift apart. It is listed here with the reason
|
|
140
|
+
* instead, and test/mcp-spec.test.mjs accepts a tool without a row only when this table
|
|
141
|
+
* says why.
|
|
142
|
+
*/
|
|
143
|
+
export const MCP_ONLY = [
|
|
144
|
+
{ tool: 'federation_query', why: 'a transport-only read: no command in this binary asks a sibling repository a question' },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
/** The dispatchable paths, sorted - the answer to "what can this binary do". */
|
|
148
|
+
export const SPEC_PATHS = SPEC.map((row) => row.path).sort();
|
|
149
|
+
|
|
150
|
+
/** group -> the subcommand its bare form resolves to. */
|
|
151
|
+
export const GROUP_DEFAULTS = new Map(SPEC.filter((row) => row.defaultFor !== undefined).map((row) => [row.defaultFor, row.path.split(' ')[1]]));
|
package/src/commands/status.mjs
CHANGED
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
// only mode this command has is `--line`, and running something else by
|
|
11
11
|
// default would invent a second answer to what the status is.
|
|
12
12
|
|
|
13
|
-
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
13
|
+
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
14
14
|
import { CLI } from '../name.mjs';
|
|
15
15
|
import { currentHead, readCheckCache, renderStatusLine } from '../status.mjs';
|
|
16
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
16
17
|
|
|
17
18
|
export async function statusCommand(args, flags) {
|
|
18
|
-
const root = flags
|
|
19
|
+
const root = commandRoot(flags, args);
|
|
19
20
|
if (flags.line !== true) {
|
|
20
21
|
return fail('status needs --line', EXIT.USAGE, `run: ${CLI} status --line --root ${root}`);
|
|
21
22
|
}
|
|
@@ -25,11 +26,7 @@ export async function statusCommand(args, flags) {
|
|
|
25
26
|
// Informational, not a failure: a repository that never ran `check` — or
|
|
26
27
|
// one a writer just invalidated — has no state to report, and a prompt
|
|
27
28
|
// 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');
|
|
29
|
+
present({ ok: true, command: 'status', root, line: null, cached: false, stale: false }, () => 'simt no cache - run check', flags);
|
|
33
30
|
return EXIT.OK;
|
|
34
31
|
}
|
|
35
32
|
|
package/src/commands/sync.mjs
CHANGED
|
@@ -19,22 +19,23 @@
|
|
|
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';
|
|
26
27
|
import { readCapability } from '../hooks/capability.mjs';
|
|
27
28
|
import { surfacesLine } from '../projection/surfaces.mjs';
|
|
28
|
-
import { CLI } from '../name.mjs';
|
|
29
|
-
import { MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
29
|
+
import { CLI, LOCK_FILE, MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
30
30
|
import { assertVersionPin, packageVersion } from '../package-version.mjs';
|
|
31
31
|
import { resolvePacks } from '../packs.mjs';
|
|
32
32
|
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
33
33
|
import { buildCacheRecord, hooksSummary, invalidateCheckCache, writeCheckCache } from '../status.mjs';
|
|
34
|
-
import { computeLock, readLock
|
|
34
|
+
import { computeLock, readLock } from '../projection/lock.mjs';
|
|
35
35
|
import { runDeclaredChecks } from '../kernel/checks.mjs';
|
|
36
|
-
import {
|
|
36
|
+
import { decisionsCutoff, decisionsDir } from '../decisions/dir.mjs';
|
|
37
37
|
import { lintDecisions } from '../memory/decide.mjs';
|
|
38
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
38
39
|
|
|
39
40
|
/** Findings `sync` reports and does not fail on.
|
|
40
41
|
*
|
|
@@ -64,6 +65,12 @@ export const SYNC_NON_BLOCKING = new Set([
|
|
|
64
65
|
'budget-cut',
|
|
65
66
|
'file-lines-warn',
|
|
66
67
|
'check-unbuilt',
|
|
68
|
+
// Review 0003 Q28: the tsconfig strictness findings. The base file is repo-owned —
|
|
69
|
+
// init no longer writes it, so neither can `sync` — and both findings' remedies are
|
|
70
|
+
// a decision about the repo's own compiler policy, not a missing file. Blocking on
|
|
71
|
+
// them would fail every fresh TypeScript adoption over a file the tool does not write.
|
|
72
|
+
'inert',
|
|
73
|
+
'inconsistent',
|
|
67
74
|
]);
|
|
68
75
|
|
|
69
76
|
/**
|
|
@@ -101,10 +108,24 @@ export const SYNC_NON_BLOCKING = new Set([
|
|
|
101
108
|
// (ADR-083). It says the projection was cut to fit a ceiling this repository set, so the
|
|
102
109
|
// remedy is a decision about the number — and a cut the repository asked for by declaring
|
|
103
110
|
// a budget is the tool working, not a repository in need of a file.
|
|
104
|
-
|
|
111
|
+
// `inert` and `inconsistent` are here for review 0003 Q28: the base file is repo-owned,
|
|
112
|
+
// so the gate reports the state of the repo's own compiler policy without failing it.
|
|
113
|
+
// A fresh TypeScript adoption has neither file in a state the tool can repair, and a
|
|
114
|
+
// gate that reddens on a policy decision is one the repo satisfies by choosing, not by
|
|
115
|
+
// running a command.
|
|
116
|
+
export const CHECK_NON_BLOCKING = new Set([
|
|
117
|
+
'orphaned',
|
|
118
|
+
'pack-unpinned',
|
|
119
|
+
'budget',
|
|
120
|
+
'budget-cut',
|
|
121
|
+
'file-lines-warn',
|
|
122
|
+
'check-unbuilt',
|
|
123
|
+
'inert',
|
|
124
|
+
'inconsistent',
|
|
125
|
+
]);
|
|
105
126
|
|
|
106
127
|
export async function syncCommand(positional, flags) {
|
|
107
|
-
const root = flags
|
|
128
|
+
const root = commandRoot(flags, positional);
|
|
108
129
|
const checkOnly = flags.check === true;
|
|
109
130
|
const mode = writeMode(flags);
|
|
110
131
|
|
|
@@ -153,6 +174,10 @@ export async function syncCommand(positional, flags) {
|
|
|
153
174
|
);
|
|
154
175
|
}
|
|
155
176
|
|
|
177
|
+
// A dangling cross-reference does not stop the projection: the region renders, and one
|
|
178
|
+
// unreachable reference is a defect to report, not a reason to withhold the rest.
|
|
179
|
+
for (const p of result.crossRefs ?? []) note(`PROBLEM ${p}`);
|
|
180
|
+
|
|
156
181
|
const broken = result.targets.filter((t) => t.action === 'malformed');
|
|
157
182
|
if (broken.length > 0) {
|
|
158
183
|
const first = broken[0];
|
|
@@ -307,23 +332,13 @@ export async function syncCommand(positional, flags) {
|
|
|
307
332
|
// mkdir; `sync` did not, so the first `sync` after a surface was added failed with
|
|
308
333
|
// ENOENT on the second target and had already written the first. The parent is
|
|
309
334
|
// derived from the target, never from the surface table, so a future surface with a
|
|
310
|
-
// deeper path needs no change here.
|
|
335
|
+
// deeper path needs no change here. The lock is one of these targets (Q29): it
|
|
336
|
+
// reaches this loop like every other file, so a deleted lock is a write the plan
|
|
337
|
+
// shows and the journal records.
|
|
311
338
|
await mkdir(dirname(t.path), { recursive: true });
|
|
312
339
|
await journaledWriteFile(root, journal, t.path, t.text);
|
|
313
340
|
note(`wrote ${t.file}`);
|
|
314
341
|
}
|
|
315
|
-
const lock = await computeLock(root, flags);
|
|
316
|
-
const lockText = `${JSON.stringify(lock, null, 2)}\n`;
|
|
317
|
-
let previousLock = null;
|
|
318
|
-
try {
|
|
319
|
-
previousLock = await readFile(join(root, LOCK_FILE), 'utf8');
|
|
320
|
-
} catch (error) {
|
|
321
|
-
if (error.code !== 'ENOENT') throw error;
|
|
322
|
-
}
|
|
323
|
-
if (previousLock !== lockText) {
|
|
324
|
-
await journaledWriteFile(root, journal, join(root, LOCK_FILE), lockText);
|
|
325
|
-
note(`wrote ${LOCK_FILE}`);
|
|
326
|
-
}
|
|
327
342
|
await commitJournal(root, 'sync', journal);
|
|
328
343
|
// The regions moved, so the last `check` no longer describes this tree.
|
|
329
344
|
// Invalidated rather than refreshed: `sync` does not run gates, checkers
|
|
@@ -396,7 +411,7 @@ export async function syncCommand(positional, flags) {
|
|
|
396
411
|
: `would write ${changed.length + pointersAdded.length + (manifestChanged ? 1 : 0)} file(s); apply with: ${CLI} sync --yes`,
|
|
397
412
|
);
|
|
398
413
|
}
|
|
399
|
-
emit(lines.join('\n')
|
|
414
|
+
emit(lines.join('\n'));
|
|
400
415
|
// After the report: the report says what, the diff shows it (R50).
|
|
401
416
|
emitDiffs(diffs, flags);
|
|
402
417
|
}
|
|
@@ -531,8 +546,9 @@ async function gateCwdFindings(root, manifest) {
|
|
|
531
546
|
// reports 77. The command below renders these facts for terminals; the tool
|
|
532
547
|
// returns them as JSON. The CLI keeps its field names; the tool adopts them.
|
|
533
548
|
export async function checkRepo(root, opts = {}) {
|
|
534
|
-
const
|
|
535
|
-
const
|
|
549
|
+
const ctx = opts.ctx ?? repoContext(root);
|
|
550
|
+
const findings = await checkTargets(root, { ...opts, ctx });
|
|
551
|
+
const rules = await ctx.rules();
|
|
536
552
|
|
|
537
553
|
// A rule file with no id would be projected without a traceable source, so it
|
|
538
554
|
// is a gate failure rather than a warning: `rules why` could not resolve it.
|
|
@@ -555,7 +571,7 @@ export async function checkRepo(root, opts = {}) {
|
|
|
555
571
|
// A declared gate this repo cannot run fails closed on every commit once hooks are
|
|
556
572
|
// installed. Reported here rather than left to `hooks status`, so the repo is red
|
|
557
573
|
// before anyone installs anything rather than after.
|
|
558
|
-
const { manifest, exists } = await
|
|
574
|
+
const { manifest, exists } = await ctx.manifest();
|
|
559
575
|
// The gates slot, counted — not just reported — so the line can print run/total.
|
|
560
576
|
// A cwd finding names a declared gate that will not run, so it counts with the
|
|
561
577
|
// unrunnable; an unknown-gate-cwd names no declared gate, so it does not.
|
|
@@ -590,24 +606,26 @@ export async function checkRepo(root, opts = {}) {
|
|
|
590
606
|
// hand either: the next `init` puts simt-dev's own bytes back and the formatter
|
|
591
607
|
// takes them away again. Reporting it here is what stops that being a surprise
|
|
592
608
|
// discovered at upgrade time.
|
|
593
|
-
const { stack } = await fingerprint(
|
|
594
|
-
const drift = await manifestDrift(root, stack, await
|
|
609
|
+
const { stack } = await ctx.fingerprint();
|
|
610
|
+
const drift = await manifestDrift(root, stack, await ctx.digest());
|
|
595
611
|
if (drift) findings.push(drift);
|
|
596
612
|
}
|
|
597
613
|
if (opts.frozen) findings.push(...(await frozenFindings(root, opts)));
|
|
598
614
|
// The checkers a selected rule declared (ADR-088). They read the rule source,
|
|
599
615
|
// not the folded region, so a line that left the prompt is still the line that runs.
|
|
600
|
-
const fp = await fingerprint(
|
|
616
|
+
const fp = await ctx.fingerprint();
|
|
601
617
|
const { selected } = select(rules.filter((r) => r.error === null), fp.stack ?? [], fp.facts ?? []);
|
|
602
618
|
findings.push(...(await runDeclaredChecks(root, selected, manifest ?? null)));
|
|
603
619
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
620
|
+
// Q55: one resolution shared with `decide --check`, so the two commands
|
|
621
|
+
// cannot disagree about whose records these are or which are historical.
|
|
622
|
+
const relDecisionsDir = await decisionsDir(root);
|
|
623
|
+
const decisionLint = await lintDecisions(join(root, relDecisionsDir), {
|
|
624
|
+
historicalCutoff: await decisionsCutoff(root),
|
|
607
625
|
});
|
|
608
626
|
findings.push(
|
|
609
627
|
...decisionLint.findings.map((f) => ({
|
|
610
|
-
file: [
|
|
628
|
+
file: [relDecisionsDir, f.file].join('/'),
|
|
611
629
|
status: f.status,
|
|
612
630
|
detail: f.detail,
|
|
613
631
|
remedy: f.remedy,
|
|
@@ -637,7 +655,7 @@ export async function checkRepo(root, opts = {}) {
|
|
|
637
655
|
}
|
|
638
656
|
|
|
639
657
|
export async function checkCommand(positional, flags) {
|
|
640
|
-
const root = flags
|
|
658
|
+
const root = commandRoot(flags, positional);
|
|
641
659
|
const { ok, findings } = await checkRepo(root, flags);
|
|
642
660
|
const payload = {
|
|
643
661
|
ok,
|
|
@@ -663,7 +681,6 @@ export async function checkCommand(positional, flags) {
|
|
|
663
681
|
return f.remedy ? [head, `${' '.repeat(11)}remedy: ${f.remedy}`] : [head];
|
|
664
682
|
})
|
|
665
683
|
.join('\n'),
|
|
666
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
667
684
|
);
|
|
668
685
|
return ok ? EXIT.OK : EXIT.FAIL;
|
|
669
686
|
}
|
package/src/commands/tests.mjs
CHANGED
|
@@ -10,11 +10,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { promisify } from 'node:util';
|
|
12
12
|
|
|
13
|
-
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
13
|
+
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
14
14
|
import { CLI } from '../name.mjs';
|
|
15
15
|
import { isTestPath } from '../bench/mine.mjs';
|
|
16
|
-
import { blobTexts, langOf, readEdges, readFiles, reindex } from '../index/store.mjs';
|
|
16
|
+
import { blobTexts, isIndexStale, langOf, readEdges, readFiles, reindex } from '../index/store.mjs';
|
|
17
17
|
import { HISTORY, importsOf, parseCoverage, testEdges, testsFor } from '../index/tests.mjs';
|
|
18
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
18
19
|
|
|
19
20
|
const exec = promisify(execFile);
|
|
20
21
|
|
|
@@ -31,7 +32,7 @@ const COVERAGE_PATHS = [
|
|
|
31
32
|
|
|
32
33
|
export async function testsCommand(args, flags) {
|
|
33
34
|
if (args.length > 1) return fail(`unknown tests argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} tests [path]`);
|
|
34
|
-
const root = flags
|
|
35
|
+
const root = commandRoot(flags);
|
|
35
36
|
const loaded = await loadFiles(root);
|
|
36
37
|
if (loaded.error) return loaded.error;
|
|
37
38
|
const { files } = loaded;
|
|
@@ -58,11 +59,11 @@ export async function testsCommand(args, flags) {
|
|
|
58
59
|
const tests = testsFor(target, edges, { cochange: flags.cochange === true });
|
|
59
60
|
const payload = { ok: true, command: 'tests', root, path: target, tests };
|
|
60
61
|
if (isJson(flags)) {
|
|
61
|
-
emitJson(payload
|
|
62
|
+
emitJson(payload);
|
|
62
63
|
return EXIT.OK;
|
|
63
64
|
}
|
|
64
65
|
const lines = [`tests ${tests.length}`, ...tests];
|
|
65
|
-
emit(lines.join('\n')
|
|
66
|
+
emit(lines.join('\n'));
|
|
66
67
|
return EXIT.OK;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -74,17 +75,16 @@ function summarise(root, flags, edges) {
|
|
|
74
75
|
else if (edge.kind === 'cochange') cochange += 1;
|
|
75
76
|
}
|
|
76
77
|
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 });
|
|
78
|
+
present(payload, () => `test ${test}\ncochange ${cochange}`, flags);
|
|
82
79
|
return EXIT.OK;
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
async function loadFiles(root) {
|
|
86
83
|
let files = await readFiles(root);
|
|
87
|
-
|
|
84
|
+
// An empty index has nothing to answer from; a stale one has yesterday's
|
|
85
|
+
// tree (review 0003 Q42). Either way the refresh is incremental, so a
|
|
86
|
+
// current repository pays for one `git ls-files -s`, not a rebuild.
|
|
87
|
+
if (files.length === 0 || (await isIndexStale(root))) {
|
|
88
88
|
try {
|
|
89
89
|
await reindex(root);
|
|
90
90
|
} catch (error) {
|
package/src/commands/tokens.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export async function tokensCommand(positional, flags) {
|
|
|
73
73
|
`tool output ${plain(session.toolOutput.bytes)} B ~${plain(session.toolOutput.tokens)} tokens (${plain(session.toolOutput.share)}%)`,
|
|
74
74
|
`total ${plain(session.total.bytes)} B ~${plain(session.total.tokens)} tokens`,
|
|
75
75
|
];
|
|
76
|
-
emit(lines.join('\n')
|
|
76
|
+
emit(lines.join('\n'));
|
|
77
77
|
return EXIT.OK;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -193,6 +193,6 @@ export async function tokensCommand(positional, flags) {
|
|
|
193
193
|
`mcp tools/list ${plain(mcp.bytes)} B of ${plain(mcp.tools)} tools (the same for every repository, so it is not in any total)`,
|
|
194
194
|
);
|
|
195
195
|
|
|
196
|
-
emit(lines.join('\n')
|
|
196
|
+
emit(lines.join('\n'));
|
|
197
197
|
return EXIT.OK;
|
|
198
198
|
}
|
package/src/commands/undo.mjs
CHANGED
|
@@ -1,62 +1,66 @@
|
|
|
1
1
|
// `simt undo [--yes] [--json]`
|
|
2
2
|
//
|
|
3
3
|
// Restore the last journaled set of writes (plan 0003 R49): every file the last
|
|
4
|
-
// writing run changed goes back to its pre-run bytes,
|
|
5
|
-
// are deleted
|
|
6
|
-
//
|
|
4
|
+
// writing run changed goes back to its pre-run bytes, files the run created
|
|
5
|
+
// are deleted, and git config keys the run set go back to their pre-run
|
|
6
|
+
// values (review 0003 Q22). A run that wrote nothing leaves no set, so there
|
|
7
|
+
// is nothing to restore and the command says so.
|
|
7
8
|
//
|
|
8
9
|
// The safety is the journal's, not the flag's: when any file in the set no
|
|
9
10
|
// longer matches the bytes the run left, the whole undo is refused — restoring
|
|
10
11
|
// the rest around a hand edit would silently merge two authors' intentions.
|
|
11
12
|
// Like every other writer this command previews without `--yes`.
|
|
12
13
|
|
|
13
|
-
import { EXIT, emit, emitJson, isJson, writeMode } from '../output/index.mjs';
|
|
14
|
+
import { EXIT, emit, emitJson, isJson, writeMode, present } from '../output/index.mjs';
|
|
14
15
|
import { CLI } from '../name.mjs';
|
|
15
|
-
import { listJournalSets, readJournalSet, undoLastJournalSet } from '../journal.mjs';
|
|
16
|
+
import { beforeText, entryLabel, listJournalSets, readGitValue, readJournalSet, undoLastJournalSet } from '../journal.mjs';
|
|
16
17
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
17
18
|
import { join } from 'node:path';
|
|
18
19
|
import { invalidateCheckCache } from '../status.mjs';
|
|
20
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
19
21
|
|
|
20
22
|
export async function undoCommand(positional, flags) {
|
|
21
|
-
const root = flags
|
|
23
|
+
const root = commandRoot(flags, positional);
|
|
22
24
|
const mode = writeMode(flags);
|
|
23
25
|
|
|
24
26
|
const [id] = await listJournalSets(root);
|
|
25
27
|
if (!id) {
|
|
26
|
-
|
|
27
|
-
emitJson({ ok: true, command: 'undo', root, set: null, restored: [], dryRun: !mode.apply });
|
|
28
|
-
return EXIT.OK;
|
|
29
|
-
}
|
|
30
|
-
emit('nothing to undo: no journaled writes', { maxBytes: flags.maxBytes ?? 0 });
|
|
28
|
+
present({ ok: true, command: 'undo', root, set: null, restored: [], dryRun: !mode.apply }, () => 'nothing to undo: no journaled writes', flags);
|
|
31
29
|
return EXIT.OK;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
const set = await readJournalSet(root, id);
|
|
35
|
-
const
|
|
33
|
+
const labels = set.entries.map(entryLabel);
|
|
36
34
|
// The `--diff` pairs: what the tree holds now against what the restore
|
|
37
|
-
// would put back. A null
|
|
38
|
-
// restore deletes it — the after side is empty
|
|
35
|
+
// would put back. A null before is a file the run created (or a key it
|
|
36
|
+
// set from unset), so the restore deletes it — the after side is empty
|
|
37
|
+
// (R50).
|
|
39
38
|
const diffs =
|
|
40
39
|
flags.diff === true
|
|
41
40
|
? await Promise.all(
|
|
42
|
-
set.entries.map(async (e) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
set.entries.map(async (e) => {
|
|
42
|
+
if (e.kind === 'git-config') {
|
|
43
|
+
return { path: entryLabel(e), before: await readGitValue(root, e.key), after: e.before ?? null };
|
|
44
|
+
}
|
|
45
|
+
return { path: e.path, before: await readBefore(join(root, e.path)), after: beforeText(e) };
|
|
46
|
+
}),
|
|
47
47
|
)
|
|
48
48
|
: [];
|
|
49
49
|
|
|
50
50
|
if (!mode.apply) {
|
|
51
51
|
if (isJson(flags)) {
|
|
52
|
-
emitJson({ ok: true, command: 'undo', root, set: id, restores:
|
|
52
|
+
emitJson({ ok: true, command: 'undo', root, set: id, restores: labels, dryRun: true, ...diffField(diffs, flags) });
|
|
53
53
|
return EXIT.OK;
|
|
54
54
|
}
|
|
55
|
+
const files = set.entries.filter((e) => e.kind !== 'git-config').length;
|
|
56
|
+
const keys = set.entries.length - files;
|
|
55
57
|
emit(
|
|
56
|
-
[
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
[
|
|
59
|
+
`last write ${set.command} (${id})`,
|
|
60
|
+
...labels.map((p) => `restore ${p}`),
|
|
61
|
+
'',
|
|
62
|
+
`would restore ${files} file(s)${keys > 0 ? ` and ${keys} git config key(s)` : ''}; apply with: ${CLI} undo --yes`,
|
|
63
|
+
].join('\n'),
|
|
60
64
|
);
|
|
61
65
|
// After the report: the report says what, the diff shows it (R50).
|
|
62
66
|
emitDiffs(diffs, flags);
|
|
@@ -65,11 +69,7 @@ export async function undoCommand(positional, flags) {
|
|
|
65
69
|
|
|
66
70
|
const result = await undoLastJournalSet(root);
|
|
67
71
|
if (result.empty) {
|
|
68
|
-
|
|
69
|
-
emitJson({ ok: true, command: 'undo', root, set: null, restored: [] });
|
|
70
|
-
return EXIT.OK;
|
|
71
|
-
}
|
|
72
|
-
emit('nothing to undo: no journaled writes', { maxBytes: flags.maxBytes ?? 0 });
|
|
72
|
+
present({ ok: true, command: 'undo', root, set: null, restored: [] }, () => 'nothing to undo: no journaled writes', flags);
|
|
73
73
|
return EXIT.OK;
|
|
74
74
|
}
|
|
75
75
|
if (result.refused) {
|
|
@@ -84,7 +84,6 @@ export async function undoCommand(positional, flags) {
|
|
|
84
84
|
'',
|
|
85
85
|
'nothing was restored; re-run the writing command or merge by hand, then undo again',
|
|
86
86
|
].join('\n'),
|
|
87
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
88
87
|
);
|
|
89
88
|
return EXIT.FAIL;
|
|
90
89
|
}
|
|
@@ -98,7 +97,6 @@ export async function undoCommand(positional, flags) {
|
|
|
98
97
|
}
|
|
99
98
|
emit(
|
|
100
99
|
[`undone ${result.set}`, ...result.restored.map((p) => `restored ${p}`)].join('\n'),
|
|
101
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
102
100
|
);
|
|
103
101
|
// After the report: the report says what, the diff shows it (R50).
|
|
104
102
|
emitDiffs(diffs, flags);
|