@pareto4/pp-core 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-code-reviewer.md +1 -1
  3. package/agents/pp-executor.md +2 -13
  4. package/agents/pp-planner.md +1 -1
  5. package/agents/pp-vault-reviewer.md +51 -0
  6. package/commands/pp-ns-review.md +2 -1
  7. package/commands/pp-vault-review.md +33 -0
  8. package/gemini-extension.json +1 -1
  9. package/package.json +1 -1
  10. package/pp-core/bin/lib/capability-registry.cjs +200 -10
  11. package/pp-core/bin/lib/check-command-router.cjs +1242 -37
  12. package/pp-core/bin/lib/clusters.cjs +1 -0
  13. package/pp-core/bin/lib/command-aliases.cjs +7 -0
  14. package/pp-core/bin/lib/compose-bindings.cjs +41 -27
  15. package/pp-core/bin/lib/decisions.cjs +104 -27
  16. package/pp-core/bin/lib/dispatch-plan.cjs +12 -1
  17. package/pp-core/bin/lib/frontmatter.cjs +37 -0
  18. package/pp-core/bin/lib/git-base-branch.cjs +136 -10
  19. package/pp-core/bin/lib/init-contract.cjs +180 -66
  20. package/pp-core/bin/lib/init-interactive.cjs +4 -20
  21. package/pp-core/bin/lib/loop-gates.cjs +213 -0
  22. package/pp-core/bin/lib/loop-resolver.cjs +107 -45
  23. package/pp-core/bin/lib/org-profile.cjs +218 -44
  24. package/pp-core/bin/lib/phase-command-router.cjs +7 -1
  25. package/pp-core/bin/lib/phase-manifest.cjs +527 -0
  26. package/pp-core/bin/lib/phase-tier.cjs +15 -2
  27. package/pp-core/bin/lib/phase.cjs +51 -1
  28. package/pp-core/bin/lib/vault-audit.cjs +2070 -0
  29. package/pp-core/bin/lib/vault-command-router.cjs +460 -1
  30. package/pp-core/bin/lib/vault-practices.cjs +1142 -104
  31. package/pp-core/bin/lib/vault-review.cjs +2359 -0
  32. package/pp-core/bin/lib/vault-trace.cjs +2831 -0
  33. package/pp-core/bin/lib/vault-update.cjs +266 -0
  34. package/pp-core/bin/lib/vault-write-back.cjs +19 -0
  35. package/pp-core/bin/lib/vault.cjs +277 -12
  36. package/pp-core/bin/pp-tools.cjs +30 -13
  37. package/pp-core/bin/shared/model-catalog.json +2 -1
  38. package/pp-core/references/executor-vault-trace.md +47 -0
  39. package/pp-core/references/loop-hook-dispatch.md +13 -2
  40. package/pp-core/references/planner-vault-practices.md +46 -1
  41. package/pp-core/references/planning-config.md +1 -1
  42. package/pp-core/templates/practices/README.md +140 -11
  43. package/pp-core/templates/project-charter.md +226 -0
  44. package/pp-core/workflows/add-tests.md +4 -0
  45. package/pp-core/workflows/audit-fix.md +2 -0
  46. package/pp-core/workflows/check-todos.md +1 -1
  47. package/pp-core/workflows/code-review-fix.md +2 -0
  48. package/pp-core/workflows/debug.md +4 -0
  49. package/pp-core/workflows/discovery-sprint/prefill.md +9 -0
  50. package/pp-core/workflows/discovery-sprint/prototype.md +2 -1
  51. package/pp-core/workflows/discovery-sprint/vault-fetch.md +37 -0
  52. package/pp-core/workflows/discovery-sprint.md +1 -1
  53. package/pp-core/workflows/discovery-to-build.md +1 -1
  54. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +1 -0
  55. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +19 -0
  56. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -1
  57. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +10 -8
  58. package/pp-core/workflows/execute-phase.md +22 -5
  59. package/pp-core/workflows/execute-plan.md +4 -0
  60. package/pp-core/workflows/fast.md +4 -0
  61. package/pp-core/workflows/help/modes/full.md +3 -2
  62. package/pp-core/workflows/init.md +2 -2
  63. package/pp-core/workflows/new-project.md +1 -1
  64. package/pp-core/workflows/plan-phase/steps/phase-tier.md +9 -0
  65. package/pp-core/workflows/plan-phase.md +7 -4
  66. package/pp-core/workflows/quick.md +4 -0
  67. package/pp-core/workflows/ship.md +5 -1
  68. package/pp-core/workflows/undo.md +1 -1
  69. package/pp-core/workflows/validate-phase.md +4 -0
  70. package/pp-core/workflows/vault-review.md +65 -0
  71. package/pp-core/workflows/verify-work.md +3 -0
  72. package/scripts/gen-capability-registry.cjs +11 -0
  73. package/scripts/lint-test-file-count.allowlist.json +10 -1
  74. package/scripts/prompt-injection-scan.sh +2 -0
@@ -1,4 +1,7 @@
1
1
  'use strict';
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  /**
3
6
  * Vault command router — CLI subcommand dispatcher for `pp-tools vault`.
4
7
  *
@@ -29,7 +32,14 @@
29
32
  */
30
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
34
  const io = require("./io.cjs");
35
+ const node_fs_1 = __importDefault(require("node:fs"));
36
+ const node_path_1 = __importDefault(require("node:path"));
32
37
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
38
+ // The trace render arm resolves its containment boundary from the project
39
+ // root. project-root.cjs is a leaf (node builtins only), so this adds no cycle.
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ const projectRootMod = require("./project-root.cjs");
42
+ const { findProjectRoot } = projectRootMod;
33
43
  const { ERROR_REASON } = io;
34
44
  // Default CoreModule implementation. _core seam overrides this for test injection.
35
45
  const _defaultCore = { output: io.output };
@@ -46,7 +56,86 @@ const VAULT_SUBCOMMANDS = Object.freeze([
46
56
  { name: 'practices', readsVerdict: false },
47
57
  { name: 'org-profile', readsVerdict: false },
48
58
  { name: 'probe', readsVerdict: false },
59
+ { name: 'trace', readsVerdict: false },
60
+ { name: 'audit', readsVerdict: false },
61
+ { name: 'update', readsVerdict: false },
62
+ { name: 'review', readsVerdict: false },
49
63
  ]);
64
+ /**
65
+ * The two trace stages and the two trace verbs, declared once: the `--stage`
66
+ * validation, the verb check and the operator-facing messages that name the
67
+ * permitted values are all derived from these lists, so they cannot disagree.
68
+ * The values are the frozen contract's (`docs/vault-trace-format.md`).
69
+ */
70
+ const TRACE_STAGES = ['plan', 'execute'];
71
+ const TRACE_VERBS = ['scaffold', 'render'];
72
+ /**
73
+ * The plan-identifier shape `vault trace scaffold --plan` accepts: a phase
74
+ * segment (digits, an optional letter, optional decimal parts) followed by one
75
+ * or more numeric segments — the plan-identifier grammar `agents/pp-planner.md`
76
+ * and `src/phase-id.cts` use. It is exactly the prefix of the artifact's own
77
+ * filename (`04-02` for `04-02-PLAN.md`, `02.3-01` for `02.3-01-PLAN.md`), so a
78
+ * trace gate comparing the recorded identifier with the filename can never be
79
+ * handed one the writer could not emit. It rejects what is not a plan
80
+ * identifier at all: a bare plan number (`01`), a task label (`T1`), a slug
81
+ * (`04-01-vault-trace`) or an empty string.
82
+ */
83
+ const SCAFFOLD_PLAN_ID_RE = /^\d+[A-Za-z]?(?:\.\d+)*(?:-\d+)+$/;
84
+ /**
85
+ * The required form, in ONE wording, stated in this rejection message and in
86
+ * `pp-core/references/planner-vault-practices.md`. ASCII only and no
87
+ * backticks, so the same bytes survive a JSON string, markdown and a terminal.
88
+ */
89
+ const SCAFFOLD_PLAN_ID_FORM = "The plan identifier must be the plan's phase-and-number identifier, exactly the " +
90
+ 'prefix of its artifact filename (for example 04-02), not a bare plan number, a task ' +
91
+ 'label or a slug.';
92
+ /**
93
+ * Bound on the head read the `trace render` arm performs. The trace lives in
94
+ * the artifact's FRONTMATTER, so a head read is the exact region the parser
95
+ * needs, and an artifact body of any size can never make this arm read more.
96
+ */
97
+ const TRACE_ARTIFACT_READ_BYTES = 262_144;
98
+ /** Repo-relative artifact paths, long enough for a workstream phase tree. */
99
+ const TRACE_REFUSAL_PATH_CAP = 200;
100
+ /** A `gitCommit` digest: lowercase hex at sha1 or sha256 length. */
101
+ const GIT_COMMIT_DIGEST_RE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
102
+ /**
103
+ * Bound and control-character-collapse a caller-supplied path before it
104
+ * reaches a refusal message: the message reaches an operator's terminal and,
105
+ * on the workflow path, an agent's context, so a hostile path must not forge
106
+ * an extra line of output nor run to arbitrary length.
107
+ */
108
+ function boundedPath(value) {
109
+ const collapsed = value
110
+ .replace(/[\u0000-\u001F\u007F]+/g, ' ')
111
+ .replace(/\s+/g, ' ')
112
+ .trim();
113
+ if (collapsed === '')
114
+ return '(an empty value)';
115
+ return collapsed.length > TRACE_REFUSAL_PATH_CAP
116
+ ? `${collapsed.slice(0, TRACE_REFUSAL_PATH_CAP)}…`
117
+ : collapsed;
118
+ }
119
+ /**
120
+ * Is the vault-practices capability fully active for this project? Both
121
+ * `vault.enabled` and `vault.practices_enabled` must be strictly `true` — the
122
+ * same requires-closure the promote preflight and `ship.practices` apply. The
123
+ * trace arms gate on this FIRST, before the flag allowlist, so an inert project
124
+ * stays byte-identically silent on every path, a mistyped one included.
125
+ */
126
+ function tracePracticesActive(cfgGet, cwd) {
127
+ return cfgGet(cwd, 'vault.enabled', false) === true && cfgGet(cwd, 'vault.practices_enabled', false) === true;
128
+ }
129
+ /** The value token after each of `flags`, by index, so a flag's value is never read as a positional. */
130
+ function flagValueIndices(argv, flags) {
131
+ const indices = new Set();
132
+ for (const flag of flags) {
133
+ const i = argv.indexOf(flag);
134
+ if (i !== -1)
135
+ indices.add(i + 1);
136
+ }
137
+ return indices;
138
+ }
50
139
  /**
51
140
  * Render a short plain-text `vault org-profile` report from its structured
52
141
  * shape — one line naming the pointer, one naming the resolution, a
@@ -98,7 +187,7 @@ function renderOrgProfileReport(report) {
98
187
  return lines.join('\n');
99
188
  }
100
189
  // ─── Implementation ───────────────────────────────────────────────────────────
101
- function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _exec, _writeBack, _orgProfile, _coordinates, _provider, }) {
190
+ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _exec, _writeBack, _orgProfile, _coordinates, _provider, _trace, _audit, _update, }) {
102
191
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
103
192
  const vault = _vault ?? require('./vault.cjs');
104
193
  const c = _core ?? _defaultCore;
@@ -268,6 +357,9 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
268
357
  [coordinates.SHARED_REF_CONFIG_KEY]: shared.ref,
269
358
  [coordinates.SHARED_HOST_CONFIG_KEY]: shared.host,
270
359
  [coordinates.PLATFORM_HOST_CONFIG_KEY]: platformHost.host,
360
+ // The policy row, through the same ladder the load gate reads, so a
361
+ // profile value that won is not reported as overridden by null.
362
+ [orgProfile.VAULT_REQUIRED_CONFIG_KEY]: String(orgProfile.resolveVaultRequired(cwd).required),
271
363
  };
272
364
  // Build `effective`'s key list from ORG_PROFILE_ALLOWLIST rather than a
273
365
  // fresh literal array, so a future allowlist change surfaces here
@@ -334,11 +426,378 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
334
426
  c.output(report, true, renderOrgProfileReport(report));
335
427
  }
336
428
  }
429
+ else if (subcommand === 'trace') {
430
+ routeTrace({ args, cwd, raw, error, vault, c, _exec, _trace });
431
+ }
432
+ else if (subcommand === 'audit') {
433
+ routeAudit({ args, cwd, raw, error, vault, c, _exec, _audit });
434
+ }
435
+ else if (subcommand === 'review') {
436
+ routeReview({ args, cwd, raw, c });
437
+ }
438
+ else if (subcommand === 'update') {
439
+ // `vault update [<tag>] [--yes]`: without --yes it reports the plan and
440
+ // writes nothing; see src/vault-update.cts for the contract.
441
+ const RECOGNIZED_UPDATE_FLAGS = new Set(['--raw', '--yes']);
442
+ const unrecognizedUpdateFlag = args.slice(2).find((a) => a.startsWith('--') && !RECOGNIZED_UPDATE_FLAGS.has(a));
443
+ if (unrecognizedUpdateFlag !== undefined) {
444
+ error(`Unrecognized flag "${boundedPath(unrecognizedUpdateFlag)}" for vault update. Supported: --yes, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
445
+ return;
446
+ }
447
+ const updatePositionals = args.slice(2).filter((a) => !a.startsWith('--'));
448
+ if (updatePositionals.length > 1) {
449
+ error('vault update takes at most one tag, e.g. "pp-tools vault update v1.4.0".', ERROR_REASON.SDK_FAIL_FAST);
450
+ return;
451
+ }
452
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
453
+ const updateMod = _update ?? require('./vault-update.cjs');
454
+ const updateResult = updateMod.vaultUpdate(cwd, {
455
+ target: updatePositionals[0] ?? null,
456
+ apply: args.includes('--yes'),
457
+ });
458
+ if (!updateResult.ok) {
459
+ error(updateResult.message, ERROR_REASON.SDK_FAIL_FAST);
460
+ return;
461
+ }
462
+ if (raw) {
463
+ c.output(updateResult, true, JSON.stringify(updateResult));
464
+ }
465
+ else {
466
+ c.output(updateResult, true, updateMod.renderVaultUpdate(updateResult));
467
+ }
468
+ }
337
469
  else {
338
470
  error(`Unknown vault subcommand. Available: ${VAULT_SUBCOMMANDS.map((x) => x.name).join(', ')}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
339
471
  }
340
472
  }
473
+ /**
474
+ * `vault trace scaffold|render` — the practice-trace producer.
475
+ *
476
+ * The CLI owns everything that must be PROVABLE — the pinned ref, the
477
+ * resolved sha, the practice-id set and the per-file digests — and the agent
478
+ * writes only the judgement half (`status`, `reason`, `tasks`). Provability
479
+ * cannot rest on an agent transcribing a forty-character sha.
480
+ *
481
+ * NEITHER ARM WRITES A FILE. The producer emits text the agent embeds in the
482
+ * single write it already performs. Patching a plan's frontmatter here would
483
+ * have to round-trip it through the shared serializer, which cannot re-emit
484
+ * nested object lists, so the same write would damage the plan's own
485
+ * must-haves block.
486
+ */
487
+ function routeTrace({ args, cwd, raw, error, vault, c, _exec, _trace, }) {
488
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
489
+ const realVault = require('./vault.cjs');
490
+ const cfgGet = vault.cfgGet ? vault.cfgGet.bind(vault) : realVault.cfgGet;
491
+ const verb = args[2];
492
+ const traceArgs = args.slice(3);
493
+ if (verb !== 'scaffold' && verb !== 'render') {
494
+ error(`Unknown trace subcommand. Available: ${TRACE_VERBS.join(', ')}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
495
+ return;
496
+ }
497
+ // Inert unless both vault keys are on: no output at all, before any flag check.
498
+ if (!tracePracticesActive(cfgGet, cwd))
499
+ return;
500
+ if (verb === 'scaffold') {
501
+ const RECOGNIZED_SCAFFOLD_FLAGS = new Set(['--stage', '--plan', '--raw']);
502
+ const unrecognized = traceArgs.find((a) => a.startsWith('--') && !RECOGNIZED_SCAFFOLD_FLAGS.has(a));
503
+ if (unrecognized !== undefined) {
504
+ error(`Unrecognized flag "${unrecognized}" for vault trace scaffold. ` +
505
+ `Supported: positional file paths, --stage <${TRACE_STAGES.join('|')}>, --plan <id>, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
506
+ return;
507
+ }
508
+ const valueIndices = flagValueIndices(traceArgs, ['--stage', '--plan']);
509
+ const stageIdx = traceArgs.indexOf('--stage');
510
+ const stage = stageIdx !== -1 ? traceArgs[stageIdx + 1] : undefined;
511
+ if (stage === undefined || !TRACE_STAGES.includes(stage)) {
512
+ error(`Missing or unrecognized --stage value for vault trace scaffold. Supported: ${TRACE_STAGES.join(', ')}.`, ERROR_REASON.SDK_FAIL_FAST);
513
+ return;
514
+ }
515
+ const planIdx = traceArgs.indexOf('--plan');
516
+ const planId = planIdx !== -1 ? traceArgs[planIdx + 1] : undefined;
517
+ if (planId === undefined || planId.startsWith('--')) {
518
+ error('No plan identifier was given. Usage: vault trace scaffold ' +
519
+ `--stage <${TRACE_STAGES.join('|')}> --plan <id> [files...] [--raw].`, ERROR_REASON.SDK_MISSING_ARG);
520
+ return;
521
+ }
522
+ if (!SCAFFOLD_PLAN_ID_RE.test(planId)) {
523
+ error(`Invalid --plan value "${boundedPath(planId)}" for vault trace scaffold. ${SCAFFOLD_PLAN_ID_FORM} ` +
524
+ 'A trace gate compares the identifier a plan artifact records against the one its own filename states.', ERROR_REASON.SDK_FAIL_FAST);
525
+ return;
526
+ }
527
+ const files = traceArgs.filter((a, i) => !a.startsWith('--') && !valueIndices.has(i));
528
+ // The execute stage records, per governed file, the commit that last
529
+ // produced it, so the statement asserts something falsifiable. A file with
530
+ // no commit yields no digest — the leaf records that gap as a loud note; a
531
+ // HEAD fallback would paper over a real coverage hole.
532
+ const runGit = _exec ?? shell_command_projection_cjs_1.execGit;
533
+ const fileInputs = files.map((name) => {
534
+ if (stage !== 'execute')
535
+ return name;
536
+ const res = runGit(['log', '-1', '--format=%H', '--', name], { cwd });
537
+ const digest = res.exitCode === 0 ? res.stdout.trim() : '';
538
+ return GIT_COMMIT_DIGEST_RE.test(digest) ? { name, gitCommit: digest } : name;
539
+ });
540
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
541
+ const traceMod = _trace ?? require('./vault-trace.cjs');
542
+ // No files is a REAL path: a documentation-only plan is still governed by
543
+ // the global practices, and its ruled-out set must stay auditable.
544
+ const statement = traceMod.buildTraceStatement({ cwd, stage, plan: planId, files: fileInputs });
545
+ if (raw) {
546
+ c.output(statement, true, JSON.stringify(statement));
547
+ }
548
+ else {
549
+ c.output(statement, true, traceMod.serializeTraceBlock(statement));
550
+ }
551
+ return;
552
+ }
553
+ // ── render ──
554
+ const RECOGNIZED_RENDER_FLAGS = new Set(['--raw']);
555
+ const unrecognizedRender = traceArgs.find((a) => a.startsWith('--') && !RECOGNIZED_RENDER_FLAGS.has(a));
556
+ if (unrecognizedRender !== undefined) {
557
+ error(`Unrecognized flag "${unrecognizedRender}" for vault trace render. Supported: a positional artifact path, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
558
+ return;
559
+ }
560
+ const artifactPath = traceArgs.find((a) => !a.startsWith('--'));
561
+ if (artifactPath === undefined) {
562
+ error('No artifact path was given. Usage: vault trace render <artifact> [--raw].', ERROR_REASON.SDK_MISSING_ARG);
563
+ return;
564
+ }
565
+ // CONTAINMENT, decided BEFORE the read (the bounded reader returns '' on any
566
+ // problem and never throws, so a decision taken on its result would be none).
567
+ // The rendered section is included verbatim in SUMMARY.md, so an out-of-tree
568
+ // file must never become its content. The predicate is the real leaf's, not
569
+ // the `_vault` seam's: a security decision is not switchable by a mock.
570
+ if (artifactPath.indexOf('\u0000') !== -1) {
571
+ error(`The artifact path is not a usable path: ${boundedPath(artifactPath)}`, ERROR_REASON.SDK_FAIL_FAST);
572
+ return;
573
+ }
574
+ let projectRoot;
575
+ try {
576
+ projectRoot = findProjectRoot(cwd) || cwd;
577
+ }
578
+ catch {
579
+ projectRoot = cwd;
580
+ }
581
+ const artifactAbs = node_path_1.default.resolve(cwd, artifactPath);
582
+ if (!realVault.isInsideDir(projectRoot, artifactAbs)) {
583
+ error(`The artifact path resolves outside the project and was not read: ${boundedPath(artifactPath)}`, ERROR_REASON.SDK_FAIL_FAST);
584
+ return;
585
+ }
586
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
587
+ const traceMod = _trace ?? require('./vault-trace.cjs');
588
+ const readHead = vault.readHeadBounded ? vault.readHeadBounded.bind(vault) : realVault.readHeadBounded;
589
+ const content = readHead(artifactAbs, TRACE_ARTIFACT_READ_BYTES);
590
+ const parsed = traceMod.parseVaultTraceBlock(content, artifactPath);
591
+ // THE LEDGER, AND ONLY THE LEDGER: this arm never calls the matcher, so the
592
+ // rendered section cannot disagree with the frontmatter it is generated
593
+ // from. An absent ledger yields no section; an empty one yields the heading
594
+ // plus one explanatory line (the leaf's renderer).
595
+ const rendered = parsed.present ? traceMod.renderPracticesApplied(parsed.statement) : '';
596
+ if (raw) {
597
+ c.output(parsed, true, JSON.stringify(parsed));
598
+ }
599
+ else {
600
+ c.output(parsed, true, rendered);
601
+ }
602
+ }
603
+ /**
604
+ * `vault audit <phase> [--range <from>..<to>] [--raw]` — the operator's view of
605
+ * the mechanical practice audit.
606
+ *
607
+ * A PASS-THROUGH: it collects arguments, calls `runVaultAudit` and prints the
608
+ * leaf's own envelope (`--raw`) or the leaf's own report. `check vault.audit`
609
+ * serializes the same envelope from the same engine, and the phase argument is
610
+ * resolved by the same `resolveDecisionPhaseDir`, so the report cannot disagree
611
+ * with the gate that blocks on it.
612
+ *
613
+ * Inert (no output at all) unless both vault keys are on, decided before the
614
+ * flag allowlist so an inert project is silent on every path. A malformed
615
+ * `--range` is refused by the leaf's own predicate and never echoed. The
616
+ * range's value is excluded by index, so the flag may precede the phase.
617
+ */
618
+ function routeAudit({ args, cwd, raw, error, vault, c, _exec, _audit, }) {
619
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
620
+ const realVault = require('./vault.cjs');
621
+ const cfgGet = vault.cfgGet ? vault.cfgGet.bind(vault) : realVault.cfgGet;
622
+ if (!tracePracticesActive(cfgGet, cwd))
623
+ return;
624
+ const auditArgs = args.slice(2);
625
+ const RECOGNIZED_AUDIT_FLAGS = new Set(['--raw', '--range']);
626
+ const unrecognized = auditArgs.find((a) => a.startsWith('--') && !RECOGNIZED_AUDIT_FLAGS.has(a));
627
+ if (unrecognized !== undefined) {
628
+ error(`Unrecognized flag "${boundedPath(unrecognized)}" for vault audit. Supported: a phase directory or number, --raw, --range <from>..<to>.`, ERROR_REASON.SDK_FAIL_FAST);
629
+ return;
630
+ }
631
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
632
+ const auditMod = _audit ?? require('./vault-audit.cjs');
633
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
634
+ const checkRouter = require('./check-command-router.cjs');
635
+ const rangeAt = auditArgs.indexOf('--range');
636
+ const rangeValue = rangeAt === -1 ? undefined : auditArgs[rangeAt + 1];
637
+ if (rangeAt !== -1) {
638
+ // The real leaf's predicate: a range check is not switchable by a test mock.
639
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
640
+ const leaf = require('./vault-audit.cjs');
641
+ if (typeof rangeValue !== 'string' || !leaf.isValidOperatorRange(rangeValue)) {
642
+ error(checkRouter.AUDIT_RANGE_REFUSED_MESSAGE, ERROR_REASON.SDK_FAIL_FAST);
643
+ return;
644
+ }
645
+ }
646
+ const phaseArg = auditArgs.find((a, i) => !a.startsWith('--') && !(rangeAt !== -1 && i === rangeAt + 1));
647
+ if (phaseArg === undefined) {
648
+ error('No phase directory or phase number was given. Usage: vault audit <phase-dir|phase-number> [--range <from>..<to>] [--raw].', ERROR_REASON.SDK_MISSING_ARG);
649
+ return;
650
+ }
651
+ let root;
652
+ try {
653
+ root = findProjectRoot(cwd) || cwd;
654
+ }
655
+ catch {
656
+ root = cwd;
657
+ }
658
+ const phase = checkRouter.resolveDecisionPhaseDir(root, cwd, phaseArg);
659
+ if (!phase) {
660
+ const unresolved = {
661
+ status: 'unresolved',
662
+ block: true,
663
+ message: `Vault audit: "${boundedPath(phaseArg)}" is neither an existing directory inside the project nor a `
664
+ + 'phase the locator resolves, so no practice was compared against anything. Repair: re-run this '
665
+ + 'command with the phase directory or number.',
666
+ };
667
+ c.output(unresolved, true, raw ? JSON.stringify(unresolved) : unresolved.message);
668
+ return;
669
+ }
670
+ const result = auditMod.runVaultAudit({
671
+ root,
672
+ phaseDir: phase.abs,
673
+ exec: _exec ?? shell_command_projection_cjs_1.execGit,
674
+ ...(typeof rangeValue === 'string' ? { range: rangeValue } : {}),
675
+ });
676
+ // The leaf's object, untouched: the operator view and the gate serialize the
677
+ // same envelope, and nothing between the leaf and the screen rebuilds it.
678
+ if (raw) {
679
+ c.output(result, true, JSON.stringify(result));
680
+ }
681
+ else {
682
+ c.output(result, true, auditMod.renderAuditReport(result));
683
+ }
684
+ }
685
+ const REVIEW_VERBS = ['scaffold', 'finalize'];
686
+ const REVIEW_USAGE = 'vault review scaffold|finalize <phase> [--force] [--raw]';
687
+ /** The artifact a review writes in its phase directory: `<NN>-VAULT-REVIEW.md`. */
688
+ function reviewArtifactName(phaseToken) {
689
+ return `${phaseToken ? `${phaseToken}-` : ''}VAULT-REVIEW.md`;
690
+ }
691
+ /**
692
+ * `vault review scaffold|finalize <phase> [--force] [--raw]` — the artifact half
693
+ * of `/pp-vault-review`. The derivation is `check vault-review`'s, through the
694
+ * same exported resolver and composer, so all three see one phase and one
695
+ * payload.
696
+ *
697
+ * - `scaffold` writes `<phase-dir>/<NN>-VAULT-REVIEW.md` from the payload:
698
+ * every computed section, every derived row, one `TODO` row per practice
699
+ * the agent must judge, and the three computed lines. It does not replace
700
+ * an existing review unless `--force` is given.
701
+ * - `finalize` re-stamps the rate, counts and coverage lines from the rows the
702
+ * agent wrote (the only writer of those lines), writes the file back when
703
+ * that changed it, and reports every problem: the validator's (a `TODO`
704
+ * left, a missing reason, a reclassified derived row, a score) and the
705
+ * population's (a practice dropped, added or duplicated).
706
+ *
707
+ * Exit 0 on every path and no `block` key on any envelope: the review reports
708
+ * and never gates, so a problem is a repair item, never a halt.
709
+ */
710
+ function routeReview({ args, cwd, raw, c, }) {
711
+ const say = (envelope, text) => {
712
+ c.output(envelope, true, raw ? JSON.stringify(envelope) : text);
713
+ };
714
+ const verb = args[2];
715
+ if (!REVIEW_VERBS.includes(verb)) {
716
+ const statement = `Vault review: "${boundedPath(typeof verb === 'string' ? verb : '')}" is not a review verb, so nothing was ` +
717
+ `reviewed. Usage: ${REVIEW_USAGE}.`;
718
+ say({ status: 'usage', statement }, statement);
719
+ return;
720
+ }
721
+ const force = verb === 'scaffold' && args.includes('--force');
722
+ const tokens = args.slice(3).filter((a) => a !== '--raw' && !(force && a === '--force'));
723
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
724
+ const checkRouter = require('./check-command-router.cjs');
725
+ const target = checkRouter.resolveVaultReviewTarget(cwd, tokens, REVIEW_USAGE);
726
+ if (!target.ok) {
727
+ say({ status: target.status, statement: target.statement }, target.statement);
728
+ return;
729
+ }
730
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
731
+ const leaf = require('./vault-review.cjs');
732
+ const payload = checkRouter.composeVaultReviewPayload(target.root, target.phase, target.phaseToken);
733
+ const name = reviewArtifactName(target.phaseToken);
734
+ const file = `${target.phase.rel}/${name}`;
735
+ const abs = node_path_1.default.join(target.phase.abs, name);
736
+ const partition = (payload.partition ?? {});
737
+ const coverage = typeof payload.mechanicalCoverageLine === 'string' ? payload.mechanicalCoverageLine : null;
738
+ if (verb === 'scaffold') {
739
+ if (node_fs_1.default.existsSync(abs) && !force) {
740
+ const statement = `Vault review: ${file} already exists, so it was left as it is. Re-run with --force to replace it ` +
741
+ 'with a fresh scaffold, or run vault review finalize to check it.';
742
+ say({ status: 'exists', file, statement }, statement);
743
+ return;
744
+ }
745
+ const text = leaf.renderReviewScaffold(payload, {
746
+ date: new Date().toISOString().slice(0, 10),
747
+ phaseToken: target.phaseToken,
748
+ });
749
+ node_fs_1.default.writeFileSync(abs, text, 'utf-8');
750
+ const toJudge = Array.isArray(partition.remainder) ? partition.remainder.length : 0;
751
+ const derived = (Array.isArray(partition.naUniverse) ? partition.naUniverse.length : 0) +
752
+ (Array.isArray(partition.naNoFile) ? partition.naNoFile.length : 0);
753
+ const statement = `Vault review: wrote ${file} (arm: ${String(payload.arm)}; ${toJudge} practice(s) to judge, ` +
754
+ `${derived} derived N-A${payload.refusesCleanRender === true ? '; no clean review may be rendered from it' : ''}).`;
755
+ say({ status: 'written', file, arm: payload.arm, toJudge, derived, refusesCleanRender: payload.refusesCleanRender === true, statement }, statement);
756
+ return;
757
+ }
758
+ // finalize
759
+ let current;
760
+ try {
761
+ current = node_fs_1.default.readFileSync(abs, 'utf-8');
762
+ }
763
+ catch {
764
+ const statement = `Vault review: ${file} does not exist, so there is nothing to finalize. Run vault review scaffold first.`;
765
+ say({ status: 'absent', file, statement }, statement);
766
+ return;
767
+ }
768
+ const stamped = leaf.stampReviewLines(current, { coverageLine: coverage });
769
+ if (stamped.stamped && stamped.text !== current)
770
+ node_fs_1.default.writeFileSync(abs, stamped.text, 'utf-8');
771
+ const result = leaf.validateReviewArtifact(stamped.text, { expectedCoverageLine: coverage });
772
+ const problems = [...result.problems, ...leaf.compareReviewPopulation(result.rows, partition)];
773
+ const envelope = {
774
+ status: problems.length === 0 ? 'valid' : 'problems',
775
+ file,
776
+ rateLine: result.expectedRateLine,
777
+ countsLine: result.expectedCountsLine,
778
+ problems,
779
+ };
780
+ const text = [
781
+ `Vault review: ${file}`,
782
+ result.expectedRateLine,
783
+ result.expectedCountsLine,
784
+ ...(problems.length === 0
785
+ ? ['No repair items.']
786
+ : [
787
+ `Repair items (${problems.length}) — fix them in the artifact, then run vault review finalize again:`,
788
+ ...problems.map((p) => `- ${p.kind}${p.id ? ` (${p.id})` : ''}${p.line ? `, line ${p.line}` : ''}: ${p.detail}`),
789
+ ]),
790
+ 'This review reports and does not gate.',
791
+ ].join('\n');
792
+ say(envelope, text);
793
+ }
341
794
  module.exports = {
342
795
  routeVaultCommand,
343
796
  VAULT_SUBCOMMANDS,
797
+ // The scaffold's plan-identifier shape and its one wording, exported so a
798
+ // trace gate's filename extractor and the planner reference are checked
799
+ // against the running module rather than a restated copy.
800
+ SCAFFOLD_PLAN_ID_RE,
801
+ SCAFFOLD_PLAN_ID_FORM,
802
+ TRACE_STAGES,
344
803
  };