@opengsd/gsd-core 1.8.0 → 1.9.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +186 -55
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +849 -2
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +5 -5
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +57 -5
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +128 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +76 -19
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unusable Input Diagnostic — the out-of-band half of ADR-1411's
|
|
4
|
+
* "corrupt is not absent" amendment (epic #1879).
|
|
5
|
+
*
|
|
6
|
+
* ADR-1411 splits the amendment's mechanism in two. Where a read already returns a
|
|
7
|
+
* provenance envelope, the cause is named *in-band* — that is `ConfigResolution.reason`
|
|
8
|
+
* (#1880, shipped). Where a read returns a bare sentinel or a plausible default it cannot
|
|
9
|
+
* extend, the return value is preserved exactly and the cause is surfaced *out-of-band*,
|
|
10
|
+
* as a deduplicated diagnostic on stderr. This module owns that second mechanism.
|
|
11
|
+
*
|
|
12
|
+
* It exists as a shared seam rather than a pattern copied per site because four call sites
|
|
13
|
+
* across four modules need identical behaviour (#1882 frontmatter, #1881 roadmap-parser,
|
|
14
|
+
* #1883 planning-workspace/verify, #1884 planning lock). Four hand-rolled copies of one
|
|
15
|
+
* behaviour is `DEFECT.GENERATIVE-FIX` by construction; one seam with a frozen reason set
|
|
16
|
+
* is the documented cure.
|
|
17
|
+
*
|
|
18
|
+
* Two contracts this module must not break:
|
|
19
|
+
*
|
|
20
|
+
* - **Unconditional.** ADR-1411 diverges deliberately from ADR-227's never-implemented
|
|
21
|
+
* `GSD_DEBUG` opt-in: "an opt-in nobody sets is indistinguishable from the silence
|
|
22
|
+
* #1879 is about". There is no config gate here, by design.
|
|
23
|
+
* - **Never throws.** Callers are leaf readers that promised a total function. A failed
|
|
24
|
+
* stderr write (closed stream, EPIPE) must not turn a silent degradation into a crash.
|
|
25
|
+
*/
|
|
26
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
+
};
|
|
29
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
30
|
+
// ─── Reason vocabulary ────────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Frozen so tests assert a typed surface instead of diagnostic prose
|
|
33
|
+
* (CONTRIBUTING.md — Prohibited: Raw Text Matching on Test Outputs).
|
|
34
|
+
*
|
|
35
|
+
* Adding a reason is three coordinated changes, matching the repo's `REASON`-enum
|
|
36
|
+
* convention: the entry here, the emitting call site, and the test that locks
|
|
37
|
+
* `Object.keys(UNUSABLE_REASON).sort()`. Each epic-#1879 phase adds only its own —
|
|
38
|
+
* pre-declaring the later phases' reasons would be speculative generality and would
|
|
39
|
+
* leave values no call site emits.
|
|
40
|
+
*/
|
|
41
|
+
const UNUSABLE_REASON = Object.freeze({
|
|
42
|
+
/**
|
|
43
|
+
* A file opened a `---` frontmatter fence at byte 0, carried at least one parseable
|
|
44
|
+
* key, and never closed the fence — a truncated or half-written file, NOT a file that
|
|
45
|
+
* legitimately has no frontmatter. (#1882)
|
|
46
|
+
*/
|
|
47
|
+
FRONTMATTER_UNTERMINATED: 'frontmatter_unterminated',
|
|
48
|
+
/**
|
|
49
|
+
* A ROADMAP.md exists but could not be read (EACCES/EIO/…). Distinct from a project that
|
|
50
|
+
* simply has no ROADMAP yet: absence returns the same sentinel, silently. (#1881)
|
|
51
|
+
*/
|
|
52
|
+
ROADMAP_UNREADABLE: 'roadmap_unreadable',
|
|
53
|
+
});
|
|
54
|
+
/** One human-readable clause per reason. Prose lives here, never in a test assertion. */
|
|
55
|
+
const REASON_PROSE = Object.freeze({
|
|
56
|
+
[UNUSABLE_REASON.FRONTMATTER_UNTERMINATED]: 'frontmatter opens with "---" but never closes; metadata was NOT applied',
|
|
57
|
+
[UNUSABLE_REASON.ROADMAP_UNREADABLE]: 'ROADMAP.md exists but could not be read; phase and milestone lookups fell back to defaults',
|
|
58
|
+
});
|
|
59
|
+
// ─── Dedup state ──────────────────────────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Process-lifetime dedup set. Mirrors `config-loader.cjs`'s `_warnedUnknownConfigKeys`
|
|
62
|
+
* guard, which ADR-1411 names as the precedent to reuse.
|
|
63
|
+
*/
|
|
64
|
+
const _warnedUnusableInputs = new Set();
|
|
65
|
+
/**
|
|
66
|
+
* Count of diagnostics actually WRITTEN, which is not the same as the size of the dedup set:
|
|
67
|
+
* one emission records every key the input could later be identified by, so set size counts
|
|
68
|
+
* identities while this counts events. Tests assert on this because the behavioural claim is
|
|
69
|
+
* "how many diagnostics did the operator see", not "how many keys are interned".
|
|
70
|
+
*/
|
|
71
|
+
let _unusableInputEmissions = 0;
|
|
72
|
+
/**
|
|
73
|
+
* ASCII control characters (including NUL) are stripped from any path before it is used
|
|
74
|
+
* as a key component or written to a terminal. Two reasons, both real:
|
|
75
|
+
*
|
|
76
|
+
* - the key separator is NUL, so a `sourcePath` containing NUL could otherwise forge a
|
|
77
|
+
* collision with a different (path, reason) pair and suppress a genuine second failure;
|
|
78
|
+
* - a path carrying ANSI escapes would be replayed verbatim into the operator's terminal.
|
|
79
|
+
*/
|
|
80
|
+
const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
|
|
81
|
+
/**
|
|
82
|
+
* Strip control characters. Deliberately does NOT normalize path separators.
|
|
83
|
+
*
|
|
84
|
+
* An earlier revision folded backslashes to `/` unconditionally, reasoning that `C:\a\b.md`
|
|
85
|
+
* and `C:/a/b.md` are one file and should not report twice. That is true on Windows, and
|
|
86
|
+
* false — destructively — everywhere else: `\` is a legal filename character on Linux and
|
|
87
|
+
* macOS, so `/repo/weird\name/PLAN.md` and `/repo/weird/name/PLAN.md` are two genuinely
|
|
88
|
+
* different files that collapsed to one key, and the second one's diagnostic was silently
|
|
89
|
+
* swallowed. ADR-1411 forbids exactly that ("keying too coarsely suppresses a genuine second
|
|
90
|
+
* failure in a different file"), and this repo targets Linux/macOS/Windows alike.
|
|
91
|
+
*
|
|
92
|
+
* The trade is now explicit and one-directional: two spellings of one Windows path may
|
|
93
|
+
* report twice (mild noise), but two distinct files can never silence each other (lost
|
|
94
|
+
* signal). Dropping a real diagnostic is the strictly worse failure.
|
|
95
|
+
*/
|
|
96
|
+
function sanitizeSource(source) {
|
|
97
|
+
return source.replace(CONTROL_CHARS, '');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Identify the offending input. A path is preferred because it is what an operator can act
|
|
101
|
+
* on. When the caller has only an in-memory string, fall back to a short content digest so
|
|
102
|
+
* that *different* bad inputs still produce *different* keys.
|
|
103
|
+
*
|
|
104
|
+
* The leading `p`/`d` tag is what keeps the two namespaces disjoint. Without it a caller
|
|
105
|
+
* whose file is literally named `<unnamed:8efa5269728e7271>` would key identically to a
|
|
106
|
+
* path-less caller whose content happens to hash to that digest — no brute force required,
|
|
107
|
+
* since the digest of any predictable content (a shared template, known boilerplate) can
|
|
108
|
+
* simply be computed and used as a filename to pre-seed suppression. Because control
|
|
109
|
+
* characters — including NUL — are stripped from `source`, a caller-supplied path can never
|
|
110
|
+
* contain the separator and so can never forge a key in the other namespace either.
|
|
111
|
+
*
|
|
112
|
+
* The digest is computed only on the emission path, which is rare, so it never costs
|
|
113
|
+
* anything on a healthy read.
|
|
114
|
+
*/
|
|
115
|
+
function sourceKey(source, content) {
|
|
116
|
+
if (typeof source === 'string' && source.trim() !== '') {
|
|
117
|
+
return `p\u0000${sanitizeSource(source)}`;
|
|
118
|
+
}
|
|
119
|
+
const digest = node_crypto_1.default.createHash('sha256').update(content ?? '').digest('hex').slice(0, 16);
|
|
120
|
+
return `d\u0000${digest}`;
|
|
121
|
+
}
|
|
122
|
+
/** Human-facing name for the offending input, derived from the same key. */
|
|
123
|
+
function displaySource(key) {
|
|
124
|
+
return key.startsWith('p\u0000') ? key.slice(2) : `<unnamed:${key.slice(2)}>`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Emit a deduplicated diagnostic naming an input that exists but cannot be used.
|
|
128
|
+
*
|
|
129
|
+
* The key is `<normalized source>\0<reason>`. ADR-1411 requires the resolved path AND the
|
|
130
|
+
* distinguishing cause — keying on the path alone would let a second, different fault on
|
|
131
|
+
* the same file go unreported; keying on the message prose would couple the guard to
|
|
132
|
+
* wording.
|
|
133
|
+
*
|
|
134
|
+
* @returns `true` when this call actually wrote a diagnostic, `false` when it was
|
|
135
|
+
* deduplicated. Returning the decision is what lets tests assert emission *counts* on a
|
|
136
|
+
* typed surface rather than scraping stderr.
|
|
137
|
+
*/
|
|
138
|
+
function warnUnusableInput({ reason, source, content }) {
|
|
139
|
+
// Defensive: an unknown reason must not emit a diagnostic with `undefined` in it.
|
|
140
|
+
const prose = Object.prototype.hasOwnProperty.call(REASON_PROSE, reason)
|
|
141
|
+
? REASON_PROSE[reason]
|
|
142
|
+
: null;
|
|
143
|
+
if (prose === null)
|
|
144
|
+
return false;
|
|
145
|
+
// The guarantee, stated precisely, because it is not symmetric:
|
|
146
|
+
//
|
|
147
|
+
// * a file reported BY NAME is reported at most once, and
|
|
148
|
+
// * an anonymous re-parse of content already reported by name stays silent, and
|
|
149
|
+
// * two DIFFERENT files always both report, even when their truncated bytes are identical.
|
|
150
|
+
//
|
|
151
|
+
// The asymmetry is the anonymous-FIRST ordering (a path-less parse, then a named parse of the
|
|
152
|
+
// same content), which emits twice. That is a deliberate limit, not an oversight. A path-less
|
|
153
|
+
// caller cannot identify its file, so suppressing the later named report would also suppress a
|
|
154
|
+
// genuine second failure in a DIFFERENT file whenever two files share byte-identical truncated
|
|
155
|
+
// content — the over-coarse keying ADR-1411 explicitly forbids. Between a duplicate line and a
|
|
156
|
+
// swallowed diagnostic the ADR ranks the swallow worse, so the duplicate is accepted; and the
|
|
157
|
+
// second line is the more useful of the two, because it carries the filename.
|
|
158
|
+
//
|
|
159
|
+
// Mechanically: check ONLY the key matching what this caller actually knows, but record every
|
|
160
|
+
// key the input could later be identified by.
|
|
161
|
+
const identity = sourceKey(source, content);
|
|
162
|
+
const keys = [`${identity}\u0000${reason}`];
|
|
163
|
+
if (typeof content === 'string' && identity.startsWith('p\u0000')) {
|
|
164
|
+
keys.push(`${sourceKey(undefined, content)}\u0000${reason}`);
|
|
165
|
+
}
|
|
166
|
+
if (_warnedUnusableInputs.has(keys[0]))
|
|
167
|
+
return false;
|
|
168
|
+
for (const k of keys)
|
|
169
|
+
_warnedUnusableInputs.add(k);
|
|
170
|
+
try {
|
|
171
|
+
process.stderr.write(`gsd: warning — ${displaySource(identity)}: ${prose}. (#1879)\n`);
|
|
172
|
+
// Counted only after a write that actually completed. Incrementing before the try counted
|
|
173
|
+
// attempts, so on a broken stderr the counter claimed a diagnostic had reached the operator
|
|
174
|
+
// when nothing had — a seam documented as "written" reporting something else.
|
|
175
|
+
_unusableInputEmissions += 1;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* a closed or broken stderr must never escalate a degraded read into a crash */
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
// ─── Test seams ───────────────────────────────────────────────────────────────
|
|
183
|
+
/**
|
|
184
|
+
* Clear the dedup state between cases.
|
|
185
|
+
*
|
|
186
|
+
* This exists because the set is process-global: without it, the second test to use a key
|
|
187
|
+
* silently observes the first test's suppression. #2674 is the cautionary precedent — a
|
|
188
|
+
* reset helper that cleared two of three sets was a silent no-op for the very suite that
|
|
189
|
+
* existed to test it, and the cases only passed because each happened to pick a key no
|
|
190
|
+
* other case reused.
|
|
191
|
+
*/
|
|
192
|
+
function _resetUnusableInputWarningsForTests() {
|
|
193
|
+
_warnedUnusableInputs.clear();
|
|
194
|
+
_unusableInputEmissions = 0;
|
|
195
|
+
}
|
|
196
|
+
/** Number of diagnostics written — the typed surface tests assert on instead of stderr prose. */
|
|
197
|
+
function _unusableInputEmissionCountForTests() {
|
|
198
|
+
return _unusableInputEmissions;
|
|
199
|
+
}
|
|
200
|
+
/** Size of the dedup set (identities interned, not events). Retained for key-shape assertions. */
|
|
201
|
+
function _unusableInputWarningCountForTests() {
|
|
202
|
+
return _warnedUnusableInputs.size;
|
|
203
|
+
}
|
|
204
|
+
/** Test seam: the sanitized form of a source, so control-char handling is asserted on a
|
|
205
|
+
* returned value instead of by scraping what reached stderr. */
|
|
206
|
+
function _sanitizeSourceForTests(source) {
|
|
207
|
+
return sanitizeSource(source);
|
|
208
|
+
}
|
|
209
|
+
module.exports = {
|
|
210
|
+
UNUSABLE_REASON,
|
|
211
|
+
_sanitizeSourceForTests,
|
|
212
|
+
warnUnusableInput,
|
|
213
|
+
_resetUnusableInputWarningsForTests,
|
|
214
|
+
_unusableInputWarningCountForTests,
|
|
215
|
+
_unusableInputEmissionCountForTests,
|
|
216
|
+
};
|
|
@@ -37,6 +37,7 @@ exports.canonicalPlanStem = canonicalPlanStem;
|
|
|
37
37
|
exports.phaseVariants = phaseVariants;
|
|
38
38
|
exports.buildRoadmapPhaseVariants = buildRoadmapPhaseVariants;
|
|
39
39
|
exports.buildNotStartedPhaseVariants = buildNotStartedPhaseVariants;
|
|
40
|
+
exports.textEncodingError = textEncodingError;
|
|
40
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
42
|
const phaseIdMod = require("./phase-id.cjs");
|
|
42
43
|
const { OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, PHASE_CONTINUATION_SEGMENT_SOURCE, } = phaseIdMod;
|
|
@@ -137,3 +138,34 @@ function buildNotStartedPhaseVariants(roadmapContent) {
|
|
|
137
138
|
}
|
|
138
139
|
return notStartedPhases;
|
|
139
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Detect binary corruption (embedded NUL bytes) in a text artifact's bytes.
|
|
143
|
+
*
|
|
144
|
+
* #2701: the plan/summary/verification/state validators must FAIL LOUD on a
|
|
145
|
+
* NUL-corrupted file instead of reporting `valid: true`. A NUL byte is the
|
|
146
|
+
* unambiguous signal — UTF-8 text never contains 0x00 — and a file carrying one
|
|
147
|
+
* is binary-classified by `file(1)`, then silently OMITTED from recursive /
|
|
148
|
+
* binary-skipping search results (`rg -l`, `grep -rI`, exit 0), so the corruption
|
|
149
|
+
* reads downstream as "file absent" rather than "file corrupt." The error message
|
|
150
|
+
* names that consequence so the next investigator is not misdirected.
|
|
151
|
+
*
|
|
152
|
+
* This is a pure, opt-in check called explicitly by each validator at its own
|
|
153
|
+
* entry point. It is deliberately NOT placed inside the shared `platformReadSync`
|
|
154
|
+
* read primitive (which dozens of best-effort, tolerant reads flow through and
|
|
155
|
+
* which must not start hard-failing on encoding). It does NOT strip, sanitize, or
|
|
156
|
+
* repair the NUL bytes — corruption is a signal of an upstream authoring-tool bug
|
|
157
|
+
* and must stay visible.
|
|
158
|
+
*
|
|
159
|
+
* @param buf the file bytes (Buffer or string; a string is searched char-wise)
|
|
160
|
+
* @param relPath a path/label for the diagnostic message
|
|
161
|
+
* @returns an error string when NUL is found, or `null` when the bytes are clean text
|
|
162
|
+
*/
|
|
163
|
+
function textEncodingError(buf, relPath) {
|
|
164
|
+
const nul = typeof buf === 'string' ? buf.indexOf('\0') : buf.indexOf(0x00);
|
|
165
|
+
if (nul === -1)
|
|
166
|
+
return null;
|
|
167
|
+
return (`${relPath}: file contains NUL bytes (first at offset ${nul}). ` +
|
|
168
|
+
'Artifact files must be UTF-8 text. A NUL-corrupted file is binary-classified ' +
|
|
169
|
+
'and silently skipped by recursive / binary-skipping search tools (rg, grep -I), ' +
|
|
170
|
+
'so downstream verification reports its contents as missing rather than corrupt.');
|
|
171
|
+
}
|
|
@@ -41,6 +41,7 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
42
42
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
43
43
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
44
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
44
45
|
const { output, error } = io;
|
|
45
46
|
const { extractPhaseToken } = phaseId;
|
|
46
47
|
const { extractFrontmatter } = frontmatterMod;
|
|
@@ -61,6 +62,12 @@ const VERIFIER_STATUSES = ['passed', 'gaps_found', 'human_needed'];
|
|
|
61
62
|
*
|
|
62
63
|
* For 'gaps_found', next_command is built at call time in readVerificationStatus
|
|
63
64
|
* by substituting the phase number — it is NOT stored as a function in the table.
|
|
65
|
+
*
|
|
66
|
+
* #2617: `next_command` here holds a BARE command name (`execute-phase`), never a
|
|
67
|
+
* prefixed one. Every return path projects it through `formatGsdSlash` with the
|
|
68
|
+
* caller's runtime, so Codex sees `$gsd-execute-phase` and slash-hyphen runtimes
|
|
69
|
+
* see `/gsd-execute-phase`. Storing a prefixed literal is what leaked the
|
|
70
|
+
* hard-coded (and deprecated) `/gsd:` colon form to every runtime.
|
|
64
71
|
*/
|
|
65
72
|
const VERIFICATION_ROUTING_TABLE = {
|
|
66
73
|
passed: {
|
|
@@ -77,7 +84,12 @@ const VERIFICATION_ROUTING_TABLE = {
|
|
|
77
84
|
human_needed: {
|
|
78
85
|
status: 'human_needed',
|
|
79
86
|
next_action: "Human verification required. Complete the manual tests in the phase's *-UAT.md, then re-run the verify step until status is passed.",
|
|
80
|
-
|
|
87
|
+
// #2617: was '' — next_action told the user to "re-run the verify step" but
|
|
88
|
+
// named no command, while init.cts's parallel projector emitted
|
|
89
|
+
// `verify-work <N>` for this same state. The two surfaces disagreed on
|
|
90
|
+
// whether a next command existed at all; init's answer was the useful one,
|
|
91
|
+
// and init now delegates here rather than re-deriving it.
|
|
92
|
+
next_command: 'verify-work',
|
|
81
93
|
},
|
|
82
94
|
stale: {
|
|
83
95
|
status: 'stale',
|
|
@@ -89,16 +101,30 @@ const VERIFICATION_ROUTING_TABLE = {
|
|
|
89
101
|
missing: {
|
|
90
102
|
status: 'missing',
|
|
91
103
|
next_action: 'No verification report found — the verify step never completed. Re-run execute-phase.',
|
|
92
|
-
next_command: '
|
|
104
|
+
next_command: 'execute-phase',
|
|
93
105
|
},
|
|
94
106
|
// INTERNAL SENTINEL: constructed when the file has a status value not in
|
|
95
107
|
// VERIFIER_STATUSES. Never emitted by the verifier.
|
|
96
108
|
unknown: {
|
|
97
109
|
status: 'unknown',
|
|
98
110
|
next_action: '', // filled in dynamically with the raw value
|
|
99
|
-
next_command: '
|
|
111
|
+
next_command: 'execute-phase',
|
|
100
112
|
},
|
|
101
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Project a BARE command name (plus optional argument tail) into the surface the
|
|
116
|
+
* given runtime actually installs (#2617).
|
|
117
|
+
*
|
|
118
|
+
* `formatGsdSlash` owns the per-runtime shape (`$gsd-<cmd>` for shell-var
|
|
119
|
+
* runtimes like Codex, `/gsd-<cmd>` otherwise) and is idempotent, so passing an
|
|
120
|
+
* already-prefixed string is safe. An empty command stays empty — "no next
|
|
121
|
+
* command" must not become a bare prefix.
|
|
122
|
+
*/
|
|
123
|
+
function projectNextCommand(bare, runtime, tail = '') {
|
|
124
|
+
if (!bare)
|
|
125
|
+
return '';
|
|
126
|
+
return `${(0, runtime_slash_cjs_1.formatGsdSlash)(bare, runtime)}${tail}`;
|
|
127
|
+
}
|
|
102
128
|
/** Normalize separators to posix (git emits `/`; callers may pass `\` on Windows). */
|
|
103
129
|
function toPosix(p) {
|
|
104
130
|
return p.replace(/\\/g, '/');
|
|
@@ -177,12 +203,12 @@ function defaultPhaseCleanCommitTimesMs(phaseDir, files, execGitFn = shell_comma
|
|
|
177
203
|
* Used for two early-return paths: no *-VERIFICATION.md file found, and
|
|
178
204
|
* file present but no parseable frontmatter status.
|
|
179
205
|
*/
|
|
180
|
-
function missingResult() {
|
|
206
|
+
function missingResult(runtime, phaseArg) {
|
|
181
207
|
const route = VERIFICATION_ROUTING_TABLE['missing'];
|
|
182
208
|
return {
|
|
183
209
|
status: route.status,
|
|
184
210
|
next_action: route.next_action,
|
|
185
|
-
next_command: route.next_command,
|
|
211
|
+
next_command: projectNextCommand(route.next_command, runtime, phaseArg),
|
|
186
212
|
};
|
|
187
213
|
}
|
|
188
214
|
function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phaseCleanCommitTimesMs = defaultPhaseCleanCommitTimesMs) {
|
|
@@ -241,14 +267,25 @@ function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phas
|
|
|
241
267
|
*
|
|
242
268
|
* @param phaseDir - Absolute path to the phase directory.
|
|
243
269
|
* @param opts - Options. `opts.fs` allows test injection (defaults to node:fs).
|
|
270
|
+
* `opts.runtime` selects the command surface `next_command` is
|
|
271
|
+
* projected into (#2617).
|
|
244
272
|
*/
|
|
245
273
|
function readVerificationStatus(phaseDir, opts = {}) {
|
|
246
274
|
const fsImpl = opts.fs ?? node_fs_1.default;
|
|
247
275
|
const phaseCleanCommitTimesMs = opts.phaseCleanCommitTimesMs ?? defaultPhaseCleanCommitTimesMs;
|
|
276
|
+
const runtime = opts.runtime ?? 'claude';
|
|
248
277
|
// Phase token for the gaps_found command
|
|
249
278
|
const baseName = node_path_1.default.basename(phaseDir);
|
|
250
279
|
const phaseToken = extractPhaseToken(baseName);
|
|
251
|
-
const
|
|
280
|
+
const derivedPhaseNumber = phaseToken.length > 0 ? phaseToken : baseName;
|
|
281
|
+
// #2617: the phase number becomes a COMMAND ARGUMENT, so it is appended only
|
|
282
|
+
// when it is unambiguously one. extractPhaseToken also returns project-code
|
|
283
|
+
// forms (`PROJ-07`), which are indistinguishable by shape from an ordinary
|
|
284
|
+
// directory name — `gsd-651-parent` yields `gsd-651` — and emitting
|
|
285
|
+
// `execute-phase gsd-651` is worse than emitting no argument at all. Callers
|
|
286
|
+
// that already know the number (init) pass it explicitly and always get it.
|
|
287
|
+
const phaseArgSource = opts.phaseNumber ?? (/^\d+(\.\d+)*$/.test(derivedPhaseNumber) ? derivedPhaseNumber : '');
|
|
288
|
+
const phaseArg = phaseArgSource ? ` ${phaseArgSource}` : '';
|
|
252
289
|
// 1. Find *-VERIFICATION.md
|
|
253
290
|
let verificationFile = null;
|
|
254
291
|
try {
|
|
@@ -261,7 +298,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
261
298
|
verificationFile = null;
|
|
262
299
|
}
|
|
263
300
|
if (!verificationFile) {
|
|
264
|
-
return missingResult();
|
|
301
|
+
return missingResult(runtime, phaseArg);
|
|
265
302
|
}
|
|
266
303
|
// 2. Read and parse frontmatter using the shared parser.
|
|
267
304
|
// extractFrontmatter anchors at byte 0, so body `status:` lines are ignored.
|
|
@@ -269,7 +306,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
269
306
|
let rawStatus = null;
|
|
270
307
|
try {
|
|
271
308
|
const content = fsImpl.readFileSync(filePath, 'utf-8');
|
|
272
|
-
const fm = extractFrontmatter(content);
|
|
309
|
+
const fm = extractFrontmatter(content, filePath);
|
|
273
310
|
const statusVal = fm['status'];
|
|
274
311
|
// status is always a scalar string in a well-formed VERIFICATION.md frontmatter;
|
|
275
312
|
// only accept string values — arrays and objects are not valid status values.
|
|
@@ -282,7 +319,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
282
319
|
rawStatus = null;
|
|
283
320
|
}
|
|
284
321
|
if (!rawStatus) {
|
|
285
|
-
return missingResult();
|
|
322
|
+
return missingResult(runtime, phaseArg);
|
|
286
323
|
}
|
|
287
324
|
// gaps_found takes priority over stale — gap closure is the correct next
|
|
288
325
|
// step regardless of whether summaries are newer than the verification file.
|
|
@@ -291,7 +328,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
291
328
|
return {
|
|
292
329
|
status: entry.status,
|
|
293
330
|
next_action: entry.next_action,
|
|
294
|
-
next_command:
|
|
331
|
+
next_command: projectNextCommand('plan-phase', runtime, `${phaseArg} --gaps`),
|
|
295
332
|
};
|
|
296
333
|
}
|
|
297
334
|
const staleVerification = findStaleVerificationSummary(phaseDir, fsImpl, phaseCleanCommitTimesMs);
|
|
@@ -300,7 +337,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
300
337
|
return {
|
|
301
338
|
status: entry.status,
|
|
302
339
|
next_action: entry.next_action,
|
|
303
|
-
next_command:
|
|
340
|
+
next_command: projectNextCommand('verify-work', runtime, phaseArg),
|
|
304
341
|
};
|
|
305
342
|
}
|
|
306
343
|
// 3. Route — exclude internal sentinels from raw-file lookup (they are
|
|
@@ -314,7 +351,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
314
351
|
return {
|
|
315
352
|
status: entry.status,
|
|
316
353
|
next_action: entry.next_action,
|
|
317
|
-
next_command: entry.next_command,
|
|
354
|
+
next_command: projectNextCommand(entry.next_command, runtime, phaseArg),
|
|
318
355
|
};
|
|
319
356
|
}
|
|
320
357
|
// Unknown value
|
|
@@ -322,7 +359,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
322
359
|
return {
|
|
323
360
|
status: unknownRoute.status,
|
|
324
361
|
next_action: `Unexpected verification status '${rawStatus}'. Re-run execute-phase verification.`,
|
|
325
|
-
next_command: unknownRoute.next_command,
|
|
362
|
+
next_command: projectNextCommand(unknownRoute.next_command, runtime, phaseArg),
|
|
326
363
|
};
|
|
327
364
|
}
|
|
328
365
|
/**
|
|
@@ -339,7 +376,7 @@ function cmdVerificationStatus(cwd, phaseDirArg, raw) {
|
|
|
339
376
|
return;
|
|
340
377
|
}
|
|
341
378
|
const phaseDir = node_path_1.default.resolve(cwd, phaseDirArg);
|
|
342
|
-
const result = readVerificationStatus(phaseDir);
|
|
379
|
+
const result = readVerificationStatus(phaseDir, { runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd) });
|
|
343
380
|
output(result, raw);
|
|
344
381
|
}
|
|
345
382
|
module.exports = {
|