@pareto4/pp-core 1.12.0 → 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-activation.cjs +19 -9
- 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/config-loader.cjs +151 -8
- 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 +111 -35
- 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 +313 -11
- 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 +20 -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/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +3 -3
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/code-review-fix.md +3 -1
- package/pp-core/workflows/code-review.md +1 -1
- 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/discuss-phase.md +2 -2
- 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 +29 -12
- 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 +13 -10
- package/pp-core/workflows/quick.md +5 -1
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +7 -3
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/validate-phase.md +5 -1
- package/pp-core/workflows/vault-review.md +65 -0
- package/pp-core/workflows/verify-work.md +6 -3
- 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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Loop gate runner — the one owner of the generic `check.query` gate contract.
|
|
4
|
+
*
|
|
5
|
+
* Command surface: pp-tools loop run-gates <point> [--phase <N>] [--tier light|standard|heavy] [--query <q>] [--raw]
|
|
6
|
+
*
|
|
7
|
+
* Resolves the active hooks at <point> through the loop resolver (the same
|
|
8
|
+
* owner `loop render-hooks` uses, so an unreadable config refuses here too),
|
|
9
|
+
* then runs every active gate in registry order and applies the two-step gate
|
|
10
|
+
* contract execute-phase states for `execute:wave:post` / `execute:post`:
|
|
11
|
+
*
|
|
12
|
+
* Step 1 — did the check COMMAND succeed? (`pp-tools check <query> [<phase>]
|
|
13
|
+
* [<check.args>…] --raw` exits 0 with a JSON object). If not: `onError: halt` halts,
|
|
14
|
+
* `onError: skip` (or absent) warns and moves on; `block` is never read.
|
|
15
|
+
* Step 2 — read `block`. A blocking gate (`blocking: true`) halts on
|
|
16
|
+
* `block: true`; that halt is never bypassed by `onError`. An advisory gate
|
|
17
|
+
* (`blocking` not true) reports `block: true` or a non-empty `message` as an
|
|
18
|
+
* advisory and never halts. `block: false` with no message passes silently;
|
|
19
|
+
* a blocking gate that passes WITH a message (a note) passes with an `ℹ` line.
|
|
20
|
+
*
|
|
21
|
+
* `check.args` (optional) are literal tokens the gate declaration appends after
|
|
22
|
+
* the phase — the gate's own policy, stated where the gate is declared (e.g.
|
|
23
|
+
* `--stage plan`). `--query <q>` runs only the gates whose `check.query` is `q`,
|
|
24
|
+
* so a host can run one gate in code beside a loop that handles the others.
|
|
25
|
+
*
|
|
26
|
+
* A gate this runner cannot run (no `check.query`, or a query that is not a
|
|
27
|
+
* check name) halts when it is blocking and is reported when it is advisory:
|
|
28
|
+
* a blocking gate is never passed having checked nothing.
|
|
29
|
+
*
|
|
30
|
+
* Output: the verdict JSON on stdout (`{ point, phase, halt, gates[], lines[] }`),
|
|
31
|
+
* every line of `lines` also on stderr so a host capturing `$(…)` still shows
|
|
32
|
+
* them, and exit 1 when `halt` is true. A host therefore writes
|
|
33
|
+
* `X=$(pp_run loop run-gates <point> …) || exit 1` and the halt stops it.
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
39
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const ioMod = require("./io.cjs");
|
|
42
|
+
const { output: coreOutput, error: coreError, ERROR_REASON } = ioMod;
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
+
const loopResolverModule = require("./loop-resolver.cjs");
|
|
45
|
+
const { resolveProjectLoopHooks } = loopResolverModule;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const configLoaderModule = require("./config-loader.cjs");
|
|
48
|
+
const { unreadableConfigFiles } = configLoaderModule;
|
|
49
|
+
const phase_tier_cjs_1 = require("./phase-tier.cjs");
|
|
50
|
+
/** A check name as `routeCheckCommand` accepts it (dotted or hyphenated); never an option or a path. */
|
|
51
|
+
const CHECK_QUERY_RE = /^[a-z0-9][a-z0-9.-]*$/i;
|
|
52
|
+
/** A declared `check.args` token: a flag or a plain word — never a path, a quote or a shell metacharacter. */
|
|
53
|
+
const CHECK_ARG_RE = /^(?:--[a-z][a-z0-9-]*|[a-z0-9][a-z0-9._-]*)$/i;
|
|
54
|
+
const CHECK_ARGS_MAX = 8;
|
|
55
|
+
const CHECK_TIMEOUT_MS = 300_000;
|
|
56
|
+
/** A check's operator message is relayed whole (line breaks kept), up to this many characters. */
|
|
57
|
+
const MESSAGE_CAP = 16_000;
|
|
58
|
+
function boundMessage(v) {
|
|
59
|
+
if (typeof v !== 'string')
|
|
60
|
+
return null;
|
|
61
|
+
const flat = v.replace(/\r\n?/g, '\n').replace(/[\u0000-\u0008\u000B-\u001F\u007F]+/g, ' ').trim();
|
|
62
|
+
if (flat === '')
|
|
63
|
+
return null;
|
|
64
|
+
return flat.length > MESSAGE_CAP ? `${flat.slice(0, MESSAGE_CAP)}…` : flat;
|
|
65
|
+
}
|
|
66
|
+
/** The declared `check.args`, or null when the declaration carries an argument this runner will not pass. */
|
|
67
|
+
function declaredCheckArgs(check) {
|
|
68
|
+
const args = check ? check['args'] : undefined;
|
|
69
|
+
if (args === undefined)
|
|
70
|
+
return [];
|
|
71
|
+
if (!Array.isArray(args) || args.length > CHECK_ARGS_MAX)
|
|
72
|
+
return null;
|
|
73
|
+
return args.every((a) => typeof a === 'string' && CHECK_ARG_RE.test(a)) ? args : null;
|
|
74
|
+
}
|
|
75
|
+
/** Run `pp-tools check <query> [<phase>] [<args>…] --raw` in a child process (check handlers exit the process). */
|
|
76
|
+
const defaultCheckRunner = (query, phase, cwd, checkArgs) => {
|
|
77
|
+
const tools = node_path_1.default.join(__dirname, '..', 'pp-tools.cjs');
|
|
78
|
+
const args = [tools, 'check', query, ...(phase ? [phase] : []), ...checkArgs, '--raw'];
|
|
79
|
+
const r = (0, shell_command_projection_cjs_1.execTool)(process.execPath, args, { cwd, timeout: CHECK_TIMEOUT_MS });
|
|
80
|
+
if (r.exitCode !== 0 || r.signal) {
|
|
81
|
+
const why = r.signal ? `killed by ${r.signal}` : `exit ${r.exitCode}`;
|
|
82
|
+
return { ok: false, result: null, error: `${why}${r.stderr ? `: ${r.stderr.split('\n')[0]}` : ''}` };
|
|
83
|
+
}
|
|
84
|
+
if (r.stdout === '')
|
|
85
|
+
return { ok: false, result: null, error: 'empty output' };
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(r.stdout);
|
|
88
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
89
|
+
return { ok: false, result: null, error: 'output is not a JSON object' };
|
|
90
|
+
return { ok: true, result: parsed, error: null };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { ok: false, result: null, error: 'output is not JSON' };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Run every active gate at `point` (already resolved) under the two-step
|
|
98
|
+
* contract. Pure apart from `runCheck`.
|
|
99
|
+
*/
|
|
100
|
+
function runGates(point, phase, activeHooks, cwd, runCheck = defaultCheckRunner) {
|
|
101
|
+
const gates = [];
|
|
102
|
+
const lines = [];
|
|
103
|
+
let halt = false;
|
|
104
|
+
for (const h of activeHooks) {
|
|
105
|
+
if (h.kind !== 'gate')
|
|
106
|
+
continue;
|
|
107
|
+
const blocking = h.blocking === true;
|
|
108
|
+
const onError = typeof h.onError === 'string' ? h.onError : 'skip';
|
|
109
|
+
const check = h.check && typeof h.check === 'object' ? h.check : null;
|
|
110
|
+
const query = check && typeof check['query'] === 'string' ? check['query'] : null;
|
|
111
|
+
const checkArgs = declaredCheckArgs(check);
|
|
112
|
+
const run = { capId: h.capId, query, blocking, onError, outcome: 'pass', block: null, message: null };
|
|
113
|
+
if (query === null || !CHECK_QUERY_RE.test(query) || checkArgs === null) {
|
|
114
|
+
run.outcome = 'unrunnable';
|
|
115
|
+
run.message = query === null
|
|
116
|
+
? 'the gate declares no check.query'
|
|
117
|
+
: checkArgs === null ? 'check.args is not a list of plain flags and words' : 'check.query is not a check name';
|
|
118
|
+
if (blocking) {
|
|
119
|
+
halt = true;
|
|
120
|
+
lines.push(`⛔ ${point} blocked: capability gate (${h.capId}) cannot be run (${run.message}). Fix the capability declaration before continuing.`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
lines.push(`⚠ ${h.capId} advisory (${point}): gate not run — ${run.message}.`);
|
|
124
|
+
}
|
|
125
|
+
gates.push(run);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// Step 1 — did the check command itself succeed?
|
|
129
|
+
const r = runCheck(query, phase, cwd, checkArgs);
|
|
130
|
+
if (!r.ok || r.result === null) {
|
|
131
|
+
run.message = boundMessage(r.error) ?? 'command error';
|
|
132
|
+
if (onError === 'halt') {
|
|
133
|
+
run.outcome = 'error-halt';
|
|
134
|
+
halt = true;
|
|
135
|
+
lines.push(`⛔ ${point} blocked: gate check command failed (${h.capId}: check ${query}): ${run.message}. Resolve before continuing.`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
run.outcome = 'error-skip';
|
|
139
|
+
lines.push(`⚠ ${point} gate check command failed (${h.capId}: check ${query}): ${run.message} — skipped (onError: ${onError}).`);
|
|
140
|
+
}
|
|
141
|
+
gates.push(run);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Step 2 — read block (only reached when the command succeeded).
|
|
145
|
+
const block = r.result['block'] === true;
|
|
146
|
+
run.block = block;
|
|
147
|
+
run.message = boundMessage(r.result['message']) ?? boundMessage(r.result['reason']);
|
|
148
|
+
if (blocking && block) {
|
|
149
|
+
run.outcome = 'blocked';
|
|
150
|
+
halt = true;
|
|
151
|
+
lines.push(`⛔ ${point} blocked by capability gate (${h.capId}): ${run.message ?? `check ${query} reported block`}. Resolve before continuing.`);
|
|
152
|
+
}
|
|
153
|
+
else if (!blocking && (block || boundMessage(r.result['message']) !== null)) {
|
|
154
|
+
run.outcome = 'advisory';
|
|
155
|
+
lines.push(`⚠ ${h.capId} advisory (${point}): ${run.message ?? `check ${query} reported block`}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
run.outcome = 'pass';
|
|
159
|
+
const note = blocking ? boundMessage(r.result['message']) : null;
|
|
160
|
+
if (note !== null)
|
|
161
|
+
lines.push(`ℹ ${h.capId} (${point}): ${note}`);
|
|
162
|
+
}
|
|
163
|
+
gates.push(run);
|
|
164
|
+
}
|
|
165
|
+
return { point, phase, halt, gates, lines };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* `loop run-gates <point> [--phase <N>] [--tier <t>]`. Exit 1 on a halt or a
|
|
169
|
+
* refusal; the verdict JSON is on stdout either way except on a refusal.
|
|
170
|
+
*/
|
|
171
|
+
function cmdLoopRunGates(cwd, point, raw, options = {}) {
|
|
172
|
+
if (!point) {
|
|
173
|
+
coreError('loop run-gates requires a <point> argument (e.g. verify:post)', ERROR_REASON.USAGE);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const phase = typeof options['phase'] === 'string' && options['phase'] !== '' ? options['phase'] : null;
|
|
177
|
+
const tierCli = typeof options['tier'] === 'string' && options['tier'] !== '' ? options['tier'] : null;
|
|
178
|
+
if (tierCli !== null && !['light', 'standard', 'heavy'].includes(tierCli)) {
|
|
179
|
+
coreError('--tier needs one of light, standard, heavy', ERROR_REASON.USAGE);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const phaseTier = (phase !== null || tierCli !== null) && unreadableConfigFiles(cwd).length === 0
|
|
183
|
+
? (0, phase_tier_cjs_1.resolvePhaseTierForProject)(cwd, phase, tierCli)
|
|
184
|
+
: null;
|
|
185
|
+
const outcome = resolveProjectLoopHooks(cwd, point, { phaseTier, what: `loop run-gates ${point}` });
|
|
186
|
+
if (!outcome.ok) {
|
|
187
|
+
coreError(outcome.message, outcome.reason === 'config_unreadable' ? ERROR_REASON.CONFIG_PARSE_FAILED : undefined);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const onlyQuery = typeof options['query'] === 'string' && options['query'] !== '' ? options['query'] : null;
|
|
191
|
+
const hooks = onlyQuery === null
|
|
192
|
+
? outcome.resolved.activeHooks
|
|
193
|
+
: outcome.resolved.activeHooks.filter((h) => {
|
|
194
|
+
const c = h.check && typeof h.check === 'object' ? h.check : null;
|
|
195
|
+
return c !== null && c['query'] === onlyQuery;
|
|
196
|
+
});
|
|
197
|
+
const verdict = runGates(point, phase, hooks, cwd);
|
|
198
|
+
if (verdict.lines.length > 0) {
|
|
199
|
+
try {
|
|
200
|
+
process.stderr.write(verdict.lines.join('\n') + '\n');
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
/* stderr closed */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
coreOutput(verdict, raw);
|
|
207
|
+
if (verdict.halt)
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
}
|
|
210
|
+
module.exports = {
|
|
211
|
+
runGates,
|
|
212
|
+
cmdLoopRunGates,
|
|
213
|
+
};
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
*/
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
32
|
const ioMod = require("./io.cjs");
|
|
33
|
-
const { output: coreOutput, error: coreError } = ioMod;
|
|
33
|
+
const { output: coreOutput, error: coreError, ERROR_REASON } = ioMod;
|
|
34
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
35
|
const configLoaderModule = require("./config-loader.cjs");
|
|
36
|
-
const { loadConfig } = configLoaderModule;
|
|
36
|
+
const { loadConfig, unreadableConfigFiles, describeUnreadableConfig } = configLoaderModule;
|
|
37
37
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
38
|
const capabilityStateModule = require("./capability-state.cjs");
|
|
39
39
|
const { resolveCapabilityRuntimeState } = capabilityStateModule;
|
|
@@ -382,6 +382,80 @@ function renderLoopHooks(resolved) {
|
|
|
382
382
|
return lines.join('\n');
|
|
383
383
|
}
|
|
384
384
|
// ─── I/O command handler ───────────────────────────────────────────────────────
|
|
385
|
+
/**
|
|
386
|
+
* The refusal text for a caller that resolves hooks over an unreadable config.
|
|
387
|
+
* One wording for every such caller (`loop render-hooks`, `phase.dispatch-plan`).
|
|
388
|
+
*/
|
|
389
|
+
function unreadableConfigRefusal(what, unreadable) {
|
|
390
|
+
return (`${what} refused: the project config cannot be read, so no hook can be resolved ` +
|
|
391
|
+
`(rendering anyway would silently drop every hook the config switches on, a required client vault among them). ` +
|
|
392
|
+
`${describeUnreadableConfig(unreadable)}. Repair or remove the file, then re-run. Stop this workflow here.`);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Resolve the active hooks at `point` for the project at `cwd`, exactly as
|
|
396
|
+
* `loop render-hooks` does: refuse an unreadable config, load the config
|
|
397
|
+
* snapshot once, overlay the phase tier's gate booleans when one is given,
|
|
398
|
+
* resolve capability state over the same snapshot, then resolve the hooks.
|
|
399
|
+
* The ONE owner of "which hooks are active here" for every caller that needs
|
|
400
|
+
* the answer in code (`loop render-hooks`, `phase.dispatch-plan`'s executor
|
|
401
|
+
* contributions). Never exits; the caller decides how to report a refusal.
|
|
402
|
+
*/
|
|
403
|
+
function resolveProjectLoopHooks(cwd, point, opts = {}) {
|
|
404
|
+
// Fail closed on a config that exists but cannot be read. Every hook's `when`
|
|
405
|
+
// and activation key is read from it, and both readers below swallow a parse
|
|
406
|
+
// failure into the defaults, so rendering anyway drops exactly the hooks the
|
|
407
|
+
// project switched on — a required client vault's fetch and blocking gate
|
|
408
|
+
// among them. A MISSING config is not refused: that is a fresh project, and
|
|
409
|
+
// the defaults are its real settings.
|
|
410
|
+
const unreadable = unreadableConfigFiles(cwd);
|
|
411
|
+
if (unreadable.length > 0) {
|
|
412
|
+
return { ok: false, reason: 'config_unreadable', message: unreadableConfigRefusal(opts.what ?? `loop render-hooks ${point}`, unreadable) };
|
|
413
|
+
}
|
|
414
|
+
// Load the config snapshot ONCE and share it with both the capability-state
|
|
415
|
+
// resolver (via configOverride) and loop-hook resolution, so federated keys
|
|
416
|
+
// present in loadConfig resolve identically for `active` and for hook when/
|
|
417
|
+
// configValues. Keys absent from loadConfig still fall through to raw
|
|
418
|
+
// .planning/config.json reads (precedence levels 2-3) in each pass.
|
|
419
|
+
let config;
|
|
420
|
+
try {
|
|
421
|
+
config = loadConfig(cwd);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
config = {};
|
|
425
|
+
}
|
|
426
|
+
if (opts.phaseTier)
|
|
427
|
+
config = (0, phase_tier_cjs_1.overlayTierGates)(config, opts.phaseTier);
|
|
428
|
+
const state = resolveCapabilityRuntimeState(cwd, opts.configDir, config);
|
|
429
|
+
// Registry is the static generated module — same object capability-state uses internally.
|
|
430
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
431
|
+
const registry = require('./capability-registry.cjs');
|
|
432
|
+
const capabilityStatesById = new Map();
|
|
433
|
+
for (const cap of state.capabilities || []) {
|
|
434
|
+
capabilityStatesById.set(cap.id, cap);
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
const resolved = resolveLoopHooks({ point, registry, config, cwd, capabilityStatesById });
|
|
438
|
+
return { ok: true, resolved, warnings: state.warnings ?? [] };
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
return { ok: false, reason: 'invalid_point', message: (err instanceof Error) ? err.message : String(err) };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* The `fragment.inline` text of every active contribution into `role`, in
|
|
446
|
+
* resolved order, with the capability that contributed it.
|
|
447
|
+
*/
|
|
448
|
+
function contributionsInto(resolved, role) {
|
|
449
|
+
const out = [];
|
|
450
|
+
for (const h of resolved.activeHooks) {
|
|
451
|
+
if (h.kind !== 'contribution' || h.into !== role)
|
|
452
|
+
continue;
|
|
453
|
+
if (typeof h.fragment?.inline !== 'string')
|
|
454
|
+
continue;
|
|
455
|
+
out.push({ capId: h.capId, fragment: h.fragment.inline });
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
385
459
|
/**
|
|
386
460
|
* Command entry point: load registry + config, resolve + render, emit envelope.
|
|
387
461
|
*
|
|
@@ -396,6 +470,10 @@ function renderLoopHooks(resolved) {
|
|
|
396
470
|
* pre-cutover keys like `workflow.ui_phase` that live in config.json but are not
|
|
397
471
|
* yet exposed through loadConfig's whitelist.
|
|
398
472
|
*
|
|
473
|
+
* Unreadable config: when any project config file exists but cannot be parsed
|
|
474
|
+
* (`unreadableConfigFiles`), every mode refuses before rendering — exit 1,
|
|
475
|
+
* empty stdout, one stderr line naming each file and its parse error.
|
|
476
|
+
*
|
|
399
477
|
* --active-cap <capId>: when present, resolves hooks for <point> exactly as the
|
|
400
478
|
* normal path does, then prints exactly `true` (if any resolved activeHook has
|
|
401
479
|
* capId === <capId>) or `false` followed by a single newline, and exits 0.
|
|
@@ -403,6 +481,13 @@ function renderLoopHooks(resolved) {
|
|
|
403
481
|
* Missing <capId> value → coreError + non-zero exit.
|
|
404
482
|
* Unknown/inactive capId → `false` (not an error).
|
|
405
483
|
*
|
|
484
|
+
* --into <role>: resolves exactly as the normal path does, then prints the
|
|
485
|
+
* `fragment.inline` text of each active contribution whose `into` equals
|
|
486
|
+
* <role>, in resolved order, separated by one blank line, with a trailing
|
|
487
|
+
* newline when anything printed. Nothing at all (empty stdout, exit 0) when no
|
|
488
|
+
* contribution matches. No JSON envelope; `--raw` is ignored. `--active-cap`
|
|
489
|
+
* takes precedence: given both, `--into` is ignored.
|
|
490
|
+
*
|
|
406
491
|
* --phase <N> [--tier light|standard|heavy] (pack 9): resolve the phase's
|
|
407
492
|
* ceremony tier (src/phase-tier.cts) and overlay its gate booleans
|
|
408
493
|
* (`workflow.research`, `workflow.pattern_mapper`, …) on the config snapshot
|
|
@@ -422,23 +507,15 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
422
507
|
coreError('--active-cap requires a <capId> value (e.g. --active-cap tdd)');
|
|
423
508
|
return;
|
|
424
509
|
}
|
|
510
|
+
// --into <role> mode: emit matching contribution fragment text only
|
|
511
|
+
const intoRole = typeof options['into'] === 'string' ? options['into'] : undefined;
|
|
512
|
+
if (activeCapId === undefined && intoRole !== undefined && intoRole.trim() === '') {
|
|
513
|
+
coreError('Missing value for --into (e.g. --into executor)', ERROR_REASON.USAGE);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
425
516
|
const runtimeConfigDir = typeof options['configDir'] === 'string'
|
|
426
517
|
? options['configDir']
|
|
427
518
|
: undefined;
|
|
428
|
-
// Load the config snapshot ONCE and share it with both the capability-state
|
|
429
|
-
// resolver (via configOverride) and loop-hook resolution, so federated keys
|
|
430
|
-
// present in loadConfig resolve identically for `active` and for hook when/
|
|
431
|
-
// configValues — eliminating the previous double loadConfig() call. Note: keys
|
|
432
|
-
// absent from loadConfig still fall through to raw .planning/config.json reads
|
|
433
|
-
// (precedence levels 2-3) in each pass; that residual re-read window is
|
|
434
|
-
// pre-existing (unchanged by this consolidation), not introduced here.
|
|
435
|
-
let config;
|
|
436
|
-
try {
|
|
437
|
-
config = loadConfig(cwd);
|
|
438
|
-
}
|
|
439
|
-
catch {
|
|
440
|
-
config = {};
|
|
441
|
-
}
|
|
442
519
|
const tierPhase = typeof options['phase'] === 'string' && options['phase'] !== '' ? options['phase'] : null;
|
|
443
520
|
const tierCli = typeof options['tier'] === 'string' && options['tier'] !== '' ? options['tier'] : null;
|
|
444
521
|
let phaseTier = null;
|
|
@@ -447,32 +524,29 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
447
524
|
coreError('--tier needs one of light, standard, heavy');
|
|
448
525
|
return;
|
|
449
526
|
}
|
|
450
|
-
|
|
451
|
-
|
|
527
|
+
// The refusal below must come first: never resolve a tier from a config it cannot read.
|
|
528
|
+
if (unreadableConfigFiles(cwd).length === 0)
|
|
529
|
+
phaseTier = (0, phase_tier_cjs_1.resolvePhaseTierForProject)(cwd, tierPhase, tierCli);
|
|
452
530
|
}
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const registry = require('./capability-registry.cjs');
|
|
457
|
-
const capabilityStatesById = new Map();
|
|
458
|
-
for (const cap of state.capabilities || []) {
|
|
459
|
-
capabilityStatesById.set(cap.id, cap);
|
|
460
|
-
}
|
|
461
|
-
let resolved;
|
|
462
|
-
try {
|
|
463
|
-
resolved = resolveLoopHooks({ point, registry, config, cwd, capabilityStatesById });
|
|
464
|
-
}
|
|
465
|
-
catch (err) {
|
|
466
|
-
const msg = (err instanceof Error) ? err.message : String(err);
|
|
467
|
-
coreError(msg);
|
|
531
|
+
const outcome = resolveProjectLoopHooks(cwd, point, { configDir: runtimeConfigDir, phaseTier });
|
|
532
|
+
if (!outcome.ok) {
|
|
533
|
+
coreError(outcome.message, outcome.reason === 'config_unreadable' ? ERROR_REASON.CONFIG_PARSE_FAILED : undefined);
|
|
468
534
|
return;
|
|
469
535
|
}
|
|
536
|
+
const resolved = outcome.resolved;
|
|
470
537
|
// --active-cap mode: print exactly 'true' or 'false' with no envelope
|
|
471
538
|
if (activeCapId !== undefined) {
|
|
472
539
|
const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId);
|
|
473
540
|
process.stdout.write(isActive ? 'true\n' : 'false\n');
|
|
474
541
|
return;
|
|
475
542
|
}
|
|
543
|
+
// --into mode: fragment text of the active contributions targeting <role>
|
|
544
|
+
if (intoRole !== undefined) {
|
|
545
|
+
const texts = contributionsInto(resolved, intoRole).map((c) => c.fragment);
|
|
546
|
+
if (texts.length > 0)
|
|
547
|
+
process.stdout.write(texts.join('\n\n') + '\n');
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
476
550
|
const rendered = renderLoopHooks(resolved);
|
|
477
551
|
const envelope = {
|
|
478
552
|
point: resolved.point,
|
|
@@ -482,8 +556,8 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
482
556
|
if (phaseTier) {
|
|
483
557
|
envelope.phase_tier = { phase: tierPhase, tier: phaseTier.tier, source: phaseTier.source, suppressed: phaseTier.suppressed };
|
|
484
558
|
}
|
|
485
|
-
if (
|
|
486
|
-
envelope.warnings =
|
|
559
|
+
if (outcome.warnings.length > 0) {
|
|
560
|
+
envelope.warnings = outcome.warnings;
|
|
487
561
|
}
|
|
488
562
|
// No rawValue third argument, so --raw emits the JSON envelope rather than
|
|
489
563
|
// plain text. This is deliberate and load-bearing: 23 workflow call sites do
|
|
@@ -497,6 +571,8 @@ module.exports = {
|
|
|
497
571
|
resolveLoopHooks,
|
|
498
572
|
renderLoopHooks,
|
|
499
573
|
cmdLoopRenderHooks,
|
|
574
|
+
resolveProjectLoopHooks,
|
|
575
|
+
contributionsInto,
|
|
500
576
|
// Exported for tests
|
|
501
577
|
_getNestedConfigValue,
|
|
502
578
|
_resolveActivationValue,
|