@opengsd/gsd-core 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +575 -101
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +134 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +24 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -16,30 +16,23 @@ exports.pickRollupWinners = pickRollupWinners;
|
|
|
16
16
|
exports.isCompletedInventory = isCompletedInventory;
|
|
17
17
|
exports.buildWorkstreamInventory = buildWorkstreamInventory;
|
|
18
18
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
19
20
|
// Internal helpers
|
|
20
21
|
function toPosixPath(p) {
|
|
21
22
|
return p.split('\\').join('/');
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* all) still resolves to plain `'missing'`, which stays OUTSIDE this set —
|
|
36
|
-
* that is what keeps a project untouched by this fix until it actually uses
|
|
37
|
-
* the verifier at least once. Post-adoption, an unrecorded phase fails
|
|
38
|
-
* CLOSED (counted here) rather than open, so a corrupt or evidence-absent
|
|
39
|
-
* ledger entry can no longer be read as "safe to complete" the way a bare
|
|
40
|
-
* `'missing'` sentinel is.
|
|
41
|
-
*/
|
|
42
|
-
const FAILING_VERIFICATION_STATUSES = new Set(['gaps_found', 'human_needed', 'unrecorded']);
|
|
24
|
+
// #2562/#2645's FAILING_VERIFICATION_STATUSES set (the verdicts that used to
|
|
25
|
+
// disqualify a phase from `complete` when combined with a local
|
|
26
|
+
// summary-count-meets-plan-count check) was removed by ADR-3180 §7.4
|
|
27
|
+
// (#3186): `complete` is now the single canonical owner's verdict
|
|
28
|
+
// (`PhaseFilesCount.complete`, computed via `isPhaseComplete` by the
|
|
29
|
+
// I/O-capable caller — see the loop below), which already requires
|
|
30
|
+
// `verification.status === 'passed'` unconditionally. Disk-strict (#2957)
|
|
31
|
+
// deliberately DROPS the prior "verifier-disabled projects fall back to
|
|
32
|
+
// summaries-met" tolerance that set existed to preserve — a phase with no
|
|
33
|
+
// `*-VERIFICATION.md` (`missing`) is no longer treated as complete just
|
|
34
|
+
// because its summaries meet its plan count. Disclosed in this phase's
|
|
35
|
+
// changeset.
|
|
43
36
|
/**
|
|
44
37
|
* #2562 / Bug #2445 / #2645 review: pick ONE winning item per key from a
|
|
45
38
|
* PRE-SORTED list — newest `mtimeMs` wins; on an exact tie the incumbent
|
|
@@ -127,11 +120,20 @@ function buildWorkstreamInventory(inputs) {
|
|
|
127
120
|
const counts = countsMap.get(dir);
|
|
128
121
|
const planCount = counts?.planCount ?? 0;
|
|
129
122
|
const summaryCount = counts?.summaryCount ?? 0;
|
|
130
|
-
//
|
|
131
|
-
// verification
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
// ADR-3180 §7.4 (issue #3186): routed through the single canonical owner
|
|
124
|
+
// (`isPhaseComplete`, src/verification.cts) — via `PhaseFilesCount.complete`,
|
|
125
|
+
// which the I/O-capable CALLER computes (this module is a PURE, I/O-free
|
|
126
|
+
// projection — see the module header: "No I/O. No async." — and cannot
|
|
127
|
+
// call the owner itself). The prior local derivation
|
|
128
|
+
// (`summaryCount >= planCount && planCount > 0` combined with a
|
|
129
|
+
// caller-supplied verification status) was this module's OWN completion
|
|
130
|
+
// verdict computed from raw counts — the exact "post-process a canonical
|
|
131
|
+
// result locally" bypass §7.4 rules out, and it reproduced the disk-strict
|
|
132
|
+
// headline case (#3168): a zero-plan phase with a passing verification
|
|
133
|
+
// read `pending` instead of `complete`. `complete` defaults to `false`
|
|
134
|
+
// when absent so a caller that has not been updated to pass it never
|
|
135
|
+
// silently reads as complete.
|
|
136
|
+
const status = (counts?.complete ?? false)
|
|
135
137
|
? 'complete'
|
|
136
138
|
: planCount > 0
|
|
137
139
|
? 'in_progress'
|
|
@@ -243,12 +245,31 @@ function buildWorkstreamInventory(inputs) {
|
|
|
243
245
|
roadmap_phase_count: effectivePhaseCount,
|
|
244
246
|
total_plans: totalPlans,
|
|
245
247
|
completed_plans: completedPlans,
|
|
246
|
-
//
|
|
247
|
-
// above throws first) and
|
|
248
|
-
// the denominator is a roadmap heading count that a caller
|
|
249
|
-
// bounds the numerator.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
// `clampPercent`'s 100 ceiling is unreachable under milestone scoping (the
|
|
249
|
+
// invariant above throws first) and matters only for the legacy unscoped
|
|
250
|
+
// path, where the denominator is a roadmap heading count that a caller
|
|
251
|
+
// cannot guarantee bounds the numerator.
|
|
252
|
+
//
|
|
253
|
+
// #3217 (ADR-3180 §7.6 rule 4) — WRITTEN REASON this site is NOT migrated
|
|
254
|
+
// onto the `SCOPE` enum this phase: `buildWorkstreamInventory` is a pure
|
|
255
|
+
// projection (no I/O — see the module header) fed `BuildWorkstreamInventoryInputs`
|
|
256
|
+
// by `workstream-inventory.cts`. Its own `milestoneScoped` is a pre-ADR-3180
|
|
257
|
+
// bespoke boolean, not a `SCOPE` value, and its caller does not currently
|
|
258
|
+
// thread a real `listMilestonePhaseDirs` scope into these inputs. Doing
|
|
259
|
+
// this honestly requires ONE of: (a) widening `BuildWorkstreamInventoryInputs`
|
|
260
|
+
// with a `Scope` field and `WorkstreamInventory.progress_percent`'s type
|
|
261
|
+
// from `number` to `number | null` — the exact "re-architecting
|
|
262
|
+
// StateProjection/WorkstreamInventory return types" the design phase
|
|
263
|
+
// (`.gsd/phase/refactor-3217-completion-ratio-scoping/40-design.md`,
|
|
264
|
+
// "Known limits") states is OUT of this phase's scope; or (b) silently
|
|
265
|
+
// reusing `milestoneScoped` as a `Scope` stand-in, which would be exactly
|
|
266
|
+
// the kind of proxy-for-a-data-flow-property this same phase's guard
|
|
267
|
+
// section explicitly rejects (a `boolean` cannot distinguish TRUNCATED
|
|
268
|
+
// from UNSCOPED from UNREADABLE, so a caller could not tell which
|
|
269
|
+
// non-answer it got). Left un-migrated rather than done dishonestly;
|
|
270
|
+
// `workstream inventory`'s `progress_percent` can still render a number
|
|
271
|
+
// derived from an under-scoped set (A8 in the phase's test matrix is
|
|
272
|
+
// NOT covered here for that reason — see this phase's PR description).
|
|
273
|
+
progress_percent: (0, phase_lifecycle_cjs_1.clampPercent)(completedPhases, effectivePhaseCount),
|
|
253
274
|
};
|
|
254
275
|
}
|
|
@@ -27,10 +27,13 @@ const planScan = require("./plan-scan.cjs");
|
|
|
27
27
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
28
|
const { planningPaths, planningRoot, getActiveWorkstream } = planningWorkspace;
|
|
29
29
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
31
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
32
|
+
const { extractFrontmatter, stripFrontmatter } = frontmatterMod;
|
|
30
33
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
31
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
|
|
32
35
|
const verificationMod = require("./verification.cjs");
|
|
33
|
-
const {
|
|
36
|
+
const { isPhaseComplete } = verificationMod;
|
|
34
37
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
35
38
|
const phaseIdMod = require("./phase-id.cjs");
|
|
36
39
|
const { phaseKeyFromDir, phaseKeyFromProse, parentPhaseKey } = phaseIdMod;
|
|
@@ -42,11 +45,28 @@ const workstream_inventory_builder_cjs_1 = require("./workstream-inventory-build
|
|
|
42
45
|
function workstreamsRoot(cwd) {
|
|
43
46
|
return node_path_1.default.join(planningRoot(cwd), 'workstreams');
|
|
44
47
|
}
|
|
45
|
-
|
|
48
|
+
/**
|
|
49
|
+
* #3185 (ADR-3180 Decision 1): count the phases the CURRENT milestone
|
|
50
|
+
* declares, not every `Phase` heading in the file.
|
|
51
|
+
*
|
|
52
|
+
* This previously matched `^#{2,4}\s+Phase\s+…` across the whole ROADMAP with
|
|
53
|
+
* no milestone window and no sentinel filter, so it counted 999.* backlog and
|
|
54
|
+
* Phase 0 headings and spanned every milestone the document had ever had.
|
|
55
|
+
* `getMilestonePhaseFilter` already computes exactly this number for the
|
|
56
|
+
* scoped window (`phaseCount`, sentinel-filtered), and `inspectWorkstream` in
|
|
57
|
+
* this same file already passes a resolved `currentVersion` to it — this
|
|
58
|
+
* function was the sibling copy that never got the fix.
|
|
59
|
+
*/
|
|
60
|
+
function countRoadmapPhases(roadmapPath, fallbackCount, cwd, ws, versionOverride) {
|
|
46
61
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
63
|
+
return fallbackCount;
|
|
64
|
+
if (!cwd)
|
|
65
|
+
return fallbackCount;
|
|
66
|
+
const filter = getMilestonePhaseFilter(cwd, versionOverride ?? null, null, ws ?? null);
|
|
67
|
+
// A pass-all degrade (phaseCount 0) means the window declared no phases —
|
|
68
|
+
// fall back rather than reporting a confident zero.
|
|
69
|
+
return filter.phaseCount > 0 ? filter.phaseCount : fallbackCount;
|
|
50
70
|
}
|
|
51
71
|
catch {
|
|
52
72
|
return fallbackCount;
|
|
@@ -312,13 +332,30 @@ function writeVerificationLedger(wsDir, ledger) {
|
|
|
312
332
|
function readStateProjection(statePath) {
|
|
313
333
|
try {
|
|
314
334
|
const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
335
|
+
// #3187: route Status/Current Phase/Last Activity through the single
|
|
336
|
+
// #1760 fallback-chain owner (state-document.cjs's stateFieldValue)
|
|
337
|
+
// instead of a frontmatter-blind stateExtractField(stateContent, …) call,
|
|
338
|
+
// mirroring cmdStateValidate/cmdStateSnapshot — a STATE.md whose fields
|
|
339
|
+
// live only in frontmatter is no longer projected as absent here.
|
|
340
|
+
const fm = extractFrontmatter(stateContent, statePath);
|
|
341
|
+
const body = stripFrontmatter(stateContent);
|
|
315
342
|
return {
|
|
316
|
-
status: (0, state_document_cjs_1.
|
|
317
|
-
current_phase: (0, state_document_cjs_1.
|
|
318
|
-
last_activity: (0, state_document_cjs_1.
|
|
343
|
+
status: (0, state_document_cjs_1.stateFieldValue)(fm, body, 'status', 'Status').value || 'unknown',
|
|
344
|
+
current_phase: (0, state_document_cjs_1.stateFieldValue)(fm, body, 'current_phase', 'Current Phase').value,
|
|
345
|
+
last_activity: (0, state_document_cjs_1.stateFieldValue)(fm, body, 'last_activity', 'Last Activity').value,
|
|
319
346
|
};
|
|
320
347
|
}
|
|
321
348
|
catch {
|
|
349
|
+
// Read/parse failure (missing file, permission fault, etc.) degrades to
|
|
350
|
+
// an all-unknown projection — unchanged. Not widened to also carry a
|
|
351
|
+
// `scope` here: doing so would ripple `StateProjection`
|
|
352
|
+
// (workstream-inventory-builder.cjs) and every consumer of this
|
|
353
|
+
// read-only rollup — the design doc's blast-radius table rates
|
|
354
|
+
// `readStateProjection` "low"/Tier-2, and this call site's migration is
|
|
355
|
+
// scoped to routing the fallback chain, not to widening the return type.
|
|
356
|
+
// The existing all-unknown degrade already distinguishes "could not
|
|
357
|
+
// read" from any real field value; only its scope-vs-absence *reason*
|
|
358
|
+
// stays uncaptured, same as before this change.
|
|
322
359
|
return {
|
|
323
360
|
status: 'unknown',
|
|
324
361
|
current_phase: null,
|
|
@@ -483,7 +520,16 @@ function inspectWorkstream(cwd, name, options = {}) {
|
|
|
483
520
|
const rawPhaseEntries = [...phaseDirNames].sort().map(dir => {
|
|
484
521
|
const phaseDir = node_path_1.default.join(p.phases, dir);
|
|
485
522
|
const counts = countPhaseFiles(phaseDir);
|
|
486
|
-
|
|
523
|
+
// ADR-3180 §7.4 (#3186): routed through the single canonical owner
|
|
524
|
+
// (`isPhaseComplete`, src/verification.cts) instead of calling
|
|
525
|
+
// `readVerificationStatus` directly and re-deriving "is this phase
|
|
526
|
+
// complete" locally from its `.status`. `completionResult.value.complete`
|
|
527
|
+
// is threaded down to the builder below (as `PhaseFilesCount.complete`)
|
|
528
|
+
// so `buildWorkstreamInventory` — a pure, I/O-free projection that
|
|
529
|
+
// cannot call the owner itself — consumes the owner's verdict rather
|
|
530
|
+
// than re-deriving a second one from summary/plan counts.
|
|
531
|
+
const completionResult = isPhaseComplete(phaseDir);
|
|
532
|
+
const verificationResult = completionResult.value.verification;
|
|
487
533
|
// #3057 B3: routing is UNCHANGED — `liveVerificationStatus` below is still
|
|
488
534
|
// `.status`, exactly as before, so the ledger/rollup logic that consumes
|
|
489
535
|
// it is unaffected. This only makes an indeterminate staleness check
|
|
@@ -502,6 +548,12 @@ function inspectWorkstream(cwd, name, options = {}) {
|
|
|
502
548
|
summaryCount: counts.summaryCount,
|
|
503
549
|
inMilestone: isDirInCurrentMilestone(dir),
|
|
504
550
|
liveVerificationStatus: verificationResult.status,
|
|
551
|
+
// ADR-3180 §7.4 (#3186): the owner's verdict, read live off disk — never
|
|
552
|
+
// ledger-adjusted (see the phaseFilesCounts map below; the ledger only
|
|
553
|
+
// ever substitutes a 'missing' status with a remembered one, and under
|
|
554
|
+
// disk-strict neither 'missing' nor 'unrecorded' is ever complete, so
|
|
555
|
+
// there is nothing for the ledger to override here).
|
|
556
|
+
complete: completionResult.value.complete,
|
|
505
557
|
};
|
|
506
558
|
});
|
|
507
559
|
// #2645: only the directory Bug #2445's de-dup rollup would actually pick
|
|
@@ -573,6 +625,7 @@ function inspectWorkstream(cwd, name, options = {}) {
|
|
|
573
625
|
summaryCount: entry.summaryCount,
|
|
574
626
|
inMilestone: entry.inMilestone,
|
|
575
627
|
verificationStatus,
|
|
628
|
+
complete: entry.complete,
|
|
576
629
|
};
|
|
577
630
|
});
|
|
578
631
|
// The denominator is the union of what the roadmap DECLARES for the current
|
|
@@ -592,7 +645,7 @@ function inspectWorkstream(cwd, name, options = {}) {
|
|
|
592
645
|
// declares in its Progress table but never scaffolded — the heading-only
|
|
593
646
|
// count drops them, even when other headings exist. Union the declared rows
|
|
594
647
|
// with the phase directories so neither source can silently shrink it.
|
|
595
|
-
let fallbackPhaseCount = countRoadmapPhases(p.roadmap, phaseDirNames.length);
|
|
648
|
+
let fallbackPhaseCount = countRoadmapPhases(p.roadmap, phaseDirNames.length, cwd, name, currentVersion);
|
|
596
649
|
if (!scoped && progressRows.length > 0) {
|
|
597
650
|
const union = new Set(progressRows.map(row => row.key));
|
|
598
651
|
for (const entry of phaseFilesCounts)
|
|
@@ -134,8 +134,8 @@ function cmdWorkstreamCreate(cwd, name, options, raw) {
|
|
|
134
134
|
}
|
|
135
135
|
else {
|
|
136
136
|
try {
|
|
137
|
-
const milestone = getMilestoneInfo(cwd);
|
|
138
|
-
existingWsName = generateSlugInternal(milestone
|
|
137
|
+
const milestone = getMilestoneInfo(cwd).value;
|
|
138
|
+
existingWsName = generateSlugInternal(milestone?.name ?? null) || 'default';
|
|
139
139
|
}
|
|
140
140
|
catch {
|
|
141
141
|
existingWsName = 'default';
|
|
@@ -366,13 +366,24 @@ function normalizeCleanupManifestEntry(entry) {
|
|
|
366
366
|
return null;
|
|
367
367
|
const rawAllowedBases = Array.isArray(e.allowed_bases) ? e.allowed_bases : [];
|
|
368
368
|
const allowedBases = Array.from(new Set([expectedBase, ...rawAllowedBases.filter((base) => typeof base === 'string' && base.length > 0)]));
|
|
369
|
-
|
|
369
|
+
// #2596: liberal in what we accept — non-array, or non-string / empty
|
|
370
|
+
// elements, are dropped rather than coerced. An EMPTY result omits the field
|
|
371
|
+
// entirely, so "declared nothing" is indistinguishable from "not recorded":
|
|
372
|
+
// that ambiguity is already resolved as *unknown* by the per-plan submodule
|
|
373
|
+
// gate's own `[ -z "$PLAN_FILES" ]` rule, and inventing a second rule here
|
|
374
|
+
// would make an unrecorded plan look 100% out of scope.
|
|
375
|
+
const filesModified = (Array.isArray(e.files_modified) ? e.files_modified : [])
|
|
376
|
+
.filter((f) => typeof f === 'string' && f.trim().length > 0);
|
|
377
|
+
const normalized = {
|
|
370
378
|
agent_id: typeof e.agent_id === 'string' ? e.agent_id : null,
|
|
371
379
|
worktree_path: worktreePath,
|
|
372
380
|
branch,
|
|
373
381
|
expected_base: expectedBase,
|
|
374
382
|
allowed_bases: allowedBases,
|
|
375
383
|
};
|
|
384
|
+
if (filesModified.length > 0)
|
|
385
|
+
normalized.files_modified = filesModified;
|
|
386
|
+
return normalized;
|
|
376
387
|
}
|
|
377
388
|
function normalizeCleanupManifest(manifest) {
|
|
378
389
|
let parsed = manifest;
|
|
@@ -461,6 +472,39 @@ function repoRootStillMidMerge(execGit, repoRoot) {
|
|
|
461
472
|
return false; // ref not found — repoRoot is not mid-merge
|
|
462
473
|
return true; // any other exit code (e.g. a fatal git error) — fail closed
|
|
463
474
|
}
|
|
475
|
+
// #2596: the single definition of "this file is an executor-written SUMMARY
|
|
476
|
+
// artifact". Shared by `defaultFindSummaryFiles` (which walks for them to
|
|
477
|
+
// rescue) and the scope advisory below (which must never flag them) — a plan's
|
|
478
|
+
// declared `files_modified` never lists a SUMMARY, because the executor writes
|
|
479
|
+
// it by orchestration contract, so a second copy of this rule would make the
|
|
480
|
+
// advisory fire on essentially every wave.
|
|
481
|
+
const SUMMARY_ARTIFACT_DIR = '.planning';
|
|
482
|
+
const SUMMARY_ARTIFACT_SUFFIX = 'SUMMARY.md';
|
|
483
|
+
/**
|
|
484
|
+
* Normalize one path for scope comparison. Applied to BOTH sides so a declared
|
|
485
|
+
* path and a git-reported path meet in the same shape: backslashes become
|
|
486
|
+
* slashes unconditionally (a backslash path is not a Windows-only input),
|
|
487
|
+
* a leading `./` and any trailing `/` are stripped. This is the single
|
|
488
|
+
* normalizer shared by the SUMMARY-artifact predicate and the scope advisory,
|
|
489
|
+
* so the two can never disagree about what `./a\b/` means.
|
|
490
|
+
*/
|
|
491
|
+
function normalizeScopePath(raw) {
|
|
492
|
+
return String(raw || '')
|
|
493
|
+
.replace(/\\/g, '/')
|
|
494
|
+
.trim()
|
|
495
|
+
.replace(/^\.\//, '')
|
|
496
|
+
.replace(/\/+$/, '');
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* True when a worktree-relative path is a SUMMARY artifact. Input may use
|
|
500
|
+
* either separator; normalization to POSIX is unconditional (backslash paths
|
|
501
|
+
* reach Linux too).
|
|
502
|
+
*/
|
|
503
|
+
function isSummaryArtifactRelPath(relPath) {
|
|
504
|
+
const normalized = normalizeScopePath(relPath);
|
|
505
|
+
return normalized.startsWith(`${SUMMARY_ARTIFACT_DIR}/`)
|
|
506
|
+
&& normalized.endsWith(SUMMARY_ARTIFACT_SUFFIX);
|
|
507
|
+
}
|
|
464
508
|
/**
|
|
465
509
|
* Walk <worktreePath>/.planning/ recursively and collect absolute paths of
|
|
466
510
|
* all files whose names match *SUMMARY.md. Returns [] when the directory
|
|
@@ -470,7 +514,7 @@ function repoRootStillMidMerge(execGit, repoRoot) {
|
|
|
470
514
|
* find "$WT/.planning" -name "*SUMMARY.md"
|
|
471
515
|
*/
|
|
472
516
|
function defaultFindSummaryFiles(worktreePath) {
|
|
473
|
-
const planningDir = node_path_1.default.join(worktreePath,
|
|
517
|
+
const planningDir = node_path_1.default.join(worktreePath, SUMMARY_ARTIFACT_DIR);
|
|
474
518
|
const results = [];
|
|
475
519
|
function walk(dir) {
|
|
476
520
|
let entries;
|
|
@@ -485,7 +529,7 @@ function defaultFindSummaryFiles(worktreePath) {
|
|
|
485
529
|
if (entry.isDirectory()) {
|
|
486
530
|
walk(full);
|
|
487
531
|
}
|
|
488
|
-
else if (entry.isFile() && entry.name.endsWith(
|
|
532
|
+
else if (entry.isFile() && entry.name.endsWith(SUMMARY_ARTIFACT_SUFFIX)) {
|
|
489
533
|
results.push(full);
|
|
490
534
|
}
|
|
491
535
|
}
|
|
@@ -588,6 +632,77 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
|
|
|
588
632
|
}
|
|
589
633
|
return { rescuedRelPaths, failures };
|
|
590
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* #2596: advisory codes emitted by the wave-cleanup gauntlet. Frozen and
|
|
637
|
+
* exported so tests assert on a code rather than on rendered prose (this repo
|
|
638
|
+
* forbids raw-text matching on test output).
|
|
639
|
+
*/
|
|
640
|
+
const WAVE_CLEANUP_WARNING = Object.freeze({
|
|
641
|
+
/** A committed path fell outside the plan's declared `files_modified`. */
|
|
642
|
+
SCOPE_OUT_OF_DECLARED: 'scope_out_of_declared',
|
|
643
|
+
/** The scope diff could not be computed, so conformance is unknown. */
|
|
644
|
+
SCOPE_CHECK_UNAVAILABLE: 'scope_check_unavailable',
|
|
645
|
+
});
|
|
646
|
+
/**
|
|
647
|
+
* The literal directory prefix a declared path covers, or `null` when the
|
|
648
|
+
* pattern begins with a glob metacharacter and therefore has no usable prefix.
|
|
649
|
+
*
|
|
650
|
+
* Deliberately literal-prefix only — NOT a glob engine. A hand-rolled
|
|
651
|
+
* `**`/`*`/`?` matcher inside a worktree-lifecycle module is an informal,
|
|
652
|
+
* undocumented pattern language living where no language belongs, and this
|
|
653
|
+
* repo forbids external deps in core. The submodule-intersection gate
|
|
654
|
+
* (`workflows/execute-phase/steps/per-plan-worktree-gate.md`) already ships
|
|
655
|
+
* exactly this glob-prefix rule; reusing it beats inventing a second one.
|
|
656
|
+
*
|
|
657
|
+
* `null` (no literal prefix, e.g. `*.md`) means "matches everything": for an
|
|
658
|
+
* ADVISORY, a false alarm costs more than a miss, so the ambiguous case
|
|
659
|
+
* suppresses rather than shouts.
|
|
660
|
+
*/
|
|
661
|
+
function declaredScopePrefix(declared) {
|
|
662
|
+
const globAt = declared.search(/[*?[]/);
|
|
663
|
+
if (globAt < 0)
|
|
664
|
+
return declared;
|
|
665
|
+
const literal = declared.slice(0, globAt).replace(/\/+$/, '');
|
|
666
|
+
return literal.length > 0 ? literal : null;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* #2596: compare a branch's actual committed paths against the plan's declared
|
|
670
|
+
* scope. Pure — no git, no IO. Returns one warning per out-of-scope path, in
|
|
671
|
+
* the order the paths were given. Returns [] when nothing usable was declared:
|
|
672
|
+
* absence of data is not evidence of over-reach.
|
|
673
|
+
*/
|
|
674
|
+
function planWaveScopeConformance(changedPaths, declaredFiles, branch) {
|
|
675
|
+
if (!Array.isArray(declaredFiles))
|
|
676
|
+
return [];
|
|
677
|
+
const prefixes = [];
|
|
678
|
+
for (const declared of declaredFiles) {
|
|
679
|
+
if (typeof declared !== 'string')
|
|
680
|
+
continue;
|
|
681
|
+
const normalized = normalizeScopePath(declared);
|
|
682
|
+
if (!normalized)
|
|
683
|
+
continue;
|
|
684
|
+
prefixes.push(declaredScopePrefix(normalized));
|
|
685
|
+
}
|
|
686
|
+
if (prefixes.length === 0)
|
|
687
|
+
return [];
|
|
688
|
+
const warnings = [];
|
|
689
|
+
const seen = new Set();
|
|
690
|
+
for (const raw of Array.isArray(changedPaths) ? changedPaths : []) {
|
|
691
|
+
if (typeof raw !== 'string')
|
|
692
|
+
continue;
|
|
693
|
+
const changed = normalizeScopePath(raw);
|
|
694
|
+
if (!changed || seen.has(changed))
|
|
695
|
+
continue;
|
|
696
|
+
seen.add(changed);
|
|
697
|
+
if (isSummaryArtifactRelPath(changed))
|
|
698
|
+
continue;
|
|
699
|
+
const covered = prefixes.some((prefix) => (prefix === null || changed === prefix || changed.startsWith(`${prefix}/`)));
|
|
700
|
+
if (covered)
|
|
701
|
+
continue;
|
|
702
|
+
warnings.push({ code: WAVE_CLEANUP_WARNING.SCOPE_OUT_OF_DECLARED, branch, path: changed });
|
|
703
|
+
}
|
|
704
|
+
return warnings;
|
|
705
|
+
}
|
|
591
706
|
function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
592
707
|
const execGit = deps.execGit || execGitDefault;
|
|
593
708
|
const entries = Array.isArray(plan?.entries) ? plan.entries : [];
|
|
@@ -598,10 +713,12 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
598
713
|
reason: plan ? (plan.reason || 'missing_entries') : 'missing_plan',
|
|
599
714
|
entries: [],
|
|
600
715
|
pending: entries,
|
|
716
|
+
warnings: [],
|
|
601
717
|
};
|
|
602
718
|
}
|
|
603
719
|
const results = [];
|
|
604
720
|
const pending = [];
|
|
721
|
+
const allWarnings = [];
|
|
605
722
|
let ok = true;
|
|
606
723
|
// #2852: every per-entry failure site marks the SAME shape — status='blocked',
|
|
607
724
|
// a reason code, the captured stderr, push to results, flip the overall `ok`
|
|
@@ -622,6 +739,7 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
622
739
|
status: 'pending',
|
|
623
740
|
reason: null,
|
|
624
741
|
stderr: '',
|
|
742
|
+
warnings: [],
|
|
625
743
|
};
|
|
626
744
|
const branchCheck = execGit(['-C', entry.worktree_path, 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: plan.repoRoot });
|
|
627
745
|
if (!gitResultOk(branchCheck) || branchCheck.stdout.trim() !== entry.branch) {
|
|
@@ -652,6 +770,25 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
652
770
|
blockEntry(result, 'branch_contains_deletions', deletions.stdout);
|
|
653
771
|
continue; // #2852: isolate
|
|
654
772
|
}
|
|
773
|
+
// #2596: advisory scope conformance — does the branch's ACTUAL committed
|
|
774
|
+
// diff stay inside the scope the plan declared? Gated on a declared scope
|
|
775
|
+
// being present: with nothing declared there is nothing to compare, so no
|
|
776
|
+
// git subprocess is spent at all (and every pre-#2596 fixture, none of
|
|
777
|
+
// which declares one, issues exactly the git calls it always did).
|
|
778
|
+
//
|
|
779
|
+
// ADVISORY ONLY. Unlike the deletions check above, a finding here does NOT
|
|
780
|
+
// call blockEntry and does NOT touch `ok` — the merge proceeds. Promotion
|
|
781
|
+
// to a hard gate is a separate, disclosed change.
|
|
782
|
+
if (Array.isArray(entry.files_modified) && entry.files_modified.length > 0) {
|
|
783
|
+
const scopeDiff = execGit(['diff', '--name-only', `HEAD...${entry.branch}`], { cwd: plan.repoRoot });
|
|
784
|
+
const scopeWarnings = !gitResultOk(scopeDiff)
|
|
785
|
+
// A broken advisory must never become a gate: record that conformance
|
|
786
|
+
// is unknown rather than blocking (or, worse, silently passing).
|
|
787
|
+
? [{ code: WAVE_CLEANUP_WARNING.SCOPE_CHECK_UNAVAILABLE, branch: entry.branch, path: null }]
|
|
788
|
+
: planWaveScopeConformance((scopeDiff.stdout || '').split('\n'), entry.files_modified, entry.branch);
|
|
789
|
+
result.warnings.push(...scopeWarnings);
|
|
790
|
+
allWarnings.push(...scopeWarnings);
|
|
791
|
+
}
|
|
655
792
|
// Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
|
|
656
793
|
// The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
|
|
657
794
|
// orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
|
|
@@ -737,6 +874,7 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
737
874
|
reason: ok ? 'ok' : 'cleanup_blocked',
|
|
738
875
|
entries: results,
|
|
739
876
|
pending,
|
|
877
|
+
warnings: allWarnings,
|
|
740
878
|
};
|
|
741
879
|
}
|
|
742
880
|
function cmdWorktreeCleanupWave(cwd, args = []) {
|
|
@@ -777,6 +915,15 @@ function cmdWorktreeCleanupWave(cwd, args = []) {
|
|
|
777
915
|
process.exitCode = 1;
|
|
778
916
|
}
|
|
779
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* #2596: split a `--files` value into declared paths. Whitespace-separated,
|
|
920
|
+
* matching the `PLAN_FILES` shape the per-plan worktree gate already builds
|
|
921
|
+
* with `jq -r '.files_modified // [] | join(" ")'`. Values are DATA compared
|
|
922
|
+
* against a diff — never opened, never passed to a shell.
|
|
923
|
+
*/
|
|
924
|
+
function parseDeclaredScopeFlag(raw) {
|
|
925
|
+
return String(raw || '').split(/\s+/).filter((token) => token.length > 0);
|
|
926
|
+
}
|
|
780
927
|
/**
|
|
781
928
|
* Pure planner for the per-agent wave-manifest append.
|
|
782
929
|
*
|
|
@@ -793,8 +940,9 @@ function cmdWorktreeCleanupWave(cwd, args = []) {
|
|
|
793
940
|
* rejected loudly — the reader dedups on that key, so a re-record would be
|
|
794
941
|
* silently dropped (the failure mode this verb exists to eliminate). The
|
|
795
942
|
* on-disk shape stays the existing 4-field entry (`agent_id`, `worktree_path`,
|
|
796
|
-
* `branch`, `expected_base`)
|
|
797
|
-
* `
|
|
943
|
+
* `branch`, `expected_base`) unless `--files` declares a scope, in which case
|
|
944
|
+
* an optional `files_modified` is appended (#2596); the reader still
|
|
945
|
+
* re-derives `allowed_bases`.
|
|
798
946
|
*/
|
|
799
947
|
function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
800
948
|
// 1. Write-strict required-field check (loud, with which flag is missing).
|
|
@@ -824,11 +972,13 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
|
824
972
|
}
|
|
825
973
|
// 2. Shared validation: run the candidate through the reader's normalizer.
|
|
826
974
|
// If it returns null the reader would drop this entry on read — reject now.
|
|
975
|
+
const declaredScope = parseDeclaredScopeFlag(fields.files);
|
|
827
976
|
const candidate = {
|
|
828
977
|
agent_id: agentId,
|
|
829
978
|
worktree_path: worktreePath,
|
|
830
979
|
branch,
|
|
831
980
|
expected_base: base,
|
|
981
|
+
...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
|
|
832
982
|
};
|
|
833
983
|
const entry = normalizeCleanupManifestEntry(candidate);
|
|
834
984
|
if (!entry) {
|
|
@@ -917,6 +1067,11 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
|
917
1067
|
branch: entry.branch,
|
|
918
1068
|
expected_base: entry.expected_base,
|
|
919
1069
|
};
|
|
1070
|
+
// #2596: only written when a scope was actually declared — conservative in
|
|
1071
|
+
// what we send, so a blank --files leaves the 4-field shape untouched.
|
|
1072
|
+
if (entry.files_modified && entry.files_modified.length > 0) {
|
|
1073
|
+
recorded.files_modified = entry.files_modified;
|
|
1074
|
+
}
|
|
920
1075
|
worktrees.push(recorded);
|
|
921
1076
|
return {
|
|
922
1077
|
ok: true,
|
|
@@ -928,7 +1083,7 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
|
928
1083
|
/**
|
|
929
1084
|
* CLI command: append a validated per-agent entry to a wave cleanup manifest.
|
|
930
1085
|
*
|
|
931
|
-
* Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
|
|
1086
|
+
* Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]
|
|
932
1087
|
*
|
|
933
1088
|
* Fails loudly (non-zero exit + recovery hint on stderr) when a field is
|
|
934
1089
|
* missing/garbled or the manifest is absent/malformed, rather than appending an
|
|
@@ -943,7 +1098,7 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
|
|
|
943
1098
|
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
944
1099
|
const manifestPath = flag('--manifest');
|
|
945
1100
|
if (!manifestPath) {
|
|
946
|
-
writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha
|
|
1101
|
+
writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]\n');
|
|
947
1102
|
process.exitCode = 2;
|
|
948
1103
|
return { ok: false, reason: 'usage', entry: null };
|
|
949
1104
|
}
|
|
@@ -965,6 +1120,7 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
|
|
|
965
1120
|
worktreePath: flag('--path'),
|
|
966
1121
|
branch: flag('--branch'),
|
|
967
1122
|
base: flag('--base'),
|
|
1123
|
+
files: flag('--files'),
|
|
968
1124
|
});
|
|
969
1125
|
if (!plan.ok || plan.manifest === null) {
|
|
970
1126
|
writeErr(`[gsd] worktree.record-agent: ${plan.reason} — ${plan.hint || ''}\n`);
|
|
@@ -1008,11 +1164,13 @@ function planWorktreeCreate(fields) {
|
|
|
1008
1164
|
entry: null,
|
|
1009
1165
|
};
|
|
1010
1166
|
}
|
|
1167
|
+
const declaredScope = parseDeclaredScopeFlag(fields.files);
|
|
1011
1168
|
const candidate = {
|
|
1012
1169
|
agent_id: agentId,
|
|
1013
1170
|
worktree_path: worktreePath,
|
|
1014
1171
|
branch,
|
|
1015
1172
|
expected_base: base,
|
|
1173
|
+
...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
|
|
1016
1174
|
};
|
|
1017
1175
|
const entry = normalizeCleanupManifestEntry(candidate);
|
|
1018
1176
|
if (!entry) {
|
|
@@ -1140,7 +1298,7 @@ function executeWorktreeCreatePlan(plan, repoRoot, deps = {}) {
|
|
|
1140
1298
|
* validated manifest entry so the worktree is immediately manageable by
|
|
1141
1299
|
* `worktree cleanup-wave` / `worktree reap-orphans`.
|
|
1142
1300
|
*
|
|
1143
|
-
* Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir>
|
|
1301
|
+
* Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]
|
|
1144
1302
|
*
|
|
1145
1303
|
* #2584 FIX 1 — ORDERING CONTRACT: every manifest read/parse/shape-validate/
|
|
1146
1304
|
* plan step runs BEFORE the git side effect (step 5). The ONLY manifest
|
|
@@ -1160,7 +1318,7 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
|
1160
1318
|
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
1161
1319
|
const manifestPath = flag('--manifest');
|
|
1162
1320
|
if (!manifestPath) {
|
|
1163
|
-
writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir
|
|
1321
|
+
writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]\n');
|
|
1164
1322
|
process.exitCode = 2;
|
|
1165
1323
|
return { ok: false, reason: 'usage' };
|
|
1166
1324
|
}
|
|
@@ -1225,6 +1383,7 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
|
1225
1383
|
worktreePath: flag('--path'),
|
|
1226
1384
|
branch: flag('--branch'),
|
|
1227
1385
|
base: flag('--base'),
|
|
1386
|
+
files: flag('--files'),
|
|
1228
1387
|
});
|
|
1229
1388
|
if (!plan.ok || !plan.entry) {
|
|
1230
1389
|
writeErr(`[gsd] worktree.create: ${plan.reason} — ${plan.hint || ''}\n`);
|
|
@@ -1291,6 +1450,11 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
|
1291
1450
|
branch: plan.entry.branch,
|
|
1292
1451
|
expected_base: plan.entry.expected_base,
|
|
1293
1452
|
};
|
|
1453
|
+
// #2596: only written when a scope was actually declared — conservative in
|
|
1454
|
+
// what we send, so a blank --files leaves the 4-field shape untouched.
|
|
1455
|
+
if (Array.isArray(plan.entry.files_modified) && plan.entry.files_modified.length > 0) {
|
|
1456
|
+
recorded.files_modified = plan.entry.files_modified;
|
|
1457
|
+
}
|
|
1294
1458
|
const dedupeKey = `${recorded.worktree_path}\0${recorded.branch}`;
|
|
1295
1459
|
const alreadyPresent = worktrees.some((existing) => {
|
|
1296
1460
|
const normalized = normalizeCleanupManifestEntry(existing);
|
|
@@ -1677,6 +1841,9 @@ module.exports = {
|
|
|
1677
1841
|
normalizeCleanupManifest,
|
|
1678
1842
|
planWorktreeWaveCleanup,
|
|
1679
1843
|
executeWorktreeWaveCleanupPlan,
|
|
1844
|
+
WAVE_CLEANUP_WARNING,
|
|
1845
|
+
planWaveScopeConformance,
|
|
1846
|
+
isSummaryArtifactRelPath,
|
|
1680
1847
|
cmdWorktreeCleanupWave,
|
|
1681
1848
|
planWorktreeRecordAgent,
|
|
1682
1849
|
cmdWorktreeRecordAgent,
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"workflow.plan_chunked",
|
|
36
36
|
"workflow.specless_probe_fallback",
|
|
37
37
|
"workflow.plan_review_convergence",
|
|
38
|
+
"workflow.agent_hint_routing",
|
|
38
39
|
"code_quality.fallow.enabled",
|
|
39
40
|
"code_quality.fallow.scope",
|
|
40
41
|
"code_quality.fallow.profile",
|
|
@@ -180,7 +181,12 @@
|
|
|
180
181
|
{
|
|
181
182
|
"topLevel": "model_policy",
|
|
182
183
|
"source": "^model_policy\\.runtime_tiers\\.[a-zA-Z0-9_-]+\\.(opus|sonnet|haiku)$",
|
|
183
|
-
"description": "model_policy.runtime_tiers.<runtime>.<opus|sonnet|haiku>"
|
|
184
|
+
"description": "model_policy.runtime_tiers.<runtime>.<opus|sonnet|haiku> (#3587)"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"topLevel": "phase_commit_docs",
|
|
188
|
+
"source": "^phase_commit_docs\\.\\d+[A-Z]?(?:\\.\\d+)*$",
|
|
189
|
+
"description": "phase_commit_docs.<phase-id> — per-phase commit_docs override (#3587). The <phase-id> segment is a hand-copy of the canonical PHASE_NUMBER_TOKEN_SOURCE grammar owned by src/phase-id.cts (#2128); this manifest is hand-maintained JSON so it cannot import that constant. Pinned against drift by the behavioral parity test in tests/commit-docs-bypass.test.cjs (folded 'phase-commit-docs' block, describe block 'E', #3587) — do not hand-edit this pattern without updating that test."
|
|
184
190
|
}
|
|
185
191
|
]
|
|
186
192
|
}
|