@pareto4/pp-core 1.9.0 → 1.10.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 +1 -1
- package/agents/pp-code-fixer.md +11 -0
- package/agents/pp-code-reviewer.md +77 -1
- package/agents/pp-executor.md +34 -1
- package/agents/pp-planner.md +24 -103
- package/commands/pp-ns-manage.md +2 -1
- package/commands/pp-vault-promote.md +31 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +111 -2
- package/pp-core/bin/lib/capability-state.cjs +41 -3
- package/pp-core/bin/lib/check-command-router.cjs +473 -3
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/io.cjs +7 -0
- package/pp-core/bin/lib/practices-review-check.cjs +938 -0
- package/pp-core/bin/lib/vault-command-router.cjs +108 -2
- package/pp-core/bin/lib/vault-practices.cjs +982 -0
- package/pp-core/bin/lib/vault-write-back.cjs +756 -0
- package/pp-core/bin/lib/vault.cjs +45 -3
- package/pp-core/bin/pp-tools.cjs +26 -0
- package/pp-core/references/planner-checkpoints.md +72 -0
- package/pp-core/references/planner-tdd.md +55 -0
- package/pp-core/references/planner-vault-practices.md +67 -0
- package/pp-core/templates/practices/README.md +122 -0
- package/pp-core/templates/practices/error-handling.md +18 -0
- package/pp-core/templates/practices/minimal-comments.md +14 -0
- package/pp-core/templates/practices/naming-convention.md +18 -0
- package/pp-core/templates/practices/practice-template.md +23 -0
- package/pp-core/workflows/code-review.md +74 -18
- package/pp-core/workflows/ship.md +1 -0
- package/pp-core/workflows/vault-write-back.md +213 -0
- package/scripts/gen-capability-registry.cjs +68 -8
- package/scripts/lint-allow-test-rule-refs.allowlist.json +8 -1
- package/scripts/lint-descriptions.cjs +3 -1
- package/scripts/lint-legacy-dir-name.cjs +5 -0
- package/scripts/lint-test-file-count.allowlist.json +10 -1
|
@@ -88,6 +88,14 @@ Code review skipped (code-review capability inactive)
|
|
|
88
88
|
Exit workflow.
|
|
89
89
|
|
|
90
90
|
Default is active through the Capability Registry schema — only skip when the registry resolves no active code-review step hook. This check runs AFTER phase validation so invalid phase errors are shown first.
|
|
91
|
+
|
|
92
|
+
Resolve the practices contribution independently, from the SAME `EXECUTE_POST_HOOKS_JSON` variable already fetched above — do not add a second `pp_run loop render-hooks` call:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
PRACTICES_CONTRIBUTION_FRAGMENT=""
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Resolve active entries in `EXECUTE_POST_HOOKS_JSON` where `kind == "contribution"` and `into == "pp-code-reviewer"`, and capture that entry's `fragment.inline` verbatim into `PRACTICES_CONTRIBUTION_FRAGMENT`. When no such entry is active, `PRACTICES_CONTRIBUTION_FRAGMENT` stays empty and the review runs exactly as it does today — the practices dimension is inert, not degraded.
|
|
91
99
|
</step>
|
|
92
100
|
|
|
93
101
|
<step name="resolve_depth">
|
|
@@ -359,9 +367,9 @@ if (resolved) process.stdout.write(resolved);
|
|
|
359
367
|
|
|
360
368
|
2) If binary is missing, fail with actionable message:
|
|
361
369
|
```bash
|
|
362
|
-
if [ -z
|
|
363
|
-
echo
|
|
364
|
-
echo
|
|
370
|
+
if [ -z "$FALLOW_BIN" ]; then
|
|
371
|
+
echo "Error: fallow is enabled but no binary was found."
|
|
372
|
+
echo "Install fallow via \`npm install -D fallow\` or \`cargo install fallow\`."
|
|
365
373
|
# Exit workflow
|
|
366
374
|
fi
|
|
367
375
|
```
|
|
@@ -375,36 +383,36 @@ FALLOW_STDERR_TMP=$(mktemp)
|
|
|
375
383
|
# Derive the phase base commit; if none is found, fall back to repo scope (fallow
|
|
376
384
|
# auto-detects the base branch).
|
|
377
385
|
FALLOW_SCOPE_ARGS=()
|
|
378
|
-
if [
|
|
379
|
-
FALLOW_PHASE_COMMITS=$(git log --oneline --all --grep
|
|
380
|
-
if [ -n
|
|
381
|
-
FALLOW_BASE=$(echo
|
|
382
|
-
FALLOW_SCOPE_ARGS=(--changed-since
|
|
386
|
+
if [ "$FALLOW_SCOPE" = "phase" ]; then
|
|
387
|
+
FALLOW_PHASE_COMMITS=$(git log --oneline --all --grep="${PADDED_PHASE}" --format="%H" 2>/dev/null)
|
|
388
|
+
if [ -n "$FALLOW_PHASE_COMMITS" ]; then
|
|
389
|
+
FALLOW_BASE=$(echo "$FALLOW_PHASE_COMMITS" | tail -1)^
|
|
390
|
+
FALLOW_SCOPE_ARGS=(--changed-since "$FALLOW_BASE")
|
|
383
391
|
fi
|
|
384
392
|
fi
|
|
385
393
|
|
|
386
|
-
timeout 120
|
|
394
|
+
timeout 120 "$FALLOW_BIN" audit --format json --quiet --max-crap "$FALLOW_MAX_CRAP" "${FALLOW_SCOPE_ARGS[@]+"${FALLOW_SCOPE_ARGS[@]}"}" > "${FALLOW_JSON_PATH}.tmp" 2>"$FALLOW_STDERR_TMP"
|
|
387
395
|
FALLOW_EXIT=$?
|
|
388
396
|
|
|
389
397
|
# fallow exits 0 (clean) or 1 (issues found) — BOTH are successful runs that produce a
|
|
390
398
|
# valid JSON report. Only a timeout (124), usage error (2), or crash yields no usable JSON.
|
|
391
399
|
# Decide success by whether the output parses as a fallow report, not by exit code.
|
|
392
|
-
FALLOW_OK=$(FALLOW_TMP
|
|
400
|
+
FALLOW_OK=$(FALLOW_TMP="${FALLOW_JSON_PATH}.tmp" node -e "
|
|
393
401
|
try {
|
|
394
402
|
const fs = require('fs');
|
|
395
403
|
const txt = fs.readFileSync(process.env.FALLOW_TMP, 'utf8');
|
|
396
404
|
const o = JSON.parse(txt);
|
|
397
405
|
process.stdout.write(o && typeof o === 'object' && 'verdict' in o ? '1' : '0');
|
|
398
406
|
} catch { process.stdout.write('0'); }
|
|
399
|
-
|
|
400
|
-
if [
|
|
401
|
-
FALLOW_STDERR_SUMMARY=$(head -5
|
|
402
|
-
rm -f
|
|
403
|
-
echo
|
|
404
|
-
FALLOW_JSON_PATH
|
|
407
|
+
")
|
|
408
|
+
if [ "$FALLOW_OK" != "1" ]; then
|
|
409
|
+
FALLOW_STDERR_SUMMARY=$(head -5 "$FALLOW_STDERR_TMP")
|
|
410
|
+
rm -f "${FALLOW_JSON_PATH}.tmp" "$FALLOW_STDERR_TMP"
|
|
411
|
+
echo "WARNING: fallow structural pre-pass failed (exit ${FALLOW_EXIT}): ${FALLOW_STDERR_SUMMARY}"
|
|
412
|
+
FALLOW_JSON_PATH=""
|
|
405
413
|
else
|
|
406
|
-
mv
|
|
407
|
-
rm -f
|
|
414
|
+
mv "${FALLOW_JSON_PATH}.tmp" "$FALLOW_JSON_PATH"
|
|
415
|
+
rm -f "$FALLOW_STDERR_TMP"
|
|
408
416
|
fi
|
|
409
417
|
```
|
|
410
418
|
|
|
@@ -477,6 +485,16 @@ if [ -n "$FALLOW_JSON_PATH" ] && [ -f "$FALLOW_JSON_PATH" ]; then
|
|
|
477
485
|
fi
|
|
478
486
|
```
|
|
479
487
|
|
|
488
|
+
Build practices contribution block for agent:
|
|
489
|
+
```bash
|
|
490
|
+
PRACTICES_CONTRIBUTION_BLOCK=""
|
|
491
|
+
if [ -n "$PRACTICES_CONTRIBUTION_FRAGMENT" ]; then
|
|
492
|
+
# Escape any literal closing tag before embedding; the closing tag literal is escaped so text arriving from outside this workflow cannot end the block early and steer the reviewer.
|
|
493
|
+
SAFE_PRACTICES_FRAGMENT=$(echo "$PRACTICES_CONTRIBUTION_FRAGMENT" | sed 's#</practices_contribution>#<\/practices_contribution>#g')
|
|
494
|
+
PRACTICES_CONTRIBUTION_BLOCK=$(printf '<practices_contribution>\n%s\n</practices_contribution>\n' "$SAFE_PRACTICES_FRAGMENT")
|
|
495
|
+
fi
|
|
496
|
+
```
|
|
497
|
+
|
|
480
498
|
Spawn the pp-code-reviewer agent:
|
|
481
499
|
|
|
482
500
|
Print: `◆ Spawning code reviewer... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)`
|
|
@@ -489,6 +507,8 @@ ${FILES_TO_READ}
|
|
|
489
507
|
|
|
490
508
|
${STRUCTURAL_FINDINGS_BLOCK}
|
|
491
509
|
|
|
510
|
+
${PRACTICES_CONTRIBUTION_BLOCK}
|
|
511
|
+
|
|
492
512
|
<config>
|
|
493
513
|
depth: ${REVIEW_DEPTH}
|
|
494
514
|
phase_dir: ${PHASE_DIR}
|
|
@@ -549,6 +569,30 @@ fi
|
|
|
549
569
|
```
|
|
550
570
|
</step>
|
|
551
571
|
|
|
572
|
+
<step name="check_practice_boundaries">
|
|
573
|
+
This is a safety report about untrusted client vault text already written to `${REVIEW_PATH}`, ahead of `dispatch_fix`, and never skipped when `--fix` short-circuits `present_results`. A boundary shortfall is a defect in the report, not the reviewed code, so this check never changes an exit status — failing the review would punish the wrong artifact. The detection logic lives in `src/practices-review-check.cts`, a compiled module whose own test suite (`tests/practices-review-check.test.cjs`) pins the property that a malformed finding can only ever change its own verdict, never a sibling's.
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
pp_run check practices-review --review "${REVIEW_PATH}" --boundaries 2>/dev/null || true
|
|
577
|
+
```
|
|
578
|
+
</step>
|
|
579
|
+
|
|
580
|
+
<step name="check_unfenced_vault_quotes">
|
|
581
|
+
Safety report about client vault text already in `${REVIEW_PATH}`, scoped to the WHOLE file — the sibling above cannot see a verbatim client quote unfenced in narrative prose. The comparison corpus is the client's own words from inside the boundary lines only, never the fixed reference-data sentences the check itself prepends to every excerpt. Reports findings only and never changes an exit status — a quote shortfall is a defect in the review, not the reviewed code. The detection logic lives in the same compiled `src/practices-review-check.cts` module as the sibling step above, with its own pinned regression tests.
|
|
582
|
+
|
|
583
|
+
**Resolve and compare in one block**, deliberately: the block resolves and consumes the corpus in one invocation AND decides whether to attempt the resolution from inside that same invocation, because a gate read from another fenced block is the identical failure the merge exists to prevent. `REVIEW_FILES` and `REVIEW_PATH` are still read from earlier in the workflow — they are this step's actual inputs, not its gate — and both now degrade visibly rather than silently: an empty `REVIEW_PATH` produces an operator-visible usage line instead of a byte-identical clean result (the CLI seam's empty-value guard, T-27-R8-05), and an empty or unset `REVIEW_FILES` produces its own one-line notice from inside this same block, before the corpus is ever resolved — an unresolved comparison corpus is never mistaken for a clean result. A test harness can supply both `REVIEW_PATH` and `VAULT_QUOTE_CORPUS` directly, no vault/matcher/model:
|
|
584
|
+
|
|
585
|
+
```bash
|
|
586
|
+
VAULT_QUOTE_CORPUS=""
|
|
587
|
+
if [ -z "${REVIEW_FILES[@]+x}" ]; then
|
|
588
|
+
echo "practices quote check: no review files in scope — corpus not resolved"
|
|
589
|
+
elif ! VAULT_QUOTE_CORPUS=$(pp_run vault practices match "${REVIEW_FILES[@]}" --raw 2>/dev/null); then
|
|
590
|
+
VAULT_QUOTE_CORPUS=""
|
|
591
|
+
fi
|
|
592
|
+
VAULT_QUOTE_CORPUS="${VAULT_QUOTE_CORPUS}" pp_run check practices-review --review "${REVIEW_PATH}" --quotes 2>/dev/null || true
|
|
593
|
+
```
|
|
594
|
+
</step>
|
|
595
|
+
|
|
552
596
|
<step name="dispatch_fix">
|
|
553
597
|
If the `--fix` flag was passed (`FIX_FLAG=true`), delegate to the `code-review-fix.md` workflow
|
|
554
598
|
to auto-apply findings from the REVIEW.md that was just written (or that already existed).
|
|
@@ -612,6 +656,15 @@ INFO=$(echo "$FRONTMATTER" | grep "info:" | head -1 | cut -d: -f2 | xargs)
|
|
|
612
656
|
TOTAL=$(echo "$FRONTMATTER" | grep "total:" | head -1 | cut -d: -f2 | xargs)
|
|
613
657
|
```
|
|
614
658
|
|
|
659
|
+
The six lines above take the first match over the whole frontmatter and therefore read the bugs/security numbers only — this is why the practices key below is read from its own slice and written after the existing counts (D-07):
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
# Scoped read: the practices: block only, so its total: line can never collide with
|
|
663
|
+
# the bugs/security first-match extraction above.
|
|
664
|
+
PRACTICES_FRONTMATTER_BLOCK=$(printf '%s\n' "$FRONTMATTER" | sed -n -e '/^practices:/,/^[^[:space:]]/{' -e '/^practices:/p' -e '/^[[:space:]]/p' -e '}')
|
|
665
|
+
PRACTICES_TOTAL=$(echo "$PRACTICES_FRONTMATTER_BLOCK" | grep -E "^[[:space:]]*total:" | head -1 | cut -d: -f2 | xargs)
|
|
666
|
+
```
|
|
667
|
+
|
|
615
668
|
Display inline summary to user:
|
|
616
669
|
|
|
617
670
|
```
|
|
@@ -630,10 +683,13 @@ Display inline summary to user:
|
|
|
630
683
|
Info: ${INFO}
|
|
631
684
|
──────────
|
|
632
685
|
Total: ${TOTAL}
|
|
686
|
+
${PRACTICES_TOTAL:+ Practices (advisory): ${PRACTICES_TOTAL}}
|
|
633
687
|
|
|
634
688
|
───────────────────────────────────────────────────────────────
|
|
635
689
|
```
|
|
636
690
|
|
|
691
|
+
When `PRACTICES_TOTAL` is empty, omit the Practices line entirely — the block renders exactly as it did before this phase, with no blank line left in its place.
|
|
692
|
+
|
|
637
693
|
If status is "clean":
|
|
638
694
|
```
|
|
639
695
|
✓ No issues found. All ${FILES_REVIEWED} files pass review at ${REVIEW_DEPTH} depth.
|
|
@@ -133,6 +133,7 @@ Verify the work is ready to ship:
|
|
|
133
133
|
ℹ Ship gate advisory ({hook.check.query}): {status}{GATE_RESULT.severity ? ` — ${GATE_RESULT.severity} drift detected` : ''} (non-blocking or below blocking threshold). Continuing.
|
|
134
134
|
```
|
|
135
135
|
- If `status` is `pass` or `inert`: continue silently.
|
|
136
|
+
- Independently of the status arms above, if `GATE_RESULT.message` is a non-empty string, print it verbatim — it is already operator-ready, possibly multi-line text — then continue: a gate that has something to tell the operator says it on every run, not only when its status is a warning.
|
|
136
137
|
</step>
|
|
137
138
|
|
|
138
139
|
<step name="push_branch">
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<purpose>
|
|
2
|
+
Take one local practice file from preview to a confirmed, pushed promotion into the client
|
|
3
|
+
vault — and nothing more. This workflow is the single human gate in front of an
|
|
4
|
+
irreversible outward action: it shows the operator the complete preview, asks once whether
|
|
5
|
+
to proceed, and only on explicit approval re-invokes the same guarded CLI arm in confirmed
|
|
6
|
+
mode. It never authors, edits, or reformats the practice file, and it performs no direct git
|
|
7
|
+
operation of its own — every outward action happens inside the promote verb, which this
|
|
8
|
+
workflow only calls.
|
|
9
|
+
</purpose>
|
|
10
|
+
|
|
11
|
+
<required_reading>
|
|
12
|
+
Read all files referenced by the invoking prompt's execution_context before starting.
|
|
13
|
+
</required_reading>
|
|
14
|
+
|
|
15
|
+
<process>
|
|
16
|
+
|
|
17
|
+
<step name="text_mode">
|
|
18
|
+
Resolve `TEXT_MODE` here, before the first question, mirroring the same resolution `init.md`
|
|
19
|
+
does before its own first prompt. Set `TEXT_MODE=true` if `--text` is present in
|
|
20
|
+
`$ARGUMENTS`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call in this
|
|
21
|
+
workflow — including the gate in Step 4 — with a plain-text numbered list, and ask the
|
|
22
|
+
operator to type their choice number. This is required because non-Claude runtimes have no
|
|
23
|
+
interactive question tool, and the confirmation in Step 4 is the entire point of this
|
|
24
|
+
workflow — it must degrade to plain text, never disappear.
|
|
25
|
+
</step>
|
|
26
|
+
|
|
27
|
+
<step name="resolve_launcher">
|
|
28
|
+
Resolve the `pp_run` launcher shim before any other step, so every call below goes through
|
|
29
|
+
the repo-local or installed `pp-tools` binary rather than assuming a global install:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
_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
|
|
33
|
+
```
|
|
34
|
+
</step>
|
|
35
|
+
|
|
36
|
+
<step name="resolve_input">
|
|
37
|
+
Take the practice file path from `$ARGUMENTS` (the first token that is not `--text`). When
|
|
38
|
+
none was given, ask the operator for one with a plain prompt:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
Path to the local practice file to promote (e.g. practices/my-practice.md):
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Do not compose, author, rewrite, or reformat the practice file at this or any later step —
|
|
45
|
+
authoring stays outside this guarded path entirely. Whatever bytes are on disk right now at
|
|
46
|
+
that path are exactly the bytes that will be previewed and, if approved, pushed.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
PRACTICE_FILE="<the resolved path>"
|
|
50
|
+
```
|
|
51
|
+
</step>
|
|
52
|
+
|
|
53
|
+
<step name="preview">
|
|
54
|
+
Run the promote verb with the file path and `--raw`, capturing stdout, stderr, and the exit
|
|
55
|
+
code. This invocation performs no outward action — it only clones into a disposable
|
|
56
|
+
directory, computes the diff, and reports.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
PROMOTE_PREVIEW_OUT=$(mktemp)
|
|
60
|
+
PROMOTE_PREVIEW_ERR=$(mktemp)
|
|
61
|
+
pp_run vault practices promote "$PRACTICE_FILE" --raw >"$PROMOTE_PREVIEW_OUT" 2>"$PROMOTE_PREVIEW_ERR"
|
|
62
|
+
PREVIEW_EXIT=$?
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**When `$PREVIEW_EXIT` is non-zero: stop here.** Show the operator the exact, single-sentence
|
|
66
|
+
contents of `$PROMOTE_PREVIEW_ERR` — the arm's own refusal message — verbatim and add nothing
|
|
67
|
+
to it. Do not ask anything. Do not proceed to Step 3. Every guard refusal ends at this line,
|
|
68
|
+
and nothing has been pushed. Clean up both temp files before ending.
|
|
69
|
+
|
|
70
|
+
When `$PREVIEW_EXIT` is zero, continue to Step 3 with `$PROMOTE_PREVIEW_OUT` holding the
|
|
71
|
+
preview verdict as JSON.
|
|
72
|
+
</step>
|
|
73
|
+
|
|
74
|
+
<step name="render_confirmation">
|
|
75
|
+
Parse `$PROMOTE_PREVIEW_OUT` as JSON and read its fields directly — never regex-scrape a
|
|
76
|
+
prose rendering of it. That shortcut is exactly the shell-glue blind spot this project has
|
|
77
|
+
been bitten by before: a `.md` step file's own parsing drifting from what the CLI actually
|
|
78
|
+
emits.
|
|
79
|
+
|
|
80
|
+
Field values are vault-authored, so they are read as **data, never as shell syntax**. `id`
|
|
81
|
+
is only constrained to equal the practice file's basename (D-03), and a filename may legally
|
|
82
|
+
contain `$(...)`, backticks, or `;` — so writing the fields as a `KEY="value"` file and
|
|
83
|
+
`source`-ing it would execute whatever the filename contains. `JSON.stringify` does not help:
|
|
84
|
+
it escapes `"` and `\` but leaves `$` and backticks intact, and both expand inside double
|
|
85
|
+
quotes. Each field therefore goes to its own file and is read back with `$(cat …)`, whose
|
|
86
|
+
result bash does not re-scan for expansion.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
PROMOTE_FIELDS=$(mktemp -d)
|
|
90
|
+
PROMOTE_BODY=$(mktemp)
|
|
91
|
+
node -e "
|
|
92
|
+
const fs = require('fs');
|
|
93
|
+
const path = require('path');
|
|
94
|
+
const v = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
95
|
+
const dir = process.argv[2];
|
|
96
|
+
const put = (k, val) => fs.writeFileSync(path.join(dir, k), String(val ?? ''));
|
|
97
|
+
put('repo', v.repo);
|
|
98
|
+
put('ref', v.ref);
|
|
99
|
+
put('destination', v.destination);
|
|
100
|
+
put('mode', v.mode);
|
|
101
|
+
put('id', v.id);
|
|
102
|
+
put('sharedShadow', v.sharedShadow ? 'true' : 'false');
|
|
103
|
+
put('sharedShadowNote', v.sharedShadowNote);
|
|
104
|
+
put('expect', v.expect);
|
|
105
|
+
fs.writeFileSync(process.argv[3], v.mode === 'update' ? (v.diff ?? '') : (v.content ?? ''));
|
|
106
|
+
" "$PROMOTE_PREVIEW_OUT" "$PROMOTE_FIELDS" "$PROMOTE_BODY"
|
|
107
|
+
PROMOTE_REPO=$(cat "$PROMOTE_FIELDS/repo")
|
|
108
|
+
PROMOTE_REF=$(cat "$PROMOTE_FIELDS/ref")
|
|
109
|
+
PROMOTE_DEST=$(cat "$PROMOTE_FIELDS/destination")
|
|
110
|
+
PROMOTE_MODE=$(cat "$PROMOTE_FIELDS/mode")
|
|
111
|
+
PROMOTE_ID=$(cat "$PROMOTE_FIELDS/id")
|
|
112
|
+
PROMOTE_SHARED=$(cat "$PROMOTE_FIELDS/sharedShadow")
|
|
113
|
+
PROMOTE_SHARED_NOTE=$(cat "$PROMOTE_FIELDS/sharedShadowNote")
|
|
114
|
+
PROMOTE_EXPECT=$(cat "$PROMOTE_FIELDS/expect")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Show the operator, in this exact order, reading every value from the parsed fields above —
|
|
118
|
+
never truncated, summarized, or collapsed to "the first N lines":
|
|
119
|
+
|
|
120
|
+
1. The target vault repository (`$PROMOTE_REPO`) and ref (`$PROMOTE_REF`).
|
|
121
|
+
2. The destination path inside that repository (`$PROMOTE_DEST`).
|
|
122
|
+
3. Whether this **creates** a new practice or **updates** an existing one
|
|
123
|
+
(`$PROMOTE_MODE`, for id `$PROMOTE_ID`).
|
|
124
|
+
4. When `$PROMOTE_SHARED` is `true`, the shared-vault disclosure sentence verbatim from
|
|
125
|
+
`$PROMOTE_SHARED_NOTE` — this id also exists in the shared Pareto vault, and promoting it
|
|
126
|
+
here makes the client's version the one that applies for this client.
|
|
127
|
+
5. One line stating plainly that the secret scan already run inside the promote verb found
|
|
128
|
+
nothing in this file — a file that failed that scan would have refused before a preview
|
|
129
|
+
was ever produced, so reaching this line already proves the scan passed.
|
|
130
|
+
6. The complete contents of `$PROMOTE_BODY` — the full diff when `$PROMOTE_MODE` is
|
|
131
|
+
`update`, or the full new-file content when it is `create`. Show every byte; the operator
|
|
132
|
+
is about to approve exactly these bytes, so nothing here may be shortened.
|
|
133
|
+
</step>
|
|
134
|
+
|
|
135
|
+
<step name="gate">
|
|
136
|
+
Ask the operator with `AskUserQuestion` (or a numbered plain-text prompt when `TEXT_MODE` is
|
|
137
|
+
active) whether to push this to the client vault. Offer exactly two outcomes — there is no
|
|
138
|
+
default, no timeout, and no auto-advance, because this is an irreversible outward action and
|
|
139
|
+
this confirmation is the whole review step:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
AskUserQuestion([
|
|
143
|
+
{
|
|
144
|
+
header: "Push to client vault",
|
|
145
|
+
question: "Push this practice to the client vault now?",
|
|
146
|
+
multiSelect: false,
|
|
147
|
+
options: [
|
|
148
|
+
{ label: "Push it", description: "Push exactly the diff/content shown above" },
|
|
149
|
+
{ label: "Stop", description: "Do not push anything — the vault stays unchanged" }
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
])
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**When the operator chooses to stop:** print one sentence saying nothing was pushed and the
|
|
156
|
+
client vault is unchanged, clean up the temp files, and end the workflow here.
|
|
157
|
+
|
|
158
|
+
**When the operator chooses to push:** continue to Step 5.
|
|
159
|
+
</step>
|
|
160
|
+
|
|
161
|
+
<step name="push">
|
|
162
|
+
Only inside this approved branch, run the promote verb again with the same file path, the
|
|
163
|
+
confirmed flag, the expect token carried verbatim from the preview verdict, and `--raw`. This
|
|
164
|
+
workflow never runs an outward git operation itself — the only thing it does here is invoke
|
|
165
|
+
this verb a second time; every push, every commit, and every other outward action happens
|
|
166
|
+
inside the verb, not in this shell block.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
PROMOTE_PUSH_OUT=$(mktemp)
|
|
170
|
+
PROMOTE_PUSH_ERR=$(mktemp)
|
|
171
|
+
pp_run vault practices promote "$PRACTICE_FILE" --confirmed --expect "$PROMOTE_EXPECT" --raw >"$PROMOTE_PUSH_OUT" 2>"$PROMOTE_PUSH_ERR"
|
|
172
|
+
PUSH_EXIT=$?
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**When `$PUSH_EXIT` is non-zero:** show the arm's own single-sentence refusal from
|
|
176
|
+
`$PROMOTE_PUSH_ERR` verbatim and end — the arm has already guaranteed nothing was pushed on a
|
|
177
|
+
refusal. A concurrent-edit refusal here is expected, ordinary behavior: it means someone else
|
|
178
|
+
changed this same practice in the client vault between the preview in Step 2 and this push.
|
|
179
|
+
The remedy is simply to run this workflow again from the top — the new preview will diff
|
|
180
|
+
against their current version. Clean up all temp files before ending.
|
|
181
|
+
|
|
182
|
+
When `$PUSH_EXIT` is zero, continue to Step 6 with `$PROMOTE_PUSH_OUT` holding the push
|
|
183
|
+
verdict as JSON.
|
|
184
|
+
</step>
|
|
185
|
+
|
|
186
|
+
<step name="report">
|
|
187
|
+
Parse `$PROMOTE_PUSH_OUT` the same way Step 3 parsed the preview verdict — direct field
|
|
188
|
+
reads, never a prose scrape:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
node -e "
|
|
192
|
+
const fs = require('fs');
|
|
193
|
+
const v = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
194
|
+
process.stdout.write(
|
|
195
|
+
'repo=' + (v.repo ?? '') + '\n' +
|
|
196
|
+
'destination=' + (v.destination ?? '') + '\n' +
|
|
197
|
+
'commit=' + (v.commit ?? '') + '\n' +
|
|
198
|
+
'cacheRefreshed=' + String(!!v.cacheRefreshed) + '\n'
|
|
199
|
+
);
|
|
200
|
+
" "$PROMOTE_PUSH_OUT"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Show the operator: the repository, the destination path, the new commit the push produced,
|
|
204
|
+
and whether the local read cache of the vault was refreshed. When the local cache refresh did
|
|
205
|
+
not succeed, say so plainly and note that the next vault fetch will pick up the change — this
|
|
206
|
+
is a cosmetic staleness in the local cache only, not a sign the push failed.
|
|
207
|
+
|
|
208
|
+
Clean up every temp path created in this workflow (`$PROMOTE_PREVIEW_OUT`,
|
|
209
|
+
`$PROMOTE_PREVIEW_ERR`, `$PROMOTE_BODY`, `$PROMOTE_PUSH_OUT`, `$PROMOTE_PUSH_ERR`, and the
|
|
210
|
+
`$PROMOTE_FIELDS` **directory**, which needs `rm -rf`) before ending.
|
|
211
|
+
</step>
|
|
212
|
+
|
|
213
|
+
</process>
|
|
@@ -1232,29 +1232,40 @@ function validateGate(gate, prefix) {
|
|
|
1232
1232
|
/**
|
|
1233
1233
|
* Validate per-capability contract constraints against the Loop Host Contract.
|
|
1234
1234
|
* This covers:
|
|
1235
|
-
* - contribution.into ∈ step's agentRoles
|
|
1235
|
+
* - contribution.into ∈ step's agentRoles, OR contribution.into names an agent
|
|
1236
|
+
* already registered by some capability's own `agents` array (a non-pipeline
|
|
1237
|
+
* consumer of the same loop point, e.g. code-review.md reading execute:post
|
|
1238
|
+
* alongside execute-phase.md — see capabilities/vault-practices's
|
|
1239
|
+
* execute:post -> pp-code-reviewer contribution)
|
|
1236
1240
|
* - when references a config key in cap.config
|
|
1237
1241
|
*
|
|
1238
1242
|
* NOTE: step.consumes satisfiability is NOT checked here — it requires the full
|
|
1239
1243
|
* set of validated capabilities (cross-capability produces). It runs in
|
|
1240
1244
|
* validateConsumesGlobal() after loadAndValidate builds capMap.
|
|
1241
1245
|
*
|
|
1242
|
-
* @param {object} cap
|
|
1243
|
-
* @param {string} capId
|
|
1246
|
+
* @param {object} cap Validated capability object
|
|
1247
|
+
* @param {string} capId Capability id (for error messages)
|
|
1248
|
+
* @param {Set<string>} [knownAgentIds] Every agent id declared by any capability's
|
|
1249
|
+
* `agents` array, precomputed once across the whole capabilities/ tree
|
|
1250
|
+
* (order-independent — does not require the target capability to have
|
|
1251
|
+
* already passed validation). Defaults to an empty set so existing callers
|
|
1252
|
+
* that validate a single in-memory fixture are unaffected.
|
|
1244
1253
|
*/
|
|
1245
|
-
function validateAgainstContract(cap, capId) {
|
|
1254
|
+
function validateAgainstContract(cap, capId, knownAgentIds) {
|
|
1246
1255
|
if (cap.role !== 'feature') return [];
|
|
1247
1256
|
const errors = [];
|
|
1248
1257
|
const prefix = 'capability "' + capId + '"';
|
|
1258
|
+
const agentIds = knownAgentIds instanceof Set ? knownAgentIds : new Set();
|
|
1249
1259
|
|
|
1250
|
-
// contribution.into must be in the step's agentRoles
|
|
1260
|
+
// contribution.into must be in the step's agentRoles, or name a registered agent
|
|
1251
1261
|
for (const contrib of cap.contributions) {
|
|
1252
1262
|
if (!VALID_LOOP_POINTS.has(contrib.point)) continue; // already reported
|
|
1253
1263
|
const contract = POINT_TO_CONTRACT.get(contrib.point);
|
|
1254
|
-
if (contract && !contract.agentRoles.includes(contrib.into)) {
|
|
1264
|
+
if (contract && !contract.agentRoles.includes(contrib.into) && !agentIds.has(contrib.into)) {
|
|
1255
1265
|
errors.push(
|
|
1256
1266
|
prefix + ' contribution.into "' + contrib.into + '" at point "' + contrib.point +
|
|
1257
|
-
'" is not in the step\'s agentRoles [' + contract.agentRoles.join(', ') +
|
|
1267
|
+
'" is not in the step\'s agentRoles [' + contract.agentRoles.join(', ') +
|
|
1268
|
+
'] and is not a registered agent id',
|
|
1258
1269
|
);
|
|
1259
1270
|
}
|
|
1260
1271
|
}
|
|
@@ -2055,6 +2066,49 @@ function validateHooksWired(cap, wiredSet) {
|
|
|
2055
2066
|
|
|
2056
2067
|
// ─── Registry builder ─────────────────────────────────────────────────────────
|
|
2057
2068
|
|
|
2069
|
+
/**
|
|
2070
|
+
* Best-effort, order-independent pre-scan of every capability.json's `agents`
|
|
2071
|
+
* array, across the whole capabilities/ tree. Used by validateAgainstContract
|
|
2072
|
+
* to recognize a contribution.into target that names an agent registered by
|
|
2073
|
+
* some OTHER capability (not just the roles of the point's own owning step
|
|
2074
|
+
* workflow) — e.g. code-review.md is a non-pipeline consumer of the same
|
|
2075
|
+
* execute:post loop point execute-phase.md owns, and pp-code-reviewer is
|
|
2076
|
+
* registered as an agent by capabilities/code-review, not by an agent-role in
|
|
2077
|
+
* execute-phase.md's <!-- gsd:loop-host --> marker.
|
|
2078
|
+
*
|
|
2079
|
+
* Deliberately tolerant of malformed/missing files here — real validation
|
|
2080
|
+
* errors for those files are reported by the main loadAndValidate loop; this
|
|
2081
|
+
* pre-scan only needs the `agents` array, never blocks on anything else.
|
|
2082
|
+
*
|
|
2083
|
+
* @param {string} capabilitiesDir
|
|
2084
|
+
* @returns {Set<string>}
|
|
2085
|
+
*/
|
|
2086
|
+
function collectDeclaredAgentIds(capabilitiesDir) {
|
|
2087
|
+
const agentIds = new Set();
|
|
2088
|
+
if (!fs.existsSync(capabilitiesDir)) return agentIds;
|
|
2089
|
+
|
|
2090
|
+
const folderEntries = fs.readdirSync(capabilitiesDir, { withFileTypes: true })
|
|
2091
|
+
.filter((e) => e.isDirectory())
|
|
2092
|
+
.map((e) => e.name);
|
|
2093
|
+
|
|
2094
|
+
for (const folderId of folderEntries) {
|
|
2095
|
+
const capPath = path.join(capabilitiesDir, folderId, 'capability.json');
|
|
2096
|
+
if (!fs.existsSync(capPath)) continue;
|
|
2097
|
+
let cap;
|
|
2098
|
+
try {
|
|
2099
|
+
cap = JSON.parse(fs.readFileSync(capPath, 'utf8'));
|
|
2100
|
+
} catch {
|
|
2101
|
+
continue; // malformed JSON reported elsewhere
|
|
2102
|
+
}
|
|
2103
|
+
if (Array.isArray(cap.agents)) {
|
|
2104
|
+
for (const agent of cap.agents) {
|
|
2105
|
+
if (typeof agent === 'string' && agent.length > 0) agentIds.add(agent);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return agentIds;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2058
2112
|
/**
|
|
2059
2113
|
* Read + validate all capabilities/<id>/capability.json files.
|
|
2060
2114
|
* Returns { capMap, errors } where capMap is Map<id, cap>.
|
|
@@ -2078,6 +2132,12 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
|
2078
2132
|
// scan is not repeated per-capability. ROOT is the repo root (defined at top of file).
|
|
2079
2133
|
const wiredSet = getWiredLoopPoints(ROOT);
|
|
2080
2134
|
|
|
2135
|
+
// Pre-scan every capability's `agents` array ONCE, order-independent, so
|
|
2136
|
+
// validateAgainstContract can recognize a contribution.into target that
|
|
2137
|
+
// names a registered agent even when that agent's owning capability sorts
|
|
2138
|
+
// after the current one alphabetically.
|
|
2139
|
+
const knownAgentIds = collectDeclaredAgentIds(resolvedCapDir);
|
|
2140
|
+
|
|
2081
2141
|
const folderEntries = fs.readdirSync(resolvedCapDir, { withFileTypes: true })
|
|
2082
2142
|
.filter((e) => e.isDirectory())
|
|
2083
2143
|
.map((e) => e.name)
|
|
@@ -2101,7 +2161,7 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
|
2101
2161
|
continue; // skip cross-validation if basic schema fails
|
|
2102
2162
|
}
|
|
2103
2163
|
|
|
2104
|
-
const contractErrors = validateAgainstContract(cap, cap.id);
|
|
2164
|
+
const contractErrors = validateAgainstContract(cap, cap.id, knownAgentIds);
|
|
2105
2165
|
if (contractErrors.length > 0) {
|
|
2106
2166
|
for (const e of contractErrors) errors.push(folderId + '/capability.json: ' + e);
|
|
2107
2167
|
// Fix #6: do NOT add contract-invalid caps to capMap — validateCrossCapability should
|
|
@@ -326,5 +326,12 @@
|
|
|
326
326
|
"tests/workflow-size-budget.test.cjs :: source-text-is-the-product",
|
|
327
327
|
"tests/workspace.test.cjs :: source-text-is-the-product",
|
|
328
328
|
"tests/worktree-cleanup.test.cjs :: source-text-is-the-product",
|
|
329
|
-
"tests/worktree.test.cjs :: source-text-is-the-product"
|
|
329
|
+
"tests/worktree.test.cjs :: source-text-is-the-product",
|
|
330
|
+
"tests/comp-01-provider-off-by-default.test.cjs :: validates runtime CLI stdout/stderr/on-disk-config behavior, not source grep",
|
|
331
|
+
"tests/discovery-annotations.test.cjs :: source-text-is-the-product",
|
|
332
|
+
"tests/discovery-architecture-deliverables.test.cjs :: source-text-is-the-product",
|
|
333
|
+
"tests/discovery-to-build-canonical-refs.test.cjs :: source-text-is-the-product",
|
|
334
|
+
"tests/discovery-to-build-fixture.test.cjs :: source-text-is-the-product",
|
|
335
|
+
"tests/discovery-to-build-path-contract.test.cjs :: source-text-is-the-product",
|
|
336
|
+
"tests/release-smoke-discuss-plan-execute.install.test.cjs :: integration-test-input"
|
|
330
337
|
]
|
|
@@ -18,7 +18,9 @@ const path = require('path');
|
|
|
18
18
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
19
19
|
|
|
20
20
|
const MAX_LENGTH = 100;
|
|
21
|
-
|
|
21
|
+
// D-12 flat-hyphen rename (05-04): command files live flat at commands/pp-<cmd>.md,
|
|
22
|
+
// no longer under a legacy nested subdirectory (mirrors lint-command-contract.cjs's COMMANDS_DIR).
|
|
23
|
+
const COMMANDS_DIR = path.join(__dirname, '..', 'commands');
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Parse the description field from frontmatter in a .md file.
|
|
@@ -53,6 +53,11 @@ const ALLOWLIST_FILES = new Set([
|
|
|
53
53
|
'README.ko-KR.md',
|
|
54
54
|
'README.pt-BR.md',
|
|
55
55
|
'README.zh-CN.md',
|
|
56
|
+
// Generated snapshot of every grandfathered legacy-token reference in the tree,
|
|
57
|
+
// so it quotes those lines by construction — including this file's own comment
|
|
58
|
+
// above. Scanning it here is circular: the baseline cannot carry per-line allow
|
|
59
|
+
// markers without breaking its byte-for-byte match against the real sources.
|
|
60
|
+
'tests/rename-guard-baseline.json',
|
|
56
61
|
]);
|
|
57
62
|
|
|
58
63
|
const ALLOWLIST_DIR_PREFIXES = [
|
|
@@ -179,12 +179,21 @@
|
|
|
179
179
|
},
|
|
180
180
|
"vault": {
|
|
181
181
|
"files": [
|
|
182
|
+
"vault-practices.property.test.cjs",
|
|
182
183
|
"vault-read-path.test.cjs",
|
|
183
184
|
"vault-security.test.cjs",
|
|
184
185
|
"vault-status-check.test.cjs",
|
|
185
186
|
"vault.test.cjs"
|
|
186
187
|
],
|
|
187
|
-
"issue": "v1.7.0 Client Vault Load — four distinct concerns: vault.test (fetch/status/summary unit), vault-security (VSEC-02/03 write-guard + secret/fence), vault-read-path (VGATE-03 SUMMARY pointer), vault-status-check (VGATE-02 gate-query handler)"
|
|
188
|
+
"issue": "v1.7.0 Client Vault Load — four distinct concerns: vault.test (fetch/status/summary unit), vault-security (VSEC-02/03 write-guard + secret/fence), vault-read-path (VGATE-03 SUMMARY pointer), vault-status-check (VGATE-02 gate-query handler). Phase 24-02 added vault-practices.property.test.cjs (fast-check fuzz of the practices glob-fallback compiler + a practices-frontmatter round-trip, PRAC-02) — it is grouped under this bucket rather than \"vault-practices\" because the test-file-count lint's hyphen-prefix matcher treats \"vault-practices.property\" (period before \"property\") as starting with \"vault-\" before it can match the more specific \"vault-practices-\" prefix; the module it actually tests is pp-core/bin/lib/vault-practices.cjs, not vault.cjs."
|
|
189
|
+
},
|
|
190
|
+
"vault-practices": {
|
|
191
|
+
"files": [
|
|
192
|
+
"vault-practices-examples.test.cjs",
|
|
193
|
+
"vault-practices-matcher.test.cjs",
|
|
194
|
+
"vault-practices.test.cjs"
|
|
195
|
+
],
|
|
196
|
+
"issue": "Phase 24 (24-01/24-02/24-04) — three distinct concerns for pp-core/bin/lib/vault-practices.cjs: vault-practices.test (24-01 parser/validation/discovery unit tests), vault-practices-matcher.test (24-02 glob matcher + two-bucket result + report unit tests), vault-practices-examples.test (24-04 shipped-example drift guard + minimal-comments global-bucket smoke case, PRAC-03 Success Criterion 3). All three test the same production module and are each independently load-bearing; consolidating would blur distinct plan-level verification gates."
|
|
188
197
|
}
|
|
189
198
|
}
|
|
190
199
|
}
|