@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
@@ -0,0 +1,145 @@
1
+ # Gated GitHub provisioning — H-2..H-5 (D-05, D-09, D-10, D-11, D-12, D-13)
2
+
3
+ Lazily Read by `pp-core/workflows/init.md` § "Gated GitLab/GitHub provisioning" — Step 3's
4
+ other sub-path — and ONLY when BOTH of that section's gates hold: `.platform-bindings.json`
5
+ is present AND `resolveProvider` answers `github`. Both gates, G-1's one-source-per-value
6
+ config reads and the empty-group guard stay in `init.md`; this file is the GitHub create
7
+ recipe those guards protect, the counterpart of `gitlab-provisioning.md` step for step.
8
+ `$GROUP` (the GitHub organization), `$SLUG`, `$VIS`, `$DESC` and `$HOST` are the variables
9
+ G-1 has already set. Every `gh` call names the host through `GH_HOST`, so a GitHub
10
+ Enterprise host in the binding is honored and `github.com` is the default:
11
+
12
+ ```bash
13
+ GH_HOSTNAME="${HOST:-github.com}"
14
+ ```
15
+
16
+ #### H-2. Auth verify-only (D-11)
17
+
18
+ `/pp-init` **never establishes auth and never reads or writes the user's token.**
19
+ Secrets stay in the environment or OS keyring. Only verify that a session exists:
20
+
21
+ ```bash
22
+ gh auth status --hostname "$GH_HOSTNAME" >/dev/null 2>&1 || {
23
+ echo "Not authenticated to GitHub ($GH_HOSTNAME). Run: gh auth login --hostname $GH_HOSTNAME"
24
+ exit 1
25
+ }
26
+ ```
27
+
28
+ If this check fails, stop immediately. Do not proceed to probe or create.
29
+ Never run `gh auth login` from within `/pp-init`.
30
+
31
+ #### H-3. Existence probe → adopt-if-exists (D-09, D-12)
32
+
33
+ Pre-check with a read-only probe before any create attempt. The create command is **not
34
+ idempotent** — re-running it against an existing repository fails, and a create whose
35
+ remote-add step fails has still created the repository. The probe avoids this landmine.
36
+
37
+ This is one self-contained block: it sets `PROJECT_EXISTS` and, on the exists path,
38
+ adopts (links the remote). It NEVER creates — create lives in its own H-4 block, gated
39
+ on `PROJECT_EXISTS=false`, so the create invocation can never fall inside the exists
40
+ branch.
41
+
42
+ ```bash
43
+ if GH_HOST="$GH_HOSTNAME" gh repo view "$GROUP/$SLUG" --json name >/dev/null 2>&1; then
44
+ # Repository already exists — ADOPT: link remote only, skip create (D-12)
45
+ PROJECT_EXISTS=true
46
+ echo "GitHub repository $GROUP/$SLUG already exists — adopting (linking remote, skipping create)."
47
+ # Match the remote scheme gh itself uses on the create path — honor the user's
48
+ # `gh config get git_protocol` (https default) instead of hardcoding SSH.
49
+ GIT_PROTO=$(gh config get git_protocol --host "$GH_HOSTNAME" 2>/dev/null || echo https)
50
+ if [ "$GIT_PROTO" = "ssh" ]; then
51
+ ADOPT_URL="git@$GH_HOSTNAME:$GROUP/$SLUG.git"
52
+ else
53
+ ADOPT_URL="https://$GH_HOSTNAME/$GROUP/$SLUG.git"
54
+ fi
55
+ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$ADOPT_URL"
56
+ echo "Remote 'origin' → $ADOPT_URL"
57
+ else
58
+ # Repository not found — defer to H-4 for confirm-then-create
59
+ PROJECT_EXISTS=false
60
+ echo "GitHub repository $GROUP/$SLUG not found — will confirm before create."
61
+ fi
62
+ ```
63
+
64
+ If the repository exists (`PROJECT_EXISTS=true`), the remote is already linked above
65
+ (skipping create entirely) — proceed straight to H-5 reconciliation. The adopt path does
66
+ not show a create confirmation — it only links the remote, which is a local-only
67
+ operation and can be undone with `git remote remove origin`.
68
+
69
+ #### H-4. Per-action confirmation, then create (D-05, D-09, D-10, SAFE-01)
70
+
71
+ **Only run this step when `PROJECT_EXISTS=false`** (the existence probe in H-3 found no
72
+ repository). If `PROJECT_EXISTS=true`, skip H-4 entirely — the repository was already
73
+ adopted — and go to H-5. When creating, **show the exact command and target before
74
+ firing**. The user must confirm explicitly. Nothing is batched. (D-05)
75
+
76
+ Present to the user (via `AskUserQuestion` or TEXT_MODE numbered list):
77
+
78
+ ```text
79
+ About to create GitHub repository:
80
+ Command: gh repo create "$GROUP/$SLUG"
81
+ Host: $GH_HOSTNAME
82
+ Organization: $GROUP
83
+ Repository: $GROUP/$SLUG
84
+ Visibility: private (enforced --private)
85
+ Requested: $VIS (from project.visibility — see note below)
86
+ Source: this directory (--source .) — nothing is pushed
87
+ Remote name: origin
88
+
89
+ Proceed? [y/N]
90
+ ```
91
+
92
+ `$VIS` (from G-1) is surfaced in the confirmation so the requested visibility is never
93
+ silently ignored (WR-01). pp-core always **creates private** regardless; a non-private
94
+ repository must be a deliberate, separate post-create change.
95
+
96
+ Only after the user confirms — and only with `PROJECT_EXISTS=false` — run the locked
97
+ create recipe (D-09, D-10). This is a standalone block: it carries no `if`/`else`/`fi`, so
98
+ it can only ever run as the deliberate, confirmed not-exists path:
99
+
100
+ ```bash
101
+ GH_HOST="$GH_HOSTNAME" gh repo create "$GROUP/$SLUG" \
102
+ --description "$DESC" \
103
+ --private \
104
+ --source . \
105
+ --remote origin
106
+ ```
107
+
108
+ **Visibility hard-default (D-10):** the create invocation always carries `--private` and
109
+ never `--public` or `--internal`. `VIS` is read in G-1 and **surfaced in the H-4
110
+ confirmation** (WR-01) so a `public`/`internal` request is visible to the user — but
111
+ pp-core still creates the repository **private**. Applying a non-private visibility is a
112
+ deliberate, separate post-create action (e.g. `gh repo edit --visibility …`) the operator
113
+ performs knowingly; `/pp-init` never auto-relaxes visibility. The create pushes nothing
114
+ (no `--push`): the first push is Step 3's own confirmed push, and the repository's default
115
+ branch is whatever that push sends.
116
+
117
+ #### H-5. Post-create reconciliation (D-09)
118
+
119
+ After the create (or after the adopt path), re-probe existence. A non-zero exit from
120
+ `gh repo create` does **not** mean the repository was not created — the repository is
121
+ created before `--remote origin` is added, so an `origin` that already exists fails the
122
+ command after the create succeeded. The `repo view` probe is the source of truth, not the
123
+ exit code:
124
+
125
+ ```bash
126
+ if GH_HOST="$GH_HOSTNAME" gh repo view "$GROUP/$SLUG" --json name >/dev/null 2>&1; then
127
+ echo "Reconciled: GitHub repository $GROUP/$SLUG confirmed."
128
+ else
129
+ echo "WARN: gh repo view $GROUP/$SLUG returned non-zero — verify the repository was created before continuing."
130
+ fi
131
+ ```
132
+
133
+ If reconciliation warns, do not auto-retry. Prompt the user to check GitHub directly
134
+ before proceeding to Step 4 (handoff routing).
135
+
136
+ **Security invariants — landmine guards:**
137
+ - Always use the organization-qualified `$GROUP/$SLUG` — never a bare slug (T-08-04, D-09).
138
+ A bare slug creates under the user's personal account, not the client organization.
139
+ G-1's empty-group guard is what actually enforces this before any create attempt.
140
+ - Always pass `--private` explicitly — never `--public` or `--internal` (T-08-05, D-10).
141
+ - Always confirm immediately before each outward action — nothing batched (T-08-06, D-05).
142
+ - Verify-only auth via `gh auth status` — never run `gh auth login`, never read or write
143
+ the user's token (T-08-08, D-11). Bindings hold only non-secret routing data.
144
+ - Always reconcile create with a `gh repo view` probe — a non-zero create exit does NOT
145
+ mean nothing happened (T-08-09, D-09).
@@ -1,10 +1,11 @@
1
1
  # Gated GitLab provisioning — G-2..G-5 (D-05, D-09, D-10, D-11, D-12, D-13)
2
2
 
3
- Lazily Read by `pp-core/workflows/init.md` § "Gated GitLab/Azure provisioning" — Step 3's
3
+ Lazily Read by `pp-core/workflows/init.md` § "Gated GitLab/GitHub provisioning" — Step 3's
4
4
  other sub-path — and ONLY when BOTH of that section's gates hold: `.platform-bindings.json`
5
5
  is present AND `resolveProvider` answers `gitlab`. Both gates, G-1's one-source-per-value
6
- config reads, the empty-group guard and the security-invariant list stay in `init.md`; this
7
- file is the create recipe those guards protect. Extracted from `init.md` (07-10) so the
6
+ config reads and the empty-group guard stay in `init.md`; this file is the create recipe
7
+ those guards protect, and ends with its security invariants (moved here from `init.md`
8
+ beside `github-provisioning.md`'s own). Extracted from `init.md` (07-10) so the
8
9
  always-loaded workflow stays under its DEFAULT size cap — relocation, not deletion: this
9
10
  text is the authority for how `/pp-init` provisions a GitLab project, and `$GROUP`, `$SLUG`,
10
11
  `$VIS`, `$DESC` and `$HOST` are the variables G-1 has already set.
@@ -26,7 +27,7 @@ Never run `glab auth login` from within `/pp-init`.
26
27
 
27
28
  #### G-3. Existence probe → adopt-if-exists (D-09, D-12)
28
29
 
29
- Pre-check with a read-only probe before any create attempt. `glab repo create` is **not
30
+ Pre-check with a read-only probe before any create attempt. The create command is **not
30
31
  idempotent** — re-running it against an existing project errors but may still produce side
31
32
  effects (the GitLab project is created before the remote-add step that then fails). The
32
33
  probe avoids this landmine.
@@ -128,3 +129,16 @@ fi
128
129
 
129
130
  If reconciliation warns, do not auto-retry. Prompt the user to check GitLab directly
130
131
  before proceeding to Step 4 (handoff routing).
132
+
133
+ **Security invariants — landmine guards:**
134
+ - Always use the group-qualified `$GROUP/$SLUG` path — never a bare slug (T-08-04, D-09).
135
+ A bare slug creates under the user's personal namespace, not the client group. G-1's
136
+ empty-group guard in `init.md` is what actually enforces this before any create attempt.
137
+ - Always pass `--private` explicitly — never inherit `glab`'s default `--internal`
138
+ visibility (T-08-05, D-10). The `--internal` flag is what makes this dangerous; the
139
+ locked recipe must carry `--private` and must NOT emit `--internal`.
140
+ - Always confirm immediately before each outward action — nothing batched (T-08-06, D-05).
141
+ - Verify-only auth via `glab auth status` — never run `glab auth login`, never read
142
+ or write the user's token (T-08-08, D-11). Bindings hold only non-secret routing data.
143
+ - Always reconcile create with a `glab repo view` probe — a non-zero create exit does
144
+ NOT mean nothing happened (T-08-09, D-09).
@@ -16,7 +16,9 @@ the probe, or this step to the user.
16
16
  named ref exists, and the client vault landed with a written `SUMMARY.md` (INIT-02).
17
17
  Continue to Step 3. A `shared` line on that card is information about the fallback
18
18
  tier (OD-5) — it is probed and shown, never required; an unreachable shared vault does
19
- not fail the proof and is not in the recorded note.
19
+ not fail the proof and is not in the recorded note. A `platform` line reading
20
+ `unreachable (not required)` is information too: the platform repo is not required, and
21
+ the record states what was measured.
20
22
 
21
23
  **(c) `PP_INIT_PROBE=unbound` or `PP_INIT_PROBE=fail`** — **STOP. `/pp-init` is not
22
24
  complete (INIT-03).** Do not continue to Step 3, and do not re-run the probe hoping for a
@@ -11,8 +11,8 @@ D-13 (gate on .platform-bindings.json via the Phase 7 reader — Step 2 now comp
11
11
  writes this file too, not only reads it, Phase 34 INIT-01).
12
12
 
13
13
  Provisioning is provider-aware (Phase 34 D-11/D-13): `resolveProvider` is the sole
14
- authority for which provisioning path runs. The gated GitLab `glab` recipe (G-2…G-5) runs
15
- only when it resolves to `gitlab`; under any other provider (`azure` today) it is
14
+ authority for which provisioning path runs. The gated create recipe runs only when it
15
+ resolves to `gitlab` (`glab`, G-2…G-5) or `github` (`gh`, H-2…H-5); under any other provider (`azure` today) it is
16
16
  **skipped, not failed**, and the run falls through to this same generic remote-wiring
17
17
  path so the user still leaves with a wired remote (Phase 34 INIT-03).
18
18
 
@@ -31,13 +31,6 @@ Read all files referenced by the invoking prompt's execution_context before star
31
31
 
32
32
  <process>
33
33
 
34
- ## Shim hoist
35
-
36
- Resolve `pp_run` before any step — required for the platform-bindings preflight (D-13).
37
-
38
- ```bash
39
- ```
40
-
41
34
  ## Text mode (resolve before any prompt — WR-02)
42
35
 
43
36
  Resolve `TEXT_MODE` here, **before Step 0**, because the Step 0 drift prompt is the first
@@ -91,13 +84,13 @@ step is skipped entirely.
91
84
 
92
85
  ## 1. Worktree-aware git init
93
86
 
94
- Parse the init signals by running the query below. `init.execute-phase` is reused here
95
- deliberately (IN-02): it is the shared verb that emits the generic git-state signals
96
- (`has_git`, `in_nested_subdir`, `git_worktree_root`) this step needs — no phase work is
97
- triggered when called with an empty phase argument.
87
+ Parse the init signals by running the query below. `init.ingest-docs` takes no argument
88
+ and emits the git-state signals this step needs (`has_git`, `in_nested_subdir`,
89
+ `git_worktree_root`) from the same #3491 reader `init.new-project` uses. Never use
90
+ `init.execute-phase ""`: it refuses an empty phase, and the `{}` fallback would disable the guard.
98
91
 
99
92
  ```bash
100
- INIT=$(pp_run query init.execute-phase "" 2>/dev/null || echo '{}')
93
+ INIT=$(pp_run query init.ingest-docs 2>/dev/null || echo '{}')
101
94
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
102
95
  ```
103
96
 
@@ -177,7 +170,12 @@ its booleans, never in `$?`, and its `error` prose is never echoed (D-07-07). Th
177
170
  runs only *after* a clean probe, so a project that cannot reach its vault never ends up
178
171
  with a half-populated cache (D-07-05).
179
172
 
173
+ The verdict is the client vault's. The platform repo is probed and shown, but a client
174
+ project may have no access to it, so an unreachable platform repo reads `(not required)`
175
+ and does not fail the proof.
176
+
180
177
  ```bash
178
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
181
179
  # D-07-01 + OD-1 + INIT-01: three signals, all read through the resolvers every other path
182
180
  # uses. Anything but the literal "true" is a fail-inert default.
183
181
  PP_INIT_PROBE=""
@@ -219,26 +217,25 @@ if [ "$VAULT_ON" = "true" ] && [ "$PROFILE_RESOLVED" = "true" ]; then
219
217
  PROBE=$(pp_run query vault probe --raw 2>/dev/null || echo '{"targets":[]}')
220
218
  # R3-CR-02 + R3-WR-02: ONE evaluation of the target set, read by every arm
221
219
  # below. FOUR hand-copied de-dup pipelines stood here and had already
222
- # diverged; the one that mattered inverted the proof. 07-18 excluded the
223
- # fallback tier AFTER the de-dup, and targets arrive in the fixed order
224
- # client, shared, platform — so a platform sharing a coordinate with the
225
- # fallback survived under the FALLBACK'S NAME, the exclusion deleted the
226
- # only representative of a REQUIRED tier, and Step 2.6 printed and durably
227
- # recorded a platform reachable line for a target measured unreachable. The
228
- # exclusion now runs BEFORE the de-dup; the DISPLAY de-dup keeps the REQUIRED
229
- # tier's identity when two tiers name one coordinate (a fallback identical to
230
- # the client is still one remote, not two). OD-5 Option A unchanged: the
231
- # fallback tier stays out of the verdict and out of the note's sick list and
232
- # keeps its "(fallback tier, not required)" line when its coordinate is its
233
- # own. The all-down flag counts EVERY distinct remote, fallback included, ON
234
- # PURPOSE: it drives the offline candidate below, and a fallback that answered
235
- # is evidence the network is up. CR-02: a REQUIRED target with no coordinate
236
- # gets its own "missing" line — without it the D-07-02 case printed a card
237
- # whose every line read healthy under a fail verdict; measured, not a
238
- # diagnosis (D-07-07). EMITS: line 1 verdict, 2 all-down, 3 note, 4+ card; a
239
- # card line can never carry a newline of its own (redactCloneTarget collapses
240
- # the control class at the source, R2-WR-03), so the split below is safe.
241
- PROBE_EVAL=$(printf '%s' "$PROBE" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const t=(JSON.parse(d).targets||[]).filter(x=>x&&x.configured===true);const key=x=>[x.repo,x.ref,x.host].join('|');const names=new Set(t.map(x=>x.name));const seen=new Set();const req=t.filter(x=>x.name!=='shared'&&!seen.has(key(x))&&seen.add(key(x)));const pick=new Map();for(const x of t){const k=key(x);const p=pick.get(k);if(!p||(p.name==='shared'&&x.name!=='shared'))pick.set(k,x);}const shown=t.filter(x=>pick.get(key(x))===x);const cfg={client:'client.vaultRepo',platform:'platform.repo'};const gone=Object.keys(cfg).filter(n=>!names.has(n));const ok=names.has('client')&&names.has('platform')&&req.length>0&&req.every(x=>x.reachable===true&&x.refFound===true);const sick=req.filter(x=>!(x.reachable===true&&x.refFound===true)).map(x=>String(x.name)+' '+String(x.repoSafe)+' @ '+String(x.ref)+' '+(x.reachable===true?'ref not found':'unreachable'));const all=gone.map(n=>n+' not configured').concat(sick);const lines=shown.map(x=>' '+String(x.name).padEnd(9)+String(x.repoSafe)+' @ '+((x.name==='platform'&&x.ref==='HEAD')?'(unpinned)':String(x.ref))+' '+(x.reachable===true?(x.refFound===true?'reachable, ref found':'ref not found'):'unreachable')+(x.name==='shared'?' (fallback tier, not required)':'')).concat(gone.map(n=>' '+n.padEnd(9)+'(not configured) missing — '+cfg[n]+' is absent from .platform-bindings.json'));process.stdout.write([ok?'pass':'fail',String(shown.length>0&&shown.every(x=>x.reachable!==true)),'failed='+(all.length?all.join('; '):'probe payload unreadable')].concat(lines).join('\n'));}catch{process.stdout.write('fail\nfalse\nfailed=probe payload unreadable');}")
220
+ # diverged, so every arm reads this one line. Only the CLIENT vault decides
221
+ # the verdict: it is what the fetch below lands. The platform repo is shown
222
+ # and recorded but not required (a client project may have no access to
223
+ # it), so an unreachable platform line carries "(not required)", like the
224
+ # fallback tier (OD-5 Option A: shared is probed and shown, out of the
225
+ # verdict and out of the note's sick list). A binding with no platform.repo
226
+ # at all still fails as "not configured". The DISPLAY de-dup keeps a
227
+ # non-shared tier's identity when two tiers name one coordinate, so a
228
+ # platform colliding with the fallback is still shown as platform. The
229
+ # all-down flag counts EVERY distinct remote, fallback included, ON
230
+ # PURPOSE: it drives the offline candidate below, and a remote that
231
+ # answered is evidence the network is up. CR-02: a target with no
232
+ # coordinate gets its own "missing" line — without it the D-07-02 case
233
+ # printed a card whose every line read healthy under a fail verdict;
234
+ # measured, not a diagnosis (D-07-07). EMITS: line 1 verdict, 2 all-down,
235
+ # 3 note, 4+ card; a card line can never carry a newline of its own
236
+ # (redactCloneTarget collapses the control class at the source, R2-WR-03),
237
+ # so the split below is safe.
238
+ PROBE_EVAL=$(printf '%s' "$PROBE" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const t=(JSON.parse(d).targets||[]).filter(x=>x&&x.configured===true);const key=x=>[x.repo,x.ref,x.host].join('|');const names=new Set(t.map(x=>x.name));const healthy=x=>x.reachable===true&&x.refFound===true;const req=t.filter(x=>x.name==='client');const pick=new Map();for(const x of t){const k=key(x);const p=pick.get(k);if(!p||(p.name==='shared'&&x.name!=='shared'))pick.set(k,x);}const shown=t.filter(x=>pick.get(key(x))===x);const cfg={client:'client.vaultRepo',platform:'platform.repo'};const gone=Object.keys(cfg).filter(n=>!names.has(n));const ok=names.has('client')&&names.has('platform')&&req.length>0&&req.every(healthy);const sick=req.filter(x=>!healthy(x)).map(x=>String(x.name)+' '+String(x.repoSafe)+' @ '+String(x.ref)+' '+(x.reachable===true?'ref not found':'unreachable'));const all=gone.map(n=>n+' not configured').concat(sick);const lines=shown.map(x=>' '+String(x.name).padEnd(9)+String(x.repoSafe)+' @ '+((x.name==='platform'&&x.ref==='HEAD')?'(unpinned)':String(x.ref))+' '+(x.reachable===true?(x.refFound===true?'reachable, ref found':'ref not found'):'unreachable')+(x.name==='shared'?' (fallback tier, not required)':(x.name==='platform'&&!healthy(x)?' (not required)':''))).concat(gone.map(n=>' '+n.padEnd(9)+'(not configured) missing — '+cfg[n]+' is absent from .platform-bindings.json'));process.stdout.write([ok?'pass':'fail',String(shown.length>0&&shown.every(x=>x.reachable!==true)),'failed='+(all.length?all.join('; '):'probe payload unreadable')].concat(lines).join('\n'));}catch{process.stdout.write('fail\nfalse\nfailed=probe payload unreadable');}")
242
239
  PROBE_VERDICT=$(printf '%s' "$PROBE_EVAL" | sed -n '1p')
243
240
  PROBE_ALL_DOWN=$(printf '%s' "$PROBE_EVAL" | sed -n '2p')
244
241
  PROBE_NOTE=$(printf '%s' "$PROBE_EVAL" | sed -n '3p')
@@ -257,6 +254,9 @@ if [ "$VAULT_ON" = "true" ] && [ "$PROFILE_RESOLVED" = "true" ]; then
257
254
  FETCH_COUNT=$(printf '%s' "$FETCH" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String((JSON.parse(d).files||[]).length));}catch{process.stdout.write('0');}")
258
255
  FETCH_SUMMARY=$(printf '%s' "$FETCH" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).summaryWritten===true));}catch{process.stdout.write('false');}")
259
256
  PLATFORM_REPO=$(printf '%s' "$PROBE" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const x=(JSON.parse(d).targets||[]).find(y=>y&&y.name==='platform');process.stdout.write(String((x&&x.repoSafe)||''));}catch{process.stdout.write('');}")
257
+ # The platform repo is not required (a client may have no access to it): its measured
258
+ # state goes on the card and into the record, it never decides the verdict.
259
+ PLATFORM_STATE=$(printf '%s' "$PROBE" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const x=(JSON.parse(d).targets||[]).find(y=>y&&y.name==='platform');process.stdout.write(x&&x.reachable===true?(x.refFound===true?'reachable':'ref not found (not required)'):'unreachable (not required)');}catch{process.stdout.write('unreachable (not required)');}")
260
260
  # Never the probe's internal HEAD placeholder — an unpinned platform says so.
261
261
  PLATFORM_TAG=$(pp_run query platform-bindings --pick platform.tag 2>/dev/null)
262
262
  if [ "$FETCH_TIER" = "client" ] && [ "$FETCH_SUMMARY" = "true" ] && { [ "$FETCH_SOURCE" = "fetched" ] || [ "$FETCH_SOURCE" = "cache" ]; }; then
@@ -264,13 +264,13 @@ if [ "$VAULT_ON" = "true" ] && [ "$PROFILE_RESOLVED" = "true" ]; then
264
264
  # card prints. A refused write used to be a warning under a PASSED card, and PASSED
265
265
  # routes to "continue to Step 3" — so "unrecorded" was defined to continue. It is a
266
266
  # failure of the step, kind FAIL_KIND=record. Markers unchanged: pass/unbound/fail.
267
- if pp_run query init-contract --record-probe "passed=platform ${PLATFORM_REPO} @ ${PLATFORM_TAG:-(unpinned)}; vault ${FETCH_REPO} @ ${FETCH_REF} tier ${FETCH_TIER} ${FETCH_COUNT} files" >/dev/null 2>&1; then
267
+ if pp_run query init-contract --record-probe "passed=platform ${PLATFORM_REPO} @ ${PLATFORM_TAG:-(unpinned)} ${PLATFORM_STATE}; vault ${FETCH_REPO} @ ${FETCH_REF} tier ${FETCH_TIER} ${FETCH_COUNT} files" >/dev/null 2>&1; then
268
268
  PP_INIT_PROBE=pass
269
269
  echo "Reachability proof: PASSED"
270
270
  # OD-3 + CR-01: the DISPLAY-SAFE coordinate (repoSafe), ref, tier and booleans
271
271
  # only. Credentials are stripped at the source, so a PAT in the binding reaches
272
272
  # neither this card nor the record. Never url, never error.
273
- printf '%s\n' " platform ${PLATFORM_REPO} @ ${PLATFORM_TAG:-(unpinned)} reachable"
273
+ printf '%s\n' " platform ${PLATFORM_REPO} @ ${PLATFORM_TAG:-(unpinned)} ${PLATFORM_STATE}"
274
274
  printf '%s\n' " vault ${FETCH_REPO} @ ${FETCH_REF} reachable, ref found — tier ${FETCH_TIER}, sha ${FETCH_SHA}, ${FETCH_COUNT} files, SUMMARY.md written (${FETCH_SOURCE})"
275
275
  printf '%s\n' "$PROBE_LINES" | grep '^ shared ' || true
276
276
  echo "PP_INIT_PROBE=pass"
@@ -353,7 +353,7 @@ outright. Take THIS generic sub-path when
353
353
  bindings ARE present, take the gated GitLab/Azure sub-path below instead — that section is
354
354
  still part of Step 3, not a separate optional step. **The provider answer never decides
355
355
  whether Step 3 runs** — inside the gated sub-path it only decides whether the
356
- GitLab-specific `glab` recipe fires or falls through to this same generic code below.
356
+ `glab` or `gh` create recipe fires or falls through to this same generic code below.
357
357
 
358
358
  On the generic path:
359
359
 
@@ -393,14 +393,14 @@ gets its own per-action confirmation immediately before it fires — nothing bat
393
393
  step makes **ZERO `gh`/`glab`/provider-API calls**. The push target is the user-supplied
394
394
  URL only.
395
395
 
396
- ### Gated GitLab/Azure provisioning (Step 3's other sub-path — bindings present)
396
+ ### Gated GitLab/GitHub provisioning (Step 3's other sub-path — bindings present)
397
397
 
398
398
  This is Step 3's other sub-path, not a separate step. It runs **instead of** the generic
399
399
  remote-wiring path above when
400
400
  `pp_run query platform-bindings --pick present 2>/dev/null` equals `"true"`. Within it,
401
- G-1 always runs first to source coordinates and apply the two guards below; the
402
- GitLab-specific create recipe (G-2…G-5) runs **only when the resolved provider is
403
- `gitlab`** — under any other provider (`azure` today) it is skipped, not failed, and the
401
+ G-1 always runs first to source coordinates and apply the two guards below; a create
402
+ recipe runs **only when the resolved provider is `gitlab` or `github`** — under any
403
+ other provider (`azure` today) it is skipped, not failed, and the
404
404
  run falls through to Step 3's generic remote-wiring path above instead (D-11, D-13).
405
405
  Implements D-05 (per-action confirmation), D-09 (existence probe → adopt-if-exists), D-10
406
406
  (private by default), D-11 (auth verify-only; gated on provider), D-12 (adopt-if-exists
@@ -442,8 +442,9 @@ comes from, skip G-2 through G-5 entirely, and fall through to Step 3's generic
442
442
  wiring:
443
443
 
444
444
  ```bash
445
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
445
446
  if [ -z "$GROUP" ] || [ -z "$SLUG" ]; then
446
- echo "Skipping GitLab provisioning: $( [ -z "$GROUP" ] && echo "GROUP (org profile seed provisioning group)" || echo "SLUG (project.slug)" ) is empty — falling through to Step 3's generic remote wiring."
447
+ echo "Skipping repository provisioning: $( [ -z "$GROUP" ] && echo "GROUP (org profile seed provisioning group)" || echo "SLUG (project.slug)" ) is empty — falling through to Step 3's generic remote wiring."
447
448
  fi
448
449
  ```
449
450
 
@@ -452,35 +453,24 @@ fi
452
453
 
453
454
  ```bash
454
455
  PROVIDER=$(pp_run query compose-bindings --offline --pick provider 2>/dev/null)
455
- if [ "$PROVIDER" != "gitlab" ]; then
456
- echo "Skipping GitLab-specific provisioning for provider \"$PROVIDER\" — falling through to Step 3's generic remote wiring instead."
456
+ if [ "$PROVIDER" != "gitlab" ] && [ "$PROVIDER" != "github" ]; then
457
+ echo "Skipping repository provisioning for provider \"$PROVIDER\" — falling through to Step 3's generic remote wiring instead."
457
458
  fi
458
459
  ```
459
460
 
460
- Continue to G-2 only when the empty-group guard above did not skip **and** `PROVIDER`
461
- equals `gitlab`. Otherwise stop here and go to Step 3's generic remote wiring (`:283-312`)
461
+ Continue only when the empty-group guard above did not skip **and** `PROVIDER`
462
+ is `gitlab` or `github`. Otherwise stop here and go to Step 3's generic remote wiring (`:283-312`)
462
463
  instead — do not fail, do not error, do not prompt for a missing CLI. "Skipped, not
463
464
  failed" is satisfied *and* the user still leaves with a wired remote, which a bare skip
464
465
  would not give them; pp-core makes zero provider-API calls it has no client for.
465
466
 
466
- When both conditions hold, **Read `pp-core/workflows/init/steps/gitlab-provisioning.md` and
467
- follow it.** That file owns G-2 (auth verify-only), G-3 (existence probe →
468
- adopt-if-exists), G-4 (per-action confirmation, then the locked `--private` create) and G-5
469
- (post-create reconciliation). Under any other provider do not read it at all — fall through
470
- to Step 3's generic remote wiring above.
471
-
472
- **Security invariants — landmine guards:**
473
- - Always use the group-qualified `$GROUP/$SLUG` path — never a bare slug (T-08-04, D-09).
474
- A bare slug creates under the user's personal namespace, not the client group. G-1's
475
- empty-group guard above is what actually enforces this before any create attempt.
476
- - Always pass `--private` explicitly — never inherit `glab`'s default `--internal`
477
- visibility (T-08-05, D-10). The `--internal` flag is what makes this dangerous; the
478
- locked recipe must carry `--private` and must NOT emit `--internal`.
479
- - Always confirm immediately before each outward action — nothing batched (T-08-06, D-05).
480
- - Verify-only auth via `glab auth status` — never run `glab auth login`, never read
481
- or write the user's token (T-08-08, D-11). Bindings hold only non-secret routing data.
482
- - Always reconcile create with a `glab repo view` probe — a non-zero create exit does
483
- NOT mean nothing happened (T-08-09, D-09).
467
+ When both conditions hold, **Read `pp-core/workflows/init/steps/gitlab-provisioning.md`
468
+ (provider `gitlab`) or `pp-core/workflows/init/steps/github-provisioning.md` (provider
469
+ `github`) and follow it.** Each owns, for its
470
+ CLI, auth verify-only, the existence probe → adopt-if-exists, per-action confirmation then
471
+ the locked `--private` create, post-create reconciliation, and its security invariants
472
+ (landmine guards: group-qualified path, explicit `--private`, verify-only auth). Under any
473
+ other provider read neither — fall through to Step 3's generic remote wiring above.
484
474
 
485
475
  ### Gated client-context capture (Pareto projects — bindings present)
486
476
 
@@ -555,9 +545,9 @@ legitimately not exist — the session declines or composes nothing on the inert
555
545
 
556
546
  ```bash
557
547
  if [ -f .platform-bindings.json ]; then
558
- pp_run query commit "docs: initialize project" --files .planning/config.json .platform-bindings.json
548
+ pp_run query commit "docs(planning): initialize project" --files .planning/config.json .platform-bindings.json
559
549
  else
560
- pp_run query commit "docs: initialize project" --files .planning/config.json
550
+ pp_run query commit "docs(planning): initialize project" --files .planning/config.json
561
551
  fi
562
552
  ```
563
553
 
@@ -327,6 +327,7 @@ Continue to verify_output.
327
327
  Verify all documents created successfully:
328
328
 
329
329
  ```bash
330
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
330
331
  ls -la .planning/codebase/
331
332
  wc -l .planning/codebase/*.md
332
333
  ```
@@ -346,6 +347,7 @@ Continue to scan_for_secrets.
346
347
  Run secret pattern detection:
347
348
 
348
349
  ```bash
350
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
349
351
  # Check for common API key patterns in generated docs
350
352
  grep -E '(sk-[a-zA-Z0-9]{20,}|sk_live_[a-zA-Z0-9]+|sk_test_[a-zA-Z0-9]+|ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|glpat-[a-zA-Z0-9_-]+|AKIA[A-Z0-9]{16}|xox[baprs]-[a-zA-Z0-9-]+|-----BEGIN.*PRIVATE KEY|eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.)' .planning/codebase/*.md 2>/dev/null && SECRETS_FOUND=true || SECRETS_FOUND=false
351
353
  ```
@@ -379,7 +381,8 @@ Continue to commit_codebase_map.
379
381
  Commit the codebase map:
380
382
 
381
383
  ```bash
382
- pp_run query commit "docs: map existing codebase" --files .planning/codebase/*.md
384
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
385
+ pp_run query commit "docs(planning): map existing codebase" --files .planning/codebase/*.md
383
386
  ```
384
387
 
385
388
  Continue to offer_next.
@@ -390,6 +393,7 @@ Present completion summary and next steps.
390
393
 
391
394
  **Get line counts:**
392
395
  ```bash
396
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
393
397
  wc -l .planning/codebase/*.md
394
398
  ```
395
399
 
@@ -78,6 +78,7 @@ git tag -l "v${VERSION}" | head -1
78
78
  ```
79
79
  If the tag exists:
80
80
  ```bash
81
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
81
82
  git log v${VERSION} --oneline | wc -l
82
83
  git diff --stat $(git log --format=%H --reverse v${VERSION} | head -1)..v${VERSION}
83
84
  ```
@@ -190,7 +191,7 @@ mkdir -p .planning/reports
190
191
 
191
192
  Write the summary, then commit:
192
193
  ```bash
193
- pp_run query commit "docs(v${VERSION}): generate milestone summary for onboarding" --files \
194
+ pp_run query commit "docs(planning): generate v${VERSION} milestone summary for onboarding" --files \
194
195
  ".planning/reports/MILESTONE_SUMMARY-v${VERSION}.md"
195
196
  ```
196
197
 
@@ -99,6 +99,7 @@ If any of the three answers is empty or whitespace-only, error and re-prompt tha
99
99
  **Validate via the centralized User Story validator.** The verb owns the canonical regex `/^As a .+, I want to .+, so that .+\.$/` and surfaces per-error guidance:
100
100
 
101
101
  ```bash
102
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
102
103
  USER_STORY_RESULT=$(pp_run query user-story.validate --story "$USER_STORY")
103
104
  if [ "$(echo "$USER_STORY_RESULT" | jq -r '.valid')" != "true" ]; then
104
105
  echo "$USER_STORY_RESULT" | jq -r '.errors[]' >&2
@@ -55,6 +55,7 @@ If the flag is absent, keep the current behavior of continuing phase numbering f
55
55
  Check `.planning/seeds/` for seed files that match the milestone goals gathered in step 2.
56
56
 
57
57
  ```bash
58
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
58
59
  ls .planning/seeds/SEED-*.md 2>/dev/null
59
60
  ```
60
61
 
@@ -217,7 +218,7 @@ pp_run query phases.clear --confirm
217
218
  ```
218
219
 
219
220
  ```bash
220
- pp_run query commit "docs: start milestone v[X.Y] [Name]" --files .planning/PROJECT.md .planning/STATE.md
221
+ pp_run query commit "docs(planning): start milestone v[X.Y] [Name]" --files .planning/PROJECT.md .planning/STATE.md
221
222
  ```
222
223
 
223
224
  ## 7. Load Context and Resolve Models
@@ -256,6 +257,7 @@ If `--reset-phase-numbers` is active:
256
257
  If `phase_dir_count > 0`, let the engine archive what the closed milestones name — never move phase directories by hand:
257
258
 
258
259
  ```bash
260
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
259
261
  ARCHIVED=$(pp_run query phases.archive-closed ${GSD_WS})
260
262
  ```
261
263
 
@@ -368,7 +370,7 @@ Commit after writing.
368
370
  **Synthesizer output self-heal (#222) — verify SUMMARY.md materialized:** The synthesizer's canonical output is `.planning/research/SUMMARY.md` on disk; its brief structured return (`## SYNTHESIS COMPLETE` plus a few `###` confirmation lines) is NOT the file content. A known LLM false-refusal (issue #222) sometimes makes the agent return the full SUMMARY.md document inline — fabricating a write restriction (e.g. "the runtime is blocking file writes") — instead of writing the file. Prompt hardening alone does not fully eliminate it, so the orchestrator MUST absorb the failure deterministically before spawning `pp-roadmapper`:
369
371
 
370
372
  1. Verify `.planning/research/SUMMARY.md` exists AND is substantive — non-empty, and free of any leftover `<!-- gsd:write-continue -->` continuation sentinel (which marks a truncated/incomplete write). You may validate with `pp-tools verify-summary .planning/research/SUMMARY.md` — it exits 0 regardless, so check its JSON `passed` field (`"passed": false` means missing or invalid), not the process exit code. If it passes, continue normally.
371
- 2. If it is MISSING or invalid AND the synthesizer's return message contains the FULL SUMMARY.md document — recognizable by the template's top-level markers `# Project Research Summary`, `## Key Findings`, `## Implications for Roadmap`, and `## Sources`, not merely the brief `## SYNTHESIS COMPLETE` confirmation — the false-refusal fired: write that returned document to `.planning/research/SUMMARY.md` with the Write tool, then commit ALL research artifacts the synthesizer owns (it commits on behalf of the four researchers) with `pp-tools query commit "docs: complete project research" --files .planning/research/` unless they are already committed. Log `⚠ #222 self-heal: synthesizer returned SUMMARY.md inline without writing it; orchestrator persisted the file.`
373
+ 2. If it is MISSING or invalid AND the synthesizer's return message contains the FULL SUMMARY.md document — recognizable by the template's top-level markers `# Project Research Summary`, `## Key Findings`, `## Implications for Roadmap`, and `## Sources`, not merely the brief `## SYNTHESIS COMPLETE` confirmation — the false-refusal fired: write that returned document to `.planning/research/SUMMARY.md` with the Write tool, then commit ALL research artifacts the synthesizer owns (it commits on behalf of the four researchers) with `pp_run query commit "docs(planning): complete project research" --files .planning/research/` unless they are already committed. Log `⚠ #222 self-heal: synthesizer returned SUMMARY.md inline without writing it; orchestrator persisted the file.`
372
374
  3. If it is MISSING or invalid AND the return is only a brief confirmation (no full SUMMARY document to recover), the synthesizer genuinely failed — surface the error and stop; do NOT spawn `pp-roadmapper` against a missing or incomplete SUMMARY.md.
373
375
 
374
376
  This guarantees `pp-roadmapper` (which lists SUMMARY.md as required reading) never runs against a missing or truncated SUMMARY.md.
@@ -456,7 +458,7 @@ If "adjust": Return to scoping.
456
458
 
457
459
  **Commit requirements:**
458
460
  ```bash
459
- pp_run query commit "docs: define milestone v[X.Y] requirements" --files .planning/REQUIREMENTS.md
461
+ pp_run query commit "docs(planning): define milestone v[X.Y] requirements" --files .planning/REQUIREMENTS.md
460
462
  ```
461
463
 
462
464
  ## 10. Create Roadmap
@@ -542,7 +544,7 @@ Success criteria:
542
544
 
543
545
  **Commit roadmap** (after approval):
544
546
  ```bash
545
- pp_run query commit "docs: create milestone v[X.Y] roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md
547
+ pp_run query commit "docs(planning): create milestone v[X.Y] roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md
546
548
  ```
547
549
 
548
550
  ## 10.5. Link Pending Todos to Roadmap Phases
@@ -551,6 +553,7 @@ After roadmap approval, scan pending todos against the newly approved phases. Fo
551
553
 
552
554
  **Check for pending todos:**
553
555
  ```bash
556
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
554
557
  PENDING_TODOS=$(ls .planning/todos/pending/*.md 2>/dev/null | head -50)
555
558
  ```
556
559
 
@@ -585,7 +588,8 @@ files: [existing]
585
588
 
586
589
  **If any todos were linked:**
587
590
  ```bash
588
- pp_run query commit "docs: tag [count] pending todos with resolves_phase after milestone v[X.Y] roadmap" --files .planning/todos/pending/*.md
591
+ [ -z "${ZSH_VERSION:-}" ] || emulate -R sh
592
+ pp_run query commit "docs(planning): tag [count] pending todos with resolves_phase after milestone v[X.Y] roadmap" --files .planning/todos/pending/*.md
589
593
  ```
590
594
 
591
595
  Print a summary: