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.
Files changed (67) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +10 -0
  3. package/QUICKSTART.md +1 -1
  4. package/README.md +7 -6
  5. package/bin/check-landing-version.mjs +63 -0
  6. package/bin/check-scripts-citation-drift.mjs +61 -13
  7. package/bin/generate-plugin-manifests.mjs +2 -0
  8. package/bin/install.mjs +29 -60
  9. package/commands/production-readiness-review.md +5 -4
  10. package/commands/simplicity-review.md +35 -0
  11. package/commands/verify-install.md +2 -2
  12. package/hooks/gateguard.mjs +22 -3
  13. package/hooks/query-cost-nudge.mjs +1 -0
  14. package/hooks/session.mjs +85 -0
  15. package/hooks/typecheck-stop.mjs +2 -1
  16. package/lib/plugin-metadata.mjs +18 -20
  17. package/llms.txt +1 -1
  18. package/package.json +6 -4
  19. package/plugins/beginner.json +1 -1
  20. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
  21. package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
  22. package/plugins/continuous-improvement/README.md +1 -0
  23. package/plugins/continuous-improvement/commands/production-readiness-review.md +5 -4
  24. package/plugins/continuous-improvement/commands/simplicity-review.md +35 -0
  25. package/plugins/continuous-improvement/commands/verify-install.md +2 -2
  26. package/plugins/continuous-improvement/hooks/gateguard.mjs +22 -3
  27. package/plugins/continuous-improvement/hooks/hooks.json +15 -16
  28. package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +1 -0
  29. package/plugins/continuous-improvement/hooks/session.mjs +85 -0
  30. package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +2 -1
  31. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +18 -20
  32. package/plugins/continuous-improvement/scripts/README.md +33 -0
  33. package/plugins/continuous-improvement/scripts/detect-deploy-target.sh +66 -0
  34. package/plugins/continuous-improvement/scripts/get-deployed-sha.sh +113 -0
  35. package/plugins/continuous-improvement/scripts/git-state-snapshot.sh +48 -0
  36. package/plugins/continuous-improvement/scripts/resolve-verify-ladder.mjs +241 -0
  37. package/plugins/continuous-improvement/scripts/route-recommendation.mjs +178 -0
  38. package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +213 -0
  39. package/plugins/continuous-improvement/scripts/run-synthetic.mjs +298 -0
  40. package/plugins/continuous-improvement/scripts/scan-past-mistakes.mjs +285 -0
  41. package/plugins/continuous-improvement/skills/README.md +1 -0
  42. package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +2 -2
  43. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +2 -2
  44. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +3 -2
  45. package/plugins/continuous-improvement/skills/reconcile/SKILL.md +1 -1
  46. package/plugins/continuous-improvement/skills/simplicity-review/SKILL.md +80 -0
  47. package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +5 -5
  48. package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
  49. package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +1 -1
  50. package/plugins/expert.json +1 -1
  51. package/scripts/README.md +33 -0
  52. package/scripts/detect-deploy-target.sh +66 -0
  53. package/scripts/get-deployed-sha.sh +113 -0
  54. package/scripts/git-state-snapshot.sh +48 -0
  55. package/scripts/resolve-verify-ladder.mjs +241 -0
  56. package/scripts/route-recommendation.mjs +178 -0
  57. package/scripts/route-recommendation.routes.json +213 -0
  58. package/scripts/run-synthetic.mjs +298 -0
  59. package/scripts/scan-past-mistakes.mjs +285 -0
  60. package/skills/deploy-receipt.md +2 -2
  61. package/skills/gateguard.md +2 -2
  62. package/skills/proceed-with-the-recommendation.md +3 -2
  63. package/skills/reconcile.md +1 -1
  64. package/skills/simplicity-review.md +80 -0
  65. package/skills/verification-loop.md +5 -5
  66. package/skills/workspace-surface-audit.md +1 -1
  67. package/skills/worktree-safety.md +1 -1
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync, readdirSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { resolveHomeDir } from "../lib/resolve-home-dir.mjs";
7
+ function read(path) {
8
+ try {
9
+ return readFileSync(path, "utf8");
10
+ }
11
+ catch {
12
+ return "";
13
+ }
14
+ }
15
+ function eventFromStdin() {
16
+ const raw = read(0);
17
+ if (!raw)
18
+ return null;
19
+ try {
20
+ const payload = JSON.parse(raw);
21
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
22
+ return null;
23
+ const event = payload.hook_event_name ?? payload.hook_type ?? payload.event_type;
24
+ return event === "SessionStart" || event === "SessionEnd" ? event : "unknown";
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ function projectRoot() {
31
+ if (process.env.CLAUDE_PROJECT_DIR)
32
+ return process.env.CLAUDE_PROJECT_DIR;
33
+ try {
34
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
35
+ encoding: "utf8",
36
+ stdio: ["ignore", "pipe", "ignore"],
37
+ }).trim() || "global";
38
+ }
39
+ catch {
40
+ return "global";
41
+ }
42
+ }
43
+ function yamlFiles(dir) {
44
+ try {
45
+ return readdirSync(dir)
46
+ .filter((name) => name.endsWith(".yaml"))
47
+ .map((name) => join(dir, name));
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ function main() {
54
+ const event = eventFromStdin();
55
+ if (event === null)
56
+ return;
57
+ if (event === "SessionEnd") {
58
+ process.stderr.write("[continuous-improvement] Session ending. Run /continuous-improvement to reflect and capture learnings.\n");
59
+ return;
60
+ }
61
+ const home = resolveHomeDir();
62
+ if (!home)
63
+ return;
64
+ const instinctsRoot = join(home, ".claude", "instincts");
65
+ const hash = createHash("sha256").update(projectRoot()).digest("hex").slice(0, 12);
66
+ const projectDir = join(instinctsRoot, hash);
67
+ const files = [...yamlFiles(projectDir), ...yamlFiles(join(instinctsRoot, "global"))];
68
+ const observations = read(join(projectDir, "observations.jsonl")).split(/\r?\n/).filter(Boolean).length;
69
+ let level = observations >= 20 || files.length > 0 ? "ANALYZE" : "CAPTURE";
70
+ for (const file of files) {
71
+ const value = Number(read(file).match(/^confidence:\s*([0-9]*\.?[0-9]+)/m)?.[1]);
72
+ if (Number.isFinite(value) && value >= 0.7) {
73
+ level = "AUTO-APPLY";
74
+ break;
75
+ }
76
+ if (Number.isFinite(value) && value >= 0.5)
77
+ level = "SUGGEST";
78
+ }
79
+ process.stderr.write(`[continuous-improvement] Level: ${level} | Observations: ${observations} | Instincts: ${files.length}\n`);
80
+ }
81
+ try {
82
+ main();
83
+ }
84
+ catch {
85
+ }
@@ -51,7 +51,8 @@ function collectChangedFiles(root) {
51
51
  };
52
52
  const unstaged = run(["diff", "--name-only", "--diff-filter=ACMR"]);
53
53
  const staged = run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
54
- return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged)];
54
+ const untracked = run(["ls-files", "--others", "--exclude-standard"]);
55
+ return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged), ...parseChangedFiles(untracked)];
55
56
  }
56
57
  function hasNpmTypecheckScript(root) {
57
58
  try {
@@ -26,7 +26,7 @@ const KEYWORDS = [
26
26
  "transcript-linter",
27
27
  ];
28
28
  const CLAUDE_PLUGIN_CATEGORY = "productivity";
29
- const SHARED_PLUGIN_DESCRIPTION = "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.";
29
+ const SHARED_PLUGIN_DESCRIPTION = "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 28 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.";
30
30
  // Four vendored upstream companions registered alongside the CI plugin.
31
31
  // Each entry points at a pinned-SHA snapshot under third-party/<name>/.
32
32
  // See third-party/MANIFEST.md for refresh recipes and per-snapshot
@@ -457,76 +457,74 @@ export function getClaudePluginManifest() {
457
457
  };
458
458
  }
459
459
  export function getPluginHooksConfig() {
460
+ // Cold Node startup on loaded Windows hosts has exceeded five seconds.
461
+ const hookTimeoutSeconds = 30;
460
462
  const gateguardCommand = {
461
463
  type: "command",
462
464
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gateguard.mjs\"",
463
- timeout: 5,
465
+ timeout: hookTimeoutSeconds,
464
466
  };
465
467
  const companionPreferenceCommand = {
466
468
  type: "command",
467
469
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/companion-preference.mjs\"",
468
- timeout: 5,
470
+ timeout: hookTimeoutSeconds,
469
471
  };
470
472
  const hookPackCommand = {
471
473
  type: "command",
472
474
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-pack.mjs\"",
473
- timeout: 5,
475
+ timeout: hookTimeoutSeconds,
474
476
  };
475
477
  const observeCommand = {
476
478
  type: "command",
477
- command: "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/observe.sh\"",
478
- timeout: 5,
479
+ command: "node \"${CLAUDE_PLUGIN_ROOT}/bin/observe.mjs\"",
480
+ timeout: hookTimeoutSeconds,
479
481
  };
480
482
  const sessionCommand = {
481
483
  type: "command",
482
- command: "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session.sh\"",
483
- timeout: 5,
484
+ command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session.mjs\"",
485
+ timeout: hookTimeoutSeconds,
484
486
  };
485
487
  const threeSectionCloseCommand = {
486
488
  type: "command",
487
489
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/three-section-close.mjs\"",
488
- timeout: 5,
490
+ timeout: hookTimeoutSeconds,
489
491
  };
490
492
  const goalDriftStopCommand = {
491
493
  type: "command",
492
494
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/goal-drift-stop.mjs\"",
493
- timeout: 5,
495
+ timeout: hookTimeoutSeconds,
494
496
  };
495
497
  const workflowDistillCommand = {
496
498
  type: "command",
497
499
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
498
- timeout: 5,
500
+ timeout: hookTimeoutSeconds,
499
501
  };
500
502
  const typecheckStopCommand = {
501
503
  type: "command",
502
504
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
503
- // Longer than the 5s hooks: tsc is slower. Opt-in via CLAUDE_TYPECHECK_GATE
504
- // (off by default) and near-zero cost when off / no TS file changed; on an
505
- // internal timeout it fails open (allow) rather than blocking.
506
- timeout: 30,
505
+ timeout: hookTimeoutSeconds,
507
506
  };
508
507
  const queryCostNudgeCommand = {
509
508
  type: "command",
510
509
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
511
- timeout: 5,
510
+ timeout: hookTimeoutSeconds,
512
511
  };
513
512
  const routePromptCommand = {
514
513
  type: "command",
515
514
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
516
- timeout: 5,
515
+ timeout: hookTimeoutSeconds,
517
516
  };
518
517
  const recallBriefingCommand = {
519
518
  type: "command",
520
519
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/recall-briefing.mjs\"",
521
- timeout: 5,
520
+ timeout: hookTimeoutSeconds,
522
521
  };
523
522
  return {
524
- description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, opt-in query-cost Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
525
523
  hooks: {
526
524
  // gateguard runs FIRST on PreToolUse so its block decision short-circuits
527
525
  // before companion-preference sees the call. companion-preference runs
528
526
  // second on Skill tool calls; it is a no-op under ci-first (the default)
529
- // and never blocks under companions-first. observe.sh only runs on
527
+ // and never blocks under companions-first. The observer only runs on
530
528
  // PostToolUse: gateguard-blocked calls are intentionally not observed so
531
529
  // PreToolUse stays at two subprocesses on the hot path. route-prompt
532
530
  // fires on UserPromptSubmit and emits a system-reminder when a prompt
@@ -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"