@opengsd/gsd-core 1.18.0 → 1.19.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 +2 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +2 -2
- package/agents/gsd-code-fixer.md +4 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/gsd-core/bin/gsd-tools.cjs +106 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +156 -74
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +17 -0
- package/gsd-core/bin/lib/config-loader.cjs +10 -0
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +34 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +65 -1
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +43 -1
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-id.cjs +21 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
- package/gsd-core/bin/lib/phase.cjs +115 -26
- package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -25
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +121 -16
- package/gsd-core/bin/lib/smart-entry.cjs +13 -0
- package/gsd-core/bin/lib/state-contract.cjs +29 -9
- package/gsd-core/bin/lib/state-document.cjs +12 -4
- package/gsd-core/bin/lib/state-transition.cjs +119 -24
- package/gsd-core/bin/lib/state.cjs +50 -28
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +2 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review.md +61 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +9 -5
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase.md +13 -13
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/review.md +16 -13
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +3 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +35 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -1
- package/hooks/dist/gsd-worktree-path-guard.js +28 -1
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +35 -2
- package/hooks/gsd-windsurf-pre-write.js +28 -1
- package/hooks/gsd-workflow-guard.js +1 -1
- package/hooks/gsd-worktree-path-guard.js +28 -1
- package/hooks/hooks.json +1 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +13 -6
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +769 -0
- package/pi/package-agents/gsd-code-reviewer.md +402 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +894 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +51 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +20 -7
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +11 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- package/scripts/xtrace-phase-timing.cjs +0 -205
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.locateProgressTable = locateProgressTable;
|
|
25
|
+
exports.normalizeMilestoneToken = normalizeMilestoneToken;
|
|
26
|
+
exports.milestoneVersionOf = milestoneVersionOf;
|
|
25
27
|
exports.deriveProgressFromRoadmap = deriveProgressFromRoadmap;
|
|
26
28
|
exports.clampPercentFromFraction = clampPercentFromFraction;
|
|
27
29
|
exports.clampPercent = clampPercent;
|
|
@@ -68,6 +70,28 @@ function locateProgressTable(roadmapContent) {
|
|
|
68
70
|
}
|
|
69
71
|
return (0, markdown_table_cjs_1.findTableWithColumns)(scoped, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
|
70
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* #5165: a milestone token compared by identity, not spelling — `v1.1`, `V1.1`
|
|
75
|
+
* and ` 1.1 ` name one milestone. A hand-decorated Progress cell
|
|
76
|
+
* (`**v1.1**`, `` `v1.1` ``, `v1.1 (current)`) still names its first
|
|
77
|
+
* whitespace-separated token. Absent/blank tokens normalize to `''`.
|
|
78
|
+
*/
|
|
79
|
+
function normalizeMilestoneToken(value) {
|
|
80
|
+
// `[v1.1](ROADMAP.md#v11)` names its link text.
|
|
81
|
+
const unlinked = (milestoneVersionOf(value) ?? '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1');
|
|
82
|
+
const first = unlinked.split(/\s+/)[0] ?? '';
|
|
83
|
+
return first.replace(/^[*_`]+|[*_`:,;]+$/g, '').replace(/^v/i, '').toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* #5165: a STATE.md `milestone:` frontmatter value as a trimmed version string,
|
|
87
|
+
* or null when it is absent, blank or not scalar text.
|
|
88
|
+
*/
|
|
89
|
+
function milestoneVersionOf(value) {
|
|
90
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
91
|
+
return null;
|
|
92
|
+
const text = String(value).trim();
|
|
93
|
+
return text === '' ? null : text;
|
|
94
|
+
}
|
|
71
95
|
/**
|
|
72
96
|
* Derive completed_phases, total_phases, and total_plans from ROADMAP content.
|
|
73
97
|
* Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.
|
|
@@ -82,7 +106,7 @@ function locateProgressTable(roadmapContent) {
|
|
|
82
106
|
* 5-column milestone-grouped variant that inserts a `Milestone` column ahead
|
|
83
107
|
* of them).
|
|
84
108
|
*/
|
|
85
|
-
function deriveProgressFromRoadmap(roadmapContent) {
|
|
109
|
+
function deriveProgressFromRoadmap(roadmapContent, milestoneFilter) {
|
|
86
110
|
let completedPhases = null;
|
|
87
111
|
let totalPhases = null;
|
|
88
112
|
let totalPlans = null;
|
|
@@ -95,10 +119,19 @@ function deriveProgressFromRoadmap(roadmapContent) {
|
|
|
95
119
|
// `RoadmapProgress` contract (nulls = absent) is unchanged.
|
|
96
120
|
const table = locateProgressTable(roadmapContent);
|
|
97
121
|
if (table) {
|
|
98
|
-
|
|
122
|
+
// #5165: the milestone-grouped Progress table (templates/roadmap.md) keeps
|
|
123
|
+
// every milestone's rows, shipped ones included. When the caller names the
|
|
124
|
+
// milestone and the table carries a `Milestone` column, count only that
|
|
125
|
+
// milestone's rows. A table without the column (or a caller without a
|
|
126
|
+
// milestone) keeps the whole-table behavior; a named milestone that matches
|
|
127
|
+
// no row yields nulls (nothing countable) rather than a foreign count.
|
|
128
|
+
const wantMilestone = normalizeMilestoneToken(milestoneFilter);
|
|
129
|
+
const scopedRows = wantMilestone !== '' && table.columns.includes('Milestone')
|
|
130
|
+
? table.rows.filter((r) => normalizeMilestoneToken(r['Milestone']) === wantMilestone)
|
|
131
|
+
: table.rows;
|
|
99
132
|
// Data rows only (exclude sentinel phases 0 and 999.x).
|
|
100
133
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
101
|
-
const dataRows =
|
|
134
|
+
const dataRows = scopedRows.filter((r) => {
|
|
102
135
|
const phase = (r['Phase'] ?? '').trim();
|
|
103
136
|
return /^\d/.test(phase) && !isSentinelPhaseId(phase);
|
|
104
137
|
});
|
|
@@ -110,7 +143,7 @@ function deriveProgressFromRoadmap(roadmapContent) {
|
|
|
110
143
|
const completed = dataRows.filter((r) => (0, phase_status_cjs_1.parseRoadmapStatusCell)(r['Status']) === phase_status_cjs_1.WIRE_STATUS.COMPLETE).length;
|
|
111
144
|
completedPhases = completed > 0 ? completed : null;
|
|
112
145
|
let totalPlansSum = 0;
|
|
113
|
-
for (const r of
|
|
146
|
+
for (const r of scopedRows) {
|
|
114
147
|
const cell = (r['Plans Complete'] ?? '').trim();
|
|
115
148
|
const m = /(\d+)\s*\/\s*(\d+)/.exec(cell);
|
|
116
149
|
if (m)
|
|
@@ -54,13 +54,13 @@ const planningScopeMod = require("./planning-scope.cjs");
|
|
|
54
54
|
const { SCOPE } = planningScopeMod;
|
|
55
55
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- roadmap-parser.cjs is an export= CommonJS module
|
|
56
56
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
57
|
-
const { stripShippedMilestones, extractCurrentMilestone, currentMilestoneRawRanges, withPhaseSection, findMilestoneScopeHeadingLines } = roadmapParserMod;
|
|
57
|
+
const { stripShippedMilestones, extractCurrentMilestone, currentMilestoneRawRanges, withPhaseSection, editOutsideShippedBlocks, findMilestoneScopeHeadingLines } = roadmapParserMod;
|
|
58
58
|
// #4129: the single owner of "count the ROADMAP's milestone Complete rows"
|
|
59
59
|
// (pure computation, no I/O — no cycle on this path) for the intent-first
|
|
60
60
|
// progress counters the phase-complete transaction passes downstream.
|
|
61
61
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-lifecycle.cjs is an export= CommonJS module
|
|
62
62
|
const phaseLifecycleMod = require("./phase-lifecycle.cjs");
|
|
63
|
-
const { deriveProgressFromRoadmap: deriveProgressFromRoadmapForIntent, clampPercent: clampPercentForIntent } = phaseLifecycleMod;
|
|
63
|
+
const { deriveProgressFromRoadmap: deriveProgressFromRoadmapForIntent, clampPercent: clampPercentForIntent, milestoneVersionOf, locateProgressTable, normalizeMilestoneToken } = phaseLifecycleMod;
|
|
64
64
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
65
65
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
66
66
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
@@ -1225,7 +1225,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1225
1225
|
timeout: 10_000,
|
|
1226
1226
|
windowsHide: true,
|
|
1227
1227
|
maxBuffer: 4 * 1024 * 1024,
|
|
1228
|
-
});
|
|
1228
|
+
}); // allow-index-refresh: lists worktrees from .git metadata, never opens the index
|
|
1229
1229
|
}
|
|
1230
1230
|
catch {
|
|
1231
1231
|
return; // not a git repo / git unavailable — unchanged behavior
|
|
@@ -1577,6 +1577,21 @@ function scanExistingDecimalPhaseNumbers(phasesDir, rawContent, base) {
|
|
|
1577
1577
|
}
|
|
1578
1578
|
return decimalSet;
|
|
1579
1579
|
}
|
|
1580
|
+
/**
|
|
1581
|
+
* #5599: true when `token` (a heading/bullet phase number such as "13.1.2") is
|
|
1582
|
+
* a STRICT descendant of `ancestor` ("13"). Compared segment-wise so "130" is
|
|
1583
|
+
* not inside "13", and digit segments compare numerically so a zero-padded
|
|
1584
|
+
* "05.1" matches "5".
|
|
1585
|
+
*/
|
|
1586
|
+
function isPhaseTokenWithin(token, ancestor) {
|
|
1587
|
+
const t = token.split('.');
|
|
1588
|
+
const a = ancestor.split('.');
|
|
1589
|
+
if (t.length <= a.length)
|
|
1590
|
+
return false;
|
|
1591
|
+
// Zero padding is not significant ("05" = "5", "03A" = "3A"); case is not either.
|
|
1592
|
+
const canon = (segment) => segment.replace(/^0+(?=\d)/, '').toLowerCase();
|
|
1593
|
+
return a.every((segment, i) => canon(segment) === canon(t[i]));
|
|
1594
|
+
}
|
|
1580
1595
|
function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested') {
|
|
1581
1596
|
if (!afterPhase || !description) {
|
|
1582
1597
|
error('after-phase and description required for phase insert');
|
|
@@ -1638,6 +1653,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
1638
1653
|
const siblingNextDecimal = siblingDecimalSet.size === 0 ? 1 : Math.max(...siblingDecimalSet) + 1;
|
|
1639
1654
|
_decimalPhase = `${parentBase}.${siblingNextDecimal}`;
|
|
1640
1655
|
}
|
|
1656
|
+
// #5599: the family whose subtree the new entry must land at the end of —
|
|
1657
|
+
// the parent for a sibling allocation, the target itself for a nested one.
|
|
1658
|
+
const subtreeRoot = allocation === 'sibling' && lastDotIndex !== -1
|
|
1659
|
+
? normalizedBase.slice(0, lastDotIndex)
|
|
1660
|
+
: normalizedBase;
|
|
1641
1661
|
const insertConfig = loadConfig(cwd);
|
|
1642
1662
|
const projectCode = insertConfig.project_code || '';
|
|
1643
1663
|
const pfx = projectCode ? `${projectCode}-` : '';
|
|
@@ -1675,13 +1695,17 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
1675
1695
|
}
|
|
1676
1696
|
const bulletLineEnd = rawContent.indexOf(bulletMatchResult[0]) + bulletMatchResult[0].length;
|
|
1677
1697
|
const afterBullet = rawContent.slice(bulletLineEnd);
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1698
|
+
// #5599: land after the last bullet inside the allocation family's
|
|
1699
|
+
// subtree, not before the first following bullet of any number (which is
|
|
1700
|
+
// the target's own later decimal child when one exists).
|
|
1701
|
+
const bulletRe = /\r?\n-\s*\[[ x]\]\s*(?:\*\*)?Phase\s+(?<tok>\d[^\s:*]*)[^\r\n]*/gi;
|
|
1702
|
+
let insertIdx = bulletLineEnd;
|
|
1703
|
+
for (const m of afterBullet.matchAll(bulletRe)) {
|
|
1704
|
+
if (!isPhaseTokenWithin(m.groups.tok, subtreeRoot)) {
|
|
1705
|
+
insertIdx = bulletLineEnd + m.index;
|
|
1706
|
+
break;
|
|
1707
|
+
}
|
|
1708
|
+
insertIdx = bulletLineEnd + m.index + m[0].length;
|
|
1685
1709
|
}
|
|
1686
1710
|
updatedContent =
|
|
1687
1711
|
rawContent.slice(0, insertIdx) + bulletEntry + rawContent.slice(insertIdx);
|
|
@@ -1723,13 +1747,19 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
1723
1747
|
// preserved exactly as the prior literal had it — a pre-existing,
|
|
1724
1748
|
// deliberately-unchanged asymmetry with the other sites in this file.
|
|
1725
1749
|
const nextPhaseHeadingPrefix = `#{2,4}\\s+${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, insertConvention)}`;
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1750
|
+
// #5599: the boundary is the first following heading OUTSIDE the
|
|
1751
|
+
// allocation family's subtree. Taking the first heading of any number
|
|
1752
|
+
// spliced the new section above the target's own later decimal children
|
|
1753
|
+
// (13, 13.2, 13.1, 14); the allocator already guarantees the new number
|
|
1754
|
+
// exceeds every existing descendant, so "after the last descendant" is
|
|
1755
|
+
// the numerically correct position.
|
|
1756
|
+
const nextPhaseRe = new RegExp(`\\r?\\n${nextPhaseHeadingPrefix}(?<tok>\\d[^\\s:]*)`, 'gi');
|
|
1757
|
+
let insertIdx = rawContent.length;
|
|
1758
|
+
for (const m of afterHeader.matchAll(nextPhaseRe)) {
|
|
1759
|
+
if (!isPhaseTokenWithin(m.groups.tok, subtreeRoot)) {
|
|
1760
|
+
insertIdx = headerIdx + headerMatch[0].length + m.index;
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1733
1763
|
}
|
|
1734
1764
|
updatedContent =
|
|
1735
1765
|
rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
|
|
@@ -3451,6 +3481,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3451
3481
|
const runPhaseCompleteTransaction = () => {
|
|
3452
3482
|
const writes = [];
|
|
3453
3483
|
let roadmapContent = null;
|
|
3484
|
+
// #5165: the phase was ALREADY complete in the ROADMAP this transaction
|
|
3485
|
+
// started from (read before any mutation below): its checklist box was
|
|
3486
|
+
// checked, or its Progress-table row read Complete.
|
|
3487
|
+
let phaseWasAlreadyComplete = false;
|
|
3454
3488
|
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
3455
3489
|
const originalRoadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
3456
3490
|
roadmapContent = originalRoadmapContent;
|
|
@@ -3488,6 +3522,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3488
3522
|
// its own `- [ ] Phase N: …` entries inside a fenced code block, but it
|
|
3489
3523
|
// is a real (and correct) behavioural difference on pathological input.
|
|
3490
3524
|
const checkboxPattern = new RegExp(`^[ \\t]*(-\\s*\\[)[ ](\\]\\s*(?:\\*\\*)?\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
|
|
3525
|
+
// #5165: pre-mutation read of the same checklist line, checked this time.
|
|
3526
|
+
const checkedBoxPattern = new RegExp(`^[ \\t]*(-\\s*\\[)[xX](\\]\\s*(?:\\*\\*)?\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`);
|
|
3527
|
+
const originalMilestoneScope = extractCurrentMilestone(originalRoadmapContent, cwd);
|
|
3528
|
+
phaseWasAlreadyComplete = originalMilestoneScope
|
|
3529
|
+
.split('\n')
|
|
3530
|
+
.some((line) => checkedBoxPattern.test(line));
|
|
3491
3531
|
// Progress table row: update Plans Complete/Status/Completed columns BY
|
|
3492
3532
|
// COLUMN NAME (handles 4- or 5-column RoadmapProgress tables) via the
|
|
3493
3533
|
// markdown-table seam (ADR-2143 §7) — supersedes the prior ordinal
|
|
@@ -3504,6 +3544,22 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3504
3544
|
const phaseCellRe = new RegExp(`^${phaseEscaped}\\.?(?:\\s|$)`, 'i');
|
|
3505
3545
|
const rowMatch = (row) => phaseCellRe.test((row['Phase'] ?? '').trim());
|
|
3506
3546
|
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
|
3547
|
+
// #5165: a ROADMAP whose checklist box was never ticked (or has none) but
|
|
3548
|
+
// whose Progress-table row already reads Complete is just as complete. A
|
|
3549
|
+
// milestone-grouped table (a `Milestone` column) is read only for the
|
|
3550
|
+
// milestone ROADMAP binds, so a shipped milestone's same-numbered phase
|
|
3551
|
+
// cannot count.
|
|
3552
|
+
if (!phaseWasAlreadyComplete) {
|
|
3553
|
+
const progressTable = locateProgressTable(originalMilestoneScope);
|
|
3554
|
+
if (progressTable !== null) {
|
|
3555
|
+
const wantMilestone = progressTable.columns.includes('Milestone')
|
|
3556
|
+
? normalizeMilestoneToken(roadmapParserMod.getMilestoneInfo(cwd).value?.version)
|
|
3557
|
+
: null;
|
|
3558
|
+
phaseWasAlreadyComplete = progressTable.rows.some((row) => rowMatch(row)
|
|
3559
|
+
&& (0, phase_status_cjs_1.parseRoadmapStatusCell)(row['Status']) === phase_status_cjs_1.WIRE_STATUS.COMPLETE
|
|
3560
|
+
&& (wantMilestone === null || (wantMilestone !== '' && normalizeMilestoneToken(row['Milestone']) === wantMilestone)));
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3507
3563
|
/**
|
|
3508
3564
|
* Within `text` (already scoped to one milestone window by the
|
|
3509
3565
|
* caller), scope further to the `## Progress` heading section (up to
|
|
@@ -3641,7 +3697,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3641
3697
|
const plansResult = (0, markdown_table_cjs_1.updateTableCell)(text, rowMatch, 'Plans Complete', ` ${summaryCount}/${planCount} `);
|
|
3642
3698
|
if (plansResult.ok)
|
|
3643
3699
|
text = plansResult.value;
|
|
3644
|
-
|
|
3700
|
+
// #5165: a Status cell that already reads Complete is left untouched —
|
|
3701
|
+
// re-padding it rewrote a cell the re-run had no business editing.
|
|
3702
|
+
const statusResult = (0, markdown_table_cjs_1.updateTableCell)(text, rowMatch, 'Status', (current) => (0, phase_status_cjs_1.parseRoadmapStatusCell)(current) === phase_status_cjs_1.WIRE_STATUS.COMPLETE
|
|
3703
|
+
? current
|
|
3704
|
+
: ` ${(0, phase_status_cjs_1.toRoadmapStatusCell)(phase_status_cjs_1.PHASE_STATUS.COMPLETE).padEnd(11)} `);
|
|
3645
3705
|
if (statusResult.ok)
|
|
3646
3706
|
text = statusResult.value;
|
|
3647
3707
|
// Preserve only a valid ISO date (#1161: idempotent; self-heal
|
|
@@ -3667,12 +3727,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3667
3727
|
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
3668
3728
|
if (!planId)
|
|
3669
3729
|
continue;
|
|
3670
|
-
|
|
3730
|
+
// #5542: only a summary paired with a live (non-superseded) plan
|
|
3731
|
+
// flips a row — the same pairing rule the counts use.
|
|
3732
|
+
if (!coreUtilsMod.summaryPairsWithAnyPlan(phaseInfo['plans'] ?? [], summaryFile))
|
|
3733
|
+
continue;
|
|
3671
3734
|
// ADR-5057 §6 (Phase 13, #5217): the per-plan flip is a bullet
|
|
3672
3735
|
// mutation, so it goes through `updateBullet` like the phase
|
|
3673
3736
|
// checkbox flip above — first matching bullet only, never a
|
|
3674
3737
|
// fenced line, never a mid-line occurrence.
|
|
3675
|
-
const planCheckboxPattern =
|
|
3738
|
+
const planCheckboxPattern = coreUtilsMod.planRowPattern(planId, { anchored: true });
|
|
3676
3739
|
b = (0, markdown_sectionizer_cjs_1.updateBullet)(b, (_bulletText, rawLine) => planCheckboxPattern.test(rawLine), (rawLine) => rawLine.replace(planCheckboxPattern, '$1x$2'));
|
|
3677
3740
|
}
|
|
3678
3741
|
return b;
|
|
@@ -3694,7 +3757,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3694
3757
|
}
|
|
3695
3758
|
}
|
|
3696
3759
|
else {
|
|
3697
|
-
|
|
3760
|
+
// #5543: the active milestone could not be resolved (no STATE
|
|
3761
|
+
// `milestone:`, no marker). Writing the whole document would let
|
|
3762
|
+
// `withPhaseSection` pick the first `### Phase N:` in document order,
|
|
3763
|
+
// which can sit inside a shipped `<details>` block. Hide the shipped
|
|
3764
|
+
// blocks from the mutation and restore them untouched; a ROADMAP with
|
|
3765
|
+
// no shipped block is mutated whole, as before.
|
|
3766
|
+
roadmapContent = editOutsideShippedBlocks(roadmapContent, mutateMilestonePhase);
|
|
3698
3767
|
}
|
|
3699
3768
|
writes.push({
|
|
3700
3769
|
filePath: roadmapPath,
|
|
@@ -4288,6 +4357,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4288
4357
|
}
|
|
4289
4358
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
4290
4359
|
const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
4360
|
+
// #5165: the milestone STATE asserts (ROADMAP-bound identity first,
|
|
4361
|
+
// else STATE's own frontmatter) scopes the post-completion progress
|
|
4362
|
+
// derivation to that milestone's Progress-table rows.
|
|
4363
|
+
const stateFm = frontmatterMod.extractFrontmatter(originalStateContent, statePath);
|
|
4364
|
+
const storedMilestone = roadmapParserMod.getMilestoneInfo(cwd).value?.version
|
|
4365
|
+
?? milestoneVersionOf(stateFm['milestone']);
|
|
4366
|
+
// #5165: re-completing a phase that is already complete in ROADMAP.md
|
|
4367
|
+
// while STATE has moved past it is a no-op for STATE's position —
|
|
4368
|
+
// the transition, the metrics row and the authoritative current-phase
|
|
4369
|
+
// pin would all rewind it. A first-time completion (box unchecked, or
|
|
4370
|
+
// STATE not past N, #2028 out-of-order) still transitions.
|
|
4371
|
+
const stateAlreadyPast = phaseWasAlreadyComplete
|
|
4372
|
+
&& (0, state_transition_cjs_1.isStatePastPhase)(originalStateContent, phaseNum, statePath);
|
|
4291
4373
|
let stateContent = originalStateContent;
|
|
4292
4374
|
// ADR-1769 Phase 3: the STATE.md field-update policy (Current Phase
|
|
4293
4375
|
// shape/name, Status, Current Plan, Last Activity + Description, and
|
|
@@ -4302,8 +4384,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4302
4384
|
// NOT go through readModifyWriteStateMd because STATE.md is
|
|
4303
4385
|
// committed atomically with ROADMAP/REQUIREMENTS, ADR-3408 §8.3 /
|
|
4304
4386
|
// #3374 / #3469).
|
|
4305
|
-
const nextPhaseDisplayName =
|
|
4306
|
-
|
|
4387
|
+
const nextPhaseDisplayName = stateAlreadyPast
|
|
4388
|
+
? null
|
|
4389
|
+
: phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
|
|
4390
|
+
phaseDisplayNameFromSlug(nextPhaseName);
|
|
4307
4391
|
// #4824: classify the SELECTED next phase from disk (the same ladder
|
|
4308
4392
|
// roadmap analyze uses) so the Status / Current Plan / Stopped-at
|
|
4309
4393
|
// handoff does not say "ready to plan" for a phase already planned or
|
|
@@ -4324,7 +4408,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4324
4408
|
/* best-effort: an unreadable next phase keeps `Ready to plan` */
|
|
4325
4409
|
}
|
|
4326
4410
|
}
|
|
4327
|
-
const completeResult = (0, state_transition_cjs_1.transitionCore)(stateContent, {
|
|
4411
|
+
const completeResult = stateAlreadyPast ? { content: stateContent } : (0, state_transition_cjs_1.transitionCore)(stateContent, {
|
|
4328
4412
|
kind: 'completePhase',
|
|
4329
4413
|
phaseNum,
|
|
4330
4414
|
nextPhaseNum,
|
|
@@ -4333,6 +4417,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4333
4417
|
planCount,
|
|
4334
4418
|
summaryCount,
|
|
4335
4419
|
nextPhaseDisk,
|
|
4420
|
+
// #5165: the body counter and the frontmatter progress below count the
|
|
4421
|
+
// SAME milestone's Progress rows.
|
|
4422
|
+
milestone: storedMilestone,
|
|
4336
4423
|
}, {
|
|
4337
4424
|
clock: clock_cjs_1.realClock,
|
|
4338
4425
|
// #4901: the body counter derives from the same current-milestone scope
|
|
@@ -4343,7 +4430,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4343
4430
|
sourcePath: statePath,
|
|
4344
4431
|
});
|
|
4345
4432
|
stateContent = completeResult.content;
|
|
4346
|
-
|
|
4433
|
+
if (!stateAlreadyPast) {
|
|
4434
|
+
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
4435
|
+
}
|
|
4347
4436
|
// #2736: the transition holds the next phase's exact display name in
|
|
4348
4437
|
// the intent; pass it as authoritative so the sync's prose
|
|
4349
4438
|
// re-derivation cannot rewrite current_phase_name to the name's own
|
|
@@ -4376,7 +4465,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
4376
4465
|
? extractCurrentMilestone(roadmapContent, cwd)
|
|
4377
4466
|
: null;
|
|
4378
4467
|
const postCompletionRoadmapProgress = postCompletionRoadmapScope !== null
|
|
4379
|
-
? deriveProgressFromRoadmapForIntent(postCompletionRoadmapScope)
|
|
4468
|
+
? deriveProgressFromRoadmapForIntent(postCompletionRoadmapScope, storedMilestone)
|
|
4380
4469
|
: null;
|
|
4381
4470
|
const authoritativeProgress = postCompletionRoadmapProgress && postCompletionRoadmapProgress.completedPhases !== null
|
|
4382
4471
|
? postCompletionRoadmapProgress.totalPhases !== null && postCompletionRoadmapProgress.totalPhases > 0
|
|
@@ -129,7 +129,9 @@ function buildPhaseSnapshot(phasesDir, dir, convention) {
|
|
|
129
129
|
dir,
|
|
130
130
|
complete: completionResult.value.complete,
|
|
131
131
|
verificationStatus: completionResult.value.verification.status,
|
|
132
|
-
verificationStatusError: statusError
|
|
132
|
+
verificationStatusError: statusError
|
|
133
|
+
? { file: statusError.file, message: statusError.message, rawStatus: statusError.rawStatus }
|
|
134
|
+
: null,
|
|
133
135
|
planCount: scanResult.planCount,
|
|
134
136
|
summaryCount: scanResult.summaryCount,
|
|
135
137
|
scope: worstScope(completionResult.scope, scanResult.scope),
|
|
@@ -630,6 +630,22 @@ function findContextMdIn(absDirOrFiles) {
|
|
|
630
630
|
return { file: null, files: [], scope: SCOPE.UNREADABLE };
|
|
631
631
|
}
|
|
632
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* Is `name` the phase's requirements SPEC (#5629)? A plain `NN-SPEC.md`: the `-AI-SPEC.md` (AI design
|
|
635
|
+
* contract) and `-UI-SPEC.md` (UI design contract) variants have other purposes and never lock requirements.
|
|
636
|
+
* The one definition behind the init payloads' `has_spec` / `spec_path`, the discuss and plan-phase workflows
|
|
637
|
+
* (which read those fields) and the context-drift gate, so they cannot disagree about which file is "the SPEC".
|
|
638
|
+
*/
|
|
639
|
+
function isPhaseSpecFile(name) {
|
|
640
|
+
return name.endsWith('-SPEC.md') && !name.endsWith('-AI-SPEC.md') && !name.endsWith('-UI-SPEC.md');
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The phase SPEC in an already-read directory listing, or `null`. The lexically first match, so the answer
|
|
644
|
+
* does not depend on the filesystem's readdir order. The caller's array is not reordered.
|
|
645
|
+
*/
|
|
646
|
+
function findPhaseSpecIn(files) {
|
|
647
|
+
return files.filter(isPhaseSpecFile).sort()[0] ?? null;
|
|
648
|
+
}
|
|
633
649
|
module.exports = {
|
|
634
650
|
worktreesOptedOut,
|
|
635
651
|
readScopedConfigValue,
|
|
@@ -655,6 +671,8 @@ module.exports = {
|
|
|
655
671
|
diagnoseUnresolvedActiveWorkstream,
|
|
656
672
|
describeUnresolvedWorkstreamReason,
|
|
657
673
|
findContextMdIn,
|
|
674
|
+
isPhaseSpecFile,
|
|
675
|
+
findPhaseSpecIn,
|
|
658
676
|
// Test seam (audit M1): inject a deterministic isPidAlive so the liveness-gated
|
|
659
677
|
// steal decision is exercised without real pids. Mirrors capability-lock.cts.
|
|
660
678
|
_setLockProbes(probes) {
|
|
@@ -34,6 +34,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
34
34
|
const node_path_1 = __importDefault(require("node:path"));
|
|
35
35
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
36
36
|
const node_child_process_1 = require("node:child_process");
|
|
37
|
+
const read_only_git_env_cjs_1 = require("./read-only-git-env.cjs");
|
|
37
38
|
/**
|
|
38
39
|
* SHA-256 hex digest of a file's raw bytes. Byte-for-byte the same digest
|
|
39
40
|
* install.js fileHash() records into manifests and backup-meta.json.
|
|
@@ -144,13 +145,20 @@ function isCleanRelativePosixPath(relPath) {
|
|
|
144
145
|
}
|
|
145
146
|
function gitExec(gitDir, args) {
|
|
146
147
|
return (0, node_child_process_1.execFileSync)('git', args, {
|
|
148
|
+
// windowsHide (#685): a console-window flash per git call would spam the
|
|
149
|
+
// user on Windows for what is a background, read-only history walk. Kept
|
|
150
|
+
// high in the options so the completeness guard's 400-char scan finds it.
|
|
151
|
+
windowsHide: true,
|
|
147
152
|
cwd: gitDir,
|
|
148
153
|
encoding: 'utf8',
|
|
154
|
+
// #5048: every call here is a history/objects walk that never opens the
|
|
155
|
+
// index, so the variable is inert today — set anyway so a future `status` /
|
|
156
|
+
// `diff` caller inherits it. The block is owned by
|
|
157
|
+
// src/read-only-git-env.cts and shared with every other read-only seam.
|
|
158
|
+
// Parity: tests/git-optional-locks-parity.test.cjs.
|
|
159
|
+
env: (0, read_only_git_env_cjs_1.readOnlyGitEnv)(),
|
|
149
160
|
timeout: GIT_SUBPROCESS_TIMEOUT_MS,
|
|
150
161
|
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
151
|
-
// windowsHide (#685): a console-window flash per git call would spam the
|
|
152
|
-
// user on Windows for what is a background, read-only history walk.
|
|
153
|
-
windowsHide: true,
|
|
154
162
|
// stderr is discarded: "file absent in commit" is an expected walk outcome,
|
|
155
163
|
// not operator-visible diagnostics.
|
|
156
164
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* read-only-git-env.cts — the single owner of the read-only git env block
|
|
4
|
+
* (#5048). CONTEXT.md / OS Shell Projection seam.
|
|
5
|
+
*
|
|
6
|
+
* By default `git status` refreshes the index and takes an *optional*
|
|
7
|
+
* `.git/index.lock` to write the refreshed copy back. That is a write the caller
|
|
8
|
+
* never asked for, and it turns a pure read into a contender for the lock a
|
|
9
|
+
* real `git add` / `git commit` needs — so a read-only statusline render can
|
|
10
|
+
* make a concurrent commit fail with
|
|
11
|
+
* `Unable to create '.git/index.lock': File exists`. `GIT_OPTIONAL_LOCKS=0`
|
|
12
|
+
* tells git to skip only those *optional* index operations. Nothing a read
|
|
13
|
+
* needs is lost: `git status` reads the index, it just doesn't need to rewrite
|
|
14
|
+
* it.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this module exists
|
|
17
|
+
*
|
|
18
|
+
* The block used to be hand-written at every seam that spawns git for a read.
|
|
19
|
+
* Six copies of `{ ...process.env, GIT_OPTIONAL_LOCKS: '0' }` is six places to
|
|
20
|
+
* forget when a seam is added, and a probe that checks each seam for the literal
|
|
21
|
+
* value passes even when a seam stopped consulting the owner. One owner makes
|
|
22
|
+
* the drift structural instead: the parity test patches THIS function to a
|
|
23
|
+
* sentinel and asserts every seam's spawn carries the sentinel, so a seam that
|
|
24
|
+
* hand-writes the variable again fails red rather than passing quietly.
|
|
25
|
+
* (tests/git-optional-locks-parity.test.cjs, "#5048 every seam reads the shared
|
|
26
|
+
* owner".)
|
|
27
|
+
*
|
|
28
|
+
* ## Why the hook scripts require the built artifact
|
|
29
|
+
*
|
|
30
|
+
* `hooks/*.js` are staged as standalone files at install time, so they reach
|
|
31
|
+
* compiled `src/` modules through `../gsd-core/bin/lib/*.cjs` rather than a
|
|
32
|
+
* sibling hooks/-tree file — the same rule hooks/lib/git-cmd.js follows for
|
|
33
|
+
* token-scanner. They call `readOnlyGitEnv()` per spawn rather than snapshotting
|
|
34
|
+
* the result at module load, so a later `process.env` mutation is visible and the
|
|
35
|
+
* two behave identically.
|
|
36
|
+
*/
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.readOnlyGitEnv = readOnlyGitEnv;
|
|
39
|
+
/**
|
|
40
|
+
* Build the env for a read-only git spawn.
|
|
41
|
+
*
|
|
42
|
+
* @param base Environment to spread, `process.env` by default. Callers that
|
|
43
|
+
* layer extra git variables on top (GIT_TERMINAL_PROMPT, a caller's own
|
|
44
|
+
* overrides) pass them in `base`; callers that must let a caller-supplied
|
|
45
|
+
* override win spread the result and apply their own keys afterwards.
|
|
46
|
+
* @returns A fresh object each call — never the cached `process.env`.
|
|
47
|
+
*/
|
|
48
|
+
function readOnlyGitEnv(base = process.env) {
|
|
49
|
+
const env = {};
|
|
50
|
+
for (const [key, value] of Object.entries(base)) {
|
|
51
|
+
// process.env carries undefined-valued keys on some platforms; spawn()
|
|
52
|
+
// rejects a non-string value rather than ignoring it.
|
|
53
|
+
if (value !== undefined)
|
|
54
|
+
env[key] = value;
|
|
55
|
+
}
|
|
56
|
+
env.GIT_OPTIONAL_LOCKS = '0';
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
@@ -58,6 +58,7 @@ exports.DOCS_PARITY_VIOLATION = exports.LANE_SLUG_RE = exports.PARITY_VIOLATION
|
|
|
58
58
|
exports.mergeReviewerLanes = mergeReviewerLanes;
|
|
59
59
|
exports.checkReviewerLaneParity = checkReviewerLaneParity;
|
|
60
60
|
exports.checkReviewerDocsParity = checkReviewerDocsParity;
|
|
61
|
+
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
61
62
|
/**
|
|
62
63
|
* Argv placeholders (Phase 5b, #2799).
|
|
63
64
|
*
|
|
@@ -576,6 +577,7 @@ exports.PARITY_VIOLATION = Object.freeze({
|
|
|
576
577
|
REGISTRY_LANE_UNDECLARED: 'registry_lane_undeclared',
|
|
577
578
|
DESCRIPTOR_LANE_NOT_IN_REGISTRY: 'descriptor_lane_not_in_registry',
|
|
578
579
|
BESPOKE_LEG_PRESENT: 'bespoke_leg_present',
|
|
580
|
+
AVAILABILITY_FALLBACK_MISMATCH: 'availability_fallback_mismatch',
|
|
579
581
|
DUPLICATE_SLUG: 'duplicate_slug',
|
|
580
582
|
DUPLICATE_FLAG: 'duplicate_flag',
|
|
581
583
|
DUPLICATE_SECTION: 'duplicate_section',
|
|
@@ -607,6 +609,10 @@ const LEG_MARKER_RE = /<!--\s*reviewer-lane:\s*([a-z0-9_-]+)\s*-->/g;
|
|
|
607
609
|
* design principle of this module.
|
|
608
610
|
*/
|
|
609
611
|
exports.LANE_SLUG_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
612
|
+
/** The workflow's availability call: `gsd_run review-lane availability ...` at command position. */
|
|
613
|
+
const AVAILABILITY_CALL_RE = /^\s*gsd_run review-lane availability(?:\s|$)/;
|
|
614
|
+
/** One `<slug>:missing` row in the printf fallback; slug is LANE_SLUG_RE's grammar capped at 32 chars. */
|
|
615
|
+
const AVAILABILITY_FALLBACK_ROW_RE = /(?:'|\\n)([a-z0-9][a-z0-9_-]{0,31}):missing/g;
|
|
610
616
|
/** Bounds of the step a bespoke leg marker could appear inside. */
|
|
611
617
|
const INVOKE_STEP_RE = /<step name="invoke_reviewers">([\s\S]*?)<\/step>/;
|
|
612
618
|
function sliceStep(workflowText, re) {
|
|
@@ -627,8 +633,10 @@ function countOccurrences(haystack, re) {
|
|
|
627
633
|
}
|
|
628
634
|
/**
|
|
629
635
|
* Bidirectional parity across three surfaces: the descriptor, the roster
|
|
630
|
-
* (`KNOWN_REVIEWER_SLUGS`), and the generated capability **registry** — plus
|
|
631
|
-
*
|
|
636
|
+
* (`KNOWN_REVIEWER_SLUGS`), and the generated capability **registry** — plus two assertions
|
|
637
|
+
* against the workflow: the anti-parity check, and the availability-fallback check (the workflow
|
|
638
|
+
* makes the availability call, has a fallback row per lane, and the fallback slug set equals the
|
|
639
|
+
* openai-http lanes).
|
|
632
640
|
*
|
|
633
641
|
* **Re-pointed by Phase 5b (#2799).** Through Phase 5a this function also required a literal
|
|
634
642
|
* `<!-- reviewer-lane: <slug> -->` per lane inside `invoke_reviewers` and a literal
|
|
@@ -671,6 +679,7 @@ function checkReviewerLaneParity(input) {
|
|
|
671
679
|
const seenSlug = new Set();
|
|
672
680
|
const seenFlag = new Set();
|
|
673
681
|
const seenSection = new Set();
|
|
682
|
+
const httpSlugs = new Set();
|
|
674
683
|
// The declared parameter type says `ReviewerLane[]`, but this function is a
|
|
675
684
|
// trust boundary — narrow from `unknown` rather than believing the annotation.
|
|
676
685
|
const rawLanes = Array.isArray(descriptor) ? descriptor : [];
|
|
@@ -689,6 +698,8 @@ function checkReviewerLaneParity(input) {
|
|
|
689
698
|
if (seenSlug.has(slug))
|
|
690
699
|
add(exports.PARITY_VIOLATION.DUPLICATE_SLUG, slug);
|
|
691
700
|
seenSlug.add(slug);
|
|
701
|
+
if (lane.transport === 'openai-http')
|
|
702
|
+
httpSlugs.add(slug);
|
|
692
703
|
const flags = Array.isArray(lane.flags) ? lane.flags : [];
|
|
693
704
|
for (const flag of flags) {
|
|
694
705
|
if (typeof flag !== 'string')
|
|
@@ -740,6 +751,28 @@ function checkReviewerLaneParity(input) {
|
|
|
740
751
|
for (const slug of markerSlugs.keys()) {
|
|
741
752
|
add(exports.PARITY_VIOLATION.BESPOKE_LEG_PRESENT, slug);
|
|
742
753
|
}
|
|
754
|
+
// --- the availability call's `||` fallback must name exactly the openai-http lanes ---
|
|
755
|
+
//
|
|
756
|
+
// review.md prints these rows only when an older gsd-tools lacks `review-lane availability`, so
|
|
757
|
+
// they are a second hand-kept copy of the roster. Checked only when the call is present.
|
|
758
|
+
// Anchored to the command position so a comment or prose mention cannot shadow the real call;
|
|
759
|
+
// a missing call is itself a violation (otherwise deleting it would silently skip this check).
|
|
760
|
+
const availabilityLine = (0, text_lines_cjs_1.splitLines)(workflowText).find((line) => AVAILABILITY_CALL_RE.test(line));
|
|
761
|
+
if (availabilityLine === undefined) {
|
|
762
|
+
add(exports.PARITY_VIOLATION.AVAILABILITY_FALLBACK_MISMATCH, 'no-call');
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
if (!availabilityLine.includes('review-lane availability ||')) {
|
|
766
|
+
add(exports.PARITY_VIOLATION.AVAILABILITY_FALLBACK_MISMATCH, 'no-fallback');
|
|
767
|
+
}
|
|
768
|
+
const fallback = new Set(Array.from(availabilityLine.matchAll(AVAILABILITY_FALLBACK_ROW_RE), (m) => m[1]));
|
|
769
|
+
for (const slug of fallback)
|
|
770
|
+
if (!httpSlugs.has(slug))
|
|
771
|
+
add(exports.PARITY_VIOLATION.AVAILABILITY_FALLBACK_MISMATCH, slug);
|
|
772
|
+
for (const slug of httpSlugs)
|
|
773
|
+
if (!fallback.has(slug))
|
|
774
|
+
add(exports.PARITY_VIOLATION.AVAILABILITY_FALLBACK_MISMATCH, slug);
|
|
775
|
+
}
|
|
743
776
|
return { ok: violations.length === 0, violations };
|
|
744
777
|
}
|
|
745
778
|
/* ------------------------------------------------------------------ *
|
|
@@ -32,6 +32,7 @@ exports.parseModelBanner = parseModelBanner;
|
|
|
32
32
|
exports.parseTranscriptModel = parseTranscriptModel;
|
|
33
33
|
exports.checkEgressHost = checkEgressHost;
|
|
34
34
|
exports.probeLane = probeLane;
|
|
35
|
+
exports.probeHttpLanes = probeHttpLanes;
|
|
35
36
|
exports.writeReviewOrStub = writeReviewOrStub;
|
|
36
37
|
exports.emptyOutputDiagnosis = emptyOutputDiagnosis;
|
|
37
38
|
exports.handleOpencodeOutput = handleOpencodeOutput;
|
|
@@ -318,14 +319,18 @@ async function probeLane(plan, deps) {
|
|
|
318
319
|
// Only a STRING config value names a host. `String(unknown)` would turn an object into the
|
|
319
320
|
// literal '[object Object]' and probe that as a URL — a nonsense destination reported as
|
|
320
321
|
// "unreachable" rather than as the misconfiguration it is.
|
|
322
|
+
// For openai-http, `plan.host` is already resolved by `resolveLanePlan` (config → default,
|
|
323
|
+
// unset sentinels like "null" handled) — probe the destination the run would use.
|
|
321
324
|
const configured = deps.configGet(probe.hostConfigKey);
|
|
322
|
-
const base = (0, review_lane_invocation_cjs_1.normalizeHost)((typeof configured === 'string' ? configured : '')
|
|
323
|
-
(plan.transport === 'openai-http' ? plan.host : ''));
|
|
325
|
+
const base = (0, review_lane_invocation_cjs_1.normalizeHost)((plan.transport === 'openai-http' ? plan.host : '') || (typeof configured === 'string' ? configured : ''));
|
|
324
326
|
if (!base) {
|
|
325
327
|
return { available: false, reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.HOST_UNREACHABLE, detail: 'no host resolved' };
|
|
326
328
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
+
// D7: reachable ⇒ available. Any HTTP answer (401 behind an auth proxy, 404, 3xx) proves a
|
|
330
|
+
// server is there; only a transport failure (status 0) does not. Redirects are not followed:
|
|
331
|
+
// a probe must not issue a request to a host the descriptor never named.
|
|
332
|
+
const r = await deps.httpJson(`${base}${probe.path}`, { method: 'GET', timeoutMs: probe.timeoutMs, probe: true });
|
|
333
|
+
return r.status > 0
|
|
329
334
|
? { available: true }
|
|
330
335
|
: {
|
|
331
336
|
available: false,
|
|
@@ -341,6 +346,25 @@ async function probeLane(plan, deps) {
|
|
|
341
346
|
};
|
|
342
347
|
}
|
|
343
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* `<slug>:available|missing` rows for the openai-http lanes in `lanes` (all, or those in `selected`).
|
|
351
|
+
* Prerequisite binaries are not folded in: the caller reports `jq` separately. An unresolvable lane
|
|
352
|
+
* reads `missing` and its reason is returned in `warnings` rather than swallowed.
|
|
353
|
+
*/
|
|
354
|
+
async function probeHttpLanes(lanes, selected, resolve, deps) {
|
|
355
|
+
// Probes run concurrently (each has its own timeout); Promise.all keeps descriptor order, and
|
|
356
|
+
// warnings are collected from the results afterwards so their order never depends on resolve timing.
|
|
357
|
+
const results = await Promise.all(lanes
|
|
358
|
+
.filter((l) => l.transport === 'openai-http' && (selected.length === 0 || selected.includes(l.slug)))
|
|
359
|
+
.map(async (lane) => {
|
|
360
|
+
const r = resolve(lane);
|
|
361
|
+
const up = r.ok && (await probeLane({ ...r.plan, requiresBinaries: [] }, deps)).available;
|
|
362
|
+
return { row: `${lane.slug}:${up ? 'available' : 'missing'}`, warning: r.ok ? null : `${lane.slug}: ${r.detail}` };
|
|
363
|
+
}));
|
|
364
|
+
const rows = results.map((x) => x.row);
|
|
365
|
+
const warnings = results.flatMap((x) => (x.warning === null ? [] : [x.warning]));
|
|
366
|
+
return { rows, warnings };
|
|
367
|
+
}
|
|
344
368
|
/* ------------------------------------------------------------------ *
|
|
345
369
|
* Empty-output policy (#2494 / #2605 / #2794)
|
|
346
370
|
* ------------------------------------------------------------------ */
|
|
@@ -940,6 +964,11 @@ async function runOpenAiCompatible(plan, promptText, deps) {
|
|
|
940
964
|
let model = plan.model;
|
|
941
965
|
if (!model && plan.modelsUrl) {
|
|
942
966
|
const listed = await deps.httpJson(plan.modelsUrl, { method: 'GET', timeoutMs: 2_000 });
|
|
967
|
+
// A redirect is a lane failure, not "discovery found nothing" (#5433): the host answered with
|
|
968
|
+
// somewhere else to go, so the configured URL is wrong and the completions request would be too.
|
|
969
|
+
if (listed.status >= 300 && listed.status < 400 && listed.error) {
|
|
970
|
+
return { review: '', rawBody: listed.error, model: recordedModel(plan.fallbackModel, exports.MODEL_SOURCE.REQUESTED) };
|
|
971
|
+
}
|
|
943
972
|
if (listed.ok) {
|
|
944
973
|
try {
|
|
945
974
|
const parsed = JSON.parse(listed.body);
|