@opengsd/gsd-core 1.7.0-rc.1 → 1.7.0-rc.3
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 +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/GEMINI.md +25 -23
- package/README.md +3 -3
- package/agents/gsd-advisor-researcher.md +3 -1
- package/agents/gsd-ai-researcher.md +2 -2
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +3 -1
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +3 -1
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-domain-researcher.md +1 -1
- package/agents/gsd-eval-auditor.md +3 -1
- package/agents/gsd-executor.md +12 -9
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +1 -1
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +3 -1
- package/agents/gsd-planner.md +4 -2
- package/agents/gsd-project-researcher.md +4 -2
- package/agents/gsd-research-synthesizer.md +3 -1
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +4 -2
- package/agents/gsd-verifier.md +6 -4
- package/bin/install.js +226 -459
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +9 -5
- package/commands/gsd/next.md +29 -0
- package/commands/gsd/ns-workflow.md +5 -2
- package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
- package/gsd-core/bin/gsd-tools.cjs +43 -8
- package/gsd-core/bin/lib/capability-loader.cjs +27 -9
- package/gsd-core/bin/lib/capability-registry.cjs +209 -205
- package/gsd-core/bin/lib/capability-source.cjs +22 -7
- package/gsd-core/bin/lib/capability-validator.cjs +24 -2
- package/gsd-core/bin/lib/check-command-router.cjs +111 -1
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +19 -1
- package/gsd-core/bin/lib/core-utils.cjs +41 -0
- package/gsd-core/bin/lib/external-job.cjs +287 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +151 -0
- package/gsd-core/bin/lib/graphify.cjs +41 -9
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +61 -0
- package/gsd-core/bin/lib/init.cjs +34 -6
- package/gsd-core/bin/lib/milestone.cjs +56 -11
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +51 -29
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/plan-scan.cjs +9 -1
- package/gsd-core/bin/lib/probe-core.cjs +44 -4
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +21 -19
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap.cjs +45 -22
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
- package/gsd-core/bin/lib/runtime-name-policy.cjs +8 -10
- package/gsd-core/bin/lib/shell-command-projection.cjs +12 -8
- package/gsd-core/bin/lib/smart-entry.cjs +521 -0
- package/gsd-core/bin/lib/state-transition.cjs +15 -0
- package/gsd-core/bin/lib/state.cjs +27 -8
- package/gsd-core/bin/lib/surface.cjs +18 -9
- package/gsd-core/bin/lib/update-context.cjs +0 -5
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -13
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/gates.md +1 -1
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/planning-config.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +23 -25
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +7 -11
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +5 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +4 -4
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +15 -1
- package/gsd-core/workflows/help/modes/topic.md +1 -0
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +7 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +12 -160
- package/gsd-core/workflows/plan-review-convergence.md +1 -1
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +1 -1
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +50 -9
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +13 -11
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +1 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +120 -0
- package/gsd-core/workflows/spec-phase.md +8 -6
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +3 -6
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +63 -6
- package/package.json +5 -5
- package/scripts/ci-test-scope.cjs +19 -1
- package/scripts/generate-package-identity.cjs +27 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -148
- package/scripts/lint-regression-test-names.allowlist.json +1 -273
- package/scripts/lint-test-file-count.allowlist.json +7 -91
- package/scripts/research-profiles.cjs +6 -6
- package/scripts/run-tests.cjs +67 -4
- package/scripts/slurm-adapter.cjs +269 -0
- package/scripts/sync-manifest-versions.cjs +67 -14
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +9 -5
- package/skills/gsd-next/SKILL.md +30 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -1
- package/skills/gsd-review/SKILL.md +6 -0
- package/gemini-extension.json +0 -6
|
@@ -29,7 +29,7 @@ const phaseIdMod = require("./phase-id.cjs");
|
|
|
29
29
|
const { escapeRegex, normalizePhaseName, phaseTokenMatches } = phaseIdMod;
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
31
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
32
|
-
const { getMilestonePhaseFilter, extractCurrentMilestone } = roadmapParserMod;
|
|
32
|
+
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo } = roadmapParserMod;
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
34
|
const coreUtilsMod = require("./core-utils.cjs");
|
|
35
35
|
const { extractOneLinerFromBody } = coreUtilsMod;
|
|
@@ -112,8 +112,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
112
112
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
113
113
|
const reqPath = planningPaths(cwd).requirements;
|
|
114
114
|
const statePath = planningPaths(cwd).state;
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
// #1911: derive the archive base from the workstream-aware planning root so
|
|
116
|
+
// `milestone complete --ws` archives into the workstream, not root. planningPaths(cwd).planning
|
|
117
|
+
// resolves to the workstream base when GSD_WORKSTREAM is set and to root .planning otherwise
|
|
118
|
+
// (flat mode is a no-op).
|
|
119
|
+
const planningBase = planningPaths(cwd).planning;
|
|
120
|
+
const milestonesPath = node_path_1.default.join(planningBase, 'MILESTONES.md');
|
|
121
|
+
const archiveDir = node_path_1.default.join(planningBase, 'milestones');
|
|
117
122
|
const phasesDir = planningPaths(cwd).phases;
|
|
118
123
|
const today = new Date().toISOString().split('T')[0];
|
|
119
124
|
const milestoneName = options.name || version;
|
|
@@ -151,7 +156,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
151
156
|
if (stateVersion && stateVersion === version) {
|
|
152
157
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
153
158
|
const scopedContent = extractCurrentMilestone(roadmapContent, cwd);
|
|
154
|
-
|
|
159
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
160
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
|
|
155
161
|
const noDirectoryPhases = [];
|
|
156
162
|
let pm;
|
|
157
163
|
const phaseDirEntries = (() => {
|
|
@@ -257,11 +263,17 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
257
263
|
// Archive REQUIREMENTS.md
|
|
258
264
|
if (node_fs_1.default.existsSync(reqPath)) {
|
|
259
265
|
const reqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
|
|
260
|
-
|
|
266
|
+
// Derive the display path from the same source the writer uses (reqPath), so a
|
|
267
|
+
// workstream archive header points at `.planning/workstreams/<ws>/REQUIREMENTS.md`
|
|
268
|
+
// instead of the hardcoded root path (#1993). Root case is byte-identical.
|
|
269
|
+
// Normalize to POSIX separators so the header is cross-platform (Windows
|
|
270
|
+
// path.relative yields backslashes; the original literal was forward-slash).
|
|
271
|
+
const reqDisplay = node_path_1.default.relative(cwd, reqPath).split(node_path_1.default.sep).join('/');
|
|
272
|
+
const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`${reqDisplay}\`.\n\n---\n\n`;
|
|
261
273
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent);
|
|
262
274
|
}
|
|
263
275
|
// Archive audit file if exists
|
|
264
|
-
const auditFile = node_path_1.default.join(
|
|
276
|
+
const auditFile = node_path_1.default.join(planningBase, `${version}-MILESTONE-AUDIT.md`);
|
|
265
277
|
if (node_fs_1.default.existsSync(auditFile)) {
|
|
266
278
|
(0, shell_command_projection_cjs_1.retryRenameSync)(auditFile, node_path_1.default.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
|
|
267
279
|
}
|
|
@@ -309,7 +321,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
309
321
|
}
|
|
310
322
|
// Archive phase directories if requested
|
|
311
323
|
let phasesArchived = false;
|
|
312
|
-
|
|
324
|
+
// #1871: archive phase dirs by default on milestone complete (opt out via --no-archive-phases).
|
|
325
|
+
if (options.archivePhases !== false) {
|
|
313
326
|
try {
|
|
314
327
|
const phaseArchiveDir = node_path_1.default.join(archiveDir, `${version}-phases`);
|
|
315
328
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(phaseArchiveDir);
|
|
@@ -398,10 +411,8 @@ function cmdPhasesClear(cwd, raw, args) {
|
|
|
398
411
|
}
|
|
399
412
|
}
|
|
400
413
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
cleared++;
|
|
404
|
-
}
|
|
414
|
+
// #1871: archive phase directories instead of destroying them (shared helper).
|
|
415
|
+
cleared = archivePhaseDirectories(cwd, phasesDir, dirs).archived;
|
|
405
416
|
}
|
|
406
417
|
catch (e) {
|
|
407
418
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -410,6 +421,40 @@ function cmdPhasesClear(cwd, raw, args) {
|
|
|
410
421
|
}
|
|
411
422
|
output({ cleared }, raw, `${cleared} phase director${cleared === 1 ? 'y' : 'ies'} cleared`);
|
|
412
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* #1871: move each non-999 phase directory under `phasesDir` into
|
|
426
|
+
* `milestones/<version>-phases/` (collision-safe; version from getMilestoneInfo,
|
|
427
|
+
* timestamp fallback). Shared by `phases clear` (archive-then-remove) and the
|
|
428
|
+
* internal milestone.complete phase archival so phase history survives a
|
|
429
|
+
* milestone switch instead of being hard-deleted.
|
|
430
|
+
*/
|
|
431
|
+
function archivePhaseDirectories(cwd, phasesDir, dirs) {
|
|
432
|
+
let archiveVersion = null;
|
|
433
|
+
try {
|
|
434
|
+
archiveVersion = getMilestoneInfo(cwd).version ?? null;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
/* ROADMAP/STATE unreadable — fall back to a dated label */
|
|
438
|
+
}
|
|
439
|
+
if (!archiveVersion) {
|
|
440
|
+
archiveVersion = `archived-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 8)}`;
|
|
441
|
+
}
|
|
442
|
+
const archivePhasesDir = node_path_1.default.join(planningPaths(cwd).planning, 'milestones', `${archiveVersion}-phases`);
|
|
443
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(archivePhasesDir);
|
|
444
|
+
let archived = 0;
|
|
445
|
+
for (const entry of dirs) {
|
|
446
|
+
const src = node_path_1.default.join(phasesDir, entry.name);
|
|
447
|
+
// Collision-safe: if a same-named archive entry exists (re-run), suffix it.
|
|
448
|
+
let dest = node_path_1.default.join(archivePhasesDir, entry.name);
|
|
449
|
+
let n = 1;
|
|
450
|
+
while (node_fs_1.default.existsSync(dest)) {
|
|
451
|
+
dest = node_path_1.default.join(archivePhasesDir, `${entry.name}.${n++}`);
|
|
452
|
+
}
|
|
453
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(src, dest);
|
|
454
|
+
archived++;
|
|
455
|
+
}
|
|
456
|
+
return { archiveDir: archivePhasesDir, archived };
|
|
457
|
+
}
|
|
413
458
|
module.exports = {
|
|
414
459
|
cmdRequirementsMarkComplete,
|
|
415
460
|
cmdMilestoneComplete,
|
|
@@ -20,6 +20,23 @@ const PROJECT_CODE_PREFIX_STRIP_RE = /^[A-Z][A-Z0-9_]*-(?=\d)/;
|
|
|
20
20
|
const PROJECT_CODE_PREFIX_STRIP_RE_I = /^[A-Z][A-Z0-9_]*-(?=\d)/i;
|
|
21
21
|
const PROJECT_CODE_PREFIX_CAPTURE_RE_I = /^([A-Z][A-Z0-9_]*)-(\d.*)/i;
|
|
22
22
|
const OPTIONAL_PROJECT_CODE_PREFIX_SOURCE = '(?:[A-Z][A-Z0-9_]*-)?';
|
|
23
|
+
// #1729: phase headers may carry a parenthetical tag between the number and the
|
|
24
|
+
// colon, e.g. `### Phase 26 (Cluster B): Title`. This optional, non-capturing
|
|
25
|
+
// fragment is injected at every phase-header regex call site (immediately after
|
|
26
|
+
// the phase-number token, before the colon/space delimiter) so the resolver
|
|
27
|
+
// tolerates the tag — mirroring how `[...]` is already tolerated before `Phase`.
|
|
28
|
+
// `[^)\n]*` keeps the match single-line (headers are one line) to avoid
|
|
29
|
+
// over-consuming across a malformed multi-line document. Injected at the call
|
|
30
|
+
// site (not baked into phaseMarkdownRegexSource) so it applies uniformly to
|
|
31
|
+
// both the numeric and project-code-exact escaped sources, and so the decimal
|
|
32
|
+
// sub-phase patterns can place it after the `.N` segment.
|
|
33
|
+
//
|
|
34
|
+
// Enumeration/parse call sites that read phase headers from a regex *literal*
|
|
35
|
+
// (rather than a `new RegExp` built from an interpolated phase number) cannot
|
|
36
|
+
// reference this constant; they inline its literal-regex mirror instead —
|
|
37
|
+
// `(?:\s*\([^)\n]*\))?` — kept character-for-character equivalent to this
|
|
38
|
+
// source. Both forms must change together; see the #1729 regression test.
|
|
39
|
+
const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]*\\))?';
|
|
23
40
|
function stripProjectCodePrefix(value, caseInsensitive = true) {
|
|
24
41
|
const input = String(value);
|
|
25
42
|
const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
|
|
@@ -215,6 +232,7 @@ function phaseTokenMatches(dirName, normalized) {
|
|
|
215
232
|
module.exports = {
|
|
216
233
|
escapeRegex,
|
|
217
234
|
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
|
235
|
+
OPTIONAL_PHASE_TAG_SOURCE,
|
|
218
236
|
stripProjectCodePrefix,
|
|
219
237
|
normalizePhaseName,
|
|
220
238
|
getMilestoneFromPhaseId,
|
|
@@ -32,7 +32,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
32
32
|
const { toPosixPath, generateSlugInternal, readSubdirectories } = coreUtilsMod;
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
34
34
|
const phaseIdMod = require("./phase-id.cjs");
|
|
35
|
-
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, } = phaseIdMod;
|
|
35
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, } = phaseIdMod;
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
37
37
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
38
38
|
const { findPhaseInternal, getArchivedPhaseDirs } = phaseLocatorMod;
|
|
@@ -181,7 +181,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
181
181
|
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
182
182
|
try {
|
|
183
183
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
184
|
-
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)\\s*:`, 'gi');
|
|
184
|
+
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
185
185
|
let pm;
|
|
186
186
|
while ((pm = phasePattern.exec(roadmapContent)) !== null) {
|
|
187
187
|
decimalSet.add(parseInt(pm[1], 10));
|
|
@@ -221,7 +221,7 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
|
|
|
221
221
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
222
222
|
const fullContent = stripShippedMilestones(rawContent);
|
|
223
223
|
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
224
|
-
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}\\s*:`, 'i');
|
|
224
|
+
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
|
|
225
225
|
for (const content of [milestoneContent, fullContent]) {
|
|
226
226
|
const headerMatch = content.match(phaseHeader);
|
|
227
227
|
if (!headerMatch || headerMatch.index === undefined)
|
|
@@ -578,7 +578,8 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
578
578
|
// Three sources are scanned so that a phase in ANY representation
|
|
579
579
|
// (section header, roadmap bullet, or on-disk directory) is counted:
|
|
580
580
|
// 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
|
|
581
|
-
|
|
581
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
582
|
+
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]*\))?:/gi;
|
|
582
583
|
// 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
|
|
583
584
|
// The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
|
|
584
585
|
// or end-of-line so titleless forms ("- [ ] **Phase 11**", "- [ ] Phase 11")
|
|
@@ -662,7 +663,8 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
662
663
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
663
664
|
let maxPhase = 0;
|
|
664
665
|
if (config.phase_naming !== 'custom') {
|
|
665
|
-
|
|
666
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
667
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]*\))?:/gi;
|
|
666
668
|
let m;
|
|
667
669
|
while ((m = phasePattern.exec(content)) !== null) {
|
|
668
670
|
const num = parseInt(m[1], 10);
|
|
@@ -740,14 +742,14 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
740
742
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
741
743
|
const normalizedAfter = normalizePhaseName(afterPhase);
|
|
742
744
|
const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
|
|
743
|
-
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
|
745
|
+
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
|
|
744
746
|
const headingMatch = targetPattern.test(content);
|
|
745
|
-
const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
|
|
747
|
+
const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
746
748
|
const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
|
|
747
749
|
const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
|
|
748
750
|
const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
|
|
749
751
|
if (!headingMatch && !isBulletStyle) {
|
|
750
|
-
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
|
|
752
|
+
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
751
753
|
if (checklistPattern.test(content)) {
|
|
752
754
|
error(`Phase ${afterPhase} exists in roadmap summary but is missing a detail section (### Phase ${afterPhase}: ...).`);
|
|
753
755
|
}
|
|
@@ -769,7 +771,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
769
771
|
catch {
|
|
770
772
|
/* intentionally empty */
|
|
771
773
|
}
|
|
772
|
-
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)\\s*:`, 'gi');
|
|
774
|
+
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
773
775
|
let rmMatch;
|
|
774
776
|
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
775
777
|
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
@@ -785,13 +787,13 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
785
787
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
786
788
|
let updatedContent;
|
|
787
789
|
if (isBulletStyle) {
|
|
788
|
-
const boldBulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
|
790
|
+
const boldBulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
|
|
789
791
|
const useBold = boldBulletPattern.test(content);
|
|
790
792
|
const phaseLabel = useBold
|
|
791
793
|
? `**Phase ${_decimalPhase}: ${description}**`
|
|
792
794
|
: `Phase ${_decimalPhase}: ${description}`;
|
|
793
795
|
const bulletEntry = `\n- [ ] ${phaseLabel}`;
|
|
794
|
-
const targetBulletPattern = new RegExp(`(-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s][^\\n]*)`, 'i');
|
|
796
|
+
const targetBulletPattern = new RegExp(`(-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
|
|
795
797
|
const bulletMatchResult = rawContent.match(targetBulletPattern);
|
|
796
798
|
if (!bulletMatchResult) {
|
|
797
799
|
error(`Could not find Phase ${afterPhase} bullet line`);
|
|
@@ -811,7 +813,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
811
813
|
}
|
|
812
814
|
else {
|
|
813
815
|
const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
|
|
814
|
-
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
816
|
+
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
|
|
815
817
|
const headerMatch = rawContent.match(headerPattern);
|
|
816
818
|
if (!headerMatch) {
|
|
817
819
|
error(`Could not find Phase ${afterPhase} header`);
|
|
@@ -940,11 +942,13 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
940
942
|
withPlanningLock(cwd, () => {
|
|
941
943
|
let content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
942
944
|
const escaped = escapeRegex(targetPhase);
|
|
943
|
-
content = content.replace(new RegExp(`\\n?(?<h>#{2,4})\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n\\k<h>(?!#)\\s+Phase\\s+[^\\n:]+\\s*:|$)`, 'i'), '');
|
|
944
|
-
content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
|
|
945
|
+
content = content.replace(new RegExp(`\\n?(?<h>#{2,4})\\s*Phase\\s+${escaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:[\\s\\S]*?(?=\\n\\k<h>(?!#)\\s+Phase\\s+[^\\n:]+\\s*:|$)`, 'i'), '');
|
|
946
|
+
content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*`, 'gi'), '');
|
|
945
947
|
content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
|
|
946
948
|
if (!isDecimal) {
|
|
947
|
-
|
|
949
|
+
// #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
|
|
950
|
+
// is re-emitted verbatim — a tagged later phase still gets renumbered.
|
|
951
|
+
content = content.replace(/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\n]*\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
|
|
948
952
|
content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
949
953
|
content = content.replace(/(\|\s*)(\d+)(\.\s)/g, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
950
954
|
content = content.replace(/(?<![0-9-])(\d{2})-(\d{2})(?=(?:(?:-[A-Za-z][A-Za-z0-9-]*)?-(?:PLAN|SUMMARY)\.md)|(?![0-9-]))/g, (_match, phaseNum, planNum) => `${decrementRoadmapPaddedPhaseNumber(phaseNum, removedInt)}-${planNum}`);
|
|
@@ -1047,7 +1051,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
|
1047
1051
|
if (!roadmapContent || !phaseNum)
|
|
1048
1052
|
return null;
|
|
1049
1053
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
1050
|
-
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}\\s*:\\s*([^\\n]+)`, 'im'));
|
|
1054
|
+
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
|
|
1051
1055
|
if (!heading)
|
|
1052
1056
|
return null;
|
|
1053
1057
|
const name = heading[1].replace(/\(INSERTED\)/i, '').trim();
|
|
@@ -1128,10 +1132,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1128
1132
|
const originalRoadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1129
1133
|
roadmapContent = originalRoadmapContent;
|
|
1130
1134
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
1131
|
-
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`, 'i');
|
|
1135
|
+
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
|
|
1132
1136
|
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
1133
1137
|
const tableRowPattern = new RegExp(`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, 'im');
|
|
1134
|
-
|
|
1138
|
+
// Scope the Progress-row search to the ## Progress section so the regex
|
|
1139
|
+
// doesn't bind to an earlier table (e.g. | Phase | Requirements | Count |)
|
|
1140
|
+
// whose rows also start with the phase number. (#2012)
|
|
1141
|
+
const updateProgressRow = (fullRow) => {
|
|
1135
1142
|
const cells = fullRow.split('|').slice(1, -1);
|
|
1136
1143
|
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
|
1137
1144
|
if (cells.length === 5) {
|
|
@@ -1149,7 +1156,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1149
1156
|
cells[3] = dateShape.test(existingDate4) ? cells[3] : ` ${today} `;
|
|
1150
1157
|
}
|
|
1151
1158
|
return '|' + cells.join('|') + '|';
|
|
1152
|
-
}
|
|
1159
|
+
};
|
|
1160
|
+
const progressIdx = roadmapContent.indexOf('## Progress');
|
|
1161
|
+
if (progressIdx >= 0) {
|
|
1162
|
+
const beforeProgress = roadmapContent.slice(0, progressIdx);
|
|
1163
|
+
const progressSection = roadmapContent.slice(progressIdx);
|
|
1164
|
+
roadmapContent = beforeProgress + progressSection.replace(tableRowPattern, updateProgressRow);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
roadmapContent = roadmapContent.replace(tableRowPattern, updateProgressRow);
|
|
1168
|
+
}
|
|
1153
1169
|
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, 'i');
|
|
1154
1170
|
roadmapContent = roadmapContent.replace(planCountPattern, `$1${summaryCount}/${planCount} plans complete`);
|
|
1155
1171
|
const phaseInfoSummaries = phaseInfo['summaries'];
|
|
@@ -1170,7 +1186,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1170
1186
|
if (node_fs_1.default.existsSync(reqPath)) {
|
|
1171
1187
|
const phaseEsc = phaseMarkdownRegexSource(phaseNum);
|
|
1172
1188
|
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
1173
|
-
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
|
|
1189
|
+
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
|
|
1174
1190
|
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
|
1175
1191
|
const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
|
|
1176
1192
|
const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
|
|
@@ -1288,15 +1304,21 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1288
1304
|
try {
|
|
1289
1305
|
const roadmapForPhases = extractCurrentMilestone(roadmapContent, cwd);
|
|
1290
1306
|
// #1591: match BOTH heading-style phases (`### Phase N:`) AND
|
|
1291
|
-
// checkbox-list items
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
// the
|
|
1299
|
-
|
|
1307
|
+
// checkbox-list items, INCLUDING the canonical bold form the roadmap
|
|
1308
|
+
// template emits (`- [ ] **Phase N: Name**`). When the active
|
|
1309
|
+
// milestone's checklist is `- [ ]` items inside a <details> block
|
|
1310
|
+
// (and the next phase has no directory yet, so the disk-based
|
|
1311
|
+
// resolver finds nothing), this roadmap-enumeration fallback is the
|
|
1312
|
+
// only path that can find the next phase. The prior heading-only
|
|
1313
|
+
// pattern missed checkbox items, and a checkbox-only broadening still
|
|
1314
|
+
// missed the bold template rows → is_last_phase=true on a mid-milestone
|
|
1315
|
+
// phase. Allow optional `**`/`__` emphasis after the marker and stop
|
|
1316
|
+
// the name capture at emphasis so bold names slug cleanly; the number
|
|
1317
|
+
// capture is unchanged.
|
|
1318
|
+
// #1729: `(?:\s*\([^)\n]*\))?` after the number tolerates a pre-colon
|
|
1319
|
+
// ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE) so
|
|
1320
|
+
// `### Phase N (Cluster B): X` resolves. Captures are unchanged.
|
|
1321
|
+
const phasePattern = /(?:#{2,4}|-\s*\[[ xX]\])\s*(?:\*\*|__)?\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n*]+)/gi;
|
|
1300
1322
|
let pm;
|
|
1301
1323
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
1302
1324
|
if (comparePhaseNum(pm[1], phaseNum) > 0) {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Keeps gsd-tools.cjs thin while preserving current CJS semantics.
|
|
5
5
|
*
|
|
6
6
|
* Unsupported in this router (treated as unknown):
|
|
7
|
-
* - archive: `phases archive` is excluded
|
|
8
|
-
*
|
|
7
|
+
* - archive: `phases archive` is excluded (#2684) — it is an internal
|
|
8
|
+
* subcommand that milestone.complete forwards to, not a public surface.
|
|
9
9
|
*
|
|
10
10
|
* ADR-457 build-at-publish: the hand-written bin/lib/phases-command-router.cjs
|
|
11
11
|
* collapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
|
|
@@ -19,7 +19,8 @@ const { routeCjsCommandFamily } = cjsCommandRouterAdapter;
|
|
|
19
19
|
function routePhasesCommand({ phase, milestone, args, cwd, raw, error }) {
|
|
20
20
|
routeCjsCommandFamily({
|
|
21
21
|
args,
|
|
22
|
-
//
|
|
22
|
+
// #2684: `phases archive` is deliberately excluded — it is an internal
|
|
23
|
+
// subcommand that milestone.complete forwards to, not a public surface.
|
|
23
24
|
subcommands: command_aliases_cjs_1.PHASES_SUBCOMMANDS.filter((s) => s !== 'archive'),
|
|
24
25
|
error,
|
|
25
26
|
unknownMessage: (_subcommand, available) => `Unknown phases subcommand. Available: ${available.join(', ')}`,
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
const node_fs_1 = require("node:fs");
|
|
11
11
|
const node_path_1 = require("node:path");
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
|
+
const coreUtils = require("./core-utils.cjs");
|
|
14
|
+
const { countMatchedSummaries } = coreUtils;
|
|
12
15
|
// Excluded derivative files
|
|
13
16
|
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
|
|
14
17
|
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
|
@@ -72,7 +75,12 @@ function scanPhasePlans(phaseDir) {
|
|
|
72
75
|
const planFiles = rootPlanFiles.concat(nestedPlanFiles);
|
|
73
76
|
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
|
|
74
77
|
const planCount = planFiles.length;
|
|
75
|
-
|
|
78
|
+
// Count only summaries that are the PLAN→SUMMARY partner of an existing plan
|
|
79
|
+
// (#1988): stray non-plan summaries (e.g. 30-FIX-CR02-SUMMARY.md,
|
|
80
|
+
// 30-GAPCLOSURE-SUMMARY.md) must not inflate summary_count or flip a phase to
|
|
81
|
+
// Complete when plans are still missing summaries. summaryFiles (the array)
|
|
82
|
+
// still holds every summary on disk for callers that read/list them.
|
|
83
|
+
const summaryCount = countMatchedSummaries(planFiles, summaryFiles);
|
|
76
84
|
return {
|
|
77
85
|
planCount,
|
|
78
86
|
summaryCount,
|
|
@@ -48,11 +48,40 @@ exports.VALID_STATUS = ['resolved', 'dismissed', 'unresolved'];
|
|
|
48
48
|
function errMessage(e) {
|
|
49
49
|
return e instanceof Error ? e.message : String(e);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Structural guard for ONE item of the report `items[]` — enforces the `Item` contract (#1907).
|
|
53
|
+
* `analyzeCoverage` always emits fully-populated, category/status-validated Items, so this never
|
|
54
|
+
* rejects legitimate output; it catches an adapter that bypasses the merge and hands back per-item
|
|
55
|
+
* garbage (e.g. `items:[{}]`) inside a well-shaped envelope — which the container-only guard let
|
|
56
|
+
* sail through as green output despite the fail-closed docstring below.
|
|
57
|
+
*/
|
|
58
|
+
function isValidItem(item) {
|
|
59
|
+
if (item == null || typeof item !== 'object')
|
|
60
|
+
return false;
|
|
61
|
+
const i = item;
|
|
62
|
+
if (typeof i.requirement_id !== 'string' || !i.requirement_id.trim())
|
|
63
|
+
return false;
|
|
64
|
+
if (typeof i.category !== 'string' || !i.category.trim())
|
|
65
|
+
return false;
|
|
66
|
+
if (!exports.VALID_STATUS.includes(i.status))
|
|
67
|
+
return false;
|
|
68
|
+
if (typeof i.probe !== 'string')
|
|
69
|
+
return false;
|
|
70
|
+
// The three nullable fields must be a string or null — never some other type.
|
|
71
|
+
if (i.verification !== null && typeof i.verification !== 'string')
|
|
72
|
+
return false;
|
|
73
|
+
if (i.resolution !== null && typeof i.resolution !== 'string')
|
|
74
|
+
return false;
|
|
75
|
+
if (i.reason !== null && typeof i.reason !== 'string')
|
|
76
|
+
return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
51
79
|
/**
|
|
52
80
|
* Structural guard for the report an adapter's `analyze` returns. The scaffold types `analyze`
|
|
53
81
|
* loosely (it runs over JSON-parsed input the adapter `as`-casts), so a future adapter (#644)
|
|
54
82
|
* that forgets to validate inside its closure could hand back a malformed object. Rather than
|
|
55
|
-
* stringify garbage as green output, `runProbeCli` checks the report shape
|
|
83
|
+
* stringify garbage as green output, `runProbeCli` checks the report shape — container AND every
|
|
84
|
+
* item — and fails closed.
|
|
56
85
|
*/
|
|
57
86
|
function isValidReport(report) {
|
|
58
87
|
if (report == null || typeof report !== 'object')
|
|
@@ -60,6 +89,8 @@ function isValidReport(report) {
|
|
|
60
89
|
const r = report;
|
|
61
90
|
if (!Array.isArray(r.items))
|
|
62
91
|
return false;
|
|
92
|
+
if (!r.items.every(isValidItem))
|
|
93
|
+
return false;
|
|
63
94
|
const c = r.coverage;
|
|
64
95
|
if (c == null || typeof c !== 'object')
|
|
65
96
|
return false;
|
|
@@ -373,13 +404,22 @@ function truthStatement(truth) {
|
|
|
373
404
|
/**
|
|
374
405
|
* Extract a truth's verification tier, or `null` when it carries none (a plain string, or an object
|
|
375
406
|
* with no/garbled marker). Failing toward `null` is the Postel-safe direction: an unrecognized marker
|
|
376
|
-
* grades NORMALLY (never a spurious abstention — the over-abstention guard, AC#3)
|
|
377
|
-
*
|
|
407
|
+
* grades NORMALLY (never a spurious abstention — the over-abstention guard, AC#3).
|
|
408
|
+
*
|
|
409
|
+
* The marker is NOT only machine-emitted: `must_haves` markers can be authored BY HAND (#1820's
|
|
410
|
+
* spec-optional predicate rail), and the frontmatter continuation-KV parser preserves stray
|
|
411
|
+
* surrounding whitespace/quotes on a hand-authored value. So we normalize before comparison
|
|
412
|
+
* (Postel: be liberal in what you accept) — `'backstop '`, `' backstop'`, `'"backstop"'` all
|
|
413
|
+
* recognize as the tier. Without this, a hand-authored non-inferable `backstop` truth with a stray
|
|
414
|
+
* trailing space silently grades green instead of abstaining — the exact #1154 false-pass (#1905).
|
|
415
|
+
* An unrecoverably-corrupted marker (e.g. an embedded quote) stays unrecognized → null → graded
|
|
416
|
+
* normally (AC#3): we cannot know its intent, and abstaining on it would be a spurious abstention.
|
|
378
417
|
*/
|
|
379
418
|
function truthVerification(truth) {
|
|
380
419
|
if (truth == null || typeof truth !== 'object')
|
|
381
420
|
return null;
|
|
382
|
-
const
|
|
421
|
+
const raw = truth.verification;
|
|
422
|
+
const v = typeof raw === 'string' ? raw.trim().replace(/^["']|["']$/g, '').trim() : raw;
|
|
383
423
|
return v === 'explicit' || v === 'backstop' ? v : null;
|
|
384
424
|
}
|
|
385
425
|
/**
|
|
@@ -1024,7 +1024,7 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
1024
1024
|
// project-instruction-file`). Previously this was a codex-only override
|
|
1025
1025
|
// (#3163) that left AGENTS-native runtimes (opencode/kilo/kimi) emitting
|
|
1026
1026
|
// CLAUDE.md; copilot now resolves to .github/copilot-instructions.md, and
|
|
1027
|
-
// antigravity
|
|
1027
|
+
// antigravity to GEMINI.md. GSD_RUNTIME env var takes precedence
|
|
1028
1028
|
// over config.runtime, mirroring detectRuntime().
|
|
1029
1029
|
//
|
|
1030
1030
|
// Non-claude runtimes always win over a stale `claude_md_path` (the #3163
|
|
@@ -515,12 +515,12 @@ function defaultProveFailFirst(check, cwd, timeoutMs) {
|
|
|
515
515
|
// a setup crash, not from the prohibition firing. Requiring the fixture to exist before spawning
|
|
516
516
|
// closes the realistic typo/stale-path case (#1279 review, Major 1).
|
|
517
517
|
//
|
|
518
|
-
// CAUSATION (#1346): existence + a non-vacuous red is necessary but not
|
|
519
|
-
// negative test that reds merely BECAUSE `GSD_PROHIB_SUBJECT` is set
|
|
520
|
-
// subject's CONTENT violates the must-NOT) would otherwise be accepted.
|
|
521
|
-
// control below proves content-dependence
|
|
522
|
-
//
|
|
523
|
-
//
|
|
518
|
+
// CAUSATION (#1346; MANDATORY as of #1906): existence + a non-vacuous red is necessary but not
|
|
519
|
+
// sufficient — a deceptive negative test that reds merely BECAUSE `GSD_PROHIB_SUBJECT` is set
|
|
520
|
+
// (rather than because the subject's CONTENT violates the must-NOT) would otherwise be accepted.
|
|
521
|
+
// The `cleanFixture` control below proves content-dependence (red on bad AND green on clean) and is
|
|
522
|
+
// now REQUIRED for the node-test kind: absent it, the check is un-provable (fail-closed), not
|
|
523
|
+
// accepted under the weaker violation-only proof (#1906 supersedes #1346's opt-in; ADR-1606 D4).
|
|
524
524
|
// Resolve the fixture against `cwd` (NOT the verify process's cwd): the spawned test reads
|
|
525
525
|
// `GSD_PROHIB_SUBJECT` and resolves a relative subject against `cwd`, so the existence check must
|
|
526
526
|
// use the SAME base or it could pass here yet ENOENT in the child (re-opening the fail-open hole).
|
|
@@ -530,20 +530,22 @@ function defaultProveFailFirst(check, cwd, timeoutMs) {
|
|
|
530
530
|
const redOut = runNodeTestWithSubject(check, cwd, fixture, timeoutMs);
|
|
531
531
|
if (!isNonVacuousNodeTestRed(redOut, check.target))
|
|
532
532
|
return { provenFailFirst: false, method: 'violation-fixture' };
|
|
533
|
-
// #
|
|
534
|
-
//
|
|
535
|
-
// CONTENT — a deceptive test that reds merely
|
|
536
|
-
// not content-dependent → not proven.
|
|
533
|
+
// #1906 CAUSATION CONTROL (MANDATORY for node-test — supersedes #1346's opt-in, ADR-1606 D4): the
|
|
534
|
+
// clean control subject is REQUIRED. Run the SAME test against it and require it to stay GREEN,
|
|
535
|
+
// proving the red above was caused by the subject's CONTENT — a deceptive test that reds merely
|
|
536
|
+
// because GSD_PROHIB_SUBJECT is SET reds here too → not content-dependent → not proven. ABSENT →
|
|
537
|
+
// the control cannot run → un-provable → fail-closed (NOT accepted under the weaker violation-only
|
|
538
|
+
// proof). This is the one behavior change vs #1346: absent `cleanFixture` was previously proven.
|
|
537
539
|
const clean = check.cleanFixture;
|
|
538
|
-
if (clean)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
540
|
+
if (!clean)
|
|
541
|
+
return { provenFailFirst: false, method: 'violation-fixture' };
|
|
542
|
+
// A supplied-but-missing/typo'd control path can't run the control → fail-closed, symmetric
|
|
543
|
+
// with the violation-fixture existence guard (resolve against the SAME `cwd` as the child).
|
|
544
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.resolve(cwd, clean)))
|
|
545
|
+
return { provenFailFirst: false, method: 'violation-fixture' };
|
|
546
|
+
const cleanOut = runNodeTestWithSubject(check, cwd, clean, timeoutMs);
|
|
547
|
+
if (!isNonVacuousNodeTestPass(cleanOut, check.target))
|
|
548
|
+
return { provenFailFirst: false, method: 'violation-fixture' };
|
|
547
549
|
return { provenFailFirst: true, method: 'violation-fixture' };
|
|
548
550
|
}
|
|
549
551
|
// Unknown kind — defensive; the LOCATE guard already rejects it.
|
|
@@ -46,9 +46,10 @@ function checkW021(content) {
|
|
|
46
46
|
// Capture the major integer.
|
|
47
47
|
const MILESTONE_RE = /^#{1,3}\s+(?:\[[^\]]+\]\s+|Roadmap\s+|[✅🚧]\s*)?v(\d+)\.\d+(?:\s|:|\s*—)/iu;
|
|
48
48
|
// Migrated phase heading: ### Phase M-NN: Name (M-NN or unpadded M-N form)
|
|
49
|
-
|
|
49
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
50
|
+
const PHASE_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+)-(\d+)(?:-\d+)*(?:\s*\([^)\n]*\))?\s*:/i;
|
|
50
51
|
// Unprefixed legacy phase heading: ### Phase N: Name (no hyphen sub-index)
|
|
51
|
-
const UNPREFIXED_PHASE_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Za-z]?(?:\.\d+)*)\s*:/i;
|
|
52
|
+
const UNPREFIXED_PHASE_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Za-z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:/i;
|
|
52
53
|
let currentMilestoneMajor = null;
|
|
53
54
|
const lines = content.split('\n');
|
|
54
55
|
for (const line of lines) {
|