@opengsd/gsd-core 1.12.0 → 1.13.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 +12 -0
- package/agents/gsd-executor.md +63 -35
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +239 -67
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +195 -50
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-registry.cjs +514 -114
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +120 -4
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +85 -2
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +337 -13
- package/gsd-core/bin/lib/config-loader.cjs +3 -0
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +132 -15
- package/gsd-core/bin/lib/install-engine.cjs +184 -12
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +22 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/milestone.cjs +22 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-id.cjs +251 -9
- package/gsd-core/bin/lib/phase.cjs +774 -35
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
- package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
- package/gsd-core/bin/lib/roadmap.cjs +187 -58
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +30 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
- package/gsd-core/bin/lib/state-transition.cjs +333 -44
- package/gsd-core/bin/lib/state.cjs +684 -125
- package/gsd-core/bin/lib/surface.cjs +23 -8
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1419 -515
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +273 -12
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +346 -16
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +14 -9
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +2 -0
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
- package/gsd-core/workflows/code-review-fix.md +2 -0
- package/gsd-core/workflows/code-review.md +73 -31
- package/gsd-core/workflows/complete-milestone.md +13 -4
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +32 -14
- package/gsd-core/workflows/execute-plan.md +8 -8
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +2 -0
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +5 -3
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +19 -4
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +130 -12
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +11 -3
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +13 -3
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +41 -38
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +49 -3
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +283 -12
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-statusline.js +7 -3
- package/hooks/dist/gsd-validate-commit.sh +444 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +283 -12
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-statusline.js +7 -3
- package/hooks/gsd-validate-commit.sh +444 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -5
- package/scripts/build-hooks.js +11 -4
- package/scripts/ci-test-scope.cjs +7 -0
- package/scripts/docs-guard-registry.cjs +10 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-test-file-count.allowlist.json +3 -1
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
|
@@ -143,6 +143,7 @@ function parseXmlTasks(content) {
|
|
|
143
143
|
acceptanceCriteria: [],
|
|
144
144
|
done: null,
|
|
145
145
|
trackerId: null,
|
|
146
|
+
tdd: null,
|
|
146
147
|
};
|
|
147
148
|
}
|
|
148
149
|
return {
|
|
@@ -154,6 +155,7 @@ function parseXmlTasks(content) {
|
|
|
154
155
|
acceptanceCriteria: splitCriteria(elementBody(block, 'acceptance_criteria')),
|
|
155
156
|
done: collapseWhitespace(elementBody(block, 'done')),
|
|
156
157
|
trackerId: tagAttribute(openTag, 'tracker-id'),
|
|
158
|
+
tdd: tagAttribute(openTag, 'tdd'),
|
|
157
159
|
};
|
|
158
160
|
});
|
|
159
161
|
}
|
|
@@ -172,6 +174,7 @@ function parseMarkdownTasks(content) {
|
|
|
172
174
|
acceptanceCriteria: [],
|
|
173
175
|
done: null,
|
|
174
176
|
trackerId: null,
|
|
177
|
+
tdd: null,
|
|
175
178
|
}));
|
|
176
179
|
}
|
|
177
180
|
// ─── Objective ────────────────────────────────────────────────────────────────
|
|
@@ -247,8 +250,15 @@ function parsePlanDocument(content, planPath = '') {
|
|
|
247
250
|
const hintStr = String(fmAgentHint).trim();
|
|
248
251
|
agentHint = hintStr !== '' ? hintStr : null;
|
|
249
252
|
}
|
|
253
|
+
let planType = null;
|
|
254
|
+
const fmType = fm['type'];
|
|
255
|
+
if (fmType !== undefined) {
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar-to-string
|
|
257
|
+
planType = String(fmType);
|
|
258
|
+
}
|
|
250
259
|
return {
|
|
251
260
|
objective: extractObjective(content) || fm['objective'] || null,
|
|
261
|
+
type: planType,
|
|
252
262
|
declaredWave,
|
|
253
263
|
dependsOn,
|
|
254
264
|
autonomous,
|
|
@@ -37,7 +37,11 @@ const { isPhaseComplete } = verificationMod;
|
|
|
37
37
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
38
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
39
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
40
|
-
|
|
40
|
+
// #612: `resolvePhaseIdConvention` is the federated (workstream -> root)
|
|
41
|
+
// `phase_id_convention` reader, from the same §7 owner module `planningPaths`
|
|
42
|
+
// comes from. Resolved once in `buildPlanningSnapshot` — see the
|
|
43
|
+
// `phaseIdConvention` field's comment for why one resolution point matters.
|
|
44
|
+
const { planningPaths, planningRoot, resolvePhaseIdConvention } = planningWorkspace;
|
|
41
45
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
42
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
47
|
const frontmatterMod = require("./frontmatter.cjs");
|
|
@@ -64,7 +68,15 @@ const configLoaderMod = require("./config-loader.cjs");
|
|
|
64
68
|
const { isGitIgnored } = configLoaderMod;
|
|
65
69
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
70
|
const phaseIdMod = require("./phase-id.cjs");
|
|
67
|
-
|
|
71
|
+
// #612: `phaseHeadingPrefixSrcFor`/`PHASE_HEADING_BASELINE` SELECT a heading
|
|
72
|
+
// intro by convention (a convention-less call compiles the byte-identical base
|
|
73
|
+
// source the literal it replaced spelled); `isSentinelPhaseId` gets its bracket
|
|
74
|
+
// reading only when handed the convention explicitly.
|
|
75
|
+
const { PHASE_NUMBER_TOKEN_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, stripProjectCodePrefix, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, isSentinelPhaseId, scopeToPhase, } = phaseIdMod;
|
|
76
|
+
// #612: `phaseTokenFromDir` is the convention-SELECTED counterpart of
|
|
77
|
+
// `PHASE_TOKEN_FROM_DIR_RE` — handed no convention it delegates to that very
|
|
78
|
+
// regex, so a legacy repo's tokenization is unchanged. `checkBracketCoherence`
|
|
79
|
+
// re-homed into `validate.cts` when #3309 deleted its `verify.cts` neighbours.
|
|
68
80
|
const validate_cjs_1 = require("./validate.cjs");
|
|
69
81
|
// ─── worstScope — the one new piece of coordination logic ───────────────────
|
|
70
82
|
/**
|
|
@@ -462,18 +474,31 @@ function buildProjectSectionsField(cwd) {
|
|
|
462
474
|
* only the mismatches" to "record every attribution" — this field exposes
|
|
463
475
|
* the parsed fact; the future W021/W026 rules make the mismatch judgment.
|
|
464
476
|
*/
|
|
465
|
-
function buildRoadmapDeclaredPhasesField(roadmapPath) {
|
|
477
|
+
function buildRoadmapDeclaredPhasesField(roadmapPath, convention) {
|
|
466
478
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
467
|
-
return {
|
|
479
|
+
return {
|
|
480
|
+
declared: { value: [], scope: SCOPE.UNREADABLE },
|
|
481
|
+
sentinelTokens: { value: [], scope: SCOPE.UNREADABLE },
|
|
482
|
+
};
|
|
468
483
|
}
|
|
469
484
|
let content;
|
|
470
485
|
try {
|
|
471
486
|
content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
472
487
|
}
|
|
473
488
|
catch {
|
|
474
|
-
return {
|
|
489
|
+
return {
|
|
490
|
+
declared: { value: [], scope: SCOPE.UNREADABLE },
|
|
491
|
+
sentinelTokens: { value: [], scope: SCOPE.UNREADABLE },
|
|
492
|
+
};
|
|
475
493
|
}
|
|
476
|
-
|
|
494
|
+
// #612: the declared-phase scan is SELECTED by the resolved convention — a
|
|
495
|
+
// non-bracket repo compiles the byte-identical pattern sources this call
|
|
496
|
+
// compiled before, so its declared set is unchanged. `sentinelPhases` is the
|
|
497
|
+
// same call's third output (empty off the bracket convention) and is surfaced
|
|
498
|
+
// rather than filtered in place: `roadmapPhases` feeds both a membership check
|
|
499
|
+
// (W002's valid-phase set) and a missing-directory warning (W006), and only
|
|
500
|
+
// the latter should ignore an icebox item.
|
|
501
|
+
const { roadmapPhases, sentinelPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(content, convention);
|
|
477
502
|
const milestoneByPhase = new Map();
|
|
478
503
|
const sectionRx = /^#{1,3}\s+(?:\[[^\]]{1,200}\]\s*)?.*v(\d+\.\d+)/gim;
|
|
479
504
|
const sections = [];
|
|
@@ -497,7 +522,10 @@ function buildRoadmapDeclaredPhasesField(roadmapPath) {
|
|
|
497
522
|
phaseId,
|
|
498
523
|
milestone: milestoneByPhase.get(phaseId) ?? null,
|
|
499
524
|
}));
|
|
500
|
-
return {
|
|
525
|
+
return {
|
|
526
|
+
declared: { value, scope: SCOPE.COMPLETE },
|
|
527
|
+
sentinelTokens: { value: [...sentinelPhases], scope: SCOPE.COMPLETE },
|
|
528
|
+
};
|
|
501
529
|
}
|
|
502
530
|
/**
|
|
503
531
|
* Resolve `roadmapPhaseCheckboxes` — parsed `[x]`/`[ ]` checkbox state per
|
|
@@ -518,7 +546,7 @@ function buildRoadmapDeclaredPhasesField(roadmapPath) {
|
|
|
518
546
|
* diagnostic (W011) whose entire purpose is flagging when the two DISAGREE —
|
|
519
547
|
* reading the data is not re-litigating who is authoritative.
|
|
520
548
|
*/
|
|
521
|
-
function buildRoadmapPhaseCheckboxesField(roadmapPath) {
|
|
549
|
+
function buildRoadmapPhaseCheckboxesField(roadmapPath, convention) {
|
|
522
550
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
523
551
|
return { value: {}, scope: SCOPE.UNREADABLE };
|
|
524
552
|
}
|
|
@@ -529,7 +557,15 @@ function buildRoadmapPhaseCheckboxesField(roadmapPath) {
|
|
|
529
557
|
catch {
|
|
530
558
|
return { value: {}, scope: SCOPE.UNREADABLE };
|
|
531
559
|
}
|
|
532
|
-
|
|
560
|
+
// #612: the `Phase\s+` label intro is SELECTED, exactly as
|
|
561
|
+
// `buildNotStartedPhaseVariants` (`validate.cts`) selects it for the same
|
|
562
|
+
// ROADMAP checklist shape — this field is what W006's not-started exclusion
|
|
563
|
+
// now reads instead of that helper, so the two must recognize the same
|
|
564
|
+
// checklist lines or a bracket repo's `- [ ] **[GSD.02] 05: Name**` entries
|
|
565
|
+
// vanish from the exclusion set and every unstarted bracket phase gains a
|
|
566
|
+
// W006. NON-capturing (`capturing` defaults false), so the phase token stays
|
|
567
|
+
// group 2 and the legacy repo compiles a byte-identical source.
|
|
568
|
+
const checkboxRe = new RegExp(`-\\s*\\[([xX ])\\].*?${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention)}0*(${PHASE_NUMBER_TOKEN_SOURCE})${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
|
|
533
569
|
const value = {};
|
|
534
570
|
let m;
|
|
535
571
|
while ((m = checkboxRe.exec(content)) !== null) {
|
|
@@ -537,6 +573,42 @@ function buildRoadmapPhaseCheckboxesField(roadmapPath) {
|
|
|
537
573
|
}
|
|
538
574
|
return { value, scope: SCOPE.COMPLETE };
|
|
539
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Resolve `roadmapBracketIncoherences` — W021's bracket half (#612). Delegates
|
|
578
|
+
* wholly to `checkBracketCoherence` (`validate.cjs`), which is pure and owns
|
|
579
|
+
* both sub-checks; this builder only supplies the ROADMAP text and the
|
|
580
|
+
* convention gate.
|
|
581
|
+
*
|
|
582
|
+
* GATED, not merely filtered downstream: off the bracket convention the ROADMAP
|
|
583
|
+
* is never parsed for this at all and the field is a `COMPLETE`-scoped empty
|
|
584
|
+
* list. Inferring 'bracket' from the SHAPE of a matched heading would run a
|
|
585
|
+
* repo-failing check against a repo that never opted in — a legacy ROADMAP
|
|
586
|
+
* containing `### [RFC.2119] 5:` is legal legacy content, and that is the exact
|
|
587
|
+
* regression PR-2's round 1 killed the original ungated design over.
|
|
588
|
+
*/
|
|
589
|
+
function buildRoadmapBracketIncoherencesField(roadmapPath, convention) {
|
|
590
|
+
// File-readability is decided FIRST, so `scope` means the same thing on every
|
|
591
|
+
// repo: UNREADABLE iff ROADMAP.md could not be read, never "this convention
|
|
592
|
+
// was skipped." Ordering the convention gate first would have made an absent
|
|
593
|
+
// ROADMAP.md report COMPLETE on a legacy repo and UNREADABLE on a bracket one
|
|
594
|
+
// — the same "empty, nothing to say" state wearing two different scopes, which
|
|
595
|
+
// is precisely the non-answer/answer distinction ADR-3180 §8.1 gives `scope`
|
|
596
|
+
// to carry.
|
|
597
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
598
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
599
|
+
// A non-bracket repo has no bracket incoherences BY DEFINITION — a real,
|
|
600
|
+
// COMPLETE answer, not a skipped read.
|
|
601
|
+
if (convention !== 'bracket')
|
|
602
|
+
return { value: [], scope: SCOPE.COMPLETE };
|
|
603
|
+
let content;
|
|
604
|
+
try {
|
|
605
|
+
content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
609
|
+
}
|
|
610
|
+
return { value: (0, validate_cjs_1.checkBracketCoherence)(content), scope: SCOPE.COMPLETE };
|
|
611
|
+
}
|
|
540
612
|
/**
|
|
541
613
|
* Resolve `researchValidationStatus` — per phase directory, whether its
|
|
542
614
|
* `*-RESEARCH.md` contains the literal heading `## Validation Architecture`,
|
|
@@ -699,7 +771,7 @@ function buildAllPhaseDirNamesField(phasesDir) {
|
|
|
699
771
|
* present-but-unreadable per-archive-dir entry is silently skipped, mirroring
|
|
700
772
|
* `forEachArchivedPhaseToken`'s own per-directory `catch { /* absent/unreadable *\/ }`.
|
|
701
773
|
*/
|
|
702
|
-
function buildArchivedPhaseTokensField(planBase) {
|
|
774
|
+
function buildArchivedPhaseTokensField(planBase, convention) {
|
|
703
775
|
const milestonesDir = node_path_1.default.join(planBase, 'milestones');
|
|
704
776
|
let archiveDirs;
|
|
705
777
|
try {
|
|
@@ -720,9 +792,19 @@ function buildArchivedPhaseTokensField(planBase) {
|
|
|
720
792
|
for (const e of entries) {
|
|
721
793
|
if (!e.isDirectory())
|
|
722
794
|
continue;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
795
|
+
// #612: composed, not chosen. The convention-aware extractor decides
|
|
796
|
+
// WHICH directory shapes are recognized (so an archived
|
|
797
|
+
// `{CODE}.{MM}-{PP}-slug` is seen at all — `PHASE_TOKEN_FROM_DIR_RE`
|
|
798
|
+
// rejects it outright, which is why every archived bracket phase used
|
|
799
|
+
// to still draw a W006/W002); `stripProjectCodePrefix` then normalizes
|
|
800
|
+
// the token it returns. The strip is a no-op on every bracket token
|
|
801
|
+
// (`01`, `01.02` — the `{CODE}.{MM}` prefix is not part of the token)
|
|
802
|
+
// and does the #2528 work on legacy ones (`MEM-05` -> `05`), so neither
|
|
803
|
+
// side loses its case. Handed no convention, `phaseTokenFromDir`
|
|
804
|
+
// delegates to `PHASE_TOKEN_FROM_DIR_RE` itself — legacy is unchanged.
|
|
805
|
+
const token = (0, validate_cjs_1.phaseTokenFromDir)(e.name, convention);
|
|
806
|
+
if (token)
|
|
807
|
+
value.push(stripProjectCodePrefix(token));
|
|
726
808
|
}
|
|
727
809
|
}
|
|
728
810
|
catch {
|
|
@@ -740,7 +822,7 @@ function buildArchivedPhaseTokensField(planBase) {
|
|
|
740
822
|
* ROADMAP.md degrades to an empty list, mirroring every other
|
|
741
823
|
* ROADMAP-sourced field's absent-file handling.
|
|
742
824
|
*/
|
|
743
|
-
function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath) {
|
|
825
|
+
function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath, convention) {
|
|
744
826
|
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
745
827
|
return { value: [], scope: SCOPE.UNREADABLE };
|
|
746
828
|
let content;
|
|
@@ -753,8 +835,32 @@ function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath) {
|
|
|
753
835
|
const scoped = extractCurrentMilestone(content, cwd);
|
|
754
836
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal
|
|
755
837
|
// mirror of OPTIONAL_PHASE_TAG_SOURCE) — verbatim from `verify.cts:2366`.
|
|
756
|
-
|
|
757
|
-
|
|
838
|
+
//
|
|
839
|
+
// #612: this scan is convention-AGNOSTIC in POSTURE — W026 is ungated, and
|
|
840
|
+
// bug-557 pins it with an empty config so it fires on every repo — but its
|
|
841
|
+
// heading grammar is still SELECTED, never widened. Under bracket the intro
|
|
842
|
+
// CAPTURES, so the phase token moves to group 2 and `bracketGroup` carries
|
|
843
|
+
// that offset; off bracket the source is byte-identical to the literal above
|
|
844
|
+
// and `bracketGroup` is 0. Inferring the convention from a matched bracket's
|
|
845
|
+
// shape would run a repo-failing check against a repo that never opted in.
|
|
846
|
+
const bracketGroup = convention === 'bracket' ? 1 : 0;
|
|
847
|
+
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention, Boolean(bracketGroup))}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
|
|
848
|
+
const value = [];
|
|
849
|
+
for (const m of scoped.matchAll(phasePattern)) {
|
|
850
|
+
const bracketId = bracketGroup ? m[1] : undefined;
|
|
851
|
+
const phaseNum = m[1 + bracketGroup];
|
|
852
|
+
// A bracket sentinel is an ICEBOX item, not an unstarted phase — it
|
|
853
|
+
// legitimately has no directory, so leaving it in this list makes W026
|
|
854
|
+
// ("STATE says milestone complete but ROADMAP lists an unstarted phase")
|
|
855
|
+
// fire on every bracket repo that keeps an icebox. Filtered here rather
|
|
856
|
+
// than in `RULE_W026` because the bracket id is only visible at the match:
|
|
857
|
+
// the emitted token is `07`, and sentinel-ness lives in the `[GSD.999]`
|
|
858
|
+
// milestone this scan just discarded. This field's only consumer is W026
|
|
859
|
+
// (see its own doc comment above).
|
|
860
|
+
if (bracketId && isSentinelPhaseId(`${bracketId}-${phaseNum}`, 'bracket'))
|
|
861
|
+
continue;
|
|
862
|
+
value.push(phaseNum);
|
|
863
|
+
}
|
|
758
864
|
return { value, scope: SCOPE.COMPLETE };
|
|
759
865
|
}
|
|
760
866
|
/**
|
|
@@ -853,11 +959,29 @@ function buildPerPhasePlanScanFields(phasesDir, phaseDirNames, enumerationScope)
|
|
|
853
959
|
*/
|
|
854
960
|
function buildPlanningSnapshot(cwd) {
|
|
855
961
|
const paths = planningPaths(cwd);
|
|
962
|
+
// #612: ONE federated (workstream -> root) resolution for the whole snapshot.
|
|
963
|
+
// See the `phaseIdConvention` field's comment for why one resolution point is
|
|
964
|
+
// load-bearing rather than a micro-optimisation.
|
|
965
|
+
const phaseIdConvention = resolvePhaseIdConvention(cwd) ?? null;
|
|
856
966
|
const milestone = getMilestoneInfo(cwd);
|
|
967
|
+
// #612: deliberately LEFT to `listMilestonePhaseDirs`'s own lazy resolve —
|
|
968
|
+
// this call is byte-identical to upstream's.
|
|
969
|
+
//
|
|
970
|
+
// Passing `phaseIdConvention` here would NOT be a no-op, which is exactly why
|
|
971
|
+
// it is not passed. The lazy path resolves `resolvePhaseIdConvention(cwd, ws)`
|
|
972
|
+
// with this call's `ws`, which defaults to `null` — the PROJECT-only reading,
|
|
973
|
+
// with no root fallback. The field above is resolved with `ws` undefined,
|
|
974
|
+
// i.e. the FEDERATED workstream -> root reading. On a workstream repo whose
|
|
975
|
+
// root opts into bracket while the workstream config does not, the two answers
|
|
976
|
+
// genuinely differ, and substituting one for the other would silently re-scope
|
|
977
|
+
// `phaseDirs` — a change this PR does not need and no test covers. The
|
|
978
|
+
// federation guarantee PR-2 exists to deliver is delivered where it is
|
|
979
|
+
// observable: in the rules that read `snapshot.phaseIdConvention`.
|
|
857
980
|
const phaseDirs = listMilestonePhaseDirs(paths.phases, { cwd });
|
|
858
981
|
const phasesValue = phaseDirs.value.map((dir) => buildPhaseSnapshot(paths.phases, dir));
|
|
859
982
|
const stateFields = buildStateFields(paths.state);
|
|
860
983
|
const allPhaseDirNames = buildAllPhaseDirNamesField(paths.phases);
|
|
984
|
+
const roadmapDeclared = buildRoadmapDeclaredPhasesField(paths.roadmap, phaseIdConvention);
|
|
861
985
|
const perPhasePlanScanFields = buildPerPhasePlanScanFields(paths.phases, allPhaseDirNames.value, allPhaseDirNames.scope);
|
|
862
986
|
return {
|
|
863
987
|
cwd: node_path_1.default.resolve(cwd),
|
|
@@ -875,17 +999,20 @@ function buildPlanningSnapshot(cwd) {
|
|
|
875
999
|
projectSections: buildProjectSectionsField(cwd),
|
|
876
1000
|
statePhaseTokens: stateFields.statePhaseTokens,
|
|
877
1001
|
stateStatus: stateFields.stateStatus,
|
|
878
|
-
roadmapDeclaredPhases:
|
|
879
|
-
roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap),
|
|
1002
|
+
roadmapDeclaredPhases: roadmapDeclared.declared,
|
|
1003
|
+
roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap, phaseIdConvention),
|
|
880
1004
|
researchValidationStatus: buildResearchValidationStatusField(paths.phases, phaseDirs.value, phaseDirs.scope),
|
|
881
1005
|
milestoneArchiveStatus: buildMilestoneArchiveStatusField(cwd),
|
|
882
1006
|
planningRootFiles: buildPlanningRootFilesField(cwd),
|
|
883
1007
|
allPhaseDirNames,
|
|
884
|
-
archivedPhaseTokens: buildArchivedPhaseTokensField(paths.planning),
|
|
885
|
-
currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap),
|
|
1008
|
+
archivedPhaseTokens: buildArchivedPhaseTokensField(paths.planning, phaseIdConvention),
|
|
1009
|
+
currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap, phaseIdConvention),
|
|
886
1010
|
perPhasePlanNumbering: perPhasePlanScanFields.perPhasePlanNumbering,
|
|
887
1011
|
perPhaseOrphanSummaries: perPhasePlanScanFields.perPhaseOrphanSummaries,
|
|
888
1012
|
perPhaseWaveMissingPlans: perPhasePlanScanFields.perPhaseWaveMissingPlans,
|
|
1013
|
+
phaseIdConvention,
|
|
1014
|
+
roadmapSentinelPhaseTokens: roadmapDeclared.sentinelTokens,
|
|
1015
|
+
roadmapBracketIncoherences: buildRoadmapBracketIncoherencesField(paths.roadmap, phaseIdConvention),
|
|
889
1016
|
};
|
|
890
1017
|
}
|
|
891
1018
|
module.exports = {
|
|
@@ -21,6 +21,9 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
21
21
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
22
22
|
const clock_cjs_1 = require("./clock.cjs");
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
25
|
+
const { SCOPE } = planningScopeMod;
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
27
|
const activeWorkstreamStore = require("./active-workstream-store.cjs");
|
|
25
28
|
const { createSharedPointerAdapter, createSessionScopedPointerAdapter, createMemoryPointerAdapter, getActiveWorkstream: getStoredActiveWorkstream, peekActiveWorkstream: peekStoredActiveWorkstream, setActiveWorkstream: setStoredActiveWorkstream, clearActiveWorkstream: clearStoredActiveWorkstream, diagnoseUnresolvedActiveWorkstream: diagnoseUnresolvedStoredActiveWorkstream, } = activeWorkstreamStore;
|
|
26
29
|
// Track .planning/.lock files held by this process so they can be removed on exit.
|
|
@@ -175,6 +178,87 @@ function worktreesOptedOutUnguarded(cwd) {
|
|
|
175
178
|
}
|
|
176
179
|
return false;
|
|
177
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* #612: resolve `phase_id_convention` with the SAME workstream->root federation
|
|
183
|
+
* config-loader uses (config-loader.cts:618/:649) — the workstream config wins,
|
|
184
|
+
* the root config is the fallback.
|
|
185
|
+
*
|
|
186
|
+
* Why this exists rather than `loadConfig(cwd)['phase_id_convention']`: as of
|
|
187
|
+
* #2997 (aa7697fe, in `next`), loadConfig surfaces `phase_id_convention` in
|
|
188
|
+
* its resolved `_baseConfig` — the "loadConfig drops keys it does not know"
|
|
189
|
+
* rationale this comment used to give is stale. The surviving reasons for the
|
|
190
|
+
* direct read are (1) the workstream->root federation below, a standalone
|
|
191
|
+
* resolution this function needs to run against a GIVEN cwd rather than
|
|
192
|
+
* whatever base a `loadConfig(cwd)` call elsewhere would federate from, and
|
|
193
|
+
* (2) convention-ENUM validation, which is still #612 PR-4 work — this
|
|
194
|
+
* function returns the raw string unvalidated, same as the now-surfaced
|
|
195
|
+
* resolved key would. #2997 surfacing the key makes consuming it from
|
|
196
|
+
* resolved config (instead of re-reading config.json here) a natural PR-4
|
|
197
|
+
* consolidation, not this PR's scope. Cycles were never the obstacle.
|
|
198
|
+
*
|
|
199
|
+
* Why federation matters here specifically: the phase-id readers were splitting
|
|
200
|
+
* on this value from two different bases — one resolving from the workstream
|
|
201
|
+
* directory, one from the root — so a workstream repo got the widened ROADMAP
|
|
202
|
+
* read with the narrow directory read, or the reverse, and reported every phase
|
|
203
|
+
* either missing from disk or malformed on disk. One resolver, one answer.
|
|
204
|
+
*
|
|
205
|
+
* The workstream is `planningDir`'s own `ws` parameter, forwarded, so this
|
|
206
|
+
* shares the canonical resolution (and its GSD_PROJECT/GSD_WORKSTREAM
|
|
207
|
+
* handling). Root is consulted as a fallback only when a workstream is active,
|
|
208
|
+
* matching config-loader; a project-scoped directory stands alone. Returns null
|
|
209
|
+
* when unset, absent, or unreadable — every caller treats null as "not the
|
|
210
|
+
* bracket convention".
|
|
211
|
+
*
|
|
212
|
+
* #2761 B1 (trek-e review): `ws` is a PARAMETER, not read from the environment
|
|
213
|
+
* here. It was omitted at first on the reasoning that "the active workstream is
|
|
214
|
+
* whatever planningDir resolves" — true only for the env-driven caller. A
|
|
215
|
+
* caller that iterates workstreams passes the name as an ARGUMENT (it cannot
|
|
216
|
+
* set `GSD_WORKSTREAM` per iteration), and `planningDir` falls back to the env
|
|
217
|
+
* only when `ws` is `undefined`, so an argument-driven call resolved this
|
|
218
|
+
* convention from the ROOT config while reading that workstream's ROADMAP. Two
|
|
219
|
+
* consequences, both reproduced: a workstream that explicitly declares its OWN
|
|
220
|
+
* convention had it ignored — the root's value decided how the workstream's
|
|
221
|
+
* roadmap was parsed, so flipping ONLY the root config changed which milestone
|
|
222
|
+
* a workstream extracted; and `--workstream foo` disagreed with
|
|
223
|
+
* `GSD_WORKSTREAM=foo` on the same repo.
|
|
224
|
+
*
|
|
225
|
+
* `undefined` (the default) keeps `planningDir`'s env fallback, so every
|
|
226
|
+
* pre-#2761 call site is byte-identical; `null` means "explicitly no
|
|
227
|
+
* workstream". Same discriminator `planningDir` and `getMilestonePhaseFilter`
|
|
228
|
+
* already carry.
|
|
229
|
+
*
|
|
230
|
+
* SCOPE: this governs the #612 bracket-selection reads ONLY. The shipped
|
|
231
|
+
* milestone-prefixed W021 gate keeps its own root-only read — re-basing a
|
|
232
|
+
* legacy convention's gate onto a different config is a behaviour change to a
|
|
233
|
+
* shipped check, in both directions, and is not part of read tolerance.
|
|
234
|
+
*/
|
|
235
|
+
function resolvePhaseIdConvention(cwd, ws) {
|
|
236
|
+
const readFrom = (dir) => {
|
|
237
|
+
const configPath = node_path_1.default.join(dir, 'config.json');
|
|
238
|
+
if (!node_fs_1.default.existsSync(configPath))
|
|
239
|
+
return null;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
242
|
+
const value = parsed['phase_id_convention'];
|
|
243
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const scoped = planningDir(cwd, ws);
|
|
250
|
+
const root = planningRoot(cwd);
|
|
251
|
+
if (scoped === root)
|
|
252
|
+
return readFrom(root);
|
|
253
|
+
// Root is a fallback only when a WORKSTREAM is active — config-loader falls
|
|
254
|
+
// back to the root config under `if (ws)` and not otherwise, so a
|
|
255
|
+
// project-scoped directory stands alone. Detected by suppressing the
|
|
256
|
+
// workstream segment rather than re-reading the environment.
|
|
257
|
+
const projectOnly = planningDir(cwd, null);
|
|
258
|
+
if (scoped === projectOnly)
|
|
259
|
+
return readFrom(scoped);
|
|
260
|
+
return readFrom(scoped) ?? readFrom(root);
|
|
261
|
+
}
|
|
178
262
|
// Sorted list of workstream directory names under `<root>/.planning/workstreams`,
|
|
179
263
|
// or `[]` when the project is flat (no workstreams dir). Single source of truth
|
|
180
264
|
// for the "workstream mode" detection shared by the #1912/#2028 fail-safe guards
|
|
@@ -449,40 +533,30 @@ function describeUnresolvedWorkstreamReason(reason) {
|
|
|
449
533
|
return 'the name is not a valid workstream name';
|
|
450
534
|
return "its workstream directory doesn't exist (it may have been renamed or removed)";
|
|
451
535
|
}
|
|
452
|
-
/**
|
|
453
|
-
* Locate the CONTEXT.md file in a phase directory, handling both the bare
|
|
454
|
-
* form (`CONTEXT.md`) and the padded-prefix convention (`NN-CONTEXT.md`,
|
|
455
|
-
* `NN.N-CONTEXT.md`, etc.) used by gsd-discuss-phase output.
|
|
456
|
-
*
|
|
457
|
-
* Returns the filename (not the full path) of the first match, or null if
|
|
458
|
-
* no CONTEXT.md exists in the directory.
|
|
459
|
-
*
|
|
460
|
-
* Canonical dual-form predicate extracted here to eliminate the 5-site
|
|
461
|
-
* duplication that previously existed across init.cjs, roadmap.cjs,
|
|
462
|
-
* core.cjs, gap-checker.cjs (#3739).
|
|
463
|
-
*
|
|
464
|
-
* @param absDirOrFiles - Absolute path to the phase directory,
|
|
465
|
-
* OR an already-read files array (avoids a redundant readdirSync at call sites
|
|
466
|
-
* that already hold a directory listing).
|
|
467
|
-
*/
|
|
468
536
|
function findContextMdIn(absDirOrFiles) {
|
|
469
|
-
|
|
470
|
-
const files = Array.isArray(absDirOrFiles)
|
|
471
|
-
? absDirOrFiles
|
|
472
|
-
: node_fs_1.default.readdirSync(absDirOrFiles);
|
|
537
|
+
const matchIn = (files) => {
|
|
473
538
|
if (files.includes('CONTEXT.md'))
|
|
474
539
|
return 'CONTEXT.md';
|
|
475
540
|
return files.find((f) => f.endsWith('-CONTEXT.md')) ?? null;
|
|
541
|
+
};
|
|
542
|
+
if (Array.isArray(absDirOrFiles)) {
|
|
543
|
+
return matchIn(absDirOrFiles);
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const files = node_fs_1.default.readdirSync(absDirOrFiles);
|
|
547
|
+
return { file: matchIn(files), files, scope: SCOPE.COMPLETE };
|
|
476
548
|
}
|
|
477
549
|
catch (err) {
|
|
478
|
-
// #1883: distinguish genuine absence from a permission/I-O
|
|
479
|
-
// ("nothing there") keeps the long-standing
|
|
480
|
-
// on; every other error (EACCES, EIO, …) is a real
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
550
|
+
// #1883 / #4014: distinguish genuine absence from a permission/I-O
|
|
551
|
+
// failure. ENOENT ("nothing there") keeps the long-standing "real empty"
|
|
552
|
+
// contract callers rely on; every other error (EACCES, EIO, …) is a real
|
|
553
|
+
// read failure — reported as SCOPE.UNREADABLE rather than thrown, so a
|
|
554
|
+
// caller no longer needs its own try/catch to keep an unreadable phase
|
|
555
|
+
// dir from being silently reported the same as "no CONTEXT.md".
|
|
556
|
+
if (err.code === 'ENOENT') {
|
|
557
|
+
return { file: null, files: [], scope: SCOPE.COMPLETE };
|
|
558
|
+
}
|
|
559
|
+
return { file: null, files: [], scope: SCOPE.UNREADABLE };
|
|
486
560
|
}
|
|
487
561
|
}
|
|
488
562
|
module.exports = {
|
|
@@ -493,6 +567,7 @@ module.exports = {
|
|
|
493
567
|
createMemoryPointerAdapter,
|
|
494
568
|
planningDir,
|
|
495
569
|
planningRoot,
|
|
570
|
+
resolvePhaseIdConvention,
|
|
496
571
|
listAvailableWorkstreams,
|
|
497
572
|
planningPaths,
|
|
498
573
|
quickDirFrom,
|