@pareto4/pp-core 1.20.0 → 1.21.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.
Files changed (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-nyquist-auditor.md +19 -14
  3. package/agents/pp-plan-checker.md +59 -36
  4. package/agents/pp-planner.md +34 -53
  5. package/agents/pp-verifier.md +3 -52
  6. package/commands/pp-autonomous.md +2 -1
  7. package/commands/pp-plan-phase.md +2 -1
  8. package/commands/pp-plan-review-convergence.md +2 -1
  9. package/gemini-extension.json +1 -1
  10. package/package.json +1 -1
  11. package/pp-core/bin/lib/capability-activation.cjs +77 -0
  12. package/pp-core/bin/lib/capability-registry.cjs +14 -14
  13. package/pp-core/bin/lib/capability-state.cjs +8 -18
  14. package/pp-core/bin/lib/ceremony.cjs +1342 -0
  15. package/pp-core/bin/lib/check-command-router.cjs +62 -1
  16. package/pp-core/bin/lib/commands.cjs +29 -3
  17. package/pp-core/bin/lib/config.cjs +5 -0
  18. package/pp-core/bin/lib/init.cjs +50 -1
  19. package/pp-core/bin/lib/io.cjs +2 -0
  20. package/pp-core/bin/lib/loop-resolver.cjs +26 -12
  21. package/pp-core/bin/lib/phase.cjs +50 -8
  22. package/pp-core/bin/lib/plan-sizing.cjs +346 -0
  23. package/pp-core/bin/lib/state.cjs +1 -0
  24. package/pp-core/bin/lib/test-substance.cjs +3190 -0
  25. package/pp-core/bin/lib/timeline.cjs +605 -0
  26. package/pp-core/bin/pp-tools.cjs +71 -4
  27. package/pp-core/bin/shared/ceremony-gates.json +17 -0
  28. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  29. package/pp-core/references/ceremony.md +81 -0
  30. package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
  31. package/pp-core/references/phase-timeline.md +107 -0
  32. package/pp-core/references/planner-antipatterns.md +32 -0
  33. package/pp-core/references/planner-guidance.md +17 -14
  34. package/pp-core/references/planner-revision.md +1 -1
  35. package/pp-core/references/planning-config.md +3 -2
  36. package/pp-core/references/test-substance.md +324 -0
  37. package/pp-core/references/verifier-stub-patterns.md +51 -0
  38. package/pp-core/templates/VALIDATION.md +1 -0
  39. package/pp-core/templates/phase-prompt.md +19 -7
  40. package/pp-core/templates/roadmap.md +1 -1
  41. package/pp-core/templates/verification-report.md +1 -1
  42. package/pp-core/workflows/audit-milestone.md +4 -1
  43. package/pp-core/workflows/autonomous.md +11 -8
  44. package/pp-core/workflows/code-review.md +21 -9
  45. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
  46. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
  47. package/pp-core/workflows/execute-phase.md +15 -4
  48. package/pp-core/workflows/init/steps/client-context-capture.md +1 -1
  49. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
  50. package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
  51. package/pp-core/workflows/plan-phase.md +47 -114
  52. package/pp-core/workflows/plan-review-convergence.md +6 -3
  53. package/pp-core/workflows/profile-user.md +2 -2
  54. package/pp-core/workflows/quick.md +1 -1
  55. package/pp-core/workflows/ship.md +3 -3
  56. package/pp-core/workflows/spec-phase.md +2 -2
  57. package/pp-core/workflows/stats.md +19 -4
  58. package/pp-core/workflows/validate-phase.md +19 -4
  59. package/pp-core/workflows/verify-phase.md +1 -1
  60. package/pp-core/workflows/verify-work.md +1 -1
  61. package/scripts/gen-capability-registry.cjs +43 -45
  62. package/scripts/run-tests.cjs +54 -30
@@ -34,6 +34,12 @@ const SCHEMA_VERSION = '1';
34
34
  // registry generator and the loop-host-contract generator share one source of truth.
35
35
  const { LOOP_HOST_CONTRACT } = require('../pp-core/bin/lib/loop-host-contract.cjs');
36
36
 
37
+ // Ceremony gate vocabulary (Phase 37, D-14) — a hook `when` conjunct `ceremony.<gate>`
38
+ // must name a gate declared here.
39
+ const CEREMONY_GATE_NAMES = new Set(
40
+ require('../pp-core/bin/shared/ceremony-gates.json').gates.map((g) => g.key),
41
+ );
42
+
37
43
  // Wired-points helper — tells us which points actually have render-hooks call sites.
38
44
  const { getWiredLoopPoints } = require('./gen-loop-host-contract.cjs');
39
45
 
@@ -1241,6 +1247,38 @@ function validateGate(gate, prefix) {
1241
1247
 
1242
1248
  // ─── Contract validation ──────────────────────────────────────────────────────
1243
1249
 
1250
+ /**
1251
+ * Validate a hook `when` (Phase 37, D-14). A `when` is one or more conjuncts joined
1252
+ * by `&&`; each is a config key the capability declares, or `ceremony.<gate>` with
1253
+ * the gate named in pp-core/bin/shared/ceremony-gates.json. A non-string `when` is
1254
+ * already reported by the schema checks and is skipped here. Pushes onto `errors`.
1255
+ * `where` is the message prefix, e.g. `capability "x" step.when`, so the message of
1256
+ * a single undeclared key stays `<where> "<key>" is not defined in capability config keys`.
1257
+ */
1258
+ function validateWhen(when, cap, where, errors) {
1259
+ if (typeof when !== 'string') return;
1260
+ for (const raw of when.split('&&')) {
1261
+ const conjunct = raw.trim();
1262
+ if (conjunct === '') {
1263
+ errors.push(where + ' has an empty conjunct');
1264
+ continue;
1265
+ }
1266
+ if (conjunct.startsWith('ceremony.')) {
1267
+ if (!CEREMONY_GATE_NAMES.has(conjunct.slice('ceremony.'.length))) {
1268
+ errors.push(where + ' "' + conjunct + '" is not a ceremony gate');
1269
+ }
1270
+ continue;
1271
+ }
1272
+ if (
1273
+ typeof cap.config === 'object' &&
1274
+ cap.config !== null &&
1275
+ !Object.prototype.hasOwnProperty.call(cap.config, conjunct)
1276
+ ) {
1277
+ errors.push(where + ' "' + conjunct + '" is not defined in capability config keys');
1278
+ }
1279
+ }
1280
+ }
1281
+
1244
1282
  /**
1245
1283
  * Validate per-capability contract constraints against the Loop Host Contract.
1246
1284
  * This covers:
@@ -1281,51 +1319,10 @@ function validateAgainstContract(cap, capId, knownAgentIds) {
1281
1319
  }
1282
1320
  }
1283
1321
 
1284
- // when references a plausibly-valid config key (string — we require it's in cap.config)
1285
- for (const step of cap.steps) {
1286
- if (step.when !== undefined) {
1287
- if (typeof step.when !== 'string') continue; // already reported above
1288
- if (
1289
- typeof cap.config === 'object' &&
1290
- cap.config !== null &&
1291
- !Object.prototype.hasOwnProperty.call(cap.config, step.when)
1292
- ) {
1293
- errors.push(
1294
- prefix + ' step.when "' + step.when + '" is not defined in capability config keys',
1295
- );
1296
- }
1297
- }
1298
- }
1299
-
1300
- for (const contrib of cap.contributions) {
1301
- if (contrib.when !== undefined) {
1302
- if (typeof contrib.when !== 'string') continue;
1303
- if (
1304
- typeof cap.config === 'object' &&
1305
- cap.config !== null &&
1306
- !Object.prototype.hasOwnProperty.call(cap.config, contrib.when)
1307
- ) {
1308
- errors.push(
1309
- prefix + ' contribution.when "' + contrib.when + '" is not defined in capability config keys',
1310
- );
1311
- }
1312
- }
1313
- }
1314
-
1315
- for (const gate of cap.gates) {
1316
- if (gate.when !== undefined) {
1317
- if (typeof gate.when !== 'string') continue;
1318
- if (
1319
- typeof cap.config === 'object' &&
1320
- cap.config !== null &&
1321
- !Object.prototype.hasOwnProperty.call(cap.config, gate.when)
1322
- ) {
1323
- errors.push(
1324
- prefix + ' gate.when "' + gate.when + '" is not defined in capability config keys',
1325
- );
1326
- }
1327
- }
1328
- }
1322
+ // when: one or more `&&` conjuncts, each a declared config key or `ceremony.<gate>`
1323
+ for (const step of cap.steps) validateWhen(step.when, cap, prefix + ' step.when', errors);
1324
+ for (const contrib of cap.contributions) validateWhen(contrib.when, cap, prefix + ' contribution.when', errors);
1325
+ for (const gate of cap.gates) validateWhen(gate.when, cap, prefix + ' gate.when', errors);
1329
1326
 
1330
1327
  return errors;
1331
1328
  }
@@ -2759,6 +2756,7 @@ function main() {
2759
2756
  module.exports = {
2760
2757
  validateCapability,
2761
2758
  validateAgainstContract,
2759
+ validateWhen,
2762
2760
  validateConsumesGlobal,
2763
2761
  validateCrossCapability,
2764
2762
  validateSealField,
@@ -31,7 +31,7 @@
31
31
 
32
32
  const { readdirSync } = require('fs');
33
33
  const { join, basename } = require('path');
34
- const { execFileSync } = require('child_process');
34
+ const { execFileSync, spawnSync } = require('child_process');
35
35
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
36
36
 
37
37
  const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
@@ -548,42 +548,66 @@ function main() {
548
548
  if (chunks.length > 1) {
549
549
  console.error(`run-tests: chunk ${i + 1}/${chunks.length} — ${chunks[i].length} files`);
550
550
  }
551
- try {
552
- execFileSync(
553
- process.execPath,
554
- ['--test', ...(forceExit ? ['--test-force-exit'] : []), concurrency, ...chunks[i]],
555
- {
556
- stdio: 'inherit',
557
- env: { ...process.env },
558
- timeout: chunkTimeoutMs,
559
- },
551
+ const chunkFiles = chunks[i].map(f => f.split(/[\\/]/).pop()).join(' ');
552
+ // stdout is captured, not inherited, so the chunk's node:test summary can be
553
+ // checked; it is written through unchanged once the chunk ends. stderr stays
554
+ // live. A chunk can exit 0 with its output cut off and no summary — on CI
555
+ // (2026-10-28) two of five chunks did, and ~1,000 tests never reported while
556
+ // the job stayed green — so a missing summary fails the run.
557
+ const result = spawnSync(
558
+ process.execPath,
559
+ ['--test', ...(forceExit ? ['--test-force-exit'] : []), concurrency, ...chunks[i]],
560
+ {
561
+ stdio: ['inherit', 'pipe', 'inherit'],
562
+ env: { ...process.env },
563
+ timeout: chunkTimeoutMs,
564
+ maxBuffer: 1024 * 1024 * 1024,
565
+ },
566
+ );
567
+ if (result.stdout && result.stdout.length > 0) process.stdout.write(result.stdout);
568
+ // When the per-chunk timeout fires, spawnSync kills the child and reports
569
+ // error.code === 'ETIMEDOUT' (POSIX) and/or a kill signal (platform-dependent).
570
+ // Check both so detection holds on Windows and POSIX.
571
+ const timedOut = (result.error && result.error.code === 'ETIMEDOUT') ||
572
+ (result.signal !== null && result.status === null && !result.error);
573
+ let code = result.status === null ? 1 : result.status;
574
+ if (result.error && result.error.code === 'ETIMEDOUT') code = 1;
575
+ if (timedOut) {
576
+ console.error(
577
+ `run-tests: chunk ${i + 1}/${chunks.length} exceeded the per-chunk timeout ` +
578
+ `of ${chunkTimeoutMs}ms and was killed — a test in this chunk is likely leaking ` +
579
+ `an open handle (un-terminated Worker, un-killed child process, or ref'd timer) ` +
580
+ `so node --test never exits. Files: ${chunkFiles}`,
560
581
  );
561
- } catch (err) {
562
- // When the per-chunk timeout fires, execFileSync kills the child and
563
- // surfaces it as err.code === 'ETIMEDOUT' (POSIX) and/or err.killed === true
564
- // (platform-dependent). Check both so detection holds on Windows and POSIX.
565
- const timedOut = err.killed === true || err.code === 'ETIMEDOUT';
566
- if (timedOut) {
567
- console.error(
568
- `run-tests: chunk ${i + 1}/${chunks.length} exceeded the per-chunk timeout ` +
569
- `of ${chunkTimeoutMs}ms and was killed — a test in this chunk is likely leaking ` +
570
- `an open handle (un-terminated Worker, un-killed child process, or ref'd timer) ` +
571
- `so node --test never exits. Files: ${chunks[i]
572
- .map(f => f.split(/[\\/]/).pop())
573
- .join(' ')}`,
574
- );
575
- }
576
- const code = err.status || 1;
577
- // Run every chunk so the operator sees all failures in one pass; report
578
- // the first non-zero exit at the end.
579
- if (firstFailureExit === 0) firstFailureExit = code;
582
+ } else if (result.error) {
583
+ console.error(`run-tests: chunk ${i + 1}/${chunks.length} could not run: ${result.error.message}`);
584
+ code = 1;
585
+ } else if (code === 0 && !hasTestSummary(result.stdout)) {
586
+ console.error(
587
+ `run-tests: chunk ${i + 1}/${chunks.length} exited without a test summary ` +
588
+ `(no "ℹ tests N" / "ℹ fail N" lines) — its output was cut off, so some of its ` +
589
+ `tests never reported. Files: ${chunkFiles}`,
590
+ );
591
+ code = 1;
580
592
  }
593
+ // Run every chunk so the operator sees all failures in one pass; report
594
+ // the first non-zero exit at the end.
595
+ if (code !== 0 && firstFailureExit === 0) firstFailureExit = code;
581
596
  }
582
597
  if (firstFailureExit !== 0) return firstFailureExit;
583
598
  }
584
599
 
600
+ /**
601
+ * Whether a chunk's stdout carries node:test's closing summary: both the
602
+ * `tests N` and `fail N` lines, from the spec reporter (`ℹ`) or TAP (`#`).
603
+ */
604
+ function hasTestSummary(stdout) {
605
+ const text = stdout ? stdout.toString('utf8') : '';
606
+ return /^(?:ℹ|#) tests \d+\s*$/m.test(text) && /^(?:ℹ|#) fail \d+\s*$/m.test(text);
607
+ }
608
+
585
609
  if (require.main === module) {
586
610
  runMain(main);
587
611
  }
588
612
 
589
- module.exports = { suiteOf, ensureBuiltArtifacts, parseShardArg, selectShard };
613
+ module.exports = { suiteOf, ensureBuiltArtifacts, parseShardArg, selectShard, hasTestSummary };