@tekyzinc/gsd-t 3.23.11 → 3.25.10

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 (74) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +7 -0
  3. package/bin/cli-preflight-checks/branch-guard.cjs +110 -0
  4. package/bin/cli-preflight-checks/contracts-stable.cjs +128 -0
  5. package/bin/cli-preflight-checks/deps-installed.cjs +89 -0
  6. package/bin/cli-preflight-checks/manifest-fresh.cjs +98 -0
  7. package/bin/cli-preflight-checks/ports-free.cjs +110 -0
  8. package/bin/cli-preflight-checks/working-tree-state.cjs +149 -0
  9. package/bin/cli-preflight.cjs +265 -0
  10. package/bin/gsd-t-context-brief-kinds/design-verify.cjs +139 -0
  11. package/bin/gsd-t-context-brief-kinds/execute.cjs +205 -0
  12. package/bin/gsd-t-context-brief-kinds/qa.cjs +130 -0
  13. package/bin/gsd-t-context-brief-kinds/red-team.cjs +131 -0
  14. package/bin/gsd-t-context-brief-kinds/scan.cjs +118 -0
  15. package/bin/gsd-t-context-brief-kinds/verify.cjs +157 -0
  16. package/bin/gsd-t-context-brief.cjs +395 -0
  17. package/bin/gsd-t-ratelimit-probe-worker.cjs +236 -0
  18. package/bin/gsd-t-ratelimit-probe.cjs +648 -0
  19. package/bin/gsd-t-verify-gate-judge.cjs +224 -0
  20. package/bin/gsd-t-verify-gate.cjs +612 -0
  21. package/bin/gsd-t.js +45 -1
  22. package/bin/live-activity-report.cjs +615 -0
  23. package/bin/m55-substrate-proof.cjs +134 -0
  24. package/bin/parallel-cli-tee.cjs +206 -0
  25. package/bin/parallel-cli.cjs +478 -0
  26. package/commands/gsd-t-execute.md +31 -0
  27. package/commands/gsd-t-help.md +21 -0
  28. package/commands/gsd-t-verify.md +38 -0
  29. package/docs/architecture.md +194 -0
  30. package/docs/diagrams/.gsd-t/.context-meter-state.json +10 -0
  31. package/docs/diagrams/.gsd-t/context-meter.log +9 -0
  32. package/docs/diagrams/.gsd-t/events/2026-05-08.jsonl +45 -0
  33. package/docs/diagrams/.gsd-t/events/2026-05-09.jsonl +1 -0
  34. package/docs/diagrams/.gsd-t/heartbeat-cd9e7f59-ba5b-406a-9ed6-16762f039e81.jsonl +48 -0
  35. package/docs/diagrams/01-top-level-map-d2.png +0 -0
  36. package/docs/diagrams/01-top-level-map.d2 +77 -0
  37. package/docs/diagrams/01-top-level-map.mmd +48 -0
  38. package/docs/diagrams/01-top-level-map.png +0 -0
  39. package/docs/diagrams/01-top-level-map.svg +126 -0
  40. package/docs/diagrams/02-milestone-lifecycle-d2.png +0 -0
  41. package/docs/diagrams/02-milestone-lifecycle.d2 +38 -0
  42. package/docs/diagrams/02-milestone-lifecycle.mmd +36 -0
  43. package/docs/diagrams/02-milestone-lifecycle.png +0 -0
  44. package/docs/diagrams/02-milestone-lifecycle.svg +114 -0
  45. package/docs/diagrams/03-wave-mode-d2.png +0 -0
  46. package/docs/diagrams/03-wave-mode.d2 +33 -0
  47. package/docs/diagrams/03-wave-mode.mmd +21 -0
  48. package/docs/diagrams/03-wave-mode.png +0 -0
  49. package/docs/diagrams/03-wave-mode.svg +113 -0
  50. package/docs/diagrams/04-design-to-code-d2.png +0 -0
  51. package/docs/diagrams/04-design-to-code.d2 +35 -0
  52. package/docs/diagrams/04-design-to-code.mmd +29 -0
  53. package/docs/diagrams/04-design-to-code.png +0 -0
  54. package/docs/diagrams/04-design-to-code.svg +115 -0
  55. package/docs/diagrams/05-backlog-d2.png +0 -0
  56. package/docs/diagrams/05-backlog.d2 +40 -0
  57. package/docs/diagrams/05-backlog.mmd +20 -0
  58. package/docs/diagrams/05-backlog.png +0 -0
  59. package/docs/diagrams/05-backlog.svg +113 -0
  60. package/docs/diagrams/06-automation-utilities-d2.png +0 -0
  61. package/docs/diagrams/06-automation-utilities.d2 +48 -0
  62. package/docs/diagrams/06-automation-utilities.mmd +47 -0
  63. package/docs/diagrams/06-automation-utilities.png +0 -0
  64. package/docs/diagrams/06-automation-utilities.svg +110 -0
  65. package/docs/diagrams/_theme.d2 +86 -0
  66. package/docs/requirements.md +48 -0
  67. package/docs/workflow-diagram.md +338 -0
  68. package/package.json +1 -1
  69. package/scripts/gsd-t-dashboard-server.js +190 -0
  70. package/scripts/gsd-t-transcript.html +200 -0
  71. package/templates/CLAUDE-global.md +46 -0
  72. package/templates/prompts/design-verify-subagent.md +3 -0
  73. package/templates/prompts/qa-subagent.md +3 -0
  74. package/templates/prompts/red-team-subagent.md +3 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.25.10] - 2026-05-09
6
+
7
+ ### Added — M55 CLI-Preflight Pattern + Parallel-CLI Substrate + Rate-Limit Map + Context Briefs + Verify-Gate (minor: new feature milestone)
8
+
9
+ - **Goal**: lift the practical parallelism ceiling from ~3 LLM workers to ~6–10 mixed workers (1 LLM judge + N CLIs) by replacing deterministic LLM work with deterministic CLI work, AND gate every spawn with deterministic state-precondition checks. Two pain points addressed in one ship: (a) silent-skip regressions caught only by retrofit hooks (M48/M49/M50/M52 pattern); (b) undocumented Claude ITPM ceiling at ~3 parallel workers because every worker re-loaded ~60k context within the same 60s window.
10
+ - **Scope**: 5 file-disjoint domains, single supervisor-driven build (5 worker iterations).
11
+ - **D1 m55-d1-state-precondition-library** — `bin/cli-preflight.cjs` + 6 pluggable checks under `bin/cli-preflight-checks/` (`branch-guard`, `ports-free`, `deps-installed`, `contracts-stable`, `manifest-fresh`, `working-tree-state`) returning deterministic `{ok, checks[], notes[]}` envelope. Schema-versioned, zero deps, mirroring `bin/parallelism-report.cjs`. STABLE contract `cli-preflight-contract.md` v1.0.0. Commit `bb514db`.
12
+ - **D2 m55-d2-parallel-cli-substrate** — `bin/parallel-cli.cjs` N-worker pool engine (`runParallel`), `bin/parallel-cli-tee.cjs` NDJSON tee, `bin/m55-substrate-proof.cjs` proof CLI. Every worker spawn flows through `bin/gsd-t-token-capture.cjs::captureSpawn` (token-capture invariant). Engine-only — does NOT touch command files yet. STABLE contract `parallel-cli-contract.md` v1.0.0. Commit `7b9e013`.
13
+ - **D3 m55-d3-ratelimit-probe-map** — `bin/gsd-t-ratelimit-probe.cjs` + `bin/gsd-t-ratelimit-probe-worker.cjs` synthetic-worker harness, 4 fixtures, populated `.gsd-t/ratelimit-map.json` artifact. STABLE contract `ratelimit-map-contract.md` v1.0.0. Commit `a0bef17`. **Note**: the original D3 sweep used Haiku and a synthetic Lorem-Ipsum fixture; post-milestone re-probes with Opus + Sonnet on real-prose fixtures (logged in `.gsd-t/m55-cliff-claude-sonnet-4-6.json` and partial `.gsd-t/m55-cliff-claude-opus-4-7-v4.json`) confirmed Sonnet ≥11 parallel workers without cliff and Opus ≥7 confirmed clean (full ramp interrupted by power outage + plan usage cap; cliff above 7 not yet located).
14
+ - **D4 m55-d4-context-brief-generator** — `bin/gsd-t-context-brief.cjs` + 6 brief kinds (`design-verify`, `execute`, `qa`, `red-team`, `scan`, `verify`) under `bin/gsd-t-context-brief-kinds/`. Produces `.gsd-t/briefs/{spawn-id}.json` (~2k JSON snapshot) replacing 30–60k context re-read per parallel worker — the dominant ITPM-relief lever. STABLE contract `context-brief-contract.md` v1.0.0. Commit `998f373`.
15
+ - **D5 m55-d5-verify-gate-and-wirein** — `bin/gsd-t-verify-gate.cjs` two-track gate (Track 1 consumes D1 envelope, hard-fails on `severity:error`; Track 2 fans out via D2 substrate across tsc/lint/tests/dead-code/secrets/complexity) + `bin/gsd-t-verify-gate-judge.cjs` ≤500-token LLM prompt scaffold (iterative shrink, deterministic). STABLE contract `verify-gate-contract.md` v1.0.0. Commit `b916d77`.
16
+ - **Wire-ins** (D5):
17
+ - `bin/gsd-t.js` — 4 dispatch subcommands (`preflight`, `brief`, `verify-gate`, `verify-gate-judge`), all 4 added to `GLOBAL_BIN_TOOLS` for `~/.claude/bin/` propagation.
18
+ - `commands/gsd-t-execute.md` Step 1 — additive `<!-- M55-D5: preflight + brief wire-in -->` block running preflight + generating brief, threading `$BRIEF_PATH` into worker prompts.
19
+ - `commands/gsd-t-verify.md` Step 2 — additive `<!-- M55-D5: verify-gate wire-in -->` block invoking verify-gate + piping into judge.
20
+ - `templates/prompts/{qa,red-team,design-verify}-subagent.md` — additive `<!-- M55-D5: brief-first rule -->` line in each.
21
+ - **Doc ripple**: `docs/architecture.md` (new "CLI-Preflight Pattern + Verify-Gate (M55)" section), `docs/requirements.md` (REQ-M55-D1..D5 + REQ-M55-VERIFY all `done`), `CLAUDE.md` project (Mandatory Preflight + Brief-First Worker Rule), `commands/gsd-t-help.md` (3 new entries), `README.md` (CLI table M55 block), `templates/CLAUDE-global.md` (Mandatory Preflight + Brief-First + Two-Track Verify-Gate after Token Capture Rule), `~/.claude/CLAUDE.md` Pre-Commit Gate addition. `.gitignore` additions: `.gsd-t/verify-gate/` (D5 raw worker output retention).
22
+ - **Tests**: 161 new across D1–D5. Suite total **2487 / 2485 pass**, 2 pre-existing documented env-bleed failures (`event-stream.test.js` GSD_T_COMMAND leak + `watch-progress-writer.test.js` supervisor-iter id-pattern leak — unchanged from M50/M52/M54 baselines, induced by running tests inside an unattended worker session, not by M55 code). **Zero regressions.**
23
+ - **Adversarial Red Team** (post-wave): 6/6 broken patches authored, applied, caught by tests, reverted. P1 `preflight-skip-on-error` (D1), P2 `parallel-substrate-bypasses-capture` (D2), P3 `verify-gate-falsy-true` (D5), P4 `branch-guard-typo` (D1), P5 `contract-staleness-ignored` (D1), P6 `brief-staleness-ignored` (D4) — all caught by named tests with collateral catches in 4 of 6. **VERDICT: GRUDGING PASS** — production code unchanged. Findings in `.gsd-t/red-team-report.md` § "M55 RED TEAM". Commit `fce2f18`.
24
+ - **Falsifiable success criteria** (per `feedback_measure_dont_claim.md`):
25
+ - **SC1 ✅** state-preflight contract `cli-preflight-contract.md` v1.0.0 STABLE published.
26
+ - **SC2 ✅** substrate proves **5.57× speedup** on real fan-out scenario via `bin/m55-substrate-proof.cjs` (T_serial=1813.3ms vs T_par=325.6ms, parallelism_factor=4.61, threshold ≥3.0× — PASS).
27
+ - **SC3 ✅** verify-gate blocks **3 distinct preflight failure classes** in `e2e/journeys/`: wrong-branch, port-conflict, contract-draft. All 3 mapped in `.gsd-t/journey-manifest.json`; `gsd-t check-coverage` reports `OK: 21 listeners, 19 specs` exit 0.
28
+ - **SC4 ⚠️ DEFERRED-BY-INSTRUMENTATION-GAP** — token-log.md captured only 2 supervisor-iter-level rows for M55 with no token cells populated. Trailing-3 milestones M50/M52/M54 show similarly sparse coverage with no per-milestone totals in comparable units. Run.log envelopes recorded $25 supervisor cost across 5 iters + $14.92 Red Team iter, establishing the new baseline going forward. The token-capture invariant M55 introduces (Pattern A `captureSpawn` + Pattern B `recordSpawnRow`) is the *fix* for this gap — M56 will be the first comparable measurement. SC4 is *unmeasurable from historical data*, not *failed*. Tagging proceeds with the gap explicitly documented; M56's first execute+verify cycle will close it retroactively.
29
+ - **SC5 ✅** zero 429 errors at parallelism level D3 declared safe — original D3 sweep showed 0/84 429s at peak 8 (Haiku). Post-milestone re-probes with corrected classifier (`stop_reason`/`is_error`/`api_error_status` from API envelope, not regex on stderr): **Sonnet 11/11 success at N=11** (clean v3 run, no cliff observed); **Opus 7/7 success at N=7** (partial v4 run, full ramp interrupted by power outage at 18:30 PDT and plan usage cap at 18:39 PDT — cliff above 7 not yet located). `.gsd-t/ratelimit-map.json::recommended.peakConcurrency` updated 8→**12**, `safeConcurrencyAt60kContext` updated 5→**11**, per documented Sonnet evidence + Opus partial evidence.
30
+ - **SC6 ✅** verify-gate dogfood wall-clock **34s** vs trailing-3 verify median 681s (M50=480s, M52=882s; M52 cron-chain=0s discounted as incomplete). 34s ≤ ½ × 681s = 340s threshold met with **20× margin**. Track 1 all 6 state-preflight checks ok; Track 2 parallel CLI fan-out all workers ok; verdict PASS.
31
+ - **SC7 ✅** Red Team GRUDGING PASS — 6/6 broken patches caught (≥5 target exceeded), 0 real bugs.
32
+ - **SC8 ✅** zero regressions on `npm test` — 2487 / 2485 pass, 2 pre-existing documented env-bleed failures unchanged.
33
+ - **Versioning**: minor bump per "new feature milestone" doctrine. Tag `v3.25.10` (local).
34
+ - **Followup**: M56 confirmed (per user 2026-05-09 18:54 PDT) — Verify-Gate CLI Fan-Out + Upper-Stage Briefs. D1: Add Playwright + npm-test + check-coverage as native verify-gate Track 2 CLIs (no Task subagent wrapper). D2: Extend `gsd-t-context-brief.cjs` with 5 new kinds (partition, plan, discuss, impact, milestone). D3: Wire briefs into corresponding command files. Falsifiable: M56 verify wall-clock < M55's 34s; first-of-milestone context brief shaves 30–60k from each subsequent phase's read-budget.
35
+
36
+ ## [3.24.10] - 2026-05-07
37
+
38
+ ### Added — M54 Live Activity Visibility (minor: new feature milestone)
39
+
40
+ - **Goal**: surface every active piece of work the orchestrator is doing — backgrounded `Bash` (`run_in_background:true`), running `Monitor` watches, slow `tool_use` blocks (>30s), AND detached `claude -p` spawns — into the dashboard left rail. The pre-M54 rail only caught spawn workers via `.gsd-t/spawns/*.json`; heavy in-session work was invisible. User: "I should see all active conversations running."
41
+ - **Scope**: 2 file-disjoint domains, 8 tasks total, single-day in-session build.
42
+ - **D1 m54-d1-server-and-detector** (5 tasks): `bin/live-activity-report.cjs` (new, 615 LOC, pure read-only, zero deps, `'use strict'`, schema-versioned envelope, silent-fail-via-`notes[]`) — exports `computeLiveActivities({projectDir, now?})` returning `{schemaVersion: 1, generatedAt, activities: [...]}`. Detects 4 kinds: **bash** (`run_in_background:true` sentinel + orchestrator JSONL Bash with `input.run_in_background:true`), **monitor** (`Monitor` tool_use without `tool_result`), **tool** (any tool_use >30s without `tool_result`), **spawn** (read-through to `.gsd-t/spawns/*.json` plan files with `endedAt:null`). 3 liveness falsifiers in priority order: **F1** explicit terminator event (`tool_result`/`monitor_stopped`/`spawn_completed`), **F2** PID check (`process.kill(pid, 0)` ESRCH), **F3** source-file mtime >60s. Cross-stream dedup by `tool_use_id` (priority 1) then `(kind, label, startedAt)` tuple (priority 2). Source-of-truth UNION: `.gsd-t/events/<today>.jsonl` + `~/.claude/projects/<slug>/<sid>.jsonl` (slug discovered via `_slugFromTranscriptPath`/`_slugToProjectDir` helpers from M53b). 3 dashboard handlers added to `scripts/gsd-t-dashboard-server.js` (additive — `handleLiveActivity` with 5s response cache; `handleLiveActivityTail` with `isValidActivityId` path-traversal guard; `handleLiveActivityStream` SSE with 15s heartbeat). 1-line edit to `bin/gsd-t.js` `GLOBAL_BIN_TOOLS` array adds `"live-activity-report.cjs"` so the global dashboard at `~/.claude/scripts/gsd-t-dashboard-server.js` resolves it from `~/.claude/bin/live-activity-report.cjs`. Hot-patched immediately. Doctor reports "All 2 global bin tools installed".
43
+ - **D2 m54-d2-rail-and-spec** (3 tasks): additive section `<section id="rail-live-activity">` in `scripts/gsd-t-transcript.html` between MAIN SESSION and LIVE SPAWNS. CSS `@keyframes accent-pulse` (~1.5s cycle) scoped to `.la-pulsing` class only. 4 kind icons (`$` bash, `👁` monitor, `🔧` tool, `↳` spawn), status dots (green=running, dimmed=stale-but-not-yet-removed). `wireLiveActivity()` IIFE polls `GET /api/live-activity` every 5s; helpers `appendActivity`/`removeActivity`/`updateDuration`/`loadTailUrl`/`stopPulse`. 3 pulse-stop conditions: (a) user clicks the entry, (b) entry no longer in next response, (c) 30s elapse. Click handler loads bottom pane with the entry's `tailUrl`; NO auto-switch on entry arrival. 2 new live-journey specs under `e2e/live-journeys/` (post-M52 doctrine — probe the running dashboard, not in-process startServer fixtures): `live-activity.spec.ts` (real `bash -c "sleep 30"` via `child_process.spawn`; asserts entry within 10s, `.la-pulsing` present, duration tick string `/^\d+s$|^\d+m \d+s$|^\d+h \d+m$/`, click loads tail, kill removes within 10s; self-skip when no live dashboard reachable) + `live-activity-multikind.spec.ts` (3 concurrent kinds, dedup by tool_use_id verified). 2 new entries added to `.gsd-t/journey-manifest.json`.
44
+ - **Contract**: `.gsd-t/contracts/live-activity-contract.md` flipped v0.1.0 PROPOSED → **v1.0.0 STABLE** on D1 T5. Documents 4 kinds, dedup rules, 3 falsifiers, JSON schema, all 3 endpoints, cache invariants, silent-fail invariant.
45
+ - **Integration checkpoints**: `.gsd-t/contracts/m54-integration-points.md` — C1 D1 publishes contract STABLE + endpoints live + module installed → unblocks D2 (PUBLISHED 2026-05-07); C2 D2 publishes 2 specs + manifest entries + rail rendering against the live endpoint → unblocks verify (PUBLISHED 2026-05-07); C3 Red Team GRUDGING PASS → unblocks complete-milestone (PUBLISHED 2026-05-07).
46
+ - **Adversarial Red Team** (post-wave): 5/5 broken patches authored, applied, caught by tests, reverted. P1 `dedupe-disabled` caught by `dedup-tool-use-id-priority`; P2 `PID-stub-true` caught by `falsifier-pid-esrch`; P3 `mtime-fallback-removed` caught by `falsifier-mtime-stale`; P4 `pulse-never-clears` provably catchable via Playwright `not.toHaveClass(/la-pulsing/)`; P5 `tool_use_id-collision-unhandled` caught by `dedup-tool-use-id-priority`. **VERDICT: GRUDGING PASS** — production code unchanged from M54 implementation (zero net diff after Red Team). Findings in `.gsd-t/red-team-report.md` § "M54 LIVE-ACTIVITY RED TEAM".
47
+ - **Verification**: full unit suite **2262/2262 pass** (baseline 2233 + 29 M54 new — 20 detector tests + 9 handler tests; zero regressions). Playwright **39 pass + 23 self-skip in 2.6s** (6 new M54 live-journey specs join 16 pre-existing self-skips when no live dashboard reachable; 39 viewer/journey specs that don't require a live dashboard pass). `gsd-t check-coverage` reports `OK: 21 listeners, 16 specs` exit 0. `gsd-t doctor` exit 0 with "All 2 global bin tools installed". Goal-Backward: PASS (12 REQs checked, 0 placeholder patterns).
48
+ - **Files** (additive only — no deletions, no replacements):
49
+ - New: `bin/live-activity-report.cjs`, `.gsd-t/contracts/live-activity-contract.md`, `.gsd-t/contracts/m54-integration-points.md`, `test/m54-d1-live-activity-report.test.js` (20 tests), `test/m54-d1-dashboard-handlers.test.js` (9 tests), `e2e/live-journeys/live-activity.spec.ts`, `e2e/live-journeys/live-activity-multikind.spec.ts`.
50
+ - Additive edits: `scripts/gsd-t-dashboard-server.js` (3 handlers + 3 routes), `bin/gsd-t.js` (1-line `GLOBAL_BIN_TOOLS` entry), `scripts/gsd-t-transcript.html` (section markup + CSS keyframes + `wireLiveActivity()` IIFE), `.gsd-t/journey-manifest.json` (+2 entries), `docs/architecture.md` (M54 section), `docs/requirements.md` (REQ-M54 rows done), `package.json` (3.23.11 → 3.24.10).
51
+ - **Versioning**: minor bump per "new feature milestone" doctrine. Tag `v3.24.10` (local).
52
+
5
53
  ## [3.23.11] - 2026-05-07
6
54
 
7
55
  ### Fixed — `/api/parallelism` 500 — install `parallelism-report.cjs` to `~/.claude/bin/`
package/README.md CHANGED
@@ -112,6 +112,13 @@ gsd-t parallel --dry-run # Print worker plan tabl
112
112
  gsd-t parallel --mode in-session --dry-run # 85% orchestrator-CW ceiling; N=1 floor
113
113
  gsd-t parallel --mode unattended --dry-run # 60% per-worker ceiling; > 60% → task_split
114
114
  gsd-t parallel --milestone M44 --domain m44-d2-parallel-cli --dry-run
115
+
116
+ # CLI-Preflight + Brief + Verify-Gate (M55 — deterministic state checks + parallel substrate)
117
+ gsd-t preflight --json # 6 built-in state checks; exit 0/4
118
+ gsd-t brief --kind execute --domain X --spawn-id Y # ≤2,500-token JSON snapshot for worker spawn
119
+ gsd-t verify-gate --json # Two-track gate: D1 preflight + D2 parallel CLIs
120
+ gsd-t verify-gate --skip-track1 --json # Diagnostic: Track 2 only
121
+ gsd-t verify-gate --max-concurrency 4 --json # Override D3-map default
115
122
  ```
116
123
 
117
124
  `gsd-t parallel` consumes the M44 task-graph (D1) and applies three pre-spawn gates (D4 depgraph validation → D5 file-disjointness → D6 economics) followed by mode-aware headroom/split math. Extends — does not replace — the M40 orchestrator. Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0.
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * branch-guard — verify current git branch matches CLAUDE.md "Expected branch" rule.
5
+ *
6
+ * Severity: error (blocks). If CLAUDE.md has no "Expected branch:" line, the check
7
+ * passes with `msg: "no expected-branch rule set"` (info-grade pass).
8
+ *
9
+ * Pure inspection — runs `git branch --show-current` (read-only) and reads CLAUDE.md.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+
16
+ const ID = 'branch-guard';
17
+
18
+ function _readClaudeMd(projectDir) {
19
+ const file = path.join(projectDir, 'CLAUDE.md');
20
+ if (!fs.existsSync(file)) return null;
21
+ try {
22
+ return fs.readFileSync(file, 'utf8');
23
+ } catch (_) {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function _extractExpectedBranch(text) {
29
+ if (!text) return null;
30
+ // Match `Expected branch:` (case-insensitive), allowing markdown emphasis like `**Expected branch**`,
31
+ // backticks around the value, optional surrounding whitespace.
32
+ // Examples that must match:
33
+ // "Expected branch: main"
34
+ // "Expected branch: `main`"
35
+ // "**Expected branch**: `develop`"
36
+ // "_Expected branch_: feature/foo"
37
+ const re = /\*{0,2}\s*expected\s+branch\s*\*{0,2}\s*:\s*\**\s*`?([^\s`*\n]+)`?/i;
38
+ const m = text.match(re);
39
+ return m ? m[1].trim() : null;
40
+ }
41
+
42
+ function _currentBranch(projectDir) {
43
+ // execSync is synchronous and read-only here.
44
+ const stdout = execSync('git branch --show-current', {
45
+ cwd: projectDir,
46
+ encoding: 'utf8',
47
+ stdio: ['ignore', 'pipe', 'ignore'],
48
+ });
49
+ return String(stdout || '').trim();
50
+ }
51
+
52
+ function run({ projectDir }) {
53
+ const md = _readClaudeMd(projectDir);
54
+ if (md == null) {
55
+ return {
56
+ ok: true,
57
+ msg: 'no CLAUDE.md found, skipping',
58
+ };
59
+ }
60
+ const expected = _extractExpectedBranch(md);
61
+ if (!expected) {
62
+ return {
63
+ ok: true,
64
+ msg: 'no expected-branch rule set',
65
+ };
66
+ }
67
+
68
+ let actual;
69
+ try {
70
+ actual = _currentBranch(projectDir);
71
+ } catch (err) {
72
+ return {
73
+ ok: false,
74
+ msg: 'git branch --show-current failed: ' + (err && err.message || err),
75
+ details: { expected },
76
+ };
77
+ }
78
+
79
+ if (!actual) {
80
+ return {
81
+ ok: false,
82
+ msg: 'detached HEAD or empty branch (expected ' + expected + ')',
83
+ details: { expected, actual: '' },
84
+ };
85
+ }
86
+
87
+ if (actual === expected) {
88
+ return {
89
+ ok: true,
90
+ msg: 'on expected branch ' + expected,
91
+ details: { expected, actual },
92
+ };
93
+ }
94
+
95
+ return {
96
+ ok: false,
97
+ msg: 'on ' + actual + ', expected ' + expected,
98
+ details: { expected, actual },
99
+ };
100
+ }
101
+
102
+ module.exports = {
103
+ id: ID,
104
+ severity: 'error',
105
+ run,
106
+ // Test-only exports
107
+ _extractExpectedBranch,
108
+ _readClaudeMd,
109
+ _currentBranch,
110
+ };
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * contracts-stable — flag DRAFT/PROPOSED contracts when project is past
5
+ * the PARTITIONED milestone state.
6
+ *
7
+ * Severity: warn.
8
+ *
9
+ * Reads:
10
+ * - .gsd-t/contracts/*.md (looking for `Status: DRAFT` or `Status: PROPOSED`)
11
+ * - .gsd-t/progress.md (looking for milestone state past PARTITIONED:
12
+ * ACTIVE, EXECUTING, EXECUTED, INTEGRATING, INTEGRATED,
13
+ * VERIFYING, VERIFIED, COMPLETED, etc.)
14
+ *
15
+ * If the project is NOT past PARTITIONED, this check is a noop pass — DRAFT/PROPOSED
16
+ * contracts are expected pre-execute. After execute starts, they should all be STABLE.
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const ID = 'contracts-stable';
23
+
24
+ // States that count as "past PARTITIONED". Anything strictly before partition
25
+ // (DEFINED, PARTITIONED itself) is fine to have DRAFT/PROPOSED contracts.
26
+ const POST_PARTITIONED_STATES = [
27
+ 'ACTIVE',
28
+ 'EXECUTING',
29
+ 'EXECUTED',
30
+ 'TEST-SYNCING',
31
+ 'TEST-SYNCED',
32
+ 'INTEGRATING',
33
+ 'INTEGRATED',
34
+ 'VERIFYING',
35
+ 'VERIFIED',
36
+ 'COMPLETED',
37
+ ];
38
+
39
+ function _isPastPartitioned(progressContent) {
40
+ if (!progressContent) return false;
41
+ // Look for any "Status: <state>" line where state is post-partitioned.
42
+ // Match on lines (case-insensitive); exclude comments.
43
+ const re = /^\s*Status\s*:\s*\**\s*([A-Za-z\-]+)/gim;
44
+ let match;
45
+ while ((match = re.exec(progressContent)) !== null) {
46
+ const state = match[1].toUpperCase();
47
+ if (POST_PARTITIONED_STATES.includes(state)) return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ function _scanContracts(contractsDir) {
53
+ if (!fs.existsSync(contractsDir)) return [];
54
+ let entries;
55
+ try {
56
+ entries = fs.readdirSync(contractsDir);
57
+ } catch (_) {
58
+ return [];
59
+ }
60
+ const offenders = [];
61
+ for (const filename of entries) {
62
+ if (!filename.endsWith('.md')) continue;
63
+ const full = path.join(contractsDir, filename);
64
+ let content;
65
+ try {
66
+ content = fs.readFileSync(full, 'utf8');
67
+ } catch (_) {
68
+ continue;
69
+ }
70
+ // Match a Status field anywhere; capture DRAFT/PROPOSED specifically.
71
+ // Must allow markdown emphasis like `**DRAFT**` and surrounding `>` blockquotes.
72
+ const re = /^[\s>]*Status\s*:\s*\**\s*(DRAFT|PROPOSED)\s*\**/im;
73
+ const m = content.match(re);
74
+ if (m) {
75
+ offenders.push({ file: filename, status: m[1].toUpperCase() });
76
+ }
77
+ }
78
+ return offenders;
79
+ }
80
+
81
+ function run({ projectDir }) {
82
+ const contractsDir = path.join(projectDir, '.gsd-t', 'contracts');
83
+ const progressFile = path.join(projectDir, '.gsd-t', 'progress.md');
84
+
85
+ let progressContent = null;
86
+ if (fs.existsSync(progressFile)) {
87
+ try {
88
+ progressContent = fs.readFileSync(progressFile, 'utf8');
89
+ } catch (_) {
90
+ progressContent = null;
91
+ }
92
+ }
93
+
94
+ const offenders = _scanContracts(contractsDir);
95
+ const past = _isPastPartitioned(progressContent);
96
+
97
+ if (!past) {
98
+ return {
99
+ ok: true,
100
+ msg: 'project not past PARTITIONED; ' + offenders.length + ' DRAFT/PROPOSED contract(s) acceptable',
101
+ details: { offenders },
102
+ };
103
+ }
104
+
105
+ if (offenders.length === 0) {
106
+ return {
107
+ ok: true,
108
+ msg: 'all contracts STABLE',
109
+ };
110
+ }
111
+
112
+ return {
113
+ ok: false,
114
+ msg: offenders.length + ' DRAFT/PROPOSED contract(s) past PARTITIONED: ' +
115
+ offenders.map((o) => o.file + '(' + o.status + ')').join(', '),
116
+ details: { offenders },
117
+ };
118
+ }
119
+
120
+ module.exports = {
121
+ id: ID,
122
+ severity: 'warn',
123
+ run,
124
+ // Test-only exports
125
+ _isPastPartitioned,
126
+ _scanContracts,
127
+ POST_PARTITIONED_STATES,
128
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * deps-installed — verify Node deps are installed and lockfile is fresh.
5
+ *
6
+ * Severity: warn (records, does not block).
7
+ *
8
+ * Pass conditions:
9
+ * - `node_modules/` exists AND
10
+ * - `package-lock.json` mtime ≥ `package.json` mtime
11
+ *
12
+ * Edge cases:
13
+ * - No `package.json` at all → ok:true (non-Node project; nothing to check)
14
+ * - `package.json` exists but no `package-lock.json` → ok:false
15
+ * - `node_modules/` missing → ok:false
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const ID = 'deps-installed';
22
+
23
+ function _mtime(file) {
24
+ try {
25
+ return fs.statSync(file).mtimeMs;
26
+ } catch (_) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function run({ projectDir }) {
32
+ const pkgPath = path.join(projectDir, 'package.json');
33
+ const lockPath = path.join(projectDir, 'package-lock.json');
34
+ const nmPath = path.join(projectDir, 'node_modules');
35
+
36
+ const pkgMtime = _mtime(pkgPath);
37
+ if (pkgMtime == null) {
38
+ return {
39
+ ok: true,
40
+ msg: 'no package.json (non-Node project)',
41
+ };
42
+ }
43
+
44
+ const lockMtime = _mtime(lockPath);
45
+ const nmExists = fs.existsSync(nmPath);
46
+
47
+ if (!nmExists && lockMtime == null) {
48
+ return {
49
+ ok: false,
50
+ msg: 'node_modules/ missing AND package-lock.json missing',
51
+ details: { nmExists, hasLock: false },
52
+ };
53
+ }
54
+ if (!nmExists) {
55
+ return {
56
+ ok: false,
57
+ msg: 'node_modules/ missing — run `npm install`',
58
+ details: { nmExists: false, hasLock: lockMtime != null },
59
+ };
60
+ }
61
+ if (lockMtime == null) {
62
+ return {
63
+ ok: false,
64
+ msg: 'package-lock.json missing',
65
+ details: { nmExists: true, hasLock: false },
66
+ };
67
+ }
68
+ if (lockMtime < pkgMtime) {
69
+ return {
70
+ ok: false,
71
+ msg: 'package-lock.json older than package.json — run `npm install`',
72
+ details: { lockMtime, pkgMtime, ageDelta_ms: pkgMtime - lockMtime },
73
+ };
74
+ }
75
+
76
+ return {
77
+ ok: true,
78
+ msg: 'node_modules/ present, lockfile fresh',
79
+ details: { lockMtime, pkgMtime },
80
+ };
81
+ }
82
+
83
+ module.exports = {
84
+ id: ID,
85
+ severity: 'warn',
86
+ run,
87
+ // Test-only exports
88
+ _mtime,
89
+ };
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * manifest-fresh — verify .gsd-t/journey-manifest.json is newer than every
5
+ * file under e2e/journeys/.
6
+ *
7
+ * Severity: info. Never blocks (top-level `ok` only flips on error-severity).
8
+ *
9
+ * If the manifest or the e2e/journeys/ dir is missing, the check is an
10
+ * info-grade noop pass with `msg: "no manifest, skipping"`.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const ID = 'manifest-fresh';
17
+
18
+ function _walkSync(dir, out) {
19
+ let entries;
20
+ try {
21
+ entries = fs.readdirSync(dir, { withFileTypes: true });
22
+ } catch (_) {
23
+ return;
24
+ }
25
+ for (const e of entries) {
26
+ const full = path.join(dir, e.name);
27
+ if (e.isDirectory()) {
28
+ _walkSync(full, out);
29
+ } else if (e.isFile()) {
30
+ out.push(full);
31
+ }
32
+ }
33
+ }
34
+
35
+ function _mtime(file) {
36
+ try {
37
+ return fs.statSync(file).mtimeMs;
38
+ } catch (_) {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function run({ projectDir }) {
44
+ const manifestPath = path.join(projectDir, '.gsd-t', 'journey-manifest.json');
45
+ const journeysDir = path.join(projectDir, 'e2e', 'journeys');
46
+
47
+ const manifestMtime = _mtime(manifestPath);
48
+ const journeysExists = fs.existsSync(journeysDir);
49
+
50
+ if (manifestMtime == null || !journeysExists) {
51
+ return {
52
+ ok: true,
53
+ msg: 'no manifest, skipping',
54
+ };
55
+ }
56
+
57
+ const files = [];
58
+ _walkSync(journeysDir, files);
59
+
60
+ if (files.length === 0) {
61
+ return {
62
+ ok: true,
63
+ msg: 'manifest present, no journey files to compare',
64
+ };
65
+ }
66
+
67
+ const stale = [];
68
+ for (const f of files) {
69
+ const m = _mtime(f);
70
+ if (m == null) continue;
71
+ if (m > manifestMtime) {
72
+ stale.push({ file: path.relative(projectDir, f), mtime: m });
73
+ }
74
+ }
75
+
76
+ if (stale.length === 0) {
77
+ return {
78
+ ok: true,
79
+ msg: 'manifest fresher than ' + files.length + ' journey file(s)',
80
+ details: { manifestMtime, scanned: files.length },
81
+ };
82
+ }
83
+
84
+ return {
85
+ ok: false,
86
+ msg: 'manifest stale: ' + stale.length + ' journey file(s) newer',
87
+ details: { manifestMtime, stale, scanned: files.length },
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ id: ID,
93
+ severity: 'info',
94
+ run,
95
+ // Test-only exports
96
+ _walkSync,
97
+ _mtime,
98
+ };
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ports-free — verify required dev ports are unbound.
5
+ *
6
+ * Severity: error (blocks).
7
+ *
8
+ * Reads `requiredFreePorts: number[]` from `.gsd-t/.unattended/config.json`.
9
+ * If absent or empty, check is a noop pass.
10
+ *
11
+ * For each port, runs `lsof -nP -iTCP:<port> -sTCP:LISTEN`. Exit 0 + non-empty
12
+ * stdout means a process is listening (FAIL). Exit 1 means no listener (PASS).
13
+ * Anything else (lsof missing, etc.) is treated as a soft fail with note.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { execSync } = require('child_process');
19
+
20
+ const ID = 'ports-free';
21
+
22
+ function _readRequiredPorts(projectDir) {
23
+ const cfgPath = path.join(projectDir, '.gsd-t', '.unattended', 'config.json');
24
+ if (!fs.existsSync(cfgPath)) return [];
25
+ let raw;
26
+ try {
27
+ raw = fs.readFileSync(cfgPath, 'utf8');
28
+ } catch (_) {
29
+ return [];
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(raw);
34
+ } catch (_) {
35
+ return [];
36
+ }
37
+ const ports = parsed && parsed.requiredFreePorts;
38
+ if (!Array.isArray(ports)) return [];
39
+ // Coerce + filter to positive integers.
40
+ return ports
41
+ .map((p) => Number(p))
42
+ .filter((p) => Number.isInteger(p) && p > 0 && p < 65536);
43
+ }
44
+
45
+ function _portInUse(port) {
46
+ // execSync throws on non-zero exit. lsof exits 1 when nothing matches.
47
+ try {
48
+ const stdout = execSync('lsof -nP -iTCP:' + port + ' -sTCP:LISTEN', {
49
+ encoding: 'utf8',
50
+ stdio: ['ignore', 'pipe', 'ignore'],
51
+ });
52
+ return { listening: String(stdout || '').trim().length > 0 };
53
+ } catch (err) {
54
+ // lsof exits 1 when no match — that's the happy "port free" path.
55
+ if (err && err.status === 1) return { listening: false };
56
+ // Anything else (e.g., lsof not installed) — surface as unknown.
57
+ return { listening: false, error: err && err.message || String(err) };
58
+ }
59
+ }
60
+
61
+ function run({ projectDir }) {
62
+ const ports = _readRequiredPorts(projectDir);
63
+ if (ports.length === 0) {
64
+ return {
65
+ ok: true,
66
+ msg: 'no requiredFreePorts configured',
67
+ };
68
+ }
69
+
70
+ const occupied = [];
71
+ const errors = [];
72
+ for (const port of ports) {
73
+ const r = _portInUse(port);
74
+ if (r.error) errors.push({ port, error: r.error });
75
+ if (r.listening) occupied.push(port);
76
+ }
77
+
78
+ if (occupied.length === 0 && errors.length === 0) {
79
+ return {
80
+ ok: true,
81
+ msg: 'all ' + ports.length + ' required ports are free',
82
+ details: { ports },
83
+ };
84
+ }
85
+
86
+ if (occupied.length > 0) {
87
+ return {
88
+ ok: false,
89
+ msg: 'occupied ports: ' + occupied.join(', '),
90
+ details: { ports, occupied, errors },
91
+ };
92
+ }
93
+
94
+ // Only errors, no occupancy — fail closed: an error-severity check shouldn't
95
+ // pass when we can't actually verify the ports.
96
+ return {
97
+ ok: false,
98
+ msg: 'lsof probe failed for ' + errors.length + ' port(s)',
99
+ details: { ports, errors },
100
+ };
101
+ }
102
+
103
+ module.exports = {
104
+ id: ID,
105
+ severity: 'error',
106
+ run,
107
+ // Test-only exports
108
+ _readRequiredPorts,
109
+ _portInUse,
110
+ };