@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.4
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/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-writer.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-mempalace-curator.md +47 -0
- package/agents/gsd-nyquist-auditor.md +1 -0
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.md +55 -2
- package/agents/gsd-security-auditor.md +1 -0
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +13 -2
- package/bin/install.js +61 -64
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/commands/gsd/progress.md +2 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +277 -95
- package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +128 -21
- package/gsd-core/bin/lib/capability-writer.cjs +354 -0
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +81 -26
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/edge-probe.cjs +25 -2
- package/gsd-core/bin/lib/frontmatter.cjs +53 -1
- package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
- package/gsd-core/bin/lib/init.cjs +36 -11
- package/gsd-core/bin/lib/install-profiles.cjs +57 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +99 -23
- package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
- package/gsd-core/bin/lib/probe-core.cjs +117 -1
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1946 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +54 -30
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +461 -94
- package/gsd-core/bin/lib/verify.cjs +92 -8
- package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/edge-probe.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
- package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
- package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
- package/gsd-core/references/prohibition-probe.md +248 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/templates/spec.md +14 -0
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/complete-milestone.md +1 -5
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +86 -146
- package/gsd-core/workflows/execute-plan.md +21 -6
- package/gsd-core/workflows/help/modes/full.md +7 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/next.md +50 -2
- package/gsd-core/workflows/pause-work.md +7 -1
- package/gsd-core/workflows/plan-phase.md +91 -221
- package/gsd-core/workflows/plan-review-convergence.md +14 -4
- package/gsd-core/workflows/pr-branch.md +4 -2
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +12 -8
- package/gsd-core/workflows/resume-project.md +17 -1
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings.md +27 -1
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/spec-phase.md +75 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-phase.md +14 -4
- package/gsd-core/workflows/verify-work.md +27 -11
- package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
- package/hooks/dist/gsd-statusline.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-ensure-canonical-path.js +305 -0
- package/hooks/gsd-statusline.js +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -4
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/new.cjs +17 -3
- package/scripts/fix-slash-commands.cjs +15 -3
- package/scripts/gen-capability-registry.cjs +373 -49
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +327 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +14 -0
- package/scripts/mutation-matrix.cjs +108 -7
- package/scripts/pr-target-policy.cjs +63 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- package/scripts/research-profiles.cjs +5 -5
- package/scripts/run-tests.cjs +178 -17
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ADR-22 Drift-Guard Decision Module
|
|
4
|
+
*
|
|
5
|
+
* Implements the authority ladder and severity classification table from
|
|
6
|
+
* ADR-22 (docs/adr/0022-source-grounding-drift-guard.md).
|
|
7
|
+
*
|
|
8
|
+
* Design constraints:
|
|
9
|
+
* - Pure module: no I/O, no require() calls, no side effects.
|
|
10
|
+
* - All inputs are validated; unknown values throw a TypeError.
|
|
11
|
+
* - Consumed by the `gsd-tools drift-guard` CLI seam and by tests.
|
|
12
|
+
*
|
|
13
|
+
* Authority ladder (rung values determine MISSING severity):
|
|
14
|
+
* grep=0 intel=1 treesitter=2 lsp=3 scip=4
|
|
15
|
+
*
|
|
16
|
+
* Hard-block threshold: rung >= 3 (lsp, scip) — these adapters can prove
|
|
17
|
+
* absence, so MISSING is a definite error (severity HIGH, hardBlock true).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.AUTHORITY_RUNGS = void 0;
|
|
21
|
+
exports.getEffectiveAuthority = getEffectiveAuthority;
|
|
22
|
+
exports.classifyDriftSeverity = classifyDriftSeverity;
|
|
23
|
+
/**
|
|
24
|
+
* Frozen map from authority name to its rung number.
|
|
25
|
+
*
|
|
26
|
+
* Rung determines whether a MISSING symbol triggers a hard block:
|
|
27
|
+
* rung >= 3 (lsp, scip) → hard block; rung < 3 → acknowledgement only.
|
|
28
|
+
*/
|
|
29
|
+
exports.AUTHORITY_RUNGS = Object.freeze({
|
|
30
|
+
grep: 0,
|
|
31
|
+
intel: 1,
|
|
32
|
+
treesitter: 2,
|
|
33
|
+
lsp: 3,
|
|
34
|
+
scip: 4,
|
|
35
|
+
});
|
|
36
|
+
/** Rung at which MISSING transitions to hard-block (inclusive). */
|
|
37
|
+
const HARD_BLOCK_RUNG_THRESHOLD = 3;
|
|
38
|
+
const VALID_AUTHORITIES = new Set(Object.keys(exports.AUTHORITY_RUNGS));
|
|
39
|
+
const VALID_STATUSES = new Set(['VERIFIED', 'MISSING', 'AMBIGUOUS', 'UNCHECKABLE']);
|
|
40
|
+
/**
|
|
41
|
+
* Validate and return an authority value, normalising undefined to 'grep'.
|
|
42
|
+
*
|
|
43
|
+
* Throws TypeError for any non-null unknown string value so callers surface
|
|
44
|
+
* configuration errors at call time rather than silently defaulting.
|
|
45
|
+
*
|
|
46
|
+
* @param value - raw authority string from config or CLI arg
|
|
47
|
+
* @returns a validated Authority value
|
|
48
|
+
*/
|
|
49
|
+
function validateAuthority(value) {
|
|
50
|
+
if (value === undefined || value === null || value === '') {
|
|
51
|
+
return 'grep';
|
|
52
|
+
}
|
|
53
|
+
if (!VALID_AUTHORITIES.has(value)) {
|
|
54
|
+
throw new TypeError(`Unknown authority: ${JSON.stringify(value)}. ` +
|
|
55
|
+
`Valid values: ${[...VALID_AUTHORITIES].join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Return the effective authority after applying the ADR-22 auto-upgrade rule.
|
|
61
|
+
*
|
|
62
|
+
* Auto-upgrade rule: if the configured authority is 'grep' AND intel is
|
|
63
|
+
* enabled (`intelEnabled === true`), upgrade to 'intel'. All other authority
|
|
64
|
+
* values are returned unchanged regardless of intelEnabled.
|
|
65
|
+
*
|
|
66
|
+
* @param authority - configured authority (undefined → 'grep')
|
|
67
|
+
* @param intelEnabled - whether the intel capability is active in this project
|
|
68
|
+
* @returns the effective Authority after upgrade
|
|
69
|
+
* @throws TypeError if authority is not one of the five valid values
|
|
70
|
+
*/
|
|
71
|
+
function getEffectiveAuthority(authority, intelEnabled) {
|
|
72
|
+
const validated = validateAuthority(authority);
|
|
73
|
+
if (validated === 'grep' && intelEnabled === true) {
|
|
74
|
+
return 'intel';
|
|
75
|
+
}
|
|
76
|
+
return validated;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Classify a symbol verification result into a drift severity and hard-block flag.
|
|
80
|
+
*
|
|
81
|
+
* ADR-22 decision table:
|
|
82
|
+
*
|
|
83
|
+
* | Status | Authority rung | severity | hardBlock |
|
|
84
|
+
* |------------- |--------------- |----------------------- |---------- |
|
|
85
|
+
* | VERIFIED | any | 'none' | false |
|
|
86
|
+
* | MISSING | rung >= 3 | 'HIGH' | true |
|
|
87
|
+
* | MISSING | rung 0-2 | 'needs-acknowledgement'| false |
|
|
88
|
+
* | AMBIGUOUS | any | 'MEDIUM' | false |
|
|
89
|
+
* | UNCHECKABLE | any | 'INFO' | false |
|
|
90
|
+
*
|
|
91
|
+
* @param opts.status - verdict from the source-grounding adapter
|
|
92
|
+
* @param opts.authority - the effective authority adapter used
|
|
93
|
+
* @returns { severity, hardBlock }
|
|
94
|
+
* @throws TypeError for unknown status or authority values
|
|
95
|
+
*/
|
|
96
|
+
function classifyDriftSeverity({ status, authority, }) {
|
|
97
|
+
if (!VALID_STATUSES.has(status)) {
|
|
98
|
+
throw new TypeError(`Unknown status: ${JSON.stringify(status)}. ` +
|
|
99
|
+
`Valid values: ${[...VALID_STATUSES].join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
// authority validation (also catches unknown values)
|
|
102
|
+
const validatedAuthority = validateAuthority(authority);
|
|
103
|
+
const rung = exports.AUTHORITY_RUNGS[validatedAuthority];
|
|
104
|
+
switch (status) {
|
|
105
|
+
case 'VERIFIED':
|
|
106
|
+
return { severity: 'none', hardBlock: false };
|
|
107
|
+
case 'MISSING':
|
|
108
|
+
if (rung >= HARD_BLOCK_RUNG_THRESHOLD) {
|
|
109
|
+
return { severity: 'HIGH', hardBlock: true };
|
|
110
|
+
}
|
|
111
|
+
return { severity: 'needs-acknowledgement', hardBlock: false };
|
|
112
|
+
case 'AMBIGUOUS':
|
|
113
|
+
return { severity: 'MEDIUM', hardBlock: false };
|
|
114
|
+
case 'UNCHECKABLE':
|
|
115
|
+
return { severity: 'INFO', hardBlock: false };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -30,10 +30,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
30
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
31
|
};
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.VALID_STATUS = void 0;
|
|
33
|
+
exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
|
|
34
34
|
exports.validateRequirement = validateRequirement;
|
|
35
35
|
exports.validateResolution = validateResolution;
|
|
36
36
|
exports.analyzeCoverage = analyzeCoverage;
|
|
37
|
+
exports.validateProhibitionResolution = validateProhibitionResolution;
|
|
38
|
+
exports.projectProhibitions = projectProhibitions;
|
|
39
|
+
exports.dispositionForProhibition = dispositionForProhibition;
|
|
37
40
|
exports.runProbeCli = runProbeCli;
|
|
38
41
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
42
|
/** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
|
|
@@ -203,6 +206,119 @@ function analyzeCoverage(items, resolutions = [], validators) {
|
|
|
203
206
|
}
|
|
204
207
|
return { items: merged, coverage: { applicable, resolved, unresolved, byVerification } };
|
|
205
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The prohibition adapter's injected runtime validators (ADR-550 #5). There is no closed
|
|
211
|
+
* category taxonomy (recall is open-vocabulary values/safety/ethics prose), so `categories`
|
|
212
|
+
* is intentionally empty — `analyzeCoverage` is not the prohibition entry point and the
|
|
213
|
+
* round-trip schema layer does not gate on category. The verification tiers are
|
|
214
|
+
* `test | judgment` (ADR-550 D7a); both require only a present `resolution`/`reason` per their
|
|
215
|
+
* lifecycle (a resolved prohibition's checkable content is the `statement`, validated by the
|
|
216
|
+
* schema layer, not a `resolution` string), so `requiredFieldsByVerification` is the minimal
|
|
217
|
+
* fail-closed set: a dismissed item still needs its reason (enforced by `validateResolution`).
|
|
218
|
+
*/
|
|
219
|
+
exports.PROHIBITION_VALIDATORS = {
|
|
220
|
+
categories: [],
|
|
221
|
+
verification: ['test', 'judgment'],
|
|
222
|
+
// A resolved prohibition's checkable content is the `statement` (schema-layer validated), NOT a
|
|
223
|
+
// `resolution` string — the canonical fixtures and the reference doc's worked examples all carry
|
|
224
|
+
// `resolution: null`. So the per-tier required set is empty: `resolved` still requires a present
|
|
225
|
+
// verification tier (enforced in validateResolution) and `dismissed` still requires a reason
|
|
226
|
+
// (enforced unconditionally), but neither tier requires a `resolution`. This matches the corpus
|
|
227
|
+
// the docs-fixtures parity test pins; the validators.test.cjs regression keeps them aligned.
|
|
228
|
+
requiredFieldsByVerification: { test: [], judgment: [] },
|
|
229
|
+
};
|
|
230
|
+
/** Validate a prohibition resolution against the prohibition verification vocabulary. */
|
|
231
|
+
function validateProhibitionResolution(resolution) {
|
|
232
|
+
return validateResolution(resolution, exports.PROHIBITION_VALIDATORS);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Deterministically project resolved prohibition items into the `must_haves.prohibitions:`
|
|
236
|
+
* list shape (the SPEC<->plan projection; ADR-550 Decision 5c). This is a FUNCTION the parity
|
|
237
|
+
* assertion round-trips, never a prompt: the same input always yields the same output, and the
|
|
238
|
+
* output is the exact re-readable block shape `parseMustHavesBlock(content, 'prohibitions')`
|
|
239
|
+
* returns — `{ statement, status, verification }` plus `reason` only when present (a dismissed
|
|
240
|
+
* item's audit trail). `resolution`/`requirement_id`/`category` are recall-stage bookkeeping
|
|
241
|
+
* and are intentionally NOT projected into the plan block (which is keyed on the must-NOT
|
|
242
|
+
* statement, not the source requirement). A non-array input projects to `[]` (fail-soft on the
|
|
243
|
+
* empty/zero-prohibition case), never a throw.
|
|
244
|
+
*/
|
|
245
|
+
function projectProhibitions(items) {
|
|
246
|
+
if (!Array.isArray(items))
|
|
247
|
+
return [];
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const item of items) {
|
|
250
|
+
if (item == null || typeof item !== 'object')
|
|
251
|
+
continue;
|
|
252
|
+
const p = item;
|
|
253
|
+
const statement = typeof p.statement === 'string' ? p.statement : '';
|
|
254
|
+
const entry = {
|
|
255
|
+
statement,
|
|
256
|
+
status: typeof p.status === 'string' ? p.status : 'unresolved',
|
|
257
|
+
};
|
|
258
|
+
if (p.verification != null)
|
|
259
|
+
entry.verification = String(p.verification);
|
|
260
|
+
if (p.reason != null && String(p.reason).trim())
|
|
261
|
+
entry.reason = String(p.reason);
|
|
262
|
+
out.push(entry);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Deterministic verify-time disposition for a single prohibition — the FAIL-CLOSED default
|
|
268
|
+
* (ADR-550 Decision 5d, the safety half of the 2026-06-12 "B-with-guard" maintainer decision).
|
|
269
|
+
*
|
|
270
|
+
* This is the cheap safety guarantee: a well-formed prohibition that reaches verify-phase with NO
|
|
271
|
+
* wired enforcement evidence can NEVER be a silent pass. It is `{ status: 'unverified', flagged:
|
|
272
|
+
* true }` — never `green` — exactly like an unresolved judgment item. The HEAVY half (a real
|
|
273
|
+
* fail-first negative-test enforcement mechanism that, given evidence, would flip a test-tier item
|
|
274
|
+
* to green) is OUT of #644 scope and defers to a follow-up PR: #644's corpus is entirely
|
|
275
|
+
* judgment-tier, so wiring a contrived test-tier consumer here would be the delete-bad-tests /
|
|
276
|
+
* gold-plating failure mode. Until that follow-up lands, ANY prohibition without enforcement
|
|
277
|
+
* evidence — test- or judgment-tier — disposes as flagged-unverified.
|
|
278
|
+
*
|
|
279
|
+
* The function is pure: same input always yields the same disposition (no LLM judgment, ADR-550
|
|
280
|
+
* D5). The LLM-judge soft-gate for judgment-tier items is a verify-phase PROSE concern (the
|
|
281
|
+
* verifier records a non-authoritative verdict + the unverified-prohibition flag); this helper
|
|
282
|
+
* only owns the deterministic fail-closed default that the plan-01-01 CI safety assertion pins.
|
|
283
|
+
*/
|
|
284
|
+
function dispositionForProhibition(prohibition, context = {}) {
|
|
285
|
+
const p = (prohibition ?? {});
|
|
286
|
+
const tier = p.verification === 'test' || p.verification === 'judgment' ? p.verification : null;
|
|
287
|
+
const evidence = Array.isArray(context.enforcementEvidence) ? context.enforcementEvidence : [];
|
|
288
|
+
const hasEnforcement = evidence.length > 0;
|
|
289
|
+
// FAIL CLOSED: no wired enforcement evidence -> flagged unverified, never green. This holds for
|
|
290
|
+
// every tier today (the real enforcement mechanism that could flip a test-tier item to green is
|
|
291
|
+
// deferred to a follow-up PR). The guard the safety assertion proves: an unwired item can never
|
|
292
|
+
// be silently skipped.
|
|
293
|
+
if (!hasEnforcement) {
|
|
294
|
+
return {
|
|
295
|
+
status: 'unverified',
|
|
296
|
+
flagged: true,
|
|
297
|
+
tier,
|
|
298
|
+
reason: tier === 'test'
|
|
299
|
+
? 'test-tier prohibition has no wired enforcement evidence — flagged unverified (fail-closed; real negative-test enforcement deferred to a follow-up PR, ADR-550 D5d)'
|
|
300
|
+
: 'prohibition has no enforcement evidence — flagged unverified (fail-closed; never a silent pass, ADR-550 D5d)',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// D4 GUARD: a judgment-tier (or unknown-tier) prohibition is NEVER a silent green from this
|
|
304
|
+
// deterministic helper — it always routes to human/LLM judgment review (ADR-550 D4; verify-phase.md).
|
|
305
|
+
// Only a test-tier item with wired enforcement evidence may go green, and even that is the deferred
|
|
306
|
+
// heavy half until the real negative-test enforcement mechanism lands (no #644 caller passes evidence).
|
|
307
|
+
if (tier === 'test') {
|
|
308
|
+
return {
|
|
309
|
+
status: 'green',
|
|
310
|
+
flagged: false,
|
|
311
|
+
tier,
|
|
312
|
+
reason: 'test-tier prohibition has wired enforcement evidence',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
status: 'unverified',
|
|
317
|
+
flagged: true,
|
|
318
|
+
tier,
|
|
319
|
+
reason: 'judgment-tier prohibition routes to judgment review — never a silent green (ADR-550 D4)',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
206
322
|
/**
|
|
207
323
|
* Read the requirements file (and optional resolutions file), run the adapter's `analyze`,
|
|
208
324
|
* and write the report as pretty JSON + newline. With no requirements path, writes the usage
|
|
@@ -653,7 +653,21 @@ function cmdWriteProfile(cwd, options, raw) {
|
|
|
653
653
|
}
|
|
654
654
|
let outputPath = options.output;
|
|
655
655
|
if (!outputPath) {
|
|
656
|
-
|
|
656
|
+
// #1114: resolve the ACTIVE runtime's config home so the profile is written
|
|
657
|
+
// where the runtime's own workflows look for it. Previously this hardcoded the
|
|
658
|
+
// Claude home, so a Codex run wrote the profile under the Claude config dir
|
|
659
|
+
// while Codex advisor-mode (installed under the Codex home) checked the Codex
|
|
660
|
+
// dir and never found it. Mirrors cmdGenerateDevPreferences' runtime resolution.
|
|
661
|
+
let effectiveRuntime = 'claude';
|
|
662
|
+
try {
|
|
663
|
+
const config = loadConfig(cwd);
|
|
664
|
+
effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime'], 'claude') || 'claude';
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], 'claude') || 'claude';
|
|
668
|
+
}
|
|
669
|
+
// path.join (not a string literal) keeps the cline-install leaked-path lint quiet.
|
|
670
|
+
outputPath = node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)(effectiveRuntime), 'gsd-core', 'USER-PROFILE.md');
|
|
657
671
|
}
|
|
658
672
|
else if (!node_path_1.default.isAbsolute(outputPath)) {
|
|
659
673
|
outputPath = node_path_1.default.join(cwd, outputPath);
|
|
@@ -911,8 +925,10 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
|
|
|
911
925
|
targetPath = node_path_1.default.isAbsolute(options.output) ? options.output : node_path_1.default.join(cwd, options.output);
|
|
912
926
|
}
|
|
913
927
|
else {
|
|
914
|
-
// Read claude_md_path from config
|
|
915
|
-
|
|
928
|
+
// Read claude_md_path from config; #1098 default is ./.claude/CLAUDE.md
|
|
929
|
+
// (kept consistent with cmdGenerateClaudeMd so the profile section and the
|
|
930
|
+
// managed sections land in the same file on a config-less project).
|
|
931
|
+
let configClaudeMdPath = './.claude/CLAUDE.md';
|
|
916
932
|
try {
|
|
917
933
|
const config = loadConfig(cwd);
|
|
918
934
|
if (config['claude_md_path'])
|
|
@@ -986,7 +1002,12 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
986
1002
|
}
|
|
987
1003
|
}
|
|
988
1004
|
let assemblyConfig = {};
|
|
989
|
-
|
|
1005
|
+
// #1098: default the Claude-family instruction file to the project-scoped
|
|
1006
|
+
// `.claude/CLAUDE.md` (a valid auto-loaded memory location) rather than a
|
|
1007
|
+
// repo-root `CLAUDE.md`, so generated GSD content does not land next to — or
|
|
1008
|
+
// pollute — a hand-crafted repo-root CLAUDE.md. An explicit `claude_md_path`
|
|
1009
|
+
// config value or `--output` still wins.
|
|
1010
|
+
let configClaudeMdPath = './.claude/CLAUDE.md';
|
|
990
1011
|
try {
|
|
991
1012
|
const config = loadConfig(cwd);
|
|
992
1013
|
if (config['claude_md_path'])
|
|
@@ -1037,6 +1058,26 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
1037
1058
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(node_path_1.default.dirname(outputPath));
|
|
1038
1059
|
(0, shell_command_projection_cjs_1.platformWriteSync)(outputPath, existingContent);
|
|
1039
1060
|
}
|
|
1061
|
+
else if (!/<!-- GSD:[a-z]+-start/.test(existingContent) && !options.force) {
|
|
1062
|
+
// #1098: the target instruction file already exists and contains NO GSD
|
|
1063
|
+
// section markers — it is a hand-crafted CLAUDE.md/AGENTS.md, not a
|
|
1064
|
+
// GSD-managed one. Do NOT clobber it with generated project documentation
|
|
1065
|
+
// (broad project detail belongs in PROJECT.md / REQUIREMENTS.md, which GSD
|
|
1066
|
+
// already owns). Leave the file untouched and report a skip; `--force`
|
|
1067
|
+
// overwrites intentionally.
|
|
1068
|
+
output({
|
|
1069
|
+
claude_md_path: outputPath,
|
|
1070
|
+
action: 'skipped',
|
|
1071
|
+
reason: 'existing instruction file has no GSD markers (hand-crafted); not overwriting. Pass --force to overwrite.',
|
|
1072
|
+
sections_generated: [],
|
|
1073
|
+
sections_fallback: [],
|
|
1074
|
+
sections_skipped: MANAGED_SECTIONS,
|
|
1075
|
+
sections_total: MANAGED_SECTIONS.length,
|
|
1076
|
+
profile_status: 'skipped',
|
|
1077
|
+
message: `Left existing ${node_path_1.default.basename(outputPath)} untouched (no GSD markers found). Broad project context lives in PROJECT.md / REQUIREMENTS.md; pass --force to overwrite this file with GSD-managed sections.`,
|
|
1078
|
+
}, raw, undefined);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1040
1081
|
else {
|
|
1041
1082
|
action = 'updated';
|
|
1042
1083
|
let fileContent = existingContent;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Profile-pipeline command router — CLI dispatcher for gsd-tools profiling commands.
|
|
4
|
+
*
|
|
5
|
+
* ADR-857 phase 6 / ADR-959: profile-pipeline capability command cutover.
|
|
6
|
+
* Extracted from hardcoded case arms in gsd-tools.cjs (lines 1324-1410).
|
|
7
|
+
* Dispatch path: default → dispatchCapabilityCommand →
|
|
8
|
+
* require(profile-pipeline-command-router.cjs) → route<X>.
|
|
9
|
+
*
|
|
10
|
+
* Router signature: { args, cwd, raw, error } — identical to existing routers.
|
|
11
|
+
* Test seams: _pipeline / _output inject mock modules; _core injects mock core.
|
|
12
|
+
*
|
|
13
|
+
* Async note: cmdExtractMessages and cmdProfileSample are async functions.
|
|
14
|
+
* dispatchCapabilityCommand (gsd-tools.cjs:366-371) explicitly errors if a
|
|
15
|
+
* router returns a Promise. Therefore these router functions call the async
|
|
16
|
+
* function WITHOUT await and WITHOUT returning the Promise. The async functions
|
|
17
|
+
* end with output() or process.exit() so the process terminates correctly once
|
|
18
|
+
* the event loop drains. Unhandled rejections are caught by the .catch() wrapper
|
|
19
|
+
* to surface errors via the error() callback.
|
|
20
|
+
*/
|
|
21
|
+
const { ERROR_REASON } = require('./io.cjs');
|
|
22
|
+
|
|
23
|
+
// ─── Pipeline phase commands ───────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function routeScanSessions({ args, cwd, raw, error, _pipeline }) {
|
|
26
|
+
void cwd; void error;
|
|
27
|
+
const p = _pipeline ?? require('./profile-pipeline.cjs');
|
|
28
|
+
const pathIdx = args.indexOf('--path');
|
|
29
|
+
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
|
|
30
|
+
const verboseFlag = args.includes('--verbose');
|
|
31
|
+
const jsonFlag = args.includes('--json');
|
|
32
|
+
// cmdScanSessions is synchronous — call directly.
|
|
33
|
+
p.cmdScanSessions(sessionsPath, { verbose: verboseFlag, json: jsonFlag }, raw);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function routeExtractMessages({ args, cwd, raw, error, _pipeline }) {
|
|
37
|
+
const p = _pipeline ?? require('./profile-pipeline.cjs');
|
|
38
|
+
const sessionIdx = args.indexOf('--session');
|
|
39
|
+
const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : null;
|
|
40
|
+
const limitIdx = args.indexOf('--limit');
|
|
41
|
+
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : null;
|
|
42
|
+
const pathIdx = args.indexOf('--path');
|
|
43
|
+
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
|
|
44
|
+
// args[0] = 'extract-messages' (family name), args[1] = project positional
|
|
45
|
+
const projectArg = args[1];
|
|
46
|
+
if (!projectArg || projectArg.startsWith('--')) {
|
|
47
|
+
error('Usage: gsd-tools extract-messages <project> [--session <id>] [--limit N] [--path <dir>]\nRun scan-sessions first to see available projects.', ERROR_REASON.USAGE);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// cmdExtractMessages is async — do NOT return the Promise.
|
|
51
|
+
// The function ends with output() or process.exit(); the event loop will drain.
|
|
52
|
+
void cwd;
|
|
53
|
+
p.cmdExtractMessages(projectArg, { sessionId, limit }, raw, sessionsPath)
|
|
54
|
+
.catch(e => { error(e && e.message ? e.message : String(e)); });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function routeProfileSample({ args, cwd, raw, error, _pipeline }) {
|
|
58
|
+
void cwd; void error;
|
|
59
|
+
const p = _pipeline ?? require('./profile-pipeline.cjs');
|
|
60
|
+
const pathIdx = args.indexOf('--path');
|
|
61
|
+
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
|
|
62
|
+
const limitIdx = args.indexOf('--limit');
|
|
63
|
+
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 150;
|
|
64
|
+
const maxPerIdx = args.indexOf('--max-per-project');
|
|
65
|
+
const maxPerProject = maxPerIdx !== -1 ? parseInt(args[maxPerIdx + 1], 10) : null;
|
|
66
|
+
const maxCharsIdx = args.indexOf('--max-chars');
|
|
67
|
+
const maxChars = maxCharsIdx !== -1 ? parseInt(args[maxCharsIdx + 1], 10) : 500;
|
|
68
|
+
// cmdProfileSample is async — do NOT return the Promise.
|
|
69
|
+
p.cmdProfileSample(sessionsPath, { limit, maxPerProject, maxChars }, raw)
|
|
70
|
+
.catch(e => { error(e && e.message ? e.message : String(e)); });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Output phase commands ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function routeWriteProfile({ args, cwd, raw, error, _output }) {
|
|
76
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
77
|
+
const inputIdx = args.indexOf('--input');
|
|
78
|
+
const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
|
|
79
|
+
if (!inputPath) {
|
|
80
|
+
error('--input <analysis-json-path> is required', ERROR_REASON.USAGE);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const outputIdx = args.indexOf('--output');
|
|
84
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
85
|
+
o.cmdWriteProfile(cwd, { input: inputPath, output: outputPath }, raw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function routeProfileQuestionnaire({ args, cwd, raw, error, _output }) {
|
|
89
|
+
void cwd; void error;
|
|
90
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
91
|
+
const answersIdx = args.indexOf('--answers');
|
|
92
|
+
const answers = answersIdx !== -1 ? args[answersIdx + 1] : null;
|
|
93
|
+
o.cmdProfileQuestionnaire({ answers }, raw);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function routeGenerateDevPreferences({ args, cwd, raw, error, _output }) {
|
|
97
|
+
void error;
|
|
98
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
99
|
+
const analysisIdx = args.indexOf('--analysis');
|
|
100
|
+
const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
|
|
101
|
+
const outputIdx = args.indexOf('--output');
|
|
102
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
103
|
+
const stackIdx = args.indexOf('--stack');
|
|
104
|
+
const stack = stackIdx !== -1 ? args[stackIdx + 1] : null;
|
|
105
|
+
o.cmdGenerateDevPreferences(cwd, { analysis: analysisPath, output: outputPath, stack }, raw);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function routeGenerateClaudeProfile({ args, cwd, raw, error, _output }) {
|
|
109
|
+
void error;
|
|
110
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
111
|
+
const analysisIdx = args.indexOf('--analysis');
|
|
112
|
+
const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
|
|
113
|
+
const outputIdx = args.indexOf('--output');
|
|
114
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
115
|
+
const globalFlag = args.includes('--global');
|
|
116
|
+
o.cmdGenerateClaudeProfile(cwd, { analysis: analysisPath, output: outputPath, global: globalFlag }, raw);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function routeGenerateClaudeMd({ args, cwd, raw, error, _output }) {
|
|
120
|
+
void error;
|
|
121
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
122
|
+
const outputIdx = args.indexOf('--output');
|
|
123
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
124
|
+
const autoFlag = args.includes('--auto');
|
|
125
|
+
const forceFlag = args.includes('--force');
|
|
126
|
+
o.cmdGenerateClaudeMd(cwd, { output: outputPath, auto: autoFlag, force: forceFlag }, raw);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
routeScanSessions,
|
|
131
|
+
routeExtractMessages,
|
|
132
|
+
routeProfileSample,
|
|
133
|
+
routeWriteProfile,
|
|
134
|
+
routeProfileQuestionnaire,
|
|
135
|
+
routeGenerateDevPreferences,
|
|
136
|
+
routeGenerateClaudeProfile,
|
|
137
|
+
routeGenerateClaudeMd,
|
|
138
|
+
};
|
|
@@ -319,8 +319,17 @@ function stripFencedLines(text) {
|
|
|
319
319
|
/**
|
|
320
320
|
* Returns a filter function that checks whether a phase directory belongs
|
|
321
321
|
* to the current milestone based on ROADMAP.md phase headings.
|
|
322
|
+
*
|
|
323
|
+
* @param cwd - Project working directory.
|
|
324
|
+
* @param versionOverride - Optional version string to scope the phase filter
|
|
325
|
+
* to a specific milestone (e.g. 'v1.2').
|
|
326
|
+
* @param phaseIdConvention - The resolved `phase_id_convention` config value.
|
|
327
|
+
* When `'milestone-prefixed'`, a deprecation warning is emitted for
|
|
328
|
+
* free-form ROADMAPs that lack versioned milestone headings. When absent or
|
|
329
|
+
* any other value, the warning is suppressed — legacy/default projects must
|
|
330
|
+
* never see spurious warnings.
|
|
322
331
|
*/
|
|
323
|
-
function getMilestonePhaseFilter(cwd, versionOverride) {
|
|
332
|
+
function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
324
333
|
const milestonePhaseNums = new Set();
|
|
325
334
|
let missingExplicitVersion = false;
|
|
326
335
|
try {
|
|
@@ -331,9 +340,10 @@ function getMilestonePhaseFilter(cwd, versionOverride) {
|
|
|
331
340
|
let roadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
332
341
|
const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
|
333
342
|
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
|
|
334
|
-
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings) {
|
|
343
|
+
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings && phaseIdConvention === 'milestone-prefixed') {
|
|
335
344
|
console.warn('[gsd] Deprecated: free-form ROADMAP.md detected (no versioned milestone headings). ' +
|
|
336
|
-
'
|
|
345
|
+
'The project has phase_id_convention set to "milestone-prefixed" in config.json but the ' +
|
|
346
|
+
'ROADMAP does not use versioned milestone headings. Run `gsd-tools roadmap upgrade --convention milestone-prefixed` to migrate (dry-run by default).');
|
|
337
347
|
}
|
|
338
348
|
if (versionOverride) {
|
|
339
349
|
const escapedVersion = escapeRegex(versionOverride);
|
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
};
|
|
12
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
14
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
16
|
const core = require("./core.cjs");
|
|
16
17
|
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, output, error, findPhaseInternal, phaseTokenMatches } = core;
|
|
@@ -386,7 +387,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
386
387
|
}
|
|
387
388
|
const isComplete = summaryCount >= planCount;
|
|
388
389
|
const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
|
|
389
|
-
const today =
|
|
390
|
+
const today = clock_cjs_1.realClock.today();
|
|
390
391
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
391
392
|
output({ updated: false, reason: 'ROADMAP.md not found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
|
|
392
393
|
return;
|
|
@@ -397,25 +398,37 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
397
398
|
const phasePattern = phaseMarkdownRegexSource(phaseNum);
|
|
398
399
|
// Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables)
|
|
399
400
|
const tableRowPattern = new RegExp(`^(\\|\\s*${phasePattern}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, 'im');
|
|
400
|
-
const dateField = isComplete ? ` ${today} ` : ' ';
|
|
401
401
|
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
402
402
|
const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split
|
|
403
|
+
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
|
403
404
|
if (cells.length === 5) {
|
|
404
405
|
// 5-col: Phase | Milestone | Plans | Status | Completed
|
|
405
406
|
cells[2] = ` ${summaryCount}/${planCount} `;
|
|
406
407
|
cells[3] = ` ${status.padEnd(11)}`;
|
|
407
|
-
|
|
408
|
+
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
|
409
|
+
const existingDate5 = cells[4].trim();
|
|
410
|
+
cells[4] = isComplete
|
|
411
|
+
? (dateShape.test(existingDate5) ? cells[4] : ` ${today} `)
|
|
412
|
+
: ' ';
|
|
408
413
|
}
|
|
409
414
|
else if (cells.length === 4) {
|
|
410
415
|
// 4-col: Phase | Plans | Status | Completed
|
|
411
416
|
cells[1] = ` ${summaryCount}/${planCount} `;
|
|
412
417
|
cells[2] = ` ${status.padEnd(11)}`;
|
|
413
|
-
|
|
418
|
+
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
|
419
|
+
const existingDate4 = cells[3].trim();
|
|
420
|
+
cells[3] = isComplete
|
|
421
|
+
? (dateShape.test(existingDate4) ? cells[3] : ` ${today} `)
|
|
422
|
+
: ' ';
|
|
414
423
|
}
|
|
415
424
|
return '|' + cells.join('|') + '|';
|
|
416
425
|
});
|
|
417
|
-
// Update plan count in phase detail section
|
|
418
|
-
|
|
426
|
+
// Update plan count in phase detail section.
|
|
427
|
+
// Three recognised forms (all tolerated; canonical template uses the first):
|
|
428
|
+
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
429
|
+
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
430
|
+
// `Plans: N plans` — plain text header
|
|
431
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
419
432
|
const planCountText = isComplete
|
|
420
433
|
? `${summaryCount}/${planCount} plans complete`
|
|
421
434
|
: `${summaryCount}/${planCount} plans executed`;
|
|
@@ -434,6 +447,80 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
434
447
|
const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
|
|
435
448
|
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
436
449
|
}
|
|
450
|
+
// Compute the active (post-</details>) region offset ONCE. Both the
|
|
451
|
+
// missing-plan DETECTION and the row INSERTION must use the same active
|
|
452
|
+
// region string so that a plan row that exists only in an archived <details>
|
|
453
|
+
// block is not counted as "already present" in the active milestone section.
|
|
454
|
+
// (Finding 1 code-review: detection was previously running against the full
|
|
455
|
+
// roadmapContent, causing archived rows to suppress active-section inserts.)
|
|
456
|
+
const lastDetailsClose = roadmapContent.lastIndexOf('</details>');
|
|
457
|
+
const activeRegion = lastDetailsClose === -1
|
|
458
|
+
? roadmapContent
|
|
459
|
+
: roadmapContent.slice(lastDetailsClose + '</details>'.length);
|
|
460
|
+
// Compute which plan files are MISSING a checkbox row in the ACTIVE region.
|
|
461
|
+
// This handles three cases:
|
|
462
|
+
// (a) Fresh template — no rows at all: all plans are missing.
|
|
463
|
+
// (b) Partial gap — some rows exist, others don't: only the absent ones.
|
|
464
|
+
// (c) All rows present — nothing to insert (idempotent).
|
|
465
|
+
//
|
|
466
|
+
// Detection is scoped to the active region so a plan that appears in an
|
|
467
|
+
// archived <details> block is still correctly detected as missing from the
|
|
468
|
+
// active milestone section.
|
|
469
|
+
const missingPlans = phaseInfo.plans.filter((planFile) => {
|
|
470
|
+
const planEscaped = escapeRegex(planFile);
|
|
471
|
+
return !new RegExp(`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${planEscaped}`, 'i').test(activeRegion);
|
|
472
|
+
});
|
|
473
|
+
if (missingPlans.length > 0) {
|
|
474
|
+
// Insert missing plan checklist rows (#1163). We prefer to anchor to the
|
|
475
|
+
// bare `Plans:` checklist header (canonical template form) and fall back to
|
|
476
|
+
// the bold `**Plans**:`/`**Plans:**` summary line only when no bare header
|
|
477
|
+
// is present. Using two separate patterns avoids the lazy-quantifier trap
|
|
478
|
+
// where a single alternation would stop at the first matching alternative
|
|
479
|
+
// (the bold summary) before reaching the checklist header.
|
|
480
|
+
//
|
|
481
|
+
// Canonical template (gsd-core/templates/roadmap.md) uses BOTH lines:
|
|
482
|
+
// **Plans**: N plans ← summary (colon outside bold)
|
|
483
|
+
// Plans: ← checklist header (PREFERRED insertion anchor)
|
|
484
|
+
// Rows must land after `Plans:`, not between the summary and the header.
|
|
485
|
+
//
|
|
486
|
+
// Pattern A: anchor to bare `Plans:` header (preferred).
|
|
487
|
+
// Pattern B: fallback to bold summary when no bare header exists.
|
|
488
|
+
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
489
|
+
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
490
|
+
const sortedMissing = [...missingPlans].sort();
|
|
491
|
+
const newRows = sortedMissing.map(p => `- [ ] ${p}`).join('\n');
|
|
492
|
+
const inserter = (match) => `${match}\n${newRows}`;
|
|
493
|
+
// Scope insertion to the active (post-</details>) milestone region to
|
|
494
|
+
// prevent duplicate phase headings in archived sections from receiving rows.
|
|
495
|
+
// replaceInCurrentMilestone only accepts a string replacement, so we
|
|
496
|
+
// perform the scoped replace manually here (same strategy as that helper).
|
|
497
|
+
// Note: lastDetailsClose was computed above (shared with detection).
|
|
498
|
+
const scopedReplace = (src, pat) => src.replace(pat, inserter);
|
|
499
|
+
let withRows;
|
|
500
|
+
if (lastDetailsClose === -1) {
|
|
501
|
+
// activeRegion === roadmapContent when there are no </details> blocks.
|
|
502
|
+
const regionA = scopedReplace(activeRegion, insertRowsPatternA);
|
|
503
|
+
withRows = regionA !== activeRegion ? regionA : scopedReplace(activeRegion, insertRowsPatternB);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const beforeDetails = roadmapContent.slice(0, lastDetailsClose + '</details>'.length);
|
|
507
|
+
const regionA = scopedReplace(activeRegion, insertRowsPatternA);
|
|
508
|
+
const afterWithRows = regionA !== activeRegion ? regionA : scopedReplace(activeRegion, insertRowsPatternB);
|
|
509
|
+
withRows = beforeDetails + afterWithRows;
|
|
510
|
+
}
|
|
511
|
+
if (withRows !== roadmapContent) {
|
|
512
|
+
roadmapContent = withRows;
|
|
513
|
+
// Mark any newly-inserted rows that already have summaries as complete
|
|
514
|
+
for (const summaryFile of phaseInfo.summaries) {
|
|
515
|
+
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
516
|
+
if (!planId)
|
|
517
|
+
continue;
|
|
518
|
+
const planEscaped = escapeRegex(planId);
|
|
519
|
+
const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
|
|
520
|
+
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
437
524
|
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, roadmapContent);
|
|
438
525
|
});
|
|
439
526
|
output({
|
|
@@ -620,7 +707,10 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
620
707
|
}
|
|
621
708
|
}
|
|
622
709
|
const newListBlock = annotatedLines.join('\n') + '\n';
|
|
623
|
-
|
|
710
|
+
// #1103: when `(?:^|\n)` consumed a leading `\n` (mid-string match), re-emit it
|
|
711
|
+
// so the line preceding the Plans: header is not fused onto it.
|
|
712
|
+
const leadingNewline = plansBlockMatch[0].startsWith('\n') ? '\n' : '';
|
|
713
|
+
const newPhaseSection = phaseSection.replace(plansBlockMatch[0], leadingNewline + plansHeader + newListBlock);
|
|
624
714
|
const nextContent = content.slice(0, phaseStart) + newPhaseSection + content.slice(phaseEnd);
|
|
625
715
|
if (nextContent === content)
|
|
626
716
|
return;
|