@pareto4/pp-core 1.17.1 → 1.19.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 (199) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +5 -3
  3. package/agents/pp-code-fixer.md +9 -16
  4. package/agents/pp-code-reviewer.md +2 -78
  5. package/agents/pp-codebase-mapper.md +3 -0
  6. package/agents/pp-debugger.md +3 -2
  7. package/agents/pp-executor.md +25 -30
  8. package/agents/pp-integration-checker.md +1 -0
  9. package/agents/pp-phase-researcher.md +3 -1
  10. package/agents/pp-plan-checker.md +3 -0
  11. package/agents/pp-planner.md +16 -12
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-auditor.md +1 -0
  14. package/agents/pp-ui-researcher.md +3 -1
  15. package/agents/pp-vault-reviewer.md +43 -189
  16. package/agents/pp-verifier.md +10 -8
  17. package/commands/pp-add-tests.md +1 -1
  18. package/commands/pp-debug.md +1 -0
  19. package/commands/pp-init.md +4 -3
  20. package/commands/pp-ns-manage.md +1 -1
  21. package/commands/pp-ns-review.md +1 -1
  22. package/commands/pp-quick.md +3 -0
  23. package/commands/pp-review-backlog.md +2 -1
  24. package/commands/pp-vault-promote.md +7 -8
  25. package/commands/pp-vault-review.md +6 -7
  26. package/gemini-extension.json +1 -1
  27. package/package.json +3 -6
  28. package/pp-core/bin/lib/capability-registry.cjs +15 -357
  29. package/pp-core/bin/lib/capability-state.cjs +15 -79
  30. package/pp-core/bin/lib/capability-writer.cjs +2 -16
  31. package/pp-core/bin/lib/check-command-router.cjs +342 -2298
  32. package/pp-core/bin/lib/clusters.cjs +2 -2
  33. package/pp-core/bin/lib/commands.cjs +79 -4
  34. package/pp-core/bin/lib/compose-bindings.cjs +99 -16
  35. package/pp-core/bin/lib/config.cjs +3 -6
  36. package/pp-core/bin/lib/discovery-rollup.cjs +9 -6
  37. package/pp-core/bin/lib/discovery-screens.cjs +20 -2
  38. package/pp-core/bin/lib/discovery-sweep.cjs +12 -3
  39. package/pp-core/bin/lib/discovery-vault-check.cjs +37 -3
  40. package/pp-core/bin/lib/git-base-branch.cjs +10 -13
  41. package/pp-core/bin/lib/git-provider.cjs +5 -6
  42. package/pp-core/bin/lib/init-contract.cjs +55 -42
  43. package/pp-core/bin/lib/init-interactive.cjs +6 -20
  44. package/pp-core/bin/lib/io.cjs +2 -8
  45. package/pp-core/bin/lib/loop-resolver.cjs +8 -32
  46. package/pp-core/bin/lib/org-profile.cjs +16 -92
  47. package/pp-core/bin/lib/vault-command-router.cjs +174 -1206
  48. package/pp-core/bin/lib/vault-coordinates.cjs +4 -4
  49. package/pp-core/bin/lib/vault-index.cjs +44 -74
  50. package/pp-core/bin/lib/vault-seal.cjs +37 -122
  51. package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
  52. package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
  53. package/pp-core/bin/lib/vault-standards.cjs +15 -16
  54. package/pp-core/bin/lib/vault-trace.cjs +40 -3016
  55. package/pp-core/bin/lib/vault-update.cjs +5 -26
  56. package/pp-core/bin/lib/vault.cjs +122 -316
  57. package/pp-core/bin/pp-tools.cjs +15 -36
  58. package/pp-core/org-profile.json +1 -1
  59. package/pp-core/references/autonomous-best-effort.md +4 -3
  60. package/pp-core/references/autonomous-lifecycle.md +1 -0
  61. package/pp-core/references/autonomous-smart-discuss.md +5 -1
  62. package/pp-core/references/discovery-id-conventions.md +35 -2
  63. package/pp-core/references/execute-mvp-tdd.md +3 -3
  64. package/pp-core/references/git-integration.md +57 -33
  65. package/pp-core/references/git-planning-commit.md +31 -9
  66. package/pp-core/references/phase-argument-parsing.md +1 -0
  67. package/pp-core/references/planner-gap-closure.md +1 -0
  68. package/pp-core/references/planner-revision.md +3 -1
  69. package/pp-core/references/planner-tdd.md +4 -4
  70. package/pp-core/references/planner-vault-standards.md +16 -25
  71. package/pp-core/references/planning-config.md +3 -3
  72. package/pp-core/references/shell-portability.md +28 -0
  73. package/pp-core/references/tdd.md +20 -16
  74. package/pp-core/references/worktree-path-safety.md +1 -0
  75. package/pp-core/templates/README.md +1 -1
  76. package/pp-core/templates/VAULT-REVIEW.md +24 -170
  77. package/pp-core/templates/milestone.md +5 -5
  78. package/pp-core/workflows/add-backlog.md +1 -1
  79. package/pp-core/workflows/add-tests.md +8 -5
  80. package/pp-core/workflows/add-todo.md +3 -1
  81. package/pp-core/workflows/ai-integration-phase.md +2 -1
  82. package/pp-core/workflows/audit-fix.md +7 -6
  83. package/pp-core/workflows/audit-milestone.md +1 -0
  84. package/pp-core/workflows/autonomous.md +6 -0
  85. package/pp-core/workflows/check-todos.md +2 -1
  86. package/pp-core/workflows/cleanup.md +3 -1
  87. package/pp-core/workflows/code-review-fix.md +15 -8
  88. package/pp-core/workflows/code-review.md +16 -58
  89. package/pp-core/workflows/complete-milestone.md +13 -3
  90. package/pp-core/workflows/debug.md +4 -2
  91. package/pp-core/workflows/diagnose-issues.md +1 -1
  92. package/pp-core/workflows/discovery-sprint/prototype.md +2 -2
  93. package/pp-core/workflows/discovery-sprint/session-13-gates.md +1 -1
  94. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +51 -7
  95. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +5 -3
  96. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +10 -8
  97. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +11 -2
  98. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +5 -5
  99. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +3 -3
  100. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +2 -2
  101. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +14 -2
  102. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +2 -2
  103. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +10 -10
  104. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +11 -8
  105. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +21 -18
  106. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +5 -3
  107. package/pp-core/workflows/discovery-sprint/templates/context-template.md +9 -3
  108. package/pp-core/workflows/discovery-sprint/vault-context.md +17 -12
  109. package/pp-core/workflows/discovery-sprint.md +11 -8
  110. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -0
  111. package/pp-core/workflows/discuss-phase-assumptions.md +5 -3
  112. package/pp-core/workflows/discuss-phase.md +8 -3
  113. package/pp-core/workflows/docs-update.md +1 -0
  114. package/pp-core/workflows/eval-review.md +2 -1
  115. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +13 -14
  116. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -0
  117. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +5 -25
  118. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -0
  119. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +18 -6
  120. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +6 -109
  121. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +1 -17
  122. package/pp-core/workflows/execute-phase.md +21 -12
  123. package/pp-core/workflows/execute-plan.md +24 -15
  124. package/pp-core/workflows/explore.md +1 -1
  125. package/pp-core/workflows/forensics.md +4 -2
  126. package/pp-core/workflows/health.md +1 -0
  127. package/pp-core/workflows/help/modes/full.md +1 -1
  128. package/pp-core/workflows/import.md +2 -1
  129. package/pp-core/workflows/ingest-docs.md +1 -0
  130. package/pp-core/workflows/init/steps/client-context-capture.md +1 -0
  131. package/pp-core/workflows/init/steps/github-provisioning.md +145 -0
  132. package/pp-core/workflows/init/steps/gitlab-provisioning.md +18 -4
  133. package/pp-core/workflows/init/steps/reachability-outcome.md +3 -1
  134. package/pp-core/workflows/init.md +56 -66
  135. package/pp-core/workflows/map-codebase.md +5 -1
  136. package/pp-core/workflows/milestone-summary.md +2 -1
  137. package/pp-core/workflows/mvp-phase.md +1 -0
  138. package/pp-core/workflows/new-milestone.md +9 -5
  139. package/pp-core/workflows/new-project.md +9 -8
  140. package/pp-core/workflows/new-workspace.md +1 -0
  141. package/pp-core/workflows/next.md +2 -1
  142. package/pp-core/workflows/pause-work.md +6 -4
  143. package/pp-core/workflows/plan-milestone-gaps.md +3 -1
  144. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +1 -1
  145. package/pp-core/workflows/plan-phase.md +21 -6
  146. package/pp-core/workflows/plan-review-convergence.md +2 -0
  147. package/pp-core/workflows/plant-seed.md +4 -2
  148. package/pp-core/workflows/pr-branch.md +2 -0
  149. package/pp-core/workflows/progress.md +5 -0
  150. package/pp-core/workflows/quick.md +19 -12
  151. package/pp-core/workflows/reapply-patches.md +3 -0
  152. package/pp-core/workflows/remove-phase.md +1 -1
  153. package/pp-core/workflows/resume-project.md +3 -0
  154. package/pp-core/workflows/review.md +7 -1
  155. package/pp-core/workflows/secure-phase.md +2 -1
  156. package/pp-core/workflows/session-report.md +1 -0
  157. package/pp-core/workflows/ship.md +8 -1
  158. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  159. package/pp-core/workflows/sketch.md +3 -2
  160. package/pp-core/workflows/spec-phase.md +2 -1
  161. package/pp-core/workflows/spike-wrap-up.md +1 -1
  162. package/pp-core/workflows/spike.md +4 -3
  163. package/pp-core/workflows/sync-skills.md +5 -2
  164. package/pp-core/workflows/thread.md +3 -2
  165. package/pp-core/workflows/transition.md +6 -0
  166. package/pp-core/workflows/ui-phase.md +3 -1
  167. package/pp-core/workflows/ui-review.md +2 -1
  168. package/pp-core/workflows/ultraplan-phase.md +1 -0
  169. package/pp-core/workflows/undo.md +14 -10
  170. package/pp-core/workflows/update.md +5 -0
  171. package/pp-core/workflows/validate-phase.md +9 -6
  172. package/pp-core/workflows/vault-review.md +65 -186
  173. package/pp-core/workflows/vault-write-back.md +78 -110
  174. package/pp-core/workflows/verify-phase.md +5 -0
  175. package/pp-core/workflows/verify-work.md +9 -2
  176. package/scripts/gen-capability-registry.cjs +12 -93
  177. package/scripts/lint-test-file-count.allowlist.json +1 -19
  178. package/scripts/lint-test-file-count.cjs +5 -6
  179. package/scripts/prompt-injection-scan.sh +1 -4
  180. package/scripts/sync-runtime-launcher.cjs +115 -5
  181. package/pp-core/bin/lib/practices-review-check.cjs +0 -944
  182. package/pp-core/bin/lib/vault-audit.cjs +0 -2594
  183. package/pp-core/bin/lib/vault-mode.cjs +0 -235
  184. package/pp-core/bin/lib/vault-practices.cjs +0 -2057
  185. package/pp-core/bin/lib/vault-review.cjs +0 -2102
  186. package/pp-core/bin/lib/vault-write-back.cjs +0 -905
  187. package/pp-core/references/planner-vault-practices.md +0 -95
  188. package/pp-core/references/practice-arithmetic-contract.md +0 -121
  189. package/pp-core/references/verifier-practice-verdicts.md +0 -263
  190. package/pp-core/templates/practices/README.md +0 -248
  191. package/pp-core/templates/practices/error-handling.md +0 -18
  192. package/pp-core/templates/practices/minimal-comments.md +0 -14
  193. package/pp-core/templates/practices/naming-convention.md +0 -18
  194. package/pp-core/templates/practices/practice-template.md +0 -23
  195. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +0 -165
  196. package/scripts/gen-practice-arithmetic-contract.cjs +0 -412
  197. package/scripts/practice-arithmetic-contract.allow.json +0 -35
  198. package/scripts/record-vault-mode-golden.cjs +0 -89
  199. package/scripts/vault-calibrate.cjs +0 -996
@@ -80,12 +80,11 @@ function prodPrefix(filename) {
80
80
  // stamps. Five forms are stripped: `.integration`, `.slow`, `.security`,
81
81
  // `.install` and the bare `.test`.
82
82
  //
83
- // `.property` is DELIBERATELY NOT stripped. The `vault` entry in
84
- // scripts/lint-test-file-count.allowlist.json documents
85
- // vault-practices.property.test.cjs as parked in that bucket precisely because
86
- // this function leaves the suffix on — stripping it would re-cluster the file
87
- // into `vault-practices` and turn that entry's own explanation into a false
88
- // statement. Changing it is a deliberate allowlist migration, not a tidy-up.
83
+ // `.property` is DELIBERATELY NOT stripped: a `<name>.property.test.cjs` keeps
84
+ // the suffix in its prefix, so the hyphen-prefix matcher clusters it under the
85
+ // shortest matching module rather than under `<name>`. Stripping it would
86
+ // re-cluster every property test the allowlist has already counted. Changing
87
+ // it is a deliberate allowlist migration, not a tidy-up.
89
88
  function testEffectivePrefix(testName) {
90
89
  const bare = testName
91
90
  .replace(/\.(integration|slow|security|install)\.test\.(ts|cjs)$/, '')
@@ -84,7 +84,7 @@ PATTERNS=(
84
84
  ALLOWLIST=(
85
85
  'scripts/prompt-injection-scan.sh::the PATTERNS array above is the literal source of every phrase this scanner looks for, so the file matches itself by construction; measured 4 pattern hits at HEAD on 2026-09-19'
86
86
  'scripts/base64-scan.sh::decodes base64 blobs and re-scans the recovered plaintext against the same injection vocabulary, so it names those phrases inline; measured 2 pattern hits at HEAD on 2026-09-19'
87
- 'scripts/secret-scan.sh::third of the three diff-mode scan scripts CI runs together in the security_scan job of .github/workflows/ci.yml; measured 0 pattern hits at HEAD on 2026-09-19, retained because its own exemption list at line 223 already names security-prompt-injection.security.test.cjs and grows with that vocabulary'
87
+ 'scripts/secret-scan.sh::third of the three diff-mode scan scripts CI runs together in the pr_gates job of .github/workflows/ci.yml; measured 0 pattern hits at HEAD on 2026-09-19, retained because its own exemption list at line 223 already names security-prompt-injection.security.test.cjs and grows with that vocabulary'
88
88
  'tests/security-scan.security.test.cjs::the behavioural suite for this very scanner, so every positive case embeds a real injection phrase as fixture input; measured 28 pattern hits at HEAD on 2026-09-19'
89
89
  'tests/security.test.cjs::the adversarial suite for the library sanitisers, whose fixtures are real override and role-manipulation strings asserted to be neutralised; measured 54 pattern hits at HEAD on 2026-09-19'
90
90
  'tests/prompt-injection-scan.security.test.cjs::drives scanForInjection in the compiled security library with crafted payloads, and carries a second allowlist of its own for that separate scanner; measured 11 pattern hits at HEAD on 2026-09-19'
@@ -95,9 +95,6 @@ ALLOWLIST=(
95
95
  'tests/fixtures/adversarial/security/::a directory of adversarial corpus files whose entire purpose is to BE injection payloads for the suites above; measured 20 pattern hits across its 7 tracked files at HEAD on 2026-09-19'
96
96
  'tests/discovery-vault-context.test.cjs::the DISCO-03/SC4 suite plants a real role-manipulation line (the PLANTED constant) into a fixture vault document and asserts the shipped scanner FINDS it, which is the non-vacuity partner for the assertion that the subflow itself scans clean; the payload has to be a literal for that half to mean anything, so the file matches the vocabulary it exists to prove is caught'
97
97
  'tests/vault-status-check.test.cjs::sanitisation seeds for the vault status message carry a real instruction and a real control character on purpose, each paired with a non-vacuity assertion that the RENDERED message does not carry it intact; measured 5 pattern hits at HEAD on 2026-09-19'
98
- 'tests/vault-practices-matcher.test.cjs::the CR-01 fence-spoof suite plants a real instruction-override line and a forged end-fence marker inside practice bodies on purpose, each paired with an assertion that the RENDERED practices block keeps the planted text inside the fence and leaves exactly one genuine closer, so the payload must be a literal for the defang proof to mean anything; measured 3 pattern hits at HEAD b4dd6b3 on 2026-10-04'
99
- 'tests/vault-review.security.test.cjs::the CR-02 suite seeds a practice body with directive-shaped text aimed at the practice judge (the DIRECTIVE_TEXT constant) and asserts the rendered review entry carries it only between the fence markers, after the reference-data preamble, so the judge reads it as data; the text must be a literal for that assertion to mean anything; measured 1 pattern hit at HEAD b4dd6b3 on 2026-10-04'
100
- 'tests/practices-review-check-property.test.cjs::a false positive rather than a payload, since the fence-parsing invariance property builds a never-closed fence whose second-opener line is ordinary English about whether the client line becomes a cut point, and one role-pattern regex matches that plain wording; the fixture text stays as written; measured 1 pattern hit at HEAD b4dd6b3 on 2026-10-04'
101
98
  'tests/vault-index.integration.test.cjs::the fence-spoof suite seeds a vault document heading and an INDEX description that carry a forged end-fence marker followed by an instruction-override phrase, and asserts the rendered SUMMARY keeps exactly one live end marker with the planted line defanged inside the fence, so the phrase must be a literal for the defang proof to mean anything; measured 4 pattern hits at HEAD 5ddb0b3 on 2026-10-12'
102
99
  'tests/vault-seal.integration.test.cjs::the uncovered-commit suite builds a commit whose subject is an instruction-override phrase padded past the cap, modelling a hostile commit message, and asserts the subject leaving the seal leaf is controls-collapsed and bounded to SEAL_SUBJECT_CAP before it can reach a record; measured 1 pattern hit at HEAD 5ddb0b3 on 2026-10-12'
103
100
  'tests/vault-standards.integration.test.cjs::the D-14 suites plant instruction-override lines in a standards document and in its INDEX description, each tagged with a random token, and assert that neither the token nor the phrase appears in any output of vault standards block or consulted, so the phrase must be a literal for the absence proof to mean anything; measured 6 pattern hits at HEAD 5ddb0b3 on 2026-10-12'
@@ -12,6 +12,13 @@
12
12
  * (document order) that contains a pp_run call. All other bash blocks
13
13
  * keep their pp_run calls with NO preamble. (Define once per file,
14
14
  * use across blocks — original footprint.)
15
+ * 4. Start every bash/sh/shell/zsh block that zsh would run differently
16
+ * from bash with ZSH_GUARD (right after the preamble where the block
17
+ * carries one), and drop the guard from a block that no longer needs it.
18
+ * Each block may run in its own shell, and an agent's shell is often zsh:
19
+ * the guard makes zsh read the block as sh (unmatched globs stay literal,
20
+ * `====` is a word, unquoted words split, arrays index from 0) and is a
21
+ * no-op under bash. See needsZshGuard for what counts.
15
22
  *
16
23
  * Run: node scripts/sync-runtime-launcher.cjs
17
24
  */
@@ -28,6 +35,57 @@ const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
28
35
  */
29
36
  const LAUNCHER_HEAD = '_PP_SHIM_NAME="pp-tools.cjs"; ';
30
37
 
38
+ /**
39
+ * The first line of every shell block (issue #12 items 2-4). Exits 0 under
40
+ * bash, so a block's status never depends on it.
41
+ */
42
+ const ZSH_GUARD = '[ -z "${ZSH_VERSION:-}" ] || emulate -R sh';
43
+ const GUARDED_LANGS = Object.freeze(['bash', 'sh', 'shell', 'zsh']);
44
+
45
+ /**
46
+ * True when zsh would run `lines` differently from bash: an unquoted glob
47
+ * character, a word starting with `=` plus more (`====`, `==`), an unquoted
48
+ * `$var` (zsh does not split it), or array syntax (zsh indexes from 1). Errs
49
+ * towards true — a needless guard costs a line, a missing one breaks a run.
50
+ * The launcher preamble is zsh-safe and is never inspected.
51
+ */
52
+ function needsZshGuard(lines) {
53
+ for (const raw of lines) {
54
+ const line = raw.trim();
55
+ if (line === '' || line.startsWith('#') || isLauncherLine(line) || line === ZSH_GUARD) continue;
56
+ if (/\$\{#?\w+\[/.test(line) || /\b\w+\+?=\(/.test(line)) return true;
57
+ // Neither shell globs, splits or `=`-expands inside `[[ … ]]` (syntax, not
58
+ // a command), in a case pattern (`*)`), or on an assignment's right side.
59
+ const bare = line
60
+ .replace(/'[^']*'/g, "''")
61
+ .replace(/"(?:\\.|[^"\\])*"/g, '""')
62
+ .replace(/\[\[\s.*?\s\]\]/g, '[[ ]]')
63
+ .replace(/(^|[\s;(|])\*\)/g, '$1)')
64
+ .replace(/(^|[\s;&|])([A-Za-z_]\w*)=\$(?=[{\w])/g, '$1$2=');
65
+ if (/(^|[\s;|&(])=[^\s]/.test(bare)) return true;
66
+ if (/\$(\w|\{)/.test(bare)) return true;
67
+ if (/[*?]/.test(bare.replace(/\$[*?#@!$-]/g, ''))) return true;
68
+ if (/[^\s\[]\[[^\]\s]*\]/.test(bare)) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * Make `lines` carry ZSH_GUARD exactly when needsZshGuard says so: at the top
75
+ * (after leading blanks, and after the preamble when the block starts with
76
+ * it), indented like the fence. Idempotent.
77
+ */
78
+ function insertZshGuard(lines, preamble, indent) {
79
+ const rest = lines.filter((l) => l.trim() !== ZSH_GUARD);
80
+ if (!needsZshGuard(rest)) return rest.length === lines.length ? lines : rest;
81
+ let at = 0;
82
+ while (at < rest.length && rest[at].trim() === '') at++;
83
+ if (at === rest.length) return lines;
84
+ if (preamble.length > 0 && preamble.every((p, k) => rest[at + k] === p)) at += preamble.length;
85
+ const out = [...rest.slice(0, at), indent + ZSH_GUARD, ...rest.slice(at)];
86
+ return out.join('\n') === lines.join('\n') ? lines : out;
87
+ }
88
+
31
89
  /**
32
90
  * Launcher carriers that live OUTSIDE this script's two roots
33
91
  * (pp-core/workflows/ and agents/).
@@ -387,7 +445,7 @@ function transformFile(content, preamble) {
387
445
  if (i < allLines.length) i++;
388
446
 
389
447
  if (isShellBlock) {
390
- shellBlockRanges.push({ openLineIdx, contentStart, contentEnd, closingLineIdx });
448
+ shellBlockRanges.push({ openLineIdx, contentStart, contentEnd, closingLineIdx, lang, indent });
391
449
  }
392
450
  }
393
451
 
@@ -418,10 +476,9 @@ function transformFile(content, preamble) {
418
476
 
419
477
  // Insert preamble into the first pp_run block only
420
478
  const finalBlocks = strippedBlocks.map((stripped, bi) => {
421
- if (bi === firstPpRunBlockIdx) {
422
- return insertPreamble(stripped, preamble);
423
- }
424
- return stripped;
479
+ const withPreamble = bi === firstPpRunBlockIdx ? insertPreamble(stripped, preamble) : stripped;
480
+ const range = shellBlockRanges[bi];
481
+ return GUARDED_LANGS.includes(range.lang) ? insertZshGuard(withPreamble, preamble, range.indent) : withPreamble;
425
482
  });
426
483
 
427
484
  // --- Pass 3: splice back into output (reverse order to preserve indices) ---
@@ -443,6 +500,43 @@ function transformFile(content, preamble) {
443
500
  return outputLines.join('\n');
444
501
  }
445
502
 
503
+ /**
504
+ * Guard-only pass for the roots whose launcher lines are kept in place
505
+ * (commands/, pp-core/references/): every non-empty bash/sh/shell/zsh block
506
+ * gets ZSH_GUARD, after a launcher line that opens it. pp-core/templates/ is
507
+ * never passed here — its blocks are text written into the user's project.
508
+ * Returns new content, or null when nothing changes.
509
+ */
510
+ function guardOnlyTransform(content) {
511
+ const lines = content.split('\n');
512
+ const out = [];
513
+ let changed = false;
514
+ let i = 0;
515
+ while (i < lines.length) {
516
+ const open = lines[i].match(/^(\s*)```(\w+)?\s*$/);
517
+ out.push(lines[i]);
518
+ i++;
519
+ if (!open) continue;
520
+ const indent = open[1];
521
+ const lang = (open[2] || '').toLowerCase();
522
+ const closing = new RegExp('^' + escapeRegExp(indent) + '```\\s*$');
523
+ const body = [];
524
+ while (i < lines.length && !closing.test(lines[i])) body.push(lines[i++]);
525
+ let next = body;
526
+ if (GUARDED_LANGS.includes(lang)) {
527
+ const first = body.find((l) => l.trim() !== '');
528
+ next = insertZshGuard(body, first !== undefined && isLauncherLine(first) ? [first] : [], indent);
529
+ if (next !== body) changed = true;
530
+ }
531
+ out.push(...next);
532
+ if (i < lines.length) out.push(lines[i++]);
533
+ }
534
+ return changed ? out.join('\n') : null;
535
+ }
536
+
537
+ /** The roots that get only the guard pass. */
538
+ const GUARD_ONLY_DIRS = Object.freeze(['commands', path.join('pp-core', 'references')]);
539
+
446
540
  function escapeRegExp(str) {
447
541
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
448
542
  }
@@ -511,6 +605,18 @@ function main() {
511
605
  }
512
606
  }
513
607
 
608
+ for (const dir of GUARD_ONLY_DIRS) {
609
+ for (const f of collectFiles(path.join(repoRoot, dir))) {
610
+ const content = fs.readFileSync(f, 'utf8');
611
+ const result = guardOnlyTransform(content);
612
+ if (result !== null) {
613
+ fs.writeFileSync(f, result, 'utf8');
614
+ transformedCount++;
615
+ console.log(`transformed (guard): ${path.relative(repoRoot, f)}`);
616
+ }
617
+ }
618
+ }
619
+
514
620
  console.log(`\nDone. ${transformedCount} files transformed, ${unchangedCount} unchanged.`);
515
621
  }
516
622
 
@@ -523,6 +629,10 @@ module.exports = {
523
629
  replaceCanonicalLines,
524
630
  EXTRA_CARRIERS,
525
631
  LAUNCHER_HEAD,
632
+ ZSH_GUARD,
633
+ needsZshGuard,
634
+ guardOnlyTransform,
635
+ GUARD_ONLY_DIRS,
526
636
  };
527
637
 
528
638
  if (require.main === module) {