@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/pp-nyquist-auditor.md +19 -14
- package/agents/pp-plan-checker.md +59 -36
- package/agents/pp-planner.md +34 -53
- package/agents/pp-verifier.md +3 -52
- package/commands/pp-autonomous.md +2 -1
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-plan-review-convergence.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/capability-activation.cjs +77 -0
- package/pp-core/bin/lib/capability-registry.cjs +14 -14
- package/pp-core/bin/lib/capability-state.cjs +8 -18
- package/pp-core/bin/lib/ceremony.cjs +1342 -0
- package/pp-core/bin/lib/check-command-router.cjs +62 -1
- package/pp-core/bin/lib/commands.cjs +29 -3
- package/pp-core/bin/lib/config.cjs +5 -0
- package/pp-core/bin/lib/init.cjs +50 -1
- package/pp-core/bin/lib/io.cjs +2 -0
- package/pp-core/bin/lib/loop-resolver.cjs +26 -12
- package/pp-core/bin/lib/phase.cjs +50 -8
- package/pp-core/bin/lib/plan-sizing.cjs +346 -0
- package/pp-core/bin/lib/state.cjs +1 -0
- package/pp-core/bin/lib/test-substance.cjs +3190 -0
- package/pp-core/bin/lib/timeline.cjs +605 -0
- package/pp-core/bin/pp-tools.cjs +71 -4
- package/pp-core/bin/shared/ceremony-gates.json +17 -0
- package/pp-core/bin/shared/config-schema.manifest.json +1 -0
- package/pp-core/references/ceremony.md +81 -0
- package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
- package/pp-core/references/phase-timeline.md +107 -0
- package/pp-core/references/planner-antipatterns.md +32 -0
- package/pp-core/references/planner-guidance.md +17 -14
- package/pp-core/references/planner-revision.md +1 -1
- package/pp-core/references/planning-config.md +3 -2
- package/pp-core/references/test-substance.md +324 -0
- package/pp-core/references/verifier-stub-patterns.md +51 -0
- package/pp-core/templates/VALIDATION.md +1 -0
- package/pp-core/templates/phase-prompt.md +19 -7
- package/pp-core/templates/roadmap.md +1 -1
- package/pp-core/templates/verification-report.md +1 -1
- package/pp-core/workflows/audit-milestone.md +4 -1
- package/pp-core/workflows/autonomous.md +11 -8
- package/pp-core/workflows/code-review.md +21 -9
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
- package/pp-core/workflows/execute-phase.md +15 -4
- package/pp-core/workflows/init/steps/client-context-capture.md +1 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
- package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
- package/pp-core/workflows/plan-phase.md +47 -114
- package/pp-core/workflows/plan-review-convergence.md +6 -3
- package/pp-core/workflows/profile-user.md +2 -2
- package/pp-core/workflows/quick.md +1 -1
- package/pp-core/workflows/ship.md +3 -3
- package/pp-core/workflows/spec-phase.md +2 -2
- package/pp-core/workflows/stats.md +19 -4
- package/pp-core/workflows/validate-phase.md +19 -4
- package/pp-core/workflows/verify-phase.md +1 -1
- package/pp-core/workflows/verify-work.md +1 -1
- package/scripts/gen-capability-registry.cjs +43 -45
- package/scripts/run-tests.cjs +54 -30
|
@@ -23,6 +23,8 @@ const phaseScopeMod = require("./phase-scope.cjs");
|
|
|
23
23
|
const { resolvePhaseForRead } = phaseScopeMod;
|
|
24
24
|
const decisions_cjs_1 = require("./decisions.cjs");
|
|
25
25
|
const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
|
|
26
|
+
const plan_sizing_cjs_1 = require("./plan-sizing.cjs");
|
|
27
|
+
const test_substance_cjs_1 = require("./test-substance.cjs");
|
|
26
28
|
const semver_compare_cjs_1 = require("./semver-compare.cjs");
|
|
27
29
|
// Quick 261003-lqi (FIX-C): the ship-time drift check resolves the platform tag
|
|
28
30
|
// through the init-time owner, so it and init cannot disagree about the tag.
|
|
@@ -663,6 +665,57 @@ function cmdUiPlanGate(projectDir, args, raw) {
|
|
|
663
665
|
}
|
|
664
666
|
output(computeUiPlanGate(projectDir, phase), raw, undefined);
|
|
665
667
|
}
|
|
668
|
+
// ─── plan-sizing ──────────────────────────────────────────────────────────────
|
|
669
|
+
/**
|
|
670
|
+
* plan-sizing: reads a phase's PLAN.md files and `context_window` and reports plans
|
|
671
|
+
* over the 6-task ceiling, plans over the per-plan budget, and serial splits that
|
|
672
|
+
* have neither a parallelism nor a budget reason (Phase 36, D-14).
|
|
673
|
+
*
|
|
674
|
+
* Invocable as: pp_run check plan-sizing <phase>
|
|
675
|
+
* Exits 0 on every analysed phase; `block` carries the verdict in the payload.
|
|
676
|
+
*/
|
|
677
|
+
function cmdPlanSizing(projectDir, args, raw) {
|
|
678
|
+
// args[0] = 'check', args[1] = 'plan-sizing', args[2] = phase
|
|
679
|
+
const phase = args[2] || '';
|
|
680
|
+
if (!phase) {
|
|
681
|
+
error('plan-sizing requires a phase argument: check plan-sizing <phase>', ERROR_REASON.SDK_MISSING_ARG);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
output((0, plan_sizing_cjs_1.computePlanSizing)(projectDir, phase), raw, undefined);
|
|
685
|
+
}
|
|
686
|
+
// ─── test-substance ───────────────────────────────────────────────────────────
|
|
687
|
+
/**
|
|
688
|
+
* test-substance: reads a phase's PLAN.md files and reports, per task, a missing or
|
|
689
|
+
* generic <behavior>, a verify command that can pass without asserting, a MISSING
|
|
690
|
+
* test file no task creates and a scaffold that describes a vacuous test
|
|
691
|
+
* (Phase 38, D-06).
|
|
692
|
+
*
|
|
693
|
+
* Invocable as: pp_run check test-substance <phase>
|
|
694
|
+
* Exits 0 on every analysed phase; `block` carries the verdict in the payload.
|
|
695
|
+
*
|
|
696
|
+
* The scan mode, `check test-substance --scan <file...>`, counts assertions per test
|
|
697
|
+
* case in the given test files (Phase 38, D-14): the validation-time half, read by
|
|
698
|
+
* validate-phase. It exits 0 on every scanned list; a file the scanner cannot read
|
|
699
|
+
* reports `unknown` with a reason.
|
|
700
|
+
*/
|
|
701
|
+
function cmdTestSubstance(projectDir, args, raw) {
|
|
702
|
+
// args[0] = 'check', args[1] = 'test-substance', args[2..] = phase (flags skipped) or --scan <file...>
|
|
703
|
+
if (args[2] === '--scan') {
|
|
704
|
+
const files = args.slice(3).filter((a) => !a.startsWith('--'));
|
|
705
|
+
if (files.length === 0) {
|
|
706
|
+
error('test-substance --scan requires at least one test file: check test-substance --scan <file...>', ERROR_REASON.SDK_MISSING_ARG);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
output((0, test_substance_cjs_1.scanTestFiles)(projectDir, files), raw, undefined);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const phase = args.slice(2).find((a) => !a.startsWith('--')) || '';
|
|
713
|
+
if (!phase) {
|
|
714
|
+
error('test-substance requires a phase argument or test files: check test-substance <phase> | check test-substance --scan <file...>', ERROR_REASON.SDK_MISSING_ARG);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
output((0, test_substance_cjs_1.computeTestSubstance)(projectDir, phase), raw, undefined);
|
|
718
|
+
}
|
|
666
719
|
// ─── ui-safety-gate ───────────────────────────────────────────────────────────
|
|
667
720
|
/**
|
|
668
721
|
* ui-safety-gate: post-wave check that verifies UI-changed files conform to
|
|
@@ -3105,6 +3158,14 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
3105
3158
|
cmdUiPlanGate(cwd, args, raw);
|
|
3106
3159
|
return;
|
|
3107
3160
|
}
|
|
3161
|
+
if (subcommand === 'plan-sizing') {
|
|
3162
|
+
cmdPlanSizing(cwd, args, raw);
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
if (subcommand === 'test-substance') {
|
|
3166
|
+
cmdTestSubstance(cwd, args, raw);
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3108
3169
|
if (subcommand === 'gap-analysis-plan-post') {
|
|
3109
3170
|
cmdGapAnalysisPlanPost(cwd, args, raw);
|
|
3110
3171
|
return;
|
|
@@ -3175,7 +3236,7 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
3175
3236
|
(0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
|
|
3176
3237
|
return;
|
|
3177
3238
|
}
|
|
3178
|
-
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, prohibition-enforcement, ship-platform-drift, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, vault-review, vault-status, vault-trace, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3239
|
+
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, plan-sizing, prohibition-enforcement, ship-platform-drift, tdd-review-checkpoint, test-substance, ui-plan-gate, ui-safety-gate, vault-review, vault-status, vault-trace, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3179
3240
|
}
|
|
3180
3241
|
module.exports = {
|
|
3181
3242
|
routeCheckCommand,
|
|
@@ -51,6 +51,10 @@ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
|
51
51
|
// Node built-ins and it never requires this module, so the edge adds no cycle.
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
53
|
const vaultSeal = require("./vault-seal.cjs");
|
|
54
|
+
// 35-02: per-phase TIMELINE.jsonl fold for cmdStats (timeline key + wall_clock column).
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
|
+
const timelineMod = require("./timeline.cjs");
|
|
57
|
+
const { foldRecords, buildPhaseTimeline } = timelineMod;
|
|
54
58
|
// ─── Phase Status ─────────────────────────────────────────────────────────────
|
|
55
59
|
/**
|
|
56
60
|
* Determine phase status by checking plan/summary counts AND verification state.
|
|
@@ -1141,6 +1145,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
1141
1145
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
1142
1146
|
// Phase & plan stats (reuse progress pattern)
|
|
1143
1147
|
const phasesByNumber = new Map();
|
|
1148
|
+
const timeline = {};
|
|
1144
1149
|
let totalPlans = 0;
|
|
1145
1150
|
let totalSummaries = 0;
|
|
1146
1151
|
try {
|
|
@@ -1160,6 +1165,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
1160
1165
|
plans: 0,
|
|
1161
1166
|
summaries: 0,
|
|
1162
1167
|
status: 'Not Started',
|
|
1168
|
+
wall_clock: null,
|
|
1163
1169
|
});
|
|
1164
1170
|
}
|
|
1165
1171
|
}
|
|
@@ -1186,12 +1192,31 @@ function cmdStats(cwd, format, raw) {
|
|
|
1186
1192
|
const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Not Started');
|
|
1187
1193
|
const normalizedNum = normalizePhaseName(phaseNum);
|
|
1188
1194
|
const existing = phasesByNumber.get(normalizedNum);
|
|
1195
|
+
// D-12/D-15: fold this phase's TIMELINE file(s). Its own try/catch keeps one
|
|
1196
|
+
// unreadable file from escaping into the outer catch and dropping later phases.
|
|
1197
|
+
let wallClock = existing?.wall_clock ?? null;
|
|
1198
|
+
try {
|
|
1199
|
+
const timelineFiles = phaseFiles.filter(f => f.endsWith('-TIMELINE.jsonl'));
|
|
1200
|
+
if (timelineFiles.length > 0) {
|
|
1201
|
+
const text = timelineFiles
|
|
1202
|
+
.map(f => node_fs_1.default.readFileSync(node_path_1.default.join(phasesDir, dir, f), 'utf-8'))
|
|
1203
|
+
.join('\n');
|
|
1204
|
+
const runs = foldRecords(text);
|
|
1205
|
+
if (runs.length > 0) {
|
|
1206
|
+
const built = buildPhaseTimeline(runs);
|
|
1207
|
+
timeline[normalizedNum] = built;
|
|
1208
|
+
wallClock = built.wall_clock;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
catch { /* a bad timeline file never drops phases */ }
|
|
1189
1213
|
phasesByNumber.set(normalizedNum, {
|
|
1190
1214
|
number: normalizedNum,
|
|
1191
1215
|
name: existing?.name || phaseName,
|
|
1192
1216
|
plans: (existing?.plans || 0) + plans,
|
|
1193
1217
|
summaries: (existing?.summaries || 0) + summaries,
|
|
1194
1218
|
status,
|
|
1219
|
+
wall_clock: wallClock,
|
|
1195
1220
|
});
|
|
1196
1221
|
}
|
|
1197
1222
|
}
|
|
@@ -1240,6 +1265,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
1240
1265
|
milestone_version: milestone.version,
|
|
1241
1266
|
milestone_name: milestone.name,
|
|
1242
1267
|
phases,
|
|
1268
|
+
timeline,
|
|
1243
1269
|
phases_completed: completedPhases,
|
|
1244
1270
|
phases_total: phases.length,
|
|
1245
1271
|
total_plans: totalPlans,
|
|
@@ -1266,10 +1292,10 @@ function cmdStats(cwd, format, raw) {
|
|
|
1266
1292
|
out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
|
|
1267
1293
|
}
|
|
1268
1294
|
out += '\n';
|
|
1269
|
-
out += `| Phase | Name | Plans | Completed | Status |\n`;
|
|
1270
|
-
out +=
|
|
1295
|
+
out += `| Phase | Name | Plans | Completed | Status | Wall-clock |\n`;
|
|
1296
|
+
out += `|-------|------|-------|-----------|--------|------------|\n`;
|
|
1271
1297
|
for (const p of phases) {
|
|
1272
|
-
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
|
|
1298
|
+
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} | ${p.wall_clock ?? '—'} |\n`;
|
|
1273
1299
|
}
|
|
1274
1300
|
if (gitCommits > 0) {
|
|
1275
1301
|
out += `\n**Git:** ${gitCommits} commits`;
|
|
@@ -589,6 +589,11 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
589
589
|
if (kp === 'workflow.drift_action' && !isStringMember(VALID_DRIFT_ACTIONS, parsedValue)) {
|
|
590
590
|
error(`Invalid workflow.drift_action '${val}'. Valid values: ${VALID_DRIFT_ACTIONS.join(', ')}`);
|
|
591
591
|
}
|
|
592
|
+
// Phase classification (D-09, CLAS-03): `full` reproduces the pre-1.21 gate stack.
|
|
593
|
+
const VALID_CEREMONY_MODES = ['proportional', 'full'];
|
|
594
|
+
if (kp === 'workflow.ceremony' && !isStringMember(VALID_CEREMONY_MODES, parsedValue)) {
|
|
595
|
+
error(`Invalid workflow.ceremony '${val}'. Valid values: ${VALID_CEREMONY_MODES.join(', ')}`);
|
|
596
|
+
}
|
|
592
597
|
if (kp === 'workflow.drift_threshold') {
|
|
593
598
|
if (typeof parsedValue !== 'number' || !Number.isInteger(parsedValue) || parsedValue < 1) {
|
|
594
599
|
error(`Invalid workflow.drift_threshold '${val}'. Must be a positive integer.`);
|
package/pp-core/bin/lib/init.cjs
CHANGED
|
@@ -32,6 +32,7 @@ const worktreeSafety = require("./worktree-safety.cjs");
|
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
33
33
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
34
34
|
const secrets_cjs_1 = require("./secrets.cjs");
|
|
35
|
+
const plan_sizing_cjs_1 = require("./plan-sizing.cjs");
|
|
35
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
36
37
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
37
38
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
@@ -222,6 +223,49 @@ function resolvePhaseForInit(cwd, phase) {
|
|
|
222
223
|
}
|
|
223
224
|
return { phaseInfo, roadmapPhase };
|
|
224
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Phase 37 (D-15): the recorded verdict as init keys, plus the research flag folded
|
|
228
|
+
* through it. Only plan-phase ever computes a verdict; this reads the recorded one,
|
|
229
|
+
* else full. Any throw returns the full keys and the configured research value, so a
|
|
230
|
+
* broken read never lowers ceremony (T-37-08). The fold can only turn research off when
|
|
231
|
+
* config has it on, the verdict says there is no codebase to match and nothing forced
|
|
232
|
+
* the researcher; it never turns a config-off gate on (T-37-10).
|
|
233
|
+
*/
|
|
234
|
+
function ceremonyInitKeys(cwd, phase, researchConfigured) {
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
236
|
+
const loadCeremony = () => require('./ceremony.cjs');
|
|
237
|
+
try {
|
|
238
|
+
const { effectiveVerdict, resolveGateConfig, deriveCeremonyKeys } = loadCeremony();
|
|
239
|
+
const { verdict } = effectiveVerdict(cwd, phase);
|
|
240
|
+
const gateConfig = resolveGateConfig(cwd);
|
|
241
|
+
const keys = deriveCeremonyKeys(verdict, gateConfig.code_review_depth);
|
|
242
|
+
const keepResearch = verdict.mode === 'full' ||
|
|
243
|
+
verdict.answers.codebase_to_match.value === true ||
|
|
244
|
+
verdict.gates['research']?.decision === 'run';
|
|
245
|
+
return {
|
|
246
|
+
keys,
|
|
247
|
+
research_enabled: researchConfigured === undefined ? researchConfigured : Boolean(researchConfigured) && keepResearch,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
let depth = 'standard';
|
|
252
|
+
try {
|
|
253
|
+
depth = loadCeremony().resolveGateConfig(cwd).code_review_depth;
|
|
254
|
+
}
|
|
255
|
+
catch { /* keep standard */ }
|
|
256
|
+
return {
|
|
257
|
+
keys: {
|
|
258
|
+
ceremony_tier: 'heavy',
|
|
259
|
+
ceremony_mode: 'full',
|
|
260
|
+
plan_check_rounds: 3,
|
|
261
|
+
plan_check_mode: 'standard',
|
|
262
|
+
verifier_depth: 'full',
|
|
263
|
+
code_review_depth: depth === 'quick' || depth === 'deep' ? depth : 'standard',
|
|
264
|
+
},
|
|
265
|
+
research_enabled: researchConfigured,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
225
269
|
function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
226
270
|
if (!phase) {
|
|
227
271
|
error('phase required for init execute-phase');
|
|
@@ -235,6 +279,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
235
279
|
: null;
|
|
236
280
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
237
281
|
const wf = (config.workflow ?? {});
|
|
282
|
+
const ceremony = ceremonyInitKeys(cwd, phase, undefined);
|
|
238
283
|
const result = {
|
|
239
284
|
executor_model: resolveModelInternal(cwd, 'pp-executor'),
|
|
240
285
|
verifier_model: resolveModelInternal(cwd, 'pp-verifier'),
|
|
@@ -247,6 +292,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
247
292
|
phase_branch_template: config.phase_branch_template,
|
|
248
293
|
milestone_branch_template: config.milestone_branch_template,
|
|
249
294
|
verifier_enabled: config.verifier,
|
|
295
|
+
...ceremony.keys,
|
|
250
296
|
phase_found: !!phaseInfo,
|
|
251
297
|
phase_dir: phaseInfo?.['directory'] || null,
|
|
252
298
|
phase_number: phaseInfo?.['phase_number'] || null,
|
|
@@ -331,13 +377,16 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
331
377
|
assertValidGranularityOverride(granularityOverride, error);
|
|
332
378
|
const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
|
|
333
379
|
const wf = (config.workflow ?? {});
|
|
380
|
+
const ceremony = ceremonyInitKeys(cwd, phase, wf['research']);
|
|
334
381
|
const result = {
|
|
335
382
|
researcher_model: resolveModelInternal(cwd, 'pp-phase-researcher'),
|
|
336
383
|
planner_model: resolveModelInternal(cwd, 'pp-planner'),
|
|
337
384
|
checker_model: resolveModelInternal(cwd, 'pp-plan-checker'),
|
|
338
385
|
tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
|
|
339
386
|
granularity,
|
|
340
|
-
|
|
387
|
+
...(0, plan_sizing_cjs_1.planBudget)(config.context_window),
|
|
388
|
+
...ceremony.keys,
|
|
389
|
+
research_enabled: ceremony.research_enabled,
|
|
341
390
|
plan_checker_enabled: config.plan_checker,
|
|
342
391
|
nyquist_validation_enabled: wf['nyquist_validation'],
|
|
343
392
|
commit_docs: config.commit_docs,
|
package/pp-core/bin/lib/io.cjs
CHANGED
|
@@ -225,6 +225,8 @@ const ERROR_REASON = Object.freeze({
|
|
|
225
225
|
DISCOVERY_SESSION_CHECK_FAILED: 'discovery_session_check_failed',
|
|
226
226
|
DISCOVERY_DEVIATIONS_CHECK_FAILED: 'discovery_deviations_check_failed',
|
|
227
227
|
DISCOVERY_DEVIATIONS_UNAPPROVED: 'discovery_deviations_unapproved',
|
|
228
|
+
// timeline (Phase 35: end with no matching open run)
|
|
229
|
+
TIMELINE_NO_OPEN_RUN: 'timeline_no_open_run',
|
|
228
230
|
// generic
|
|
229
231
|
USAGE: 'usage',
|
|
230
232
|
UNKNOWN: 'unknown',
|
|
@@ -40,7 +40,7 @@ const { resolveCapabilityRuntimeState, SEAL_FIELD, sealRenderVerdict, resolveInj
|
|
|
40
40
|
// ─── Capability-activation engine (single owner for config-key precedence) ────
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
42
|
const capabilityActivationModule = require("./capability-activation.cjs");
|
|
43
|
-
const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey } = capabilityActivationModule;
|
|
43
|
+
const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey, evaluateWhen } = capabilityActivationModule;
|
|
44
44
|
// ─── Canonical points (derived from LOOP_HOST_CONTRACT — authoritative 12) ───
|
|
45
45
|
// FIX 2: Derive the authoritative canonical set from LOOP_HOST_CONTRACT so it
|
|
46
46
|
// cannot drift from the host contract. CANONICAL_POINTS_FALLBACK is kept as an
|
|
@@ -99,11 +99,14 @@ function _getCanonicalPoints(_registry) {
|
|
|
99
99
|
* Ordering: steps first, then contributions, then gates. Within each array,
|
|
100
100
|
* the materialized registry order is preserved.
|
|
101
101
|
*
|
|
102
|
-
* Activation: a hook with no `when` is always active. With `when` (dotted key
|
|
103
|
-
*
|
|
102
|
+
* Activation: a hook with no `when` is always active. With `when` (a dotted key,
|
|
103
|
+
* or dotted keys joined by `&&`), resolved against `config` by evaluateWhen in
|
|
104
|
+
* capability-activation; active iff every conjunct holds. A `ceremony.<gate>`
|
|
105
|
+
* conjunct reads the phase verdict through the `ceremony` thunk and is true
|
|
106
|
+
* without one. Inactive hooks are filtered out.
|
|
104
107
|
*/
|
|
105
108
|
function resolveLoopHooks(input) {
|
|
106
|
-
const { point, registry, config, cwd, capabilityStatesById, recordedArtifacts, platformBindingsPresent, } = input;
|
|
109
|
+
const { point, registry, config, cwd, capabilityStatesById, recordedArtifacts, platformBindingsPresent, ceremony, } = input;
|
|
107
110
|
// Validate point
|
|
108
111
|
const canonicalPoints = _getCanonicalPoints(registry);
|
|
109
112
|
if (!canonicalPoints.includes(point)) {
|
|
@@ -124,14 +127,10 @@ function resolveLoopHooks(input) {
|
|
|
124
127
|
const activeHooks = [];
|
|
125
128
|
// Helper: check activation using single-key precedence resolver (FIX 1 + FIX 3)
|
|
126
129
|
function isActive(hook) {
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// FIX 3: `when` present but not a non-empty string → malformed registry data → INACTIVE
|
|
132
|
-
if (typeof when !== 'string' || when.length === 0)
|
|
133
|
-
return false;
|
|
134
|
-
return _resolveActivationValue(when, config, cwd, registry);
|
|
130
|
+
// One evaluator owns the `when` rules (no `when` -> active; malformed -> inactive;
|
|
131
|
+
// `&&` conjuncts; `ceremony.<gate>` through the injected thunk), shared with
|
|
132
|
+
// capability-state so the two cannot drift (Phase 37 D-14).
|
|
133
|
+
return evaluateWhen(hook['when'], { config, cwd, registry, ceremony });
|
|
135
134
|
}
|
|
136
135
|
function isCapabilityActive(capId) {
|
|
137
136
|
if (!capabilityStatesById)
|
|
@@ -487,6 +486,11 @@ function renderLoopHooks(resolved) {
|
|
|
487
486
|
* seal gate render (Phase 16 D-19). `--active-cap <cap>` also requires the
|
|
488
487
|
* capability's own state to be active (D-27).
|
|
489
488
|
*
|
|
489
|
+
* options.phase + options.resolveCeremonyGate: an injected
|
|
490
|
+
* `(phase) => (gate) => boolean`; with a non-empty `--phase` a `ceremony.<gate>`
|
|
491
|
+
* conjunct of a hook's `when` answers from that phase's recorded verdict. Without
|
|
492
|
+
* both, or under `--active-cap`, every such conjunct is true (Phase 37 D-14).
|
|
493
|
+
*
|
|
490
494
|
* options.resolvePlatformBindingsPresent: an injected `() => boolean` (pp-tools
|
|
491
495
|
* builds it on the vault leaf's hasPlatformBindings); only literal `false`
|
|
492
496
|
* drops a `requiresPlatformBindings` capability's hooks (QD-13). `--into` and
|
|
@@ -544,6 +548,15 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
544
548
|
// (pp-tools passes one built on the vault leaf's hasPlatformBindings).
|
|
545
549
|
const resolvePlatformBindingsPresent = options['resolvePlatformBindingsPresent'];
|
|
546
550
|
const platformBindingsPresent = () => resolveInjectedPlatformBindingsPresent(resolvePlatformBindingsPresent);
|
|
551
|
+
// Phase 37 D-14: the ceremony verdict arrives the same way. Only a non-empty
|
|
552
|
+
// `--phase` plus an injected resolver builds the thunk (once, so the verdict
|
|
553
|
+
// is read once per run); anything else passes none and every `ceremony.*`
|
|
554
|
+
// conjunct stays true. `--active-cap` reads the unfiltered resolution.
|
|
555
|
+
const ceremonyPhase = typeof options['phase'] === 'string' ? options['phase'] : '';
|
|
556
|
+
const resolveCeremonyGate = options['resolveCeremonyGate'];
|
|
557
|
+
const ceremonyThunk = activeCapId === undefined && ceremonyPhase.length > 0 && typeof resolveCeremonyGate === 'function'
|
|
558
|
+
? resolveCeremonyGate(ceremonyPhase)
|
|
559
|
+
: undefined;
|
|
547
560
|
let resolved;
|
|
548
561
|
try {
|
|
549
562
|
// --active-cap reads the UNFILTERED resolution (16-16 D-81, see above):
|
|
@@ -551,6 +564,7 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
551
564
|
resolved = resolveLoopHooks({
|
|
552
565
|
point, registry, config, cwd, capabilityStatesById, recordedArtifacts,
|
|
553
566
|
...(activeCapId === undefined ? { platformBindingsPresent } : {}),
|
|
567
|
+
...(ceremonyThunk !== undefined ? { ceremony: ceremonyThunk } : {}),
|
|
554
568
|
});
|
|
555
569
|
}
|
|
556
570
|
catch (err) {
|
|
@@ -58,7 +58,7 @@ const uatPredicate = require("./uat-predicate.cjs");
|
|
|
58
58
|
const { evaluateUatPassed } = uatPredicate;
|
|
59
59
|
const { planningDir, withPlanningLock, milestonePathsFor } = planningWorkspace;
|
|
60
60
|
const { extractFrontmatter } = frontmatterMod;
|
|
61
|
-
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
61
|
+
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, syncStateFrontmatter, stripFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
62
62
|
// #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
|
|
63
63
|
const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md';
|
|
64
64
|
// Any .md file with PLAN anywhere in the basename — diagnostic net
|
|
@@ -347,6 +347,43 @@ function extractObjective(content) {
|
|
|
347
347
|
const m = content.match(/<objective>\s*\n?\s*(.+)/);
|
|
348
348
|
return m ? m[1].trim() : null;
|
|
349
349
|
}
|
|
350
|
+
// Tasks in a PLAN.md body: `<task>` elements (case-insensitive; the `<tasks>` wrapper
|
|
351
|
+
// does not match), else `## Task N` headings for markdown-format plans. A task counts
|
|
352
|
+
// only where it really is one: the opening tag (or heading) starts a line and the tag
|
|
353
|
+
// has a closing `</task>`, so a `<task>` mentioned in prose or inline code is not a
|
|
354
|
+
// task. Shared with `check plan-sizing`, where the count feeds `over_ceiling`.
|
|
355
|
+
//
|
|
356
|
+
// One pass (36-W-1b): the earlier single regex `^[ \t]*<task(?=[\s>])[^>]*>[\s\S]*?</task>`
|
|
357
|
+
// rescanned to the end of the file for every unclosed open line, which is quadratic. Here
|
|
358
|
+
// the open tag is found, its `>` is the first one after it (what `[^>]*>` always ended
|
|
359
|
+
// at), and the close is searched from there (what the lazy `[\s\S]*?` ended at). When
|
|
360
|
+
// either search fails the loop stops: any later line-start open tag begins after this
|
|
361
|
+
// one, so its first `>` and its close search start no earlier and fail too. After a
|
|
362
|
+
// match the open search resumes where the old global match resumed, at the end of the
|
|
363
|
+
// close tag. The content is not lowercased: `toLowerCase` maps U+212A to `k` and
|
|
364
|
+
// changes the length of U+0130, which the regex `i` flag (non-unicode) does not do.
|
|
365
|
+
function countPlanTasks(content) {
|
|
366
|
+
const open = /^[ \t]*<task(?=[\s>])/gim;
|
|
367
|
+
const close = /<\/task>/gi;
|
|
368
|
+
let xmlTasks = 0;
|
|
369
|
+
for (;;) {
|
|
370
|
+
const found = open.exec(content);
|
|
371
|
+
if (found === null)
|
|
372
|
+
break;
|
|
373
|
+
const gt = content.indexOf('>', open.lastIndex);
|
|
374
|
+
if (gt < 0)
|
|
375
|
+
break;
|
|
376
|
+
close.lastIndex = gt + 1;
|
|
377
|
+
const closed = close.exec(content);
|
|
378
|
+
if (closed === null)
|
|
379
|
+
break;
|
|
380
|
+
xmlTasks++;
|
|
381
|
+
open.lastIndex = close.lastIndex;
|
|
382
|
+
}
|
|
383
|
+
if (xmlTasks > 0)
|
|
384
|
+
return xmlTasks;
|
|
385
|
+
return (content.match(/^[ \t]*#{2,}\s*Task\s*\d+/gim) || []).length;
|
|
386
|
+
}
|
|
350
387
|
// O(V + E). Assigns each in-phase plan its longest-path topological level over the
|
|
351
388
|
// in-phase dependsOn DAG (Kahn's algorithm). Returns { level: Map<id,number>, visited: number }.
|
|
352
389
|
// visited < rawPlans.length signals a dependency cycle.
|
|
@@ -444,9 +481,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
444
481
|
const planPath = node_path_1.default.join(phaseDir, planFile);
|
|
445
482
|
const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
|
|
446
483
|
const fm = extractFrontmatter(content);
|
|
447
|
-
const
|
|
448
|
-
const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
449
|
-
const taskCount = xmlTasks.length || mdTasks.length;
|
|
484
|
+
const taskCount = countPlanTasks(content);
|
|
450
485
|
const parsedWave = parseInt(fm['wave'], 10);
|
|
451
486
|
const declaredWave = Number.isNaN(parsedWave) ? null : parsedWave;
|
|
452
487
|
let dependsOn = [];
|
|
@@ -1320,7 +1355,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1320
1355
|
}
|
|
1321
1356
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
1322
1357
|
const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
1323
|
-
|
|
1358
|
+
// #1257 class: the body-field patterns are case-insensitive and line-anchored, so
|
|
1359
|
+
// run over the whole file they hit the YAML keys first (`status:`, `progress:`)
|
|
1360
|
+
// and the body Status line never moved. Edit the body only; syncStateFrontmatter
|
|
1361
|
+
// below re-derives the frontmatter from it.
|
|
1362
|
+
const strippedBody = stripFrontmatter(originalStateContent);
|
|
1363
|
+
let frontmatterPrefix = originalStateContent.slice(0, originalStateContent.length - strippedBody.length);
|
|
1364
|
+
let stateContent = strippedBody;
|
|
1324
1365
|
const phaseValue = nextPhaseNum || phaseNum;
|
|
1325
1366
|
const nextPhaseDisplayName = phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
|
|
1326
1367
|
phaseDisplayNameFromSlug(nextPhaseName);
|
|
@@ -1385,7 +1426,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1385
1426
|
const newPercent = (0, phase_lifecycle_cjs_1.clampPercent)(newCompleted, totalPhases);
|
|
1386
1427
|
stateContent =
|
|
1387
1428
|
stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
|
|
1388
|
-
|
|
1429
|
+
frontmatterPrefix = frontmatterPrefix.replace(/(percent:\s*)\d+/, `$1${newPercent}`);
|
|
1389
1430
|
}
|
|
1390
1431
|
}
|
|
1391
1432
|
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
@@ -1395,14 +1436,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1395
1436
|
// one still on disk.
|
|
1396
1437
|
// D-10: this transaction sets Last activity itself; that explicit date flows.
|
|
1397
1438
|
const activityFieldSet = (0, state_document_cjs_1.extractPositionalField)(stateContent, 'Last Activity', 'Last activity') !==
|
|
1398
|
-
(0, state_document_cjs_1.extractPositionalField)(
|
|
1439
|
+
(0, state_document_cjs_1.extractPositionalField)(strippedBody, 'Last Activity', 'Last activity');
|
|
1399
1440
|
// 261007-rly D-11: phase complete opts OUT of the stopped_at hold and keeps
|
|
1400
1441
|
// re-deriving the key from the Session line, as it did before the hold moved
|
|
1401
1442
|
// into syncStateFrontmatter: bug #3517's pin (tests/phase.test.cjs) needs a
|
|
1402
1443
|
// newer Session line to replace a stale stored value here, the opposite of
|
|
1403
1444
|
// the operator-note shape the hold protects. Filed, not decided here:
|
|
1404
1445
|
// todos/pending/phase-complete-rederives-stopped-at-from-the-session-line.md.
|
|
1405
|
-
stateContent = syncStateFrontmatter(stateContent, cwd, roadmapContent, activityFieldSet, true);
|
|
1446
|
+
stateContent = syncStateFrontmatter(frontmatterPrefix + stateContent, cwd, roadmapContent, activityFieldSet, true);
|
|
1406
1447
|
stateFrontmatterUpdated = (0, state_document_cjs_1.diffFrontmatterFields)(extractFrontmatter(originalStateContent), extractFrontmatter(stateContent));
|
|
1407
1448
|
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
|
|
1408
1449
|
}
|
|
@@ -1477,4 +1518,5 @@ module.exports = {
|
|
|
1477
1518
|
cmdPhaseComplete,
|
|
1478
1519
|
cmdPhaseUatPassed,
|
|
1479
1520
|
computeDependencyLevels,
|
|
1521
|
+
countPlanTasks,
|
|
1480
1522
|
};
|