@opengsd/gsd-core 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -47,7 +47,7 @@ const { planningDir, planningRoot, resolveEnvWorkstream, resolvePhaseIdConventio
|
|
|
47
47
|
// Naming readers and writers share the loader's scoped-key precedence and defaults.
|
|
48
48
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
49
|
const configLoaderModule = require("./config-loader.cjs");
|
|
50
|
-
const { CONFIG_DEFAULTS, _deepMergeConfig, _getConfigValue, loadConfig } = configLoaderModule;
|
|
50
|
+
const { CONFIG_DEFAULTS, _deepMergeConfig, _getConfigValue, loadConfig, readConfigFileBounded } = configLoaderModule;
|
|
51
51
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
53
|
const frontmatterModule = require("./frontmatter.cjs");
|
|
@@ -253,7 +253,12 @@ function phaseDirectoryOptions(cwd, config, roadmap, ws) {
|
|
|
253
253
|
if (naming === undefined) {
|
|
254
254
|
const readConfig = (file) => {
|
|
255
255
|
try {
|
|
256
|
-
|
|
256
|
+
// Bounded read (#5759): null is an absent file (no configError); a FIFO/device/oversized
|
|
257
|
+
// file throws and is reported through configError like any other unreadable config.
|
|
258
|
+
const raw = readConfigFileBounded(file);
|
|
259
|
+
if (raw === null)
|
|
260
|
+
return null;
|
|
261
|
+
const value = JSON.parse(raw);
|
|
257
262
|
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
258
263
|
return value;
|
|
259
264
|
configError = { path: file, message: 'configuration must be a JSON object' };
|
|
@@ -305,7 +310,7 @@ function phaseDirectoryOptions(cwd, config, roadmap, ws) {
|
|
|
305
310
|
// Windows may omit these flags: lstat/fstat still require a regular file,
|
|
306
311
|
// but the open is not atomically no-follow there. Adopt Windows no-follow
|
|
307
312
|
// flags here if Node exposes them; no caller should weaken these checks.
|
|
308
|
-
identityFd = node_fs_1.default.openSync(markerPath, node_fs_1.default.constants.O_RDONLY | node_fs_1.default.constants.O_NONBLOCK | node_fs_1.default.constants.O_NOFOLLOW);
|
|
313
|
+
identityFd = node_fs_1.default.openSync(markerPath, node_fs_1.default.constants.O_RDONLY | node_fs_1.default.constants.O_NONBLOCK | node_fs_1.default.constants.O_NOFOLLOW | (node_fs_1.default.constants.O_NOCTTY ?? 0));
|
|
309
314
|
const opened = node_fs_1.default.fstatSync(identityFd);
|
|
310
315
|
if (!opened.isFile() || opened.size > 1024)
|
|
311
316
|
continue;
|
|
@@ -42,7 +42,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
42
42
|
const { toPosixPath, generateSlugInternal, readSubdirectories, extractCanonicalPlanId, findUnsummarizedPlans, } = coreUtilsMod;
|
|
43
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
44
44
|
const phaseIdMod = require("./phase-id.cjs");
|
|
45
|
-
const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhaseDirs, phaseLookupQuery, phaseNumberForMatch, phaseKeyFromDir, customPhaseDirectoryParts, customDirectoryOwner, phaseScanTokenSource, CUSTOM_PHASE_ID_FILE, isCustomPhaseId, isSentinelPhaseId, scopeToPhase, parsePhaseId, renderPhaseId, renderMilestoneId, toDir, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, buildPhaseDeclarationScans, BRACKET_DIR_PREFIX_SRC, tokenizePhaseDependencyReferences, foldBracketId, } = phaseIdMod;
|
|
45
|
+
const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhaseDirs, phaseLookupQuery, phaseNumberForMatch, phaseKeyFromDir, customPhaseDirectoryParts, customDirectoryOwner, phaseScanTokenSource, phaseNumberTokenSource, CUSTOM_PHASE_ID_FILE, isCustomPhaseId, isSentinelPhaseId, scopeToPhase, parsePhaseId, renderPhaseId, renderMilestoneId, toDir, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, buildPhaseDeclarationScans, BRACKET_DIR_PREFIX_SRC, tokenizePhaseDependencyReferences, foldBracketId, } = phaseIdMod;
|
|
46
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
47
|
const phaseIdDisplayMod = require("./phase-id-display.cjs");
|
|
48
48
|
// #4304: reuse the Phase Id Display Module's milestone/phase
|
|
@@ -51,6 +51,7 @@ const phaseIdDisplayMod = require("./phase-id-display.cjs");
|
|
|
51
51
|
const { milestoneToken, phaseToken } = phaseIdDisplayMod;
|
|
52
52
|
const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
|
|
53
53
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
54
|
+
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
54
55
|
const gate_exit_cjs_1 = require("./gate-exit.cjs");
|
|
55
56
|
const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
56
57
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
@@ -61,7 +62,10 @@ const planningScopeMod = require("./planning-scope.cjs");
|
|
|
61
62
|
const { SCOPE } = planningScopeMod;
|
|
62
63
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- roadmap-parser.cjs is an export= CommonJS module
|
|
63
64
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
64
|
-
const { stripShippedMilestones, extractCurrentMilestone, currentMilestoneRawRanges, withPhaseSection, editCurrentMilestone, findMilestoneScopeHeadingLines, getMilestoneInfo,
|
|
65
|
+
const { stripShippedMilestones, extractCurrentMilestone, extractCurrentMilestoneScoped, currentMilestoneRawRanges, withPhaseSection, editCurrentMilestone, findMilestoneScopeHeadingLines, getMilestoneInfo,
|
|
66
|
+
// #5679: the STATE.md milestone read getMilestoneInfo itself uses, and the
|
|
67
|
+
// archive-aware owner of a ROADMAP offset.
|
|
68
|
+
readStateMilestoneScalar, classifyMilestoneScalar, milestoneVersionAtOffset, blankCodeAndComments, scanMilestonePhaseIds,
|
|
65
69
|
// #4304: every recognized milestone heading in the document,
|
|
66
70
|
// not just the active one — see bracketProgressSectionOwnedByOtherMilestone.
|
|
67
71
|
listMilestoneHeadings, selectMilestoneHeading,
|
|
@@ -94,7 +98,7 @@ const planning_document_cjs_1 = require("./planning-document.cjs");
|
|
|
94
98
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
95
99
|
const clock_cjs_1 = require("./clock.cjs");
|
|
96
100
|
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
97
|
-
const
|
|
101
|
+
const markdown_table_cjs_2 = require("./markdown-table.cjs");
|
|
98
102
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
99
103
|
const security_cjs_1 = require("./security.cjs");
|
|
100
104
|
// #5060: the Phase Status Module owns the ROADMAP Status-cell token vocabulary.
|
|
@@ -496,6 +500,9 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
496
500
|
const directoryContext = phaseDirectoryOptions(cwd);
|
|
497
501
|
const lookup = resolvePhaseDirectoryLookup(cwd, phase, undefined, directoryContext);
|
|
498
502
|
const { normalized, convention } = lookup;
|
|
503
|
+
// #5676: `lookup.convention` is only ever 'bracket'; the directory token grammar
|
|
504
|
+
// (`03-02-beta` -> `03-02` + `beta`) needs the repo's own convention.
|
|
505
|
+
const tokenConvention = resolvePhaseIdConvention(cwd);
|
|
499
506
|
const notFound = {
|
|
500
507
|
found: false,
|
|
501
508
|
directory: null,
|
|
@@ -561,7 +568,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
561
568
|
&& BRACKET_DIRECTORY_PREFIX_RE.test(foldBracketId(match));
|
|
562
569
|
const dirMatch = isBracketDirectory
|
|
563
570
|
? null
|
|
564
|
-
: match.match(new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}(${
|
|
571
|
+
: match.match(new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}(${phaseNumberTokenSource(tokenConvention)})-?(.*)`, 'i')) || match.match(new RegExp(`^(${phaseNumberTokenSource(tokenConvention)})-?(.*)`, 'i'));
|
|
565
572
|
const customParts = custom ? customPhaseDirectoryParts(match, custom) : null;
|
|
566
573
|
const phaseNumber = isBracketDirectory
|
|
567
574
|
? phaseNumberForMatch(match, usedBareFallback, convention)
|
|
@@ -1244,7 +1251,7 @@ function recordNewPhase(raw, cwd, id, description, after, label) {
|
|
|
1244
1251
|
if (findPhaseTableHeader(scoped) === -1)
|
|
1245
1252
|
return scoped;
|
|
1246
1253
|
const cell = { Phase: `${id}. ${description}`, 'Plans Complete': '0/0', Status: 'Not started' };
|
|
1247
|
-
const r = (0,
|
|
1254
|
+
const r = (0, markdown_table_cjs_2.insertTableRow)(scoped, (col) => cell[col]);
|
|
1248
1255
|
if (!r.ok)
|
|
1249
1256
|
return scoped;
|
|
1250
1257
|
wrote = true;
|
|
@@ -1635,6 +1642,54 @@ function assertBracketGeneratedIdentity(command, config, customId) {
|
|
|
1635
1642
|
'Custom naming precedence under the bracket convention lands with #5067.');
|
|
1636
1643
|
}
|
|
1637
1644
|
}
|
|
1645
|
+
const STATE_MILESTONE_ECHO_LIMIT = 80;
|
|
1646
|
+
/**
|
|
1647
|
+
* #5726: STATE.md's `milestone:` value is untrusted text. Cap it at
|
|
1648
|
+
* `STATE_MILESTONE_ECHO_LIMIT` characters (ellipsis past the cap), then quote it
|
|
1649
|
+
* with `JSON.stringify` (the `phase.cts` precedent for echoed user text), which
|
|
1650
|
+
* escapes ESC/BEL/newlines; the characters JSON leaves raw but a terminal acts on
|
|
1651
|
+
* are escaped too: DEL and C1 controls, zero-width and direction marks
|
|
1652
|
+
* (U+200B-U+200F), line/paragraph separators and bidi embeddings/overrides
|
|
1653
|
+
* (U+2028-U+202E), the invisible-format block (U+2060-U+206F, incl. bidi
|
|
1654
|
+
* isolates), soft hyphen (U+00AD), Arabic letter mark (U+061C), Mongolian
|
|
1655
|
+
* vowel separator (U+180E), interlinear annotations (U+FFF9-U+FFFB) and the
|
|
1656
|
+
* BOM (U+FEFF).
|
|
1657
|
+
*/
|
|
1658
|
+
function echoStateMilestone(value) {
|
|
1659
|
+
const chars = Array.from(value);
|
|
1660
|
+
const capped = chars.length > STATE_MILESTONE_ECHO_LIMIT
|
|
1661
|
+
? `${chars.slice(0, STATE_MILESTONE_ECHO_LIMIT).join('')}…`
|
|
1662
|
+
: value;
|
|
1663
|
+
return JSON.stringify(capped).replace(/[\u007f-\u009f\u{ad}\u{61c}\u{180e}\u{200b}-\u{200f}\u{2028}-\u{202e}\u{2060}-\u{206f}\u{feff}\u{fff9}-\u{fffb}]/gu, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* #5726: the current-milestone window for a verb that WRITES to the ROADMAP.
|
|
1667
|
+
* When STATE.md names a milestone that has no heading in a ROADMAP carrying
|
|
1668
|
+
* versioned milestones, the window owner falls back to the whole unshipped
|
|
1669
|
+
* roadmap (scope UNSCOPED, `unresolvedMilestone` set), and number allocation,
|
|
1670
|
+
* the `Depends on` line and the insert target would run over every milestone.
|
|
1671
|
+
* ADR-3180: that is a non-answer, refused where it is destructive. Refuses
|
|
1672
|
+
* before any write, naming the milestone; every other scope (a real heading,
|
|
1673
|
+
* a free-form legacy ROADMAP, no milestone field) returns the window unchanged.
|
|
1674
|
+
* Read-only readers keep calling `extractCurrentMilestone` and keep degrading.
|
|
1675
|
+
*/
|
|
1676
|
+
function currentMilestoneWindowOrRefuse(rawContent, cwd, command) {
|
|
1677
|
+
const { value, unresolvedMilestone, shippedMilestone } = extractCurrentMilestoneScoped(rawContent, cwd);
|
|
1678
|
+
if (shippedMilestone) {
|
|
1679
|
+
error(`${command}: STATE.md names milestone ${echoStateMilestone(shippedMilestone)}, which ROADMAP.md marks as already shipped, `
|
|
1680
|
+
+ 'so the current milestone window is a closed one. Refusing to number the phase or link its dependencies '
|
|
1681
|
+
+ 'against a shipped milestone. Start the next milestone (or correct the milestone field in STATE.md), then retry. '
|
|
1682
|
+
+ 'If the heading is not shipped and its title merely contains a status word (such as "Failed Payment Recovery"), '
|
|
1683
|
+
+ 'rename the milestone, or add an active marker to the heading, for example \u{1f6a7} or "(in progress)".');
|
|
1684
|
+
}
|
|
1685
|
+
if (unresolvedMilestone) {
|
|
1686
|
+
error(`${command}: STATE.md names milestone ${echoStateMilestone(unresolvedMilestone)}, but ROADMAP.md has no milestone heading for it, `
|
|
1687
|
+
+ 'so the current milestone window cannot be resolved. Refusing to use the whole roadmap, which would number '
|
|
1688
|
+
+ 'the phase and link its dependencies against other milestones. Add the milestone heading to ROADMAP.md '
|
|
1689
|
+
+ 'or correct the milestone field in STATE.md, then retry.');
|
|
1690
|
+
}
|
|
1691
|
+
return value;
|
|
1692
|
+
}
|
|
1638
1693
|
/**
|
|
1639
1694
|
* #4304: refuse a bracket `phase add` / `add-batch` whose located milestone
|
|
1640
1695
|
* window is a decoy. A non-milestone heading carrying the active version
|
|
@@ -1706,9 +1761,9 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1706
1761
|
const bracketContext = convention === 'bracket' ? bracketWriteContext(cwd, config) : null;
|
|
1707
1762
|
const { newPhaseId, dirName, roadmapEntry } = withPlanningLock(cwd, () => {
|
|
1708
1763
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1764
|
+
const content = currentMilestoneWindowOrRefuse(rawContent, cwd, 'phase add');
|
|
1709
1765
|
if (bracketContext)
|
|
1710
1766
|
assertBracketAddWindowLocated(rawContent, cwd, bracketContext, 'phase add');
|
|
1711
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1712
1767
|
let _newPhaseId;
|
|
1713
1768
|
let _dirName = '';
|
|
1714
1769
|
if (bracketContext) {
|
|
@@ -1815,9 +1870,9 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1815
1870
|
const bracketContext = convention === 'bracket' ? bracketWriteContext(cwd, config) : null;
|
|
1816
1871
|
const results = withPlanningLock(cwd, () => {
|
|
1817
1872
|
let rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1873
|
+
const content = currentMilestoneWindowOrRefuse(rawContent, cwd, 'phase add-batch');
|
|
1818
1874
|
if (bracketContext)
|
|
1819
1875
|
assertBracketAddWindowLocated(rawContent, cwd, bracketContext, 'phase add-batch');
|
|
1820
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1821
1876
|
let maxPhase = 0;
|
|
1822
1877
|
if (bracketContext) {
|
|
1823
1878
|
const used = collectBracketPhaseNumbers(content, node_path_1.default.join(planningDir(cwd), 'phases'), bracketContext);
|
|
@@ -2187,6 +2242,34 @@ function isPhaseTokenWithin(token, ancestor) {
|
|
|
2187
2242
|
const canon = (segment) => segment.replace(/^0+(?=\d)/, '').toLowerCase();
|
|
2188
2243
|
return a.every((segment, i) => canon(segment) === canon(t[i]));
|
|
2189
2244
|
}
|
|
2245
|
+
/**
|
|
2246
|
+
* #5679: the `phase insert` error for a phase that exists in the ROADMAP but
|
|
2247
|
+
* outside the current milestone. Names the milestone that holds it (archive
|
|
2248
|
+
* aware, never the current one) and the current milestone, saying "STATE.md's"
|
|
2249
|
+
* only when STATE.md's `milestone:` is where that version came from.
|
|
2250
|
+
*/
|
|
2251
|
+
function otherMilestonePhaseMessage(rawContent, foundAt, cwd, afterPhase) {
|
|
2252
|
+
let stateVersion = null;
|
|
2253
|
+
try {
|
|
2254
|
+
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'STATE.md'));
|
|
2255
|
+
const scalar = stateRaw === null ? null : readStateMilestoneScalar(stateRaw);
|
|
2256
|
+
if (scalar !== null)
|
|
2257
|
+
stateVersion = classifyMilestoneScalar(scalar).version;
|
|
2258
|
+
}
|
|
2259
|
+
catch { /* unreadable STATE.md: getMilestoneInfo's own fallback below decides */ }
|
|
2260
|
+
const derivedVersion = getMilestoneInfo(cwd).value?.version ?? null;
|
|
2261
|
+
const current = stateVersion ?? derivedVersion;
|
|
2262
|
+
const owner = milestoneVersionAtOffset(rawContent, foundAt);
|
|
2263
|
+
const ownerClause = owner && owner.toLowerCase() !== current?.toLowerCase()
|
|
2264
|
+
? `is in milestone ${owner}`
|
|
2265
|
+
: 'is outside the current milestone';
|
|
2266
|
+
let currentClause = 'the current milestone does not include it';
|
|
2267
|
+
if (stateVersion)
|
|
2268
|
+
currentClause = `STATE.md's current milestone is ${stateVersion}`;
|
|
2269
|
+
else if (derivedVersion)
|
|
2270
|
+
currentClause = `the current milestone is ${derivedVersion}`;
|
|
2271
|
+
return `Phase ${afterPhase} ${ownerClause}, but ${currentClause}. phase insert only targets phases in the current milestone.`;
|
|
2272
|
+
}
|
|
2190
2273
|
function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested') {
|
|
2191
2274
|
if (!afterPhase || !description) {
|
|
2192
2275
|
error('after-phase and description required for phase insert');
|
|
@@ -2202,7 +2285,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
2202
2285
|
const bracketContext = convention === 'bracket' ? bracketWriteContext(cwd, insertConfig) : null;
|
|
2203
2286
|
const { decimalPhase, dirName, roadmapEntry } = withPlanningLock(cwd, () => {
|
|
2204
2287
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
2205
|
-
const content =
|
|
2288
|
+
const content = currentMilestoneWindowOrRefuse(rawContent, cwd, 'phase insert');
|
|
2206
2289
|
// #4304: canonicalize a bracket argument
|
|
2207
2290
|
// through the SAME adapter `phase remove` uses
|
|
2208
2291
|
// (canonicalizeBracketPhaseArgument) instead of the legacy
|
|
@@ -2257,7 +2340,8 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
2257
2340
|
const anyHeadingPattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, insertConvention)}\\d`, 'i');
|
|
2258
2341
|
const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
|
|
2259
2342
|
// #4304: bracket identities live in headings only, so a bracket ROADMAP never takes the legacy bullet-insertion branch — a bullet-only bracket ROADMAP falls through to the checklist-refusal path below instead.
|
|
2260
|
-
const
|
|
2343
|
+
const liveCurrent = blankCodeAndComments(content);
|
|
2344
|
+
const isBulletStyle = !bracketContext && !headingMatch && bulletPattern.test(liveCurrent) && !roadmapHasHeadingPhases;
|
|
2261
2345
|
if (bracketContext && !bracketHeading) {
|
|
2262
2346
|
const searchedRanges = bracketSectionRanges?.details
|
|
2263
2347
|
? 'primary and Phase Details ranges'
|
|
@@ -2266,9 +2350,19 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
2266
2350
|
}
|
|
2267
2351
|
if (!headingMatch && !isBulletStyle) {
|
|
2268
2352
|
const checklistPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`${headingIntro}${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
2269
|
-
if (checklistPattern.test(
|
|
2353
|
+
if (checklistPattern.test(liveCurrent)) {
|
|
2270
2354
|
error(`Phase ${afterPhase} exists in roadmap summary but is missing a detail section (### Phase ${afterPhase}: ...).`);
|
|
2271
2355
|
}
|
|
2356
|
+
// #5679: this whole-file probe only chooses the error message; insert
|
|
2357
|
+
// stays current-milestone-only and never writes to another milestone.
|
|
2358
|
+
// Fenced blocks and HTML comments are masked (same length, offsets stay aligned) so example text never counts as a phase.
|
|
2359
|
+
const liveContent = blankCodeAndComments(rawContent);
|
|
2360
|
+
const headingOffset = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(liveContent).find((h) => targetPattern.test(h.text))?.offset;
|
|
2361
|
+
const checklistOffset = checklistPattern.exec(liveContent)?.index;
|
|
2362
|
+
const foundOffsets = [headingOffset, checklistOffset].filter((o) => o !== undefined);
|
|
2363
|
+
if (foundOffsets.length > 0) {
|
|
2364
|
+
error(otherMilestonePhaseMessage(rawContent, Math.min(...foundOffsets), cwd, afterPhase));
|
|
2365
|
+
}
|
|
2272
2366
|
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
|
2273
2367
|
}
|
|
2274
2368
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
@@ -2333,7 +2427,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
2333
2427
|
let roadmapEntry;
|
|
2334
2428
|
if (isBulletStyle) {
|
|
2335
2429
|
const boldBulletPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i', { bold: 'required' });
|
|
2336
|
-
const useBold = boldBulletPattern.test(
|
|
2430
|
+
const useBold = boldBulletPattern.test(liveCurrent);
|
|
2337
2431
|
const phaseLabel = useBold
|
|
2338
2432
|
? `**Phase ${_decimalPhase}: ${description}**`
|
|
2339
2433
|
: `Phase ${_decimalPhase}: ${description}`;
|
|
@@ -2356,11 +2450,12 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
2356
2450
|
// true line-content boundary so bulletLineEnd lands cleanly before the
|
|
2357
2451
|
// terminator.
|
|
2358
2452
|
const targetBulletPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\r\\n]*`, 'i');
|
|
2359
|
-
|
|
2453
|
+
// Match against the masked text (same length, offsets aligned) so a fenced or commented example bullet is never the splice anchor; splice into the original text at the masked match index.
|
|
2454
|
+
const bulletMatchResult = blankCodeAndComments(rawContent).match(targetBulletPattern);
|
|
2360
2455
|
if (!bulletMatchResult) {
|
|
2361
2456
|
error(`Could not find Phase ${afterPhase} bullet line`);
|
|
2362
2457
|
}
|
|
2363
|
-
const bulletLineEnd =
|
|
2458
|
+
const bulletLineEnd = bulletMatchResult.index + bulletMatchResult[0].length;
|
|
2364
2459
|
const afterBullet = rawContent.slice(bulletLineEnd);
|
|
2365
2460
|
// #5599: land after the last bullet inside the allocation family's
|
|
2366
2461
|
// subtree, not before the first following bullet of any number (which is
|
|
@@ -2791,7 +2886,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
2791
2886
|
return parseInt(leadingMatch[1], 10) === removedInt;
|
|
2792
2887
|
};
|
|
2793
2888
|
content = (0, roadmap_progress_cjs_1.editRoadmapProgress)(content, (progressSection) => {
|
|
2794
|
-
const deleteResult = (0,
|
|
2889
|
+
const deleteResult = (0, markdown_table_cjs_2.deleteTableRow)(progressSection, matchRemovedProgressRow);
|
|
2795
2890
|
return deleteResult.ok ? deleteResult.value : progressSection;
|
|
2796
2891
|
}, { headingless: 'skip' });
|
|
2797
2892
|
if (!isDecimal && !customTarget) {
|
|
@@ -2871,7 +2966,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
2871
2966
|
let matchedRowIndex = null;
|
|
2872
2967
|
for (;;) {
|
|
2873
2968
|
matchedRowIndex = null;
|
|
2874
|
-
const cellResult = (0,
|
|
2969
|
+
const cellResult = (0, markdown_table_cjs_2.updateTableCell)(ordinalSection, (row, index) => {
|
|
2875
2970
|
if (processedOrdinalRows.has(index))
|
|
2876
2971
|
return false;
|
|
2877
2972
|
const m = phaseCellShapeRe.exec(row['Phase'] ?? '');
|
|
@@ -2892,11 +2987,11 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
2892
2987
|
const newContent = `${decremented}${m[2]}${current.slice(m[0].length)}`;
|
|
2893
2988
|
const targetLine = matchedRowIndex === null ? null : findDataRowLine(ordinalSection, matchedRowIndex);
|
|
2894
2989
|
const padMatch = targetLine
|
|
2895
|
-
? new RegExp(`^[ \\t]*\\|(\\s*)${(0, pattern_cjs_1.escapeRegex)((0,
|
|
2990
|
+
? new RegExp(`^[ \\t]*\\|(\\s*)${(0, pattern_cjs_1.escapeRegex)((0, markdown_table_cjs_2.escapeCell)(current))}(\\s*)\\|`).exec(targetLine)
|
|
2896
2991
|
: null;
|
|
2897
2992
|
const leadPad = padMatch ? padMatch[1] : ' ';
|
|
2898
2993
|
const trailPad = padMatch ? padMatch[2] : ' ';
|
|
2899
|
-
return `${leadPad}${(0,
|
|
2994
|
+
return `${leadPad}${(0, markdown_table_cjs_2.escapeCell)(newContent)}${trailPad}`;
|
|
2900
2995
|
});
|
|
2901
2996
|
if (!cellResult.ok)
|
|
2902
2997
|
break;
|
|
@@ -3486,7 +3581,7 @@ function classifyBracketOwnedLine(line) {
|
|
|
3486
3581
|
if (checklistId)
|
|
3487
3582
|
return { kind: 'checklist', id: checklistId };
|
|
3488
3583
|
if (/^[ \t]*\|/.test(line)) {
|
|
3489
|
-
const firstCell = (0,
|
|
3584
|
+
const firstCell = (0, markdown_table_cjs_2.splitTableRow)(line)[0]?.replace(/^\*\*(.*)\*\*$/, '$1') ?? '';
|
|
3490
3585
|
const progressId = phaseIdFromOwnedLineMatch(BRACKET_CELL_ID_RE.exec(firstCell));
|
|
3491
3586
|
if (progressId)
|
|
3492
3587
|
return { kind: 'progress', id: progressId };
|
|
@@ -4848,7 +4943,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
|
4848
4943
|
const heading = matchedHeading.text.match(headingPattern);
|
|
4849
4944
|
if (!heading)
|
|
4850
4945
|
return null;
|
|
4851
|
-
const name =
|
|
4946
|
+
const name = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(heading[1]);
|
|
4852
4947
|
return name || null;
|
|
4853
4948
|
}
|
|
4854
4949
|
function phaseDisplayNameFromSlug(slug) {
|
|
@@ -6027,12 +6122,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6027
6122
|
// (`editProgressTableSlice` -> `editRoadmapProgress`), the same one `roadmap update-plan-progress` uses.
|
|
6028
6123
|
s = editProgressTableSlice(s, (scoped) => {
|
|
6029
6124
|
let text = scoped;
|
|
6030
|
-
const plansResult = (0,
|
|
6125
|
+
const plansResult = (0, markdown_table_cjs_2.updateTableCell)(text, rowMatch, 'Plans Complete', ` ${summaryCount}/${planCount} `);
|
|
6031
6126
|
if (plansResult.ok)
|
|
6032
6127
|
text = plansResult.value;
|
|
6033
6128
|
// #5165: a Status cell that already reads Complete is left untouched —
|
|
6034
6129
|
// re-padding it rewrote a cell the re-run had no business editing.
|
|
6035
|
-
const statusResult = (0,
|
|
6130
|
+
const statusResult = (0, markdown_table_cjs_2.updateTableCell)(text, rowMatch, 'Status', (current) => (0, phase_status_cjs_1.parseRoadmapStatusCell)(current) === phase_status_cjs_1.WIRE_STATUS.COMPLETE
|
|
6036
6131
|
? current
|
|
6037
6132
|
: ` ${(0, phase_status_cjs_1.toRoadmapStatusCell)(phase_status_cjs_1.PHASE_STATUS.COMPLETE).padEnd(11)} `);
|
|
6038
6133
|
if (statusResult.ok)
|
|
@@ -6044,7 +6139,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6044
6139
|
// findTableWithColumns (which requires the WHOLE table to parse —
|
|
6045
6140
|
// a ragged SIBLING row elsewhere used to silently no-op this
|
|
6046
6141
|
// row's date stamp too).
|
|
6047
|
-
const completedResult = (0,
|
|
6142
|
+
const completedResult = (0, markdown_table_cjs_2.updateTableCell)(text, rowMatch, 'Completed', (current) => dateShape.test(current.trim()) ? current : ` ${today} `);
|
|
6048
6143
|
if (completedResult.ok)
|
|
6049
6144
|
text = completedResult.value;
|
|
6050
6145
|
return text;
|
|
@@ -6182,7 +6277,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6182
6277
|
// checkbox rollback below can distinguish "row existed and accepted" from
|
|
6183
6278
|
// "row existed and rejected".
|
|
6184
6279
|
let tableHit = false;
|
|
6185
|
-
const reqUpdate = (0,
|
|
6280
|
+
const reqUpdate = (0, markdown_table_cjs_2.updateTraceabilityCell)(reqContent, reqRowMatch, 'Status', (current) => {
|
|
6186
6281
|
// #2788: accept `Gaps Found` too so a phase stranded by revert-phase (the
|
|
6187
6282
|
// gaps_found response) can complete without hand-editing the table.
|
|
6188
6283
|
if (/^(?:pending|in progress|gaps found)$/i.test(current.trim())) {
|
|
@@ -6359,9 +6454,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6359
6454
|
// which section (deferred or not) it lives under.
|
|
6360
6455
|
const reqIsRegisteredAnywhere = (id) => {
|
|
6361
6456
|
const reqEscaped = (0, pattern_cjs_1.escapeRegex)(id);
|
|
6362
|
-
// Surface 1 — checkbox,
|
|
6363
|
-
// insensitive: existence check, not the write's space-only match.
|
|
6364
|
-
if (new RegExp(`-\\s*\\[[
|
|
6457
|
+
// Surface 1 — checkbox, ANY single mark (`[ ]`, `[x]`, `[~]`, …; #5844),
|
|
6458
|
+
// case-insensitive: existence check, not the write's space-only match.
|
|
6459
|
+
if (new RegExp(`-\\s*\\[[^\\]\\r\\n]\\]\\s*\\*\\*${reqEscaped}\\*\\*`, 'i').test(reqContent)) {
|
|
6365
6460
|
return true;
|
|
6366
6461
|
}
|
|
6367
6462
|
// Surface 2 — Traceability row exists at all (any Status value),
|
|
@@ -6369,7 +6464,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6369
6464
|
// technique milestone.cts's `hasRow` uses (:210-214): a case-
|
|
6370
6465
|
// insensitive first-cell match, regardless of current Status.
|
|
6371
6466
|
const rowProbeMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === id.toLowerCase();
|
|
6372
|
-
|
|
6467
|
+
// #5844: a table with no Status column fails the probe above with
|
|
6468
|
+
// `unknown column`, so row existence is also read directly.
|
|
6469
|
+
return (0, markdown_table_cjs_2.updateTraceabilityCell)(reqContent, rowProbeMatch, 'Status', (current) => current).ok
|
|
6470
|
+
|| (0, markdown_table_cjs_1.traceabilityRowExists)(reqContent, id);
|
|
6373
6471
|
};
|
|
6374
6472
|
const ghostReqIds = citedReqIds.filter((id) => !isPlaceholderReqId(id) && !reqIsRegisteredAnywhere(id));
|
|
6375
6473
|
if (ghostReqIds.length > 0) {
|
|
@@ -6381,8 +6479,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6381
6479
|
// IDs already reported above as ghosts to avoid a duplicate message
|
|
6382
6480
|
// for the same root cause.
|
|
6383
6481
|
const traceabilityWriteFailures = traceabilityWriteMisses.filter((id) => !ghostReqIds.includes(id));
|
|
6384
|
-
|
|
6385
|
-
|
|
6482
|
+
// #5844: a row that EXISTS but has no Status cell to write is not a
|
|
6483
|
+
// missing row — name the real cause instead of blaming the row.
|
|
6484
|
+
const noStatusCellIds = traceabilityWriteFailures.filter((id) => (0, markdown_table_cjs_1.traceabilityRowExists)(reqContent, id));
|
|
6485
|
+
const noRowIds = traceabilityWriteFailures.filter((id) => !noStatusCellIds.includes(id));
|
|
6486
|
+
if (noRowIds.length > 0) {
|
|
6487
|
+
warnings.push(`REQUIREMENTS.md: Traceability row write skipped for REQ-ID(s) cited by ROADMAP (no matching row found): ${noRowIds.join(', ')}`);
|
|
6488
|
+
}
|
|
6489
|
+
if (noStatusCellIds.length > 0) {
|
|
6490
|
+
warnings.push(`REQUIREMENTS.md: Traceability row write skipped for REQ-ID(s) cited by ROADMAP (row found but the table has no Status column to write): ${noStatusCellIds.join(', ')}`);
|
|
6386
6491
|
}
|
|
6387
6492
|
writes.push({ filePath: reqPath, before: originalReqContent, after: reqContent });
|
|
6388
6493
|
// #2316-3: `requirements_updated` must reflect whether REQUIREMENTS.md
|
|
@@ -6424,6 +6529,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6424
6529
|
// where the roadmap would give `2` and a slugified title. Promoting the
|
|
6425
6530
|
// roadmap without this would silently change the reported value on
|
|
6426
6531
|
// every aligned project, which is the majority case.
|
|
6532
|
+
// #5676: the numeric phase-token grammar follows the convention, so a
|
|
6533
|
+
// milestone-prefixed `03-02-beta` is phase `3-02`, not phase `3`.
|
|
6534
|
+
const phaseConvention = resolvePhaseIdConvention(cwd);
|
|
6427
6535
|
let diskNextNum = null;
|
|
6428
6536
|
let diskNextName = null;
|
|
6429
6537
|
let roadmapNextNum = null;
|
|
@@ -6442,7 +6550,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6442
6550
|
if (roadmapContent !== null) {
|
|
6443
6551
|
try {
|
|
6444
6552
|
const milestoneForComplete = extractCurrentMilestone(roadmapContent, cwd);
|
|
6445
|
-
const completePattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+(${phaseScanTokenSource(directoryContext)})`, 'gi');
|
|
6553
|
+
const completePattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+(${phaseScanTokenSource(directoryContext, phaseConvention)})`, 'gi');
|
|
6446
6554
|
let cm;
|
|
6447
6555
|
while ((cm = completePattern.exec(milestoneForComplete)) !== null) {
|
|
6448
6556
|
if (!(0, phase_entry_grammar_cjs_1.isPhaseEntryChecked)(cm[0]))
|
|
@@ -6472,7 +6580,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6472
6580
|
for (const dir of dirs) {
|
|
6473
6581
|
const custom = phaseIdMod.customPhaseQuery(dir, directoryContext);
|
|
6474
6582
|
const owner = custom && customDirectoryOwner(dir, custom);
|
|
6475
|
-
const dm = owner ? [dir, owner.id, owner.slug] : dir.match(new RegExp(`^(${
|
|
6583
|
+
const dm = owner ? [dir, owner.id, owner.slug] : dir.match(new RegExp(`^(${phaseNumberTokenSource(phaseConvention)})-?(.*)`, 'i'));
|
|
6476
6584
|
if (dm) {
|
|
6477
6585
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
6478
6586
|
if (isSentinelPhaseId(dm[1]))
|
|
@@ -6535,8 +6643,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6535
6643
|
// checkbox branch widens, and only to the separators #2199 already
|
|
6536
6644
|
// accepts. The two branches keep separate capture groups, normalized
|
|
6537
6645
|
// just below the loop.
|
|
6538
|
-
const phasePattern = new RegExp(`(?:#{2,4}\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${phaseScanTokenSource(directoryContext)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n*]+)` +
|
|
6539
|
-
`|${(0, phase_entry_grammar_cjs_1.phaseEntryPrefixSrc)()}Phase\\s+(${phaseScanTokenSource(directoryContext)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*[—–:\\-]\\s*([^\\n*]+))`, 'gim');
|
|
6646
|
+
const phasePattern = new RegExp(`(?:#{2,4}\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${phaseScanTokenSource(directoryContext, phaseConvention)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n*]+)` +
|
|
6647
|
+
`|${(0, phase_entry_grammar_cjs_1.phaseEntryPrefixSrc)()}Phase\\s+(${phaseScanTokenSource(directoryContext, phaseConvention)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*[—–:\\-]\\s*([^\\n*]+))`, 'gim');
|
|
6540
6648
|
let pm;
|
|
6541
6649
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
6542
6650
|
// #4078: normalize the two alternation branches' captures (heading
|
|
@@ -6570,9 +6678,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6570
6678
|
if (comparePhaseNum(pmNum, phaseNum, directoryContext) > 0
|
|
6571
6679
|
&& (roadmapNextNum === null || comparePhaseNum(pmNum, roadmapNextNum, directoryContext) < 0)) {
|
|
6572
6680
|
roadmapNextNum = pmNum;
|
|
6573
|
-
roadmapNextName = pmName
|
|
6574
|
-
.replace(/\(INSERTED\)/i, '')
|
|
6575
|
-
.trim()
|
|
6681
|
+
roadmapNextName = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(pmName)
|
|
6576
6682
|
.toLowerCase()
|
|
6577
6683
|
.replace(/\s+/g, '-');
|
|
6578
6684
|
}
|
|
@@ -6636,7 +6742,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6636
6742
|
// Without it, this lowest-outstanding override was blind to dash-grammar
|
|
6637
6743
|
// rows and could not correct an out-of-order completion on the same
|
|
6638
6744
|
// mixed-grammar roadmaps that broke stage 2.
|
|
6639
|
-
const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+(${phaseScanTokenSource(directoryContext)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*[—–:\\-]\\s*([^\\n*]+)`, 'gi', { state: 'captured' });
|
|
6745
|
+
const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+(${phaseScanTokenSource(directoryContext, phaseConvention)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*[—–:\\-]\\s*([^\\n*]+)`, 'gi', { state: 'captured' });
|
|
6640
6746
|
let cbm;
|
|
6641
6747
|
let lowestOutstanding = null;
|
|
6642
6748
|
while ((cbm = cbPattern.exec(milestoneScope)) !== null) {
|
|
@@ -6652,7 +6758,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6652
6758
|
if (lowestOutstanding === null || comparePhaseNum(cbm[2], lowestOutstanding.num, directoryContext) < 0) {
|
|
6653
6759
|
lowestOutstanding = {
|
|
6654
6760
|
num: cbm[2],
|
|
6655
|
-
name:
|
|
6761
|
+
name: (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(cbm[3]).toLowerCase().replace(/\s+/g, '-'),
|
|
6656
6762
|
};
|
|
6657
6763
|
}
|
|
6658
6764
|
}
|
|
@@ -6685,7 +6791,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6685
6791
|
// pin would all rewind it. A first-time completion (box unchecked, or
|
|
6686
6792
|
// STATE not past N, #2028 out-of-order) still transitions.
|
|
6687
6793
|
const stateAlreadyPast = phaseWasAlreadyComplete
|
|
6688
|
-
&& (0, state_transition_cjs_1.isStatePastPhase)(originalStateContent, phaseNum, statePath);
|
|
6794
|
+
&& (0, state_transition_cjs_1.isStatePastPhase)(originalStateContent, phaseNum, statePath, phaseConvention);
|
|
6689
6795
|
let stateContent = originalStateContent;
|
|
6690
6796
|
// ADR-1769 Phase 3: the STATE.md field-update policy (Current Phase
|
|
6691
6797
|
// shape/name, Status, Current Plan, Last Activity + Description, and
|
|
@@ -6715,7 +6821,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6715
6821
|
const nextInfo = findPhaseInternal(cwd, nextPhaseNum, undefined, directoryContext);
|
|
6716
6822
|
if (nextInfo && nextInfo.found && nextInfo.directory) {
|
|
6717
6823
|
const { value } = (0, phase_status_cjs_1.phaseStatus)(node_path_1.default.join(cwd, nextInfo.directory), {
|
|
6718
|
-
convention:
|
|
6824
|
+
convention: phaseConvention,
|
|
6719
6825
|
});
|
|
6720
6826
|
nextPhaseDisk = { status: value.status, planCount: value.planCount, summaryCount: value.summaryCount };
|
|
6721
6827
|
}
|
|
@@ -6882,22 +6988,39 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6882
6988
|
error(`Phase ${phaseNum} verification is incomplete: ${verificationBlocked.next_action}${nextStep}${indeterminateNote}`, ERROR_REASON.PHASE_VERIFICATION_INCOMPLETE, { verification_stale_check_indeterminate: staleCheckIndeterminate });
|
|
6883
6989
|
}
|
|
6884
6990
|
let autoPruned = false;
|
|
6991
|
+
// #5831: a failed auto-prune stays non-fatal (exit code and completion are
|
|
6992
|
+
// unchanged), but is reported instead of swallowed — prune now fails loudly
|
|
6993
|
+
// when it cannot write the archive, and that must reach the user.
|
|
6994
|
+
let autoPruneError = null;
|
|
6885
6995
|
try {
|
|
6886
6996
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
6887
|
-
|
|
6888
|
-
|
|
6997
|
+
// Bounded read (#5759): null is an absent file; a FIFO/device/oversized one throws to the
|
|
6998
|
+
// best-effort catch below.
|
|
6999
|
+
const rawConfigText = configLoaderMod.readConfigFileBounded(configPath);
|
|
7000
|
+
if (rawConfigText !== null) {
|
|
7001
|
+
const rawConfig = JSON.parse(rawConfigText);
|
|
6889
7002
|
const workflow = rawConfig['workflow'];
|
|
6890
7003
|
const autoPruneEnabled = workflow && workflow['auto_prune_state'] === true;
|
|
6891
7004
|
if (autoPruneEnabled && node_fs_1.default.existsSync(statePath)) {
|
|
6892
7005
|
// Non-hoisted: load-order matters (stateMod must be fully resolved first).
|
|
6893
7006
|
const { cmdStatePrune } = stateMod;
|
|
6894
|
-
|
|
6895
|
-
|
|
7007
|
+
try {
|
|
7008
|
+
const pruneResult = cmdStatePrune(cwd, { keepRecent: '3', dryRun: false, silent: true }, true);
|
|
7009
|
+
// Only a prune that moved entries counts: a no-op prune leaves auto_pruned false.
|
|
7010
|
+
autoPruned = pruneResult.pruned === true;
|
|
7011
|
+
}
|
|
7012
|
+
catch (err) {
|
|
7013
|
+
// Best-effort: completion proceeds; STATE.md and STATE-ARCHIVE.md are as
|
|
7014
|
+
// the failed prune left them (it never loses entries).
|
|
7015
|
+
autoPruneError = err instanceof Error ? err.message : String(err);
|
|
7016
|
+
warnings.push(`auto-prune of STATE.md failed (phase completion is unaffected; run \`state prune\` to retry): ${autoPruneError}`);
|
|
7017
|
+
}
|
|
6896
7018
|
}
|
|
6897
7019
|
}
|
|
6898
7020
|
}
|
|
6899
7021
|
catch {
|
|
6900
|
-
|
|
7022
|
+
// Best-effort: an unreadable or invalid config.json means auto-prune is not
|
|
7023
|
+
// enabled; completion proceeds silently.
|
|
6901
7024
|
}
|
|
6902
7025
|
const result = {
|
|
6903
7026
|
completed_phase: phaseNum,
|
|
@@ -6911,6 +7034,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6911
7034
|
state_updated: stateUpdated,
|
|
6912
7035
|
requirements_updated: requirementsUpdated,
|
|
6913
7036
|
auto_pruned: autoPruned,
|
|
7037
|
+
// #5831: ADDITIVE — present only when auto-prune threw.
|
|
7038
|
+
...(autoPruneError !== null ? { auto_prune_error: autoPruneError } : {}),
|
|
6914
7039
|
warnings,
|
|
6915
7040
|
has_warnings: warnings.length > 0,
|
|
6916
7041
|
// ADDITIVE, never a change to `warnings[]`'s element shape — that array is
|
|
@@ -85,10 +85,11 @@ function isHaltedStatus(status) {
|
|
|
85
85
|
* @param summaryPath - absolute or relative path to a `*-SUMMARY.md` file.
|
|
86
86
|
*/
|
|
87
87
|
function isSummaryFileHalted(summaryPath) {
|
|
88
|
+
const content = readSummaryFrontmatterPrefix(summaryPath);
|
|
89
|
+
if (content === null)
|
|
90
|
+
return false;
|
|
88
91
|
try {
|
|
89
|
-
|
|
90
|
-
const fm = extractFrontmatter(content, summaryPath);
|
|
91
|
-
return isHaltedStatus(fm['status']);
|
|
92
|
+
return isHaltedStatus(extractFrontmatter(content, summaryPath)['status']);
|
|
92
93
|
}
|
|
93
94
|
catch {
|
|
94
95
|
return false;
|
|
@@ -140,6 +141,46 @@ function isBlockedStatus(status) {
|
|
|
140
141
|
// kept here because this predicate's primary caller (scanPhasePlans) loops
|
|
141
142
|
// over every phase directory on hot paths (state sync, roadmap progress).
|
|
142
143
|
const SUMMARY_FRONTMATTER_READ_CAP = 64 * 1024;
|
|
144
|
+
/**
|
|
145
|
+
* #5789: the ONE reader behind both SUMMARY status predicates
|
|
146
|
+
* (`isSummaryFileHalted`, `isSummaryFileBlocked`), so they agree on which
|
|
147
|
+
* inputs are read. Opens O_NONBLOCK (never blocks on a FIFO; the flag is
|
|
148
|
+
* undefined on Windows, where FIFOs do not exist), decides the type on the
|
|
149
|
+
* opened fd (no stat-then-open swap window), refuses anything but a regular
|
|
150
|
+
* file (FIFO, /dev/zero via a symlink, socket, directory), and reads at most
|
|
151
|
+
* SUMMARY_FRONTMATTER_READ_CAP bytes from offset 0. Symlinks to regular files
|
|
152
|
+
* are still followed. Returns null on any error or non-regular input.
|
|
153
|
+
*/
|
|
154
|
+
function readSummaryFrontmatterPrefix(summaryPath) {
|
|
155
|
+
let fd;
|
|
156
|
+
try {
|
|
157
|
+
fd = node_fs_1.default.openSync(summaryPath, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NONBLOCK || 0));
|
|
158
|
+
if (!node_fs_1.default.fstatSync(fd).isFile())
|
|
159
|
+
return null;
|
|
160
|
+
const buf = Buffer.allocUnsafe(SUMMARY_FRONTMATTER_READ_CAP);
|
|
161
|
+
let total = 0;
|
|
162
|
+
while (total < SUMMARY_FRONTMATTER_READ_CAP) {
|
|
163
|
+
const n = node_fs_1.default.readSync(fd, buf, total, SUMMARY_FRONTMATTER_READ_CAP - total, total);
|
|
164
|
+
if (n === 0)
|
|
165
|
+
break;
|
|
166
|
+
total += n;
|
|
167
|
+
}
|
|
168
|
+
return buf.toString('utf8', 0, total);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
if (fd !== undefined) {
|
|
175
|
+
try {
|
|
176
|
+
node_fs_1.default.closeSync(fd);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// close failure on a read-only fd is not actionable
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
143
184
|
/**
|
|
144
185
|
* #3345: read a SUMMARY file's frontmatter `status` (bounded-prefix read) and
|
|
145
186
|
* report whether it declares `status: blocked`. Returns false — never throws —
|
|
@@ -153,27 +194,9 @@ const SUMMARY_FRONTMATTER_READ_CAP = 64 * 1024;
|
|
|
153
194
|
* re-diverge on the blocked rule.
|
|
154
195
|
*/
|
|
155
196
|
function isSummaryFileBlocked(summaryPath) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const st = node_fs_1.default.statSync(summaryPath); // follows symlinks → resolves to the target's real type
|
|
159
|
-
if (!st.isFile())
|
|
160
|
-
return false;
|
|
161
|
-
const length = Math.min(st.size, SUMMARY_FRONTMATTER_READ_CAP);
|
|
162
|
-
if (length === 0)
|
|
163
|
-
return false;
|
|
164
|
-
const fd = node_fs_1.default.openSync(summaryPath, 'r');
|
|
165
|
-
try {
|
|
166
|
-
const buf = Buffer.allocUnsafe(length);
|
|
167
|
-
const bytesRead = node_fs_1.default.readSync(fd, buf, 0, length, 0);
|
|
168
|
-
content = buf.toString('utf8', 0, bytesRead);
|
|
169
|
-
}
|
|
170
|
-
finally {
|
|
171
|
-
node_fs_1.default.closeSync(fd);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
197
|
+
const content = readSummaryFrontmatterPrefix(summaryPath);
|
|
198
|
+
if (content === null || content.length === 0)
|
|
175
199
|
return false;
|
|
176
|
-
}
|
|
177
200
|
return isBlockedStatus(extractFrontmatter(content, summaryPath)['status']);
|
|
178
201
|
}
|
|
179
202
|
/**
|