@opengsd/gsd-core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -28,8 +28,10 @@ const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs"
|
|
|
28
28
|
const runtimeNamePolicy = require("./runtime-name-policy.cjs");
|
|
29
29
|
const installProfiles = require("./install-profiles.cjs");
|
|
30
30
|
const installerMigrations = require("./installer-migrations.cjs");
|
|
31
|
+
const retiredArtifactCleanup = require("./retired-artifact-cleanup.cjs");
|
|
31
32
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
32
33
|
const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs");
|
|
34
|
+
const commonjs_marker_cjs_1 = require("./commonjs-marker.cjs");
|
|
33
35
|
const { processAttribution } = runtimeArtifactConversion;
|
|
34
36
|
// resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so
|
|
35
37
|
// test stubs that monkeypatch the module's exports are seen at call time.
|
|
@@ -315,13 +317,25 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
|
|
|
315
317
|
if (!saved || !saved.has('dev-preferences.md'))
|
|
316
318
|
return false;
|
|
317
319
|
let skillDir;
|
|
320
|
+
// #2911: the actual install root the skill dir resolves under — defaults to
|
|
321
|
+
// targetDir, but a skills-kind `home` override (e.g. Codex -> $HOME/.agents)
|
|
322
|
+
// moves it entirely outside targetDir. Every confinement/guard check below
|
|
323
|
+
// must confine against installRoot, not targetDir, or it would flag the
|
|
324
|
+
// legitimate override destination as an escape.
|
|
325
|
+
let installRoot = targetDir;
|
|
318
326
|
if (runtime) {
|
|
319
327
|
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
320
328
|
const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
|
|
321
329
|
if (!skillsKindEntry)
|
|
322
330
|
return false; // runtime has no skills layout at this scope (e.g. cline local)
|
|
323
331
|
const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences';
|
|
324
|
-
|
|
332
|
+
// #2911: same destination-root defect as _copyStaged/applySurface — honor
|
|
333
|
+
// skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
|
|
334
|
+
// -> $HOME/.agents) instead of always resolving against targetDir, so a
|
|
335
|
+
// legacy dev-preferences migration lands in the SAME tree the installer
|
|
336
|
+
// and surface-apply use. Runtimes with no `home` override are unaffected.
|
|
337
|
+
installRoot = skillsKindEntry.home ?? targetDir;
|
|
338
|
+
skillDir = node_path_1.default.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath), stemName);
|
|
325
339
|
}
|
|
326
340
|
else {
|
|
327
341
|
// Legacy fallback for callers that have not yet been updated to pass runtime
|
|
@@ -330,11 +344,11 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
|
|
|
330
344
|
const skillFile = node_path_1.default.join(skillDir, 'SKILL.md');
|
|
331
345
|
if (node_fs_1.default.existsSync(skillFile))
|
|
332
346
|
return false;
|
|
333
|
-
// Symlink-escape guard: reject if any path component between
|
|
334
|
-
// skillDir is a symlink that would redirect writes outside the
|
|
347
|
+
// Symlink-escape guard: reject if any path component between installRoot and
|
|
348
|
+
// skillDir is a symlink that would redirect writes outside the install root.
|
|
335
349
|
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
336
|
-
if (hasExistingSymlinkBetween(node_path_1.default.resolve(
|
|
337
|
-
throw new Error(`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${
|
|
350
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), skillDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
351
|
+
throw new Error(`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
|
|
338
352
|
}
|
|
339
353
|
try {
|
|
340
354
|
node_fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
@@ -675,6 +689,10 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
|
|
|
675
689
|
* (fail closed), matching the layout resolver's own optional-registry contract.
|
|
676
690
|
*/
|
|
677
691
|
function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, capabilityRegistry) {
|
|
692
|
+
// A removed descriptor kind is no longer visited by the layout loop, so it
|
|
693
|
+
// cannot prune its own previous output. Clean manifest-proven retired files
|
|
694
|
+
// before materializing the current layout (#2644).
|
|
695
|
+
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
|
|
678
696
|
// Combined-family runtimes (OpenCode/Kilo, ADR-1239 / #2087): route through
|
|
679
697
|
// the dedicated combined commands+skills+plugin orchestrator instead of the
|
|
680
698
|
// generic layout-driven loop below, mirroring the bespoke install path that
|
|
@@ -849,12 +867,19 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
849
867
|
if (!converter) {
|
|
850
868
|
throw new TypeError(`installOpencodeFamilySkills: unknown skills converter '${String(converterName)}' for runtime '${runtime}'`);
|
|
851
869
|
}
|
|
852
|
-
|
|
853
|
-
//
|
|
854
|
-
//
|
|
870
|
+
// #2911: same destination-root defect as _copyStaged/migrateLegacyDevPreferencesToSkill
|
|
871
|
+
// — honor skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
|
|
872
|
+
// -> $HOME/.agents) instead of always resolving against targetDir, so this bespoke
|
|
873
|
+
// OpenCode/Kilo writer lands in the SAME tree the installer and surface-apply use.
|
|
874
|
+
// Runtimes with no `home` override (opencode, kilo today) are unaffected. Must stay
|
|
875
|
+
// in lockstep with the sibling writers — the destination-parity test enforces it.
|
|
876
|
+
const installRoot = skillsKindEntry.home ?? targetDir;
|
|
877
|
+
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath);
|
|
878
|
+
// Symlink-escape guard: reject if any path component between installRoot and
|
|
879
|
+
// dest is a symlink that would redirect writes outside the install root.
|
|
855
880
|
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
856
|
-
if (hasExistingSymlinkBetween(node_path_1.default.resolve(
|
|
857
|
-
throw new Error(`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${
|
|
881
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
882
|
+
throw new Error(`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
|
|
858
883
|
}
|
|
859
884
|
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
860
885
|
// Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
|
|
@@ -1037,6 +1062,30 @@ function _installNativePluginIfDeclared(runtime, configDir, behaviors, src) {
|
|
|
1037
1062
|
const destPath = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, node_path_1.default.join(np.dir, np.file));
|
|
1038
1063
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(destPath), { recursive: true });
|
|
1039
1064
|
node_fs_1.default.copyFileSync(pluginSrc, destPath);
|
|
1065
|
+
// #2544: the staged adapter is a `.js` file, so Node decides its module
|
|
1066
|
+
// type by walking up for the nearest package.json. It used to find the
|
|
1067
|
+
// marker the installer wrote at the config root — the write that
|
|
1068
|
+
// clobbered user-authored files. Pin it from the plugin's own directory
|
|
1069
|
+
// instead, leaving the config root alone. The marker cannot disturb
|
|
1070
|
+
// plugin discovery: OpenCode auto-discovers `plugins/*.{ts,js}` and pi's
|
|
1071
|
+
// isExtensionFile() accepts only `.ts`/`.js` (see installer-migration
|
|
1072
|
+
// 006), so a package.json here is never treated as a plugin. Never
|
|
1073
|
+
// written over a package.json GSD does not own — but when one is already
|
|
1074
|
+
// there, say so: the adapter is CommonJS and will not load under a
|
|
1075
|
+
// foreign `"type": "module"`, and a silent no-op would leave every guard
|
|
1076
|
+
// the adapter spawns dead with no diagnostic (the #2305 failure shape).
|
|
1077
|
+
const markerOutcome = (0, commonjs_marker_cjs_1.ensureCommonJsMarker)(node_path_1.default.dirname(destPath));
|
|
1078
|
+
if (markerOutcome === 'preserved-foreign') {
|
|
1079
|
+
console.warn(` ⚠ ${np.dir}/package.json is not GSD's CommonJS marker — left untouched. `
|
|
1080
|
+
+ `If it declares "type": "module", ${np.file} will not load.`);
|
|
1081
|
+
}
|
|
1082
|
+
else if (markerOutcome === 'failed') {
|
|
1083
|
+
// Best-effort, never fatal: an unwritable plugin dir must not abort the
|
|
1084
|
+
// install. Same warn-and-continue posture as the foreign-marker branch —
|
|
1085
|
+
// the adapter is staged either way, it just may not resolve as CommonJS.
|
|
1086
|
+
console.warn(` ⚠ Could not write ${np.dir}/package.json (CommonJS marker) — install continued. `
|
|
1087
|
+
+ `If the config root declares "type": "module", ${np.file} will not load.`);
|
|
1088
|
+
}
|
|
1040
1089
|
}
|
|
1041
1090
|
}
|
|
1042
1091
|
}
|
|
@@ -1180,6 +1229,11 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
|
|
|
1180
1229
|
* @param scope
|
|
1181
1230
|
*/
|
|
1182
1231
|
function uninstallRuntimeArtifacts(runtime, configDir, scope) {
|
|
1232
|
+
// A retired descriptor kind is absent from the current uninstall plan, just
|
|
1233
|
+
// as it is absent from the install plan. Sweep manifest-proven output from
|
|
1234
|
+
// retired kinds before removing the current layout so a direct uninstall
|
|
1235
|
+
// cannot leave stale runtime surfaces behind (#2644).
|
|
1236
|
+
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
|
|
1183
1237
|
// Legacy cleanup before layout-driven removal (scope-aware to avoid
|
|
1184
1238
|
// removing Claude local commands/gsd/ which is the primary install dir).
|
|
1185
1239
|
// Returns saved user artifacts so we can migrate AFTER layout removal
|
|
@@ -20,6 +20,13 @@ const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs
|
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
21
|
const conversionModule = require("./runtime-artifact-conversion.cjs");
|
|
22
22
|
const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, } = conversionModule;
|
|
23
|
+
// #2995 (epic #1671 Phase 6.4): agent bodies join the fragment model. Markers are
|
|
24
|
+
// stripped at emit BEFORE any path rewrite or converter runs, so a `.claude/` ->
|
|
25
|
+
// `.windsurf/` regex can never reach inside a marker attribute and corrupt it —
|
|
26
|
+
// the same ordering #2930 established for workflows.
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const workflowFragmentsModule = require("./workflow-fragments.cjs");
|
|
29
|
+
const { composeWorkflow: _composeWorkflow } = workflowFragmentsModule;
|
|
23
30
|
// ---------------------------------------------------------------------------
|
|
24
31
|
// Profile definitions
|
|
25
32
|
// ---------------------------------------------------------------------------
|
|
@@ -329,6 +336,19 @@ function stageSkillsForProfile(srcDir, resolvedProfile) {
|
|
|
329
336
|
* For tiered profiles, copies only agents whose full stem (e.g. 'gsd-planner')
|
|
330
337
|
* is in resolvedProfile.agents — which is populated by resolveProfile() from
|
|
331
338
|
* the _calls_agents_* entries in the manifest.
|
|
339
|
+
*
|
|
340
|
+
* ⚠️ RAW STAGER — ITS OUTPUT IS NOT EMISSION-READY (#2995). This stager performs a
|
|
341
|
+
* plain `fs.copyFileSync` and — under the default `full` profile — short-circuits
|
|
342
|
+
* and returns the real source directory unstaged. It does NOT strip `gsd:section`
|
|
343
|
+
* markers. It is still called, by `bin/install.js`'s `_stageAgents`, whose output
|
|
344
|
+
* feeds the inline agent loop and `installCodexConfig`; both of those compose the
|
|
345
|
+
* content themselves before writing, so the raw output never reaches disk. What
|
|
346
|
+
* changed in #2995 is that `agentsKind` and `kimiAgentsKind` no longer use it —
|
|
347
|
+
* they route through `stageAgentsForRuntimeWithConverter`, which composes.
|
|
348
|
+
*
|
|
349
|
+
* The invariant to preserve: anything that takes this function's output and WRITES
|
|
350
|
+
* it as a runtime artifact must call `composeWorkflow` on each file first, or it
|
|
351
|
+
* ships markers verbatim.
|
|
332
352
|
*/
|
|
333
353
|
function stageAgentsForProfile(srcAgentsDir, resolvedProfile) {
|
|
334
354
|
if (resolvedProfile.skills === '*')
|
|
@@ -796,7 +816,13 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
|
|
|
796
816
|
continue;
|
|
797
817
|
}
|
|
798
818
|
}
|
|
799
|
-
|
|
819
|
+
const agentSourcePath = node_path_1.default.join(srcAgentsDir, entry.name);
|
|
820
|
+
let content = node_fs_1.default.readFileSync(agentSourcePath, 'utf8');
|
|
821
|
+
// #2995: strip gsd:section markers FIRST — before path rewrites, attribution,
|
|
822
|
+
// and the per-runtime converter. Byte-identical (no-op) for an unmarked agent;
|
|
823
|
+
// throws loudly naming the file for a malformed marker, never emitting a
|
|
824
|
+
// half-composed agent.
|
|
825
|
+
content = _composeWorkflow(content, { sourcePath: agentSourcePath });
|
|
800
826
|
if (agentCtx) {
|
|
801
827
|
// ADR-1235 §1: pre-converter cross-cutting (matches inline loop order exactly)
|
|
802
828
|
// Step 1: path rewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
|
|
@@ -109,7 +109,9 @@ function validateInstallerMigrationActions(actions, migration) {
|
|
|
109
109
|
// Ownership and runtime-contract evidence are required by
|
|
110
110
|
// docs/installer-migrations.md#action-types and
|
|
111
111
|
// docs/adr/0008-installer-migration-module.md#runtime-contract-decision.
|
|
112
|
-
|
|
112
|
+
// `remove-empty-dir` carries the same evidence bar as `remove-managed`: it is
|
|
113
|
+
// still a destructive removal, just of a directory node instead of a file.
|
|
114
|
+
if (actType === 'remove-managed' || actType === 'rewrite-json' || actType === 'remove-empty-dir') {
|
|
113
115
|
requireActionEvidence(act, 'ownershipEvidence', migration);
|
|
114
116
|
}
|
|
115
117
|
if (actType === 'rewrite-json') {
|
|
@@ -32,6 +32,7 @@ const VALID_CHOICES = ['keep', 'remove'];
|
|
|
32
32
|
// on-disk `hooks/` directory in both directions: whitelist-but-missing
|
|
33
33
|
// AND shipped-but-not-whitelisted both fail CI.
|
|
34
34
|
exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
|
|
35
|
+
'hooks/gsd-agent-isolation-guard.js',
|
|
35
36
|
'hooks/gsd-check-update-worker.js',
|
|
36
37
|
'hooks/gsd-check-update.js',
|
|
37
38
|
'hooks/gsd-config-reload.js',
|
|
@@ -57,6 +58,7 @@ exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
|
|
|
57
58
|
'hooks/gsd-validate-commit.sh',
|
|
58
59
|
'hooks/gsd-workflow-guard.js',
|
|
59
60
|
'hooks/gsd-worktree-path-guard.js',
|
|
61
|
+
'hooks/gsd-write-guard.js',
|
|
60
62
|
]));
|
|
61
63
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
62
64
|
function installerMigrationActionLabel(action) {
|
|
@@ -74,6 +76,8 @@ function installerMigrationActionLabel(action) {
|
|
|
74
76
|
return 'preserved';
|
|
75
77
|
if (action.type === 'preserve-user')
|
|
76
78
|
return 'preserved';
|
|
79
|
+
if (action.type === 'remove-empty-dir')
|
|
80
|
+
return 'removed';
|
|
77
81
|
if (action.type === 'prompt-user')
|
|
78
82
|
return 'blocked';
|
|
79
83
|
return 'skipped';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installer migration: retire the config-root `{"type":"commonjs"}` marker that
|
|
4
|
+
* pre-#2544 installs wrote over `<configRoot>/package.json`.
|
|
5
|
+
*
|
|
6
|
+
* What old artifact is being retired?
|
|
7
|
+
* `package.json` at the runtime config root. Before #2544,
|
|
8
|
+
* `installSharedHooksBundle(destRootDir)` wrote `{"type":"commonjs"}` there
|
|
9
|
+
* unconditionally on every install and every re-install, to pin GSD's staged
|
|
10
|
+
* `.js` hook scripts to CommonJS via Node's ancestor walk. #2544 moved that
|
|
11
|
+
* marker into the directories GSD actually fills (`hooks/`, and the
|
|
12
|
+
* `nativePlugin.dir` for runtimes declaring one) and stopped writing the
|
|
13
|
+
* config root at all. Without this migration an upgrader keeps BOTH markers:
|
|
14
|
+
* the new one under `hooks/` and the stale one at the root, so the config
|
|
15
|
+
* root stays pinned to CommonJS and the PR's own claim — that GSD no longer
|
|
16
|
+
* writes the shared config root — is false for every install made since the
|
|
17
|
+
* marker was introduced, until the user uninstalls.
|
|
18
|
+
*
|
|
19
|
+
* How do we prove it is GSD-owned?
|
|
20
|
+
* By exact content match, NOT by the manifest. The config-root marker was
|
|
21
|
+
* never recorded in `gsd-file-manifest.json` — `writeManifest()` records
|
|
22
|
+
* `hooks/`, `agents/`, `commands/`, `scripts/`, and the native plugin, and
|
|
23
|
+
* has never had a `manifest.files['package.json']` entry — so
|
|
24
|
+
* `classifyArtifact('package.json')` answers `unknown` and the planner's
|
|
25
|
+
* own guard would downgrade a `remove-managed` to `preserve-user`.
|
|
26
|
+
* This migration therefore supplies the "purpose-built detector for an old
|
|
27
|
+
* GSD-owned shape" that `docs/installer-migrations.md#remove-managed`
|
|
28
|
+
* sanctions, and declares the resulting classification on the action: the
|
|
29
|
+
* file is removed only when its bytes are exactly the marker GSD writes
|
|
30
|
+
* (`{"type":"commonjs"}`, trailing whitespace tolerated). That is the same
|
|
31
|
+
* predicate `removeCommonJsMarker` has always used on the uninstall side, so
|
|
32
|
+
* install, uninstall, and migration cannot drift apart.
|
|
33
|
+
*
|
|
34
|
+
* What happens if the user modified it?
|
|
35
|
+
* Then it is not the marker, and this migration does not touch it. Any
|
|
36
|
+
* `package.json` carrying a `name`, `dependencies`, `scripts`, or any key
|
|
37
|
+
* beyond the single `type` — i.e. every file the #2544 defect destroyed —
|
|
38
|
+
* fails the exact-content test and is left exactly as found. There is
|
|
39
|
+
* deliberately no `backup-and-remove` branch: a modified file here is not a
|
|
40
|
+
* patched GSD artifact, it is somebody else's file.
|
|
41
|
+
*
|
|
42
|
+
* What happens if it is missing?
|
|
43
|
+
* No actions. Fresh post-#2544 installs never wrote it, already-migrated
|
|
44
|
+
* installs no longer have it, and the executor additionally journals a
|
|
45
|
+
* `missing` outcome if it disappears between plan and apply. Idempotent.
|
|
46
|
+
*
|
|
47
|
+
* What runtime and scope does it affect?
|
|
48
|
+
* Every runtime whose config root received the marker — i.e. every runtime
|
|
49
|
+
* not excluded from `installSharedHooksBundle(targetDir)` by
|
|
50
|
+
* `hostBehaviors.skipSharedHooksInstall` and not Codex: antigravity,
|
|
51
|
+
* augment, claude, claude-local, codebuddy, hermes, qwen, kilo, opencode,
|
|
52
|
+
* and pi. The `runtimes` field is OMITTED — the framework's "all runtimes" —
|
|
53
|
+
* rather than carrying that hand-list: a runtime that never received the
|
|
54
|
+
* marker simply has no file to match, so enumerating them would add a second
|
|
55
|
+
* place for the set to drift out of date without changing behavior. Note it
|
|
56
|
+
* must be omitted and not `[]`; see the field's own comment below.
|
|
57
|
+
*
|
|
58
|
+
* ONE DELIBERATE CARVE-OUT — kimi. Kimi's marker was written to its native
|
|
59
|
+
* hook root (`~/.kimi`, `resolveKimiHooksTomlDir`), which is NOT under
|
|
60
|
+
* kimi's `configDir` (its generic Agent-Skills root). Migration relPaths are
|
|
61
|
+
* structurally confined to `configDir` (`validateSafeRelPath` /
|
|
62
|
+
* `ensureInsideConfig`), so this framework cannot address that path at all.
|
|
63
|
+
* Kimi's stale root marker is retired by the installer instead, at the same
|
|
64
|
+
* call site that writes its replacement — see the `kimi-hooks-toml` branch
|
|
65
|
+
* in `bin/install.js`. Named here so the gap is not mistaken for an
|
|
66
|
+
* oversight.
|
|
67
|
+
*
|
|
68
|
+
* Is the action safe in non-interactive install?
|
|
69
|
+
* Yes. `remove-managed` is non-interactive and journaled, the executor takes
|
|
70
|
+
* a rollback snapshot before unlinking, and no branch of this migration can
|
|
71
|
+
* emit `prompt-user`. A file that is not byte-identical to GSD's marker
|
|
72
|
+
* produces no action at all.
|
|
73
|
+
*
|
|
74
|
+
* See docs/installer-migrations.md#shipped-migrations and #action-types.
|
|
75
|
+
*/
|
|
76
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
77
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
78
|
+
};
|
|
79
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
80
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
81
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
82
|
+
/** The config-root path the pre-#2544 installer wrote, relative to configDir. */
|
|
83
|
+
const STALE_ROOT_MARKER = 'package.json';
|
|
84
|
+
/**
|
|
85
|
+
* The exact marker content GSD wrote. Duplicated as a literal rather than
|
|
86
|
+
* imported from `src/commonjs-marker.cts` on purpose: a migration record is a
|
|
87
|
+
* frozen historical statement about what a PAST version installed, and it must
|
|
88
|
+
* keep matching those bytes even if the live module's constant is ever changed.
|
|
89
|
+
* Importing would silently re-point this detector at a future value.
|
|
90
|
+
*/
|
|
91
|
+
const LEGACY_MARKER_CONTENT = '{"type":"commonjs"}';
|
|
92
|
+
const OWNERSHIP_EVIDENCE = 'file content is byte-identical to the {"type":"commonjs"} marker pre-#2544 installs '
|
|
93
|
+
+ 'wrote at the config root (installSharedHooksBundle); same exact-content predicate '
|
|
94
|
+
+ 'removeCommonJsMarker uses on uninstall. Never manifest-recorded, so this is the '
|
|
95
|
+
+ 'purpose-built detector permitted by docs/installer-migrations.md#remove-managed';
|
|
96
|
+
const REASON = 'superseded by the hooks/ and plugin-dir markers (#2544); leaving it pins the shared '
|
|
97
|
+
+ 'config root to CommonJS and keeps GSD occupying a file it no longer writes';
|
|
98
|
+
const migration = {
|
|
99
|
+
id: '2026-07-28-retire-config-root-commonjs-marker',
|
|
100
|
+
title: 'Retire the pre-#2544 config-root CommonJS marker',
|
|
101
|
+
description: 'Remove <configRoot>/package.json when it is exactly the {"type":"commonjs"} marker '
|
|
102
|
+
+ 'pre-#2544 installs wrote there, now superseded by markers scoped to the directories '
|
|
103
|
+
+ 'GSD owns. A package.json with any other content is left untouched.',
|
|
104
|
+
introducedIn: '1.8.0',
|
|
105
|
+
scopes: ['global', 'local'],
|
|
106
|
+
destructive: true,
|
|
107
|
+
plan: (ctx) => {
|
|
108
|
+
const markerPath = node_path_1.default.join(ctx.configDir, STALE_ROOT_MARKER);
|
|
109
|
+
let stat;
|
|
110
|
+
try {
|
|
111
|
+
// lstat, not existsSync: existsSync follows symlinks and reports false for
|
|
112
|
+
// a dangling one. A symlink here is not something GSD wrote, and removing
|
|
113
|
+
// it is never ours to do — mirrors classifyMarker's fail-closed posture.
|
|
114
|
+
stat = node_fs_1.default.lstatSync(markerPath);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
if (!stat.isFile())
|
|
120
|
+
return [];
|
|
121
|
+
let content;
|
|
122
|
+
try {
|
|
123
|
+
content = node_fs_1.default.readFileSync(markerPath, 'utf8');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Present but unreadable never downgrades to the permissive answer.
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
if (content.trim() !== LEGACY_MARKER_CONTENT)
|
|
130
|
+
return [];
|
|
131
|
+
const hash = node_crypto_1.default.createHash('sha256').update(content).digest('hex');
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
type: 'remove-managed',
|
|
135
|
+
relPath: STALE_ROOT_MARKER,
|
|
136
|
+
reason: REASON,
|
|
137
|
+
ownershipEvidence: OWNERSHIP_EVIDENCE,
|
|
138
|
+
// Declared, not derived: classifyArtifact answers 'unknown' for this
|
|
139
|
+
// never-manifested path, and the planner downgrades a remove-managed on
|
|
140
|
+
// an 'unknown' classification to preserve-user. The exact-content match
|
|
141
|
+
// above IS the ownership proof, so the classification is stated here.
|
|
142
|
+
classification: 'managed-pristine',
|
|
143
|
+
originalHash: hash,
|
|
144
|
+
currentHash: hash,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
module.exports = migration;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installer migration: retire Cursor's duplicate commands/ surface (#2644).
|
|
4
|
+
*
|
|
5
|
+
* Cursor discovers GSD skills as slash-menu entries while also keeping them
|
|
6
|
+
* model-invocable. Older GSD releases installed the same workflows again as
|
|
7
|
+
* commands/gsd-*.md, so every action appeared twice. The skills remain the
|
|
8
|
+
* sole workflow surface; this migration removes only old command files proven
|
|
9
|
+
* managed by gsd-file-manifest.json. Modified files are backed up first and
|
|
10
|
+
* unmanifested files are preserved.
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const COMMANDS_DIR = 'commands';
|
|
18
|
+
const REASON = 'Cursor exposes skills directly in the slash menu; the parallel command file duplicated the same GSD action (#2644)';
|
|
19
|
+
const OWNERSHIP_EVIDENCE = 'pre-#2644 Cursor installs record commands/gsd-*.md in gsd-file-manifest.json';
|
|
20
|
+
const migration = {
|
|
21
|
+
id: '2026-07-29-cursor-retire-commands-surface',
|
|
22
|
+
title: 'Retire Cursor duplicate commands surface',
|
|
23
|
+
description: 'Remove manifest-managed Cursor commands/gsd-*.md files now that skills are the single slash-menu and model-invocation surface.',
|
|
24
|
+
introducedIn: '1.8.1',
|
|
25
|
+
runtimes: ['cursor'],
|
|
26
|
+
scopes: ['global', 'local'],
|
|
27
|
+
destructive: true,
|
|
28
|
+
plan: (ctx) => {
|
|
29
|
+
const commandsDir = node_path_1.default.join(ctx.configDir, COMMANDS_DIR);
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
if (!node_fs_1.default.existsSync(commandsDir) || node_fs_1.default.lstatSync(commandsDir).isSymbolicLink())
|
|
33
|
+
return [];
|
|
34
|
+
entries = node_fs_1.default.readdirSync(commandsDir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const actions = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry.isFile() || !entry.name.startsWith('gsd-') || !entry.name.endsWith('.md'))
|
|
42
|
+
continue;
|
|
43
|
+
const relPath = node_path_1.default.posix.join(COMMANDS_DIR, entry.name);
|
|
44
|
+
const artifact = ctx.classifyArtifact(relPath);
|
|
45
|
+
if (artifact.classification === 'managed-pristine') {
|
|
46
|
+
actions.push({ type: 'remove-managed', relPath, reason: REASON, ownershipEvidence: OWNERSHIP_EVIDENCE });
|
|
47
|
+
}
|
|
48
|
+
else if (artifact.classification === 'managed-modified') {
|
|
49
|
+
actions.push({ type: 'backup-and-remove', relPath, reason: REASON, ownershipEvidence: OWNERSHIP_EVIDENCE });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return actions;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
module.exports = migration;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installer migration: retire pi's legacy `<piConfigDir>/hooks/` directory
|
|
4
|
+
* after GSD's shared hook bundle moved to `<piConfigDir>/gsd-hooks/` (#3023).
|
|
5
|
+
*
|
|
6
|
+
* What old artifact is being retired?
|
|
7
|
+
* `hooks/` (and its `hooks/lib/` subdirectory) at the pi config root. pi
|
|
8
|
+
* reserves that exact name as its own deprecated extension directory and
|
|
9
|
+
* warns on every startup whenever it exists — pi's
|
|
10
|
+
* `checkDeprecatedExtensionDirs()` fires on mere PATH EXISTENCE, not on the
|
|
11
|
+
* directory having contents (unlike the sibling `tools/` check, which does
|
|
12
|
+
* `readdir` first). GSD used to install its shared hook bundle at exactly
|
|
13
|
+
* that reserved path, so every pi install carried the warning permanently.
|
|
14
|
+
* The fix moved the install target to `gsd-hooks/`
|
|
15
|
+
* (`hostBehaviors.sharedHooksDirName`), but an EXISTING install that
|
|
16
|
+
* upgrades still has the old `hooks/` tree sitting on disk — nothing
|
|
17
|
+
* removes it on its own, so the warning would persist forever without this
|
|
18
|
+
* migration.
|
|
19
|
+
*
|
|
20
|
+
* How do we prove it is GSD-owned?
|
|
21
|
+
* Per file, by manifest membership — the same `classifyArtifact()` check
|
|
22
|
+
* every other migration in this directory uses. Pre-#3023 pi installs
|
|
23
|
+
* record the shared hook bundle under `hooks/…` keys in
|
|
24
|
+
* `gsd-file-manifest.json`; the new install target writes `gsd-hooks/…`
|
|
25
|
+
* keys instead, which this migration structurally never sees because it
|
|
26
|
+
* only ever walks the `hooks/` subtree.
|
|
27
|
+
*
|
|
28
|
+
* What happens if the user modified it?
|
|
29
|
+
* `backup-and-remove` instead of `remove-managed`, so a locally patched
|
|
30
|
+
* hook script is recoverable from the backup rather than silently
|
|
31
|
+
* destroyed — mirrors migration 006.
|
|
32
|
+
*
|
|
33
|
+
* What happens to files the manifest never recorded?
|
|
34
|
+
* Nothing. An unmanifested file under `hooks/` (classification `unknown`)
|
|
35
|
+
* is left exactly where it is, and — because its presence keeps the
|
|
36
|
+
* directory non-empty — it also keeps the directory itself from being
|
|
37
|
+
* retired. That is a deliberate consequence of directory removal being
|
|
38
|
+
* gated on emptiness, not a special case.
|
|
39
|
+
*
|
|
40
|
+
* What happens to the directory itself?
|
|
41
|
+
* `hooks/lib/` and then `hooks/` each get a `remove-empty-dir` action (see
|
|
42
|
+
* `evaluateRemoveEmptyDir` in `../installer-migrations.cts`). That action
|
|
43
|
+
* only ever calls `fs.rmdirSync` — never a recursive removal — and
|
|
44
|
+
* re-checks emptiness immediately before doing so, so a directory that
|
|
45
|
+
* still holds anything (an unmanifested file, or a file-level action that
|
|
46
|
+
* failed to apply) is left in place rather than assumed empty. Actions are
|
|
47
|
+
* emitted deepest-first (`hooks/lib` before `hooks`) so the parent has a
|
|
48
|
+
* chance to become empty in the same pass.
|
|
49
|
+
*
|
|
50
|
+
* What happens if it is missing?
|
|
51
|
+
* No actions. A fresh post-#3023 pi install never creates `hooks/` at all,
|
|
52
|
+
* and an already-migrated install has nothing left to retire — both plan
|
|
53
|
+
* empty, so the migration is idempotent.
|
|
54
|
+
*
|
|
55
|
+
* What runtime and scope does it affect?
|
|
56
|
+
* pi only, global and local. No other runtime's install is affected:
|
|
57
|
+
* `hostBehaviors.sharedHooksDirName` defaults to `'hooks'` for every other
|
|
58
|
+
* runtime, and none of them reserve that name the way pi does, so a
|
|
59
|
+
* claude/kimi/opencode/etc. `hooks/` directory is a live, in-use install
|
|
60
|
+
* surface that must never be touched here. The runtime check is the FIRST
|
|
61
|
+
* thing `plan()` does, ahead of even checking whether the directory exists,
|
|
62
|
+
* as defense in depth beyond the `runtimes: ['pi']` record-level filter the
|
|
63
|
+
* framework itself already enforces.
|
|
64
|
+
*
|
|
65
|
+
* Is the action safe in non-interactive install?
|
|
66
|
+
* Yes. Every emitted action type (`remove-managed`, `backup-and-remove`,
|
|
67
|
+
* `remove-empty-dir`) is non-interactive and journaled; none requires a
|
|
68
|
+
* user choice, and unknown files never produce an action.
|
|
69
|
+
*
|
|
70
|
+
* See docs/installer-migrations.md#shipped-migrations, the pi row of
|
|
71
|
+
* docs/installer-migrations.md#runtime-configuration-contract-registry, and
|
|
72
|
+
* the 2026-08-07 amendment to docs/adr/0008-installer-migration-module.md.
|
|
73
|
+
*/
|
|
74
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
75
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
76
|
+
};
|
|
77
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
78
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
79
|
+
/** pi's reserved (and, pre-#3023, GSD-populated) legacy hook directory name. */
|
|
80
|
+
const HOOKS_DIR = 'hooks';
|
|
81
|
+
const FILE_REASON = "pi's startup check warns whenever hooks/ exists (checkDeprecatedExtensionDirs), and GSD's shared " +
|
|
82
|
+
'hook bundle now installs at gsd-hooks/ instead (#3023), so the legacy files are superseded';
|
|
83
|
+
const FILE_OWNERSHIP_EVIDENCE = 'pre-#3023 pi installs record the shared hook bundle under hooks/… keys in gsd-file-manifest.json; ' +
|
|
84
|
+
'the new install target is gsd-hooks/…, which this migration never touches because it only walks the ' +
|
|
85
|
+
'hooks/ subtree';
|
|
86
|
+
const DIR_REASON = "pi's checkDeprecatedExtensionDirs() warns on hooks/'s mere existence, not its contents (#3023); the " +
|
|
87
|
+
'reserved container is retired once every GSD-owned entry inside it is gone';
|
|
88
|
+
const DIR_OWNERSHIP_EVIDENCE = 'hooks/ and hooks/lib/ are GSD-installed container directories under the pi config root (the pre-#3023 ' +
|
|
89
|
+
'default of hostBehaviors.sharedHooksDirName); removal is gated on emptiness by the shared ' +
|
|
90
|
+
'remove-empty-dir action, so a directory that still holds an unmanifested user file — or any file-level ' +
|
|
91
|
+
'action that failed to apply — is left in place rather than assumed empty';
|
|
92
|
+
/**
|
|
93
|
+
* Recursively collect files and directories under `relDir`, never following a
|
|
94
|
+
* symlink (whether it names a file or a directory) and never emitting a path
|
|
95
|
+
* that resolves outside `baseResolved`. Mirrors the traversal guard in
|
|
96
|
+
* migration 003 (`walkLegacyFiles`).
|
|
97
|
+
*/
|
|
98
|
+
function walkPiHooksTree(root, relDir, baseResolved, files, dirs) {
|
|
99
|
+
const dir = node_path_1.default.join(root, relDir);
|
|
100
|
+
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
// Never follow a symlink into or through: it must not be traversed,
|
|
103
|
+
// hashed, or removed, regardless of what it points at.
|
|
104
|
+
if (entry.isSymbolicLink())
|
|
105
|
+
continue;
|
|
106
|
+
const relPath = node_path_1.default.posix.join(relDir, entry.name);
|
|
107
|
+
const resolved = node_path_1.default.resolve(root, relPath);
|
|
108
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + node_path_1.default.sep))
|
|
109
|
+
continue;
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
dirs.push(relPath);
|
|
112
|
+
walkPiHooksTree(root, relPath, baseResolved, files, dirs);
|
|
113
|
+
}
|
|
114
|
+
else if (entry.isFile()) {
|
|
115
|
+
files.push(relPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const migration = {
|
|
120
|
+
id: '2026-08-07-pi-retire-reserved-hooks-dir',
|
|
121
|
+
title: "Retire pi's reserved hooks/ directory",
|
|
122
|
+
description: 'Remove manifest-managed files under <piConfigDir>/hooks/ and, once empty, the directory itself ' +
|
|
123
|
+
'(and its hooks/lib/ subdirectory), now that the shared hook bundle installs at gsd-hooks/ instead. pi ' +
|
|
124
|
+
'reserves hooks/ as its own deprecated extension directory and warns on every startup while it exists (#3023).',
|
|
125
|
+
introducedIn: '1.9.2',
|
|
126
|
+
runtimes: ['pi'],
|
|
127
|
+
scopes: ['global', 'local'],
|
|
128
|
+
destructive: true,
|
|
129
|
+
plan: (ctx) => {
|
|
130
|
+
// Defense in depth ahead of the framework's own runtimes filter: a
|
|
131
|
+
// claude/kimi/opencode/etc. hooks/ directory is a live install surface,
|
|
132
|
+
// never a retirement target.
|
|
133
|
+
if (ctx.runtime !== 'pi')
|
|
134
|
+
return [];
|
|
135
|
+
const hooksRoot = node_path_1.default.join(ctx.configDir, HOOKS_DIR);
|
|
136
|
+
let rootLstat;
|
|
137
|
+
try {
|
|
138
|
+
rootLstat = node_fs_1.default.lstatSync(hooksRoot);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return []; // absent -> nothing to retire, idempotent
|
|
142
|
+
}
|
|
143
|
+
// Never follow a symlinked hooks/ root: walking through it could plan
|
|
144
|
+
// actions against paths outside the pi config directory entirely.
|
|
145
|
+
if (rootLstat.isSymbolicLink())
|
|
146
|
+
return [];
|
|
147
|
+
if (!rootLstat.isDirectory())
|
|
148
|
+
return [];
|
|
149
|
+
const baseResolved = node_path_1.default.resolve(ctx.configDir);
|
|
150
|
+
const files = [];
|
|
151
|
+
const dirs = [];
|
|
152
|
+
try {
|
|
153
|
+
walkPiHooksTree(ctx.configDir, HOOKS_DIR, baseResolved, files, dirs);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Unreadable directory: nothing safe to plan.
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const actions = [];
|
|
160
|
+
for (const relPath of files) {
|
|
161
|
+
const { classification } = ctx.classifyArtifact(relPath);
|
|
162
|
+
if (classification === 'managed-pristine') {
|
|
163
|
+
actions.push({ type: 'remove-managed', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
|
|
164
|
+
}
|
|
165
|
+
else if (classification === 'managed-modified') {
|
|
166
|
+
actions.push({ type: 'backup-and-remove', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
|
|
167
|
+
}
|
|
168
|
+
// 'unknown' (user-added, not manifest-recorded): no action, preserved.
|
|
169
|
+
// 'missing' / 'managed-missing': impossible here — relPath was just
|
|
170
|
+
// discovered by walking the live filesystem, so it currently exists.
|
|
171
|
+
}
|
|
172
|
+
// Deepest directories first, so a child has already been evaluated (and
|
|
173
|
+
// possibly removed) before its parent's own emptiness is re-checked by
|
|
174
|
+
// the executor. `hooks/` itself is appended last, unconditionally: the
|
|
175
|
+
// executor's own emptiness re-check is what actually decides whether it
|
|
176
|
+
// goes, not this ordering — this ordering only gives it the chance to.
|
|
177
|
+
const orderedDirs = [...dirs].sort((a, b) => b.split('/').length - a.split('/').length);
|
|
178
|
+
orderedDirs.push(HOOKS_DIR);
|
|
179
|
+
for (const relPath of orderedDirs) {
|
|
180
|
+
actions.push({
|
|
181
|
+
type: 'remove-empty-dir',
|
|
182
|
+
relPath,
|
|
183
|
+
reason: DIR_REASON,
|
|
184
|
+
ownershipEvidence: DIR_OWNERSHIP_EVIDENCE,
|
|
185
|
+
// Declared, not derived: classifyArtifact() hashes file contents via
|
|
186
|
+
// sha256File(), which throws EISDIR against a directory path. These
|
|
187
|
+
// relPaths name directories, so classification is stated directly
|
|
188
|
+
// (never 'unknown', so the planner's unknown-classification block
|
|
189
|
+
// never fires for them) rather than routed through the file
|
|
190
|
+
// classifier.
|
|
191
|
+
classification: 'managed-pristine',
|
|
192
|
+
originalHash: null,
|
|
193
|
+
currentHash: null,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return actions;
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
module.exports = migration;
|