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
package/src/commands/help.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { emit, fail, jsonFailMode } from '../output/index.mjs';
|
|
1
|
+
import { emit, emitJson, fail, jsonFailMode } from '../output/index.mjs';
|
|
2
|
+
import { SPEC, SPEC_PATHS, tierOf } from './spec.mjs';
|
|
2
3
|
import { parse, rawJsonFlag } from '../argv.mjs';
|
|
3
4
|
import { CLI, MANIFEST_FILE, STATE_DIR } from '../name.mjs';
|
|
4
5
|
import { SESSION_ENV } from '../agenthooks/policy.mjs';
|
|
5
6
|
import { DEFAULT_DIRECTORIES, DEFAULT_TEMPLATE } from '../plan/template.mjs';
|
|
7
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
8
|
+
import { readManifest } from '../projection/manifest.mjs';
|
|
9
|
+
import { assertVersionPin } from '../package-version.mjs';
|
|
6
10
|
|
|
7
11
|
const COMMANDS = `${CLI} init [path] adopt or scaffold a repo into the ecosystem
|
|
8
12
|
(dry-run unless --yes; conflicts block without
|
|
@@ -145,7 +149,7 @@ ${CLI} lessons list deterministic lesson candidates in .simt/local/le
|
|
|
145
149
|
and promoted lessons in docs/lessons/*.md (reads nothing else)
|
|
146
150
|
${CLI} lessons capture scan the ledger and git history for new candidates (the write half of list)
|
|
147
151
|
${CLI} lessons review [<id>] promote a lesson candidate to docs/lessons/NNNN-<slug>.md
|
|
148
|
-
(dry-run unless --yes; writes
|
|
152
|
+
(dry-run unless --yes; writes the lesson frontmatter)
|
|
149
153
|
${CLI} explain <CODE> what a check finding means: the decision behind it, how
|
|
150
154
|
to fix it, and a concrete trigger (DRIFT, UNMANAGED,
|
|
151
155
|
PACK-DIGEST-MISMATCH, ...; case-insensitive)
|
|
@@ -168,7 +172,7 @@ ${CLI} distill <skill|doc> a budgeted proposal summary of a skill body or do
|
|
|
168
172
|
from the configured chat model instead; the cache is
|
|
169
173
|
keyed by source digest and budget, --refresh regenerates)
|
|
170
174
|
${CLI} review [--local] an ADR-008 verdict skeleton for the staged diff,
|
|
171
|
-
the brief listing everything that governs it
|
|
175
|
+
the brief listing everything that governs it
|
|
172
176
|
(deterministic without --local: the brief and its
|
|
173
177
|
governing ids only; --local proposes from the
|
|
174
178
|
configured chat model and writes
|
|
@@ -219,6 +223,16 @@ ${CLI} bench critic precision and recall of the local critic over a lab
|
|
|
219
223
|
overrides --labels; labels are human-written verdicts
|
|
220
224
|
under ADR-008; the critic is advisory until precision
|
|
221
225
|
reaches 0.8 on 30 labelled diffs)
|
|
226
|
+
${CLI} bench export <file> [--yes]
|
|
227
|
+
sealed export of the mined corpus: every task
|
|
228
|
+
descriptor and both hidden patches in one signed
|
|
229
|
+
JSON file, for import on another machine
|
|
230
|
+
(dry-run unless --yes; --root selects the repository)
|
|
231
|
+
${CLI} bench import <file> [--yes]
|
|
232
|
+
import a sealed corpus: verify the seal, then write
|
|
233
|
+
the task descriptors and hidden patches into the
|
|
234
|
+
repository's vault (dry-run unless --yes; --root
|
|
235
|
+
selects the repository)
|
|
222
236
|
${CLI} pack new <name> --source <spec>
|
|
223
237
|
scaffold a versioned pack: pack.json, an unmeasured
|
|
224
238
|
bench.json, and one stack rule (dry-run unless --yes;
|
|
@@ -231,6 +245,7 @@ ${CLI} pack publish [dir] [--yes]
|
|
|
231
245
|
publish an npm-sourced pack to the registry
|
|
232
246
|
(dry-run unless --yes; skips a version already live;
|
|
233
247
|
a git-sourced pack names the tag-and-push instead)
|
|
248
|
+
${CLI} docs readme --yes render the README command list from the spec
|
|
234
249
|
${CLI} mcp the read-only surface as MCP tools over stdio
|
|
235
250
|
(--list prints it without reading stdin; init, sync,
|
|
236
251
|
upgrade and the installers stay CLI-only by design)`;
|
|
@@ -283,6 +298,8 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
283
298
|
--accept-conflicts init: adopt rules that collide with hand-written ones
|
|
284
299
|
--backup init: tag the pre-adoption commit and branch onto
|
|
285
300
|
simt-dev/adopt before writing (the undo path)
|
|
301
|
+
--no-agents init: skip the agent files and the agent-hooks in one flag;
|
|
302
|
+
the standalone agents / agent-hooks install commands remain
|
|
286
303
|
--spec <name> graph lint: also check every skill against a named
|
|
287
304
|
specification (one exists: agentskills)
|
|
288
305
|
--user harness detect: also report the directories in your home
|
|
@@ -302,7 +319,9 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
302
319
|
--harness <id> bench run: adapter to invoke (claude, codex, gemini, opencode,
|
|
303
320
|
cursor, aider, gold, noop; default: gold);
|
|
304
321
|
agent-hook: which dialect to answer in (claude, cursor,
|
|
305
|
-
codex, antigravity)
|
|
322
|
+
codex, antigravity); agent-hooks / agents install: which
|
|
323
|
+
reader to write (one, not all); init: narrows the agent
|
|
324
|
+
files and the agent-hooks to one reader
|
|
306
325
|
--model <id> bench run: model identifier recorded and passed to the adapter
|
|
307
326
|
--reps <n> bench run: repetitions per task and arm (default: 3)
|
|
308
327
|
--regrade <run> bench run: re-grade a stored run from its archived agent patch
|
|
@@ -310,6 +329,8 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
310
329
|
--test <cmd> plan done / slice finish: the test command that closes the row
|
|
311
330
|
--state <name> plan done: target state (default: Complete)
|
|
312
331
|
--type <name> slice finish: the Conventional Commits type (default: feat)
|
|
332
|
+
--id <name> slice start: the slice id when -- starts one from a prompt
|
|
333
|
+
(default: a slug of the prompt text)
|
|
313
334
|
--ttl <seconds> lease: how long a claim lasts (default: 3600)
|
|
314
335
|
--level <name> map: detail level (outline, names, signatures)
|
|
315
336
|
--supersedes <spec> decide: superseded ADR or ruling (ADR-NNN or ADR-NNN:<ruling>[:<kind>]; repeatable)
|
|
@@ -322,8 +343,14 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
322
343
|
--embed index / map / brief: compute or query optional local vector embeddings
|
|
323
344
|
(off by default unless recall@10 gains at least 10 points)
|
|
324
345
|
--labels <path> bench critic: the labelled set of historical diffs to score
|
|
346
|
+
--cross-group bench ablate: compare across harness, model and experiment
|
|
347
|
+
groups instead of refusing the comparison
|
|
348
|
+
--expires <when> lessons capture: how long a captured candidate stays eligible
|
|
325
349
|
(default .simt/critic-labels.jsonl; one human-written
|
|
326
350
|
{sha, verdict} pair per line)
|
|
351
|
+
--replace bench mine: delete task files the current mine does not
|
|
352
|
+
reproduce (off by default: mining is append-only; a
|
|
353
|
+
re-mine adds, it does not replace the corpus)
|
|
327
354
|
--refresh distill / review: regenerate a proposal that is already cached
|
|
328
355
|
--local distill / review: propose the answer from the configured
|
|
329
356
|
local chat model (local.chat); off by default, which
|
|
@@ -338,42 +365,164 @@ const PRINCIPLES = `design rules:
|
|
|
338
365
|
zero dependencies a DX tool that needs an install step fails on a fresh machine
|
|
339
366
|
bounded output one fact per line, fixed field order, explicit TRUNCATED lines`;
|
|
340
367
|
|
|
341
|
-
export async function help(argv) {
|
|
342
|
-
|
|
368
|
+
export async function help(argv, dispatched) {
|
|
369
|
+
// `dispatched` is what `dispatch` already parsed. Called from a test or another
|
|
370
|
+
// command with only argv, help parses it itself.
|
|
371
|
+
const parsed =
|
|
372
|
+
dispatched === undefined
|
|
373
|
+
? parse(argv)
|
|
374
|
+
: dispatched.parseError === undefined
|
|
375
|
+
? { ok: true, value: { flags: dispatched.flags, positional: dispatched.positional } }
|
|
376
|
+
: { ok: false, error: dispatched.parseError };
|
|
343
377
|
if (!parsed.ok) {
|
|
344
378
|
jsonFailMode(rawJsonFlag(argv));
|
|
345
379
|
return fail(parsed.error.message, parsed.error.code);
|
|
346
380
|
}
|
|
347
|
-
const { flags } = parsed.value;
|
|
381
|
+
const { flags, positional } = parsed.value;
|
|
348
382
|
if (flags.version) {
|
|
349
383
|
emit((await version()).trim());
|
|
350
384
|
return 0;
|
|
351
385
|
}
|
|
386
|
+
// A reader who names one command gets that command, not the whole surface (review
|
|
387
|
+
// 0003 Q61): `simt help gate` and `simt gate --help` both printed 23 KB, so the one
|
|
388
|
+
// fact being asked for was 40 screens down.
|
|
389
|
+
const asked = commandFromHelp(positional);
|
|
390
|
+
if (asked !== null) {
|
|
391
|
+
emit(commandPage(asked));
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
if (flags.json === true) {
|
|
395
|
+
emitJson(SPEC.map(specRow));
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
// The standard page is where a reader asks "what can this do, and where do I start".
|
|
399
|
+
// A repository that has not been adopted, or that pins a version this binary is not,
|
|
400
|
+
// has a prerequisite worth stating before the catalogue: no manifest means init is the
|
|
401
|
+
// first move, a stale pin means sync is the fix. Fully ready, the page stands on its
|
|
402
|
+
// own and the banner is nothing.
|
|
403
|
+
const banner = await prerequisiteBanner(flags, positional);
|
|
404
|
+
const bannerLines = banner === null ? [] : ['', banner, ''];
|
|
405
|
+
// `--all` prints every command and every flag; the default page lists the core tier and
|
|
406
|
+
// says where the rest is. A reader asking "what can this do" needs the shape, not 23 KB
|
|
407
|
+
// of flag reference (review 0003 Q61).
|
|
408
|
+
if (flags.all === true) {
|
|
409
|
+
emit(
|
|
410
|
+
[
|
|
411
|
+
'simt-dev - shared agent rules, skills and gates for a repo ecosystem',
|
|
412
|
+
'',
|
|
413
|
+
`usage: ${CLI} <command> [args] [flags]`,
|
|
414
|
+
...bannerLines,
|
|
415
|
+
'commands:',
|
|
416
|
+
COMMANDS,
|
|
417
|
+
'',
|
|
418
|
+
'flags:',
|
|
419
|
+
FLAGS,
|
|
420
|
+
'',
|
|
421
|
+
PRINCIPLES,
|
|
422
|
+
'',
|
|
423
|
+
`rulings ${CLI} rules list the decisions behind the rules you just read`,
|
|
424
|
+
].join('\n'),
|
|
425
|
+
);
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
// One line per group and one per bare command: listing every subcommand with its
|
|
429
|
+
// summary does not fit in the 2 KB a reader will actually read (review 0003 Q61).
|
|
430
|
+
const groups = new Map();
|
|
431
|
+
const bare = [];
|
|
432
|
+
for (const row of SPEC) {
|
|
433
|
+
if (tierOf(row.path) !== 'core') continue;
|
|
434
|
+
const [head, sub] = row.path.split(' ');
|
|
435
|
+
if (sub === undefined) bare.push(row);
|
|
436
|
+
else groups.set(head, [...(groups.get(head) ?? []), sub]);
|
|
437
|
+
}
|
|
352
438
|
emit(
|
|
353
439
|
[
|
|
354
440
|
'simt-dev - shared agent rules, skills and gates for a repo ecosystem',
|
|
355
441
|
'',
|
|
356
442
|
`usage: ${CLI} <command> [args] [flags]`,
|
|
357
|
-
|
|
443
|
+
...bannerLines,
|
|
358
444
|
'commands:',
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
'flags:',
|
|
362
|
-
FLAGS,
|
|
363
|
-
'',
|
|
364
|
-
PRINCIPLES,
|
|
445
|
+
...[...groups].map(([group, subs]) => ` ${CLI} ${group} <${subs.join('|')}>`),
|
|
446
|
+
...bare.map((row) => ` ${CLI} ${row.path.padEnd(8)} ${row.summary.length > 46 ? `${row.summary.slice(0, 43)}...` : row.summary}`),
|
|
365
447
|
'',
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
// are an implementation record; the rulings are the part a reader can act on.
|
|
369
|
-
`rulings ${CLI} rules list the decisions behind the rules you just read`,
|
|
448
|
+
`more ${CLI} help --all (every command and flag), ${CLI} help <command>`,
|
|
449
|
+
`rulings ${CLI} rules list (the decisions behind the rules)`,
|
|
370
450
|
].join('\n'),
|
|
371
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
372
451
|
);
|
|
373
452
|
return 0;
|
|
374
453
|
}
|
|
375
454
|
|
|
455
|
+
/**
|
|
456
|
+
* The prerequisite line for the standard help page, or null when the repository is fully
|
|
457
|
+
* ready. No manifest (or one that cannot be read) means the adoption has not happened and
|
|
458
|
+
* `init` is the first move; a manifest that pins a version the running binary is not means
|
|
459
|
+
* `sync` is the fix. These are the two states in which a reader opening help is about to
|
|
460
|
+
* run a command that would fail in a predictable way, so the page says so before it lists
|
|
461
|
+
* the commands.
|
|
462
|
+
*/
|
|
463
|
+
async function prerequisiteBanner(flags, positional) {
|
|
464
|
+
const root = commandRoot(flags, positional);
|
|
465
|
+
const { manifest, exists, error } = await readManifest(root);
|
|
466
|
+
if (!exists) {
|
|
467
|
+
return `prerequisite no ${MANIFEST_FILE} here; run: ${CLI} init <path>`;
|
|
468
|
+
}
|
|
469
|
+
if (error !== null) {
|
|
470
|
+
return `prerequisite ${MANIFEST_FILE} cannot be read (${error}); run: ${CLI} init <path>`;
|
|
471
|
+
}
|
|
472
|
+
const stale = assertVersionPin(manifest);
|
|
473
|
+
if (stale !== null) {
|
|
474
|
+
return `prerequisite ${stale}; run: ${CLI} sync`;
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
376
479
|
async function version() {
|
|
377
480
|
const { describeVersion } = await import('../package-version.mjs');
|
|
378
481
|
return `${describeVersion()}\n`;
|
|
379
482
|
}
|
|
483
|
+
|
|
484
|
+
/** The command a help request names, or null when it names none. */
|
|
485
|
+
function commandFromHelp(positional) {
|
|
486
|
+
const words = positional[0] === 'help' ? positional.slice(1) : positional;
|
|
487
|
+
const two = words.slice(0, 2).join(' ');
|
|
488
|
+
if (SPEC_PATHS.includes(two)) return two;
|
|
489
|
+
return SPEC_PATHS.includes(words[0]) ? words[0] : null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** One spec row as the JSON a UI or a harness reads. */
|
|
493
|
+
function specRow(row) {
|
|
494
|
+
return { path: row.path, summary: row.summary, ...(row.defaultFor === undefined ? {} : { defaultFor: row.defaultFor }) };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const PAGE_WIDTH = 78;
|
|
498
|
+
|
|
499
|
+
/** The summary broken to the page width, so a narrow terminal keeps it readable. */
|
|
500
|
+
function wrapped(text) {
|
|
501
|
+
const lines = [];
|
|
502
|
+
let line = '';
|
|
503
|
+
for (const word of text.split(' ')) {
|
|
504
|
+
if (line !== '' && line.length + 1 + word.length > PAGE_WIDTH) {
|
|
505
|
+
lines.push(line);
|
|
506
|
+
line = word;
|
|
507
|
+
} else {
|
|
508
|
+
line = line === '' ? word : `${line} ${word}`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (line !== '') lines.push(line);
|
|
512
|
+
return lines;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* One command's page: what it is, and where the rest of the answers are. The flags a
|
|
517
|
+
* single command declares arrive with the spec's `flags` field (Q62); until then the
|
|
518
|
+
* page points at the global list rather than inventing one per command.
|
|
519
|
+
*/
|
|
520
|
+
export function commandPage(path) {
|
|
521
|
+
const row = SPEC.find((r) => r.path === path);
|
|
522
|
+
if (row === undefined) return '';
|
|
523
|
+
const lines = [`${CLI} ${path} [args] [flags]`, '', ...wrapped(row.summary), ''];
|
|
524
|
+
if (row.defaultFor !== undefined) lines.push(`default ${CLI} ${row.defaultFor} runs this command`);
|
|
525
|
+
lines.push(`flags --json --pretty --root --max-bytes, and every global flag`);
|
|
526
|
+
lines.push(`all ${CLI} --help every command, and every flag it takes`);
|
|
527
|
+
return `${lines.join('\n')}\n`;
|
|
528
|
+
}
|
package/src/commands/hooks.mjs
CHANGED
|
@@ -12,27 +12,33 @@
|
|
|
12
12
|
// DEFAULT location, which core.hooksPath being unset does not reveal, so a
|
|
13
13
|
// foreign hook is found by asking git rather than by reading config.
|
|
14
14
|
|
|
15
|
-
import { mkdir, readFile, realpath } from 'node:fs/promises';
|
|
16
|
-
import {
|
|
15
|
+
import { mkdir, readFile, readdir, realpath, rm, stat } from 'node:fs/promises';
|
|
16
|
+
import { repoContext } from '../context.mjs';
|
|
17
|
+
import { commitJournal, journaledGitConfig, journaledRemoveFile } from '../journal.mjs';
|
|
18
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
17
19
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
18
20
|
import { invalidateCheckCache } from '../status.mjs';
|
|
19
21
|
import { isAbsolute, join, relative } from 'node:path';
|
|
20
22
|
|
|
21
23
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
22
24
|
import { MANIFEST, readManifest } from '../projection/manifest.mjs';
|
|
23
|
-
import { EVENTS, HOOKS_DIR, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderEvidenceShim, renderShim } from '../hooks/shim.mjs';
|
|
25
|
+
import { CHAIN_MARKER, EVENTS, HOOKS_DIR, HOOKS_DIR_LEGACY, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderEvidenceShim, renderShim } from '../hooks/shim.mjs';
|
|
24
26
|
import { GATE_KEY, gateCount, readHooksPath, readHooksState, wantsShim } from '../compare/hooksstate.mjs';
|
|
25
|
-
import {
|
|
26
|
-
import { knownGates } from '../hooks/resolve.mjs';
|
|
27
|
+
import { gateNames, getGate } from '../hooks/gates.mjs';
|
|
27
28
|
import { renderGateReport, runGates } from '../hooks/dispatcher.mjs';
|
|
28
29
|
import { CLI } from '../name.mjs';
|
|
29
30
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
30
31
|
import { packageVersion, hasNpm } from '../package-version.mjs';
|
|
32
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
33
|
+
import { readIfExists } from '../util/fs.mjs';
|
|
31
34
|
|
|
32
35
|
export async function hooksCommand(sub, args, flags) {
|
|
33
|
-
const root = flags
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const root = commandRoot(flags, args);
|
|
37
|
+
// One context for the subcommand: install and status each ask about the same
|
|
38
|
+
// repository, and `doctor --fix` asks alongside them (review 0003 Q67).
|
|
39
|
+
const ctx = repoContext(root);
|
|
40
|
+
if (sub === 'install') return installHooks(root, flags, ctx);
|
|
41
|
+
if (sub === 'status') return hooksStatus(root, flags, ctx);
|
|
36
42
|
if (sub === 'run') return runHook(args, flags);
|
|
37
43
|
return fail(`unknown hooks subcommand ${sub}`, EXIT.USAGE, `run: ${CLI} hooks status`);
|
|
38
44
|
}
|
|
@@ -40,11 +46,12 @@ export async function hooksCommand(sub, args, flags) {
|
|
|
40
46
|
/** The shim execs this; the event name is baked into the shim, so it is arg 0. */
|
|
41
47
|
async function runHook(args, flags) {
|
|
42
48
|
const event = args[0];
|
|
43
|
-
const root = flags
|
|
49
|
+
const root = commandRoot(flags);
|
|
50
|
+
const ctx = repoContext(root);
|
|
44
51
|
if (!EVENTS.includes(event)) {
|
|
45
52
|
return fail(`unknown hook event ${event}`, EXIT.USAGE, `known events: ${EVENTS.join(', ')}`);
|
|
46
53
|
}
|
|
47
|
-
const { manifest, exists, error } = await
|
|
54
|
+
const { manifest, exists, error } = await ctx.manifest();
|
|
48
55
|
if (error) return fail(`${MANIFEST_FILE}: ${error}`, EXIT.CONFIG);
|
|
49
56
|
if (!exists) {
|
|
50
57
|
return fail(`${root} has no ${MANIFEST_FILE}; the hook cannot know its gate list`, EXIT.CONFIG, `run: ${CLI} init --root ${root}`);
|
|
@@ -64,7 +71,7 @@ async function runHook(args, flags) {
|
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
for (const n of result.notes ?? []) note(`note ${n}`);
|
|
67
|
-
emit(renderGateReport(event, manifest.gates[GATE_KEY[event]] ?? [], result).join('\n')
|
|
74
|
+
emit(renderGateReport(event, manifest.gates[GATE_KEY[event]] ?? [], result).join('\n'));
|
|
68
75
|
return result.failed ? EXIT.FAIL : EXIT.OK;
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -135,7 +142,7 @@ async function relativeTarget(root, target) {
|
|
|
135
142
|
* caller enforces in its own voice (`takeover`, and the `wanted`/`obsolete`
|
|
136
143
|
* emptiness the command refuses on)
|
|
137
144
|
*/
|
|
138
|
-
export async function planHooksInstall(root, { manifest, exists }) {
|
|
145
|
+
export async function planHooksInstall(root, { manifest, exists }, ctx = null) {
|
|
139
146
|
const declaresGate = (event) => wantsShim(manifest, exists, event);
|
|
140
147
|
const state = await readHooksState(root, { declaresGate });
|
|
141
148
|
const current = state.current;
|
|
@@ -159,8 +166,10 @@ export async function planHooksInstall(root, { manifest, exists }) {
|
|
|
159
166
|
} catch {
|
|
160
167
|
// Unreadable here leaves the npm default; the manifest path reports it.
|
|
161
168
|
}
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
// The dynamic import stays: a hook run must not pull the fingerprint module in
|
|
170
|
+
// just by loading this one. A caller that already holds a context passes it and
|
|
171
|
+
// the walk is shared (review 0003 Q67).
|
|
172
|
+
const detected = ctx ? await ctx.fingerprint() : await (await import('../resolve/fingerprint.mjs')).fingerprint(root);
|
|
164
173
|
manager = resolveNodeRunner({ facts: detected.facts, packageManager }).id;
|
|
165
174
|
}
|
|
166
175
|
|
|
@@ -190,8 +199,8 @@ export async function planHooksInstall(root, { manifest, exists }) {
|
|
|
190
199
|
//
|
|
191
200
|
// `foreignHooks` lists every executable file in the directory git reads hooks from -
|
|
192
201
|
// and when that directory is this tool's own, a file a person dropped in there is
|
|
193
|
-
// listed too. Chaining it means writing a shim at `.simt
|
|
194
|
-
// `.simt
|
|
202
|
+
// listed too. Chaining it means writing a shim at `.simt/hooks/<name>` whose TARGET is
|
|
203
|
+
// `.simt/hooks/<name>`: the same path. The shim overwrites the hook it was written to
|
|
195
204
|
// preserve and then runs itself, so the hook stops working and git recurses in a loop
|
|
196
205
|
// nothing reports. Confirmed at the previous release, which printed
|
|
197
206
|
// `UPDATED .simt-hooks/mine (chained: runs .simt-hooks/mine)`.
|
|
@@ -298,9 +307,50 @@ export async function planHooksInstall(root, { manifest, exists }) {
|
|
|
298
307
|
evidenceExisting,
|
|
299
308
|
routed,
|
|
300
309
|
takeover,
|
|
310
|
+
// The legacy directory's removal rides on the same planned write, so the
|
|
311
|
+
// scan is here and the deletion is in `applyHooksInstall` (ADR-122).
|
|
312
|
+
legacy: await legacyHooksScan(root),
|
|
301
313
|
};
|
|
302
314
|
}
|
|
303
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Scan the legacy top-level shim directory (ADR-122). A repository installed
|
|
318
|
+
* before the move into the state directory still carries it, and it is
|
|
319
|
+
* removable only when every entry in it is a regular file that carries a
|
|
320
|
+
* simt-dev marker: the install re-routes git to the new directory, and a
|
|
321
|
+
* file this tool cannot recognise as its own is never deleted on its way.
|
|
322
|
+
*
|
|
323
|
+
* @returns {Promise<{path: string, removable: boolean}|null>} null when the
|
|
324
|
+
* directory is absent or empty; otherwise the directory and whether the
|
|
325
|
+
* install may take it down. `applyHooksInstall` acts on this, so the
|
|
326
|
+
* judgment is made once, in the plan. Module-local: one caller needs no
|
|
327
|
+
* export, and an export is a promise to a caller (deadcode).
|
|
328
|
+
*/
|
|
329
|
+
async function legacyHooksScan(root) {
|
|
330
|
+
const dir = join(root, HOOKS_DIR_LEGACY);
|
|
331
|
+
let entries;
|
|
332
|
+
try {
|
|
333
|
+
entries = await readdir(dir);
|
|
334
|
+
} catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (entries.length === 0) return null;
|
|
338
|
+
let removable = true;
|
|
339
|
+
for (const name of entries) {
|
|
340
|
+
const st = await stat(join(dir, name));
|
|
341
|
+
if (!st.isFile()) {
|
|
342
|
+
removable = false;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
const text = await readFile(join(dir, name), 'utf8');
|
|
346
|
+
if (!text.includes(SHIM_MARKER) && !text.includes(CHAIN_MARKER)) {
|
|
347
|
+
removable = false;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { path: HOOKS_DIR_LEGACY, removable };
|
|
352
|
+
}
|
|
353
|
+
|
|
304
354
|
/**
|
|
305
355
|
* Write a planned install: shims, chains, removals, and the git routing (plan 0003 R47).
|
|
306
356
|
*
|
|
@@ -309,10 +359,11 @@ export async function planHooksInstall(root, { manifest, exists }) {
|
|
|
309
359
|
* names the run that actually changed something. Reporting stays with the
|
|
310
360
|
* caller for the same reason it does in `planHooksInstall`.
|
|
311
361
|
*
|
|
312
|
-
* @returns {{wrote: string[]}} the shim paths written,
|
|
362
|
+
* @returns {{wrote: string[], removed: string[]}} the shim paths written,
|
|
363
|
+
* removed or refreshed, and the legacy-directory files taken down
|
|
313
364
|
*/
|
|
314
365
|
export async function applyHooksInstall(root, planned, journal) {
|
|
315
|
-
const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current } =
|
|
366
|
+
const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current, legacy } =
|
|
316
367
|
planned;
|
|
317
368
|
// Shim and evidence bytes are journaled so `undo` restores them (R49), with
|
|
318
369
|
// the executable bit they are written with: a shim the run chmodded is one
|
|
@@ -322,27 +373,33 @@ export async function applyHooksInstall(root, planned, journal) {
|
|
|
322
373
|
// at their directory silently stops the repository's own hooks (review 0003
|
|
323
374
|
// Q22).
|
|
324
375
|
const wrote = [];
|
|
376
|
+
const removed = [];
|
|
325
377
|
await mkdir(join(root, HOOKS_DIR), { recursive: true });
|
|
378
|
+
const hookChanges = [];
|
|
326
379
|
for (const event of wanted) {
|
|
327
380
|
const path = join(root, fileFor(event));
|
|
328
|
-
|
|
381
|
+
hookChanges.push(change({ path, before: await readBefore(path), after: renderShim(event, { npm, manager, version }), owner: 'hooks install', reason: event, mode: 0o755 }));
|
|
329
382
|
wrote.push(fileFor(event));
|
|
330
383
|
}
|
|
331
384
|
for (const c of chained) {
|
|
332
385
|
const path = join(root, fileFor(c.event));
|
|
333
|
-
|
|
386
|
+
hookChanges.push(change({ path, before: await readBefore(path), after: renderChainedShim(c.event, c.target), owner: 'hooks install', reason: c.event, mode: 0o755 }));
|
|
334
387
|
wrote.push(fileFor(c.event));
|
|
335
388
|
}
|
|
336
389
|
for (const h of obsolete) {
|
|
337
|
-
|
|
390
|
+
const obsoletePath = join(root, fileFor(h.event));
|
|
391
|
+
hookChanges.push(
|
|
392
|
+
change({ path: obsoletePath, kind: 'remove', before: await readBefore(obsoletePath), after: null, owner: 'hooks install', reason: h.event }),
|
|
393
|
+
);
|
|
338
394
|
wrote.push(fileFor(h.event));
|
|
339
395
|
}
|
|
340
396
|
// After the chain loop, so a chained commit-msg cannot replace the claim check.
|
|
341
397
|
if (evidenceSource) {
|
|
342
398
|
const path = join(root, evidenceFile);
|
|
343
|
-
await
|
|
399
|
+
hookChanges.push(change({ path, before: await readBefore(path), after: evidenceSource, owner: 'hooks install', reason: 'the evidence shim', mode: 0o755 }));
|
|
344
400
|
wrote.push(evidenceFile);
|
|
345
401
|
}
|
|
402
|
+
await applyPlan(root, hookChanges, { mode: { apply: true }, journal, commit: false });
|
|
346
403
|
// `core.hooksPath` tells git to read ONE directory for EVERY event, so leaving it
|
|
347
404
|
// pointing at a directory of no shims stops the repository's own hooks - the ones in
|
|
348
405
|
// `.git/hooks` that predate this tool - from running, silently. Unsetting it is the
|
|
@@ -352,7 +409,20 @@ export async function applyHooksInstall(root, planned, journal) {
|
|
|
352
409
|
} else if (current === wants) {
|
|
353
410
|
await journaledGitConfig(root, journal, HOOKS_PATH_KEY, null);
|
|
354
411
|
}
|
|
355
|
-
|
|
412
|
+
// The move into the state directory (ADR-122): the routing now points
|
|
413
|
+
// elsewhere, so a legacy directory left behind is the only place that looks
|
|
414
|
+
// like a hooks directory while holding nothing git reads. The scan in the
|
|
415
|
+
// plan already judged it removable — every file a simt-dev marker — so
|
|
416
|
+
// deletion is journaled per file, and `undo` restores the directory.
|
|
417
|
+
if (legacy?.removable) {
|
|
418
|
+
for (const name of await readdir(join(root, HOOKS_DIR_LEGACY))) {
|
|
419
|
+
await journaledRemoveFile(root, journal, join(root, HOOKS_DIR_LEGACY, name));
|
|
420
|
+
removed.push(`${HOOKS_DIR_LEGACY}/${name}`);
|
|
421
|
+
}
|
|
422
|
+
await rm(join(root, HOOKS_DIR_LEGACY), { recursive: true });
|
|
423
|
+
}
|
|
424
|
+
return { wrote, removed };
|
|
425
|
+
|
|
356
426
|
}
|
|
357
427
|
|
|
358
428
|
/**
|
|
@@ -382,8 +452,8 @@ export async function plannedDiffs(root, planned) {
|
|
|
382
452
|
return pairs;
|
|
383
453
|
}
|
|
384
454
|
|
|
385
|
-
async function installHooks(root, flags) {
|
|
386
|
-
const { manifest, exists, error } = await
|
|
455
|
+
async function installHooks(root, flags, ctx = repoContext(root)) {
|
|
456
|
+
const { manifest, exists, error } = await ctx.manifest();
|
|
387
457
|
if (error) return fail(`${MANIFEST_FILE}: ${error}`, EXIT.CONFIG);
|
|
388
458
|
if (!exists) {
|
|
389
459
|
return fail(`${root} has no ${MANIFEST_FILE}; gates must be declared before hooks can run them`, EXIT.CONFIG, `run: ${CLI} init --root ${root}`);
|
|
@@ -409,7 +479,8 @@ async function installHooks(root, flags) {
|
|
|
409
479
|
evidenceExisting,
|
|
410
480
|
routed,
|
|
411
481
|
takeover,
|
|
412
|
-
|
|
482
|
+
legacy,
|
|
483
|
+
} = await planHooksInstall(root, { manifest, exists }, ctx);
|
|
413
484
|
|
|
414
485
|
// Read through `writeMode` like every other writer, and only now: this command
|
|
415
486
|
// used to consult `flags.yes` and `flags.dryRun` directly and write when neither
|
|
@@ -438,7 +509,6 @@ async function installHooks(root, flags) {
|
|
|
438
509
|
' and replace it with a gate that checks nothing.',
|
|
439
510
|
`REMEDY add the gates this repo can actually run to ${MANIFEST_FILE}, then re-run.`,
|
|
440
511
|
].join('\n'),
|
|
441
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
442
512
|
);
|
|
443
513
|
return EXIT.FAIL;
|
|
444
514
|
}
|
|
@@ -487,11 +557,21 @@ async function installHooks(root, flags) {
|
|
|
487
557
|
note: 'commit-msg checks a verification claim against the ledger',
|
|
488
558
|
});
|
|
489
559
|
}
|
|
560
|
+
if (legacy?.removable) {
|
|
561
|
+
plan.push({
|
|
562
|
+
file: HOOKS_DIR_LEGACY,
|
|
563
|
+
action: 'removed',
|
|
564
|
+
// Named in the plan for the same reason the obsolete shims are: the
|
|
565
|
+
// reader approving this write is about to lose a directory, and the
|
|
566
|
+
// routing line above does not say the old one goes too (ADR-122).
|
|
567
|
+
note: `legacy shim directory; the shims now live in ${HOOKS_DIR}`,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
490
570
|
|
|
491
571
|
// The routing this install leaves behind, which is not always `wants`. A repository
|
|
492
572
|
// that removed its last gate has no shims to route to, and leaving `core.hooksPath`
|
|
493
573
|
// pointed at the directory would stop its own hooks running - the plan has to say so,
|
|
494
|
-
// because a plan that prints "core.hooksPath updated .simt
|
|
574
|
+
// because a plan that prints "core.hooksPath updated .simt/hooks -> .simt/hooks" while
|
|
495
575
|
// the command unsets it is worse than no plan.
|
|
496
576
|
const leaves = routed ? wants : null;
|
|
497
577
|
const pathAction =
|
|
@@ -521,7 +601,6 @@ async function installHooks(root, flags) {
|
|
|
521
601
|
' Taking over replaces that routing. Inspect the existing hooks first,',
|
|
522
602
|
' then re-run with --yes to accept, or set core.hooksPath yourself.',
|
|
523
603
|
].join('\n'),
|
|
524
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
525
604
|
);
|
|
526
605
|
return EXIT.FAIL;
|
|
527
606
|
}
|
|
@@ -566,7 +645,6 @@ async function installHooks(root, flags) {
|
|
|
566
645
|
: `PLAN nothing written; apply with: ${CLI} hooks install --root ${root} --yes`,
|
|
567
646
|
manifest,
|
|
568
647
|
),
|
|
569
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
570
648
|
);
|
|
571
649
|
// After the report: the report says what, the diff shows it (R50).
|
|
572
650
|
emitDiffs(diffs, flags);
|
|
@@ -578,7 +656,7 @@ async function installHooks(root, flags) {
|
|
|
578
656
|
const journal = [];
|
|
579
657
|
await applyHooksInstall(
|
|
580
658
|
root,
|
|
581
|
-
{ wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current },
|
|
659
|
+
{ wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current, legacy },
|
|
582
660
|
journal,
|
|
583
661
|
);
|
|
584
662
|
await commitJournal(root, 'hooks install', journal);
|
|
@@ -655,7 +733,6 @@ async function installHooks(root, flags) {
|
|
|
655
733
|
routed ? 'WROTE shims and set core.hooksPath' : 'WROTE no shims remain; core.hooksPath unset',
|
|
656
734
|
manifest,
|
|
657
735
|
),
|
|
658
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
659
736
|
);
|
|
660
737
|
// After the report: the report says what, the diff shows it (R50).
|
|
661
738
|
emitDiffs(diffs, flags);
|
|
@@ -679,13 +756,13 @@ function describeGates(manifest, key) {
|
|
|
679
756
|
const list = manifest?.gates?.[key];
|
|
680
757
|
if (!Array.isArray(list) || list.length === 0) return '(none declared)';
|
|
681
758
|
return list.map((name) => {
|
|
682
|
-
const gate =
|
|
759
|
+
const gate = getGate(name, manifest?.gates?.commands);
|
|
683
760
|
return gate ? name : `${name} (UNKNOWN - the hook will fail)`;
|
|
684
761
|
}).join(', ');
|
|
685
762
|
}
|
|
686
763
|
|
|
687
|
-
async function hooksStatus(root, flags) {
|
|
688
|
-
const { manifest, exists, error } = await
|
|
764
|
+
async function hooksStatus(root, flags, ctx = repoContext(root)) {
|
|
765
|
+
const { manifest, exists, error } = await ctx.manifest();
|
|
689
766
|
if (error) return fail(`${MANIFEST_FILE}: ${error}`, EXIT.CONFIG);
|
|
690
767
|
|
|
691
768
|
const state = await readHooksState(root, { declaresGate: (e) => wantsShim(manifest, exists, e) });
|
|
@@ -748,7 +825,7 @@ async function hooksStatus(root, flags) {
|
|
|
748
825
|
lines.push('', 'would run:');
|
|
749
826
|
for (const key of ['preCommit', 'prePush']) {
|
|
750
827
|
for (const name of manifest?.gates?.[key] ?? []) {
|
|
751
|
-
const known =
|
|
828
|
+
const known = getGate(name, commands);
|
|
752
829
|
// A declared command wins for every kind, so the status must say so too —
|
|
753
830
|
// printing "run script" for a gate the dispatcher will shell out to `./gradlew`
|
|
754
831
|
// for is the same one-question-two-answers defect ADR-018 was written about.
|
|
@@ -767,17 +844,17 @@ async function hooksStatus(root, flags) {
|
|
|
767
844
|
if (undeclared.length > 0) {
|
|
768
845
|
lines.push('', `declared but never run: ${undeclared.join(', ')}`);
|
|
769
846
|
}
|
|
770
|
-
const unknown = [...(manifest?.gates?.preCommit ?? []), ...(manifest?.gates?.prePush ?? [])].filter((n) => !
|
|
847
|
+
const unknown = [...(manifest?.gates?.preCommit ?? []), ...(manifest?.gates?.prePush ?? [])].filter((n) => !getGate(n, commands));
|
|
771
848
|
if (unknown.length > 0) {
|
|
772
849
|
lines.push('', `unknown gates (the hook will fail): ${unknown.join(', ')}`);
|
|
773
|
-
lines.push(`known gates: ${
|
|
850
|
+
lines.push(`known gates: ${gateNames().join(', ')}`);
|
|
774
851
|
}
|
|
775
852
|
}
|
|
776
853
|
if (problems.length > 0) {
|
|
777
854
|
lines.push('', 'problems:');
|
|
778
855
|
for (const p of problems) lines.push(` ${p}`);
|
|
779
856
|
}
|
|
780
|
-
emit(lines.join('\n')
|
|
857
|
+
emit(lines.join('\n'));
|
|
781
858
|
return code;
|
|
782
859
|
}
|
|
783
860
|
|
|
@@ -799,8 +876,9 @@ function collectProblems({ exists, current, installed, manifest, foreign = [] })
|
|
|
799
876
|
if (exists) {
|
|
800
877
|
for (const key of ['preCommit', 'prePush']) {
|
|
801
878
|
for (const name of manifest?.gates?.[key] ?? []) {
|
|
802
|
-
|
|
803
|
-
|
|
879
|
+
const gate = getGate(name, manifest?.gates?.commands);
|
|
880
|
+
if (!gate) problems.push(`gate "${name}" in ${key} is not a known gate`);
|
|
881
|
+
else if (gate.kind === 'command' && !manifest?.gates?.commands?.[name]) {
|
|
804
882
|
problems.push(`gate "${name}" in ${key} is a command gate with no declared command; the hook will fail`);
|
|
805
883
|
}
|
|
806
884
|
}
|
|
@@ -808,11 +886,3 @@ function collectProblems({ exists, current, installed, manifest, foreign = [] })
|
|
|
808
886
|
}
|
|
809
887
|
return problems;
|
|
810
888
|
}
|
|
811
|
-
|
|
812
|
-
async function readIfExists(path) {
|
|
813
|
-
try {
|
|
814
|
-
return await readFile(path, 'utf8');
|
|
815
|
-
} catch {
|
|
816
|
-
return null;
|
|
817
|
-
}
|
|
818
|
-
}
|