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,34 +1,70 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
2
3
  import { join } from 'node:path';
3
- const CANDIDATE_FILES = [
4
- 'src/services/worktree/lease-store.ts',
5
- 'src/services/worktree/worktree-lease.ts',
6
- 'src/services/dispatch/sub-agent-dispatcher.ts',
7
- 'src/services/lease-metrics/lease-metrics-service.ts'
8
- ];
9
- const REQUIRED_FRAGMENTS = ['lease', 'release'];
4
+ import { deserializeLease, finalizeLease, isLeaseActive, isLeaseGcEligible, leaseFilePath, leaseStoreDir, listLeasesSync, markReleased, recordConsumption, renewLease, serializeLease, ttlForRole } from '../../worktree/worktree-lease.js';
5
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
6
+ const NOW = 1_700_000_000_000;
7
+ const DRAFT = {
8
+ leaseId: '0123456789abcdef',
9
+ rid: '2026-09-15-s1-gate-contracts-honest',
10
+ role: 'rd',
11
+ path: 'C:/tmp/worktrees/0123456789abcdef',
12
+ branch: 'peaks/guard-J12',
13
+ createdAt: NOW - 1000,
14
+ expiresAt: NOW + 60_000,
15
+ purpose: 'J12 guard probe'
16
+ };
17
+ /**
18
+ * Behavioural probe of the lease lifecycle.
19
+ *
20
+ * The previous version passed if one of four worktree/dispatch files mentioned
21
+ * both "lease" and "release" — `worktree-lease.ts` satisfies that from its own
22
+ * filename.
23
+ *
24
+ * Here the real pure module is driven: a fresh lease must be active, releasing
25
+ * twice must be a no-op, a duplicate consumption must not append, an in-flight
26
+ * lease must never be gc-eligible, and a persisted lease must survive a
27
+ * serialize/deserialize round trip.
28
+ */
10
29
  export async function runJ12Contract(ctx) {
11
- const existing = CANDIDATE_FILES.filter((f) => existsSync(join(ctx.projectRoot, f)));
12
- if (existing.length === 0) {
13
- return {
14
- journeyId: 'J12',
15
- contract: 'concurrency-lease',
16
- status: 'fail',
17
- diff: { before: 'a worktree/lease/dispatch service exists', after: 'no candidate found', reason: 'J12#1 broken: no lease infrastructure is present' },
18
- artifactPath: 'src/services/worktree/'
19
- };
30
+ const row = requireBaselineRow(ctx);
31
+ const missing = missingSourceFiles(ctx, row);
32
+ const lease = finalizeLease(DRAFT);
33
+ const onceReleased = markReleased(lease);
34
+ const twiceReleased = markReleased(onceReleased);
35
+ const consumed = recordConsumption(lease, 'batch-1');
36
+ const consumedTwice = recordConsumption(consumed, 'batch-1');
37
+ const renewed = renewLease(onceReleased, NOW + 120_000);
38
+ const root = mkdtempSync(join(tmpdir(), 'cbl-J12-'));
39
+ try {
40
+ const storeDir = leaseStoreDir(root);
41
+ const missingStore = listLeasesSync(storeDir, { readdir: readdirSync, readFile: (p) => readFileSync(p, 'utf8'), existsSync });
42
+ mkdirSync(storeDir, { recursive: true });
43
+ writeFileSync(leaseFilePath(root, lease.leaseId), serializeLease(lease));
44
+ const listed = listLeasesSync(storeDir, { readdir: readdirSync, readFile: (p) => readFileSync(p, 'utf8'), existsSync });
45
+ const result = combineProbes([
46
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
47
+ probe(lease.status === 'active' && lease.consumedBySubAgents.length === 0, 'a fresh lease is active with no consumers'),
48
+ probe(leaseStoreDir(root).replace(/\\/g, '/').endsWith('/worktree-leases'), `leases live under the session worktree-leases dir (${leaseStoreDir(root)})`),
49
+ probe(missingStore.kind === 'store-missing', 'an absent lease store is reported, not invented'),
50
+ probe(listed.kind === 'ok' && listed.leases.length === 1, `a persisted lease is listed back (${listed.kind === 'ok' ? String(listed.leases.length) : listed.kind})`),
51
+ probe(JSON.stringify(deserializeLease(serializeLease(lease))) === JSON.stringify(lease), 'serialize/deserialize round-trips the lease'),
52
+ probe(onceReleased.status === 'released', 'release moves the lease to released'),
53
+ probe(JSON.stringify(twiceReleased) === JSON.stringify(onceReleased), 'a duplicate release is a no-op'),
54
+ probe(JSON.stringify(consumedTwice) === JSON.stringify(consumed) && consumed.consumedBySubAgents.length === 1, 'a duplicate consumption is not appended twice'),
55
+ probe(renewed.status === 'active' && renewed.expiresAt === NOW + 120_000, 'renew extends expiry and returns the lease to active'),
56
+ probe(isLeaseActive(lease, NOW), 'an unexpired active lease is in flight'),
57
+ probe(!isLeaseGcEligible(lease, NOW), 'gc is NOT eligible for an in-flight lease'),
58
+ probe(isLeaseGcEligible(onceReleased, NOW), 'a released lease is gc-eligible'),
59
+ probe(isLeaseGcEligible(finalizeLease({ ...DRAFT, expiresAt: NOW - 1 }), NOW), 'an expired lease is gc-eligible'),
60
+ probe(ttlForRole('rd') > 0 && ttlForRole('no-such-role') === ttlForRole('rd'), 'unknown roles fall back to the rd TTL')
61
+ ]);
62
+ const artifact = row.sourceFiles[0] ?? 'src/services/worktree/worktree-lease.ts';
63
+ if (result.ok)
64
+ return pass(ctx, artifact);
65
+ return fail(ctx, artifact, 'the lease lifecycle is complete: release is idempotent and gc never touches an in-flight lease', result.detail, 'J12 invariant broken: the lease release lifecycle or its gc guard no longer holds');
66
+ }
67
+ finally {
68
+ rmSync(root, { recursive: true, force: true });
20
69
  }
21
- // Soft: at least one file references BOTH 'lease' AND 'release' in its content.
22
- const fullCoverage = existing.some((f) => {
23
- const src = readFileSync(join(ctx.projectRoot, f), 'utf8').toLowerCase();
24
- return REQUIRED_FRAGMENTS.every((frag) => src.includes(frag));
25
- });
26
- const ok = fullCoverage;
27
- return {
28
- journeyId: 'J12',
29
- contract: 'concurrency-lease',
30
- status: ok ? 'pass' : 'fail',
31
- ...(ok ? {} : { diff: { before: 'a worktree/lease service references both lease and release', after: 'no such file', reason: 'J12#1 broken: lease lifecycle (lease + release) is incomplete' } }),
32
- artifactPath: existing[0]
33
- };
34
70
  }
@@ -1,2 +1,13 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe of the content-pipeline boundary.
4
+ *
5
+ * The previous version read the skill plus three unrelated files and passed if
6
+ * three of the five words draft/edit/tone/publish/archive appeared anywhere —
7
+ * and it counted "tone" (a GATE in the skill) as a STAGE.
8
+ *
9
+ * Here the skill's declared stages are anchored on the bees that implement
10
+ * them, and every file under the skill directory is scanned for a specifier
11
+ * that would reach into peaks-code internals.
12
+ */
2
13
  export declare function runJ13Contract(ctx: GuardContext): Promise<GuardRunResult>;
@@ -1,45 +1,67 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- const CANDIDATE_FILES = [
4
- 'skills/peaks-content/SKILL.md',
5
- 'src/services/content/',
6
- 'src/services/workflow/workflow-router-service.ts',
7
- '.peaks/standards/loop-engineering-guidelines.md'
8
- ];
9
- const STAGES = ['draft', 'edit', 'tone', 'publish', 'archive'];
10
- export async function runJ13Contract(ctx) {
11
- const existing = CANDIDATE_FILES.filter((f) => existsSync(join(ctx.projectRoot, f)));
12
- if (existing.length === 0) {
13
- return {
14
- journeyId: 'J13',
15
- contract: 'workflow-trace',
16
- status: 'fail',
17
- diff: { before: 'a content / peaks-content skill exists', after: 'no candidate found', reason: 'J13#1 broken: no content production surface is present' },
18
- artifactPath: 'skills/peaks-content/SKILL.md'
19
- };
3
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
4
+ const SKILL_DIR = ['skills', 'peaks-content'];
5
+ const SKILL_MD = [...SKILL_DIR, 'SKILL.md'];
6
+ /**
7
+ * The content stages the skill must still declare, each anchored on the bee
8
+ * that implements it. "draft" appearing anywhere in the file is not evidence;
9
+ * `bee-content-draft` disappearing means the stage is gone.
10
+ */
11
+ const STAGES = ['draft', 'edit', 'publish', 'archive'];
12
+ /** Specifiers that would mean the skill reached into peaks-code domain logic. */
13
+ const FORBIDDEN_SPECIFIER = /(^|[./\\])peaks-code([/\\]|$)|(^|\/)src\/(services|cli)\//;
14
+ function walk(dir, out = []) {
15
+ if (!existsSync(dir))
16
+ return out;
17
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
18
+ const abs = join(dir, entry.name);
19
+ if (entry.isDirectory())
20
+ walk(abs, out);
21
+ else
22
+ out.push(abs);
20
23
  }
21
- // Soft check: at least 3 of 5 stages appear in any existing file's content.
22
- const matchedStages = new Set();
23
- for (const file of existing) {
24
- const src = readFileSync(join(ctx.projectRoot, file), 'utf8').toLowerCase();
25
- for (const stage of STAGES) {
26
- if (src.includes(stage))
27
- matchedStages.add(stage);
24
+ return out;
25
+ }
26
+ /**
27
+ * Behavioural probe of the content-pipeline boundary.
28
+ *
29
+ * The previous version read the skill plus three unrelated files and passed if
30
+ * three of the five words draft/edit/tone/publish/archive appeared anywhere —
31
+ * and it counted "tone" (a GATE in the skill) as a STAGE.
32
+ *
33
+ * Here the skill's declared stages are anchored on the bees that implement
34
+ * them, and every file under the skill directory is scanned for a specifier
35
+ * that would reach into peaks-code internals.
36
+ */
37
+ export async function runJ13Contract(ctx) {
38
+ const row = requireBaselineRow(ctx);
39
+ const missing = missingSourceFiles(ctx, row);
40
+ const skillAbs = join(ctx.projectRoot, ...SKILL_MD);
41
+ const body = existsSync(skillAbs) ? readFileSync(skillAbs, 'utf8') : '';
42
+ const absentStages = STAGES.filter((stage) => !body.includes(`bee-content-${stage}`));
43
+ const toneGatePresent = body.includes('Tone gate');
44
+ const files = walk(join(ctx.projectRoot, ...SKILL_DIR));
45
+ const offenders = [];
46
+ for (const file of files) {
47
+ const text = readFileSync(file, 'utf8');
48
+ for (const line of text.split('\n')) {
49
+ const spec = /(?:from|import|require\()\s*['"]([^'"]+)['"]/.exec(line);
50
+ if (spec !== null && FORBIDDEN_SPECIFIER.test(spec[1])) {
51
+ offenders.push(`${file.replace(/\\/g, '/')} → ${spec[1]}`);
52
+ }
28
53
  }
29
54
  }
30
- if (matchedStages.size < 3) {
31
- return {
32
- journeyId: 'J13',
33
- contract: 'workflow-trace',
34
- status: 'fail',
35
- diff: { before: 'at least 3 of draft/edit/tone/publish/archive stages are referenced', after: `only ${[...matchedStages].join(',')} found`, reason: 'J13#1 broken: content pipeline has too few stages' },
36
- artifactPath: existing[0]
37
- };
38
- }
39
- return {
40
- journeyId: 'J13',
41
- contract: 'workflow-trace',
42
- status: 'pass',
43
- artifactPath: existing[0]
44
- };
55
+ const result = combineProbes([
56
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
57
+ probe(body.length > 0, 'the peaks-content skill file is readable'),
58
+ probe(absentStages.length === 0, `every content stage is anchored on its bee (missing: ${absentStages.join(',') || 'none'})`),
59
+ probe(toneGatePresent, 'the tone gate is still declared between draft and edit'),
60
+ probe(files.length > 0, `the skill directory is non-empty (${String(files.length)} files)`),
61
+ probe(offenders.length === 0, `no skill file imports peaks-code internals (${offenders.join('; ') || 'none'})`)
62
+ ]);
63
+ const artifact = row.sourceFiles[0] ?? 'skills/peaks-content/SKILL.md';
64
+ if (result.ok)
65
+ return pass(ctx, artifact);
66
+ return fail(ctx, artifact, 'peaks-content declares its four stages and imports no peaks-code internals', result.detail, 'J13 invariant broken: the content pipeline stages or the peaks-code boundary changed');
45
67
  }
@@ -1,2 +1,13 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe of the RL-3 AI-modified declaration rule.
4
+ *
5
+ * The previous version read the skill plus two markdown files and passed if at
6
+ * least two of the words triage/classify/fix/commit appeared — words that
7
+ * appear throughout the prose regardless of whether any rule survived.
8
+ *
9
+ * Here the rule is anchored on its heading, its literal commit-body markers,
10
+ * the forbidden-trailer list and the survey-banned globs. Deleting the rule
11
+ * section reddens the journey.
12
+ */
2
13
  export declare function runJ14Contract(ctx: GuardContext): Promise<GuardRunResult>;
@@ -1,36 +1,65 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- const CANDIDATE_FILES = [
4
- 'skills/peaks-issue-fix-orchestrator/SKILL.md',
5
- '.peaks/memory/peaks-loop-positioning-loop-engineering.md',
6
- '.peaks/standards/loop-engineering-guidelines.md'
3
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
4
+ const SKILL_MD = ['skills', 'peaks-issue-fix-orchestrator', 'SKILL.md'];
5
+ /**
6
+ * The RL-3 declaration's literal lines. Anchored on the exact marker text the
7
+ * self-check greps for — "AI-modified" appearing once in the frontmatter
8
+ * description is not evidence that the rule survived.
9
+ */
10
+ const REQUIRED_MARKERS = [
11
+ 'AI-modified: yes',
12
+ 'Repository:',
13
+ 'Modifying tool: peaks-loop / peaks-issue-fix-orchestrator',
14
+ "grep -E 'Repository:|AI-modified:'",
15
+ 'Forbidden trailers:'
7
16
  ];
8
- const REQUIRED_STAGES = ['triage', 'classify', 'fix', 'commit'];
17
+ /** Every trailer the repo's own red rule bans (CLAUDE.md, 2026-07-01). */
18
+ const FORBIDDEN_TRAILERS = [
19
+ 'Co-Authored-By: Claude',
20
+ 'Co-Authored-By: Anthropic'
21
+ ];
22
+ /** Survey-banned target globs that must still be refused before any commit. */
23
+ const BANNED_TARGETS = [
24
+ 'tools/mcp_oauth.py',
25
+ 'agent/auth/*',
26
+ 'agent/conversation_loop.py',
27
+ 'agent/tool_guardrails.py',
28
+ '*/prompt_cache*',
29
+ '*/token_cache*'
30
+ ];
31
+ /**
32
+ * Behavioural probe of the RL-3 AI-modified declaration rule.
33
+ *
34
+ * The previous version read the skill plus two markdown files and passed if at
35
+ * least two of the words triage/classify/fix/commit appeared — words that
36
+ * appear throughout the prose regardless of whether any rule survived.
37
+ *
38
+ * Here the rule is anchored on its heading, its literal commit-body markers,
39
+ * the forbidden-trailer list and the survey-banned globs. Deleting the rule
40
+ * section reddens the journey.
41
+ */
9
42
  export async function runJ14Contract(ctx) {
10
- const existing = CANDIDATE_FILES.filter((f) => existsSync(join(ctx.projectRoot, f)));
11
- if (existing.length === 0) {
12
- return {
13
- journeyId: 'J14',
14
- contract: 'workflow-trace',
15
- status: 'fail',
16
- diff: { before: 'a peaks-issue-fix-orchestrator skill or memory file exists', after: 'no candidate found', reason: 'J14#1 broken: no issue-sweep surface is present' },
17
- artifactPath: 'skills/peaks-issue-fix-orchestrator/SKILL.md'
18
- };
19
- }
20
- const matched = new Set();
21
- for (const file of existing) {
22
- const src = readFileSync(join(ctx.projectRoot, file), 'utf8').toLowerCase();
23
- for (const stage of REQUIRED_STAGES) {
24
- if (src.includes(stage))
25
- matched.add(stage);
26
- }
27
- }
28
- const ok = matched.size >= 2;
29
- return {
30
- journeyId: 'J14',
31
- contract: 'workflow-trace',
32
- status: ok ? 'pass' : 'fail',
33
- ...(ok ? {} : { diff: { before: 'at least 2 of triage/classify/fix/commit are referenced', after: `only ${[...matched].join(',')} found`, reason: 'J14#1 broken: issue-sweep pipeline has too few stages' } }),
34
- artifactPath: existing[0]
35
- };
43
+ const row = requireBaselineRow(ctx);
44
+ const missing = missingSourceFiles(ctx, row);
45
+ const abs = join(ctx.projectRoot, ...SKILL_MD);
46
+ const body = existsSync(abs) ? readFileSync(abs, 'utf8') : '';
47
+ // The heading carries an em dash, so match the two stable halves instead of
48
+ // the literal punctuation.
49
+ const headingPresent = /###\s+RL-3\b[^\n]*AI-modified declaration/.test(body);
50
+ const missingMarkers = REQUIRED_MARKERS.filter((m) => !body.includes(m));
51
+ const missingTrailers = FORBIDDEN_TRAILERS.filter((t) => !body.includes(t));
52
+ const missingTargets = BANNED_TARGETS.filter((t) => !body.includes(t));
53
+ const result = combineProbes([
54
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
55
+ probe(body.length > 0, 'the issue-fix orchestrator skill file is readable'),
56
+ probe(headingPresent, 'the RL-3 AI-modified declaration section is still present'),
57
+ probe(missingMarkers.length === 0, `the commit-body markers are intact (missing: ${missingMarkers.join(', ') || 'none'})`),
58
+ probe(missingTrailers.length === 0, `the AI trailer ban is intact (missing: ${missingTrailers.join(', ') || 'none'})`),
59
+ probe(missingTargets.length === 0, `the survey-banned target globs are intact (missing: ${missingTargets.join(', ') || 'none'})`)
60
+ ]);
61
+ const artifact = row.sourceFiles[0] ?? 'skills/peaks-issue-fix-orchestrator/SKILL.md';
62
+ if (result.ok)
63
+ return pass(ctx, artifact);
64
+ return fail(ctx, artifact, 'every fix carries the RL-3 AI-modified declaration and the banned-target list survives', result.detail, 'J14 invariant broken: the AI-modified declaration rule or the banned-target list was removed');
36
65
  }
@@ -1,2 +1,13 @@
1
1
  import type { GuardContext, GuardRunResult } from '../types.js';
2
+ /**
3
+ * Behavioural probe of the fixed coverage-summary discovery order.
4
+ *
5
+ * The previous version listed three openspec files and passed if any of them
6
+ * mentioned "Capability Mapping" AND "coverage" — the string "coverage" appears
7
+ * in `coverage-evidence-reader.ts` from its own filename.
8
+ *
9
+ * Here the real resolver is driven against a tmp project where BOTH candidate
10
+ * files exist, then only the second, then neither. Only a resolver that honours
11
+ * the documented order can produce `coverage/` → `openspec/` → missing.
12
+ */
2
13
  export declare function runJ15Contract(ctx: GuardContext): Promise<GuardRunResult>;
@@ -1,40 +1,75 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
2
3
  import { join } from 'node:path';
3
- const CANDIDATE_FILES = [
4
- 'src/services/openspec/openspec-archive-service.ts',
5
- 'src/services/openspec/coverage-evidence-reader.ts',
6
- 'src/cli/commands/openspec-commands.ts'
7
- ];
8
- const REQUIRED_FRAGMENTS = ['Capability Mapping', 'coverage'];
4
+ import { parseCapabilityMapping, resolveCoverageSummaryPath } from '../../openspec/coverage-evidence-reader.js';
5
+ import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
6
+ const COVERAGE_REL = ['coverage', 'coverage-summary.json'];
7
+ const OPENSPEC_REL = ['openspec', 'coverage-summary.json'];
8
+ function writeJson(root, parts, body) {
9
+ const abs = join(root, ...parts);
10
+ mkdirSync(join(root, ...parts.slice(0, -1)), { recursive: true });
11
+ writeFileSync(abs, JSON.stringify(body));
12
+ }
13
+ const PROPOSAL_WITH_MAPPING = [
14
+ '# change',
15
+ '',
16
+ '## Capability Mapping',
17
+ '',
18
+ '| capability | source | test anchor |',
19
+ '| --- | --- | --- |',
20
+ '| runner | src/services/capability-guard-runner/runner.ts | tests/unit/capability-guard-runner/runner.test.ts |',
21
+ '',
22
+ '## Next section',
23
+ 'trailing prose'
24
+ ].join('\n');
25
+ /**
26
+ * Behavioural probe of the fixed coverage-summary discovery order.
27
+ *
28
+ * The previous version listed three openspec files and passed if any of them
29
+ * mentioned "Capability Mapping" AND "coverage" — the string "coverage" appears
30
+ * in `coverage-evidence-reader.ts` from its own filename.
31
+ *
32
+ * Here the real resolver is driven against a tmp project where BOTH candidate
33
+ * files exist, then only the second, then neither. Only a resolver that honours
34
+ * the documented order can produce `coverage/` → `openspec/` → missing.
35
+ */
9
36
  export async function runJ15Contract(ctx) {
10
- const existing = CANDIDATE_FILES.filter((f) => existsSync(join(ctx.projectRoot, f)));
11
- if (existing.length === 0) {
12
- return {
13
- journeyId: 'J15',
14
- contract: 'spec-coverage',
15
- status: 'fail',
16
- diff: { before: 'an openspec archive/coverage service exists', after: 'no candidate found', reason: 'J15#1 broken: no openspec/coverage surface is present' },
17
- artifactPath: 'src/services/openspec/'
18
- };
37
+ const row = requireBaselineRow(ctx);
38
+ const missing = missingSourceFiles(ctx, row);
39
+ const root = mkdtempSync(join(tmpdir(), 'cbl-J15-'));
40
+ try {
41
+ const both = join(root, ...COVERAGE_REL);
42
+ const fallback = join(root, ...OPENSPEC_REL);
43
+ writeJson(root, COVERAGE_REL, { total: { lines: { pct: 100 } } });
44
+ writeJson(root, OPENSPEC_REL, { total: { lines: { pct: 0 } } });
45
+ const first = await resolveCoverageSummaryPath({ projectRoot: root });
46
+ rmSync(both, { force: true });
47
+ const second = await resolveCoverageSummaryPath({ projectRoot: root });
48
+ rmSync(fallback, { force: true });
49
+ const neither = await resolveCoverageSummaryPath({ projectRoot: root });
50
+ const proposalPath = join(root, 'proposal.md');
51
+ writeFileSync(proposalPath, PROPOSAL_WITH_MAPPING);
52
+ const withBlock = await parseCapabilityMapping(proposalPath);
53
+ writeFileSync(proposalPath, '# change\n\n## Other section\n');
54
+ const withoutBlock = await parseCapabilityMapping(proposalPath);
55
+ const triedPaths = !neither.ok && neither.error.code === 'missing' ? neither.error.triedPaths : [];
56
+ const result = combineProbes([
57
+ probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
58
+ probe(first.ok && first.value.replace(/\\/g, '/').endsWith('/coverage/coverage-summary.json'), `with both summaries present the c8 wrapper default wins (${first.ok ? first.value : first.error.code})`),
59
+ probe(second.ok && second.value.replace(/\\/g, '/').endsWith('/openspec/coverage-summary.json'), `with only the openspec summary present the override is used (${second.ok ? second.value : second.error.code})`),
60
+ probe(!neither.ok && neither.error.code === 'missing', `with neither summary present resolution fails (${neither.ok ? 'resolved' : neither.error.code})`),
61
+ probe(triedPaths.length === 2 &&
62
+ triedPaths[0].replace(/\\/g, '/').endsWith('/coverage/coverage-summary.json') &&
63
+ triedPaths[1].replace(/\\/g, '/').endsWith('/openspec/coverage-summary.json'), `the tried-path list reports the fixed order (${triedPaths.join(' | ') || 'none'})`),
64
+ probe(withBlock.present && withBlock.rows.length === 1, `a ## Capability Mapping block parses to one row (present=${String(withBlock.present)} rows=${String(withBlock.rows.length)})`),
65
+ probe(!withoutBlock.present && withoutBlock.rows.length === 0, 'a proposal without the block is reported as absent, not guessed')
66
+ ]);
67
+ const artifact = row.sourceFiles[0] ?? 'src/services/openspec/coverage-evidence-reader.ts';
68
+ if (result.ok)
69
+ return pass(ctx, artifact);
70
+ return fail(ctx, artifact, 'coverage-summary discovery order is coverage/ then openspec/, and the Capability Mapping block is parsed not guessed', result.detail, 'J15 invariant broken: the coverage evidence discovery order or the Capability Mapping parse changed');
19
71
  }
20
- // At least one file references BOTH "Capability Mapping" AND "coverage".
21
- const fullCoverage = existing.some((f) => {
22
- const src = readFileSync(join(ctx.projectRoot, f), 'utf8');
23
- return REQUIRED_FRAGMENTS.every((frag) => src.includes(frag));
24
- });
25
- if (!fullCoverage) {
26
- return {
27
- journeyId: 'J15',
28
- contract: 'spec-coverage',
29
- status: 'fail',
30
- diff: { before: 'openspec services reference both "Capability Mapping" and "coverage"', after: 'no such service', reason: 'J15#1 broken: openspec archive gate is missing the Capability Mapping + coverage cross-check' },
31
- artifactPath: existing[0]
32
- };
72
+ finally {
73
+ rmSync(root, { recursive: true, force: true });
33
74
  }
34
- return {
35
- journeyId: 'J15',
36
- contract: 'spec-coverage',
37
- status: 'pass',
38
- artifactPath: existing[0]
39
- };
40
75
  }
@@ -0,0 +1,24 @@
1
+ import type { CapabilityBaselineRow } from '../../capability-baseline/types.js';
2
+ import type { GuardContext, GuardRunResult } from '../types.js';
3
+ /**
4
+ * The frozen baseline row this contract is bound to. Throws when the baseline
5
+ * is unreadable or the row is absent — a contract with no baseline behind it
6
+ * must not be able to report green.
7
+ */
8
+ export declare function requireBaselineRow(ctx: GuardContext): CapabilityBaselineRow;
9
+ /**
10
+ * Every `sourceFiles` entry of the row must still exist. The baseline names the
11
+ * files that carry the journey; a vanished file is drift, so this returns the
12
+ * missing ones rather than filtering them out.
13
+ */
14
+ export declare function missingSourceFiles(ctx: GuardContext, row: CapabilityBaselineRow): ReadonlyArray<string>;
15
+ export declare function pass(ctx: GuardContext, artifactPath: string): GuardRunResult;
16
+ export declare function fail(ctx: GuardContext, artifactPath: string, before: string, after: string, reason: string): GuardRunResult;
17
+ /** Result of a source-level probe; `ok` drives pass/fail, `detail` the diff. */
18
+ export interface Probe {
19
+ readonly ok: boolean;
20
+ readonly detail: string;
21
+ }
22
+ export declare function combineProbes(probes: ReadonlyArray<Probe>): Probe;
23
+ /** Convenience: build a probe whose ok-ness is a boolean and a label. */
24
+ export declare function probe(ok: boolean, detail: string): Probe;
@@ -0,0 +1,67 @@
1
+ // src/services/capability-guard-runner/contracts/_shared.ts
2
+ //
3
+ // Helpers shared by the J01–J15 guard contracts.
4
+ //
5
+ // The point of these contracts is to answer "is this journey's behaviour still
6
+ // right?", not "does some file still contain some word?". The helpers here
7
+ // keep that honest and cheap:
8
+ // - the file list comes from the FROZEN BASELINE ROW, never from a
9
+ // hand-written candidate list inside the contract (so a baseline/source
10
+ // drift is visible instead of being papered over by a fallback path);
11
+ // - a missing source file is a contract failure, not a silently narrower
12
+ // check.
13
+ import { existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { readBaselineFile } from '../../capability-baseline/store.js';
16
+ /**
17
+ * The frozen baseline row this contract is bound to. Throws when the baseline
18
+ * is unreadable or the row is absent — a contract with no baseline behind it
19
+ * must not be able to report green.
20
+ */
21
+ export function requireBaselineRow(ctx) {
22
+ const journeyId = ctx.contract.source.baselineRow;
23
+ const r = readBaselineFile(ctx.projectRoot);
24
+ if (!r.ok) {
25
+ throw new Error(`baseline unreadable at ${ctx.projectRoot} (${r.error.code}: ${r.error.message})`);
26
+ }
27
+ const row = r.file.rows.find((x) => x.journeyId === journeyId);
28
+ if (!row) {
29
+ throw new Error(`baseline row ${journeyId} is not in the frozen baseline`);
30
+ }
31
+ return row;
32
+ }
33
+ /**
34
+ * Every `sourceFiles` entry of the row must still exist. The baseline names the
35
+ * files that carry the journey; a vanished file is drift, so this returns the
36
+ * missing ones rather than filtering them out.
37
+ */
38
+ export function missingSourceFiles(ctx, row) {
39
+ return row.sourceFiles.filter((f) => !existsSync(join(ctx.projectRoot, f)));
40
+ }
41
+ export function pass(ctx, artifactPath) {
42
+ return {
43
+ journeyId: ctx.contract.journeyId,
44
+ contract: ctx.contract.kind,
45
+ status: 'pass',
46
+ artifactPath
47
+ };
48
+ }
49
+ export function fail(ctx, artifactPath, before, after, reason) {
50
+ return {
51
+ journeyId: ctx.contract.journeyId,
52
+ contract: ctx.contract.kind,
53
+ status: 'fail',
54
+ diff: { before, after, reason },
55
+ artifactPath
56
+ };
57
+ }
58
+ export function combineProbes(probes) {
59
+ const bad = probes.filter((p) => !p.ok);
60
+ if (bad.length === 0)
61
+ return { ok: true, detail: probes.map((p) => p.detail).join('; ') };
62
+ return { ok: false, detail: bad.map((p) => p.detail).join('; ') };
63
+ }
64
+ /** Convenience: build a probe whose ok-ness is a boolean and a label. */
65
+ export function probe(ok, detail) {
66
+ return { ok, detail };
67
+ }
@@ -0,0 +1,5 @@
1
+ import { type JourneyId } from '../capability-baseline/types.js';
2
+ import type { GuardContract } from './types.js';
3
+ export declare const GUARD_CONTRACTS: ReadonlyArray<GuardContract>;
4
+ export declare function getGuardContract(journeyId: JourneyId): GuardContract | undefined;
5
+ export declare function isJourneyId(value: string): value is JourneyId;