@pareto4/pp-core 1.12.1 → 1.17.1

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 (196) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +3 -2
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +7 -21
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +274 -0
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +2 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +35 -0
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +526 -22
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3867 -274
  35. package/pp-core/bin/lib/clusters.cjs +4 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -28
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +253 -76
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +104 -27
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -1
  45. package/pp-core/bin/lib/git-base-branch.cjs +138 -10
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +653 -629
  49. package/pp-core/bin/lib/init-interactive.cjs +77 -21
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +169 -47
  54. package/pp-core/bin/lib/org-profile.cjs +544 -53
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -23
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -203
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +2594 -0
  66. package/pp-core/bin/lib/vault-command-router.cjs +2756 -60
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +1194 -89
  71. package/pp-core/bin/lib/vault-review.cjs +2102 -0
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +3184 -0
  75. package/pp-core/bin/lib/vault-update.cjs +242 -0
  76. package/pp-core/bin/lib/vault-write-back.cjs +78 -31
  77. package/pp-core/bin/lib/vault.cjs +2713 -634
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -37
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +1 -0
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -14
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +31 -3
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +137 -11
  100. package/pp-core/templates/project-charter.md +226 -0
  101. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  102. package/pp-core/workflows/add-tests.md +55 -0
  103. package/pp-core/workflows/audit-fix.md +63 -0
  104. package/pp-core/workflows/audit-milestone.md +1 -1
  105. package/pp-core/workflows/autonomous.md +85 -11
  106. package/pp-core/workflows/check-todos.md +13 -2
  107. package/pp-core/workflows/code-review-fix.md +59 -1
  108. package/pp-core/workflows/code-review.md +10 -1
  109. package/pp-core/workflows/complete-milestone.md +1 -1
  110. package/pp-core/workflows/debug.md +56 -2
  111. package/pp-core/workflows/discovery-sprint/prefill.md +21 -0
  112. package/pp-core/workflows/discovery-sprint/prototype.md +46 -48
  113. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  114. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  118. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  123. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  124. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  127. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  128. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  129. package/pp-core/workflows/discovery-sprint.md +7 -3
  130. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  131. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  132. package/pp-core/workflows/discovery-to-build.md +9 -1
  133. package/pp-core/workflows/discuss-phase.md +3 -10
  134. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  135. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  136. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  137. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  138. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  139. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  140. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  141. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -18
  142. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  143. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  144. package/pp-core/workflows/execute-phase.md +154 -110
  145. package/pp-core/workflows/execute-plan.md +58 -4
  146. package/pp-core/workflows/fast.md +53 -0
  147. package/pp-core/workflows/help/modes/full.md +1 -0
  148. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  149. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  150. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  151. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  152. package/pp-core/workflows/init.md +232 -272
  153. package/pp-core/workflows/new-project.md +31 -23
  154. package/pp-core/workflows/plan-phase.md +99 -43
  155. package/pp-core/workflows/quick.md +120 -2
  156. package/pp-core/workflows/secure-phase.md +1 -1
  157. package/pp-core/workflows/ship.md +57 -24
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +60 -1
  160. package/pp-core/workflows/vault-review.md +325 -0
  161. package/pp-core/workflows/verify-work.md +13 -12
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -0
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +13 -2
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -15
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -472
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/phase-tier.cjs +0 -467
  186. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  189. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  190. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  191. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -72
  192. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -34
  193. package/scripts/lib/size-budget.cjs +0 -311
  194. package/scripts/lint-neutrality-names.cjs +0 -694
  195. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  196. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -16,14 +16,11 @@ only when it resolves to `gitlab`; under any other provider (`azure` today) it i
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
 
19
- Step 2 is a single instruction to run `pp-tools init` in a real terminal, not a prose
20
- questionnaire and not a relay this document drives (Phase 34-09 gap closure). Four prior
21
- UAT walks — plain prose, hardened prose, a tested CLI arm, and a session state machine the
22
- agent was still handed one step at a time — all failed at the identical point: the agent's
23
- own model of which steps exist overrode this document and it substituted its own
24
- `AskUserQuestion` wizard for the question set (`34-VALIDATION.md`). The interaction now
25
- lives entirely inside `pp-tools init` itself — a human types answers directly into the
26
- process that acts on them — so this document has nothing left to compress, reorder or drop.
19
+ Step 2 relays the `init-contract` session: the engine returns one step at a time and the
20
+ agent shows each prompt verbatim and passes the reply back, adding no questions of its own
21
+ (earlier relays failed exactly there, `34-VALIDATION.md`). With the embedded org profile the
22
+ session asks only the project name, the description and the compose confirmation.
23
+ `pp-tools init` in a terminal is the same session for a user who prefers it.
27
24
 
28
25
  Inert of all Pareto/GitLab behavior when no .platform-bindings.json is present.
29
26
  </purpose>
@@ -67,23 +64,9 @@ _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_
67
64
  # Gate on presence via reader — absent file → {present:false} exit 0 (inert, step skipped, no jq)
68
65
  if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" = "true" ]; then
69
66
  PINNED=$(pp_run query platform-bindings --pick platform.tag)
70
- REPO=$(pp_run query platform-bindings --pick platform.repo)
71
- HOST_FROM_BINDINGS=$(pp_run query platform-bindings --pick platform.host 2>/dev/null)
72
- PLATFORM_HOST="${HOST_FROM_BINDINGS:-${HOST:-gitlab.com}}"
73
- PLATFORM_REPO_URL="https://${PLATFORM_HOST}/${REPO}.git"
74
- # Resolve latest STABLE tag from remote — never hard-block if offline (empty LATEST → skip drift check)
75
- # Keep --refs to strip ^{} annotated-tag rows; --sort=-v:refname sorts server-side
76
- # Stable filter fixes prerelease mis-ordering: git ranks v1.4.0-rc.1 after v1.4.0 (wrong by semver)
77
- # semver-compare.cjs is the single blessed source for comparison — do NOT reimplement
78
- REMOTE_TAGS=$(git ls-remote --tags --refs --sort=-v:refname "$PLATFORM_REPO_URL" 'v*' 2>/dev/null | sed 's#.*/##')
79
- # IN-03: pass the lib dir via env (PP_LIB_DIR) rather than interpolating ${PP_TOOLS%/*}
80
- # into the JS program text — keeps the path out of the evaluated source.
81
- LATEST=$(printf '%s\n' "$REMOTE_TAGS" | PP_LIB_DIR="${PP_TOOLS%/*}/lib" node -e "
82
- const sc = require(process.env.PP_LIB_DIR + '/semver-compare.cjs');
83
- const lines = require('fs').readFileSync(0, 'utf-8').split('\n').map(s=>s.trim()).filter(Boolean);
84
- const stable = lines.filter(sc.isStableTripletSemver).sort(sc.compareSemverCore);
85
- process.stdout.write((stable[stable.length-1] || '') + '\n');
86
- " 2>/dev/null)
67
+ # The owner is compose-bindings resolveLatestPlatformTag — the same lookup pp-tools init and check ship.platform-drift use — so the URL is never composed in shell.
68
+ # Tagless, offline and unreachable all print as empty: latest=<unresolved> and the drift comparison is skipped (never hard-block).
69
+ LATEST=$(pp_run query platform-latest-tag --pick latest 2>/dev/null)
87
70
  echo "platform preflight: pinned=${PINNED:-<none>} latest=${LATEST:-<unresolved>}"
88
71
  if [ -n "$LATEST" ] && [ -n "$PINNED" ] && [ "$PINNED" != "$LATEST" ]; then
89
72
  echo "PP_PLATFORM_DRIFT=1"
@@ -100,8 +83,8 @@ tag. STOP before Step 1 and use `AskUserQuestion` to let the user choose:
100
83
  - **Proceed on the older pin** — continue knowingly; record the deferral in `PROJECT.md`
101
84
  so it is a conscious decision, not silent drift.
102
85
 
103
- If `LATEST` was unresolved (offline / no network), note it and proceed — never hard-block
104
- initiation on an unreachable remote. For non-Pareto repos the block is a no-op and this
86
+ If `LATEST` was unresolved (no stable release tag, offline, or an unreachable remote), note it
87
+ and proceed — never hard-block initiation on an unresolved tag. For non-Pareto repos the block is a no-op and this
105
88
  step is skipped entirely.
106
89
 
107
90
  ---
@@ -125,7 +108,8 @@ Apply the `#3491` guard (never nest `.git` inside an existing worktree):
125
108
  - If `has_git` is `true` and `in_nested_subdir` is `true`: **skip `git init`**; warn the
126
109
  user: "Initializing inside an existing worktree (`${git_worktree_root}`); planning files
127
110
  will track to the outer repo." Do not call `git init`. This case pairs with the
128
- `--skipGitInit` flag used by the gated GitLab provisioning path in Plan 02.
111
+ skip-git-init flag the gated GitLab provisioning path's locked create recipe
112
+ carries (G-4).
129
113
  - If `has_git` is `true` and `in_nested_subdir` is `false`: **skip `git init`** (already
130
114
  at the worktree root).
131
115
  - If `has_git` is `false`: run `git init` to initialize a new repository.
@@ -135,50 +119,42 @@ Step 0). Continue honoring it for every `AskUserQuestion` call in this and later
135
119
 
136
120
  ---
137
121
 
138
- ## 2. Identity, provider and binding — run `pp-tools init` (D-05, D-07, INIT-01, INIT-02, INIT-03)
122
+ ## 2. Identity, provider and binding — relay the `init-contract` session (D-05, D-07, INIT-01, INIT-02, INIT-03)
139
123
 
140
- Tell the user to run this in their own terminal, then **stop** and wait for them to confirm
141
- they are done before continuing to Step 3:
124
+ `pp-tools init` is a loop over this same session, so relaying it here makes the same writes
125
+ (`.planning/config.json`, `vault.provider`, `vault.enabled`, `.platform-bindings.json`). The
126
+ engine owns which questions exist, their order and every write; you only relay. Repeat until
127
+ the step is `done`:
142
128
 
143
- ```text
144
- node "<dir>/bin/pp-tools.cjs" init
129
+ ```bash
130
+ pp_run query init-contract --session-next
145
131
  ```
146
132
 
147
- `<dir>` is the directory on this run's `pp-core answered from:` line, so the terminal runs the
148
- install this session runs; a bare `pp-tools` resolves through `PATH` and can be another one.
149
- With no such line, give `pp-tools init`.
150
-
151
- **Do not ask the identity questions yourself. Do not describe the question set, the
152
- provider options, or the compose confirmation. Do not attempt to drive the `init-contract`
153
- session on the user's behalf — not even one step of it, even via `--session-next` /
154
- `--session-answer`.** Four dated UAT walks (`34-VALIDATION.md` § Manual-Only Verifications)
155
- found the IDENTICAL failure at exactly this boundary, across four escalating strategies —
156
- plain prose, hardened prose, a data-supplying CLI arm, and a session state machine the
157
- agent was still handed one step at a time. Every pure-bash step ran reliably every time;
158
- the one step whose job is to ask a structured question set was always overridden by the
159
- agent's own `AskUserQuestion` wizard instead. The only strategy left is to remove the
160
- interaction from this document entirely: `pp-tools init` prompts on a real terminal, reads
161
- the answers itself, and performs every write (`.planning/config.json`, `vault.provider`,
162
- `.platform-bindings.json`) — a human types answers directly into the process that acts on
163
- them, with nothing in between. It refuses to run when stdin is not a TTY.
133
+ Act on the JSON it prints and on nothing else. Ask in plain chat text, never with
134
+ `AskUserQuestion`: the answers are free text, and invented options or extra questions are the
135
+ failure this step guards against.
136
+
137
+ - **A question** (`step` is a question id): first list each new `autoAnswered` entry once as
138
+ `id = value (source)`, then ask `prompt` word for word. With `optionalAnswer: true` the user
139
+ may answer `none`. Pass the reply unchanged, in single quotes (a `'` written as `'\''`):
140
+ `pp_run query init-contract --session-answer '<step>=<reply>'`. On an error, show its
141
+ message and ask the same `prompt` again.
142
+ - **`compose_confirm`**: show `info.provenance` as a field / value / source table, ask
143
+ `prompt`, and pass the reply the same way.
144
+ - **`verify`**: show each violation's code and message and stop; the user fixes the cause, or
145
+ runs `pp_run query init-contract --session-reset` and Step 2 starts over.
146
+ - **`done`**: continue to Step 2.6.
147
+
148
+ A user who would rather type into the engine directly can run
149
+ `node "<dir>/bin/pp-tools.cjs" init` in their own terminal instead, `<dir>` being the directory
150
+ on this run's `pp-core answered from:` line (a bare `pp-tools` resolves through `PATH` and can
151
+ be another install); wait for them to confirm before Step 2.6.
164
152
 
165
153
  **If this step is skipped, `/pp-init` has not completed** — no identity config, no
166
154
  `vault.provider`, and no binding are produced without it. `pp_run query init-contract
167
155
  --verify` reports exactly what is still missing if the operator wants to confirm before
168
156
  continuing.
169
157
 
170
- **Then arm and probe the client vault** (after `pp-tools init` finishes):
171
-
172
- ```bash
173
- pp_run query init-contract --arm-vault
174
- pp_run vault probe
175
- ```
176
-
177
- Show both outputs verbatim. `--arm-vault` sets `vault.enabled: true` only where the key is
178
- absent (an explicit `false` is kept); `ok:false` means no client vault is bound — re-run
179
- `pp-tools init`. The probe line says whether the client vault is reachable at its pinned ref;
180
- it prints nothing when the vault is disabled. Neither ever blocks this step.
181
-
182
158
  **Scope boundary:** this step writes identity, provider and the platform binding only. All
183
159
  coding-standards, architecture context, client-engagement CLAUDE.md blocks, and
184
160
  discovery-session scaffolding are out of scope here. Phase 9 owns those; /pp-discovery
@@ -186,6 +162,188 @@ self-generates its own session files (D-03).
186
162
 
187
163
  ---
188
164
 
165
+ ## 2.6. Reachability proof (D-07-01, D-07-05, INIT-01, INIT-02, INIT-03)
166
+
167
+ Runs immediately after the Step 2 session is `done`, before any remote wiring. Three gate
168
+ signals decide whether it does anything at all, in this order: `vault.enabled` is the
169
+ literal `true` (OD-1 — `pp-tools init` writes it once a profile resolved and composition
170
+ succeeded); an org profile resolves (D-07-01); then a platform binding is present ⇒ probe,
171
+ absent ⇒ the loud failure below (D-07-02). A generic or non-vault project fails the first
172
+ two signals, and this whole step then prints **nothing at all** — no probe, no cache, no
173
+ record, no side effect of any kind (INIT-01).
174
+
175
+ The probe's exit code is `0` on every path by contract (STRICT-02): the verdict lives in
176
+ its booleans, never in `$?`, and its `error` prose is never echoed (D-07-07). The fetch
177
+ runs only *after* a clean probe, so a project that cannot reach its vault never ends up
178
+ with a half-populated cache (D-07-05).
179
+
180
+ ```bash
181
+ # D-07-01 + OD-1 + INIT-01: three signals, all read through the resolvers every other path
182
+ # uses. Anything but the literal "true" is a fail-inert default.
183
+ PP_INIT_PROBE=""
184
+ # 16-16 D-81: the gate answers for itself (isVaultGateActive). A string "true" and a
185
+ # flat-only key read OFF there, in --verify (15-04 D-32) and here alike. It reads no
186
+ # bindings, so the unbound arm below stays reachable.
187
+ VAULT_ON=$(pp_run loop render-hooks plan:pre --active-cap vault 2>/dev/null || echo false)
188
+ # --raw is load-bearing on the org-profile read: without it that arm prints prose.
189
+ PROFILE_RESOLVED=$(pp_run query vault org-profile --raw --pick profile.resolved 2>/dev/null || echo false)
190
+ if [ "$VAULT_ON" = "true" ] && [ "$PROFILE_RESOLVED" = "true" ]; then
191
+ PROFILE_NAME=$(pp_run query vault org-profile --raw --pick profile.name 2>/dev/null || echo "")
192
+ if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" != "true" ]; then
193
+ # D-07-02: bindings ABSENT on a profile-bound project is the loud failure, never a skip; a
194
+ # present but malformed file is named as such (D-47 b: the verb treats it as present).
195
+ PP_INIT_PROBE=unbound
196
+ PP_INIT_BINDINGS="is absent"; PP_INIT_WHY="no platform binding"; PP_INIT_SEEN="--verify cannot see an unbound stop without a record: it reports ok:true until .planning is writable and /pp-init is re-run"
197
+ [ -f .platform-bindings.json ] && { PP_INIT_BINDINGS="is present and will not parse"; PP_INIT_WHY="malformed platform binding"; PP_INIT_SEEN="--verify reports INIT_PROBE_NOT_RUN until a re-run records it"; }
198
+ printf '%s\n' "Reachability proof: NOT POSSIBLE — org profile \"${PROFILE_NAME}\" resolved, so a platform binding is expected, but .platform-bindings.json ${PP_INIT_BINDINGS}."
199
+ # WR-02: the remedy branches on a MEASUREMENT (D-07-07): `--reset` reaches the compose
200
+ # confirmation only when something is composable. Offline dry run, exit 0.
201
+ COMPOSABLE=$(pp_run query compose-bindings --offline --pick composable 2>/dev/null || echo false)
202
+ if [ "$COMPOSABLE" = "true" ]; then
203
+ printf '%s\n' "Nothing was probed. Run \"pp-tools init --reset\" to redo the question set and reach the compose confirmation, then re-run /pp-init."
204
+ else
205
+ printf '%s\n' "Nothing was probed, and \"pp-tools init --reset\" cannot help — the org profile seeds no binding to compose. Add platform.repo and client.vaultRepo / client.vaultRef to .platform-bindings.json by hand (or update the profile), then re-run /pp-init."
206
+ fi
207
+ # WR-02: nothing was probed, but the stop IS recorded, or --verify reports ok:true on a
208
+ # project that halted. R2-WR-04: a REFUSED write is a detail line (reachability-outcome.md
209
+ # c-unrecorded) stating the MEASURED consequence: owesAProof keys on the bindings file, so
210
+ # --verify sees the stop only when the file exists (the malformed case). Both arms' wording
211
+ # is re-measured by the Step 2.6 test file.
212
+ if ! pp_run query init-contract --record-probe "failed=${PP_INIT_WHY}; nothing could be probed" >/dev/null 2>&1; then
213
+ printf '%s\n' " record .planning/config.json not written — this stop is unrecorded, and ${PP_INIT_SEEN}"
214
+ fi
215
+ echo "PP_INIT_PROBE=unbound"
216
+ else
217
+ # D-07-05: probe first. ONE --raw capture; every read below parses this text, because
218
+ # each --pick would re-run the whole probe. The exit code is 0 on every path.
219
+ PROBE=$(pp_run query vault probe --raw 2>/dev/null || echo '{"targets":[]}')
220
+ # R3-CR-02 + R3-WR-02: ONE evaluation of the target set, read by every arm
221
+ # 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');}")
242
+ PROBE_VERDICT=$(printf '%s' "$PROBE_EVAL" | sed -n '1p')
243
+ PROBE_ALL_DOWN=$(printf '%s' "$PROBE_EVAL" | sed -n '2p')
244
+ PROBE_NOTE=$(printf '%s' "$PROBE_EVAL" | sed -n '3p')
245
+ PROBE_LINES=$(printf '%s' "$PROBE_EVAL" | sed -n '4,$p')
246
+ FAIL_KIND=""
247
+ FAIL_DETAIL=""
248
+ FAIL_NOTE=""
249
+ if [ "$PROBE_VERDICT" = "pass" ]; then
250
+ # D-07-05: the fetch is what produces INIT-02's count, warm cache and SUMMARY.md.
251
+ FETCH=$(pp_run query vault fetch --raw 2>/dev/null || echo '{}')
252
+ FETCH_TIER=$(printf '%s' "$FETCH" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).tier||''));}catch{process.stdout.write('');}")
253
+ FETCH_SOURCE=$(printf '%s' "$FETCH" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).source||''));}catch{process.stdout.write('');}")
254
+ FETCH_REPO=$(printf '%s' "$FETCH" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).repoSafe||''));}catch{process.stdout.write('');}")
255
+ FETCH_REF=$(printf '%s' "$FETCH" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).ref||''));}catch{process.stdout.write('');}")
256
+ FETCH_SHA=$(printf '%s' "$FETCH" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).resolvedSha||'').slice(0,8));}catch{process.stdout.write('');}")
257
+ 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
+ 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
+ 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('');}")
260
+ # Never the probe's internal HEAD placeholder — an unpinned platform says so.
261
+ PLATFORM_TAG=$(pp_run query platform-bindings --pick platform.tag 2>/dev/null)
262
+ if [ "$FETCH_TIER" = "client" ] && [ "$FETCH_SUMMARY" = "true" ] && { [ "$FETCH_SOURCE" = "fetched" ] || [ "$FETCH_SOURCE" = "cache" ]; }; then
263
+ # WR-03: the record write runs BEFORE the card and its exit status decides which
264
+ # card prints. A refused write used to be a warning under a PASSED card, and PASSED
265
+ # routes to "continue to Step 3" — so "unrecorded" was defined to continue. It is a
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
268
+ PP_INIT_PROBE=pass
269
+ echo "Reachability proof: PASSED"
270
+ # OD-3 + CR-01: the DISPLAY-SAFE coordinate (repoSafe), ref, tier and booleans
271
+ # only. Credentials are stripped at the source, so a PAT in the binding reaches
272
+ # neither this card nor the record. Never url, never error.
273
+ printf '%s\n' " platform ${PLATFORM_REPO} @ ${PLATFORM_TAG:-(unpinned)} reachable"
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
+ printf '%s\n' "$PROBE_LINES" | grep '^ shared ' || true
276
+ echo "PP_INIT_PROBE=pass"
277
+ else
278
+ PP_INIT_PROBE=fail
279
+ FAIL_KIND=record
280
+ FAIL_NOTE=""
281
+ FAIL_DETAIL="$PROBE_LINES
282
+ record .planning/config.json not written — the proof is unrecorded; --verify reports it as never run"
283
+ fi
284
+ else
285
+ PP_INIT_PROBE=fail
286
+ FAIL_KIND=fetch
287
+ FAIL_DETAIL=" The probe answered, but the fetch did not land on the client vault: tier ${FETCH_TIER:-none}, source ${FETCH_SOURCE:-none}, SUMMARY.md written ${FETCH_SUMMARY:-false}."
288
+ FAIL_NOTE="failed=fetch tier ${FETCH_TIER:-none} source ${FETCH_SOURCE:-none} summaryWritten ${FETCH_SUMMARY:-false}"
289
+ fi
290
+ else
291
+ PP_INIT_PROBE=fail
292
+ FAIL_KIND=probe
293
+ FAIL_DETAIL="$PROBE_LINES"
294
+ # R3-WR-02: the recorded miss comes from the SAME evaluation the verdict and
295
+ # the card came from — recomputing it here is how the four copies drifted.
296
+ # CR-02: filtering the RENDERING was the original defect (an ABSENT target
297
+ # left the note a bare failed=, which recordProbeOutcome refused).
298
+ FAIL_NOTE="$PROBE_NOTE"
299
+ fi
300
+ # INIT-03 + D-07-07: ONE failure block for both fail arms — the header, the candidate
301
+ # heading and the escape sentence have a single home, so they cannot drift apart.
302
+ if [ "$PP_INIT_PROBE" = "fail" ]; then
303
+ echo "Reachability proof: FAILED — init is not complete."
304
+ printf '%s\n' "$FAIL_DETAIL"
305
+ echo "Things to check (candidates, not diagnoses):"
306
+ if [ "$FAIL_KIND" = "record" ]; then
307
+ echo " - .planning/config.json or its directory is not writable (permissions, a read-only mount, a full disk)"
308
+ echo " - re-run /pp-init once it is — the probe and fetch are cheap to repeat"
309
+ elif [ "$FAIL_KIND" = "fetch" ]; then
310
+ echo " - the client vault ref exists at the remote but its content could not be cloned (sparse paths, permissions)"
311
+ echo " - a fresh cache from an earlier run recorded a fallback tier — remove .planning/vault and re-run /pp-init"
312
+ echo " - .planning/vault/SUMMARY.md could not be written (directory permissions)"
313
+ else
314
+ if [ "$PROBE_ALL_DOWN" = "true" ]; then
315
+ echo " - no network / offline — every distinct remote was unreachable (record a deferral if you are offline; re-run /pp-init when back)"
316
+ fi
317
+ echo " - SSH key not loaded in the agent (scp-style targets)"
318
+ echo " - HTTPS credentials not configured for this host — git refused to prompt (GIT_TERMINAL_PROMPT=0)"
319
+ echo " - az login / host CLI session expired"
320
+ echo " - the ref does not exist at the remote (a 'ref not found' line above)"
321
+ echo " - a typo in the org profile or in .platform-bindings.json"
322
+ echo " - a global git url.<base>.insteadOf rewrite sending git to a different host than the one printed"
323
+ fi
324
+ echo "Next: fix and re-run /pp-init, or record an attributed deferral (see below)."
325
+ # Skipped when the failure IS the record write — it just failed (FAIL_KIND=record).
326
+ if [ "$FAIL_KIND" != "record" ]; then
327
+ pp_run query init-contract --record-probe "$FAIL_NOTE" >/dev/null 2>&1 || echo "warning: the probe outcome could not be recorded in .planning/config.json — --verify will report INIT_PROBE_NOT_RUN until a re-run records it"
328
+ fi
329
+ echo "PP_INIT_PROBE=fail"
330
+ fi
331
+ fi
332
+ fi
333
+ ```
334
+
335
+ **What the block printed decides what happens next.** Nothing at all ⇒ Step 2.6 was inert
336
+ (generic project, vault off, or no org profile resolved): continue to Step 3 without
337
+ comment, and do not mention the vault, the probe or this step to the user. On any other
338
+ outcome — `PP_INIT_PROBE=pass`, `PP_INIT_PROBE=unbound` or `PP_INIT_PROBE=fail` — **Read
339
+ `pp-core/workflows/init/steps/reachability-outcome.md` now and follow it.** That file owns
340
+ the pass narration, the INIT-03 stop, the two-option `AskUserQuestion`, the
341
+ `init-contract --defer-probe` escape and its three reason rules, and D-07-06 (being offline
342
+ is not a reason to skip). Do not invent a fourth outcome, and do not re-run the probe
343
+ hoping for a different answer.
344
+
345
+ ---
346
+
189
347
  ## 3. Remote wiring — provider-agnostic (non-Pareto / no bindings)
190
348
 
191
349
  **Gate (D-06, D-13) — bindings presence ONLY, never the provider answer.** Step 3 always
@@ -305,6 +463,12 @@ instead — do not fail, do not error, do not prompt for a missing CLI. "Skipped
305
463
  failed" is satisfied *and* the user still leaves with a wired remote, which a bare skip
306
464
  would not give them; pp-core makes zero provider-API calls it has no client for.
307
465
 
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
+
308
472
  **Security invariants — landmine guards:**
309
473
  - Always use the group-qualified `$GROUP/$SLUG` path — never a bare slug (T-08-04, D-09).
310
474
  A bare slug creates under the user's personal namespace, not the client group. G-1's
@@ -318,126 +482,6 @@ would not give them; pp-core makes zero provider-API calls it has no client for.
318
482
  - Always reconcile create with a `glab repo view` probe — a non-zero create exit does
319
483
  NOT mean nothing happened (T-08-09, D-09).
320
484
 
321
- #### G-2. Auth verify-only (D-11)
322
-
323
- `/pp-init` **never establishes auth and never reads or writes the user's token.**
324
- Secrets stay in the environment or OS keyring. Only verify that a session exists:
325
-
326
- ```bash
327
- glab auth status >/dev/null 2>&1 || {
328
- echo "Not authenticated to GitLab. Run: glab auth login"
329
- exit 1
330
- }
331
- ```
332
-
333
- If this check fails, stop immediately. Do not proceed to probe or create.
334
- Never run `glab auth login` from within `/pp-init`.
335
-
336
- #### G-3. Existence probe → adopt-if-exists (D-09, D-12)
337
-
338
- Pre-check with a read-only probe before any create attempt. `glab repo create` is **not
339
- idempotent** — re-running it against an existing project errors but may still produce side
340
- effects (the GitLab project is created before the remote-add step that then fails). The
341
- probe avoids this landmine.
342
-
343
- This is one self-contained block: it sets `PROJECT_EXISTS` and, on the exists path,
344
- adopts (links the remote). It NEVER creates — create lives in its own G-4 block, gated
345
- on `PROJECT_EXISTS=false`, so the create invocation can never fall inside the exists
346
- branch.
347
-
348
- ```bash
349
- if glab repo view "$GROUP/$SLUG" -F json >/dev/null 2>&1; then
350
- # Project already exists — ADOPT: link remote only, skip create (D-12)
351
- PROJECT_EXISTS=true
352
- echo "GitLab project $GROUP/$SLUG already exists — adopting (linking remote, skipping create)."
353
- # IN-01: match the remote scheme glab itself uses on the create path — honor the
354
- # user's `glab config get git_protocol` (https default) instead of hardcoding SSH,
355
- # so adopt and create produce a consistent origin URL.
356
- GIT_PROTO=$(glab config get git_protocol 2>/dev/null || echo https)
357
- if [ "$GIT_PROTO" = "ssh" ]; then
358
- ADOPT_URL="git@${HOST:-gitlab.com}:$GROUP/$SLUG.git"
359
- else
360
- ADOPT_URL="https://${HOST:-gitlab.com}/$GROUP/$SLUG.git"
361
- fi
362
- git remote get-url origin >/dev/null 2>&1 || git remote add origin "$ADOPT_URL"
363
- echo "Remote 'origin' → $ADOPT_URL"
364
- else
365
- # Project not found — defer to G-4 for confirm-then-create
366
- PROJECT_EXISTS=false
367
- echo "GitLab project $GROUP/$SLUG not found — will confirm before create."
368
- fi
369
- ```
370
-
371
- If the project exists (`PROJECT_EXISTS=true`), the remote is already linked above
372
- (skipping create entirely) — proceed straight to G-5 reconciliation. The adopt path does
373
- not show a create confirmation — it only links the remote, which is a local-only
374
- operation and can be undone with `git remote remove origin`.
375
-
376
- #### G-4. Per-action confirmation, then create (D-05, D-09, D-10, SAFE-01)
377
-
378
- **Only run this step when `PROJECT_EXISTS=false`** (the existence probe in G-3 found no
379
- project). If `PROJECT_EXISTS=true`, skip G-4 entirely — the project was already adopted —
380
- and go to G-5. When creating, **show the exact command and target before firing**. The
381
- user must confirm explicitly. Nothing is batched. (D-05)
382
-
383
- Present to the user (via `AskUserQuestion` or TEXT_MODE numbered list):
384
-
385
- ```text
386
- About to create GitLab project:
387
- Command: glab repo create "$GROUP/$SLUG"
388
- Group path: $GROUP/$SLUG
389
- Visibility: private (enforced --private; never the glab default --internal)
390
- Requested: $VIS (from bindings gitlab.visibility — see note below)
391
- Default branch: main
392
- Skip git init: yes (local repo already initialized — --skipGitInit)
393
- Remote name: origin
394
-
395
- Proceed? [y/N]
396
- ```
397
-
398
- `$VIS` (from G-1) is surfaced in the confirmation so the binding's requested visibility is
399
- never silently ignored (WR-01). pp-core always **creates private** regardless; a
400
- non-private binding must be applied as a deliberate, separate post-create change.
401
-
402
- Only after the user confirms — and only with `PROJECT_EXISTS=false` — run the locked
403
- create recipe (D-09, D-10). This is a standalone block: it carries no `if`/`else`/`fi`, so
404
- it can only ever run as the deliberate, confirmed not-exists path:
405
-
406
- ```bash
407
- glab repo create "$GROUP/$SLUG" \
408
- --description "$DESC" \
409
- --private \
410
- --defaultBranch main \
411
- --skipGitInit \
412
- --remoteName origin
413
- ```
414
-
415
- **Visibility hard-default (D-10):** the create invocation always carries `--private`.
416
- Never omit it and let `glab` silently apply `--internal`. `VIS` from bindings is read in
417
- G-1 and **surfaced in the G-4 confirmation** (WR-01) so a `public`/`internal` request is
418
- visible to the user — but pp-core still creates the project **private**. Applying a
419
- non-private visibility is a deliberate, separate post-create action (e.g. `glab repo edit
420
- --visibility …`) the operator performs knowingly; `/pp-init` never auto-relaxes visibility.
421
-
422
- #### G-5. Post-create reconciliation (D-09)
423
-
424
- After the create (or after the adopt path), re-probe existence. A non-zero exit from
425
- `glab repo create` does **not** mean the project was not created — `glab` exits non-zero
426
- (code 3) when the GitLab project was created but the subsequent `git remote add` step
427
- then failed (e.g., because `origin` already existed). The `repo view` probe is the source
428
- of truth, not the exit code (research §1 "Remote-add caveat — VERIFIED"):
429
-
430
- ```bash
431
- if glab repo view "$GROUP/$SLUG" -F json >/dev/null 2>&1; then
432
- echo "Reconciled: GitLab project $GROUP/$SLUG confirmed."
433
- else
434
- echo "WARN: glab repo view $GROUP/$SLUG returned non-zero — verify the project was created before continuing."
435
- fi
436
- ```
437
-
438
- If reconciliation warns, do not auto-retry. Prompt the user to check GitLab directly
439
- before proceeding to Step 4 (handoff routing).
440
-
441
485
  ### Gated client-context capture (Pareto projects — bindings present)
442
486
 
443
487
  This step runs **only when** `pp_run query platform-bindings --pick present 2>/dev/null` equals
@@ -445,96 +489,12 @@ This step runs **only when** `pp_run query platform-bindings --pick present 2>/d
445
489
  success criterion #4). All capture is **offline-only** — no Obsidian, no MCP, no network (D-05 /
446
490
  T-09-05).
447
491
 
448
- ```bash
449
- if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" = "true" ]; then
450
- # ── D-05: edit-on-re-run — read existing block first ──────────────────────────
451
- EXISTING=$(pp_run query generate-client-context --read 2>/dev/null || echo '{"present":false}')
452
- EXISTING_PRESENT=$(echo "$EXISTING" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const o=JSON.parse(d);process.stdout.write(String(o.present||'false'));}catch{process.stdout.write('false');}")
453
-
454
- if [ "$EXISTING_PRESENT" = "true" ]; then
455
- echo ""
456
- echo "Existing client-context block found. Press Enter to keep each value, or type to replace it."
457
- EXISTING_CLIENT_NAME=$(echo "$EXISTING" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const o=JSON.parse(d);process.stdout.write(o.client_name||'');}catch{}" 2>/dev/null)
458
- EXISTING_CLIENT_SLUG=$(echo "$EXISTING" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const o=JSON.parse(d);process.stdout.write(o.client_slug||'');}catch{}" 2>/dev/null)
459
- EXISTING_CODING=$(echo "$EXISTING" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const o=JSON.parse(d);process.stdout.write(o.coding_standards||'');}catch{}" 2>/dev/null)
460
- EXISTING_ARCH=$(echo "$EXISTING" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const o=JSON.parse(d);process.stdout.write(o.architecture_patterns||'');}catch{}" 2>/dev/null)
461
- EXISTING_UI=$(echo "$EXISTING" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const o=JSON.parse(d);process.stdout.write(o.ui_brand_conventions||'');}catch{}" 2>/dev/null)
462
- EXISTING_DOD=$(echo "$EXISTING" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const o=JSON.parse(d);process.stdout.write(o.definition_of_done||'');}catch{}" 2>/dev/null)
463
- fi
464
- fi
465
- ```
466
-
467
- **Field capture — one field at a time (D-01, D-02):** Prompt the user for each of the five fields
468
- sequentially. All five are required — **re-prompt on empty input until a non-empty value is given**
469
- (no skip, no catch-all). Multi-line values are accepted: instruct the user to type or paste, then
470
- enter a blank line (or a sentinel line such as `EOF`) to signal end-of-field. Do not call any
471
- network resource during capture.
472
-
473
- Capture these fields, in this order:
474
-
475
- 1. **Client name** — the human-readable client or project name (e.g. "Acme Corp").
476
- - On re-run: show the current value (`$EXISTING_CLIENT_NAME`) and let the user keep or replace.
477
- - After capturing, derive a URL-safe slug via `pp_run query generate-slug` on the name; present
478
- the suggested slug and let the user confirm or override. Store as `CLIENT_SLUG`.
479
-
480
- 2. **Coding standards** — the conventions and rules the Build agent must follow (e.g. language,
481
- style guide, patterns to use/avoid). Multi-line OK.
482
- - On re-run: show `$EXISTING_CODING`.
483
-
484
- 3. **Architecture patterns** — the structural patterns and constraints the Build agent must respect
485
- (e.g. layering, module boundaries, framework idioms). Multi-line OK.
486
- - On re-run: show `$EXISTING_ARCH`.
487
-
488
- 4. **UI / brand conventions** — visual and component standards the Build agent must apply (e.g.
489
- design system, color tokens, typography rules). Multi-line OK.
490
- - On re-run: show `$EXISTING_UI`.
491
-
492
- 5. **Definition of done** — the quality gates that must pass before any task is marked complete
493
- (e.g. tests green, lint clean, accessibility checked). Multi-line OK.
494
- - On re-run: show `$EXISTING_DOD`.
495
-
496
- **Write block** — after all five fields are captured, write them to a temp file and invoke the
497
- managed-block writer:
498
-
499
- ```bash
500
- if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" = "true" ]; then
501
- # Write collected values to a temp JSON file using node + JSON.stringify so that
502
- # multi-line values, embedded quotes, backslashes, and $(...) sequences are encoded
503
- # correctly (CR-02: an unquoted heredoc would produce invalid JSON for any multi-line
504
- # field, breaking cmdGenerateClientContext with "Failed to parse JSON input").
505
- TMP_CTX=$(mktemp /tmp/pp-client-ctx-XXXXXX.json)
506
- CLIENT_NAME="$CLIENT_NAME" \
507
- CLIENT_SLUG="$CLIENT_SLUG" \
508
- CODING_STANDARDS="$CODING_STANDARDS" \
509
- ARCHITECTURE_PATTERNS="$ARCHITECTURE_PATTERNS" \
510
- UI_BRAND_CONVENTIONS="$UI_BRAND_CONVENTIONS" \
511
- DEFINITION_OF_DONE="$DEFINITION_OF_DONE" \
512
- node -e "
513
- const e = process.env;
514
- const fs = require('fs');
515
- fs.writeFileSync(process.argv[1], JSON.stringify({
516
- client_name: e.CLIENT_NAME,
517
- client_slug: e.CLIENT_SLUG,
518
- coding_standards: e.CODING_STANDARDS,
519
- architecture_patterns: e.ARCHITECTURE_PATTERNS,
520
- ui_brand_conventions: e.UI_BRAND_CONVENTIONS,
521
- definition_of_done: e.DEFINITION_OF_DONE
522
- }));
523
- " "$TMP_CTX"
524
-
525
- # Invoke the Plan 09-01 writer — routes through updateSection, marker-bounded, idempotent
526
- pp_run query generate-client-context --from-json "$TMP_CTX"
527
- rm -f "$TMP_CTX"
528
- echo "Client context written to CLAUDE.md (<!-- PP:client-context-start --> block)."
529
- fi
530
- ```
531
-
532
- This writes a `## Client Context & Patterns` block bounded by
533
- `<!-- PP:client-context-start -->` … `<!-- PP:client-context-end -->` markers into the local
534
- CLAUDE.md (config-driven path, default `./.claude/CLAUDE.md`). The write is idempotent —
535
- re-running `/pp-init` replaces the bounded block, never duplicates it. All values stay local;
536
- no network or external service is involved.
537
-
492
+ When — and only when — that gate answers `"true"`, **Read
493
+ `pp-core/workflows/init/steps/client-context-capture.md` and follow it.** That file owns
494
+ the re-run edit path (D-05), the five required fields with their re-prompt-on-empty and
495
+ multi-line rules (D-01, D-02), and the marker-bounded `generate-client-context` write into
496
+ the local CLAUDE.md. On a non-Pareto project do not read it at all — skip straight to
497
+ Step 4, leaving no prompt fired and no block written.
538
498
  ---
539
499
 
540
500
  ## 4. Handoff routing (D-04)