@opengsd/gsd-core 1.9.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +453 -289
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +579 -66
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +75 -47
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -28
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +10 -2
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -160
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/run-tests.cjs +3 -2
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
* the additive, importable server surface a host (or the bin shim) drives.
|
|
28
28
|
*/
|
|
29
29
|
'use strict';
|
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
|
+
};
|
|
30
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
34
|
exports.SERVER_NAME = exports.PROTOCOL_VERSION = void 0;
|
|
32
35
|
exports.handleMessage = handleMessage;
|
|
@@ -36,9 +39,62 @@ const stateIo = require("./state-io.cjs");
|
|
|
36
39
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
40
|
const shellCommandProjection = require("./shell-command-projection.cjs");
|
|
38
41
|
const { dispatchGsdCommand } = shellCommandProjection;
|
|
42
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
44
|
+
const mcp_catalog_cjs_1 = require("./mcp-catalog.cjs");
|
|
39
45
|
exports.PROTOCOL_VERSION = '2024-11-05';
|
|
40
46
|
exports.SERVER_NAME = 'gsd-core';
|
|
41
|
-
|
|
47
|
+
// #3072: resolve SERVER_VERSION from package.json (source tree) or the
|
|
48
|
+
// installed gsd-core/VERSION marker, WITHOUT a top-level `require(...)` —
|
|
49
|
+
// mirrors the established, already-precedented resolver in
|
|
50
|
+
// `runtime-artifact-conversion.cts`'s `gsdVersion()` (#1383): a top-level
|
|
51
|
+
// require would throw on any runtime whose root has no package.json/VERSION,
|
|
52
|
+
// and `scripts/sync-manifest-versions.cjs` is not a fit here — its
|
|
53
|
+
// `VERSIONED_MANIFESTS` list stamps a `version` FIELD into hand-authored JSON
|
|
54
|
+
// manifests (plugin.json, marketplace.json, vscode/package.json); a `.cts`
|
|
55
|
+
// source constant is not a JSON document that script can round-trip through
|
|
56
|
+
// `readJson`/`setByPath`, and teaching it to text-edit TypeScript source
|
|
57
|
+
// would be a much larger footprint than this lazy, cached, defensive read.
|
|
58
|
+
// Resolved once per process (never per-request) and cached; a failed lookup
|
|
59
|
+
// degrades to '0.0.0' (never `undefined`, never a crash) rather than making
|
|
60
|
+
// `initialize`'s `serverInfo.version` field absent or malformed.
|
|
61
|
+
const SEMVER_PREFIX = /^\d+\.\d+\.\d+/;
|
|
62
|
+
let cachedServerVersion;
|
|
63
|
+
function resolveServerVersion() {
|
|
64
|
+
if (cachedServerVersion !== undefined)
|
|
65
|
+
return cachedServerVersion;
|
|
66
|
+
let version = '0.0.0';
|
|
67
|
+
try {
|
|
68
|
+
const v = node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, '..', '..', 'VERSION'), 'utf8').trim();
|
|
69
|
+
if (SEMVER_PREFIX.test(v))
|
|
70
|
+
version = v;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* not an installed tree (no gsd-core/VERSION) */
|
|
74
|
+
}
|
|
75
|
+
if (version === '0.0.0') {
|
|
76
|
+
try {
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy, defensive: a top-level require would throw on a runtime root with no package.json.
|
|
78
|
+
const pkg = require(node_path_1.default.join(__dirname, '..', '..', '..', 'package.json'));
|
|
79
|
+
if (pkg && typeof pkg.version === 'string' && SEMVER_PREFIX.test(pkg.version))
|
|
80
|
+
version = pkg.version;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* runtime root has no package.json */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
cachedServerVersion = version;
|
|
87
|
+
return version;
|
|
88
|
+
}
|
|
89
|
+
// Catalog is built once per process, lazily, and cached (design "Shape" /
|
|
90
|
+
// Gall's Law — no watching, no invalidation; Known limits: the package is
|
|
91
|
+
// immutable in every install mode we ship).
|
|
92
|
+
let cachedCatalog;
|
|
93
|
+
function getCatalog() {
|
|
94
|
+
if (cachedCatalog === undefined)
|
|
95
|
+
cachedCatalog = (0, mcp_catalog_cjs_1.buildCatalog)();
|
|
96
|
+
return cachedCatalog;
|
|
97
|
+
}
|
|
42
98
|
// JSON-RPC 2.0 error codes.
|
|
43
99
|
const PARSE_ERROR = -32700;
|
|
44
100
|
const INVALID_REQUEST = -32600;
|
|
@@ -93,6 +149,32 @@ function okResponse(id, result) {
|
|
|
93
149
|
function asString(v) {
|
|
94
150
|
return typeof v === 'string' ? v : null;
|
|
95
151
|
}
|
|
152
|
+
/** Extract the {@link REASON} carried by a `CatalogError`, if any (see `mcp-catalog.cts`). */
|
|
153
|
+
function catalogErrorReason(err) {
|
|
154
|
+
const reason = err && typeof err === 'object' ? err.reason : undefined;
|
|
155
|
+
return typeof reason === 'string' ? reason : undefined;
|
|
156
|
+
}
|
|
157
|
+
function catalogErrorMessage(err) {
|
|
158
|
+
return err instanceof Error ? err.message : String(err);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Map a `mcp-catalog.cts` `CatalogError.reason` to a JSON-RPC error code.
|
|
162
|
+
* `READ_FAILED` is a server-side IO/compose failure (`INTERNAL_ERROR`);
|
|
163
|
+
* every other reason (unknown uri/prompt/cursor/root, malformed/traversal
|
|
164
|
+
* uri, wrong-typed param) is a client-supplied-value problem (`INVALID_PARAMS`)
|
|
165
|
+
* — deliberately never `METHOD_NOT_FOUND`, so a refusal is never
|
|
166
|
+
* indistinguishable from the method simply not existing (test-matrix rows
|
|
167
|
+
* 33/35).
|
|
168
|
+
*/
|
|
169
|
+
function catalogErrorCode(err) {
|
|
170
|
+
return catalogErrorReason(err) === mcp_catalog_cjs_1.REASON.READ_FAILED ? INTERNAL_ERROR : INVALID_PARAMS;
|
|
171
|
+
}
|
|
172
|
+
function wireResource(entry) {
|
|
173
|
+
return { uri: entry.uri, name: entry.name, title: entry.title, description: entry.description, mimeType: entry.mimeType };
|
|
174
|
+
}
|
|
175
|
+
function wirePrompt(entry) {
|
|
176
|
+
return { name: entry.name, title: entry.title, description: entry.description };
|
|
177
|
+
}
|
|
96
178
|
function callTool(name, args, ctx) {
|
|
97
179
|
const a = (args && typeof args === 'object' ? args : {});
|
|
98
180
|
const cwd = asString(ctx.cwd) || process.cwd();
|
|
@@ -148,8 +230,12 @@ function handleMessage(request, ctx = {}) {
|
|
|
148
230
|
case 'initialize':
|
|
149
231
|
result = {
|
|
150
232
|
protocolVersion: exports.PROTOCOL_VERSION,
|
|
151
|
-
|
|
152
|
-
|
|
233
|
+
// #3072: resources/prompts declared alongside tools. Deliberately NO
|
|
234
|
+
// subscribe/listChanged — nothing ever sends those notifications
|
|
235
|
+
// (design row 2 / Hyrum's Law: advertising an unimplemented
|
|
236
|
+
// notification is a lie a host would act on).
|
|
237
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
238
|
+
serverInfo: { name: exports.SERVER_NAME, version: resolveServerVersion() },
|
|
153
239
|
};
|
|
154
240
|
break;
|
|
155
241
|
case 'tools/list':
|
|
@@ -163,6 +249,52 @@ function handleMessage(request, ctx = {}) {
|
|
|
163
249
|
result = callTool(toolName, params.arguments, ctx);
|
|
164
250
|
break;
|
|
165
251
|
}
|
|
252
|
+
case 'resources/list': {
|
|
253
|
+
const params = (request.params && typeof request.params === 'object' ? request.params : {});
|
|
254
|
+
try {
|
|
255
|
+
const cursor = typeof params.cursor === 'string' ? params.cursor : undefined;
|
|
256
|
+
const pageSize = typeof params.pageSize === 'number' ? params.pageSize : undefined;
|
|
257
|
+
const page = (0, mcp_catalog_cjs_1.listResources)(getCatalog(), { cursor, pageSize });
|
|
258
|
+
result = page.nextCursor === undefined
|
|
259
|
+
? { resources: page.resources.map(wireResource) }
|
|
260
|
+
: { resources: page.resources.map(wireResource), nextCursor: page.nextCursor };
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
return errorResponse(id, catalogErrorCode(e), catalogErrorMessage(e));
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case 'resources/read': {
|
|
268
|
+
const params = (request.params && typeof request.params === 'object' ? request.params : {});
|
|
269
|
+
try {
|
|
270
|
+
const read = (0, mcp_catalog_cjs_1.readResource)(getCatalog(), params.uri);
|
|
271
|
+
result = { contents: [{ uri: read.uri, mimeType: read.mimeType, text: read.text }] };
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
return errorResponse(id, catalogErrorCode(e), catalogErrorMessage(e));
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'prompts/list':
|
|
279
|
+
result = {
|
|
280
|
+
prompts: [...getCatalog().prompts.values()]
|
|
281
|
+
.map(wirePrompt)
|
|
282
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
|
|
283
|
+
};
|
|
284
|
+
break;
|
|
285
|
+
case 'prompts/get': {
|
|
286
|
+
const params = (request.params && typeof request.params === 'object' ? request.params : {});
|
|
287
|
+
const name = asString(params.name);
|
|
288
|
+
if (!name)
|
|
289
|
+
return errorResponse(id, INVALID_PARAMS, 'prompts/get requires string "name".');
|
|
290
|
+
try {
|
|
291
|
+
result = (0, mcp_catalog_cjs_1.getPrompt)(getCatalog(), name, params.arguments);
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
return errorResponse(id, catalogErrorCode(e), catalogErrorMessage(e));
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
166
298
|
default:
|
|
167
299
|
if (isNotification)
|
|
168
300
|
return null;
|
|
@@ -464,13 +464,25 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
464
464
|
// Guard: prevent marking complete when ROADMAP still lists phases that have
|
|
465
465
|
// no directory on disk (disk_status: no_directory). This catches the case
|
|
466
466
|
// where the active milestone was erroneously marked complete before phases
|
|
467
|
-
// were even started.
|
|
468
|
-
//
|
|
469
|
-
//
|
|
467
|
+
// were even started. The scan scopes the ROADMAP via the `version` argument
|
|
468
|
+
// (getMilestonePhaseFilter / extractCurrentMilestone above) and runs whenever
|
|
469
|
+
// --force is absent — a fresh project with no `### Phase N:` headings in the
|
|
470
|
+
// scoped slice yields an empty `noDirectoryPhases` and the guard is a no-op,
|
|
471
|
+
// so no STATE match is required to avoid false positives.
|
|
470
472
|
// Pass --force to override this guard.
|
|
473
|
+
//
|
|
474
|
+
// #2946: the scan used to be nested inside `if (stateVersion && stateVersion
|
|
475
|
+
// === version)`, which silently disarmed the guard whenever STATE.md's
|
|
476
|
+
// `milestone:` field was desynced or absent — functionally an implicit
|
|
477
|
+
// --force on a one-way-door operation (ROADMAP/REQUIREMENTS archived, phase
|
|
478
|
+
// directories MOVED). The STATE field is not the source of truth for which
|
|
479
|
+
// phases belong to this milestone; the ROADMAP scoping is. The scan now runs
|
|
480
|
+
// unconditionally, and a present-but-mismatched STATE field emits a WARNING
|
|
481
|
+
// so the suspicious condition is visible rather than silent.
|
|
471
482
|
if (!options.force) {
|
|
472
483
|
try {
|
|
473
|
-
//
|
|
484
|
+
// Read STATE.md's milestone field only to detect a suspicious mismatch;
|
|
485
|
+
// it no longer gates the scan. (#2946)
|
|
474
486
|
let stateVersion = null;
|
|
475
487
|
try {
|
|
476
488
|
const stateRaw = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : null;
|
|
@@ -481,58 +493,74 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
481
493
|
}
|
|
482
494
|
}
|
|
483
495
|
catch {
|
|
484
|
-
/* skip */
|
|
496
|
+
/* skip — stateVersion stays null, scan still runs */
|
|
485
497
|
}
|
|
486
|
-
if (stateVersion && stateVersion
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// with a matching token exists on disk. Use the same phaseTokenMatches
|
|
517
|
-
// helper that roadmap.analyze uses to avoid false positives on decimal
|
|
518
|
-
// (2.1) and letter-suffix (12A) phase IDs.
|
|
519
|
-
const hasDirectory = phaseDirEntries.some((d) => phaseTokenMatches(d, normalized));
|
|
520
|
-
if (!hasDirectory) {
|
|
521
|
-
noDirectoryPhases.push(phaseNum);
|
|
522
|
-
}
|
|
498
|
+
if (stateVersion !== null && stateVersion !== version) {
|
|
499
|
+
// #2946: emit a WARNING so the suspicious STATE mismatch is visible
|
|
500
|
+
// rather than silently disarming the guard. Plain-text diagnostic on
|
|
501
|
+
// stderr, matching the existing [gsd-tools] WARNING convention
|
|
502
|
+
// (state.cts). A missing STATE.md `milestone:` field is not warned
|
|
503
|
+
// here — the scan still runs, and "no milestone declared" is a normal
|
|
504
|
+
// state for a fresh project, not a suspicious drift.
|
|
505
|
+
//
|
|
506
|
+
// `stateVersion` comes from a user-controlled file (STATE.md) and is
|
|
507
|
+
// not validated like the CLI `version` arg (ARCHIVE_VERSION_LABEL_RE).
|
|
508
|
+
// Sanitize before interpolating into stderr so ANSI escapes / control
|
|
509
|
+
// chars / secret-looking strings cannot be echoed verbatim into a CI
|
|
510
|
+
// log or terminal (CONTRIBUTING.md security: secret-looking values in
|
|
511
|
+
// stderr). `version` is already constrained to [A-Za-z0-9._-].
|
|
512
|
+
const safeStateVersion = stateVersion.replace(/[\x00-\x1f\x7f]/g, '?').slice(0, 80);
|
|
513
|
+
process.stderr.write(`[gsd-tools] WARNING: STATE.md milestone: "${safeStateVersion}" ≠ requested "${version}" — ` +
|
|
514
|
+
`running the unstarted-phase guard against the ROADMAP scoped for "${version}" anyway.\n`);
|
|
515
|
+
}
|
|
516
|
+
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
517
|
+
const scopedContent = extractCurrentMilestone(roadmapContent, cwd);
|
|
518
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
519
|
+
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
520
|
+
const noDirectoryPhases = [];
|
|
521
|
+
let pm;
|
|
522
|
+
const phaseDirEntries = (() => {
|
|
523
|
+
try {
|
|
524
|
+
return node_fs_1.default
|
|
525
|
+
.readdirSync(phasesDir, { withFileTypes: true })
|
|
526
|
+
.filter((e) => e.isDirectory())
|
|
527
|
+
.map((e) => e.name);
|
|
523
528
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
529
|
+
catch {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
})();
|
|
533
|
+
while ((pm = phasePattern.exec(scopedContent)) !== null) {
|
|
534
|
+
const phaseNum = pm[1];
|
|
535
|
+
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not
|
|
536
|
+
// real phases — they legitimately have no directory and must not block
|
|
537
|
+
// milestone completion. Mirrors the engine-wide sentinel convention
|
|
538
|
+
// (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS,
|
|
539
|
+
// the #1445 /^999/ progress filters). (#1580)
|
|
540
|
+
const major = parseInt(phaseNum, 10);
|
|
541
|
+
if (major === 0 || major === 999)
|
|
542
|
+
continue;
|
|
543
|
+
const normalized = normalizePhaseName(phaseNum);
|
|
544
|
+
// A phase has disk_status: 'no_directory' when no phase directory
|
|
545
|
+
// with a matching token exists on disk. Use the same phaseTokenMatches
|
|
546
|
+
// helper that roadmap.analyze uses to avoid false positives on decimal
|
|
547
|
+
// (2.1) and letter-suffix (12A) phase IDs.
|
|
548
|
+
const hasDirectory = phaseDirEntries.some((d) => phaseTokenMatches(d, normalized));
|
|
549
|
+
if (!hasDirectory) {
|
|
550
|
+
noDirectoryPhases.push(phaseNum);
|
|
527
551
|
}
|
|
528
552
|
}
|
|
553
|
+
if (noDirectoryPhases.length > 0) {
|
|
554
|
+
error(`Cannot mark milestone complete: ROADMAP lists ${noDirectoryPhases.length} unstarted phase(s) ` +
|
|
555
|
+
`(e.g. Phase ${noDirectoryPhases[0]}). Re-run with --force to override.`);
|
|
556
|
+
}
|
|
529
557
|
}
|
|
530
558
|
catch (e) {
|
|
531
559
|
// If the error came from our guard, re-throw it; otherwise skip silently.
|
|
532
560
|
const message = e instanceof Error ? e.message : String(e);
|
|
533
561
|
if (message && message.startsWith('Cannot mark milestone complete:'))
|
|
534
562
|
throw e;
|
|
535
|
-
// Phase scan failed
|
|
563
|
+
// Phase scan failed (e.g. ROADMAP unreadable) — allow completion to proceed.
|
|
536
564
|
}
|
|
537
565
|
}
|
|
538
566
|
// Gather stats from phases (scoped to current milestone only)
|
|
@@ -695,7 +723,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
695
723
|
kind: 'milestoneComplete',
|
|
696
724
|
version,
|
|
697
725
|
nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
|
|
698
|
-
}, { clock: clock_cjs_1.realClock,
|
|
726
|
+
}, { clock: clock_cjs_1.realClock, sourcePath: statePath });
|
|
699
727
|
writeStateMd(statePath, result.content, cwd);
|
|
700
728
|
}
|
|
701
729
|
// Archive phase directories if requested
|
|
@@ -520,6 +520,65 @@ function phaseTokenMatches(dirName, normalized) {
|
|
|
520
520
|
}
|
|
521
521
|
return false;
|
|
522
522
|
}
|
|
523
|
+
// ─── Canonical phase KEY surface (#2562) ─────────────────────────────────────
|
|
524
|
+
//
|
|
525
|
+
// A phase "key" is the padding-, case- and project-code-insensitive identity of
|
|
526
|
+
// a phase, for use as a Map/Set key when two independently-derived phase
|
|
527
|
+
// references (a ROADMAP table cell and a phase directory name, say) must be
|
|
528
|
+
// compared. Promoted here from a local pair in state.cts (#2445) so every
|
|
529
|
+
// consumer derives BOTH sides of a comparison from the SAME function — deriving
|
|
530
|
+
// one side with a bespoke regex is the #2562 defect class (a `01` table cell
|
|
531
|
+
// never matching a `1-slug` directory, silently zeroing a rollup).
|
|
532
|
+
/**
|
|
533
|
+
* Canonical key for an already-extracted phase TOKEN (`"5"`, `"05"`, `"005"`,
|
|
534
|
+
* `"12A"`, `"30.1"`, `"PROJ-05"`). Padding- and case-insensitive: every
|
|
535
|
+
* spelling of a number collapses to one key.
|
|
536
|
+
*
|
|
537
|
+
* Leading zeros are stripped per hyphen-separated segment BEFORE
|
|
538
|
+
* `normalizePhaseName` pads to the 2-digit convention. Padding alone is not a
|
|
539
|
+
* normalisation — `padStart(2)` is a no-op once the input is already ≥2
|
|
540
|
+
* characters, so `5` yielded `05` while `005` stayed `005` and the two never
|
|
541
|
+
* compared equal. The strip is deliberately confined to this key surface:
|
|
542
|
+
* `normalizePhaseName` itself is a RENDERING function whose verbatim treatment
|
|
543
|
+
* of wide IDs (`001.10`) is relied on by plan-ID capture and wave assignment.
|
|
544
|
+
* Arithmetic is avoided (`parseInt` would lose precision on a long digit run).
|
|
545
|
+
*/
|
|
546
|
+
function phaseKeyFromToken(token) {
|
|
547
|
+
const stripped = String(token)
|
|
548
|
+
.split('-')
|
|
549
|
+
.map(segment => segment.replace(/^0+(?=\d)/, ''))
|
|
550
|
+
.join('-');
|
|
551
|
+
return normalizePhaseName(stripped).toUpperCase();
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Canonical key for a phase DIRECTORY name (`"05-schedule-8"` → `"05"`,
|
|
555
|
+
* `"PROJ-5-x"` → `"05"`, `"30.1-follow-up"` → `"30.1"`).
|
|
556
|
+
*/
|
|
557
|
+
function phaseKeyFromDir(dirName) {
|
|
558
|
+
return phaseKeyFromToken(extractPhaseToken(dirName));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Canonical key for a phase referenced in PROSE — a ROADMAP `## Progress` table
|
|
562
|
+
* cell (`"30. Schedule 8 rollout"`, `"**05.1 Follow-up**"`) or a STATE.md
|
|
563
|
+
* `Phase:` value. Markdown emphasis is stripped first so a bolded cell is not
|
|
564
|
+
* mistaken for a non-phase. Returns null when the value does not BEGIN with a
|
|
565
|
+
* phase token (`parsePhaseFromProse` anchoring, #2111).
|
|
566
|
+
*/
|
|
567
|
+
function phaseKeyFromProse(value) {
|
|
568
|
+
if (value == null)
|
|
569
|
+
return null;
|
|
570
|
+
const { phase } = parsePhaseFromProse(String(value).replace(/[*_`~]/g, ''));
|
|
571
|
+
return phase === null ? null : phaseKeyFromToken(phase);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* The PARENT phase key of a sub-phase key (`"30.1"` → `"30"`), or null for a
|
|
575
|
+
* top-level phase. A sub-phase directory inserted mid-milestone frequently has
|
|
576
|
+
* no ROADMAP row of its own and inherits its parent's milestone (#2562).
|
|
577
|
+
*/
|
|
578
|
+
function parentPhaseKey(key) {
|
|
579
|
+
const dot = key.indexOf('.');
|
|
580
|
+
return dot === -1 ? null : key.slice(0, dot);
|
|
581
|
+
}
|
|
523
582
|
// ─── #2121 canonical surface (ADR-2121) ──────────────────────────────────────
|
|
524
583
|
/**
|
|
525
584
|
* Parse a phase identifier from a STATE.md `Phase:` prose field VALUE — the text
|
|
@@ -662,6 +721,10 @@ module.exports = {
|
|
|
662
721
|
comparePhaseNum,
|
|
663
722
|
extractPhaseToken,
|
|
664
723
|
phaseTokenMatches,
|
|
724
|
+
phaseKeyFromToken,
|
|
725
|
+
phaseKeyFromDir,
|
|
726
|
+
phaseKeyFromProse,
|
|
727
|
+
parentPhaseKey,
|
|
665
728
|
parsePhaseFromProse,
|
|
666
729
|
stripConfiguredProjectCodePrefix,
|
|
667
730
|
isForeignPrefixedPhaseQuery,
|
|
@@ -30,7 +30,67 @@ const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPa
|
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
31
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
32
32
|
const { planningDir } = planningWorkspace;
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const frontmatterModule = require("./frontmatter.cjs");
|
|
35
|
+
const { extractFrontmatter } = frontmatterModule;
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const planDependencyGraphModule = require("./plan-dependency-graph.cjs");
|
|
38
|
+
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted } = planDependencyGraphModule;
|
|
39
|
+
/**
|
|
40
|
+
* #2830: parse a plan file's `depends_on` frontmatter. Returns [] — never
|
|
41
|
+
* throws — on a missing/unreadable/malformed plan or absent field, matching
|
|
42
|
+
* this primitive's existing fail-safe posture (a plan directory this
|
|
43
|
+
* primitive can otherwise read must never throw here).
|
|
44
|
+
*/
|
|
45
|
+
function parsePlanDependsOn(phaseDir, planFile) {
|
|
46
|
+
try {
|
|
47
|
+
const planPath = node_path_1.default.join(phaseDir, planFile);
|
|
48
|
+
const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
|
|
49
|
+
const fm = extractFrontmatter(content, planPath);
|
|
50
|
+
const fmDeps = fm['depends_on'];
|
|
51
|
+
if (Array.isArray(fmDeps))
|
|
52
|
+
return fmDeps.map(String);
|
|
53
|
+
if (typeof fmDeps === 'string' && fmDeps.trim() !== '')
|
|
54
|
+
return [fmDeps];
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
33
61
|
// ─── Phase search helpers ─────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* #2855: single source of truth for resolving and enumerating a project's
|
|
64
|
+
* (or, when a workstream is active, that workstream's OWN) archived-milestone
|
|
65
|
+
* directories — `<planningDir(cwd)>/milestones/vX.Y-phases/`. Both
|
|
66
|
+
* `findPhaseInternal`'s archive fallback and `getArchivedPhaseDirs` used to
|
|
67
|
+
* carry independent copies of this resolve-then-enumerate logic, which is
|
|
68
|
+
* exactly the shape that let the original #2855 bug (hardcoded root path)
|
|
69
|
+
* exist in one copy and not the other. Sharing this seam means a future
|
|
70
|
+
* change to how the archive tree is located only needs to happen once.
|
|
71
|
+
* Most-recent-milestone-first order (reverse-sorted directory names).
|
|
72
|
+
* Never throws: an absent/unreadable milestones/ dir yields [].
|
|
73
|
+
*/
|
|
74
|
+
function listArchiveVersionDirs(cwd) {
|
|
75
|
+
const milestonesDir = node_path_1.default.join(planningDir(cwd), 'milestones');
|
|
76
|
+
if (!node_fs_1.default.existsSync(milestonesDir))
|
|
77
|
+
return [];
|
|
78
|
+
try {
|
|
79
|
+
const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
|
|
80
|
+
return milestoneEntries
|
|
81
|
+
.filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
|
|
82
|
+
.map(e => e.name)
|
|
83
|
+
.sort()
|
|
84
|
+
.reverse()
|
|
85
|
+
.map(archiveName => ({
|
|
86
|
+
version: archiveName.match(/^(v[\d.]+)-phases$/)[1],
|
|
87
|
+
archivePath: node_path_1.default.join(milestonesDir, archiveName),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
34
94
|
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
35
95
|
try {
|
|
36
96
|
const dirs = readSubdirectories(baseDir, true);
|
|
@@ -55,6 +115,9 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
55
115
|
has_verification: false,
|
|
56
116
|
has_reviews: false,
|
|
57
117
|
ambiguous_matches: matches,
|
|
118
|
+
halted_plans: [],
|
|
119
|
+
blocked_by: {},
|
|
120
|
+
runnable_plans: [],
|
|
58
121
|
};
|
|
59
122
|
}
|
|
60
123
|
const match = matches[0];
|
|
@@ -76,6 +139,50 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
76
139
|
const canonical = extractCanonicalPlanId(p);
|
|
77
140
|
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
|
78
141
|
});
|
|
142
|
+
// #2830: reverse lookup from a completed plan's id (exact or canonical) to
|
|
143
|
+
// its actual summary filename. Shared builder (also used by phase.cts's
|
|
144
|
+
// cmdPhasePlanIndex) so the two can never disagree about which summary
|
|
145
|
+
// belongs to which plan.
|
|
146
|
+
const summaryFileByPlanId = buildSummaryFileIndex(summaries, extractCanonicalPlanId);
|
|
147
|
+
// #2830: this primitive previously never parsed depends_on at all — see
|
|
148
|
+
// src/plan-dependency-graph.cts's file header. Build the same
|
|
149
|
+
// PlanHaltNode[] shape phase.cts's cmdPhasePlanIndex builds (id resolution
|
|
150
|
+
// mirrors its planMap/canonicalToId pattern) and hand it to the ONE
|
|
151
|
+
// shared halt-propagation traversal so this reader and the wave-grouping
|
|
152
|
+
// reader can never diverge on the halt rule again.
|
|
153
|
+
const planIds = plans.map(p => p.replace('-PLAN.md', '').replace('PLAN.md', ''));
|
|
154
|
+
const planIdByLower = new Map(planIds.map(id => [id.toLowerCase(), id]));
|
|
155
|
+
const canonicalToPlanId = new Map(plans.map((p, i) => [extractCanonicalPlanId(p).toLowerCase(), planIds[i]]));
|
|
156
|
+
const haltNodes = plans.map((p, i) => {
|
|
157
|
+
const planId = planIds[i];
|
|
158
|
+
const canonical = extractCanonicalPlanId(p);
|
|
159
|
+
const summaryFile = summaryFileByPlanId.get(planId) ?? summaryFileByPlanId.get(canonical);
|
|
160
|
+
const halted = summaryFile !== undefined && isSummaryFileHalted(node_path_1.default.join(phaseDir, summaryFile));
|
|
161
|
+
const resolvedDependsOn = parsePlanDependsOn(phaseDir, p)
|
|
162
|
+
.map((dep) => {
|
|
163
|
+
const lower = dep.toLowerCase();
|
|
164
|
+
return planIdByLower.get(lower) ?? canonicalToPlanId.get(lower) ?? null;
|
|
165
|
+
})
|
|
166
|
+
.filter((id) => id !== null);
|
|
167
|
+
return { id: planId, resolvedDependsOn, halted };
|
|
168
|
+
});
|
|
169
|
+
const { blockedBy } = computeHaltPropagation(haltNodes);
|
|
170
|
+
const haltedPlans = plans.filter((_, i) => haltNodes[i].halted);
|
|
171
|
+
const incompletePlanSet = new Set(incompletePlans);
|
|
172
|
+
const blockedByFiles = {};
|
|
173
|
+
const runnablePlans = [];
|
|
174
|
+
for (let i = 0; i < plans.length; i++) {
|
|
175
|
+
const p = plans[i];
|
|
176
|
+
if (!incompletePlanSet.has(p))
|
|
177
|
+
continue;
|
|
178
|
+
const causes = blockedBy.get(planIds[i]) ?? [];
|
|
179
|
+
if (causes.length > 0) {
|
|
180
|
+
blockedByFiles[p] = causes;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
runnablePlans.push(p);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
79
186
|
return {
|
|
80
187
|
found: true,
|
|
81
188
|
directory: toPosixPath(node_path_1.default.join(relBase, match)),
|
|
@@ -89,6 +196,9 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
89
196
|
has_context: hasContext,
|
|
90
197
|
has_verification: hasVerification,
|
|
91
198
|
has_reviews: hasReviews,
|
|
199
|
+
halted_plans: haltedPlans,
|
|
200
|
+
blocked_by: blockedByFiles,
|
|
201
|
+
runnable_plans: runnablePlans,
|
|
92
202
|
};
|
|
93
203
|
}
|
|
94
204
|
catch {
|
|
@@ -104,59 +214,42 @@ function findPhaseInternal(cwd, phase) {
|
|
|
104
214
|
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
|
|
105
215
|
if (current)
|
|
106
216
|
return current;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
123
|
-
if (result) {
|
|
124
|
-
result.archived = version;
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
217
|
+
// #2855: scope the archived-milestone fallback to the SAME workstream as the
|
|
218
|
+
// active-phase search above (planningDir(cwd) resolves GSD_WORKSTREAM/GSD_PROJECT
|
|
219
|
+
// the identical way both places), not the hardcoded project-root tree. Archived
|
|
220
|
+
// phases genuinely live under a workstream's own `.planning/workstreams/<ws>/
|
|
221
|
+
// milestones/` — that is where archivePhaseDirectories (milestone.cts) writes
|
|
222
|
+
// them via the same planningDir(cwd) resolution. Hardcoding root here let a
|
|
223
|
+
// pending workstream phase resolve to an unrelated workstream's (or a flat-mode
|
|
224
|
+
// project's) archived phase that merely shares a phase number. Shared with
|
|
225
|
+
// getArchivedPhaseDirs via listArchiveVersionDirs (see its doc comment).
|
|
226
|
+
for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
|
|
227
|
+
const relBase = toPosixPath(node_path_1.default.relative(cwd, archivePath));
|
|
228
|
+
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
229
|
+
if (result) {
|
|
230
|
+
result.archived = version;
|
|
231
|
+
return result;
|
|
127
232
|
}
|
|
128
233
|
}
|
|
129
|
-
catch { /* intentionally empty */ }
|
|
130
234
|
return null;
|
|
131
235
|
}
|
|
132
236
|
function getArchivedPhaseDirs(cwd) {
|
|
133
|
-
|
|
237
|
+
// #2855: same workstream-scoped resolution as findPhaseInternal above, via
|
|
238
|
+
// the shared listArchiveVersionDirs helper. `phase.list --include-archived`
|
|
239
|
+
// (the primary non-init consumer) must not leak a different workstream's
|
|
240
|
+
// archive either.
|
|
134
241
|
const results = [];
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
for (const archiveName of phaseDirs) {
|
|
145
|
-
const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
146
|
-
const version = versionMatch[1];
|
|
147
|
-
const archivePath = node_path_1.default.join(milestonesDir, archiveName);
|
|
148
|
-
const dirs = readSubdirectories(archivePath, true);
|
|
149
|
-
for (const dir of dirs) {
|
|
150
|
-
results.push({
|
|
151
|
-
name: dir,
|
|
152
|
-
milestone: version,
|
|
153
|
-
basePath: node_path_1.default.join('.planning', 'milestones', archiveName),
|
|
154
|
-
fullPath: node_path_1.default.join(archivePath, dir),
|
|
155
|
-
});
|
|
156
|
-
}
|
|
242
|
+
for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
|
|
243
|
+
const dirs = readSubdirectories(archivePath, true);
|
|
244
|
+
for (const dir of dirs) {
|
|
245
|
+
results.push({
|
|
246
|
+
name: dir,
|
|
247
|
+
milestone: version,
|
|
248
|
+
basePath: toPosixPath(node_path_1.default.relative(cwd, archivePath)),
|
|
249
|
+
fullPath: node_path_1.default.join(archivePath, dir),
|
|
250
|
+
});
|
|
157
251
|
}
|
|
158
252
|
}
|
|
159
|
-
catch { /* intentionally empty */ }
|
|
160
253
|
return results;
|
|
161
254
|
}
|
|
162
255
|
module.exports = {
|