@tekyzinc/gsd-t 4.1.10 → 4.2.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.
- package/CHANGELOG.md +17 -0
- package/README.md +3 -0
- package/bin/gsd-t-traceability-gate.cjs +338 -0
- package/bin/gsd-t.js +13 -0
- package/commands/gsd-t-help.md +8 -0
- package/commands/gsd-t-plan.md +5 -3
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +1 -1
- package/templates/prompts/pre-mortem-subagent.md +46 -0
- package/templates/workflows/gsd-t-phase.workflow.js +82 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.2.10] - 2026-06-05 (M83 Left-Shifted Plan Hardening - minor)
|
|
6
|
+
|
|
7
|
+
### Added - Plan-phase hardening: catch dead deliverables and edge cases BEFORE execute
|
|
8
|
+
|
|
9
|
+
Left-shifts failure detection from verify to plan. Adversarial validation (the Red Team) ran only at verify — after code exists — so a milestone whose headline capability shipped as DEAD CODE (the NiceNote M5 incident: a 100MB+ chunked reader built but never wired into `openPath`, with no test exercising it) burned **four verify cycles** re-litigating the milestone's reason to exist. The root cause was in the plan: it never bound each acceptance criterion to a code path + a killing test, and nothing adversarial reviewed the design before code was written. The `plan` phase now runs two blocking gates before execute.
|
|
10
|
+
|
|
11
|
+
- **Acceptance-traceability gate** (deterministic) — `bin/gsd-t-traceability-gate.cjs`, dispatched as `gsd-t traceability-gate`. Parses `.gsd-t/domains/*/tasks.md`; every behavioral task (one declaring acceptance criteria) must bind its ACs to a `**Files**` code path AND a named killing test; a `**Headline:** true` task must have BOTH a real implementation path and a test. Exit 4 blocks execute. Field detection is emphasis-stripped + colon-position-agnostic (`**Label**:` ≡ `**Label:**`); task blocks are detected by any non-structural heading bearing an AC (descriptive headings are not dropped); the test check is tied to the Test/Files/AC fields only (an incidental runner word in a Dependencies note does not clear it); pytest `test_*.py` / `*_test.py` conventions are preserved.
|
|
12
|
+
- **Adversarial pre-mortem** (generative) — `templates/prompts/pre-mortem-subagent.md`, an opus, fresh-context, assume-the-plan-is-flawed reviewer wired into the plan workflow. Predicts edge-case / dead-deliverable / NFR / shallow-test failures and converts each blocking finding into a **required test** the plan must adopt (advisory notes forbidden — that is how M5's chunk reader shipped three data-loss bugs across three cycles). Verdict `BLOCK` / `CLEARED`.
|
|
13
|
+
- The two gates are the temporal dual of the Red Team: attack the design at plan, not just the code at verify. The deterministic gate runs first and fails CLOSED (an unevaluable gate blocks); the pre-mortem cannot approve a gate-blocked plan.
|
|
14
|
+
- New CLI `gsd-t traceability-gate [--milestone Mxx] [--tasks FILE]` (exit 0/4/64), added to project + global bin tools. Contract `.gsd-t/contracts/plan-hardening-contract.md` v1.0.0 STABLE. `gsd-t-plan.md` + the phase-workflow plan objective updated to require traceable tasks up front.
|
|
15
|
+
- **Verification**: orthogonal triad ran. Adversarial Workflow Red Team (Opus, fresh context) FAILed first pass (1 CRITICAL — colon-inside-bold markdown defeated all field detection, silently passing the literal M5 dead-code plan — + 2 HIGH + 2 MEDIUM), all fixed; re-validation found a regression the CRITICAL fix introduced (underscore-stripping broke pytest paths, HIGH), fixed; final re-validation GRUDGING-PASS (14/14 checks, no new HIGH/CRITICAL). Real-sandbox acceptance gate passed (gate fires through the Workflow sandbox and blocks the bad plan). Suite 1372/0/4 (+15 M83 tests). Self-tested against the actual NiceNote M5 dead-code plan — the gate FAILs it at plan time, which is the milestone's reason to exist.
|
|
16
|
+
- Origin: review of the NiceNote 9-milestone build, where the triad caught real bugs at verify but late; the user's proposal for an adversarial risk-assessment agent at plan.
|
|
17
|
+
|
|
18
|
+
### Versioning
|
|
19
|
+
|
|
20
|
+
Minor bump 4.1.10 → 4.2.10 (new feature, additive; patch reset to 10).
|
|
21
|
+
|
|
5
22
|
## [4.1.10] - 2026-06-05 (M82 Competition Mode - minor)
|
|
6
23
|
|
|
7
24
|
### Added - Competition Mode: generate-and-judge for upstream, pre-contract phases
|
package/README.md
CHANGED
|
@@ -123,8 +123,11 @@ gsd-t ci-parity --json # M57: reproduce the pro
|
|
|
123
123
|
gsd-t test-data --list [--run ID] [--json] # M58: list test-data ledger entries
|
|
124
124
|
gsd-t test-data --purge --run ID [--dry-run] [--json] # M58: purge tagged test data after Verify (Step 4.5)
|
|
125
125
|
gsd-t competition-judge --in SPEC.json [--project-dir P] # M82: generate-and-judge selection oracle (partition / generic)
|
|
126
|
+
gsd-t traceability-gate --milestone Mxx [--project-dir P] # M83: plan-phase acceptance-traceability gate (AC → path → killing test)
|
|
126
127
|
```
|
|
127
128
|
|
|
129
|
+
**Plan Hardening (M83).** The `plan` phase now runs two blocking gates before execute, so a plan can't ship a dead deliverable: a deterministic **acceptance-traceability gate** (`gsd-t traceability-gate` — every AC must bind to a code path + a killing test; the headline capability needs both impl and test) and an adversarial **pre-mortem** agent (opus, fresh-context, predicts edge-case/NFR/dead-deliverable failures and requires a test for each). The temporal dual of the Red Team — attack the design at plan, not just the code at verify. Origin: a build where the headline capability shipped as dead code and burned 4 verify cycles. See `.gsd-t/contracts/plan-hardening-contract.md`.
|
|
130
|
+
|
|
128
131
|
**Competition Mode (M82).** Opt-in `--competition N` (N 2–5) on upstream, pre-contract phases (`/gsd-t-partition`, `/gsd-t-milestone`, `/gsd-t-design-decompose`) fans out N parallel candidate producers and a judge selects the winner — the generative dual of the orthogonal validation triad. Partition uses an *objective* file-disjointness oracle as the judge (a calculator, not a biased critic); subjective phases use a blind + different-model + rubric judge. Default off. See `.gsd-t/contracts/competition-mode-contract.md`.
|
|
129
132
|
|
|
130
133
|
`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,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gsd-t-traceability-gate — M83 D1
|
|
5
|
+
*
|
|
6
|
+
* The plan-phase acceptance-traceability gate. The deterministic half of
|
|
7
|
+
* Left-Shifted Plan Hardening (the adversarial pre-mortem agent is the
|
|
8
|
+
* generative half). Contract: .gsd-t/contracts/plan-hardening-contract.md.
|
|
9
|
+
*
|
|
10
|
+
* ORIGIN (NiceNote M5 incident, 2026-06-05): M5's headline capability (AC-6,
|
|
11
|
+
* 100MB+ chunked read) shipped as DEAD CODE — the chunk reader was built but
|
|
12
|
+
* openPath still materialized whole files, and NO test asserted the headline
|
|
13
|
+
* capability, so the suite stayed green. The triad burned 4 verify cycles
|
|
14
|
+
* re-litigating the milestone's reason to exist. Root cause: the plan never
|
|
15
|
+
* bound each acceptance criterion to (a) a real code path and (b) a test that
|
|
16
|
+
* FAILS if that path is absent. This gate enforces that binding BEFORE execute.
|
|
17
|
+
*
|
|
18
|
+
* What it checks, per `.gsd-t/domains/* /tasks.md` task block:
|
|
19
|
+
* - Every task that declares **Acceptance criteria** MUST declare **Files**
|
|
20
|
+
* (an implementing code path) — an AC with no path is an unbacked promise.
|
|
21
|
+
* - Every such task MUST declare a TEST reference (a Test/Tests field, a
|
|
22
|
+
* test-runner mention, or a Files entry matching a test path pattern) — an
|
|
23
|
+
* AC with no killing test is the dead-code class (passes vacuously / never
|
|
24
|
+
* exercised). The milestone's HEADLINE capability without a test is exactly
|
|
25
|
+
* the M5 failure.
|
|
26
|
+
* - A task tagged as the milestone HEADLINE (**Headline:** true, or an AC
|
|
27
|
+
* referencing the milestone's named capability) gets a STRICTER check: it
|
|
28
|
+
* MUST have a non-test Files entry (real implementation, not just a test)
|
|
29
|
+
* AND a test entry. A headline with only a test, or only an impl, fails.
|
|
30
|
+
*
|
|
31
|
+
* It does NOT judge whether the code is correct (that's verify) — only whether
|
|
32
|
+
* the PLAN is complete enough that execute can't produce a dead deliverable.
|
|
33
|
+
*
|
|
34
|
+
* Input: --milestone Mxx --project-dir PATH (reads .gsd-t/domains/* /tasks.md).
|
|
35
|
+
* OR --tasks <file> to check a single tasks.md (used by tests).
|
|
36
|
+
* Output: JSON envelope { ok, exitCode, milestone, tasks:[...], violations:[...] }.
|
|
37
|
+
* Exit: 0 all tasks traceable · 4 ≥1 violation (blocks execute) · 64 bad input.
|
|
38
|
+
*
|
|
39
|
+
* Hard rules: zero deps, never throws, pure/read-only.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const fs = require("node:fs");
|
|
43
|
+
const path = require("node:path");
|
|
44
|
+
|
|
45
|
+
// ─── tasks.md parsing ────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
// Red Team CRITICAL/HIGH-3/MEDIUM-1 (M83 verify): markdown field labels appear in
|
|
48
|
+
// BOTH `**Label**: v` (colon outside bold) and `**Label:** v` (colon inside) forms.
|
|
49
|
+
// Matching against the raw line missed the colon-inside form — defeating the entire
|
|
50
|
+
// gate on the canonical M5 dead-code plan. Fix: STRIP emphasis markers first, then
|
|
51
|
+
// match the colon-agnostic bare text. All field detection runs on the bared line.
|
|
52
|
+
function _bare(line) {
|
|
53
|
+
return String(line == null ? "" : line).replace(/[*_`]/g, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Path-safe bare: strips only emphasis that wraps labels (* and backtick), but
|
|
57
|
+
// PRESERVES underscores — pytest's test_*.py / *_test.py conventions depend on
|
|
58
|
+
// them, and TEST_PATH_RE has `_test\.` / `test_` alternatives (Red Team M83
|
|
59
|
+
// recheck HIGH: stripping `_` before the test-path scan false-failed Python plans).
|
|
60
|
+
function _barePath(s) {
|
|
61
|
+
return String(s == null ? "" : s).replace(/[*`]/g, "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A test reference is: an explicit Test/Tests field, a known runner mention, or a
|
|
65
|
+
// Files path that looks like a test file. Kept broad on purpose — the gate asserts
|
|
66
|
+
// a test is NAMED, not that it exists yet (plan precedes execute).
|
|
67
|
+
const TEST_PATH_RE = /(\.test\.|\.spec\.|(^|\/)tests?\/|(^|\/)e2e\/|_test\.|test_|cargo test|vitest|playwright|pytest|jest)/i;
|
|
68
|
+
// Field regexes run on the BARED line, so the colon can be anywhere the label ends.
|
|
69
|
+
const TEST_FIELD_RE = /^\s*[-*]?\s*(tests?|test\s*ref|test\s*coverage|verified\s*by)\s*:/i;
|
|
70
|
+
const FILES_FIELD_RE = /^\s*[-*]?\s*files?\s*:/i;
|
|
71
|
+
const AC_FIELD_RE = /^\s*[-*]?\s*(acceptance(\s*criteria)?|accept|ac)\s*:/i;
|
|
72
|
+
const HEADLINE_FIELD_RE = /^\s*[-*]?\s*headline\s*:\s*(true|yes)/i;
|
|
73
|
+
const HEADING_RE = /^(#{2,4})\s+(.*\S.*)$/;
|
|
74
|
+
|
|
75
|
+
// Headings that are structural, never tasks — so we don't mis-parse a Summary/
|
|
76
|
+
// Overview block as a behavioral task. Everything else that bears an AC field IS
|
|
77
|
+
// assessed (Red Team HIGH-2: do NOT gate task detection on heading wording —
|
|
78
|
+
// anchor on the AC, so a descriptive heading like "Implement the reader" is caught).
|
|
79
|
+
const NON_TASK_HEADING_RE = /^(summary|overview|notes?|context|goal|background|wave\s*history|index|integration\s*points?|dependencies|references?|appendix|tasks)\s*$/i;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a tasks.md into candidate blocks: every `##`–`####` heading starts a
|
|
83
|
+
* block (except the structural-heading skip list). A block becomes a TASK for
|
|
84
|
+
* assessment iff it contains an acceptance-criteria field (decided later in
|
|
85
|
+
* assessTask) — but we keep ALL non-structural blocks so no AC-bearing block is
|
|
86
|
+
* ever dropped on heading wording.
|
|
87
|
+
* @returns {Array<{title, raw, lines}>}
|
|
88
|
+
*/
|
|
89
|
+
function parseTasks(md) {
|
|
90
|
+
const lines = (md || "").split(/\r?\n/);
|
|
91
|
+
const blocks = [];
|
|
92
|
+
let cur = null;
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const m = line.match(HEADING_RE);
|
|
95
|
+
if (m) {
|
|
96
|
+
const title = m[2].trim();
|
|
97
|
+
// Close any open block at every heading.
|
|
98
|
+
if (cur) { blocks.push(cur); cur = null; }
|
|
99
|
+
// Structural headings start no block; everything else does.
|
|
100
|
+
if (!NON_TASK_HEADING_RE.test(_bare(title).trim())) {
|
|
101
|
+
cur = { title, lines: [] };
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (cur) cur.lines.push(line);
|
|
106
|
+
}
|
|
107
|
+
if (cur) blocks.push(cur);
|
|
108
|
+
return blocks.map((t) => ({ title: t.title, raw: t.lines.join("\n"), lines: t.lines }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── per-task traceability assessment ────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
// All field matching runs on the BARED line (emphasis stripped) so colon
|
|
114
|
+
// position inside/outside bold is irrelevant (Red Team CRITICAL fix).
|
|
115
|
+
function fieldValue(lines, re) {
|
|
116
|
+
for (const ln of lines) {
|
|
117
|
+
const bare = _bare(ln);
|
|
118
|
+
if (re.test(bare)) {
|
|
119
|
+
const idx = bare.indexOf(":");
|
|
120
|
+
return idx >= 0 ? bare.slice(idx + 1).trim() : "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Like fieldValue but PRESERVES underscores in the returned value (label is still
|
|
127
|
+
// matched emphasis-agnostically) — used for value-level test-path scans so
|
|
128
|
+
// test_*.py / *_test.py survive (Red Team recheck HIGH).
|
|
129
|
+
function fieldValueRaw(lines, re) {
|
|
130
|
+
for (const ln of lines) {
|
|
131
|
+
if (re.test(_bare(ln))) {
|
|
132
|
+
const raw = _barePath(ln);
|
|
133
|
+
const idx = raw.indexOf(":");
|
|
134
|
+
return idx >= 0 ? raw.slice(idx + 1).trim() : "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasMultiField(lines, re) {
|
|
141
|
+
return lines.some((ln) => re.test(_bare(ln)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Collect the indented/bulleted sub-lines that follow an Acceptance-criteria
|
|
145
|
+
// label up to the next top-level field — these ARE the acceptance criteria, and
|
|
146
|
+
// an AC may name its own verifying test there ("…; verified by cargo test").
|
|
147
|
+
function _acBulletText(lines) {
|
|
148
|
+
const out = [];
|
|
149
|
+
let inAc = false;
|
|
150
|
+
for (const ln of lines) {
|
|
151
|
+
const bare = _bare(ln);
|
|
152
|
+
if (AC_FIELD_RE.test(bare)) { inAc = true; continue; }
|
|
153
|
+
if (!inAc) continue;
|
|
154
|
+
// A new NON-INDENTED "Label:" line closes the AC block.
|
|
155
|
+
if (/^\s*[-*]?\s*[a-z][a-z\s]{1,24}:/i.test(bare) && !/^\s{2,}/.test(ln)) {
|
|
156
|
+
inAc = false; continue;
|
|
157
|
+
}
|
|
158
|
+
out.push(_barePath(ln)); // preserve underscores for test-path detection
|
|
159
|
+
}
|
|
160
|
+
return out.join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A task is "behavioral" (subject to the gate) if it declares acceptance
|
|
165
|
+
* criteria — i.e. it promises an observable behavior. Pure-scaffolding tasks
|
|
166
|
+
* with no ACs are out of scope (nothing to trace).
|
|
167
|
+
*/
|
|
168
|
+
function assessTask(task) {
|
|
169
|
+
const lines = task.lines;
|
|
170
|
+
const hasAc = hasMultiField(lines, AC_FIELD_RE);
|
|
171
|
+
if (!hasAc) {
|
|
172
|
+
return { title: task.title, behavioral: false, violations: [] };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Underscore-preserving values for path/runner scans (Red Team recheck HIGH).
|
|
176
|
+
const filesVal = fieldValueRaw(lines, FILES_FIELD_RE) || "";
|
|
177
|
+
const hasFiles = hasMultiField(lines, FILES_FIELD_RE) && filesVal.replace(/[—–-]/g, "").trim().length > 0;
|
|
178
|
+
|
|
179
|
+
// Test reference (MEDIUM-1 fix): satisfied ONLY by a runner/test-path tied to a
|
|
180
|
+
// RELEVANT field — the Test field, the Files field, or the Acceptance-criteria
|
|
181
|
+
// value (where an AC may name its own verifying test, e.g. "…; verified by cargo
|
|
182
|
+
// test"). An incidental runner mention in an UNRELATED field (Dependencies,
|
|
183
|
+
// Notes, Scope) must NOT vacuously clear the killing-test requirement.
|
|
184
|
+
const hasTestField = hasMultiField(lines, TEST_FIELD_RE);
|
|
185
|
+
const testFieldVal = fieldValueRaw(lines, TEST_FIELD_RE) || "";
|
|
186
|
+
const acVal = fieldValueRaw(lines, AC_FIELD_RE) || "";
|
|
187
|
+
// AC criteria often span bullet sub-lines after the label; gather those too
|
|
188
|
+
// (underscore-preserving, so a test_*.py named in a bullet still matches).
|
|
189
|
+
const acBullets = _acBulletText(lines);
|
|
190
|
+
const filesHasTestPath = TEST_PATH_RE.test(filesVal);
|
|
191
|
+
const testFieldHasRunner = TEST_PATH_RE.test(testFieldVal);
|
|
192
|
+
const acHasRunner = TEST_PATH_RE.test(acVal) || TEST_PATH_RE.test(acBullets);
|
|
193
|
+
const hasTest = hasTestField || filesHasTestPath || testFieldHasRunner || acHasRunner;
|
|
194
|
+
|
|
195
|
+
// A non-test implementing path: a Files entry that is NOT only test files.
|
|
196
|
+
const fileTokens = filesVal.split(/[,\s]+/).map((s) => s.replace(/[`*()]/g, "").trim()).filter(Boolean);
|
|
197
|
+
const implTokens = fileTokens.filter((f) => /[./]/.test(f) && !TEST_PATH_RE.test(f));
|
|
198
|
+
const hasImplPath = implTokens.length > 0;
|
|
199
|
+
|
|
200
|
+
const isHeadline = lines.some((ln) => HEADLINE_FIELD_RE.test(_bare(ln)));
|
|
201
|
+
|
|
202
|
+
const violations = [];
|
|
203
|
+
if (!hasFiles) {
|
|
204
|
+
violations.push({ kind: "ac-without-path", detail: "task declares acceptance criteria but no **Files** implementing path — an unbacked promise." });
|
|
205
|
+
}
|
|
206
|
+
if (!hasTest) {
|
|
207
|
+
violations.push({ kind: "ac-without-test", detail: "task declares acceptance criteria but names no test (Test field, test path, or runner) — the dead-code class: it can pass vacuously / never be exercised." });
|
|
208
|
+
}
|
|
209
|
+
if (isHeadline && !hasImplPath) {
|
|
210
|
+
violations.push({ kind: "headline-without-impl", detail: "HEADLINE task has no non-test implementing path — the milestone's reason to exist is not bound to real code (the M5 AC-6 dead-code failure)." });
|
|
211
|
+
}
|
|
212
|
+
if (isHeadline && !hasTest) {
|
|
213
|
+
violations.push({ kind: "headline-without-test", detail: "HEADLINE task has no test proving the milestone's core capability is delivered (the missing >100MB-fixture failure)." });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
title: task.title,
|
|
218
|
+
behavioral: true,
|
|
219
|
+
isHeadline,
|
|
220
|
+
hasFiles, hasTest, hasImplPath,
|
|
221
|
+
violations,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── driver ──────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function listTasksFiles(projectDir, milestone) {
|
|
228
|
+
const domainsDir = path.join(projectDir, ".gsd-t", "domains");
|
|
229
|
+
let entries = [];
|
|
230
|
+
try {
|
|
231
|
+
entries = fs.readdirSync(domainsDir, { withFileTypes: true });
|
|
232
|
+
} catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const out = [];
|
|
236
|
+
const mPrefix = milestone ? milestone.toLowerCase() : null;
|
|
237
|
+
for (const e of entries) {
|
|
238
|
+
if (!e.isDirectory()) continue;
|
|
239
|
+
// When a milestone is given, prefer domains whose name carries that mNN
|
|
240
|
+
// prefix; if none match, fall back to all domains (single-milestone repos).
|
|
241
|
+
const tasksPath = path.join(domainsDir, e.name, "tasks.md");
|
|
242
|
+
if (fs.existsSync(tasksPath)) out.push({ domain: e.name, tasksPath });
|
|
243
|
+
}
|
|
244
|
+
if (mPrefix) {
|
|
245
|
+
const matched = out.filter((d) => d.domain.toLowerCase().startsWith(mPrefix));
|
|
246
|
+
if (matched.length) return matched;
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = null } = {}) {
|
|
252
|
+
let files;
|
|
253
|
+
if (tasksFile) {
|
|
254
|
+
files = [{ domain: path.basename(path.dirname(tasksFile)), tasksPath: tasksFile }];
|
|
255
|
+
} else {
|
|
256
|
+
files = listTasksFiles(projectDir, milestone);
|
|
257
|
+
}
|
|
258
|
+
if (!files.length) {
|
|
259
|
+
return { ok: false, exitCode: 64, milestone, reason: "no-tasks-files", tasks: [], violations: [] };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const taskResults = [];
|
|
263
|
+
const violations = [];
|
|
264
|
+
let behavioralCount = 0;
|
|
265
|
+
for (const f of files) {
|
|
266
|
+
let md;
|
|
267
|
+
try { md = fs.readFileSync(f.tasksPath, "utf8"); } catch { continue; }
|
|
268
|
+
for (const t of parseTasks(md)) {
|
|
269
|
+
const r = assessTask(t);
|
|
270
|
+
r.domain = f.domain;
|
|
271
|
+
taskResults.push(r);
|
|
272
|
+
if (r.behavioral) behavioralCount++;
|
|
273
|
+
for (const v of r.violations) {
|
|
274
|
+
violations.push({ domain: f.domain, task: r.title, ...v });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const ok = violations.length === 0;
|
|
280
|
+
return {
|
|
281
|
+
ok,
|
|
282
|
+
exitCode: ok ? 0 : 4,
|
|
283
|
+
milestone,
|
|
284
|
+
summary: {
|
|
285
|
+
tasksTotal: taskResults.length,
|
|
286
|
+
behavioral: behavioralCount,
|
|
287
|
+
violations: violations.length,
|
|
288
|
+
},
|
|
289
|
+
tasks: taskResults,
|
|
290
|
+
violations,
|
|
291
|
+
...(ok ? {} : { reason: "untraceable-acceptance-criteria" }),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
function parseArgs(argv) {
|
|
298
|
+
const o = { projectDir: process.cwd(), milestone: null, tasksFile: null, help: false };
|
|
299
|
+
for (let i = 0; i < argv.length; i++) {
|
|
300
|
+
const a = argv[i];
|
|
301
|
+
if (a === "--help" || a === "-h") o.help = true;
|
|
302
|
+
else if (a === "--project-dir") o.projectDir = argv[++i];
|
|
303
|
+
else if (a === "--milestone") o.milestone = argv[++i];
|
|
304
|
+
else if (a === "--tasks") o.tasksFile = argv[++i];
|
|
305
|
+
else if (a === "--json") {/* default */}
|
|
306
|
+
}
|
|
307
|
+
return o;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const HELP = `Usage: gsd-t traceability-gate [--milestone Mxx] [--project-dir PATH] [--tasks FILE]
|
|
311
|
+
|
|
312
|
+
Plan-phase acceptance-traceability gate (M83). Asserts every behavioral task in
|
|
313
|
+
the milestone's .gsd-t/domains/* /tasks.md binds its acceptance criteria to an
|
|
314
|
+
implementing **Files** path AND a named test. Headline tasks must have BOTH a
|
|
315
|
+
real implementation path and a test. Blocks execute on any violation.
|
|
316
|
+
|
|
317
|
+
--milestone Mxx Limit to domains whose name carries the mNN prefix.
|
|
318
|
+
--project-dir P Project root (default: cwd).
|
|
319
|
+
--tasks FILE Check a single tasks.md (overrides domain discovery).
|
|
320
|
+
|
|
321
|
+
Exit: 0 all traceable · 4 ≥1 violation · 64 no tasks files / bad input.`;
|
|
322
|
+
|
|
323
|
+
function main() {
|
|
324
|
+
const o = parseArgs(process.argv.slice(2));
|
|
325
|
+
if (o.help) { process.stdout.write(HELP + "\n"); process.exit(0); }
|
|
326
|
+
let res;
|
|
327
|
+
try {
|
|
328
|
+
res = runGate(o);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
res = { ok: false, exitCode: 64, milestone: o.milestone, reason: `gate-error: ${e && e.message}`, tasks: [], violations: [] };
|
|
331
|
+
}
|
|
332
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
333
|
+
process.exit(res.exitCode);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (require.main === module) main();
|
|
337
|
+
|
|
338
|
+
module.exports = { runGate, parseTasks, assessTask, _internal: { fieldValue, TEST_PATH_RE } };
|
package/bin/gsd-t.js
CHANGED
|
@@ -1184,6 +1184,8 @@ const GLOBAL_BIN_TOOLS = [
|
|
|
1184
1184
|
"gsd-t-ci-parity.cjs",
|
|
1185
1185
|
// M82 — Competition Mode generate-and-judge selection oracle.
|
|
1186
1186
|
"gsd-t-competition-judge.cjs",
|
|
1187
|
+
// M83 — Plan-phase acceptance-traceability gate.
|
|
1188
|
+
"gsd-t-traceability-gate.cjs",
|
|
1187
1189
|
];
|
|
1188
1190
|
|
|
1189
1191
|
function installGlobalBinTools() {
|
|
@@ -2475,6 +2477,8 @@ const PROJECT_BIN_TOOLS = [
|
|
|
2475
2477
|
// project's gsd-t-phase workflow can score candidate partitions via the
|
|
2476
2478
|
// project-local bin (runCli prefers bin/<tool>.cjs over the global binary).
|
|
2477
2479
|
"gsd-t-competition-judge.cjs", "gsd-t-file-disjointness.cjs",
|
|
2480
|
+
// M83 — Plan-phase acceptance-traceability gate (runs in the plan workflow).
|
|
2481
|
+
"gsd-t-traceability-gate.cjs",
|
|
2478
2482
|
];
|
|
2479
2483
|
|
|
2480
2484
|
// Files that older versions of this installer copied into project bin/ but
|
|
@@ -4562,6 +4566,15 @@ if (require.main === module) {
|
|
|
4562
4566
|
});
|
|
4563
4567
|
process.exit(res.status == null ? 1 : res.status);
|
|
4564
4568
|
}
|
|
4569
|
+
case "traceability-gate": {
|
|
4570
|
+
// M83 D1 — `gsd-t traceability-gate` plan-phase acceptance-traceability gate.
|
|
4571
|
+
const { spawnSync } = require("child_process");
|
|
4572
|
+
const js = path.join(__dirname, "gsd-t-traceability-gate.cjs");
|
|
4573
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
4574
|
+
stdio: "inherit",
|
|
4575
|
+
});
|
|
4576
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
4577
|
+
}
|
|
4565
4578
|
case "metrics":
|
|
4566
4579
|
doMetrics(args.slice(1));
|
|
4567
4580
|
break;
|
package/commands/gsd-t-help.md
CHANGED
|
@@ -487,6 +487,14 @@ Use these when user asks for help on a specific command:
|
|
|
487
487
|
- **CLI**: `gsd-t competition-judge [--in <spec.json>] [--project-dir <dir>]` (spec via stdin or `--in`). Exit 0 winner · 4 no valid candidate · 64 bad input.
|
|
488
488
|
- **Contract**: `.gsd-t/contracts/competition-mode-contract.md` v1.0.0 STABLE.
|
|
489
489
|
|
|
490
|
+
### traceability-gate (M83)
|
|
491
|
+
- **Summary**: Plan-phase acceptance-traceability gate — the deterministic half of Left-Shifted Plan Hardening. Parses `.gsd-t/domains/*/tasks.md` and asserts every behavioral task binds its acceptance criteria to a `**Files**` code path AND a named killing test; a `**Headline:** true` task must have both a real implementation path and a test. Catches the dead-deliverable class (a capability built but never tested/wired) at PLAN time instead of at verify.
|
|
492
|
+
- **Auto-invoked**: Yes — by `gsd-t-phase.workflow.js` at the end of the `plan` phase, blocking before execute (alongside the adversarial pre-mortem agent, protocol `templates/prompts/pre-mortem-subagent.md`).
|
|
493
|
+
- **Files**: `bin/gsd-t-traceability-gate.cjs`.
|
|
494
|
+
- **Use when**: Every plan phase (automatic). Origin: NiceNote M5 shipped its headline 100MB+ chunked-read as dead code with no test → 4 verify cycles.
|
|
495
|
+
- **CLI**: `gsd-t traceability-gate [--milestone <Mxx>] [--project-dir <dir>] [--tasks <file>]`. Exit 0 all traceable · 4 ≥1 untraceable AC (blocks execute) · 64 no tasks files.
|
|
496
|
+
- **Contract**: `.gsd-t/contracts/plan-hardening-contract.md` v1.0.0 STABLE.
|
|
497
|
+
|
|
490
498
|
## Unknown Command
|
|
491
499
|
|
|
492
500
|
If user asks for help on unrecognized command:
|
package/commands/gsd-t-plan.md
CHANGED
|
@@ -33,12 +33,14 @@ Read `.gsd-t/progress.md` and each domain's `scope.md`/`constraints.md`. The par
|
|
|
33
33
|
|
|
34
34
|
## Step 3: Interpret the result
|
|
35
35
|
|
|
36
|
-
The Workflow returns `{ status, artifacts, summary, decisions }`.
|
|
36
|
+
The Workflow returns `{ status, artifacts, summary, decisions, traceability?, preMortem? }`.
|
|
37
37
|
|
|
38
|
-
- `status === "complete"`: every domain has atomic tasks; `gsd-t parallel --dry-run` validates disjointness. Auto-advance to `/gsd-t-execute`.
|
|
39
|
-
- `status === "partial" | "blocked"`: read `summary` (e.g. file-overlap between domains
|
|
38
|
+
- `status === "complete"`: every domain has atomic tasks; `gsd-t parallel --dry-run` validates disjointness; **M83 plan hardening passed** (acceptance-traceability gate + adversarial pre-mortem). Auto-advance to `/gsd-t-execute`.
|
|
39
|
+
- `status === "partial" | "blocked"`: read `summary` (e.g. file-overlap between domains; or **M83 plan hardening blocked** — see `traceability.violations` / `preMortem.findings`: an AC not bound to a code path + killing test, or a predicted failure condition with no planned test. Fix `tasks.md` and re-run plan).
|
|
40
40
|
- `status === "failed"`: read `summary`.
|
|
41
41
|
|
|
42
|
+
**M83 Plan Hardening (runs automatically at the end of plan, blocking before execute).** Two gates ensure the plan can't produce a dead deliverable: (1) the deterministic **acceptance-traceability gate** (`gsd-t traceability-gate`) — every behavioral task's ACs must bind to a `**Files**` code path + a named test; the **Headline:** task needs both a real impl path and a test. (2) the adversarial **pre-mortem** agent (opus, fresh-context) — predicts edge-case/dead-deliverable/NFR failures and requires a test for each. Origin: NiceNote M5 shipped its headline (100MB+ chunked read) as dead code with no test, burning 4 verify cycles. Contract: `.gsd-t/contracts/plan-hardening-contract.md`.
|
|
43
|
+
|
|
42
44
|
## Document Ripple
|
|
43
45
|
|
|
44
46
|
The plan agent writes per-domain `tasks.md`, updates `integration-points.md`, and adds a Decision Log entry.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.10",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -328,7 +328,7 @@ Canonical scripts:
|
|
|
328
328
|
- `gsd-t-integrate.workflow.js` — cross-domain wire-up + light verify-gate
|
|
329
329
|
- `gsd-t-debug.workflow.js` — 2-cycle diagnose/fix/verify (CLAUDE.md Prime Rule)
|
|
330
330
|
- `gsd-t-quick.workflow.js` — preflight + brief + single-task + verify-gate (M56-D4)
|
|
331
|
-
- `gsd-t-phase.workflow.js` — generic upper-stage runner (partition / plan / discuss / impact / milestone / prd / design-decompose / doc-ripple). **M82 Competition Mode:** an opt-in `competition: N` arg (N 2–5) on eligible upstream phases (partition / milestone / discuss / design-decompose) fans out N parallel Self-MoA producers → a judge stage → a finalizer. Partition's judge is the OBJECTIVE file-disjointness oracle (`gsd-t competition-judge --kind partition` — a calculator, not an LLM critic, immune to judge bias, the v1 beachhead); subjective phases use a blind + shuffled + different-model + rubric judge whose pick is finalized deterministically by `--kind generic`. The generative dual of the orthogonal validation triad; watershed rule = generate-and-judge ABOVE the contract, attack-and-filter BELOW. Default off. Contract: `competition-mode-contract.md` v1.0.0.
|
|
331
|
+
- `gsd-t-phase.workflow.js` — generic upper-stage runner (partition / plan / discuss / impact / milestone / prd / design-decompose / doc-ripple). **M82 Competition Mode:** an opt-in `competition: N` arg (N 2–5) on eligible upstream phases (partition / milestone / discuss / design-decompose) fans out N parallel Self-MoA producers → a judge stage → a finalizer. Partition's judge is the OBJECTIVE file-disjointness oracle (`gsd-t competition-judge --kind partition` — a calculator, not an LLM critic, immune to judge bias, the v1 beachhead); subjective phases use a blind + shuffled + different-model + rubric judge whose pick is finalized deterministically by `--kind generic`. The generative dual of the orthogonal validation triad; watershed rule = generate-and-judge ABOVE the contract, attack-and-filter BELOW. Default off. Contract: `competition-mode-contract.md` v1.0.0. **M83 Plan Hardening:** the `plan` phase runs two blocking gates before execute — a deterministic acceptance-traceability gate (`gsd-t traceability-gate`: every AC binds to a code path + a killing test; the `Headline:` task needs both impl and test) and an adversarial pre-mortem agent (opus, fresh-context, protocol `pre-mortem-subagent.md`: predicts edge-case/dead-deliverable/NFR failures, each → a required test). The temporal dual of the Red Team (attack the design at plan, not just code at verify). Contract: `plan-hardening-contract.md` v1.0.0.
|
|
332
332
|
- `gsd-t-scan.workflow.js` — preflight → volume-probe → pipeline(per-slice deep finder → single verify) → synthesis → document → render (M66: fans out by codebase VOLUME, not a fixed 5-teammate dimension count; M67: deep document phase deterministically produces the full living-doc set + dimension files, per-doc fan-out)
|
|
333
333
|
|
|
334
334
|
**Runtime-native invariant (M81 — v4.0.29+):** the Workflow sandbox provides ONLY `agent/parallel/pipeline/log/phase/budget/args` — NO `require`/`fs`/`path`/`child_process`/`process`, and `args` arrives as a JSON STRING. Each workflow is self-contained: it `JSON.parse`s `args` and delegates every CLI call (preflight, verify-gate, brief, build-coverage, ci-parity, test-data, disjointness) to inline `async` helpers that run the command via an `agent()`'s Bash (preferring project-local `bin/<tool>.cjs`, else the global `gsd-t` PATH binary) and parse the JSON envelope — preserving the M55-D5 project-local-bin invariant. The old `require("./_lib.js")` pattern threw `ReferenceError` on first eval and silently broke every workflow except scan (TD-113, fixed M81); `_lib.js` is retired as a workflow dependency.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Pre-Mortem Subagent Prompt — Adversarial Plan Review (pre-execute)
|
|
2
|
+
|
|
3
|
+
You are an adversarial Pre-Mortem reviewer. You attack the PLAN, not the code — because the code does not exist yet. Your job is to predict, BEFORE a single line is executed, how this milestone will fail: the edge cases it will hit, the deliverables it will leave hollow, and the assumptions it is quietly making. You are the generative-adversarial dual of the Red Team: the Red Team attacks finished code at verify; you attack the design at plan, so the milestone is built right the FIRST time instead of being re-litigated across verify cycles.
|
|
4
|
+
|
|
5
|
+
**Inverted incentives.** Your value is measured by REAL failure conditions surfaced now, not by approving the plan. A plan you bless that later burns verify cycles is YOUR failure. Assume the plan is flawed and find where.
|
|
6
|
+
|
|
7
|
+
<!-- Workflow-stage invocation -->
|
|
8
|
+
**Invocation context.** When this protocol runs as a native Workflow `agent()` stage (via `templates/workflows/gsd-t-phase.workflow.js` plan phase), your **final emission MUST be a single StructuredOutput object** matching the PRE_MORTEM schema declared by the Workflow. Bash/git/Read tool use is permitted DURING analysis; the final emission is the JSON verdict.
|
|
9
|
+
|
|
10
|
+
<!-- brief-first rule -->
|
|
11
|
+
**Brief first.** If you're about to grep, read, or run something, check the brief at `$BRIEF_PATH` first (a ≤2,500-token snapshot of CLAUDE.md + contracts + scope + requirements). It identifies the milestone's acceptance criteria and high-risk surfaces — your starting attack surface. If unset/missing, fall back to reading the plan artifacts directly, but log the gap.
|
|
12
|
+
|
|
13
|
+
## What you are given
|
|
14
|
+
|
|
15
|
+
The milestone's PLAN: `.gsd-t/domains/*/{scope,constraints,tasks}.md`, the relevant `.gsd-t/contracts/`, and the acceptance criteria / FRs / NFRs in `docs/requirements.md`. Read the milestone's stated GOAL and its HEADLINE capability (the one thing the milestone exists to deliver).
|
|
16
|
+
|
|
17
|
+
## Hard Rules
|
|
18
|
+
|
|
19
|
+
- **Failure conditions = value.** A short list is failure. Exhaust every category below.
|
|
20
|
+
- **A finding must be CONCRETE and FALSIFIABLE.** "Could have edge cases" is not a finding. "A multi-byte UTF-8 codepoint split across a chunk boundary in `read_file_chunk` will corrupt or stall — there is no test for it" IS a finding.
|
|
21
|
+
- **Every blocking finding must become a REQUIRED TEST.** This is the core rule. Do not emit advisory notes — advisory notes get deferred, and a deferred edge case is exactly how the NiceNote M5 chunk reader shipped three distinct data-loss bugs across three verify cycles. For each finding, state the test that must exist in the plan before execute may start. If the plan already names that test, it is not a finding.
|
|
22
|
+
- **The headline capability gets the hardest scrutiny.** Ask explicitly: is the milestone's reason-to-exist (a) bound to a real code path in the plan, (b) reachable from a user action / entry point, and (c) covered by a test that FAILS if that path is dead? The NiceNote M5 milestone shipped its headline (100MB+ chunked read) as DEAD CODE because the plan never required a test that exercised it. Catch that here.
|
|
23
|
+
- **Deferral is illegitimate for a milestone's own headline.** If the plan defers the milestone's defining capability (or a core AC) to a later milestone, that is a blocking finding — an incomplete milestone, not a warning.
|
|
24
|
+
- Style/taste is NOT a finding. Theoretical purity is NOT a finding. Only predicted, concrete, testable failure.
|
|
25
|
+
|
|
26
|
+
## Attack Categories (exhaust ALL)
|
|
27
|
+
|
|
28
|
+
1. **Dead-deliverable / wiring gaps** — Is every acceptance criterion bound to a code path that is actually CALLED from an entry point? Could a capability be built but never invoked (the M5 dead-code class)? Is the headline reachable from a real user action?
|
|
29
|
+
2. **Boundary & edge inputs** — empty / null / huge / zero-length / off-by-one / max-size. For each data path the plan introduces: what is the worst input, and is there a test for it? (split codepoints, chunk boundaries, 0-byte files, files at exactly the threshold, unicode, path traversal.)
|
|
30
|
+
3. **Resource / NFR conditions** — memory, time, file-handle, DOM-node, payload-size ceilings. Does any NFR (performance, bounded memory, scale) have a FALSIFIABLE measured acceptance check in the plan? An NFR with no measured test is a blocking finding (the NiceNote NFR-1 160k-DOM-node class).
|
|
31
|
+
4. **Error & failure paths** — what happens when the new code's dependency fails, the input is malformed, the operation is interrupted mid-flight? Does the plan specify graceful degradation, and is there a test for the failure path (not just the happy path)?
|
|
32
|
+
5. **State / ordering / concurrency** — actions out of order, partial completion, re-entry, two things racing over a shared resource (the verify-gate port-race class). Does the plan account for it?
|
|
33
|
+
6. **Contract & integration seams** — at every cross-domain boundary the plan defines, do both sides agree on shape, error behavior, and who owns the shared file? Is there an integration test for the seam, not just unit tests on each side?
|
|
34
|
+
7. **Shallow-test traps** — does the plan's testing approach risk vacuous passes? (assertions gated behind `if (count > 0)`, `toBeVisible()` standing in for a functional check, `toHaveCount` with no state assertion.) Flag any planned test that would pass on a broken implementation.
|
|
35
|
+
8. **Missing acceptance coverage** — read requirements. Is there an AC / FR / NFR with no task that delivers it, or no test that proves it?
|
|
36
|
+
|
|
37
|
+
## Verdict
|
|
38
|
+
|
|
39
|
+
- **BLOCK** — one or more concrete, falsifiable failure conditions that the plan does not yet cover with a required test. The plan may NOT proceed to execute until each blocking finding is answered by a named required test (or the design is changed to make the condition impossible). This is the FAIL-equivalent.
|
|
40
|
+
- **CLEARED** — exhaustive search; every predicted failure condition is already covered by a named test in the plan, the headline is bound+reachable+tested, and every NFR has a measured acceptance check. (The plan-quality equivalent of GRUDGING-PASS — earned by exhaustion, not by haste.)
|
|
41
|
+
|
|
42
|
+
## Output (StructuredOutput)
|
|
43
|
+
|
|
44
|
+
Emit a single object: `{ verdict: "BLOCK" | "CLEARED", findings: [ { severity: "CRITICAL"|"HIGH"|"MEDIUM"|"LOW", category, condition, whyItFails, requiredTest, affectedAC? } ], headlineAssessment: { capability, boundToPath, reachable, hasKillingTest }, notes }`.
|
|
45
|
+
|
|
46
|
+
`requiredTest` is the load-bearing field: the specific test that must be added to the plan to close the finding. A finding without a `requiredTest` is incomplete — every blocking finding converts to a test the plan must adopt before execute.
|
|
@@ -67,6 +67,14 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
67
67
|
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
68
68
|
}
|
|
69
69
|
async function runPreflight(projectDir, label = "preflight", phaseNameOpt) { return runCli(projectDir, "preflight", ["--json"], "cli-preflight.cjs", label, true, phaseNameOpt); }
|
|
70
|
+
// M83: the deterministic plan-hardening gate. Returns the parsed envelope
|
|
71
|
+
// ({ ok, exitCode, violations, ... }); ok:false means ≥1 untraceable AC.
|
|
72
|
+
async function runTraceabilityGate(projectDir, milestone, label = "traceability-gate", phaseNameOpt) {
|
|
73
|
+
const argv = ["--json"];
|
|
74
|
+
if (milestone) argv.push("--milestone", milestone);
|
|
75
|
+
const r = await runCli(projectDir, "traceability-gate", argv, "gsd-t-traceability-gate.cjs", label, true, phaseNameOpt);
|
|
76
|
+
return r.envelope || { ok: r.ok, exitCode: r.exitCode, violations: [], reason: "gate-unparsed" };
|
|
77
|
+
}
|
|
70
78
|
async function generateBrief(projectDir, { kind = "execute", milestone, domain, id, label = "brief", phaseNameOpt } = {}) {
|
|
71
79
|
const argv = ["--kind", kind, "--spawn-id", id, "--out", `${projectDir}/.gsd-t/briefs/${id}.json`];
|
|
72
80
|
if (milestone) argv.push("--milestone", milestone);
|
|
@@ -184,7 +192,9 @@ const brief = await generateBrief(projectDir, { kind: phaseName, milestone, id:
|
|
|
184
192
|
phase("Phase");
|
|
185
193
|
const promptByPhase = {
|
|
186
194
|
partition: `Decompose the milestone into 2-5 independent domains. Write .gsd-t/domains/{domain}/{scope,constraints,tasks}.md. Cross-domain contracts in .gsd-t/contracts/.`,
|
|
187
|
-
plan: `For each domain, write atomic tasks.md entries with files, contract refs, dependencies, acceptance criteria. Update .gsd-t/contracts/integration-points.md with wave groupings
|
|
195
|
+
plan: `For each domain, write atomic tasks.md entries with files, contract refs, dependencies, acceptance criteria. Update .gsd-t/contracts/integration-points.md with wave groupings.
|
|
196
|
+
|
|
197
|
+
M83 PLAN HARDENING (mandatory — the plan is BLOCKED from execute otherwise): every task that declares acceptance criteria MUST also declare (1) **Files** = the concrete code path that implements it, and (2) a TEST that fails if that path is dead — name it in a **Test** field, a test-file path (\`*.test.*\` / \`*.spec.*\` / \`e2e/\`), or a runner (vitest/cargo test/playwright). The ONE task that delivers the milestone's HEADLINE capability MUST be tagged **Headline:** true and carry BOTH a real implementation path AND a test that exercises that capability end-to-end (e.g. for a "100MB+ file" milestone, a test that actually opens a >100MB fixture). NEVER defer a milestone's own headline capability or a core AC to a later milestone. This exists because NiceNote M5 shipped its headline (100MB+ chunked read) as DEAD CODE with no test and burned 4 verify cycles.`,
|
|
188
198
|
discuss: `Multi-perspective exploration of design questions. Settle locked decisions into .gsd-t/CONTEXT.md. Do NOT implement.`,
|
|
189
199
|
impact: `Analyze downstream effects of proposed changes. Identify breaking changes, affected consumers, migration paths.`,
|
|
190
200
|
milestone: `Define a new milestone — origin, goal, success criteria, falsifiable acceptance. Append to .gsd-t/progress.md. Defer partition/plan.`,
|
|
@@ -434,4 +444,75 @@ if (!competitionOn) {
|
|
|
434
444
|
result.competition = { n: candidates.length, winner: winner.id, ranked };
|
|
435
445
|
}
|
|
436
446
|
|
|
447
|
+
// ── M83 Left-Shifted Plan Hardening (plan phase only) ──
|
|
448
|
+
// Two blocking gates run AFTER the plan agent writes tasks.md and BEFORE the plan
|
|
449
|
+
// is declared complete — so execute can never start on a plan that would produce a
|
|
450
|
+
// dead deliverable or an unguarded edge case. Contract: plan-hardening-contract.md.
|
|
451
|
+
// (1) Deterministic acceptance-traceability gate — every behavioral task's ACs
|
|
452
|
+
// must bind to a code path + a killing test; the headline must be impl+test.
|
|
453
|
+
// (2) Adversarial pre-mortem agent (opus, fresh-context, assume-the-plan-is-flawed)
|
|
454
|
+
// — predicts edge-case / dead-deliverable / NFR failures; each blocking
|
|
455
|
+
// finding must become a required test before execute.
|
|
456
|
+
if (phaseName === "plan" && result && result.status !== "failed") {
|
|
457
|
+
phase("Plan Hardening");
|
|
458
|
+
|
|
459
|
+
// (1) Deterministic gate. FAIL-CLOSED (Red Team MEDIUM-2): a deterministic gate
|
|
460
|
+
// that can't be evaluated (CLI error / unparsed envelope) is NOT a pass — block.
|
|
461
|
+
const trace = await runTraceabilityGate(projectDir, milestone, "traceability-gate", "Plan Hardening");
|
|
462
|
+
const traceUnparsed = trace && trace.reason === "gate-unparsed";
|
|
463
|
+
if (trace && (trace.ok === false || traceUnparsed)) {
|
|
464
|
+
const vcount = (trace.violations || []).length;
|
|
465
|
+
const why = traceUnparsed
|
|
466
|
+
? `traceability gate could not be evaluated (CLI error / unparsed output) — failing closed; re-run plan.`
|
|
467
|
+
: `${vcount} acceptance criteria not bound to a code path + killing test (M83 traceability gate). Fix tasks.md, then re-run plan.`;
|
|
468
|
+
log(`plan-hardening: traceability gate BLOCKED — ${traceUnparsed ? "unevaluable (fail-closed)" : vcount + " untraceable AC"}.`);
|
|
469
|
+
result.status = "blocked";
|
|
470
|
+
result.summary = `plan blocked: ${why} ${result.summary || ""}`.trim();
|
|
471
|
+
result.traceability = trace;
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
result.traceability = trace;
|
|
475
|
+
|
|
476
|
+
// (2) Adversarial pre-mortem. The agent reads its own protocol at spawn time
|
|
477
|
+
// (the orchestrator has no fs); blocking findings convert to required tests.
|
|
478
|
+
const PRE_MORTEM_SCHEMA = {
|
|
479
|
+
type: "object", required: ["verdict", "findings"], additionalProperties: true,
|
|
480
|
+
properties: {
|
|
481
|
+
verdict: { type: "string", enum: ["BLOCK", "CLEARED"] },
|
|
482
|
+
findings: {
|
|
483
|
+
type: "array", items: {
|
|
484
|
+
type: "object", required: ["severity", "condition", "requiredTest"], additionalProperties: true,
|
|
485
|
+
properties: {
|
|
486
|
+
severity: { type: "string", enum: ["CRITICAL", "HIGH", "MEDIUM", "LOW"] },
|
|
487
|
+
category: { type: "string" }, condition: { type: "string" },
|
|
488
|
+
whyItFails: { type: "string" }, requiredTest: { type: "string" }, affectedAC: { type: "string" },
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
headlineAssessment: { type: "object", additionalProperties: true },
|
|
493
|
+
notes: { type: "string" },
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
const preMortem = await agent(
|
|
497
|
+
[
|
|
498
|
+
`You are the adversarial Pre-Mortem reviewer for milestone ${milestone || "(current)"}.`,
|
|
499
|
+
`FIRST read your protocol via the Read tool: templates/prompts/pre-mortem-subagent.md (in the installed @tekyzinc/gsd-t package, or this project's copy). Follow it exactly.`,
|
|
500
|
+
`**Brief (REQUIRED):** ${brief.briefPath || "(no brief — read plan artifacts directly)"}`,
|
|
501
|
+
`Attack the PLAN at .gsd-t/domains/*/{scope,constraints,tasks}.md + .gsd-t/contracts/ + docs/requirements.md.`,
|
|
502
|
+
`Predict, before any code is executed, how this milestone will FAIL: edge cases, dead deliverables, unguarded NFRs, shallow-test traps. Scrutinize the HEADLINE capability hardest — is it bound to a real path, reachable, and covered by a killing test?`,
|
|
503
|
+
`Every blocking finding MUST convert to a concrete requiredTest the plan must adopt. Advisory notes are forbidden.`,
|
|
504
|
+
`Verdict BLOCK if any concrete, falsifiable failure condition lacks a named required test; else CLEARED. Return JSON per the schema.`,
|
|
505
|
+
].join("\n"),
|
|
506
|
+
{ label: "pre-mortem", phase: "Plan Hardening", schema: PRE_MORTEM_SCHEMA, model: "opus" }
|
|
507
|
+
).catch((e) => ({ verdict: "BLOCK", findings: [{ severity: "HIGH", condition: `pre-mortem agent error: ${e && e.message}`, requiredTest: "re-run pre-mortem" }], notes: "agent-error" }));
|
|
508
|
+
|
|
509
|
+
result.preMortem = preMortem;
|
|
510
|
+
if (preMortem && preMortem.verdict === "BLOCK") {
|
|
511
|
+
const n = (preMortem.findings || []).length;
|
|
512
|
+
log(`plan-hardening: pre-mortem BLOCKED — ${n} predicted failure condition(s) need required tests in the plan.`);
|
|
513
|
+
result.status = "blocked";
|
|
514
|
+
result.summary = `plan blocked: pre-mortem found ${n} falsifiable failure condition(s) not covered by a planned test (M83). Add the required tests to tasks.md, then re-run plan. ${result.summary || ""}`.trim();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
437
518
|
return result;
|