@opengsd/gsd-core 1.16.0 → 1.18.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 -2
- package/.opencode/plugins/gsd-core.js +251 -1
- package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
- package/GEMINI.md +1 -1
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +1 -1
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +398 -95
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +34 -12
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +29 -7
- package/gsd-core/bin/lib/capability-registry.cjs +138 -80
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +6 -3
- package/gsd-core/bin/lib/capability-validator.cjs +8 -5
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +33 -9
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
- package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
- package/gsd-core/bin/lib/graphify.cjs +3 -2
- package/gsd-core/bin/lib/init.cjs +43 -16
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +5 -3
- package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
- package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
- package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
- package/gsd-core/bin/lib/mcp-server.cjs +5 -1
- package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
- package/gsd-core/bin/lib/opencode-version.cjs +486 -0
- package/gsd-core/bin/lib/phase.cjs +141 -11
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap.cjs +56 -5
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
- package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +100 -53
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +98 -19
- package/gsd-core/bin/lib/state.cjs +544 -126
- package/gsd-core/bin/lib/task-command-router.cjs +2 -3
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/bin/lib/verify.cjs +36 -1
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +3 -3
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/tdd.md +4 -3
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +21 -2
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone.md +25 -5
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
- package/gsd-core/workflows/discuss-phase.md +5 -1
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
- package/gsd-core/workflows/execute-phase.md +40 -7
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/import.md +5 -1
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +41 -73
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +22 -7
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +19 -11
- package/gsd-core/workflows/review.md +6 -2
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +3 -3
- package/gsd-core/workflows/ship.md +13 -10
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +11 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +189 -15
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +189 -15
- package/hooks/gsd-statusline.js +321 -22
- package/hooks/gsd-validate-commit.sh +27 -5
- package/package.json +1 -1
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
- package/scripts/diagnose-validate-commit-phases.cjs +348 -0
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-capability-registry.cjs +7 -1
- package/scripts/gen-loop-host-contract.cjs +4 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +15 -7
- package/scripts/xtrace-phase-timing.cjs +205 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -1067,6 +1067,62 @@ function describeGoalShapedTitle(description) {
|
|
|
1067
1067
|
return (`description looks goal-shaped, not title-shaped (${reasons}). It was written verbatim ` +
|
|
1068
1068
|
`as the phase title; consider a short title with the detail moved to **Goal:**.`);
|
|
1069
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Phase numbers the roadmap itself declares — a section header or a roadmap bullet — sentinels
|
|
1072
|
+
* excluded. This is deliberately NOT the allocation set: allocation also counts on-disk directories
|
|
1073
|
+
* and sibling-worktree phases so a new number cannot collide, but a `Depends on:` line has to point
|
|
1074
|
+
* at something a reader of THIS roadmap can find (#5113).
|
|
1075
|
+
*/
|
|
1076
|
+
function roadmapDeclaredPhaseNums(rawContent) {
|
|
1077
|
+
const { regex: headerPattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.LABEL_ONLY);
|
|
1078
|
+
const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
|
|
1079
|
+
const nums = new Set();
|
|
1080
|
+
let m;
|
|
1081
|
+
while ((m = headerPattern.exec(rawContent)) !== null) {
|
|
1082
|
+
const num = parseInt(m[phaseNumGroup], 10);
|
|
1083
|
+
if (!isSentinelPhaseId(num))
|
|
1084
|
+
nums.add(num);
|
|
1085
|
+
}
|
|
1086
|
+
while ((m = bulletPattern.exec(rawContent)) !== null) {
|
|
1087
|
+
const num = parseInt(m[1], 10);
|
|
1088
|
+
if (!isSentinelPhaseId(num))
|
|
1089
|
+
nums.add(num);
|
|
1090
|
+
}
|
|
1091
|
+
return [...nums];
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* The `**Depends on:**` line for a freshly added numeric phase: the nearest lower phase the roadmap
|
|
1095
|
+
* declares, or `Nothing` when it declares none. It was `newId - 1` regardless, which named a phase
|
|
1096
|
+
* that exists only on a sibling branch, as an orphan directory, or not at all when numbering has a
|
|
1097
|
+
* gap (#5113). Custom-named phases carry no dependency line.
|
|
1098
|
+
*/
|
|
1099
|
+
function dependsOnLine(newPhaseId, declared) {
|
|
1100
|
+
if (typeof newPhaseId !== 'number')
|
|
1101
|
+
return '';
|
|
1102
|
+
let nearest = null;
|
|
1103
|
+
for (const n of declared)
|
|
1104
|
+
if (n < newPhaseId && (nearest === null || n > nearest))
|
|
1105
|
+
nearest = n;
|
|
1106
|
+
return `\n**Depends on:** ${nearest === null ? 'Nothing' : `Phase ${nearest}`}`;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Splice a phase entry into `rawContent` at `at`, changing nothing else: the blank lines around the
|
|
1110
|
+
* block and its line endings are the block's own, so the surrounding document is published byte for
|
|
1111
|
+
* byte (#5113). `block` is LF text that starts and ends with a newline.
|
|
1112
|
+
*/
|
|
1113
|
+
function spliceRoadmapBlock(rawContent, atOffset, block) {
|
|
1114
|
+
const eol = rawContent.includes('\r\n') ? '\r\n' : '\n';
|
|
1115
|
+
// The insertion offset is found by searching for '\n---', which lands between the CR and the LF of
|
|
1116
|
+
// a CRLF pair; splice before the pair instead of through it.
|
|
1117
|
+
const at = rawContent[atOffset - 1] === '\r' && rawContent[atOffset] === '\n' ? atOffset - 1 : atOffset;
|
|
1118
|
+
const before = rawContent.slice(0, at);
|
|
1119
|
+
const after = rawContent.slice(at);
|
|
1120
|
+
const body = block.replace(/^\n+/, '').replace(/\n+$/, '\n').replace(/\n/g, eol);
|
|
1121
|
+
const blankBefore = before === '' || /(?:\r?\n){2}$/.test(before);
|
|
1122
|
+
const lead = blankBefore ? '' : /\n$/.test(before) ? eol : eol + eol;
|
|
1123
|
+
const tail = after === '' || /^\r?\n/.test(after) ? '' : eol;
|
|
1124
|
+
return before + lead + body + tail + after;
|
|
1125
|
+
}
|
|
1070
1126
|
/**
|
|
1071
1127
|
* #3163: compute the byte offset in `rawContent` where a new `### Phase N:`
|
|
1072
1128
|
* entry should be inserted — at the end of the active phase list, scoped to the
|
|
@@ -1310,13 +1366,11 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1310
1366
|
const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', _dirName);
|
|
1311
1367
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
|
|
1312
1368
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
1313
|
-
const dependsOn = config.phase_naming === 'custom'
|
|
1314
|
-
? ''
|
|
1315
|
-
: `\n**Depends on:** Phase ${typeof _newPhaseId === 'number' ? _newPhaseId - 1 : 'TBD'}`;
|
|
1369
|
+
const dependsOn = config.phase_naming === 'custom' ? '' : dependsOnLine(_newPhaseId, roadmapDeclaredPhaseNums(rawContent));
|
|
1316
1370
|
const phaseEntry = `\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_newPhaseId} to break down)\n`;
|
|
1317
1371
|
const insertAt = phaseEntryInsertOffset(rawContent, cwd);
|
|
1318
|
-
const updatedContent = rawContent
|
|
1319
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent);
|
|
1372
|
+
const updatedContent = spliceRoadmapBlock(rawContent, insertAt, phaseEntry);
|
|
1373
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent, { verbatim: true });
|
|
1320
1374
|
return { newPhaseId: _newPhaseId, dirName: _dirName };
|
|
1321
1375
|
});
|
|
1322
1376
|
const titleWarning = describeGoalShapedTitle(description);
|
|
@@ -1432,12 +1486,10 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1432
1486
|
const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', dirName);
|
|
1433
1487
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
|
|
1434
1488
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
1435
|
-
const dependsOn = config.phase_naming === 'custom'
|
|
1436
|
-
? ''
|
|
1437
|
-
: `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
|
|
1489
|
+
const dependsOn = config.phase_naming === 'custom' ? '' : dependsOnLine(newPhaseId, roadmapDeclaredPhaseNums(rawContent));
|
|
1438
1490
|
const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${newPhaseId} to break down)\n`;
|
|
1439
1491
|
const insertAt = phaseEntryInsertOffset(rawContent, cwd);
|
|
1440
|
-
rawContent = rawContent
|
|
1492
|
+
rawContent = spliceRoadmapBlock(rawContent, insertAt, phaseEntry);
|
|
1441
1493
|
added.push({
|
|
1442
1494
|
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
1443
1495
|
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
|
@@ -1447,7 +1499,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1447
1499
|
naming_mode: config.phase_naming,
|
|
1448
1500
|
});
|
|
1449
1501
|
}
|
|
1450
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, rawContent);
|
|
1502
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, rawContent, { verbatim: true });
|
|
1451
1503
|
return added;
|
|
1452
1504
|
});
|
|
1453
1505
|
output({ phases: results, count: results.length }, raw);
|
|
@@ -3061,6 +3113,42 @@ function formatRequirementsLineWarning(phaseNum, rawLine, analysis) {
|
|
|
3061
3113
|
oversized,
|
|
3062
3114
|
};
|
|
3063
3115
|
}
|
|
3116
|
+
/**
|
|
3117
|
+
* #4972: advisory gaps for the phase artifacts that active `verify:post` steps
|
|
3118
|
+
* declared `onError: "halt"` and `produces` (SECURITY.md, VALIDATION.md).
|
|
3119
|
+
* Missing file, an unresolved `status: draft`, or (SECURITY.md) `threats_open`
|
|
3120
|
+
* above zero. Any other `status` spelling counts as closed — the audit skills
|
|
3121
|
+
* do not pin one value, so this does not invent a vocabulary.
|
|
3122
|
+
*/
|
|
3123
|
+
function auditArtifactWarnings(cwd, phaseFullDir) {
|
|
3124
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy: loop-resolver pulls in the capability registry
|
|
3125
|
+
const loopResolver = require('./loop-resolver.cjs');
|
|
3126
|
+
const out = [];
|
|
3127
|
+
const files = node_fs_1.default.readdirSync(phaseFullDir);
|
|
3128
|
+
for (const hook of loopResolver.resolveActiveHooksForPoint(cwd, 'verify:post').activeHooks) {
|
|
3129
|
+
if (hook.kind !== 'step' || hook.onError !== 'halt')
|
|
3130
|
+
continue;
|
|
3131
|
+
for (const artifact of hook.produces ?? []) {
|
|
3132
|
+
if (!/^[A-Z][A-Z0-9-]*\.md$/.test(artifact))
|
|
3133
|
+
continue;
|
|
3134
|
+
const skill = hook.ref?.skill ?? hook.capId;
|
|
3135
|
+
const found = files.filter((f) => f === artifact || f.endsWith(`-${artifact}`)).sort()[0];
|
|
3136
|
+
if (!found) {
|
|
3137
|
+
out.push(`${artifact}: missing — the ${hook.capId} audit (/gsd:${skill}) never ran for this phase, and a verify:post step cannot block the close (#4972)`);
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
const fm = frontmatterMod.extractFrontmatter(node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, found), 'utf-8'));
|
|
3141
|
+
if (typeof fm['status'] === 'string' && fm['status'].trim().toLowerCase() === 'draft') {
|
|
3142
|
+
out.push(`${found}: status is still draft — the ${hook.capId} audit (/gsd:${skill}) was not completed (#4972)`);
|
|
3143
|
+
}
|
|
3144
|
+
const open = Number(fm['threats_open']);
|
|
3145
|
+
if (artifact === 'SECURITY.md' && Number.isFinite(open) && open > 0) {
|
|
3146
|
+
out.push(`${found}: ${open} threat(s) still open (#4972)`);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
return out;
|
|
3151
|
+
}
|
|
3064
3152
|
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
3065
3153
|
if (!phaseNum) {
|
|
3066
3154
|
error('phase number required for phase complete');
|
|
@@ -3274,6 +3362,23 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3274
3362
|
/* best-effort, same posture as the #2245 pre-scan above: an unreadable
|
|
3275
3363
|
* SUMMARY means one fewer advisory this run, never a blocked completion. */
|
|
3276
3364
|
}
|
|
3365
|
+
// #4972: a `verify:post` STEP declared `onError: "halt"` that produces a
|
|
3366
|
+
// phase artifact (security -> SECURITY.md, nyquist -> VALIDATION.md) reads as
|
|
3367
|
+
// a blocking precondition, but a step is advisory by construction (ADR-894
|
|
3368
|
+
// "steps are additive, gates block") and the workflow only prints a line the
|
|
3369
|
+
// operator can ignore — so a phase could close with the audit never run. This
|
|
3370
|
+
// verb is the one place every close passes through, so it names the gap on
|
|
3371
|
+
// the same ADVISORY `warnings[]` channel as the #2572 check above. Advisory,
|
|
3372
|
+
// not a refusal: both capabilities are on by default, so a hard block here
|
|
3373
|
+
// would stop every existing project's next phase close; the security
|
|
3374
|
+
// threats-open gate at `ship:pre` is the enforced one and is untouched.
|
|
3375
|
+
try {
|
|
3376
|
+
warnings.push(...auditArtifactWarnings(cwd, phaseFullDir));
|
|
3377
|
+
}
|
|
3378
|
+
catch {
|
|
3379
|
+
/* best-effort, same posture as the advisories above: an unresolvable hook
|
|
3380
|
+
* set or unreadable artifact means one fewer advisory, never a blocked close. */
|
|
3381
|
+
}
|
|
3277
3382
|
let nextPhaseNum = null;
|
|
3278
3383
|
let nextPhaseName = null;
|
|
3279
3384
|
let isLastPhase = true;
|
|
@@ -4199,6 +4304,26 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4199
4304
|
// #3374 / #3469).
|
|
4200
4305
|
const nextPhaseDisplayName = phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
|
|
4201
4306
|
phaseDisplayNameFromSlug(nextPhaseName);
|
|
4307
|
+
// #4824: classify the SELECTED next phase from disk (the same ladder
|
|
4308
|
+
// roadmap analyze uses) so the Status / Current Plan / Stopped-at
|
|
4309
|
+
// handoff does not say "ready to plan" for a phase already planned or
|
|
4310
|
+
// executed. A missing directory or unreadable phase keeps the
|
|
4311
|
+
// historical wording.
|
|
4312
|
+
let nextPhaseDisk = null;
|
|
4313
|
+
if (nextPhaseNum && !isLastPhase) {
|
|
4314
|
+
try {
|
|
4315
|
+
const nextInfo = findPhaseInternal(cwd, nextPhaseNum);
|
|
4316
|
+
if (nextInfo && nextInfo.found && nextInfo.directory) {
|
|
4317
|
+
const { value } = (0, phase_status_cjs_1.phaseStatus)(node_path_1.default.join(cwd, nextInfo.directory), {
|
|
4318
|
+
convention: resolvePhaseIdConvention(cwd),
|
|
4319
|
+
});
|
|
4320
|
+
nextPhaseDisk = { status: value.status, planCount: value.planCount, summaryCount: value.summaryCount };
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
catch {
|
|
4324
|
+
/* best-effort: an unreadable next phase keeps `Ready to plan` */
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4202
4327
|
const completeResult = (0, state_transition_cjs_1.transitionCore)(stateContent, {
|
|
4203
4328
|
kind: 'completePhase',
|
|
4204
4329
|
phaseNum,
|
|
@@ -4207,9 +4332,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4207
4332
|
isLastPhase,
|
|
4208
4333
|
planCount,
|
|
4209
4334
|
summaryCount,
|
|
4335
|
+
nextPhaseDisk,
|
|
4210
4336
|
}, {
|
|
4211
4337
|
clock: clock_cjs_1.realClock,
|
|
4212
|
-
|
|
4338
|
+
// #4901: the body counter derives from the same current-milestone scope
|
|
4339
|
+
// as its frontmatter twin below (postCompletionRoadmapScope), not the
|
|
4340
|
+
// whole file, so the two writers cannot disagree on which
|
|
4341
|
+
// `## Progress` table they count.
|
|
4342
|
+
roadmapProvider: () => (roadmapContent !== null ? extractCurrentMilestone(roadmapContent, cwd) : null),
|
|
4213
4343
|
sourcePath: statePath,
|
|
4214
4344
|
});
|
|
4215
4345
|
stateContent = completeResult.content;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* ADR-457 build-at-publish: compiled by tsc to gsd-core/bin/lib/quick-batch-dispatch.cjs.
|
|
28
28
|
*/
|
|
29
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
29
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
31
|
const planDocumentMod = require("./plan-document.cjs");
|
|
31
32
|
const { parsePlanDocument } = planDocumentMod;
|
|
@@ -42,7 +43,7 @@ function parseQuickBatchArgs(args) {
|
|
|
42
43
|
return { ok: false, reason: 'quick-batch args must be an array' };
|
|
43
44
|
}
|
|
44
45
|
if (args.includes('--discuss')) {
|
|
45
|
-
return { ok: false, reason:
|
|
46
|
+
return { ok: false, reason: `quick-batch does not support --discuss in v1 — use ${(0, runtime_slash_cjs_1.runtimeCommand)(null, 'quick --discuss')} per item instead` };
|
|
46
47
|
}
|
|
47
48
|
if (args.includes('--full')) {
|
|
48
49
|
return { ok: false, reason: 'quick-batch does not support --full in v1' };
|
|
@@ -6,7 +6,7 @@ exports.parseTestReport = parseTestReport;
|
|
|
6
6
|
const tap_parser_cjs_1 = require("./vendor/tap-parser.cjs");
|
|
7
7
|
const saxes_cjs_1 = require("./vendor/saxes.cjs");
|
|
8
8
|
exports.ReportFormat = {
|
|
9
|
-
Tap: 'tap', Junit: 'junit', SwiftTesting: 'swift-testing', Unittest: 'unittest', Unknown: 'unknown',
|
|
9
|
+
Tap: 'tap', Junit: 'junit', SwiftTesting: 'swift-testing', Unittest: 'unittest', GoTest: 'go-test', Unknown: 'unknown',
|
|
10
10
|
};
|
|
11
11
|
exports.TestStatus = { Passed: 'passed', Failed: 'failed', Skipped: 'skipped', Todo: 'todo' };
|
|
12
12
|
function parseTap(output) {
|
|
@@ -247,7 +247,63 @@ function parseUnittest(output) {
|
|
|
247
247
|
report.valid = report.issues.length === 0;
|
|
248
248
|
return report;
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* `go test` text output (#5106). Plain console text, so the shape is matched by line:
|
|
252
|
+
* --- FAIL: TestName (0.00s) one per failed test (every test with -v; subtests indented)
|
|
253
|
+
* FAIL\texample.com/pkg\t0.004s one package summary per package, after that package's tests
|
|
254
|
+
* FAIL\texample.com/pkg [build failed] the package did not compile — nothing asserted anything
|
|
255
|
+
* A test's name carries no package, so each test is also known as `<package>.<name>`, attributed to
|
|
256
|
+
* the next package summary line. The same name failing in two packages is ambiguous until qualified.
|
|
257
|
+
* `go test -json` is not text and stays unsupported.
|
|
258
|
+
*/
|
|
259
|
+
const GO_TEST_RESULT = /^[ \t]*-{3} (PASS|FAIL|SKIP): (\S+) \(\d+(?:\.\d+)?s\)$/; // -{3}: not a literal fence, see frontmatter-fence-drift
|
|
260
|
+
const GO_PACKAGE_LINE = /^(ok|FAIL|\?)[ \t]+(\S+)[ \t]+(?:\d+(?:\.\d+)?s\b|\(cached\)|\[([a-z ]+)\])/;
|
|
261
|
+
function isGoTestOutput(output) {
|
|
262
|
+
if (/^TAP version \d+/.test(output.trimStart()))
|
|
263
|
+
return false;
|
|
264
|
+
return output.split(/\r?\n/).some((line) => GO_TEST_RESULT.test(line) || GO_PACKAGE_LINE.test(line));
|
|
265
|
+
}
|
|
266
|
+
function parseGoTest(output) {
|
|
267
|
+
const report = { format: exports.ReportFormat.GoTest, valid: true, tests: [], issues: [] };
|
|
268
|
+
let segment = [];
|
|
269
|
+
let packages = 0;
|
|
270
|
+
for (const line of output.split(/\r?\n/)) {
|
|
271
|
+
const result = GO_TEST_RESULT.exec(line);
|
|
272
|
+
if (result) {
|
|
273
|
+
const name = result[2];
|
|
274
|
+
const status = result[1] === 'FAIL' ? exports.TestStatus.Failed : result[1] === 'SKIP' ? exports.TestStatus.Skipped : exports.TestStatus.Passed;
|
|
275
|
+
if (!segment.some((test) => test.name === name && test.status === status)) {
|
|
276
|
+
segment.push({ name, identities: [name], group: null, groupIdentities: [], status });
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const pkg = GO_PACKAGE_LINE.exec(line);
|
|
281
|
+
if (!pkg)
|
|
282
|
+
continue;
|
|
283
|
+
packages++;
|
|
284
|
+
const [, outcome, importPath, bracket] = pkg;
|
|
285
|
+
if (outcome === 'FAIL' && (bracket === 'build failed' || bracket === 'setup failed')) {
|
|
286
|
+
report.buildFailed = true;
|
|
287
|
+
report.issues.push(`go test package ${importPath} ${bracket}`);
|
|
288
|
+
}
|
|
289
|
+
else if (outcome === 'ok' && segment.some((test) => test.status === exports.TestStatus.Failed)) {
|
|
290
|
+
report.issues.push(`go test package ${importPath} is ok but its output reports a failed test`);
|
|
291
|
+
}
|
|
292
|
+
for (const test of segment)
|
|
293
|
+
test.identities.push(`${importPath}.${test.name}`);
|
|
294
|
+
report.tests.push(...segment);
|
|
295
|
+
segment = [];
|
|
296
|
+
}
|
|
297
|
+
if (packages === 0)
|
|
298
|
+
report.issues.push('Missing go test package summary');
|
|
299
|
+
if (segment.length > 0)
|
|
300
|
+
report.issues.push('go test results without a following package summary');
|
|
301
|
+
report.valid = report.issues.length === 0;
|
|
302
|
+
return report;
|
|
303
|
+
}
|
|
250
304
|
const adapters = [
|
|
305
|
+
// Before TAP: a run whose first package is fine begins `ok <TAB>pkg`, which the TAP matcher reads as a point.
|
|
306
|
+
{ matches: isGoTestOutput, parse: parseGoTest },
|
|
251
307
|
{ matches: (output) => /^(?:TAP version \d+|(?:not )?ok\b|1\.\.\d+|#)/.test(output.trimStart()), parse: parseTap },
|
|
252
308
|
{ matches: (output) => output.trimStart().startsWith('<'), parse: parseJunit },
|
|
253
309
|
{ matches: (output) => new RegExp(SWIFT_AGGREGATE.source, 'm').test(output), parse: parseSwiftTesting },
|
|
@@ -31,6 +31,7 @@ exports.resolveTimeoutMs = resolveTimeoutMs;
|
|
|
31
31
|
exports.nativeTimeoutToken = nativeTimeoutToken;
|
|
32
32
|
exports.isEmptyReview = isEmptyReview;
|
|
33
33
|
exports.fileRefPrompt = fileRefPrompt;
|
|
34
|
+
exports.applyPromptOverride = applyPromptOverride;
|
|
34
35
|
exports.artifactPaths = artifactPaths;
|
|
35
36
|
exports.resolveLaneBudget = resolveLaneBudget;
|
|
36
37
|
exports.resolveLanePlan = resolveLanePlan;
|
|
@@ -229,6 +230,45 @@ function fileRefPrompt(promptPath, repoRoot) {
|
|
|
229
230
|
`review request against that absolute root. Output only the resulting markdown review. ` +
|
|
230
231
|
`Do not edit any files.`);
|
|
231
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Re-point a resolved plan at a different prompt file (`review-lane invoke --prompt-file`, #5049).
|
|
235
|
+
*
|
|
236
|
+
* The override carries a budget-trimmed prompt, so it has to land wherever THIS lane actually
|
|
237
|
+
* reads its prompt, and that differs by channel:
|
|
238
|
+
* - `stdin`: the runner streams the file `plan.stdin` names.
|
|
239
|
+
* - `argv-file-ref`: the prompt PATH is embedded in a sentence in argv (`fileRefPrompt`), already
|
|
240
|
+
* expanded by the time an override can run. Swapping `plan.promptPath` alone changes nothing
|
|
241
|
+
* the CLI reads, which is how the override was dropped silently for cursor, antigravity and
|
|
242
|
+
* kimi-code.
|
|
243
|
+
* - `argv`: the bare path is an argv element.
|
|
244
|
+
* - `openai-http`: the runner reads `plan.promptPath`.
|
|
245
|
+
* Argv elements are matched whole (the file-ref sentence, or the bare path), never by substring,
|
|
246
|
+
* so an unrelated argument that merely contains the path is left alone. `promptPath` moves with
|
|
247
|
+
* argv: the antigravity handler re-derives the sentence it swaps from `plan.promptPath` and would
|
|
248
|
+
* otherwise stop recognising it. A plan that takes no prompt (`none`) is returned unchanged.
|
|
249
|
+
* Pure: returns a new plan and never mutates the one it is given.
|
|
250
|
+
*/
|
|
251
|
+
function applyPromptOverride(plan, promptPath, repoRoot) {
|
|
252
|
+
if (plan.transport !== 'spawn')
|
|
253
|
+
return { ...plan, promptPath };
|
|
254
|
+
if (plan.stdin)
|
|
255
|
+
return { ...plan, stdin: promptPath, promptPath };
|
|
256
|
+
const oldRef = fileRefPrompt(plan.promptPath, repoRoot);
|
|
257
|
+
const newRef = fileRefPrompt(promptPath, repoRoot);
|
|
258
|
+
let replaced = false;
|
|
259
|
+
const argv = plan.argv.map((a) => {
|
|
260
|
+
if (a === oldRef) {
|
|
261
|
+
replaced = true;
|
|
262
|
+
return newRef;
|
|
263
|
+
}
|
|
264
|
+
if (a === plan.promptPath) {
|
|
265
|
+
replaced = true;
|
|
266
|
+
return promptPath;
|
|
267
|
+
}
|
|
268
|
+
return a;
|
|
269
|
+
});
|
|
270
|
+
return replaced ? { ...plan, argv, promptPath } : plan;
|
|
271
|
+
}
|
|
232
272
|
/** Run-dir artifact paths. POSIX-joined: these are workflow-visible strings, not OS paths. */
|
|
233
273
|
function artifactPaths(runDir, slug) {
|
|
234
274
|
const base = String(runDir ?? '').replace(/\/+$/, '');
|
|
@@ -113,6 +113,22 @@ exports.KNOWN_REVIEWER_SLUGS = (() => {
|
|
|
113
113
|
return [];
|
|
114
114
|
}
|
|
115
115
|
})();
|
|
116
|
+
/**
|
|
117
|
+
* #5359: map a lane flag alias (`agy` for `--agy`) to its canonical slug via the
|
|
118
|
+
* descriptor roster's declared `flags` (same source as `review-lane explicit-from-argv`).
|
|
119
|
+
* Guarded like KNOWN_REVIEWER_SLUGS: a load failure degrades to the plain error.
|
|
120
|
+
*/
|
|
121
|
+
function laneSlugForAlias(name, lanes) {
|
|
122
|
+
try {
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
124
|
+
const roster = lanes ?? require('./review-lane-descriptor.cjs').REVIEWER_LANES;
|
|
125
|
+
// `l.slug !== name`: an undetected canonical slug (`--codex` flag) is not an alias.
|
|
126
|
+
return roster.find((l) => l.slug !== name && l.flags.includes(`--${name}`))?.slug;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
116
132
|
/** Instance names are lowercase slugs that must not shadow a built-in slug. */
|
|
117
133
|
exports.INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
118
134
|
function normalizeConfiguredDefaultReviewers(rawValue) {
|
|
@@ -239,7 +255,10 @@ function resolveReviewerSelection(input) {
|
|
|
239
255
|
// Sorted so the error order does not depend on flag order on the command line.
|
|
240
256
|
const missing = [...explicitFlags].filter((slug) => !detected.has(slug)).sort();
|
|
241
257
|
for (const slug of missing) {
|
|
242
|
-
|
|
258
|
+
const canonical = laneSlugForAlias(slug, input.lanes);
|
|
259
|
+
errors.push(canonical
|
|
260
|
+
? `explicit reviewer '${slug}' is a flag alias; pass its canonical slug '${canonical}'`
|
|
261
|
+
: `explicit reviewer '${slug}' is not available on this host`);
|
|
243
262
|
}
|
|
244
263
|
// Guarded on the error count as it stood BEFORE this branch, so the
|
|
245
264
|
// aggregate message fires under exactly the conditions it did previously.
|
|
@@ -1075,6 +1075,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1075
1075
|
// refusal below declines with the analyzer's own `missing_phase_details`
|
|
1076
1076
|
// vocabulary and leaves ROADMAP.md byte-identical.
|
|
1077
1077
|
let missingPhaseDetails = false;
|
|
1078
|
+
const warnings = [];
|
|
1078
1079
|
withPlanningLock(cwd, () => {
|
|
1079
1080
|
// #3957 (B9.4): captured BEFORE any transform runs, so the write/report
|
|
1080
1081
|
// decision below reflects whether the transforms actually changed
|
|
@@ -1190,6 +1191,16 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1190
1191
|
}
|
|
1191
1192
|
return text;
|
|
1192
1193
|
});
|
|
1194
|
+
// #4947: a word-prefixed Phase cell (`Phase 68`) is deliberately not a
|
|
1195
|
+
// writable table target (#4247 — the read side ignores it too), but when the
|
|
1196
|
+
// phase's heading section is still written the command reports
|
|
1197
|
+
// `updated: true` and the unwritten Status cell looked like a silent bug.
|
|
1198
|
+
// The row stays byte-identical; the output now says so.
|
|
1199
|
+
const wordPrefixedCellRe = new RegExp(`^Phase\\s+${phasePattern}\\.?(?:\\s|$)`, 'i');
|
|
1200
|
+
const progressTable = tableRowFound ? null : (0, markdown_table_cjs_1.findTableWithColumns)(gateActiveRegion, ['Phase', 'Status']);
|
|
1201
|
+
if (progressTable && progressTable.rows.some((row) => wordPrefixedCellRe.test((row['Phase'] ?? '').trim()))) {
|
|
1202
|
+
warnings.push(`Progress table row for phase ${formatDiagnosticToken(String(phaseNum))} was not updated: its Phase cell is word-prefixed ("Phase N"), and only a bare phase number (N or N.) is recognized. Edit that row by hand or rewrite the cell as the bare number.`);
|
|
1203
|
+
}
|
|
1193
1204
|
// Update plan count in phase detail section.
|
|
1194
1205
|
// Three recognised forms (all tolerated; canonical template uses the first):
|
|
1195
1206
|
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
@@ -1247,6 +1258,13 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1247
1258
|
// wording, so it correctly falls through to arm 3 untouched. Kept as a
|
|
1248
1259
|
// caller-side classifier (NOT seam grammar) reused below against the
|
|
1249
1260
|
// `boldField` node's own parsed `value`.
|
|
1261
|
+
// #4947: a value that is exactly `TBD` is the planner's own "no plans yet"
|
|
1262
|
+
// marker, not an operator annotation — once plans exist it contradicts the
|
|
1263
|
+
// checklist this verb inserts right under it. `TBD — note` keeps its note
|
|
1264
|
+
// (row 5): on the bold path the note is the node's trailing span, which the
|
|
1265
|
+
// caller checks is empty; on the plain path the note is part of the value,
|
|
1266
|
+
// so the whole-value comparison already excludes it.
|
|
1267
|
+
const isBareTbd = (value) => value.trim() === 'TBD';
|
|
1250
1268
|
const isTemplatePlaceholder = (value) => {
|
|
1251
1269
|
const trimmed = value.trim();
|
|
1252
1270
|
return /^\[\s*Number of plans\b[\s\S]*\]$/i.test(trimmed);
|
|
@@ -1272,6 +1290,35 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1272
1290
|
const m = value.match(/^\d+\s+plans?/i);
|
|
1273
1291
|
return m ? m[0].length : -1;
|
|
1274
1292
|
};
|
|
1293
|
+
// #4947: the pending-plan clause a human or planner glues after the count
|
|
1294
|
+
// (`; 2 gap-closure plans pending (48-18, 48-19)`) is DERIVED from the same
|
|
1295
|
+
// plan/summary facts this verb already computes, so a completed plan left
|
|
1296
|
+
// listed as pending contradicts the count the verb just wrote. The verb
|
|
1297
|
+
// owns that one clause: ids that now have a SUMMARY.md are removed, the
|
|
1298
|
+
// number is recounted, and an emptied clause is dropped. Everything else in
|
|
1299
|
+
// the tail (`in 11 waves`, free prose) stays the operator's — a wave total
|
|
1300
|
+
// does not change when a plan completes. A clause whose ids are not all
|
|
1301
|
+
// plan ids (`(see backlog)`) is not provably derived, so it is left alone.
|
|
1302
|
+
const summarizedPlanIds = new Set(phaseInfo.summaries
|
|
1303
|
+
.map((summaryFile) => summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
|
1304
|
+
.filter((id) => id !== ''));
|
|
1305
|
+
const pendingClauseRe = /;[ \t]*(\d+)[ \t]+((?:[\w-]+[ \t]+)*?)plans?[ \t]+pending[ \t]*\(([^()\r\n]*)\)/i;
|
|
1306
|
+
const planIdShape = /^\d+(?:\.\d+)?-\d+(?:\.\d+)?$/;
|
|
1307
|
+
const reconcilePendingClause = (tail) => {
|
|
1308
|
+
const m = pendingClauseRe.exec(tail);
|
|
1309
|
+
if (!m)
|
|
1310
|
+
return tail;
|
|
1311
|
+
const ids = m[3].split(',').map((id) => id.trim());
|
|
1312
|
+
if (ids.length === 0 || !ids.every((id) => planIdShape.test(id)))
|
|
1313
|
+
return tail;
|
|
1314
|
+
const stillPending = ids.filter((id) => !summarizedPlanIds.has(id));
|
|
1315
|
+
if (stillPending.length === ids.length)
|
|
1316
|
+
return tail;
|
|
1317
|
+
const replacement = stillPending.length === 0
|
|
1318
|
+
? ''
|
|
1319
|
+
: `; ${stillPending.length} ${m[2]}plan${stillPending.length === 1 ? '' : 's'} pending (${stillPending.join(', ')})`;
|
|
1320
|
+
return tail.slice(0, m.index) + replacement + tail.slice(m.index + m[0].length);
|
|
1321
|
+
};
|
|
1275
1322
|
roadmapContent = replaceInCurrentMilestone(roadmapContent, planSectionPattern, (sectionText) => {
|
|
1276
1323
|
const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(sectionText, 'ROADMAP.md');
|
|
1277
1324
|
if (!parsed.ok) {
|
|
@@ -1302,12 +1349,12 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1302
1349
|
const plainFractionLen = fractionCountPrefixLength(plainValue);
|
|
1303
1350
|
const plainBareLen = bareCountPrefixLength(plainValue);
|
|
1304
1351
|
const plainCountPrefixLen = plainFractionLen >= 0 ? plainFractionLen : plainBareLen;
|
|
1305
|
-
if (plainCountPrefixLen < 0 && !isTemplatePlaceholder(plainValue)) {
|
|
1352
|
+
if (plainCountPrefixLen < 0 && !isTemplatePlaceholder(plainValue) && !isBareTbd(plainValue)) {
|
|
1306
1353
|
// Arm 3: freeform prose, TBD, a bracketed human annotation, or an
|
|
1307
1354
|
// empty value — leave the section exactly as it was.
|
|
1308
1355
|
return sectionText;
|
|
1309
1356
|
}
|
|
1310
|
-
const plainSuffix = plainCountPrefixLen >= 0 ? plainValue.slice(plainCountPrefixLen) : '';
|
|
1357
|
+
const plainSuffix = plainCountPrefixLen >= 0 ? reconcilePendingClause(plainValue.slice(plainCountPrefixLen)) : '';
|
|
1311
1358
|
const newPlainLine = `${indent}Plans:${spacing}${planCountText}${plainSuffix}`;
|
|
1312
1359
|
const start = plainMatch.index ?? sectionText.indexOf(whole);
|
|
1313
1360
|
return sectionText.slice(0, start) + newPlainLine + sectionText.slice(start + whole.length);
|
|
@@ -1320,7 +1367,11 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1320
1367
|
const fractionPrefixLen = fractionCountPrefixLength(currentValue);
|
|
1321
1368
|
const barePrefixLen = bareCountPrefixLength(currentValue);
|
|
1322
1369
|
const countPrefixLen = fractionPrefixLen >= 0 ? fractionPrefixLen : barePrefixLen;
|
|
1323
|
-
|
|
1370
|
+
const fieldNode = parsed.value.nodes.find((n) => n.id === fieldId);
|
|
1371
|
+
const trailingText = fieldNode && fieldNode.kind === 'boldField'
|
|
1372
|
+
? parsed.value.source.slice(fieldNode.trailingSpan.start, fieldNode.trailingSpan.end)
|
|
1373
|
+
: '';
|
|
1374
|
+
if (countPrefixLen < 0 && !isTemplatePlaceholder(currentValue) && !(isBareTbd(currentValue) && trailingText.trim() === '')) {
|
|
1324
1375
|
// Arm 3: freeform prose, TBD, a bracketed human annotation, or an
|
|
1325
1376
|
// empty value — leave the section exactly as it was.
|
|
1326
1377
|
return sectionText;
|
|
@@ -1331,7 +1382,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1331
1382
|
// `setFieldValue`'s valueSpan/trailingSpan split additionally preserves
|
|
1332
1383
|
// any EM-DASH-separated trailing annotation (#2853) automatically — no
|
|
1333
1384
|
// separate "preserve trailing" branch needed for that shape.
|
|
1334
|
-
const suffix = countPrefixLen >= 0 ? currentValue.slice(countPrefixLen) : '';
|
|
1385
|
+
const suffix = countPrefixLen >= 0 ? reconcilePendingClause(currentValue.slice(countPrefixLen)) : '';
|
|
1335
1386
|
const newValueToWrite = planCountText + suffix;
|
|
1336
1387
|
const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
|
|
1337
1388
|
if (!staged.ok) {
|
|
@@ -1490,7 +1541,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
1490
1541
|
verification_stale_check_indeterminate: verificationStaleCheckIndeterminate,
|
|
1491
1542
|
};
|
|
1492
1543
|
if (updated) {
|
|
1493
|
-
output({ updated: true, ...computed }, raw, `${summaryCount}/${planCount} ${status}`);
|
|
1544
|
+
output(warnings.length > 0 ? { updated: true, ...computed, warnings } : { updated: true, ...computed }, raw, `${summaryCount}/${planCount} ${status}`);
|
|
1494
1545
|
}
|
|
1495
1546
|
else if (missingPhaseDetails) {
|
|
1496
1547
|
// #4247: honest refusal — the reason names the real condition (the
|