peaks-loop 4.0.49 → 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 (119) hide show
  1. package/CHANGELOG.md +24 -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/core/skill-command.js +53 -4
  7. package/dist/cli/commands/core/standards-command.d.ts +24 -0
  8. package/dist/cli/commands/core/standards-command.js +74 -0
  9. package/dist/cli/commands/hooks-commands.js +55 -38
  10. package/dist/cli/commands/share-commands.js +37 -11
  11. package/dist/cli/commands/web-commands.js +8 -1
  12. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
  13. package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
  14. package/dist/services/adapter/adapter.d.ts +30 -0
  15. package/dist/services/adapter/auto-adapter.d.ts +13 -0
  16. package/dist/services/adapter/claude-adapter.js +12 -0
  17. package/dist/services/adapter/codex-adapter.d.ts +12 -0
  18. package/dist/services/adapter/codex-adapter.js +12 -0
  19. package/dist/services/adapter/copilot-adapter.d.ts +12 -0
  20. package/dist/services/adapter/copilot-adapter.js +12 -0
  21. package/dist/services/audit/backing-detector.d.ts +25 -7
  22. package/dist/services/audit/backing-detector.js +33 -17
  23. package/dist/services/audit/enforcer-liveness.d.ts +12 -0
  24. package/dist/services/audit/enforcer-liveness.js +100 -0
  25. package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
  26. package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
  27. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
  28. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
  29. package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
  30. package/dist/services/audit/enforcers/lint-style.js +38 -2
  31. package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
  32. package/dist/services/audit/prose-ratio-calculator.js +25 -18
  33. package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
  34. package/dist/services/audit/red-lines-service.js +51 -7
  35. package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
  36. package/dist/services/capability-audit-service/independent-checker.js +140 -0
  37. package/dist/services/capability-audit-service/index.d.ts +3 -1
  38. package/dist/services/capability-audit-service/index.js +1 -0
  39. package/dist/services/capability-audit-service/runner.d.ts +17 -13
  40. package/dist/services/capability-audit-service/runner.js +76 -15
  41. package/dist/services/capability-audit-service/types.d.ts +48 -0
  42. package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
  43. package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
  44. package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
  45. package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
  46. package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
  47. package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
  48. package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
  49. package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
  50. package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
  51. package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
  52. package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
  53. package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
  54. package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
  55. package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
  56. package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
  57. package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
  58. package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
  59. package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
  60. package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
  61. package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
  62. package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
  63. package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
  64. package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
  65. package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
  66. package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
  67. package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
  68. package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
  69. package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
  70. package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
  71. package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
  72. package/dist/services/capability-guard-runner/registry.d.ts +5 -0
  73. package/dist/services/capability-guard-runner/registry.js +140 -0
  74. package/dist/services/capability-guard-runner/runner.d.ts +26 -0
  75. package/dist/services/capability-guard-runner/runner.js +63 -6
  76. package/dist/services/code/auto-compact-modes.d.ts +13 -2
  77. package/dist/services/code/auto-compact-modes.js +20 -4
  78. package/dist/services/code/post-compact-detector.js +20 -11
  79. package/dist/services/code/step-08-gate.js +21 -6
  80. package/dist/services/config/config-safety.js +11 -9
  81. package/dist/services/final-review/pre-post-diff.js +10 -2
  82. package/dist/services/observability/observability-service.d.ts +1 -1
  83. package/dist/services/scan/api-diff-types.js +20 -2
  84. package/dist/services/security/safe-settings-path.js +19 -1
  85. package/dist/services/skill/skill-search-service.d.ts +3 -3
  86. package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
  87. package/dist/services/standards/loop-engineering-lint.js +6 -0
  88. package/dist/services/web/daemon-registry.js +27 -2
  89. package/dist/services/workspace/claude-settings-template.d.ts +53 -37
  90. package/dist/services/workspace/claude-settings-template.js +105 -83
  91. package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
  92. package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
  93. package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
  94. package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
  95. package/dist/services/workspace/workspace-service.js +11 -1
  96. package/dist/shared/fs-utils.d.ts +26 -0
  97. package/dist/shared/fs-utils.js +35 -0
  98. package/package.json +9 -7
  99. package/scripts/copy-templates.mjs +0 -12
  100. package/scripts/install-skills.mjs +154 -53
  101. package/skills/bee/peaks-qa/SKILL.md +0 -1
  102. package/skills/bee/peaks-rd/SKILL.md +0 -1
  103. package/skills/peaks-code/SKILL.md +12 -10
  104. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  105. package/skills/peaks-code/references/runbook.md +3 -0
  106. package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
  107. package/skills/peaks-code/references/startup-sequence.md +2 -2
  108. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  109. package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
  110. package/dist/cli/commands/context-builder-commands.d.ts +0 -11
  111. package/dist/cli/commands/context-builder-commands.js +0 -85
  112. package/dist/services/hooks/write-gate.js +0 -111
  113. package/skills/bee/peaks-prd/references/command-migration.md +0 -3
  114. package/skills/bee/peaks-qa/references/command-migration.md +0 -3
  115. package/skills/bee/peaks-rd/references/command-migration.md +0 -3
  116. package/skills/bee/peaks-sc/references/command-migration.md +0 -3
  117. package/skills/bee/peaks-txt/references/command-migration.md +0 -3
  118. package/skills/bee/peaks-ui/references/command-migration.md +0 -3
  119. package/skills/peaks-code/references/command-migration.md +0 -3
@@ -1,34 +1,120 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { mkdtempSync } from 'node:fs';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, resolve } from 'node:path';
5
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
5
6
  const STATES = ['spec-locked', 'implemented', 'qa-handoff', 'handed-off'];
6
- export async function runJ02Contract(ctx, projectRoot = ctx.projectRoot) {
7
- const bin = join(ctx.projectRoot, 'bin', 'peaks.js');
7
+ // Per-child timeout. J02 runs six peaks CLIs in a fresh cwd; on cold CI
8
+ // runners the first invocation pays tsx startup before the resolved file is
9
+ // hot, and any single child that hangs would block the whole guard run until
10
+ // the default node timeout (forever). 60s is comfortable on warm hosts and
11
+ // tight enough that a genuine hang surfaces in the gate step within the
12
+ // publish workflow's per-step budget.
13
+ const CHILD_TIMEOUT_MS = 60_000;
14
+ export async function runJ02Contract(ctx) {
15
+ const row = requireBaselineRow(ctx);
16
+ const missing = missingSourceFiles(ctx, row);
17
+ // Must be ABSOLUTE: the child runs with `cwd: tmp`, so a relative
18
+ // `bin/peaks.js` would be resolved against the temp workspace.
19
+ const bin = resolve(ctx.projectRoot, 'bin', 'peaks.js');
8
20
  const tmp = mkdtempSync(join(tmpdir(), 'cbl-J02-'));
9
- const ws = execFileSync('node', [bin, 'workspace', 'init', '--project', tmp, '--json'], { cwd: tmp, windowsHide: true }).toString('utf8');
10
- const { data: { sessionId } } = JSON.parse(ws);
11
- const rid = '2026-08-03-j02-fixture';
12
- const initOut = execFileSync('node', [bin, 'request', 'init', '--role', 'rd', '--id', rid, '--project', tmp, '--session-id', sessionId, '--apply', '--json'], { cwd: tmp, windowsHide: true }).toString('utf8');
13
- const initEnv = JSON.parse(initOut);
14
- // `request init` writes the file as `NNN-<id-slug>.md`. The transition CLI accepts
15
- // the file's basename (without .md) as the requestId. Derive it from data.path.
16
- const baseName = initEnv.data.path.split(/[\\/]/).pop() ?? '';
17
- const requestId = baseName.replace(/\.md$/i, '');
18
- let last = '';
19
- for (const s of STATES) {
20
- const out = execFileSync('node', [bin, 'request', 'transition', requestId, '--role', 'rd', '--state', s,
21
- '--project', tmp, '--session-id', sessionId, '--confirm', '--allow-incomplete',
22
- '--reason', 'J02 contract fixture', '--json'], { cwd: tmp, windowsHide: true }).toString('utf8');
23
- const env = JSON.parse(out);
24
- last = env.data.state;
25
- }
26
- const ok = last === 'handed-off';
27
- return {
28
- journeyId: 'J02',
29
- contract: 'workflow-trace',
30
- status: ok ? 'pass' : 'fail',
31
- ...(ok ? {} : { diff: { before: 'handed-off', after: last, reason: 'J02#1 broken: RD state machine no longer reaches handed-off' } }),
32
- artifactPath: 'tests/integration/business-capability-e2e.test.ts'
21
+ // Each peaks-CLI invocation in this contract is wrapped so a child that
22
+ // hangs (or a child that produces no stdout) cannot silently pin the gate
23
+ // step until CI's job-level timeout fires. `execFileSync` already throws on
24
+ // non-zero exit; we additionally enforce timeout and surface the actual
25
+ // stderr in the error so a future failure can be diagnosed without re-running
26
+ // with a debugger.
27
+ const run = (args) => {
28
+ try {
29
+ const stdout = execFileSync('node', [bin, ...args], {
30
+ cwd: tmp,
31
+ windowsHide: true,
32
+ timeout: CHILD_TIMEOUT_MS,
33
+ encoding: 'utf8',
34
+ // The contract spawns peaks CLIs in a temp workspace; on a CI runner
35
+ // there is no IDE context, so peaks would refuse every `request.*`
36
+ // command with CALLER_ID_INVALID. Provide a deterministic caller id
37
+ // tied to the contract name. The contract is the only producer of
38
+ // this artifact, so the synthetic id cannot collide with anything
39
+ // real a developer is working on.
40
+ env: { ...process.env, PEAKS_CALLER_ID: `guard-J02-${ctx.sessionId}` }
41
+ });
42
+ return { stdout, stderr: '' };
43
+ }
44
+ catch (e) {
45
+ const err = e;
46
+ const stderr = typeof err.stderr === 'string'
47
+ ? err.stderr
48
+ : Buffer.isBuffer(err.stderr) ? err.stderr.toString('utf8') : '';
49
+ const stdout = typeof err.stdout === 'string'
50
+ ? err.stdout
51
+ : Buffer.isBuffer(err.stdout) ? err.stdout.toString('utf8') : '';
52
+ // Preserve the original error type/message but attach stderr so the
53
+ // outer try-catch's `e.message.slice(0, 160)` sees something useful.
54
+ // Truncate stdout aggressively to keep the audit envelope bounded, but
55
+ // pick the HEAD and TAIL of the buffer so the leading envelope header
56
+ // and the trailing error are both visible.
57
+ const stdoutHead = stdout.slice(0, 600);
58
+ const stdoutTail = stdout.length > 1200 ? stdout.slice(-400) : '';
59
+ const stdoutPart = stdoutTail
60
+ ? `${stdoutHead}...<truncated ${stdout.length - 1000}B>...${stdoutTail}`
61
+ : stdoutHead;
62
+ const wrapped = new Error(`${err.message} | stderr=${stderr.slice(0, 400)} | stdout=${stdoutPart}`);
63
+ wrapped.stdout = stdout;
64
+ throw wrapped;
65
+ }
33
66
  };
67
+ try {
68
+ const ws = run(['workspace', 'init', '--project', tmp, '--json']);
69
+ const { data: { sessionId } } = JSON.parse(ws.stdout);
70
+ const rid = '2026-08-03-j02-fixture';
71
+ const initOut = run(['request', 'init', '--role', 'rd', '--id', rid, '--project', tmp, '--session-id', sessionId, '--apply', '--json']);
72
+ const initEnv = JSON.parse(initOut.stdout);
73
+ // `request init` writes the file as `NNN-<id-slug>.md`. The transition CLI accepts
74
+ // the file's basename (without .md) as the requestId. Derive it from data.path.
75
+ const baseName = initEnv.data.path.split(/[\\/]/).pop() ?? '';
76
+ const requestId = baseName.replace(/\.md$/i, '');
77
+ const transition = (state, extra) => run([
78
+ 'request', 'transition', requestId, '--role', 'rd', '--state', state,
79
+ '--project', tmp, '--session-id', sessionId, '--confirm',
80
+ '--reason', 'J02 contract fixture', ...extra, '--json'
81
+ ]).stdout;
82
+ // Hard-gate probe, run FIRST: from the freshly initialised state, jumping
83
+ // straight to the terminal state skips every intermediate gate and must not
84
+ // be accepted without the explicit incomplete-work escape hatch. Doing this
85
+ // before the legal walk is what makes it a skip (from `qa-handoff` the move
86
+ // to `handed-off` is legal and proves nothing).
87
+ let gateSkipRefused = false;
88
+ let skipError = 'not attempted';
89
+ try {
90
+ transition('handed-off', []);
91
+ skipError = 'transition was accepted';
92
+ }
93
+ catch (e) {
94
+ gateSkipRefused = true;
95
+ skipError = e.message.slice(0, 160);
96
+ }
97
+ let last = '';
98
+ try {
99
+ for (const s of STATES) {
100
+ const env = JSON.parse(transition(s, ['--allow-incomplete']));
101
+ last = env.data.state;
102
+ }
103
+ }
104
+ catch (e) {
105
+ last = `error: ${e.message.slice(0, 160)}`;
106
+ }
107
+ const result = combineProbes([
108
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
109
+ probe(gateSkipRefused, `an incomplete jump to handed-off is refused (${skipError})`),
110
+ probe(last === 'handed-off', `the RD state machine reaches handed-off (saw ${last})`)
111
+ ]);
112
+ const artifact = row.sourceFiles[3] ?? 'tests/integration/job-e2e.test.ts';
113
+ if (result.ok)
114
+ return pass(ctx, artifact);
115
+ return fail(ctx, artifact, 'every hard gate is enforced and the RD state machine still reaches handed-off', result.detail, 'J02 invariant broken: the RD state machine or its hard-gate enforcement changed');
116
+ }
117
+ finally {
118
+ rmSync(tmp, { recursive: true, force: true });
119
+ }
34
120
  }
@@ -1,2 +1,15 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe of the "no silent-catch / fake-green reintroduced"
4
+ * invariant.
5
+ *
6
+ * The previous version read `final-review-types.ts` and asserted it contained
7
+ * four dimension strings — a file whose own name is `final-review`, so the
8
+ * check restated its filename.
9
+ *
10
+ * Here the repository's own AST guard (`scripts/lint/silent-warning-detector.mjs`)
11
+ * is executed and its per-rule violation counts are compared against the frozen
12
+ * ceilings. A newly swallowed `catch` in `src/**` raises a count and reddens the
13
+ * journey.
14
+ */
2
15
  export declare function runJ03Contract(ctx: GuardContext): Promise<GuardRunResult>;
@@ -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>;