@opengsd/gsd-core 1.10.0 → 1.11.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-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +575 -101
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +134 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +24 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* installed-surface-resolver.cts — Installed Surface Resolver Module (#2872,
|
|
4
|
+
* ADR-2866, Phase 3 — governed by
|
|
5
|
+
* `.gsd/phase/feat-2872-manifest-scope-runtime/40-design.md`).
|
|
6
|
+
*
|
|
7
|
+
* `resolveInstalledSurfaces(runtime?, opts?)` answers a question no existing
|
|
8
|
+
* module answers: "which surfaces actually exist on THIS machine, across
|
|
9
|
+
* BOTH install scopes, right now?" `capability-state.cts` answers "which
|
|
10
|
+
* capabilities are enabled in THIS config dir" — a different question at a
|
|
11
|
+
* different altitude. This module is a new leaf: it reads two scopes at
|
|
12
|
+
* once, composing three already-shipped, already-tested pieces
|
|
13
|
+
* (`resolveScope`, Phase 1; `resolveTriggerSurface`, Phase 2; and
|
|
14
|
+
* `readInstallManifest`, widened in this same phase) rather than
|
|
15
|
+
* re-implementing any of their rules.
|
|
16
|
+
*
|
|
17
|
+
* ── Probe-not-declared keying ───────────────────────────────────────────────
|
|
18
|
+
* A scope RECORD is keyed by the PROBED scope (`resolveScope`'s own `id`),
|
|
19
|
+
* never by what the manifest itself claims. `manifestVersion`/`runtime`/
|
|
20
|
+
* `scope` recorded inside a v2 manifest are corroboration, not identity: a
|
|
21
|
+
* manifest copied between config dirs, or a `--config-dir` install, must
|
|
22
|
+
* still be reported at the scope this machine actually resolves to.
|
|
23
|
+
*
|
|
24
|
+
* ── Report, don't correct ───────────────────────────────────────────────────
|
|
25
|
+
* When a declared `runtime`/`scope` disagrees with the probe, this module
|
|
26
|
+
* reports the mismatch (`declaredScopeMatchesProbe: false` /
|
|
27
|
+
* `declaredRuntimeMatchesProbe: false`) and otherwise proceeds exactly as it
|
|
28
|
+
* would for an undeclared (v1) manifest. It never silently substitutes the
|
|
29
|
+
* declared value for the probed one, and never throws on a mismatch — see
|
|
30
|
+
* B-row "Postel's Law" discussion in the design doc.
|
|
31
|
+
*
|
|
32
|
+
* ── One trigger call over installed scopes only ─────────────────────────────
|
|
33
|
+
* `resolveTriggerSurface` is called AT MOST once per runtime, with the
|
|
34
|
+
* scopes that are actually installed on this machine (per manifest
|
|
35
|
+
* presence — never per the new corroboration fields, which is what keeps a
|
|
36
|
+
* v1-only install fully functional with no reinstall required). A
|
|
37
|
+
* hypothetical "what if both scopes were installed" answer is deliberately
|
|
38
|
+
* not offered; #2218 is a fact about THIS machine, not a simulation.
|
|
39
|
+
*
|
|
40
|
+
* ── Installed-ness is manifest PRESENCE, never the new fields ───────────────
|
|
41
|
+
* `installed` is `manifestVersion !== null`. A v1 manifest (no
|
|
42
|
+
* `manifestVersion` key at all, `manifestVersion: 1` after normalization) is
|
|
43
|
+
* a correct manifest written by an older GSD — never a broken one, never
|
|
44
|
+
* grounds for `installed: false`, a warning, or a reinstall prompt.
|
|
45
|
+
*
|
|
46
|
+
* ── Stems come from the manifest, not the source tree ───────────────────────
|
|
47
|
+
* `resolveTriggerSurface` needs `stems`. They are derived from the
|
|
48
|
+
* INSTALLED manifest's own `files` keys (see the private stem-derivation
|
|
49
|
+
* helpers below), never from a roster read of the source tree — a global
|
|
50
|
+
* `claude` install ships no `commands/gsd` source, so a roster read would
|
|
51
|
+
* return `[]` in exactly the configuration #2218 is about. The derivation
|
|
52
|
+
* consumes `isNamespacedByDir` and `composeCommandFilename`
|
|
53
|
+
* (`runtime-artifact-layout.cjs`, #2871 Phase 2's exported helpers) rather
|
|
54
|
+
* than re-deriving either rule as a fourth independent copy.
|
|
55
|
+
*/
|
|
56
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
57
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
58
|
+
};
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.resolveInstalledSurfaces = resolveInstalledSurfaces;
|
|
61
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
62
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
63
|
+
const install_scope_cjs_1 = require("./install-scope.cjs");
|
|
64
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
|
+
const runtimeArtifactLayoutMod = require("./runtime-artifact-layout.cjs");
|
|
67
|
+
const { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, resolveTriggerSurface, isNamespacedByDir, composeCommandFilename, } = runtimeArtifactLayoutMod;
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
+
const installerMigrationsMod = require("./installer-migrations.cjs");
|
|
70
|
+
const { readInstallManifest, MANIFEST_NAME } = installerMigrationsMod;
|
|
71
|
+
// In .cts (CommonJS output) files, `require` is available as a global.
|
|
72
|
+
const _require = require;
|
|
73
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* A trigger stem is a kebab-case token and nothing else. Verified against the
|
|
76
|
+
* real roster: all 71 `commands/gsd/*.md` stems match this, so it has no false
|
|
77
|
+
* negatives today.
|
|
78
|
+
*
|
|
79
|
+
* This is a SECURITY boundary, not cosmetics. A stem is derived from a manifest
|
|
80
|
+
* key — attacker-influenceable text, since a project-local
|
|
81
|
+
* `gsd-file-manifest.json` lives inside a repository a user may merely have
|
|
82
|
+
* cloned — and Phase 4 (#2873) renders it back to the user as `/gsd-<stem>`.
|
|
83
|
+
* Without this, `skills/gsd-../../../x` yields the stem `..`, and control
|
|
84
|
+
* characters, newlines, ANSI escapes and RTL-override codepoints all survive
|
|
85
|
+
* into that rendered trigger. The `commands` branch happened to be protected by
|
|
86
|
+
* its `composeCommandFilename` round-trip; the `skills` branch had no
|
|
87
|
+
* equivalent, so the rule is stated once here and applied to both.
|
|
88
|
+
*/
|
|
89
|
+
const SAFE_STEM = /^[a-z0-9][a-z0-9-]*$/;
|
|
90
|
+
function getDefaultRuntimeRegistry() {
|
|
91
|
+
return _require('./capability-registry.cjs');
|
|
92
|
+
}
|
|
93
|
+
/** C7: every registered runtime id, sorted for determinism. Reads
|
|
94
|
+
* `opts.registry` when provided (so a sweep is assertable without the real
|
|
95
|
+
* capability registry), else the real one. */
|
|
96
|
+
function listRegisteredRuntimeIds(opts) {
|
|
97
|
+
const registry = opts.registry ?? getDefaultRuntimeRegistry();
|
|
98
|
+
return Object.keys(registry.runtimes).sort();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* D — the stem inverse for one `commands`/`skills` layout kind entry.
|
|
102
|
+
* Consumes `isNamespacedByDir` (to decide the commands filename shape) and
|
|
103
|
+
* `composeCommandFilename` (to VERIFY a candidate stem round-trips to the
|
|
104
|
+
* exact filename seen) rather than re-deriving either rule locally (design
|
|
105
|
+
* "Rejected" #6). Manifest keys are normalized with an unconditional
|
|
106
|
+
* `.replace(/\\/g, '/')` (D7) — never gated on `path.sep` — matching this
|
|
107
|
+
* repo's recorded path-separator-normalization defect class.
|
|
108
|
+
*/
|
|
109
|
+
function deriveStemsForKindEntry(kind, destSubpath, prefix, fileKeys) {
|
|
110
|
+
const destSubpathNorm = (0, shell_command_projection_cjs_1.posixNormalize)(destSubpath);
|
|
111
|
+
const boundary = `${destSubpathNorm}/`;
|
|
112
|
+
const namespacedByDir = isNamespacedByDir(kind, destSubpath, prefix);
|
|
113
|
+
const stems = new Set();
|
|
114
|
+
for (const rawKey of fileKeys) {
|
|
115
|
+
const key = rawKey.replace(/\\/g, '/');
|
|
116
|
+
if (!key.startsWith(boundary))
|
|
117
|
+
continue; // D4: key not under this declared subpath
|
|
118
|
+
const rest = key.slice(boundary.length);
|
|
119
|
+
if (rest === '')
|
|
120
|
+
continue;
|
|
121
|
+
const segments = rest.split('/');
|
|
122
|
+
if (kind === 'skills') {
|
|
123
|
+
const dirSegment = segments[0];
|
|
124
|
+
if (!dirSegment.startsWith(prefix))
|
|
125
|
+
continue; // D5: subpath matches, prefix does not
|
|
126
|
+
const stem = dirSegment.slice(prefix.length);
|
|
127
|
+
if (stem === '')
|
|
128
|
+
continue; // D7/B7: empty stem never emitted
|
|
129
|
+
if (!SAFE_STEM.test(stem))
|
|
130
|
+
continue; // security: reject anything but a kebab-case token
|
|
131
|
+
stems.add(stem); // D6: several files under one dir -> one stem
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// commands: exactly one path segment — the composed filename itself.
|
|
135
|
+
if (segments.length !== 1)
|
|
136
|
+
continue;
|
|
137
|
+
const filename = segments[0];
|
|
138
|
+
if (!filename.endsWith('.md'))
|
|
139
|
+
continue;
|
|
140
|
+
const base = filename.slice(0, -3);
|
|
141
|
+
const candidateStem = namespacedByDir
|
|
142
|
+
? base
|
|
143
|
+
: (base.startsWith(prefix) ? base.slice(prefix.length) : '');
|
|
144
|
+
if (candidateStem === '')
|
|
145
|
+
continue;
|
|
146
|
+
if (composeCommandFilename(namespacedByDir, prefix, candidateStem) !== filename)
|
|
147
|
+
continue;
|
|
148
|
+
if (!SAFE_STEM.test(candidateStem))
|
|
149
|
+
continue; // security: reject anything but a kebab-case token
|
|
150
|
+
stems.add(candidateStem);
|
|
151
|
+
}
|
|
152
|
+
return [...stems];
|
|
153
|
+
}
|
|
154
|
+
/** D — full stem set for one scope: every `commands`/`skills` layout kind
|
|
155
|
+
* entry, unioned. Empty `files` (C13) short-circuits to `[]` without
|
|
156
|
+
* resolving a layout at all. */
|
|
157
|
+
function deriveStemsFromManifest(runtime, scopeId, configHome, files, opts) {
|
|
158
|
+
const fileKeys = Object.keys(files);
|
|
159
|
+
if (fileKeys.length === 0)
|
|
160
|
+
return [];
|
|
161
|
+
const layout = opts.registry !== undefined
|
|
162
|
+
? resolveRuntimeArtifactLayoutFromRegistry(opts.registry, runtime, configHome, scopeId)
|
|
163
|
+
: resolveRuntimeArtifactLayout(runtime, configHome, scopeId);
|
|
164
|
+
const stems = new Set();
|
|
165
|
+
for (const kindEntry of layout.kinds) {
|
|
166
|
+
if (kindEntry.kind !== 'commands' && kindEntry.kind !== 'skills')
|
|
167
|
+
continue; // excludes agents/kimi-agents (D4)
|
|
168
|
+
for (const stem of deriveStemsForKindEntry(kindEntry.kind, kindEntry.destSubpath, kindEntry.prefix, fileKeys)) {
|
|
169
|
+
stems.add(stem);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return [...stems].sort();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* True when `p` is a symlink. An `lstatSync` throw (ENOENT — nothing at this
|
|
176
|
+
* path) is NOT evidence of a symlink; it is treated as "not a symlink" here
|
|
177
|
+
* and left for `readManifest` to classify (it already owns the absent-file
|
|
178
|
+
* case, per C14 above).
|
|
179
|
+
*/
|
|
180
|
+
function isSymlinkPath(p, lstatSync) {
|
|
181
|
+
try {
|
|
182
|
+
return lstatSync(p).isSymbolicLink();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The shared "not installed" degraded shape (C14's EACCES path, and this
|
|
190
|
+
* phase's new symlink-guard path). A FACTORY, not a module-level constant
|
|
191
|
+
* object: a `const` object spread at each return site would still share the
|
|
192
|
+
* same `stems` ARRAY reference across every call (`...` shallow-copies the
|
|
193
|
+
* object but not the array a property points at), which would violate this
|
|
194
|
+
* module's own "builds fresh arrays/objects on every call" contract (C15) —
|
|
195
|
+
* a caller mutating one degraded record's `stems` must never be visible on
|
|
196
|
+
* another's.
|
|
197
|
+
*/
|
|
198
|
+
function notInstalledScopeRecordFields() {
|
|
199
|
+
return {
|
|
200
|
+
installed: false,
|
|
201
|
+
manifestVersion: null,
|
|
202
|
+
declaredRuntime: null,
|
|
203
|
+
declaredScope: null,
|
|
204
|
+
declaredScopeMatchesProbe: null,
|
|
205
|
+
declaredRuntimeMatchesProbe: null,
|
|
206
|
+
stems: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Build one scope's record. `resolvedConfigHome` has already been probed
|
|
211
|
+
* successfully by the time this is called (a `resolveScope` `TypeError` is
|
|
212
|
+
* handled by the caller, per C8 — it is never this function's concern).
|
|
213
|
+
*
|
|
214
|
+
* Two distinct failure modes are handled with two distinct, narrowly-scoped
|
|
215
|
+
* try/catches — NOT one catch-all around the whole function:
|
|
216
|
+
*
|
|
217
|
+
* - The manifest READ (`readManifest`) is the only thing that can justify
|
|
218
|
+
* `installed: false` (C14, e.g. EACCES). A failure here means "we could
|
|
219
|
+
* not even tell whether this scope is installed".
|
|
220
|
+
* - The stem DERIVATION (`deriveStemsFromManifest`, which resolves a
|
|
221
|
+
* runtime artifact layout) is a separate concern. A `TypeError` thrown
|
|
222
|
+
* while deriving stems is a layout-lookup failure, not evidence the
|
|
223
|
+
* manifest is absent — the manifest was already read successfully, so
|
|
224
|
+
* `installed` and every declared/*MatchesProbe field stay exactly as the
|
|
225
|
+
* manifest reported. Conflating the two would report a genuinely
|
|
226
|
+
* installed runtime as `installed: false`, hiding a real install from
|
|
227
|
+
* #2218 shadow detection precisely when this module exists to surface it.
|
|
228
|
+
*/
|
|
229
|
+
function buildScopeRecord(runtime, scopeId, resolvedConfigHome, opts) {
|
|
230
|
+
// Hardening requirement 2 (#2873 design doc, "Hardening requirements
|
|
231
|
+
// claimed from #2873's comment"): `readInstallManifest` -> `readJsonIfPresent`
|
|
232
|
+
// uses `existsSync` + `readFileSync` and therefore FOLLOWS symlinks, and
|
|
233
|
+
// this module resolves `local` against `process.cwd()` — a directory that,
|
|
234
|
+
// as of this phase, becomes reachable from an arbitrary cloned repository
|
|
235
|
+
// (this is the same phase that makes the local scope's manifest a first
|
|
236
|
+
// read target, not merely a write target). The in-tree precedent is
|
|
237
|
+
// `getAgentsDir` (`agent-install-check.cts`), which probes with
|
|
238
|
+
// `fs.lstatSync(...).isDirectory()`/`.isFile()` and deliberately does not
|
|
239
|
+
// follow. #2872 left this resolver's read un-guarded only because the path
|
|
240
|
+
// had zero callers at the time; refusing to follow a symlinked config dir
|
|
241
|
+
// or manifest here closes that asymmetry rather than carrying it forward.
|
|
242
|
+
// Degrading to `installed: false` reuses the SAME shape the EACCES catch
|
|
243
|
+
// below already returns — no new failure shape is introduced.
|
|
244
|
+
const lstatSync = opts.lstatSync ?? node_fs_1.default.lstatSync;
|
|
245
|
+
const manifestPath = node_path_1.default.join(resolvedConfigHome, MANIFEST_NAME);
|
|
246
|
+
if (isSymlinkPath(resolvedConfigHome, lstatSync) || isSymlinkPath(manifestPath, lstatSync)) {
|
|
247
|
+
return { scope: scopeId, configHome: resolvedConfigHome, ...notInstalledScopeRecordFields() };
|
|
248
|
+
}
|
|
249
|
+
let manifest;
|
|
250
|
+
try {
|
|
251
|
+
const readManifest = opts.readManifest ?? readInstallManifest;
|
|
252
|
+
manifest = readManifest(resolvedConfigHome);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// C14: manifest read/probe failure degrades to not-installed, never throws.
|
|
256
|
+
// Deliberately a BARE catch, unlike the stem-derivation catch below: any
|
|
257
|
+
// read failure at all (EACCES, ENOENT-after-race, a corrupt filesystem)
|
|
258
|
+
// legitimately means "cannot tell whether installed" (design row C14), so
|
|
259
|
+
// there is no error TYPE here that should instead propagate.
|
|
260
|
+
return { scope: scopeId, configHome: resolvedConfigHome, ...notInstalledScopeRecordFields() };
|
|
261
|
+
}
|
|
262
|
+
const installed = manifest.manifestVersion !== null; // C9: presence, never the new fields
|
|
263
|
+
const declaredRuntime = manifest.runtime;
|
|
264
|
+
const declaredScope = manifest.scope;
|
|
265
|
+
const declaredRuntimeMatchesProbe = declaredRuntime === null ? null : declaredRuntime === runtime;
|
|
266
|
+
const declaredScopeMatchesProbe = declaredScope === null ? null : declaredScope === scopeId;
|
|
267
|
+
let stems = [];
|
|
268
|
+
if (installed) {
|
|
269
|
+
try {
|
|
270
|
+
stems = deriveStemsFromManifest(runtime, scopeId, resolvedConfigHome, manifest.files, opts);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (!(error instanceof TypeError))
|
|
274
|
+
throw error;
|
|
275
|
+
// A layout-lookup failure means "we could not enumerate this scope's
|
|
276
|
+
// triggers", NOT "this scope is not installed" — the manifest read
|
|
277
|
+
// already succeeded above, so `installed` and the declared fields
|
|
278
|
+
// stay as reported. Conflating the two would hide a real install.
|
|
279
|
+
// Anything that is not the expected `TypeError` (a genuine bug in
|
|
280
|
+
// `deriveStemsFromManifest`/`resolveRuntimeArtifactLayout`) is
|
|
281
|
+
// rethrown rather than silently degrading to `stems: []` — this
|
|
282
|
+
// matches `resolveInstalledSurfaces`'s own `TypeError` narrowing
|
|
283
|
+
// below, so the two catches cannot drift apart.
|
|
284
|
+
stems = [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
scope: scopeId,
|
|
289
|
+
configHome: resolvedConfigHome,
|
|
290
|
+
installed,
|
|
291
|
+
manifestVersion: manifest.manifestVersion,
|
|
292
|
+
declaredRuntime,
|
|
293
|
+
declaredScope,
|
|
294
|
+
declaredScopeMatchesProbe,
|
|
295
|
+
declaredRuntimeMatchesProbe,
|
|
296
|
+
stems,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Resolve one runtime's full installed surface. A `resolveScope` `TypeError`
|
|
301
|
+
* (unknown runtime, or `configHome.kind === 'none'`, e.g. vscode) propagates
|
|
302
|
+
* from here uncaught — `resolveInstalledSurfaces` decides whether that
|
|
303
|
+
* means "skip" (C7 sweep) or "propagate" (explicit single-runtime ask, C8).
|
|
304
|
+
*/
|
|
305
|
+
function resolveOneRuntime(runtime, opts) {
|
|
306
|
+
const scopes = install_scope_cjs_1.SCOPE_ORDER.map((scopeId) => {
|
|
307
|
+
const resolved = (0, install_scope_cjs_1.resolveScope)({
|
|
308
|
+
id: scopeId,
|
|
309
|
+
runtime,
|
|
310
|
+
env: opts.env,
|
|
311
|
+
home: opts.home,
|
|
312
|
+
existsSync: opts.existsSync,
|
|
313
|
+
cwd: opts.cwd,
|
|
314
|
+
});
|
|
315
|
+
return buildScopeRecord(runtime, scopeId, resolved.configHome, opts);
|
|
316
|
+
});
|
|
317
|
+
// C12: dedupe by resolved configHome BEFORE the trigger call, keeping the
|
|
318
|
+
// higher-ranked (global, first in SCOPE_ORDER) scope. Both scope RECORDS
|
|
319
|
+
// above are unaffected — only the scope list handed to resolveTriggerSurface
|
|
320
|
+
// is deduped.
|
|
321
|
+
const seenConfigHomes = new Set();
|
|
322
|
+
const triggerScopeIds = [];
|
|
323
|
+
for (const record of scopes) {
|
|
324
|
+
if (!record.installed)
|
|
325
|
+
continue;
|
|
326
|
+
if (seenConfigHomes.has(record.configHome))
|
|
327
|
+
continue;
|
|
328
|
+
seenConfigHomes.add(record.configHome);
|
|
329
|
+
triggerScopeIds.push(record.scope);
|
|
330
|
+
}
|
|
331
|
+
const stemUnion = new Set();
|
|
332
|
+
for (const scopeId of triggerScopeIds) {
|
|
333
|
+
const record = scopes.find((s) => s.scope === scopeId);
|
|
334
|
+
for (const stem of record?.stems ?? [])
|
|
335
|
+
stemUnion.add(stem);
|
|
336
|
+
}
|
|
337
|
+
const triggers = triggerScopeIds.length > 0
|
|
338
|
+
? resolveTriggerSurface(runtime, triggerScopeIds, {
|
|
339
|
+
stems: [...stemUnion].sort(),
|
|
340
|
+
registry: opts.registry,
|
|
341
|
+
})
|
|
342
|
+
: [];
|
|
343
|
+
return { runtime, scopes, triggers };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Read-only. Probes both install scopes for a runtime (or every registered
|
|
347
|
+
* runtime, sorted by id, when `runtime` is omitted), reads each scope's
|
|
348
|
+
* manifest, and resolves the trigger surface across the scopes that are
|
|
349
|
+
* actually installed on this machine. See the module-level comment for the
|
|
350
|
+
* non-obvious choices (probe-not-declared keying, report-don't-correct, one
|
|
351
|
+
* trigger call over installed scopes only).
|
|
352
|
+
*
|
|
353
|
+
* Pure with respect to caller-visible state: builds fresh arrays/objects on
|
|
354
|
+
* every call (C15) and performs no writes. Filesystem reads happen only via
|
|
355
|
+
* `resolveScope`'s injected `existsSync`/`env`/`home`/`cwd` and via
|
|
356
|
+
* `readManifest` (default: `readInstallManifest`).
|
|
357
|
+
*
|
|
358
|
+
* @throws {TypeError} when `runtime` is given explicitly and it is unknown,
|
|
359
|
+
* or has no installable config directory (`configHome.kind === 'none'`,
|
|
360
|
+
* e.g. vscode) — same contract `resolveScope` throws. In the all-runtimes
|
|
361
|
+
* sweep (`runtime` omitted), a runtime that would throw this same
|
|
362
|
+
* `TypeError` is skipped instead, so one non-installable runtime cannot
|
|
363
|
+
* kill the sweep (C7/C8). Any other error type is never swallowed here.
|
|
364
|
+
*/
|
|
365
|
+
function resolveInstalledSurfaces(runtime, opts = {}) {
|
|
366
|
+
if (typeof runtime === 'string') {
|
|
367
|
+
return [resolveOneRuntime(runtime, opts)];
|
|
368
|
+
}
|
|
369
|
+
const results = [];
|
|
370
|
+
for (const runtimeId of listRegisteredRuntimeIds(opts)) {
|
|
371
|
+
try {
|
|
372
|
+
results.push(resolveOneRuntime(runtimeId, opts));
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof TypeError)
|
|
376
|
+
continue; // C8: sweep skips, never dies
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
@@ -16,51 +16,61 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
16
16
|
const installer_migration_authoring_cjs_1 = require("./installer-migration-authoring.cjs");
|
|
17
17
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
18
|
const clock_cjs_1 = require("./clock.cjs");
|
|
19
|
+
const install_scope_cjs_1 = require("./install-scope.cjs");
|
|
20
|
+
// #2874 (ADR-58 cleanup phase): this file is the ~1200-line migration
|
|
21
|
+
// plan/apply/rollback/lock/journal engine — almost none of it is on the
|
|
22
|
+
// installRuntimeArtifacts call tree. Only `readInstallManifest` and
|
|
23
|
+
// `classifyArtifact` are reached (via install-engine.cts's
|
|
24
|
+
// _migrateLegacyOpencodeCommandDir and retired-artifact-cleanup.cts's
|
|
25
|
+
// pruneRetiredRuntimeArtifacts), so only those two entry points — plus their
|
|
26
|
+
// shared `readJsonIfPresent` helper and `classifyArtifact`'s `sha256File`
|
|
27
|
+
// hashing helper — are routed through the injectable seam. Everything else
|
|
28
|
+
// in this file (locking, journal, apply/rollback, migration discovery)
|
|
29
|
+
// keeps using real `fs` directly: it is not reachable from
|
|
30
|
+
// installRuntimeArtifacts, so routing it would grow this seam past what
|
|
31
|
+
// AC2 actually requires. See install-fs-adapter.cts's module doc.
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
|
+
const installFsAdapter = require("./install-fs-adapter.cjs");
|
|
34
|
+
const { installFs } = installFsAdapter;
|
|
19
35
|
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
|
20
36
|
const INSTALL_STATE_NAME = 'gsd-install-state.json';
|
|
21
37
|
const INSTALL_MIGRATION_LOCK_NAME = 'gsd-install-migration.lock';
|
|
22
38
|
const DEFAULT_MIGRATIONS_DIR = node_path_1.default.join(__dirname, 'installer-migrations');
|
|
23
39
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
24
40
|
const STRICT_JSON = Symbol('strict-json');
|
|
41
|
+
// #2874: routed through installFs()'s openSync/readSync/closeSync trio
|
|
42
|
+
// instead of importing `node:fs` directly, so classifyArtifact — reachable
|
|
43
|
+
// from installRuntimeArtifacts — can be exercised against an injected
|
|
44
|
+
// adapter. This function was briefly converted to a single
|
|
45
|
+
// `installFs().readFileSync` call (buffering the whole file); that broke
|
|
46
|
+
// tests/installer-migrations.test.cjs's "classifies large files without
|
|
47
|
+
// loading the whole file through readFileSync", which monkeypatches real
|
|
48
|
+
// fs.readFileSync to throw for the file under test and asserts hashing still
|
|
49
|
+
// succeeds — an explicit, pre-existing contract that large files must be
|
|
50
|
+
// streamed, not buffered. Restored to the original raw-fd streaming shape,
|
|
51
|
+
// now going through the adapter instead of `node:fs` directly. This is the
|
|
52
|
+
// ONLY call site of sha256File in this file (confirmed by inspection) — no
|
|
53
|
+
// other caller is affected.
|
|
25
54
|
function sha256File(filePath) {
|
|
26
55
|
const hash = node_crypto_1.default.createHash('sha256');
|
|
27
56
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
28
|
-
const fd =
|
|
57
|
+
const fd = installFs().openSync(filePath, 'r');
|
|
29
58
|
try {
|
|
30
59
|
while (true) {
|
|
31
|
-
const bytesRead =
|
|
60
|
+
const bytesRead = installFs().readSync(fd, buffer, 0, buffer.length, null);
|
|
32
61
|
if (bytesRead === 0)
|
|
33
62
|
break;
|
|
34
63
|
hash.update(buffer.subarray(0, bytesRead));
|
|
35
64
|
}
|
|
36
65
|
}
|
|
37
66
|
finally {
|
|
38
|
-
|
|
67
|
+
installFs().closeSync(fd);
|
|
39
68
|
}
|
|
40
69
|
return hash.digest('hex');
|
|
41
70
|
}
|
|
42
71
|
function sha256Text(value) {
|
|
43
72
|
return node_crypto_1.default.createHash('sha256').update(value).digest('hex');
|
|
44
73
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Copy a managed path for the rollback snapshot or the user-facing backup,
|
|
47
|
-
* WITHOUT dereferencing a symlink.
|
|
48
|
-
*
|
|
49
|
-
* `fs.copyFileSync` follows symlinks, so a managed path that has been replaced
|
|
50
|
-
* by a link (tampering, or an unexpected user layout) would have had the
|
|
51
|
-
* LINK TARGET's bytes copied into `gsd-migration-journal/…-backups/` — e.g. a
|
|
52
|
-
* `gsd.cjs` symlinked at `~/.ssh/id_rsa` would land that key's contents in the
|
|
53
|
-
* backup tree. Nothing GSD installs is ever a symlink, so the faithful snapshot
|
|
54
|
-
* of a symlinked managed path is the link itself: recreating it preserves
|
|
55
|
-
* rollback fidelity (restore re-creates the same link) while never reading the
|
|
56
|
-
* referent. Deletion was already safe — `fs.rmSync` unlinks the link, never the
|
|
57
|
-
* target.
|
|
58
|
-
*
|
|
59
|
-
* Windows note: `fs.symlinkSync` can throw EPERM for unprivileged users. That
|
|
60
|
-
* surfaces as an apply failure and triggers the normal rollback path, which is
|
|
61
|
-
* the correct outcome — refusing to proceed beats silently copying referent
|
|
62
|
-
* bytes.
|
|
63
|
-
*/
|
|
64
74
|
/**
|
|
65
75
|
* Evaluate and, if safe, perform a `remove-empty-dir` action against `fullPath`.
|
|
66
76
|
*
|
|
@@ -133,23 +143,54 @@ function evaluateRemoveEmptyDir(configDir, fullPath) {
|
|
|
133
143
|
return 'left-in-place';
|
|
134
144
|
}
|
|
135
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Copy a managed path for the rollback snapshot or the user-facing backup,
|
|
148
|
+
* WITHOUT dereferencing a symlink.
|
|
149
|
+
*
|
|
150
|
+
* `fs.copyFileSync` follows symlinks, so a managed path that has been replaced
|
|
151
|
+
* by a link (tampering, or an unexpected user layout) would have had the
|
|
152
|
+
* LINK TARGET's bytes copied into `gsd-migration-journal/…-backups/` — e.g. a
|
|
153
|
+
* `gsd.cjs` symlinked at `~/.ssh/id_rsa` would land that key's contents in the
|
|
154
|
+
* backup tree. Nothing GSD installs is ever a symlink, so the faithful snapshot
|
|
155
|
+
* of a symlinked managed path is the link itself: recreating it preserves
|
|
156
|
+
* rollback fidelity (restore re-creates the same link) while never reading the
|
|
157
|
+
* referent. Deletion was already safe — `fs.rmSync` unlinks the link, never the
|
|
158
|
+
* target.
|
|
159
|
+
*
|
|
160
|
+
* Windows note: `fs.symlinkSync` can throw EPERM for unprivileged users. That
|
|
161
|
+
* surfaces as an apply failure and triggers the normal rollback path, which is
|
|
162
|
+
* the correct outcome — refusing to proceed beats silently copying referent
|
|
163
|
+
* bytes.
|
|
164
|
+
*
|
|
165
|
+
* #2875 (epic #2866 Phase 6): all five fs calls routed through `installFs()`
|
|
166
|
+
* so this primitive can be reused on the routed install path (by
|
|
167
|
+
* user-artifact-staging.cts) without punching a hole through the seam Phase 5
|
|
168
|
+
* built. Every EXISTING caller of this function is on the migration
|
|
169
|
+
* plan/apply/rollback tree, which never wraps a call in `withInstallFs` — the
|
|
170
|
+
* ambient adapter there resolves to real `node:fs` by default, so this
|
|
171
|
+
* routing is behavior-preserving for them (test-matrix D2).
|
|
172
|
+
*/
|
|
136
173
|
function copyPreservingSymlink(srcPath, destPath) {
|
|
137
|
-
if (
|
|
174
|
+
if (installFs().lstatSync(srcPath).isSymbolicLink()) {
|
|
138
175
|
// symlinkSync fails with EEXIST on an occupied path, so clear it first.
|
|
139
176
|
// Scoped to this branch on purpose: the regular-file path below keeps
|
|
140
177
|
// copyFileSync's overwrite-in-place, so a mid-restore failure cannot leave
|
|
141
178
|
// the destination destroyed.
|
|
142
|
-
|
|
143
|
-
|
|
179
|
+
installFs().rmSync(destPath, { force: true });
|
|
180
|
+
installFs().symlinkSync(installFs().readlinkSync(srcPath), destPath);
|
|
144
181
|
return;
|
|
145
182
|
}
|
|
146
|
-
|
|
183
|
+
installFs().copyFileSync(srcPath, destPath);
|
|
147
184
|
}
|
|
185
|
+
// Shared by readInstallManifest (on the installRuntimeArtifacts call tree —
|
|
186
|
+
// routed) and readInstallState/readJson (not on that call tree — the
|
|
187
|
+
// ambient default resolves to real fs for those, unchanged). Routing once
|
|
188
|
+
// here is safe for all three callers.
|
|
148
189
|
function readJsonIfPresent(filePath, fallback) {
|
|
149
|
-
if (!
|
|
190
|
+
if (!installFs().existsSync(filePath))
|
|
150
191
|
return fallback;
|
|
151
192
|
try {
|
|
152
|
-
return JSON.parse(
|
|
193
|
+
return JSON.parse(installFs().readFileSync(filePath, 'utf8'));
|
|
153
194
|
}
|
|
154
195
|
catch (error) {
|
|
155
196
|
if (fallback === STRICT_JSON) {
|
|
@@ -158,17 +199,81 @@ function readJsonIfPresent(filePath, fallback) {
|
|
|
158
199
|
return fallback;
|
|
159
200
|
}
|
|
160
201
|
}
|
|
202
|
+
/** Lowest manifest schema version that records `runtime`/`scope` (#2872). */
|
|
203
|
+
const MANIFEST_SCHEMA_VERSION = 2;
|
|
204
|
+
/**
|
|
205
|
+
* Longest `runtime` string this reader will report. Real runtime ids are
|
|
206
|
+
* registry keys (`claude`, `antigravity`, `kimi-code` — 11 chars at the
|
|
207
|
+
* longest), so this loses nothing legitimate; it exists because the manifest
|
|
208
|
+
* is attacker-influenceable (a project-local one lives inside a repository a
|
|
209
|
+
* user may merely have cloned) and the value reaches a consumer that renders
|
|
210
|
+
* it. Same 64-char convention as `truncatePostureValue`
|
|
211
|
+
* (`agent-install-check.cts`), deliberately, so the subsystem caps reported
|
|
212
|
+
* values one way.
|
|
213
|
+
*/
|
|
214
|
+
const MAX_REPORTED_RUNTIME_LENGTH = 64;
|
|
215
|
+
/**
|
|
216
|
+
* A manifest's `runtime` is reported as a FACT about the file — it is
|
|
217
|
+
* deliberately NOT validated against the capability registry, because an
|
|
218
|
+
* unregistered id is exactly the kind of mismatch the Installed Surface
|
|
219
|
+
* Resolver exists to surface (#2872 design row B8). It is, however, LENGTH
|
|
220
|
+
* bounded: "report the fact" never required "report unbounded bytes".
|
|
221
|
+
*/
|
|
222
|
+
function normalizeReportedRuntime(raw) {
|
|
223
|
+
if (typeof raw !== 'string')
|
|
224
|
+
return null;
|
|
225
|
+
if (raw.trim() === '')
|
|
226
|
+
return null;
|
|
227
|
+
return raw.length > MAX_REPORTED_RUNTIME_LENGTH
|
|
228
|
+
? `${raw.slice(0, MAX_REPORTED_RUNTIME_LENGTH)}…`
|
|
229
|
+
: raw;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Normalize a raw `manifestVersion`. Only a finite integer >= 1 is a version
|
|
233
|
+
* claim; everything else (absent, `"2"`, `0`, `-1`, `2.5`, `NaN`, `Infinity`)
|
|
234
|
+
* reads as `1` — a pre-#2872 manifest. Liberal in what it accepts, but the
|
|
235
|
+
* normalization is a stated value rather than a silent guess: a caller can
|
|
236
|
+
* always tell v1 (`1`) from "no manifest at all" (`null`).
|
|
237
|
+
*/
|
|
238
|
+
function normalizeManifestVersion(raw) {
|
|
239
|
+
if (typeof raw !== 'number')
|
|
240
|
+
return 1;
|
|
241
|
+
if (!Number.isInteger(raw))
|
|
242
|
+
return 1;
|
|
243
|
+
if (raw < 1)
|
|
244
|
+
return 1;
|
|
245
|
+
return raw;
|
|
246
|
+
}
|
|
161
247
|
function readInstallManifest(configDir) {
|
|
162
248
|
const manifest = readJsonIfPresent(node_path_1.default.join(configDir, MANIFEST_NAME), null);
|
|
163
|
-
|
|
164
|
-
|
|
249
|
+
// `typeof [] === 'object'` in JS, so a bare `typeof !== 'object'` guard lets
|
|
250
|
+
// a top-level JSON array (valid JSON, but not the manifest's documented
|
|
251
|
+
// object shape) fall through to the field reads below — `m.manifestVersion`
|
|
252
|
+
// reads `undefined` off an array, which `normalizeManifestVersion` then
|
|
253
|
+
// reports as `1` (a v1 manifest), misclassifying "not an object" as
|
|
254
|
+
// "installed". `Array.isArray` closes that gap explicitly rather than
|
|
255
|
+
// relying on the object-shape checks below to catch it incidentally.
|
|
256
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
257
|
+
return {
|
|
258
|
+
version: null,
|
|
259
|
+
timestamp: null,
|
|
260
|
+
mode: null,
|
|
261
|
+
files: {},
|
|
262
|
+
manifestVersion: null,
|
|
263
|
+
runtime: null,
|
|
264
|
+
scope: null,
|
|
265
|
+
};
|
|
165
266
|
}
|
|
166
267
|
const m = manifest;
|
|
268
|
+
const rawRuntime = m.runtime;
|
|
167
269
|
return {
|
|
168
270
|
version: typeof m.version === 'string' ? m.version : null,
|
|
169
271
|
timestamp: typeof m.timestamp === 'string' ? m.timestamp : null,
|
|
170
272
|
mode: typeof m.mode === 'string' ? m.mode : null,
|
|
171
273
|
files: m.files && typeof m.files === 'object' ? m.files : {},
|
|
274
|
+
manifestVersion: normalizeManifestVersion(m.manifestVersion),
|
|
275
|
+
runtime: normalizeReportedRuntime(rawRuntime),
|
|
276
|
+
scope: (0, install_scope_cjs_1.isInstallScopeId)(m.scope) ? m.scope : null,
|
|
172
277
|
};
|
|
173
278
|
}
|
|
174
279
|
function readInstallState(configDir) {
|
|
@@ -235,7 +340,7 @@ function classifyArtifact(configDir, relPath, manifest) {
|
|
|
235
340
|
const normalized = normalizeRelPath(relPath);
|
|
236
341
|
const originalHash = manifest.files[normalized] || null;
|
|
237
342
|
const fullPath = node_path_1.default.join(configDir, normalized);
|
|
238
|
-
if (!
|
|
343
|
+
if (!installFs().existsSync(fullPath)) {
|
|
239
344
|
return { classification: originalHash ? 'managed-missing' : 'missing', originalHash, currentHash: null };
|
|
240
345
|
}
|
|
241
346
|
const currentHash = sha256File(fullPath);
|
|
@@ -937,8 +1042,10 @@ module.exports = {
|
|
|
937
1042
|
acquireInstallMigrationLock,
|
|
938
1043
|
applyInstallerMigrationPlan,
|
|
939
1044
|
classifyArtifact,
|
|
1045
|
+
copyPreservingSymlink,
|
|
940
1046
|
discoverInstallerMigrations,
|
|
941
1047
|
evaluateRemoveEmptyDir,
|
|
1048
|
+
MANIFEST_SCHEMA_VERSION,
|
|
942
1049
|
migrationChecksum,
|
|
943
1050
|
planInstallerMigrations,
|
|
944
1051
|
readInstallManifest,
|
package/gsd-core/bin/lib/io.cjs
CHANGED
|
@@ -186,11 +186,21 @@ const ERROR_REASON = Object.freeze({
|
|
|
186
186
|
PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
|
|
187
187
|
PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
|
|
188
188
|
SUMMARY_NO_PLANNING: 'summary_no_planning',
|
|
189
|
+
// #3579: workstream-mode fail-safe guards (init.progress, phase.complete) —
|
|
190
|
+
// distinguishes "no marker/pointer anywhere" from "a marker exists but
|
|
191
|
+
// didn't resolve" so a JSON-error-mode caller can branch on `reason`
|
|
192
|
+
// instead of regexing the human message.
|
|
193
|
+
WORKSTREAM_MODE_NONE_ACTIVE: 'workstream_mode_none_active',
|
|
194
|
+
WORKSTREAM_MODE_MARKER_UNRESOLVED: 'workstream_mode_marker_unresolved',
|
|
189
195
|
// graphify
|
|
190
196
|
GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
|
|
191
197
|
GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
|
|
192
198
|
// hooks
|
|
193
199
|
HOOKS_OPT_OUT: 'hooks_opt_out',
|
|
200
|
+
// commit-docs-guard (#3588)
|
|
201
|
+
COMMIT_DOCS_GUARD_NOT_A_REPO: 'commit_docs_guard_not_a_repo',
|
|
202
|
+
COMMIT_DOCS_GUARD_FOREIGN_HOOK: 'commit_docs_guard_foreign_hook',
|
|
203
|
+
COMMIT_DOCS_GUARD_HOOKS_PATH_SET: 'commit_docs_guard_hooks_path_set',
|
|
194
204
|
// security-scan
|
|
195
205
|
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
196
206
|
// generic
|
|
@@ -26,6 +26,7 @@ exports.stripTaggedBlocks = stripTaggedBlocks;
|
|
|
26
26
|
exports.replaceSection = replaceSection;
|
|
27
27
|
exports.withSection = withSection;
|
|
28
28
|
exports.deleteSection = deleteSection;
|
|
29
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
29
30
|
// ─── stripFencedCode ──────────────────────────────────────────────────────────
|
|
30
31
|
/**
|
|
31
32
|
* CommonMark-correct fenced-code-block stripper.
|
|
@@ -792,7 +793,7 @@ function extractTaggedBlocks(content, tagName, allowAttributes = false) {
|
|
|
792
793
|
* open>` marks the ACTIVE milestone and must be preserved, not stripped (#557).
|
|
793
794
|
*/
|
|
794
795
|
function taggedBlockPattern(tagName, flags, allowAttributes) {
|
|
795
|
-
const esc =
|
|
796
|
+
const esc = (0, pattern_cjs_1.escapeRegex)(tagName);
|
|
796
797
|
const open = allowAttributes ? `<${esc}(?:\\s[^>]{0,1000})?>` : `<${esc}>`;
|
|
797
798
|
const boundary = allowAttributes ? `<${esc}[\\s>]` : `<${esc}>`;
|
|
798
799
|
return new RegExp(`${open}((?:(?!${boundary})[\\s\\S])*?)</${esc}>`, flags);
|