@opengsd/gsd-core 1.20.1 → 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 +234 -126
- package/commands/gsd/autonomous.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +450 -753
- 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 +80 -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/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +13 -11
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +11 -19
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- 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-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +107 -31
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
- 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 +328 -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 +7 -4
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- 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 +91 -41
- package/gsd-core/bin/lib/state.cjs +446 -61
- 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/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 +3 -2
- 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 +8 -5
- 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 +97 -17
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +3 -2
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +8 -5
- 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 +97 -17
- 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/scripts/changeset/cli.cjs +2 -2
- 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 +55 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -26,6 +26,7 @@ exports.appendQuickTaskRow = appendQuickTaskRow;
|
|
|
26
26
|
exports.migrateQuickTasksTable = migrateQuickTasksTable;
|
|
27
27
|
exports.resetQuickTaskRows = resetQuickTaskRows;
|
|
28
28
|
exports.updateTraceabilityCell = updateTraceabilityCell;
|
|
29
|
+
exports.traceabilityRowExists = traceabilityRowExists;
|
|
29
30
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
30
31
|
// ─── Schema registry ──────────────────────────────────────────────────────────
|
|
31
32
|
/**
|
|
@@ -979,6 +980,8 @@ function resetQuickTaskRows(stateContent) {
|
|
|
979
980
|
const content = (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody);
|
|
980
981
|
return { ok: true, value: { content, cleared, variant: match.label } };
|
|
981
982
|
}
|
|
983
|
+
/** The one Traceability heading rule shared by the write and the existence read. */
|
|
984
|
+
const TRACEABILITY_HEADING_RE = /^Traceability(?:[ \t]+Status)?\b/i;
|
|
982
985
|
/**
|
|
983
986
|
* `updateTableCell` scoped to REQUIREMENTS.md's `## Traceability` (or
|
|
984
987
|
* `## Traceability Status`) section (#2245 F1, #5762): `updateTableCell` binds
|
|
@@ -991,7 +994,7 @@ function resetQuickTaskRows(stateContent) {
|
|
|
991
994
|
*/
|
|
992
995
|
function updateTraceabilityCell(text, match, column, newValue) {
|
|
993
996
|
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
|
|
994
|
-
const index = headings.findIndex((h) => h.level === 2 &&
|
|
997
|
+
const index = headings.findIndex((h) => h.level === 2 && TRACEABILITY_HEADING_RE.test(h.text));
|
|
995
998
|
if (index === -1)
|
|
996
999
|
return updateTableCell(text, match, column, newValue);
|
|
997
1000
|
let result = { ok: false, reason: 'no matching row' };
|
|
@@ -1001,5 +1004,40 @@ function updateTraceabilityCell(text, match, column, newValue) {
|
|
|
1001
1004
|
});
|
|
1002
1005
|
return result.ok ? { ok: true, value: edited } : result;
|
|
1003
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Row-existence READ for REQUIREMENTS.md's Traceability section (#5844): true
|
|
1009
|
+
* when any table row in the section has `id` (case-insensitive) as its first
|
|
1010
|
+
* cell, whatever the table's columns. `updateTraceabilityCell(..., 'Status', …)`
|
|
1011
|
+
* answers "is there a row" only for tables that HAVE a Status column; this
|
|
1012
|
+
* answers it for every table. Scoping mirrors `updateTraceabilityCell`: the
|
|
1013
|
+
* same heading rule, and only the FIRST table of the scoped text (with no
|
|
1014
|
+
* Traceability heading that is the first table in the file), so a table
|
|
1015
|
+
* elsewhere whose first cell equals the ID is not a Traceability row.
|
|
1016
|
+
*/
|
|
1017
|
+
function traceabilityRowExists(text, id) {
|
|
1018
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
|
|
1019
|
+
const index = headings.findIndex((h) => h.level === 2 && TRACEABILITY_HEADING_RE.test(h.text));
|
|
1020
|
+
let scoped = text;
|
|
1021
|
+
if (index !== -1) {
|
|
1022
|
+
(0, markdown_sectionizer_cjs_1.editHeadingSlice)(text, (0, markdown_sectionizer_cjs_1.headingSliceRange)(text, headings, index, 2), (slice) => {
|
|
1023
|
+
scoped = slice;
|
|
1024
|
+
return slice;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
const wanted = id.trim().toLowerCase();
|
|
1028
|
+
let inTable = false;
|
|
1029
|
+
for (const line of scoped.split(/\r?\n/)) {
|
|
1030
|
+
if (!line.trim().startsWith('|')) {
|
|
1031
|
+
if (inTable)
|
|
1032
|
+
return false;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
inTable = true;
|
|
1036
|
+
const cells = splitTableRow(line);
|
|
1037
|
+
if (cells.length > 0 && cells[0].trim().toLowerCase() === wanted)
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1004
1042
|
// Consumers: require('../gsd-core/bin/lib/markdown-table.cjs')
|
|
1005
1043
|
// Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
|
|
@@ -43,7 +43,7 @@ const pattern_cjs_1 = require("./pattern.cjs");
|
|
|
43
43
|
const milestone_release_map_cjs_1 = require("./milestone-release-map.cjs");
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
45
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
46
|
-
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo, sliceMilestoneWindow, } = roadmapParserMod;
|
|
46
|
+
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo, sliceMilestoneWindow, classifyMilestoneScalar, readStateMilestoneScalar, sameMilestoneVersion, } = roadmapParserMod;
|
|
47
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
48
|
const planningScopeMod = require("./planning-scope.cjs");
|
|
49
49
|
const { SCOPE } = planningScopeMod;
|
|
@@ -225,7 +225,9 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
|
225
225
|
// Surface traceability drift: checkbox reconciled (this run or before) but the
|
|
226
226
|
// table has no row for this ID. This is what makes a partial reconcile
|
|
227
227
|
// distinguishable from a full one (#2140).
|
|
228
|
-
|
|
228
|
+
// #5844: a row in a table with no Status column is still a row; only the
|
|
229
|
+
// Status write is impossible there, so existence is read directly.
|
|
230
|
+
if (hasTable && doneCheckbox && !hasRow && !(0, markdown_table_cjs_1.traceabilityRowExists)(reqContent, reqId)) {
|
|
229
231
|
tableUnmatched.push(reqId);
|
|
230
232
|
}
|
|
231
233
|
}
|
|
@@ -603,15 +605,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
603
605
|
try {
|
|
604
606
|
const stateRaw = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : null;
|
|
605
607
|
if (stateRaw) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
608
|
+
// One owner for the STATE `milestone:` scalar (#5726): quoted, null and
|
|
609
|
+
// empty values and a body `milestone:` line are not drift.
|
|
610
|
+
stateVersion = classifyMilestoneScalar(readStateMilestoneScalar(stateRaw)).version;
|
|
609
611
|
}
|
|
610
612
|
}
|
|
611
613
|
catch {
|
|
612
614
|
/* skip — stateVersion stays null, scan still runs */
|
|
613
615
|
}
|
|
614
|
-
if (stateVersion !== null && stateVersion
|
|
616
|
+
if (stateVersion !== null && !sameMilestoneVersion(stateVersion, version)) {
|
|
615
617
|
// #2946: emit a WARNING so the suspicious STATE mismatch is visible
|
|
616
618
|
// rather than silently disarming the guard. Plain-text diagnostic on
|
|
617
619
|
// stderr, matching the existing [gsd-tools] WARNING convention
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* epic #1267; callers import resolvers from model-resolver.cjs directly.
|
|
12
12
|
*
|
|
13
13
|
* Dependencies (leaf modules only):
|
|
14
|
-
* - node:
|
|
14
|
+
* - node:path (project config path for the #2297 omit gate; read via config-loader's bounded reader, #5759)
|
|
15
15
|
* - ./runtime-name-policy.cjs (resolveRuntimeNameFromCandidates — canonicalize the active runtime)
|
|
16
16
|
* - ./planning-workspace.cjs (planningDir — workstream/project-aware project-config path)
|
|
17
17
|
* - ./config-loader.cjs (loadConfig)
|
|
@@ -25,14 +25,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
};
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
27
|
const configLoaderModule = require("./config-loader.cjs");
|
|
28
|
-
const { loadConfig } = configLoaderModule;
|
|
28
|
+
const { loadConfig, readConfigFileBounded } = configLoaderModule;
|
|
29
29
|
// ─── Configuration Module (for CANONICAL_CONFIG_DEFAULTS used by effort/fast_mode resolvers) ─
|
|
30
30
|
const configuration_cjs_1 = require("./configuration.cjs");
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
32
|
const modelProfiles = require("./model-profiles.cjs");
|
|
33
33
|
const { MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier } = modelProfiles;
|
|
34
34
|
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
35
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
36
35
|
const node_path_1 = __importDefault(require("node:path"));
|
|
37
36
|
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
38
37
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
@@ -87,7 +86,12 @@ function projectExplicitlySetsOmit(cwd) {
|
|
|
87
86
|
const layers = wsDir === rootDir ? [rootDir] : [wsDir, rootDir]; // workstream > root
|
|
88
87
|
for (const dir of layers) {
|
|
89
88
|
try {
|
|
90
|
-
|
|
89
|
+
// Same bounded regular-file reader as the loader (#5759): a FIFO/device/oversized
|
|
90
|
+
// layer throws instead of blocking, and an absent layer returns null.
|
|
91
|
+
const raw = readConfigFileBounded(node_path_1.default.join(dir, 'config.json'));
|
|
92
|
+
if (raw === null)
|
|
93
|
+
continue;
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
91
95
|
const value = parsed?.['resolve_model_ids'];
|
|
92
96
|
// First layer that sets the key wins (matches loadConfig's deep-merge
|
|
93
97
|
// precedence). A layer that omits the key falls through to the next.
|
|
@@ -32,6 +32,7 @@ exports.phaseEntryRegExp = phaseEntryRegExp;
|
|
|
32
32
|
exports.phaseEntrySplitRegExp = phaseEntrySplitRegExp;
|
|
33
33
|
exports.isPhaseEntryChecked = isPhaseEntryChecked;
|
|
34
34
|
exports.isBoxCharChecked = isBoxCharChecked;
|
|
35
|
+
exports.stripInsertedMarker = stripInsertedMarker;
|
|
35
36
|
const MARKER_SRC = '[-*]';
|
|
36
37
|
// A list item has 1 to 4 spaces or tabs after its marker (CommonMark: none is not a list item,
|
|
37
38
|
// five or more makes the rest an indented code block), so `-[ ] **Phase 1**` is not an entry.
|
|
@@ -117,3 +118,16 @@ function isPhaseEntryChecked(entryText) {
|
|
|
117
118
|
function isBoxCharChecked(boxChar) {
|
|
118
119
|
return boxChar.toLowerCase() === 'x';
|
|
119
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* #5824: `phase insert` writes `(INSERTED)` into the heading text as
|
|
123
|
+
* bookkeeping; it is not part of the phase's name. The single place that
|
|
124
|
+
* removes it: only a TRAILING `(INSERTED)` (case-insensitive) is dropped, then
|
|
125
|
+
* a trim — `phase insert` always appends it last, and a `(INSERTED)` inside the
|
|
126
|
+
* user's own text is kept. A name that is only the marker is returned as
|
|
127
|
+
* written rather than reduced to an empty name. Every phase-name reader goes
|
|
128
|
+
* through here so none can drift from the others.
|
|
129
|
+
*/
|
|
130
|
+
function stripInsertedMarker(name) {
|
|
131
|
+
const stripped = name.replace(/\s*\(INSERTED\)\s*$/i, '').trim();
|
|
132
|
+
return stripped === '' ? name.trim() : stripped;
|
|
133
|
+
}
|
|
@@ -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;
|
|
@@ -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,7 @@ 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,
|
|
65
66
|
// #5679: the STATE.md milestone read getMilestoneInfo itself uses, and the
|
|
66
67
|
// archive-aware owner of a ROADMAP offset.
|
|
67
68
|
readStateMilestoneScalar, classifyMilestoneScalar, milestoneVersionAtOffset, blankCodeAndComments, scanMilestonePhaseIds,
|
|
@@ -97,7 +98,7 @@ const planning_document_cjs_1 = require("./planning-document.cjs");
|
|
|
97
98
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
98
99
|
const clock_cjs_1 = require("./clock.cjs");
|
|
99
100
|
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
100
|
-
const
|
|
101
|
+
const markdown_table_cjs_2 = require("./markdown-table.cjs");
|
|
101
102
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
102
103
|
const security_cjs_1 = require("./security.cjs");
|
|
103
104
|
// #5060: the Phase Status Module owns the ROADMAP Status-cell token vocabulary.
|
|
@@ -1250,7 +1251,7 @@ function recordNewPhase(raw, cwd, id, description, after, label) {
|
|
|
1250
1251
|
if (findPhaseTableHeader(scoped) === -1)
|
|
1251
1252
|
return scoped;
|
|
1252
1253
|
const cell = { Phase: `${id}. ${description}`, 'Plans Complete': '0/0', Status: 'Not started' };
|
|
1253
|
-
const r = (0,
|
|
1254
|
+
const r = (0, markdown_table_cjs_2.insertTableRow)(scoped, (col) => cell[col]);
|
|
1254
1255
|
if (!r.ok)
|
|
1255
1256
|
return scoped;
|
|
1256
1257
|
wrote = true;
|
|
@@ -1641,6 +1642,54 @@ function assertBracketGeneratedIdentity(command, config, customId) {
|
|
|
1641
1642
|
'Custom naming precedence under the bracket convention lands with #5067.');
|
|
1642
1643
|
}
|
|
1643
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
|
+
}
|
|
1644
1693
|
/**
|
|
1645
1694
|
* #4304: refuse a bracket `phase add` / `add-batch` whose located milestone
|
|
1646
1695
|
* window is a decoy. A non-milestone heading carrying the active version
|
|
@@ -1712,9 +1761,9 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1712
1761
|
const bracketContext = convention === 'bracket' ? bracketWriteContext(cwd, config) : null;
|
|
1713
1762
|
const { newPhaseId, dirName, roadmapEntry } = withPlanningLock(cwd, () => {
|
|
1714
1763
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1764
|
+
const content = currentMilestoneWindowOrRefuse(rawContent, cwd, 'phase add');
|
|
1715
1765
|
if (bracketContext)
|
|
1716
1766
|
assertBracketAddWindowLocated(rawContent, cwd, bracketContext, 'phase add');
|
|
1717
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1718
1767
|
let _newPhaseId;
|
|
1719
1768
|
let _dirName = '';
|
|
1720
1769
|
if (bracketContext) {
|
|
@@ -1821,9 +1870,9 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1821
1870
|
const bracketContext = convention === 'bracket' ? bracketWriteContext(cwd, config) : null;
|
|
1822
1871
|
const results = withPlanningLock(cwd, () => {
|
|
1823
1872
|
let rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1873
|
+
const content = currentMilestoneWindowOrRefuse(rawContent, cwd, 'phase add-batch');
|
|
1824
1874
|
if (bracketContext)
|
|
1825
1875
|
assertBracketAddWindowLocated(rawContent, cwd, bracketContext, 'phase add-batch');
|
|
1826
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1827
1876
|
let maxPhase = 0;
|
|
1828
1877
|
if (bracketContext) {
|
|
1829
1878
|
const used = collectBracketPhaseNumbers(content, node_path_1.default.join(planningDir(cwd), 'phases'), bracketContext);
|
|
@@ -2236,7 +2285,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
2236
2285
|
const bracketContext = convention === 'bracket' ? bracketWriteContext(cwd, insertConfig) : null;
|
|
2237
2286
|
const { decimalPhase, dirName, roadmapEntry } = withPlanningLock(cwd, () => {
|
|
2238
2287
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
2239
|
-
const content =
|
|
2288
|
+
const content = currentMilestoneWindowOrRefuse(rawContent, cwd, 'phase insert');
|
|
2240
2289
|
// #4304: canonicalize a bracket argument
|
|
2241
2290
|
// through the SAME adapter `phase remove` uses
|
|
2242
2291
|
// (canonicalizeBracketPhaseArgument) instead of the legacy
|
|
@@ -2837,7 +2886,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
2837
2886
|
return parseInt(leadingMatch[1], 10) === removedInt;
|
|
2838
2887
|
};
|
|
2839
2888
|
content = (0, roadmap_progress_cjs_1.editRoadmapProgress)(content, (progressSection) => {
|
|
2840
|
-
const deleteResult = (0,
|
|
2889
|
+
const deleteResult = (0, markdown_table_cjs_2.deleteTableRow)(progressSection, matchRemovedProgressRow);
|
|
2841
2890
|
return deleteResult.ok ? deleteResult.value : progressSection;
|
|
2842
2891
|
}, { headingless: 'skip' });
|
|
2843
2892
|
if (!isDecimal && !customTarget) {
|
|
@@ -2917,7 +2966,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
2917
2966
|
let matchedRowIndex = null;
|
|
2918
2967
|
for (;;) {
|
|
2919
2968
|
matchedRowIndex = null;
|
|
2920
|
-
const cellResult = (0,
|
|
2969
|
+
const cellResult = (0, markdown_table_cjs_2.updateTableCell)(ordinalSection, (row, index) => {
|
|
2921
2970
|
if (processedOrdinalRows.has(index))
|
|
2922
2971
|
return false;
|
|
2923
2972
|
const m = phaseCellShapeRe.exec(row['Phase'] ?? '');
|
|
@@ -2938,11 +2987,11 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
2938
2987
|
const newContent = `${decremented}${m[2]}${current.slice(m[0].length)}`;
|
|
2939
2988
|
const targetLine = matchedRowIndex === null ? null : findDataRowLine(ordinalSection, matchedRowIndex);
|
|
2940
2989
|
const padMatch = targetLine
|
|
2941
|
-
? 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)
|
|
2942
2991
|
: null;
|
|
2943
2992
|
const leadPad = padMatch ? padMatch[1] : ' ';
|
|
2944
2993
|
const trailPad = padMatch ? padMatch[2] : ' ';
|
|
2945
|
-
return `${leadPad}${(0,
|
|
2994
|
+
return `${leadPad}${(0, markdown_table_cjs_2.escapeCell)(newContent)}${trailPad}`;
|
|
2946
2995
|
});
|
|
2947
2996
|
if (!cellResult.ok)
|
|
2948
2997
|
break;
|
|
@@ -3532,7 +3581,7 @@ function classifyBracketOwnedLine(line) {
|
|
|
3532
3581
|
if (checklistId)
|
|
3533
3582
|
return { kind: 'checklist', id: checklistId };
|
|
3534
3583
|
if (/^[ \t]*\|/.test(line)) {
|
|
3535
|
-
const firstCell = (0,
|
|
3584
|
+
const firstCell = (0, markdown_table_cjs_2.splitTableRow)(line)[0]?.replace(/^\*\*(.*)\*\*$/, '$1') ?? '';
|
|
3536
3585
|
const progressId = phaseIdFromOwnedLineMatch(BRACKET_CELL_ID_RE.exec(firstCell));
|
|
3537
3586
|
if (progressId)
|
|
3538
3587
|
return { kind: 'progress', id: progressId };
|
|
@@ -4894,7 +4943,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
|
4894
4943
|
const heading = matchedHeading.text.match(headingPattern);
|
|
4895
4944
|
if (!heading)
|
|
4896
4945
|
return null;
|
|
4897
|
-
const name =
|
|
4946
|
+
const name = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(heading[1]);
|
|
4898
4947
|
return name || null;
|
|
4899
4948
|
}
|
|
4900
4949
|
function phaseDisplayNameFromSlug(slug) {
|
|
@@ -6073,12 +6122,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6073
6122
|
// (`editProgressTableSlice` -> `editRoadmapProgress`), the same one `roadmap update-plan-progress` uses.
|
|
6074
6123
|
s = editProgressTableSlice(s, (scoped) => {
|
|
6075
6124
|
let text = scoped;
|
|
6076
|
-
const plansResult = (0,
|
|
6125
|
+
const plansResult = (0, markdown_table_cjs_2.updateTableCell)(text, rowMatch, 'Plans Complete', ` ${summaryCount}/${planCount} `);
|
|
6077
6126
|
if (plansResult.ok)
|
|
6078
6127
|
text = plansResult.value;
|
|
6079
6128
|
// #5165: a Status cell that already reads Complete is left untouched —
|
|
6080
6129
|
// re-padding it rewrote a cell the re-run had no business editing.
|
|
6081
|
-
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
|
|
6082
6131
|
? current
|
|
6083
6132
|
: ` ${(0, phase_status_cjs_1.toRoadmapStatusCell)(phase_status_cjs_1.PHASE_STATUS.COMPLETE).padEnd(11)} `);
|
|
6084
6133
|
if (statusResult.ok)
|
|
@@ -6090,7 +6139,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6090
6139
|
// findTableWithColumns (which requires the WHOLE table to parse —
|
|
6091
6140
|
// a ragged SIBLING row elsewhere used to silently no-op this
|
|
6092
6141
|
// row's date stamp too).
|
|
6093
|
-
const completedResult = (0,
|
|
6142
|
+
const completedResult = (0, markdown_table_cjs_2.updateTableCell)(text, rowMatch, 'Completed', (current) => dateShape.test(current.trim()) ? current : ` ${today} `);
|
|
6094
6143
|
if (completedResult.ok)
|
|
6095
6144
|
text = completedResult.value;
|
|
6096
6145
|
return text;
|
|
@@ -6228,7 +6277,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6228
6277
|
// checkbox rollback below can distinguish "row existed and accepted" from
|
|
6229
6278
|
// "row existed and rejected".
|
|
6230
6279
|
let tableHit = false;
|
|
6231
|
-
const reqUpdate = (0,
|
|
6280
|
+
const reqUpdate = (0, markdown_table_cjs_2.updateTraceabilityCell)(reqContent, reqRowMatch, 'Status', (current) => {
|
|
6232
6281
|
// #2788: accept `Gaps Found` too so a phase stranded by revert-phase (the
|
|
6233
6282
|
// gaps_found response) can complete without hand-editing the table.
|
|
6234
6283
|
if (/^(?:pending|in progress|gaps found)$/i.test(current.trim())) {
|
|
@@ -6405,9 +6454,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6405
6454
|
// which section (deferred or not) it lives under.
|
|
6406
6455
|
const reqIsRegisteredAnywhere = (id) => {
|
|
6407
6456
|
const reqEscaped = (0, pattern_cjs_1.escapeRegex)(id);
|
|
6408
|
-
// Surface 1 — checkbox,
|
|
6409
|
-
// insensitive: existence check, not the write's space-only match.
|
|
6410
|
-
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)) {
|
|
6411
6460
|
return true;
|
|
6412
6461
|
}
|
|
6413
6462
|
// Surface 2 — Traceability row exists at all (any Status value),
|
|
@@ -6415,7 +6464,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6415
6464
|
// technique milestone.cts's `hasRow` uses (:210-214): a case-
|
|
6416
6465
|
// insensitive first-cell match, regardless of current Status.
|
|
6417
6466
|
const rowProbeMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === id.toLowerCase();
|
|
6418
|
-
|
|
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);
|
|
6419
6471
|
};
|
|
6420
6472
|
const ghostReqIds = citedReqIds.filter((id) => !isPlaceholderReqId(id) && !reqIsRegisteredAnywhere(id));
|
|
6421
6473
|
if (ghostReqIds.length > 0) {
|
|
@@ -6427,8 +6479,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6427
6479
|
// IDs already reported above as ghosts to avoid a duplicate message
|
|
6428
6480
|
// for the same root cause.
|
|
6429
6481
|
const traceabilityWriteFailures = traceabilityWriteMisses.filter((id) => !ghostReqIds.includes(id));
|
|
6430
|
-
|
|
6431
|
-
|
|
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(', ')}`);
|
|
6432
6491
|
}
|
|
6433
6492
|
writes.push({ filePath: reqPath, before: originalReqContent, after: reqContent });
|
|
6434
6493
|
// #2316-3: `requirements_updated` must reflect whether REQUIREMENTS.md
|
|
@@ -6619,9 +6678,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6619
6678
|
if (comparePhaseNum(pmNum, phaseNum, directoryContext) > 0
|
|
6620
6679
|
&& (roadmapNextNum === null || comparePhaseNum(pmNum, roadmapNextNum, directoryContext) < 0)) {
|
|
6621
6680
|
roadmapNextNum = pmNum;
|
|
6622
|
-
roadmapNextName = pmName
|
|
6623
|
-
.replace(/\(INSERTED\)/i, '')
|
|
6624
|
-
.trim()
|
|
6681
|
+
roadmapNextName = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(pmName)
|
|
6625
6682
|
.toLowerCase()
|
|
6626
6683
|
.replace(/\s+/g, '-');
|
|
6627
6684
|
}
|
|
@@ -6701,7 +6758,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6701
6758
|
if (lowestOutstanding === null || comparePhaseNum(cbm[2], lowestOutstanding.num, directoryContext) < 0) {
|
|
6702
6759
|
lowestOutstanding = {
|
|
6703
6760
|
num: cbm[2],
|
|
6704
|
-
name:
|
|
6761
|
+
name: (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(cbm[3]).toLowerCase().replace(/\s+/g, '-'),
|
|
6705
6762
|
};
|
|
6706
6763
|
}
|
|
6707
6764
|
}
|
|
@@ -6931,22 +6988,39 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6931
6988
|
error(`Phase ${phaseNum} verification is incomplete: ${verificationBlocked.next_action}${nextStep}${indeterminateNote}`, ERROR_REASON.PHASE_VERIFICATION_INCOMPLETE, { verification_stale_check_indeterminate: staleCheckIndeterminate });
|
|
6932
6989
|
}
|
|
6933
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;
|
|
6934
6995
|
try {
|
|
6935
6996
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
6936
|
-
|
|
6937
|
-
|
|
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);
|
|
6938
7002
|
const workflow = rawConfig['workflow'];
|
|
6939
7003
|
const autoPruneEnabled = workflow && workflow['auto_prune_state'] === true;
|
|
6940
7004
|
if (autoPruneEnabled && node_fs_1.default.existsSync(statePath)) {
|
|
6941
7005
|
// Non-hoisted: load-order matters (stateMod must be fully resolved first).
|
|
6942
7006
|
const { cmdStatePrune } = stateMod;
|
|
6943
|
-
|
|
6944
|
-
|
|
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
|
+
}
|
|
6945
7018
|
}
|
|
6946
7019
|
}
|
|
6947
7020
|
}
|
|
6948
7021
|
catch {
|
|
6949
|
-
|
|
7022
|
+
// Best-effort: an unreadable or invalid config.json means auto-prune is not
|
|
7023
|
+
// enabled; completion proceeds silently.
|
|
6950
7024
|
}
|
|
6951
7025
|
const result = {
|
|
6952
7026
|
completed_phase: phaseNum,
|
|
@@ -6960,6 +7034,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
6960
7034
|
state_updated: stateUpdated,
|
|
6961
7035
|
requirements_updated: requirementsUpdated,
|
|
6962
7036
|
auto_pruned: autoPruned,
|
|
7037
|
+
// #5831: ADDITIVE — present only when auto-prune threw.
|
|
7038
|
+
...(autoPruneError !== null ? { auto_prune_error: autoPruneError } : {}),
|
|
6963
7039
|
warnings,
|
|
6964
7040
|
has_warnings: warnings.length > 0,
|
|
6965
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
|
/**
|