@opengsd/gsd-core 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +97 -9
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +97 -9
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -31,6 +31,7 @@ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
|
31
31
|
const schema_detect_cjs_1 = require("./schema-detect.cjs");
|
|
32
32
|
const artifacts_cjs_1 = require("./artifacts.cjs");
|
|
33
33
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
34
|
+
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
34
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
|
|
35
36
|
const agentInstallCheck = require("./agent-install-check.cjs");
|
|
36
37
|
const { checkAgentsInstalled } = agentInstallCheck;
|
|
@@ -42,7 +43,7 @@ const configLoaderMod = require("./config-loader.cjs");
|
|
|
42
43
|
const { loadConfig, CONFIG_DEFAULTS } = configLoaderMod;
|
|
43
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
45
|
const phaseIdMod = require("./phase-id.cjs");
|
|
45
|
-
const { normalizePhaseName, phaseTokenMatches, escapeRegex, getMilestoneFromPhaseId, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
46
|
+
const { normalizePhaseName, phaseTokenMatches, escapeRegex, getMilestoneFromPhaseId, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, extractPhaseToken, comparePhaseNum } = phaseIdMod;
|
|
46
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
48
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
48
49
|
const { findPhaseInternal } = phaseLocatorMod;
|
|
@@ -52,6 +53,9 @@ const { getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone } = ro
|
|
|
52
53
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
54
|
const worktreeSafetyMod = require("./worktree-safety.cjs");
|
|
54
55
|
const { inspectWorktreeHealth } = worktreeSafetyMod;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- commands.cjs is an export= CommonJS module
|
|
57
|
+
const commandsMod = require("./commands.cjs");
|
|
58
|
+
const { determinePhaseStatus } = commandsMod;
|
|
55
59
|
const { planningDir, planningRoot } = planningWorkspace;
|
|
56
60
|
const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
|
|
57
61
|
const { writeStateMd } = stateMod;
|
|
@@ -59,14 +63,35 @@ const { MODEL_PROFILES } = modelProfilesMod;
|
|
|
59
63
|
// Unused but imported for structural parity
|
|
60
64
|
void stripShippedMilestones;
|
|
61
65
|
void schema_detect_cjs_1.detectSchemaFiles;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Pure core of `verify-summary` (#2572).
|
|
68
|
+
*
|
|
69
|
+
* Same artifact↔git checks the CLI verb has always run, lifted out of the
|
|
70
|
+
* `output()` wrapper so other verbs can consume the structured
|
|
71
|
+
* `{ passed, checks, errors }` contract directly instead of shelling out and
|
|
72
|
+
* re-parsing JSON. `cmdVerifySummary` is now a thin adapter over this.
|
|
73
|
+
*
|
|
74
|
+
* Never throws and never writes to stdout: a missing SUMMARY, a non-repo, or an
|
|
75
|
+
* unresolvable commit all come back as structured `false`/`missing` values.
|
|
76
|
+
*
|
|
77
|
+
* Caveat for callers surfacing `commits_exist`: the hash pattern is a loose
|
|
78
|
+
* `\b[0-9a-f]{7,40}\b`, so any hex-shaped token in the prose counts as a
|
|
79
|
+
* candidate. That is cheap as an advisory signal and unacceptable as a gate.
|
|
80
|
+
*
|
|
81
|
+
* @param checkFileCount How many extracted candidates to probe. Defaults to 2 —
|
|
82
|
+
* the value the CLI verb has always used. Pass `Infinity` to probe every
|
|
83
|
+
* candidate (see `cmdPhaseComplete`, which reports on all of them).
|
|
84
|
+
* @param opts.checkCommits When `false`, the `git cat-file` probes are skipped
|
|
85
|
+
* entirely and `commits_exist` comes back `false` meaning *not checked*.
|
|
86
|
+
* Callers that do not surface `commits_exist` should pass `false` so this
|
|
87
|
+
* stays a pure-filesystem check with no subprocess cost.
|
|
88
|
+
*/
|
|
89
|
+
function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
|
|
66
90
|
const fullPath = node_path_1.default.join(cwd, summaryPath);
|
|
67
91
|
const checkCount = checkFileCount || 2;
|
|
92
|
+
const checkCommits = opts?.checkCommits !== false;
|
|
68
93
|
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
69
|
-
|
|
94
|
+
return {
|
|
70
95
|
passed: false,
|
|
71
96
|
checks: {
|
|
72
97
|
summary_exists: false,
|
|
@@ -76,21 +101,75 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
|
76
101
|
},
|
|
77
102
|
errors: ['SUMMARY.md not found'],
|
|
78
103
|
};
|
|
79
|
-
output(result, raw, 'failed');
|
|
80
|
-
return;
|
|
81
104
|
}
|
|
82
105
|
const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
83
106
|
const errors = [];
|
|
107
|
+
const projectRoot = node_path_1.default.resolve(cwd);
|
|
108
|
+
/**
|
|
109
|
+
* Is `candidate` plausibly a repo-relative file this check should probe?
|
|
110
|
+
*
|
|
111
|
+
* Deliberately narrowing. This is an ADVISORY, so the two error directions are
|
|
112
|
+
* not symmetric: a false positive tells a user their healthy project is
|
|
113
|
+
* missing a file that was never claimed, while a false negative just means one
|
|
114
|
+
* reference goes unprobed. Every rejection below is a noise class confirmed on
|
|
115
|
+
* #2685; when in doubt, skip rather than warn.
|
|
116
|
+
*/
|
|
117
|
+
const isProbableProjectFile = (candidate) => {
|
|
118
|
+
// Only repo-relative paths — a bare filename is too ambiguous to locate.
|
|
119
|
+
if (!candidate.includes('/'))
|
|
120
|
+
return false;
|
|
121
|
+
// URLs, protocol-relative links, and any other scheme.
|
|
122
|
+
if (candidate.startsWith('http') || candidate.startsWith('//'))
|
|
123
|
+
return false;
|
|
124
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(candidate))
|
|
125
|
+
return false;
|
|
126
|
+
// Globs name a set, not a file: `src/**/*.cts` is never "missing".
|
|
127
|
+
if (/[*?]/.test(candidate))
|
|
128
|
+
return false;
|
|
129
|
+
// Bare hostnames (`docs.example.com/guide.html`). A repo-relative path's
|
|
130
|
+
// first segment is a directory name, which in practice contains a dot only
|
|
131
|
+
// when it is a dotfile directory (`.github/`, `.changeset/`, `.planning/`)
|
|
132
|
+
// — i.e. the dot is at index 0. A dot anywhere later marks a hostname.
|
|
133
|
+
const firstSegment = candidate.split('/')[0] || '';
|
|
134
|
+
if (firstSegment.indexOf('.') > 0)
|
|
135
|
+
return false;
|
|
136
|
+
// Containment guard: a `../`-bearing reference must not turn this advisory
|
|
137
|
+
// into a filesystem existence probe outside the project.
|
|
138
|
+
const resolved = node_path_1.default.resolve(projectRoot, candidate);
|
|
139
|
+
if (resolved !== projectRoot && !resolved.startsWith(projectRoot + node_path_1.default.sep))
|
|
140
|
+
return false;
|
|
141
|
+
return true;
|
|
142
|
+
};
|
|
143
|
+
// Pattern 2 excludes `[` and `]` from its path class (#2685 Blocker 1). All
|
|
144
|
+
// three SUMMARY templates prescribe a YAML flow sequence for `key-files`:
|
|
145
|
+
//
|
|
146
|
+
// key-files:
|
|
147
|
+
// created: [src/auth/login.ts, src/auth/session.ts]
|
|
148
|
+
//
|
|
149
|
+
// and the label matches `(?:Created|Modified|…):` case-insensitively. Without
|
|
150
|
+
// the bracket exclusion the class captures the literal `[` as part of the
|
|
151
|
+
// first path, yielding `[src/auth/login.ts` — a candidate that can never exist
|
|
152
|
+
// on disk. That fired on healthy projects built from GSD's own shipped
|
|
153
|
+
// template. The exclusion also stops a markdown list in the body from
|
|
154
|
+
// reintroducing the same artifact.
|
|
155
|
+
//
|
|
156
|
+
// Stripping frontmatter first was the other remedy offered on #2685. It is a
|
|
157
|
+
// verified no-op on top of this exclusion — measured identical extraction
|
|
158
|
+
// across all three shipped templates — because the exclusion already makes a
|
|
159
|
+
// flow-sequence line contribute nothing. Consequence worth naming: the
|
|
160
|
+
// `key-files` block, the most authoritative statement of what a phase created,
|
|
161
|
+
// is still not read. Recovering it needs a real frontmatter parse, which is
|
|
162
|
+
// deliberately left as a follow-up rather than smuggled in here.
|
|
84
163
|
const mentionedFiles = new Set();
|
|
85
164
|
const patterns = [
|
|
86
165
|
/`([^`]+\.[a-zA-Z]+)`/g,
|
|
87
|
-
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
|
|
166
|
+
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`[\]]+\.[a-zA-Z]+)`?/gi,
|
|
88
167
|
];
|
|
89
168
|
for (const pattern of patterns) {
|
|
90
169
|
let m;
|
|
91
170
|
while ((m = pattern.exec(content)) !== null) {
|
|
92
171
|
const filePath = m[1];
|
|
93
|
-
if (filePath &&
|
|
172
|
+
if (filePath && isProbableProjectFile(filePath)) {
|
|
94
173
|
mentionedFiles.add(filePath);
|
|
95
174
|
}
|
|
96
175
|
}
|
|
@@ -98,12 +177,12 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
|
98
177
|
const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
|
|
99
178
|
const missing = [];
|
|
100
179
|
for (const file of filesToCheck) {
|
|
101
|
-
if (!node_fs_1.default.existsSync(node_path_1.default.
|
|
180
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.resolve(projectRoot, file))) {
|
|
102
181
|
missing.push(file);
|
|
103
182
|
}
|
|
104
183
|
}
|
|
105
184
|
const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
|
|
106
|
-
const hashes = content.match(commitHashPattern) || [];
|
|
185
|
+
const hashes = checkCommits ? content.match(commitHashPattern) || [] : [];
|
|
107
186
|
let commitsExist = false;
|
|
108
187
|
if (hashes.length > 0) {
|
|
109
188
|
for (const hash of hashes.slice(0, 3)) {
|
|
@@ -140,8 +219,15 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
|
140
219
|
self_check: selfCheck,
|
|
141
220
|
};
|
|
142
221
|
const passed = missing.length === 0 && selfCheck !== 'failed';
|
|
143
|
-
|
|
144
|
-
|
|
222
|
+
return { passed, checks, errors };
|
|
223
|
+
}
|
|
224
|
+
/** CLI adapter over verifySummaryCore — arg guard + output shaping only. */
|
|
225
|
+
function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
226
|
+
if (!summaryPath) {
|
|
227
|
+
error('summary-path required');
|
|
228
|
+
}
|
|
229
|
+
const result = verifySummaryCore(cwd, summaryPath, checkFileCount);
|
|
230
|
+
output(result, raw, result.passed ? 'passed' : 'failed');
|
|
145
231
|
}
|
|
146
232
|
/**
|
|
147
233
|
* Issue #429 — negative-grep comment-text echo gate.
|
|
@@ -516,17 +602,153 @@ function scanFileWideNegativeGateConflict(content) {
|
|
|
516
602
|
// This detector is warn-only: it never sets valid=false.
|
|
517
603
|
return { warnings, valid: true };
|
|
518
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Single pass over `<task ...>…</task>` blocks. The body pattern is
|
|
607
|
+
* ReDoS-safe stop-at-next-open (mirrors `taggedBlockPattern` in
|
|
608
|
+
* markdown-sectionizer.cts): bounded attributes (`[^>]{0,1000}`) and a body
|
|
609
|
+
* boundary that terminates at the NEXT `<task[\s>]` opening, so a document
|
|
610
|
+
* full of unclosed `<task>` openings scans linearly. Captures both the
|
|
611
|
+
* attribute string (group 1, so the `type=` selector is not lost the way it is
|
|
612
|
+
* with `extractTaggedBlocks`) and the body (group 2).
|
|
613
|
+
*/
|
|
614
|
+
const PLAN_TASK_BLOCK_RE = /<task(\s[^>]{0,1000})?>((?:(?!<task[\s>])[\s\S])*?)<\/task>/g;
|
|
615
|
+
/**
|
|
616
|
+
* Extract one `PlanTaskInfo` per `<task …>…</task>` block in `content`.
|
|
617
|
+
*
|
|
618
|
+
* Why a dedicated regex instead of `extractTaggedBlocks('task', true)`:
|
|
619
|
+
* `extractTaggedBlocks` discards the opening tag, so the task's `type=`
|
|
620
|
+
* attribute (which selects the validation branch) is lost. This helper
|
|
621
|
+
* captures both the attribute string and the body in one pass, then reuses
|
|
622
|
+
* `extractTaggedBlocks` on the body for sub-element extraction.
|
|
623
|
+
*/
|
|
624
|
+
function extractPlanTaskInfos(content) {
|
|
625
|
+
const infos = [];
|
|
626
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
627
|
+
return infos;
|
|
628
|
+
PLAN_TASK_BLOCK_RE.lastIndex = 0;
|
|
629
|
+
let match;
|
|
630
|
+
while ((match = PLAN_TASK_BLOCK_RE.exec(content)) !== null) {
|
|
631
|
+
const attrs = match[1] ?? '';
|
|
632
|
+
const body = match[2] ?? '';
|
|
633
|
+
const typeMatch = attrs.match(/\btype\s*=\s*["']?([\w:-]+)/i);
|
|
634
|
+
const type = typeMatch ? typeMatch[1].toLowerCase() : '';
|
|
635
|
+
const nameArr = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(body, 'name');
|
|
636
|
+
const hasName = nameArr.length > 0;
|
|
637
|
+
const name = hasName ? nameArr[0].trim() : '';
|
|
638
|
+
infos.push({
|
|
639
|
+
name,
|
|
640
|
+
type,
|
|
641
|
+
hasName,
|
|
642
|
+
hasFiles: /<files>/.test(body),
|
|
643
|
+
hasAction: /<action>/.test(body),
|
|
644
|
+
hasVerify: /<verify>/.test(body),
|
|
645
|
+
hasDone: /<done>/.test(body),
|
|
646
|
+
hasWhatBuilt: /<what-built>/.test(body),
|
|
647
|
+
hasHowToVerify: /<how-to-verify>/.test(body),
|
|
648
|
+
hasDecision: /<decision>/.test(body),
|
|
649
|
+
hasOptions: /<options>/.test(body),
|
|
650
|
+
hasInstructions: /<instructions>/.test(body),
|
|
651
|
+
hasVerification: /<verification>/.test(body),
|
|
652
|
+
hasResumeSignal: /<resume-signal>/.test(body),
|
|
653
|
+
});
|
|
654
|
+
// Guard against zero-length matches looping forever.
|
|
655
|
+
if (match.index === PLAN_TASK_BLOCK_RE.lastIndex) {
|
|
656
|
+
PLAN_TASK_BLOCK_RE.lastIndex++;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return infos;
|
|
660
|
+
}
|
|
661
|
+
function isCheckpointType(type) {
|
|
662
|
+
return type.startsWith('checkpoint:');
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Validate one plan task's structure against its type-specific canonical field
|
|
666
|
+
* set (per `gsd-core/references/checkpoints.md`):
|
|
667
|
+
* - `checkpoint:human-verify` requires `<what-built>` / `<how-to-verify>` /
|
|
668
|
+
* `<resume-signal>` (the "checkpoint triple").
|
|
669
|
+
* - `checkpoint:decision` requires `<decision>` / `<options>` /
|
|
670
|
+
* `<resume-signal>`.
|
|
671
|
+
* - `checkpoint:human-action` requires `<action>` / `<instructions>` /
|
|
672
|
+
* `<verification>` / `<resume-signal>`.
|
|
673
|
+
* - Unknown `checkpoint:*` subtypes require only the universal
|
|
674
|
+
* `<resume-signal>` (forward-compat — newer checkpoint types registered
|
|
675
|
+
* in the reference don't need a verifier change to pass structure
|
|
676
|
+
* validation).
|
|
677
|
+
* - All other types (`auto`, `tracer`, `manual`, bare `<task>`, …) keep the
|
|
678
|
+
* historical `<action>` / `<verify>` / `<done>` / `<files>` requirements.
|
|
679
|
+
*/
|
|
680
|
+
function validatePlanTaskStructure(task) {
|
|
681
|
+
const errors = [];
|
|
682
|
+
const warnings = [];
|
|
683
|
+
const taskName = task.hasName ? task.name : 'unnamed';
|
|
684
|
+
if (!task.hasName) {
|
|
685
|
+
errors.push('Task missing <name> element');
|
|
686
|
+
}
|
|
687
|
+
if (isCheckpointType(task.type)) {
|
|
688
|
+
if (!task.hasResumeSignal) {
|
|
689
|
+
errors.push(`Task '${taskName}' missing <resume-signal>`);
|
|
690
|
+
}
|
|
691
|
+
switch (task.type) {
|
|
692
|
+
case 'checkpoint:human-verify':
|
|
693
|
+
if (!task.hasWhatBuilt)
|
|
694
|
+
errors.push(`Task '${taskName}' missing <what-built>`);
|
|
695
|
+
if (!task.hasHowToVerify)
|
|
696
|
+
errors.push(`Task '${taskName}' missing <how-to-verify>`);
|
|
697
|
+
break;
|
|
698
|
+
case 'checkpoint:decision':
|
|
699
|
+
if (!task.hasDecision)
|
|
700
|
+
errors.push(`Task '${taskName}' missing <decision>`);
|
|
701
|
+
if (!task.hasOptions)
|
|
702
|
+
errors.push(`Task '${taskName}' missing <options>`);
|
|
703
|
+
break;
|
|
704
|
+
case 'checkpoint:human-action':
|
|
705
|
+
if (!task.hasAction)
|
|
706
|
+
errors.push(`Task '${taskName}' missing <action>`);
|
|
707
|
+
if (!task.hasInstructions)
|
|
708
|
+
errors.push(`Task '${taskName}' missing <instructions>`);
|
|
709
|
+
if (!task.hasVerification)
|
|
710
|
+
errors.push(`Task '${taskName}' missing <verification>`);
|
|
711
|
+
break;
|
|
712
|
+
default:
|
|
713
|
+
// Unknown checkpoint:* subtype: <resume-signal> is the only universal
|
|
714
|
+
// requirement (forward-compat).
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
if (!task.hasAction)
|
|
720
|
+
errors.push(`Task '${taskName}' missing <action>`);
|
|
721
|
+
if (!task.hasVerify)
|
|
722
|
+
warnings.push(`Task '${taskName}' missing <verify>`);
|
|
723
|
+
if (!task.hasDone)
|
|
724
|
+
warnings.push(`Task '${taskName}' missing <done>`);
|
|
725
|
+
if (!task.hasFiles)
|
|
726
|
+
warnings.push(`Task '${taskName}' missing <files>`);
|
|
727
|
+
}
|
|
728
|
+
return { errors, warnings };
|
|
729
|
+
}
|
|
519
730
|
function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
520
731
|
if (!filePath) {
|
|
521
732
|
error('file path required');
|
|
522
733
|
}
|
|
734
|
+
if (filePath.includes('\0')) {
|
|
735
|
+
error('file path contains null bytes');
|
|
736
|
+
}
|
|
523
737
|
const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
|
|
524
738
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
|
|
525
739
|
if (!content) {
|
|
526
740
|
output({ error: 'File not found', path: filePath }, raw);
|
|
527
741
|
return;
|
|
528
742
|
}
|
|
529
|
-
|
|
743
|
+
// #2701: fail loud on NUL/binary corruption before structure checks. A
|
|
744
|
+
// structurally intact-but-NUL-corrupted plan otherwise passes as valid and is
|
|
745
|
+
// silently skipped by recursive/binary-skipping searchers downstream.
|
|
746
|
+
const encErr = (0, validate_cjs_2.textEncodingError)(content, filePath);
|
|
747
|
+
if (encErr) {
|
|
748
|
+
output({ valid: false, errors: [encErr] }, raw);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const fm = extractFrontmatter(content, fullPath);
|
|
530
752
|
const errors = [];
|
|
531
753
|
const warnings = [];
|
|
532
754
|
const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
|
|
@@ -534,25 +756,20 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
534
756
|
if (fm[field] === undefined)
|
|
535
757
|
errors.push(`Missing required frontmatter field: ${field}`);
|
|
536
758
|
}
|
|
759
|
+
const extractedTasks = extractPlanTaskInfos(content);
|
|
537
760
|
const tasks = [];
|
|
538
|
-
for (const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
warnings.push(`Task '${taskName}' missing <verify>`);
|
|
551
|
-
if (!hasDone)
|
|
552
|
-
warnings.push(`Task '${taskName}' missing <done>`);
|
|
553
|
-
if (!hasFiles)
|
|
554
|
-
warnings.push(`Task '${taskName}' missing <files>`);
|
|
555
|
-
tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
|
|
761
|
+
for (const task of extractedTasks) {
|
|
762
|
+
const verdict = validatePlanTaskStructure(task);
|
|
763
|
+
errors.push(...verdict.errors);
|
|
764
|
+
warnings.push(...verdict.warnings);
|
|
765
|
+
tasks.push({
|
|
766
|
+
name: task.hasName ? task.name : 'unnamed',
|
|
767
|
+
type: task.type,
|
|
768
|
+
hasFiles: task.hasFiles,
|
|
769
|
+
hasAction: task.hasAction,
|
|
770
|
+
hasVerify: task.hasVerify,
|
|
771
|
+
hasDone: task.hasDone,
|
|
772
|
+
});
|
|
556
773
|
}
|
|
557
774
|
if (tasks.length === 0)
|
|
558
775
|
warnings.push('No <task> elements found');
|
|
@@ -567,6 +784,24 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
567
784
|
if (hasCheckpoints && fm['autonomous'] !== 'false' && String(fm['autonomous']) !== 'false') {
|
|
568
785
|
errors.push('Has checkpoint tasks but autonomous is not false');
|
|
569
786
|
}
|
|
787
|
+
// #1951: a decision rated one-way is supposed to be confirmed before it is
|
|
788
|
+
// walked through. Warn (never error — <reversibility> stays additive) when a
|
|
789
|
+
// one-way rating has no checkpoint:decision anywhere ahead of it in the plan,
|
|
790
|
+
// which is the planner emitting the rating but skipping the gate.
|
|
791
|
+
const decisionCheckpointOffsets = [];
|
|
792
|
+
for (const m of content.matchAll(/<task\s+type=["']?checkpoint:decision/g)) {
|
|
793
|
+
if (m.index !== undefined)
|
|
794
|
+
decisionCheckpointOffsets.push(m.index);
|
|
795
|
+
}
|
|
796
|
+
for (const m of content.matchAll(/<reversibility\s[^>]*rating=["']?one-way/g)) {
|
|
797
|
+
const at = m.index;
|
|
798
|
+
if (at === undefined)
|
|
799
|
+
continue;
|
|
800
|
+
if (!decisionCheckpointOffsets.some((offset) => offset < at)) {
|
|
801
|
+
warnings.push('Task rated <reversibility rating="one-way"> has no preceding checkpoint:decision — '
|
|
802
|
+
+ 'a one-way door must be confirmed before the agent walks through it');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
570
805
|
const echoScan = scanNegativeGrepCommentEcho(content);
|
|
571
806
|
errors.push(...echoScan.errors);
|
|
572
807
|
warnings.push(...echoScan.warnings);
|
|
@@ -767,7 +1002,7 @@ function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
|
|
|
767
1002
|
const planContent = (0, shell_command_projection_cjs_1.platformReadSync)(planFullPath);
|
|
768
1003
|
if (!planContent)
|
|
769
1004
|
continue;
|
|
770
|
-
const fm = extractFrontmatter(planContent);
|
|
1005
|
+
const fm = extractFrontmatter(planContent, planFullPath);
|
|
771
1006
|
const waveRaw = fm['wave'];
|
|
772
1007
|
const wave = typeof waveRaw === 'string' ? parseInt(waveRaw, 10) : (typeof waveRaw === 'number' ? waveRaw : NaN);
|
|
773
1008
|
if (isNaN(wave) || wave < minWave)
|
|
@@ -802,7 +1037,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
802
1037
|
}
|
|
803
1038
|
// Derive the current plan's wave number and phase directory for wave-aware
|
|
804
1039
|
// missing-file handling (fix #1202).
|
|
805
|
-
const currentFm = extractFrontmatter(content);
|
|
1040
|
+
const currentFm = extractFrontmatter(content, fullPath);
|
|
806
1041
|
const currentWaveRaw = currentFm['wave'];
|
|
807
1042
|
const currentWave = typeof currentWaveRaw === 'string'
|
|
808
1043
|
? parseInt(currentWaveRaw, 10)
|
|
@@ -899,8 +1134,16 @@ function listMilestoneArchiveDirs(planBase) {
|
|
|
899
1134
|
.map((e) => node_path_1.default.join(milestonesDir, e.name))
|
|
900
1135
|
.sort((a, b) => node_path_1.default.basename(a).localeCompare(node_path_1.default.basename(b), undefined, { numeric: true }));
|
|
901
1136
|
}
|
|
902
|
-
catch {
|
|
903
|
-
|
|
1137
|
+
catch (err) {
|
|
1138
|
+
// #1883: distinguish genuine absence from a permission/I-O failure. ENOENT
|
|
1139
|
+
// (no milestones/ dir yet) keeps the long-standing [] contract that
|
|
1140
|
+
// collectPhaseRoots / forEachArchivedPhaseToken depend on for "no archives";
|
|
1141
|
+
// every other error (EACCES, EIO, …) must propagate — otherwise an unreadable
|
|
1142
|
+
// milestones/ dir is silently reported as "no archives" and active-milestone
|
|
1143
|
+
// resolution / archived-phase filtering misbehaves.
|
|
1144
|
+
if (err.code === 'ENOENT')
|
|
1145
|
+
return [];
|
|
1146
|
+
throw err;
|
|
904
1147
|
}
|
|
905
1148
|
}
|
|
906
1149
|
function forEachArchivedPhaseToken(planBase, onPhase) {
|
|
@@ -1073,8 +1316,9 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
1073
1316
|
}
|
|
1074
1317
|
}
|
|
1075
1318
|
for (const plan of plans) {
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1319
|
+
const planFilePath = node_path_1.default.join(phasePath, plan);
|
|
1320
|
+
const content = node_fs_1.default.readFileSync(planFilePath, 'utf-8');
|
|
1321
|
+
const fmData = extractFrontmatter(content, planFilePath);
|
|
1078
1322
|
if (!fmData['wave']) {
|
|
1079
1323
|
warnings.push(`${phaseLabel}/${plan}: missing 'wave' in frontmatter`);
|
|
1080
1324
|
}
|
|
@@ -1207,9 +1451,22 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1207
1451
|
try {
|
|
1208
1452
|
const rawCfg = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
1209
1453
|
const parsed = JSON.parse(rawCfg);
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1454
|
+
if (parsed['model_profile'] && !model_catalog_cjs_1.VALID_PROFILES.includes(parsed['model_profile'])) {
|
|
1455
|
+
addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed['model_profile']}"`, `Valid values: ${model_catalog_cjs_1.VALID_PROFILES.join(', ')}`);
|
|
1456
|
+
}
|
|
1457
|
+
const configModels = parsed['models'];
|
|
1458
|
+
if (configModels && typeof configModels === 'object' && !Array.isArray(configModels)) {
|
|
1459
|
+
for (const [phaseType, tierValue] of Object.entries(configModels)) {
|
|
1460
|
+
if (!model_catalog_cjs_1.VALID_PHASE_TYPES.has(phaseType)) {
|
|
1461
|
+
addIssue('warning', 'W022', `config.json: models has an unknown phase type "${phaseType}" which will be ignored`, `Valid phase types: ${[...model_catalog_cjs_1.VALID_PHASE_TYPES].join(', ')}`);
|
|
1462
|
+
}
|
|
1463
|
+
else if (typeof tierValue !== 'string' || !model_catalog_cjs_1.VALID_TIERS.has(tierValue)) {
|
|
1464
|
+
addIssue('warning', 'W022', `config.json: models.${phaseType} has an invalid tier value ${JSON.stringify(tierValue)} which will be ignored`, `Valid tiers: ${[...model_catalog_cjs_1.VALID_TIERS].join(', ')}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
else if (configModels !== undefined && configModels !== null) {
|
|
1469
|
+
addIssue('warning', 'W022', `config.json: models is set to ${JSON.stringify(configModels)}, but must be an object mapping phase types to tiers — this value will be ignored`, `Set models to an object like {"planning": "sonnet"}, or remove the key to use profile defaults`);
|
|
1213
1470
|
}
|
|
1214
1471
|
}
|
|
1215
1472
|
catch (err) {
|
|
@@ -1260,6 +1517,49 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1260
1517
|
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
|
1261
1518
|
}
|
|
1262
1519
|
}
|
|
1520
|
+
// W023 (#2408): detect two or more real on-disk phase directories that
|
|
1521
|
+
// normalize to the same phase key (e.g. `05-real/` + `05-real-stray/`).
|
|
1522
|
+
// The collision silently breaks /gsd-stats status accuracy (now folded by
|
|
1523
|
+
// precedence — see commands.cts foldPhaseStatus) and forces an operator
|
|
1524
|
+
// decision. Wording is neutral — never guesses which directory is "real".
|
|
1525
|
+
{
|
|
1526
|
+
const groups = new Map();
|
|
1527
|
+
for (const e of phaseDirEntries) {
|
|
1528
|
+
// extractPhaseToken never returns empty — for unparseable dir names it
|
|
1529
|
+
// falls back to the dir name itself. Two distinct unparseable names
|
|
1530
|
+
// therefore normalize to distinct keys and cannot false-positive here;
|
|
1531
|
+
// only dirs whose tokens collapse to the same key (e.g. `05-real` and
|
|
1532
|
+
// `05-real-stray` → token `05`) produce a collision group.
|
|
1533
|
+
const token = extractPhaseToken(e.name);
|
|
1534
|
+
const key = normalizePhaseName(token);
|
|
1535
|
+
const list = groups.get(key);
|
|
1536
|
+
if (list)
|
|
1537
|
+
list.push(e.name);
|
|
1538
|
+
else
|
|
1539
|
+
groups.set(key, [e.name]);
|
|
1540
|
+
}
|
|
1541
|
+
for (const [key, dirs] of groups) {
|
|
1542
|
+
if (dirs.length < 2)
|
|
1543
|
+
continue;
|
|
1544
|
+
// Compute each dir's status independently so the warning is informative.
|
|
1545
|
+
// Sort by phase id for stable output regardless of readdir order; tie-
|
|
1546
|
+
// break on the dir name itself so two dirs sharing the same phase token
|
|
1547
|
+
// (the collision case itself) still sort deterministically (V8's stable
|
|
1548
|
+
// sort would otherwise fall back to non-portable fs.readdirSync order).
|
|
1549
|
+
const described = dirs
|
|
1550
|
+
.slice()
|
|
1551
|
+
.sort((a, b) => comparePhaseNum(a, b) || String(a).localeCompare(String(b)))
|
|
1552
|
+
.map((d) => {
|
|
1553
|
+
const files = phaseDirFiles.get(d) || [];
|
|
1554
|
+
const plans = files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
1555
|
+
const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
1556
|
+
const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, d), 'Not Started');
|
|
1557
|
+
return `${d} (${status})`;
|
|
1558
|
+
})
|
|
1559
|
+
.join(', ');
|
|
1560
|
+
addIssue('warning', 'W023', `Phase directories collide on normalized key "${key}": ${described}`, 'Inspect each directory; rename or remove the duplicate so only one directory maps to this phase key');
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1263
1563
|
for (const e of phaseDirEntries) {
|
|
1264
1564
|
const phaseFiles = phaseDirFiles.get(e.name) || [];
|
|
1265
1565
|
const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
@@ -1949,6 +2249,7 @@ module.exports = {
|
|
|
1949
2249
|
scanNegativeGrepCommentEcho,
|
|
1950
2250
|
scanFileWideNegativeGateConflict,
|
|
1951
2251
|
cmdVerifySummary,
|
|
2252
|
+
verifySummaryCore,
|
|
1952
2253
|
cmdVerifyPlanStructure,
|
|
1953
2254
|
cmdVerifyPhaseCompleteness,
|
|
1954
2255
|
cmdVerifyReferences,
|
|
@@ -1960,4 +2261,9 @@ module.exports = {
|
|
|
1960
2261
|
cmdValidateAgents,
|
|
1961
2262
|
cmdVerifySchemaDrift,
|
|
1962
2263
|
cmdVerifyCodebaseDrift,
|
|
2264
|
+
// Test seam (#1883): listMilestoneArchiveDirs is private and exercised through
|
|
2265
|
+
// the validate command, which runs in a subprocess — an fs monkeypatch in the
|
|
2266
|
+
// test process cannot reach it. Exposed under a leading underscore so the
|
|
2267
|
+
// permission-error path can be unit-tested directly (no chmod 0o000).
|
|
2268
|
+
_listMilestoneArchiveDirs: listMilestoneArchiveDirs,
|
|
1963
2269
|
};
|