continuous-improvement 3.20.0 → 3.21.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/marketplace.json +2 -2
- package/CHANGELOG.md +10 -0
- package/QUICKSTART.md +1 -1
- package/README.md +7 -6
- package/bin/check-landing-version.mjs +63 -0
- package/bin/check-scripts-citation-drift.mjs +61 -13
- package/bin/generate-plugin-manifests.mjs +2 -0
- package/bin/install.mjs +29 -60
- package/commands/production-readiness-review.md +5 -4
- package/commands/simplicity-review.md +35 -0
- package/commands/verify-install.md +2 -2
- package/hooks/gateguard.mjs +22 -3
- package/hooks/query-cost-nudge.mjs +1 -0
- package/hooks/session.mjs +85 -0
- package/hooks/typecheck-stop.mjs +2 -1
- package/lib/plugin-metadata.mjs +18 -20
- package/llms.txt +1 -1
- package/package.json +6 -4
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/README.md +1 -0
- package/plugins/continuous-improvement/commands/production-readiness-review.md +5 -4
- package/plugins/continuous-improvement/commands/simplicity-review.md +35 -0
- package/plugins/continuous-improvement/commands/verify-install.md +2 -2
- package/plugins/continuous-improvement/hooks/gateguard.mjs +22 -3
- package/plugins/continuous-improvement/hooks/hooks.json +15 -16
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +1 -0
- package/plugins/continuous-improvement/hooks/session.mjs +85 -0
- package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +2 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +18 -20
- package/plugins/continuous-improvement/scripts/README.md +33 -0
- package/plugins/continuous-improvement/scripts/detect-deploy-target.sh +66 -0
- package/plugins/continuous-improvement/scripts/get-deployed-sha.sh +113 -0
- package/plugins/continuous-improvement/scripts/git-state-snapshot.sh +48 -0
- package/plugins/continuous-improvement/scripts/resolve-verify-ladder.mjs +241 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.mjs +178 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +213 -0
- package/plugins/continuous-improvement/scripts/run-synthetic.mjs +298 -0
- package/plugins/continuous-improvement/scripts/scan-past-mistakes.mjs +285 -0
- package/plugins/continuous-improvement/skills/README.md +1 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +3 -2
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/simplicity-review/SKILL.md +80 -0
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +5 -5
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +1 -1
- package/plugins/expert.json +1 -1
- package/scripts/README.md +33 -0
- package/scripts/detect-deploy-target.sh +66 -0
- package/scripts/get-deployed-sha.sh +113 -0
- package/scripts/git-state-snapshot.sh +48 -0
- package/scripts/resolve-verify-ladder.mjs +241 -0
- package/scripts/route-recommendation.mjs +178 -0
- package/scripts/route-recommendation.routes.json +213 -0
- package/scripts/run-synthetic.mjs +298 -0
- package/scripts/scan-past-mistakes.mjs +285 -0
- package/skills/deploy-receipt.md +2 -2
- package/skills/gateguard.md +2 -2
- package/skills/proceed-with-the-recommendation.md +3 -2
- package/skills/reconcile.md +1 -1
- package/skills/simplicity-review.md +80 -0
- package/skills/verification-loop.md +5 -5
- package/skills/workspace-surface-audit.md +1 -1
- package/skills/worktree-safety.md +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: simplicity-review
|
|
3
|
+
tier: "2"
|
|
4
|
+
description: Enforces Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline. Reviews the current diff for over-engineering (code that could reuse an existing file, a stdlib or native feature, or fewer lines) and reports trim findings without touching code, so 'it works' is never mistaken for 'it is the minimum that works'.
|
|
5
|
+
origin: continuous-improvement
|
|
6
|
+
user-invocable: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Simplicity Review — Flag the Over-Build Before It Ships
|
|
10
|
+
|
|
11
|
+
Law 4 says verify before reporting. "It compiles and the test passes" is a claim about correctness, not about whether the change is the minimum that works. This skill runs a diff-scoped review with one question: could this have been smaller? It reads the change like the laziest senior dev in the room, walks a fixed reuse ladder, and reports what to trim. It never edits code. The best code is the code you never wrote.
|
|
12
|
+
|
|
13
|
+
## When to Activate
|
|
14
|
+
|
|
15
|
+
- After writing a feature or fix, before opening a PR, when the diff feels larger than the task warranted.
|
|
16
|
+
- When you reached for a new file, a new dependency, or a wrapper component and a native or existing option might already cover it.
|
|
17
|
+
- As a routed step from `proceed-with-the-recommendation` at the "Refactor / dead code cleanup" line, as the repo-owned alternative to the built-in `simplify`.
|
|
18
|
+
- Before a release cut, over the diff since the last tag, as a last pass on accreted complexity.
|
|
19
|
+
|
|
20
|
+
## The Ladder
|
|
21
|
+
|
|
22
|
+
Read each added or changed block and stop at the first rung that holds:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
1. Does this need to exist? -> skip it (YAGNI)
|
|
26
|
+
2. Already in this codebase? -> reuse it
|
|
27
|
+
3. Stdlib does it? -> use it
|
|
28
|
+
4. Native platform feature? -> use it
|
|
29
|
+
5. Installed dependency? -> use it
|
|
30
|
+
6. One line? -> one line
|
|
31
|
+
7. Only then: the minimum that works
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Most real cuts come from rungs 2 to 4: a hand-rolled helper that duplicates an existing one, a component built where a native input, a stdlib call, or a platform primitive already does the job.
|
|
35
|
+
|
|
36
|
+
## Read Before You Cut
|
|
37
|
+
|
|
38
|
+
Lazy about the solution, never about the reading. The ladder shortens the solution, not the investigation. Trace the whole change and what it touches first. A "simpler" path proposed without reading the surrounding code is a guess, and a wrong trim is worse than the over-build.
|
|
39
|
+
|
|
40
|
+
## Safety Carve-Out (Never Flag)
|
|
41
|
+
|
|
42
|
+
Lazy, not negligent. These are never on the chopping block, no matter how many lines they cost:
|
|
43
|
+
|
|
44
|
+
- Input validation at trust boundaries.
|
|
45
|
+
- Error handling that prevents data loss.
|
|
46
|
+
- Security controls (authz checks, escaping, secret handling).
|
|
47
|
+
- Accessibility.
|
|
48
|
+
|
|
49
|
+
This aligns with `rules/common/security.md` and `rules/common/coding-style.md`. If a trim would weaken any of the above, it is not a finding.
|
|
50
|
+
|
|
51
|
+
## Output
|
|
52
|
+
|
|
53
|
+
A findings list. For each item:
|
|
54
|
+
|
|
55
|
+
- `file:line`
|
|
56
|
+
- `over-built:` what the code does the long way
|
|
57
|
+
- `simpler:` the specific shorter path (reuse existing `X` / stdlib `Y` / native `Z` / one line / delete)
|
|
58
|
+
- one-line rationale
|
|
59
|
+
|
|
60
|
+
Close with a verdict:
|
|
61
|
+
|
|
62
|
+
- `GO` — the diff is already at or near minimal; nothing to trim.
|
|
63
|
+
- `TRIM` — findings listed above; apply, then re-verify.
|
|
64
|
+
|
|
65
|
+
No score, no line-count target. A net-negative diff is a nice side effect, not the goal. Optimizing for fewer lines invites golfing and deletion of things that earn their keep; the ladder, not a number, decides.
|
|
66
|
+
|
|
67
|
+
## Review Only
|
|
68
|
+
|
|
69
|
+
This skill reports; it does not edit. Apply the trims yourself or route them through `proceed-with-the-recommendation`, then run the `verification-loop` ladder on what changed. This mirrors `audit`'s confirm-before-fix: a finding is a hypothesis until the simpler path is proven to exist and to preserve behavior.
|
|
70
|
+
|
|
71
|
+
## Overlap With `simplify`
|
|
72
|
+
|
|
73
|
+
The Claude Code built-in `simplify` and this skill share intent but differ in kind. `simplify` auto-applies broad reuse, efficiency, and altitude cleanups across changed code. `simplicity-review` is a repo-owned, Law-4-tagged, review-only lens scoped to over-engineering in the current diff, with an explicit safety carve-out, that feeds its findings into the 7-Laws flow. Use `simplify` when you want the fixes applied; use `simplicity-review` when you want the diff judged first.
|
|
74
|
+
|
|
75
|
+
## Pairs With
|
|
76
|
+
|
|
77
|
+
- **`verification-loop`** (Law 4) — the build/test/lint ladder you run after applying a trim.
|
|
78
|
+
- **`gateguard`** (Law 1) — the write-time reuse gate ("confirm no existing file serves the same purpose"); this skill catches at review time what slipped past it.
|
|
79
|
+
- **`proceed-with-the-recommendation`** — applies the trims under the 7 Laws.
|
|
80
|
+
- **`wild-risa-balance`** (Law 2) — the recommendation-layer analog: the same anti-padding instinct applied to advice instead of code.
|
|
@@ -23,10 +23,10 @@ Invoke this skill:
|
|
|
23
23
|
|
|
24
24
|
Every project has its own actual invocation for build / typecheck / lint / test / security / deploy-receipt. Hardcoding `npm run build` and `npm run test` works when the project happens to use those exact scripts; for everything else (pnpm, yarn, cargo, go, mise, just, custom scripts, monorepos with workspace-scoped commands) it returns "deps not installed" or "config not found" misreads from the wrong invocation. Phase 0 runs first so Phases 1–6 never have to guess.
|
|
25
25
|
|
|
26
|
-
**Run
|
|
26
|
+
**Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-verify-ladder.mjs"`** (source: `scripts/resolve-verify-ladder.mjs`) at the repo root. It encodes the full four-step resolution priority and emits the fenced block below. Use `--json` for machine consumption.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
$ node scripts/resolve-verify-ladder.mjs
|
|
29
|
+
$ node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-verify-ladder.mjs"
|
|
30
30
|
verify-ladder (resolved):
|
|
31
31
|
build: npm run build (sniff:package.json:scripts.build)
|
|
32
32
|
typecheck: npm run typecheck (sniff:package.json:scripts.typecheck)
|
|
@@ -147,9 +147,9 @@ If either is `No`, the verification report goes back to the operator with the ex
|
|
|
147
147
|
|
|
148
148
|
For repos whose `verify-ladder.json` declares a `deploy_receipt` field — or whose sniff path detects an auto-deploy target — the verify is not complete until the deployed SHA matches the merge SHA and a healthcheck returns 200. Hand off to the `deploy-receipt` skill (Law 4 deploy-seam companion landed in PR #83) and treat its `Receipt status: COMPLETE` as the gate.
|
|
149
149
|
|
|
150
|
-
**Detection.** Run
|
|
150
|
+
**Detection.** Run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/detect-deploy-target.sh"` (source: `scripts/detect-deploy-target.sh`) at the repo root. Output is one of `railway` / `cloudflare` / `vercel` / `netlify` / `fly` / `appengine` / `apprunner` / `gha-deploy` / `none`. Anything except `none` triggers handoff to `deploy-receipt`; `none` means Phase 8 is skipped (no deploy seam exists).
|
|
151
151
|
|
|
152
|
-
**SHA extraction.** For the detected provider,
|
|
152
|
+
**SHA extraction.** For the detected provider, `bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-deployed-sha.sh" <provider>` (source: `scripts/get-deployed-sha.sh`) returns the currently-deployed SHA via the provider CLI; `bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-deployed-sha.sh" --show-command <provider>` prints the pipeline shape without executing (useful for dry-runs and citation). `deploy-receipt` owns the receipt's other components (health endpoint, build artifact, on-incomplete modes) and Route B/C fallbacks.
|
|
153
153
|
|
|
154
154
|
INCOMPLETE receipts move to "Immediate operator action" in the close, never to "ready". Library-only / package-published repos skip this phase entirely (no deploy seam exists).
|
|
155
155
|
|
|
@@ -164,7 +164,7 @@ Phase 8 confirms the deploy seam. Phase 9 confirms the deployed surface matches
|
|
|
164
164
|
|
|
165
165
|
**What the runner does:**
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/run-synthetic.mjs"` (source: `scripts/run-synthetic.mjs`); it encodes the lexical walk, interpreter map, env injection, and exit-code aggregation below. The prose is documentation, not the contract — when the two disagree, the script wins.
|
|
168
168
|
|
|
169
169
|
1. List every `*.synthetic.{sh,mjs,ts,py}` file in the resolved directory in lexical order.
|
|
170
170
|
2. For each file, set the input env vars: `BASE_URL` (production base from project config), `BASELINE_URL` (staging baseline from project config), `EXPECTED_SHA` (the merge SHA Phase 8 reported COMPLETE), `DEPLOY_BRANCH` (the deploy branch name), `RECEIPT_TIMESTAMP` (ISO-8601 of the receipt).
|
|
@@ -74,7 +74,7 @@ Probe and record (no destructive commands; quote results inline):
|
|
|
74
74
|
- **jq availability.** `command -v jq` (or `Get-Command jq`). When jq is missing, observation-pipeline hooks fall back to a thin schema and curl/JSON one-liners need a node/python rewrite.
|
|
75
75
|
- **Case-sensitive filesystem.** Test by creating two paths differing only in case in a tempdir. NTFS (Windows) and APFS (macOS default) are case-insensitive; Linux ext4 and case-sensitive APFS are case-sensitive. Affects `CLAUDE.md` vs `claude.md` resolution and import paths.
|
|
76
76
|
- **CWD baseline.** `pwd` (or `Get-Location`) recorded at session start. `tsc`, build scripts, and some test runners change CWD as a side effect; subsequent commands run from the wrong directory return "deps not installed" or "config not found" misreads.
|
|
77
|
-
- **Parallel-actor expectation.** Document whether a second Claude / Codex / Maulana session may operate on the same working tree. If yes, the `gateguard` Parallel-Actor Gate
|
|
77
|
+
- **Parallel-actor expectation.** Document whether a second Claude / Codex / Maulana session may operate on the same working tree. If yes, the `gateguard` Parallel-Actor Gate runs `bash "${CLAUDE_PLUGIN_ROOT}/scripts/git-state-snapshot.sh"` (source: `scripts/git-state-snapshot.sh`) to produce a single JSON envelope (`{head, upstream, dirty, root, branch}`) for the baseline and the divergence check. This skill records whether parallel-actor is expected; gateguard owns the runtime mechanics, so the audit doesn't restate the git-command triple.
|
|
78
78
|
|
|
79
79
|
Output the recorded grain as a single fenced block so it survives context compaction and any later phase can reference it without re-probing:
|
|
80
80
|
|
|
@@ -32,7 +32,7 @@ The continuous-improvement repo runs on Windows + Git Bash with `autocrlf=true`
|
|
|
32
32
|
|
|
33
33
|
Before any source-writing call, verify all five. Fail closed on any miss.
|
|
34
34
|
|
|
35
|
-
1. **Root validity** — `git rev-parse --show-toplevel` resolves; the resolved path matches CWD after symlink-safe canonicalization.
|
|
35
|
+
1. **Root validity** — `git rev-parse --show-toplevel` resolves; the resolved path matches CWD after symlink-safe canonicalization. Run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/git-state-snapshot.sh"` (source: `scripts/git-state-snapshot.sh`); its `root` field carries the canonicalized root, and its non-zero exit on `{"error":"not-a-git-repo"}` is itself the fail-closed signal — no second probe needed.
|
|
36
36
|
2. **`.git` presence** — `.git` exists (file pointer for worktrees, directory for primary checkout). A missing or unreadable `.git` is an immediate stop.
|
|
37
37
|
3. **Worktree registration** — `git worktree list` includes the resolved root with no `prunable` flag. Prunable worktrees can be deleted by another process at any moment.
|
|
38
38
|
4. **Branch alignment** — current branch matches the lease ledger; `HEAD` is not detached unless the unit explicitly asked for detached state. The snapshot script's `branch` field carries either the branch name or the literal `"detached"`, so detached-state is observable without a second probe.
|
package/plugins/expert.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.0",
|
|
4
4
|
"mode": "expert",
|
|
5
5
|
"description": "Expert mode: tune confidence, manage instincts, and persist plans on disk. Adds safety, token-budget, and strategic-compact skills plus the /learn-eval command so long sessions stay sharp and learnings survive context resets.",
|
|
6
6
|
"tools": [
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# `scripts/` — deterministic primitives skills cite
|
|
2
|
+
|
|
3
|
+
This directory holds small, hand-authored scripts that skills (and hooks) cite instead of restating fixed operations inline. The motivation is the skills audit's "deterministic vs non-deterministic" axis: when a step inside a skill is the same operation every time (a fixed git command, a regex parse, a path lookup), it does not belong in the LLM-driven part of the skill — it belongs here, where the script runs the same way every invocation, costs no tokens, and stays in one place so multiple skills can share it.
|
|
4
|
+
|
|
5
|
+
## Conventions
|
|
6
|
+
|
|
7
|
+
- **Hand-authored, no `.mts` source.** Files in `scripts/` are not part of the `tsc` build pipeline (see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline"). The pipeline owns `bin/`, `lib/`, `test/`. `scripts/` is the bare-metal home for shell scripts and one-off Node utilities skills cite directly.
|
|
8
|
+
- **One concern per script.** Each script does one thing and prints either machine-readable output (JSON envelope, single value) or a stable fenced block. Skills consume the output; they do not re-derive it.
|
|
9
|
+
- **Cross-platform shape.** Bash scripts run via Git Bash on Windows, native bash on macOS/Linux. Tests for bash scripts skip when bash is not on PATH (see `src/test/hook.test.mts` for the established skip pattern).
|
|
10
|
+
- **Cite from skills.** When a skill needs a primitive, the skill body cites the script path (`scripts/<name>.<ext>`) and quotes the expected output shape once. The skill does not restate the script's inner mechanics.
|
|
11
|
+
|
|
12
|
+
## Inventory
|
|
13
|
+
|
|
14
|
+
| Script | Purpose | Cited by |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `git-state-snapshot.sh` | JSON envelope `{head, upstream, dirty, root, branch}` for the current git working tree | `skills/gateguard.md` (Parallel-Actor Gate), `skills/worktree-safety.md` (Root + branch), `skills/workspace-surface-audit.md` (Environment Grain — parallel-actor row), `skills/reconcile.md` (Detect a Concurrent Writer) |
|
|
17
|
+
| `detect-deploy-target.sh` | Detect the auto-deploy provider for the repo at the current working directory (or first arg). Prints one of `railway`/`cloudflare`/`vercel`/`netlify`/`fly`/`appengine`/`apprunner`/`gha-deploy`/`none`. Always exits 0. | `skills/verification-loop.md` (Phase 8 deploy-receipt handoff), `skills/deploy-receipt.md` (When to Activate gate) |
|
|
18
|
+
| `get-deployed-sha.sh` | Per-provider deployed-SHA extraction. Default mode runs the CLI; `--show-command <provider>` prints the pipeline shape without executing (useful for citation, dry-run, tests). | `skills/verification-loop.md` (Phase 8), `skills/deploy-receipt.md` (Route A — provider CLI extraction) |
|
|
19
|
+
| `resolve-verify-ladder.mjs` | Resolve the per-project verification ladder for Phase 0 of `verification-loop`. Encodes the four-step priority — manifest > package.json sniff > per-language toolchain > ask-operator. Default mode prints the fenced block the skill displays; `--json` mode emits a JSON object for machine consumption. | `skills/verification-loop.md` (Phase 0) |
|
|
20
|
+
| `scan-past-mistakes.mjs` | Scan the three Past-Mistake Acknowledgment Gate surfaces — `~/.claude/instincts/<hash>/observations.jsonl` (last N failure/correction rows), `~/.claude/projects/<hash>/memory/feedback_*.md`, and `<root>/CLAUDE.md` "## Past Mistakes" table. Active-in-scope judgment is the LLM's job; the script provides quotes + citations only. | `skills/proceed-with-the-recommendation.md` (Phase 0 Rule 1) |
|
|
21
|
+
| `route-recommendation.mjs` + `route-recommendation.routes.json` | Match a recommendation item to its preferred-skill chain + inline fallback from the Phase 3 routing table (29 rows). Data file is the programmatic source of truth; the skill's table is documentation that mirrors it. Default mode prints a match block; `--json` for machine consumption; `--list` enumerates all rows. | `skills/proceed-with-the-recommendation.md` (Phase 3 routing table) |
|
|
22
|
+
| `run-synthetic.mjs` | Phase 9 runner: invoke every `*.synthetic.{sh,mjs,ts,py}` in `synthetic-checks/`, inject `BASE_URL`/`BASELINE_URL`/`EXPECTED_SHA`/`DEPLOY_BRANCH`/`RECEIPT_TIMESTAMP` (unset vars pass through as `""`), capture stdout/stderr/exit per check, aggregate. Default mode prints the report block; `--json` for machine consumption; `--fail-fast` halts on first drift or timeout; `--timeout <sec>` per-check wall-clock cap; `--show-command` dry-run. Exit 0 all-pass, 1 drift, 2 config error, 3 usage. | `skills/verification-loop.md` (Phase 9 — production-vs-baseline diff) |
|
|
23
|
+
|
|
24
|
+
When a new script lands here, add a row to this table in the same PR and cite the script from at least one skill — otherwise the script is dead code on arrival.
|
|
25
|
+
|
|
26
|
+
## Relationship to other locations
|
|
27
|
+
|
|
28
|
+
- `bin/` — generated CLI entrypoints (`.mjs` from `src/bin/*.mts`). Do not hand-edit; see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline".
|
|
29
|
+
- `lib/` — generated library code (`.mjs` from `src/lib/*.mts`). Same rule.
|
|
30
|
+
- `hooks/` — generated PreToolUse / PostToolUse / Stop hooks (`.mjs` from `src/hooks/*.mts`). Wired in `plugins/continuous-improvement/hooks/hooks.json`.
|
|
31
|
+
- `scripts/` — this directory. Hand-authored primitives skills cite.
|
|
32
|
+
|
|
33
|
+
When deciding where a new piece of code belongs: if it implements a runtime hook the harness will call, it goes in `src/hooks/`. If it's a verification or CLI entrypoint the plugin or `npm run` invokes, it goes in `src/bin/`. If it's reusable logic shared between those, it goes in `src/lib/`. If it's a small primitive a skill body cites by path (and the operator or harness runs ad hoc), it goes here.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/detect-deploy-target.sh
|
|
3
|
+
#
|
|
4
|
+
# Detect the auto-deploy provider for the repo rooted at the current working
|
|
5
|
+
# directory (or the first argument, if supplied). Composability primitive used
|
|
6
|
+
# by skills and hooks that need to know "does this repo auto-deploy, and if so
|
|
7
|
+
# from where?" without each restating the file-marker table.
|
|
8
|
+
#
|
|
9
|
+
# Output: one of
|
|
10
|
+
# railway | cloudflare | vercel | netlify | fly | appengine | apprunner |
|
|
11
|
+
# gha-deploy | none
|
|
12
|
+
#
|
|
13
|
+
# Resolution priority (first match wins):
|
|
14
|
+
# 1. railway.toml | railway.json → railway
|
|
15
|
+
# 2. wrangler.toml | wrangler.jsonc → cloudflare
|
|
16
|
+
# 3. vercel.json | .vercel/ → vercel
|
|
17
|
+
# 4. netlify.toml → netlify
|
|
18
|
+
# 5. fly.toml → fly
|
|
19
|
+
# 6. app.yaml → appengine
|
|
20
|
+
# 7. apprunner.yaml → apprunner
|
|
21
|
+
# 8. .github/workflows/*.{yml,yaml} containing "deploy:" job → gha-deploy
|
|
22
|
+
# 9. nothing matched → none
|
|
23
|
+
#
|
|
24
|
+
# Always exits 0. `none` is a valid result, not an error condition.
|
|
25
|
+
#
|
|
26
|
+
# Cited by:
|
|
27
|
+
# - skills/verification-loop.md Phase 8 (deploy-receipt handoff trigger)
|
|
28
|
+
# - skills/deploy-receipt.md "When to Activate" gate
|
|
29
|
+
|
|
30
|
+
set -u
|
|
31
|
+
|
|
32
|
+
ROOT="${1:-$PWD}"
|
|
33
|
+
|
|
34
|
+
emit() {
|
|
35
|
+
printf '%s\n' "$1"
|
|
36
|
+
exit 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Order is the contract — earlier rows shadow later ones when multiple
|
|
40
|
+
# markers exist in the same repo.
|
|
41
|
+
[ -f "$ROOT/railway.toml" ] && emit railway
|
|
42
|
+
[ -f "$ROOT/railway.json" ] && emit railway
|
|
43
|
+
[ -f "$ROOT/wrangler.toml" ] && emit cloudflare
|
|
44
|
+
[ -f "$ROOT/wrangler.jsonc" ] && emit cloudflare
|
|
45
|
+
[ -f "$ROOT/vercel.json" ] && emit vercel
|
|
46
|
+
[ -d "$ROOT/.vercel" ] && emit vercel
|
|
47
|
+
[ -f "$ROOT/netlify.toml" ] && emit netlify
|
|
48
|
+
[ -f "$ROOT/fly.toml" ] && emit fly
|
|
49
|
+
[ -f "$ROOT/app.yaml" ] && emit appengine
|
|
50
|
+
[ -f "$ROOT/apprunner.yaml" ] && emit apprunner
|
|
51
|
+
|
|
52
|
+
# GitHub Actions deploy workflow: scan .github/workflows/*.{yml,yaml} for a
|
|
53
|
+
# job whose key is literally `deploy:`. Cheap and conservative — false
|
|
54
|
+
# positives (a non-deploy job named "deploy") are unlikely; false negatives
|
|
55
|
+
# (a deploy job named something else) require the repo to declare the
|
|
56
|
+
# provider via one of the file markers above instead.
|
|
57
|
+
if [ -d "$ROOT/.github/workflows" ]; then
|
|
58
|
+
if grep -lE '^[[:space:]]*deploy:[[:space:]]*$' \
|
|
59
|
+
"$ROOT/.github/workflows/"*.yml \
|
|
60
|
+
"$ROOT/.github/workflows/"*.yaml 2>/dev/null \
|
|
61
|
+
| head -n 1 | grep -q .; then
|
|
62
|
+
emit gha-deploy
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
emit none
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/get-deployed-sha.sh
|
|
3
|
+
#
|
|
4
|
+
# Print the currently-deployed commit SHA for an auto-deploy provider, or
|
|
5
|
+
# print the CLI command shape (with `--show-command`) without executing it.
|
|
6
|
+
# Composability primitive that owns the per-provider CLI knowledge so skills
|
|
7
|
+
# can cite one path instead of restating the 5-provider extraction table.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash scripts/get-deployed-sha.sh <provider>
|
|
11
|
+
# bash scripts/get-deployed-sha.sh --show-command <provider>
|
|
12
|
+
#
|
|
13
|
+
# Providers: railway | cloudflare | vercel | netlify | fly
|
|
14
|
+
#
|
|
15
|
+
# Default mode: runs the provider CLI, pipes through jq, prints the SHA on
|
|
16
|
+
# stdout. Requires the CLI to be installed and authenticated; exits 3 with a
|
|
17
|
+
# clear error if the CLI is missing.
|
|
18
|
+
#
|
|
19
|
+
# --show-command mode: prints the command pipeline that would run, without
|
|
20
|
+
# executing it. Useful for skill citations, dry-runs, and tests that should
|
|
21
|
+
# not require live CLI auth.
|
|
22
|
+
#
|
|
23
|
+
# Exit codes:
|
|
24
|
+
# 0 — SHA printed (default mode) or command printed (--show-command mode)
|
|
25
|
+
# 2 — missing or unknown provider (usage error)
|
|
26
|
+
# 3 — required CLI not installed (default mode only)
|
|
27
|
+
# non-zero — CLI failure (passed through)
|
|
28
|
+
#
|
|
29
|
+
# Cited by:
|
|
30
|
+
# - skills/verification-loop.md Phase 8 (deploy-receipt handoff trigger)
|
|
31
|
+
# - skills/deploy-receipt.md Route A (provider CLI extraction)
|
|
32
|
+
|
|
33
|
+
set -u
|
|
34
|
+
|
|
35
|
+
SHOW_COMMAND=false
|
|
36
|
+
PROVIDER=""
|
|
37
|
+
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--show-command)
|
|
41
|
+
SHOW_COMMAND=true
|
|
42
|
+
shift
|
|
43
|
+
;;
|
|
44
|
+
-h|--help)
|
|
45
|
+
sed -n '2,/^$/p' "$0" >&2
|
|
46
|
+
exit 0
|
|
47
|
+
;;
|
|
48
|
+
*)
|
|
49
|
+
if [ -z "$PROVIDER" ]; then
|
|
50
|
+
PROVIDER="$1"
|
|
51
|
+
else
|
|
52
|
+
printf 'usage: get-deployed-sha.sh [--show-command] <provider>\n' >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
shift
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
if [ -z "$PROVIDER" ]; then
|
|
61
|
+
printf 'usage: get-deployed-sha.sh [--show-command] <provider>\n' >&2
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Single source of truth for the per-provider command pipeline. Each value is
|
|
66
|
+
# the literal pipeline that would run; the jq filter extracts the SHA.
|
|
67
|
+
case "$PROVIDER" in
|
|
68
|
+
railway)
|
|
69
|
+
CMD='railway status --json | jq -r .deployments[0].meta.commitHash'
|
|
70
|
+
CLI=railway
|
|
71
|
+
;;
|
|
72
|
+
cloudflare)
|
|
73
|
+
CMD='wrangler deployments list --json | jq -r .[0].metadata.deployment_trigger.metadata.commit_hash'
|
|
74
|
+
CLI=wrangler
|
|
75
|
+
;;
|
|
76
|
+
vercel)
|
|
77
|
+
CMD='vercel inspect "$(vercel ls --json | jq -r .[0].url)" --json | jq -r .gitSource.sha'
|
|
78
|
+
CLI=vercel
|
|
79
|
+
;;
|
|
80
|
+
netlify)
|
|
81
|
+
CMD='netlify api listSiteDeploys --data="{\"site_id\":\"$NETLIFY_SITE_ID\"}" | jq -r .[0].commit_ref'
|
|
82
|
+
CLI=netlify
|
|
83
|
+
;;
|
|
84
|
+
fly)
|
|
85
|
+
CMD='fly releases --json | jq -r .[0].commit_sha'
|
|
86
|
+
CLI=fly
|
|
87
|
+
;;
|
|
88
|
+
*)
|
|
89
|
+
printf 'unknown or unsupported provider: %s\n' "$PROVIDER" >&2
|
|
90
|
+
printf 'supported: railway | cloudflare | vercel | netlify | fly\n' >&2
|
|
91
|
+
exit 2
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
|
|
95
|
+
if [ "$SHOW_COMMAND" = "true" ]; then
|
|
96
|
+
printf '%s\n' "$CMD"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if ! command -v "$CLI" >/dev/null 2>&1; then
|
|
101
|
+
printf 'required CLI "%s" not installed for provider "%s"\n' "$CLI" "$PROVIDER" >&2
|
|
102
|
+
exit 3
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
106
|
+
printf 'required CLI "jq" not installed\n' >&2
|
|
107
|
+
exit 3
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Execute the pipeline. eval is intentional — the per-provider CMD contains
|
|
111
|
+
# pipes and command substitution that need shell interpretation. CMDs are
|
|
112
|
+
# sourced from the literal table above, not user input.
|
|
113
|
+
eval "$CMD"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/git-state-snapshot.sh
|
|
3
|
+
#
|
|
4
|
+
# Emit a single-line JSON envelope describing the current git working-tree
|
|
5
|
+
# state. Composability primitive used by skills that need to baseline or check
|
|
6
|
+
# git state without each restating the same 3-command triple.
|
|
7
|
+
#
|
|
8
|
+
# Fields:
|
|
9
|
+
# - head: short SHA returned by `git rev-parse --short HEAD`
|
|
10
|
+
# - upstream: short SHA of `@{u}` if the branch tracks an upstream, else "none"
|
|
11
|
+
# - dirty: integer count of lines from `git status --porcelain` (0 == clean)
|
|
12
|
+
# - root: absolute path from `git rev-parse --show-toplevel`
|
|
13
|
+
# - branch: `git symbolic-ref --short HEAD`, else "detached"
|
|
14
|
+
#
|
|
15
|
+
# Outside a git repository the script prints `{"error":"not-a-git-repo"}` and
|
|
16
|
+
# exits 1. All other failures are treated as a non-git-repo condition rather
|
|
17
|
+
# than emitting a partial envelope.
|
|
18
|
+
#
|
|
19
|
+
# Cited by:
|
|
20
|
+
# - skills/gateguard.md (Parallel-Actor Gate baseline + divergence)
|
|
21
|
+
# - skills/worktree-safety.md (Root + branch alignment)
|
|
22
|
+
# - skills/workspace-surface-audit.md (Environment Grain — parallel-actor row)
|
|
23
|
+
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
head=$(git rev-parse --short HEAD 2>/dev/null) || {
|
|
27
|
+
printf '{"error":"not-a-git-repo"}\n'
|
|
28
|
+
exit 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if upstream=$(git rev-parse --short '@{u}' 2>/dev/null); then
|
|
32
|
+
upstream_field=$(printf '"%s"' "$upstream")
|
|
33
|
+
else
|
|
34
|
+
upstream_field='"none"'
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
38
|
+
|
|
39
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null || printf 'unknown')
|
|
40
|
+
|
|
41
|
+
if branch=$(git symbolic-ref --short HEAD 2>/dev/null); then
|
|
42
|
+
branch_field=$(printf '"%s"' "$branch")
|
|
43
|
+
else
|
|
44
|
+
branch_field='"detached"'
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
printf '{"head":"%s","upstream":%s,"dirty":%s,"root":"%s","branch":%s}\n' \
|
|
48
|
+
"$head" "$upstream_field" "$dirty" "$root" "$branch_field"
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/resolve-verify-ladder.mjs
|
|
3
|
+
//
|
|
4
|
+
// Resolve the per-project verification ladder for `verification-loop` Phase 0.
|
|
5
|
+
// Encodes the four-step resolution priority — manifest > package.json sniff >
|
|
6
|
+
// per-language toolchain > ask-operator — so the skill body cites this script
|
|
7
|
+
// instead of restating the 80+ lines of priority prose inline.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node scripts/resolve-verify-ladder.mjs # pretty fenced block, cwd
|
|
11
|
+
// node scripts/resolve-verify-ladder.mjs <repo-root> # pretty, explicit root
|
|
12
|
+
// node scripts/resolve-verify-ladder.mjs --json # JSON for machines
|
|
13
|
+
// node scripts/resolve-verify-ladder.mjs --json <repo-root>
|
|
14
|
+
//
|
|
15
|
+
// Output (default — pretty fenced block):
|
|
16
|
+
//
|
|
17
|
+
// verify-ladder (resolved):
|
|
18
|
+
// build: npm run build (sniff:package.json:scripts.build)
|
|
19
|
+
// typecheck: npx tsc --noEmit (manifest)
|
|
20
|
+
// ...
|
|
21
|
+
//
|
|
22
|
+
// Output (--json — single JSON object on stdout):
|
|
23
|
+
//
|
|
24
|
+
// { "build": { "command": "npm run build", "source": "sniff:..." }, ... }
|
|
25
|
+
//
|
|
26
|
+
// Phases: build, typecheck, lint, test, security, deploy_receipt, synthetic_checks.
|
|
27
|
+
// Sources: "manifest", "manifest:null" (explicitly skipped), "sniff:package.json:<key>",
|
|
28
|
+
// "sniff:Cargo.toml" | "sniff:go.mod" | "sniff:pyproject.toml" | "sniff:Gemfile",
|
|
29
|
+
// "ask-operator" (nothing matched).
|
|
30
|
+
//
|
|
31
|
+
// Cited by:
|
|
32
|
+
// - skills/verification-loop.md Phase 0
|
|
33
|
+
|
|
34
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { argv, cwd, exit, stdout } from "node:process";
|
|
37
|
+
|
|
38
|
+
const PHASES = [
|
|
39
|
+
"build",
|
|
40
|
+
"typecheck",
|
|
41
|
+
"lint",
|
|
42
|
+
"test",
|
|
43
|
+
"security",
|
|
44
|
+
"deploy_receipt",
|
|
45
|
+
"synthetic_checks",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function parseArgs() {
|
|
49
|
+
const args = argv.slice(2);
|
|
50
|
+
let json = false;
|
|
51
|
+
let root = cwd();
|
|
52
|
+
for (const a of args) {
|
|
53
|
+
if (a === "--json") {
|
|
54
|
+
json = true;
|
|
55
|
+
} else if (a === "-h" || a === "--help") {
|
|
56
|
+
stdout.write(
|
|
57
|
+
"usage: resolve-verify-ladder.mjs [--json] [<repo-root>]\n",
|
|
58
|
+
);
|
|
59
|
+
exit(0);
|
|
60
|
+
} else {
|
|
61
|
+
root = a;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { json, root };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function safeReadJson(path) {
|
|
68
|
+
if (!existsSync(path)) return null;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readManifest(root) {
|
|
77
|
+
return safeReadJson(join(root, ".claude", "verify-ladder.json"));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readPackageJson(root) {
|
|
81
|
+
return safeReadJson(join(root, "package.json"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sniffPackageJsonScript(pkg, phase) {
|
|
85
|
+
if (!pkg || !pkg.scripts) return null;
|
|
86
|
+
const scripts = pkg.scripts;
|
|
87
|
+
// Priority: verify:<phase> > <phase> > <phase>:* (wildcard tail).
|
|
88
|
+
// Phase aliases:
|
|
89
|
+
// typecheck also matches `tsc` (common in repos that just use `tsc`).
|
|
90
|
+
// security also matches `audit` (common in npm projects).
|
|
91
|
+
const candidates = [`verify:${phase}`, phase];
|
|
92
|
+
if (phase === "typecheck") candidates.push("tsc");
|
|
93
|
+
if (phase === "security") candidates.push("audit");
|
|
94
|
+
|
|
95
|
+
for (const name of candidates) {
|
|
96
|
+
if (typeof scripts[name] === "string") {
|
|
97
|
+
const cmd = name === "test" ? "npm test" : `npm run ${name}`;
|
|
98
|
+
return {
|
|
99
|
+
command: cmd,
|
|
100
|
+
source: `sniff:package.json:scripts.${name}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Last resort: <phase>:* wildcard tail.
|
|
106
|
+
const wildcardKey = Object.keys(scripts).find((k) => k.startsWith(`${phase}:`));
|
|
107
|
+
if (wildcardKey) {
|
|
108
|
+
return {
|
|
109
|
+
command: `npm run ${wildcardKey}`,
|
|
110
|
+
source: `sniff:package.json:scripts.${wildcardKey}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const PER_LANGUAGE = [
|
|
118
|
+
{
|
|
119
|
+
marker: "Cargo.toml",
|
|
120
|
+
commands: {
|
|
121
|
+
build: "cargo build",
|
|
122
|
+
typecheck: "cargo check --all-targets",
|
|
123
|
+
lint: "cargo clippy --all-targets -- -D warnings",
|
|
124
|
+
test: "cargo test",
|
|
125
|
+
security: "cargo audit",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
marker: "go.mod",
|
|
130
|
+
commands: {
|
|
131
|
+
build: "go build ./...",
|
|
132
|
+
typecheck: "go vet ./...",
|
|
133
|
+
lint: "go vet ./...",
|
|
134
|
+
test: "go test ./...",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
marker: "pyproject.toml",
|
|
139
|
+
commands: {
|
|
140
|
+
typecheck: "pyright",
|
|
141
|
+
lint: "ruff check .",
|
|
142
|
+
test: "pytest",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
marker: "Gemfile",
|
|
147
|
+
commands: {
|
|
148
|
+
lint: "bundle exec rubocop",
|
|
149
|
+
test: "bundle exec rspec",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
function sniffPerLanguage(root, phase) {
|
|
155
|
+
for (const lang of PER_LANGUAGE) {
|
|
156
|
+
if (!existsSync(join(root, lang.marker))) continue;
|
|
157
|
+
if (lang.commands[phase]) {
|
|
158
|
+
return { command: lang.commands[phase], source: `sniff:${lang.marker}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolve(root) {
|
|
165
|
+
const manifest = readManifest(root);
|
|
166
|
+
const pkg = readPackageJson(root);
|
|
167
|
+
const ladder = {};
|
|
168
|
+
|
|
169
|
+
for (const phase of PHASES) {
|
|
170
|
+
// 1. Manifest layer. Underscore-prefixed keys in the manifest are
|
|
171
|
+
// documentation/examples — verify-ladder.example.json uses keys like
|
|
172
|
+
// "_doc", "_node_example" — so PHASES never starts with "_" and we
|
|
173
|
+
// can rely on hasOwn for an exact phase match.
|
|
174
|
+
if (
|
|
175
|
+
manifest &&
|
|
176
|
+
Object.prototype.hasOwnProperty.call(manifest, phase) &&
|
|
177
|
+
!phase.startsWith("_")
|
|
178
|
+
) {
|
|
179
|
+
const value = manifest[phase];
|
|
180
|
+
if (value === null) {
|
|
181
|
+
ladder[phase] = { command: null, source: "manifest:null" };
|
|
182
|
+
} else if (typeof value === "string") {
|
|
183
|
+
ladder[phase] = { command: value, source: "manifest" };
|
|
184
|
+
} else {
|
|
185
|
+
ladder[phase] = { command: null, source: "manifest:invalid" };
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 2. package.json scripts sniff.
|
|
191
|
+
const pkgHit = sniffPackageJsonScript(pkg, phase);
|
|
192
|
+
if (pkgHit) {
|
|
193
|
+
ladder[phase] = pkgHit;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 3. Per-language toolchain sniff.
|
|
198
|
+
const langHit = sniffPerLanguage(root, phase);
|
|
199
|
+
if (langHit) {
|
|
200
|
+
ladder[phase] = langHit;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 4. Nothing matched — operator must declare or set to null.
|
|
205
|
+
ladder[phase] = { command: null, source: "ask-operator" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return ladder;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function pretty(ladder) {
|
|
212
|
+
const widest = Math.max(...Object.keys(ladder).map((k) => k.length));
|
|
213
|
+
const lines = ["verify-ladder (resolved):"];
|
|
214
|
+
for (const [phase, entry] of Object.entries(ladder)) {
|
|
215
|
+
const label = `${phase}:`.padEnd(widest + 2);
|
|
216
|
+
let line;
|
|
217
|
+
if (entry.command === null && entry.source === "ask-operator") {
|
|
218
|
+
line = ` ${label} (ask operator — no marker found)`;
|
|
219
|
+
} else if (entry.command === null && entry.source === "manifest:null") {
|
|
220
|
+
line = ` ${label} (skipped — manifest set this field to null)`;
|
|
221
|
+
} else if (entry.command === null) {
|
|
222
|
+
line = ` ${label} (skipped — ${entry.source})`;
|
|
223
|
+
} else {
|
|
224
|
+
line = ` ${label} ${entry.command} (${entry.source})`;
|
|
225
|
+
}
|
|
226
|
+
lines.push(line);
|
|
227
|
+
}
|
|
228
|
+
return lines.join("\n") + "\n";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function main() {
|
|
232
|
+
const { json, root } = parseArgs();
|
|
233
|
+
const ladder = resolve(root);
|
|
234
|
+
if (json) {
|
|
235
|
+
stdout.write(JSON.stringify(ladder, null, 2) + "\n");
|
|
236
|
+
} else {
|
|
237
|
+
stdout.write(pretty(ladder));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
main();
|