@pareto4/pp-core 1.11.0 → 1.12.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.
Files changed (256) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -0
  3. package/agents/pp-code-fixer.md +1 -1
  4. package/agents/pp-debug-session-manager.md +1 -1
  5. package/agents/pp-debugger.md +1 -1
  6. package/agents/pp-executor.md +20 -10
  7. package/agents/pp-intel-updater.md +1 -1
  8. package/agents/pp-phase-researcher.md +1 -1
  9. package/agents/pp-plan-checker.md +5 -5
  10. package/agents/pp-planner.md +10 -24
  11. package/agents/pp-project-researcher.md +1 -1
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-researcher.md +1 -1
  14. package/agents/pp-verifier.md +8 -4
  15. package/bin/install.js +429 -82
  16. package/commands/pp-autonomous.md +8 -6
  17. package/commands/pp-discovery-to-build.md +1 -1
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-discuss-phase.md +2 -2
  20. package/commands/pp-execute-phase.md +3 -1
  21. package/commands/pp-graphify.md +5 -5
  22. package/commands/pp-import.md +1 -1
  23. package/commands/pp-map-codebase.md +1 -1
  24. package/commands/pp-ns-project.md +2 -3
  25. package/commands/pp-ns-workflow.md +1 -1
  26. package/commands/pp-plan-phase.md +2 -1
  27. package/commands/pp-verify-work.md +2 -1
  28. package/gemini-extension.json +1 -1
  29. package/hooks/dist/pp-cursor-session-start.js +2 -2
  30. package/hooks/dist/pp-session-state.sh +1 -1
  31. package/hooks/dist/pp-statusline.js +21 -2
  32. package/hooks/pp-cursor-session-start.js +2 -2
  33. package/hooks/pp-session-state.sh +1 -1
  34. package/hooks/pp-statusline.js +21 -2
  35. package/package.json +18 -5
  36. package/pareto-skills/pp-project-setup/SKILL.md +5 -63
  37. package/pp-core/bin/detect-build-commands.sh +182 -0
  38. package/pp-core/bin/lib/capability-registry.cjs +65 -7
  39. package/pp-core/bin/lib/check-command-router.cjs +79 -38
  40. package/pp-core/bin/lib/clusters.cjs +2 -3
  41. package/pp-core/bin/lib/command-aliases.cjs +36 -0
  42. package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
  43. package/pp-core/bin/lib/commands.cjs +21 -6
  44. package/pp-core/bin/lib/compose-bindings.cjs +128 -15
  45. package/pp-core/bin/lib/config-schema.cjs +16 -0
  46. package/pp-core/bin/lib/config.cjs +142 -27
  47. package/pp-core/bin/lib/core-utils.cjs +43 -7
  48. package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
  49. package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
  50. package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
  51. package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
  52. package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
  53. package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
  54. package/pp-core/bin/lib/discovery-table.cjs +98 -0
  55. package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
  56. package/pp-core/bin/lib/frontmatter.cjs +17 -2
  57. package/pp-core/bin/lib/init-command-router.cjs +8 -5
  58. package/pp-core/bin/lib/init-contract.cjs +213 -33
  59. package/pp-core/bin/lib/init-interactive.cjs +20 -1
  60. package/pp-core/bin/lib/init.cjs +124 -134
  61. package/pp-core/bin/lib/install-profiles.cjs +5 -0
  62. package/pp-core/bin/lib/install-run.cjs +536 -0
  63. package/pp-core/bin/lib/io.cjs +15 -0
  64. package/pp-core/bin/lib/loop-resolver.cjs +24 -0
  65. package/pp-core/bin/lib/milestone.cjs +345 -48
  66. package/pp-core/bin/lib/phase-command-router.cjs +23 -1
  67. package/pp-core/bin/lib/phase-locator.cjs +15 -6
  68. package/pp-core/bin/lib/phase-scope.cjs +91 -0
  69. package/pp-core/bin/lib/phase-tier.cjs +467 -0
  70. package/pp-core/bin/lib/phase.cjs +247 -21
  71. package/pp-core/bin/lib/phases-command-router.cjs +2 -0
  72. package/pp-core/bin/lib/plan-graph.cjs +368 -0
  73. package/pp-core/bin/lib/planning-workspace.cjs +53 -0
  74. package/pp-core/bin/lib/platform-bindings.cjs +32 -2
  75. package/pp-core/bin/lib/profile-output.cjs +11 -3
  76. package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
  77. package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
  78. package/pp-core/bin/lib/roadmap.cjs +141 -39
  79. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  80. package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
  81. package/pp-core/bin/lib/state-command-router.cjs +82 -13
  82. package/pp-core/bin/lib/state-document.cjs +124 -7
  83. package/pp-core/bin/lib/state.cjs +784 -257
  84. package/pp-core/bin/lib/template.cjs +22 -6
  85. package/pp-core/bin/lib/vault-command-router.cjs +57 -6
  86. package/pp-core/bin/lib/vault-practices.cjs +6 -36
  87. package/pp-core/bin/lib/vault-write-back.cjs +20 -4
  88. package/pp-core/bin/lib/vault.cjs +555 -23
  89. package/pp-core/bin/lib/verify.cjs +53 -31
  90. package/pp-core/bin/lib/workstream.cjs +1 -1
  91. package/pp-core/bin/lib/worktree-safety.cjs +26 -15
  92. package/pp-core/bin/portable-timeout.sh +89 -0
  93. package/pp-core/bin/pp-tools.cjs +158 -8
  94. package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
  95. package/pp-core/bin/shared/config-schema.manifest.json +3 -0
  96. package/pp-core/references/autonomous-best-effort.md +461 -0
  97. package/pp-core/references/autonomous-lifecycle.md +146 -0
  98. package/pp-core/references/autonomous-smart-discuss.md +16 -12
  99. package/pp-core/references/checkpoints.md +1 -1
  100. package/pp-core/references/discovery-id-conventions.md +202 -0
  101. package/pp-core/references/discovery-rebaseline.md +254 -0
  102. package/pp-core/references/discovery-research-track.md +283 -0
  103. package/pp-core/references/discovery-traceability.md +400 -0
  104. package/pp-core/references/loop-hook-dispatch.md +7 -0
  105. package/pp-core/references/mandatory-initial-read.md +1 -1
  106. package/pp-core/references/planner-gap-closure.md +1 -0
  107. package/pp-core/references/planner-human-verify-mode.md +1 -1
  108. package/pp-core/references/planner-load-graph-context.md +1 -1
  109. package/pp-core/references/planner-plan-graph.md +66 -0
  110. package/pp-core/references/planner-tdd.md +1 -0
  111. package/pp-core/references/planning-config.md +8 -0
  112. package/pp-core/references/sketch-tooling.md +4 -0
  113. package/pp-core/references/tdd.md +1 -0
  114. package/pp-core/templates/claude-md.md +1 -1
  115. package/pp-core/templates/phase-prompt.md +6 -2
  116. package/pp-core/templates/prototype-review/review.js +303 -0
  117. package/pp-core/templates/state.md +6 -0
  118. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
  119. package/pp-core/workflows/add-backlog.md +1 -1
  120. package/pp-core/workflows/add-phase.md +1 -1
  121. package/pp-core/workflows/add-tests.md +1 -1
  122. package/pp-core/workflows/add-todo.md +1 -1
  123. package/pp-core/workflows/ai-integration-phase.md +2 -2
  124. package/pp-core/workflows/audit-fix.md +12 -16
  125. package/pp-core/workflows/audit-milestone.md +1 -1
  126. package/pp-core/workflows/audit-uat.md +1 -1
  127. package/pp-core/workflows/autonomous.md +68 -306
  128. package/pp-core/workflows/check-todos.md +1 -1
  129. package/pp-core/workflows/cleanup.md +1 -1
  130. package/pp-core/workflows/code-review-fix.md +1 -1
  131. package/pp-core/workflows/code-review.md +19 -7
  132. package/pp-core/workflows/complete-milestone.md +41 -60
  133. package/pp-core/workflows/debug.md +1 -1
  134. package/pp-core/workflows/diagnose-issues.md +1 -1
  135. package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
  136. package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
  137. package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
  138. package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
  139. package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
  140. package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
  141. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
  142. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
  143. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
  144. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
  145. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
  146. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
  147. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
  148. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
  149. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
  150. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
  151. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
  152. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
  153. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
  154. package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
  155. package/pp-core/workflows/discovery-sprint.md +79 -1714
  156. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
  157. package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
  158. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
  159. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
  160. package/pp-core/workflows/discovery-to-build.md +21 -238
  161. package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
  162. package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
  163. package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
  164. package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
  165. package/pp-core/workflows/discuss-phase.md +9 -2
  166. package/pp-core/workflows/do.md +4 -4
  167. package/pp-core/workflows/docs-update.md +1 -1
  168. package/pp-core/workflows/edit-phase.md +1 -1
  169. package/pp-core/workflows/eval-review.md +1 -1
  170. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
  171. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
  172. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  173. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
  174. package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
  175. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
  176. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
  177. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
  178. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
  179. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
  180. package/pp-core/workflows/execute-phase.md +102 -216
  181. package/pp-core/workflows/execute-plan.md +6 -2
  182. package/pp-core/workflows/explore.md +1 -1
  183. package/pp-core/workflows/extract-learnings.md +1 -1
  184. package/pp-core/workflows/forensics.md +4 -4
  185. package/pp-core/workflows/graduation.md +1 -1
  186. package/pp-core/workflows/health.md +1 -1
  187. package/pp-core/workflows/help/modes/brief.md +1 -0
  188. package/pp-core/workflows/help/modes/default.md +2 -1
  189. package/pp-core/workflows/help/modes/full.md +12 -5
  190. package/pp-core/workflows/import.md +1 -1
  191. package/pp-core/workflows/ingest-docs.md +1 -1
  192. package/pp-core/workflows/init.md +18 -2
  193. package/pp-core/workflows/insert-phase.md +1 -1
  194. package/pp-core/workflows/list-workspaces.md +1 -1
  195. package/pp-core/workflows/manager.md +3 -3
  196. package/pp-core/workflows/map-codebase.md +2 -2
  197. package/pp-core/workflows/milestone-summary.md +4 -4
  198. package/pp-core/workflows/mvp-phase.md +1 -1
  199. package/pp-core/workflows/new-milestone.md +7 -8
  200. package/pp-core/workflows/new-project.md +6 -1
  201. package/pp-core/workflows/new-workspace.md +1 -1
  202. package/pp-core/workflows/next.md +2 -2
  203. package/pp-core/workflows/note.md +2 -2
  204. package/pp-core/workflows/pause-work.md +1 -1
  205. package/pp-core/workflows/plan-milestone-gaps.md +1 -1
  206. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
  207. package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
  208. package/pp-core/workflows/plan-phase.md +43 -167
  209. package/pp-core/workflows/plan-review-convergence.md +2 -2
  210. package/pp-core/workflows/plant-seed.md +1 -1
  211. package/pp-core/workflows/pr-branch.md +1 -1
  212. package/pp-core/workflows/profile-user.md +1 -1
  213. package/pp-core/workflows/progress.md +3 -3
  214. package/pp-core/workflows/quick.md +3 -1
  215. package/pp-core/workflows/remove-phase.md +1 -1
  216. package/pp-core/workflows/remove-workspace.md +1 -1
  217. package/pp-core/workflows/resume-project.md +6 -12
  218. package/pp-core/workflows/review.md +1 -1
  219. package/pp-core/workflows/scan.md +1 -1
  220. package/pp-core/workflows/secure-phase.md +1 -1
  221. package/pp-core/workflows/settings-advanced.md +1 -1
  222. package/pp-core/workflows/settings-integrations.md +1 -1
  223. package/pp-core/workflows/settings.md +1 -1
  224. package/pp-core/workflows/ship.md +25 -8
  225. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  226. package/pp-core/workflows/sketch.md +2 -1
  227. package/pp-core/workflows/spec-phase.md +1 -1
  228. package/pp-core/workflows/spike-wrap-up.md +1 -1
  229. package/pp-core/workflows/spike.md +1 -1
  230. package/pp-core/workflows/stats.md +2 -2
  231. package/pp-core/workflows/thread.md +1 -1
  232. package/pp-core/workflows/transition.md +5 -9
  233. package/pp-core/workflows/ui-phase.md +23 -2
  234. package/pp-core/workflows/ui-review.md +1 -1
  235. package/pp-core/workflows/ultraplan-phase.md +2 -2
  236. package/pp-core/workflows/validate-phase.md +1 -1
  237. package/pp-core/workflows/vault-write-back.md +1 -1
  238. package/pp-core/workflows/verify-phase.md +29 -26
  239. package/pp-core/workflows/verify-work.md +14 -10
  240. package/scripts/base64-scan.sh +7 -41
  241. package/scripts/changeset/cli.cjs +25 -5
  242. package/scripts/dev-env.cjs +462 -0
  243. package/scripts/lib/allowlist-ratchet.cjs +103 -1
  244. package/scripts/lib/size-budget.cjs +311 -0
  245. package/scripts/lint-neutrality-names.cjs +694 -0
  246. package/scripts/lint-org-coordinates.allowlist.json +35 -0
  247. package/scripts/lint-org-coordinates.cjs +426 -0
  248. package/scripts/lint-skill-deps.cjs +48 -9
  249. package/scripts/lint-test-file-count.cjs +17 -2
  250. package/scripts/prompt-injection-scan.sh +41 -20
  251. package/scripts/sync-manifest-versions.cjs +41 -7
  252. package/scripts/sync-runtime-launcher.cjs +106 -1
  253. package/scripts/update-size-baseline.cjs +274 -30
  254. package/scripts/workflow-size.cjs +61 -9
  255. package/commands/pp-build-handoff.md +0 -46
  256. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
package/bin/install.js CHANGED
@@ -186,22 +186,6 @@ const GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER = '<!-- /GSD Configuration -->';
186
186
  // nothing that can dangle if a script copy is skipped. See
187
187
  // https://docs.github.com/en/copilot/reference/hooks-configuration
188
188
  const GSD_COPILOT_HOOK_FILE = 'pp-session.json';
189
- // Copilot parses a command hook's stdout as the hook-output JSON. For sessionStart
190
- // the schema is `{ additionalContext?: string }` (the text is prepended to the
191
- // session as context). So the hook must emit that JSON envelope — not bare text.
192
- // The two messages contain no JSON-special characters, so they embed verbatim.
193
- const GSD_COPILOT_SESSION_MSG_PRESENT =
194
- 'GSD: .planning/STATE.md present - review the current phase and any blockers before acting.';
195
- const GSD_COPILOT_SESSION_MSG_ABSENT =
196
- 'GSD: no .planning/ workflow found - run /pp-new-project to start a tracked workflow.';
197
- const GSD_COPILOT_SESSION_HOOK_BASH =
198
- 'if [ -f .planning/STATE.md ]; then ' +
199
- `printf '%s' '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_PRESENT}"}'; else ` +
200
- `printf '%s' '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_ABSENT}"}'; fi`;
201
- const GSD_COPILOT_SESSION_HOOK_PWSH =
202
- 'if (Test-Path .planning/STATE.md) ' +
203
- `{ '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_PRESENT}"}' } ` +
204
- `else { '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_ABSENT}"}' }`;
205
189
 
206
190
  // #777 — Cursor CLI lifecycle hook constants.
207
191
  // Cursor reads hook configs from <project-root>/.cursor/hooks.json (local) or
@@ -630,6 +614,203 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
630
614
  return `${resolvedTarget}/`;
631
615
  }
632
616
 
617
+ /**
618
+ * Does Claude Code substitute `${CLAUDE_PROJECT_DIR}` inside a user-scope
619
+ * SKILL.md body?
620
+ *
621
+ * MEASURED 2026-09-19, `09-PROBE.md` arm B: **yes** — a personal skill whose
622
+ * body contained `${CLAUDE_PROJECT_DIR}` was delivered to the model as
623
+ * `B: [<the project root>]`. (`$HOME`, `~` and
624
+ * `${VAR:-default}` are NOT substituted — arms E, G, D.)
625
+ *
626
+ * true → the dispatcher emits the literal `${CLAUDE_PROJECT_DIR}` and needs no
627
+ * shell step to find the project root.
628
+ * false → the dispatcher first instructs one Bash derivation
629
+ * (`git rev-parse --show-toplevel 2>/dev/null || pwd`) and refers to the
630
+ * result as PROJECT_ROOT.
631
+ *
632
+ * Either way the dispatcher is a Read instruction, never an `@` include:
633
+ * `09-PROBE.md` arms A, A2–A6 and H1 measured that `@` inlines no file at all,
634
+ * in any path form, so a static include could never express "local if present,
635
+ * else global" (D-09-15).
636
+ */
637
+ const CLAUDE_PROJECT_DIR_SUBSTITUTES_IN_SKILL_BODY = true;
638
+
639
+ /**
640
+ * Replace every `@<prefix>pp-core/<rel>` include line in a GLOBAL Claude
641
+ * SKILL.md with a local-first dispatcher.
642
+ *
643
+ * Why (HINST-04 / D-09-14): Claude Code's documented skill precedence is
644
+ * Personal (`~/.claude/skills/`) > Project (`.claude/skills/`), confirmed on
645
+ * this build by `09-PROBE.md` arm F — a personal skill beat both a project
646
+ * command and a same-named project skill. A local pp-core install writes no
647
+ * `skills/` at all, so the personal skill is ALWAYS the one that answers, and
648
+ * its hard-coded global include made a 1.11.1 global workflow run over a 1.13.0
649
+ * project engine for ~47 days. Shipping the skill project-side would be dead
650
+ * weight; the fix is to make the personal skill resolve the project's install.
651
+ *
652
+ * @param {string} content SKILL.md text, after the path rewrites
653
+ * @param {object} args
654
+ * @param {string} args.pathPrefix trailing-slash prefix the rewrites emitted
655
+ * (`$HOME/.claude/` on a home-dir install)
656
+ * @param {string} args.absolutePrefix trailing-slash absolute config dir
657
+ * @returns {string} content, unchanged when no include line matched
658
+ */
659
+ function rewriteGlobalClaudeSkillIncludes(content, { pathPrefix, absolutePrefix } = {}) {
660
+ if (!content || !pathPrefix || !absolutePrefix) return content;
661
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
662
+ const prefixes = [...new Set([pathPrefix, absolutePrefix])].map(escapeRe);
663
+ const prefixAlt = '(?:' + prefixes.join('|') + ')';
664
+ // Transform A — a line whose only content is an `@`-include.
665
+ //
666
+ // G-4 (2026-09-20): the leading `(\\s*[-*+]\\s+)?` and trailing `(\\s+\\S.*?)?`
667
+ // groups are new. The 09-07 form was anchored `^@…$`, which could not see
668
+ // `- @$HOME/.claude/pp-core/workflows/complete-milestone.md (main workflow)`
669
+ // — a list-marker include with a trailing label. That shape reaches TWO
670
+ // references in pp-complete-milestone (one of them a `templates/` path), and
671
+ // the skill consequently carried no dispatcher at all.
672
+ // `\s*$` absorbs a trailing \r so CRLF-authored bodies match identically.
673
+ const includeRe = new RegExp(
674
+ '^(\\s*(?:[-*+]\\s+)?)@' + prefixAlt + 'pp-core/(\\S+?)(\\s+\\S.*?)?\\s*$'
675
+ );
676
+ // Transform B — an IN-PROSE reference, with or without `@`, on a line that is
677
+ // not a bare include. Three shapes ship today and none were visible to the
678
+ // 09-07 form:
679
+ // `Execute the ship workflow from @$HOME/…/ship.md end-to-end.`
680
+ // ``Read and execute `$HOME/…/discuss-phase.md` end-to-end.``
681
+ // `Follow $HOME/…/help.md with $ARGUMENTS.`
682
+ // Six skills carried a correct dispatcher AND one of these, so the run read
683
+ // the project-local workflow first and the GLOBAL copy next, under a
684
+ // `pp-core answered from:` line that only described the first read. A
685
+ // whole-line replacement would destroy the sentence, so this transform
686
+ // rewrites the PATH TOKEN in place and leaves the prose around it alone.
687
+ //
688
+ // TWO exclusions, and both are load-bearing — a first cut of this transform
689
+ // carried neither and corrupted the launcher in pp-discuss-phase, pp-graphify
690
+ // and pp-import, because the shim's `elif [ -f "$HOME/.claude/pp-core/bin/…" ]`
691
+ // branch is a prefixed reference like any other:
692
+ // (a) `pp-core/bin/…` is the shim's own engine lookup, never a document to
693
+ // read. `(?!bin/)` keeps it out.
694
+ // (b) anything inside a ``` fence is code, not prose. Rewriting there both
695
+ // mangles the shell AND — since the header block is emitted before the
696
+ // first touched line — injects markdown prose into the middle of a bash
697
+ // block, the same fence-breaking shape `sync-runtime-launcher` hit.
698
+ const proseRe = new RegExp('@?' + prefixAlt + 'pp-core/(?!bin/)([^\\s`)\\]]+)', 'g');
699
+
700
+ // Split on '\n' only (never a `\r?\n` rejoin): untouched lines keep their own
701
+ // line ending byte-for-byte, and each emitted line copies the ending of the
702
+ // include line it replaces.
703
+ const lines = content.split('\n');
704
+ const matchedIndexes = [];
705
+ const proseIndexes = [];
706
+ let inFence = false;
707
+ for (let i = 0; i < lines.length; i++) {
708
+ if (/^\s*(?:```|~~~)/.test(lines[i])) {
709
+ inFence = !inFence;
710
+ continue;
711
+ }
712
+ if (inFence) continue;
713
+ if (includeRe.test(lines[i])) {
714
+ matchedIndexes.push(i);
715
+ continue;
716
+ }
717
+ proseRe.lastIndex = 0;
718
+ if (proseRe.test(lines[i])) proseIndexes.push(i);
719
+ }
720
+ if (matchedIndexes.length === 0 && proseIndexes.length === 0) return content;
721
+
722
+ const projectRootExpr = CLAUDE_PROJECT_DIR_SUBSTITUTES_IN_SKILL_BODY
723
+ ? '${CLAUDE_PROJECT_DIR}'
724
+ : 'PROJECT_ROOT';
725
+ const header = [
726
+ '<!-- pp-core install resolution (HINST-04, D-09-14): a project-local pp-core install answers before this global one. -->',
727
+ 'Before anything else, resolve which pp-core install answers this skill:',
728
+ ];
729
+ if (!CLAUDE_PROJECT_DIR_SUBSTITUTES_IN_SKILL_BODY) {
730
+ header.push('Run `git rev-parse --show-toplevel 2>/dev/null || pwd` once and call the result PROJECT_ROOT.');
731
+ }
732
+ const answeredFrom =
733
+ 'Then print exactly one line: `pp-core answered from: <dir> (VERSION: <contents of <dir>/VERSION, ' +
734
+ 'or "no VERSION file">)` where <dir> is the pp-core directory you read from (D-09-13).';
735
+
736
+ // The header and the `answered from` line are emitted ONCE, bracketing every
737
+ // rewritten reference in the file. When only Transform B matched
738
+ // (pp-discuss-phase, which states all three of its workflow hand-offs in
739
+ // prose) the block hangs off B's first and last line instead — otherwise
740
+ // those skills would resolve locally while printing no provenance line at
741
+ // all, which is the one outcome D-09-13 exists to prevent.
742
+ const allIndexes = [...matchedIndexes, ...proseIndexes].sort((a, b) => a - b);
743
+ const firstTouched = allIndexes[0];
744
+ const lastTouched = allIndexes[allIndexes.length - 1];
745
+ const proseSet = new Set(proseIndexes);
746
+ const out = [];
747
+ for (let i = 0; i < lines.length; i++) {
748
+ const line = lines[i];
749
+ const eol = line.endsWith('\r') ? '\r' : '';
750
+ const m = includeRe.exec(line);
751
+ if (!m && !proseSet.has(i)) {
752
+ out.push(line);
753
+ continue;
754
+ }
755
+ // The full header reads "Before anything else, resolve …", which is true
756
+ // above a bare include but NOT true spliced into a conditional branch —
757
+ // pp-discuss-phase states its three hand-offs under `If --assumptions …` /
758
+ // `Otherwise …`, and that sentence there would invite a resolve-and-read on
759
+ // a branch that was not taken. So a file with no bare include gets the
760
+ // marker comment only: each rewritten sentence already carries its own
761
+ // local-first conditional, and the provenance line still lands below.
762
+ if (i === firstTouched) {
763
+ const preamble = matchedIndexes.length > 0 ? header : header.slice(0, 1);
764
+ for (const h of preamble) out.push(h + eol);
765
+ }
766
+ if (m) {
767
+ const [, marker, rel] = m;
768
+ // Emit at the include's own indentation, always as a `- ` bullet: a bare
769
+ // column-0 include keeps the exact 09-07 output, and an indented list
770
+ // include stays inside its list.
771
+ const indent = /^\s*/.exec(marker)[0];
772
+ out.push(
773
+ indent + '- Read `' + projectRootExpr + '/.claude/pp-core/' + rel + '` if that file exists; ' +
774
+ 'otherwise Read `' + absolutePrefix + 'pp-core/' + rel + '`. ' +
775
+ "Follow whichever you read as this skill's execution context." + eol
776
+ );
777
+ } else {
778
+ proseRe.lastIndex = 0;
779
+ out.push(
780
+ line.replace(proseRe, (_match, rel) =>
781
+ projectRootExpr + '/.claude/pp-core/' + rel +
782
+ ' (or ' + absolutePrefix + 'pp-core/' + rel +
783
+ ' if no project-local install exists)'
784
+ )
785
+ );
786
+ }
787
+ if (i === lastTouched) out.push(answeredFrom + eol);
788
+ }
789
+ return out.join('\n');
790
+ }
791
+
792
+ /**
793
+ * The ONE owner of "which settings file does this runtime+scope use".
794
+ *
795
+ * #338: a local Claude install writes to settings.local.json — Claude Code's
796
+ * per-user, gitignored slot — so engineer-specific absolute paths (Node binary,
797
+ * home dir) never land in the repo-shared settings.json. Global installs and
798
+ * every non-Claude runtime use settings.json.
799
+ *
800
+ * D-09-06: install and uninstall MUST share this one decision. They did not:
801
+ * install resolved the name through an inline ternary while uninstall hardcoded
802
+ * 'settings.json', so a local uninstall never opened the file the matching
803
+ * install had written and left every pp-core hook entry behind, pointing at
804
+ * scripts it had just deleted. Both paths now call this function.
805
+ *
806
+ * @param {string} runtime - Target runtime ('claude', 'gemini', ...)
807
+ * @param {boolean} isGlobal - Whether this is a global install
808
+ * @returns {string} Settings file basename
809
+ */
810
+ function resolveSettingsFileName(runtime, isGlobal) {
811
+ return runtime === 'claude' && !isGlobal ? 'settings.local.json' : 'settings.json';
812
+ }
813
+
633
814
  // normalizeNodePath, resolveNodeRunner, resolveBashRunner, referencesHook are
634
815
  // now owned by the runtime-hooks-surface module. Import them here so
635
816
  // install.js callers continue to work and so there is a single implementation
@@ -1726,8 +1907,8 @@ function convertClaudeToCopilotContent(content, isGlobal = false) {
1726
1907
  } else {
1727
1908
  c = c.replace(/\$HOME\/\.claude\//g, '.github/');
1728
1909
  c = c.replace(/~\/\.claude\//g, '.github/');
1729
- c = c.replace(/\$HOME\/\.claude\b/g, '.github');
1730
- c = c.replace(/~\/\.claude\b/g, '.github');
1910
+ c = c.replace(/\$HOME\/\.claude(?![\w-])/g, '.github');
1911
+ c = c.replace(/~\/\.claude(?![\w-])/g, '.github');
1731
1912
  }
1732
1913
  c = c.replace(/\.\/\.claude\//g, './.github/');
1733
1914
  c = c.replace(/\.claude\//g, '.github/');
@@ -2228,14 +2409,14 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) {
2228
2409
  c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/');
2229
2410
  c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/');
2230
2411
  // Bare form (no trailing slash) — must come after slash form to avoid double-replace
2231
- c = c.replace(/\$HOME\/\.claude\b/g, '$HOME/.gemini/antigravity');
2232
- c = c.replace(/~\/\.claude\b/g, '~/.gemini/antigravity');
2412
+ c = c.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.gemini/antigravity');
2413
+ c = c.replace(/~\/\.claude(?![\w-])/g, '~/.gemini/antigravity');
2233
2414
  } else {
2234
2415
  c = c.replace(/\$HOME\/\.claude\//g, '.agents/');
2235
2416
  c = c.replace(/~\/\.claude\//g, '.agents/');
2236
2417
  // Bare form (no trailing slash) — must come after slash form to avoid double-replace
2237
- c = c.replace(/\$HOME\/\.claude\b/g, '.agents');
2238
- c = c.replace(/~\/\.claude\b/g, '.agents');
2418
+ c = c.replace(/\$HOME\/\.claude(?![\w-])/g, '.agents');
2419
+ c = c.replace(/~\/\.claude(?![\w-])/g, '.agents');
2239
2420
  }
2240
2421
  c = c.replace(/\.\/\.claude\//g, './.agents/');
2241
2422
  c = c.replace(/\.claude\//g, '.agents/');
@@ -2849,8 +3030,8 @@ function convertClaudeToCliineMarkdown(content) {
2849
3030
  converted = converted.replace(/\.\/\.claude\//g, './.cline/');
2850
3031
  converted = converted.replace(/\.claude\//g, '.cline/');
2851
3032
  // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite
2852
- converted = converted.replace(/~\/\.claude\b/g, '~/.cline');
2853
- converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.cline');
3033
+ converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.cline');
3034
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.cline');
2854
3035
  // Environment variable name rewrite
2855
3036
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR');
2856
3037
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
@@ -2955,8 +3136,8 @@ function convertClaudeToCodexMarkdown(content) {
2955
3136
  converted = converted.replace(/~\/\.claude\//g, '~/.codex/');
2956
3137
  converted = converted.replace(/\.\/\.claude\//g, './.codex/');
2957
3138
  // Bare ~/.claude without trailing slash (e.g. configDir = ~/.claude)
2958
- converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.codex');
2959
- converted = converted.replace(/~\/\.claude\b/g, '~/.codex');
3139
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.codex');
3140
+ converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.codex');
2960
3141
  // Bare/project-relative .claude/... references (#2639). Covers strings like
2961
3142
  // "check `.claude/skills/`" where there is no ~/, $HOME/, or ./ anchor.
2962
3143
  // Negative lookbehind prevents double-replacing already-anchored forms and
@@ -5943,8 +6124,8 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
5943
6124
  // Replace /gsd-command colon variant with /gsd-command for opencode (flat command structure)
5944
6125
  convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-');
5945
6126
  // Replace ~/.claude and $HOME/.claude with OpenCode's config location
5946
- convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
5947
- convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
6127
+ convertedContent = convertedContent.replace(/~\/\.claude(?![\w-])/g, '~/.config/opencode');
6128
+ convertedContent = convertedContent.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.config/opencode');
5948
6129
  // Replace general-purpose subagent type with OpenCode's equivalent "general"
5949
6130
  convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
5950
6131
  // Runtime-neutral agent name replacement (#766)
@@ -6100,8 +6281,8 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
6100
6281
  // Replace /gsd-command colon variant with /gsd-command for Kilo (flat command structure)
6101
6282
  convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-');
6102
6283
  // Replace ~/.claude and $HOME/.claude with Kilo's config location
6103
- convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/kilo');
6104
- convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/kilo');
6284
+ convertedContent = convertedContent.replace(/~\/\.claude(?![\w-])/g, '~/.config/kilo');
6285
+ convertedContent = convertedContent.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.config/kilo');
6105
6286
  convertedContent = convertedContent.replace(/\.\/\.claude\//g, './.kilo/');
6106
6287
  // Normalize both Claude skill directory variants to Kilo's canonical skills dir.
6107
6288
  convertedContent = replaceRelativePathReference(convertedContent, '.claude/skills/', '.kilo/skills/');
@@ -6554,7 +6735,7 @@ function writeHermesCategoryDescription(categoryDir) {
6554
6735
  '---',
6555
6736
  'name: gsd',
6556
6737
  `version: ${pkg.version}`,
6557
- 'description: pp-core — The Pareto delivery framework. Disciplined planning, execution, and shipping workflows. Use any pp-* skill in this category to drive a project through new-project → discuss-phase → plan-phase → execute-phase → ship.',
6738
+ 'description: pp-core — The Pareto delivery framework. Disciplined planning, execution, and shipping workflows. Use any pp-* skill in this category to drive a project through init → new-project → discuss-phase → plan-phase → execute-phase → ship.',
6558
6739
  '---',
6559
6740
  '',
6560
6741
  '# pp-core',
@@ -6696,8 +6877,10 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
6696
6877
  * @param {string} runtime
6697
6878
  * @param {string} pathPrefix e.g. "~/.codex/" — trailing-slash string
6698
6879
  * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
6880
+ * @param {object} [opts={}] extra rewrite inputs; `opts.configDir` supplies the
6881
+ * absolute global install path the HINST-04 dispatcher names in its else-clause
6699
6882
  */
6700
- function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal = false) {
6883
+ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal = false, opts = {}) {
6701
6884
  if (!fs.existsSync(stagedDir)) return;
6702
6885
 
6703
6886
  // Walk all SKILL.md files under stagedDir
@@ -6708,7 +6891,7 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGl
6708
6891
  walkAndRewrite(fullPath);
6709
6892
  } else if (entry.name.endsWith('.md')) {
6710
6893
  let content = fs.readFileSync(fullPath, 'utf8');
6711
- content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
6894
+ content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, opts);
6712
6895
  fs.writeFileSync(fullPath, content);
6713
6896
  }
6714
6897
  }
@@ -6765,9 +6948,10 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
6765
6948
  * @param {string} runtime
6766
6949
  * @param {string} pathPrefix trailing-slash string
6767
6950
  * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
6951
+ * @param {object} [opts={}] extra rewrite inputs (see applyRuntimeContentRewritesInPlace)
6768
6952
  * @returns {string}
6769
6953
  */
6770
- function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6954
+ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, opts = {}) {
6771
6955
  const dirName = getDirName(runtime);
6772
6956
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
6773
6957
 
@@ -6789,8 +6973,8 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6789
6973
  content = content.replace(/~\/\.cline\//g, pathPrefix);
6790
6974
  content = content.replace(/\$HOME\/\.cline\//g, pathPrefix);
6791
6975
  // Bare forms (no trailing slash)
6792
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6793
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6976
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6977
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6794
6978
  content = content.replace(/~\/\.cline\b/g, normalizedPathPrefix);
6795
6979
  content = content.replace(/\$HOME\/\.cline\b/g, normalizedPathPrefix);
6796
6980
  content = processAttribution(content, getCommitAttribution(runtime));
@@ -6853,9 +7037,9 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6853
7037
  content = content.replace(/~\/\.claude\//g, pathPrefix);
6854
7038
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6855
7039
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6856
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6857
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6858
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7040
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
7041
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
7042
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6859
7043
  content = content.replace(/~\/\.trae\//g, pathPrefix);
6860
7044
  content = processAttribution(content, getCommitAttribution(runtime));
6861
7045
  break;
@@ -6864,9 +7048,9 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6864
7048
  content = content.replace(/~\/\.claude\//g, pathPrefix);
6865
7049
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6866
7050
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6867
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6868
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6869
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7051
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
7052
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
7053
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6870
7054
  // The codebuddy converter rewrites `.claude/` → `.codebuddy/` at stage
6871
7055
  // time, so `$HOME/.claude/...` arrives here as `$HOME/.codebuddy/...`.
6872
7056
  // Normalize BOTH the `~/` and `$HOME/` forms (slash + bare) to the install
@@ -6892,6 +7076,16 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6892
7076
  content = content.replace(/~\/\.claude\//g, pathPrefix);
6893
7077
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6894
7078
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
7079
+ // HINST-04 / D-09-14: only the GLOBAL Claude skills kind reaches this —
7080
+ // `opts.configDir` is supplied by installRuntimeArtifacts for that kind
7081
+ // alone, and the local commands path never passes opts. A local install
7082
+ // writes no skills/, so its commands keep their absolute include (D-09-12).
7083
+ if (isGlobal && opts && opts.configDir) {
7084
+ content = rewriteGlobalClaudeSkillIncludes(content, {
7085
+ pathPrefix,
7086
+ absolutePrefix: path.resolve(opts.configDir).replace(/\\/g, '/').replace(/\/$/, '') + '/',
7087
+ });
7088
+ }
6895
7089
  content = processAttribution(content, getCommitAttribution(runtime));
6896
7090
  break;
6897
7091
 
@@ -6940,9 +7134,9 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6940
7134
  content = content.replace(/~\/\.claude\//g, pathPrefix);
6941
7135
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6942
7136
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6943
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6944
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6945
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7137
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
7138
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
7139
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6946
7140
  content = processAttribution(content, getCommitAttribution(runtime));
6947
7141
  break;
6948
7142
 
@@ -7326,7 +7520,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7326
7520
  let stagedForCopy = staged;
7327
7521
  const isGlobal = scope === 'global';
7328
7522
  if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
7329
- applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix, isGlobal);
7523
+ applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix, isGlobal, { configDir });
7330
7524
  } else if (kind.kind === 'commands') {
7331
7525
  // Returns a temp dir with rewritten content so source files are never mutated.
7332
7526
  stagedForCopy = applyRuntimeContentRewritesForCommandsInPlace(staged, runtime, pathPrefix, isGlobal);
@@ -7693,9 +7887,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
7693
7887
  content = content.replace(globalClaudeRegex, pathPrefix);
7694
7888
  content = content.replace(globalClaudeHomeRegex, pathPrefix);
7695
7889
  content = content.replace(localClaudeRegex, `./${dirName}/`);
7696
- content = content.replace(/~\/\.claude\b/g, pathPrefix.replace(/\/$/, ''));
7697
- content = content.replace(/\$HOME\/\.claude\b/g, pathPrefix.replace(/\/$/, ''));
7698
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7890
+ content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7891
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7892
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
7699
7893
  content = content.replace(/~\/\.qwen\//g, pathPrefix);
7700
7894
  content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
7701
7895
  content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
@@ -8005,8 +8199,42 @@ const GSD_UNINSTALL_HOOKS = [
8005
8199
  'pp-validate-commit.sh',
8006
8200
  'pp-phase-boundary.sh',
8007
8201
  'pp-graphify-update.sh',
8202
+ // D-09-08: absent from this list until phase 9 while still being registered
8203
+ // in the settings file, so a project the operator believed was uninstalled
8204
+ // kept a live pp-core PreToolUse hook on disk and wired.
8205
+ 'pp-worktree-path-guard.js',
8008
8206
  ];
8009
8207
 
8208
+ /**
8209
+ * Files pp-core ships into the runtime hooks/ dir and deliberately does NOT
8210
+ * remove on uninstall, each with the reason. D-09-08 locks the retention and
8211
+ * requires uninstall to NAME what it keeps rather than leave it silently.
8212
+ *
8213
+ * The pin in tests/hooks-opt-in.test.cjs asserts that this table plus
8214
+ * GSD_UNINSTALL_HOOKS together cover every name in
8215
+ * hooks/managed-hooks-registry.cjs (plus managed-hooks-registry.cjs itself,
8216
+ * which ships via HOOKS_TO_COPY and is not a hook), so the two lists cannot
8217
+ * drift apart silently again.
8218
+ */
8219
+ const GSD_UNINSTALL_RETAINED_HOOKS = Object.freeze([
8220
+ Object.freeze({
8221
+ file: 'managed-hooks-registry.cjs',
8222
+ reason: 'retained per D-09-08: the list pp-check-update-worker.js requires at runtime; '
8223
+ + 'not a hook, never registered in any settings file, referenced by no settings entry after uninstall',
8224
+ }),
8225
+ Object.freeze({
8226
+ file: 'pp-check-update-worker.js',
8227
+ reason: 'retained per D-09-08: a detached worker that pp-check-update.js spawns by path on '
8228
+ + 'SessionStart, never registered as a hook; a worker still running from the last session keeps '
8229
+ + 'resolving its require',
8230
+ }),
8231
+ Object.freeze({
8232
+ file: 'pp-ensure-canonical-path.js',
8233
+ reason: 'retained per D-09-08: the marketplace-plugin SessionStart bootstrap (#997); a classic '
8234
+ + 'install never registers it, so no settings entry references it after uninstall',
8235
+ }),
8236
+ ]);
8237
+
8010
8238
  /**
8011
8239
  * Uninstall pp-core from the specified directory for a specific runtime
8012
8240
  * Removes only GSD-specific files/directories, preserves user content
@@ -8405,6 +8633,9 @@ function uninstall(isGlobal, runtime = 'claude') {
8405
8633
  }
8406
8634
 
8407
8635
  // 4. Remove pp-core hooks
8636
+ // Populated by the removal block below and reported in the completion message
8637
+ // (D-09-08): uninstall names the files it deliberately keeps.
8638
+ let retainedHooks = [];
8408
8639
  const hooksDir = path.join(targetDir, 'hooks');
8409
8640
  if (fs.existsSync(hooksDir)) {
8410
8641
  let hookCount = 0;
@@ -8420,6 +8651,16 @@ function uninstall(isGlobal, runtime = 'claude') {
8420
8651
  console.log(` ${green}✓${reset} Removed ${hookCount} pp-core hooks`);
8421
8652
  }
8422
8653
 
8654
+ // D-09-08: name what survives, and why. Driven by what is actually still on
8655
+ // disk rather than by the table alone, so a runtime that never received a
8656
+ // given file is never told it was kept.
8657
+ retainedHooks = GSD_UNINSTALL_RETAINED_HOOKS.filter(
8658
+ (entry) => fs.existsSync(path.join(hooksDir, entry.file))
8659
+ );
8660
+ for (const entry of retainedHooks) {
8661
+ console.log(` ${yellow}i${reset} retained hooks/${entry.file} — ${entry.reason}`);
8662
+ }
8663
+
8423
8664
  // Remove only the pp-core-managed files from hooks/lib/ (git-cmd.js + pp-graphify-rebuild.sh).
8424
8665
  // hooks/lib/ lives inside the user's runtime hooks directory (shared space) and
8425
8666
  // may contain user-owned custom helpers. We must not recursively delete the dir.
@@ -8458,7 +8699,7 @@ function uninstall(isGlobal, runtime = 'claude') {
8458
8699
  'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
8459
8700
  'README.md', // documentation only — not user-authored
8460
8701
  ];
8461
- const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs'];
8702
+ const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'size-budget.cjs'];
8462
8703
 
8463
8704
  const changesetUninstallDir = path.join(targetDir, 'scripts', 'changeset');
8464
8705
  if (fs.existsSync(changesetUninstallDir)) {
@@ -8514,13 +8755,31 @@ function uninstall(isGlobal, runtime = 'claude') {
8514
8755
  }
8515
8756
  }
8516
8757
 
8517
- // 6. Clean up settings.json (remove pp-core hooks and statusline)
8518
- const settingsPath = path.join(targetDir, 'settings.json');
8519
- if (fs.existsSync(settingsPath)) {
8520
- let settings = readSettings(settingsPath);
8758
+ // 6. Clean up the settings file (remove pp-core hooks, statusline,
8759
+ // permissions and the worktree residue the installer wrote).
8760
+ //
8761
+ // D-09-06: the filename comes from resolveSettingsFileName — the SAME owner
8762
+ // the install path calls — so a local Claude uninstall opens the file a local
8763
+ // Claude install actually wrote. Hardcoding 'settings.json' here (as this did
8764
+ // before) left every pp-core hook entry behind in settings.local.json,
8765
+ // pointing at scripts the same uninstall had just deleted.
8766
+ //
8767
+ // @param {string} targetSettingsPath - Absolute path to the settings file
8768
+ // @param {boolean} opts.removeWhenEmpty - Delete the file when nothing but
8769
+ // pp-core-authored content was in it. True ONLY for local Claude's
8770
+ // settings.local.json; the repo-shared settings.json is user territory even
8771
+ // when it parses to {}.
8772
+ // @param {boolean} opts.pruneWorktreeBaseRef - Drop worktree:{baseRef:'head'}.
8773
+ // True ONLY where the installer can have written it (#683 applies it to
8774
+ // local Claude's settings.local.json and nowhere else).
8775
+ const cleanupSettingsFile = (targetSettingsPath, { removeWhenEmpty = false, pruneWorktreeBaseRef = false } = {}) => {
8776
+ if (!fs.existsSync(targetSettingsPath)) return;
8777
+ const settingsName = path.basename(targetSettingsPath);
8778
+ const settings = readSettings(targetSettingsPath);
8521
8779
  if (settings === null) {
8522
- console.log(` ${yellow}i${reset} Skipping settings.json cleanup — file could not be parsed`);
8523
- settings = {}; // prevent downstream crashes, but don't write back
8780
+ // Unparseable: never write it back and never delete it.
8781
+ console.log(` ${yellow}i${reset} Skipping ${settingsName} cleanup — file could not be parsed`);
8782
+ return;
8524
8783
  }
8525
8784
  let settingsModified = false;
8526
8785
 
@@ -8540,6 +8799,7 @@ function uninstall(isGlobal, runtime = 'claude') {
8540
8799
  // Claude-only FileChanged event added in #770 — safe to iterate for all
8541
8800
  // runtimes; installs that don't register these events simply find no
8542
8801
  // entries and skip.
8802
+ let hooksModified = false;
8543
8803
  for (const eventName of ['SessionStart', 'PostToolUse', 'AfterTool', 'PreToolUse', 'BeforeTool', 'SubagentStop', 'Stop', 'PreCompact', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'FileChanged']) {
8544
8804
  if (settings.hooks && settings.hooks[eventName]) {
8545
8805
  const before = JSON.stringify(settings.hooks[eventName]);
@@ -8558,14 +8818,15 @@ function uninstall(isGlobal, runtime = 'claude') {
8558
8818
  .filter(Boolean);
8559
8819
  if (JSON.stringify(settings.hooks[eventName]) !== before) {
8560
8820
  settingsModified = true;
8821
+ hooksModified = true;
8561
8822
  }
8562
8823
  if (settings.hooks[eventName].length === 0) {
8563
8824
  delete settings.hooks[eventName];
8564
8825
  }
8565
8826
  }
8566
8827
  }
8567
- if (settingsModified) {
8568
- console.log(` ${green}✓${reset} Removed pp-core hooks from settings`);
8828
+ if (hooksModified) {
8829
+ console.log(` ${green}✓${reset} Removed pp-core hooks from ${settingsName}`);
8569
8830
  }
8570
8831
 
8571
8832
  // Clean up empty hooks object
@@ -8574,6 +8835,9 @@ function uninstall(isGlobal, runtime = 'claude') {
8574
8835
  }
8575
8836
 
8576
8837
  // #768 — Remove GSD-owned Claude permissions from settings.json.
8838
+ // D-09-06: "settings.json" above is the historical wording; the file is now
8839
+ // whatever resolveSettingsFileName picked, which for a local Claude
8840
+ // install/uninstall is settings.local.json.
8577
8841
  // Applies only to Claude uninstalls. Filter only the exact GSD-owned entries
8578
8842
  // to preserve any user-added allow/deny entries.
8579
8843
  // Uses a local flag to avoid the shared `settingsModified` producing a false
@@ -8600,14 +8864,83 @@ function uninstall(isGlobal, runtime = 'claude') {
8600
8864
  }
8601
8865
  if (permissionsModified) {
8602
8866
  settingsModified = true;
8603
- console.log(` ${green}✓${reset} Removed pp-core permissions from settings.json`);
8867
+ console.log(` ${green}✓${reset} Removed pp-core permissions from ${settingsName}`);
8868
+ // D-09-06: prune ONLY the arrays this filter just emptied. A user's own
8869
+ // empty allow[] that we never touched is their content and stays — that
8870
+ // is why this is gated on permissionsModified rather than run always.
8871
+ for (const key of ['allow', 'deny']) {
8872
+ if (Array.isArray(settings.permissions[key]) && settings.permissions[key].length === 0) {
8873
+ delete settings.permissions[key];
8874
+ }
8875
+ }
8876
+ if (Object.keys(settings.permissions).length === 0) {
8877
+ delete settings.permissions;
8878
+ }
8604
8879
  }
8605
8880
  }
8606
8881
 
8607
- if (settingsModified) {
8608
- writeSettings(settingsPath, settings);
8882
+ // D-09-06: worktree:{baseRef:'head'} is the ONE shape applyWorktreeBaseRef
8883
+ // writes (src/worktree-base-ref.cts) and it never overwrites an explicit
8884
+ // other value — so exactly that object, with no extra keys, is installer
8885
+ // residue. Any other value, or extra keys, is user content and stays.
8886
+ if (pruneWorktreeBaseRef && settings.worktree && typeof settings.worktree === 'object' &&
8887
+ !Array.isArray(settings.worktree) &&
8888
+ Object.keys(settings.worktree).length === 1 && settings.worktree.baseRef === 'head') {
8889
+ delete settings.worktree;
8890
+ settingsModified = true;
8891
+ console.log(` ${green}✓${reset} Removed pp-core worktree.baseRef from ${settingsName}`);
8892
+ }
8893
+
8894
+ if (!settingsModified) return;
8895
+
8896
+ // D-09-06: when the filters emptied the file completely, nothing but
8897
+ // pp-core content had been in it — leave no husk behind. Gated on
8898
+ // settingsModified so a pre-existing empty file we never touched survives.
8899
+ if (removeWhenEmpty && Object.keys(settings).length === 0) {
8900
+ fs.unlinkSync(targetSettingsPath);
8609
8901
  removedCount++;
8902
+ console.log(` ${green}✓${reset} Removed ${settingsName} (only pp-core content remained)`);
8903
+ return;
8610
8904
  }
8905
+
8906
+ writeSettings(targetSettingsPath, settings);
8907
+ removedCount++;
8908
+ };
8909
+
8910
+ const isLocalClaudeUninstall = runtime === 'claude' && !isGlobal;
8911
+ cleanupSettingsFile(path.join(targetDir, resolveSettingsFileName(runtime, isGlobal)), {
8912
+ removeWhenEmpty: isLocalClaudeUninstall,
8913
+ pruneWorktreeBaseRef: isLocalClaudeUninstall,
8914
+ });
8915
+ if (isLocalClaudeUninstall) {
8916
+ // Legacy sweep, the mirror of install's #338 migration read: a pre-#338
8917
+ // local install wrote its managed entries into the repo-shared
8918
+ // settings.json. Filter them out of there too, but never delete that file
8919
+ // and never prune worktree residue from it — the installer writes neither
8920
+ // there.
8921
+ cleanupSettingsFile(path.join(targetDir, 'settings.json'), {
8922
+ removeWhenEmpty: false,
8923
+ pruneWorktreeBaseRef: false,
8924
+ });
8925
+ }
8926
+
8927
+ // The installer-migrations journal: installer-written, deliberately absent
8928
+ // from pp-file-manifest.json, and left behind by every uninstall before this
8929
+ // one. A stale journal makes a later install skip migrations it should run —
8930
+ // a stored record asserting something untrue about a directory that no longer
8931
+ // holds an install.
8932
+ const installStatePath = path.join(targetDir, 'pp-install-state.json');
8933
+ if (fs.existsSync(installStatePath)) {
8934
+ fs.rmSync(installStatePath, { force: true });
8935
+ removedCount++;
8936
+ console.log(` ${green}✓${reset} Removed pp-install-state.json`);
8937
+ }
8938
+
8939
+ // Installer-created artifact dirs that the layout-driven removal empties but
8940
+ // does not itself remove — mirror of the scripts/ handling above. A non-empty
8941
+ // dir (user content) throws and stays.
8942
+ for (const artifactDir of ['agents', 'commands']) {
8943
+ try { fs.rmdirSync(path.join(targetDir, artifactDir)); } catch (_) { /* Not empty — leave it */ }
8611
8944
  }
8612
8945
 
8613
8946
  // 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
@@ -8705,9 +9038,12 @@ function uninstall(isGlobal, runtime = 'claude') {
8705
9038
  console.log(` ${yellow}⚠${reset} No pp-core files found to remove.`);
8706
9039
  }
8707
9040
 
9041
+ const retainedSentence = retainedHooks.length > 0
9042
+ ? `\n Retained on disk (named per HINST-01): ${retainedHooks.map((e) => `hooks/${e.file}`).join(', ')}`
9043
+ : '';
8708
9044
  console.log(`
8709
9045
  ${green}Done!${reset} GSD has been uninstalled from ${runtimeLabel}.
8710
- Your other files and settings have been preserved.
9046
+ Your other files and settings have been preserved.${retainedSentence}
8711
9047
  `);
8712
9048
  }
8713
9049
 
@@ -10335,8 +10671,8 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10335
10671
  // Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
10336
10672
  const dirRegex = /~\/\.claude\//g;
10337
10673
  const homeDirRegex = /\$HOME\/\.claude\//g;
10338
- const bareDirRegex = /~\/\.claude\b/g;
10339
- const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
10674
+ const bareDirRegex = /~\/\.claude(?![\w-])/g;
10675
+ const bareHomeDirRegex = /\$HOME\/\.claude(?![\w-])/g;
10340
10676
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
10341
10677
  if (!isCopilot && !isAntigravity) {
10342
10678
  content = content.replace(dirRegex, pathPrefix);
@@ -10716,7 +11052,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10716
11052
  }
10717
11053
  throw err;
10718
11054
  }
10719
- const matches = content.match(/(?:~|\$HOME)\/\.claude\b/g);
11055
+ const matches = content.match(/(?:~|\$HOME)\/\.claude(?![\w-])/g);
10720
11056
  if (matches) {
10721
11057
  leakedPaths.push({ file: relPath, count: matches.length });
10722
11058
  }
@@ -11205,7 +11541,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11205
11541
  // so engineer-specific absolute paths (Node binary, home dir) never land in the repo-shared
11206
11542
  // settings.json. Global installs and all other runtimes continue to use settings.json.
11207
11543
  const isLocalClaude = (runtime === 'claude' && !isGlobal);
11208
- const settingsFileName = isLocalClaude ? 'settings.local.json' : 'settings.json';
11544
+ const settingsFileName = resolveSettingsFileName(runtime, isGlobal);
11209
11545
  const settingsPath = path.join(targetDir, settingsFileName);
11210
11546
 
11211
11547
  // #338 migration: if a prior local Claude install wrote GSD-shaped entries to settings.json,
@@ -11497,6 +11833,25 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11497
11833
  };
11498
11834
  }
11499
11835
 
11836
+ // The ONE owner of "how to start a project" in the installer. Every
11837
+ // runtime's Done! line names the init front door (git init, remote wiring, vault
11838
+ // binding, then handoff to /pp-discovery or /pp-new-project), derived from this
11839
+ // single stem so no runtime can drift back to naming the second step.
11840
+ const START_COMMAND_STEM = 'init';
11841
+
11842
+ /**
11843
+ * Resolve the start command in a runtime's own invocation syntax.
11844
+ * @param {string} runtime - installer runtime id (unknown ids get the default form)
11845
+ * @returns {string}
11846
+ */
11847
+ function resolveStartCommand(runtime) {
11848
+ if (runtime === 'gemini') return `/pp:${START_COMMAND_STEM}`;
11849
+ if (runtime === 'codex') return `$pp-${START_COMMAND_STEM}`;
11850
+ if (runtime === 'cursor') return `pp-${START_COMMAND_STEM} (mention the skill name)`;
11851
+ if (runtime === 'kimi') return `/skill:pp-${START_COMMAND_STEM}`;
11852
+ return `/pp-${START_COMMAND_STEM}`;
11853
+ }
11854
+
11500
11855
  /**
11501
11856
  * Apply statusline config, then print completion message
11502
11857
  */
@@ -11628,21 +11983,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11628
11983
  if (runtime === 'hermes') program = 'Hermes Agent';
11629
11984
  if (runtime === 'kimi') program = 'Kimi CLI';
11630
11985
 
11631
- let command = '/pp-new-project';
11632
- if (runtime === 'opencode') command = '/pp-new-project';
11633
- if (runtime === 'kilo') command = '/pp-new-project';
11634
- if (runtime === 'gemini') command = '/pp:new-project';
11635
- if (runtime === 'codex') command = '$pp-new-project';
11636
- if (runtime === 'copilot') command = '/pp-new-project';
11637
- if (runtime === 'antigravity') command = '/pp-new-project';
11638
- if (runtime === 'cursor') command = 'pp-new-project (mention the skill name)';
11639
- if (runtime === 'windsurf') command = '/pp-new-project';
11640
- if (runtime === 'augment') command = '/pp-new-project';
11641
- if (runtime === 'trae') command = '/pp-new-project';
11642
- if (runtime === 'cline') command = '/pp-new-project';
11643
- if (runtime === 'qwen') command = '/pp-new-project';
11644
- if (runtime === 'hermes') command = '/pp-new-project';
11645
- if (runtime === 'kimi') command = '/skill:pp-new-project';
11986
+ const command = resolveStartCommand(runtime);
11646
11987
 
11647
11988
  // Claude Code global installs use the skills/ format (CC 2.1.88+).
11648
11989
  // Restart is required for CC to pick up newly-installed skills, and the
@@ -11650,7 +11991,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11650
11991
  // cover both invocation paths to avoid #2957-style "no commands appear".
11651
11992
  if (runtime === 'claude' && isGlobal) {
11652
11993
  console.log(`
11653
- ${green}Done!${reset} Restart ${program}, then in any directory either type ${cyan}${command}${reset} or ask Claude to run the ${cyan}pp-new-project${reset} skill.
11994
+ ${green}Done!${reset} Restart ${program}, then in any directory either type ${cyan}${command}${reset} or ask Claude to run the ${cyan}pp-${START_COMMAND_STEM}${reset} skill.
11654
11995
  `);
11655
11996
  return;
11656
11997
  }
@@ -12315,6 +12656,9 @@ module.exports = {
12315
12656
  normalizeAgentBodyForRuntime,
12316
12657
  yamlIdentifier,
12317
12658
  computePathPrefix,
12659
+ // HINST-04 / D-09-14 — global Claude SKILL.md local-first dispatcher
12660
+ rewriteGlobalClaudeSkillIncludes,
12661
+ CLAUDE_PROJECT_DIR_SUBSTITUTES_IN_SKILL_BODY,
12318
12662
  applyRuntimeContentRewritesInPlace,
12319
12663
  getCodexSkillAdapterHeader,
12320
12664
  convertClaudeCommandToCursorSkill,
@@ -12429,12 +12773,15 @@ module.exports = {
12429
12773
  populatePristineDir,
12430
12774
  USER_OWNED_ARTIFACTS,
12431
12775
  finishInstall,
12776
+ resolveStartCommand,
12432
12777
  homePathCoveredByRc,
12433
12778
  maybeSuggestPathExport,
12434
12779
  runtimeMap,
12435
12780
  allRuntimes,
12436
12781
  selectRuntimesFromArgs,
12437
12782
  GSD_UNINSTALL_HOOKS,
12783
+ GSD_UNINSTALL_RETAINED_HOOKS,
12784
+ resolveSettingsFileName,
12438
12785
  parseRuntimeInput,
12439
12786
  buildRuntimePromptText,
12440
12787
  buildUpdateBannerPromptText,