@pareto4/pp-core 1.12.1 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +3 -3
- package/README.md +26 -9
- package/agents/pp-code-reviewer.md +3 -2
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +7 -21
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +23 -9
- package/agents/pp-vault-reviewer.md +274 -0
- package/agents/pp-verifier.md +7 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-review.md +2 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-review.md +35 -0
- package/commands/pp-verify-work.md +1 -2
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update.js +0 -0
- package/hooks/dist/pp-cursor-session-start.js +1 -1
- package/hooks/dist/pp-statusline.js +1 -1
- package/hooks/dist/pp-update-banner.js +0 -0
- package/hooks/pp-check-update.js +0 -0
- package/hooks/pp-cursor-session-start.js +1 -1
- package/hooks/pp-statusline.js +1 -1
- package/hooks/pp-update-banner.js +0 -0
- package/package.json +14 -19
- package/pareto-skills/_shared/create-project-board.sh +0 -0
- package/pareto-skills/pp-project-setup/SKILL.md +1 -1
- package/pp-core/bin/check-latest-version.cjs +0 -0
- package/pp-core/bin/detect-build-commands.sh +2 -2
- package/pp-core/bin/lib/capability-activation.cjs +83 -9
- package/pp-core/bin/lib/capability-registry.cjs +526 -22
- package/pp-core/bin/lib/capability-state.cjs +160 -4
- package/pp-core/bin/lib/capability-writer.cjs +19 -5
- package/pp-core/bin/lib/check-command-router.cjs +3867 -274
- package/pp-core/bin/lib/clusters.cjs +4 -0
- package/pp-core/bin/lib/command-aliases.cjs +0 -28
- package/pp-core/bin/lib/commands.cjs +9 -4
- package/pp-core/bin/lib/compose-bindings.cjs +253 -76
- package/pp-core/bin/lib/config-loader.cjs +7 -103
- package/pp-core/bin/lib/config-schema.cjs +1 -1
- package/pp-core/bin/lib/config.cjs +154 -62
- package/pp-core/bin/lib/decisions.cjs +104 -27
- package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
- package/pp-core/bin/lib/frontmatter.cjs +1 -1
- package/pp-core/bin/lib/git-base-branch.cjs +138 -10
- package/pp-core/bin/lib/git-provider.cjs +4 -4
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +653 -629
- package/pp-core/bin/lib/init-interactive.cjs +77 -21
- package/pp-core/bin/lib/init.cjs +24 -51
- package/pp-core/bin/lib/install-profiles.cjs +1 -1
- package/pp-core/bin/lib/io.cjs +8 -0
- package/pp-core/bin/lib/loop-resolver.cjs +169 -47
- package/pp-core/bin/lib/org-profile.cjs +544 -53
- package/pp-core/bin/lib/package-identity.cjs +3 -3
- package/pp-core/bin/lib/phase-command-router.cjs +1 -23
- package/pp-core/bin/lib/phase-scope.cjs +1 -1
- package/pp-core/bin/lib/phase.cjs +11 -203
- package/pp-core/bin/lib/platform-bindings.cjs +40 -19
- package/pp-core/bin/lib/practices-review-check.cjs +15 -9
- package/pp-core/bin/lib/profile-output.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
- package/pp-core/bin/lib/state.cjs +11 -85
- package/pp-core/bin/lib/template.cjs +1 -1
- package/pp-core/bin/lib/vault-audit.cjs +2594 -0
- package/pp-core/bin/lib/vault-command-router.cjs +2756 -60
- package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
- package/pp-core/bin/lib/vault-index.cjs +699 -0
- package/pp-core/bin/lib/vault-mode.cjs +235 -0
- package/pp-core/bin/lib/vault-practices.cjs +1194 -89
- package/pp-core/bin/lib/vault-review.cjs +2102 -0
- package/pp-core/bin/lib/vault-seal.cjs +1936 -0
- package/pp-core/bin/lib/vault-standards.cjs +1505 -0
- package/pp-core/bin/lib/vault-trace.cjs +3184 -0
- package/pp-core/bin/lib/vault-update.cjs +242 -0
- package/pp-core/bin/lib/vault-write-back.cjs +78 -31
- package/pp-core/bin/lib/vault.cjs +2713 -634
- package/pp-core/bin/lib/verify.cjs +0 -11
- package/pp-core/bin/portable-timeout.sh +1 -1
- package/pp-core/bin/pp-tools.cjs +184 -37
- package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
- package/pp-core/bin/shared/config-schema.manifest.json +0 -3
- package/pp-core/bin/shared/model-catalog.json +1 -0
- package/pp-core/bin/verify-reapply-patches.cjs +0 -0
- package/pp-core/org-profile.json +26 -0
- package/pp-core/references/autonomous-best-effort.md +8 -7
- package/pp-core/references/loop-hook-dispatch.md +0 -14
- package/pp-core/references/planner-gap-closure.md +0 -1
- package/pp-core/references/planner-tdd.md +0 -1
- package/pp-core/references/planner-vault-practices.md +31 -3
- package/pp-core/references/planner-vault-standards.md +320 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/practice-arithmetic-contract.md +121 -0
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/references/verifier-practice-verdicts.md +263 -0
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +202 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- package/pp-core/templates/practices/README.md +137 -11
- package/pp-core/templates/project-charter.md +226 -0
- package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
- package/pp-core/workflows/add-tests.md +55 -0
- package/pp-core/workflows/audit-fix.md +63 -0
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +85 -11
- package/pp-core/workflows/check-todos.md +13 -2
- package/pp-core/workflows/code-review-fix.md +59 -1
- package/pp-core/workflows/code-review.md +10 -1
- package/pp-core/workflows/complete-milestone.md +1 -1
- package/pp-core/workflows/debug.md +56 -2
- package/pp-core/workflows/discovery-sprint/prefill.md +21 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +46 -48
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
- package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
- package/pp-core/workflows/discovery-sprint.md +7 -3
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
- package/pp-core/workflows/discovery-to-build.md +9 -1
- package/pp-core/workflows/discuss-phase.md +3 -10
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
- package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -18
- package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
- package/pp-core/workflows/execute-phase.md +154 -110
- package/pp-core/workflows/execute-plan.md +58 -4
- package/pp-core/workflows/fast.md +53 -0
- package/pp-core/workflows/help/modes/full.md +1 -0
- package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
- package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
- package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
- package/pp-core/workflows/init.md +232 -272
- package/pp-core/workflows/new-project.md +31 -23
- package/pp-core/workflows/plan-phase.md +99 -43
- package/pp-core/workflows/quick.md +120 -2
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +57 -24
- package/pp-core/workflows/update.md +3 -3
- package/pp-core/workflows/validate-phase.md +60 -1
- package/pp-core/workflows/vault-review.md +325 -0
- package/pp-core/workflows/verify-work.md +13 -12
- package/scripts/changeset/cli.cjs +6 -5
- package/scripts/changeset/lint.cjs +0 -0
- package/scripts/changeset/new.cjs +33 -50
- package/scripts/dev-env.cjs +25 -85
- package/scripts/gen-capability-registry.cjs +199 -0
- package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
- package/scripts/lib/allowlist-ratchet.cjs +1 -103
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
- package/scripts/lint-docs-required.cjs +0 -0
- package/scripts/lint-pr-check-project-dir.cjs +1 -3
- package/scripts/lint-test-file-count.allowlist.json +13 -2
- package/scripts/practice-arithmetic-contract.allow.json +35 -0
- package/scripts/prompt-injection-scan.sh +22 -15
- package/scripts/record-vault-mode-golden.cjs +89 -0
- package/scripts/release-channel.cjs +0 -0
- package/scripts/sync-manifest-versions.cjs +5 -4
- package/scripts/sync-rulesets.sh +1 -1
- package/scripts/update-size-baseline.cjs +30 -274
- package/scripts/vault-calibrate.cjs +996 -0
- package/scripts/verify-opengsd-sweep.cjs +3 -5
- package/scripts/workflow-size.cjs +9 -61
- package/pp-core/bin/lib/dispatch-plan.cjs +0 -472
- package/pp-core/bin/lib/install-run.cjs +0 -536
- package/pp-core/bin/lib/phase-tier.cjs +0 -467
- package/pp-core/bin/lib/plan-graph.cjs +0 -368
- package/pp-core/references/planner-plan-graph.md +0 -66
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
- package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -72
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -34
- package/scripts/lib/size-budget.cjs +0 -311
- package/scripts/lint-neutrality-names.cjs +0 -694
- package/scripts/lint-org-coordinates.allowlist.json +0 -35
- package/scripts/lint-org-coordinates.cjs +0 -426
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery vault-context check (PBI 75336).
|
|
4
|
+
*
|
|
5
|
+
* Each Discovery session selects the client vault documents that bear on its
|
|
6
|
+
* topics from the vault's own `INDEX.md` (falling back to the engine-written
|
|
7
|
+
* `SUMMARY.md`), and records the selection in a `## Vault Context` section of
|
|
8
|
+
* `.planning/discovery/session-NN.md` — one line per document, naming what was
|
|
9
|
+
* selected and why. This module checks that record at session close:
|
|
10
|
+
*
|
|
11
|
+
* - the section exists and names the index the selection used;
|
|
12
|
+
* - every entry is one well-formed line (`- `<path>` — <why>`, or
|
|
13
|
+
* `- (none) — <why>` when nothing was selected);
|
|
14
|
+
* - every selected document was USED: cited `[vault: <path>]` somewhere in
|
|
15
|
+
* the session notes, or marked `— not used: <reason>` on its own line.
|
|
16
|
+
*
|
|
17
|
+
* The selection itself is the facilitator's judgement and is not re-derived
|
|
18
|
+
* here; what is checked is that the record exists, is complete, and that what
|
|
19
|
+
* was loaded either reached the notes or was explicitly set aside.
|
|
20
|
+
*
|
|
21
|
+
* Parsing: the entry head grammar (prefix, closing backtick, separator) and the
|
|
22
|
+
* vault-path safety rule belong to the vault-index leaf (`src/vault-index.cts`,
|
|
23
|
+
* D-06/D-07), shared with the INDEX.md parser; only the tail grammar — the
|
|
24
|
+
* `(none)` entry, `why` and the `— not used:` mark — lives here.
|
|
25
|
+
*
|
|
26
|
+
* Content-free contract (the T-02-05 precedent): output names the session
|
|
27
|
+
* file, vault paths, structured reasons and counts — never note contents.
|
|
28
|
+
*
|
|
29
|
+
* Traversal: the only argument is a session NUMBER, resolved against the fixed
|
|
30
|
+
* convention path `.planning/discovery/session-NN.md`. No caller string is
|
|
31
|
+
* ever joined into a path (the T-02-08 precedent).
|
|
32
|
+
*/
|
|
33
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
34
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
35
|
+
};
|
|
36
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
+
const io = require("./io.cjs");
|
|
40
|
+
const { output, error, ERROR_REASON } = io;
|
|
41
|
+
// The entry head split and the vault-path rule are the vault-index leaf's
|
|
42
|
+
// (D-06/D-07). The leaf imports nothing, so this cannot close a cycle.
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
+
const vaultIndexLeaf = require("./vault-index.cjs");
|
|
45
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
46
|
+
const DISCOVERY_DIR = '.planning/discovery';
|
|
47
|
+
const SECTION_HEADING = '## Vault Context';
|
|
48
|
+
const INDEX_PREFIX = 'Index: ';
|
|
49
|
+
/**
|
|
50
|
+
* `INDEX.md` — the vault-owned index (preferred). `SUMMARY.md` — the
|
|
51
|
+
* engine-written fallback when the vault ships no `INDEX.md`. `none` — no
|
|
52
|
+
* cache index at all, so nothing could be selected.
|
|
53
|
+
*/
|
|
54
|
+
const INDEX_VALUES = Object.freeze(['INDEX.md', 'SUMMARY.md', 'none']);
|
|
55
|
+
// Discovery's tail grammar, built on the leaf's separator (one spelling in src/).
|
|
56
|
+
// No trailing space: section lines are trimEnd()-ed, so a mark with an empty
|
|
57
|
+
// reason must still be recognised as a mark (and refused), not read as `why`.
|
|
58
|
+
const NOT_USED_MARKER = vaultIndexLeaf.VAULT_ENTRY_SEPARATOR + 'not used:';
|
|
59
|
+
const NONE_ENTRY_PREFIX = '- (none)' + vaultIndexLeaf.VAULT_ENTRY_SEPARATOR;
|
|
60
|
+
const SESSION_MIN = 1;
|
|
61
|
+
const SESSION_MAX = 13;
|
|
62
|
+
const USAGE = 'Usage: pp-tools discovery vault-check <session 1-13>';
|
|
63
|
+
// ─── Parsing ──────────────────────────────────────────────────────────────────
|
|
64
|
+
/** The `## Vault Context` section's lines (with 1-based file line numbers), or null. */
|
|
65
|
+
function extractSection(content) {
|
|
66
|
+
const all = content.split(/\r?\n/);
|
|
67
|
+
const start = all.findIndex((l) => l.trimEnd() === SECTION_HEADING);
|
|
68
|
+
if (start === -1)
|
|
69
|
+
return null;
|
|
70
|
+
const out = [];
|
|
71
|
+
for (let i = start + 1; i < all.length; i += 1) {
|
|
72
|
+
if (/^#{1,2} /.test(all[i]))
|
|
73
|
+
break;
|
|
74
|
+
out.push({ n: i + 1, text: all[i].trimEnd() });
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** Parse one entry line. Returns the entry, or a violation for the line. */
|
|
79
|
+
function parseEntry(n, text) {
|
|
80
|
+
if (text.startsWith(NONE_ENTRY_PREFIX)) {
|
|
81
|
+
const why = text.slice(NONE_ENTRY_PREFIX.length).trim();
|
|
82
|
+
return why === '' ? { reason: 'entry_malformed', line: n } : { line: n, path: null, notUsed: false };
|
|
83
|
+
}
|
|
84
|
+
const head = vaultIndexLeaf.splitVaultEntryLine(text);
|
|
85
|
+
if (head === null || 'malformed' in head)
|
|
86
|
+
return { reason: 'entry_malformed', line: n };
|
|
87
|
+
const p = head.path;
|
|
88
|
+
// The leaf's `rest` starts AFTER the separator, but the not-used marker
|
|
89
|
+
// begins with that same separator: a remainder that opens `not used:` is a
|
|
90
|
+
// mark with an empty `why`, and must stay one. So the tail is searched with
|
|
91
|
+
// the separator re-joined in front, exactly as before the split moved.
|
|
92
|
+
const sep = vaultIndexLeaf.VAULT_ENTRY_SEPARATOR;
|
|
93
|
+
const rest = sep + head.rest;
|
|
94
|
+
const notUsedAt = rest.indexOf(NOT_USED_MARKER);
|
|
95
|
+
const why = (notUsedAt === -1 ? rest.slice(sep.length) : rest.slice(sep.length, notUsedAt)).trim();
|
|
96
|
+
if (why === '')
|
|
97
|
+
return { reason: 'entry_malformed', line: n };
|
|
98
|
+
if (notUsedAt !== -1 && rest.slice(notUsedAt + NOT_USED_MARKER.length).trim() === '') {
|
|
99
|
+
return { reason: 'entry_malformed', line: n };
|
|
100
|
+
}
|
|
101
|
+
if (!vaultIndexLeaf.isSafeVaultPath(p))
|
|
102
|
+
return { reason: 'path_unsafe', line: n, path: p };
|
|
103
|
+
return { line: n, path: p, notUsed: notUsedAt !== -1 };
|
|
104
|
+
}
|
|
105
|
+
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
106
|
+
/**
|
|
107
|
+
* Check one session file's `## Vault Context` record. Pure over `content`.
|
|
108
|
+
*
|
|
109
|
+
* A citation counts only OUTSIDE the section: the record naming a path is not
|
|
110
|
+
* evidence that the path was used.
|
|
111
|
+
*/
|
|
112
|
+
function checkVaultContext(content) {
|
|
113
|
+
const violations = [];
|
|
114
|
+
const section = extractSection(content);
|
|
115
|
+
if (section === null) {
|
|
116
|
+
return { ok: false, index: null, documents: 0, cited: 0, not_used: 0, violations: [{ reason: 'section_missing' }] };
|
|
117
|
+
}
|
|
118
|
+
let index = null;
|
|
119
|
+
const entries = [];
|
|
120
|
+
for (const { n, text } of section) {
|
|
121
|
+
if (text.trim() === '')
|
|
122
|
+
continue;
|
|
123
|
+
if (text.startsWith(INDEX_PREFIX)) {
|
|
124
|
+
const value = text.slice(INDEX_PREFIX.length).trim();
|
|
125
|
+
if (!INDEX_VALUES.includes(value))
|
|
126
|
+
violations.push({ reason: 'index_invalid', line: n });
|
|
127
|
+
else
|
|
128
|
+
index = value;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const parsed = parseEntry(n, text);
|
|
132
|
+
if ('reason' in parsed)
|
|
133
|
+
violations.push(parsed);
|
|
134
|
+
else
|
|
135
|
+
entries.push(parsed);
|
|
136
|
+
}
|
|
137
|
+
if (index === null && !violations.some((v) => v.reason === 'index_invalid')) {
|
|
138
|
+
violations.push({ reason: 'index_line_missing' });
|
|
139
|
+
}
|
|
140
|
+
if (entries.length === 0 && !violations.some((v) => v.reason === 'entry_malformed' || v.reason === 'path_unsafe')) {
|
|
141
|
+
violations.push({ reason: 'no_entries' });
|
|
142
|
+
}
|
|
143
|
+
const docs = entries.filter((e) => e.path !== null);
|
|
144
|
+
const nones = entries.filter((e) => e.path === null);
|
|
145
|
+
if (nones.length > 0 && docs.length > 0) {
|
|
146
|
+
violations.push({ reason: 'none_mixed', line: nones[0].line });
|
|
147
|
+
}
|
|
148
|
+
// Citations are searched in the file with the section's own lines removed.
|
|
149
|
+
const sectionLines = new Set(section.map((s) => s.n));
|
|
150
|
+
const notes = content
|
|
151
|
+
.split(/\r?\n/)
|
|
152
|
+
.filter((_, i) => !sectionLines.has(i + 1))
|
|
153
|
+
.join('\n');
|
|
154
|
+
let cited = 0;
|
|
155
|
+
let notUsed = 0;
|
|
156
|
+
for (const doc of docs) {
|
|
157
|
+
const isCited = notes.includes(`[vault: ${doc.path}]`);
|
|
158
|
+
if (isCited)
|
|
159
|
+
cited += 1;
|
|
160
|
+
if (doc.notUsed)
|
|
161
|
+
notUsed += 1;
|
|
162
|
+
if (!isCited && !doc.notUsed) {
|
|
163
|
+
violations.push({ reason: 'document_unused', line: doc.line, path: doc.path });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { ok: violations.length === 0, index, documents: docs.length, cited, not_used: notUsed, violations };
|
|
167
|
+
}
|
|
168
|
+
/** Parse the session argument: a bare integer 1–13, leading zeros allowed (`03` is session 3). */
|
|
169
|
+
function parseSessionArg(raw) {
|
|
170
|
+
if (typeof raw !== 'string' || !/^\d{1,3}$/.test(raw))
|
|
171
|
+
return null;
|
|
172
|
+
const n = Number(raw);
|
|
173
|
+
return n >= SESSION_MIN && n <= SESSION_MAX ? n : null;
|
|
174
|
+
}
|
|
175
|
+
function sessionFileRel(n) {
|
|
176
|
+
return `${DISCOVERY_DIR}/session-${String(n).padStart(2, '0')}.md`;
|
|
177
|
+
}
|
|
178
|
+
/** Check the record of session `n` in the project at `cwd`. */
|
|
179
|
+
function checkSessionVaultContext(cwd, n) {
|
|
180
|
+
const rel = sessionFileRel(n);
|
|
181
|
+
const abs = node_path_1.default.join(cwd, rel);
|
|
182
|
+
let content;
|
|
183
|
+
try {
|
|
184
|
+
content = node_fs_1.default.readFileSync(abs, 'utf-8');
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return {
|
|
188
|
+
ok: false, session: n, file: rel, index: null, documents: 0, cited: 0, not_used: 0,
|
|
189
|
+
violations: [{ reason: 'session_file_missing' }],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { session: n, file: rel, ...checkVaultContext(content) };
|
|
193
|
+
}
|
|
194
|
+
function formatViolation(v) {
|
|
195
|
+
const where = v.line !== undefined ? `line ${v.line}: ` : '';
|
|
196
|
+
// The path is facilitator-written text echoed to a terminal: it goes through
|
|
197
|
+
// the leaf's path display (D-27, T-14-04; 14-REVIEW WR-02), so a control or
|
|
198
|
+
// bidi character in a refused path is shown as an escape, never emitted raw,
|
|
199
|
+
// and a path is never echoed under a plausible different name.
|
|
200
|
+
const what = v.path !== undefined ? ` (${vaultIndexLeaf.displayVaultPath(v.path)})` : '';
|
|
201
|
+
return ` - ${where}${v.reason}${what}`;
|
|
202
|
+
}
|
|
203
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* CLI adapter for `pp-tools discovery vault-check <session>` (dotted form
|
|
206
|
+
* `pp-tools query discovery.vault-check <session>`).
|
|
207
|
+
*
|
|
208
|
+
* Exit semantics: any violation exits non-zero via
|
|
209
|
+
* `ERROR_REASON.DISCOVERY_VAULT_CHECK_FAILED`, naming each by line, reason and
|
|
210
|
+
* vault path. The payload is emitted on stdout first on the failure path too,
|
|
211
|
+
* so the counts survive alongside the error.
|
|
212
|
+
*/
|
|
213
|
+
function cmdDiscoveryVaultCheck(cwd, args, raw) {
|
|
214
|
+
// args arrive shaped ['discovery', 'vault-check', <session>].
|
|
215
|
+
const rest = args.slice(2);
|
|
216
|
+
const n = rest.length === 1 ? parseSessionArg(rest[0]) : null;
|
|
217
|
+
if (n === null) {
|
|
218
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
219
|
+
return; // unreachable — error() exits
|
|
220
|
+
}
|
|
221
|
+
const result = checkSessionVaultContext(cwd, n);
|
|
222
|
+
const payload = {
|
|
223
|
+
ok: result.ok,
|
|
224
|
+
session: result.session,
|
|
225
|
+
file: result.file,
|
|
226
|
+
index: result.index,
|
|
227
|
+
documents: result.documents,
|
|
228
|
+
cited: result.cited,
|
|
229
|
+
not_used: result.not_used,
|
|
230
|
+
};
|
|
231
|
+
if (!result.ok) {
|
|
232
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
233
|
+
error(`discovery vault-check: ${result.violations.length} violation(s) in ${result.file}:\n` +
|
|
234
|
+
result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_VAULT_CHECK_FAILED);
|
|
235
|
+
return; // unreachable — error() exits
|
|
236
|
+
}
|
|
237
|
+
output(payload, raw);
|
|
238
|
+
}
|
|
239
|
+
module.exports = {
|
|
240
|
+
SECTION_HEADING,
|
|
241
|
+
INDEX_VALUES,
|
|
242
|
+
checkVaultContext,
|
|
243
|
+
checkSessionVaultContext,
|
|
244
|
+
parseSessionArg,
|
|
245
|
+
cmdDiscoveryVaultCheck,
|
|
246
|
+
};
|
|
@@ -390,7 +390,7 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
390
390
|
}
|
|
391
391
|
// ─── Frontmatter CRUD commands ────────────────────────────────────────────────
|
|
392
392
|
const FRONTMATTER_SCHEMAS = {
|
|
393
|
-
plan: { required: ['phase', 'plan', '
|
|
393
|
+
plan: { required: ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'] },
|
|
394
394
|
summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
|
|
395
395
|
verification: { required: ['phase', 'verified', 'status', 'score'] },
|
|
396
396
|
};
|
|
@@ -27,10 +27,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
27
27
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
28
|
};
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.BASE_BRANCH_REFUSED_REASON = void 0;
|
|
31
|
+
exports.isPlausibleBranchRef = isPlausibleBranchRef;
|
|
30
32
|
exports.readConfigBaseBranch = readConfigBaseBranch;
|
|
31
33
|
exports.trySymbolicRef = trySymbolicRef;
|
|
32
34
|
exports.tryRemoteShow = tryRemoteShow;
|
|
33
35
|
exports.tryLocalBranch = tryLocalBranch;
|
|
36
|
+
exports.resolveBaseBranchResult = resolveBaseBranchResult;
|
|
34
37
|
exports.resolveBaseBranch = resolveBaseBranch;
|
|
35
38
|
exports.gitWorktreeInfoInternal = gitWorktreeInfoInternal;
|
|
36
39
|
exports.cmdGitBaseBranch = cmdGitBaseBranch;
|
|
@@ -38,6 +41,100 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
38
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
39
42
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
40
43
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* The one thing that makes a base branch dangerous, and the only rule here that
|
|
46
|
+
* is OURS rather than git's.
|
|
47
|
+
*
|
|
48
|
+
* `execGit` uses argv, so a configured base branch is not shell injection. It is
|
|
49
|
+
* something narrower and worse: a value beginning with `-` is not a revision to
|
|
50
|
+
* git, it is an OPTION. `git log "--output=<path>..HEAD"` exits 0 and creates
|
|
51
|
+
* that file; so does `git diff`. A `.planning/config.json` shipped inside a
|
|
52
|
+
* cloned repository was therefore an arbitrary-file-creation-and-truncation
|
|
53
|
+
* primitive running as the developer or the CI runner (T-6-49).
|
|
54
|
+
*
|
|
55
|
+
* A dash INSIDE a name is an ordinary branch: `feature/-x` is a name git creates
|
|
56
|
+
* and resolves. Only the leading position turns a value into an option, so only
|
|
57
|
+
* the leading position is refused.
|
|
58
|
+
*/
|
|
59
|
+
const OPTION_SHAPED = /^-/;
|
|
60
|
+
/**
|
|
61
|
+
* The characters git itself forbids in a ref name: ASCII control characters and
|
|
62
|
+
* space (`\x00`-`\x20`), DEL, and `~ ^ : ? * [ \`.
|
|
63
|
+
*
|
|
64
|
+
* REFUSING MORE THAN THIS IS ITS OWN DEFECT (W16). The first form of this
|
|
65
|
+
* validator was an allowlist of `[A-Za-z0-9._/-]`, which reads as prudent and is
|
|
66
|
+
* not: `feature/a+b`, `feature/a@b` and `café` are all names
|
|
67
|
+
* `git check-ref-format --branch` accepts and `git branch` creates, and all
|
|
68
|
+
* three were silently discarded in favour of whatever the ladder found next — a
|
|
69
|
+
* review computed over a range the project never asked for, which is the exact
|
|
70
|
+
* failure the ladder exists to end. None of `+ @ % = ( ) ; | $ !` can make git
|
|
71
|
+
* do anything, because nothing here reaches a shell. So the rule is git's rule,
|
|
72
|
+
* plus the leading dash, and nothing else.
|
|
73
|
+
*/
|
|
74
|
+
const FORBIDDEN_REF_CHARS = /[\x00-\x20\x7f~^:?*[\\]/;
|
|
75
|
+
/** Sequences git forbids anywhere inside a ref name. */
|
|
76
|
+
const FORBIDDEN_REF_SEQUENCES = ['..', '@{', '//'];
|
|
77
|
+
/** A bound, so a pathological value cannot become a pathological command line. */
|
|
78
|
+
const MAX_REF_LENGTH = 255;
|
|
79
|
+
/**
|
|
80
|
+
* Is this a name git could actually resolve as a branch?
|
|
81
|
+
*
|
|
82
|
+
* The predicate is `git check-ref-format --branch` re-implemented, with a
|
|
83
|
+
* leading `-` refused on top. Re-implemented rather than shelled out to: this
|
|
84
|
+
* function is pure, total and called on a path that must never spawn a
|
|
85
|
+
* subprocess to decide whether it is safe to spawn a subprocess. The equivalence
|
|
86
|
+
* is not asserted here in prose — `tests/vault-review.security.test.cjs` drives
|
|
87
|
+
* the admit half from `git check-ref-format --branch` AS THE ORACLE, so git
|
|
88
|
+
* decides what must be admitted and this comment cannot quietly stop being true.
|
|
89
|
+
*
|
|
90
|
+
* Total over any input; never throws. Answering `false` is not an error — it is
|
|
91
|
+
* the resolver's cue to fall THROUGH to the next tier of the ladder, which is
|
|
92
|
+
* the behaviour a project with no override already gets.
|
|
93
|
+
*/
|
|
94
|
+
function isPlausibleBranchRef(value) {
|
|
95
|
+
if (typeof value !== 'string')
|
|
96
|
+
return false;
|
|
97
|
+
if (value === '' || value.length > MAX_REF_LENGTH)
|
|
98
|
+
return false;
|
|
99
|
+
if (OPTION_SHAPED.test(value))
|
|
100
|
+
return false;
|
|
101
|
+
if (FORBIDDEN_REF_CHARS.test(value))
|
|
102
|
+
return false;
|
|
103
|
+
for (const bad of FORBIDDEN_REF_SEQUENCES) {
|
|
104
|
+
if (value.includes(bad))
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (value.startsWith('/') || value.endsWith('/') || value.endsWith('.'))
|
|
108
|
+
return false;
|
|
109
|
+
// No path component may be empty, begin with `.`, or end with `.lock`.
|
|
110
|
+
for (const part of value.split('/')) {
|
|
111
|
+
if (part === '' || part.startsWith('.') || part.endsWith('.lock'))
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// git's branch shorthand refuses this one by name, so this resolver does too.
|
|
115
|
+
if (value === 'HEAD')
|
|
116
|
+
return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* WHAT A CALLER SAYS WHEN THE OVERRIDE IS REFUSED. One sentence, exported, for
|
|
121
|
+
* every channel — three call sites state it and none of them spells it.
|
|
122
|
+
*
|
|
123
|
+
* A second spelling of this sentence would be a second definition of what the
|
|
124
|
+
* mechanism did, and this phase has already paid for that twice. The
|
|
125
|
+
* `check vault-review` payload, the `check vault.audit` notes and the
|
|
126
|
+
* `check ship.practices` message all carry these exact bytes.
|
|
127
|
+
*
|
|
128
|
+
* The reason carries NO copy of the refused value: the value is client-supplied
|
|
129
|
+
* and one of its shapes is an absolute filesystem path, so echoing it back onto
|
|
130
|
+
* an agent-readable surface would trade an injection for a disclosure (CR-03).
|
|
131
|
+
*/
|
|
132
|
+
exports.BASE_BRANCH_REFUSED_REASON = 'the configured git.base_branch is not a name git could resolve as a branch, so it was REJECTED ' +
|
|
133
|
+
'and the base-branch ladder continued without it. A value beginning with "-" is an option to git ' +
|
|
134
|
+
'rather than a revision, and options such as --output= make git create and truncate the file they ' +
|
|
135
|
+
'name; every other rule applied here is git\'s own ref-name grammar, so a name git itself would ' +
|
|
136
|
+
'refuse is refused too. The refused value is deliberately not repeated: it is client-supplied, ' +
|
|
137
|
+
'and one of its shapes is a filesystem path.';
|
|
41
138
|
/**
|
|
42
139
|
* Safely look up `git.base_branch` from the project's config.json.
|
|
43
140
|
* Returns the configured value (a non-empty, non-null string) or null.
|
|
@@ -153,33 +250,64 @@ function tryLocalBranch(cwd, execGit) {
|
|
|
153
250
|
}
|
|
154
251
|
}
|
|
155
252
|
/**
|
|
156
|
-
* Resolve the default/base branch for the repository at `cwd
|
|
253
|
+
* Resolve the default/base branch for the repository at `cwd`, and REPORT a
|
|
254
|
+
* refused config override rather than swallowing it.
|
|
157
255
|
*
|
|
158
|
-
* Consults the full precedence ladder and always returns a non-empty
|
|
256
|
+
* Consults the full precedence ladder and always returns a non-empty branch.
|
|
159
257
|
* Never throws.
|
|
258
|
+
*
|
|
259
|
+
* ONLY TIER 1 IS SHAPE-CHECKED, and that is the whole point. Tier 1 is the one
|
|
260
|
+
* value that comes from a file a cloned repository can carry; tiers 2-4 are
|
|
261
|
+
* git's own answers about its own refs, and putting them through the same
|
|
262
|
+
* allowlist would silently demote a legitimate branch whose name this pattern
|
|
263
|
+
* happens not to describe — trading a closed threat for a new silent failure in
|
|
264
|
+
* the one function whose job is to stop guessing.
|
|
160
265
|
*/
|
|
161
|
-
function
|
|
266
|
+
function resolveBaseBranchResult(cwd, deps) {
|
|
162
267
|
const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
|
|
163
268
|
// Derive .planning dir relative to cwd (mirrors planningDir() in planning-workspace.cjs)
|
|
164
269
|
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
165
|
-
// 1. Config override
|
|
270
|
+
// 1. Config override — the only untrusted tier, so the only checked one.
|
|
166
271
|
const configured = readConfigBaseBranch(planningDir, deps);
|
|
167
|
-
|
|
168
|
-
|
|
272
|
+
let configRejected = null;
|
|
273
|
+
if (configured) {
|
|
274
|
+
if (isPlausibleBranchRef(configured))
|
|
275
|
+
return { branch: configured, configRejected: null };
|
|
276
|
+
configRejected = { reason: exports.BASE_BRANCH_REFUSED_REASON };
|
|
277
|
+
}
|
|
169
278
|
// 2. symbolic-ref (fast, no network)
|
|
170
279
|
const symref = trySymbolicRef(cwd, execGit);
|
|
171
280
|
if (symref)
|
|
172
|
-
return symref;
|
|
281
|
+
return { branch: symref, configRejected };
|
|
173
282
|
// 3. git remote show origin (authoritative when origin/HEAD unset)
|
|
174
283
|
const remoteShow = tryRemoteShow(cwd, execGit);
|
|
175
284
|
if (remoteShow)
|
|
176
|
-
return remoteShow;
|
|
285
|
+
return { branch: remoteShow, configRejected };
|
|
177
286
|
// 4. Local branch existence
|
|
178
287
|
const local = tryLocalBranch(cwd, execGit);
|
|
179
288
|
if (local)
|
|
180
|
-
return local;
|
|
289
|
+
return { branch: local, configRejected };
|
|
181
290
|
// 5. Last-resort default
|
|
182
|
-
return 'main';
|
|
291
|
+
return { branch: 'main', configRejected };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Resolve the default/base branch for the repository at `cwd`.
|
|
295
|
+
*
|
|
296
|
+
* Consults the full precedence ladder and always returns a non-empty string.
|
|
297
|
+
* Never throws.
|
|
298
|
+
*
|
|
299
|
+
* The bare-string form. EVERY consumer that computes a range now takes
|
|
300
|
+
* `resolveBaseBranchResult` instead and states the refusal (W16): the two that
|
|
301
|
+
* took this wrapper — `vault-audit.cts`, which is the shipped BLOCKING command,
|
|
302
|
+
* and `cmdShipPractices`, a declared ship:pre gate — refused in silence, so a
|
|
303
|
+
* gate ran over a range the project never configured and reported success on
|
|
304
|
+
* it. What remains here is `cmdGitBaseBranch`, whose entire contract is one
|
|
305
|
+
* branch name on stdout and which therefore genuinely has nowhere to put a
|
|
306
|
+
* sentence. A new caller reaching for this form should ask whether that is true
|
|
307
|
+
* of it as well; if it has a channel, it owes the operator the reason.
|
|
308
|
+
*/
|
|
309
|
+
function resolveBaseBranch(cwd, deps) {
|
|
310
|
+
return resolveBaseBranchResult(cwd, deps).branch;
|
|
183
311
|
}
|
|
184
312
|
/**
|
|
185
313
|
* Detect whether `cwd` sits inside a git worktree, and if so, return the
|
|
@@ -43,12 +43,12 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
43
43
|
const org_profile_cjs_1 = require("./org-profile.cjs");
|
|
44
44
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
45
45
|
/** The complete, ordered set of supported provider ids. */
|
|
46
|
-
exports.PROVIDERS = Object.freeze(['gitlab', 'azure']);
|
|
46
|
+
exports.PROVIDERS = Object.freeze(['gitlab', 'azure', 'github']);
|
|
47
47
|
/**
|
|
48
48
|
* Every project that has never heard of this phase resolves to this provider
|
|
49
49
|
* (PROV-04): an absent config key is the normal case, not an error.
|
|
50
50
|
*/
|
|
51
|
-
exports.DEFAULT_PROVIDER = '
|
|
51
|
+
exports.DEFAULT_PROVIDER = 'github';
|
|
52
52
|
/** The dotted `.planning/config.json` key that selects the provider. */
|
|
53
53
|
exports.PROVIDER_CONFIG_KEY = 'vault.provider';
|
|
54
54
|
// ─── normalizeProvider ───────────────────────────────────────────────────────
|
|
@@ -244,8 +244,8 @@ function composeCloneTarget(provider, repo, host) {
|
|
|
244
244
|
}
|
|
245
245
|
return { url, warnings: [] };
|
|
246
246
|
}
|
|
247
|
-
// gitlab
|
|
248
|
-
//
|
|
247
|
+
// gitlab and github compose the same `{host}/{slug}.git` shape —
|
|
248
|
+
// byte-identical to the template this replaces.
|
|
249
249
|
return { url: `https://${host}/${trimmed}.git`, warnings: [] };
|
|
250
250
|
}
|
|
251
251
|
// ─── cloneEnv ────────────────────────────────────────────────────────────────
|
|
@@ -30,22 +30,19 @@ function routeInitCommand({ init, args, cwd, raw, error }) {
|
|
|
30
30
|
unknownMessage: (_subcommand, available) => `Unknown init workflow: ${_subcommand}\nAvailable: ${available.join(', ')}`,
|
|
31
31
|
handlers: {
|
|
32
32
|
'execute-phase': () => {
|
|
33
|
-
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [
|
|
34
|
-
init.cmdInitExecutePhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd']
|
|
33
|
+
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['validate', 'tdd']);
|
|
34
|
+
init.cmdInitExecutePhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'] });
|
|
35
35
|
},
|
|
36
36
|
'plan-phase': () => {
|
|
37
|
-
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, ['granularity'
|
|
38
|
-
init.cmdInitPlanPhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'], granularity: namedArgs['granularity']
|
|
37
|
+
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, ['granularity'], ['validate', 'tdd']);
|
|
38
|
+
init.cmdInitPlanPhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'], granularity: namedArgs['granularity'] });
|
|
39
39
|
},
|
|
40
40
|
'new-project': () => init.cmdInitNewProject(cwd, raw),
|
|
41
41
|
'new-milestone': () => init.cmdInitNewMilestone(cwd, raw),
|
|
42
42
|
quick: () => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw),
|
|
43
43
|
'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw),
|
|
44
44
|
resume: () => init.cmdInitResume(cwd, raw),
|
|
45
|
-
'verify-work': () =>
|
|
46
|
-
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, ['tier'], []);
|
|
47
|
-
init.cmdInitVerifyWork(cwd, args[2], raw, { tier: namedArgs['tier'] });
|
|
48
|
-
},
|
|
45
|
+
'verify-work': () => init.cmdInitVerifyWork(cwd, args[2], raw),
|
|
49
46
|
'phase-op': () => init.cmdInitPhaseOp(cwd, args[2], raw),
|
|
50
47
|
todos: () => init.cmdInitTodos(cwd, args[2], raw),
|
|
51
48
|
'milestone-op': () => init.cmdInitMilestoneOp(cwd, raw),
|