plumbbob 0.9.3 → 0.10.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/.claude-plugin/plugin.json +1 -1
- package/README.md +92 -89
- package/dist/cli-core.js +25 -8
- package/dist/cli.js +1 -1
- package/dist/lib/agents.js +26 -26
- package/dist/lib/buildlog.js +11 -11
- package/dist/lib/buildlogsync.js +8 -8
- package/dist/lib/check.js +55 -22
- package/dist/lib/commitbody.js +72 -0
- package/dist/lib/commitmsg.js +7 -7
- package/dist/lib/git.js +44 -15
- package/dist/lib/intent.js +21 -20
- package/dist/lib/latch.js +19 -19
- package/dist/lib/orient.js +36 -17
- package/dist/lib/reentry.js +70 -0
- package/dist/lib/settings.js +16 -16
- package/dist/lib/sidecar.js +54 -54
- package/dist/lib/templates.js +1 -1
- package/dist/verbs/agent.js +27 -27
- package/dist/verbs/build.js +9 -9
- package/dist/verbs/check.js +6 -6
- package/dist/verbs/checkpoint.js +48 -47
- package/dist/verbs/doctor.js +31 -30
- package/dist/verbs/finish.js +28 -41
- package/dist/verbs/handoff.js +9 -8
- package/dist/verbs/init.js +9 -9
- package/dist/verbs/park.js +3 -3
- package/dist/verbs/recover.js +297 -0
- package/dist/verbs/revert.js +13 -13
- package/dist/verbs/spike.js +25 -19
- package/dist/verbs/start.js +49 -19
- package/dist/verbs/status.js +8 -8
- package/dist/verbs/turn.js +16 -16
- package/dist/verbs/use.js +4 -4
- package/hooks/post-edit.sh +4 -4
- package/hooks/pre-bash-commit.sh +3 -3
- package/node_modules/checkride/.claude-plugin/plugin.json +11 -0
- package/node_modules/checkride/CHANGELOG.md +1404 -0
- package/node_modules/checkride/LICENSE +201 -21
- package/node_modules/checkride/README.md +197 -38
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts +15 -0
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts.map +1 -0
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js +17 -0
- package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js.map +1 -0
- package/node_modules/checkride/dist/adapters.d.ts +30 -10
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +62 -23
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/claude.d.ts +88 -0
- package/node_modules/checkride/dist/agent-setup/claude.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/claude.js +305 -0
- package/node_modules/checkride/dist/agent-setup/claude.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/command.d.ts +43 -0
- package/node_modules/checkride/dist/agent-setup/command.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/command.js +77 -0
- package/node_modules/checkride/dist/agent-setup/command.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts +69 -0
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/cursor.js +247 -0
- package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/files.d.ts +50 -0
- package/node_modules/checkride/dist/agent-setup/files.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/files.js +80 -0
- package/node_modules/checkride/dist/agent-setup/files.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/harness.d.ts +101 -0
- package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/harness.js +93 -0
- package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/hook.d.ts +45 -52
- package/node_modules/checkride/dist/agent-setup/hook.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/hook.js +64 -95
- package/node_modules/checkride/dist/agent-setup/hook.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/index.d.ts +10 -4
- package/node_modules/checkride/dist/agent-setup/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/index.js +9 -4
- package/node_modules/checkride/dist/agent-setup/index.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts +81 -0
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/scripts.js +604 -0
- package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/skills.d.ts +31 -0
- package/node_modules/checkride/dist/agent-setup/skills.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/skills.js +56 -0
- package/node_modules/checkride/dist/agent-setup/skills.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/configured.d.ts +30 -0
- package/node_modules/checkride/dist/artifacts/configured.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/configured.js +45 -0
- package/node_modules/checkride/dist/artifacts/configured.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/format.d.ts +33 -0
- package/node_modules/checkride/dist/artifacts/format.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/format.js +56 -0
- package/node_modules/checkride/dist/artifacts/format.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/freshness.d.ts +32 -0
- package/node_modules/checkride/dist/artifacts/freshness.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/freshness.js +37 -0
- package/node_modules/checkride/dist/artifacts/freshness.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/index.d.ts +22 -0
- package/node_modules/checkride/dist/artifacts/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/index.js +19 -0
- package/node_modules/checkride/dist/artifacts/index.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/json.d.ts +31 -0
- package/node_modules/checkride/dist/artifacts/json.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/json.js +47 -0
- package/node_modules/checkride/dist/artifacts/json.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/raw.d.ts +60 -0
- package/node_modules/checkride/dist/artifacts/raw.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/raw.js +91 -0
- package/node_modules/checkride/dist/artifacts/raw.js.map +1 -0
- package/node_modules/checkride/dist/artifacts/summary.d.ts +69 -0
- package/node_modules/checkride/dist/artifacts/summary.d.ts.map +1 -0
- package/node_modules/checkride/dist/artifacts/summary.js +108 -0
- package/node_modules/checkride/dist/artifacts/summary.js.map +1 -0
- package/node_modules/checkride/dist/atomic.d.ts +16 -1
- package/node_modules/checkride/dist/atomic.d.ts.map +1 -1
- package/node_modules/checkride/dist/atomic.js +25 -3
- package/node_modules/checkride/dist/atomic.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.d.ts +9 -5
- package/node_modules/checkride/dist/baseline/fallow.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.js +163 -60
- package/node_modules/checkride/dist/baseline/fallow.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +20 -11
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.js +73 -23
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
- package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
- package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/history.js +232 -0
- package/node_modules/checkride/dist/baseline/history.js.map +1 -0
- package/node_modules/checkride/dist/baseline/index.d.ts +7 -5
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/index.js +5 -4
- package/node_modules/checkride/dist/baseline/index.js.map +1 -1
- package/node_modules/checkride/dist/baseline/store.d.ts +41 -10
- package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/store.js +65 -24
- package/node_modules/checkride/dist/baseline/store.js.map +1 -1
- package/node_modules/checkride/dist/baseline-command.d.ts +5 -5
- package/node_modules/checkride/dist/baseline-command.js +7 -7
- package/node_modules/checkride/dist/baseline-command.js.map +1 -1
- package/node_modules/checkride/dist/cli.d.ts +21 -4
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +350 -36
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +114 -9
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +142 -23
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.d.ts +5 -5
- package/node_modules/checkride/dist/digest/digest.js +6 -6
- package/node_modules/checkride/dist/digest/index.d.ts +2 -2
- package/node_modules/checkride/dist/digest/index.js +2 -2
- package/node_modules/checkride/dist/doctor.d.ts +27 -6
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +231 -27
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/gate.d.ts +180 -0
- package/node_modules/checkride/dist/gate.d.ts.map +1 -0
- package/node_modules/checkride/dist/gate.js +617 -0
- package/node_modules/checkride/dist/gate.js.map +1 -0
- package/node_modules/checkride/dist/init.d.ts +79 -12
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +385 -81
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/json.d.ts +21 -0
- package/node_modules/checkride/dist/json.d.ts.map +1 -0
- package/node_modules/checkride/dist/json.js +21 -0
- package/node_modules/checkride/dist/json.js.map +1 -0
- package/node_modules/checkride/dist/links.d.ts +31 -5
- package/node_modules/checkride/dist/links.d.ts.map +1 -1
- package/node_modules/checkride/dist/links.js +141 -32
- package/node_modules/checkride/dist/links.js.map +1 -1
- package/node_modules/checkride/dist/node-pin.d.ts +129 -0
- package/node_modules/checkride/dist/node-pin.d.ts.map +1 -0
- package/node_modules/checkride/dist/node-pin.js +211 -0
- package/node_modules/checkride/dist/node-pin.js.map +1 -0
- package/node_modules/checkride/dist/orchestrator.d.ts +53 -10
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +312 -113
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pack.d.ts +8 -9
- package/node_modules/checkride/dist/pack.d.ts.map +1 -1
- package/node_modules/checkride/dist/pack.js +53 -17
- package/node_modules/checkride/dist/pack.js.map +1 -1
- package/node_modules/checkride/dist/pm/detect.d.ts +6 -1
- package/node_modules/checkride/dist/pm/detect.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/detect.js +6 -1
- package/node_modules/checkride/dist/pm/detect.js.map +1 -1
- package/node_modules/checkride/dist/pm/index.d.ts +7 -2
- package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/index.js +6 -2
- package/node_modules/checkride/dist/pm/index.js.map +1 -1
- package/node_modules/checkride/dist/pm/launch.d.ts +72 -0
- package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -0
- package/node_modules/checkride/dist/pm/launch.js +108 -0
- package/node_modules/checkride/dist/pm/launch.js.map +1 -0
- package/node_modules/checkride/dist/pm/tools.d.ts +73 -0
- package/node_modules/checkride/dist/pm/tools.d.ts.map +1 -0
- package/node_modules/checkride/dist/pm/tools.js +124 -0
- package/node_modules/checkride/dist/pm/tools.js.map +1 -0
- package/node_modules/checkride/dist/pm/translate.d.ts +35 -9
- package/node_modules/checkride/dist/pm/translate.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/translate.js +78 -18
- package/node_modules/checkride/dist/pm/translate.js.map +1 -1
- package/node_modules/checkride/dist/proc.d.ts +30 -0
- package/node_modules/checkride/dist/proc.d.ts.map +1 -0
- package/node_modules/checkride/dist/proc.js +42 -0
- package/node_modules/checkride/dist/proc.js.map +1 -0
- package/node_modules/checkride/dist/qa/cli.d.ts +20 -0
- package/node_modules/checkride/dist/qa/cli.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/cli.js +24 -0
- package/node_modules/checkride/dist/qa/cli.js.map +1 -0
- package/node_modules/checkride/dist/qa/dead.d.ts +39 -0
- package/node_modules/checkride/dist/qa/dead.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/dead.js +88 -0
- package/node_modules/checkride/dist/qa/dead.js.map +1 -0
- package/node_modules/checkride/dist/qa/dupes.d.ts +45 -0
- package/node_modules/checkride/dist/qa/dupes.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/dupes.js +62 -0
- package/node_modules/checkride/dist/qa/dupes.js.map +1 -0
- package/node_modules/checkride/dist/qa/health.d.ts +85 -0
- package/node_modules/checkride/dist/qa/health.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/health.js +109 -0
- package/node_modules/checkride/dist/qa/health.js.map +1 -0
- package/node_modules/checkride/dist/qa/index.d.ts +23 -0
- package/node_modules/checkride/dist/qa/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/index.js +22 -0
- package/node_modules/checkride/dist/qa/index.js.map +1 -0
- package/node_modules/checkride/dist/qa/mutation.d.ts +80 -0
- package/node_modules/checkride/dist/qa/mutation.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/mutation.js +195 -0
- package/node_modules/checkride/dist/qa/mutation.js.map +1 -0
- package/node_modules/checkride/dist/qa/qa.d.ts +71 -0
- package/node_modules/checkride/dist/qa/qa.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/qa.js +158 -0
- package/node_modules/checkride/dist/qa/qa.js.map +1 -0
- package/node_modules/checkride/dist/qa/read.d.ts +40 -0
- package/node_modules/checkride/dist/qa/read.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/read.js +47 -0
- package/node_modules/checkride/dist/qa/read.js.map +1 -0
- package/node_modules/checkride/dist/qa/render.d.ts +26 -0
- package/node_modules/checkride/dist/qa/render.d.ts.map +1 -0
- package/node_modules/checkride/dist/qa/render.js +310 -0
- package/node_modules/checkride/dist/qa/render.js.map +1 -0
- package/node_modules/checkride/dist/recover.d.ts +48 -0
- package/node_modules/checkride/dist/recover.d.ts.map +1 -0
- package/node_modules/checkride/dist/recover.js +220 -0
- package/node_modules/checkride/dist/recover.js.map +1 -0
- package/node_modules/checkride/dist/security.d.ts +45 -0
- package/node_modules/checkride/dist/security.d.ts.map +1 -0
- package/node_modules/checkride/dist/security.js +86 -0
- package/node_modules/checkride/dist/security.js.map +1 -0
- package/node_modules/checkride/dist/smoke.d.ts +6 -6
- package/node_modules/checkride/dist/smoke.js +6 -6
- package/node_modules/checkride/dist/snippets.d.ts +27 -30
- package/node_modules/checkride/dist/snippets.d.ts.map +1 -1
- package/node_modules/checkride/dist/snippets.js +26 -30
- package/node_modules/checkride/dist/snippets.js.map +1 -1
- package/node_modules/checkride/dist/tool-json.d.ts +33 -0
- package/node_modules/checkride/dist/tool-json.d.ts.map +1 -0
- package/node_modules/checkride/dist/tool-json.js +58 -0
- package/node_modules/checkride/dist/tool-json.js.map +1 -0
- package/node_modules/checkride/dist/triage/cli.d.ts +21 -0
- package/node_modules/checkride/dist/triage/cli.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/cli.js +26 -0
- package/node_modules/checkride/dist/triage/cli.js.map +1 -0
- package/node_modules/checkride/dist/triage/doctor-fold.d.ts +51 -0
- package/node_modules/checkride/dist/triage/doctor-fold.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/doctor-fold.js +88 -0
- package/node_modules/checkride/dist/triage/doctor-fold.js.map +1 -0
- package/node_modules/checkride/dist/triage/env.d.ts +54 -0
- package/node_modules/checkride/dist/triage/env.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/env.js +81 -0
- package/node_modules/checkride/dist/triage/env.js.map +1 -0
- package/node_modules/checkride/dist/triage/gate.d.ts +61 -0
- package/node_modules/checkride/dist/triage/gate.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/gate.js +121 -0
- package/node_modules/checkride/dist/triage/gate.js.map +1 -0
- package/node_modules/checkride/dist/triage/index.d.ts +22 -0
- package/node_modules/checkride/dist/triage/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/index.js +20 -0
- package/node_modules/checkride/dist/triage/index.js.map +1 -0
- package/node_modules/checkride/dist/triage/render.d.ts +19 -0
- package/node_modules/checkride/dist/triage/render.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/render.js +419 -0
- package/node_modules/checkride/dist/triage/render.js.map +1 -0
- package/node_modules/checkride/dist/triage/triage.d.ts +83 -0
- package/node_modules/checkride/dist/triage/triage.d.ts.map +1 -0
- package/node_modules/checkride/dist/triage/triage.js +131 -0
- package/node_modules/checkride/dist/triage/triage.js.map +1 -0
- package/node_modules/checkride/package.json +18 -15
- package/node_modules/checkride/schema/checkride.config.schema.json +70 -5
- package/node_modules/checkride/schema/checkride.summary.schema.json +1 -1
- package/node_modules/checkride/skills/check/SKILL.md +265 -0
- package/node_modules/checkride/skills/qa/SKILL.md +231 -0
- package/node_modules/checkride/templates/shared/cspell.json +1 -0
- package/node_modules/checkride/templates/shared/oxlintrc.json +1 -1
- package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
- package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
- package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
- package/node_modules/checkride/templates/shared/vale.ini +34 -0
- package/package.json +10 -9
- package/skills/build/SKILL.md +85 -76
- package/skills/doctor/SKILL.md +11 -11
- package/skills/finish/SKILL.md +21 -18
- package/skills/harvest/SKILL.md +13 -11
- package/skills/park/SKILL.md +10 -10
- package/skills/plan/SKILL.md +69 -59
- package/skills/recover/SKILL.md +28 -0
- package/skills/refine/SKILL.md +21 -18
- package/skills/revert/SKILL.md +6 -6
- package/skills/spike/SKILL.md +9 -9
- package/skills/status/SKILL.md +5 -5
- package/skills/step/SKILL.md +20 -15
- package/skills/verify/SKILL.md +56 -46
- package/templates/build-log.md +7 -7
- package/templates/intent.md +52 -41
- package/templates/spike-report.md +10 -10
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The check orchestrator.
|
|
3
3
|
*
|
|
4
4
|
* Resolves slots to adapters, selects which to run (flags), spawns each command
|
|
5
5
|
* (or runs a built-in), captures raw output to `.check/`, and writes the
|
|
@@ -7,22 +7,26 @@
|
|
|
7
7
|
* diagnostics — agents read the per-tool JSON directly.
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
+
import { readdirSync } from 'node:fs';
|
|
10
11
|
import { mkdir, rm } from 'node:fs/promises';
|
|
11
12
|
import { cpus } from 'node:os';
|
|
12
13
|
import { join } from 'node:path';
|
|
13
14
|
import { performance } from 'node:perf_hooks';
|
|
14
15
|
import { ADAPTERS, SCHEMA_VERSION, SLOTS } from './adapters.js';
|
|
15
16
|
import { writeFileAtomic } from './atomic.js';
|
|
16
|
-
import { applyBaseline, BASELINE_FILE, baselinesEqual, countBaselineKeys, fallowVerdict, fingerprint,
|
|
17
|
+
import { applyBaseline, BASELINE_FILE, baselinesEqual, countBaselineKeys, fallowVerdict, fingerprint, historicalHint, ratchet, readBaselineStatus, writeBaseline, } from './baseline/index.js';
|
|
17
18
|
import { loadConfig, resolveChecks } from './config.js';
|
|
18
19
|
import { writeDigest } from './digest/index.js';
|
|
19
20
|
import { checkLinks } from './links.js';
|
|
20
21
|
import { checkPack } from './pack.js';
|
|
21
|
-
import { detectPackageManager, isAvailableUnder, translateExec } from './pm/index.js';
|
|
22
|
+
import { detectPackageManager, execTool, execUsesGlobalCache, installCommand, isAvailableUnder, resolveSlotTool, translateExec, } from './pm/index.js';
|
|
23
|
+
import { KILL_GRACE_SECONDS, killGroup } from './proc.js';
|
|
24
|
+
import { checkSecurity } from './security.js';
|
|
22
25
|
import { checkSmoke } from './smoke.js';
|
|
23
26
|
import { checkSnippets } from './snippets.js';
|
|
27
|
+
import { parseToolJson } from './tool-json.js';
|
|
24
28
|
/**
|
|
25
|
-
*
|
|
29
|
+
* Only/skip/opt-in selection by slot name.
|
|
26
30
|
* An opt-in slot runs when `--all`/`--include` names it, or when it was
|
|
27
31
|
* explicitly configured in `checks` (`r.explicit`) — naming a slot is opting in.
|
|
28
32
|
*/
|
|
@@ -42,17 +46,30 @@ export function selectChecks(resolved, flags) {
|
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
45
|
-
* Reject
|
|
46
|
-
* `--only lints`
|
|
49
|
+
* Reject bad slot selections in `--only`/`--skip`/`--include`. A typo like
|
|
50
|
+
* `--only lints` would otherwise slip through `selectChecks` as a filter that matched
|
|
47
51
|
* nothing, silently disabling the gate — the worst kind of vacuous green in a
|
|
48
52
|
* definition-of-done check. It is a usage error instead (thrown here, surfaced
|
|
49
53
|
* as exit 2 at the CLI). The valid set is every resolved slot: the catalogue
|
|
50
54
|
* slots plus any config custom-check names.
|
|
55
|
+
*
|
|
56
|
+
* A *present but empty* list is rejected for the same reason. `[]` is truthy,
|
|
57
|
+
* so `selectChecks` reads it as "match nothing" rather than "no filter", and an
|
|
58
|
+
* empty `only` selects zero checks. The CLI already rejects `--only ,` when it
|
|
59
|
+
* parses (see `parseList`); this covers the programmatic API, where a caller can
|
|
60
|
+
* hand `runChecks` an array it computed. `selectChecks` itself is left alone —
|
|
61
|
+
* it is exported public surface, and the error belongs before it runs.
|
|
51
62
|
*/
|
|
52
63
|
function validateSelection(resolved, flags) {
|
|
53
64
|
const valid = new Set(resolved.map((r) => r.slot));
|
|
54
65
|
for (const flag of ['only', 'skip', 'include']) {
|
|
55
|
-
const
|
|
66
|
+
const names = flags[flag];
|
|
67
|
+
if (names === undefined || names === null)
|
|
68
|
+
continue;
|
|
69
|
+
if (names.length === 0) {
|
|
70
|
+
throw new Error(`--${flag} was given an empty list, which selects nothing. Valid slots: ${[...valid].join(', ')}.`);
|
|
71
|
+
}
|
|
72
|
+
const unknown = names.filter((name) => !valid.has(name));
|
|
56
73
|
if (unknown.length === 0)
|
|
57
74
|
continue;
|
|
58
75
|
const named = unknown.map((n) => `'${n}'`).join(', ');
|
|
@@ -76,33 +93,18 @@ export function runtimeArgs(adapter, changed) {
|
|
|
76
93
|
export const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
77
94
|
/**
|
|
78
95
|
* Default pool width for a wave (equal-`order` group): `min(4, max(1, cores −
|
|
79
|
-
*
|
|
96
|
+
* reserve))`. Heavy checks (test, mutation, build) parallelize internally, so
|
|
80
97
|
* oversubscribing every core is worse than a conservative cap; one reserved
|
|
81
|
-
* core keeps the machine responsive.
|
|
98
|
+
* core keeps the machine responsive — for the human at it. A hosted CI runner
|
|
99
|
+
* has no such human, and a standard GitHub-hosted runner reports 2 CPUs, so
|
|
100
|
+
* reserving one there collapsed the pool to 1 and wave scheduling silently
|
|
101
|
+
* degenerated to fully sequential — on exactly the machine class the docs say
|
|
102
|
+
* to gate on. Every CI provider sets `CI`, so no core is reserved there.
|
|
103
|
+
* Override with `--concurrency`; `--bail` stays fail-fast sequential.
|
|
82
104
|
*/
|
|
83
|
-
function defaultConcurrency() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/** Grace between SIGTERM and SIGKILL when a timed-out check won't die politely. */
|
|
87
|
-
const KILL_GRACE_SECONDS = 5;
|
|
88
|
-
/**
|
|
89
|
-
* Signal a spawned check's whole process group. The check is spawned
|
|
90
|
-
* `detached`, so it leads its own group and a negated-pid `kill` reaches every
|
|
91
|
-
* descendant — a wrapper's grandchildren included — not just the direct child.
|
|
92
|
-
* That is the difference between a timed-out `sh -c 'slow-tool …'` leaving its
|
|
93
|
-
* real worker orphaned and alive versus taking the whole tree down. ESRCH is
|
|
94
|
-
* swallowed: the group racing to exit on its own before we signal is success,
|
|
95
|
-
* not an error (and `pid` is absent only when the spawn itself failed).
|
|
96
|
-
*/
|
|
97
|
-
function killGroup(pid, signal) {
|
|
98
|
-
if (pid === undefined)
|
|
99
|
-
return;
|
|
100
|
-
try {
|
|
101
|
-
process.kill(-pid, signal);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// Group already gone (raced with a clean exit) — nothing left to kill.
|
|
105
|
-
}
|
|
105
|
+
export function defaultConcurrency(env = process.env, cores = cpus().length) {
|
|
106
|
+
const reserve = env['CI'] ? 0 : 1;
|
|
107
|
+
return Math.min(4, Math.max(1, cores - reserve));
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
110
|
* Process groups of checks currently in flight: pid → a promise that settles
|
|
@@ -221,51 +223,164 @@ function spawnCheck(command, args, cwd, timeoutSec) {
|
|
|
221
223
|
});
|
|
222
224
|
});
|
|
223
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Dispatch a built-in check, or `null` when the adapter spawns a real tool.
|
|
228
|
+
* Every built-in that runs a subprocess goes through `spawnCheck`, so its
|
|
229
|
+
* child registers in `liveChecks` and inherits the timeout + reaping like any
|
|
230
|
+
* other check. `security` alone runs the adapter's own args — its
|
|
231
|
+
* `--audit-level` doubles as the threshold the evaluator enforces, since
|
|
232
|
+
* pnpm's JSON-mode exit code ignores the level. The snippets pair shares one
|
|
233
|
+
* execution path, differing only in mode.
|
|
234
|
+
*/
|
|
235
|
+
function runBuiltin(adapter, ctx, timeout) {
|
|
236
|
+
const base = { cwd: ctx.cwd, spawn: spawnCheck, timeoutSec: timeout };
|
|
237
|
+
switch (adapter.builtin) {
|
|
238
|
+
case 'pack':
|
|
239
|
+
return checkPack({ ...base, pm: ctx.pm });
|
|
240
|
+
case 'smoke':
|
|
241
|
+
return checkSmoke(base);
|
|
242
|
+
case 'security':
|
|
243
|
+
return checkSecurity({ ...base, command: adapter.command, args: runtimeArgs(adapter, ctx.changed) });
|
|
244
|
+
case 'snippets':
|
|
245
|
+
return checkSnippets({ ...base, mode: 'src', pm: ctx.pm });
|
|
246
|
+
case 'snippets-dist':
|
|
247
|
+
return checkSnippets({ ...base, mode: 'dist', pm: ctx.pm });
|
|
248
|
+
default:
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Refuse a slot whose tool this repo never declared, rather than letting the
|
|
254
|
+
* launcher's per-user cache decide the verdict.
|
|
255
|
+
*
|
|
256
|
+
* `--no-install` stops `npx`/`bunx` fetching a missing tool, but both still run
|
|
257
|
+
* a copy cached from some earlier, unrelated invocation — so the same commit
|
|
258
|
+
* passes on a machine that happens to hold one and fails on a clean checkout,
|
|
259
|
+
* which is the CI runner. A gate whose result depends on that is not reporting
|
|
260
|
+
* on the code. Resolving the binary in the local tree first moves the failure
|
|
261
|
+
* to every machine equally, and to the one place it is cheap to fix.
|
|
262
|
+
*
|
|
263
|
+
* Scoped to the launchers that *have* such a cache ({@link
|
|
264
|
+
* execUsesGlobalCache}): `pnpm exec` and `yarn` resolve from the project tree
|
|
265
|
+
* already, and pre-flighting Yarn PnP — which has no `node_modules/.bin` — would
|
|
266
|
+
* report every tool missing. `null` means the check may spawn.
|
|
267
|
+
*/
|
|
268
|
+
export function missingToolOutcome(slot, adapter, args, ctx) {
|
|
269
|
+
if (!execUsesGlobalCache(ctx.pm))
|
|
270
|
+
return null;
|
|
271
|
+
const tool = execTool(adapter.command, args);
|
|
272
|
+
if (!tool || resolveSlotTool(ctx.cwd, tool))
|
|
273
|
+
return null;
|
|
274
|
+
const stderr = [
|
|
275
|
+
`checkride: the \`${slot}\` slot needs \`${tool}\`, which is not installed in this project.`,
|
|
276
|
+
'',
|
|
277
|
+
` looked for: node_modules/.bin/${tool} (from ${ctx.cwd} up to the repo root)`,
|
|
278
|
+
'',
|
|
279
|
+
'checkride never fetches a tool mid-run, and a launcher cache can still supply',
|
|
280
|
+
"one this repo never declared — so a tool that isn't a dependency here would",
|
|
281
|
+
'pass on your machine and fail on a clean checkout. Declare it instead:',
|
|
282
|
+
'',
|
|
283
|
+
` ${installCommand(ctx.pm, tool)}`,
|
|
284
|
+
'',
|
|
285
|
+
].join('\n');
|
|
286
|
+
// Exit 1, not -1. Nothing spawned, so there is no real status to report — but
|
|
287
|
+
// -1 is reserved for a spawn failure or timeout, which `triage` discounts as
|
|
288
|
+
// "a harness problem, not a finding" (see `docs/plugin.md`). An undeclared
|
|
289
|
+
// tool is the opposite: the finding the run exists to surface, and the one
|
|
290
|
+
// the reader has to act on. Reporting it as -1 would tell them to ignore it.
|
|
291
|
+
return { ok: false, exit_code: 1, stdout: '', stderr };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Fail the `prose` check when its configured `exemplars` directory is missing
|
|
295
|
+
* or holds no files, rather than letting the tool pass while the anchor texts
|
|
296
|
+
* rot away.
|
|
297
|
+
*
|
|
298
|
+
* The exemplars are the repo's hand-written voice reference: the AGENTS.md
|
|
299
|
+
* stanza tells writing sessions to imitate them, so a config that names a
|
|
300
|
+
* directory that no longer exists (renamed, deleted, never created) would leave
|
|
301
|
+
* that instruction aimed at nothing while the check stayed green. Presence is
|
|
302
|
+
* the whole assertion — checkride never scores prose against the exemplars;
|
|
303
|
+
* that read belongs to a human. `null` means the check may spawn.
|
|
304
|
+
*/
|
|
305
|
+
export function missingExemplarsOutcome(adapter, cwd) {
|
|
306
|
+
const dir = adapter.exemplars;
|
|
307
|
+
if (dir === undefined)
|
|
308
|
+
return null;
|
|
309
|
+
let state = 'does not exist';
|
|
310
|
+
try {
|
|
311
|
+
const files = readdirSync(join(cwd, dir), { withFileTypes: true }).filter((e) => e.isFile() && !e.name.startsWith('.'));
|
|
312
|
+
state = files.length > 0 ? null : 'has no files in it';
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
// Unreadable reads as missing. A file sitting at the path is named for what
|
|
316
|
+
// it is — "does not exist" would send the reader hunting a typo that isn't
|
|
317
|
+
// there.
|
|
318
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOTDIR')
|
|
319
|
+
state = 'is a file, not a directory';
|
|
320
|
+
}
|
|
321
|
+
if (state === null)
|
|
322
|
+
return null;
|
|
323
|
+
const stderr = [
|
|
324
|
+
`checkride: the \`prose\` slot names \`${dir}\` as its voice exemplars, and that path ${state}.`,
|
|
325
|
+
'',
|
|
326
|
+
'Exemplars are hand-written prose the agent contract points writing sessions at',
|
|
327
|
+
'as the voice to imitate, so a config naming an empty directory aims that',
|
|
328
|
+
'instruction at nothing. Write a few short samples of prose in your own voice',
|
|
329
|
+
'there (or fix the `exemplars` path in checkride.config.json), then re-run.',
|
|
330
|
+
'',
|
|
331
|
+
].join('\n');
|
|
332
|
+
// Exit 1 for the same reason as missingToolOutcome above: nothing spawned,
|
|
333
|
+
// but this is the finding the run exists to surface, not a harness problem.
|
|
334
|
+
return { ok: false, exit_code: 1, stdout: '', stderr };
|
|
335
|
+
}
|
|
224
336
|
const defaultRunner = (resolved, ctx) => {
|
|
225
337
|
const adapter = resolved.adapter;
|
|
226
338
|
if (!adapter)
|
|
227
339
|
return Promise.resolve({ ok: true, exit_code: 0, stdout: '', stderr: '' });
|
|
228
|
-
if (adapter.builtin === 'links')
|
|
229
|
-
return checkLinks(ctx.cwd);
|
|
230
|
-
const timeout = adapter.timeout ?? ctx.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
231
|
-
// The pack built-in spawns its own subprocess (the PM's pack dry-run) through
|
|
232
|
-
// `spawnCheck`, so its child registers in `liveChecks` and inherits the
|
|
233
|
-
// timeout + reaping like every other check (D16/C6).
|
|
234
|
-
if (adapter.builtin === 'pack')
|
|
235
|
-
return checkPack({ cwd: ctx.cwd, pm: ctx.pm, spawn: spawnCheck, timeoutSec: timeout });
|
|
236
|
-
// The smoke built-in spawns a `node` probe of the built package through
|
|
237
|
-
// `spawnCheck`, so that child registers in `liveChecks` and inherits the
|
|
238
|
-
// timeout + reaping like every other check (D9/D16/C6).
|
|
239
|
-
if (adapter.builtin === 'smoke')
|
|
240
|
-
return checkSmoke({ cwd: ctx.cwd, spawn: spawnCheck, timeoutSec: timeout });
|
|
241
|
-
// The snippets built-ins spawn `<pm> exec tsc` through `spawnCheck` (D16/C6);
|
|
242
|
-
// the two adapters share one execution path, differing only in mode (D12).
|
|
243
|
-
if (adapter.builtin === 'snippets') {
|
|
244
|
-
return checkSnippets({ cwd: ctx.cwd, mode: 'src', pm: ctx.pm, spawn: spawnCheck, timeoutSec: timeout });
|
|
245
|
-
}
|
|
246
|
-
if (adapter.builtin === 'snippets-dist') {
|
|
247
|
-
return checkSnippets({ cwd: ctx.cwd, mode: 'dist', pm: ctx.pm, spawn: spawnCheck, timeoutSec: timeout });
|
|
340
|
+
if (adapter.builtin === 'links') {
|
|
341
|
+
return checkLinks(ctx.cwd, { exclude: adapter.exclude, allowlist: adapter.allowlist });
|
|
248
342
|
}
|
|
249
|
-
const
|
|
343
|
+
const timeout = adapter.timeout ?? ctx.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
344
|
+
const builtin = runBuiltin(adapter, ctx, timeout);
|
|
345
|
+
if (builtin)
|
|
346
|
+
return builtin;
|
|
347
|
+
const missingExemplars = missingExemplarsOutcome(adapter, ctx.cwd);
|
|
348
|
+
if (missingExemplars)
|
|
349
|
+
return Promise.resolve(missingExemplars);
|
|
350
|
+
const declared = runtimeArgs(adapter, ctx.changed);
|
|
351
|
+
const missing = missingToolOutcome(resolved.slot, adapter, declared, ctx);
|
|
352
|
+
if (missing)
|
|
353
|
+
return Promise.resolve(missing);
|
|
354
|
+
const { command, args } = translateExec(adapter.command, declared, ctx.pm);
|
|
250
355
|
return spawnCheck(command, args, ctx.cwd, timeout);
|
|
251
356
|
};
|
|
252
|
-
/**
|
|
357
|
+
/**
|
|
358
|
+
* Persist raw output atomically: JSON to `.check/<outputFile>`, else
|
|
359
|
+
* stdout/stderr text.
|
|
360
|
+
*
|
|
361
|
+
* Returns the JSON file it wrote, or `null` when it fell through to text — the
|
|
362
|
+
* summary's `output_file` is that return value, never the adapter's
|
|
363
|
+
* *declaration*. A slot that declares an `outputFile` does not always produce
|
|
364
|
+
* one (the tool printed a warning first, crashed, or emitted plain text this
|
|
365
|
+
* run), and naming a file that was never written sends every consumer to an
|
|
366
|
+
* ENOENT while the real bytes sit in `<slot>.stdout.txt` beside it.
|
|
367
|
+
*/
|
|
253
368
|
async function persistOutput(cwd, adapter, outcome) {
|
|
254
369
|
const dir = join(cwd, '.check');
|
|
255
370
|
if (adapter.outputFile && outcome.stdout.trim()) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// Not JSON after all; fall through to raw text.
|
|
371
|
+
// Tolerates a launcher preamble ahead of the JSON; writes the tool's own
|
|
372
|
+
// bytes from the first JSON character on, so the artifact actually parses.
|
|
373
|
+
const parsed = parseToolJson(outcome.stdout);
|
|
374
|
+
if (parsed) {
|
|
375
|
+
await writeFileAtomic(join(dir, adapter.outputFile), parsed.text);
|
|
376
|
+
return adapter.outputFile;
|
|
263
377
|
}
|
|
264
378
|
}
|
|
265
379
|
if (outcome.stdout.trim())
|
|
266
380
|
await writeFileAtomic(join(dir, `${adapter.slot}.stdout.txt`), outcome.stdout);
|
|
267
381
|
if (outcome.stderr.trim())
|
|
268
382
|
await writeFileAtomic(join(dir, `${adapter.slot}.stderr.txt`), outcome.stderr);
|
|
383
|
+
return null;
|
|
269
384
|
}
|
|
270
385
|
/**
|
|
271
386
|
* Remove a slot's prior `.check/` artifacts before it re-runs, so a later clean
|
|
@@ -273,7 +388,7 @@ async function persistOutput(cwd, adapter, outcome) {
|
|
|
273
388
|
* the previous run's output lingering as authoritative. Covers everything
|
|
274
389
|
* `persistOutput` writes (`<slot>.stdout.txt`, `<slot>.stderr.txt`, the adapter's
|
|
275
390
|
* JSON `outputFile`) plus the conventional `<slot>.json` a tool may write itself
|
|
276
|
-
* (
|
|
391
|
+
* (for example vitest's `--outputFile=.check/test.json`, where `outputFile` is null).
|
|
277
392
|
* Runs *before* the check so this run's own tool-written artifacts survive.
|
|
278
393
|
*/
|
|
279
394
|
async function clearSlotOutputs(cwd, adapter) {
|
|
@@ -290,9 +405,18 @@ async function clearSlotOutputs(cwd, adapter) {
|
|
|
290
405
|
function writeLine(out, line) {
|
|
291
406
|
out.write(`${line}\n`);
|
|
292
407
|
}
|
|
293
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Width of the slot-name column, measured from the names this run will print
|
|
410
|
+
* (never below the historical 8). A fixed 8 was sized for catalogue slots, and
|
|
411
|
+
* a config custom check — `typecheck-tests` is 15 — pushed the duration and
|
|
412
|
+
* description columns right on its row alone.
|
|
413
|
+
*/
|
|
414
|
+
function nameWidth(selected) {
|
|
415
|
+
return selected.reduce((n, r) => Math.max(n, r.slot.length), 8);
|
|
416
|
+
}
|
|
417
|
+
function formatStatusLine(check, width) {
|
|
294
418
|
const mark = check.ok ? '✔' : '✘';
|
|
295
|
-
const name = check.name.padEnd(
|
|
419
|
+
const name = check.name.padEnd(width);
|
|
296
420
|
const duration = `${check.duration_ms}ms`.padStart(8);
|
|
297
421
|
return ` ${mark} ${name} ${duration} ${check.description}`;
|
|
298
422
|
}
|
|
@@ -306,14 +430,16 @@ function skippedEntry(resolved) {
|
|
|
306
430
|
reason: resolved.skip ?? 'skipped',
|
|
307
431
|
exit_code: null,
|
|
308
432
|
duration_ms: 0,
|
|
309
|
-
|
|
433
|
+
// A skipped check ran nothing and wrote nothing; naming its adapter's
|
|
434
|
+
// declared output file would point at whatever an earlier run left there.
|
|
435
|
+
output_file: null,
|
|
310
436
|
};
|
|
311
437
|
}
|
|
312
438
|
/**
|
|
313
|
-
* `total_duration_ms` is the wall-clock span of the whole execution phase
|
|
439
|
+
* `total_duration_ms` is the wall-clock span of the whole execution phase,
|
|
314
440
|
* not the sum of per-check durations — under concurrency those diverge, and
|
|
315
|
-
* wall-clock is what the field honestly means. It equals the
|
|
316
|
-
* execution is sequential (one check in flight at a time).
|
|
441
|
+
* wall-clock is what the field honestly means. It equals the per-check sum
|
|
442
|
+
* whenever execution is sequential (one check in flight at a time).
|
|
317
443
|
*/
|
|
318
444
|
function buildSummary(checks, totalDurationMs) {
|
|
319
445
|
return {
|
|
@@ -335,8 +461,8 @@ function enableHint(slot, adapters) {
|
|
|
335
461
|
}
|
|
336
462
|
/**
|
|
337
463
|
* The first-party vacuous-green signal: "green because nothing ran" must be
|
|
338
|
-
* unmissable, not something only a consumer that hand-
|
|
339
|
-
*
|
|
464
|
+
* unmissable, not something only a consumer that hand-rolls its own check can
|
|
465
|
+
* distinguish from "green because everything passed".
|
|
340
466
|
* Names why each slot sat out and what would enable it.
|
|
341
467
|
*/
|
|
342
468
|
function warnVacuous(stderr, checks, adapters) {
|
|
@@ -354,9 +480,8 @@ function warnVacuous(stderr, checks, adapters) {
|
|
|
354
480
|
}
|
|
355
481
|
/**
|
|
356
482
|
* Apply the defaults every command shares: cwd, the slot/adapter catalogues, the
|
|
357
|
-
* config (loaded from `cwd` unless injected), and the two streams
|
|
358
|
-
* `runChecks`/`runFix`/`runDoctor` resolve this identical block one way
|
|
359
|
-
* copy-pasted across all three).
|
|
483
|
+
* config (loaded from `cwd` unless injected), and the two streams, so
|
|
484
|
+
* `runChecks`/`runFix`/`runDoctor` resolve this identical block one way.
|
|
360
485
|
*/
|
|
361
486
|
export function resolveCommonOptions(options) {
|
|
362
487
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -369,18 +494,36 @@ export function resolveCommonOptions(options) {
|
|
|
369
494
|
stderr: options.stderr ?? process.stderr,
|
|
370
495
|
};
|
|
371
496
|
}
|
|
372
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* Apply every run default: the common options plus runner, flags, PM, and
|
|
499
|
+
* baseline. `nameWidth` is a placeholder here — the selection it measures is
|
|
500
|
+
* not known until after `selectChecks`, so `runChecks` narrows it then.
|
|
501
|
+
*/
|
|
502
|
+
/**
|
|
503
|
+
* The baseline a run masks with: the option override, or the committed file.
|
|
504
|
+
* An injected baseline bypasses the file, so its state carries no file
|
|
505
|
+
* diagnosis — it is classified by presence alone.
|
|
506
|
+
*/
|
|
507
|
+
function resolveBaselineRead(options, cwd) {
|
|
508
|
+
if (options.baseline !== undefined) {
|
|
509
|
+
return { baseline: options.baseline, state: options.baseline === null ? 'absent' : 'ok' };
|
|
510
|
+
}
|
|
511
|
+
return readBaselineStatus(cwd);
|
|
512
|
+
}
|
|
373
513
|
function resolveRunContext(options) {
|
|
374
514
|
const common = resolveCommonOptions(options);
|
|
515
|
+
const read = resolveBaselineRead(options, common.cwd);
|
|
375
516
|
return {
|
|
376
517
|
...common,
|
|
518
|
+
nameWidth: 8,
|
|
377
519
|
runner: options.runner ?? defaultRunner,
|
|
378
520
|
json: options.json ?? false,
|
|
379
521
|
bail: options.bail ?? false,
|
|
380
522
|
changed: options.changed ?? false,
|
|
381
523
|
timeout: common.config?.timeout,
|
|
382
524
|
pm: options.pm ?? detectPackageManager({ cwd: common.cwd }),
|
|
383
|
-
baseline:
|
|
525
|
+
baseline: read.baseline,
|
|
526
|
+
baselineState: read.state,
|
|
384
527
|
concurrency: Math.max(1, Math.floor(options.concurrency ?? defaultConcurrency())),
|
|
385
528
|
};
|
|
386
529
|
}
|
|
@@ -403,15 +546,15 @@ function maskOutcome(adapter, outcome, baseline, slot) {
|
|
|
403
546
|
return { ok: outcome.ok, baselined: 0, newKeys: [], reason: null, observed: null };
|
|
404
547
|
}
|
|
405
548
|
/** Build (and print) the skipped-entry row for a slot that won't run this pass. */
|
|
406
|
-
function handleSkip(r, unavailable,
|
|
407
|
-
const entry = skippedEntry(unavailable ? { ...r, skip: `'${r.adapter?.command} ${r.adapter?.args[0]}' is unavailable under ${pm}` } : r);
|
|
408
|
-
if (!json)
|
|
409
|
-
writeLine(stderr, ` ○ ${entry.name.padEnd(
|
|
549
|
+
function handleSkip(r, unavailable, ctx) {
|
|
550
|
+
const entry = skippedEntry(unavailable ? { ...r, skip: `'${r.adapter?.command} ${r.adapter?.args[0]}' is unavailable under ${ctx.pm}` } : r);
|
|
551
|
+
if (!ctx.json)
|
|
552
|
+
writeLine(ctx.stderr, ` ○ ${entry.name.padEnd(ctx.nameWidth)} skip ${entry.reason ?? ''}`);
|
|
410
553
|
return entry;
|
|
411
554
|
}
|
|
412
555
|
/** Print one check's status line, its baselined count, and any new (non-grandfathered) findings. */
|
|
413
|
-
function reportCheckResult(stderr, entry, mask) {
|
|
414
|
-
writeLine(stderr, formatStatusLine(entry));
|
|
556
|
+
function reportCheckResult(stderr, entry, mask, width) {
|
|
557
|
+
writeLine(stderr, formatStatusLine(entry, width));
|
|
415
558
|
if (mask.baselined > 0)
|
|
416
559
|
writeLine(stderr, ` ${mask.baselined} baselined (grandfathered)`);
|
|
417
560
|
if (!entry.ok && mask.reason)
|
|
@@ -428,12 +571,12 @@ async function runOneCheck(r, adapter, ctx) {
|
|
|
428
571
|
writeLine(ctx.stderr, ` ▸ ${r.slot} ${adapter.description}`);
|
|
429
572
|
// Wipe this slot's stale `.check/` artifacts before it runs, so a leaner re-run
|
|
430
573
|
// can't leave last run's output behind as authoritative — and so any artifact
|
|
431
|
-
// the tool writes during *this* run (
|
|
574
|
+
// the tool writes during *this* run (for example `test.json`) survives.
|
|
432
575
|
await clearSlotOutputs(ctx.cwd, adapter);
|
|
433
576
|
const start = performance.now();
|
|
434
577
|
const outcome = await ctx.runner(r, { cwd: ctx.cwd, changed: ctx.changed, pm: ctx.pm, ...(ctx.timeout !== undefined ? { timeout: ctx.timeout } : {}) });
|
|
435
578
|
const duration_ms = Math.round(performance.now() - start);
|
|
436
|
-
await persistOutput(ctx.cwd, adapter, outcome);
|
|
579
|
+
const outputFile = await persistOutput(ctx.cwd, adapter, outcome);
|
|
437
580
|
// Masking is always on (even under a partial run); only the ratchet is gated.
|
|
438
581
|
// The raw `.check/<slot>.json` is persisted untouched — masking changes the
|
|
439
582
|
// pass/fail verdict, never the authoritative output.
|
|
@@ -445,7 +588,7 @@ async function runOneCheck(r, adapter, ctx) {
|
|
|
445
588
|
ok: mask.ok,
|
|
446
589
|
exit_code: outcome.exit_code,
|
|
447
590
|
duration_ms,
|
|
448
|
-
output_file:
|
|
591
|
+
output_file: outputFile,
|
|
449
592
|
...(mask.baselined > 0 ? { baselined: mask.baselined } : {}),
|
|
450
593
|
};
|
|
451
594
|
return { entry, run: { slot: r.slot, adapter, outcome }, mask };
|
|
@@ -455,19 +598,19 @@ async function runOneCheck(r, adapter, ctx) {
|
|
|
455
598
|
* shared by the sequential (`--bail`) and wave paths. A skipped/unavailable slot
|
|
456
599
|
* records a skip row and runs nothing; an active one runs, prints its status
|
|
457
600
|
* line, and reports its fingerprint for the ratchet. It mutates no shared state,
|
|
458
|
-
* so N of these can be in flight at once
|
|
601
|
+
* so N of these can be in flight at once.
|
|
459
602
|
*/
|
|
460
603
|
async function runSelectedCheck(r, ctx) {
|
|
461
|
-
// Skip when unresolved, or when the adapter can't run under this PM —
|
|
462
|
-
// `pnpm audit` (the `security` slot) is unavailable off pnpm
|
|
604
|
+
// Skip when unresolved, or when the adapter can't run under this PM — for
|
|
605
|
+
// example `pnpm audit` (the `security` slot) is unavailable off pnpm.
|
|
463
606
|
const unavailable = Boolean(r.adapter && !isAvailableUnder(r.adapter.command, r.adapter.args, ctx.pm));
|
|
464
607
|
if (r.skip || !r.adapter || unavailable) {
|
|
465
|
-
return { entry: handleSkip(r, unavailable, ctx
|
|
608
|
+
return { entry: handleSkip(r, unavailable, ctx), run: null, observed: null, newKeys: [] };
|
|
466
609
|
}
|
|
467
610
|
const { entry, run, mask } = await runOneCheck(r, r.adapter, ctx);
|
|
468
611
|
if (!ctx.json)
|
|
469
|
-
reportCheckResult(ctx.stderr, entry, mask);
|
|
470
|
-
return { entry, run, observed: mask.observed !== null ? { slot: r.slot, fp: mask.observed } : null };
|
|
612
|
+
reportCheckResult(ctx.stderr, entry, mask, ctx.nameWidth);
|
|
613
|
+
return { entry, run, observed: mask.observed !== null ? { slot: r.slot, fp: mask.observed } : null, newKeys: mask.newKeys };
|
|
471
614
|
}
|
|
472
615
|
/** Fold one result into the report accumulators, in call order. */
|
|
473
616
|
function collect(res, acc) {
|
|
@@ -476,15 +619,16 @@ function collect(res, acc) {
|
|
|
476
619
|
acc.runs.push(res.run);
|
|
477
620
|
if (res.observed)
|
|
478
621
|
acc.observed.set(res.observed.slot, res.observed.fp);
|
|
622
|
+
acc.newKeys.push(...res.newKeys);
|
|
479
623
|
}
|
|
480
624
|
/** A fresh, empty {@link Execution}. */
|
|
481
625
|
function emptyExecution(brokeEarly = false) {
|
|
482
|
-
return { checks: [], runs: [], observed: new Map(), brokeEarly };
|
|
626
|
+
return { checks: [], runs: [], observed: new Map(), newKeys: [], brokeEarly };
|
|
483
627
|
}
|
|
484
628
|
/**
|
|
485
|
-
* Scheduling coordinates for a check on
|
|
486
|
-
* numeric line 1, singles 2, lasts 3) and its position on the numeric line
|
|
487
|
-
* (`'any'`/`'middle'` sit at 0 —
|
|
629
|
+
* Scheduling coordinates for a check on the order line: the group `rank` (firsts 0,
|
|
630
|
+
* the numeric line 1, singles 2, lasts 3) and its position on the numeric line
|
|
631
|
+
* (`'any'`/`'middle'` sit at 0 — the conservative placement). This must agree
|
|
488
632
|
* with `config.ts`'s group sort, which already put `selected` in exactly this
|
|
489
633
|
* sequence; here it only re-derives the wave *boundaries* the sort collapsed.
|
|
490
634
|
*/
|
|
@@ -547,16 +691,16 @@ async function runPool(items, width, worker) {
|
|
|
547
691
|
await Promise.all(workers);
|
|
548
692
|
}
|
|
549
693
|
/**
|
|
550
|
-
* `--bail`: the flat sequential path
|
|
551
|
-
*
|
|
694
|
+
* `--bail`: the flat sequential path. Checks run one at a time in group order
|
|
695
|
+
* and the run stops at the first *executed* failure — fail-fast is
|
|
552
696
|
* incompatible with already-launched concurrent work, so `--concurrency` is moot
|
|
553
697
|
* here (the caller notes it if one was passed). For the default set (all `'any'`)
|
|
554
|
-
* this is exactly
|
|
698
|
+
* this is exactly the cheapest-first order.
|
|
555
699
|
*/
|
|
556
700
|
async function executeBail(selected, ctx) {
|
|
557
701
|
const acc = emptyExecution();
|
|
558
702
|
for (const r of selected) {
|
|
559
|
-
// oxlint-disable-next-line no-await-in-loop -- --bail is fail-fast: checks run one at a time so the run can stop at the first failure
|
|
703
|
+
// oxlint-disable-next-line no-await-in-loop -- --bail is fail-fast: checks run one at a time so the run can stop at the first failure.
|
|
560
704
|
const res = await runSelectedCheck(r, ctx);
|
|
561
705
|
collect(res, acc);
|
|
562
706
|
if (res.run && !res.entry.ok)
|
|
@@ -565,17 +709,17 @@ async function executeBail(selected, ctx) {
|
|
|
565
709
|
return acc;
|
|
566
710
|
}
|
|
567
711
|
/**
|
|
568
|
-
* The wave scheduler
|
|
712
|
+
* The wave scheduler: run each wave's members concurrently through the pool,
|
|
569
713
|
* with a barrier between waves. Results are placed by their `selected` index and
|
|
570
714
|
* assembled in that order afterwards, so the report is deterministic regardless
|
|
571
|
-
* of which check finishes first
|
|
715
|
+
* of which check finishes first. Every selected check runs — there is no
|
|
572
716
|
* early stop off the `--bail` path.
|
|
573
717
|
*/
|
|
574
718
|
async function executeWaves(selected, ctx) {
|
|
575
719
|
const indexed = selected.map((r, i) => ({ r, i }));
|
|
576
720
|
const results = Array.from({ length: selected.length });
|
|
577
721
|
for (const wave of partitionWaves(indexed)) {
|
|
578
|
-
// oxlint-disable-next-line no-await-in-loop -- the between-values barrier
|
|
722
|
+
// oxlint-disable-next-line no-await-in-loop -- the between-values barrier: a wave runs to completion before the next distinct order value begins.
|
|
579
723
|
await runPool(wave, ctx.concurrency, async ({ r, i }) => { results[i] = await runSelectedCheck(r, ctx); });
|
|
580
724
|
}
|
|
581
725
|
const acc = emptyExecution();
|
|
@@ -595,12 +739,12 @@ function executeChecks(selected, ctx) {
|
|
|
595
739
|
/**
|
|
596
740
|
* Whether this run saw less than the full pipeline — an `--only`/`--skip`/
|
|
597
741
|
* `--changed` filter or an early `--bail` break. The ratchet is gated off for
|
|
598
|
-
* these: an unobserved diagnostic must not be mistaken for a fixed one
|
|
742
|
+
* these: an unobserved diagnostic must not be mistaken for a fixed one.
|
|
599
743
|
*/
|
|
600
744
|
function isPartialRun(options, changed, brokeEarly) {
|
|
601
745
|
return (options.only ?? null) !== null || (options.skip?.length ?? 0) > 0 || changed || brokeEarly;
|
|
602
746
|
}
|
|
603
|
-
/** On a fully
|
|
747
|
+
/** On a fully observed run, prune grandfathered diagnostics now fixed (shrink-only, atomic). */
|
|
604
748
|
async function maybeRatchet(cwd, baseline, observed, restricted, json, stderr) {
|
|
605
749
|
if (!baseline || restricted)
|
|
606
750
|
return;
|
|
@@ -612,6 +756,26 @@ async function maybeRatchet(cwd, baseline, observed, restricted, json, stderr) {
|
|
|
612
756
|
writeLine(stderr, `\nbaseline: ratcheted ${BASELINE_FILE} to ${countBaselineKeys(pruned)} grandfathered diagnostic(s)`);
|
|
613
757
|
}
|
|
614
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* How much of the selection actually ran, said only when it was not all of it.
|
|
761
|
+
*
|
|
762
|
+
* `✔ all checks passed` is a true sentence about the checks that ran and a
|
|
763
|
+
* misleading one about the repo, and the gap is invisible at exactly the moment
|
|
764
|
+
* it matters most: a repo that configures almost nothing reports the same
|
|
765
|
+
* confident green as one that configures everything. `--strict` does not catch
|
|
766
|
+
* this, and cannot — its floor is *zero* checks, which a slot like `links` that
|
|
767
|
+
* needs no tool keeps a repo off by itself.
|
|
768
|
+
*
|
|
769
|
+
* Only skipped slots are counted here. The per-slot `○ … skip <reason>` lines
|
|
770
|
+
* are already on screen above; this is the one line someone scrolled past them
|
|
771
|
+
* to read.
|
|
772
|
+
*/
|
|
773
|
+
function coverage(summary, checks) {
|
|
774
|
+
const skipped = checks.length - summary.checks_run;
|
|
775
|
+
if (skipped <= 0)
|
|
776
|
+
return '';
|
|
777
|
+
return ` — only ${summary.checks_run} of ${checks.length} checks ran, ${skipped} skipped`;
|
|
778
|
+
}
|
|
615
779
|
/** Print the human run summary: the vacuous-green warning, the status line, and artifact paths. */
|
|
616
780
|
function reportSummary(stderr, summary, checks, adapters, digestWritten) {
|
|
617
781
|
if (summary.checks_run === 0)
|
|
@@ -620,7 +784,8 @@ function reportSummary(stderr, summary, checks, adapters, digestWritten) {
|
|
|
620
784
|
const status = summary.checks_run === 0
|
|
621
785
|
? '⚠ no checks ran'
|
|
622
786
|
: summary.ok ? '✔ all checks passed' : '✘ one or more checks failed';
|
|
623
|
-
|
|
787
|
+
const ran = summary.checks_run === 0 ? '' : coverage(summary, checks);
|
|
788
|
+
writeLine(stderr, `${status} in ${summary.total_duration_ms}ms${ran}`);
|
|
624
789
|
writeLine(stderr, 'report: .check/summary.json');
|
|
625
790
|
if (digestWritten)
|
|
626
791
|
writeLine(stderr, 'digest: .check/digest.md');
|
|
@@ -639,34 +804,60 @@ function computeExitCode(summary, strict, json, stderr) {
|
|
|
639
804
|
}
|
|
640
805
|
return summary.ok ? 0 : 1;
|
|
641
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* On a red run, the best-effort recover hint: when the run's new-not-in-baseline
|
|
809
|
+
* findings were grandfathered in a recent committed baseline, one stderr line
|
|
810
|
+
* says so. The difference between "a wall of new debt" and "a merge dropped
|
|
811
|
+
* baseline entries" is a diagnosis the run output alone cannot make — and this
|
|
812
|
+
* is the moment someone is actually reading it. Strictly bounded and silent on
|
|
813
|
+
* any failure (see {@link historicalHint}); a hint must never slow or break a run.
|
|
814
|
+
*/
|
|
815
|
+
async function maybeRecoverHint(ctx, ok, newKeys) {
|
|
816
|
+
if (ok || newKeys.length === 0)
|
|
817
|
+
return;
|
|
818
|
+
const hint = await historicalHint(ctx.cwd, newKeys);
|
|
819
|
+
if (hint === null)
|
|
820
|
+
return;
|
|
821
|
+
writeLine(ctx.stderr, `hint: ${hint.matched} of ${hint.total} new finding(s) were grandfathered until ${hint.shortSha} — a merge may have dropped baseline entries; see \`checkride recover\`\n`);
|
|
822
|
+
}
|
|
642
823
|
/** Run the selected checks against `cwd`, persist output, write the summary. */
|
|
643
824
|
export async function runChecks(options) {
|
|
644
|
-
|
|
645
|
-
|
|
825
|
+
// Clear the interrupt latch. On the CLI path this is a no-op — the process
|
|
826
|
+
// re-raises the signal and dies — but `runChecks` is exported, and a
|
|
827
|
+
// long-lived programmatic consumer that took one SIGINT would otherwise have
|
|
828
|
+
// every later run silently spawn nothing (`spawnCheck` starts nothing while
|
|
829
|
+
// latched). A new call necessarily follows the previous one's return, so
|
|
830
|
+
// there is no in-flight run for this to un-latch.
|
|
831
|
+
interrupted = false;
|
|
832
|
+
const base = resolveRunContext(options);
|
|
833
|
+
const resolved = resolveChecks({ slots: base.slots, adapters: base.adapters, config: base.config, cwd: base.cwd });
|
|
646
834
|
// A usage error before any side effect: no `.check/` dir, no run, exit 2.
|
|
647
835
|
validateSelection(resolved, options);
|
|
648
|
-
await mkdir(join(
|
|
836
|
+
await mkdir(join(base.cwd, '.check'), { recursive: true });
|
|
649
837
|
const selected = selectChecks(resolved, options);
|
|
838
|
+
// The status-line column is sized once the selection is known, so a long
|
|
839
|
+
// custom-check name widens the column instead of overflowing it.
|
|
840
|
+
const ctx = { ...base, nameWidth: nameWidth(selected) };
|
|
650
841
|
if (!ctx.json)
|
|
651
842
|
writeLine(ctx.stderr, `\nRunning ${selected.length} check(s)...\n`);
|
|
652
|
-
// `--bail` is fail-fast sequential
|
|
843
|
+
// `--bail` is fail-fast sequential; a `--concurrency > 1` passed alongside
|
|
653
844
|
// is safe but moot, so say so once — not a usage error, the run just goes slow.
|
|
654
845
|
if (ctx.bail && (options.concurrency ?? 0) > 1 && !ctx.json) {
|
|
655
846
|
writeLine(ctx.stderr, '--concurrency ignored under --bail (fail-fast runs sequentially).');
|
|
656
847
|
}
|
|
657
|
-
// `total_duration_ms` is wall-clock of the execution phase
|
|
848
|
+
// `total_duration_ms` is wall-clock of the execution phase, so measure
|
|
658
849
|
// around it rather than summing per-check durations (which diverge under
|
|
659
850
|
// concurrency). The report array is assembled in `selected` order, not
|
|
660
851
|
// completion order, so it stays byte-reproducible regardless of interleaving.
|
|
661
852
|
const startedAt = performance.now();
|
|
662
|
-
const { checks, runs, observed, brokeEarly } = await executeChecks(selected, ctx);
|
|
853
|
+
const { checks, runs, observed, newKeys, brokeEarly } = await executeChecks(selected, ctx);
|
|
663
854
|
const totalDurationMs = Math.round(performance.now() - startedAt);
|
|
664
855
|
await maybeRatchet(ctx.cwd, ctx.baseline, observed, isPartialRun(options, ctx.changed, brokeEarly), ctx.json, ctx.stderr);
|
|
665
856
|
const summary = buildSummary(checks, totalDurationMs);
|
|
666
857
|
await writeFileAtomic(join(ctx.cwd, '.check', 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`);
|
|
667
858
|
// `--digest`: write (or, on green, clear) the token-bounded failure excerpt.
|
|
668
859
|
// A file beside summary.json, never a stdout stream, so the machine-output
|
|
669
|
-
// split holds
|
|
860
|
+
// split holds. Raw `.check/<slot>.json` files are already persisted and
|
|
670
861
|
// untouched — the digest only reads them.
|
|
671
862
|
const digestWritten = (options.digest ?? false) ? await writeDigest(ctx.cwd, runs, checks) : false;
|
|
672
863
|
if (ctx.json) {
|
|
@@ -674,6 +865,13 @@ export async function runChecks(options) {
|
|
|
674
865
|
}
|
|
675
866
|
else {
|
|
676
867
|
reportSummary(ctx.stderr, summary, checks, ctx.adapters, digestWritten);
|
|
868
|
+
// A mangled committed baseline (a botched merge, usually) masks nothing, so
|
|
869
|
+
// every grandfathered finding above reported red — say so where the person
|
|
870
|
+
// reading the wall of red will actually look, right under the summary.
|
|
871
|
+
if (ctx.baselineState === 'unparseable') {
|
|
872
|
+
writeLine(ctx.stderr, `baseline: ${BASELINE_FILE} is present but unparseable — possibly a botched merge; grandfathered findings report red. See \`checkride recover\`.\n`);
|
|
873
|
+
}
|
|
874
|
+
await maybeRecoverHint(ctx, summary.ok, newKeys);
|
|
677
875
|
}
|
|
678
876
|
const exitCode = computeExitCode(summary, options.strict ?? false, ctx.json, ctx.stderr);
|
|
679
877
|
return { ok: summary.ok, summary, exitCode, runs };
|
|
@@ -712,13 +910,14 @@ export async function runFix(options) {
|
|
|
712
910
|
return { ok: true, exitCode: 0, ran: [] };
|
|
713
911
|
}
|
|
714
912
|
const ran = [];
|
|
913
|
+
const width = nameWidth(fixable);
|
|
715
914
|
let ok = true;
|
|
716
915
|
for (const r of fixable) {
|
|
717
916
|
const adapter = r.adapter;
|
|
718
917
|
if (!adapter)
|
|
719
918
|
continue;
|
|
720
|
-
writeLine(stderr, ` ▸ fix ${r.slot.padEnd(
|
|
721
|
-
// oxlint-disable-next-line no-await-in-loop -- fixers mutate the working tree; running them sequentially prevents two (
|
|
919
|
+
writeLine(stderr, ` ▸ fix ${r.slot.padEnd(width)} (${adapter.name})`);
|
|
920
|
+
// oxlint-disable-next-line no-await-in-loop -- fixers mutate the working tree; running them sequentially prevents two (for example oxlint --fix and prettier --write) racing on the same files.
|
|
722
921
|
const outcome = await fixRunner(adapter, { cwd, pm });
|
|
723
922
|
ran.push(adapter.name);
|
|
724
923
|
writeLine(stderr, outcome.ok ? ` ✔ ${r.slot}` : ` ✘ ${r.slot} (exit ${outcome.exit_code})`);
|