@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/pp-code-reviewer.md +1 -1
- package/agents/pp-executor.md +2 -13
- package/agents/pp-planner.md +1 -1
- package/agents/pp-vault-reviewer.md +51 -0
- package/commands/pp-ns-review.md +2 -1
- package/commands/pp-vault-review.md +33 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +200 -10
- package/pp-core/bin/lib/check-command-router.cjs +1242 -37
- package/pp-core/bin/lib/clusters.cjs +1 -0
- package/pp-core/bin/lib/command-aliases.cjs +7 -0
- package/pp-core/bin/lib/compose-bindings.cjs +41 -27
- package/pp-core/bin/lib/decisions.cjs +104 -27
- package/pp-core/bin/lib/dispatch-plan.cjs +12 -1
- package/pp-core/bin/lib/frontmatter.cjs +37 -0
- package/pp-core/bin/lib/git-base-branch.cjs +136 -10
- package/pp-core/bin/lib/init-contract.cjs +180 -66
- package/pp-core/bin/lib/init-interactive.cjs +4 -20
- package/pp-core/bin/lib/loop-gates.cjs +213 -0
- package/pp-core/bin/lib/loop-resolver.cjs +107 -45
- package/pp-core/bin/lib/org-profile.cjs +218 -44
- package/pp-core/bin/lib/phase-command-router.cjs +7 -1
- package/pp-core/bin/lib/phase-manifest.cjs +527 -0
- package/pp-core/bin/lib/phase-tier.cjs +15 -2
- package/pp-core/bin/lib/phase.cjs +51 -1
- package/pp-core/bin/lib/vault-audit.cjs +2070 -0
- package/pp-core/bin/lib/vault-command-router.cjs +460 -1
- package/pp-core/bin/lib/vault-practices.cjs +1142 -104
- package/pp-core/bin/lib/vault-review.cjs +2359 -0
- package/pp-core/bin/lib/vault-trace.cjs +2831 -0
- package/pp-core/bin/lib/vault-update.cjs +266 -0
- package/pp-core/bin/lib/vault-write-back.cjs +19 -0
- package/pp-core/bin/lib/vault.cjs +277 -12
- package/pp-core/bin/pp-tools.cjs +30 -13
- package/pp-core/bin/shared/model-catalog.json +2 -1
- package/pp-core/references/executor-vault-trace.md +47 -0
- package/pp-core/references/loop-hook-dispatch.md +13 -2
- package/pp-core/references/planner-vault-practices.md +46 -1
- package/pp-core/references/planning-config.md +1 -1
- package/pp-core/templates/practices/README.md +140 -11
- package/pp-core/templates/project-charter.md +226 -0
- package/pp-core/workflows/add-tests.md +4 -0
- package/pp-core/workflows/audit-fix.md +2 -0
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/code-review-fix.md +2 -0
- package/pp-core/workflows/debug.md +4 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +9 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +2 -1
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +37 -0
- package/pp-core/workflows/discovery-sprint.md +1 -1
- package/pp-core/workflows/discovery-to-build.md +1 -1
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +19 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -1
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +10 -8
- package/pp-core/workflows/execute-phase.md +22 -5
- package/pp-core/workflows/execute-plan.md +4 -0
- package/pp-core/workflows/fast.md +4 -0
- package/pp-core/workflows/help/modes/full.md +3 -2
- package/pp-core/workflows/init.md +2 -2
- package/pp-core/workflows/new-project.md +1 -1
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +9 -0
- package/pp-core/workflows/plan-phase.md +7 -4
- package/pp-core/workflows/quick.md +4 -0
- package/pp-core/workflows/ship.md +5 -1
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/validate-phase.md +4 -0
- package/pp-core/workflows/vault-review.md +65 -0
- package/pp-core/workflows/verify-work.md +3 -0
- package/scripts/gen-capability-registry.cjs +11 -0
- package/scripts/lint-test-file-count.allowlist.json +10 -1
- package/scripts/prompt-injection-scan.sh +2 -0
|
@@ -37,6 +37,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
37
37
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
38
38
|
const git_provider_cjs_1 = require("./git-provider.cjs");
|
|
39
39
|
const vault_coordinates_cjs_1 = require("./vault-coordinates.cjs");
|
|
40
|
+
const org_profile_cjs_1 = require("./org-profile.cjs");
|
|
40
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
42
|
const configLoaderModule = require("./config-loader.cjs");
|
|
42
43
|
const { unreadableConfigFiles } = configLoaderModule;
|
|
@@ -333,6 +334,15 @@ function readManifest(vaultDirPath) {
|
|
|
333
334
|
return null;
|
|
334
335
|
}
|
|
335
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Does a manifest record a miss — a fetch that reached no vault content? The
|
|
339
|
+
* one predicate for that fact: the verdict, the vault summary, the practices
|
|
340
|
+
* block and the practice trace all read it here, never by comparing `source`
|
|
341
|
+
* inline, so the surfaces cannot disagree about a miss.
|
|
342
|
+
*/
|
|
343
|
+
function manifestRecordsMiss(m) {
|
|
344
|
+
return m?.source === 'miss';
|
|
345
|
+
}
|
|
336
346
|
/** Write the manifest with two-space JSON via the repo's normalizing writer (display-safe, D-07). */
|
|
337
347
|
function writeManifest(vaultDirPath, manifest) {
|
|
338
348
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(vaultDirPath);
|
|
@@ -473,6 +483,201 @@ function cloneTier(tier, exec, destDir, warnings) {
|
|
|
473
483
|
return { ok: true };
|
|
474
484
|
return { ok: false, stderr: gi.stderr || hostCliStderr };
|
|
475
485
|
}
|
|
486
|
+
// ─── Keep the cache out of git (shared info/exclude) ──────────────────────────────
|
|
487
|
+
/** The fixed warning when the shared exclude cannot be written; only the errno code is appended. */
|
|
488
|
+
const VAULT_EXCLUDE_WARNING = "the repository's shared git info/exclude could not be updated, so the vault cache (.planning/vault/) may show as untracked";
|
|
489
|
+
function realPathOrSelf(p) {
|
|
490
|
+
try {
|
|
491
|
+
return node_fs_1.default.realpathSync(p);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return node_path_1.default.resolve(p);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Resolve the shared git directory and the work tree's top level for `cwd` from
|
|
499
|
+
* the filesystem alone — no subprocess, so the fresh-cache reuse path still
|
|
500
|
+
* spawns nothing and a fetch spawns exactly what its clone needs.
|
|
501
|
+
*
|
|
502
|
+
* Walks up from `cwd` the way git's discovery does. At the first `.git`: a
|
|
503
|
+
* directory is the common dir and its parent the top level; a file is followed
|
|
504
|
+
* to the admin directory it names, whose `commondir` file (a linked worktree)
|
|
505
|
+
* leads to the shared directory — with no `commondir` (a submodule's
|
|
506
|
+
* `.git/modules/<name>`) the admin directory is the git dir itself. No
|
|
507
|
+
* environment variable is read, so an inherited `GIT_DIR` cannot redirect it.
|
|
508
|
+
* It must agree with `git rev-parse --git-common-dir --show-toplevel`; a test
|
|
509
|
+
* pins the two against each other across real layouts. `null` when no work
|
|
510
|
+
* tree is found.
|
|
511
|
+
*/
|
|
512
|
+
function resolveGitDirsFromFs(cwd) {
|
|
513
|
+
try {
|
|
514
|
+
let dir = node_path_1.default.resolve(cwd);
|
|
515
|
+
for (let depth = 0; depth < 256; depth++) {
|
|
516
|
+
const dotGit = node_path_1.default.join(dir, '.git');
|
|
517
|
+
let st = null;
|
|
518
|
+
try {
|
|
519
|
+
st = node_fs_1.default.statSync(dotGit);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
st = null;
|
|
523
|
+
}
|
|
524
|
+
if (st !== null) {
|
|
525
|
+
if (st.isDirectory())
|
|
526
|
+
return { commonDir: dotGit, toplevel: dir };
|
|
527
|
+
if (!st.isFile())
|
|
528
|
+
return null;
|
|
529
|
+
const named = /^gitdir:\s*(.+?)\s*$/m.exec(readHeadBounded(dotGit, 4096));
|
|
530
|
+
if (!named)
|
|
531
|
+
return null;
|
|
532
|
+
const admin = node_path_1.default.resolve(dir, named[1]);
|
|
533
|
+
const common = readHeadBounded(node_path_1.default.join(admin, 'commondir'), 4096).trim();
|
|
534
|
+
return { commonDir: common === '' ? admin : node_path_1.default.resolve(admin, common), toplevel: dir };
|
|
535
|
+
}
|
|
536
|
+
const parent = node_path_1.default.dirname(dir);
|
|
537
|
+
if (parent === dir)
|
|
538
|
+
return null;
|
|
539
|
+
dir = parent;
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** True when `child` is `parent` or lies under it, compared by real path. */
|
|
548
|
+
function isInsideDir(parent, child) {
|
|
549
|
+
const P = realPathOrSelf(parent);
|
|
550
|
+
const C = realPathOrSelf(child);
|
|
551
|
+
return C === P || C.startsWith(P + node_path_1.default.sep);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Does `<cwd>/.git` name an admin directory under `<commonDir>/worktrees/` whose
|
|
555
|
+
* own `gitdir` file names `<cwd>/.git` back? That is the layout `git worktree
|
|
556
|
+
* add` writes. Compared by real path; any read or resolution failure is false.
|
|
557
|
+
*/
|
|
558
|
+
function worktreeLinksBack(cwd, commonDir) {
|
|
559
|
+
try {
|
|
560
|
+
const dotGit = node_path_1.default.join(cwd, '.git');
|
|
561
|
+
const named = /^gitdir:\s*(.+?)\s*$/m.exec(readHeadBounded(dotGit, 4096));
|
|
562
|
+
if (!named)
|
|
563
|
+
return false;
|
|
564
|
+
const admin = node_fs_1.default.realpathSync(node_path_1.default.resolve(cwd, named[1]));
|
|
565
|
+
if (node_path_1.default.dirname(admin) !== node_path_1.default.join(node_fs_1.default.realpathSync(commonDir), 'worktrees'))
|
|
566
|
+
return false;
|
|
567
|
+
const backLink = readHeadBounded(node_path_1.default.join(admin, 'gitdir'), 4096).trim();
|
|
568
|
+
if (backLink === '')
|
|
569
|
+
return false;
|
|
570
|
+
return node_fs_1.default.realpathSync(node_path_1.default.resolve(admin, backLink)) === node_fs_1.default.realpathSync(dotGit);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* The vault cache directory a READER should use for `cwd`. An executor in a
|
|
578
|
+
* linked git worktree has no `.planning/vault/` (the cache is untracked), so a
|
|
579
|
+
* practice trace written there would always be degraded; the owning main
|
|
580
|
+
* checkout, where the plan was written, carries it.
|
|
581
|
+
*
|
|
582
|
+
* Read-only and spawn-free: it returns a path, and nothing that writes the
|
|
583
|
+
* cache uses it. Answers `<cwd>/.planning/vault` (`local`) unless every check
|
|
584
|
+
* holds: no local manifest; `<cwd>/.git` is a FILE (the root of a linked
|
|
585
|
+
* worktree); the shared git directory `resolveGitDirsFromFs` finds is named
|
|
586
|
+
* `.git` and its worktree entry links back to `cwd`; the owner's
|
|
587
|
+
* `.planning/vault/.manifest.json` exists and resolves inside the owner. Any
|
|
588
|
+
* doubt lands on the local path, which then degrades exactly as before.
|
|
589
|
+
*/
|
|
590
|
+
function resolveVaultCacheDir(cwd) {
|
|
591
|
+
const local = { dir: node_path_1.default.join(cwd, '.planning', 'vault'), source: 'local' };
|
|
592
|
+
try {
|
|
593
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(local.dir, '.manifest.json')))
|
|
594
|
+
return local;
|
|
595
|
+
const dotGit = node_path_1.default.join(cwd, '.git');
|
|
596
|
+
if (!node_fs_1.default.existsSync(dotGit) || !node_fs_1.default.lstatSync(dotGit).isFile())
|
|
597
|
+
return local;
|
|
598
|
+
const dirs = resolveGitDirsFromFs(cwd);
|
|
599
|
+
if (dirs === null || node_path_1.default.basename(dirs.commonDir) !== '.git')
|
|
600
|
+
return local;
|
|
601
|
+
if (!worktreeLinksBack(cwd, dirs.commonDir))
|
|
602
|
+
return local;
|
|
603
|
+
const owner = node_path_1.default.dirname(dirs.commonDir);
|
|
604
|
+
const ownerCache = node_path_1.default.join(owner, '.planning', 'vault');
|
|
605
|
+
const ownerManifest = node_path_1.default.join(ownerCache, '.manifest.json');
|
|
606
|
+
if (!node_fs_1.default.existsSync(ownerManifest))
|
|
607
|
+
return local;
|
|
608
|
+
if (!isInsideDir(owner, ownerCache) || !isInsideDir(owner, ownerManifest))
|
|
609
|
+
return local;
|
|
610
|
+
return { dir: ownerCache, source: 'main-worktree' };
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return local;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The one writer of the vault-cache exclude line. `null` dirs, a common dir
|
|
618
|
+
* that is not an existing directory, or a cache outside the top level is
|
|
619
|
+
* `'skipped'`, silently. The pattern is `/<cache relative to top level>/`,
|
|
620
|
+
* derived from paths this process owns. A line that already equals it is
|
|
621
|
+
* `'present'` and nothing is written. A read or write failure pushes ONE
|
|
622
|
+
* bounded warning (fixed sentence plus the errno `code`, never `err.message`,
|
|
623
|
+
* never a path) and returns `'failed'`. Never throws.
|
|
624
|
+
*/
|
|
625
|
+
function writeVaultExcludeLine(dirs, finalDir, warnings) {
|
|
626
|
+
let pattern;
|
|
627
|
+
try {
|
|
628
|
+
if (dirs === null)
|
|
629
|
+
return 'skipped';
|
|
630
|
+
if (!node_fs_1.default.existsSync(dirs.commonDir) || !node_fs_1.default.statSync(dirs.commonDir).isDirectory())
|
|
631
|
+
return 'skipped';
|
|
632
|
+
const top = realPathOrSelf(dirs.toplevel);
|
|
633
|
+
const fin = realPathOrSelf(finalDir);
|
|
634
|
+
if (fin !== top && !fin.startsWith(top + node_path_1.default.sep))
|
|
635
|
+
return 'skipped';
|
|
636
|
+
const rel = node_path_1.default.relative(top, fin).split(node_path_1.default.sep).join('/');
|
|
637
|
+
if (rel === '' || rel.startsWith('..'))
|
|
638
|
+
return 'skipped';
|
|
639
|
+
pattern = `/${rel}/`;
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
return 'skipped';
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
const excludePath = node_path_1.default.join(dirs.commonDir, 'info', 'exclude');
|
|
646
|
+
let existing = '';
|
|
647
|
+
try {
|
|
648
|
+
existing = node_fs_1.default.readFileSync(excludePath, 'utf-8');
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
if (e.code !== 'ENOENT')
|
|
652
|
+
throw e;
|
|
653
|
+
}
|
|
654
|
+
if (existing.split(/\r?\n/).some((l) => l.trim() === pattern))
|
|
655
|
+
return 'present';
|
|
656
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(excludePath), { recursive: true });
|
|
657
|
+
const lead = existing !== '' && !existing.endsWith('\n') ? '\n' : '';
|
|
658
|
+
node_fs_1.default.appendFileSync(excludePath, `${lead}# pp-core vault fetch: its client-vault cache is a nested clone, never committed\n${pattern}\n`);
|
|
659
|
+
return 'added';
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
const code = e?.code;
|
|
663
|
+
warnings.push(`${VAULT_EXCLUDE_WARNING} (${typeof code === 'string' && code !== '' ? code : 'unknown'}).`);
|
|
664
|
+
return 'failed';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Keep the vault cache out of git — the ONE owner, called by every path that
|
|
669
|
+
* leaves a cache in the work tree: the post-swap fetch, the fresh-cache reuse
|
|
670
|
+
* and the keep-prior-cache degrade.
|
|
671
|
+
*
|
|
672
|
+
* WHY: `.planning/vault/` is a nested clone written into the project's work
|
|
673
|
+
* tree. Nothing ignored it, so a `git add -A` could commit the client vault.
|
|
674
|
+
* The pattern goes into the repository's SHARED `info/exclude` (the git common
|
|
675
|
+
* dir, so every linked worktree is covered by one write) rather than into the
|
|
676
|
+
* project's own committed `.gitignore`. Spawns nothing. Never throws.
|
|
677
|
+
*/
|
|
678
|
+
function ensureVaultCacheExcluded(cwd, finalDir, warnings) {
|
|
679
|
+
return writeVaultExcludeLine(resolveGitDirsFromFs(cwd), finalDir, warnings);
|
|
680
|
+
}
|
|
476
681
|
// ─── Degrade (never-abort terminal states) ─────────────────────────────────────
|
|
477
682
|
function priorContentExists(vaultDirPath) {
|
|
478
683
|
return node_fs_1.default.existsSync(vaultDirPath) && usableFiles(vaultDirPath).length > 0;
|
|
@@ -630,7 +835,14 @@ function vaultFetch(cwd, deps = {}) {
|
|
|
630
835
|
if (prior && prior.source === 'fetched' && ttlHours > 0 && effectiveRef && prior.ref === effectiveRef) {
|
|
631
836
|
const age = Date.now() - new Date(prior.fetchedAt).getTime();
|
|
632
837
|
if (Number.isFinite(age) && age < ttlHours * 3_600_000) {
|
|
633
|
-
|
|
838
|
+
// A reused cache is still on disk in the work tree, so it is owed the
|
|
839
|
+
// exclusion too — spawn-free, so this path still makes zero exec calls.
|
|
840
|
+
const reuseWarnings = [];
|
|
841
|
+
ensureVaultCacheExcluded(cwd, finalDir, reuseWarnings);
|
|
842
|
+
const reused = { ...prior, source: 'cache' };
|
|
843
|
+
if (reuseWarnings.length > 0)
|
|
844
|
+
reused.warnings = [...(prior.warnings ?? []), ...reuseWarnings];
|
|
845
|
+
return reused;
|
|
634
846
|
}
|
|
635
847
|
}
|
|
636
848
|
// Walk the ladder; stop at the first tier producing >=1 usable file.
|
|
@@ -682,6 +894,10 @@ function vaultFetch(cwd, deps = {}) {
|
|
|
682
894
|
void e;
|
|
683
895
|
continue;
|
|
684
896
|
}
|
|
897
|
+
// Fresh bytes just landed in .planning/vault/ through the swap above, so
|
|
898
|
+
// the exclusion is owed now; before the manifest, so a 'failed' warning
|
|
899
|
+
// reaches manifest.warnings.
|
|
900
|
+
ensureVaultCacheExcluded(cwd, finalDir, warnings);
|
|
685
901
|
const manifest = {
|
|
686
902
|
source: 'fetched',
|
|
687
903
|
tier: tier.kind,
|
|
@@ -711,6 +927,10 @@ function vaultFetch(cwd, deps = {}) {
|
|
|
711
927
|
? `${fallbackReason}; ${suppressedReason}`
|
|
712
928
|
: suppressedReason
|
|
713
929
|
: fallbackReason;
|
|
930
|
+
// degrade keeps a prior cache on disk when one exists, so that cache is
|
|
931
|
+
// owed the exclusion as well (a miss leaves nothing to exclude).
|
|
932
|
+
if (priorContentExists(finalDir))
|
|
933
|
+
ensureVaultCacheExcluded(cwd, finalDir, warnings);
|
|
714
934
|
return degrade(finalDir, degradeReason, warnings);
|
|
715
935
|
}
|
|
716
936
|
catch (e) {
|
|
@@ -911,7 +1131,12 @@ function vaultSummary(cwd) {
|
|
|
911
1131
|
'excluded from this listing. Remove credential files from the vault repo.\n');
|
|
912
1132
|
}
|
|
913
1133
|
if (shown.length === 0) {
|
|
914
|
-
|
|
1134
|
+
// A miss manifest is not a cache: the last fetch reached no vault, so the
|
|
1135
|
+
// index says the client's standards could not be checked rather than
|
|
1136
|
+
// describing an empty vault that was read.
|
|
1137
|
+
lines.push(manifestRecordsMiss(readManifest(vaultDirPath))
|
|
1138
|
+
? '\n_No vault content was reached: the last fetch recorded a miss, so the client\'s standards could not be checked._\n'
|
|
1139
|
+
: '\n_Vault is cached but contains no usable files._\n');
|
|
915
1140
|
}
|
|
916
1141
|
else {
|
|
917
1142
|
lines.push('');
|
|
@@ -1086,7 +1311,7 @@ function boundText(s, max = 160) {
|
|
|
1086
1311
|
const flat = s.replace(/[\u0000-\u001F\u007F]+/g, ' ').trim();
|
|
1087
1312
|
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
1088
1313
|
}
|
|
1089
|
-
function readCachedView(cwd) {
|
|
1314
|
+
function readCachedView(cwd, vaultDir) {
|
|
1090
1315
|
const empty = {
|
|
1091
1316
|
present: false,
|
|
1092
1317
|
source: null,
|
|
@@ -1098,7 +1323,7 @@ function readCachedView(cwd) {
|
|
|
1098
1323
|
fileCount: 0,
|
|
1099
1324
|
fallbackReason: null,
|
|
1100
1325
|
};
|
|
1101
|
-
const m = readManifest(node_path_1.default.join(cwd, '.planning', 'vault'));
|
|
1326
|
+
const m = readManifest(typeof vaultDir === 'string' && vaultDir !== '' ? vaultDir : node_path_1.default.join(cwd, '.planning', 'vault'));
|
|
1102
1327
|
if (!m)
|
|
1103
1328
|
return empty;
|
|
1104
1329
|
const source = m.source === 'fetched' || m.source === 'cache' || m.source === 'miss' ? m.source : null;
|
|
@@ -1131,8 +1356,14 @@ function isStaleForVerdict(cwd, cached) {
|
|
|
1131
1356
|
* spawns, never throws. `block = enabled && required && verdict !== 'loaded'`;
|
|
1132
1357
|
* `action` is `stop` when blocking, `confirm` when degraded and not required,
|
|
1133
1358
|
* `proceed` when loaded, `none` when inert (D-03).
|
|
1359
|
+
*
|
|
1360
|
+
* `opts.vaultDir` names the cache to judge instead of `<cwd>/.planning/vault`
|
|
1361
|
+
* (the practice trace passes `resolveVaultCacheDir`'s answer, so an executor in
|
|
1362
|
+
* a linked worktree is judged against the owning checkout's cache). Config and
|
|
1363
|
+
* bindings are always read from `cwd`.
|
|
1134
1364
|
*/
|
|
1135
|
-
function computeVaultVerdict(cwd) {
|
|
1365
|
+
function computeVaultVerdict(cwd, opts = {}) {
|
|
1366
|
+
const vaultDir = typeof opts.vaultDir === 'string' && opts.vaultDir !== '' ? opts.vaultDir : undefined;
|
|
1136
1367
|
// Fail closed before reading a single key: `cfgGet` swallows a parse failure
|
|
1137
1368
|
// into "absent", which would read a required vault as switched off (inert).
|
|
1138
1369
|
if (unreadableConfigFiles(cwd).length > 0) {
|
|
@@ -1140,7 +1371,7 @@ function computeVaultVerdict(cwd) {
|
|
|
1140
1371
|
let cachedView = readCachedView('');
|
|
1141
1372
|
try {
|
|
1142
1373
|
binding = readClientBinding(cwd);
|
|
1143
|
-
cachedView = readCachedView(cwd);
|
|
1374
|
+
cachedView = readCachedView(cwd, vaultDir);
|
|
1144
1375
|
}
|
|
1145
1376
|
catch {
|
|
1146
1377
|
/* display-only; the verdict below does not depend on them */
|
|
@@ -1157,12 +1388,15 @@ function computeVaultVerdict(cwd) {
|
|
|
1157
1388
|
configuredRef: binding.ref,
|
|
1158
1389
|
cached: cachedView,
|
|
1159
1390
|
requiredRaw: null,
|
|
1391
|
+
requiredSource: 'config',
|
|
1160
1392
|
};
|
|
1161
1393
|
}
|
|
1162
1394
|
const enabled = cfgGet(cwd, 'vault.enabled', undefined) === true;
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
const
|
|
1395
|
+
// One ladder: project config, then the org profile, then the default false.
|
|
1396
|
+
const requiredResolution = (0, org_profile_cjs_1.resolveVaultRequired)(cwd);
|
|
1397
|
+
const requiredRawValue = requiredResolution.raw;
|
|
1398
|
+
const requiredValid = requiredResolution.valid;
|
|
1399
|
+
const required = requiredResolution.required;
|
|
1166
1400
|
let requiredRaw = null;
|
|
1167
1401
|
if (!requiredValid) {
|
|
1168
1402
|
try {
|
|
@@ -1177,14 +1411,14 @@ function computeVaultVerdict(cwd) {
|
|
|
1177
1411
|
let reason;
|
|
1178
1412
|
try {
|
|
1179
1413
|
binding = readClientBinding(cwd);
|
|
1180
|
-
cached = readCachedView(cwd);
|
|
1414
|
+
cached = readCachedView(cwd, vaultDir);
|
|
1181
1415
|
if (!enabled)
|
|
1182
1416
|
reason = 'vault_disabled';
|
|
1183
1417
|
else if (!binding.bound)
|
|
1184
1418
|
reason = 'no_bindings';
|
|
1185
1419
|
else if (!cached.present)
|
|
1186
1420
|
reason = 'never_fetched';
|
|
1187
|
-
else if (cached
|
|
1421
|
+
else if (manifestRecordsMiss(cached) || cached.fileCount === 0)
|
|
1188
1422
|
reason = 'fetch_miss';
|
|
1189
1423
|
else if (cached.tier !== 'client')
|
|
1190
1424
|
reason = 'fallback_tier';
|
|
@@ -1215,8 +1449,25 @@ function computeVaultVerdict(cwd) {
|
|
|
1215
1449
|
configuredRef: binding.ref,
|
|
1216
1450
|
cached,
|
|
1217
1451
|
requiredRaw,
|
|
1452
|
+
requiredSource: requiredResolution.source,
|
|
1218
1453
|
};
|
|
1219
1454
|
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Does the agent practice review (`/pp-vault-review`, and the heavy tier's
|
|
1457
|
+
* `verify-vault-review` spawn) apply to this project? The ONE predicate: read
|
|
1458
|
+
* by `check vault-review`, `vault review` and the dispatch plan alike.
|
|
1459
|
+
* `vault.enabled` and `vault.practices_enabled` both strictly `true` in the
|
|
1460
|
+
* project's `.planning/config.json` — the same keys, read the same way, as the
|
|
1461
|
+
* practice gates' kill switch.
|
|
1462
|
+
*
|
|
1463
|
+
* STANDARDS MODE PARKS THE REVIEW HERE. When `vault.mode` lands (`practices`,
|
|
1464
|
+
* the default, or `standards`), a `standards` project answers false: the review
|
|
1465
|
+
* judges practice records, and a standards-mode vault carries prose documents
|
|
1466
|
+
* instead. No other surface needs to change.
|
|
1467
|
+
*/
|
|
1468
|
+
function vaultReviewApplies(cwd) {
|
|
1469
|
+
return cfgGet(cwd, 'vault.enabled', false) === true && cfgGet(cwd, 'vault.practices_enabled', false) === true;
|
|
1470
|
+
}
|
|
1220
1471
|
/** The verdict fields a reader spreads into its output (D-01). */
|
|
1221
1472
|
function verdictFields(v) {
|
|
1222
1473
|
const out = {};
|
|
@@ -1239,7 +1490,11 @@ function renderVaultCard(v, point) {
|
|
|
1239
1490
|
const subject = point === 'plan' ? 'Planning' : 'Build';
|
|
1240
1491
|
const lines = [];
|
|
1241
1492
|
const unreadableConfig = v.reason === 'config_unreadable';
|
|
1242
|
-
const headlineNote = unreadableConfig
|
|
1493
|
+
const headlineNote = unreadableConfig
|
|
1494
|
+
? ' — the project config cannot be read'
|
|
1495
|
+
: v.required
|
|
1496
|
+
? ` — vault.required is true${v.requiredSource === 'profile' ? ' (set by the org profile)' : ''}`
|
|
1497
|
+
: '';
|
|
1243
1498
|
lines.push(`Client vault check (${subject}): ${v.verdict === 'loaded' ? 'LOADED' : 'DEGRADED'}${headlineNote}`);
|
|
1244
1499
|
lines.push(` Configured: ${v.configuredRepo || v.configuredRef ? coordText(v.configuredRepo, v.configuredRef) : 'no client vault bound'}`);
|
|
1245
1500
|
const c = v.cached;
|
|
@@ -1342,6 +1597,7 @@ module.exports = {
|
|
|
1342
1597
|
// Helpers exported for tests / the future router (12-02/12-03).
|
|
1343
1598
|
readManifest,
|
|
1344
1599
|
writeManifest,
|
|
1600
|
+
manifestRecordsMiss,
|
|
1345
1601
|
usableFiles,
|
|
1346
1602
|
cfgGet,
|
|
1347
1603
|
readBindings,
|
|
@@ -1361,6 +1617,8 @@ module.exports = {
|
|
|
1361
1617
|
// Pack 6 D-01..D-06: the one verdict owner and the card it renders.
|
|
1362
1618
|
computeVaultVerdict,
|
|
1363
1619
|
verdictFields,
|
|
1620
|
+
// The agent practice review's one applicability predicate (where standards mode parks it).
|
|
1621
|
+
vaultReviewApplies,
|
|
1364
1622
|
renderVaultCard,
|
|
1365
1623
|
VAULT_VERDICTS,
|
|
1366
1624
|
VAULT_REASONS,
|
|
@@ -1371,4 +1629,11 @@ module.exports = {
|
|
|
1371
1629
|
// Pack 6 D-09: the reachability probe, and the binding reader --arm-vault shares with the verdict.
|
|
1372
1630
|
vaultProbe,
|
|
1373
1631
|
readClientBinding,
|
|
1632
|
+
// The vault cache exclude: the one writer, and the spawn-free git-dir resolver it uses.
|
|
1633
|
+
ensureVaultCacheExcluded,
|
|
1634
|
+
resolveGitDirsFromFs,
|
|
1635
|
+
// The practice trace's readers: the cache a linked worktree reads, and the
|
|
1636
|
+
// containment predicate `vault trace render` checks before it reads.
|
|
1637
|
+
resolveVaultCacheDir,
|
|
1638
|
+
isInsideDir,
|
|
1374
1639
|
};
|
package/pp-core/bin/pp-tools.cjs
CHANGED
|
@@ -178,6 +178,7 @@
|
|
|
178
178
|
*
|
|
179
179
|
* Loop Extension Point Queries (ADR-857 phase 3c):
|
|
180
180
|
* loop render-hooks <point> Resolve + render active Capability hooks at a loop point
|
|
181
|
+
* loop run-gates <point> Run the active gates at a loop point (exit 1 on a halt)
|
|
181
182
|
* Returns JSON envelope { point, activeHooks, rendered }
|
|
182
183
|
* Valid points: discuss:pre/post, plan:pre/post,
|
|
183
184
|
* execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post
|
|
@@ -726,19 +727,13 @@ function createReadlinePrompter({ input = process.stdin, output = process.stdout
|
|
|
726
727
|
)
|
|
727
728
|
.join(' / ');
|
|
728
729
|
rendered += `\n Options: ${optionsRendered}`;
|
|
729
|
-
} else if (question.preselected) {
|
|
730
|
-
// INIT-PATCH-C: an option-less question carrying a `preselected`
|
|
731
|
-
// (e.g. a detected org profile) rendered no default at all today —
|
|
732
|
-
// render one, naming it via `preselectedLabel` when present.
|
|
733
|
-
rendered += `\n Default: ${question.preselectedLabel || question.preselected} (press Enter to accept)`;
|
|
734
730
|
}
|
|
735
731
|
if (question.optionalAnswer) {
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
const skipHint = question.preselected ? 'type "none" to skip' : 'press Enter to skip';
|
|
732
|
+
// A detected org profile is a hint inside the prompt text, so no
|
|
733
|
+
// option-less question carries a default and Enter always skips an
|
|
734
|
+
// optional question. The question text may already carry the hint
|
|
735
|
+
// (both org profile prompts do), so it is added only when absent (I-1).
|
|
736
|
+
const skipHint = 'press Enter to skip';
|
|
742
737
|
if (!String(question.prompt).toLowerCase().includes(skipHint.toLowerCase())) {
|
|
743
738
|
rendered += `\n (${skipHint})`;
|
|
744
739
|
}
|
|
@@ -1552,7 +1547,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
1552
1547
|
}
|
|
1553
1548
|
|
|
1554
1549
|
case 'loop': {
|
|
1555
|
-
// loop render-hooks <point>
|
|
1550
|
+
// loop render-hooks <point> [--config-dir <dir>] [--active-cap <capId>] [--into <role>] [--phase <N>] [--tier <t>]
|
|
1556
1551
|
const loopSubcommand = args[1];
|
|
1557
1552
|
if (loopSubcommand === 'render-hooks') {
|
|
1558
1553
|
let loopConfigDir = null;
|
|
@@ -1602,10 +1597,32 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
1602
1597
|
activeCap: loopActiveCap,
|
|
1603
1598
|
phase: loopValue('phase'),
|
|
1604
1599
|
tier: loopValue('tier'),
|
|
1600
|
+
// --into <role>: print the fragment text of active contributions into <role>
|
|
1601
|
+
into: loopValue('into'),
|
|
1602
|
+
});
|
|
1603
|
+
} else if (loopSubcommand === 'run-gates') {
|
|
1604
|
+
// loop run-gates <point> [--phase <N>] [--tier <t>] [--query <q>]: run the point's
|
|
1605
|
+
// active gates (only those whose check.query is <q>, when given) under the
|
|
1606
|
+
// two-step gate contract; exit 1 on a halt.
|
|
1607
|
+
const gateValue = (flag) => {
|
|
1608
|
+
const eq = args.find(arg => arg.startsWith(`--${flag}=`));
|
|
1609
|
+
if (eq) return eq.slice(flag.length + 3).trim();
|
|
1610
|
+
const idx = args.indexOf(`--${flag}`);
|
|
1611
|
+
if (idx === -1) return undefined;
|
|
1612
|
+
const value = args[idx + 1];
|
|
1613
|
+
if (!value || value.startsWith('--')) {
|
|
1614
|
+
error(`Missing value for --${flag}`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1615
|
+
}
|
|
1616
|
+
return value;
|
|
1617
|
+
};
|
|
1618
|
+
require('./lib/loop-gates.cjs').cmdLoopRunGates(cwd, args[2], raw, {
|
|
1619
|
+
phase: gateValue('phase'),
|
|
1620
|
+
tier: gateValue('tier'),
|
|
1621
|
+
query: gateValue('query'),
|
|
1605
1622
|
});
|
|
1606
1623
|
} else {
|
|
1607
1624
|
error(
|
|
1608
|
-
`Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
|
|
1625
|
+
`Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks, run-gates`,
|
|
1609
1626
|
ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
|
|
1610
1627
|
);
|
|
1611
1628
|
}
|
|
@@ -155,6 +155,7 @@
|
|
|
155
155
|
"pp-intel-updater": { "golden": "opus", "balanced": "sonnet", "budget": "haiku", "phaseType": "research", "routingTier": "light" },
|
|
156
156
|
"pp-mempalace-curator": { "golden": "sonnet", "balanced": "sonnet", "budget": "haiku", "phaseType": "research", "routingTier": "light" },
|
|
157
157
|
"pp-security-auditor": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "verification", "routingTier": "heavy" },
|
|
158
|
-
"pp-user-profiler": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "research", "routingTier": "heavy" }
|
|
158
|
+
"pp-user-profiler": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "research", "routingTier": "heavy" },
|
|
159
|
+
"pp-vault-reviewer": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "verification", "routingTier": "heavy" }
|
|
159
160
|
}
|
|
160
161
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Executor — the practice record in SUMMARY.md
|
|
2
|
+
|
|
3
|
+
Load this when your brief carries the vault-practices executor fragment AND the plan you are executing
|
|
4
|
+
carries a `<vault_practices>` block (degraded or not). A plan without that block gets no `vault:` ledger
|
|
5
|
+
and no `## Practices Applied` section — that is how a vault-disabled project's SUMMARY stays unchanged.
|
|
6
|
+
|
|
7
|
+
It applies to every plan you execute, one SUMMARY each: an isolated executor runs one plan; a
|
|
8
|
+
serial-group executor runs this once per plan, in plan order, before that plan's SUMMARY Write.
|
|
9
|
+
|
|
10
|
+
## 1. Scaffold — after the last task commit, before the SUMMARY Write
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pp_run vault trace scaffold --stage execute --plan <phase>-<plan> <the source files your tasks committed...>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `--plan` is the phase-and-number identifier that prefixes this SUMMARY's own filename (for example
|
|
17
|
+
04-02); the command refuses any other shape.
|
|
18
|
+
- Each governed file's digest is read from that file's own last commit. A file whose work is still
|
|
19
|
+
uncommitted gets no digest and is named in the ledger's `notes` — commit first, then scaffold. Never
|
|
20
|
+
pass a path the project does not commit (a gitignored `.planning/` artifact).
|
|
21
|
+
- In a git worktree with no `.planning/vault/` of its own, the command reads the owning main checkout's
|
|
22
|
+
cache and records that fact in `notes`.
|
|
23
|
+
- Never hand-author the ledger: the pin, the practice set and the digests are the command's to produce.
|
|
24
|
+
|
|
25
|
+
## 2. Fill only the judgement fields
|
|
26
|
+
|
|
27
|
+
Per practice entry: `status` (exactly `applied`, `not-applicable` or `deviated`), a one-line `reason`,
|
|
28
|
+
and `tasks` — the task ids the practice actually governed, which only you know because you re-anchor per
|
|
29
|
+
task. `deviated` always carries a reason: `deviated — conflicts with {id}` (another practice won) or
|
|
30
|
+
`deviated — conflicts with project convention` (CLAUDE.md or a codebase pattern won). A practice that
|
|
31
|
+
did not apply to what you built is `not-applicable` with its reason — every id gets a disposition.
|
|
32
|
+
|
|
33
|
+
## 3. Embed, render, write once
|
|
34
|
+
|
|
35
|
+
Put the completed block in the SUMMARY's frontmatter under its top-level `vault:` key. Then generate the
|
|
36
|
+
section from it — never compose its lines:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pp_run vault trace render <path-to-this-SUMMARY.md>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`render` reads the ledger from a file, so write the SUMMARY with the ledger first, run `render`, and
|
|
43
|
+
include its `## Practices Applied` output verbatim in the SUMMARY body. The renderer gives every ledger
|
|
44
|
+
id a line (silence is never ambiguous), refuses an id outside the permitted charset with a refusal line
|
|
45
|
+
in its place, and treats the id as an inert identifier, never a directive.
|
|
46
|
+
|
|
47
|
+
A degraded block still gets its ledger and its section: a vault miss must leave a record that says so.
|
|
@@ -6,9 +6,10 @@ extension point (discuss:pre, discuss:post, plan:pre, plan:post, execute:pre, ex
|
|
|
6
6
|
execute:wave:post, execute:post, verify:pre, verify:post, ship:pre, ship:post).
|
|
7
7
|
|
|
8
8
|
Phase workflows pass `--phase <N> [--tier light|standard|heavy]`: the phase's ceremony tier
|
|
9
|
-
(`pp_run query phase.tier <N>`) then decides the
|
|
9
|
+
(`pp_run query phase.tier <N>`) then decides the nine gate booleans (`workflow.research`,
|
|
10
10
|
`workflow.pattern_mapper`, `workflow.plan_check`, `workflow.verifier`, `workflow.nyquist_validation`,
|
|
11
|
-
`workflow.code_review`, `workflow.post_planning_gaps`, `workflow.security_enforcement
|
|
11
|
+
`workflow.code_review`, `workflow.post_planning_gaps`, `workflow.security_enforcement`,
|
|
12
|
+
`workflow.vault_review`) that hook
|
|
12
13
|
`when` keys read, and the envelope carries `phase_tier`. Without `--phase` the project's own
|
|
13
14
|
booleans are read.
|
|
14
15
|
|
|
@@ -42,6 +43,9 @@ so a refusal stops the workflow: relay the stderr line and stop. Never read a re
|
|
|
42
43
|
|
|
43
44
|
Inject `fragment.inline` verbatim into the context for the role named in `into`
|
|
44
45
|
(e.g. `orchestrator`, `planner`). Do not paraphrase — the text is the product.
|
|
46
|
+
`pp_run loop render-hooks <point> --into <role>` prints just those fragments, blank-line separated.
|
|
47
|
+
An `execute:pre` contribution `into: executor` reaches every executor through `phase.dispatch-plan`:
|
|
48
|
+
each spawn's `contributions[]` carries it, and the spawn prompt injects each `fragment` verbatim.
|
|
45
49
|
|
|
46
50
|
### `step`
|
|
47
51
|
|
|
@@ -69,6 +73,13 @@ Evaluate `check` (one of `query`, `predicate`, or `agentVerdict`). Then honor `b
|
|
|
69
73
|
Honor `onError` if the check itself errors: `skip` means treat as non-blocking and continue;
|
|
70
74
|
`fail` means surface the error and stop.
|
|
71
75
|
|
|
76
|
+
`pp_run loop run-gates <point> --phase <N> [--query <q>]` applies this contract in code for every
|
|
77
|
+
active `check.query` gate at the point (only those whose query is `<q>` with `--query`), appending
|
|
78
|
+
the gate's declared `check.args` after the phase: exit 1 with a `⛔` line on a halt, `⚠` lines for
|
|
79
|
+
advisories and skipped command errors, an `ℹ` line for a passing blocking gate's note, the verdict
|
|
80
|
+
JSON on stdout. It runs every `verify:post` gate, and the `vault.trace` gate at `plan:post`,
|
|
81
|
+
`execute:post` and `ship:pre` (`--query vault.trace`); the hosts' own gate loops skip that query.
|
|
82
|
+
|
|
72
83
|
## Empty / absent `activeHooks`
|
|
73
84
|
|
|
74
85
|
If `activeHooks` is absent, null, or an empty array, skip silently and continue to the next
|
|
@@ -53,7 +53,7 @@ Applies at the `write_phase_prompt` step.
|
|
|
53
53
|
|
|
54
54
|
**Vault practices block (only when the vault-practices capability is active):** This whole subsection applies ONLY when the injected `plan:pre -> into: planner` fragment for vault-practices is present in your context — i.e. `vault.enabled` AND `vault.practices_enabled` are both true for this project. When that fragment is absent, do nothing described here: write no `<vault_practices>` block at all, and this PLAN.md stays byte-identical to a plan produced without the capability.
|
|
55
55
|
|
|
56
|
-
When the fragment IS present: for EACH plan, after that plan's `files_modified` list is finalized and BEFORE calling Write on that plan's PLAN.md, run `
|
|
56
|
+
When the fragment IS present: for EACH plan, after that plan's `files_modified` list is finalized and BEFORE calling Write on that plan's PLAN.md, run `pp_run vault practices match <files_modified...> --block` as an ordinary Bash action against that plan's own files — this call happens in-turn, mid-planning, not before planning starts (the CLI genuinely cannot run any earlier, since `files_modified` does not exist yet at `plan:pre` time).
|
|
57
57
|
|
|
58
58
|
**Embed that command's output VERBATIM as the plan's `<vault_practices>` block — byte for byte, from `<vault_practices>` to `</vault_practices>`.** Do not rename a field, re-order an entry, add or move a fence marker, re-wrap, summarise, or omit anything. The CLI already emits the finished block in the exact shape `docs/practices-format.md` defines: the opening notice, the scoped and global groups, the documented field names, and the whole-entry external-content fencing. It ALWAYS includes every practice the matcher returns with `scope: global`, labeled global — that is now guaranteed by the command rather than left to a plan-time judgement call (D-03), and it is never yours to drop one. It also emits the correct degraded one-line state on its own when there is nothing to show, so the block is never silently absent.
|
|
59
59
|
|
|
@@ -65,3 +65,48 @@ Write the block using exactly one of these three degraded one-line states — ve
|
|
|
65
65
|
- Matcher ran, zero matched AND zero global practices returned: `Practices checked — none apply to this plan's files.` (D-13)
|
|
66
66
|
- Matcher ran, no practices directory found in any fetched vault tier: `Practices checked — none found in the vault.` (D-14)
|
|
67
67
|
- The matcher CLI call itself failed: planning continues regardless — this is advisory, not a blocker, mirroring the vault fetch step's own skip-on-error discipline — and the block carries `Practices could not be checked: <reason>.` (D-15)
|
|
68
|
+
|
|
69
|
+
### Frontmatter trace ledger — after the plan graph, then fill
|
|
70
|
+
|
|
71
|
+
**When:** last. Write the plans, run `pp_run query phase-plan-index "$PHASE"`, write back each plan's
|
|
72
|
+
computed `wave` / `serial_group`, and apply any `merge_candidates` merge FIRST (`planner-plan-graph.md`).
|
|
73
|
+
A merge changes a plan's `files_modified` and its identifier, and the ledger's subject list and practice
|
|
74
|
+
set are derived from exactly those, so a ledger written before the plan graph settled describes a
|
|
75
|
+
different plan. Only then, for each final plan, re-run the `--block` match if its `files_modified`
|
|
76
|
+
changed, and add the ledger.
|
|
77
|
+
|
|
78
|
+
**What to run:** `pp_run vault trace scaffold --stage plan --plan <plan-id> <files_modified...>`. Put the
|
|
79
|
+
returned block in that plan's **frontmatter**, under its top-level `vault:` key, exactly as emitted —
|
|
80
|
+
after `title:`, `wave`, `serial_group` and `chain_reason`, the same verbatim discipline the body block
|
|
81
|
+
gets. A later `pp_run frontmatter set` on the plan keeps the ledger's lines as they are.
|
|
82
|
+
|
|
83
|
+
**The `<plan-id>` is not free-form.** The plan identifier must be the plan's phase-and-number identifier, exactly the prefix of its artifact filename (for example 04-02), not a bare plan number, a task label or a slug. The command refuses any other shape outright rather than emitting a block you would have to redo.
|
|
84
|
+
|
|
85
|
+
**What you fill, and only this:** per practice entry, the `status`, a one-line `reason`, and `tasks` —
|
|
86
|
+
the task ids in this plan the practice is expected to govern. **The pin and the practice list are not
|
|
87
|
+
yours to write.** Never type a `ref`, a `resolvedSha` or a digest, and never re-derive, re-order or trim
|
|
88
|
+
the practice list: those are the fields the command exists to produce.
|
|
89
|
+
|
|
90
|
+
**The three status words** are the existing vocabulary and are never re-coined:
|
|
91
|
+
- `applied` — the practice governs the work and will be followed.
|
|
92
|
+
- `not-applicable` — the practice was considered and ruled out for this plan.
|
|
93
|
+
- `deviated` — the practice loses to something that outranks it, and the `reason` says to what.
|
|
94
|
+
|
|
95
|
+
**A ruled-out practice gets a line, not silence.** A practice you considered and set aside is recorded
|
|
96
|
+
`not-applicable` **with its reason**. That auditable ruled-out set is the point of the ledger: an
|
|
97
|
+
omitted entry and a never-considered one look the same to every later reader.
|
|
98
|
+
|
|
99
|
+
**The ledger and the body block have different jobs.** The `vault:` ledger is the **audit record**: our
|
|
100
|
+
keys, our values, one bounded client-authored token (the practice `id`), and **no client prose** — YAML
|
|
101
|
+
has no fence, so practice text copied into frontmatter has left the fence. The `<vault_practices>` body
|
|
102
|
+
block is the **reference content** the executor re-anchors against per task; it **keeps its fences**.
|
|
103
|
+
|
|
104
|
+
**Degraded or absent vault:** the ledger is still obtained and embedded, carrying the degraded state and
|
|
105
|
+
its reason — a degraded run must leave an artifact that says so, not one indistinguishable from a
|
|
106
|
+
project that was never vault-enabled. The three degraded one-line states above still govern the body
|
|
107
|
+
block. The `override` field is always `null`; it is never yours to write.
|
|
108
|
+
|
|
109
|
+
**Re-run rule:** if a plan-checker revision or `plan_bounce` later changes that plan's `files_modified`,
|
|
110
|
+
re-run the scaffold and replace the block, exactly as the body block is re-run.
|
|
111
|
+
|
|
112
|
+
**Shape:** `docs/vault-trace-format.md` is the contract for the block's fields — read the shape there.
|