peaks-loop 4.0.48 → 4.0.50

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 (156) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/audit-commands.js +1 -0
  5. package/dist/cli/commands/baseline-commands.js +163 -25
  6. package/dist/cli/commands/compact-command.js +1 -3
  7. package/dist/cli/commands/core/skill-command.js +53 -4
  8. package/dist/cli/commands/core/standards-command.d.ts +24 -0
  9. package/dist/cli/commands/core/standards-command.js +74 -0
  10. package/dist/cli/commands/feedback-commands.d.ts +11 -7
  11. package/dist/cli/commands/feedback-commands.js +49 -17
  12. package/dist/cli/commands/final-review-commands.js +12 -0
  13. package/dist/cli/commands/hooks-commands.js +55 -38
  14. package/dist/cli/commands/loop-eval-commands.js +22 -6
  15. package/dist/cli/commands/share-commands.js +37 -11
  16. package/dist/cli/commands/slice-integrate-commands.js +17 -0
  17. package/dist/cli/commands/web-commands.js +8 -1
  18. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
  19. package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
  20. package/dist/services/adapter/adapter.d.ts +30 -0
  21. package/dist/services/adapter/auto-adapter.d.ts +13 -0
  22. package/dist/services/adapter/claude-adapter.js +12 -0
  23. package/dist/services/adapter/codex-adapter.d.ts +12 -0
  24. package/dist/services/adapter/codex-adapter.js +12 -0
  25. package/dist/services/adapter/copilot-adapter.d.ts +12 -0
  26. package/dist/services/adapter/copilot-adapter.js +12 -0
  27. package/dist/services/artifacts/artifact-prerequisites.js +10 -0
  28. package/dist/services/artifacts/request-artifact-service.js +59 -38
  29. package/dist/services/audit/backing-detector.d.ts +25 -7
  30. package/dist/services/audit/backing-detector.js +33 -17
  31. package/dist/services/audit/enforcer-liveness.d.ts +12 -0
  32. package/dist/services/audit/enforcer-liveness.js +100 -0
  33. package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
  34. package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
  35. package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
  36. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
  37. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
  38. package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
  39. package/dist/services/audit/enforcers/lint-style.js +38 -2
  40. package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
  41. package/dist/services/audit/prose-ratio-calculator.js +25 -18
  42. package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
  43. package/dist/services/audit/red-lines-service.js +51 -7
  44. package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
  45. package/dist/services/capability-audit-service/independent-checker.js +140 -0
  46. package/dist/services/capability-audit-service/index.d.ts +3 -1
  47. package/dist/services/capability-audit-service/index.js +1 -0
  48. package/dist/services/capability-audit-service/runner.d.ts +17 -13
  49. package/dist/services/capability-audit-service/runner.js +76 -15
  50. package/dist/services/capability-audit-service/types.d.ts +48 -0
  51. package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
  52. package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
  53. package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
  54. package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
  55. package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
  56. package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
  57. package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
  58. package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
  59. package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
  60. package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
  61. package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
  62. package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
  63. package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
  64. package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
  65. package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
  66. package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
  67. package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
  68. package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
  69. package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
  70. package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
  71. package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
  72. package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
  73. package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
  74. package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
  75. package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
  76. package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
  77. package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
  78. package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
  79. package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
  80. package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
  81. package/dist/services/capability-guard-runner/registry.d.ts +5 -0
  82. package/dist/services/capability-guard-runner/registry.js +140 -0
  83. package/dist/services/capability-guard-runner/runner.d.ts +26 -0
  84. package/dist/services/capability-guard-runner/runner.js +63 -6
  85. package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
  86. package/dist/services/code/auto-compact-lifecycle.js +65 -16
  87. package/dist/services/code/auto-compact-modes.d.ts +13 -2
  88. package/dist/services/code/auto-compact-modes.js +20 -4
  89. package/dist/services/code/auto-compact-orchestrator.js +119 -19
  90. package/dist/services/code/compact-event-settle.d.ts +20 -8
  91. package/dist/services/code/compact-event-settle.js +21 -0
  92. package/dist/services/code/post-compact-detector.js +20 -11
  93. package/dist/services/code/step-08-gate.js +21 -6
  94. package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
  95. package/dist/services/config/config-safety.js +11 -9
  96. package/dist/services/context/auto-compact-types.d.ts +20 -2
  97. package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
  98. package/dist/services/feedback/feedback-promotion-service.js +341 -20
  99. package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
  100. package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
  101. package/dist/services/final-review/pre-post-diff.js +10 -2
  102. package/dist/services/job/job-progress-store.js +18 -3
  103. package/dist/services/observability/jsonl-store.d.ts +19 -0
  104. package/dist/services/observability/jsonl-store.js +27 -2
  105. package/dist/services/observability/observability-service.d.ts +11 -4
  106. package/dist/services/observability/observability-service.js +16 -3
  107. package/dist/services/prd/handoff-service.js +43 -0
  108. package/dist/services/qa/qa-business-review-state.js +19 -5
  109. package/dist/services/sc/sc-service.d.ts +8 -0
  110. package/dist/services/sc/sc-service.js +8 -1
  111. package/dist/services/scan/api-diff-types.js +20 -2
  112. package/dist/services/security/safe-settings-path.js +19 -1
  113. package/dist/services/session/getSessionDir.d.ts +33 -0
  114. package/dist/services/session/getSessionDir.js +60 -0
  115. package/dist/services/skill/skill-search-service.d.ts +3 -3
  116. package/dist/services/slice/slice-review-state.js +19 -4
  117. package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
  118. package/dist/services/standards/loop-engineering-lint.js +6 -0
  119. package/dist/services/web/daemon-registry.js +27 -2
  120. package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
  121. package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
  122. package/dist/services/workflow/pipeline-verify-service.js +23 -10
  123. package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
  124. package/dist/services/workspace/claude-settings-template.d.ts +53 -37
  125. package/dist/services/workspace/claude-settings-template.js +105 -83
  126. package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
  127. package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
  128. package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
  129. package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
  130. package/dist/services/workspace/workspace-service.js +11 -1
  131. package/dist/shared/fs-utils.d.ts +26 -0
  132. package/dist/shared/fs-utils.js +35 -0
  133. package/dist/shared/runtime-root.d.ts +73 -0
  134. package/dist/shared/runtime-root.js +77 -0
  135. package/package.json +9 -7
  136. package/scripts/copy-templates.mjs +0 -12
  137. package/scripts/install-skills.mjs +154 -53
  138. package/skills/bee/peaks-qa/SKILL.md +0 -1
  139. package/skills/bee/peaks-rd/SKILL.md +0 -1
  140. package/skills/peaks-code/SKILL.md +12 -10
  141. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  142. package/skills/peaks-code/references/runbook.md +3 -0
  143. package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
  144. package/skills/peaks-code/references/startup-sequence.md +2 -2
  145. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  146. package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
  147. package/dist/cli/commands/context-builder-commands.d.ts +0 -11
  148. package/dist/cli/commands/context-builder-commands.js +0 -85
  149. package/dist/services/hooks/write-gate.js +0 -111
  150. package/skills/bee/peaks-prd/references/command-migration.md +0 -3
  151. package/skills/bee/peaks-qa/references/command-migration.md +0 -3
  152. package/skills/bee/peaks-rd/references/command-migration.md +0 -3
  153. package/skills/bee/peaks-sc/references/command-migration.md +0 -3
  154. package/skills/bee/peaks-txt/references/command-migration.md +0 -3
  155. package/skills/bee/peaks-ui/references/command-migration.md +0 -3
  156. package/skills/peaks-code/references/command-migration.md +0 -3
@@ -1,24 +1,75 @@
1
- // src/services/capability-guard-runner/contracts/J03.ts
2
- import { readFileSync } from 'node:fs';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- const REQUIRED_DIMENSIONS = [
5
- 'functional-completeness',
6
- 'problem-resolution',
7
- 'no-new-bugs',
8
- 'existing-functionality-intact'
9
- ];
4
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
5
+ const DETECTOR = ['scripts', 'lint', 'silent-warning-detector.mjs'];
6
+ /**
7
+ * Violation ceilings measured on the frozen tree when this contract was
8
+ * written (2026-09-15, 767 files scanned). They are a RATCHET, not a target:
9
+ * the invariant is "no silent-catch or fake-green pattern is REINTRODUCED", so
10
+ * the check is that the count does not grow. Fixing violations lowers the
11
+ * ceiling in the same slice that fixes them.
12
+ */
13
+ const CEILING = {
14
+ 'catch-return-null': 41,
15
+ 'empty-catch': 59
16
+ };
17
+ function parseCounts(stdout) {
18
+ const line = /\[silent-warning-detector\] summary: (.+)$/m.exec(stdout);
19
+ if (line === null)
20
+ return null;
21
+ const counts = {};
22
+ for (const pair of line[1].split(',')) {
23
+ const m = /^\s*([a-z-]+)=(\d+)\s*$/.exec(pair);
24
+ if (m !== null)
25
+ counts[m[1]] = Number(m[2]);
26
+ }
27
+ return counts;
28
+ }
29
+ /**
30
+ * Behavioural probe of the "no silent-catch / fake-green reintroduced"
31
+ * invariant.
32
+ *
33
+ * The previous version read `final-review-types.ts` and asserted it contained
34
+ * four dimension strings — a file whose own name is `final-review`, so the
35
+ * check restated its filename.
36
+ *
37
+ * Here the repository's own AST guard (`scripts/lint/silent-warning-detector.mjs`)
38
+ * is executed and its per-rule violation counts are compared against the frozen
39
+ * ceilings. A newly swallowed `catch` in `src/**` raises a count and reddens the
40
+ * journey.
41
+ */
10
42
  export async function runJ03Contract(ctx) {
11
- // Static check: the 4-dim final-review type module MUST still export those 4 dimension strings.
12
- // This is a contract against accidental removal, not a runtime test of an LLM.
13
- const typesPath = join(ctx.projectRoot, 'src', 'services', 'final-review', 'final-review-types.ts');
14
- const src = readFileSync(typesPath, 'utf8');
15
- const missing = REQUIRED_DIMENSIONS.filter((d) => !src.includes(`'${d}'`));
16
- const ok = missing.length === 0;
17
- return {
18
- journeyId: 'J03',
19
- contract: 'workflow-trace',
20
- status: ok ? 'pass' : 'fail',
21
- ...(ok ? {} : { diff: { before: REQUIRED_DIMENSIONS.join(','), after: missing.join(','), reason: `J03#1 broken: missing dimensions ${missing.join(',')}` } }),
22
- artifactPath: 'src/services/final-review/final-review-types.ts'
23
- };
43
+ const row = requireBaselineRow(ctx);
44
+ const missing = missingSourceFiles(ctx, row);
45
+ const detectorPath = join(ctx.projectRoot, ...DETECTOR);
46
+ const detectorPresent = existsSync(detectorPath);
47
+ const run = detectorPresent
48
+ ? spawnSync('node', [detectorPath], { cwd: ctx.projectRoot, encoding: 'utf8', windowsHide: true })
49
+ : null;
50
+ const stdout = run?.stdout ?? '';
51
+ const counts = parseCounts(stdout);
52
+ const scanned = /scanned (\d+) files/.exec(stdout);
53
+ const grew = [];
54
+ const missingRules = [];
55
+ if (counts !== null) {
56
+ for (const rule of Object.keys(CEILING)) {
57
+ if (counts[rule] === undefined)
58
+ missingRules.push(rule);
59
+ else if (counts[rule] > CEILING[rule])
60
+ grew.push(`${rule}: ${String(counts[rule])} > ${String(CEILING[rule])}`);
61
+ }
62
+ }
63
+ const result = combineProbes([
64
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
65
+ probe(detectorPresent, `the repository AST guard is present (${detectorPath})`),
66
+ probe(scanned !== null && Number(scanned[1]) > 0, `the AST guard scanned files (${scanned?.[1] ?? 'none'})`),
67
+ probe(counts !== null, `the AST guard reported per-rule counts (${counts === null ? 'unparseable' : JSON.stringify(counts)})`),
68
+ probe(missingRules.length === 0, `every ratcheted rule was reported (missing: ${missingRules.join(',') || 'none'})`),
69
+ probe(grew.length === 0, `no silent-catch rule count grew past its ceiling (${grew.join('; ') || 'none'})`)
70
+ ]);
71
+ const artifact = row.sourceFiles[1] ?? 'src/services/final-review/final-review-service.ts';
72
+ if (result.ok)
73
+ return pass(ctx, artifact);
74
+ return fail(ctx, artifact, `silent-catch violation counts stay at or below ${JSON.stringify(CEILING)}`, result.detail, 'J03 invariant broken: a silent-catch or fake-green pattern was reintroduced under src/**');
24
75
  }
@@ -1,2 +1,8 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe: drive `auditGoal()` with a complete and an incomplete
4
+ * payload and assert it accepts exactly one of them. Counting `dimension`
5
+ * occurrences in the source would have passed even if the six-element guard
6
+ * were replaced by a comment.
7
+ */
2
8
  export declare function runJ04Contract(ctx: GuardContext): Promise<GuardRunResult>;
@@ -1,42 +1,75 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { mkdtempSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
1
+ import { auditGoal, IncompleteAuditError } from '../../audit/audit-goal-service.js';
2
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
3
+ /** The six dimensions the frozen baseline names, in the baseline's own order. */
4
+ const SIX_DIMENSIONS = [
5
+ 'correctness', 'completeness', 'scope', 'risks', 'alternatives', 'constraints'
6
+ ];
7
+ function runnerReturning(output) {
8
+ return { call: async () => ({ output, tokens: { input: 1, output: 1 } }) };
9
+ }
10
+ function envelope(dimensions, severity = 'info') {
11
+ return JSON.stringify({
12
+ summary: 's',
13
+ audit: dimensions.map((dimension) => ({ dimension, finding: 'f', severity })),
14
+ proposedGoal: 'g',
15
+ successCriteria: ['c'],
16
+ roughEffort: 'small',
17
+ confidence: 'high',
18
+ rationale: 'r'
19
+ });
20
+ }
21
+ /**
22
+ * Behavioural probe: drive `auditGoal()` with a complete and an incomplete
23
+ * payload and assert it accepts exactly one of them. Counting `dimension`
24
+ * occurrences in the source would have passed even if the six-element guard
25
+ * were replaced by a comment.
26
+ */
5
27
  export async function runJ04Contract(ctx) {
6
- const bin = join(ctx.projectRoot, 'bin', 'peaks.js');
7
- const tmp = mkdtempSync(join(tmpdir(), 'cbl-J04-'));
8
- // Probe: `peaks audit goal` must be a registered subcommand (returns a help-like response or a structured envelope).
9
- let helpOk = false;
28
+ const row = requireBaselineRow(ctx);
29
+ const missing = missingSourceFiles(ctx, row);
30
+ let completeAccepted = false;
31
+ let completeCount = -1;
10
32
  try {
11
- const stdout = execFileSync('node', [bin, 'audit', 'goal', '--help'], {
12
- cwd: ctx.projectRoot,
13
- env: { ...process.env, PEAKS_CALLER_ID: `guard-J04-${ctx.sessionId}` },
14
- windowsHide: true
15
- }).toString('utf8');
16
- helpOk = stdout.length > 0 && !stdout.includes('unknown command');
33
+ const out = await auditGoal({ need: 'n' }, runnerReturning(envelope(SIX_DIMENSIONS)));
34
+ completeAccepted = true;
35
+ completeCount = out.audit.length;
17
36
  }
18
37
  catch {
19
- helpOk = false;
38
+ completeAccepted = false;
20
39
  }
21
- // Probe: `peaks audit goal` without a goal must not silently succeed.
22
- let rejectsEmpty = false;
40
+ let fiveRejected = false;
41
+ let rejectName = 'none';
23
42
  try {
24
- execFileSync('node', [bin, 'audit', 'goal', '--project', tmp, '--json'], {
25
- cwd: ctx.projectRoot,
26
- env: { ...process.env, PEAKS_CALLER_ID: `guard-J04-${ctx.sessionId}` },
27
- stdio: ['ignore', 'pipe', 'pipe'],
28
- windowsHide: true
29
- });
43
+ await auditGoal({ need: 'n' }, runnerReturning(envelope(SIX_DIMENSIONS.slice(0, 5))));
30
44
  }
31
45
  catch (e) {
32
- rejectsEmpty = true;
46
+ fiveRejected = true;
47
+ rejectName = e.name;
48
+ }
49
+ // Any one dimension dropped must be refused — not just the last one.
50
+ let eachDropRejected = true;
51
+ const accepted = new Set();
52
+ for (const dropped of SIX_DIMENSIONS) {
53
+ const dims = SIX_DIMENSIONS.filter((d) => d !== dropped);
54
+ try {
55
+ await auditGoal({ need: 'n' }, runnerReturning(envelope(dims)));
56
+ eachDropRejected = false;
57
+ accepted.add(dropped);
58
+ }
59
+ catch {
60
+ /* expected */
61
+ }
33
62
  }
34
- const ok = helpOk && rejectsEmpty;
35
- return {
36
- journeyId: 'J04',
37
- contract: 'hook-assertion',
38
- status: ok ? 'pass' : 'fail',
39
- ...(ok ? {} : { diff: { before: 'audit goal is registered and rejects empty input', after: `helpOk=${helpOk} rejectsEmpty=${rejectsEmpty}`, reason: 'J04#1 broken: audit goal binding is bypassed or missing' } }),
40
- artifactPath: 'src/services/audit/audit-goal-service.ts'
41
- };
63
+ const result = combineProbes([
64
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
65
+ probe(completeAccepted, 'six-dimension audit is accepted'),
66
+ probe(completeCount === 6, `accepted audit carries exactly 6 dimensions (saw ${completeCount})`),
67
+ probe(fiveRejected, 'five-dimension audit is rejected'),
68
+ probe(rejectName === 'IncompleteAuditError', `rejection is IncompleteAuditError (saw ${rejectName}, ${IncompleteAuditError.name})`),
69
+ probe(eachDropRejected, `dropping any single dimension is rejected (accepted: ${[...accepted].join(',') || 'none'})`)
70
+ ]);
71
+ const artifact = row.sourceFiles[0] ?? 'src/services/audit/audit-goal-service.ts';
72
+ if (result.ok)
73
+ return pass(ctx, artifact);
74
+ return fail(ctx, artifact, 'auditGoal() accepts a complete six-dimension payload and rejects any five-dimension payload', result.detail, 'J04 invariant broken: the audit does not cover exactly six dimensions');
42
75
  }
@@ -1,21 +1,123 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
2
3
  import { join } from 'node:path';
3
- const REQUIRED_FRAGMENTS = [
4
- "'functional-completeness'",
5
- "'problem-resolution'",
6
- "'no-new-bugs'",
7
- "'existing-functionality-intact'"
4
+ import { IncompleteFinalReviewError, MAX_EVIDENCE_BYTES_PER_FILE, outputBudgetForEvidence, prepareFinalReview, resolveOutputBudget, undeliverableDimensions } from '../../final-review/final-review-service.js';
5
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
6
+ /** The four dimensions the frozen baseline names. The 5th is NOT one of them. */
7
+ const FOUR_DIMENSIONS = [
8
+ 'functional-completeness', 'problem-resolution', 'no-new-bugs', 'existing-functionality-intact'
8
9
  ];
9
- export async function runJ05Contract(ctx) {
10
- const servicePath = join(ctx.projectRoot, 'src', 'services', 'final-review', 'final-review-service.ts');
11
- const src = readFileSync(servicePath, 'utf8');
12
- const missing = REQUIRED_FRAGMENTS.filter((f) => !src.includes(f));
13
- const ok = missing.length === 0;
10
+ const FIFTH_DIMENSION = 'capability-consistency';
11
+ /**
12
+ * One oversized, omitted source that backs `dimension`. `omitted` + over-cap +
13
+ * zero delivered bytes is the structural case: nothing can ever deliver it.
14
+ */
15
+ function undeliverableSource(dimension, totalBytes) {
14
16
  return {
15
- journeyId: 'J05',
16
- contract: 'workflow-trace',
17
- status: ok ? 'pass' : 'fail',
18
- ...(ok ? {} : { diff: { before: 'all 4 dim strings present in final-review-service.ts', after: missing.join(','), reason: `J05#1 broken: missing fragments ${missing.join(',')}` } }),
19
- artifactPath: 'src/services/final-review/final-review-service.ts'
17
+ source: {
18
+ key: `k-${dimension}`,
19
+ label: dimension,
20
+ segments: [`${dimension}.md`],
21
+ supports: [dimension],
22
+ delivery: { kind: 'whole' }
23
+ },
24
+ relativePath: `${dimension}.md`,
25
+ absolutePath: `/nonexistent/${dimension}.md`,
26
+ status: 'omitted',
27
+ totalBytes,
28
+ includedBytes: 0,
29
+ content: '',
30
+ reason: 'over-cap'
20
31
  };
21
32
  }
33
+ function reviewJson(dimensions) {
34
+ return JSON.stringify({
35
+ rid: 'J05-fixture',
36
+ generatedAt: new Date(0).toISOString(),
37
+ dimensions: dimensions.map((dimension) => ({
38
+ dimension,
39
+ verdict: 'pass',
40
+ summary: 's',
41
+ evidence: [],
42
+ confidence: 'high'
43
+ })),
44
+ overallSummary: 's',
45
+ allPass: true,
46
+ needsAttention: []
47
+ });
48
+ }
49
+ function runnerReturning(output) {
50
+ return { call: async () => ({ output, tokens: { input: 1, output: 1024 } }) };
51
+ }
52
+ /**
53
+ * Drive the real 4-dimension gate: a reply covering three of four dimensions
54
+ * must be refused, a reply covering all four must be accepted. Reading the
55
+ * four strings out of `final-review-service.ts` proved only that the literals
56
+ * were still typed somewhere in the file.
57
+ */
58
+ async function probePrepareFinalReview() {
59
+ const root = mkdtempSync(join(tmpdir(), 'cbl-J05-'));
60
+ try {
61
+ const sessionId = 'guard-J05';
62
+ mkdirSync(join(root, '.peaks', '_runtime', sessionId, 'audit-goal'), { recursive: true });
63
+ writeFileSync(join(root, '.peaks', '_runtime', sessionId, 'audit-goal', 'J05-fixture.json'), JSON.stringify({ successCriteria: ['c'] }));
64
+ const opts = { projectRoot: root, sessionId };
65
+ let rejected = false;
66
+ let rejectName = 'none';
67
+ try {
68
+ await prepareFinalReview('J05-fixture', { ...opts, llmRunner: runnerReturning(reviewJson(FOUR_DIMENSIONS.slice(0, 3))) });
69
+ }
70
+ catch (e) {
71
+ rejected = e.name === IncompleteFinalReviewError.name;
72
+ rejectName = e.name;
73
+ }
74
+ let accepted = false;
75
+ let acceptError = '';
76
+ try {
77
+ const out = await prepareFinalReview('J05-fixture', { ...opts, llmRunner: runnerReturning(reviewJson(FOUR_DIMENSIONS)) });
78
+ accepted = out.dimensions.length === 4;
79
+ }
80
+ catch (e) {
81
+ acceptError = `${e.name}: ${e.message.slice(0, 160)}`;
82
+ }
83
+ return {
84
+ rejected,
85
+ accepted,
86
+ detail: `three-dimension reply rejected=${String(rejected)} (${rejectName}); four-dimension reply accepted=${String(accepted)}${acceptError ? ` [${acceptError}]` : ''}`
87
+ };
88
+ }
89
+ finally {
90
+ rmSync(root, { recursive: true, force: true });
91
+ }
92
+ }
93
+ export async function runJ05Contract(ctx) {
94
+ const row = requireBaselineRow(ctx);
95
+ const missing = missingSourceFiles(ctx, row);
96
+ const prepared = await probePrepareFinalReview();
97
+ const oversized = FOUR_DIMENSIONS.map((d) => undeliverableSource(d, MAX_EVIDENCE_BYTES_PER_FILE + 1000));
98
+ const reported = undeliverableDimensions(oversized).map((r) => r.dimension);
99
+ const fifthOnly = undeliverableDimensions([undeliverableSource(FIFTH_DIMENSION, MAX_EVIDENCE_BYTES_PER_FILE + 1000)]);
100
+ const floor = outputBudgetForEvidence(0);
101
+ const ceiling = outputBudgetForEvidence(Number.MAX_SAFE_INTEGER);
102
+ let badOverrideThrows = false;
103
+ try {
104
+ resolveOutputBudget(0, { PEAKS_FINAL_REVIEW_MAX_OUTPUT_TOKENS: 'abc' });
105
+ }
106
+ catch {
107
+ badOverrideThrows = true;
108
+ }
109
+ const result = combineProbes([
110
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
111
+ probe(prepared.rejected, `a three-dimension review is refused (${prepared.detail})`),
112
+ probe(prepared.accepted, `a four-dimension review is accepted (${prepared.detail})`),
113
+ probe(reported.length === 4 && FOUR_DIMENSIONS.every((d) => reported.includes(d)), `the canonical dimension set is exactly the four baseline dimensions (reported: ${reported.join(',') || 'none'})`),
114
+ probe(fifthOnly.length === 0, `the fifth dimension is not part of the required set (reported: ${fifthOnly.length})`),
115
+ probe(MAX_EVIDENCE_BYTES_PER_FILE === 10240, `per-file evidence cap divides the total by four (${String(MAX_EVIDENCE_BYTES_PER_FILE)})`),
116
+ probe(floor === 15288 && ceiling === 32000, `output budget is clamped on both ends (${String(floor)} / ${String(ceiling)})`),
117
+ probe(badOverrideThrows, 'a non-integer budget override throws instead of falling back silently')
118
+ ]);
119
+ const artifact = row.sourceFiles[0] ?? 'src/services/final-review/final-review-service.ts';
120
+ if (result.ok)
121
+ return pass(ctx, artifact);
122
+ return fail(ctx, artifact, 'every final review contains exactly four dimensions, and ambiguous ones never auto-pass', result.detail, 'J05 invariant broken: the final review no longer enforces exactly four dimensions');
123
+ }
@@ -1,2 +1,16 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe for "the resume option always identifies the DEEPEST
4
+ * completed gate".
5
+ *
6
+ * The load-bearing part is that the SAME fixture is walked from shallow to
7
+ * deep: with only a PRD request the classifier must stop at `rd-planning`;
8
+ * the moment a deeper RD `qa-handoff` request appears (with its review
9
+ * artifacts) the point must move to `qa-validation`; adding the txt handoff
10
+ * must end the workflow. A classifier that returned a constant point, or that
11
+ * looked at file existence alone, cannot track that progression.
12
+ *
13
+ * The old check asserted that a file named `session-resume-service.ts` exists
14
+ * and contains the word "resume" — which the filename already guarantees.
15
+ */
2
16
  export declare function runJ06Contract(ctx: GuardContext): Promise<GuardRunResult>;
@@ -1,43 +1,61 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- // v2: existence is the primary check; the single fragment is best-effort.
4
- // v1 required 3 strict fragments (deepestGate / resume / etc.) and was too strict for
5
- // 4.0.8 — the actual resume service uses camelCase variants and lives under session/.
6
- const CANDIDATE_FILES = [
7
- 'src/services/resume/resume-service.ts',
8
- 'src/services/resume/resume-types.ts',
9
- 'src/services/session/session-resume-service.ts',
10
- 'src/services/session/session-checkpoint-service.ts'
11
- ];
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { classifyResume } from '../../skill/resume-detector.js';
5
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
6
+ const SESSION_ID = '2026-09-15-guard-j06';
7
+ function write(sessionDir, relative, body) {
8
+ const abs = join(sessionDir, relative);
9
+ mkdirSync(dirname(abs), { recursive: true });
10
+ writeFileSync(abs, body);
11
+ }
12
+ function request(body) {
13
+ return `# request\n\n## Status\n\n- state: ${body}\n`;
14
+ }
15
+ /**
16
+ * Behavioural probe for "the resume option always identifies the DEEPEST
17
+ * completed gate".
18
+ *
19
+ * The load-bearing part is that the SAME fixture is walked from shallow to
20
+ * deep: with only a PRD request the classifier must stop at `rd-planning`;
21
+ * the moment a deeper RD `qa-handoff` request appears (with its review
22
+ * artifacts) the point must move to `qa-validation`; adding the txt handoff
23
+ * must end the workflow. A classifier that returned a constant point, or that
24
+ * looked at file existence alone, cannot track that progression.
25
+ *
26
+ * The old check asserted that a file named `session-resume-service.ts` exists
27
+ * and contains the word "resume" — which the filename already guarantees.
28
+ */
12
29
  export async function runJ06Contract(ctx) {
13
- const existing = CANDIDATE_FILES.filter((f) => existsSync(join(ctx.projectRoot, f)));
14
- if (existing.length === 0) {
15
- return {
16
- journeyId: 'J06',
17
- contract: 'workflow-trace',
18
- status: 'fail',
19
- diff: { before: 'a resume service file exists', after: 'none of the candidates found', reason: 'J06#1 broken: no resume service file is present' },
20
- artifactPath: 'src/services/resume/'
21
- };
30
+ const row = requireBaselineRow(ctx);
31
+ const missing = missingSourceFiles(ctx, row);
32
+ const root = mkdtempSync(join(tmpdir(), 'cbl-J06-'));
33
+ const peaksRoot = join(root, '_runtime');
34
+ const sessionDir = join(peaksRoot, SESSION_ID);
35
+ mkdirSync(sessionDir, { recursive: true });
36
+ try {
37
+ write(sessionDir, 'prd/requests/001-shallow.md', request('handed-off'));
38
+ const shallow = classifyResume(SESSION_ID, peaksRoot);
39
+ // `ridOf()` strips the `NNN-` index prefix, so the review artifacts are
40
+ // keyed by `deeper`, not `001-deeper`.
41
+ write(sessionDir, 'rd/requests/001-deeper.md', request('qa-handoff'));
42
+ write(sessionDir, 'rd/code-review-deeper.md', '# code review\n');
43
+ write(sessionDir, 'audit/security-deeper.md', '# security\n');
44
+ const deeper = classifyResume(SESSION_ID, peaksRoot);
45
+ write(sessionDir, 'txt/handoff.md', '# handoff\n');
46
+ const done = classifyResume(SESSION_ID, peaksRoot);
47
+ const result = combineProbes([
48
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
49
+ probe(shallow.kind === 'resume' && shallow.point === 'rd-planning', `shallow fixture resumes at rd-planning (saw kind=${shallow.kind} point=${String(shallow.point)})`),
50
+ probe(deeper.kind === 'resume' && deeper.point === 'qa-validation', `adding an RD qa-handoff request moves the point DEEPER to qa-validation (saw kind=${deeper.kind} point=${String(deeper.point)})`),
51
+ probe(done.kind === 'complete', `adding txt/handoff.md marks the workflow complete (saw kind=${done.kind} point=${String(done.point)})`)
52
+ ]);
53
+ const artifact = row.sourceFiles[0] ?? 'src/services/skill/resume-detector.ts';
54
+ if (result.ok)
55
+ return pass(ctx, artifact);
56
+ return fail(ctx, artifact, 'resume identifies the deepest completed gate (rd-planning → qa-validation → complete)', result.detail, 'J06 invariant broken: resume no longer tracks the deepest completed gate');
22
57
  }
23
- // Soft check: at least one existing file mentions "resume".
24
- const mentionsResume = existing.some((f) => {
25
- const src = readFileSync(join(ctx.projectRoot, f), 'utf8');
26
- return src.toLowerCase().includes('resume');
27
- });
28
- if (!mentionsResume) {
29
- return {
30
- journeyId: 'J06',
31
- contract: 'workflow-trace',
32
- status: 'fail',
33
- diff: { before: 'resume service files mention "resume"', after: 'none mention resume', reason: 'J06#1 broken: resume service files do not reference the resume concept' },
34
- artifactPath: existing[0]
35
- };
58
+ finally {
59
+ rmSync(root, { recursive: true, force: true });
36
60
  }
37
- return {
38
- journeyId: 'J06',
39
- contract: 'workflow-trace',
40
- status: 'pass',
41
- artifactPath: existing[0]
42
- };
43
61
  }
@@ -1,2 +1,11 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe of the (mtime, sha256) gate.
4
+ *
5
+ * A cache entry is written for a passing test, then the file's CONTENT is
6
+ * changed while its mtime is restored to the original value. If the gate ever
7
+ * degraded to an mtime-only (or filename-only) check, the cache would report a
8
+ * hit for a file whose bytes changed — exactly the fake-green the invariant
9
+ * forbids. The mtime-only and status gates are probed the same way.
10
+ */
2
11
  export declare function runJ07Contract(ctx: GuardContext): Promise<GuardRunResult>;
@@ -1,54 +1,83 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
- // v2: existence-of-CLI + source-contains-vitest, not actual invocation.
5
- // v1 failed with `spawn vitest ENOENT` because the test runner spawns a child vitest
6
- // that needs `pnpm`/PATH configured; the test fixture doesn't have that. A dry-run
7
- // probe is enough to prove the `peaks test` subcommand is registered and the runner
8
- // is wired to vitest, without actually executing a vitest child process.
9
- const TEST_COMMAND_FILES = [
10
- 'src/cli/commands/test-commands.ts',
11
- 'src/services/test/',
12
- 'src/services/test-runner/'
13
- ];
14
- export async function runJ07Contract(ctx) {
15
- // 1. CLI probe: `peaks test --help` must produce non-empty output.
16
- let helpOk = false;
4
+ import { TEST_CACHE_DIR, isCacheable, mtimeOfFile, readTestCache, recordTestResult, sha256OfFile, testCacheDir, writeTestCache } from '../../test-cache/test-cache-service.js';
5
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
6
+ const TEST_NAME = 'fingerprint probe';
7
+ /** Highest vitest major the frozen baseline allows ("MUST NOT be locked at 5.x"). */
8
+ const MAX_VITEST_MAJOR = 4;
9
+ function vitestMajor(projectRoot) {
17
10
  try {
18
- const bin = join(ctx.projectRoot, 'bin', 'peaks.js');
19
- const out = execFileSync('node', [bin, 'test', '--help'], {
20
- cwd: ctx.projectRoot,
21
- env: { ...process.env, PEAKS_CALLER_ID: `guard-J07-${ctx.sessionId}` },
22
- stdio: ['ignore', 'pipe', 'pipe'],
23
- windowsHide: true
24
- }).toString('utf8');
25
- helpOk = out.length > 0 && !out.includes('unknown command');
11
+ const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
12
+ const raw = pkg.devDependencies?.['vitest'] ?? pkg.dependencies?.['vitest'];
13
+ if (typeof raw !== 'string')
14
+ return null;
15
+ const major = /(\d+)/.exec(raw);
16
+ return major === null ? null : Number(major[1]);
26
17
  }
27
18
  catch {
28
- helpOk = false;
19
+ return null;
20
+ }
21
+ }
22
+ /**
23
+ * Behavioural probe of the (mtime, sha256) gate.
24
+ *
25
+ * A cache entry is written for a passing test, then the file's CONTENT is
26
+ * changed while its mtime is restored to the original value. If the gate ever
27
+ * degraded to an mtime-only (or filename-only) check, the cache would report a
28
+ * hit for a file whose bytes changed — exactly the fake-green the invariant
29
+ * forbids. The mtime-only and status gates are probed the same way.
30
+ */
31
+ export async function runJ07Contract(ctx) {
32
+ const row = requireBaselineRow(ctx);
33
+ const missing = missingSourceFiles(ctx, row);
34
+ const root = mkdtempSync(join(tmpdir(), 'cbl-J07-'));
35
+ const file = join(root, 'probe.test.ts');
36
+ try {
37
+ writeFileSync(file, 'export const a = 1;\n');
38
+ recordTestResult(root, file, 'vitest', {
39
+ testName: TEST_NAME,
40
+ status: 'passed',
41
+ durationMs: 1,
42
+ lastRun: new Date().toISOString()
43
+ });
44
+ const baseline = isCacheable(root, file, TEST_NAME);
45
+ const cached = readTestCache(root, file);
46
+ // Gate 1, isolated: the file's bytes change but the recorded mtime is
47
+ // up-to-date, so ONLY the sha check can catch it. If the gate degraded to
48
+ // an mtime-only check this reports a hit for a file nobody verified.
49
+ writeFileSync(file, 'export const a = 2;\n');
50
+ writeTestCache(root, { ...cached, fileMtime: mtimeOfFile(file) });
51
+ const afterSilentEdit = isCacheable(root, file, TEST_NAME);
52
+ // Gate 2, isolated: the sha is up-to-date but the recorded mtime is stale.
53
+ writeTestCache(root, { ...cached, fileSha256: sha256OfFile(file), fileMtime: cached.fileMtime - 5_000 });
54
+ const afterTouch = isCacheable(root, file, TEST_NAME);
55
+ // A non-passing status must never be served as a hit.
56
+ recordTestResult(root, file, 'vitest', {
57
+ testName: 'skipped probe',
58
+ status: 'skipped',
59
+ durationMs: 0,
60
+ lastRun: new Date().toISOString()
61
+ });
62
+ const skipped = isCacheable(root, file, 'skipped probe');
63
+ const unknown = isCacheable(root, file, 'never recorded');
64
+ const major = vitestMajor(ctx.projectRoot);
65
+ const result = combineProbes([
66
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
67
+ probe(baseline.hit, `an unchanged passing test is a cache hit (reason=${String(baseline.reason)})`),
68
+ probe(!afterSilentEdit.hit && afterSilentEdit.reason === 'sha-changed', `a same-mtime content edit is NOT a hit (hit=${String(afterSilentEdit.hit)} reason=${String(afterSilentEdit.reason)})`),
69
+ probe(!afterTouch.hit && afterTouch.reason === 'mtime-changed', `a touched file is NOT a hit (hit=${String(afterTouch.hit)} reason=${String(afterTouch.reason)})`),
70
+ probe(!skipped.hit && skipped.reason === 'previous-skipped', `a previously skipped test is NOT a hit (hit=${String(skipped.hit)} reason=${String(skipped.reason)})`),
71
+ probe(!unknown.hit && unknown.reason === 'no-cache', `an unrecorded test is NOT a hit (reason=${String(unknown.reason)})`),
72
+ probe(testCacheDir(ctx.projectRoot).replace(/\\/g, '/').endsWith(`.peaks/_runtime/${TEST_CACHE_DIR}`), `the cache writes under .peaks/_runtime/${TEST_CACHE_DIR} (got ${testCacheDir(ctx.projectRoot)})`),
73
+ probe(major !== null && major <= MAX_VITEST_MAJOR, `vitest major is <= ${String(MAX_VITEST_MAJOR)} (saw ${String(major)})`)
74
+ ]);
75
+ const artifact = row.sourceFiles[1] ?? 'src/services/test-cache/test-cache-service.ts';
76
+ if (result.ok)
77
+ return pass(ctx, artifact);
78
+ return fail(ctx, artifact, 'the per-test fingerprint cache only serves a hit for a file whose mtime AND sha256 are unchanged and whose last status passed', result.detail, 'J07 invariant broken: the fingerprint cache can return passed for an unverified file');
29
79
  }
30
- // 2. Source probe: at least one test command file references vitest.
31
- const sourceMentionsVitest = TEST_COMMAND_FILES.some((f) => {
32
- const abs = join(ctx.projectRoot, f);
33
- if (!existsSync(abs))
34
- return false;
35
- if (existsSync(abs) && readFileSync(abs, 'utf8').includes('vitest'))
36
- return true;
37
- return false;
38
- });
39
- // 3. package.json probe: vitest is in dependencies.
40
- const pkgPath = join(ctx.projectRoot, 'package.json');
41
- let pkgHasVitest = false;
42
- if (existsSync(pkgPath)) {
43
- const pkg = readFileSync(pkgPath, 'utf8');
44
- pkgHasVitest = /"vitest"\s*:/i.test(pkg);
80
+ finally {
81
+ rmSync(root, { recursive: true, force: true });
45
82
  }
46
- const ok = helpOk && sourceMentionsVitest && pkgHasVitest;
47
- return {
48
- journeyId: 'J07',
49
- contract: 'cli-output-golden',
50
- status: ok ? 'pass' : 'fail',
51
- ...(ok ? {} : { diff: { before: 'peaks test --help works, test commands reference vitest, package.json has vitest', after: `helpOk=${helpOk} sourceMentionsVitest=${sourceMentionsVitest} pkgHasVitest=${pkgHasVitest}`, reason: 'J07#1 broken: peaks test does not delegate to vitest' } }),
52
- artifactPath: 'src/cli/commands/test-commands.ts'
53
- };
54
83
  }
@@ -1,2 +1,13 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe of the crystallization gate.
4
+ *
5
+ * Before this the contract only asserted that one of three candidate files
6
+ * exists and mentions "crystalliz"/"sediment"/"promot" — the file it found is
7
+ * named `crystallization-service.ts`, so the check restated its own filename.
8
+ *
9
+ * Here the real gate is driven with a completed and an incomplete task state:
10
+ * a task whose `gates_passed` is false must be refused at the Zod boundary AND
11
+ * again at the service boundary.
12
+ */
2
13
  export declare function runJ08Contract(ctx: GuardContext): Promise<GuardRunResult>;