@pareto4/pp-core 1.13.0 → 1.18.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.
- package/.claude-plugin/plugin.json +3 -3
- package/README.md +31 -12
- package/agents/pp-code-fixer.md +0 -11
- package/agents/pp-code-reviewer.md +1 -77
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +12 -29
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +34 -20
- package/agents/pp-vault-reviewer.md +100 -23
- package/agents/pp-verifier.md +1 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-manage.md +1 -1
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-promote.md +7 -8
- package/commands/pp-vault-review.md +6 -5
- package/commands/pp-verify-work.md +1 -2
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update.js +0 -0
- package/hooks/dist/pp-cursor-session-start.js +1 -1
- package/hooks/dist/pp-statusline.js +1 -1
- package/hooks/dist/pp-update-banner.js +0 -0
- package/hooks/pp-check-update.js +0 -0
- package/hooks/pp-cursor-session-start.js +1 -1
- package/hooks/pp-statusline.js +1 -1
- package/hooks/pp-update-banner.js +0 -0
- package/package.json +10 -18
- package/pareto-skills/_shared/create-project-board.sh +0 -0
- package/pareto-skills/pp-project-setup/SKILL.md +1 -1
- package/pp-core/bin/check-latest-version.cjs +0 -0
- package/pp-core/bin/detect-build-commands.sh +2 -2
- package/pp-core/bin/lib/capability-activation.cjs +83 -9
- package/pp-core/bin/lib/capability-registry.cjs +201 -229
- package/pp-core/bin/lib/capability-state.cjs +93 -1
- package/pp-core/bin/lib/capability-writer.cjs +3 -3
- package/pp-core/bin/lib/check-command-router.cjs +1906 -1514
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/command-aliases.cjs +0 -35
- package/pp-core/bin/lib/commands.cjs +9 -4
- package/pp-core/bin/lib/compose-bindings.cjs +251 -88
- package/pp-core/bin/lib/config-loader.cjs +7 -103
- package/pp-core/bin/lib/config-schema.cjs +1 -1
- package/pp-core/bin/lib/config.cjs +151 -62
- package/pp-core/bin/lib/decisions.cjs +4 -4
- package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
- package/pp-core/bin/lib/frontmatter.cjs +1 -38
- package/pp-core/bin/lib/git-base-branch.cjs +11 -12
- package/pp-core/bin/lib/git-provider.cjs +9 -10
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +620 -738
- package/pp-core/bin/lib/init-interactive.cjs +62 -4
- package/pp-core/bin/lib/init.cjs +24 -51
- package/pp-core/bin/lib/install-profiles.cjs +1 -1
- package/pp-core/bin/lib/io.cjs +4 -2
- package/pp-core/bin/lib/loop-resolver.cjs +165 -129
- package/pp-core/bin/lib/org-profile.cjs +402 -161
- package/pp-core/bin/lib/package-identity.cjs +3 -3
- package/pp-core/bin/lib/phase-command-router.cjs +1 -29
- package/pp-core/bin/lib/phase-scope.cjs +1 -1
- package/pp-core/bin/lib/phase.cjs +11 -253
- package/pp-core/bin/lib/platform-bindings.cjs +40 -19
- package/pp-core/bin/lib/profile-output.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
- package/pp-core/bin/lib/state.cjs +11 -85
- package/pp-core/bin/lib/template.cjs +1 -1
- package/pp-core/bin/lib/vault-command-router.cjs +1682 -477
- package/pp-core/bin/lib/vault-coordinates.cjs +16 -7
- package/pp-core/bin/lib/vault-index.cjs +656 -0
- package/pp-core/bin/lib/vault-seal.cjs +1851 -0
- package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
- package/pp-core/bin/lib/vault-standards.cjs +1504 -0
- package/pp-core/bin/lib/vault-trace.cjs +155 -2778
- package/pp-core/bin/lib/vault-update.cjs +10 -59
- package/pp-core/bin/lib/vault.cjs +2570 -950
- package/pp-core/bin/lib/verify.cjs +0 -11
- package/pp-core/bin/portable-timeout.sh +1 -1
- package/pp-core/bin/pp-tools.cjs +157 -54
- package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
- package/pp-core/bin/shared/config-schema.manifest.json +0 -3
- package/pp-core/bin/shared/model-catalog.json +2 -2
- package/pp-core/bin/verify-reapply-patches.cjs +0 -0
- package/pp-core/org-profile.json +26 -0
- package/pp-core/references/autonomous-best-effort.md +8 -7
- package/pp-core/references/loop-hook-dispatch.md +0 -25
- package/pp-core/references/planner-gap-closure.md +0 -1
- package/pp-core/references/planner-tdd.md +0 -1
- package/pp-core/references/planner-vault-standards.md +311 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +56 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
- package/pp-core/workflows/add-tests.md +55 -4
- package/pp-core/workflows/audit-fix.md +63 -2
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +85 -11
- package/pp-core/workflows/check-todos.md +13 -2
- package/pp-core/workflows/code-review-fix.md +59 -3
- package/pp-core/workflows/code-review.md +10 -57
- package/pp-core/workflows/complete-milestone.md +1 -1
- package/pp-core/workflows/debug.md +54 -4
- package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
- package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
- package/pp-core/workflows/discovery-sprint/vault-context.md +207 -0
- package/pp-core/workflows/discovery-sprint.md +7 -3
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
- package/pp-core/workflows/discovery-to-build.md +10 -2
- package/pp-core/workflows/discuss-phase.md +3 -10
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +327 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
- package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +78 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +219 -20
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +147 -0
- package/pp-core/workflows/execute-phase.md +152 -128
- package/pp-core/workflows/execute-plan.md +54 -6
- package/pp-core/workflows/fast.md +53 -4
- package/pp-core/workflows/help/modes/full.md +3 -3
- package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
- package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
- package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
- package/pp-core/workflows/init.md +232 -272
- package/pp-core/workflows/new-project.md +31 -23
- package/pp-core/workflows/plan-phase.md +98 -45
- package/pp-core/workflows/quick.md +117 -3
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +57 -28
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/update.md +3 -3
- package/pp-core/workflows/validate-phase.md +58 -3
- package/pp-core/workflows/vault-review.md +161 -23
- package/pp-core/workflows/vault-write-back.md +73 -110
- package/pp-core/workflows/verify-work.md +12 -14
- package/scripts/changeset/cli.cjs +6 -5
- package/scripts/changeset/lint.cjs +0 -0
- package/scripts/changeset/new.cjs +33 -50
- package/scripts/dev-env.cjs +25 -85
- package/scripts/gen-capability-registry.cjs +120 -13
- package/scripts/lib/allowlist-ratchet.cjs +1 -103
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
- package/scripts/lint-docs-required.cjs +0 -0
- package/scripts/lint-pr-check-project-dir.cjs +1 -3
- package/scripts/lint-test-file-count.allowlist.json +3 -19
- package/scripts/lint-test-file-count.cjs +5 -6
- package/scripts/prompt-injection-scan.sh +19 -17
- package/scripts/release-channel.cjs +0 -0
- package/scripts/sync-manifest-versions.cjs +5 -4
- package/scripts/sync-rulesets.sh +1 -1
- package/scripts/update-size-baseline.cjs +30 -274
- package/scripts/verify-opengsd-sweep.cjs +3 -5
- package/scripts/workflow-size.cjs +9 -61
- package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
- package/pp-core/bin/lib/install-run.cjs +0 -536
- package/pp-core/bin/lib/loop-gates.cjs +0 -213
- package/pp-core/bin/lib/phase-manifest.cjs +0 -527
- package/pp-core/bin/lib/phase-tier.cjs +0 -480
- package/pp-core/bin/lib/plan-graph.cjs +0 -368
- package/pp-core/bin/lib/practices-review-check.cjs +0 -938
- package/pp-core/bin/lib/vault-audit.cjs +0 -2070
- package/pp-core/bin/lib/vault-practices.cjs +0 -1990
- package/pp-core/bin/lib/vault-review.cjs +0 -2359
- package/pp-core/bin/lib/vault-write-back.cjs +0 -877
- package/pp-core/references/executor-vault-trace.md +0 -47
- package/pp-core/references/planner-plan-graph.md +0 -66
- package/pp-core/references/planner-vault-practices.md +0 -112
- package/pp-core/templates/practices/README.md +0 -251
- package/pp-core/templates/practices/error-handling.md +0 -18
- package/pp-core/templates/practices/minimal-comments.md +0 -14
- package/pp-core/templates/practices/naming-convention.md +0 -18
- package/pp-core/templates/practices/practice-template.md +0 -23
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
- package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
- package/scripts/lib/size-budget.cjs +0 -311
- package/scripts/lint-neutrality-names.cjs +0 -694
- package/scripts/lint-org-coordinates.allowlist.json +0 -35
- package/scripts/lint-org-coordinates.cjs +0 -426
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Generic `check.query` gate enforcer
|
|
2
|
+
|
|
3
|
+
**Naming note (read this first).** This file lives under `pp-core/workflows/execute-phase/steps/`
|
|
4
|
+
because that directory is the established home for lazily-Read workflow step fragments —
|
|
5
|
+
`plan-phase.md` already Reads two of its siblings across a workflow boundary, and creating a new
|
|
6
|
+
top-level directory has unverified installer implications. The directory name says *execute-phase*,
|
|
7
|
+
but this file serves **ship**, **plan** and **execute** call sites alike. Do not read the path as a
|
|
8
|
+
scope restriction.
|
|
9
|
+
|
|
10
|
+
This is the **single** generic `check.query` gate enforcer. Before it existed the same enforcement
|
|
11
|
+
loop was written twice — `ship.md` step 6 and `execute-phase.md` step 5.75 — against two contracts
|
|
12
|
+
that disagreed about which envelope field carries the verdict. GATE-02 collapses them into this one
|
|
13
|
+
implementation; **D-VH-7** fixes which contract survives: the two-step one from step 5.75, reading
|
|
14
|
+
the boolean `block`, not `status == "block"`.
|
|
15
|
+
|
|
16
|
+
This file is **Read lazily** by its callers. It is never eagerly imported, so it costs its callers
|
|
17
|
+
nothing until the moment a gate actually has to be enforced.
|
|
18
|
+
|
|
19
|
+
## Parameters — set these before Reading this file
|
|
20
|
+
|
|
21
|
+
| Variable | Required | Meaning |
|
|
22
|
+
|----------|----------|---------|
|
|
23
|
+
| `GATE_POINT` | yes | Loop point label used in operator messages, e.g. `ship:pre`, `execute:pre`, `execute:wave:post`. Message text only; it never changes control flow. |
|
|
24
|
+
| `GATE_HOOKS_JSON` | yes (hook mode) | The already-rendered hooks JSON from the caller's own `pp_run loop render-hooks <point> --raw` call. This file never renders hooks itself. **`GATE_HOOKS_JSON=$X` — in a caller's prose and in the Caller contract below — means the JSON on the `X=` line that caller's render fence PRINTED** (16 review iteration 2 CR-01, D-99): the hooks are read in-context, and the agent's Bash tool starts a fresh shell for every call, so a payload a fence only assigned is invisible, and the guard below would skip a blocking gate silently. Every render fence that feeds this enforcer therefore prints `X={"activeHooks":[…]}` (without the large `rendered` text), and a test fails when one does not. |
|
|
25
|
+
| `GATE_POSITIONAL_ARG` | no | When non-empty, passed to `pp_run check` as a single positional argument after the query name. This is how the wave gate passes the phase number. |
|
|
26
|
+
| `GATE_POSITIONAL_ARG_2` | no | When non-empty, passed to `pp_run check` as a **second** positional argument, after `GATE_POSITIONAL_ARG` and in that order. The `plan:post` caller in `plan-phase.md` §13e uses it to pass the requirement-ID list after the phase directory, which is what `gap-analysis.plan-post` has always received in its fourth argument slot. Setting this while leaving `GATE_POSITIONAL_ARG` empty is a **caller error**: the enforcer passes them positionally in order, so an absent first argument silently shifts this one into the first slot. |
|
|
27
|
+
| `GATE_WS` | no | The active workstream name (the value of `--ws`), empty in flat mode. When non-empty the enforcer appends `--ws "$GATE_WS"` to the check command, because a phase **number** positional cannot be resolved without it on a partitioned project: the locator falls back to the flat `.planning/phases`, which does not exist there, and the handler reports the phase as unresolvable. Handlers are workstream-aware; historically only the call sites were not, and an omitted `--ws` here turned a `blocking: true` gate into a hard halt on every workstream project. Callers passing a phase **directory** in `GATE_POSITIONAL_ARG` do not need it — the path already carries the partition — but setting it is harmless and correct. |
|
|
28
|
+
| `GATE_STAGE` | no | The lifecycle stage the dispatching point represents — `plan` or `execute`. When non-empty the enforcer appends `--stage "$GATE_STAGE"` to the check command. A query that does not understand the flag ignores it, because the flag pair is appended **after** both positionals and therefore never occupies a positional slot. `plan:post` sets it to `plan`, so the trace gate validates that point's own artifacts — the PLAN.md files — and not the SUMMARY.md files of a previous execution. `execute:post` deliberately leaves it **unset**, so that point validates both stages, which is the documented invariant: `plan:post` sees PLAN.md files only, `execute:post` sees both and the PLAN.md side is already known good. |
|
|
29
|
+
| `GATE_UNPAIRED` | no | Whether a plan artifact carrying no executed counterpart is a defect — `strict` or `tolerant`. When non-empty the enforcer appends `--unpaired "$GATE_UNPAIRED"` to the check command; `vault.trace` reads it (04-07, D-10) and every other query ignores an unrecognised named flag, because the pair is appended **after** both positionals and therefore never occupies a positional slot. **The default when the variable is absent is `tolerant`, and that default is deliberate**: two of the four `vault.trace` call sites are mid-lifecycle — `plan:post` runs when no plan has been executed yet, and `execute:wave:post` runs while later waves are still pending — so at those points an unpaired plan is the normal state of a correct run, not a fault. A `strict` default would turn a forgotten token at either of them into a continuous halt on every project that has the capability on. The two points that DO set `strict` are `ship:pre` and `verify:post`, which is the same claim from the other side: those are the two points at which every plan must already have an executed counterpart, so an unpaired plan there is a real defect. `verify:post` qualifies that with the run mode — it sets `strict` only when the host's `WAVE_FILTER` is unset, because a `--wave N` run is the third mid-lifecycle case (04-12, CR-04). |
|
|
30
|
+
| `GATE_DIRECT_QUERY` | no | Direct-query mode (see below) for build entry points that declare no loop-host frontmatter. Only consulted when `GATE_HOOKS_JSON` is empty. |
|
|
31
|
+
|
|
32
|
+
`GATE_POSITIONAL_ARG` may also name one `*-PLAN.md` file: `check vault.trace` then judges that plan's
|
|
33
|
+
records alone (16-05, D-38), and every other query refuses a file. `execute-plan.md`'s `plan:post`
|
|
34
|
+
row below is the caller that passes one.
|
|
35
|
+
|
|
36
|
+
## Producer audit — every declared `check.query` in the repo
|
|
37
|
+
|
|
38
|
+
Enumerated from `capabilities/*/capability.json` (`grep -rho '"query": *"[^"]*"' capabilities/*/capability.json | sort -u`)
|
|
39
|
+
and traced to its handler branch in `src/check-command-router.cts`. The router normalises dots to
|
|
40
|
+
hyphens, so `verify.schema-drift` routes to the `verify-schema-drift` branch.
|
|
41
|
+
|
|
42
|
+
**D-08-30 — the citations stay line numbers, and that is a decision rather than an inheritance.**
|
|
43
|
+
Every row below rots on ANY line change to the router, and this phase watched it happen twice inside
|
|
44
|
+
a single plan (08-02) and three more times across the wave. Two alternatives were weighed: citing the
|
|
45
|
+
emitting *expressions* instead (unstable in a different way — the same literal `block: true,` appears
|
|
46
|
+
five times in `cmdVaultTrace` alone, so a citation could not name one), and dropping the column
|
|
47
|
+
(which is what the rows were BEFORE 04-16, and is exactly how one of them came to claim a handler
|
|
48
|
+
emitted a constant `false` for three plans after it stopped doing so). Line numbers are kept because
|
|
49
|
+
the probe at `tests/vault-trace.integration.test.cjs:9235,9258` (`handlerRange`, `emitsBlock`) is TWO-DIRECTIONAL: a rotted citation is a
|
|
50
|
+
loud red on the next run, never a silent lie, and the completeness half also forbids a row that names
|
|
51
|
+
a convenient subset. The rot is real; it is now the cheapest kind — a mechanically re-derivable one.
|
|
52
|
+
Re-derive with the probe's own `handlerRange`/`emitsBlock` predicates; never by arithmetic on the
|
|
53
|
+
old numbers, which is how 05-13 shifted three rows ~200 lines into a different function.
|
|
54
|
+
|
|
55
|
+
| Query | Handler branch / function | Emits boolean `block` on every path? | Emission site(s) |
|
|
56
|
+
|-------|---------------------------|--------------------------------------|------------------|
|
|
57
|
+
| `gap-analysis.plan-post` | `gap-analysis-plan-post` branch (`cmdGapAnalysisPlanPost`) | yes — always `false` (advisory by design) | `src/check-command-router.cts:1067` |
|
|
58
|
+
| `ship.platform-drift` | `ship-platform-drift` branch (`cmdShipPlatformDrift`) | yes — the only producer that can emit `block: true` from a threshold comparison | `src/check-command-router.cts:1169,1179,1188,1205,1247,1256,1270` |
|
|
59
|
+
| `tdd.review-checkpoint` | `tdd-review-checkpoint` branch | yes — `block = violations > 0`, advisory in the capability | `src/check-command-router.cts:928,1022` |
|
|
60
|
+
| `ui.plan-gate` | `ui-plan-gate` branch (`computeUiPlanGate`) | yes — `block = frontend && !hasUiSpec` | `src/check-command-router.cts:703` (emitted at `:727`) |
|
|
61
|
+
| `ui.safety-gate` | `ui-safety-gate` branch (`computeUiSafetyGate`) | yes — `block = frontend && hasUiFiles && !hasUiSpec` | `src/check-command-router.cts:830` (emitted at `:856`) |
|
|
62
|
+
| `vault.status` | `vault-status` branch (`cmdVaultStatus`) | yes — COMPUTED, not constant: `block` is the `vault.required` policy applied to the shared tier-keyed verdict leaf (`computeVaultVerdict`, GATE-01 / D-VH-4). It is `false` with the policy off (STRICT-01's byte-identical promise), on the inert path (`.platform-bindings.json` absent — that branch runs FIRST by requirement, T-02-28, because `tier !== 'client'` is true of `undefined` and would otherwise block every non-Pareto project), and for a present, fresh, client-tier vault; it is `true` on a required miss, on a fallback-tier load, on a stale client vault whatever its source (D-08-01; the block skips staleness only when `vault.cache_ttl_hours` is 0, D-08-28), and on a client cache fetched from a ref the project no longer configures (D-08-23). The attributed override (`applyVaultOverride`, GATE-07) then gets the last word on every blocking path, and that overwrite is the last emission site cited; and on a passing required-on verdict the envelope now carries a `message` (the success line, D-08-20) — the enforcer's every-run passthrough prints it. | `src/check-command-router.cts:1626,1641,1680,1707,1768` (02-11: CHECKED by the same two-directional probe as the other rows — every cited line lies inside `cmdVaultStatus` and emits a block, and every emission site is cited. This was the one row the probe did NOT cover, so it is the row that rotted: wave 1 wrote the claim before 02-06 computed the block, and 05-13 shifted the citations arithmetically into the manifest-sanitiser, ~200 lines outside the handler) 14-06: + the envelope always carries `reason`, `status` may be `degraded` (index missing / unparseable / empty / unresolvable / unchecked, D-12), the flat index fields follow the override keys, and the healthy message is the one-line standards card (D-15); a degraded index blocks exactly when `vault.required` is on, through the same override (D-13), so it adds no emission site of its own. |
|
|
63
|
+
| `vault.trace` | `vault-trace` branch (`cmdVaultTrace`) — declared by the `vault` capability (`when: vault.enabled`) at **four** points, `plan:post`, `execute:post`, `ship:pre` and `verify:post`, all `blocking: true` / `onError: halt`, and once more at `ship:pre` as a seal gate (`onRecordedArtifacts: true`) that renders with `vault.enabled` off over a phase holding vault records (16-08). It judges each PLAN's `<vault_standards>` block and each SUMMARY's `## Standards Consulted` section through `src/vault-standards.cts` and fails closed on the `standards_*` kinds (`standards_block_absent`, `standards_block_duplicate`, `standards_block_unterminated`, `standards_block_malformed`, `standards_reason_invalid`, `standards_document_duplicate`, `standards_degraded_mixed`, `standards_none_without_reason`, `standards_path_not_resolved`, `standards_pin_mismatch`, `standards_cache_unreadable`, `standards_degraded_untrue`, `standards_consulted_absent`, `standards_consulted_mismatch`, `standards_plan_id_mismatch`, `standards_pair_missing`, `standards_orphan_summary`, `standards_anchor_malformed`). Reads `--stage` (04-03) and `--unpaired` (04-07, D-10); `ship:pre` sets `GATE_UNPAIRED=strict` unconditionally, `verify:post` sets it only on a full-phase run and leaves it unset when **either** partial-execution mode is active — the host's `WAVE_FILTER` set, or `--gaps-only` (04-12, CR-04; 04-18, WR-01) — and the other two leave the tolerant default | yes — `false` before any policy is read (inert), `false` with status `inactive` when `vault.enabled` is off and the phase holds no vault records, `true` once a standards record fails to validate; an agreeing degraded record with `vault.required` off is `false` with a message (D-29), and a recorded override is `false` at the tolerant points and `true` at the strict ones; `true` with status `disabled_with_artifacts` when `vault.enabled` is off and the positional's phase (or plan file) holds engine-written vault records (16-09, SEAL-02, D-21) — the answer the `ship:pre` seal gate dispatches for | `src/check-command-router.cts:2982,2994,2997,3029,3060,3085,3115,3141,3416` (04-16: CHECKED — a test asserts every line cited here lies inside `cmdVaultTrace` and carries a `block` emission, because four plans in this pass moved these numbers and nothing noticed). 16-07 (SEAL-01): at the strict points only, `appendSealProblems` compares the phase's recorded end anchors with `HEAD` through bounded git and adds `uncovered_source_commit` (a source commit after the ends that no record's start-end range covers), `anchor_missing`, `anchor_start_missing`, `anchor_unknown`, `anchor_not_ancestor` and `seal_could_not_check` (all blocking) and the `anchor_backfilled` note; it pushes problems into `checked` and owns no emission site and no status. 16-09 (SEAL-02): + the `disabled_with_artifacts` emission inside the switch-off guard. Standards-only vault: re-derived with the probe's own `handlerRange`/`emitsBlock` after the second record family, the invalid-config refusal and the envelope's mode tag left the handler. |
|
|
64
|
+
| `verify.codebase-drift` | `verify-codebase-drift` branch (`verify.cmdVerifyCodebaseDrift`) | yes — `block = action_required` | `src/verify.cts:2134,2149,2164,2186,2231,2246` |
|
|
65
|
+
| `verify.schema-drift` | `verify-schema-drift` branch (`verify.cmdVerifySchemaDrift`) | yes — `block = !skipped && driftDetected` | `src/verify.cts:2051,2071,2108` |
|
|
66
|
+
|
|
67
|
+
**Footnote — the missing-field rule.** Every producer above emits a boolean `block` on every
|
|
68
|
+
*successful* path today, but that is an observation, not a guarantee: a producer added later, or an
|
|
69
|
+
`error()` early-return on a missing positional argument, can reach a caller with no `block` field at
|
|
70
|
+
all. Therefore the verdict read below (in step 1's fence, printed for step 2) treats a **missing or
|
|
71
|
+
non-boolean `block` as `false` (advisory)**. An
|
|
72
|
+
absent field must never be coerced into a halt — a gate that stops the build because a producer
|
|
73
|
+
forgot a key is a worse failure than the one it was trying to prevent. Producers that reach the
|
|
74
|
+
enforcer through `error()` are *command* errors and are handled by step 1, never by step 2.
|
|
75
|
+
|
|
76
|
+
## Guard — inactive capability, zero work
|
|
77
|
+
|
|
78
|
+
Write this branch first, because it is what makes an inactive capability **structurally inert**:
|
|
79
|
+
|
|
80
|
+
- If `GATE_HOOKS_JSON` is empty, unset, or its `activeHooks` array is missing or empty **and**
|
|
81
|
+
`GATE_DIRECT_QUERY` is unset, **skip silently and return to the caller.** Print nothing.
|
|
82
|
+
|
|
83
|
+
A project whose capability is off never reaches any of the logic below. That is the whole inertness
|
|
84
|
+
promise — not a suppressed message, an unreached code path.
|
|
85
|
+
|
|
86
|
+
## The loop
|
|
87
|
+
|
|
88
|
+
Process each entry of `activeHooks` where `kind == "gate"` **and** a `check.query` is present, in
|
|
89
|
+
array order. Read `activeHooks` in-context; do not pipe it through a shell parser.
|
|
90
|
+
|
|
91
|
+
A caller that handles a specific predicate itself — `ship.md` handles a `security` predicate branch
|
|
92
|
+
above its generic loop — is expected to have handled or skipped that entry **before** Reading this
|
|
93
|
+
file. This enforcer never sees it.
|
|
94
|
+
|
|
95
|
+
For each hook, bind `GATE_QUERY` to `hook.check.query`, `GATE_BLOCKING` to `hook.blocking`, and
|
|
96
|
+
`GATE_ONERROR` to `hook.onError` (absent means `skip`).
|
|
97
|
+
|
|
98
|
+
## Step 1 — did the check COMMAND succeed?
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
_PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
|
102
|
+
GATE_RESULT=$(pp_run check "${GATE_QUERY}" ${GATE_POSITIONAL_ARG:+"${GATE_POSITIONAL_ARG}"} ${GATE_POSITIONAL_ARG_2:+"${GATE_POSITIONAL_ARG_2}"} ${GATE_WS:+--ws} ${GATE_WS:+"${GATE_WS}"} ${GATE_STAGE:+--stage} ${GATE_STAGE:+"${GATE_STAGE}"} ${GATE_UNPAIRED:+--unpaired} ${GATE_UNPAIRED:+"${GATE_UNPAIRED}"} --raw)
|
|
103
|
+
CHECK_EXIT=$?
|
|
104
|
+
if [ "$CHECK_EXIT" -ne 0 ] || [ -z "$GATE_RESULT" ] || ! printf '%s' "$GATE_RESULT" | node -e "const j=JSON.parse(require('fs').readFileSync(0,'utf-8'));if(j===null||typeof j!=='object')process.exit(1)" 2>/dev/null; then
|
|
105
|
+
echo "GATE_CHECK=failed"
|
|
106
|
+
else
|
|
107
|
+
GATE_BLOCK=$(printf '%s' "$GATE_RESULT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String(JSON.parse(d).block===true));}catch{process.stdout.write('true');}")
|
|
108
|
+
GATE_MESSAGE=$(printf '%s' "$GATE_RESULT" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{const m=JSON.parse(d).message;process.stdout.write(typeof m==='string'?m.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u0084\u0086-\u009F]/g,' ').replace(/\r\n|[\r\n\u0085\u2028\u2029]/g,'\n '):'');}catch{process.stdout.write('');}")
|
|
109
|
+
echo "GATE_CHECK=ok"
|
|
110
|
+
echo "GATE_BLOCK=$GATE_BLOCK"
|
|
111
|
+
if [ -n "$GATE_MESSAGE" ]; then printf 'GATE_MESSAGE=%s\n' "$GATE_MESSAGE"; fi
|
|
112
|
+
fi
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Capture `$?` on the line immediately after the assignment — any command in between overwrites it.
|
|
116
|
+
|
|
117
|
+
**This one fence runs the check AND reads its verdict, and it prints everything the prose below
|
|
118
|
+
routes on** (16 review W-1, D-97). It prints exactly one of two shapes:
|
|
119
|
+
|
|
120
|
+
- `GATE_CHECK=failed` — a **command** error (see below). Nothing else: no verdict is read.
|
|
121
|
+
- `GATE_CHECK=ok`, then `GATE_BLOCK=true` or `GATE_BLOCK=false`, then — only when the envelope
|
|
122
|
+
carries a non-empty `message` — `GATE_MESSAGE=` followed by that message. The message is printed
|
|
123
|
+
last, so everything after `GATE_MESSAGE=` to the end of the output is the message, however many
|
|
124
|
+
lines it has. Every line of it after the first is indented by two spaces (each line break, `\r`
|
|
125
|
+
and Unicode line separator included, becomes a newline plus two spaces, and other control
|
|
126
|
+
characters become spaces), so no message line can begin with `GATE_` or with the halt token: the
|
|
127
|
+
verdict is the one `GATE_BLOCK=` line at column 0, and a message cannot print a second one (16
|
|
128
|
+
review iteration 3 WR-01).
|
|
129
|
+
|
|
130
|
+
The verdict is computed here, in the shell that ran the check, for two measured reasons. **The
|
|
131
|
+
engine:** this is the only fence in this file that carries the launcher preamble (the launcher
|
|
132
|
+
generator keeps ONE preamble per file, in its first `pp_run` fence, and strips any other), so a
|
|
133
|
+
`pp_run` in any later fence of this file, run as its own call, resolves whatever `pp_run` is on
|
|
134
|
+
`PATH` — measured on a host with a global 1.14.0 engine there: the repo engine answered
|
|
135
|
+
`disabled_with_artifacts, block: true`, the PATH engine answered `inactive`, and a blocking gate
|
|
136
|
+
passed. **The parameters:** a second check call depends on the agent re-binding every `GATE_*`
|
|
137
|
+
parameter for it; one that drops `GATE_UNPAIRED=strict` at `ship:pre` or `verify:post` re-runs a
|
|
138
|
+
tolerant check, which skips SEAL-01, and the gate passes having checked less than step 1 did. Step 2
|
|
139
|
+
therefore runs no command at all; it reads what this fence printed.
|
|
140
|
+
|
|
141
|
+
Both positionals use the **conditional-expansion** idiom `${VAR:+"${VAR}"}` deliberately: an unset or
|
|
142
|
+
empty variable expands to *no word at all*, not to an empty string. A bare `"${GATE_POSITIONAL_ARG_2}"`
|
|
143
|
+
would hand every caller that leaves it unset an extra empty argument, which the router would read as a
|
|
144
|
+
present-but-blank slot. The two must stay identical in form for that reason.
|
|
145
|
+
|
|
146
|
+
`GATE_WS` uses the same idiom for the same reason, and it is a **flag pair** rather than a positional.
|
|
147
|
+
A flag pair must be written as **two separate expansions** — `${GATE_WS:+--ws} ${GATE_WS:+"${GATE_WS}"}`
|
|
148
|
+
— so that it emits both words or neither.
|
|
149
|
+
|
|
150
|
+
**Do NOT collapse it to `${GATE_WS:+--ws "${GATE_WS}"}`.** That single-expansion form is bash-specific
|
|
151
|
+
and fails SILENTLY elsewhere. POSIX and bash word-split the unquoted expansion into two arguments;
|
|
152
|
+
**zsh does not**, so the whole thing arrives as one argument `--ws demo-ws`, which the router does not
|
|
153
|
+
recognise as `--ws` and ignores. The gate then answers `status: unresolved / block: true /
|
|
154
|
+
phase_unresolved` on a partitioned project — the exact symptom of the omitted-`--ws` defect this
|
|
155
|
+
parameter exists to prevent, reintroduced at execution time rather than at authoring time. Reproduce
|
|
156
|
+
with `set -- ${V:+--ws "$V"}`: bash reports `argc=2`, zsh reports `argc=1`.
|
|
157
|
+
|
|
158
|
+
This matters because the shell that runs this line is the *dispatching agent's* shell, not this
|
|
159
|
+
document's fence. Several runtimes (macOS Claude Code among them) execute tool commands under zsh.
|
|
160
|
+
The two-expansion form is byte-identical in both shells, so it removes the dependency entirely.
|
|
161
|
+
|
|
162
|
+
Written correctly, flat mode still produces a byte-identical
|
|
163
|
+
command line to the one that shipped before this parameter existed. It is appended after the
|
|
164
|
+
positionals so it can never occupy a positional slot — the router reads `--ws` by name, and a phase
|
|
165
|
+
argument shifted into the wrong slot is the failure mode the two positionals above already warn about.
|
|
166
|
+
|
|
167
|
+
`GATE_STAGE` is the third use of that idiom, and it carries the same two properties for the same two
|
|
168
|
+
reasons: written as the two-expansion pair `${GATE_STAGE:+--stage} ${GATE_STAGE:+"${GATE_STAGE}"}` it
|
|
169
|
+
emits both words or neither, so a caller that sets
|
|
170
|
+
no stage produces a command line byte-identical to the one that shipped before this parameter
|
|
171
|
+
existed — and it carries the same zsh hazard described above, where collapsing it to one expansion
|
|
172
|
+
makes `--stage plan` arrive as a single ignored argument and silently reinstates the stage-blind
|
|
173
|
+
collection defect; and it too is appended after both positionals, so it reaches every gate declared at the
|
|
174
|
+
point without ever displacing a phase argument. A query with no `--stage` handling ignores an
|
|
175
|
+
unrecognised named flag — which is what makes it safe to append to an invocation line that is shared
|
|
176
|
+
by every gate at a loop point, not owned by the one that wanted it.
|
|
177
|
+
|
|
178
|
+
`GATE_UNPAIRED` is the fourth use of that idiom and carries all three of the same properties, for
|
|
179
|
+
the same three reasons: written as `${GATE_UNPAIRED:+--unpaired} ${GATE_UNPAIRED:+"${GATE_UNPAIRED}"}`
|
|
180
|
+
it emits both words or neither, so a caller that sets no strictness produces a command line
|
|
181
|
+
byte-identical to the one that shipped before this parameter existed; collapsing it to the single
|
|
182
|
+
expansion `${GATE_UNPAIRED:+--unpaired "${GATE_UNPAIRED}"}` makes `--unpaired strict` arrive as one
|
|
183
|
+
argument under zsh, which the router does not recognise, so the strict call sites would silently
|
|
184
|
+
degrade to the tolerant default — a gate that reports success because its own flag was dropped; and
|
|
185
|
+
it is appended after both positionals and after both other pairs, so it can never displace a phase
|
|
186
|
+
argument at the three points that pass one.
|
|
187
|
+
|
|
188
|
+
The command has failed when **any** of these holds: `CHECK_EXIT` is non-zero, `GATE_RESULT` is
|
|
189
|
+
empty, or `GATE_RESULT` does not parse as a JSON object. The fence tests exactly that and prints
|
|
190
|
+
`GATE_CHECK=failed`. That is a **command** error and *only* a command error — and so is a run that
|
|
191
|
+
printed **no** `GATE_CHECK=` line at all (the fence died before it, e.g. the launcher found no
|
|
192
|
+
engine): with nothing printed there is no verdict to read. Route it by the hook's `onError`:
|
|
193
|
+
|
|
194
|
+
- `onError == "halt"` → fatal. Print
|
|
195
|
+
`⚠ Gate check command failed (${GATE_POINT} / ${GATE_QUERY}): command error. Resolve before continuing.`,
|
|
196
|
+
run the halt block below, and stop the host workflow.
|
|
197
|
+
- `onError == "skip"` (or absent) → print
|
|
198
|
+
`⚠ Gate check skipped (${GATE_POINT} / ${GATE_QUERY}): check command error.`, move to the next
|
|
199
|
+
hook, and — this is an imperative, not a preference — **do NOT read a verdict.** The fence
|
|
200
|
+
prints none on this path, and none may be inferred.
|
|
201
|
+
|
|
202
|
+
Merging these two steps is the fail-open this gate exists to close: if a non-zero exit were read as
|
|
203
|
+
a verdict, `onError: skip` would swallow a genuine block as a command warning.
|
|
204
|
+
|
|
205
|
+
## Step 2 — read the verdict
|
|
206
|
+
|
|
207
|
+
Reached **only** when step 1 passed — that is, when it printed `GATE_CHECK=ok`. **Step 2 runs no
|
|
208
|
+
command** — in particular it
|
|
209
|
+
never runs `pp_run check` again. It reads the `GATE_BLOCK=` line and, when present, the
|
|
210
|
+
`GATE_MESSAGE=` text that step 1's fence printed, and nothing else (step 1 explains why: a second
|
|
211
|
+
check call would answer from whatever engine is on `PATH`, with whatever parameters were re-bound).
|
|
212
|
+
The verdict therefore survives the agent's Bash tool starting a fresh shell for every call: what
|
|
213
|
+
crosses from step 1 to step 2 is printed text, never a shell variable.
|
|
214
|
+
|
|
215
|
+
Step 1 extracts the verdict with the same JSON round-trip idiom the sibling `vault-load-gate.md`
|
|
216
|
+
uses. It deliberately uses `node -e` rather than a JSON command-line filter, because no such filter
|
|
217
|
+
is a declared dependency of this project. The `.block === true` comparison **is** the missing-field
|
|
218
|
+
rule from the audit footnote: a missing, null, string or numeric `block` all evaluate to `false`, so
|
|
219
|
+
the fence prints `GATE_BLOCK=false` for them. (Its `catch` arm, which prints `true`, is unreachable
|
|
220
|
+
behind the command-error test; it exists so that a future edit which removes that test fails closed.)
|
|
221
|
+
|
|
222
|
+
- **`GATE_BLOCKING` is `true` AND `GATE_BLOCK` is `true` → HALT.** Print
|
|
223
|
+
`⚠ Gate blocked at ${GATE_POINT} (${GATE_QUERY}). Resolve before continuing.` and run the halt
|
|
224
|
+
block below.
|
|
225
|
+
|
|
226
|
+
**This halt is never bypassed by `onError`.** `onError` governs command errors from step 1 and
|
|
227
|
+
nothing else; it has no authority over the gate's own decision. A gate that reports a real block
|
|
228
|
+
and is then waved through by an error policy is not a gate.
|
|
229
|
+
|
|
230
|
+
- **`GATE_BLOCKING` is `false` → never halt.** If `GATE_BLOCK` is `true`, print one advisory line:
|
|
231
|
+
`ℹ Gate advisory at ${GATE_POINT} (${GATE_QUERY}). Continuing.` Then continue to the next hook.
|
|
232
|
+
|
|
233
|
+
- **`GATE_BLOCK` is `false` → continue.** No status line.
|
|
234
|
+
|
|
235
|
+
**Output-independence rule (a later plan's byte-identical regression pin depends on this).** On
|
|
236
|
+
every NON-halting path, the enforcer's printed output must be **identical** whether `GATE_BLOCKING`
|
|
237
|
+
is `true` or `false`. `blocking` decides one thing only: whether a `true` block halts. It must never
|
|
238
|
+
change what a passing or advisory verdict prints. Otherwise flipping a gate declaration from
|
|
239
|
+
advisory to blocking would change output for projects whose policy key is off — exactly what the
|
|
240
|
+
byte-identical-when-off promise forbids.
|
|
241
|
+
|
|
242
|
+
## The halt mechanism (D-VH-7, option A — the workflow owns the exit)
|
|
243
|
+
|
|
244
|
+
There are two halves and both ship, because they serve two different observers.
|
|
245
|
+
|
|
246
|
+
**Half one — the named token, for the agent.** Print, on its own line:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
PARETO_GATE_BLOCKED:${GATE_QUERY}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
On seeing that token the agent stops the host workflow immediately, runs no further step, and
|
|
253
|
+
reports the block to the user.
|
|
254
|
+
|
|
255
|
+
**Half two — the process exit, for the runner.** In the *same* bash block:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
echo "PARETO_GATE_BLOCKED:${GATE_QUERY}"
|
|
259
|
+
exit 1
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
An unattended runner or CI job cannot honour a sentence; it can only observe an exit code. Without
|
|
263
|
+
half two a blocked build reports success to the pipeline.
|
|
264
|
+
|
|
265
|
+
`pp_run check` itself still **exits 0 on every path**, by design (`output()` in `src/io.cts` writes
|
|
266
|
+
to fd 1 and returns; it never sets a non-zero code). That is deliberate and must not be "fixed":
|
|
267
|
+
making the check command exit non-zero would turn every block into a step-1 command error, which
|
|
268
|
+
`onError: skip` would then swallow. The exit code belongs to the workflow, not to the check.
|
|
269
|
+
|
|
270
|
+
## Message passthrough
|
|
271
|
+
|
|
272
|
+
Independently of status, and on **every** run including passes: if step 1 printed a
|
|
273
|
+
`GATE_MESSAGE=` line, print it verbatim — the message is the text after `GATE_MESSAGE=`, to the end
|
|
274
|
+
of step 1's output, its later lines indented by two spaces — it is already operator-ready, possibly
|
|
275
|
+
multi-line text. A gate that has
|
|
276
|
+
something to tell the operator says it on every run, not only when its verdict is bad. This rule is
|
|
277
|
+
preserved word-for-word in effect from the ship.md loop this file replaced; a later plan makes both
|
|
278
|
+
the vault gate's actionable block text and its success-path observability ride on it.
|
|
279
|
+
|
|
280
|
+
## Direct-query mode
|
|
281
|
+
|
|
282
|
+
Two entry points — the build entry point `fast` and the discovery sprint's
|
|
283
|
+
`discovery-sprint/vault-context.md` subflow — declare no `gsd:loop-host` frontmatter at all. They
|
|
284
|
+
have no rendered hooks, and adding points to them would bump the twelve-point loop-host contract.
|
|
285
|
+
They are supported here rather than in a third copy of the halt logic. The count in this sentence is
|
|
286
|
+
pinned against the `direct` rows of the Caller contract below, so a third caller must update both.
|
|
287
|
+
(`execute-plan` left this mode in 16-10 and `quick` in 16-11: each renders its own loop points and
|
|
288
|
+
filters the payload to the vault capability, so they are hook rows below.)
|
|
289
|
+
|
|
290
|
+
When `GATE_HOOKS_JSON` is empty **and** `GATE_DIRECT_QUERY` is set to a query name, synthesise a
|
|
291
|
+
single in-memory hook entry — that query, `blocking: true`, `onError: skip` — and run the identical
|
|
292
|
+
loop above over it.
|
|
293
|
+
|
|
294
|
+
**A direct caller must perform its own activation guard before Reading this file.** A synthesised
|
|
295
|
+
hook has not been through the registry's `when` evaluation and therefore carries none of its
|
|
296
|
+
inertness. The two guards such a caller must run first are:
|
|
297
|
+
|
|
298
|
+
1. the **platform-bindings presence probe** (`pp_run query platform-bindings --pick present`; when
|
|
299
|
+
not `true`, do not Read this file at all — non-Pareto project, fully inert), and
|
|
300
|
+
2. the **admission verb** (`pp_run vault admission`, D-28; `on` → Read this file, `off` or `inert`
|
|
301
|
+
→ do not, anything else halts with `PARETO_GATE_BLOCKED:vault.admission`, D-05). Both direct
|
|
302
|
+
callers, `fast` (16-13) and the discovery subflow (16-04), run it.
|
|
303
|
+
|
|
304
|
+
## Caller contract
|
|
305
|
+
|
|
306
|
+
| Caller | Mode | Sets |
|
|
307
|
+
|--------|------|------|
|
|
308
|
+
| `pp-core/workflows/ship.md` step 6 | hook | `GATE_POINT=ship:pre`, `GATE_HOOKS_JSON=$SHIP_PRE_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_DIR}"`, `GATE_UNPAIRED=strict` — sets **no** `GATE_WS` (a directory carries the partition) and **no** `GATE_STAGE` (this point validates both stages). The positional was empty until 04-09; emptying it again makes a blocking `vault.trace` gate halt every ship on every vault-enabled project |
|
|
309
|
+
| `pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md` (dispatched by the host's execute:pre step) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"`, `GATE_WS=<workstream>` |
|
|
310
|
+
| `pp-core/workflows/code-review-fix.md` `vault_admission_gate` (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$CRF_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
|
|
311
|
+
| `pp-core/workflows/validate-phase.md` § 0.5 (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$VP_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
|
|
312
|
+
| `pp-core/workflows/add-tests.md` `vault_admission_gate` (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$AT_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
|
|
313
|
+
| `pp-core/workflows/audit-fix.md` `vault_admission_gate` (16-15) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$AF_EXECUTE_PRE_HOOKS_JSON` (the door's own `execute:pre` render, filtered in the fence to the vault capabilities, D-30), after its own admission fence and before its agent writes source. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
|
|
314
|
+
| `pp-core/workflows/execute-phase.md` step 5.75 | hook | `GATE_POINT=execute:wave:post`, `GATE_HOOKS_JSON=$WAVE_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"`, `GATE_WS=<workstream>` |
|
|
315
|
+
| `pp-core/workflows/execute-phase.md` execute:post step | hook | `GATE_POINT=execute:post`, `GATE_HOOKS_JSON=$EXECUTE_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_NUMBER}"`, `GATE_WS=<workstream>` — sets **no** `GATE_STAGE`, deliberately: this is the point that validates both stages |
|
|
316
|
+
| `pp-core/workflows/execute-phase/steps/verify-post-gate.md` (dispatched by TWO callers: the host's end-of-phase verification step, and `verify-work.md`'s issues-are-zero branch, which exposes no partial-execution mode and is therefore always strict) | hook | `GATE_POINT=verify:post`, `GATE_HOOKS_JSON=$VERIFY_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_DIR}"`, and `GATE_UNPAIRED=strict` **only on a full-phase run: when the host's `WAVE_FILTER` is unset AND the invocation carried no `--gaps-only`** — on a `--wave N` or a `--gaps-only` run it leaves `GATE_UNPAIRED` unset so the tolerant default applies, because a partially-executed run legitimately leaves untouched plans unexecuted and this dispatch runs BEFORE the host's `handle_partial_wave_execution` step, so an unconditional strict verdict here halted every partial run on every vault-enabled project (04-12, CR-04; 04-18 WR-01 widened the step file's condition to `--gaps-only`, and this row named `WAVE_FILTER` alone until 260912-kz7 — a stale row is a lie the next author implements). This is the one conditional strictness in the table, and the condition is deliberate: unlike `ship:pre`, whose preflight requires a passed verification and therefore admits no partial run, this point is reachable in a mode where an unpaired plan is the correct state. Sets **no** `GATE_WS` (a directory carries the partition) and **no** `GATE_STAGE`, deliberately, exactly as `execute:post` omits it: this point validates both stages. The body lives in a step file because the host is under a phase-6 ratchet that is never re-baselined (D-05) |
|
|
317
|
+
| `pp-core/workflows/plan-phase.md` §13e | hook | `GATE_POINT=plan:post`, `GATE_HOOKS_JSON=$PLAN_POST_HOOKS_JSON`, `GATE_POSITIONAL_ARG="${PHASE_DIR}"`, `GATE_POSITIONAL_ARG_2="${PHASE_REQ_IDS}"`, `GATE_WS=<workstream>`, `GATE_STAGE=plan` — the only caller using the second positional, and the only one setting a stage |
|
|
318
|
+
| `pp-core/workflows/plan-phase.md` plan:pre vault gate | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$PLAN_PRE_HOOKS_JSON` — skips `ui.plan-gate`, which §5.6 above already ran with its own phase positional and whose outcome it owns. `GATE_POSITIONAL_ARG` is left **empty** deliberately: `vault.status` reads project state, not a phase, and every remaining gate at this point that needs a positional is caller-handled (D-VH-7). Sets **no** `GATE_WS` (there is no phase-number positional to resolve) and **no** `GATE_STAGE`. Handing the whole payload here with no skip made the enforcer issue `check ui.plan-gate` with no argument; `onError: halt` turned that command error into a hard stop on every vault-enabled plan run, before the vault verdict was read (02-09, G-1) |
|
|
319
|
+
| `pp-core/workflows/autonomous.md` §3a.5-iii | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$HOOKS_JSON` — skips `ui.plan-gate`, which §3a.5-ii above owns on every activation set that renders it, the gate-only set (`ui_phase=false` + `ui_safety_gate=true`) included. `GATE_POSITIONAL_ARG` is left **empty** deliberately, for the same reason as the `plan-phase.md` plan:pre row. Sets **no** `GATE_WS` and **no** `GATE_STAGE`. The skip is sound only because §3a.5-ii's run trigger covers the gate-only set — an unconditional skip without that trigger would leave a `blocking: true` gate enforced by nobody under `--auto` (02-09, T-02-41) |
|
|
320
|
+
| `pp-core/workflows/quick.md` Step 2.4 (16-11) | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$QUICK_PLAN_PRE_HOOKS_JSON` (its own `plan:pre` render, filtered in the fence to the `vault` capability, D-30, so no `ui.plan-gate` reaches the enforcer), after its own admission fence. `GATE_POSITIONAL_ARG` is left **empty** deliberately (`vault.status` reads project state); sets **no** `GATE_WS` and **no** `GATE_STAGE`. Quick's `execute:pre` render feeds only the executor prompt's fragments, never a gate |
|
|
321
|
+
| `pp-core/workflows/debug.md` § 0b (16-14) | hook | `GATE_POINT=plan:pre`, `GATE_HOOKS_JSON=$DEBUG_PLAN_PRE_HOOKS_JSON` (its own `plan:pre` render, filtered in the fence to the `vault` capability, D-30), after its own admission fence and before either session-manager spawn. `GATE_POSITIONAL_ARG` is left **empty** deliberately (`vault.status` reads project state); sets **no** `GATE_WS` and **no** `GATE_STAGE` |
|
|
322
|
+
| `pp-core/workflows/quick.md` Step 5.55 (16-11) | hook | `GATE_POINT=plan:post`, `GATE_HOOKS_JSON=$QUICK_PLAN_POST_HOOKS_JSON` (filtered the same way), `GATE_POSITIONAL_ARG="${QUICK_DIR}"`, `GATE_STAGE=plan` — before Step 5.6's plan commit and the executor spawn (D-34). Sets **no** `GATE_WS`: a quick directory carries its partition |
|
|
323
|
+
| `pp-core/workflows/quick.md` Step 6.1 (16-11) | hook | `GATE_POINT=execute:post`, `GATE_HOOKS_JSON=$QUICK_EXECUTE_POST_HOOKS_JSON` (filtered the same way), `GATE_POSITIONAL_ARG="${QUICK_DIR}"`, `GATE_UNPAIRED=strict` (one plan, one SUMMARY, nothing pending, D-03) — before the STATE.md row and the final commit (D-34). Sets **no** `GATE_STAGE` and **no** `GATE_WS` |
|
|
324
|
+
| `pp-core/workflows/fast.md` (plan 02-05; admission on the verb since 16-13) | direct | `GATE_POINT=fast:pre`, `GATE_DIRECT_QUERY=vault.status`, own activation guards (the bindings probe, then `vault admission`); with the vault on, `fast` also refuses edits outside `.planning/` and backstops its commit (D-07) |
|
|
325
|
+
| `pp-core/workflows/execute-plan.md` `vault_admission_gate` (16-10) | hook | `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON` (its own `execute:pre` render, filtered in the fence to the `vault` capability, D-30), after its own admission fence. Sets **no** positional, **no** `GATE_WS` and **no** `GATE_STAGE`: `vault.status` reads project state |
|
|
326
|
+
| `pp-core/workflows/execute-plan.md` `vault_trace_gate` (16-10) | hook | `GATE_POINT=plan:post`, `GATE_HOOKS_JSON=$PLAN_POST_HOOKS_JSON` (its own `plan:post` render, filtered the same way), `GATE_POSITIONAL_ARG="$PLAN_PATH"` (the ONE `*-PLAN.md` file this run executes, never its phase directory, D-06), `GATE_STAGE=plan`. Sets **no** `GATE_WS`: the file carries its partition |
|
|
327
|
+
| `pp-core/workflows/discovery-sprint/vault-context.md` (plan 11-03) | direct | `GATE_POINT=discovery:pre`, `GATE_DIRECT_QUERY=vault.status`, own activation guards; Read from the discovery sprint's Flow 2 Step 1 and prefill Step 0 only, never from a read-only arm (D-11-12); runs `vault-fetch.md` in direct mode FIRST, so both `plan:pre` halt surfaces are inherited in their order (D-11-11); `discovery:pre` is a message label, not a loop point — `GATE_POINT` never changes control flow, which is what makes a non-canonical label safe here |
|
|
@@ -10,12 +10,13 @@ out of scope here.
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
_PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
|
13
|
-
CTX_GATE_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo
|
|
13
|
+
CTX_GATE_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
|
|
14
14
|
CTX_BLOCK=$(pp_run query generate-client-context --read 2>/dev/null || echo '{"present":false}')
|
|
15
15
|
CTX_BLOCK_PRESENT=$(printf '%s' "$CTX_BLOCK" | node -e "const d=require('fs').readFileSync(0,'utf-8');try{process.stdout.write(String((JSON.parse(d).present)||false));}catch{process.stdout.write('false');}")
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
**If `CTX_GATE_PRESENT` != `true`:** skip entirely (non-Pareto project — fully inert).
|
|
18
|
+
**If `CTX_GATE_PRESENT` != `true`:** skip entirely (non-Pareto project — fully inert). A probe that
|
|
19
|
+
cannot answer (a malformed bindings file) reads as present, as `hasPlatformBindings` does (D-47 b).
|
|
19
20
|
|
|
20
21
|
**If `CTX_GATE_PRESENT` == `true` and `CTX_BLOCK_PRESENT` == `true`:** the client-context
|
|
21
22
|
block exists and is on the executor's read path. Surface a one-line confirmation
|
|
@@ -5,7 +5,7 @@ unchanged; the host now lazily Reads it.
|
|
|
5
5
|
|
|
6
6
|
**Why it moved.** Phase 9's W-1 fix gave this step's `portable-timeout.sh` source the same
|
|
7
7
|
resolve-then-check shape the other eight sites use, which grew `execute-phase.md` by 417 bytes to
|
|
8
|
-
|
|
8
|
+
93943. That passed `tests/workflow-size-baseline.json` after a conscious re-baseline, but a SECOND
|
|
9
9
|
and independent ratchet — `tests/phase6-capstone-conformance.test.cjs:207`, the ADR-857 shrink
|
|
10
10
|
ratchet — asserts `execute-phase.md` stays strictly **under 93600**, and it was never re-baselined
|
|
11
11
|
because it is not supposed to be: its whole purpose is that the host loop body only ever shrinks.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# `execute:pre` capability dispatch — fetch, then decide (GATE-03)
|
|
2
|
+
|
|
3
|
+
Extracted from `execute-phase.md` so the host loop body keeps shrinking, per ADR-857 Phase 6.
|
|
4
|
+
`steps/` files are outside `tests/workflow-size-baseline.json` by design; the host is not.
|
|
5
|
+
|
|
6
|
+
This step is dispatched from `execute-phase.md`'s `execute_pre_dispatch` step, which sits *before*
|
|
7
|
+
`safe_resume_gate` deliberately: that is the first step that can spawn an executor, and the gate must
|
|
8
|
+
run before any code is written, not after.
|
|
9
|
+
|
|
10
|
+
Read the `activeHooks` array from the `EXECUTE_PRE_HOOKS_JSON=` line the host's render fence printed, in-context (do NOT pipe it through a
|
|
11
|
+
shell parser).
|
|
12
|
+
|
|
13
|
+
**Admission first (D-28, D-05).** `execute-phase.md` Reads this file unconditionally, so this is the
|
|
14
|
+
first point on the execute path that can refuse a config the gate cannot read. It precedes the inert
|
|
15
|
+
branch below because that branch is exactly what a broken config reaches: no hook renders at all.
|
|
16
|
+
Without `.platform-bindings.json` it is inert and prints nothing.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
_PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
|
20
|
+
VAULT_ADMIT_PRESENT=$(pp_run query platform-bindings --pick present 2>/dev/null || echo true)
|
|
21
|
+
VAULT_ADMIT=inert
|
|
22
|
+
if [ "$VAULT_ADMIT_PRESENT" = true ]; then VAULT_ADMIT=$(pp_run vault admission 2>/dev/null || echo unrecognised); fi
|
|
23
|
+
case "$VAULT_ADMIT" in on) echo "VAULT_ADMIT=on" ;; off|inert) : ;; *) pp_run vault admission --explain; echo "PARETO_GATE_BLOCKED:vault.admission"; exit 1 ;; esac
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**If `activeHooks` is empty or absent:** skip the rest of this file silently. This branch is what
|
|
27
|
+
keeps a project with no active `execute:pre` capability structurally inert.
|
|
28
|
+
|
|
29
|
+
**Otherwise, in this order — the order is the requirement:**
|
|
30
|
+
|
|
31
|
+
1. **Fetch first.** For any active entry where `kind == "step"` and `capId == "vault"`, set
|
|
32
|
+
`VAULT_FETCH_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`, then Read and
|
|
33
|
+
execute `pp-core/workflows/execute-phase/steps/vault-fetch.md`. The fetch is attempted *before*
|
|
34
|
+
any block decision so a fresh clone or a CI runner with a reachable vault fetches and passes,
|
|
35
|
+
rather than being blocked for a cache it never had a chance to populate (GATE-03).
|
|
36
|
+
|
|
37
|
+
The binding is not decoration and it is not the same shape as step 2's: `vault-fetch.md` is
|
|
38
|
+
Read from three call sites at two loop points, so it guards on the **parameter**
|
|
39
|
+
`VAULT_FETCH_HOOKS_JSON` and names no caller's own payload variable. Until 02-10 it named
|
|
40
|
+
`plan-phase.md`'s, which this dispatcher never sets and never aliased — and the instruction
|
|
41
|
+
immediately after the guard is *"if no active vault step hook exists: skip"*. A literal
|
|
42
|
+
reading therefore skipped the `execute:pre` fetch outright and let step 2 decide on whatever
|
|
43
|
+
cache happened to exist, which is exactly the fresh-clone false block this ordering exists to
|
|
44
|
+
prevent (02-VERIFICATION, G-2). Binding it here is the whole fix at this call site.
|
|
45
|
+
|
|
46
|
+
2. **Then gate.** Set `GATE_POINT=execute:pre`, `GATE_HOOKS_JSON=$EXECUTE_PRE_HOOKS_JSON`,
|
|
47
|
+
`GATE_POSITIONAL_ARG="${PHASE_NUMBER}"` and `GATE_WS` to the active workstream name (empty in flat
|
|
48
|
+
mode) — a phase-number positional cannot resolve without it on a partitioned project — then Read and follow
|
|
49
|
+
`pp-core/workflows/execute-phase/steps/check-query-gate.md` — the single generic `check.query`
|
|
50
|
+
gate enforcer (GATE-02, D-VH-7). It owns the halt decision; do not restate its loop here.
|
|
51
|
+
|
|
52
|
+
3. **Then render.** If the workflow is still running, set `VAULT_GATE_POINT=execute` and Read and
|
|
53
|
+
execute `pp-core/workflows/execute-phase/steps/vault-load-gate.md`. It reports the load state for
|
|
54
|
+
the operator and decides nothing — the decision was step 2's.
|
|
55
|
+
|
|
56
|
+
This line is what makes the load-gate file's `execute:pre` message column live. Until this phase
|
|
57
|
+
that column was written and never dispatched: the execute half of a file that claimed to serve
|
|
58
|
+
both call sites was dead code.
|
|
59
|
+
|
|
60
|
+
4. **Then record the start anchor (D-31).** If the workflow is still running and the
|
|
61
|
+
`activeHooks` array holds an entry with `capId == "vault"` (the same mechanical check step 1 uses
|
|
62
|
+
for the fetch), run `pp_run vault anchor start "${PHASE_DIR}" --raw` and print its one-line result
|
|
63
|
+
to the operator. Otherwise skip it.
|
|
64
|
+
|
|
65
|
+
It writes `${PHASE_DIR}/.vault-anchors.json` with `start` = HEAD. Every record the phase's
|
|
66
|
+
executors write later (the `vault standards consulted` anchor line) reads it as its START anchor,
|
|
67
|
+
beside the END anchor (HEAD at write time) the CLI records itself. A commit is then covered only
|
|
68
|
+
when it falls inside some record's start..end range, so an ungoverned commit followed by any
|
|
69
|
+
record can no longer hide behind that record's end. Re-running at a new HEAD (a resumed run, `--wave N`, `--gaps-only`) records that HEAD as the start of every
|
|
70
|
+
record written from now on (D-98), so nothing committed between two runs is claimed by a later
|
|
71
|
+
record; the earlier start stays in the file's history, where a worktree executor's end-only record
|
|
72
|
+
written before the resume still finds the start that preceded its own end. The phase's own commits
|
|
73
|
+
made between its last record and the resume are then uncovered, and the strict gate names them with
|
|
74
|
+
their repair: `vault anchor backfill <summary> --from <a recorded start> --attest <commit>`, the
|
|
75
|
+
operator's attestation of each commit by its own id; nothing is widened, and every check lists it.
|
|
76
|
+
The verb prints nothing and writes nothing unless `vault admission` answers
|
|
77
|
+
`on`, exits 0 on every path, and reports a refusal as `ok: false` with a reason that is printed
|
|
78
|
+
and never halts. A phase that has no start anchor fails at the strict points instead.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Per-plan worktree decision (#2772)
|
|
2
|
+
|
|
3
|
+
Run this for **each plan in the current wave** before its `Agent()` dispatch. The output `USE_WORKTREES_FOR_PLAN` gates the dispatch branch (worktree mode vs sequential mode) for that plan only — other plans in the same wave can still take the worktree path.
|
|
4
|
+
|
|
5
|
+
`SUBMODULE_PATHS` is computed once in the `initialize` step (parsed from `.gitmodules`).
|
|
6
|
+
|
|
7
|
+
`PLAN_FILES` is the whitespace-separated list of paths the plan declared it will touch, extracted from the `phase-plan-index` JSON loaded in `discover_and_group_plans`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# plan_json is the JSON object for this plan from PLAN_INDEX.plans[]
|
|
11
|
+
# files_modified is an array of strings (repo-relative paths or globs)
|
|
12
|
+
PLAN_FILES=$(jq -r '.files_modified // [] | join(" ")' <<<"$plan_json")
|
|
13
|
+
plan_id=$(jq -r '.id' <<<"$plan_json")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then run the per-plan gate:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
USE_WORKTREES_FOR_PLAN="$USE_WORKTREES"
|
|
20
|
+
|
|
21
|
+
if [ -n "$SUBMODULE_PATHS" ] && [ "$USE_WORKTREES_FOR_PLAN" != "false" ]; then
|
|
22
|
+
if [ -z "$PLAN_FILES" ]; then
|
|
23
|
+
# Fallback: planned paths are unknown/unparseable — fall back to the safe
|
|
24
|
+
# behavior (disable worktree isolation for this plan) and log why.
|
|
25
|
+
echo "[worktree] Plan ${plan_id}: files_modified missing/unparseable — disabling worktree isolation as a safety fallback (submodule project)"
|
|
26
|
+
USE_WORKTREES_FOR_PLAN=false
|
|
27
|
+
else
|
|
28
|
+
# Compute intersection with glob-safe normalization. Both sides are
|
|
29
|
+
# normalized (strip leading "./", strip trailing "/") and matched
|
|
30
|
+
# bidirectionally so a globby planned path like "vendor/**/*.c" still
|
|
31
|
+
# matches submodule "vendor/foo", and "./vendor/foo/bar.c" matches
|
|
32
|
+
# submodule "vendor/foo".
|
|
33
|
+
INTERSECT=""
|
|
34
|
+
set -f # disable globbing while iterating literal patterns
|
|
35
|
+
for sm_raw in $SUBMODULE_PATHS; do
|
|
36
|
+
# Normalize submodule path: strip ./ prefix and trailing /
|
|
37
|
+
sm="${sm_raw#./}"
|
|
38
|
+
sm="${sm%/}"
|
|
39
|
+
[ -z "$sm" ] && continue
|
|
40
|
+
for pf_raw in $PLAN_FILES; do
|
|
41
|
+
# Normalize planned path the same way
|
|
42
|
+
pf="${pf_raw#./}"
|
|
43
|
+
pf="${pf%/}"
|
|
44
|
+
[ -z "$pf" ] && continue
|
|
45
|
+
matched=0
|
|
46
|
+
# Direction 1: planned path is the submodule or lies inside it
|
|
47
|
+
case "$pf" in
|
|
48
|
+
"$sm"|"$sm"/*) matched=1 ;;
|
|
49
|
+
esac
|
|
50
|
+
# Direction 2: submodule lies inside the planned path (e.g. plan
|
|
51
|
+
# declares "vendor" or a glob expanding to a directory containing
|
|
52
|
+
# the submodule).
|
|
53
|
+
if [ "$matched" -eq 0 ]; then
|
|
54
|
+
case "$sm" in
|
|
55
|
+
"$pf"|"$pf"/*) matched=1 ;;
|
|
56
|
+
esac
|
|
57
|
+
fi
|
|
58
|
+
# Direction 3: planned path uses a glob — strip glob wildcards
|
|
59
|
+
# and check whether the resulting prefix overlaps the submodule
|
|
60
|
+
# path in either direction.
|
|
61
|
+
if [ "$matched" -eq 0 ]; then
|
|
62
|
+
case "$pf" in
|
|
63
|
+
*'*'*|*'?'*|*'['*)
|
|
64
|
+
# Take the literal prefix before the first glob metachar.
|
|
65
|
+
prefix="${pf%%[*?[]*}"
|
|
66
|
+
prefix="${prefix%/}"
|
|
67
|
+
if [ -n "$prefix" ]; then
|
|
68
|
+
case "$sm" in
|
|
69
|
+
"$prefix"|"$prefix"/*) matched=1 ;;
|
|
70
|
+
esac
|
|
71
|
+
if [ "$matched" -eq 0 ]; then
|
|
72
|
+
case "$prefix" in
|
|
73
|
+
"$sm"|"$sm"/*) matched=1 ;;
|
|
74
|
+
esac
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
fi
|
|
80
|
+
if [ "$matched" -eq 1 ]; then
|
|
81
|
+
INTERSECT="$INTERSECT $pf_raw"
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
done
|
|
85
|
+
set +f
|
|
86
|
+
if [ -n "$INTERSECT" ]; then
|
|
87
|
+
echo "[worktree] Plan ${plan_id}: planned paths intersect submodule paths (${INTERSECT# }) — disabling worktree isolation for this plan"
|
|
88
|
+
USE_WORKTREES_FOR_PLAN=false
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
After running this for the plan, the dispatch branches in `execute_waves` step 3 MUST gate on `USE_WORKTREES_FOR_PLAN` for the current plan, not on the project-level `USE_WORKTREES`. Track which plans in this wave actually used worktrees (append `plan_id` to a `WAVE_WORKTREE_PLANS` accumulator when `USE_WORKTREES_FOR_PLAN != false`) — the post-wave cleanup step (5.5) uses this to decide whether worktree-merge cleanup is needed at all.
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# Step: post_merge_gate
|
|
2
2
|
|
|
3
|
-
Post-merge build & test gate. Runs
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
**Step A — Build gate:**
|
|
9
|
-
When it is false (no plan in the wave touches source), print
|
|
10
|
-
`⚠ Post-merge build gate skipped — no plan in this wave touches source` and go to Step B.
|
|
3
|
+
Post-merge build & test gate. Runs after all worktrees in a wave are merged
|
|
4
|
+
(parallel mode), or after the last plan completes (serial mode). Catches
|
|
5
|
+
cross-plan integration failures that individual worktree self-checks cannot
|
|
6
|
+
detect.
|
|
7
|
+
|
|
8
|
+
**Step A — Build gate:**
|
|
11
9
|
|
|
12
10
|
```bash
|
|
13
11
|
_PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|