@pareto4/pp-core 1.5.4 → 1.6.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.
@@ -0,0 +1,530 @@
1
+ <purpose>
2
+ Universal front door for initializing any project with Pareto. Takes any directory — empty
3
+ or containing an existing codebase — from zero to: git-initialized, minimal identity
4
+ captured, remote wired, and handed off to either /pp-discovery or /pp-new-project.
5
+
6
+ Implements decisions: D-02 (no Obsidian/board scaffolding), D-03 (no discovery session
7
+ scaffolding here — /pp-discovery self-generates them), D-04 (ASK handoff with smart
8
+ default), D-06 (generic remote wiring: prompt URL → git remote add → confirmed push,
9
+ zero provider API), D-07 (minimal identity only — no CLAUDE.md client-context block),
10
+ D-13 (gate on .platform-bindings.json via the Phase 7 reader).
11
+
12
+ Inert of all Pareto/GitLab behavior when no .platform-bindings.json is present.
13
+ </purpose>
14
+
15
+ <required_reading>
16
+ Read all files referenced by the invoking prompt's execution_context before starting.
17
+ </required_reading>
18
+
19
+ <process>
20
+
21
+ ## Shim hoist
22
+
23
+ Resolve `pp_run` before any step — required for the platform-bindings preflight (D-13).
24
+
25
+ ```bash
26
+ ```
27
+
28
+ ## Text mode (resolve before any prompt — WR-02)
29
+
30
+ Resolve `TEXT_MODE` here, **before Step 0**, because the Step 0 drift prompt is the first
31
+ `AskUserQuestion` a non-Claude runtime hits. Set `TEXT_MODE=true` if `--text` is present in
32
+ `$ARGUMENTS`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call (including the
33
+ Step 0 drift prompt) with a plain-text numbered list and ask the user to type their choice
34
+ number. This is required for non-Claude runtimes where `AskUserQuestion` is unavailable.
35
+
36
+ ---
37
+
38
+ ## 0. Platform-version preflight (Pareto satellite repos)
39
+
40
+ **Runs before Step 1. Inert for non-Pareto projects** — gated entirely on
41
+ `.platform-bindings.json` existing (D-13), so generic projects skip it with no effect.
42
+
43
+ A repo scaffolded by `/pareto-new-app` pins the Pareto platform at a specific `vX.Y.Z` tag
44
+ (module `?ref=` pins, the `.gitlab-ci.yml` include `ref:`, and the canonical
45
+ `.platform-bindings.json` → `platform.tag`). Project initiation is the moment to confirm
46
+ the app is on the **latest** platform before any building starts — bumping after a
47
+ milestone is built is far more expensive.
48
+
49
+ ```bash
50
+ _PP_SHIM_NAME="pp-tools.cjs"; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; pp_run() { "$PP_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
51
+ # Gate on presence via reader — absent file → {present:false} exit 0 (inert, step skipped, no jq)
52
+ if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" = "true" ]; then
53
+ PINNED=$(pp_run query platform-bindings --pick platform.tag)
54
+ REPO=$(pp_run query platform-bindings --pick platform.repo)
55
+ HOST_FROM_BINDINGS=$(pp_run query platform-bindings --pick platform.host 2>/dev/null)
56
+ PLATFORM_HOST="${HOST_FROM_BINDINGS:-${HOST:-gitlab.com}}"
57
+ PLATFORM_REPO_URL="https://${PLATFORM_HOST}/${REPO}.git"
58
+ # Resolve latest STABLE tag from remote — never hard-block if offline (empty LATEST → skip drift check)
59
+ # Keep --refs to strip ^{} annotated-tag rows; --sort=-v:refname sorts server-side
60
+ # Stable filter fixes prerelease mis-ordering: git ranks v1.4.0-rc.1 after v1.4.0 (wrong by semver)
61
+ # semver-compare.cjs is the single blessed source for comparison — do NOT reimplement
62
+ REMOTE_TAGS=$(git ls-remote --tags --refs --sort=-v:refname "$PLATFORM_REPO_URL" 'v*' 2>/dev/null | sed 's#.*/##')
63
+ # IN-03: pass the lib dir via env (PP_LIB_DIR) rather than interpolating ${PP_TOOLS%/*}
64
+ # into the JS program text — keeps the path out of the evaluated source.
65
+ LATEST=$(printf '%s\n' "$REMOTE_TAGS" | PP_LIB_DIR="${PP_TOOLS%/*}/lib" node -e "
66
+ const sc = require(process.env.PP_LIB_DIR + '/semver-compare.cjs');
67
+ const lines = require('fs').readFileSync(0, 'utf-8').split('\n').map(s=>s.trim()).filter(Boolean);
68
+ const stable = lines.filter(sc.isStableTripletSemver).sort(sc.compareSemverCore);
69
+ process.stdout.write((stable[stable.length-1] || '') + '\n');
70
+ " 2>/dev/null)
71
+ echo "platform preflight: pinned=${PINNED:-<none>} latest=${LATEST:-<unresolved>}"
72
+ if [ -n "$LATEST" ] && [ -n "$PINNED" ] && [ "$PINNED" != "$LATEST" ]; then
73
+ echo "PP_PLATFORM_DRIFT=1"
74
+ fi
75
+ fi
76
+ ```
77
+
78
+ **If the block printed `PP_PLATFORM_DRIFT=1`:** the app is pinned behind the latest platform
79
+ tag. STOP before Step 1 and use `AskUserQuestion` to let the user choose:
80
+ - **Bump first (recommended)** — pause initiation; bump the module `?ref=` pins, the
81
+ `.gitlab-ci.yml` include `ref:`, and `.platform-bindings.json` `platform.tag` to
82
+ `${LATEST}` (review `CHANGELOG.md` between the two tags for breaking changes), then
83
+ re-run `/pp-init`.
84
+ - **Proceed on the older pin** — continue knowingly; record the deferral in `PROJECT.md`
85
+ so it is a conscious decision, not silent drift.
86
+
87
+ If `LATEST` was unresolved (offline / no network), note it and proceed — never hard-block
88
+ initiation on an unreachable remote. For non-Pareto repos the block is a no-op and this
89
+ step is skipped entirely.
90
+
91
+ ---
92
+
93
+ ## 1. Worktree-aware git init
94
+
95
+ Parse the init signals by running the query below. `init.execute-phase` is reused here
96
+ deliberately (IN-02): it is the shared verb that emits the generic git-state signals
97
+ (`has_git`, `in_nested_subdir`, `git_worktree_root`) this step needs — no phase work is
98
+ triggered when called with an empty phase argument.
99
+
100
+ ```bash
101
+ INIT=$(pp_run query init.execute-phase "" 2>/dev/null || echo '{}')
102
+ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
103
+ ```
104
+
105
+ Extract from the JSON: `has_git`, `in_nested_subdir`, `git_worktree_root`.
106
+
107
+ Apply the `#3491` guard (never nest `.git` inside an existing worktree):
108
+
109
+ - If `has_git` is `true` and `in_nested_subdir` is `true`: **skip `git init`**; warn the
110
+ user: "Initializing inside an existing worktree (`${git_worktree_root}`); planning files
111
+ will track to the outer repo." Do not call `git init`. This case pairs with the
112
+ `--skipGitInit` flag used by the gated GitLab provisioning path in Plan 02.
113
+ - If `has_git` is `true` and `in_nested_subdir` is `false`: **skip `git init`** (already
114
+ at the worktree root).
115
+ - If `has_git` is `false`: run `git init` to initialize a new repository.
116
+
117
+ **Text mode:** `TEXT_MODE` was already resolved in the "Text mode" section above (before
118
+ Step 0). Continue honoring it for every `AskUserQuestion` call in this and later steps.
119
+
120
+ ---
121
+
122
+ ## 2. Minimal identity capture (D-07)
123
+
124
+ Capture ONLY the five identity fields below. Persist them as a stub in
125
+ `.planning/config.json` under a top-level `"project"` key, merged with any existing
126
+ config. These fields are used by the gated GitLab provisioning path (Plan 02) for
127
+ `group/repo-slug` and `visibility`; they do NOT produce a `CLAUDE.md` client-context
128
+ block — that is Phase 9 (CTX-01..03) territory. Do NOT write a coding-standards,
129
+ architecture, or DoD block here; executor and Phase 9 must not double-capture these.
130
+
131
+ Ask the user with `AskUserQuestion` (or plain-text prompts in TEXT_MODE):
132
+
133
+ ```text
134
+ 1. Project name (human-readable, e.g. "Acme Dashboard")
135
+ 2. Project slug (kebab-case, e.g. "acme-dashboard")
136
+ 3. Client name (company / team, e.g. "Acme Corp") — type "internal" if none
137
+ 4. Client slug (kebab-case, e.g. "acme") — type "internal" if none
138
+ 5. One-line description (what this project does)
139
+ ```
140
+
141
+ Also ask — used only by the gated GitLab path (D-13, Plan 02):
142
+
143
+ ```text
144
+ 6. Provider (gitlab / github / other / none)
145
+ 7. Visibility (private / public / internal)
146
+ ```
147
+
148
+ Write these to `.planning/config.json`:
149
+
150
+ ```json
151
+ {
152
+ "project": {
153
+ "name": "<value>",
154
+ "slug": "<value>",
155
+ "client_name": "<value>",
156
+ "client_slug": "<value>",
157
+ "description": "<value>",
158
+ "provider": "<value>",
159
+ "visibility": "<value>"
160
+ }
161
+ }
162
+ ```
163
+
164
+ `mkdir -p .planning` before writing if the directory does not exist.
165
+
166
+ **Scope boundary:** this step writes identity only. All coding-standards, architecture
167
+ context, client-engagement CLAUDE.md blocks, and discovery-session scaffolding are out of
168
+ scope here. Phase 9 owns those; /pp-discovery self-generates its own session files (D-03).
169
+
170
+ ---
171
+
172
+ ## 3. Remote wiring — provider-agnostic (non-Pareto / no bindings)
173
+
174
+ **Gate (D-06, D-13):** this step is the active path when
175
+ `pp_run query platform-bindings --pick present 2>/dev/null` does NOT equal `"true"`.
176
+ When bindings ARE present, see the gated GitLab provisioning section below instead.
177
+
178
+ On the generic path:
179
+
180
+ 1. Ask the user for the remote URL (any host — GitHub, GitLab, Gitea, Bitbucket, etc.):
181
+
182
+ ```text
183
+ Remote URL for this repository (e.g. https://github.com/org/repo.git):
184
+ ```
185
+
186
+ 2. Show the exact command and target, then confirm before firing:
187
+
188
+ ```text
189
+ About to run:
190
+ git remote add origin <url>
191
+
192
+ Proceed? [y/N]
193
+ ```
194
+
195
+ If confirmed, run `git remote add origin <url>`. If a remote named `origin` already
196
+ exists, offer to update it with `git remote set-url origin <url>` instead (show the
197
+ same per-action confirm pattern).
198
+
199
+ 3. Offer a first push. Show the exact command and target before firing:
200
+
201
+ ```text
202
+ About to run:
203
+ git push -u origin <current-branch>
204
+
205
+ Proceed? [y/N]
206
+ ```
207
+
208
+ If confirmed, run `git push -u origin <current-branch>`. If the user declines, skip
209
+ the push and note that they can run `git push -u origin <branch>` later.
210
+
211
+ **Security (T-08-01, T-08-02):** every outward action (`git remote add`, `git push`)
212
+ gets its own per-action confirmation immediately before it fires — nothing batched. This
213
+ step makes **ZERO `gh`/`glab`/provider-API calls**. The push target is the user-supplied
214
+ URL only.
215
+
216
+ ### Gated GitLab provisioning (Pareto projects — bindings present)
217
+
218
+ This path runs **instead of** the generic remote-wiring path above when
219
+ `pp_run query platform-bindings --pick present 2>/dev/null` equals `"true"`. Implements
220
+ D-05 (per-action confirmation), D-09 (existence probe → adopt-if-exists), D-10 (private
221
+ by default), D-11 (auth verify-only), D-12 (adopt-if-exists reconciliation), D-13
222
+ (bindings gate). Covers INIT-02 and the cross-cutting SAFE-01 invariant.
223
+
224
+ **Security invariants — landmine guards:**
225
+ - Always use the group-qualified `$GROUP/$SLUG` path — never a bare slug (T-08-04, D-09).
226
+ A bare slug creates under the user's personal namespace, not the client group.
227
+ - Always pass `--private` explicitly — never inherit `glab`'s default `--internal`
228
+ visibility (T-08-05, D-10). The `--internal` flag is what makes this dangerous; the
229
+ locked recipe must carry `--private` and must NOT emit `--internal`.
230
+ - Always confirm immediately before each outward action — nothing batched (T-08-06, D-05).
231
+ - Verify-only auth via `glab auth status` — never run `glab auth login`, never read
232
+ or write the user's token (T-08-08, D-11). Bindings hold only non-secret routing data.
233
+ - Always reconcile create with a `glab repo view` probe — a non-zero create exit does
234
+ NOT mean nothing happened (T-08-09, D-09).
235
+
236
+ #### G-1. Config reads from the bindings reader (Pattern C, D-09, D-13)
237
+
238
+ All values come from the Phase 7 bindings reader. **Never hardcoded.**
239
+
240
+ ```bash
241
+ GROUP=$(pp_run query platform-bindings --pick gitlab.group 2>/dev/null)
242
+ SLUG=$(pp_run query platform-bindings --pick gitlab.repo_slug 2>/dev/null)
243
+ VIS_RAW=$(pp_run query platform-bindings --pick gitlab.visibility 2>/dev/null)
244
+ HOST=$(pp_run query platform-bindings --pick gitlab.host 2>/dev/null)
245
+ # D-10: hard-default to private when field is absent or empty
246
+ VIS="${VIS_RAW:-private}"
247
+ DESC=$(pp_run query config-get project.description 2>/dev/null || echo "")
248
+ ```
249
+
250
+ The `gitlab.*` namespace is provisional — the bindings reader is lenient on unknown fields
251
+ (it only hard-validates `schemaVersion`, `platform.repo`, and `platform.tag`). Treat any
252
+ absent `gitlab.*` field gracefully: fall back to safe defaults, never fail hard on a
253
+ missing key. `HOST` defaults to `gitlab.com` in remote URLs when empty.
254
+
255
+ #### G-2. Auth verify-only (D-11)
256
+
257
+ `/pp-init` **never establishes auth and never reads or writes the user's token.**
258
+ Secrets stay in the environment or OS keyring. Only verify that a session exists:
259
+
260
+ ```bash
261
+ glab auth status >/dev/null 2>&1 || {
262
+ echo "Not authenticated to GitLab. Run: glab auth login"
263
+ exit 1
264
+ }
265
+ ```
266
+
267
+ If this check fails, stop immediately. Do not proceed to probe or create.
268
+ Never run `glab auth login` from within `/pp-init`.
269
+
270
+ #### G-3. Existence probe → adopt-if-exists (D-09, D-12)
271
+
272
+ Pre-check with a read-only probe before any create attempt. `glab repo create` is **not
273
+ idempotent** — re-running it against an existing project errors but may still produce side
274
+ effects (the GitLab project is created before the remote-add step that then fails). The
275
+ probe avoids this landmine.
276
+
277
+ This is one self-contained block: it sets `PROJECT_EXISTS` and, on the exists path,
278
+ adopts (links the remote). It NEVER creates — create lives in its own G-4 block, gated
279
+ on `PROJECT_EXISTS=false`, so the create invocation can never fall inside the exists
280
+ branch.
281
+
282
+ ```bash
283
+ if glab repo view "$GROUP/$SLUG" -F json >/dev/null 2>&1; then
284
+ # Project already exists — ADOPT: link remote only, skip create (D-12)
285
+ PROJECT_EXISTS=true
286
+ echo "GitLab project $GROUP/$SLUG already exists — adopting (linking remote, skipping create)."
287
+ # IN-01: match the remote scheme glab itself uses on the create path — honor the
288
+ # user's `glab config get git_protocol` (https default) instead of hardcoding SSH,
289
+ # so adopt and create produce a consistent origin URL.
290
+ GIT_PROTO=$(glab config get git_protocol 2>/dev/null || echo https)
291
+ if [ "$GIT_PROTO" = "ssh" ]; then
292
+ ADOPT_URL="git@${HOST:-gitlab.com}:$GROUP/$SLUG.git"
293
+ else
294
+ ADOPT_URL="https://${HOST:-gitlab.com}/$GROUP/$SLUG.git"
295
+ fi
296
+ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$ADOPT_URL"
297
+ echo "Remote 'origin' → $ADOPT_URL"
298
+ else
299
+ # Project not found — defer to G-4 for confirm-then-create
300
+ PROJECT_EXISTS=false
301
+ echo "GitLab project $GROUP/$SLUG not found — will confirm before create."
302
+ fi
303
+ ```
304
+
305
+ If the project exists (`PROJECT_EXISTS=true`), the remote is already linked above
306
+ (skipping create entirely) — proceed straight to G-5 reconciliation. The adopt path does
307
+ not show a create confirmation — it only links the remote, which is a local-only
308
+ operation and can be undone with `git remote remove origin`.
309
+
310
+ #### G-4. Per-action confirmation, then create (D-05, D-09, D-10, SAFE-01)
311
+
312
+ **Only run this step when `PROJECT_EXISTS=false`** (the existence probe in G-3 found no
313
+ project). If `PROJECT_EXISTS=true`, skip G-4 entirely — the project was already adopted —
314
+ and go to G-5. When creating, **show the exact command and target before firing**. The
315
+ user must confirm explicitly. Nothing is batched. (D-05)
316
+
317
+ Present to the user (via `AskUserQuestion` or TEXT_MODE numbered list):
318
+
319
+ ```text
320
+ About to create GitLab project:
321
+ Command: glab repo create "$GROUP/$SLUG"
322
+ Group path: $GROUP/$SLUG
323
+ Visibility: private (enforced --private; never the glab default --internal)
324
+ Requested: $VIS (from bindings gitlab.visibility — see note below)
325
+ Default branch: main
326
+ Skip git init: yes (local repo already initialized — --skipGitInit)
327
+ Remote name: origin
328
+
329
+ Proceed? [y/N]
330
+ ```
331
+
332
+ `$VIS` (from G-1) is surfaced in the confirmation so the binding's requested visibility is
333
+ never silently ignored (WR-01). pp-core always **creates private** regardless; a
334
+ non-private binding must be applied as a deliberate, separate post-create change.
335
+
336
+ Only after the user confirms — and only with `PROJECT_EXISTS=false` — run the locked
337
+ create recipe (D-09, D-10). This is a standalone block: it carries no `if`/`else`/`fi`, so
338
+ it can only ever run as the deliberate, confirmed not-exists path:
339
+
340
+ ```bash
341
+ glab repo create "$GROUP/$SLUG" \
342
+ --description "$DESC" \
343
+ --private \
344
+ --defaultBranch main \
345
+ --skipGitInit \
346
+ --remoteName origin
347
+ ```
348
+
349
+ **Visibility hard-default (D-10):** the create invocation always carries `--private`.
350
+ Never omit it and let `glab` silently apply `--internal`. `VIS` from bindings is read in
351
+ G-1 and **surfaced in the G-4 confirmation** (WR-01) so a `public`/`internal` request is
352
+ visible to the user — but pp-core still creates the project **private**. Applying a
353
+ non-private visibility is a deliberate, separate post-create action (e.g. `glab repo edit
354
+ --visibility …`) the operator performs knowingly; `/pp-init` never auto-relaxes visibility.
355
+
356
+ #### G-5. Post-create reconciliation (D-09)
357
+
358
+ After the create (or after the adopt path), re-probe existence. A non-zero exit from
359
+ `glab repo create` does **not** mean the project was not created — `glab` exits non-zero
360
+ (code 3) when the GitLab project was created but the subsequent `git remote add` step
361
+ then failed (e.g., because `origin` already existed). The `repo view` probe is the source
362
+ of truth, not the exit code (research §1 "Remote-add caveat — VERIFIED"):
363
+
364
+ ```bash
365
+ if glab repo view "$GROUP/$SLUG" -F json >/dev/null 2>&1; then
366
+ echo "Reconciled: GitLab project $GROUP/$SLUG confirmed."
367
+ else
368
+ echo "WARN: glab repo view $GROUP/$SLUG returned non-zero — verify the project was created before continuing."
369
+ fi
370
+ ```
371
+
372
+ If reconciliation warns, do not auto-retry. Prompt the user to check GitLab directly
373
+ before proceeding to Step 4 (handoff routing).
374
+
375
+ ### Gated client-context capture (Pareto projects — bindings present)
376
+
377
+ This step runs **only when** `pp_run query platform-bindings --pick present 2>/dev/null` equals
378
+ `"true"`. Non-Pareto projects are **fully inert**: no prompts fire and no block is written (D-04,
379
+ success criterion #4). All capture is **offline-only** — no Obsidian, no MCP, no network (D-05 /
380
+ T-09-05).
381
+
382
+ ```bash
383
+ if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" = "true" ]; then
384
+ # ── D-05: edit-on-re-run — read existing block first ──────────────────────────
385
+ EXISTING=$(pp_run query generate-client-context --read 2>/dev/null || echo '{"present":false}')
386
+ 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');}")
387
+
388
+ if [ "$EXISTING_PRESENT" = "true" ]; then
389
+ echo ""
390
+ echo "Existing client-context block found. Press Enter to keep each value, or type to replace it."
391
+ 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)
392
+ 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)
393
+ 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)
394
+ 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)
395
+ 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)
396
+ 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)
397
+ fi
398
+ fi
399
+ ```
400
+
401
+ **Field capture — one field at a time (D-01, D-02):** Prompt the user for each of the five fields
402
+ sequentially. All five are required — **re-prompt on empty input until a non-empty value is given**
403
+ (no skip, no catch-all). Multi-line values are accepted: instruct the user to type or paste, then
404
+ enter a blank line (or a sentinel line such as `EOF`) to signal end-of-field. Do not call any
405
+ network resource during capture.
406
+
407
+ Capture these fields, in this order:
408
+
409
+ 1. **Client name** — the human-readable client or project name (e.g. "Acme Corp").
410
+ - On re-run: show the current value (`$EXISTING_CLIENT_NAME`) and let the user keep or replace.
411
+ - After capturing, derive a URL-safe slug via `pp_run query generate-slug` on the name; present
412
+ the suggested slug and let the user confirm or override. Store as `CLIENT_SLUG`.
413
+
414
+ 2. **Coding standards** — the conventions and rules the Build agent must follow (e.g. language,
415
+ style guide, patterns to use/avoid). Multi-line OK.
416
+ - On re-run: show `$EXISTING_CODING`.
417
+
418
+ 3. **Architecture patterns** — the structural patterns and constraints the Build agent must respect
419
+ (e.g. layering, module boundaries, framework idioms). Multi-line OK.
420
+ - On re-run: show `$EXISTING_ARCH`.
421
+
422
+ 4. **UI / brand conventions** — visual and component standards the Build agent must apply (e.g.
423
+ design system, color tokens, typography rules). Multi-line OK.
424
+ - On re-run: show `$EXISTING_UI`.
425
+
426
+ 5. **Definition of done** — the quality gates that must pass before any task is marked complete
427
+ (e.g. tests green, lint clean, accessibility checked). Multi-line OK.
428
+ - On re-run: show `$EXISTING_DOD`.
429
+
430
+ **Write block** — after all five fields are captured, write them to a temp file and invoke the
431
+ managed-block writer:
432
+
433
+ ```bash
434
+ if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" = "true" ]; then
435
+ # Write collected values to a temp JSON file using node + JSON.stringify so that
436
+ # multi-line values, embedded quotes, backslashes, and $(...) sequences are encoded
437
+ # correctly (CR-02: an unquoted heredoc would produce invalid JSON for any multi-line
438
+ # field, breaking cmdGenerateClientContext with "Failed to parse JSON input").
439
+ TMP_CTX=$(mktemp /tmp/pp-client-ctx-XXXXXX.json)
440
+ CLIENT_NAME="$CLIENT_NAME" \
441
+ CLIENT_SLUG="$CLIENT_SLUG" \
442
+ CODING_STANDARDS="$CODING_STANDARDS" \
443
+ ARCHITECTURE_PATTERNS="$ARCHITECTURE_PATTERNS" \
444
+ UI_BRAND_CONVENTIONS="$UI_BRAND_CONVENTIONS" \
445
+ DEFINITION_OF_DONE="$DEFINITION_OF_DONE" \
446
+ node -e "
447
+ const e = process.env;
448
+ const fs = require('fs');
449
+ fs.writeFileSync(process.argv[1], JSON.stringify({
450
+ client_name: e.CLIENT_NAME,
451
+ client_slug: e.CLIENT_SLUG,
452
+ coding_standards: e.CODING_STANDARDS,
453
+ architecture_patterns: e.ARCHITECTURE_PATTERNS,
454
+ ui_brand_conventions: e.UI_BRAND_CONVENTIONS,
455
+ definition_of_done: e.DEFINITION_OF_DONE
456
+ }));
457
+ " "$TMP_CTX"
458
+
459
+ # Invoke the Plan 09-01 writer — routes through updateSection, marker-bounded, idempotent
460
+ pp_run query generate-client-context --from-json "$TMP_CTX"
461
+ rm -f "$TMP_CTX"
462
+ echo "Client context written to CLAUDE.md (<!-- PP:client-context-start --> block)."
463
+ fi
464
+ ```
465
+
466
+ This writes a `## Client Context & Patterns` block bounded by
467
+ `<!-- PP:client-context-start -->` … `<!-- PP:client-context-end -->` markers into the local
468
+ CLAUDE.md (config-driven path, default `./.claude/CLAUDE.md`). The write is idempotent —
469
+ re-running `/pp-init` replaces the bounded block, never duplicates it. All values stay local;
470
+ no network or external service is involved.
471
+
472
+ ---
473
+
474
+ ## 4. Handoff routing (D-04)
475
+
476
+ After git init, metadata, and remote wiring are complete, ask the user which workflow to
477
+ continue with. `/pp-init` does NOT scaffold discovery session files itself — `/pp-discovery`
478
+ self-generates them (D-03). Do not silently auto-chain to either option.
479
+
480
+ Determine the smart default from signals captured in Steps 0–2:
481
+ - `platform-bindings --pick present` == `true`, OR `provider` == `"gitlab"`, OR
482
+ `client_slug` is not `"internal"` → pre-select **`/pp-discovery`** (Pareto engagement
483
+ or client work).
484
+ - Otherwise (internal project, known stack, no bindings) → pre-select
485
+ **`/pp-new-project`** (freeform planning, requirements, roadmap).
486
+
487
+ Present to the user with `AskUserQuestion`:
488
+
489
+ ```text
490
+ AskUserQuestion([
491
+ {
492
+ header: "Next step",
493
+ question: "Where would you like to go next?",
494
+ multiSelect: false,
495
+ options: [
496
+ {
497
+ label: "/pp-discovery (Recommended for client/Pareto projects)",
498
+ description: "Run the 13-session Discovery Sprint — self-generates all session files"
499
+ },
500
+ {
501
+ label: "/pp-new-project",
502
+ description: "Gather requirements and generate a phased roadmap for a known-stack project"
503
+ },
504
+ {
505
+ label: "Stop here",
506
+ description: "I will continue manually — the repo is initialized"
507
+ }
508
+ ]
509
+ }
510
+ ])
511
+ ```
512
+
513
+ Pre-select the option matching the smart default computed above (indicate it with
514
+ `(Recommended)` in the label, as shown). When the user confirms or overrides, invoke the
515
+ chosen command or stop cleanly.
516
+
517
+ ---
518
+
519
+ ## 5. Commit
520
+
521
+ Commit the `.planning/config.json` identity stub created in Step 2:
522
+
523
+ ```bash
524
+ pp_run query commit "docs: initialize project" --files .planning/config.json
525
+ ```
526
+
527
+ If `.planning/config.json` is the only new file, stage only that file. If other planning
528
+ artifacts were created (e.g. `.planning/` directory structure), include them.
529
+
530
+ </process>
@@ -52,12 +52,65 @@ The document should describe what you want to build.
52
52
 
53
53
  <process>
54
54
 
55
+ ## 0. Platform-version preflight (Pareto satellite repos)
56
+
57
+ **Runs before Setup. Inert for non-Pareto projects** — gated entirely on
58
+ `.platform-bindings.json` existing, so generic projects skip it with no effect.
59
+
60
+ A repo scaffolded by `/pareto-new-app` pins the Pareto platform at a specific
61
+ `vX.Y.Z` tag (module `?ref=` pins, the `.gitlab-ci.yml` include `ref:`, and the
62
+ canonical `.platform-bindings.json` → `platform.tag`). Project initiation is the
63
+ moment to confirm the app is on the **latest** platform before any building
64
+ starts — bumping after a milestone is built is far more expensive.
65
+
66
+ ```bash
67
+ _PP_SHIM_NAME="pp-tools.cjs"; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; pp_run() { "$PP_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
68
+ # Resolve pp_run before Step 1 — reader required for platform-bindings preflight (BIND-04)
69
+
70
+ # Gate on presence via reader — absent file → {present:false} exit 0 (inert, step skipped, no jq)
71
+ if [ "$(pp_run query platform-bindings --pick present 2>/dev/null)" = "true" ]; then
72
+ PINNED=$(pp_run query platform-bindings --pick platform.tag)
73
+ REPO=$(pp_run query platform-bindings --pick platform.repo)
74
+ HOST_FROM_BINDINGS=$(pp_run query platform-bindings --pick platform.host 2>/dev/null)
75
+ PLATFORM_HOST="${HOST_FROM_BINDINGS:-${HOST:-gitlab.com}}"
76
+ PLATFORM_REPO_URL="https://${PLATFORM_HOST}/${REPO}.git"
77
+ # Resolve latest STABLE tag from remote — never hard-block if offline (empty LATEST → skip drift check)
78
+ # Keep --refs to strip ^{} annotated-tag rows; --sort=-v:refname sorts server-side
79
+ # Stable filter fixes prerelease mis-ordering: git ranks v1.4.0-rc.1 after v1.4.0 (wrong by semver)
80
+ # semver-compare.cjs is the single blessed source for comparison — do NOT reimplement
81
+ REMOTE_TAGS=$(git ls-remote --tags --refs --sort=-v:refname "$PLATFORM_REPO_URL" 'v*' 2>/dev/null | sed 's#.*/##')
82
+ LATEST=$(printf '%s\n' "$REMOTE_TAGS" | node -e "
83
+ const sc = require('${PP_TOOLS%/*}/lib/semver-compare.cjs');
84
+ const lines = require('fs').readFileSync(0, 'utf-8').split('\n').map(s=>s.trim()).filter(Boolean);
85
+ const stable = lines.filter(sc.isStableTripletSemver).sort(sc.compareSemverCore);
86
+ process.stdout.write((stable[stable.length-1] || '') + '\n');
87
+ " 2>/dev/null)
88
+ echo "platform preflight: pinned=${PINNED:-<none>} latest=${LATEST:-<unresolved>}"
89
+ if [ -n "$LATEST" ] && [ -n "$PINNED" ] && [ "$PINNED" != "$LATEST" ]; then
90
+ echo "PP_PLATFORM_DRIFT=1"
91
+ fi
92
+ fi
93
+ ```
94
+
95
+ **If the block printed `PP_PLATFORM_DRIFT=1`:** the app is pinned behind the
96
+ latest platform tag. STOP before Setup and use `AskUserQuestion` to let the user
97
+ choose:
98
+ - **Bump first (recommended)** — pause initiation; bump the module `?ref=` pins,
99
+ the `.gitlab-ci.yml` include `ref:`, and `.platform-bindings.json` `platform.tag`
100
+ to `${LATEST}` (review `CHANGELOG.md` between the two tags for breaking changes),
101
+ then re-run `/pp-new-project`.
102
+ - **Proceed on the older pin** — continue knowingly; record the deferral in
103
+ `PROJECT.md` so it's a conscious decision, not silent drift.
104
+
105
+ If `LATEST` was unresolved (offline / no network), note it and proceed — never
106
+ hard-block initiation on an unreachable remote. For non-Pareto repos the block is
107
+ a no-op and this step is skipped entirely.
108
+
55
109
  ## 1. Setup
56
110
 
57
111
  **MANDATORY FIRST STEP — Execute these checks before ANY user interaction:**
58
112
 
59
113
  ```bash
60
- _PP_SHIM_NAME="pp-tools.cjs"; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; pp_run() { "$PP_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; pp_run() { node "$PP_TOOLS" "$@"; }; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
61
114
  INIT=$(pp_run query init.new-project)
62
115
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
63
116
  AGENT_SKILLS_RESEARCHER=$(pp_run query agent-skills pp-project-researcher)