@opengsd/gsd-core 1.5.0-rc.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +1 -1
- package/agents/gsd-executor.md +22 -0
- package/bin/install.js +53 -99
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +36 -5
- package/gsd-core/bin/lib/decisions.cjs +19 -1
- package/gsd-core/bin/lib/phase-id.cjs +1 -1
- package/gsd-core/bin/lib/phase.cjs +41 -6
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +201 -26
- package/gsd-core/bin/lib/roadmap-parser.cjs +25 -20
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +4 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +24 -3
- package/gsd-core/bin/lib/state.cjs +64 -15
- package/gsd-core/bin/lib/teams-status.cjs +74 -0
- package/gsd-core/bin/lib/uat.cjs +56 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +7 -1
- package/gsd-core/references/prohibition-probe.md +48 -19
- package/gsd-core/references/worktree-branch-check.md +11 -5
- package/gsd-core/workflows/docs-update.md +23 -31
- package/gsd-core/workflows/execute-phase.md +2 -1
- package/gsd-core/workflows/map-codebase.md +8 -10
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/quick.md +23 -2
- package/gsd-core/workflows/spec-phase.md +11 -6
- package/gsd-core/workflows/verify-phase.md +3 -3
- package/hooks/dist/gsd-worktree-path-guard.js +34 -18
- package/hooks/gsd-worktree-path-guard.js +34 -18
- package/package.json +2 -2
- package/scripts/ci-prepare-test-scope.cjs +56 -14
- package/scripts/diff-touches-shipped-paths.cjs +5 -11
- package/scripts/run-tests.cjs +1 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Teams Status Module — issue #1355
|
|
4
|
+
*
|
|
5
|
+
* Read-only detector for claude-code's experimental agent-teams feature.
|
|
6
|
+
* Exposes a PURE core function (env injected, no process.env/disk inside) and
|
|
7
|
+
* a thin CLI wrapper that reuses resolveRuntime from runtime-slash.cjs.
|
|
8
|
+
*
|
|
9
|
+
* Exports:
|
|
10
|
+
* resolveTeamsStatus({ runtime, env }) → TeamsStatus
|
|
11
|
+
* cmdTeamsStatus(cwd, opts) — I/O entry point
|
|
12
|
+
*
|
|
13
|
+
* resolveTeamsStatus is PURE: env and runtime are injected, no process.env or
|
|
14
|
+
* disk access inside the function. Pass process.env explicitly at call sites.
|
|
15
|
+
*
|
|
16
|
+
* cmdTeamsStatus is the I/O handler. It reads process.env, resolves the
|
|
17
|
+
* runtime via resolveRuntime(cwd) from runtime-slash.cjs (GSD_RUNTIME →
|
|
18
|
+
* config.runtime → 'claude' precedence), then:
|
|
19
|
+
* - default: prints JSON.stringify(status) to stdout via io.output, exits 0.
|
|
20
|
+
* - --active: prints nothing, exits 0 if status.active, exit 1 otherwise.
|
|
21
|
+
*
|
|
22
|
+
* Strictly read-only — no config writes, no disk mutation.
|
|
23
|
+
*
|
|
24
|
+
* Dependencies:
|
|
25
|
+
* - ./io.cjs (output)
|
|
26
|
+
* - ./runtime-slash.cjs (resolveRuntime)
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.resolveTeamsStatus = resolveTeamsStatus;
|
|
30
|
+
exports.cmdTeamsStatus = cmdTeamsStatus;
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const ioMod = require("./io.cjs");
|
|
33
|
+
const { output: coreOutput } = ioMod;
|
|
34
|
+
// ─── Pure core ────────────────────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the agent-teams status from injected runtime and env.
|
|
37
|
+
*
|
|
38
|
+
* Strict truthiness: only '1' and 'true' (case-insensitive, trimmed) are on.
|
|
39
|
+
* '0', 'false', '', and unset are all off.
|
|
40
|
+
*
|
|
41
|
+
* @param opts.runtime The resolved runtime name (e.g. 'claude', 'codex')
|
|
42
|
+
* @param opts.env The environment map to read from (typically process.env)
|
|
43
|
+
*/
|
|
44
|
+
function resolveTeamsStatus(opts) {
|
|
45
|
+
const raw = (opts.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] ?? '').trim().toLowerCase();
|
|
46
|
+
const envOn = raw === '1' || raw === 'true'; // strict: never '0'/'false'/'' as on
|
|
47
|
+
const isClaude = opts.runtime === 'claude';
|
|
48
|
+
const source = !isClaude ? 'off: non-claude' : (envOn ? 'on: env' : 'off: flag absent');
|
|
49
|
+
return { active: envOn && isClaude, runtime: opts.runtime, env_present: envOn, source };
|
|
50
|
+
}
|
|
51
|
+
// ─── CLI command handler ──────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Command entry point: resolve runtime via resolveRuntime(cwd), read process.env,
|
|
54
|
+
* call resolveTeamsStatus, and emit the result.
|
|
55
|
+
*
|
|
56
|
+
* @param cwd Project root directory (used by resolveRuntime for config.json)
|
|
57
|
+
* @param opts Command options
|
|
58
|
+
* @param opts.active When true: print nothing, exit 0 if active, exit 1 otherwise
|
|
59
|
+
*/
|
|
60
|
+
function cmdTeamsStatus(cwd, opts) {
|
|
61
|
+
// Resolve runtime via the canonical precedence:
|
|
62
|
+
// GSD_RUNTIME → config.runtime → 'claude'
|
|
63
|
+
// Reuses resolveRuntime from runtime-slash.cjs — no reimplementation.
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
65
|
+
const runtimeSlash = require('./runtime-slash.cjs');
|
|
66
|
+
const runtime = runtimeSlash.resolveRuntime(cwd);
|
|
67
|
+
const status = resolveTeamsStatus({ runtime, env: process.env });
|
|
68
|
+
if (opts.active) {
|
|
69
|
+
// --active mode: no output, exit code encodes the boolean
|
|
70
|
+
process.exit(status.active ? 0 : 1);
|
|
71
|
+
}
|
|
72
|
+
// Default: emit JSON to stdout via io.output, exit 0
|
|
73
|
+
coreOutput(status, false);
|
|
74
|
+
}
|
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -145,6 +145,13 @@ function parseCurrentTest(content) {
|
|
|
145
145
|
|| section.match(/^expected:\s*\|\n([\s\S]+)/m);
|
|
146
146
|
const expectedInlineMatch = section.match(/^expected:\s*(.+)\s*$/m);
|
|
147
147
|
if (!numberMatch || !nameMatch || (!expectedBlockMatch && !expectedInlineMatch)) {
|
|
148
|
+
if (!numberMatch && !nameMatch && !expectedBlockMatch && !expectedInlineMatch) {
|
|
149
|
+
const pendingTest = parseFirstPendingTest(content);
|
|
150
|
+
if (pendingTest) {
|
|
151
|
+
return pendingTest;
|
|
152
|
+
}
|
|
153
|
+
error('Current Test section is non-structured and no pending UAT test remains to resume');
|
|
154
|
+
}
|
|
148
155
|
error('Current Test section is malformed');
|
|
149
156
|
}
|
|
150
157
|
let expected;
|
|
@@ -165,6 +172,55 @@ function parseCurrentTest(content) {
|
|
|
165
172
|
expected: (0, security_cjs_1.sanitizeForDisplay)(expected),
|
|
166
173
|
};
|
|
167
174
|
}
|
|
175
|
+
function parseFirstPendingTest(content) {
|
|
176
|
+
const testsMatch = content.match(/##\s*Tests\s*\n([\s\S]*?)(?=\n##\s|$)/i);
|
|
177
|
+
if (!testsMatch) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const testsSection = testsMatch[1];
|
|
181
|
+
const headingPattern = /^###\s*(\d+)\.\s*([^\n]+)\s*$/gm;
|
|
182
|
+
const headings = [];
|
|
183
|
+
let headingMatch;
|
|
184
|
+
while ((headingMatch = headingPattern.exec(testsSection)) !== null) {
|
|
185
|
+
headings.push({
|
|
186
|
+
index: headingMatch.index,
|
|
187
|
+
number: parseInt(headingMatch[1], 10),
|
|
188
|
+
name: headingMatch[2].trim(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
for (let i = 0; i < headings.length; i += 1) {
|
|
192
|
+
const current = headings[i];
|
|
193
|
+
const next = headings[i + 1];
|
|
194
|
+
const block = testsSection.slice(current.index, next ? next.index : undefined);
|
|
195
|
+
if (!/^result:\s*\[?pending\]?\s*$/im.test(block)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const expected = parseExpectedFromTestBlock(block);
|
|
199
|
+
if (!expected) {
|
|
200
|
+
error(`Pending UAT test ${current.number} is missing an expected field`);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
complete: false,
|
|
204
|
+
number: current.number,
|
|
205
|
+
name: (0, security_cjs_1.sanitizeForDisplay)(current.name),
|
|
206
|
+
expected: (0, security_cjs_1.sanitizeForDisplay)(expected),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function parseExpectedFromTestBlock(block) {
|
|
212
|
+
const expectedBlockMatch = block.match(/^expected:\s*\|\n([\s\S]*?)(?=^\w[\w-]*:\s)/m)
|
|
213
|
+
|| block.match(/^expected:\s*\|\n([\s\S]+)/m);
|
|
214
|
+
if (expectedBlockMatch) {
|
|
215
|
+
return expectedBlockMatch[1]
|
|
216
|
+
.split('\n')
|
|
217
|
+
.map((line) => line.replace(/^ {2}/, ''))
|
|
218
|
+
.join('\n')
|
|
219
|
+
.trim();
|
|
220
|
+
}
|
|
221
|
+
const expectedInlineMatch = block.match(/^expected:\s*(.+)\s*$/m);
|
|
222
|
+
return expectedInlineMatch ? expectedInlineMatch[1].trim() : null;
|
|
223
|
+
}
|
|
168
224
|
// ─── buildCheckpoint ──────────────────────────────────────────────────────────
|
|
169
225
|
function buildCheckpoint(currentTest) {
|
|
170
226
|
return [
|
|
@@ -304,11 +304,14 @@ function normalizeCleanupManifestEntry(entry) {
|
|
|
304
304
|
return null;
|
|
305
305
|
if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch))
|
|
306
306
|
return null;
|
|
307
|
+
const rawAllowedBases = Array.isArray(e.allowed_bases) ? e.allowed_bases : [];
|
|
308
|
+
const allowedBases = Array.from(new Set([expectedBase, ...rawAllowedBases.filter((base) => typeof base === 'string' && base.length > 0)]));
|
|
307
309
|
return {
|
|
308
310
|
agent_id: typeof e.agent_id === 'string' ? e.agent_id : null,
|
|
309
311
|
worktree_path: worktreePath,
|
|
310
312
|
branch,
|
|
311
313
|
expected_base: expectedBase,
|
|
314
|
+
allowed_bases: allowedBases,
|
|
312
315
|
};
|
|
313
316
|
}
|
|
314
317
|
function normalizeCleanupManifest(manifest) {
|
|
@@ -529,7 +532,10 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
529
532
|
break;
|
|
530
533
|
}
|
|
531
534
|
const mergeBase = execGit(['merge-base', 'HEAD', entry.branch], { cwd: plan.repoRoot });
|
|
532
|
-
|
|
535
|
+
const allowedBases = Array.isArray(entry.allowed_bases) && entry.allowed_bases.length > 0
|
|
536
|
+
? entry.allowed_bases
|
|
537
|
+
: [entry.expected_base];
|
|
538
|
+
if (!gitResultOk(mergeBase) || !allowedBases.includes(mergeBase.stdout.trim())) {
|
|
533
539
|
result.status = 'blocked';
|
|
534
540
|
result.reason = 'base_mismatch';
|
|
535
541
|
result.stderr = mergeBase?.stderr || '';
|
|
@@ -112,15 +112,38 @@ lifecycle is identical to the edge-probe, the verification tiers differ):
|
|
|
112
112
|
|
|
113
113
|
At verify time these tiers are routed differently (ADR-550 D4):
|
|
114
114
|
- A **test**-tier prohibition is enforced + hard-gates via the deterministic
|
|
115
|
-
`check prohibition-enforcement` sub-command (#1259, ADR-550 D5d): it locates the wired
|
|
115
|
+
`check prohibition-enforcement` sub-command (#1259 + #1279, ADR-550 D5d): it locates the wired
|
|
116
116
|
mechanical check (a `node --test` negative test OR a lint/AST rule run as
|
|
117
|
-
`eslint --format json` and filtered by `ruleId`),
|
|
118
|
-
|
|
119
|
-
`dispositionForProhibition()` verdict. A passing wired check disposes **green**
|
|
120
|
-
→ can reach `passed`); a missing,
|
|
121
|
-
(flagged, never green → `gaps_found`) in BOTH interactive and autonomous modes —
|
|
122
|
-
silent pass.
|
|
123
|
-
|
|
117
|
+
`eslint --format json` and filtered by `ruleId`), **machine-proves it is fail-first** against a
|
|
118
|
+
known violation, runs it for a genuine **non-vacuous** pass, and emits the
|
|
119
|
+
`dispositionForProhibition()` verdict. A passing, fail-first-proven wired check disposes **green**
|
|
120
|
+
(satisfiable → can reach `passed`); a missing, un-provable, or genuinely-non-passing check
|
|
121
|
+
**hard-gates** (flagged, never green → `gaps_found`) in BOTH interactive and autonomous modes —
|
|
122
|
+
never a silent pass.
|
|
123
|
+
|
|
124
|
+
**Machine-proven fail-first (#1279).** `failFirst` is now **machine-proven, not caller-attested**:
|
|
125
|
+
before a clean pass greens, the producer independently runs the wired check against a KNOWN
|
|
126
|
+
VIOLATION and confirms it goes RED (any other outcome — passes-on-violation, can't-prove, throws,
|
|
127
|
+
times out, no violation source — hard-gates). The violation is sourced from a descriptor field:
|
|
128
|
+
- **`violationFixture`** — an author-supplied path to a KNOWN-BAD subject. For `lint-rule`: a file
|
|
129
|
+
whose content violates `rule`; the prover lints it and requires the rule id to appear in the
|
|
130
|
+
JSON report (the rule must have teeth). For `node-test`: a subject the negative test exercises,
|
|
131
|
+
expected to drive it RED.
|
|
132
|
+
- **`GSD_PROHIB_SUBJECT`** — the node-test subject-injection convention: the producer spawns the
|
|
133
|
+
negative test with `GSD_PROHIB_SUBJECT=<violationFixture>` in the child env; the test reads that
|
|
134
|
+
env var to locate its subject-under-test and is expected to go red against the violating subject.
|
|
135
|
+
- **lint-fixture authoring gotcha** — the violating fixture must actually trigger the rule. For the
|
|
136
|
+
`local/no-source-grep` dogfood anchor specifically, use the `path.join('lib','foo.cjs')` form
|
|
137
|
+
(a standalone quoted dir token); a single string literal like `'src/x.cjs'` does NOT trigger the
|
|
138
|
+
rule, so a mis-authored fixture makes the prover report "not proven" and hard-gate a legitimately
|
|
139
|
+
wired check. (`no-source-grep` has no filename guard — any `.cjs` with the pattern fires.)
|
|
140
|
+
|
|
141
|
+
> **PROPOSED, renamable conventions (zero live consumers).** Both `GSD_PROHIB_SUBJECT` and
|
|
142
|
+
> `violationFixture` are net-new surface with **no live in-tree consumer yet** — there is no
|
|
143
|
+
> in-tree `node --test` prohibition; node-test fail-first proof is exercised only by SYNTHETIC
|
|
144
|
+
> temp fixtures in the tests, and the real dogfood remains the LINT-rule `local/no-source-grep`.
|
|
145
|
+
> They are therefore **open to maintainer adjustment (rename, or replacing the env var with an
|
|
146
|
+
> argv) at PR review with zero migration cost.** See the ADR-550 2026-06-15 addendum (#1279).
|
|
124
147
|
- A **judgment**-tier prohibition routes to a never-silent / never-hard-halt soft gate
|
|
125
148
|
(autonomous emits an `unverified-prohibition — human review recommended` flag).
|
|
126
149
|
|
|
@@ -128,33 +151,39 @@ Splitting these axes keeps the lifecycle enum free of a verification fact and le
|
|
|
128
151
|
prohibition adapter declare `test | judgment` without forking the shared lifecycle enum that
|
|
129
152
|
the edge-probe's `explicit | backstop` also uses.
|
|
130
153
|
|
|
131
|
-
## Optional wired-check descriptor (deterministic locate, #1278)
|
|
154
|
+
## Optional wired-check descriptor (deterministic locate + machine-proof, #1278 + #1346)
|
|
132
155
|
|
|
133
156
|
A `resolved`/`test`-tier prohibition MAY carry an **optional `check` descriptor** that names
|
|
134
157
|
the wired mechanical check, so verify-phase locates it deterministically instead of inventing
|
|
135
158
|
`{kind, target, rule}` each run. The descriptor is captured at spec-phase (soft / optional —
|
|
136
159
|
the author wires it when the negative test or lint rule already exists) and is represented as
|
|
137
|
-
**
|
|
160
|
+
**four flat scalar keys** on the `must_haves.prohibitions` item — never a nested `check: {}`
|
|
138
161
|
object:
|
|
139
162
|
|
|
140
163
|
- `check_kind` — `node-test` | `lint-rule` (which producer mechanism runs the check).
|
|
141
164
|
- `check_target` — the test file (`node-test`) or the file the rule runs against (`lint-rule`).
|
|
142
165
|
- `check_rule` — the `ruleId` to filter on, **lint-rule only** (absent for `node-test`).
|
|
166
|
+
- `check_violation_fixture` — path to a KNOWN-BAD subject the #1279 prover runs the check against to
|
|
167
|
+
machine-prove fail-first (rides BOTH kinds; for `node-test` it is injected via `GSD_PROHIB_SUBJECT`).
|
|
143
168
|
|
|
144
169
|
The flat-scalar shape is load-bearing: the shared `parseMustHavesBlock` is a flat parser and a
|
|
145
170
|
nested object would flatten/mangle the round-trip (ADR-550 2026-06-15 addendum; #644 "no parser
|
|
146
171
|
rewrite" precedent). `projectProhibitions` emits these keys **only for a well-formed descriptor**
|
|
147
|
-
(valid `check_kind` + non-empty `check_target`; `check_rule` only on the lint-rule path
|
|
148
|
-
verify-phase reads them back via
|
|
149
|
-
`
|
|
150
|
-
|
|
172
|
+
(valid `check_kind` + non-empty `check_target`; `check_rule` only on the lint-rule path;
|
|
173
|
+
`check_violation_fixture` only when non-empty), and verify-phase reads them back via
|
|
174
|
+
`descriptorFromProjection` into the `CheckDescriptor` handed to `check prohibition-enforcement`. This
|
|
175
|
+
closes **both** the locate (#1278) and the machine-proof-fixture (#1346) halves with **zero manual
|
|
176
|
+
descriptor authoring**: a prohibition authored with all four scalars greens end-to-end through the
|
|
177
|
+
projection alone.
|
|
151
178
|
|
|
152
179
|
**Fail-closed + backward-compat.** A partial descriptor (`lint-rule` missing `check_rule`), an
|
|
153
|
-
unknown `check_kind`,
|
|
154
|
-
producer's
|
|
155
|
-
parses and disposes byte-identically to today.
|
|
156
|
-
|
|
157
|
-
#1279; the `dispositionForProhibition`
|
|
180
|
+
unknown `check_kind`, an **absent** descriptor, OR a descriptor with **no `check_violation_fixture`**
|
|
181
|
+
falls through to the producer's fail-closed paths (`located: false`, or located-but-unprovable) —
|
|
182
|
+
never a silent green. A prohibition with no descriptor parses and disposes byte-identically to today.
|
|
183
|
+
`failFirst` is **not** sourced from the descriptor and is **demoted** (machine-proven fail-first
|
|
184
|
+
DELIVERED in #1279 — no path greens on attestation alone, FF-08); the `dispositionForProhibition`
|
|
185
|
+
policy is unchanged. Residual (tracked **#1346**): the node-test proof confirms the fixture exists and
|
|
186
|
+
the check goes RED, but cannot generically prove the red was *caused by* the subject's content.
|
|
158
187
|
|
|
159
188
|
## Output schema
|
|
160
189
|
|
|
@@ -6,9 +6,13 @@ block — do not inline a copy elsewhere. History of coordinated edits: #2924, #
|
|
|
6
6
|
|
|
7
7
|
**Contract for orchestrators:** before dispatch, capture `EXPECTED_BASE=$(git rev-parse HEAD)`,
|
|
8
8
|
then embed the block below into the sub-agent prompt verbatim, substituting `{EXPECTED_BASE}`
|
|
9
|
-
with that captured SHA.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
with that captured SHA. Orchestrators that intentionally create a docs-only pre-dispatch
|
|
10
|
+
plan commit may also substitute `{EXPECTED_BASE_ALTERNATE}` with that commit's immediate
|
|
11
|
+
parent so runtimes that fork from either side of the docs-only commit pass the same
|
|
12
|
+
fail-closed guard (#1265). Otherwise substitute `{EXPECTED_BASE_ALTERNATE}` with an empty
|
|
13
|
+
string. The sub-agent only *verifies* and fails closed; the orchestrator (the worktree
|
|
14
|
+
lifecycle owner) performs any base recovery — the sub-agent never rewrites a worktree it
|
|
15
|
+
did not create (#48).
|
|
12
16
|
|
|
13
17
|
<worktree_branch_check>
|
|
14
18
|
FIRST ACTION: HEAD assertion MUST run before anything else, and this block is
|
|
@@ -30,8 +34,10 @@ if ! echo "$ACTUAL_BRANCH" | grep -Eq '^worktree-agent-[A-Za-z0-9._/-]+$'; then
|
|
|
30
34
|
echo "FATAL: worktree HEAD '$ACTUAL_BRANCH' is not in the worktree-agent-* namespace; refusing to commit (#2924)." >&2
|
|
31
35
|
exit 42
|
|
32
36
|
fi
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
ACTUAL_BASE=$(git rev-parse HEAD)
|
|
38
|
+
EXPECTED_BASE_ALTERNATE="{EXPECTED_BASE_ALTERNATE}"
|
|
39
|
+
if [ "$ACTUAL_BASE" != "{EXPECTED_BASE}" ] && { [ -z "$EXPECTED_BASE_ALTERNATE" ] || [ "$ACTUAL_BASE" != "$EXPECTED_BASE_ALTERNATE" ]; }; then
|
|
40
|
+
echo "FATAL: worktree base mismatch — HEAD is $ACTUAL_BASE, expected {EXPECTED_BASE}${EXPECTED_BASE_ALTERNATE:+ or $EXPECTED_BASE_ALTERNATE}. Orchestrator owns recovery; sub-agent refuses to rewrite the worktree (#48)." >&2
|
|
35
41
|
exit 42
|
|
36
42
|
fi
|
|
37
43
|
```
|
|
@@ -457,27 +457,23 @@ Continue to collect_wave_1.
|
|
|
457
457
|
<step name="collect_wave_1">
|
|
458
458
|
**Read the work manifest first:** `Read .planning/tmp/docs-work-manifest.json` — update `status` to `"completed"` or `"failed"` for each Wave 1 item after collection. Write the updated manifest back to disk.
|
|
459
459
|
|
|
460
|
-
Wait for all 3 Wave 1 agents to
|
|
460
|
+
Wait for all 3 Wave 1 background agents to finish, then read each agent's output file to collect confirmations.
|
|
461
461
|
|
|
462
|
-
|
|
462
|
+
Each `Agent(...)` call above with `run_in_background=true` returns an `async_launched` result that carries an `outputFile` path (and `canReadOutputFile: true`). Each agent's completion arrives as a message in this conversation when it finishes — do NOT issue a separate blocking call to wait. Once all 3 agents have reported completion, read their output files in parallel (single message with 3 Read calls):
|
|
463
463
|
|
|
464
464
|
```
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
block: true
|
|
468
|
-
timeout: 300000
|
|
465
|
+
Read tool:
|
|
466
|
+
file_path: "{outputFile from README agent result}"
|
|
469
467
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
block: true
|
|
473
|
-
timeout: 300000
|
|
468
|
+
Read tool:
|
|
469
|
+
file_path: "{outputFile from ARCHITECTURE agent result}"
|
|
474
470
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
block: true
|
|
478
|
-
timeout: 300000
|
|
471
|
+
Read tool:
|
|
472
|
+
file_path: "{outputFile from CONFIGURATION agent result}"
|
|
479
473
|
```
|
|
480
474
|
|
|
475
|
+
> Allow up to 5 minutes (300000 ms) for the slowest agent to finish before treating it as failed.
|
|
476
|
+
|
|
481
477
|
**Expected confirmation format from each agent:**
|
|
482
478
|
```
|
|
483
479
|
## Doc Generation Complete
|
|
@@ -676,29 +672,25 @@ Continue to collect_wave_2.
|
|
|
676
672
|
<step name="collect_wave_2">
|
|
677
673
|
**Read the work manifest first:** `Read .planning/tmp/docs-work-manifest.json` — update `status` to `"completed"` or `"failed"` for each Wave 2 item after collection. Write the updated manifest back to disk.
|
|
678
674
|
|
|
679
|
-
Wait for all Wave 2 agents to
|
|
675
|
+
Wait for all Wave 2 background agents to finish, then read each agent's output file to collect confirmations.
|
|
680
676
|
|
|
681
|
-
|
|
677
|
+
Each `Agent(...)` call above with `run_in_background=true` returns an `async_launched` result that carries an `outputFile` path (and `canReadOutputFile: true`). Each agent's completion arrives as a message in this conversation when it finishes — do NOT issue a separate blocking call to wait. Once all Wave 2 agents have reported completion, read their output files in parallel (single message with N Read calls — one per spawned Wave 2 agent):
|
|
682
678
|
|
|
683
679
|
```
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
block: true
|
|
687
|
-
timeout: 300000
|
|
680
|
+
Read tool:
|
|
681
|
+
file_path: "{outputFile from GETTING-STARTED agent result}"
|
|
688
682
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
block: true
|
|
692
|
-
timeout: 300000
|
|
683
|
+
Read tool:
|
|
684
|
+
file_path: "{outputFile from DEVELOPMENT agent result}"
|
|
693
685
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
block: true
|
|
697
|
-
timeout: 300000
|
|
686
|
+
Read tool:
|
|
687
|
+
file_path: "{outputFile from TESTING agent result}"
|
|
698
688
|
|
|
699
|
-
# Add one
|
|
689
|
+
# Add one Read call per conditional agent spawned (API, DEPLOYMENT, CONTRIBUTING)
|
|
700
690
|
```
|
|
701
691
|
|
|
692
|
+
> Allow up to 5 minutes (300000 ms) for the slowest agent to finish before treating it as failed.
|
|
693
|
+
|
|
702
694
|
**After collection, verify all Wave 2 files exist on disk** using the `resolved_path` from each manifest entry:
|
|
703
695
|
```bash
|
|
704
696
|
ls -la {resolved_path for each wave 2 item} 2>/dev/null
|
|
@@ -752,9 +744,9 @@ Write {package_dir}/README.md directly. Return confirmation only — do not retu
|
|
|
752
744
|
)
|
|
753
745
|
```
|
|
754
746
|
|
|
755
|
-
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling all per-package Agent() calls above with `run_in_background=true`, do NOT generate any package READMEs independently while the subagents are active. Wait for all agents to complete
|
|
747
|
+
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling all per-package Agent() calls above with `run_in_background=true`, do NOT generate any package READMEs independently while the subagents are active. Wait for all agents to complete before proceeding. This prevents duplicate work and wasted context.
|
|
756
748
|
|
|
757
|
-
Collect confirmations
|
|
749
|
+
Collect confirmations by reading each package agent's `outputFile` once it reports completion — each `run_in_background=true` Agent call returns an `async_launched` result carrying an `outputFile` path (with `canReadOutputFile: true`). Note failures in the final report.
|
|
758
750
|
|
|
759
751
|
**Fallback when Task tool is unavailable:** Generate per-package READMEs sequentially inline after the `sequential_generation` step. For each package directory with a `package.json`, construct the equivalent `doc_assignment` block and generate the README following gsd-doc-writer instructions.
|
|
760
752
|
|
|
@@ -635,6 +635,7 @@ increases monotonically across waves. `{status}` is `complete` (success),
|
|
|
635
635
|
this commit — the orchestrator force-removes the worktree after you return, and
|
|
636
636
|
any uncommitted SUMMARY.md will be permanently lost (#2070).
|
|
637
637
|
REQUIRED ORDER: Write SUMMARY.md → commit → only then any narration. No text between Write and commit (truncation risk; #2070 rescue is not primary defense).
|
|
638
|
+
|
|
638
639
|
</parallel_execution>
|
|
639
640
|
|
|
640
641
|
<execution_context>
|
|
@@ -682,7 +683,7 @@ increases monotonically across waves. `{status}` is `complete` (success),
|
|
|
682
683
|
)
|
|
683
684
|
```
|
|
684
685
|
|
|
685
|
-
|
|
686
|
+
After each `Agent()` returns, parse executor-returned worktree metadata (`<worktree_metadata>`) before harness metadata, then atomically append `{agent_id, worktree_path, branch, expected_base}` to `WAVE_WORKTREE_MANIFEST`. Missing: stop and ask for recovery instead of scanning worktrees.
|
|
686
687
|
|
|
687
688
|
> **Worktree recovery policy (#48 + #1292):** See `execute-phase/steps/worktree-recovery-policy.md` — FAIL-CLOSED rule for base/HEAD-namespace mismatches AND isolated-run fail-safe recovery.
|
|
688
689
|
|
|
@@ -255,21 +255,19 @@ Continue to collect_confirmations.
|
|
|
255
255
|
</step>
|
|
256
256
|
|
|
257
257
|
<step name="collect_confirmations">
|
|
258
|
-
Wait for all 4 agents to
|
|
258
|
+
Wait for all 4 background agents to finish, then read each agent's output file to collect confirmations.
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
Each `Agent(...)` call above with `run_in_background=true` returns an `async_launched` result that carries an `outputFile` path (and `canReadOutputFile: true`). The 4 agents run concurrently and each one's completion arrives as a message in this conversation when it finishes — do NOT issue a separate blocking call to wait for them.
|
|
261
|
+
|
|
262
|
+
**Once all 4 agents have reported completion, read each agent's output file (single message with 4 Read calls):**
|
|
261
263
|
```
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
block: true
|
|
265
|
-
timeout: {subagent_timeout from init context, default 300000}
|
|
264
|
+
Read tool:
|
|
265
|
+
file_path: "{outputFile from that agent's async_launched result}"
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
-
> The timeout is configurable via `workflow.subagent_timeout` in `.planning/config.json` (milliseconds). Default: 300000 (5 minutes). Increase for large codebases or slower models.
|
|
269
|
-
|
|
270
|
-
Call TaskOutput for all 4 agents in parallel (single message with 4 TaskOutput calls).
|
|
268
|
+
> Allow up to `workflow.subagent_timeout` for the slowest agent to finish before treating it as failed. The timeout is configurable via `workflow.subagent_timeout` in `.planning/config.json` (milliseconds). Default: 300000 (5 minutes). Increase for large codebases or slower models.
|
|
271
269
|
|
|
272
|
-
|
|
270
|
+
Each output file contains that agent's completion confirmation. Parse the confirmation marker (see below) from the file contents.
|
|
273
271
|
|
|
274
272
|
**Expected confirmation format from each agent:**
|
|
275
273
|
```
|
|
@@ -499,6 +499,12 @@ Display banner:
|
|
|
499
499
|
|
|
500
500
|
### Spawn gsd-phase-researcher
|
|
501
501
|
|
|
502
|
+
```bash
|
|
503
|
+
if gsd_run query teams-status --active >/dev/null 2>&1; then
|
|
504
|
+
echo "⚠️ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS detected. GSD's multi-agent orchestration is not validated under claude-code agent-teams and may stall (a subagent's completion can fail to route to the orchestrator). Recommend disabling agent-teams for GSD workflows. See https://github.com/open-gsd/gsd-core/issues/1355" >&2
|
|
505
|
+
fi
|
|
506
|
+
```
|
|
507
|
+
|
|
502
508
|
```bash
|
|
503
509
|
PHASE_DESC=$(gsd_run query roadmap.get-phase "${PHASE}" --pick section)
|
|
504
510
|
if [ -z "${PLAN_PRE_HOOKS_JSON:-}" ]; then
|
|
@@ -632,7 +632,10 @@ When `USE_WORKTREES !== "false"`, commit PLAN.md to the current branch **before*
|
|
|
632
632
|
Skip this step entirely if `USE_WORKTREES === "false"` (non-worktree mode: PLAN.md is committed in Step 8 as usual).
|
|
633
633
|
|
|
634
634
|
```bash
|
|
635
|
+
QUICK_PLAN_PARENT=""
|
|
636
|
+
QUICK_PLAN_COMMIT=""
|
|
635
637
|
if [ "${USE_WORKTREES}" != "false" ]; then
|
|
638
|
+
QUICK_PLAN_PARENT=$(git rev-parse HEAD)
|
|
636
639
|
COMMIT_DOCS=$(gsd_run query config-get commit_docs 2>/dev/null || echo "true")
|
|
637
640
|
if [ "$COMMIT_DOCS" != "false" ]; then
|
|
638
641
|
git add "${QUICK_DIR}/${quick_id}-PLAN.md"
|
|
@@ -650,8 +653,12 @@ if [ "${USE_WORKTREES}" != "false" ]; then
|
|
|
650
653
|
git commit -m "docs(${quick_id}): pre-dispatch plan for ${DESCRIPTION}" -- "${QUICK_DIR}/${quick_id}-PLAN.md" \
|
|
651
654
|
|| { echo "ERROR: pre-dispatch PLAN.md commit failed — likely a pre-commit hook failure. Fix the hook output above (or set workflow.worktree_skip_hooks=true to bypass) and re-run." >&2; exit 1; }
|
|
652
655
|
fi
|
|
656
|
+
QUICK_PLAN_COMMIT=$(git rev-parse HEAD)
|
|
653
657
|
fi
|
|
654
658
|
fi
|
|
659
|
+
if [ -z "$QUICK_PLAN_COMMIT" ]; then
|
|
660
|
+
QUICK_PLAN_COMMIT=$(git rev-parse HEAD)
|
|
661
|
+
fi
|
|
655
662
|
fi
|
|
656
663
|
```
|
|
657
664
|
|
|
@@ -678,8 +685,22 @@ Execute quick task ${quick_id}.
|
|
|
678
685
|
|
|
679
686
|
${USE_WORKTREES !== "false" ? `
|
|
680
687
|
<worktree_branch_check>
|
|
681
|
-
ORCHESTRATOR build-time embed (NOT a sub-agent runtime step): before this dispatch, read \`gsd-core/references/worktree-branch-check.md\`, substitute \`{EXPECTED_BASE}\` with the base SHA captured above (${EXPECTED_BASE}), and replace this note with that fragment's \`<worktree_branch_check>\` block so the dispatched prompt carries the runnable guard verbatim — do not pass this instruction through in its place.
|
|
688
|
+
ORCHESTRATOR build-time embed (NOT a sub-agent runtime step): before this dispatch, read \`gsd-core/references/worktree-branch-check.md\`, substitute \`{EXPECTED_BASE}\` with the base SHA captured above (${EXPECTED_BASE}), substitute \`{EXPECTED_BASE_ALTERNATE}\` with \`${QUICK_PLAN_PARENT}\` when it differs from \`${EXPECTED_BASE}\` (otherwise empty), and replace this note with that fragment's \`<worktree_branch_check>\` block so the dispatched prompt carries the runnable guard verbatim — do not pass this instruction through in its place.
|
|
682
689
|
</worktree_branch_check>
|
|
690
|
+
|
|
691
|
+
FIRST ACTION after the worktree branch check: ensure the quick PLAN.md exists at a worktree-rooted relative path before any Read/Edit/Write path can be primed. If \`${QUICK_DIR}/${quick_id}-PLAN.md\` is absent, materialize it from the shared git object store:
|
|
692
|
+
|
|
693
|
+
\`\`\`bash
|
|
694
|
+
QUICK_PLAN_COMMIT="${QUICK_PLAN_COMMIT}"
|
|
695
|
+
QUICK_PLAN_PATH="${QUICK_DIR}/${quick_id}-PLAN.md"
|
|
696
|
+
if [ ! -f "$QUICK_PLAN_PATH" ]; then
|
|
697
|
+
mkdir -p "$(dirname "$QUICK_PLAN_PATH")"
|
|
698
|
+
git show "${QUICK_PLAN_COMMIT}:${QUICK_PLAN_PATH}" > "$QUICK_PLAN_PATH" || {
|
|
699
|
+
echo "FATAL: unable to materialize quick plan from ${QUICK_PLAN_COMMIT}:${QUICK_PLAN_PATH}; refusing to continue." >&2
|
|
700
|
+
exit 42
|
|
701
|
+
}
|
|
702
|
+
fi
|
|
703
|
+
\`\`\`
|
|
683
704
|
` : ''}
|
|
684
705
|
|
|
685
706
|
<files_to_read>
|
|
@@ -743,7 +764,7 @@ SUMMARY.md and stop — the user must rerun with worktrees disabled.
|
|
|
743
764
|
|
|
744
765
|
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
|
|
745
766
|
|
|
746
|
-
If the executor ran with `isolation="worktree"`, append its returned `{agent_id, worktree_path, branch, expected_base}` metadata to `QUICK_WORKTREE_MANIFEST` before cleanup. If any field is unavailable, stop and ask for recovery; do not discover global worktrees.
|
|
767
|
+
If the executor ran with `isolation="worktree"`, append its returned `{agent_id, worktree_path, branch, expected_base, allowed_bases}` metadata to `QUICK_WORKTREE_MANIFEST` before cleanup. Set `expected_base` to `${EXPECTED_BASE}` and `allowed_bases` to `["${EXPECTED_BASE}", "${QUICK_PLAN_PARENT}"]` with duplicates removed. If any required field is unavailable, stop and ask for recovery; do not discover global worktrees.
|
|
747
768
|
|
|
748
769
|
After executor returns:
|
|
749
770
|
1. **Worktree cleanup:** If the executor ran with `isolation="worktree"`, merge the worktree branch back and clean up:
|
|
@@ -365,11 +365,16 @@ For each Requirement gathered so far, run the two-stage recall→precision pass:
|
|
|
365
365
|
- `check_target` — the negative-test file path (for `node-test`), or the path to lint
|
|
366
366
|
(for `lint-rule`).
|
|
367
367
|
- `check_rule` — the eslint rule id (e.g. `local/no-source-grep`); `lint-rule` only.
|
|
368
|
+
- `check_violation_fixture` (#1346) — path to a KNOWN-BAD subject the wired check is run
|
|
369
|
+
against to **machine-prove fail-first**; rides BOTH kinds. Capture it to let the item green
|
|
370
|
+
end-to-end with zero hand-authoring at verify time; for `node-test` the negative test should
|
|
371
|
+
read its subject from the `GSD_PROHIB_SUBJECT` env var so the prover can inject this fixture.
|
|
368
372
|
This is a **SOFT capture (CHK-04): a `test`-tier prohibition WITHOUT a descriptor is still
|
|
369
373
|
allowed** — if the author cannot yet name the wired check, leave the descriptor empty and
|
|
370
374
|
proceed. It is NOT a hard authoring block; the item simply stays fail-closed/flagged
|
|
371
|
-
downstream (an absent/partial descriptor
|
|
372
|
-
→
|
|
375
|
+
downstream (an absent/partial descriptor — or one with no `check_violation_fixture` —
|
|
376
|
+
→ `descriptorFromProjection` null/under-specified/fixture-less → producer fail-closed
|
|
377
|
+
locate-or-unprovable, never green). Do NOT capture `failFirst` here — it is a
|
|
373
378
|
verify-time caller attestation, not a spec-authored field (#1279).
|
|
374
379
|
- **Dismiss (reason)** → mark `dismissed` with a REQUIRED non-empty reason (PROB-05). The
|
|
375
380
|
reason string is the audit trail; silence is not a valid dismissal.
|
|
@@ -390,8 +395,8 @@ For each Requirement gathered so far, run the two-stage recall→precision pass:
|
|
|
390
395
|
written (test or judgment tier); otherwise leave `unresolved`. **`--auto` NEVER auto-dismisses
|
|
391
396
|
a prohibition** — a wrong dismissal is the exact silent failure this probe eliminates (PROB-06,
|
|
392
397
|
the load-bearing safety property). On a `test`-tier auto-resolution, capture the `check_kind` /
|
|
393
|
-
`check_target` / `check_rule` descriptor **only when a wired check is unambiguous**; otherwise
|
|
394
|
-
leave it empty — `--auto` NEVER fabricates a check path (a wrong locate is re-validated and
|
|
398
|
+
`check_target` / `check_rule` / `check_violation_fixture` descriptor **only when a wired check is unambiguous**; otherwise
|
|
399
|
+
leave it empty — `--auto` NEVER fabricates a check path or fixture (a wrong locate is re-validated and
|
|
395
400
|
fails closed at the producer, but a fabricated path is still noise to avoid). Log:
|
|
396
401
|
`[auto] prohibitions: R resolved, U unresolved`.
|
|
397
402
|
|
|
@@ -402,8 +407,8 @@ runs identically for non-Claude / text-mode hosts.
|
|
|
402
407
|
Populate the `## Prohibitions` section of SPEC.md from the resolved prohibitions (each
|
|
403
408
|
`resolved`/`test` row is a checkable negative acceptance criterion; `resolved`/`judgment`
|
|
404
409
|
rows route to judgment review; `⚠ UNRESOLVED` rows are flagged as assumptions). A
|
|
405
|
-
`resolved`/`test` row ALSO carries its captured `check_kind` / `check_target` / `check_rule`
|
|
406
|
-
descriptor when present (so the projection feeds `verify-phase`'s deterministic locate, #1278);
|
|
410
|
+
`resolved`/`test` row ALSO carries its captured `check_kind` / `check_target` / `check_rule` /
|
|
411
|
+
`check_violation_fixture` descriptor when present (so the projection feeds `verify-phase`'s deterministic locate + machine-proof, #1278 + #1346);
|
|
407
412
|
a `test` row with no captured descriptor is still valid — it stays fail-closed/flagged
|
|
408
413
|
downstream rather than blocking authoring.
|
|
409
414
|
|
|
@@ -70,17 +70,17 @@ Aggregate all must_haves across plans for phase-level verification.
|
|
|
70
70
|
**Prohibitions (`must_haves.prohibitions`, ADR-550 D3 — the must-NOT sibling block):** When a plan carries `must_haves.prohibitions`, extract each `{ statement, status, verification }` item and route it by `verification` tier in verdict assembly (ADR-550 D4, "B-with-guard", 2026-06-12 maintainer decision). These are NEGATIVE checks (the must-NOT must NOT have happened), distinct from positive `truths`:
|
|
71
71
|
|
|
72
72
|
- **judgment-tier → mode-dependent soft-gate.** Interactive verify defers each item to the end-of-phase human checkpoint (`human_verify_mode: end-of-phase`). Autonomous verify records a NON-AUTHORITATIVE LLM-judge verdict + a prominent `unverified-prohibition — human review recommended` flag (autonomous completion reads "complete with N flagged prohibitions"). NEVER a silent pass; NEVER a hard halt of an AFK run.
|
|
73
|
-
- **test-tier → ENFORCED via `check prohibition-enforcement` (green on pass, hard-gate on miss/fail).** Accept the `verification: test` value (the SPEC↔must_haves.prohibitions projection contract holds — no forced schema change later). For each test-tier item, the verifier builds `request.check` **DETERMINISTICALLY from the projected descriptor** — it does NOT invent `{ kind, target, rule }`. Read the flat scalar keys `check_kind` / `check_target` / `check_rule` off the `must_haves.prohibitions` item and reconstruct the `CheckDescriptor` via the `descriptorFromProjection` adapter in `prohibition-enforcement` (`descriptorFromProjection(projectedItem)` → `{ kind: check_kind, target: check_target, rule?: check_rule }`).
|
|
73
|
+
- **test-tier → ENFORCED via `check prohibition-enforcement` (green on pass, hard-gate on miss/fail).** Accept the `verification: test` value (the SPEC↔must_haves.prohibitions projection contract holds — no forced schema change later). For each test-tier item, the verifier builds `request.check` **DETERMINISTICALLY from the projected descriptor** — it does NOT invent `{ kind, target, rule }`. Read the flat scalar keys `check_kind` / `check_target` / `check_rule` / `check_violation_fixture` off the `must_haves.prohibitions` item and reconstruct the `CheckDescriptor` via the `descriptorFromProjection` adapter in `prohibition-enforcement` (`descriptorFromProjection(projectedItem)` → `{ kind: check_kind, target: check_target, rule?: check_rule, violationFixture?: check_violation_fixture }`). The `violationFixture` (a path to a KNOWN-BAD subject) is the field that gates **green** and it is **now projected** (`check_violation_fixture`, #1346) — so a prohibition authored with all four scalars greens through the projection alone, **zero hand-authoring at verify time**. Do NOT rely on `failFirst`: it is DEMOTED (#1279) and greens nothing on its own; an item with no projected fixture hard-gates fail-closed. Invoke the producer (CLI surface unchanged):
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
gsd_run check prohibition-enforcement <request.json>
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
where `<request.json>` carries `{ prohibition, check, mode }` — `check` being the wired mechanical-check descriptor `{ kind: 'node-test' | 'lint-rule', target, rule?, failFirst
|
|
79
|
+
where `<request.json>` carries `{ prohibition, check, mode }` — `check` being the wired mechanical-check descriptor `{ kind: 'node-test' | 'lint-rule', target, rule?, violationFixture, failFirst? }`, with `kind`/`target`/`rule`/`violationFixture` now sourced from the projected `check_*` scalars (not author/verifier invention — #1278 + #1346). For `node-test`, `target` (from `check_target`) is the negative-test file path; for `lint-rule`, `target` is the PATH to lint and `rule` (from `check_rule`) is the eslint rule id (e.g. `local/no-source-grep`) — both required (a lint-rule without `rule` is not a valid wired check). `violationFixture` (from `check_violation_fixture`) is the path to a KNOWN-BAD subject the producer runs the check against to **machine-prove fail-first** (for `node-test`, injected via the `GSD_PROHIB_SUBJECT` env convention — #1279); `failFirst` is a DEMOTED, non-authoritative hint kept only for backward route-JSON shape (no path greens on it alone — FF-08). The producer LOCATES the wired check from the projection, **machine-proves it is fail-first** by running it against the violation and confirming it goes RED, RUNS it for a genuine non-vacuous pass, builds `enforcementEvidence`, and emits the `dispositionForProhibition()` verdict (#1259 + #1278 + #1279, ADR-550 D5d). Fail-first is **machine-proven, not caller-attested** — absent a provable violation the producer fails closed, never falling back to attestation. Route the result by its typed fields:
|
|
80
80
|
- **`status: 'green'`, `flagged: false`** (a genuinely-passing wired negative test / lint rule, `located: true`, non-empty `evidence`) → the item is satisfiable → it can reach **passed**.
|
|
81
81
|
- **missing, non-attested, or genuinely-non-passing check** (`located: false` OR `status: 'unverified'`, `flagged: true`) → **hard-gate**: disposes flagged-unverified, NEVER green, routing to `gaps_found` in BOTH interactive and autonomous modes (a failing mechanical check blocks even AFK; ADR-550 D4 / D3). The deterministic fail-closed default backing every miss/fail is `dispositionForProhibition()` in probe-core (`status: 'unverified'`, `flagged: true` on empty `enforcementEvidence`).
|
|
82
82
|
|
|
83
|
-
> **Descriptor source — deterministic locate (#1278, DELIVERED).** The `check` descriptor's `{ kind, target, rule }` is now sourced **deterministically from the projected `check_kind` / `check_target` / `check_rule` scalars** on the `must_haves.prohibitions` item (authored at `/gsd:spec-phase`, projected by `projectProhibitions`, read back via the `descriptorFromProjection` adapter). So
|
|
83
|
+
> **Descriptor source — deterministic locate + machine-proof compose (#1278 + #1346, DELIVERED).** The `check` descriptor's `{ kind, target, rule, violationFixture }` is now sourced **deterministically from the projected `check_kind` / `check_target` / `check_rule` / `check_violation_fixture` scalars** on the `must_haves.prohibitions` item (authored at `/gsd:spec-phase`, projected by `projectProhibitions`, read back via the `descriptorFromProjection` adapter). So both halves close with **zero manual descriptor authoring** — the verifier neither invents the locate (#1278) nor hand-supplies the violation fixture (#1346): a prohibition authored with all four scalars machine-proves fail-first and greens end-to-end through the projection alone (removing the spoofable invent-at-verify-time surface; ADR-857 §147 exogenous grading). **Fail-closed is preserved:** an item with NO projected descriptor, a PARTIAL one (e.g. a `lint-rule` missing `check_rule`), OR a descriptor with **no `check_violation_fixture`** makes `descriptorFromProjection` return `null` / an under-specified or fixture-less descriptor, which falls through to the producer's fail-closed paths (`located: false`, or located-but-unprovable) → flagged-unverified, NEVER green, in BOTH modes. `failFirst` is demoted and greens nothing on its own (#1279, FF-08). Residual (tracked **#1346**): the node-test proof confirms the fixture exists and the check goes RED, but cannot generically prove the red was *caused by* the subject's content vs the env merely being set.
|
|
84
84
|
|
|
85
85
|
**Option B: Use Success Criteria from ROADMAP.md**
|
|
86
86
|
|