@pareto4/pp-core 1.18.0 → 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 (147) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-code-fixer.md +9 -5
  3. package/agents/pp-code-reviewer.md +1 -0
  4. package/agents/pp-codebase-mapper.md +3 -0
  5. package/agents/pp-debugger.md +3 -2
  6. package/agents/pp-executor.md +16 -7
  7. package/agents/pp-integration-checker.md +1 -0
  8. package/agents/pp-phase-researcher.md +3 -1
  9. package/agents/pp-plan-checker.md +3 -0
  10. package/agents/pp-planner.md +5 -1
  11. package/agents/pp-research-synthesizer.md +1 -1
  12. package/agents/pp-ui-auditor.md +1 -0
  13. package/agents/pp-ui-researcher.md +3 -1
  14. package/agents/pp-verifier.md +10 -2
  15. package/commands/pp-add-tests.md +1 -1
  16. package/commands/pp-debug.md +1 -0
  17. package/commands/pp-init.md +4 -3
  18. package/commands/pp-quick.md +3 -0
  19. package/commands/pp-review-backlog.md +2 -1
  20. package/gemini-extension.json +1 -1
  21. package/package.json +1 -1
  22. package/pp-core/bin/lib/capability-registry.cjs +2 -2
  23. package/pp-core/bin/lib/check-command-router.cjs +60 -20
  24. package/pp-core/bin/lib/commands.cjs +79 -4
  25. package/pp-core/bin/lib/compose-bindings.cjs +97 -14
  26. package/pp-core/bin/lib/discovery-rollup.cjs +9 -6
  27. package/pp-core/bin/lib/discovery-screens.cjs +20 -2
  28. package/pp-core/bin/lib/discovery-sweep.cjs +12 -3
  29. package/pp-core/bin/lib/discovery-vault-check.cjs +37 -3
  30. package/pp-core/bin/lib/init-contract.cjs +48 -7
  31. package/pp-core/bin/lib/vault-coordinates.cjs +1 -1
  32. package/pp-core/bin/lib/vault-index.cjs +18 -5
  33. package/pp-core/bin/lib/vault-update.cjs +4 -0
  34. package/pp-core/bin/lib/vault.cjs +2 -2
  35. package/pp-core/bin/pp-tools.cjs +10 -4
  36. package/pp-core/org-profile.json +1 -1
  37. package/pp-core/references/autonomous-best-effort.md +3 -2
  38. package/pp-core/references/autonomous-lifecycle.md +1 -0
  39. package/pp-core/references/autonomous-smart-discuss.md +5 -1
  40. package/pp-core/references/discovery-id-conventions.md +35 -2
  41. package/pp-core/references/execute-mvp-tdd.md +3 -3
  42. package/pp-core/references/git-integration.md +57 -33
  43. package/pp-core/references/git-planning-commit.md +31 -9
  44. package/pp-core/references/phase-argument-parsing.md +1 -0
  45. package/pp-core/references/planner-gap-closure.md +1 -0
  46. package/pp-core/references/planner-revision.md +3 -1
  47. package/pp-core/references/planner-tdd.md +4 -4
  48. package/pp-core/references/planning-config.md +3 -3
  49. package/pp-core/references/shell-portability.md +28 -0
  50. package/pp-core/references/tdd.md +20 -16
  51. package/pp-core/references/worktree-path-safety.md +1 -0
  52. package/pp-core/templates/milestone.md +5 -5
  53. package/pp-core/workflows/add-backlog.md +1 -1
  54. package/pp-core/workflows/add-tests.md +5 -2
  55. package/pp-core/workflows/add-todo.md +3 -1
  56. package/pp-core/workflows/ai-integration-phase.md +2 -1
  57. package/pp-core/workflows/audit-fix.md +4 -3
  58. package/pp-core/workflows/audit-milestone.md +1 -0
  59. package/pp-core/workflows/autonomous.md +6 -0
  60. package/pp-core/workflows/check-todos.md +2 -1
  61. package/pp-core/workflows/cleanup.md +3 -1
  62. package/pp-core/workflows/code-review-fix.md +12 -5
  63. package/pp-core/workflows/code-review.md +15 -1
  64. package/pp-core/workflows/complete-milestone.md +13 -3
  65. package/pp-core/workflows/debug.md +2 -0
  66. package/pp-core/workflows/diagnose-issues.md +1 -1
  67. package/pp-core/workflows/discovery-sprint/session-13-gates.md +1 -1
  68. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +51 -7
  69. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +5 -3
  70. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +10 -8
  71. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +11 -2
  72. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +5 -5
  73. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +3 -3
  74. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +2 -2
  75. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +14 -2
  76. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +2 -2
  77. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +10 -10
  78. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +11 -8
  79. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +21 -18
  80. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +5 -3
  81. package/pp-core/workflows/discovery-sprint/templates/context-template.md +9 -3
  82. package/pp-core/workflows/discovery-sprint/vault-context.md +14 -3
  83. package/pp-core/workflows/discovery-sprint.md +11 -8
  84. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -0
  85. package/pp-core/workflows/discuss-phase-assumptions.md +5 -3
  86. package/pp-core/workflows/discuss-phase.md +8 -3
  87. package/pp-core/workflows/docs-update.md +1 -0
  88. package/pp-core/workflows/eval-review.md +2 -1
  89. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -6
  90. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -0
  91. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -0
  92. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +12 -0
  93. package/pp-core/workflows/execute-phase.md +19 -7
  94. package/pp-core/workflows/execute-plan.md +18 -7
  95. package/pp-core/workflows/explore.md +1 -1
  96. package/pp-core/workflows/forensics.md +4 -2
  97. package/pp-core/workflows/health.md +1 -0
  98. package/pp-core/workflows/import.md +2 -1
  99. package/pp-core/workflows/ingest-docs.md +1 -0
  100. package/pp-core/workflows/init/steps/client-context-capture.md +1 -0
  101. package/pp-core/workflows/init/steps/github-provisioning.md +145 -0
  102. package/pp-core/workflows/init/steps/gitlab-provisioning.md +18 -4
  103. package/pp-core/workflows/init/steps/reachability-outcome.md +3 -1
  104. package/pp-core/workflows/init.md +56 -66
  105. package/pp-core/workflows/map-codebase.md +5 -1
  106. package/pp-core/workflows/milestone-summary.md +2 -1
  107. package/pp-core/workflows/mvp-phase.md +1 -0
  108. package/pp-core/workflows/new-milestone.md +9 -5
  109. package/pp-core/workflows/new-project.md +9 -8
  110. package/pp-core/workflows/new-workspace.md +1 -0
  111. package/pp-core/workflows/next.md +2 -1
  112. package/pp-core/workflows/pause-work.md +6 -4
  113. package/pp-core/workflows/plan-milestone-gaps.md +3 -1
  114. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +1 -1
  115. package/pp-core/workflows/plan-phase.md +21 -6
  116. package/pp-core/workflows/plan-review-convergence.md +2 -0
  117. package/pp-core/workflows/plant-seed.md +4 -2
  118. package/pp-core/workflows/pr-branch.md +2 -0
  119. package/pp-core/workflows/progress.md +5 -0
  120. package/pp-core/workflows/quick.md +11 -4
  121. package/pp-core/workflows/reapply-patches.md +3 -0
  122. package/pp-core/workflows/remove-phase.md +1 -1
  123. package/pp-core/workflows/resume-project.md +3 -0
  124. package/pp-core/workflows/review.md +7 -1
  125. package/pp-core/workflows/secure-phase.md +2 -1
  126. package/pp-core/workflows/session-report.md +1 -0
  127. package/pp-core/workflows/ship.md +8 -1
  128. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  129. package/pp-core/workflows/sketch.md +3 -2
  130. package/pp-core/workflows/spec-phase.md +2 -1
  131. package/pp-core/workflows/spike-wrap-up.md +1 -1
  132. package/pp-core/workflows/spike.md +4 -3
  133. package/pp-core/workflows/sync-skills.md +5 -2
  134. package/pp-core/workflows/thread.md +3 -2
  135. package/pp-core/workflows/transition.md +6 -0
  136. package/pp-core/workflows/ui-phase.md +3 -1
  137. package/pp-core/workflows/ui-review.md +2 -1
  138. package/pp-core/workflows/ultraplan-phase.md +1 -0
  139. package/pp-core/workflows/undo.md +14 -10
  140. package/pp-core/workflows/update.md +5 -0
  141. package/pp-core/workflows/validate-phase.md +6 -3
  142. package/pp-core/workflows/vault-review.md +2 -1
  143. package/pp-core/workflows/vault-write-back.md +5 -0
  144. package/pp-core/workflows/verify-phase.md +5 -0
  145. package/pp-core/workflows/verify-work.md +8 -1
  146. package/scripts/prompt-injection-scan.sh +1 -1
  147. package/scripts/sync-runtime-launcher.cjs +115 -5
@@ -38,6 +38,7 @@ Extract from init JSON: `phase_dir`, `phase_number`, `phase_name`, `has_plans`,
38
38
 
39
39
  Then load phase details and list plans/summaries:
40
40
  ```bash
41
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
41
42
  pp_run query roadmap.get-phase "${phase_number}"
42
43
  grep -E "^| ${phase_number}" .planning/REQUIREMENTS.md 2>/dev/null || true
43
44
  ls "$phase_dir"/*-SUMMARY.md "$phase_dir"/*-PLAN.md 2>/dev/null || true
@@ -57,6 +58,7 @@ Extract **phase goal** from ROADMAP.md (the outcome to verify, not tasks), **req
57
58
  Use `pp-tools.cjs query` verify handlers (or legacy pp-tools) to extract must_haves from each PLAN:
58
59
 
59
60
  ```bash
61
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
60
62
  for plan in "$PHASE_DIR"/*-PLAN.md; do
61
63
  MUST_HAVES=$(pp_run query frontmatter.get "$plan" --field must_haves)
62
64
  echo "=== $plan ===" && echo "$MUST_HAVES"
@@ -124,6 +126,7 @@ For each truth: identify supporting artifacts → check artifact status → chec
124
126
  Use `pp-tools.cjs query verify.artifacts` (or legacy pp-tools) for artifact verification against must_haves in each PLAN:
125
127
 
126
128
  ```bash
129
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
127
130
  for plan in "$PHASE_DIR"/*-PLAN.md; do
128
131
  ARTIFACT_RESULT=$(pp_run query verify.artifacts "$plan")
129
132
  echo "=== $plan ===" && echo "$ARTIFACT_RESULT"
@@ -167,6 +170,7 @@ wiring or leftover code from plan revisions.
167
170
  Use `pp-tools.cjs query verify.key-links` (or legacy pp-tools) for key link verification against must_haves in each PLAN:
168
171
 
169
172
  ```bash
173
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
170
174
  for plan in "$PHASE_DIR"/*-PLAN.md; do
171
175
  LINKS_RESULT=$(pp_run query verify.key-links "$plan")
172
176
  echo "=== $plan ===" && echo "$LINKS_RESULT"
@@ -223,6 +227,7 @@ phase.
223
227
  no `<decisions>` block.
224
228
 
225
229
  ```bash
230
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
226
231
  GATE_CFG=$(pp_run query config-get workflow.context_coverage_gate 2>/dev/null || echo "true")
227
232
  if [ "$GATE_CFG" != "false" ]; then
228
233
  # Discover the phase CONTEXT.md via glob expansion rather than `ls | head`
@@ -38,6 +38,7 @@ If $ARGUMENTS contains a phase number, load context:
38
38
 
39
39
  ```bash
40
40
  _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
41
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
41
42
  GSD_WS=""
42
43
  echo "$ARGUMENTS" | grep -qE -- '--ws[[:space:]]+[^[:space:]]+' && GSD_WS=$(echo "$ARGUMENTS" | grep -oE -- '--ws[[:space:]]+[^[:space:]]+')
43
44
  PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[^[:space:]]+//g' | xargs)
@@ -51,6 +52,7 @@ AGENT_SKILLS_CHECKER=$(pp_run query agent-skills pp-plan-checker)
51
52
  Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`, `uat_path`.
52
53
 
53
54
  ```bash
55
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
54
56
  # MVP mode detection via the centralized phase.mvp-mode resolver.
55
57
  # verify-work has no --mvp CLI flag (mode is inherited from the planned phase),
56
58
  # so we omit --cli-flag — the verb falls through roadmap → config → false.
@@ -111,6 +113,7 @@ Continue to `create_uat_file`.
111
113
  Before UAT, check UI capability activation and whether Playwright/Puppeteer MCP tools are available.
112
114
 
113
115
  ```bash
116
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
114
117
  PLAN_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw)
115
118
  UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
116
119
  ```
@@ -148,6 +151,7 @@ UI checkpoints: {N} auto-verified, {M} queued for manual review
148
151
  Use `phase_dir` from init (or run init if not already done).
149
152
 
150
153
  ```bash
154
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
151
155
  ls "$phase_dir"/*-SUMMARY.md 2>/dev/null || true
152
156
  ```
153
157
 
@@ -167,6 +171,7 @@ When `MVP_MODE=false` (mode is null, absent, or the phase has no `**Mode:**` lin
167
171
  **User-story format guard.** When `MVP_MODE=true`, also verify the phase's goal is in User Story format via the centralized validator:
168
172
 
169
173
  ```bash
174
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
170
175
  PHASE_GOAL=$(pp_run query roadmap.get-phase "${phase_number}" ${GSD_WS} --pick goal)
171
176
  USER_STORY_VALID=$(pp_run query user-story.validate --story "$PHASE_GOAL" --pick valid)
172
177
  if [ "$USER_STORY_VALID" != "true" ]; then
@@ -434,7 +439,7 @@ Clear Current Test section:
434
439
 
435
440
  Commit the UAT file:
436
441
  ```bash
437
- pp_run query commit "test({phase_num}): complete UAT - {passed} passed, {issues} issues" --files ".planning/phases/XX-name/{phase_num}-UAT.md"
442
+ pp_run query commit "docs(planning): complete UAT - {passed} passed, {issues} issues" --phase "{phase_num}" --files ".planning/phases/XX-name/{phase_num}-UAT.md"
438
443
  ```
439
444
 
440
445
  Present summary:
@@ -458,6 +463,7 @@ Present summary:
458
463
  **If issues == 0:**
459
464
 
460
465
  ```bash
466
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
461
467
  VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw)
462
468
  printf '%s' "$VERIFY_POST_HOOKS_JSON" | node -e "let h=[];try{h=JSON.parse(require('fs').readFileSync(0,'utf-8')).activeHooks||[]}catch{}console.log('VERIFY_POST_HOOKS_JSON='+JSON.stringify({activeHooks:h}))"
463
469
  SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
@@ -478,6 +484,7 @@ Skill(skill="pp-${ref.skill}", args="{phase}")
478
484
  After the skill returns, refresh `SECURITY_FILE`:
479
485
 
480
486
  ```bash
487
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
481
488
  SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
482
489
  ```
483
490
 
@@ -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'
@@ -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) {