@tekyzinc/gsd-t 3.24.10 → 3.25.11
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/CHANGELOG.md +43 -0
- package/README.md +7 -0
- package/bin/cli-preflight-checks/branch-guard.cjs +110 -0
- package/bin/cli-preflight-checks/contracts-stable.cjs +128 -0
- package/bin/cli-preflight-checks/deps-installed.cjs +89 -0
- package/bin/cli-preflight-checks/manifest-fresh.cjs +98 -0
- package/bin/cli-preflight-checks/ports-free.cjs +110 -0
- package/bin/cli-preflight-checks/working-tree-state.cjs +149 -0
- package/bin/cli-preflight.cjs +265 -0
- package/bin/gsd-t-context-brief-kinds/design-verify.cjs +139 -0
- package/bin/gsd-t-context-brief-kinds/execute.cjs +205 -0
- package/bin/gsd-t-context-brief-kinds/qa.cjs +130 -0
- package/bin/gsd-t-context-brief-kinds/red-team.cjs +131 -0
- package/bin/gsd-t-context-brief-kinds/scan.cjs +118 -0
- package/bin/gsd-t-context-brief-kinds/verify.cjs +157 -0
- package/bin/gsd-t-context-brief.cjs +395 -0
- package/bin/gsd-t-ratelimit-probe-worker.cjs +236 -0
- package/bin/gsd-t-ratelimit-probe.cjs +648 -0
- package/bin/gsd-t-verify-gate-judge.cjs +224 -0
- package/bin/gsd-t-verify-gate.cjs +612 -0
- package/bin/gsd-t.js +58 -2
- package/bin/m55-substrate-proof.cjs +134 -0
- package/bin/parallel-cli-tee.cjs +206 -0
- package/bin/parallel-cli.cjs +478 -0
- package/commands/gsd-t-execute.md +31 -0
- package/commands/gsd-t-help.md +21 -0
- package/commands/gsd-t-verify.md +38 -0
- package/docs/architecture.md +129 -0
- package/docs/diagrams/.gsd-t/.context-meter-state.json +10 -0
- package/docs/diagrams/.gsd-t/context-meter.log +9 -0
- package/docs/diagrams/.gsd-t/events/2026-05-08.jsonl +45 -0
- package/docs/diagrams/.gsd-t/events/2026-05-09.jsonl +1 -0
- package/docs/diagrams/.gsd-t/heartbeat-cd9e7f59-ba5b-406a-9ed6-16762f039e81.jsonl +48 -0
- package/docs/diagrams/01-top-level-map-d2.png +0 -0
- package/docs/diagrams/01-top-level-map.d2 +77 -0
- package/docs/diagrams/01-top-level-map.mmd +48 -0
- package/docs/diagrams/01-top-level-map.png +0 -0
- package/docs/diagrams/01-top-level-map.svg +126 -0
- package/docs/diagrams/02-milestone-lifecycle-d2.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.d2 +38 -0
- package/docs/diagrams/02-milestone-lifecycle.mmd +36 -0
- package/docs/diagrams/02-milestone-lifecycle.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.svg +114 -0
- package/docs/diagrams/03-wave-mode-d2.png +0 -0
- package/docs/diagrams/03-wave-mode.d2 +33 -0
- package/docs/diagrams/03-wave-mode.mmd +21 -0
- package/docs/diagrams/03-wave-mode.png +0 -0
- package/docs/diagrams/03-wave-mode.svg +113 -0
- package/docs/diagrams/04-design-to-code-d2.png +0 -0
- package/docs/diagrams/04-design-to-code.d2 +35 -0
- package/docs/diagrams/04-design-to-code.mmd +29 -0
- package/docs/diagrams/04-design-to-code.png +0 -0
- package/docs/diagrams/04-design-to-code.svg +115 -0
- package/docs/diagrams/05-backlog-d2.png +0 -0
- package/docs/diagrams/05-backlog.d2 +40 -0
- package/docs/diagrams/05-backlog.mmd +20 -0
- package/docs/diagrams/05-backlog.png +0 -0
- package/docs/diagrams/05-backlog.svg +113 -0
- package/docs/diagrams/06-automation-utilities-d2.png +0 -0
- package/docs/diagrams/06-automation-utilities.d2 +48 -0
- package/docs/diagrams/06-automation-utilities.mmd +47 -0
- package/docs/diagrams/06-automation-utilities.png +0 -0
- package/docs/diagrams/06-automation-utilities.svg +110 -0
- package/docs/diagrams/_theme.d2 +86 -0
- package/docs/requirements.md +31 -0
- package/docs/workflow-diagram.md +338 -0
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +46 -0
- package/templates/prompts/design-verify-subagent.md +3 -0
- package/templates/prompts/qa-subagent.md +3 -0
- package/templates/prompts/red-team-subagent.md +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [3.25.11] - 2026-05-09
|
|
6
|
+
|
|
7
|
+
### Fixed — M55 propagation gaps + misleading update-all status
|
|
8
|
+
|
|
9
|
+
Three small fixes to `bin/gsd-t.js`, all caught immediately post-v3.25.10 ship by the user noticing "18 already current" couldn't possibly be right one minute after publish.
|
|
10
|
+
|
|
11
|
+
- **Patch 1**: Added `parallel-cli.cjs` to `GLOBAL_BIN_TOOLS`. M55 D5 wired 4 of 5 new substrate binaries into the global propagation list (`cli-preflight`, `gsd-t-context-brief`, `gsd-t-verify-gate`, `gsd-t-verify-gate-judge`) but missed `parallel-cli.cjs` — the substrate engine itself. Result: `~/.claude/bin/parallel-cli.cjs` was missing on every install. Now propagates.
|
|
12
|
+
- **Patch 2**: Added 6 M55 binaries to `PROJECT_BIN_TOOLS` (`cli-preflight.cjs`, `parallel-cli.cjs`, `parallel-cli-tee.cjs`, `gsd-t-context-brief.cjs`, `gsd-t-verify-gate.cjs`, `gsd-t-verify-gate-judge.cjs`). M55 D5 only added them to the global tier — projects had to reach for the global CLI for every M55 dispatch. Now they're available locally per-project too, enabling `node bin/cli-preflight.cjs` style invocations from project workflows. Each registered project gets these copied on the next `gsd-t update-all`.
|
|
13
|
+
- **Patch 3**: Replaced `"X — already up to date"` with `"X — no migrations needed (CLAUDE.md guard, CHANGELOG, bin tools, unattended config all current)"` in `updateSingleProject`. The previous string falsely implied the project was at the latest GSD-T version, but the function only checks 7 specific migration ops — a project showing "already up to date" was just one whose 7 migrations had already run, not one running v3.25.10. Honest message now explains what was actually checked.
|
|
14
|
+
- **Tests**: 2487 / 2487 pass clean (zero regressions).
|
|
15
|
+
- **Why a same-day patch and not a defer**: M55's whole point is making the new substrate available for M56 to wire into Quick + Debug + upper-stage commands. Shipping with the binaries half-propagated would force M56 to wait on a propagation fix anyway. Better to land the fix now and have a clean foundation.
|
|
16
|
+
|
|
17
|
+
## [3.25.10] - 2026-05-09
|
|
18
|
+
|
|
19
|
+
### Added — M55 CLI-Preflight Pattern + Parallel-CLI Substrate + Rate-Limit Map + Context Briefs + Verify-Gate (minor: new feature milestone)
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
- **Scope**: 5 file-disjoint domains, single supervisor-driven build (5 worker iterations).
|
|
23
|
+
- **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`.
|
|
24
|
+
- **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`.
|
|
25
|
+
- **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).
|
|
26
|
+
- **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`.
|
|
27
|
+
- **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`.
|
|
28
|
+
- **Wire-ins** (D5):
|
|
29
|
+
- `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.
|
|
30
|
+
- `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.
|
|
31
|
+
- `commands/gsd-t-verify.md` Step 2 — additive `<!-- M55-D5: verify-gate wire-in -->` block invoking verify-gate + piping into judge.
|
|
32
|
+
- `templates/prompts/{qa,red-team,design-verify}-subagent.md` — additive `<!-- M55-D5: brief-first rule -->` line in each.
|
|
33
|
+
- **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).
|
|
34
|
+
- **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.**
|
|
35
|
+
- **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`.
|
|
36
|
+
- **Falsifiable success criteria** (per `feedback_measure_dont_claim.md`):
|
|
37
|
+
- **SC1 ✅** state-preflight contract `cli-preflight-contract.md` v1.0.0 STABLE published.
|
|
38
|
+
- **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).
|
|
39
|
+
- **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.
|
|
40
|
+
- **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.
|
|
41
|
+
- **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.
|
|
42
|
+
- **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.
|
|
43
|
+
- **SC7 ✅** Red Team GRUDGING PASS — 6/6 broken patches caught (≥5 target exceeded), 0 real bugs.
|
|
44
|
+
- **SC8 ✅** zero regressions on `npm test` — 2487 / 2485 pass, 2 pre-existing documented env-bleed failures unchanged.
|
|
45
|
+
- **Versioning**: minor bump per "new feature milestone" doctrine. Tag `v3.25.10` (local).
|
|
46
|
+
- **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.
|
|
47
|
+
|
|
5
48
|
## [3.24.10] - 2026-05-07
|
|
6
49
|
|
|
7
50
|
### Added — M54 Live Activity Visibility (minor: new feature milestone)
|
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
|
+
};
|