@opengsd/gsd-core 1.7.0-rc.6 → 1.8.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 +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +31 -3
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1157 -84
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +31 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +3 -2
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +1866 -2434
- 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 +341 -49
- package/gsd-core/bin/lib/audit.cjs +7 -6
- 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-registry.cjs +157 -88
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +129 -26
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +129 -13
- package/gsd-core/bin/lib/config-loader.cjs +20 -4
- package/gsd-core/bin/lib/config.cjs +81 -18
- package/gsd-core/bin/lib/core-utils.cjs +14 -3
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/init.cjs +138 -60
- package/gsd-core/bin/lib/install-engine.cjs +301 -25
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -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 +45 -6
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +449 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/milestone.cjs +463 -43
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +636 -72
- package/gsd-core/bin/lib/plan-scan.cjs +73 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +225 -17
- package/gsd-core/bin/lib/roadmap.cjs +113 -52
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +14 -7
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +41 -17
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +1 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
- package/gsd-core/bin/lib/smart-entry.cjs +73 -7
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +122 -46
- package/gsd-core/bin/lib/state.cjs +456 -137
- package/gsd-core/bin/lib/surface.cjs +53 -11
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +474 -13
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +224 -25
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -2
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +13 -1
- package/gsd-core/references/common-bug-patterns.md +13 -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/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/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +4 -2
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +27 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -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 +30 -37
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +33 -5
- package/gsd-core/workflows/quick.md +21 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +123 -68
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +4 -1
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +252 -17
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +252 -17
- package/package.json +8 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +31 -1
- 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 +3 -2
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -10,7 +10,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
11
|
};
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
|
|
13
|
+
exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.ADAPTIVE_TIER_VALUES = exports.VALID_TIERS = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
|
|
14
14
|
exports.nextTier = nextTier;
|
|
15
15
|
exports.formatAgentToModelMapAsTable = formatAgentToModelMapAsTable;
|
|
16
16
|
exports.getAgentToModelMapForProfile = getAgentToModelMapForProfile;
|
|
@@ -23,11 +23,19 @@ const _require = require;
|
|
|
23
23
|
// works in every layout:
|
|
24
24
|
//
|
|
25
25
|
// 1. Co-located install path — gsd-core/bin/shared/model-catalog.json
|
|
26
|
-
// 2.
|
|
27
|
-
//
|
|
26
|
+
// 2. GSD_MODEL_CATALOG env override
|
|
27
|
+
//
|
|
28
|
+
// A third candidate — `sdk/shared/model-catalog.json`, three levels up — used to
|
|
29
|
+
// sit between them. It was the legacy source-repo path kept as a fallback by the
|
|
30
|
+
// #3288 fix, whose contract was "check the co-located path FIRST, before the
|
|
31
|
+
// legacy source-repo path". ADR-0174 then retired the `@opengsd/gsd-sdk` package
|
|
32
|
+
// boundary and deleted the `sdk/` tree outright, so that candidate can no longer
|
|
33
|
+
// resolve in any layout: in a source repo there is no `sdk/`, and in an install
|
|
34
|
+
// layout it points at `~/.claude/sdk/shared/`, which the installer never writes
|
|
35
|
+
// (the original #3288 bug). It is removed rather than left as dead weight that
|
|
36
|
+
// implies a package boundary this repo no longer has.
|
|
28
37
|
const _catalogCandidates = [
|
|
29
38
|
node_path_1.default.resolve(__dirname, '..', 'shared', 'model-catalog.json'),
|
|
30
|
-
node_path_1.default.resolve(__dirname, '..', '..', '..', 'sdk', 'shared', 'model-catalog.json'),
|
|
31
39
|
...(process.env['GSD_MODEL_CATALOG'] ? [node_path_1.default.resolve(process.env['GSD_MODEL_CATALOG'])] : []),
|
|
32
40
|
];
|
|
33
41
|
let catalog = null;
|
|
@@ -54,6 +62,13 @@ exports.catalog = _catalog;
|
|
|
54
62
|
exports.VALID_PROFILES = [..._catalog.profiles];
|
|
55
63
|
exports.VALID_PHASE_TYPES = new Set(_catalog.phaseTypes);
|
|
56
64
|
exports.VALID_AGENT_TIERS = new Set(Object.keys(_catalog.adaptiveTierMap));
|
|
65
|
+
// Catalog-derived so this can never drift from the resolver's tier gate:
|
|
66
|
+
// Object.values(adaptiveTierMap) === ['opus', 'sonnet', 'haiku'] today, plus 'inherit'.
|
|
67
|
+
exports.VALID_TIERS = new Set([...Object.values(_catalog.adaptiveTierMap), 'inherit']);
|
|
68
|
+
// Same catalog-derived tier values as VALID_TIERS but WITHOUT 'inherit' — used
|
|
69
|
+
// by config-loader's runtime-override validation (model_profile_overrides /
|
|
70
|
+
// model_policy.runtime_tiers), which does not accept 'inherit' as a tier.
|
|
71
|
+
exports.ADAPTIVE_TIER_VALUES = new Set(Object.values(_catalog.adaptiveTierMap));
|
|
57
72
|
exports.MODEL_PROFILES = Object.fromEntries(Object.entries(_catalog.agents).map(([agent, meta]) => [agent, {
|
|
58
73
|
quality: meta.golden,
|
|
59
74
|
balanced: meta.balanced,
|
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
* epic #1267; callers import resolvers from model-resolver.cjs directly.
|
|
12
12
|
*
|
|
13
13
|
* Dependencies (leaf modules only):
|
|
14
|
-
* - node:fs / node:path (
|
|
14
|
+
* - node:fs / node:path (read the per-install .gsd-runtime marker + project config for the #2297 omit gate)
|
|
15
|
+
* - ./runtime-name-policy.cjs (resolveRuntimeNameFromCandidates — canonicalize the active runtime)
|
|
16
|
+
* - ./planning-workspace.cjs (planningDir — workstream/project-aware project-config path)
|
|
15
17
|
* - ./config-loader.cjs (loadConfig)
|
|
16
18
|
* - ./configuration.cjs (CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS)
|
|
17
19
|
* - ./model-profiles.cjs (MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier)
|
|
18
|
-
* - ./model-catalog.cjs (MODEL_ALIAS_MAP, RUNTIME_PROFILE_MAP, PROVIDER_PRESETS)
|
|
20
|
+
* - ./model-catalog.cjs (MODEL_ALIAS_MAP, RUNTIME_PROFILE_MAP, PROVIDER_PRESETS, VALID_TIERS)
|
|
19
21
|
*/
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
20
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
26
|
const configLoaderModule = require("./config-loader.cjs");
|
|
22
27
|
const { loadConfig } = configLoaderModule;
|
|
@@ -26,6 +31,92 @@ const configuration_cjs_1 = require("./configuration.cjs");
|
|
|
26
31
|
const modelProfiles = require("./model-profiles.cjs");
|
|
27
32
|
const { MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier } = modelProfiles;
|
|
28
33
|
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
34
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
35
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
36
|
+
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
+
const planningWorkspaceMod = require("./planning-workspace.cjs");
|
|
39
|
+
const { planningDir } = planningWorkspaceMod;
|
|
40
|
+
// ─── #2297: per-install runtime identity for the resolve_model_ids:"omit" gate ─
|
|
41
|
+
//
|
|
42
|
+
// The installer writes `resolve_model_ids:"omit"` into the SHARED
|
|
43
|
+
// ~/.gsd/defaults.json for every runtime that lacks native model aliases (#1156).
|
|
44
|
+
// Because that file is machine-wide, a non-Claude install would otherwise poison
|
|
45
|
+
// a Claude no-project resolution into returning '' — silently defeating Claude's
|
|
46
|
+
// adaptive tier aliases. The "omit" must therefore apply only when a runtime that
|
|
47
|
+
// genuinely lacks native aliases is the one resolving.
|
|
48
|
+
//
|
|
49
|
+
// In a no-project session there is no `.planning/config.json` (so config.runtime
|
|
50
|
+
// is null) and GSD_RUNTIME is not exported by gsd-core, so the only reliable
|
|
51
|
+
// current-runtime signal is the per-install marker the installer co-locates next
|
|
52
|
+
// to VERSION at <install>/gsd-core/.gsd-runtime (this file's dir is
|
|
53
|
+
// <install>/gsd-core/bin/lib). Precedence for the gate: project config.runtime →
|
|
54
|
+
// GSD_RUNTIME env (manual/CI override + test seam) → install marker → 'claude'.
|
|
55
|
+
//
|
|
56
|
+
// `claude` is currently the ONLY runtime with nativeModelAliases:true; a
|
|
57
|
+
// registry-parity test guards this set so a future alias-capable runtime fails
|
|
58
|
+
// loudly here instead of silently omitting.
|
|
59
|
+
const RUNTIMES_WITH_NATIVE_ALIASES = new Set(['claude']);
|
|
60
|
+
let _installMarkerCache;
|
|
61
|
+
function readInstallRuntimeMarker() {
|
|
62
|
+
if (_installMarkerCache !== undefined)
|
|
63
|
+
return _installMarkerCache;
|
|
64
|
+
try {
|
|
65
|
+
const markerPath = node_path_1.default.join(__dirname, '..', '..', '.gsd-runtime');
|
|
66
|
+
const raw = node_fs_1.default.readFileSync(markerPath, 'utf8').trim();
|
|
67
|
+
_installMarkerCache = raw || null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// No marker: dev/source tree, or an install predating #2297. Fall through to
|
|
71
|
+
// the 'claude' default (keeps tier aliases — never worse than the bug).
|
|
72
|
+
_installMarkerCache = null;
|
|
73
|
+
}
|
|
74
|
+
return _installMarkerCache;
|
|
75
|
+
}
|
|
76
|
+
// Test seams for the install-marker rung (the dev/source tree has no marker, so
|
|
77
|
+
// the file read always bottoms out at 'claude' — these let tests exercise the
|
|
78
|
+
// third precedence rung and reset the module-level cache between cases).
|
|
79
|
+
function _setInstallRuntimeMarkerForTests(value) {
|
|
80
|
+
_installMarkerCache = value;
|
|
81
|
+
}
|
|
82
|
+
function _resetInstallRuntimeMarkerCacheForTests() {
|
|
83
|
+
_installMarkerCache = undefined;
|
|
84
|
+
}
|
|
85
|
+
// The runtime whose install is actually resolving, canonicalized so an alias or
|
|
86
|
+
// case variant (e.g. "claude-code"/"Claude") cannot defeat the native-alias
|
|
87
|
+
// check below (#2297 review). Precedence mirrors resolveRuntime()
|
|
88
|
+
// (runtime-slash.cts): GSD_RUNTIME env → project config.runtime → per-install
|
|
89
|
+
// .gsd-runtime marker → 'claude'.
|
|
90
|
+
function resolveActiveRuntime(config) {
|
|
91
|
+
return (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime'], readInstallRuntimeMarker()) || 'claude';
|
|
92
|
+
}
|
|
93
|
+
// Did the PROJECT's own config (root `.planning/config.json` or the active
|
|
94
|
+
// workstream/project override) explicitly set resolve_model_ids to "omit"?
|
|
95
|
+
// Project config takes precedence over the shared ~/.gsd/defaults.json (#2297
|
|
96
|
+
// out-of-scope guard + #2517 finding #4): an explicit project "omit" is honored
|
|
97
|
+
// regardless of runtime, whereas an "omit" that came only from the global
|
|
98
|
+
// defaults is ignored by native-alias runtimes. Workstream/project-scope aware
|
|
99
|
+
// via planningDir (mirrors loadConfig's precedence: workstream value wins over
|
|
100
|
+
// root); a plain read avoids loadConfig's normalization side effects.
|
|
101
|
+
function projectExplicitlySetsOmit(cwd) {
|
|
102
|
+
const wsDir = planningDir(cwd);
|
|
103
|
+
const rootDir = node_path_1.default.join(cwd, '.planning');
|
|
104
|
+
const layers = wsDir === rootDir ? [rootDir] : [wsDir, rootDir]; // workstream > root
|
|
105
|
+
for (const dir of layers) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, 'config.json'), 'utf8'));
|
|
108
|
+
const value = parsed?.['resolve_model_ids'];
|
|
109
|
+
// First layer that sets the key wins (matches loadConfig's deep-merge
|
|
110
|
+
// precedence). A layer that omits the key falls through to the next.
|
|
111
|
+
if (value !== undefined)
|
|
112
|
+
return value === 'omit';
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Absent/unreadable layer — try the next.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
29
120
|
/**
|
|
30
121
|
* #2517 — Resolve the runtime-aware tier entry for (runtime, tier).
|
|
31
122
|
*/
|
|
@@ -209,8 +300,7 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
209
300
|
const phaseTypeTier = (phaseType && configModels && typeof configModels === 'object')
|
|
210
301
|
? configModels[phaseType]
|
|
211
302
|
: undefined;
|
|
212
|
-
const
|
|
213
|
-
const tier = (phaseTypeTier && VALID_TIERS.has(phaseTypeTier))
|
|
303
|
+
const tier = (phaseTypeTier && model_catalog_cjs_1.VALID_TIERS.has(phaseTypeTier))
|
|
214
304
|
? phaseTypeTier
|
|
215
305
|
: (profile === 'inherit'
|
|
216
306
|
? 'inherit'
|
|
@@ -248,8 +338,18 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
248
338
|
if (entry?.model)
|
|
249
339
|
return entry.model;
|
|
250
340
|
}
|
|
251
|
-
// 4. resolve_model_ids: "omit"
|
|
252
|
-
|
|
341
|
+
// 4. resolve_model_ids: "omit" — runtime-aware (#2297). Honor "omit" when the
|
|
342
|
+
// PROJECT explicitly set it (user intent — project config wins, #2517 finding
|
|
343
|
+
// #4) OR when the active runtime genuinely lacks native model aliases. Only a
|
|
344
|
+
// native-alias runtime (Claude) ignores an "omit" that came solely from the
|
|
345
|
+
// SHARED ~/.gsd/defaults.json — the #2297 poisoning fix — and falls through to
|
|
346
|
+
// its tier aliases below. Active runtime: GSD_RUNTIME → config.runtime → the
|
|
347
|
+
// per-install .gsd-runtime marker → 'claude' (canonicalized).
|
|
348
|
+
// NOTE: a non-Claude runtime that HAS a populated runtime-tier map already
|
|
349
|
+
// returned its own model id at step 3 above, before this gate — for those the
|
|
350
|
+
// explicit-project-omit honoring here is moot (step 3 wins, by #2517 design).
|
|
351
|
+
if (config['resolve_model_ids'] === 'omit'
|
|
352
|
+
&& (projectExplicitlySetsOmit(cwd) || !RUNTIMES_WITH_NATIVE_ALIASES.has(resolveActiveRuntime(config)))) {
|
|
253
353
|
return '';
|
|
254
354
|
}
|
|
255
355
|
// 5. Profile lookup (Claude-native default).
|
|
@@ -262,7 +362,11 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
262
362
|
if (tier === 'inherit')
|
|
263
363
|
return 'inherit';
|
|
264
364
|
const alias = tier;
|
|
265
|
-
|
|
365
|
+
// Only the explicit `true` opt-in materializes full model IDs (#1569). Guard
|
|
366
|
+
// against the loose-truthy check catching a "omit" that a native-alias runtime
|
|
367
|
+
// ignored above (#2297): "omit" must fall through to the tier ALIAS here, not
|
|
368
|
+
// be materialized into a full ID Claude's Agent tool cannot spawn.
|
|
369
|
+
if (config['resolve_model_ids'] === true) {
|
|
266
370
|
return model_catalog_cjs_1.MODEL_ALIAS_MAP[alias] || alias;
|
|
267
371
|
}
|
|
268
372
|
return alias;
|
|
@@ -353,6 +457,80 @@ function resolveModelForTier(cwd, agentType, attempt) {
|
|
|
353
457
|
}
|
|
354
458
|
return alias;
|
|
355
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Keep only usable model ids: non-empty strings. A malformed config can put
|
|
462
|
+
* anything in here (nulls, numbers, blank strings), and a blank model id would
|
|
463
|
+
* resolve to an unusable agent invocation rather than failing visibly. Invalid
|
|
464
|
+
* entries are dropped and the surviving order is preserved, so the ladder stays
|
|
465
|
+
* predictable (ADR 227 — validate shape, not just type).
|
|
466
|
+
*/
|
|
467
|
+
function sanitizeProviderEscalation(raw) {
|
|
468
|
+
if (!Array.isArray(raw))
|
|
469
|
+
return [];
|
|
470
|
+
return raw.filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* #2296 — Resolve the model for one attempt of the PROVIDER escalation ladder.
|
|
474
|
+
*
|
|
475
|
+
* The tier ladder (`resolveModelForTier`) escalates within one provider's
|
|
476
|
+
* `tier_models`, which does not help when that provider is the thing that is
|
|
477
|
+
* throttled. This walks `dynamic_routing.provider_escalation` instead: an
|
|
478
|
+
* ordered list of alternative model ids, capped by
|
|
479
|
+
* `min(max_escalations, list length)`.
|
|
480
|
+
*
|
|
481
|
+
* `applicable` is the caller's policy decision (only a quota-exceeded
|
|
482
|
+
* classification should consult this ladder). It is a parameter rather than a
|
|
483
|
+
* class check here so this module keeps depending only on leaf modules, per
|
|
484
|
+
* CONTEXT.md's Model Resolution module contract.
|
|
485
|
+
*
|
|
486
|
+
* Attempt 0 — and every non-applicable call — stays on the source model.
|
|
487
|
+
* `exhausted` reports that the ladder is spent so the caller can fail loudly
|
|
488
|
+
* naming every model it tried.
|
|
489
|
+
*/
|
|
490
|
+
function resolveProviderEscalation(cwd, agentType, attempt, applicable) {
|
|
491
|
+
// The model that would be used with no provider escalation at all.
|
|
492
|
+
const from = resolveModelForTier(cwd, agentType, 0);
|
|
493
|
+
const stay = (exhausted = false) => ({
|
|
494
|
+
from,
|
|
495
|
+
to: from,
|
|
496
|
+
escalated: false,
|
|
497
|
+
exhausted,
|
|
498
|
+
attempted: [from],
|
|
499
|
+
index: 0,
|
|
500
|
+
});
|
|
501
|
+
if (!applicable)
|
|
502
|
+
return stay();
|
|
503
|
+
const config = loadConfig(cwd);
|
|
504
|
+
const dr = config['dynamic_routing'];
|
|
505
|
+
if (!dr || typeof dr !== 'object' || dr['enabled'] !== true)
|
|
506
|
+
return stay();
|
|
507
|
+
if (dr['escalate_on_failure'] === false)
|
|
508
|
+
return stay();
|
|
509
|
+
const list = sanitizeProviderEscalation(dr['provider_escalation']);
|
|
510
|
+
if (list.length === 0)
|
|
511
|
+
return stay();
|
|
512
|
+
// Same default and same validity rule as the tier ladder above — a negative or
|
|
513
|
+
// non-integer max_escalations is invalid config, not a request for zero.
|
|
514
|
+
const maxEscalations = Number.isInteger(dr['max_escalations']) && dr['max_escalations'] >= 0
|
|
515
|
+
? dr['max_escalations']
|
|
516
|
+
: 1;
|
|
517
|
+
const cap = Math.min(maxEscalations, list.length);
|
|
518
|
+
// An explicit cap of 0 means the ladder exists but is spent before it starts.
|
|
519
|
+
if (cap === 0)
|
|
520
|
+
return stay(true);
|
|
521
|
+
const attemptN = Number.isInteger(attempt) && attempt > 0 ? attempt : 0;
|
|
522
|
+
if (attemptN === 0)
|
|
523
|
+
return stay();
|
|
524
|
+
const index = Math.min(attemptN, cap);
|
|
525
|
+
return {
|
|
526
|
+
from,
|
|
527
|
+
to: list[index - 1],
|
|
528
|
+
escalated: true,
|
|
529
|
+
exhausted: attemptN > cap,
|
|
530
|
+
attempted: [from, ...list.slice(0, index)],
|
|
531
|
+
index,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
356
534
|
// ─── #443 — Unified effort + fast_mode resolvers ─────────────────────────────
|
|
357
535
|
const VALID_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
358
536
|
const EFFORT_SET = new Set(VALID_EFFORTS);
|
|
@@ -515,14 +693,18 @@ function resolveEffortForTier(cwd, agentType, attempt) {
|
|
|
515
693
|
}
|
|
516
694
|
module.exports = {
|
|
517
695
|
resolveTierEntry,
|
|
696
|
+
CLAUDE_AGENT_ALIASES,
|
|
518
697
|
resolveModelPolicy,
|
|
519
698
|
resolveModelInternal,
|
|
520
699
|
_resetModelPolicyWarningCacheForTests,
|
|
521
700
|
_resetModelOverrideWarningCacheForTests,
|
|
701
|
+
_setInstallRuntimeMarkerForTests,
|
|
702
|
+
_resetInstallRuntimeMarkerCacheForTests,
|
|
522
703
|
VALID_GRANULARITIES,
|
|
523
704
|
resolveGranularityInternal,
|
|
524
705
|
assertValidGranularityOverride,
|
|
525
706
|
resolveModelForTier,
|
|
707
|
+
resolveProviderEscalation,
|
|
526
708
|
VALID_EFFORTS,
|
|
527
709
|
EFFORT_SET,
|
|
528
710
|
nextEffort,
|
|
@@ -269,7 +269,9 @@ function buildOnboardProjection(cwd, options) {
|
|
|
269
269
|
projectExists,
|
|
270
270
|
mapReadiness: mapReadinessValue,
|
|
271
271
|
onboardingSummaryExists,
|
|
272
|
-
|
|
272
|
+
// #2376: absolute (anchored on cwd/project_root), not orchestrator-cwd-relative —
|
|
273
|
+
// a spawned subagent's own cwd may differ from the orchestrator's.
|
|
274
|
+
onboardingSummaryPath: toPosixPath(onboardingSummaryPath),
|
|
273
275
|
hasPlanningArtifacts,
|
|
274
276
|
missingPlanningFiles,
|
|
275
277
|
handoffCommands,
|
|
@@ -289,13 +291,14 @@ function buildOnboardProjection(cwd, options) {
|
|
|
289
291
|
doc_candidate_count: docCandidates.length,
|
|
290
292
|
doc_candidates: docCandidates,
|
|
291
293
|
onboarding_summary_exists: onboardingSummaryExists,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
294
|
+
// #2376: absolute — see comment on onboardingSummaryPath above.
|
|
295
|
+
onboarding_summary_path: toPosixPath(onboardingSummaryPath),
|
|
296
|
+
project_path: toPosixPath(node_fs_1.default.existsSync(projectRootPath) ? projectRootPath : projectScopedPath),
|
|
297
|
+
requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
|
|
298
|
+
roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
299
|
+
state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
300
|
+
codebase_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'codebase')),
|
|
301
|
+
onboarding_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'onboarding')),
|
|
299
302
|
};
|
|
300
303
|
}
|
|
301
304
|
module.exports = {
|
|
@@ -137,7 +137,32 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
137
137
|
return { ok: true, data: null };
|
|
138
138
|
},
|
|
139
139
|
complete: (_ctx) => {
|
|
140
|
-
phase
|
|
140
|
+
// #2201: accept --phase N as well as the positional form (the state
|
|
141
|
+
// family already accepts --phase). An unrecognized flag is a usage
|
|
142
|
+
// error, not "Phase --phase not found".
|
|
143
|
+
let phaseNum = null;
|
|
144
|
+
for (let i = 2; i < args.length; i++) {
|
|
145
|
+
if (args[i] === '--phase') {
|
|
146
|
+
phaseNum = args[++i];
|
|
147
|
+
if (!phaseNum || phaseNum.startsWith('--'))
|
|
148
|
+
return makeInvalidArgs('--phase', '--phase requires a value');
|
|
149
|
+
}
|
|
150
|
+
else if (args[i].startsWith('--phase=')) {
|
|
151
|
+
phaseNum = args[i].slice(8);
|
|
152
|
+
}
|
|
153
|
+
else if (args[i] === '--raw') {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
else if (args[i].startsWith('--')) {
|
|
157
|
+
return makeInvalidArgs(args[i], `phase complete does not support ${args[i]}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
phaseNum = args[i];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!phaseNum)
|
|
164
|
+
return makeInvalidArgs('--phase', 'phase number required (positional or --phase N)');
|
|
165
|
+
phase.cmdPhaseComplete(cwd, phaseNum, raw);
|
|
141
166
|
return { ok: true, data: null };
|
|
142
167
|
},
|
|
143
168
|
'uat-passed': (_ctx) => {
|
|
@@ -162,7 +187,30 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
162
187
|
},
|
|
163
188
|
// #1437 — list plan files for a phase
|
|
164
189
|
'list-plans': (_ctx) => {
|
|
165
|
-
phase
|
|
190
|
+
// #2201: accept --phase N as well as positional.
|
|
191
|
+
let phaseNum = null;
|
|
192
|
+
for (let i = 2; i < args.length; i++) {
|
|
193
|
+
if (args[i] === '--phase') {
|
|
194
|
+
phaseNum = args[++i];
|
|
195
|
+
if (!phaseNum || phaseNum.startsWith('--'))
|
|
196
|
+
return makeInvalidArgs('--phase', '--phase requires a value');
|
|
197
|
+
}
|
|
198
|
+
else if (args[i].startsWith('--phase=')) {
|
|
199
|
+
phaseNum = args[i].slice(8);
|
|
200
|
+
}
|
|
201
|
+
else if (args[i] === '--raw') {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
else if (args[i].startsWith('--')) {
|
|
205
|
+
return makeInvalidArgs(args[i], `phase list-plans does not support ${args[i]}`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
phaseNum = args[i];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!phaseNum)
|
|
212
|
+
return makeInvalidArgs('--phase', 'phase number required (positional or --phase N)');
|
|
213
|
+
phase.cmdPhaseListPlans(cwd, phaseNum, raw);
|
|
166
214
|
return { ok: true, data: null };
|
|
167
215
|
},
|
|
168
216
|
},
|
|
@@ -48,6 +48,24 @@ const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]{0,200}\\))?';
|
|
|
48
48
|
// (scripts/lint-phase-id-drift.cjs) fails CI if a literal re-derivation is
|
|
49
49
|
// introduced outside this module without a `// phase-id-owner:` justification.
|
|
50
50
|
const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
|
|
51
|
+
// #2232: the canonical CONTINUATION-segment grammar — a dash-separated segment
|
|
52
|
+
// that extends a phase token (a zero-padded sub-phase or plan number, e.g. the
|
|
53
|
+
// "01" in "02-01-setup"). getPhaseDirFromPhaseId writes these zero-padded to
|
|
54
|
+
// exactly 2 digits, so the digit RUN of a genuine continuation is exactly 2:
|
|
55
|
+
// #2043's `\d{2,}` (2-or-more) over-collected a slug word that merely leads
|
|
56
|
+
// with ≥2 digits (a year: "14-2026-photos-…" yielded token "14-2026", so every
|
|
57
|
+
// phase-locating verb reported the phase as missing). The `(?!\d)` guard caps
|
|
58
|
+
// the run at 2 without anchoring what may follow, so call sites keep their own
|
|
59
|
+
// trailing grammar (letter suffixes, dotted sub-phases, segment boundaries).
|
|
60
|
+
// POLICY (locked by boundary tests): sub-phase/plan numbers ≥100 are out of the
|
|
61
|
+
// dir-token grammar — the LEADING phase number stays unbounded (`\d+`), only
|
|
62
|
+
// continuation segments are width-capped. Shared from here so the five #2043
|
|
63
|
+
// call sites cannot drift independently (see scripts/lint-phase-id-drift.cjs).
|
|
64
|
+
const PHASE_CONTINUATION_SEGMENT_SOURCE = '\\d{2}(?!\\d)';
|
|
65
|
+
const PHASE_CONTINUATION_SEGMENT_PREFIX_RE = new RegExp(`^${PHASE_CONTINUATION_SEGMENT_SOURCE}`);
|
|
66
|
+
function isPhaseContinuationSegment(seg) {
|
|
67
|
+
return PHASE_CONTINUATION_SEGMENT_PREFIX_RE.test(seg);
|
|
68
|
+
}
|
|
51
69
|
function stripProjectCodePrefix(value, caseInsensitive = true) {
|
|
52
70
|
const input = String(value);
|
|
53
71
|
const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
|
|
@@ -211,9 +229,11 @@ function extractPhaseToken(dirName) {
|
|
|
211
229
|
}
|
|
212
230
|
const segments = rest.split('-');
|
|
213
231
|
const tokenSegments = [];
|
|
214
|
-
// #2043: distinguish a real (zero-padded
|
|
215
|
-
//
|
|
216
|
-
// continues with
|
|
232
|
+
// #2043: distinguish a real (zero-padded) phase/sub-phase segment from a
|
|
233
|
+
// single-digit slug word. A pure-numeric leading segment ("46") only
|
|
234
|
+
// continues with exactly-2-digit segments (#2232: a ≥3-digit run is a slug
|
|
235
|
+
// word such as a year — "14-2026-photos-…" yields "14", not "14-2026"), so
|
|
236
|
+
// "46-6-rs-…" yields "46" (the "6" is the
|
|
217
237
|
// slug's first word), not "46-6". Milestone-prefixed ids like "M1-2" reach here
|
|
218
238
|
// with "M1-" already stripped as a project-code prefix (see
|
|
219
239
|
// PROJECT_CODE_PREFIX_CAPTURE_RE_I), so "2" is the leading segment and the same
|
|
@@ -236,7 +256,7 @@ function extractPhaseToken(dirName) {
|
|
|
236
256
|
break;
|
|
237
257
|
}
|
|
238
258
|
}
|
|
239
|
-
else if (
|
|
259
|
+
else if (isPhaseContinuationSegment(seg) || (firstLetterPrefixed && /^\d/.test(seg))) {
|
|
240
260
|
tokenSegments.push(seg);
|
|
241
261
|
}
|
|
242
262
|
else {
|
|
@@ -358,6 +378,8 @@ module.exports = {
|
|
|
358
378
|
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
|
359
379
|
OPTIONAL_PHASE_TAG_SOURCE,
|
|
360
380
|
PHASE_NUMBER_TOKEN_SOURCE,
|
|
381
|
+
PHASE_CONTINUATION_SEGMENT_SOURCE,
|
|
382
|
+
isPhaseContinuationSegment,
|
|
361
383
|
stripProjectCodePrefix,
|
|
362
384
|
normalizePhaseName,
|
|
363
385
|
getMilestoneFromPhaseId,
|
|
@@ -23,53 +23,79 @@
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.deriveProgressFromRoadmap = deriveProgressFromRoadmap;
|
|
25
25
|
exports.clampPercent = clampPercent;
|
|
26
|
+
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
26
27
|
/**
|
|
27
28
|
* Derive completed_phases, total_phases, and total_plans from ROADMAP content.
|
|
28
29
|
* Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.
|
|
30
|
+
*
|
|
31
|
+
* ADR-2143 §3 ("addressed by NAME, never ordinal"): the Progress table is
|
|
32
|
+
* located via the markdown-table seam's `findTableWithColumns`, which is
|
|
33
|
+
* column-NAME/order/count-invariant — it matches the first table whose header
|
|
34
|
+
* is a SUPERSET of the canonical `Phase` / `Plans Complete` / `Status` /
|
|
35
|
+
* `Completed` names, in any order, tolerating extra/injected unrelated
|
|
36
|
+
* columns (#2137's fast-check property test shuffles headers and injects
|
|
37
|
+
* columns and asserts the derived counts never change). This supersedes the
|
|
38
|
+
* earlier `findTableBySchema` exact-schema lookup, which required an exact
|
|
39
|
+
* canonical column SET+ORDER and returned all-null on any reordering or
|
|
40
|
+
* injection.
|
|
41
|
+
*
|
|
42
|
+
* Scoped to the `## Progress` section when the document has one (#2012 decoy
|
|
43
|
+
* avoidance — a differently-headed table sharing the same column names must
|
|
44
|
+
* not be picked up instead); a headingless milestone slice (#1445) falls back
|
|
45
|
+
* to scanning the whole input, preserving the "Progress table not under a
|
|
46
|
+
* `## Progress` heading, or not the first table in the document, still
|
|
47
|
+
* resolves" behaviour.
|
|
48
|
+
*
|
|
49
|
+
* Cells are read by column NAME (`r['Status']`, `r['Plans Complete']`,
|
|
50
|
+
* `r['Phase']`), fixing #2137 (the old position-based regex assumed "Status"
|
|
51
|
+
* was always the 3rd cell and "Plans Complete" the 2nd, which broke for the
|
|
52
|
+
* 5-column milestone-grouped variant that inserts a `Milestone` column ahead
|
|
53
|
+
* of them).
|
|
29
54
|
*/
|
|
30
55
|
function deriveProgressFromRoadmap(roadmapContent) {
|
|
31
56
|
let completedPhases = null;
|
|
32
57
|
let totalPhases = null;
|
|
33
58
|
let totalPlans = null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
// ADR-2143 §5 (fail-loud, no null-swallow): this used to be wrapped in a
|
|
60
|
+
// try/catch that silently fell through to the existing (null) values on any
|
|
61
|
+
// thrown error. `findTableWithColumns`/`parseMarkdownTable` never throw —
|
|
62
|
+
// an unparseable or absent table resolves to `null` /
|
|
63
|
+
// `{ ok: false, reason }`, not an exception — so the catch was masking
|
|
64
|
+
// nothing but dead code paths. Removed per ADR-2143 §5; the public
|
|
65
|
+
// `RoadmapProgress` contract (nulls = absent) is unchanged.
|
|
66
|
+
//
|
|
67
|
+
// ADR-2143 §3: read the Progress table by column NAME (order/injection-invariant),
|
|
68
|
+
// via the markdown-table seam. Scope to the `## Progress` section when present
|
|
69
|
+
// (#2012 decoy avoidance); a headingless milestone slice (#1445) falls back to the
|
|
70
|
+
// whole input. Requires the canonical Phase/Plans Complete/Status/Completed columns
|
|
71
|
+
// in any order (extra columns ignored) — supersedes findTableBySchema's exact-schema lookup.
|
|
72
|
+
const progressMatch = roadmapContent.match(/^##[ \t]+Progress\b/im);
|
|
73
|
+
let scoped = roadmapContent;
|
|
74
|
+
if (progressMatch && progressMatch.index !== undefined) {
|
|
75
|
+
const afterHeading = roadmapContent.slice(progressMatch.index);
|
|
76
|
+
const nextHeading = afterHeading.search(/\n#{1,2}[ \t]/);
|
|
77
|
+
scoped = nextHeading >= 0 ? afterHeading.slice(0, nextHeading) : afterHeading;
|
|
78
|
+
}
|
|
79
|
+
const table = (0, markdown_table_cjs_1.findTableWithColumns)(scoped, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
|
80
|
+
if (table) {
|
|
81
|
+
const allRows = table.rows;
|
|
82
|
+
const completed = allRows.filter((r) => /^complete$/i.test((r['Status'] ?? '').trim())).length;
|
|
83
|
+
completedPhases = completed > 0 ? completed : null;
|
|
84
|
+
// Data rows only (exclude 999.x backlog phases). Mirrors init.cts /^999(?:\.|$)/ filter.
|
|
85
|
+
const dataRows = allRows.filter((r) => {
|
|
86
|
+
const phase = (r['Phase'] ?? '').trim();
|
|
87
|
+
return /^\d/.test(phase) && !/^999\b/.test(phase);
|
|
88
|
+
});
|
|
89
|
+
totalPhases = dataRows.length > 0 ? dataRows.length : null;
|
|
63
90
|
let totalPlansSum = 0;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
for (const r of allRows) {
|
|
92
|
+
const cell = (r['Plans Complete'] ?? '').trim();
|
|
93
|
+
const m = /(\d+)\s*\/\s*(\d+)/.exec(cell);
|
|
94
|
+
if (m)
|
|
95
|
+
totalPlansSum += parseInt(m[2], 10);
|
|
68
96
|
}
|
|
69
|
-
|
|
70
|
-
totalPlans = totalPlansSum;
|
|
97
|
+
totalPlans = totalPlansSum > 0 ? totalPlansSum : null;
|
|
71
98
|
}
|
|
72
|
-
catch { /* intentionally empty — fall through to existing values */ }
|
|
73
99
|
return { completedPhases, totalPhases, totalPlans };
|
|
74
100
|
}
|
|
75
101
|
/**
|
|
@@ -34,9 +34,30 @@ const { planningDir } = planningWorkspace;
|
|
|
34
34
|
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
35
35
|
try {
|
|
36
36
|
const dirs = readSubdirectories(baseDir, true);
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
37
|
+
const matches = dirs.filter(d => phaseTokenMatches(d, normalized));
|
|
38
|
+
if (matches.length === 0)
|
|
39
39
|
return null;
|
|
40
|
+
// #2237: fail loud when multiple directories match the same bare phase
|
|
41
|
+
// number — this happens when unrelated projects share a .planning/phases/
|
|
42
|
+
// tree. Silently taking the first match risks cross-project file writes.
|
|
43
|
+
if (matches.length > 1) {
|
|
44
|
+
return {
|
|
45
|
+
found: false,
|
|
46
|
+
directory: '',
|
|
47
|
+
phase_number: normalized,
|
|
48
|
+
phase_name: null,
|
|
49
|
+
phase_slug: null,
|
|
50
|
+
plans: [],
|
|
51
|
+
summaries: [],
|
|
52
|
+
incomplete_plans: [],
|
|
53
|
+
has_research: false,
|
|
54
|
+
has_context: false,
|
|
55
|
+
has_verification: false,
|
|
56
|
+
has_reviews: false,
|
|
57
|
+
ambiguous_matches: matches,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const match = matches[0];
|
|
40
61
|
const phaseToken = extractPhaseToken(match);
|
|
41
62
|
const phaseNumber = phaseToken || normalized;
|
|
42
63
|
const afterToken = match.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
|