@pareto4/pp-core 1.12.0 → 1.12.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 +1 -1
- 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/config-loader.cjs +151 -8
- package/pp-core/bin/lib/loop-resolver.cjs +16 -2
- package/pp-core/bin/lib/vault.cjs +38 -1
- package/pp-core/references/loop-hook-dispatch.md +7 -0
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +3 -3
- package/pp-core/workflows/code-review-fix.md +1 -1
- package/pp-core/workflows/code-review.md +1 -1
- package/pp-core/workflows/discuss-phase.md +2 -2
- package/pp-core/workflows/execute-phase.md +7 -7
- package/pp-core/workflows/plan-phase.md +6 -6
- package/pp-core/workflows/quick.md +1 -1
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +2 -2
- package/pp-core/workflows/validate-phase.md +1 -1
- package/pp-core/workflows/verify-work.md +3 -3
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pp-core",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "PP Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads pp-core's operating context into every Gemini CLI session.",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pareto4/pp-core",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "Pareto pp-core — The Pareto Project's AI-assisted engineering framework: meta-prompting, context engineering, and spec-driven development for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pp-core": "bin/install.js",
|
|
@@ -55,6 +55,20 @@ function _readRawConfigKey(filePath, dotKey) {
|
|
|
55
55
|
return { found: false, value: undefined };
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The raw config files levels 2 and 3 read, in read order: the active
|
|
60
|
+
* `planningDir(cwd)/config.json`, then the root `planningRoot(cwd)/config.json`
|
|
61
|
+
* when it is a different file. Exported so `unreadableConfigFiles`
|
|
62
|
+
* (src/config-loader.cts) parses every file the hook resolver reads — this list
|
|
63
|
+
* beside `configFilesFor`, the loader's — without restating either. Both levels
|
|
64
|
+
* skip an unparseable file with a stderr warning and read on, which is the
|
|
65
|
+
* swallow a gate-governing caller must refuse.
|
|
66
|
+
*/
|
|
67
|
+
function activationConfigFilesFor(cwd) {
|
|
68
|
+
const wsConfigPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
69
|
+
const rootConfigPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
70
|
+
return wsConfigPath !== rootConfigPath ? [wsConfigPath, rootConfigPath] : [wsConfigPath];
|
|
71
|
+
}
|
|
58
72
|
/**
|
|
59
73
|
* Resolve the raw value for a dotted config key using the four-level precedence
|
|
60
74
|
* walk. Returns { found, value } with the RAW value (not coerced to boolean),
|
|
@@ -76,15 +90,10 @@ function resolveConfigKey(dotKey, opts) {
|
|
|
76
90
|
return { found: true, value: fromConfig.value };
|
|
77
91
|
// Level 2 + 3: raw config.json files (only when cwd is available)
|
|
78
92
|
if (cwd) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return { found: true, value: fromWs.value };
|
|
84
|
-
if (wsConfigPath !== rootConfigPath) {
|
|
85
|
-
const fromRoot = _readRawConfigKey(rootConfigPath, dotKey);
|
|
86
|
-
if (fromRoot.found)
|
|
87
|
-
return { found: true, value: fromRoot.value };
|
|
93
|
+
for (const filePath of activationConfigFilesFor(cwd)) {
|
|
94
|
+
const fromFile = _readRawConfigKey(filePath, dotKey);
|
|
95
|
+
if (fromFile.found)
|
|
96
|
+
return { found: true, value: fromFile.value };
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
// Level 4: registry configSchema default
|
|
@@ -110,4 +119,5 @@ module.exports = {
|
|
|
110
119
|
_readRawConfigKey,
|
|
111
120
|
_resolveActivationValue,
|
|
112
121
|
resolveConfigKey,
|
|
122
|
+
activationConfigFilesFor,
|
|
113
123
|
};
|
|
@@ -43,6 +43,9 @@ const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
|
43
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
44
|
const federatedConfigModule = require("./federated-config.cjs");
|
|
45
45
|
const { mergeFederatedConfig } = federatedConfigModule;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const capabilityActivationModule = require("./capability-activation.cjs");
|
|
48
|
+
const { activationConfigFilesFor } = capabilityActivationModule;
|
|
46
49
|
// The capability-registry.cjs is generated and lives in the same gsd-core/bin/lib/ output dir.
|
|
47
50
|
// Both config-loader.cjs and capability-registry.cjs land in gsd-core/bin/lib/ at build time.
|
|
48
51
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
@@ -341,16 +344,154 @@ function _applyFederatedOverlay(baseConfig, userConfig) {
|
|
|
341
344
|
_applyFederatedValues(cloned, _fedOverlay.values, _fedOverlay.validKeys);
|
|
342
345
|
return cloned;
|
|
343
346
|
}
|
|
344
|
-
|
|
347
|
+
/**
|
|
348
|
+
* The workstream `loadConfig` reads for `options`: an explicit `workstream`
|
|
349
|
+
* option, then `workstreamContext.ws`, then `GSD_WORKSTREAM`. Shared with
|
|
350
|
+
* `configFilesFor` so the two can never resolve different workstreams.
|
|
351
|
+
*/
|
|
352
|
+
function _activeWorkstreamFor(options) {
|
|
345
353
|
const activeWorkstream = Object.prototype.hasOwnProperty.call(options, 'workstream')
|
|
346
354
|
? options['workstream']
|
|
347
355
|
: (options['workstreamContext'] && Object.prototype.hasOwnProperty.call(options['workstreamContext'], 'ws'))
|
|
348
356
|
? options['workstreamContext']['ws']
|
|
349
357
|
: (process.env['GSD_WORKSTREAM'] || null);
|
|
358
|
+
return typeof activeWorkstream === 'string' ? activeWorkstream : null;
|
|
359
|
+
}
|
|
360
|
+
function _configPathsFor(cwd, ws) {
|
|
361
|
+
return {
|
|
362
|
+
rootConfigPath: ws ? node_path_1.default.join(planningRoot(cwd), 'config.json') : null,
|
|
363
|
+
configPath: node_path_1.default.join(planningDir(cwd, ws), 'config.json'),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** `~/.gsd/defaults.json` (or under `GSD_HOME`): read only in a pre-project context (#1683). */
|
|
367
|
+
function _globalDefaultsPath() {
|
|
368
|
+
return node_path_1.default.join(process.env['GSD_HOME'] || node_os_1.default.homedir(), '.gsd', 'defaults.json');
|
|
369
|
+
}
|
|
370
|
+
/** The #1683 predicate: the global defaults are consulted only when the active planning dir is absent. */
|
|
371
|
+
function _readsGlobalDefaults(cwd, ws) {
|
|
372
|
+
return !node_fs_1.default.existsSync(planningDir(cwd, ws));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Every config file `loadConfig(cwd, options)` reads, as absolute paths in read
|
|
376
|
+
* order. ONE owner of "which config files does the loader read": `loadConfig`
|
|
377
|
+
* takes its paths from the same `_configPathsFor`, so a reader that must refuse
|
|
378
|
+
* an unreadable config (`unreadableConfigFiles` below) parses exactly these
|
|
379
|
+
* files without restating the workstream ladder.
|
|
380
|
+
*
|
|
381
|
+
* Why a caller needs it: `loadConfig` swallows a parse failure in every one of
|
|
382
|
+
* these files (root → "workstream config stands alone", active → the defaults),
|
|
383
|
+
* so its RESULT cannot say whether it read the project's config or read
|
|
384
|
+
* nothing. Files that do not exist are still listed; absence is not a failure
|
|
385
|
+
* for `loadConfig`, and the caller decides what it means.
|
|
386
|
+
*/
|
|
387
|
+
function configFilesFor(cwd, options = {}) {
|
|
388
|
+
const ws = _activeWorkstreamFor(options);
|
|
389
|
+
const { rootConfigPath, configPath } = _configPathsFor(cwd, ws);
|
|
390
|
+
const files = [];
|
|
391
|
+
if (rootConfigPath)
|
|
392
|
+
files.push(rootConfigPath);
|
|
393
|
+
files.push(configPath);
|
|
394
|
+
if (_readsGlobalDefaults(cwd, ws))
|
|
395
|
+
files.push(_globalDefaultsPath());
|
|
396
|
+
return files;
|
|
397
|
+
}
|
|
398
|
+
/** The error text is bounded; composed text never reaches the cap, but it holds anyway. */
|
|
399
|
+
const UNREADABLE_ERROR_CAP = 200;
|
|
400
|
+
/**
|
|
401
|
+
* Describe why a config file could not be used WITHOUT echoing it. V8's
|
|
402
|
+
* `JSON.parse` messages sometimes quote the input (`Unexpected token '}',
|
|
403
|
+
* "{"a":tru}" is not valid JSON`), so only the position is carried over.
|
|
404
|
+
*/
|
|
405
|
+
function _describeConfigReadFailure(err) {
|
|
406
|
+
const e = err;
|
|
407
|
+
if (e && typeof e.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(e.code))
|
|
408
|
+
return `cannot be read (${e.code})`;
|
|
409
|
+
const message = e && typeof e.message === 'string' ? e.message : '';
|
|
410
|
+
const lineCol = /\(line (\d+) column (\d+)\)/.exec(message);
|
|
411
|
+
if (lineCol)
|
|
412
|
+
return `not valid JSON (line ${lineCol[1]}, column ${lineCol[2]})`;
|
|
413
|
+
const position = /at position (\d+)/.exec(message);
|
|
414
|
+
if (position)
|
|
415
|
+
return `not valid JSON (at position ${position[1]})`;
|
|
416
|
+
if (/Unexpected end of JSON input/.test(message))
|
|
417
|
+
return 'not valid JSON (the text ends before the JSON does)';
|
|
418
|
+
return 'not valid JSON';
|
|
419
|
+
}
|
|
420
|
+
/** How a refusal names a config file: never an absolute user path. */
|
|
421
|
+
function _configDisplayPath(cwd, abs) {
|
|
422
|
+
const under = (base) => {
|
|
423
|
+
const rel = node_path_1.default.relative(base, abs);
|
|
424
|
+
return rel !== '' && !rel.startsWith('..') && !node_path_1.default.isAbsolute(rel) ? rel.split(node_path_1.default.sep).join('/') : null;
|
|
425
|
+
};
|
|
426
|
+
const inProject = under(cwd);
|
|
427
|
+
if (inProject !== null)
|
|
428
|
+
return inProject;
|
|
429
|
+
const homeOverride = process.env['GSD_HOME'];
|
|
430
|
+
const inHomeOverride = homeOverride ? under(homeOverride) : null;
|
|
431
|
+
if (inHomeOverride !== null)
|
|
432
|
+
return `$GSD_HOME/${inHomeOverride}`;
|
|
433
|
+
const inHome = under(node_os_1.default.homedir());
|
|
434
|
+
if (inHome !== null)
|
|
435
|
+
return `~/${inHome}`;
|
|
436
|
+
return node_path_1.default.basename(abs);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Every config file the hook resolver would read that EXISTS but cannot be
|
|
440
|
+
* used: unreadable, not JSON, or JSON that is not an object (`loadConfig`
|
|
441
|
+
* reads none of an array's or a null's keys and answers the defaults, the same
|
|
442
|
+
* silent fallback as a parse failure). Empty when every file is usable or
|
|
443
|
+
* absent: a MISSING config is a fresh project and reads as the defaults.
|
|
444
|
+
*
|
|
445
|
+
* The files are the union, in first-seen order, of the loader's list
|
|
446
|
+
* (`configFilesFor`) and the activation resolver's raw levels
|
|
447
|
+
* (`activationConfigFilesFor`, which can name a root config the loader never
|
|
448
|
+
* opens under `GSD_PROJECT`). Reads go through `platformReadSync`, the reader
|
|
449
|
+
* `loadConfig` uses, so a file the loader can read is never called unreadable.
|
|
450
|
+
*
|
|
451
|
+
* This is the ONE owner of "can the project's config be read". Both swallowing
|
|
452
|
+
* readers (`loadConfig`, `resolveConfigKey`) stay tolerant for their many
|
|
453
|
+
* callers; a caller whose answer governs a gate (`loop render-hooks`, the vault
|
|
454
|
+
* load verdict) asks this first and refuses. Never throws.
|
|
455
|
+
*/
|
|
456
|
+
function unreadableConfigFiles(cwd, options = {}) {
|
|
457
|
+
const out = [];
|
|
458
|
+
let files;
|
|
459
|
+
try {
|
|
460
|
+
files = [...new Set([...configFilesFor(cwd, options), ...activationConfigFilesFor(cwd)])];
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
// An invalid GSD_WORKSTREAM / GSD_PROJECT segment: the config cannot be located, so it cannot be read.
|
|
464
|
+
return [{ file: '.planning/config.json', error: `cannot be located (${err instanceof Error ? err.message : String(err)})`.slice(0, UNREADABLE_ERROR_CAP) }];
|
|
465
|
+
}
|
|
466
|
+
for (const file of files) {
|
|
467
|
+
let error = null;
|
|
468
|
+
try {
|
|
469
|
+
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(file);
|
|
470
|
+
if (raw === null)
|
|
471
|
+
continue;
|
|
472
|
+
const parsed = JSON.parse(raw);
|
|
473
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
474
|
+
error = 'valid JSON but not an object, so none of its settings can be read';
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
error = _describeConfigReadFailure(err);
|
|
479
|
+
}
|
|
480
|
+
if (error !== null)
|
|
481
|
+
out.push({ file: _configDisplayPath(cwd, file), error: error.slice(0, UNREADABLE_ERROR_CAP) });
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
/** One line per unreadable file, `<file>: <error>`, for a refusal message. */
|
|
486
|
+
function describeUnreadableConfig(unreadable) {
|
|
487
|
+
return unreadable.map((u) => `${u.file}: ${u.error}`).join('; ');
|
|
488
|
+
}
|
|
489
|
+
function loadConfig(cwd, options = {}) {
|
|
350
490
|
// When GSD_WORKSTREAM is set, load root config first so workstream config
|
|
351
491
|
// can inherit from it. This prevents users from duplicating model_overrides,
|
|
352
492
|
// workflow.*, etc. across every workstream config (#2714).
|
|
353
|
-
const ws =
|
|
493
|
+
const ws = _activeWorkstreamFor(options);
|
|
494
|
+
const paths = _configPathsFor(cwd, ws);
|
|
354
495
|
// #315 — per-call lazy memo: all three detection sites inside this loadConfig
|
|
355
496
|
// call operate on the same cwd and the subrepo set cannot change mid-call, so
|
|
356
497
|
// a single scan is sufficient. The memo is scoped to THIS call (not module-level)
|
|
@@ -364,8 +505,8 @@ function loadConfig(cwd, options = {}) {
|
|
|
364
505
|
return cachedSubRepos.slice();
|
|
365
506
|
};
|
|
366
507
|
let rootParsed = null;
|
|
367
|
-
if (
|
|
368
|
-
const rootConfigPath =
|
|
508
|
+
if (paths.rootConfigPath) {
|
|
509
|
+
const rootConfigPath = paths.rootConfigPath;
|
|
369
510
|
try {
|
|
370
511
|
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(rootConfigPath);
|
|
371
512
|
if (raw === null)
|
|
@@ -400,7 +541,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
400
541
|
// Root config missing or unparseable — workstream config stands alone
|
|
401
542
|
}
|
|
402
543
|
}
|
|
403
|
-
const configPath =
|
|
544
|
+
const configPath = paths.configPath;
|
|
404
545
|
const defaults = CONFIG_DEFAULTS;
|
|
405
546
|
try {
|
|
406
547
|
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(configPath);
|
|
@@ -625,7 +766,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
625
766
|
}
|
|
626
767
|
catch {
|
|
627
768
|
// Fall back to ~/.gsd/defaults.json only for truly pre-project contexts (#1683)
|
|
628
|
-
if (
|
|
769
|
+
if (!_readsGlobalDefaults(cwd, ws)) {
|
|
629
770
|
if (rootParsed) {
|
|
630
771
|
// Workstream has no config.json: re-parse using root config as the sole source.
|
|
631
772
|
// (FIX 2: overlay is applied recursively in the re-entrant loadConfig call)
|
|
@@ -642,8 +783,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
642
783
|
}
|
|
643
784
|
}
|
|
644
785
|
try {
|
|
645
|
-
const
|
|
646
|
-
const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
786
|
+
const globalDefaultsPath = _globalDefaultsPath();
|
|
647
787
|
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(globalDefaultsPath);
|
|
648
788
|
if (raw === null)
|
|
649
789
|
throw new Error('missing');
|
|
@@ -698,6 +838,9 @@ function loadConfig(cwd, options = {}) {
|
|
|
698
838
|
}
|
|
699
839
|
module.exports = {
|
|
700
840
|
loadConfig,
|
|
841
|
+
configFilesFor,
|
|
842
|
+
unreadableConfigFiles,
|
|
843
|
+
describeUnreadableConfig,
|
|
701
844
|
isGitIgnored,
|
|
702
845
|
CONFIG_DEFAULTS,
|
|
703
846
|
_getConfigDefault,
|
|
@@ -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;
|
|
@@ -422,6 +422,20 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
422
422
|
coreError('--active-cap requires a <capId> value (e.g. --active-cap tdd)');
|
|
423
423
|
return;
|
|
424
424
|
}
|
|
425
|
+
// Fail closed on a config that exists but cannot be read. Every hook's `when`
|
|
426
|
+
// and activation key is read from it, and both readers below swallow a parse
|
|
427
|
+
// failure into the defaults, so rendering anyway drops exactly the hooks the
|
|
428
|
+
// project switched on — a required client vault's fetch and blocking gate
|
|
429
|
+
// among them — and exits 0. A MISSING config is not refused: that is a fresh
|
|
430
|
+
// project, and the defaults are its real settings. stdout stays empty so a
|
|
431
|
+
// host capturing `$(…)` never reads a refusal as "no hooks".
|
|
432
|
+
const unreadable = unreadableConfigFiles(cwd);
|
|
433
|
+
if (unreadable.length > 0) {
|
|
434
|
+
coreError(`loop render-hooks ${point} refused: the project config cannot be read, so no hook can be resolved ` +
|
|
435
|
+
`(rendering anyway would silently drop every hook the config switches on, a required client vault among them). ` +
|
|
436
|
+
`${describeUnreadableConfig(unreadable)}. Repair or remove the file, then re-run. Stop this workflow here.`, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
425
439
|
const runtimeConfigDir = typeof options['configDir'] === 'string'
|
|
426
440
|
? options['configDir']
|
|
427
441
|
: undefined;
|
|
@@ -37,6 +37,9 @@ 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
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const configLoaderModule = require("./config-loader.cjs");
|
|
42
|
+
const { unreadableConfigFiles } = configLoaderModule;
|
|
40
43
|
const CLONE_TIMEOUT_MS = 60_000; // explicit cap so a hung clone surfaces as failed → next tier
|
|
41
44
|
const REVPARSE_TIMEOUT_MS = 10_000;
|
|
42
45
|
// Default built-in config values — Phase 12 reads-with-default because the
|
|
@@ -1020,6 +1023,7 @@ function assertVaultWriteTarget(cwd, targetRepo, targetHost) {
|
|
|
1020
1023
|
}
|
|
1021
1024
|
const VAULT_VERDICTS = Object.freeze(['inert', 'loaded', 'degraded']);
|
|
1022
1025
|
const VAULT_REASONS = Object.freeze([
|
|
1026
|
+
'config_unreadable',
|
|
1023
1027
|
'vault_disabled',
|
|
1024
1028
|
'no_bindings',
|
|
1025
1029
|
'never_fetched',
|
|
@@ -1045,6 +1049,7 @@ const VAULT_VERDICT_FIELDS = Object.freeze([
|
|
|
1045
1049
|
]);
|
|
1046
1050
|
/** One static card line per reason (D-02). The card test enumerates VAULT_REASONS against this table. */
|
|
1047
1051
|
const VAULT_REASON_LINES = Object.freeze({
|
|
1052
|
+
config_unreadable: 'The project config cannot be read, so vault.enabled and vault.required are unknown — both are treated as true.',
|
|
1048
1053
|
vault_disabled: 'The client vault is disabled (vault.enabled is not true).',
|
|
1049
1054
|
no_bindings: 'No client vault is bound: .platform-bindings.json needs client.vaultRepo and client.vaultRef.',
|
|
1050
1055
|
never_fetched: 'The vault has not been fetched — .planning/vault/ holds no readable manifest.',
|
|
@@ -1055,6 +1060,7 @@ const VAULT_REASON_LINES = Object.freeze({
|
|
|
1055
1060
|
client_loaded: 'The client vault is loaded at its pinned ref.',
|
|
1056
1061
|
});
|
|
1057
1062
|
const VAULT_REASON_REMEDIES = Object.freeze({
|
|
1063
|
+
config_unreadable: 'Remedy: repair or remove the config file `pp-tools loop render-hooks plan:pre` names, then re-run this step.',
|
|
1058
1064
|
vault_disabled: '',
|
|
1059
1065
|
no_bindings: 'Remedy: run /pp-init to bind the client vault at a pinned ref.',
|
|
1060
1066
|
never_fetched: 'Remedy: run `pp-tools vault fetch`, then re-run this step.',
|
|
@@ -1127,6 +1133,32 @@ function isStaleForVerdict(cwd, cached) {
|
|
|
1127
1133
|
* `proceed` when loaded, `none` when inert (D-03).
|
|
1128
1134
|
*/
|
|
1129
1135
|
function computeVaultVerdict(cwd) {
|
|
1136
|
+
// Fail closed before reading a single key: `cfgGet` swallows a parse failure
|
|
1137
|
+
// into "absent", which would read a required vault as switched off (inert).
|
|
1138
|
+
if (unreadableConfigFiles(cwd).length > 0) {
|
|
1139
|
+
let binding = { bound: false, repo: null, ref: null };
|
|
1140
|
+
let cachedView = readCachedView('');
|
|
1141
|
+
try {
|
|
1142
|
+
binding = readClientBinding(cwd);
|
|
1143
|
+
cachedView = readCachedView(cwd);
|
|
1144
|
+
}
|
|
1145
|
+
catch {
|
|
1146
|
+
/* display-only; the verdict below does not depend on them */
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
enabled: true,
|
|
1150
|
+
required: true,
|
|
1151
|
+
requiredValid: true,
|
|
1152
|
+
verdict: 'degraded',
|
|
1153
|
+
reason: 'config_unreadable',
|
|
1154
|
+
action: 'stop',
|
|
1155
|
+
block: true,
|
|
1156
|
+
configuredRepo: redactCloneTarget(binding.repo),
|
|
1157
|
+
configuredRef: binding.ref,
|
|
1158
|
+
cached: cachedView,
|
|
1159
|
+
requiredRaw: null,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1130
1162
|
const enabled = cfgGet(cwd, 'vault.enabled', undefined) === true;
|
|
1131
1163
|
const requiredRawValue = cfgGet(cwd, 'vault.required', undefined);
|
|
1132
1164
|
const requiredValid = requiredRawValue === undefined || typeof requiredRawValue === 'boolean';
|
|
@@ -1206,7 +1238,9 @@ function renderVaultCard(v, point) {
|
|
|
1206
1238
|
return '';
|
|
1207
1239
|
const subject = point === 'plan' ? 'Planning' : 'Build';
|
|
1208
1240
|
const lines = [];
|
|
1209
|
-
|
|
1241
|
+
const unreadableConfig = v.reason === 'config_unreadable';
|
|
1242
|
+
const headlineNote = unreadableConfig ? ' — the project config cannot be read' : v.required ? ' — vault.required is true' : '';
|
|
1243
|
+
lines.push(`Client vault check (${subject}): ${v.verdict === 'loaded' ? 'LOADED' : 'DEGRADED'}${headlineNote}`);
|
|
1210
1244
|
lines.push(` Configured: ${v.configuredRepo || v.configuredRef ? coordText(v.configuredRepo, v.configuredRef) : 'no client vault bound'}`);
|
|
1211
1245
|
const c = v.cached;
|
|
1212
1246
|
if (c.present) {
|
|
@@ -1229,6 +1263,9 @@ function renderVaultCard(v, point) {
|
|
|
1229
1263
|
else if (v.action === 'confirm') {
|
|
1230
1264
|
lines.push(` ${subject} can continue WITHOUT the client's standards — confirm to proceed (vault.required is false).`);
|
|
1231
1265
|
}
|
|
1266
|
+
else if (v.action === 'stop' && unreadableConfig) {
|
|
1267
|
+
lines.push(` STOP — ${subject.toLowerCase()} is blocked until the project config can be read.`);
|
|
1268
|
+
}
|
|
1232
1269
|
else if (v.action === 'stop') {
|
|
1233
1270
|
lines.push(` STOP — vault.required is true: ${subject.toLowerCase()} is blocked until the client vault loads.`);
|
|
1234
1271
|
}
|
|
@@ -29,6 +29,13 @@ booleans are read.
|
|
|
29
29
|
`activeHooks` is an array of enabled hook entries for the named point. It is empty (or absent)
|
|
30
30
|
when no capability has registered an active hook at this point — treat that as a no-op.
|
|
31
31
|
|
|
32
|
+
## Refusal
|
|
33
|
+
|
|
34
|
+
`render-hooks` refuses — exit 1, empty stdout, one stderr line naming each config file and its
|
|
35
|
+
parse error — when a project config file exists but cannot be read (not JSON, or not a JSON
|
|
36
|
+
object). A missing config is not refused. Every host writes `X=$(pp_run loop render-hooks …) || exit 1`,
|
|
37
|
+
so a refusal stops the workflow: relay the stderr line and stop. Never read a refusal as "no hooks".
|
|
38
|
+
|
|
32
39
|
## Dispatch rules by `kind`
|
|
33
40
|
|
|
34
41
|
### `contribution`
|
|
@@ -146,7 +146,7 @@ For each REQ-ID, determine status using all three sources:
|
|
|
146
146
|
Skip if the Nyquist capability is inactive.
|
|
147
147
|
|
|
148
148
|
```bash
|
|
149
|
-
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw)
|
|
149
|
+
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw) || exit 1
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "validate-phase"`.
|
|
@@ -273,7 +273,7 @@ Resolve active `plan:pre` hooks:
|
|
|
273
273
|
|
|
274
274
|
```bash
|
|
275
275
|
UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
|
276
|
-
HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "${PHASE_NUM}")
|
|
276
|
+
HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "${PHASE_NUM}") || exit 1
|
|
277
277
|
```
|
|
278
278
|
|
|
279
279
|
Read the `activeHooks` array directly from `HOOKS_JSON` (in-context — do NOT invoke a shell pipeline). **Compute the active UI step hooks** = entries from `activeHooks` where `kind == "step"` and `ref.skill` is set. **If there are NO active step hooks → skip silently to 3b.** (This covers `workflow.ui_phase=false` — including configurations where only a gate-only entry is present, e.g. `ui_phase=false` + `ui_safety_gate=true` produces `activeHooks=[{kind:"gate"}]`. Autonomous never runs the plan:pre gate — it is always pipeline mode — so a gate-only active set is equivalent to no active step and is silently skipped here. This matches OLD §3a.5 behaviour.)
|
|
@@ -406,7 +406,7 @@ Auto-invoke code review and fix chain. Autonomous mode chains both review and fi
|
|
|
406
406
|
|
|
407
407
|
**Capability dispatch:**
|
|
408
408
|
```bash
|
|
409
|
-
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw --phase "${PHASE_NUM}")
|
|
409
|
+
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw --phase "${PHASE_NUM}") || exit 1
|
|
410
410
|
```
|
|
411
411
|
|
|
412
412
|
Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
|
|
@@ -481,7 +481,7 @@ Resolve the active post-verification hooks and the UI-SPEC gate:
|
|
|
481
481
|
|
|
482
482
|
```bash
|
|
483
483
|
UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
|
484
|
-
HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw --phase "${PHASE_NUM}")
|
|
484
|
+
HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw --phase "${PHASE_NUM}") || exit 1
|
|
485
485
|
```
|
|
486
486
|
|
|
487
487
|
Read the `activeHooks` array directly from the `HOOKS_JSON` value already in context (do not invoke a shell `jq` pipeline — parse as the JSON object it is). **If `activeHooks` is empty or absent:** skip silently to the iterate step.
|
|
@@ -77,7 +77,7 @@ FIX_REPORT_PATH="${PHASE_DIR}/${PADDED_PHASE}-REVIEW-FIX.md"
|
|
|
77
77
|
Check if code review is active via the capability registry:
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
|
-
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw)
|
|
80
|
+
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw) || exit 1
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
|
|
@@ -78,7 +78,7 @@ fi
|
|
|
78
78
|
Check if code review is active via the capability registry:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw)
|
|
81
|
+
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw) || exit 1
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
|
|
@@ -299,7 +299,7 @@ Read `@~/.claude/pp-core/references/scout-codebase.md` — it contains the phase
|
|
|
299
299
|
|
|
300
300
|
<step name="dispatch_discuss_pre_hooks">
|
|
301
301
|
```bash
|
|
302
|
-
DISCUSS_PRE_HOOKS_JSON=$(pp_run loop render-hooks discuss:pre --raw)
|
|
302
|
+
DISCUSS_PRE_HOOKS_JSON=$(pp_run loop render-hooks discuss:pre --raw) || exit 1
|
|
303
303
|
```
|
|
304
304
|
Apply each entry in `activeHooks` per @~/.claude/pp-core/references/loop-hook-dispatch.md. Empty list → continue to `analyze_phase`.
|
|
305
305
|
</step>
|
|
@@ -418,7 +418,7 @@ Write the file.
|
|
|
418
418
|
|
|
419
419
|
<step name="dispatch_discuss_post_hooks">
|
|
420
420
|
```bash
|
|
421
|
-
DISCUSS_POST_HOOKS_JSON=$(pp_run loop render-hooks discuss:post --raw)
|
|
421
|
+
DISCUSS_POST_HOOKS_JSON=$(pp_run loop render-hooks discuss:post --raw) || exit 1
|
|
422
422
|
```
|
|
423
423
|
Apply each entry in `activeHooks` per @~/.claude/pp-core/references/loop-hook-dispatch.md. Empty list → continue to `propose_tier`.
|
|
424
424
|
</step>
|
|
@@ -70,7 +70,7 @@ Parse `$ARGUMENTS` before loading any context:
|
|
|
70
70
|
- First positional token → `PHASE_ARG`
|
|
71
71
|
- Optional `--wave N` → `WAVE_FILTER`
|
|
72
72
|
- Optional `--gaps-only` → `GAPS_ONLY=true` (only `gap_closure: true` plans)
|
|
73
|
-
- Optional `--tier light|standard|heavy` → `TIER_FLAG="--tier
|
|
73
|
+
- Optional `--tier light|standard|heavy` → `TIER_FLAG="--tier=<value>"` (one word, so zsh passes it as one argument; the phase's ceremony tier for this run; passed to init, dispatch plan and render-hooks)
|
|
74
74
|
- Optional `--cross-ai` → `CROSS_AI_FORCE=true` (force all plans through cross-AI execution)
|
|
75
75
|
- Optional `--no-cross-ai` → `CROSS_AI_DISABLED=true` (disable cross-AI for this run, overrides config and frontmatter)
|
|
76
76
|
- Optional `--unattended` → `UNATTENDED=true` — set by `/pp:autonomous`; prompt sites marked `U-En` apply the rules in `pp-core/references/autonomous-best-effort.md` (Read it at the first such site)
|
|
@@ -183,8 +183,8 @@ Resolve `MVP_MODE` once via the centralized `phase.mvp-mode` query verb (precede
|
|
|
183
183
|
MVP_FLAG_ARG=""
|
|
184
184
|
if [[ "$ARGUMENTS" =~ (^|[[:space:]])--mvp([[:space:]]|$) ]]; then MVP_FLAG_ARG="--cli-flag"; fi
|
|
185
185
|
MVP_MODE=$(pp_run query phase.mvp-mode "${PHASE_NUMBER}" $MVP_FLAG_ARG --pick active)
|
|
186
|
-
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw --phase "${PHASE_NUMBER}" ${TIER_FLAG})
|
|
187
|
-
TDD_MODE=$(pp_run loop render-hooks execute:post --active-cap tdd)
|
|
186
|
+
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw --phase "${PHASE_NUMBER}" ${TIER_FLAG}) || exit 1
|
|
187
|
+
TDD_MODE=$(pp_run loop render-hooks execute:post --active-cap tdd) || exit 1
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
<step name="safe_resume_gate">
|
|
@@ -347,7 +347,7 @@ Read and execute `pp-core/workflows/execute-phase/steps/client-context-gate.md`
|
|
|
347
347
|
</step>
|
|
348
348
|
|
|
349
349
|
<step name="execute_pre_vault">
|
|
350
|
-
Before any executor: `EXECUTE_PRE_HOOKS_JSON=$(pp_run loop render-hooks execute:pre --raw)`. For an active `capId == "vault"` step, Read and execute `pp-core/workflows/execute-phase/steps/vault-fetch.md`; then, for an active `vault.status` gate, set `VAULT_GATE_POINT=execute` and Read and execute `pp-core/workflows/execute-phase/steps/vault-load-gate.md`. None active → skip.
|
|
350
|
+
Before any executor: `EXECUTE_PRE_HOOKS_JSON=$(pp_run loop render-hooks execute:pre --raw) || exit 1`. For an active `capId == "vault"` step, Read and execute `pp-core/workflows/execute-phase/steps/vault-fetch.md`; then, for an active `vault.status` gate, set `VAULT_GATE_POINT=execute` and Read and execute `pp-core/workflows/execute-phase/steps/vault-load-gate.md`. None active → skip.
|
|
351
351
|
</step>
|
|
352
352
|
|
|
353
353
|
<step name="discover_and_group_plans">
|
|
@@ -851,7 +851,7 @@ increases monotonically across waves. `{status}` is `complete` (success),
|
|
|
851
851
|
After worktree merge, post-merge tests, and tracking updates, dispatch capability hooks registered at `execute:wave:post`. The primary hook is the `ui.safety-gate` gate from the UI capability — it verifies that any frontend files changed in this wave conform to the UI-SPEC contract.
|
|
852
852
|
|
|
853
853
|
```bash
|
|
854
|
-
WAVE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:wave:post --raw)
|
|
854
|
+
WAVE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:wave:post --raw) || exit 1
|
|
855
855
|
```
|
|
856
856
|
|
|
857
857
|
Read the `activeHooks` array from `WAVE_POST_HOOKS_JSON` in-context (do NOT pipe through a shell parser).
|
|
@@ -1016,7 +1016,7 @@ After all waves:
|
|
|
1016
1016
|
|
|
1017
1017
|
**Security gate check:**
|
|
1018
1018
|
```bash
|
|
1019
|
-
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw --phase "${PHASE_NUMBER}" ${TIER_FLAG})
|
|
1019
|
+
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw --phase "${PHASE_NUMBER}" ${TIER_FLAG}) || exit 1
|
|
1020
1020
|
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
|
|
1021
1021
|
```
|
|
1022
1022
|
|
|
@@ -1074,7 +1074,7 @@ Selected wave finished successfully. This phase still has incomplete plans, so p
|
|
|
1074
1074
|
|
|
1075
1075
|
**Capability gate:**
|
|
1076
1076
|
```bash
|
|
1077
|
-
EXECUTE_POST_HOOKS_JSON=${EXECUTE_POST_HOOKS_JSON:-$(pp_run loop render-hooks execute:post --raw --phase "${PHASE_NUMBER}" ${TIER_FLAG})}
|
|
1077
|
+
EXECUTE_POST_HOOKS_JSON=${EXECUTE_POST_HOOKS_JSON:-$(pp_run loop render-hooks execute:post --raw --phase "${PHASE_NUMBER}" ${TIER_FLAG})} || exit 1
|
|
1078
1078
|
```
|
|
1079
1079
|
|
|
1080
1080
|
Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
|
|
@@ -72,7 +72,7 @@ Load all context in one call (paths only to minimize orchestrator context):
|
|
|
72
72
|
```bash
|
|
73
73
|
_PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$_v"; done; _c=$(git rev-parse --git-common-dir 2>/dev/null); if [ "${_c##*/}" = .git ] && cd "$_c/.." 2>/dev/null; then pwd -P; else git rev-parse --show-toplevel 2>/dev/null || pwd; fi ); }; pp_run() { case "$PP_TOOLS" in *.cjs) node "$PP_TOOLS" "$@";; *) "$PP_TOOLS" "$@";; esac; }; _PP_RUNTIME_ROOT="${RUNTIME_DIR:-$(_pp_root)}"; PP_TOOLS="${_PP_RUNTIME_ROOT}/pp-core/bin/${_PP_SHIM_NAME}"; if [ -f "$PP_TOOLS" ]; then :; elif [ -f "${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${_PP_RUNTIME_ROOT}/.codex/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="$HOME/.claude/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${HERMES_HOME:-$HOME/.hermes}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEX_HOME:-$HOME/.codex}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/pp-core/bin/${_PP_SHIM_NAME}"; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}" ]; then PP_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/pp-core/bin/${_PP_SHIM_NAME}"; elif command -v pp-tools >/dev/null 2>&1; then PP_TOOLS="$(command -v pp-tools)"; else echo "ERROR: pp-tools.cjs not found at $PP_TOOLS and pp-tools is not on PATH. Run: npx -y @pareto4/pp-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${PP_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${PP_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
|
74
74
|
GRAN_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--granularity[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then GRAN_PARAM="--granularity ${BASH_REMATCH[2]}"; fi
|
|
75
|
-
TIER_FLAG
|
|
75
|
+
TIER_FLAG=$(printf '%s\n' "$ARGUMENTS" | grep -oE -- '--tier[[:space:]=]+(light|standard|heavy)' | head -1 | sed -E 's/^--tier[[:space:]=]+/--tier=/')
|
|
76
76
|
INIT=$(pp_run query init.plan-phase "$PHASE" $GRAN_PARAM $TIER_FLAG)
|
|
77
77
|
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
|
78
78
|
AGENT_SKILLS_RESEARCHER=$(pp_run query agent-skills pp-phase-researcher)
|
|
@@ -432,7 +432,7 @@ If `AI_SPEC_FILE` is non-empty, pass `AI_SPEC_PATH` and `FRAMEWORK_LINE` to the
|
|
|
432
432
|
|
|
433
433
|
### 5.00. plan:pre vault load — ALWAYS, before the skip rule below
|
|
434
434
|
|
|
435
|
-
Every path runs this before any agent: `PLAN_PRE_HOOKS_JSON=${PLAN_PRE_HOOKS_JSON:-$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG)}`. Then Read and execute `pp-core/workflows/execute-phase/steps/vault-fetch.md` (active only for a `capId == "vault"` step hook; writes `.planning/vault/SUMMARY.md`; `onError:skip`). Then, if an active plan:pre gate has `check.query == "vault.status"`, set `VAULT_GATE_POINT=plan` and Read and execute `pp-core/workflows/execute-phase/steps/vault-load-gate.md` — it relays the CLI's vault card and acts on its `action` only (`stop` halts planning in every mode; `confirm` asks, or logs under --auto/--chain). Skip silently when neither hook is active.
|
|
435
|
+
Every path runs this before any agent: `PLAN_PRE_HOOKS_JSON=${PLAN_PRE_HOOKS_JSON:-$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG)} || exit 1`. Then Read and execute `pp-core/workflows/execute-phase/steps/vault-fetch.md` (active only for a `capId == "vault"` step hook; writes `.planning/vault/SUMMARY.md`; `onError:skip`). Then, if an active plan:pre gate has `check.query == "vault.status"`, set `VAULT_GATE_POINT=plan` and Read and execute `pp-core/workflows/execute-phase/steps/vault-load-gate.md` — it relays the CLI's vault card and acts on its `action` only (`stop` halts planning in every mode; `confirm` asks, or logs under --auto/--chain). Skip silently when neither hook is active.
|
|
436
436
|
|
|
437
437
|
**Skip if:** `--gaps` flag or `--skip-research` flag or `--reviews` flag.
|
|
438
438
|
|
|
@@ -501,7 +501,7 @@ fi
|
|
|
501
501
|
```bash
|
|
502
502
|
PHASE_DESC=$(pp_run query roadmap.get-phase "${PHASE}" --pick section)
|
|
503
503
|
if [ -z "${PLAN_PRE_HOOKS_JSON:-}" ]; then
|
|
504
|
-
PLAN_PRE_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG)
|
|
504
|
+
PLAN_PRE_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG) || exit 1
|
|
505
505
|
fi
|
|
506
506
|
```
|
|
507
507
|
|
|
@@ -584,7 +584,7 @@ test -f "${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md" && echo "VALIDATION_CREATED
|
|
|
584
584
|
> Capability-driven dispatch. Resolves active `plan:pre` hooks via the capability registry; the security hook's `when` condition is evaluated by the registry.
|
|
585
585
|
|
|
586
586
|
```bash
|
|
587
|
-
PLAN_PRE_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG)
|
|
587
|
+
PLAN_PRE_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG) || exit 1
|
|
588
588
|
```
|
|
589
589
|
|
|
590
590
|
Resolve active contribution hooks from `PLAN_PRE_HOOKS_JSON` where `kind == "contribution"` and `capId == "security"`.
|
|
@@ -614,7 +614,7 @@ Continue to step 5.6. Security config is passed to the planner in step 8.
|
|
|
614
614
|
> **Config semantics (cutover fix):** `workflow.ui_phase` gates UI-SPEC *generation* (step); `workflow.ui_safety_gate` gates the *planning block* (gate). Both-on = identical to OLD §5.6. Intended change: `{ui_phase:true, ui_safety_gate:false}` now auto-generates in pipelines but does NOT block manual planning (each key controls exactly what its description says).
|
|
615
615
|
|
|
616
616
|
```bash
|
|
617
|
-
PLAN_PRE_HOOKS_JSON=${PLAN_PRE_HOOKS_JSON:-$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG)}
|
|
617
|
+
PLAN_PRE_HOOKS_JSON=${PLAN_PRE_HOOKS_JSON:-$(pp_run loop render-hooks plan:pre --raw --phase "$PHASE" $TIER_FLAG)} || exit 1
|
|
618
618
|
HOOKS_JSON="$PLAN_PRE_HOOKS_JSON"
|
|
619
619
|
```
|
|
620
620
|
|
|
@@ -1475,7 +1475,7 @@ Proactive, non-blocking coverage report gated on `workflow.post_planning_gaps`
|
|
|
1475
1475
|
`<decisions>` and cross-references each REQ-ID / D-ID against `${PHASE_DIR}/*-PLAN.md`.
|
|
1476
1476
|
|
|
1477
1477
|
```bash
|
|
1478
|
-
PLAN_POST_HOOKS_JSON=$(pp_run loop render-hooks plan:post --raw --phase "$PHASE" $TIER_FLAG)
|
|
1478
|
+
PLAN_POST_HOOKS_JSON=$(pp_run loop render-hooks plan:post --raw --phase "$PHASE" $TIER_FLAG) || exit 1
|
|
1479
1479
|
PHASE_REQ_IDS=$(pp_run query init.plan-phase "$PHASE" --pick phase_req_ids 2>/dev/null || echo TBD)
|
|
1480
1480
|
```
|
|
1481
1481
|
|
|
@@ -808,7 +808,7 @@ Skip this step entirely if `$FULL_MODE` is false.
|
|
|
808
808
|
|
|
809
809
|
**Capability gate:**
|
|
810
810
|
```bash
|
|
811
|
-
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw)
|
|
811
|
+
EXECUTE_POST_HOOKS_JSON=$(pp_run loop render-hooks execute:post --raw) || exit 1
|
|
812
812
|
```
|
|
813
813
|
|
|
814
814
|
Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
|
|
@@ -26,7 +26,7 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
AUDITOR_MODEL=$(pp_run query resolve-model pp-security-auditor --raw)
|
|
29
|
-
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw)
|
|
29
|
+
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw) || exit 1
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
|
|
@@ -90,7 +90,7 @@ Verify the work is ready to ship:
|
|
|
90
90
|
Resolve active `ship:pre` gate hooks from the capability registry — the registry evaluates each hook's `when` condition, so do **not** read `workflow.security_enforcement` directly:
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
SHIP_PRE_HOOKS_JSON=$(pp_run loop render-hooks ship:pre --raw --phase "${phase_number}")
|
|
93
|
+
SHIP_PRE_HOOKS_JSON=$(pp_run loop render-hooks ship:pre --raw --phase "${phase_number}") || exit 1
|
|
94
94
|
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
|
|
95
95
|
```
|
|
96
96
|
|
|
@@ -448,7 +448,7 @@ pp_run query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #$
|
|
|
448
448
|
> Capability-driven dispatch. Resolves active `ship:post` hooks via the capability registry; each hook's `when` is evaluated by the registry — no inline `config-get`. All `ship:post` hooks are post-ship and additive (`onError: skip`); a failure here never affects the already-created PR.
|
|
449
449
|
|
|
450
450
|
```bash
|
|
451
|
-
SHIP_POST_HOOKS_JSON=$(pp_run loop render-hooks ship:post --raw)
|
|
451
|
+
SHIP_POST_HOOKS_JSON=$(pp_run loop render-hooks ship:post --raw) || exit 1
|
|
452
452
|
```
|
|
453
453
|
|
|
454
454
|
Read the `activeHooks` array directly from `SHIP_POST_HOOKS_JSON` in-context (do NOT pipe it through a shell parser).
|
|
@@ -30,7 +30,7 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
|
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
AUDITOR_MODEL=$(pp_run query resolve-model pp-nyquist-auditor --raw)
|
|
33
|
-
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw)
|
|
33
|
+
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw) || exit 1
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "validate-phase"`.
|
|
@@ -41,7 +41,7 @@ _PP_SHIM_NAME="pp-tools.cjs"; _pp_root() { ( for _v in $(env | sed -n 's/^\(GIT_
|
|
|
41
41
|
GSD_WS=""
|
|
42
42
|
echo "$ARGUMENTS" | grep -qE -- '--ws[[:space:]]+[^[:space:]]+' && GSD_WS=$(echo "$ARGUMENTS" | grep -oE -- '--ws[[:space:]]+[^[:space:]]+')
|
|
43
43
|
PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[^[:space:]]+//g' | xargs)
|
|
44
|
-
TIER_FLAG=$(
|
|
44
|
+
TIER_FLAG=$(printf '%s\n' "$ARGUMENTS" | grep -oE -- '--tier[[:space:]=]+(light|standard|heavy)' | head -1 | sed -E 's/^--tier[[:space:]=]+/--tier=/')
|
|
45
45
|
PHASE_ARG=$(echo "$PHASE_ARG" | sed -E 's/--tier[[:space:]]+[a-z]+//g' | xargs)
|
|
46
46
|
|
|
47
47
|
INIT=$(pp_run query init.verify-work "${PHASE_ARG}" ${GSD_WS} ${TIER_FLAG})
|
|
@@ -113,7 +113,7 @@ Continue to `create_uat_file`.
|
|
|
113
113
|
Before UAT, check UI capability activation and whether Playwright/Puppeteer MCP tools are available.
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
PLAN_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "${phase_number}" ${TIER_FLAG})
|
|
116
|
+
PLAN_HOOKS_JSON=$(pp_run loop render-hooks plan:pre --raw --phase "${phase_number}" ${TIER_FLAG}) || exit 1
|
|
117
117
|
UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
|
118
118
|
```
|
|
119
119
|
|
|
@@ -460,7 +460,7 @@ Present summary:
|
|
|
460
460
|
**If issues == 0:**
|
|
461
461
|
|
|
462
462
|
```bash
|
|
463
|
-
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw --phase "${phase_number}" ${GSD_WS} ${TIER_FLAG})
|
|
463
|
+
VERIFY_POST_HOOKS_JSON=$(pp_run loop render-hooks verify:post --raw --phase "${phase_number}" ${GSD_WS} ${TIER_FLAG}) || exit 1
|
|
464
464
|
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
|
|
465
465
|
```
|
|
466
466
|
|