@opengsd/gsd-core 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -23,6 +23,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
23
23
|
const node_os_1 = __importDefault(require("node:os"));
|
|
24
24
|
const node_path_1 = __importDefault(require("node:path"));
|
|
25
25
|
const runtimeArtifactConversion = require("./runtime-artifact-conversion.cjs");
|
|
26
|
+
const security_cjs_1 = require("./security.cjs");
|
|
26
27
|
const runtimeArtifactLayout = require("./runtime-artifact-layout.cjs");
|
|
27
28
|
const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs");
|
|
28
29
|
const runtimeNamePolicy = require("./runtime-name-policy.cjs");
|
|
@@ -111,7 +112,7 @@ function previousOwnedCorpusFiles(configDir, prefix) {
|
|
|
111
112
|
}
|
|
112
113
|
function pruneEmptyCorpusParents(start, stop) {
|
|
113
114
|
let current = node_path_1.default.dirname(start);
|
|
114
|
-
while (current !== stop && current.startsWith(stop + node_path_1.default.sep)) {
|
|
115
|
+
while (current !== stop && current.startsWith(stop + node_path_1.default.sep)) { // allow-handrolled-containment: ancestor-walk loop condition, not a containment gate
|
|
115
116
|
if (installFs().readdirSync(current).length > 0)
|
|
116
117
|
return;
|
|
117
118
|
installFs().rmdirSync(current);
|
|
@@ -347,8 +348,11 @@ function hasExistingSymlinkBetween(root, fullPath, options = {}) {
|
|
|
347
348
|
const resolvedFullPath = node_path_1.default.resolve(fullPath);
|
|
348
349
|
// (a) Path-traversal refusal — ALWAYS enforced, even with opt-in. An untrusted
|
|
349
350
|
// destSubpath string that escapes the install root via '..' is rejected
|
|
350
|
-
// regardless of user opt-in state (ADR-1239 Phase B threat (a)).
|
|
351
|
-
|
|
351
|
+
// regardless of user opt-in state (ADR-1239 Phase B threat (a)). Lexical
|
|
352
|
+
// (ADR-4650 decision 6): this function's whole purpose is to DETECT
|
|
353
|
+
// symlinks between root and target, so resolving them here would erase what
|
|
354
|
+
// it measures.
|
|
355
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(resolvedFullPath, resolvedRoot) === null) {
|
|
352
356
|
return true;
|
|
353
357
|
}
|
|
354
358
|
// #2393 (security-review finding): realpathSync fully resolves all symlink
|
|
@@ -1492,6 +1496,15 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
1492
1496
|
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
1493
1497
|
content = processAttribution(content, resolveAttribution(runtime));
|
|
1494
1498
|
const skillDir = node_path_1.default.join(dest, skillName);
|
|
1499
|
+
// isPathConfined is lexical and cannot see a symlink. mkdirSync({recursive:true})
|
|
1500
|
+
// does NOT throw when skillDir already exists as a symlink to a directory, so a
|
|
1501
|
+
// pre-planted link would redirect the SKILL.md write outside `dest`. Refuse to
|
|
1502
|
+
// write through a link (epic #4636; mirrors retired-artifact-cleanup.cts:77).
|
|
1503
|
+
try {
|
|
1504
|
+
if (installFs().lstatSync(skillDir).isSymbolicLink())
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
catch { /* ENOENT: not created yet — the normal case */ }
|
|
1495
1508
|
installFs().mkdirSync(skillDir, { recursive: true });
|
|
1496
1509
|
installFs().writeFileSync(node_path_1.default.join(skillDir, 'SKILL.md'), content);
|
|
1497
1510
|
// #2322 HIGH-3 parity: persist the capability-owned marker so a later
|
|
@@ -740,6 +740,11 @@ function readInstalledCapabilitySkill(stem, registry) {
|
|
|
740
740
|
return null;
|
|
741
741
|
const skillPath = node_path_1.default.join(capDir, relSkillPath);
|
|
742
742
|
try {
|
|
743
|
+
// statSync follows symlinks; isPathConfined is lexical and cannot see one.
|
|
744
|
+
// Refuse to read through a link so an outside file's content cannot be
|
|
745
|
+
// installed as a capability skill (epic #4636).
|
|
746
|
+
if (node_fs_1.default.lstatSync(skillPath).isSymbolicLink())
|
|
747
|
+
return null;
|
|
743
748
|
if (!node_fs_1.default.statSync(skillPath).isFile())
|
|
744
749
|
return null;
|
|
745
750
|
return { capId, content: node_fs_1.default.readFileSync(skillPath, 'utf8') };
|
|
@@ -858,6 +863,15 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
|
|
|
858
863
|
if (!(0, external_descriptor_trust_cjs_1.isPathConfined)(skillName, stageDir))
|
|
859
864
|
continue; // defense-in-depth
|
|
860
865
|
const destDir = node_path_1.default.join(stageDir, skillName);
|
|
866
|
+
// isPathConfined is lexical and cannot see a symlink. mkdirSync({recursive:true})
|
|
867
|
+
// does NOT throw when destDir already exists as a symlink to a directory, so a
|
|
868
|
+
// pre-planted link would redirect the SKILL.md write outside `stageDir`. Refuse
|
|
869
|
+
// to write through a link (epic #4636; mirrors retired-artifact-cleanup.cts:77).
|
|
870
|
+
try {
|
|
871
|
+
if (installFs().lstatSync(destDir).isSymbolicLink())
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
catch { /* ENOENT: not created yet — the normal case */ }
|
|
861
875
|
installFs().mkdirSync(destDir, { recursive: true });
|
|
862
876
|
installFs().writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), found.content);
|
|
863
877
|
// #2322 HIGH-3: persist the capability-owned marker so a later prune
|
|
@@ -17,6 +17,7 @@ const installer_migration_authoring_cjs_1 = require("./installer-migration-autho
|
|
|
17
17
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
18
|
const clock_cjs_1 = require("./clock.cjs");
|
|
19
19
|
const install_scope_cjs_1 = require("./install-scope.cjs");
|
|
20
|
+
const security_cjs_1 = require("./security.cjs");
|
|
20
21
|
// #2874 (ADR-58 cleanup phase): this file is the ~1200-line migration
|
|
21
22
|
// plan/apply/rollback/lock/journal engine — almost none of it is on the
|
|
22
23
|
// installRuntimeArtifacts call tree. Only `readInstallManifest` and
|
|
@@ -122,8 +123,17 @@ function evaluateRemoveEmptyDir(configDir, fullPath) {
|
|
|
122
123
|
catch {
|
|
123
124
|
return 'left-in-place';
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
// `resolvedTarget === resolvedRoot` is a DELIBERATE ADDITIONAL rejection,
|
|
127
|
+
// separate from the containment decision: `tryWithinRootLexical` treats
|
|
128
|
+
// target === root as CONTAINED, but removing the config root itself is
|
|
129
|
+
// never in scope for this action (see the doc comment above) — this arm
|
|
130
|
+
// prevents `rmdirSync` from ever being asked to remove `configDir` itself.
|
|
131
|
+
// Kept as its own check per ADR-4650 decision 6 (a wrapper may add its own
|
|
132
|
+
// conditions on top of the canonical predicate, never invert it).
|
|
133
|
+
if (resolvedTarget === resolvedRoot)
|
|
134
|
+
return 'left-in-place';
|
|
135
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(resolvedTarget, resolvedRoot) === null) {
|
|
136
|
+
// Refuses "target escaped configDir".
|
|
127
137
|
return 'left-in-place';
|
|
128
138
|
}
|
|
129
139
|
let entries;
|
|
@@ -598,11 +608,30 @@ function acquireInstallMigrationLock(configDir, { timeoutMs = DEFAULT_LOCK_TIMEO
|
|
|
598
608
|
}
|
|
599
609
|
}
|
|
600
610
|
}
|
|
611
|
+
// DELIBERATELY LEXICAL — the RESOLUTION policy stays lexical, never realpath
|
|
612
|
+
// (`assertWithinRoot` / `tryWithinRoot`, src/security.cts).
|
|
613
|
+
//
|
|
614
|
+
// Reviewed under epic #4636 Phase 3 and reverted after the remote matrix proved
|
|
615
|
+
// the realpath collapse wrong. This module's contract is that a symlinked
|
|
616
|
+
// managed path is treated AS A LINK and never dereferenced — it is snapshotted
|
|
617
|
+
// as a link, restored as a link, and backed up as a link. The realpath-based
|
|
618
|
+
// predicate dereferences exactly the symlinks this module exists to preserve
|
|
619
|
+
// and then rejects them for escaping configDir
|
|
620
|
+
// ("migration path escapes configDir: extensions/gsd.cjs"). Four tests in
|
|
621
|
+
// tests/installer-migrations.test.cjs pin that behavior.
|
|
622
|
+
//
|
|
623
|
+
// The containment DECISION now routes through the canonical LEXICAL predicate
|
|
624
|
+
// (`tryWithinRootLexical`, ADR-4650 decision 6) — only the comparison moved;
|
|
625
|
+
// the lexical policy itself remains this module's own required choice, and
|
|
626
|
+
// the thrown message / returned `fullPath` are unchanged.
|
|
627
|
+
//
|
|
628
|
+
// `normalizeRelPath` is the pre-gate: it throws on absolute paths and on any
|
|
629
|
+
// '..' segment BEFORE this runs, so the check below is defense-in-depth over
|
|
630
|
+
// already-traversal-free input rather than the primary boundary.
|
|
601
631
|
function ensureInsideConfig(configDir, relPath) {
|
|
602
632
|
const normalized = normalizeRelPath(relPath);
|
|
603
633
|
const fullPath = node_path_1.default.resolve(configDir, normalized);
|
|
604
|
-
|
|
605
|
-
if (fullPath !== root && !fullPath.startsWith(root + node_path_1.default.sep)) {
|
|
634
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(fullPath, configDir) === null) {
|
|
606
635
|
throw new Error(`migration path escapes configDir: ${relPath}`);
|
|
607
636
|
}
|
|
608
637
|
return { normalized, fullPath };
|
|
@@ -224,6 +224,9 @@ function resolveLoopHooks(input) {
|
|
|
224
224
|
active.consumes = consumes;
|
|
225
225
|
if (onError !== undefined)
|
|
226
226
|
active.onError = onError;
|
|
227
|
+
// #4209 DISP-02: only a literal `true` projects; absent/false stay inert.
|
|
228
|
+
if (hook['supportsReviewerLanes'] === true)
|
|
229
|
+
active.supportsReviewerLanes = true;
|
|
227
230
|
activeHooks.push(active);
|
|
228
231
|
}
|
|
229
232
|
// Process contributions
|
|
@@ -422,17 +425,20 @@ function sanitizeLoadFailReason(reason) {
|
|
|
422
425
|
.slice(0, 300);
|
|
423
426
|
return cleaned || '(no reason given)';
|
|
424
427
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
428
|
+
/**
|
|
429
|
+
* The full config/registry/capability-state resolution `cmdLoopRenderHooks` performs, minus its
|
|
430
|
+
* CLI-only output formatting — extracted so an in-process caller (e.g. `review-lane dispatch-step`
|
|
431
|
+
* self-verifying a `supportsReviewerLanes` trait) can reach the SAME resolution `gsd_run loop
|
|
432
|
+
* render-hooks <point> --raw` would give it, without spawning a subprocess and re-parsing its
|
|
433
|
+
* stdout (which was subject to `io.cjs`'s `@file:` overflow protocol on the rendered-string
|
|
434
|
+
* envelope — a bug class this in-process call cannot hit, since it never touches that envelope
|
|
435
|
+
* or its rendering at all).
|
|
436
|
+
*
|
|
437
|
+
* Throws on an invalid `point` (mirrors `resolveLoopHooks`); callers convert to their own error
|
|
438
|
+
* channel. Emits the same loud stderr load-failure warnings `cmdLoopRenderHooks` always has,
|
|
439
|
+
* regardless of caller — a skipped gate must never be silently invisible.
|
|
440
|
+
*/
|
|
441
|
+
function resolveActiveHooksForPoint(cwd, point, options = {}) {
|
|
436
442
|
const runtimeConfigDir = typeof options['configDir'] === 'string'
|
|
437
443
|
? options['configDir']
|
|
438
444
|
: undefined;
|
|
@@ -468,15 +474,7 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
468
474
|
for (const cap of state.capabilities || []) {
|
|
469
475
|
capabilityStatesById.set(cap.id, cap);
|
|
470
476
|
}
|
|
471
|
-
|
|
472
|
-
try {
|
|
473
|
-
resolved = resolveLoopHooks({ point, registry, config, cwd, capabilityStatesById });
|
|
474
|
-
}
|
|
475
|
-
catch (err) {
|
|
476
|
-
const msg = (err instanceof Error) ? err.message : String(err);
|
|
477
|
-
coreError(msg);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
477
|
+
const resolved = resolveLoopHooks({ point, registry, config, cwd, capabilityStatesById });
|
|
480
478
|
// ── ADR-1244 D2: load-failed capability gates FAIL OPEN with a loud warning ────
|
|
481
479
|
// Decision (#2009): a capability that failed to LOAD must not block the loop.
|
|
482
480
|
// The prior behavior injected a BLOCKING synthetic gate (blocking:true,
|
|
@@ -525,24 +523,44 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
525
523
|
for (const w of loadFailWarnings) {
|
|
526
524
|
process.stderr.write(`gsd: warning — ${w}\n`);
|
|
527
525
|
}
|
|
528
|
-
//
|
|
526
|
+
// Surface capability-state warnings and the #2009 load-failure fail-open warnings together
|
|
527
|
+
// (in addition to the stderr emission above, which is the channel host workflows actually see).
|
|
528
|
+
const combinedWarnings = [...(state.warnings || []), ...loadFailWarnings];
|
|
529
|
+
return { point: resolved.point, activeHooks: resolved.activeHooks, warnings: combinedWarnings };
|
|
530
|
+
}
|
|
531
|
+
function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
532
|
+
if (!point) {
|
|
533
|
+
coreError('loop render-hooks requires a <point> argument. Valid points: ' + CANONICAL_POINTS.join(', '));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// --active-cap <capId> mode: emit 'true' or 'false' only (scanner-safe, no JSON envelope)
|
|
537
|
+
const activeCapId = typeof options['activeCap'] === 'string' ? options['activeCap'] : undefined;
|
|
538
|
+
if (activeCapId !== undefined && activeCapId === '') {
|
|
539
|
+
coreError('--active-cap requires a <capId> value (e.g. --active-cap tdd)');
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
let result;
|
|
543
|
+
try {
|
|
544
|
+
result = resolveActiveHooksForPoint(cwd, point, options);
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
const msg = (err instanceof Error) ? err.message : String(err);
|
|
548
|
+
coreError(msg);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
529
551
|
if (activeCapId !== undefined) {
|
|
530
|
-
const isActive =
|
|
552
|
+
const isActive = result.activeHooks.some((h) => h.capId === activeCapId);
|
|
531
553
|
process.stdout.write(isActive ? 'true\n' : 'false\n');
|
|
532
554
|
return;
|
|
533
555
|
}
|
|
534
|
-
const rendered = renderLoopHooks(
|
|
556
|
+
const rendered = renderLoopHooks({ point: result.point, activeHooks: result.activeHooks });
|
|
535
557
|
const envelope = {
|
|
536
|
-
point:
|
|
537
|
-
activeHooks:
|
|
558
|
+
point: result.point,
|
|
559
|
+
activeHooks: result.activeHooks,
|
|
538
560
|
rendered,
|
|
539
561
|
};
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
// stderr emission above, which is the channel host workflows actually see).
|
|
543
|
-
const combinedWarnings = [...(state.warnings || []), ...loadFailWarnings];
|
|
544
|
-
if (combinedWarnings.length > 0) {
|
|
545
|
-
envelope.warnings = combinedWarnings;
|
|
562
|
+
if (result.warnings.length > 0) {
|
|
563
|
+
envelope.warnings = result.warnings;
|
|
546
564
|
}
|
|
547
565
|
coreOutput(envelope, raw);
|
|
548
566
|
}
|
|
@@ -550,6 +568,7 @@ module.exports = {
|
|
|
550
568
|
resolveLoopHooks,
|
|
551
569
|
renderLoopHooks,
|
|
552
570
|
cmdLoopRenderHooks,
|
|
571
|
+
resolveActiveHooksForPoint,
|
|
553
572
|
// Exported for tests
|
|
554
573
|
_getNestedConfigValue,
|
|
555
574
|
_resolveActivationValue,
|
|
@@ -402,8 +402,8 @@ function classifyUnindexedUri(uri) {
|
|
|
402
402
|
}
|
|
403
403
|
/** Second, independent gate (design "Hostile inputs" gate 2): re-validate an INDEXED entry's relPath against the catalog root, catching a symlink planted after the index was built. */
|
|
404
404
|
function readIndexedResource(catalog, entry) {
|
|
405
|
-
const
|
|
406
|
-
if (
|
|
405
|
+
const contained = (0, security_cjs_1.tryWithinRoot)(entry.relPath, catalog.root);
|
|
406
|
+
if (contained === null) {
|
|
407
407
|
fail(exports.REASON.TRAVERSAL_REFUSED, `indexed resource escapes catalog root: ${entry.relPath}`);
|
|
408
408
|
}
|
|
409
409
|
let raw;
|
|
@@ -826,7 +826,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
826
826
|
// never disagree with what a real run actually archives. Absent
|
|
827
827
|
// --archive-quick this stays `[]` and nothing on disk is touched either
|
|
828
828
|
// way (dry-run always returns before any mutation below).
|
|
829
|
-
const quickDirsToArchive = options.archiveQuick ? listQuickTaskDirsForArchive(cwd) : [];
|
|
829
|
+
const quickDirsToArchive = options.archiveQuick ? listQuickTaskDirsForArchive(cwd).map((d) => d.name) : [];
|
|
830
830
|
const dryRunResult = {
|
|
831
831
|
dry_run: true,
|
|
832
832
|
version,
|
|
@@ -1482,6 +1482,16 @@ function writeQuickArchiveReadme(archiveQuickDir) {
|
|
|
1482
1482
|
* written three times, and only the real-run copy applied `requireSafePath`,
|
|
1483
1483
|
* so a dry-run preview could list a directory the real run would silently
|
|
1484
1484
|
* skip).
|
|
1485
|
+
*
|
|
1486
|
+
* Returns the proven `ContainedPath` alongside each entry's bare `name`
|
|
1487
|
+
* (`no-unconfined-path-join`'s `discardedContainmentResult` arm — a bare
|
|
1488
|
+
* statement call to `requireSafePath` throws away the exact answer it just
|
|
1489
|
+
* computed). The two dry-run previews only need `name` for display;
|
|
1490
|
+
* `archiveQuickTaskDirectories` deliberately does NOT reuse `abs` for its
|
|
1491
|
+
* rename — it re-derives and re-validates independently as TOCTOU
|
|
1492
|
+
* defense-in-depth (see its own comment), so `abs` exists here only to make
|
|
1493
|
+
* this function's own discard explicit, not to be trusted downstream as a
|
|
1494
|
+
* stale-safe proof.
|
|
1485
1495
|
*/
|
|
1486
1496
|
function listQuickTaskDirsForArchive(cwd) {
|
|
1487
1497
|
const planningBase = planningPaths(cwd).planning;
|
|
@@ -1494,19 +1504,20 @@ function listQuickTaskDirsForArchive(cwd) {
|
|
|
1494
1504
|
// .planning/quick absent or unreadable — nothing to select.
|
|
1495
1505
|
return [];
|
|
1496
1506
|
}
|
|
1497
|
-
const
|
|
1507
|
+
const results = [];
|
|
1498
1508
|
for (const entry of sourceEntries) {
|
|
1499
1509
|
if (!entry.isDirectory())
|
|
1500
1510
|
continue; // excludes symlinks too — see MAJOR 3 note above
|
|
1511
|
+
let abs;
|
|
1501
1512
|
try {
|
|
1502
|
-
(0, security_cjs_1.requireSafePath)(node_path_1.default.join(quickDir, entry.name), planningBase, 'quick task dir',
|
|
1513
|
+
abs = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(quickDir, entry.name), planningBase, 'quick task dir', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1503
1514
|
}
|
|
1504
1515
|
catch {
|
|
1505
1516
|
continue; // symlink/escape attempt — never a candidate, in preview OR real run
|
|
1506
1517
|
}
|
|
1507
|
-
|
|
1518
|
+
results.push({ name: entry.name, abs });
|
|
1508
1519
|
}
|
|
1509
|
-
return
|
|
1520
|
+
return results.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
1510
1521
|
}
|
|
1511
1522
|
/**
|
|
1512
1523
|
* #2142: move each DIRECTORY entry under `.planning/quick/` into
|
|
@@ -1555,7 +1566,7 @@ function archiveQuickTaskDirectories(cwd, version) {
|
|
|
1555
1566
|
// #2142 MAJOR 5 (review): dirNames is the SAME selection
|
|
1556
1567
|
// `listQuickTaskDirsForArchive` hands to both dry-run previews — this is
|
|
1557
1568
|
// the real run, so it cannot disagree with what a preview reported.
|
|
1558
|
-
const dirNames = listQuickTaskDirsForArchive(cwd);
|
|
1569
|
+
const dirNames = listQuickTaskDirsForArchive(cwd).map((d) => d.name);
|
|
1559
1570
|
if (dirNames.length === 0) {
|
|
1560
1571
|
// Boundary 0 (#2142): zero (safe) directory entries (empty dir, only
|
|
1561
1572
|
// stray files, or every entry excluded by the selection rule) must not
|
|
@@ -1576,7 +1587,7 @@ function archiveQuickTaskDirectories(cwd, version) {
|
|
|
1576
1587
|
// rename are two separate filesystem observations, and an entry
|
|
1577
1588
|
// that was a safe real directory at selection time could in theory
|
|
1578
1589
|
// be swapped for a symlink before this loop reaches it.
|
|
1579
|
-
safeSrc = (0, security_cjs_1.requireSafePath)(src, planningBase, 'quick task dir',
|
|
1590
|
+
safeSrc = (0, security_cjs_1.requireSafePath)(src, planningBase, 'quick task dir', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1580
1591
|
}
|
|
1581
1592
|
catch {
|
|
1582
1593
|
continue; // symlink/escape attempt — skip, not archived
|
|
@@ -1669,7 +1680,7 @@ function cmdQuickArchive(cwd, version, options, raw) {
|
|
|
1669
1680
|
// `cmdMilestoneComplete`'s own dry-run preview and the real
|
|
1670
1681
|
// `archiveQuickTaskDirectories` both use, so all three can never disagree.
|
|
1671
1682
|
if (options.dryRun) {
|
|
1672
|
-
const quickDirsToArchive = listQuickTaskDirsForArchive(cwd);
|
|
1683
|
+
const quickDirsToArchive = listQuickTaskDirsForArchive(cwd).map((d) => d.name);
|
|
1673
1684
|
output({
|
|
1674
1685
|
dry_run: true,
|
|
1675
1686
|
version,
|
|
@@ -128,6 +128,67 @@ function _resolveRuntimeTier(config, tier) {
|
|
|
128
128
|
overrides: config['model_profile_overrides'],
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* #4192 — Resolve the claude-runtime TIER OVERRIDE model for (config, tier).
|
|
133
|
+
*
|
|
134
|
+
* Step 3's runtime-aware resolution deliberately skips the claude runtime to
|
|
135
|
+
* preserve the alias-native posture (#1156/#2297): with no user override, the
|
|
136
|
+
* resolver must keep returning bare tier aliases, and the builtin claude tier
|
|
137
|
+
* map (`opus → claude-opus-4-8`, …) must never force full-ID emission on every
|
|
138
|
+
* default install. But `model_profile_overrides.<runtime>.<tier>` is a
|
|
139
|
+
* documented override point (docs/CONFIGURATION.md § Runtime-Aware Profiles)
|
|
140
|
+
* that `workflows/settings-advanced.md` actively writes for claude-runtime
|
|
141
|
+
* users — and #4192 Finding 1 measured the key inert on this runtime.
|
|
142
|
+
*
|
|
143
|
+
* This helper reads ONLY the user's override entry for the effective claude
|
|
144
|
+
* runtime and tier — never the builtin claude tier map — so an install with no
|
|
145
|
+
* override is byte-identical to before the fix. The runtime is resolved the
|
|
146
|
+
* same way steps 1-3 resolve it (config['runtime'], defaulting to 'claude'),
|
|
147
|
+
* NOT via resolveActiveRuntime (GSD_RUNTIME/marker): the value policy must
|
|
148
|
+
* key off the config the operator wrote, matching mapClaudeOverrideForRuntime.
|
|
149
|
+
*
|
|
150
|
+
* Value policy mirrors the model_overrides path (#2041/#4192): an override
|
|
151
|
+
* value that maps to a current tier alias collapses to that alias
|
|
152
|
+
* (byte-equivalent resolution, alias-form emission); anything else — a pinned
|
|
153
|
+
* older generation (`claude-opus-4-7`), a bare alias/tier repoint (`sonnet`),
|
|
154
|
+
* or a non-Claude vendor id (`openai/o3`) — is emitted verbatim as pinned.
|
|
155
|
+
* Malformed entries (no usable `model` string) return null so the caller falls
|
|
156
|
+
* through to normal alias resolution (ADR-443 D1: invalid values fall through).
|
|
157
|
+
*/
|
|
158
|
+
function resolveClaudeTierOverrideModel(configRuntime, tier, overrides) {
|
|
159
|
+
if (!tier || tier === 'inherit')
|
|
160
|
+
return null;
|
|
161
|
+
const effectiveRuntime = configRuntime || 'claude';
|
|
162
|
+
if (effectiveRuntime !== 'claude')
|
|
163
|
+
return null; // non-claude runtimes resolve at step 3
|
|
164
|
+
const overridesMap = overrides;
|
|
165
|
+
if (!overridesMap || typeof overridesMap !== 'object')
|
|
166
|
+
return null;
|
|
167
|
+
// Own-property guards throughout: both levels are config-supplied plain
|
|
168
|
+
// objects, so a prototype-chain key ("constructor", "toString") must not
|
|
169
|
+
// resolve an inherited member instead of falling through (same hardening as
|
|
170
|
+
// every other config-keyed lookup in this module).
|
|
171
|
+
const runtimeEntry = Object.hasOwn(overridesMap, effectiveRuntime)
|
|
172
|
+
? overridesMap[effectiveRuntime]
|
|
173
|
+
: undefined;
|
|
174
|
+
if (!runtimeEntry || typeof runtimeEntry !== 'object')
|
|
175
|
+
return null;
|
|
176
|
+
const userRaw = Object.hasOwn(runtimeEntry, tier) ? runtimeEntry[tier] : undefined;
|
|
177
|
+
if (userRaw === undefined || userRaw === null)
|
|
178
|
+
return null;
|
|
179
|
+
const entry = typeof userRaw === 'string'
|
|
180
|
+
? { model: userRaw }
|
|
181
|
+
: userRaw;
|
|
182
|
+
if (!entry || typeof entry !== 'object')
|
|
183
|
+
return null;
|
|
184
|
+
const model = entry['model'];
|
|
185
|
+
if (typeof model !== 'string' || model.length === 0)
|
|
186
|
+
return null;
|
|
187
|
+
if (Object.hasOwn(CLAUDE_POLICY_ID_TO_ALIAS, model)) {
|
|
188
|
+
return CLAUDE_POLICY_ID_TO_ALIAS[model];
|
|
189
|
+
}
|
|
190
|
+
return model;
|
|
191
|
+
}
|
|
131
192
|
// Reverse of the Claude tier-default IDs, plus the Fable alias which Claude
|
|
132
193
|
// Code's Agent tool accepts but which is not a GSD model-profile tier (#1133).
|
|
133
194
|
const CLAUDE_POLICY_ID_TO_ALIAS = {
|
|
@@ -154,7 +215,10 @@ function warnModelPolicyUnmappable(agentType, policyModel, tier) {
|
|
|
154
215
|
function _resetModelPolicyWarningCacheForTests() {
|
|
155
216
|
_modelPolicyUnmappableWarned.clear();
|
|
156
217
|
}
|
|
157
|
-
// Dedupe stderr warnings for unmappable model_overrides Claude IDs (#2041
|
|
218
|
+
// Dedupe stderr warnings for unmappable model_overrides Claude IDs (#2041 /
|
|
219
|
+
// #4192). #2041 originally warned that such a value was being DROPPED to tier
|
|
220
|
+
// resolution; #4192 keeps the warn-once breadcrumb but changes the behavior to
|
|
221
|
+
// a verbatim pass-through, so the text now describes the pass-through.
|
|
158
222
|
const _modelOverrideUnmappableWarned = new Set();
|
|
159
223
|
function warnModelOverrideUnmappable(agentType, overrideValue) {
|
|
160
224
|
const key = `${agentType}::${overrideValue}`;
|
|
@@ -165,8 +229,9 @@ function warnModelOverrideUnmappable(agentType, overrideValue) {
|
|
|
165
229
|
// full to stderr/logs (#2041 security review). MUST go to stderr — resolve-
|
|
166
230
|
// model's JSON result is parsed from stdout.
|
|
167
231
|
const safe = overrideValue.length > 64 ? overrideValue.slice(0, 64) + '…' : overrideValue;
|
|
168
|
-
process.stderr.write(`gsd: warning — model_overrides value "${safe}" for ${agentType} ` +
|
|
169
|
-
`
|
|
232
|
+
process.stderr.write(`gsd: warning — model_overrides value "${safe}" for ${agentType} is a fully-qualified ` +
|
|
233
|
+
`Claude model ID with no tier alias; passing it through verbatim. Claude Code setups ` +
|
|
234
|
+
`whose Agent tool accepts only tier aliases will not honor it. (#4192)\n`);
|
|
170
235
|
}
|
|
171
236
|
// Test-only: reset the model_overrides warn-dedupe cache between cases (#2041).
|
|
172
237
|
function _resetModelOverrideWarningCacheForTests() {
|
|
@@ -175,12 +240,24 @@ function _resetModelOverrideWarningCacheForTests() {
|
|
|
175
240
|
/**
|
|
176
241
|
* #2041 — Map a `model_overrides` value to its Claude Agent-tool alias on the
|
|
177
242
|
* claude runtime, mirroring the `model_policy` path (#1144). Claude Code's
|
|
178
|
-
* Agent tool `model` parameter documents
|
|
179
|
-
* fable)
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
243
|
+
* Agent tool `model` parameter documents tier aliases (opus/sonnet/haiku/
|
|
244
|
+
* fable) as the always-accepted form. Returns the value to emit verbatim, or
|
|
245
|
+
* null to signal "fall through to normal tier/dynamic-routing resolution".
|
|
246
|
+
* Non-Claude runtimes and non-Claude values always pass through verbatim.
|
|
247
|
+
*
|
|
248
|
+
* #4192 — an unmappable `claude-*` value (a pinned generation that is not the
|
|
249
|
+
* current catalog default, e.g. `claude-opus-4-7`) is now PASSED THROUGH
|
|
250
|
+
* VERBATIM with a warn-once stderr breadcrumb, instead of being dropped to
|
|
251
|
+
* tier resolution. #2041's drop was correct when the value was plausibly a
|
|
252
|
+
* mis-typed current default, but for an explicit pin it silently UNPINNED the
|
|
253
|
+
* operator's choice — the resolver would report a tier the config never asked
|
|
254
|
+
* for, the exact "profile can misrepresent what actually runs" defect #4192
|
|
255
|
+
* files. The documented contract ("any fully-qualified model ID",
|
|
256
|
+
* docs/CONFIGURATION.md § Per-Agent Overrides,
|
|
257
|
+
* gsd-core/references/model-profiles.md § Per-Agent Overrides) is restored:
|
|
258
|
+
* the pin is resolved as configured. Values that DO map to a current tier
|
|
259
|
+
* alias still collapse to that alias — byte-equivalent resolution, the #2041
|
|
260
|
+
* protection preserved — and a mappable pin never warns.
|
|
184
261
|
*
|
|
185
262
|
* Hardening (code+security review): a `typeof` guard preserves the pre-fix
|
|
186
263
|
* no-crash behavior if a malformed config surfaces a non-string value, and an
|
|
@@ -204,8 +281,9 @@ function mapClaudeOverrideForRuntime(override, configRuntime, agentType) {
|
|
|
204
281
|
if (model_catalog_cjs_1.CLAUDE_AGENT_ALIASES.has(override))
|
|
205
282
|
return override;
|
|
206
283
|
if (override.startsWith('claude-')) {
|
|
284
|
+
// #4192: explicit generation pin — resolve as configured (see docblock).
|
|
207
285
|
warnModelOverrideUnmappable(agentType, override);
|
|
208
|
-
return
|
|
286
|
+
return override;
|
|
209
287
|
}
|
|
210
288
|
return override;
|
|
211
289
|
}
|
|
@@ -467,6 +545,19 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
467
545
|
&& (projectExplicitlySetsOmit(cwd) || !RUNTIMES_WITH_NATIVE_ALIASES.has(resolveActiveRuntime(config)))) {
|
|
468
546
|
return '';
|
|
469
547
|
}
|
|
548
|
+
// 4.5. Claude-runtime tier override (#4192 Finding 1). Sits AFTER the omit
|
|
549
|
+
// gate so an explicit project `resolve_model_ids:"omit"` still wins (#2297:
|
|
550
|
+
// explicit project omit is honored regardless of runtime), and BEFORE the
|
|
551
|
+
// alias return so a pinned generation is not re-collapsed to a tier alias or
|
|
552
|
+
// re-materialized to the LATEST catalog id by step 5's
|
|
553
|
+
// `resolve_model_ids:true` path. Fires ONLY when the user wrote a
|
|
554
|
+
// `model_profile_overrides.claude.<tier>` entry for this tier — see
|
|
555
|
+
// resolveClaudeTierOverrideModel for why the builtin map stays out.
|
|
556
|
+
if (tier && tier !== 'inherit') {
|
|
557
|
+
const claudeOverrideModel = resolveClaudeTierOverrideModel(configRuntime, tier, config['model_profile_overrides']);
|
|
558
|
+
if (claudeOverrideModel !== null)
|
|
559
|
+
return claudeOverrideModel;
|
|
560
|
+
}
|
|
470
561
|
// 5. Profile lookup (Claude-native default).
|
|
471
562
|
if (!agentModels) {
|
|
472
563
|
return profile === 'quality' ? 'opus'
|
|
@@ -126,7 +126,13 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
126
126
|
if (args.includes('--dry-run')) {
|
|
127
127
|
return makeInvalidArgs('--dry-run', 'phase insert does not support --dry-run');
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
// #4569: --sibling opts into joining afterPhase's parent decimal level
|
|
130
|
+
// instead of nesting one level deeper. Filtered out like other
|
|
131
|
+
// boolean flags (see `remove`'s --force handling above) so it never
|
|
132
|
+
// leaks into the free-text description.
|
|
133
|
+
const sibling = args.includes('--sibling');
|
|
134
|
+
const insertArgs = args.slice(2).filter(token => token !== '--sibling');
|
|
135
|
+
phase.cmdPhaseInsert(cwd, insertArgs[0], insertArgs.slice(1).join(' '), raw, sibling ? 'sibling' : 'nested');
|
|
130
136
|
return { ok: true, data: null };
|
|
131
137
|
},
|
|
132
138
|
remove: (_ctx) => {
|