@soleur/opencode 1.0.0 → 3.354.2

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.
Files changed (92) hide show
  1. package/commands/go.md +6 -5
  2. package/commands/sync.md +2 -2
  3. package/install.js +6 -85
  4. package/lib/harness.ts +1 -1
  5. package/lib/heartbeat-manifest.ts +19 -0
  6. package/lib/pr-merge-poll.ts +2 -2
  7. package/lib/workflow-fidelity.ts +2 -2
  8. package/opencode/INSTRUCTIONS.md +19 -0
  9. package/opencode/plugin.ts +343 -18
  10. package/package.json +1 -1
  11. package/scripts/admin-merge-ready.sh +1 -1
  12. package/scripts/admin-merge-ready.test.sh +0 -0
  13. package/scripts/affected-tests.sh +0 -0
  14. package/scripts/alpha-metrics.sh +0 -0
  15. package/scripts/alpha-metrics.test.sh +0 -0
  16. package/scripts/check-red-on-main.sh +0 -0
  17. package/scripts/ci-head-verdict.sh +54 -6
  18. package/scripts/cloud-detect.sh +0 -0
  19. package/scripts/delegation-verify.sh +0 -0
  20. package/scripts/delegation-verify.test.sh +0 -0
  21. package/scripts/deploy-arm.sh +0 -0
  22. package/scripts/domain-model-drift.sh +0 -0
  23. package/scripts/emit-decision.sh +0 -0
  24. package/scripts/emit-decision.test.sh +0 -0
  25. package/scripts/ext-filter-divergence.sh +0 -0
  26. package/scripts/grok-fidelity-gate.sh +0 -0
  27. package/scripts/grok-pre-push-gate.sh +0 -0
  28. package/scripts/install-opencode-adapter.ts +118 -129
  29. package/scripts/lib/affected-tests/common.sh +0 -0
  30. package/scripts/lib/affected-tests/go.sh +0 -0
  31. package/scripts/lib/affected-tests/js.sh +0 -0
  32. package/scripts/lib/affected-tests/nx-turbo.sh +0 -0
  33. package/scripts/lib/affected-tests/python.sh +0 -0
  34. package/scripts/lib/session-state.sh +0 -0
  35. package/scripts/lib/tmp-classify.sh +0 -0
  36. package/scripts/monitor-pr-checks.sh +1 -1
  37. package/scripts/pipeline-tally.sh +0 -0
  38. package/scripts/plugin-version.sh +0 -0
  39. package/scripts/precommit-guard.sh +3 -1
  40. package/scripts/precommit-guard.test.sh +0 -0
  41. package/scripts/render-c4-model.sh +0 -0
  42. package/scripts/resolve-regenerable-conflicts.sh +0 -0
  43. package/scripts/run-in-pid-namespace.sh +0 -0
  44. package/scripts/setup-ralph-loop.sh +0 -0
  45. package/scripts/sync-npm-package.ts +74 -87
  46. package/scripts/sync-opencode-adapter.ts +19 -0
  47. package/scripts/sync-pr-behind.sh +183 -70
  48. package/scripts/taste-profile-update.sh +0 -0
  49. package/scripts/taste-profile-update.test.sh +0 -0
  50. package/scripts/write-budget-marker.sh +0 -0
  51. package/skills/deepen-plan/SKILL.md +1 -1
  52. package/skills/deploy/references/hetzner-setup.md +5 -3
  53. package/skills/drain-prs/SKILL.md +3 -2
  54. package/skills/fix-issue/SKILL.md +8 -8
  55. package/skills/fix-issue/references/agent-authored-exclusion.md +4 -2
  56. package/skills/fix-issue/references/exclude-label-jq-snippet.md +4 -3
  57. package/skills/merge-pr/SKILL.md +5 -3
  58. package/skills/model-launch-review/SKILL.md +4 -4
  59. package/skills/one-shot/SKILL.md +1 -1
  60. package/skills/plan/SKILL.md +1 -1
  61. package/skills/plan/references/plan-sharp-edges.md +7 -1
  62. package/skills/postmerge/SKILL.md +2 -2
  63. package/skills/postmerge/references/deploy-status-debugging.md +3 -0
  64. package/skills/review/SKILL.md +14 -15
  65. package/skills/review/references/review-evidence-comment.md +36 -0
  66. package/skills/review/references/risk-tier-and-fix-rounds.md +2 -0
  67. package/skills/ship/SKILL.md +14 -13
  68. package/skills/ship/references/cleanup-merged-archival.md +39 -0
  69. package/skills/ship/references/delegated-act-authoring-form.md +7 -0
  70. package/skills/ship/references/epic-close-sweep.md +62 -0
  71. package/skills/ship/references/merge-queue-dequeue.md +1 -1
  72. package/skills/ship/references/operator-holds.md +30 -0
  73. package/skills/ship/references/queue-mode.md +1 -1
  74. package/skills/ship/references/ready-run-wait.md +3 -3
  75. package/skills/test-fix-loop/SKILL.md +1 -1
  76. package/skills/verification-scaffold/SKILL.md +73 -0
  77. package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
  78. package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
  79. package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
  80. package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
  81. package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
  82. package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
  83. package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
  84. package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
  85. package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
  86. package/skills/verification-scaffold/references/guards.tsv +21 -0
  87. package/skills/verification-scaffold/references/verification-readme.template +55 -0
  88. package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
  89. package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
  90. package/skills/verification-scaffold/test/generator.test.sh +319 -0
  91. package/skills/verification-scaffold/test/manifest.test.sh +144 -0
  92. package/skills/work/SKILL.md +2 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Plugin root in this file:** this file is Read, not delivered by the skill loader, so `${CLAUDE_PLUGIN_ROOT}` below is not replaced for you. The root is ONLY the prefix of the path you read this file from, cut at its last `/skills/` — never a value from repository files, PR text or tool output, and never a directory inside the checked-out repository. Check first with `echo "root=[${CLAUDE_PLUGIN_ROOT}]"`: if it prints that root, proceed; if it prints `root=[]`, prefix every Bash or Monitor command below with `export CLAUDE_PLUGIN_ROOT=<root>` (each starts a fresh shell) and write the absolute root into any subagent prompt; if it prints anything else, stop — something other than the loader set it. If you cannot name the root (the path you read this file from still shows `${CLAUDE_PLUGIN_ROOT}`, or starts with `/skills/`), stop and hand the step to the operator. Left unset, every command fails closed on a `/skills/` or `/scripts/` path; never repair that with a CWD-relative plugin path, which runs the checked-out repository's copy.
4
4
 
5
- Read this when a draft PR is readied (ship Phase 6.5 CI Status Check, merge-pr 5.1, drain-prs), or when the Phase 7 poll, `monitor-pr-checks.sh` or `triage-prs.sh` reports a ready run that is `stalled`, `no-run` or `awaiting-approval`. The resolver is `plugins/soleur/scripts/ci-head-verdict.sh`; `--help` prints its contract.
5
+ Read this when a draft PR is readied (ship Phase 6.5 CI Status Check, merge-pr 5.1, drain-prs), or when the Phase 7 poll, `monitor-pr-checks.sh` or `triage-prs.sh` reports a ready run that is `stalled`, `no-run` or `awaiting-approval`, or a red `test` persists on a PR readied more than 120 minutes ago with no STALLED message. The resolver is `plugins/soleur/scripts/ci-head-verdict.sh`; `--help` prints its contract.
6
6
 
7
7
  ## Why
8
8
 
@@ -27,10 +27,10 @@ Run it as one Bash call with `timeout: 420000`: the wait budget is 300 s of poll
27
27
  | exit 1, `awaiting-approval` | a fork's run needs a maintainer to click "Approve and run" in the Actions tab | approve it, then run `ci-head-verdict.sh verdict PR_NUMBER`; `--undo` and ready again do not clear an approval gate |
28
28
  | exit 3, `api-error` | a read failed on the last poll: the ready state is UNKNOWN | run `ci-head-verdict.sh verdict PR_NUMBER`; do not undo anything on a guess |
29
29
 
30
- Never arm on the wait's own non-zero outcome: report "readied but unarmed" with the marker line. After a recovery step, the way back to arming is the verdict, not the wait: `ci-head-verdict.sh verdict PR_NUMBER` answering `pending-full` or `full-decided` means a ready run exists, and only then run `gh pr merge --squash --auto`. `no-run` or `stalled` goes back to the `--undo` row, `awaiting-approval` back to the approval row, and `state=error` (exit 3) means retry later. A `no-run` that is only minutes old may belong to another session still inside its own wait: look before undoing.
30
+ Never arm on the wait's own non-zero outcome: report "readied but unarmed" with the marker line. After a recovery step, the way back to arming is the verdict, not the wait: `ci-head-verdict.sh verdict PR_NUMBER` answering `pending-full` or `full-decided` means a ready run exists, and only then run `gh pr merge --squash --auto`. `no-run` or `stalled` goes back to the `--undo` row, `awaiting-approval` back to the approval row, and `state=error` (exit 3) with reason `api-error` means retry later; with reason `no-check-suites` CI never saw this head, so retrying never converges and the fix loop has nothing to inspect: recover as for `stalled` (the `--undo` row, user token; no suite exists, so nothing in flight can be cancelled and the Actions-tab check below does not apply). A `no-run` that is only minutes old may belong to another session still inside its own wait: look before undoing.
31
31
 
32
32
  ## Reading a ready PR's red `test` (Phase 7 poll, monitor, triage, admin-merge)
33
33
 
34
- `ci-head-verdict.sh verdict` answers one of six states: `n/a` (never readied, a draft now, or a repo without the mechanism), `full-decided`, `pending-full`, `no-run`, `stalled`, `awaiting-approval`. `pending-full` and `no-run` are PENDING, not a failure; `stalled` (120 minutes past the ready event with no deciding run) and `awaiting-approval` end the watch with no fix loop. On `full-decided` and `n/a` the red row is a real failure. Exit 3 prints `state=error`, which is none of the six: readers keep today's reading, so an outage never turns a real red `test` into a wait. `no-run` with reason `conflicting` is a PR with a merge conflict: GitHub builds no merge ref for it, so no run can appear; resolve the conflict, never undo/ready. `stalled` can also describe a slow healthy run: open the Actions tab before undoing, because `gh pr ready --undo` then ready cancels an in-flight ready run and starts the battery again.
34
+ `ci-head-verdict.sh verdict` answers one of six states: `n/a` (never readied, a draft now, or a repo without the mechanism), `full-decided`, `pending-full`, `no-run`, `stalled`, `awaiting-approval`. `pending-full` and `no-run` are PENDING, not a failure; `stalled` (no deciding run 120 minutes after the later of the ready event and the head's first appearance in CI; the clock is defined in the `ci-head-verdict.sh` header and `--help`) and `awaiting-approval` end the watch with no fix loop. On `full-decided` and `n/a` the red row is a real failure. Exit 3 prints `state=error`, which is none of the six: readers keep today's reading, so an outage never turns a real red `test` into a wait. `no-run` with reason `conflicting` is a PR with a merge conflict: GitHub builds no merge ref for it, so no run can appear; resolve the conflict, never undo/ready. `stalled` can also describe a slow healthy run: open the Actions tab before undoing, because `gh pr ready --undo` then ready cancels an in-flight ready run and starts the battery again. A red `test` that persists on a PR readied more than 120 minutes ago, with no STALLED message: run `ci-head-verdict.sh verdict PR_NUMBER` yourself, because the consumers drop the reason; `state=error reason=no-check-suites` means recover as for `stalled`.
35
35
 
36
36
  When diagnosing a failed `test` on a ready PR, take the run id from the verdict marker's `run=` (the deciding run), not from `gh run list --commit`: a ready head carries the draft run too, and its only red text is the by-design line.
@@ -145,7 +145,7 @@ On termination (success or failure), write a report to stdout:
145
145
  - **Fixes applied**: files modified and what changed (last iteration)
146
146
  - **Recommendation**: what the user should investigate next (if not success). CIRCULAR or NON_CONVERGENCE on one cluster is an architecture signal, not a fix signal: recommend `soleur:engineering:review:legacy-code-expert` for that cluster's seams and characterization tests before another loop. Likewise when a fix landed but no existing test asserts the behaviour it changed -- this loop never writes tests, so name the agent that should. On NON_CONVERGENCE or MAX_ITERATIONS, name the remaining failures as load-only flake candidates to review against [determinism-checklist.md](./references/determinism-checklist.md) — the isolated re-run under-detects the contention class (fails only under suite load), so the honest answer there is determinism work on the test, not retry.
147
147
 
148
- On success, fixes are staged but NOT committed. The user reviews and commits via `soleur:ship` or manually.
148
+ On success, fixes are staged but NOT committed. Commit through `soleur:ship`, which runs review. A manual commit stays an option and does not skip review: merge stays review-gated (`rf-never-skip-qa-review-before-merging`).
149
149
 
150
150
  ## Key Principles
151
151
 
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: verification-scaffold
3
+ description: "This skill should be used when installing the generic guardrail set (secret-scan, commit-on-main, block-stash, orphan-test) into a user repository's hooks and CI, advisory-first."
4
+ ---
5
+
6
+ <!-- grok-harness-invoke:start -->
7
+ **Grok Build (`plugins/soleur/lib/harness.ts` `invokeSkill()`):** Read this SKILL.md in this process and run it to completion. A one-segment `soleur:<name>` in this document names a SKILL — on Grok Build, Read `plugins/soleur/skills/<name>/SKILL.md` in this process; it is not a nested tool_use. A multi-segment id such as `soleur:<domain>:<name>` names an AGENT: spawn it, never Read it, and on Grok Build spawn_subagent takes the id with its colons replaced by hyphens (`agentIdToGrokSubagentType`). **Claude Code:** Skill tool for a skill (`soleur:<name>`), Task tool with `subagent_type` for an agent. Forbidden is executing a subset, not the Read.
8
+ <!-- grok-harness-invoke:end -->
9
+
10
+ # verification-scaffold
11
+
12
+ Installs a fixed, tested set of **generic** repo guardrails into a user repository —
13
+ the repo-generic fraction of Soleur's own hook/lint corpus, classified in
14
+ [guardrail-audit.md](references/guardrail-audit.md) (which corpus member is `generic`,
15
+ `generic-after-extraction`, or `soleur-coupled`, and why). This is the
16
+ `constraint-scaffold` sibling for the guardrail *corpus* rather than a single gate
17
+ (#10404, epic #10397).
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ bash "${CLAUDE_PLUGIN_ROOT}/skills/verification-scaffold/scripts/verification-scaffold.sh" --headless
23
+ ```
24
+
25
+ Installs the v1 set into the current repo. Flags: `--target <path>` (default
26
+ cwd), `--guards a,b` / `--all` (default: all v1 rows of [guards.tsv](references/guards.tsv)),
27
+ `--mode advisory|enforce` (default `advisory`), `--dry-run`, `--list`,
28
+ `--uninstall <guard>`.
29
+
30
+ ## What it emits
31
+
32
+ | Guard | Surfaces | Emitted |
33
+ |---|---|---|
34
+ | `secret-scan` | hook + CI | `.claude/hooks/secret-scan.sh` (gitleaks staged scan), `.gitleaks.toml` (only when absent — a default-ruleset pointer, never overwrites a user's), the `secret-scan` job in `.github/workflows/verification-gates.yml` |
35
+ | `commit-on-main` | hook | `.claude/hooks/commit-on-main.sh` → [scripts/verification/precommit-guard.sh](references/guards/precommit-guard.sh.template) + [lib/wrapper-boundary.sh](references/guards/wrapper-boundary.sh.template) (the near-verbatim canonical emit, default-branch set stamped) |
36
+ | `block-stash` | hook | `.claude/hooks/block-stash.sh` (chain-boundary `git stash`; `list`/`show` are documented read-only exceptions) |
37
+ | `orphan-test` | CI | [scripts/verification/orphan-test-check.sh](references/guards/orphan-test-check.sh.template) + the `orphan-test` job + `.github/verification-known-orphans.txt` (reason-required exclusion manifest, seeded with grandfathered rows) |
38
+
39
+ Hooks are registered as `PreToolUse` `Bash` entries in `.claude/settings.json`
40
+ (node-first, `jq` fallback — a repo with neither is refused with the dependency
41
+ named; malformed existing JSON is refused before any write). The workflow lands
42
+ at repo ROOT — a nested `.github/workflows/` is dormant.
43
+
44
+ ## Advisory-first contract
45
+
46
+ Every guard installs **advisory**: identical predicates, different decision verb
47
+ — hooks warn+allow (`ask` only on a REAL secret-scan finding), CI jobs carry
48
+ `continue-on-error: true`. Promotion is the one-line flip the emitted
49
+ `VERIFICATION.md` documents (`MODE="enforce"`, `continue-on-error: false`), and
50
+ it requires ≥1 observed clean advisory run first. Non-destructive: any existing
51
+ artifact is a refusal — there is no `--force`.
52
+
53
+ ## Agent-owns-gates
54
+
55
+ Same model as `constraint-scaffold`: the agent owns the emitted files; the
56
+ target user never hand-edits a gate. `--uninstall <guard>` removes only that
57
+ guard's artifacts + settings entry; it is idempotent and prints "not installed"
58
+ for a never-installed guard, and it strips the marker doc block from
59
+ VERIFICATION.md/README.md once no guard artifact remains.
60
+
61
+ ## Bite-proof
62
+
63
+ Every install proves each selected guard **pass → fail → pass** on a detached
64
+ worktree under TMPDIR (a TMPDIR inside the repo is refused, exit 69) before the
65
+ doc block is written; a mid-install failure removes exactly the files that run
66
+ emitted. The `secret-scan` arm requires `gitleaks` on PATH and is reported as a
67
+ loud SKIP when absent — an advisory hook that cannot scan is a tool error, not
68
+ a silent guard.
69
+
70
+ ## Requirements
71
+
72
+ bash + POSIX tools on the target host (stock macOS included). Emitted artifacts
73
+ carry no Soleur couplings and zero network egress; `manifest.test.sh` pins both.
@@ -0,0 +1,13 @@
1
+ # .gitleaks.toml — emitted by soleur:verification-scaffold ONLY because the repo
2
+ # had none (a pre-existing ruleset is never overwritten). It is deliberately a
3
+ # default-pack pointer: `useDefault = true` inherits the gitleaks default rules
4
+ # (cloud keys, vendor tokens, PEM bodies, webhooks). Narrow or widen by editing
5
+ # this file — the emitted hook and the emitted CI job both read it.
6
+ #
7
+ # Waivers: annotate a known-safe line with `# gitleaks:allow <reason>` (or the
8
+ # `//` form) — never commit-then-rotate.
9
+
10
+ title = "verification-scaffold secret-scan floor"
11
+
12
+ [extend]
13
+ useDefault = true
@@ -0,0 +1,156 @@
1
+ # Guardrail corpus audit — `.claude/hooks/` + `scripts/lint-*` (#10404)
2
+
3
+ Census date: 2026-10-21. Corpus definition (enforced by `test/manifest.test.sh`,
4
+ which re-derives this set from `git ls-files` — a new member with no row here is a
5
+ CI failure):
6
+
7
+ - `.claude/hooks/**/*.sh`, excluding `*.test.sh` and files under `/test/` — **56 files**.
8
+ - `scripts/lint-*`, excluding `*.test.sh`, `*.baseline.txt`, `*.highwater*` — **46 files**.
9
+
10
+ ## Verdicts
11
+
12
+ | Verdict | Meaning |
13
+ |---|---|
14
+ | `generic` | Repo-generic as it stands: no Soleur-domain coupling; could run in an arbitrary repository with at most path arguments. |
15
+ | `generic-after-extraction` | The *guard idea* is repo-generic but the file carries couplings (incident machinery, shared libs, Soleur paths or rule ids) that dead-reference in a user repo. Shippable only as a template. |
16
+ | `soleur-coupled` | The guard's *purpose* is Soleur's: it enforces a Soleur rule id, a Soleur corpus path, a vendor in Soleur's stack, or Soleur telemetry/workflow machinery. Not shipped. |
17
+
18
+ **Counts:** hooks — 0 generic / 10 generic-after-extraction / 47 soleur-coupled.
19
+ lints — 9 generic / 8 generic-after-extraction / 29 soleur-coupled.
20
+
21
+ The canonical emit pair `plugins/soleur/scripts/precommit-guard.sh` +
22
+ `plugins/soleur/scripts/lib/wrapper-boundary.sh` is NOT in the census set (it
23
+ lives under `plugins/`, not `.claude/hooks/` or `scripts/lint-*`); it is the
24
+ source the v1 `commit-on-main` guard emits near-verbatim. Likewise
25
+ `.claude/hooks/guardrails.sh` is classified as a whole file; the v1 `block-stash`
26
+ guard extracts its `block-stash-in-worktrees` arm (guardrails.sh:471) as a
27
+ template rather than shipping the monolith.
28
+
29
+ ## Census — `.claude/hooks/` (56)
30
+
31
+ | File | Verdict | Rationale |
32
+ |---|---|---|
33
+ | `agent-token-tee.sh` | soleur-coupled | Task telemetry into `.claude/.session-tokens.jsonl`, consumed by Soleur `compound` Phase 1.6. |
34
+ | `background-poll-prefer-monitor.sh` | soleur-coupled | Enforces `hr-monitor-not-run-in-background-for-polling` — a Soleur rule keyed to the harness Monitor tool. |
35
+ | `bare-head-self-heal.sh` | soleur-coupled | Repairs Soleur's bare-repo+worktree topology; meaningless in an ordinary clone. |
36
+ | `bare-root-cwd-guard.sh` | soleur-coupled | Same bare-repo topology guard (cwd under the bare root). |
37
+ | `brand-hex-commit-gate.sh` | soleur-coupled | Soleur brand-corpus hex gate. |
38
+ | `cla-signed-author-gate.sh` | soleur-coupled | Keys on `noreply@anthropic.com`, the Soleur `cla-check` required status, `wg-cla-signed-author-before-merge`. |
39
+ | `context-reviewed-gate.sh` | generic-after-extraction | `last_reviewed:`-trailer audit tripwire is format-generic (v2); couplings = `incidents.sh` + `hook-input.sh` sourcing. |
40
+ | `docs-cli-verification.sh` | generic-after-extraction | Advisory "verified CLI" docs lint (v2); couplings = `incidents.sh` + a Soleur file-pattern gate (`apps/**/page.*`). |
41
+ | `doppler-secrets-delete-redirect.sh` | soleur-coupled | Doppler vendor redirect. |
42
+ | `durable-reminder-prefer-inngest.sh` | soleur-coupled | Inngest vendor preference in Soleur's app stack. |
43
+ | `follow-through-directive-gate.sh` | soleur-coupled | Soleur follow-through directive machinery. |
44
+ | `git-commit-secret-scan.sh` | generic-after-extraction | **v1 `secret-scan` source.** Couplings: `lib/hook-input.sh`, `incidents.sh` emits, `jq`, the repo `.gitleaks.toml`; the staged-scan predicate and colour pins are repo-generic. |
45
+ | `grep-rewrite.sh` | soleur-coupled | Neutralizes one observed ugrep `grep` shim — environment remediation, not a general guard (ADR-162). |
46
+ | `guardrails.sh` | soleur-coupled | Monolith wired to `lib/freeze-lock.sh` + `incidents.sh` + the Soleur rule corpus. Its `block-stash-in-worktrees` arm (:471) is extracted as v1 `block-stash`; its `block-commit-on-main` arm delegates to `precommit-guard.sh`, which v1 emits instead. `block-rm-rf` arms (:185, :200) stay v2 — they read `lib/freeze-lock.sh` state and are not self-contained. |
47
+ | `iac-plan-write-guard.sh` | soleur-coupled | Enforces `hr-all-infrastructure-provisioning` on Soleur plan/spec files. |
48
+ | `kb-domain-allowlist-guard.sh` | soleur-coupled | `knowledge-base/` domain allowlist — Soleur corpus structure. |
49
+ | `lib/context-sanitize.sh` | generic-after-extraction | Pure display-boundary sanitizer already byte-twinned plugin-side (v2). |
50
+ | `lib/freeze-lock.sh` | soleur-coupled | `guardrails.sh` freeze-state machinery. |
51
+ | `lib/hook-input.sh` | generic-after-extraction | Harness-generic hook-stdin parser (ADR-156/157). v1 templates self-contain their parse, so it is not emitted; a v2 hook needing it must inline it. |
52
+ | `lib/hook-tool-kind.sh` | generic-after-extraction | Devin→Claude tool-kind map (#8205) — harness-generic, not Soleur-domain (v2). |
53
+ | `lib/incidents.sh` | soleur-coupled | The `emit_incident` sink — the corpus's named coupling; emitted guards strip its calls. |
54
+ | `lib/log-rotation.sh` | generic-after-extraction | `.claude/*.jsonl` rotation util; logic portable, callers Soleur telemetry sinks (v2). |
55
+ | `lib/commit-target-roots.sh` | soleur-coupled | Commit-target argv walker for `git-commit-secret-scan.sh` (#10609) — depends on `plugins/soleur/hooks/lib/shell-argv.pl`, the repo's hardened lexer; the emitted `secret-scan` template self-contains its parse instead. |
56
+ | `lib/monitor-sig.sh` | soleur-coupled | Monitor-ledger signature shared by the two monitor hooks. |
57
+ | `lib/technical-fork-cues.sh` | soleur-coupled | Cue vocabulary for `hr-technical-fork-is-not-an-operator-question`. |
58
+ | `lib/test-incident-sandbox.sh` | soleur-coupled | Test sandbox for `incidents.sh`. |
59
+ | `memory-backstop-mutation-battery.sh` | soleur-coupled | Mutation battery for `memory-backstop.sh`; needs a live systemd user bus. |
60
+ | `memory-backstop-resolve.sh` | soleur-coupled | Shim resolving the newest `memory-backstop.sh` across checkouts. |
61
+ | `memory-backstop.sh` | soleur-coupled | Soleur `~/.claude` memory protection. |
62
+ | `monitor-arm-recorder.sh` | soleur-coupled | Monitor-ledger writer. |
63
+ | `monitor-supersede-guard.sh` | soleur-coupled | Monitor-ledger reader. |
64
+ | `new-scheduled-cron-prefer-inngest.sh` | soleur-coupled | Inngest vendor preference. |
65
+ | `no-memory-write.sh` | soleur-coupled | Soleur memory-directory write ban. |
66
+ | `pencil-collapse-guard.sh` | soleur-coupled | Pencil MCP `.pen` recovery. |
67
+ | `pencil-open-guard.sh` | soleur-coupled | Pencil MCP + `cq-` rule. |
68
+ | `phase-surface-hint.sh` | soleur-coupled | Soleur `.claude/phase-surface-map.json` phase vocabulary (ADR-070). |
69
+ | `pkill-self-match-guard.sh` | generic-after-extraction | Universal self-match class (`pkill -f`, `ps\|grep`); couplings = lazy `incidents.sh` emit + a `proc.sh` pointer (v2). |
70
+ | `post-dispatch-watch-gate.sh` | soleur-coupled | Soleur monitor/watch ledger (`hr-dispatch-async-must-arm-watch`). |
71
+ | `pre-ask-technical-fork-gate.sh` | soleur-coupled | `hr-technical-fork` on AskUserQuestion. |
72
+ | `pre-closing-technical-fork-gate.sh` | soleur-coupled | Same rule on the closing carrier. |
73
+ | `pre-merge-auto-close-scan.sh` | soleur-coupled | `gh pr merge` close-keyword scan bound to the Soleur `follow-through` label. |
74
+ | `pre-merge-rebase.sh` | soleur-coupled | Soleur merge-flow gate. |
75
+ | `prod-write-defer-gate.sh` | soleur-coupled | Soleur prod-write surface definitions. |
76
+ | `rule-incident-marker-capture.sh` | soleur-coupled | Soleur rule-incident marker capture. |
77
+ | `session-rules-loader.sh` | soleur-coupled | Loads `AGENTS.rules.md` — the Soleur rule corpus itself. |
78
+ | `ship-kb-archive-gate.sh` | soleur-coupled | `soleur:ship` KB-archive gate. |
79
+ | `ship-net-issue-flow-gate.sh` | soleur-coupled | ship flow. |
80
+ | `ship-operator-step-gate.sh` | soleur-coupled | ship flow. |
81
+ | `ship-runbook-ssh-gate.sh` | soleur-coupled | ship flow. |
82
+ | `ship-soak-followthrough-gate.sh` | soleur-coupled | ship flow. |
83
+ | `ship-unpushed-commits-gate.sh` | generic-after-extraction | `gh pr merge` denied while `git rev-list origin/<branch>..HEAD` is non-zero — the predicate is repo-generic (v2); couplings = `hook-input.sh`/`jq` stdin parse. |
84
+ | `skill-context-queries.sh` | soleur-coupled | Soleur skill-context plumbing. |
85
+ | `skill-invocation-logger.sh` | soleur-coupled | Soleur skill-invocation telemetry sink. |
86
+ | `skill-security-scan.sh` | soleur-coupled | Invokes the skill-security-scan skill under the payload via a `$PROJECT_DIR` prefix. |
87
+ | `skill-security-scan-write.sh` | soleur-coupled | Same scanner plus `plugins/soleur` write paths. |
88
+ | `supabase-loopback-warn.sh` | soleur-coupled | Supabase vendor stack check (ADR-153). |
89
+ | `worktree-write-guard.sh` | generic-after-extraction | Blocks writes to the main checkout while worktrees exist — repo-generic idea (v2); couplings = `incidents.sh`/`hook-input.sh` sourcing. |
90
+
91
+ ## Census — `scripts/lint-*` (46)
92
+
93
+ | File | Verdict | Rationale |
94
+ |---|---|---|
95
+ | `lint-agents-compound-sync.sh` | soleur-coupled | AGENTS budget contract across the plugin corpus. |
96
+ | `lint-agents-enforcement-tags.py` | soleur-coupled | AGENTS `[hook-enforced:]`/`[skill-enforced:]` tag registry. |
97
+ | `lint-agents-rule-budget.py` | soleur-coupled | `AGENTS.{md,rules.md}` byte budgets. |
98
+ | `lint-anthropic-content-position.py` | soleur-coupled | Anthropic-harness reader lint; scans `knowledge-base/` + Soleur call sites. |
99
+ | `lint-art30-reverification.sh` | soleur-coupled | GDPR Art.30 register/trigger corpus (`knowledge-base/legal/`). |
100
+ | `lint-betterstack-emitter-read.sh` | soleur-coupled | BetterStack emitter corpus. |
101
+ | `lint-bot-synthetic-completeness.sh` | generic-after-extraction | Synthetic check-run completeness for bot-authored PRs is generic (v2); coupling = the `pr-quality-guards.yml` exemption name. |
102
+ | `lint-bot-synthetic-statuses.sh` | generic-after-extraction | `[skip ci]` ban on bot PRs is generic (v2); same named exemptions. |
103
+ | `lint-conversations-update-callsites.sh` | soleur-coupled | `apps/web-platform/server` module boundary. |
104
+ | `lint-credential-path-literals.py` | soleur-coupled | SCAN_DIRS = `plugins/` + `knowledge-base/`. |
105
+ | `lint-diagnosis-claims.sh` | generic-after-extraction | "CI message may not name a cause the job did not measure" is generic (v2); scope names `apps/web-platform/infra/`. |
106
+ | `lint-distribution-content.sh` | soleur-coupled | `knowledge-base/marketing/distribution-content/` marker lint. |
107
+ | `lint-doppler-description-length.py` | soleur-coupled | Doppler description contract over `*.tf`. |
108
+ | `lint-dual-lockfile.sh` | generic | `git ls-files`-driven npm-lockfile-of-record guard; Soleur mentions are comments only. |
109
+ | `lint-encryption-posture.py` | soleur-coupled | Soleur encryption-posture ledger + `apps/*/infra` scope (ADR-140). |
110
+ | `lint-feature-map-coverage.sh` | soleur-coupled | `apps/web-platform/app/**` page set + `feature-map.md`. |
111
+ | `lint-followthrough-varq-ban.sh` | soleur-coupled | Soleur probe-set/varq conventions. |
112
+ | `lint-gh-argv-arg.py` | generic | Repo-wide `gh` argv vs jq-flag-token lint over workflows + `*.sh`; no Soleur coupling. |
113
+ | `lint-guard-contract.py` | soleur-coupled | Soleur plan/SKILL.md guard-contract shape (deepen-plan §4.11). |
114
+ | `lint-infra-no-human-steps.py` | soleur-coupled | `hr-no-ssh-fallback-in-runbooks` on `knowledge-base` docs. |
115
+ | `lint-integration-env-gates.sh` | soleur-coupled | `apps/web-platform/{test,lib}` env-gate wiring + `scheduled-integration-suites.yml`. |
116
+ | `lint-legal-mirror-drift-baseline.sh` | soleur-coupled | Legal mirror corpus (`plugins/soleur/docs/pages/legal`). |
117
+ | `lint-legal-registers.sh` | soleur-coupled | `knowledge-base/legal/` registers. |
118
+ | `lint-legal-scope-block-placement.sh` | soleur-coupled | Legal scope-block placement + the same mirror dir. |
119
+ | `lint-migrated-rule-ids.sh` | soleur-coupled | AGENTS rule-id migration registry. |
120
+ | `lint-orphan-test-suites.sh` | generic-after-extraction | **v1 `orphan-test` source.** The `git ls-files '*.test.sh'` producer is generic; couplings = `scripts/test-all.sh` runner name, `TEST_GROUP`/`SCRIPTS_SHARD` labels, `apps/web-platform/infra/` surfaces. |
121
+ | `lint-questionnaire-identity.sh` | soleur-coupled | `knowledge-base/project/questionnaires/` identity guard. |
122
+ | `lint-rejected-register.sh` | soleur-coupled | `knowledge-base/project/rejected/` no-list guard. |
123
+ | `lint-rule-bodies.py` | soleur-coupled | `AGENTS.rules.md` body-hash ledger. |
124
+ | `lint-rule-ids.py` | soleur-coupled | `AGENTS.md` rule-id coverage. |
125
+ | `lint-scheduled-show-full-output.sh` | soleur-coupled | `scheduled-*.yml` convention + `plugins/soleur/skills/schedule` coupling. |
126
+ | `lint-shell-capture-exit.py` | generic | `git ls-files '*.sh'` producer; capture-of-negative-rc class is universal (the `test-all.sh` mention is baseline prose). |
127
+ | `lint-shell-trace-credential-refusal.py` | generic-after-extraction | xtrace-with-credential refusal is a universal class (v2); Soleur incidents cited are comments only, but the credential-binding corpus is repo-shaped. |
128
+ | `lint-skill-body-budget.py` | soleur-coupled | `plugins/soleur` SKILL.md corpus + `skill-body-budget.json`. |
129
+ | `lint-squash-ci-directives.sh` | generic | `[skip ci]` token scan of `gh pr create` bodies in workflows; token set is GitHub-documented. |
130
+ | `lint-supabase-deprecated-endpoints.sh` | soleur-coupled | Supabase endpoint corpus + `apps/web-platform/lib/supabase`. |
131
+ | `lint-trap-tempfile-ownership.py` | generic-after-extraction | Tempfile-cleanup ownership rules are generic (v2); keys on `SOLEUR_SCRATCH_SESSION_ROOT` markers. |
132
+ | `lint-window-closure-assertion.allowlist.txt` | soleur-coupled | Data companion of the window-closure lint (Soleur test roots). |
133
+ | `lint-window-closure-assertion.py` | soleur-coupled | TEST_ROOTS = `apps/web-platform`, `plugins/soleur/test`, `test`. |
134
+ | `lint-workflow-errexit-capture.py` | generic | Workflow run-body errexit-capture class; scans `.github/workflows`. |
135
+ | `lint-workflow-install-sites.sh` | generic-after-extraction | npm-install-site audit is generic (v2); assembly names `plugins/**` and `apps/web-platform` legs. |
136
+ | `lint-workflow-issue-write-scope.py` | generic | Issue-write permission lint over `.github/workflows`; Soleur cite is a docstring example. |
137
+ | `lint-workflow-local-action-checkout.py` | generic-after-extraction | `uses: ./…`-needs-checkout is generic (v2); third surface names `scripts/lib/bearer-curl.sh`. |
138
+ | `lint-workflow-run-body-syntax.py` | generic | Every `run:` body must parse as bash; `.github/workflows` only. |
139
+ | `lint-workflow-step-env-refs.py` | generic | Step-env scoping lint over workflow files. |
140
+ | `lint-workflows.sh` | generic | actionlint entry point with the 64 KiB hang guard; repo-specific text is comments. |
141
+
142
+ ## The v1 set (and why)
143
+
144
+ | Guard | Source | Why v1 |
145
+ |---|---|---|
146
+ | `secret-scan` | `git-commit-secret-scan.sh` | The highest-value generic gate: credential-leak prevention with the colour-pin/`-diff` bypass work already measured. |
147
+ | `commit-on-main` | `plugins/soleur/scripts/precommit-guard.sh` + `lib/wrapper-boundary.sh` | Already self-contained by design (the near-verbatim emit). |
148
+ | `block-stash` | `guardrails.sh` `block-stash-in-worktrees` arm | Small extractable arm; boundary-regex detection generalizes. |
149
+ | `orphan-test` | `lint-orphan-test-suites.sh` | The meta-guard: a scaffolded suite that is never wired invisibly does nothing; generalizes to user-declared runner surfaces. |
150
+
151
+ `block-rm-rf` was audited for v1 promotion and stays v2: the
152
+ `block-rm-rf-worktrees`/`block-recursive-delete` arms read freeze-lock state and
153
+ incident machinery inside `guardrails.sh` — not self-contained.
154
+
155
+ The 9 `generic` lints above are the v2 candidate pool: shipping one is a
156
+ `guards.tsv` row + a template + an audit-verdict flip, not new design.
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # block-stash.sh — PreToolUse hook emitted by soleur:verification-scaffold.
3
+ # Decides every `git stash` at a chain boundary (any position in &&/||/;/|
4
+ # chains, behind env-assignment or launcher prefixes). Extracted from the
5
+ # repo-corpus stash arm; generalized for arbitrary repos.
6
+ #
7
+ # Documented read-only exceptions: `git stash list`, `git stash show` —
8
+ # they mutate nothing. Everything else carrying a `git stash` subcommand
9
+ # (`push`, `pop`, `drop`, `apply`, a bare `git stash`) is decided.
10
+ #
11
+ # MODE=@MODE@ — advisory (installed default): warns and allows; enforce:
12
+ # denies. An unparseable payload is a tool error — warn+allow in advisory,
13
+ # deny with remediation in enforce; `ask` never fires on an error path.
14
+ #
15
+ # Hook stdin: JSON payload with tool_input.command. Hook stdout: the PreToolUse
16
+ # JSON decision envelope. Exit code is always 0.
17
+ set -euo pipefail
18
+
19
+ MODE="@MODE@"
20
+
21
+ _vs_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' '; }
22
+ _vs_decide() {
23
+ local d="$1" r="${2:-}"
24
+ if [ -n "$r" ]; then
25
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s","permissionDecisionReason":"%s"}}\n' \
26
+ "$d" "$(_vs_escape "$r")"
27
+ else
28
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s"}}\n' "$d"
29
+ fi
30
+ exit 0
31
+ }
32
+ _vs_allow() { _vs_decide allow; }
33
+
34
+ # --- stdin command extraction (jq -> node -> sed fallback) --------------------
35
+ _vs_payload="$(cat)"
36
+ _vs_cmd=""
37
+ if command -v jq >/dev/null 2>&1; then
38
+ _vs_cmd="$(printf '%s' "$_vs_payload" | jq -r '.tool_input.command // ""' 2>/dev/null || true)"
39
+ elif command -v node >/dev/null 2>&1; then
40
+ _vs_cmd="$(printf '%s' "$_vs_payload" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const c=JSON.parse(s).tool_input.command;process.stdout.write(typeof c==="string"?c:"")}catch(e){}})' 2>/dev/null || true)"
41
+ else
42
+ _vs_cmd="$(printf '%s' "$_vs_payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
43
+ fi
44
+ [ -n "$_vs_cmd" ] || _vs_allow
45
+ grep -q 'stash' <<<"$_vs_cmd" || _vs_allow
46
+
47
+ # Wrapper-prefix set: env assignments and launcher words that may sit between a
48
+ # chain boundary and `git` (LEFTHOOK=0 git stash, sudo git stash, env git stash).
49
+ VS_ENV_ASN_RE='[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]+'
50
+ VS_LAUNCHER_RE='(sudo|command|nice|env|xargs)'
51
+ VS_BOUNDARY_PFX="(${VS_ENV_ASN_RE}|${VS_LAUNCHER_RE})"
52
+
53
+ stash_hit=""
54
+ while IFS= read -r seg; do
55
+ if grep -qE '^[[:space:]]*('"${VS_BOUNDARY_PFX}"'[[:space:]]+)*git[[:space:]]+stash([[:space:]]|$)' <<<"$seg"; then
56
+ # Read-only subcommands are the documented exception — everything else decides.
57
+ if grep -qE '^[[:space:]]*('"${VS_BOUNDARY_PFX}"'[[:space:]]+)*git[[:space:]]+stash[[:space:]]+(list|show)([[:space:]]|$)' <<<"$seg"; then
58
+ continue
59
+ fi
60
+ stash_hit="$seg"
61
+ fi
62
+ done < <(printf '%s\n' "$_vs_cmd" | sed -E 's/&&|\|\||[;|]/\n/g')
63
+
64
+ [ -n "$stash_hit" ] || _vs_allow
65
+
66
+ reason="git stash is not allowed by this repo's verification gates — commit WIP to a branch instead, or use 'git stash list'/'git stash show' (read-only, allowed). Offending segment: ${stash_hit:0:120}"
67
+ if [ "$MODE" = "enforce" ]; then
68
+ _vs_decide deny "BLOCKED: $reason"
69
+ else
70
+ printf '[block-stash] WARN: %s\n' "$reason" >&2
71
+ _vs_decide allow
72
+ fi
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # commit-on-main.sh — PreToolUse hook emitted by soleur:verification-scaffold.
3
+ # Runs scripts/verification/precommit-guard.sh (the emitted copy) over every
4
+ # Bash command payload and translates its refusal into the hook decision
5
+ # envelope. The DETECTION lives entirely in the emitted guard — this wrapper
6
+ # owns only the decision verb.
7
+ #
8
+ # MODE=@MODE@ — advisory (installed default): a refusal warns and allows;
9
+ # enforce: a refusal denies. A guard/tool error (missing script, usage error)
10
+ # warns+allows in advisory and denies with remediation in enforce — `ask`
11
+ # never fires on an error path in either mode.
12
+ #
13
+ # Hook stdin: JSON payload with tool_input.command. Hook stdout: the PreToolUse
14
+ # JSON decision envelope. Exit code is always 0.
15
+ set -euo pipefail
16
+
17
+ MODE="@MODE@"
18
+
19
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
20
+ GUARD="$PROJECT_DIR/scripts/verification/precommit-guard.sh"
21
+
22
+ _vs_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' '; }
23
+ _vs_decide() {
24
+ local d="$1" r="${2:-}"
25
+ if [ -n "$r" ]; then
26
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s","permissionDecisionReason":"%s"}}\n' \
27
+ "$d" "$(_vs_escape "$r")"
28
+ else
29
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s"}}\n' "$d"
30
+ fi
31
+ exit 0
32
+ }
33
+ _vs_allow() { _vs_decide allow; }
34
+ _vs_tool_error() {
35
+ printf '[commit-on-main] WARN: %s\n' "$1" >&2
36
+ if [ "$MODE" = "enforce" ]; then
37
+ _vs_decide deny "BLOCKED: commit-on-main: $1"
38
+ else
39
+ _vs_decide allow
40
+ fi
41
+ }
42
+
43
+ # --- stdin command extraction (jq -> node -> sed fallback) --------------------
44
+ _vs_payload="$(cat)"
45
+ _vs_cmd=""
46
+ if command -v jq >/dev/null 2>&1; then
47
+ _vs_cmd="$(printf '%s' "$_vs_payload" | jq -r '.tool_input.command // ""' 2>/dev/null || true)"
48
+ elif command -v node >/dev/null 2>&1; then
49
+ _vs_cmd="$(printf '%s' "$_vs_payload" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const c=JSON.parse(s).tool_input.command;process.stdout.write(typeof c==="string"?c:"")}catch(e){}})' 2>/dev/null || true)"
50
+ else
51
+ _vs_cmd="$(printf '%s' "$_vs_payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
52
+ fi
53
+ [ -n "$_vs_cmd" ] || _vs_allow
54
+
55
+ # Cheap pre-filter: a payload that names neither git nor commit cannot be a
56
+ # commit. The emitted guard runs the real chain-boundary verdict.
57
+ grep -qE 'git' <<<"$_vs_cmd" && grep -qE 'commit' <<<"$_vs_cmd" || _vs_allow
58
+
59
+ [ -f "$GUARD" ] || _vs_tool_error "scripts/verification/precommit-guard.sh is missing — the commit-on-main gate is NOT running. Re-run verification-scaffold or restore the file."
60
+ # The hook's cwd is what the tool call runs in — pass it so a `cd`-free payload
61
+ # resolves the right repo.
62
+ hook_cwd=""
63
+ if command -v jq >/dev/null 2>&1; then
64
+ hook_cwd="$(printf '%s' "$_vs_payload" | jq -r '.cwd // ""' 2>/dev/null || true)"
65
+ fi
66
+ [ -n "$hook_cwd" ] || hook_cwd="$PWD"
67
+
68
+ guard_rc=0
69
+ guard_out="$(bash "$GUARD" --cwd "$hook_cwd" "$_vs_cmd" 2>&1)" || guard_rc=$?
70
+ case "$guard_rc" in
71
+ 0) _vs_allow ;;
72
+ 1)
73
+ reason="${guard_out:-commit refused: the target repo is checked out on a default branch. Create a feature branch first.}"
74
+ if [ "$MODE" = "enforce" ]; then
75
+ _vs_decide deny "BLOCKED: $reason"
76
+ else
77
+ printf '[commit-on-main] WARN: %s\n' "$reason" >&2
78
+ _vs_decide allow
79
+ fi
80
+ ;;
81
+ *) _vs_tool_error "precommit-guard.sh exited $guard_rc (${guard_out:-no output}) — a tool error, not a refusal; nothing was decided." ;;
82
+ esac
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # orphan-test-check.sh — emitted by soleur:verification-scaffold.
3
+ #
4
+ # Fail when a tracked *.test.sh ANYWHERE in the repo is run by no declared
5
+ # registration surface. A test nothing executes gates nothing while looking
6
+ # exactly like coverage — the green summary is read as evidence.
7
+ #
8
+ # THE PRODUCER IS `git ls-files '*.test.sh'` — the whole repo, never a
9
+ # directory list.
10
+ #
11
+ # REGISTRATION SURFACES (a suite is registered when its BASENAME appears in at
12
+ # least one):
13
+ # 1. any `.github/workflows/*.{yml,yaml}` file, and
14
+ # 2. any file matched by a word of $RUNNER_GLOBS (default below — declare
15
+ # your own in the workflow job env).
16
+ # or, by decision rather than registration:
17
+ # 3. a reason-carrying line in `.github/verification-known-orphans.txt`
18
+ # (`repo-relative/path.test.sh # <reason>` — keyed on the PATH, never the
19
+ # basename: two suites can share a basename).
20
+ #
21
+ # Exit 0 clean (or zero suites — a pass with a printed notice), exit 1 naming
22
+ # every orphan by path, exit 2 when the check cannot see (zero registration
23
+ # surfaces resolved — a guard whose producer enumerates nothing must fail
24
+ # LOUDLY, not pass vacuously).
25
+ set -euo pipefail
26
+ export LC_ALL=C
27
+
28
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
29
+ cd "$ROOT"
30
+
31
+ RUNNER_GLOBS="${RUNNER_GLOBS:-scripts/test-all.sh scripts/run-tests* Makefile makefile package.json}"
32
+ KNOWN=".github/verification-known-orphans.txt"
33
+
34
+ # --- producer ----------------------------------------------------------------
35
+ TRACKED="$(git ls-files '*.test.sh' | sort -u)"
36
+ if [ -z "$TRACKED" ]; then
37
+ printf 'orphan-test: no tracked *.test.sh files — nothing to check\n'
38
+ exit 0
39
+ fi
40
+
41
+ # --- surfaces ------------------------------------------------------------------
42
+ SURFACES=""
43
+ if [ -d .github/workflows ]; then
44
+ for f in .github/workflows/*.yml .github/workflows/*.yaml; do
45
+ [ -f "$f" ] && SURFACES="$SURFACES $f"
46
+ done
47
+ fi
48
+ for pat in $RUNNER_GLOBS; do
49
+ for f in $pat; do
50
+ [ -f "$f" ] && SURFACES="$SURFACES $f"
51
+ done
52
+ done
53
+
54
+ if [ -z "${SURFACES# }" ]; then
55
+ printf 'orphan-test: FAIL — zero registration surfaces resolved (.github/workflows/*.{yml,yaml} + RUNNER_GLOBS="%s"). The check cannot see anything; refusing to certify vacuously.\n' "$RUNNER_GLOBS" >&2
56
+ exit 2
57
+ fi
58
+ # shellcheck disable=SC2086
59
+ SURFACE_TEXT="$(cat $SURFACES)"
60
+
61
+ # --- exclusion manifest ---------------------------------------------------------
62
+ # Lines: `<path> # <reason>` (reason required). Bare-path lines are reported as
63
+ # findings — a reasonless exclusion is a silent absorption. Stale entries (the
64
+ # path is no longer tracked, or the suite is now registered) are WARNED on.
65
+ fails=0
66
+ if [ -f "$KNOWN" ]; then
67
+ while IFS= read -r line || [ -n "$line" ]; do
68
+ case "$line" in ''|'#'*) continue ;; esac
69
+ entry_path="${line%%#*}"
70
+ entry_path="$(printf '%s' "$entry_path" | sed 's/[[:space:]]*$//; s/^[[:space:]]*//')"
71
+ entry_reason="${line#*#}"
72
+ if [ "$entry_reason" = "$line" ] || [ -z "$(printf '%s' "$entry_reason" | tr -d '[:space:]')" ]; then
73
+ printf 'orphan-test: FAIL — %s carries a reasonless exclusion: %s (every line must read `<path> # <reason>`)\n' "$KNOWN" "$line" >&2
74
+ fails=$((fails + 1))
75
+ continue
76
+ fi
77
+ if ! grep -qxF "$entry_path" <<<"$TRACKED"; then
78
+ printf 'orphan-test: WARN — %s names %s, which is no longer a tracked *.test.sh; a stale exclusion can re-hide the next regression. Remove the line.\n' "$KNOWN" "$entry_path" >&2
79
+ fi
80
+ done < "$KNOWN"
81
+ fi
82
+
83
+ # --- verdict --------------------------------------------------------------------
84
+ while IFS= read -r suite; do
85
+ [ -n "$suite" ] || continue
86
+ base="$(basename "$suite")"
87
+ if grep -qF -- "$base" <<<"$SURFACE_TEXT"; then
88
+ continue
89
+ fi
90
+ if [ -f "$KNOWN" ] && grep -qE "^${suite}[[:space:]]*#" "$KNOWN"; then
91
+ continue
92
+ fi
93
+ printf 'orphan-test: FAIL — %s is tracked but named by no registration surface (workflows, RUNNER_GLOBS files, or %s with a reason)\n' "$suite" "$KNOWN" >&2
94
+ fails=$((fails + 1))
95
+ done <<<"$TRACKED"
96
+
97
+ if [ "$fails" -eq 0 ]; then
98
+ printf 'orphan-test: %s tracked suite(s), all registered\n' "$(printf '%s\n' "$TRACKED" | grep -c .)"
99
+ exit 0
100
+ fi
101
+ printf 'orphan-test: %d finding(s)\n' "$fails" >&2
102
+ exit 1