peaks-loop 4.0.49 → 4.0.51

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 (179) hide show
  1. package/CHANGELOG.md +38 -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 +113 -20
  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/dispatch/sub-agent-dispatcher.d.ts +11 -30
  82. package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
  83. package/dist/services/final-review/pre-post-diff.js +10 -2
  84. package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
  85. package/dist/services/ide/adapters/codex-adapter.js +1 -2
  86. package/dist/services/ide/adapters/cursor-adapter.js +1 -2
  87. package/dist/services/ide/adapters/hermes-adapter.js +1 -2
  88. package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
  89. package/dist/services/ide/adapters/qoder-adapter.js +1 -2
  90. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
  91. package/dist/services/ide/adapters/trae-adapter.js +1 -2
  92. package/dist/services/ide/adapters/zcode-adapter.js +0 -1
  93. package/dist/services/ide/ide-types.d.ts +0 -2
  94. package/dist/services/observability/observability-service.d.ts +1 -1
  95. package/dist/services/scan/api-diff-types.js +20 -2
  96. package/dist/services/security/safe-settings-path.js +19 -1
  97. package/dist/services/skill/skill-search-service.d.ts +3 -3
  98. package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
  99. package/dist/services/standards/loop-engineering-lint.js +6 -0
  100. package/dist/services/web/daemon-registry.js +27 -2
  101. package/dist/services/workspace/claude-settings-template.d.ts +53 -37
  102. package/dist/services/workspace/claude-settings-template.js +105 -83
  103. package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
  104. package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
  105. package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
  106. package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
  107. package/dist/services/workspace/workspace-service.js +11 -1
  108. package/dist/shared/fs-utils.d.ts +26 -0
  109. package/dist/shared/fs-utils.js +35 -0
  110. package/package.json +9 -7
  111. package/scripts/copy-templates.mjs +0 -12
  112. package/scripts/install-skills.mjs +154 -53
  113. package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
  114. package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
  115. package/skills/bee/peaks-prd/SKILL.md +3 -3
  116. package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
  117. package/skills/bee/peaks-prd/references/workflow.md +1 -1
  118. package/skills/bee/peaks-qa/SKILL.md +6 -7
  119. package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
  120. package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
  121. package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
  122. package/skills/bee/peaks-rd/SKILL.md +2 -3
  123. package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
  124. package/skills/bee/peaks-rd/references/external-references.md +1 -1
  125. package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
  126. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
  127. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
  128. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
  129. package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
  130. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
  131. package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
  132. package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
  133. package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
  134. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
  135. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  136. package/skills/bee/peaks-security-audit/SKILL.md +3 -3
  137. package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
  138. package/skills/bee/peaks-txt/references/context-capsule.md +1 -1
  139. package/skills/bee/peaks-ui/SKILL.md +1 -1
  140. package/skills/peaks-audit/SKILL.md +1 -1
  141. package/skills/peaks-code/SKILL.md +20 -18
  142. package/skills/peaks-code/references/context-governance.md +1 -1
  143. package/skills/peaks-code/references/dag-orchestrator.md +3 -4
  144. package/skills/peaks-code/references/external-references.md +1 -1
  145. package/skills/peaks-code/references/external-skill-invocation.md +2 -2
  146. package/skills/peaks-code/references/fanout-mandatory.md +3 -3
  147. package/skills/peaks-code/references/frontend-only-mode.md +2 -2
  148. package/skills/peaks-code/references/gstack-integration.md +1 -1
  149. package/skills/peaks-code/references/micro-cycle.md +1 -1
  150. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  151. package/skills/peaks-code/references/project-scan-checklist.md +1 -1
  152. package/skills/peaks-code/references/resume-detection.md +1 -1
  153. package/skills/peaks-code/references/runbook.md +6 -3
  154. package/skills/peaks-code/references/session-overload-signal-index.md +6 -4
  155. package/skills/peaks-code/references/startup-sequence.md +17 -17
  156. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  157. package/skills/peaks-code/references/step-11-memory-sediment.md +2 -2
  158. package/skills/peaks-code/references/sub-agent-dispatch.md +26 -25
  159. package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
  160. package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
  161. package/skills/peaks-code/references/worktree-governance.md +1 -1
  162. package/skills/peaks-final-review/SKILL.md +3 -3
  163. package/skills/peaks-ide/references/audit-log-helper.md +5 -4
  164. package/skills/peaks-resume/SKILL.md +1 -1
  165. package/skills/peaks-slice-decompose/SKILL.md +4 -4
  166. package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
  167. package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
  168. package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
  169. package/skills/peaks-solo/SKILL.md +1 -2
  170. package/dist/cli/commands/context-builder-commands.d.ts +0 -11
  171. package/dist/cli/commands/context-builder-commands.js +0 -85
  172. package/dist/services/hooks/write-gate.js +0 -111
  173. package/skills/bee/peaks-prd/references/command-migration.md +0 -3
  174. package/skills/bee/peaks-qa/references/command-migration.md +0 -3
  175. package/skills/bee/peaks-rd/references/command-migration.md +0 -3
  176. package/skills/bee/peaks-sc/references/command-migration.md +0 -3
  177. package/skills/bee/peaks-txt/references/command-migration.md +0 -3
  178. package/skills/bee/peaks-ui/references/command-migration.md +0 -3
  179. package/skills/peaks-code/references/command-migration.md +0 -3
@@ -2,9 +2,16 @@
2
2
  * Backing detector — classifies each red line as `cli-backed`, `partial`,
3
3
  * or `prose-only`. The classifier already sets the backing for catalog hits
4
4
  * (cli-backed when an enforcer file path is present). This module exists to
5
- * handle the post-classification nuance: heuristics for the "partial" tier
6
- * (a gate exists but the LLM can bypass it) and verification that the
7
- * enforcer file actually exists on disk.
5
+ * handle the post-classification nuances: heuristics for the "partial" tier
6
+ * (a gate exists but the LLM can bypass it), verification that the enforcer
7
+ * file exists on disk, and — since A9 of the 2026-09-15 diagnosis —
8
+ * verification that the enforcer is actually reachable from a call site.
9
+ *
10
+ * `cli-backed` means "a CLI surface runs this rule". A file that exists but
11
+ * that nothing imports runs nothing, so it is `prose-only` with the same
12
+ * weight as an unwritten rule. Callers pass the live set from
13
+ * `enforcer-liveness.ts`; `null` means "liveness undecidable for this
14
+ * project" (no `src/` tree), in which case the existing file is trusted.
8
15
  */
9
16
  import { existsSync } from 'node:fs';
10
17
  import { resolve } from 'node:path';
@@ -23,37 +30,46 @@ function detectPartial(context) {
23
30
  }
24
31
  /**
25
32
  * Re-classify a single RedLineEntry. Returns a new entry with the
26
- * `backing` field updated and `enforcerRef` possibly nulled if the
27
- * referenced file does not exist on disk.
33
+ * `backing` field updated; `enforcerRef` is preserved even when the
34
+ * backing is downgraded, because the dead reference is the triage
35
+ * information the report needs.
28
36
  */
29
- export function classifyBacking(entry, projectRoot) {
37
+ export function classifyBacking(entry, projectRoot, liveEnforcers) {
38
+ const enforcerPath = entry.enforcerRef === null ? null : resolve(projectRoot, entry.enforcerRef);
39
+ const exists = enforcerPath !== null && existsSync(enforcerPath);
40
+ const dead = entry.enforcerRef !== null && exists && liveEnforcers !== null && !liveEnforcers.has(entry.enforcerRef);
30
41
  if (detectPartial(entry.source.context)) {
31
42
  return {
32
43
  entry: { ...entry, backing: 'partial' },
33
- enforcerExists: entry.enforcerRef !== null && existsSync(resolve(projectRoot, entry.enforcerRef)),
44
+ enforcerExists: exists,
45
+ enforcerDead: dead,
34
46
  };
35
47
  }
36
48
  if (entry.enforcerRef === null) {
37
- return { entry, enforcerExists: false };
49
+ return { entry, enforcerExists: false, enforcerDead: false };
38
50
  }
39
- const enforcerPath = resolve(projectRoot, entry.enforcerRef);
40
- const exists = existsSync(enforcerPath);
51
+ const backed = exists && !dead;
41
52
  return {
42
- entry: { ...entry, backing: exists ? 'cli-backed' : 'prose-only' },
53
+ entry: { ...entry, backing: backed ? 'cli-backed' : 'prose-only' },
43
54
  enforcerExists: exists,
55
+ enforcerDead: dead,
44
56
  };
45
57
  }
46
- export function classifyBackingBatch(entries, projectRoot) {
58
+ export function classifyBackingBatch(entries, projectRoot, liveEnforcers) {
47
59
  const updated = [];
48
60
  const warnings = [];
61
+ const dead = new Set();
49
62
  for (const entry of entries) {
50
- const { entry: reclassified, enforcerExists } = classifyBacking(entry, projectRoot);
51
- updated.push(reclassified);
52
- if (reclassified.backing === 'cli-backed' && !enforcerExists) {
63
+ const result = classifyBacking(entry, projectRoot, liveEnforcers);
64
+ updated.push(result.entry);
65
+ if (result.enforcerDead && result.entry.enforcerRef !== null) {
66
+ dead.add(result.entry.enforcerRef);
67
+ }
68
+ if (result.entry.backing === 'cli-backed' && !result.enforcerExists) {
53
69
  // Defensive: should not happen because classifyBacking downgrades to
54
70
  // prose-only, but keep the assertion in case of future drift.
55
- warnings.push(`enforcer ref "${reclassified.enforcerRef}" missing on disk for ${reclassified.id}`);
71
+ warnings.push(`enforcer ref "${result.entry.enforcerRef}" missing on disk for ${result.entry.id}`);
56
72
  }
57
73
  }
58
- return { entries: updated, warnings };
74
+ return { entries: updated, warnings, deadEnforcers: [...dead].sort() };
59
75
  }
@@ -0,0 +1,12 @@
1
+ export interface LiveEnforcerScan {
2
+ /** Refs proven imported from a call site. Empty when `unknown` is true. */
3
+ readonly live: ReadonlySet<string>;
4
+ /** True when the project has no `src/` tree, so liveness is undecidable. */
5
+ readonly unknown: boolean;
6
+ readonly warnings: readonly string[];
7
+ }
8
+ /**
9
+ * Scan `projectRoot/src` for relative imports that name one of `enforcerRefs`.
10
+ * Returns the subset of refs with a call site outside the enforcers directory.
11
+ */
12
+ export declare function computeLiveEnforcers(projectRoot: string, enforcerRefs: readonly string[]): LiveEnforcerScan;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Enforcer liveness — A9 of `docs/diagnosis-2026-09-15-peaks-loop-state.md`.
3
+ *
4
+ * `cli-backed` used to mean "the `enforcerRef` path exists on disk"
5
+ * (`backing-detector.ts`). It did not mean "the enforcer runs". Ten catalog
6
+ * enforcerRefs name a file no module imports — `lint-rd-handoff-coverage.ts`
7
+ * among them — so the audit reported 108/152 red lines as CLI-enforced while
8
+ * a quarter of those had no caller at all.
9
+ *
10
+ * This module answers the narrower, checkable question: is the enforcer
11
+ * module imported by some file OUTSIDE the enforcers directory? A relative
12
+ * import is cheap proof that a call site exists — production code cannot
13
+ * invoke the module without one.
14
+ *
15
+ * Deliberate limits, stated rather than hidden:
16
+ * - An import is not a call. A module can be imported and never invoked.
17
+ * This check is necessary, not sufficient: it eliminates the "no caller
18
+ * exists at all" class and nothing beyond it.
19
+ * - Imports written inside `src/…/audit/enforcers/` do not count. A dead
20
+ * enforcer that imports another dead enforcer must not resurrect it.
21
+ * - A project with no `src/` tree yields `unknown: true`, never an empty
22
+ * live-set: peaks-loop audits consumer projects, and "cannot tell" must
23
+ * not read as "dead".
24
+ */
25
+ import { readdirSync, readFileSync } from 'node:fs';
26
+ import { dirname, join, relative, resolve } from 'node:path';
27
+ const SOURCE_FILE = /\.(ts|tsx|mts|cts)$/;
28
+ const ENFORCER_DIR_FRAGMENT = '/services/audit/enforcers/';
29
+ /** Matches `from './x.js'`, `import('./x.js')` and `export … from './x.js'`. */
30
+ const RELATIVE_JS_IMPORT = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+\.js)['"]/g;
31
+ function toPosix(value) {
32
+ return value.split('\\').join('/');
33
+ }
34
+ function* walkSourceFiles(root, warnings) {
35
+ const stack = [root];
36
+ while (stack.length > 0) {
37
+ const dir = stack.pop();
38
+ if (dir === undefined)
39
+ continue;
40
+ let dirents;
41
+ try {
42
+ dirents = readdirSync(dir, { withFileTypes: true });
43
+ }
44
+ catch (error) {
45
+ warnings.push(`enforcer-liveness: cannot read ${toPosix(dir)} (${String(error)})`);
46
+ continue;
47
+ }
48
+ for (const dirent of dirents) {
49
+ const full = join(dir, dirent.name);
50
+ if (dirent.isDirectory()) {
51
+ if (dirent.name === 'node_modules' || dirent.name === 'dist' || dirent.name.startsWith('.'))
52
+ continue;
53
+ stack.push(full);
54
+ }
55
+ else if (dirent.isFile() && SOURCE_FILE.test(dirent.name)) {
56
+ yield full;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Scan `projectRoot/src` for relative imports that name one of `enforcerRefs`.
63
+ * Returns the subset of refs with a call site outside the enforcers directory.
64
+ */
65
+ export function computeLiveEnforcers(projectRoot, enforcerRefs) {
66
+ const srcRoot = join(projectRoot, 'src');
67
+ const warnings = [];
68
+ const live = new Set();
69
+ const wanted = new Set(enforcerRefs);
70
+ let sawAnySource = false;
71
+ for (const absFile of walkSourceFiles(srcRoot, warnings)) {
72
+ sawAnySource = true;
73
+ const relFile = toPosix(relative(projectRoot, absFile));
74
+ // An import written inside the enforcers directory proves nothing.
75
+ if (relFile.includes(ENFORCER_DIR_FRAGMENT))
76
+ continue;
77
+ let source;
78
+ try {
79
+ source = readFileSync(absFile, 'utf8');
80
+ }
81
+ catch (error) {
82
+ warnings.push(`enforcer-liveness: cannot read ${relFile} (${String(error)})`);
83
+ continue;
84
+ }
85
+ RELATIVE_JS_IMPORT.lastIndex = 0;
86
+ let match = RELATIVE_JS_IMPORT.exec(source);
87
+ while (match !== null) {
88
+ const specifier = match[1] ?? '';
89
+ const resolved = toPosix(relative(projectRoot, resolve(dirname(absFile), specifier)));
90
+ const asTs = resolved.replace(/\.js$/, '.ts');
91
+ if (wanted.has(asTs))
92
+ live.add(asTs);
93
+ match = RELATIVE_JS_IMPORT.exec(source);
94
+ }
95
+ }
96
+ if (!sawAnySource) {
97
+ return { live, unknown: true, warnings };
98
+ }
99
+ return { live, unknown: false, warnings };
100
+ }
@@ -3,11 +3,24 @@
3
3
  *
4
4
  * Two enforcers: catalog size must grow to ≥ 40 (the P2-a target),
5
5
  * and the prose-only ratio must stay ≤ 7% (per spec §10.2 L2
6
- * acceptance; tightened from the pre-v2.12.1 5% target to reflect
7
- * the catalog governance reform — see `.peaks/memory/2026-06-27-
8
- * prose-only-catalog-followup.md` for the full rationale and the
9
- * per-entry backlog triage). Both fire on the catalog's static
10
- * state — no file scan, just the catalog itself.
6
+ * acceptance). Both fire on the state the classifier produced — no
7
+ * extra file scan beyond what the audit already did.
8
+ *
9
+ * C6 of the 2026-09-15 diagnosis: this gate and
10
+ * `prose-ratio-calculator.computeProseRatio` (behind
11
+ * `peaks audit prose-ratio`) are the two "prose-only ratio" gates, and
12
+ * they used to disagree. The calculator excluded `informational` rows
13
+ * from its numerator; this one excluded `informational` rows from *its*
14
+ * numerator by a different route (red-lines-service passed it a
15
+ * `proseOnlyCount` that had already been filtered). Both now measure the
16
+ * same quantity, identically defined:
17
+ *
18
+ * numerator = rows classified `backing === 'prose-only'`, all of them
19
+ * denominator = every classified row (`entries.length`)
20
+ *
21
+ * They still carry different *thresholds* — 7% here, 5% as the
22
+ * `peaks audit prose-ratio` default — which is a policy difference, not
23
+ * an accounting one. Nothing redefines what is being counted.
11
24
  */
12
25
  import type { LintHit } from './lint-style.js';
13
26
  export declare const CATALOG_SIZE_TARGET = 40;
@@ -22,11 +35,10 @@ export interface CatalogProseOnlyRatio {
22
35
  }
23
36
  export declare function lintCatalogSize(actualSize: number): readonly LintHit[];
24
37
  /**
25
- * Prose-only ratio: count catalog entries whose `enforcerRef` is
26
- * null (i.e. not backed by a CLI surface) divided by the total
27
- * catalog size. Per spec §10.2, the L2 acceptance is ≤ 10% at
28
- * P2-a; v2.12.1 catalog governance tightened the gate to ≤ 7%
29
- * after the discovered-prose-only reform (see
30
- * `.peaks/memory/2026-06-27-prose-only-catalog-followup.md`).
38
+ * Prose-only ratio: rows the classifier tagged `prose-only`, divided by
39
+ * every row the classifier produced. `catalogSize` is a slight misnomer
40
+ * — it is `entries.length`, the count of classified rows, not the size of
41
+ * the hand-maintained catalog. Same numerator and denominator as
42
+ * `computeProseRatio`; see the module docstring above.
31
43
  */
32
44
  export declare function lintCatalogProseOnlyRatio(catalogSize: number, proseOnlyCount: number): readonly LintHit[];
@@ -1,12 +1,9 @@
1
1
  export const CATALOG_SIZE_TARGET = 40;
2
- // v2.12.1 catalog governance: 5% was unreachable without demoting the
3
- // 80 discovered prose-only entries (which are advisory SKILL.md
4
- // phrases, not actionable red lines). After the v2.12.1 reform the
5
- // ratio dropped from 60.1% (89/148) to 6.1% (9/148); the remaining
6
- // 9 entries are the real backlog (5 unique catalog ids: prototype-
7
- // fidelity-001/002, mock-placement-001, resume-detection-001,
8
- // pre-rd-scan-001, design-draft-confirm-001). Bumping the target to
9
- // 7% acknowledges the reform while keeping the gate active.
2
+ // The v2.12.1 reform left this at 7% by demoting discovered advisory
3
+ // rows out of the numerator. S3 of the 2026-09-15 diagnosis-remediation
4
+ // job removed that demotion, so the observed ratio is 66% (101/153) —
5
+ // the gate now fires, which is the intended outcome: the number was
6
+ // always this bad, it was just being reported as 0%.
10
7
  export const PROSE_ONLY_RATIO_TARGET = 0.07;
11
8
  function syntheticHit(catalogId, rule, matched) {
12
9
  // No specific file to point at — return a synthetic hit against
@@ -31,12 +28,11 @@ export function lintCatalogSize(actualSize) {
31
28
  return [syntheticHit('rl-catalog-total-001', 'Catalog governance: catalog size must grow to ≥ 40 (L2.3 P2-a target)', `(catalog size ${actualSize} < target ${CATALOG_SIZE_TARGET})`)];
32
29
  }
33
30
  /**
34
- * Prose-only ratio: count catalog entries whose `enforcerRef` is
35
- * null (i.e. not backed by a CLI surface) divided by the total
36
- * catalog size. Per spec §10.2, the L2 acceptance is ≤ 10% at
37
- * P2-a; v2.12.1 catalog governance tightened the gate to ≤ 7%
38
- * after the discovered-prose-only reform (see
39
- * `.peaks/memory/2026-06-27-prose-only-catalog-followup.md`).
31
+ * Prose-only ratio: rows the classifier tagged `prose-only`, divided by
32
+ * every row the classifier produced. `catalogSize` is a slight misnomer
33
+ * — it is `entries.length`, the count of classified rows, not the size of
34
+ * the hand-maintained catalog. Same numerator and denominator as
35
+ * `computeProseRatio`; see the module docstring above.
40
36
  */
41
37
  export function lintCatalogProseOnlyRatio(catalogSize, proseOnlyCount) {
42
38
  if (catalogSize === 0)
@@ -1,18 +1,8 @@
1
+ import type { LintHit, SkillFile } from './lint-style.js';
1
2
  /**
2
- * P2-b sweep 005 — peaks-rd handoff + coverage enforcers.
3
- *
4
- * Closes three peaks-rd discovered lines:
5
- * - md-121 : "do not hand off to QA without [tech-doc.md]" (BLOCKING)
6
- * - md-127 : "do not hand off to QA without a perf-baseline" (BLOCKING)
7
- * - md-162 : "100% coverage target on testable files is meaningful"
8
- *
9
- * Two enforcers (handoff + coverage). The handoff enforcer
10
- * checks for both 'tech-doc' and 'perf-baseline' handoff
11
- * markers; the coverage enforcer checks for the 100%-target
12
- * phrasing + the no-coverage-padding rule.
13
- *
14
- * scope: peaks-rd only.
3
+ * peaks-rd must not hand off to QA without a reviewable RD artifact. The
4
+ * check reads `.peaks/_runtime/<sessionId>/rd/requests/*.md`; it does not
5
+ * read the SKILL.md sentence that promises one.
15
6
  */
16
- import type { LintHit, SkillFile } from './lint-style.js';
17
- export declare function lintRdHandoffContract(skill: SkillFile): ReadonlyArray<LintHit>;
7
+ export declare function lintRdHandoffContract(skill: SkillFile, projectRoot: string): ReadonlyArray<LintHit>;
18
8
  export declare function lintRdCoverageDiscipline(skill: SkillFile): ReadonlyArray<LintHit>;
@@ -1,17 +1,85 @@
1
- const TECH_DOC_HANDOFF = /do not hand off to qa without[^\n]*tech-doc/im;
2
- const PERF_BASELINE_HANDOFF = /do not hand off to qa without[^\n]*perf-baseline/im;
1
+ /**
2
+ * P2-b sweep 005 — peaks-rd handoff + coverage enforcers.
3
+ *
4
+ * Closes three peaks-rd discovered lines:
5
+ * - md-121 : "do not hand off to QA without [the RD artifact]" (BLOCKING)
6
+ * - md-127 : "do not hand off to QA without a perf-baseline" (BLOCKING)
7
+ * - md-162 : "100% coverage target on testable files is meaningful"
8
+ *
9
+ * A10 of the 2026-09-15 diagnosis — `lintRdHandoffContract` used to regex
10
+ * the peaks-rd SKILL.md prose for the sentence *"do not hand off to QA
11
+ * without this file"* and call that enforcement. It never opened a file
12
+ * under `.peaks/_runtime/`. The gate was verifying a sentence about the
13
+ * artifact instead of the artifact: the exact failure the 4.0.49 release
14
+ * note named, found alive inside the enforcer layer.
15
+ *
16
+ * It now reads `.peaks/_runtime/<sessionId>/rd/requests/*.md` — the
17
+ * artifact the sentence is about (contract:
18
+ * `skills/bee/peaks-rd/references/artifact-per-request.md`) — and reports
19
+ * when nothing is there. When no session binding can be resolved it
20
+ * reports nothing, matching the soft-pass convention of its sibling
21
+ * enforcers (`pre-rd-scan.ts`, `lint-audit-regression.ts`).
22
+ *
23
+ * `lintRdCoverageDiscipline` still reads the skill doc, because its rule
24
+ * *is* about what the skill doc declares. It is not a handoff gate.
25
+ *
26
+ * scope: peaks-rd only.
27
+ */
28
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
29
+ import { join } from 'node:path';
3
30
  const COVERAGE_TARGET = /100%\s*coverage target[^\n]*testable files/i;
4
31
  const NO_PADDING = /must not write coverage-padding tests/i;
5
- function findHandoffContract(lines) {
6
- let techDoc = false;
7
- let perfBaseline = false;
8
- for (const line of lines) {
9
- if (TECH_DOC_HANDOFF.test(line))
10
- techDoc = true;
11
- if (PERF_BASELINE_HANDOFF.test(line))
12
- perfBaseline = true;
32
+ /** The artifact whose absence makes an RD→QA handoff invalid. */
33
+ const RD_ARTIFACT_RELATIVE = '.peaks/_runtime';
34
+ /**
35
+ * Resolve the bound session id from `.peaks/_runtime/session.json`. Both
36
+ * key spellings are accepted: the file has shipped as `peakSessionId` and
37
+ * as `sessionId`, and reading only one silently disables the check.
38
+ */
39
+ function resolveSessionBinding(projectRoot) {
40
+ const sessionJsonPath = join(projectRoot, '.peaks', '_runtime', 'session.json');
41
+ if (!existsSync(sessionJsonPath)) {
42
+ return { sessionId: null, reason: 'no .peaks/_runtime/session.json' };
43
+ }
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(readFileSync(sessionJsonPath, 'utf8'));
47
+ }
48
+ catch (error) {
49
+ return { sessionId: null, reason: `session.json is not readable JSON (${String(error)})` };
50
+ }
51
+ if (typeof parsed !== 'object' || parsed === null) {
52
+ return { sessionId: null, reason: 'session.json is not an object' };
53
+ }
54
+ const record = parsed;
55
+ for (const key of ['sessionId', 'peakSessionId']) {
56
+ const value = record[key];
57
+ if (typeof value === 'string' && value.length > 0) {
58
+ return { sessionId: value, reason: `bound via session.json:${key}` };
59
+ }
60
+ }
61
+ return { sessionId: null, reason: 'session.json carries no session id' };
62
+ }
63
+ /** Count non-empty `*.md` files in `dir`. A missing dir counts as zero. */
64
+ function countNonEmptyArtifacts(dir) {
65
+ let names;
66
+ try {
67
+ names = readdirSync(dir).filter((name) => name.endsWith('.md'));
68
+ }
69
+ catch {
70
+ return 0;
13
71
  }
14
- return { techDoc, perfBaseline };
72
+ let count = 0;
73
+ for (const name of names) {
74
+ try {
75
+ if (statSync(join(dir, name)).size > 0)
76
+ count += 1;
77
+ }
78
+ catch {
79
+ continue;
80
+ }
81
+ }
82
+ return count;
15
83
  }
16
84
  function findCoverageContract(lines) {
17
85
  let target = false;
@@ -24,26 +92,27 @@ function findCoverageContract(lines) {
24
92
  }
25
93
  return { target, noPadding };
26
94
  }
27
- export function lintRdHandoffContract(skill) {
95
+ /**
96
+ * peaks-rd must not hand off to QA without a reviewable RD artifact. The
97
+ * check reads `.peaks/_runtime/<sessionId>/rd/requests/*.md`; it does not
98
+ * read the SKILL.md sentence that promises one.
99
+ */
100
+ export function lintRdHandoffContract(skill, projectRoot) {
28
101
  if (skill.name !== 'peaks-rd')
29
102
  return [];
30
- const lines = skill.lines.length > 0
31
- ? skill.lines
32
- : skill.body.split(/\r?\n/);
33
- const { techDoc, perfBaseline } = findHandoffContract(lines);
34
- if (techDoc && perfBaseline)
103
+ const binding = resolveSessionBinding(projectRoot);
104
+ if (binding.sessionId === null)
105
+ return [];
106
+ const requestsDir = join(projectRoot, RD_ARTIFACT_RELATIVE, binding.sessionId, 'rd', 'requests');
107
+ const artifactCount = countNonEmptyArtifacts(requestsDir);
108
+ if (artifactCount > 0)
35
109
  return [];
36
- const missing = [];
37
- if (!techDoc)
38
- missing.push('tech-doc handoff BLOCKING');
39
- if (!perfBaseline)
40
- missing.push('perf-baseline handoff BLOCKING');
41
110
  return [{
42
111
  catalogId: 'rl-rd-handoff-contract-001',
43
- rule: 'peaks-rd SKILL.md must declare the QA-handoff BLOCKING contract (tech-doc + perf-baseline)',
44
- file: skill.path,
112
+ rule: 'peaks-rd must not hand off to QA without a non-empty RD artifact under rd/requests/',
113
+ file: requestsDir,
45
114
  line: 1,
46
- matchedText: `missing markers: ${missing.join(', ')}`
115
+ matchedText: `no non-empty .md artifact under ${requestsDir} (${binding.reason})`,
47
116
  }];
48
117
  }
49
118
  export function lintRdCoverageDiscipline(skill) {
@@ -14,7 +14,15 @@ export interface SkillFile {
14
14
  export declare function readSkillFiles(skillsRoot: string, names: readonly string[]): readonly SkillFile[];
15
15
  /** Theme A — section structure. Returns lint hits (positive = rule
16
16
  * satisfied, so a missing heading fires the lint hit; downstream
17
- * audit service decides whether to WARN or pass). */
17
+ * audit service decides whether to WARN or pass).
18
+ *
19
+ * A11 of the 2026-09-15 diagnosis: the `Hard contracts` rule used to
20
+ * match the *heading text* and stop there. `peaks-perf-audit/SKILL.md`
21
+ * carries `## Hard contracts (BLOCKING)` — the word BLOCKING is in the
22
+ * heading — while every bullet beneath it is unmarked prose, so the
23
+ * audit counted a BLOCKING red line with no contract behind it. The
24
+ * rule now also requires at least one marker line inside the section
25
+ * body, which is what makes a contract visible to the classifier. */
18
26
  export declare function lintSectionShape(skill: SkillFile): readonly LintHit[];
19
27
  /**
20
28
  * ASCII wireframe section-order check (spec §5.4 line 647).
@@ -45,13 +45,36 @@ function matchedText(lines, line) {
45
45
  return '';
46
46
  return (lines[line - 1] ?? '').trim();
47
47
  }
48
+ /** A red-line marker that makes a contract line machine-visible. */
49
+ const CONTRACT_MARKER = /\b(MANDATORY|BLOCKING|MUST NOT|RED LINE)\b/;
50
+ /**
51
+ * Collect the body of the section that starts at `headingLine` (1-based):
52
+ * every line up to the next `## ` heading.
53
+ */
54
+ function sectionBody(lines, headingLine) {
55
+ const body = [];
56
+ for (let i = headingLine; i < lines.length; i += 1) {
57
+ if (/^##\s/.test(lines[i] ?? ''))
58
+ break;
59
+ body.push(lines[i] ?? '');
60
+ }
61
+ return body;
62
+ }
48
63
  /** Theme A — section structure. Returns lint hits (positive = rule
49
64
  * satisfied, so a missing heading fires the lint hit; downstream
50
- * audit service decides whether to WARN or pass). */
65
+ * audit service decides whether to WARN or pass).
66
+ *
67
+ * A11 of the 2026-09-15 diagnosis: the `Hard contracts` rule used to
68
+ * match the *heading text* and stop there. `peaks-perf-audit/SKILL.md`
69
+ * carries `## Hard contracts (BLOCKING)` — the word BLOCKING is in the
70
+ * heading — while every bullet beneath it is unmarked prose, so the
71
+ * audit counted a BLOCKING red line with no contract behind it. The
72
+ * rule now also requires at least one marker line inside the section
73
+ * body, which is what makes a contract visible to the classifier. */
51
74
  export function lintSectionShape(skill) {
52
75
  const hits = [];
53
76
  const rules = [
54
- { id: 'rl-section-hard-contracts-001', rule: 'Hard contracts for browser/IO surface', pattern: SECTION_HARD_CONTRACTS_HEADING },
77
+ { id: 'rl-section-hard-contracts-001', rule: 'Hard contracts for browser/IO surface', pattern: SECTION_HARD_CONTRACTS_HEADING, requiresBodyMarker: true },
55
78
  { id: 'rl-section-mandatory-artifact-001', rule: 'Mandatory per-request artifact', pattern: SECTION_MANDATORY_HEADING },
56
79
  { id: 'rl-section-default-runbook-001', rule: 'Default runbook pointer', pattern: SECTION_DEFAULT_RUNBOOK_HEADING },
57
80
  { id: 'rl-section-gate-index-001', rule: 'Gate index', pattern: SECTION_GATE_INDEX_HEADING },
@@ -67,6 +90,19 @@ export function lintSectionShape(skill) {
67
90
  line: 1,
68
91
  matchedText: '(missing section)'
69
92
  });
93
+ continue;
94
+ }
95
+ if (r.requiresBodyMarker === true) {
96
+ const declared = sectionBody(skill.lines, line).some((l) => CONTRACT_MARKER.test(l));
97
+ if (!declared) {
98
+ hits.push({
99
+ catalogId: r.id,
100
+ rule: r.rule,
101
+ file: skill.path,
102
+ line,
103
+ matchedText: `(heading at line ${line} but no BLOCKING/MANDATORY/MUST NOT/RED LINE line inside the section)`
104
+ });
105
+ }
70
106
  }
71
107
  }
72
108
  return hits;
@@ -1,33 +1,44 @@
1
1
  /**
2
- * Prose-only ratio calculator — Slice C Group G3 (v2.14.0).
2
+ * Prose-only ratio calculator — Slice C Group G3 (v2.14.0),
3
+ * corrected in S3 of the 2026-09-15 diagnosis-remediation job.
3
4
  *
4
- * Computes the prose-only ratio for a set of red-line entries. Per
5
- * spec §10.2 + the v2.12.1 reform (`.peaks/memory/2026-06-27-
6
- * prose-only-catalog-followup.md`), an entry counts as prose-only
7
- * only when BOTH:
8
- * 1. `backing === 'prose-only'`
9
- * 2. `informational !== true`
5
+ * An entry counts as prose-only when `backing === 'prose-only'`.
6
+ * Full stop. There is no second condition.
10
7
  *
11
- * The 80 discovered advisory SKILL.md phrases (auto-marked
12
- * `informational=true` by `classifier.ts:141`) are excluded from
13
- * the ratio so the gate (≤ 5% per slice C AC A3.1) reflects the
14
- * actionable backlog.
8
+ * The pre-S3 version also required `informational !== true`, on the
9
+ * reasoning that auto-discovered advisory SKILL.md phrases are "not
10
+ * actionable red lines". Whatever the merits of that reading, the effect
11
+ * was to move 44 of 152 rows — 29% of the catalog — out of the
12
+ * denominator, so the gate reported `proseOnly: 0` while the same JSON
13
+ * carried 44 rows with `"backing": "prose-only"`. A metric whose
14
+ * denominator can be redefined by the code it measures is not a metric.
15
15
  *
16
- * Karpathy §2 simplicity: one exported function plus a thin
17
- * calculator interface; no I/O. The pure form makes the ≥8
18
- * test cases in prose-ratio-calculator.test.ts trivial.
16
+ * `informational` survives as a triage label — `discoveredProseOnly`
17
+ * below counts those rows — but it no longer moves any number that the
18
+ * ratio is computed from. Expect the ratio to look much worse than it
19
+ * did; that is this correction working.
20
+ *
21
+ * Karpathy §2 simplicity: one exported function plus a thin calculator
22
+ * interface; no I/O.
19
23
  */
20
24
  import type { RedLineEntry } from './types.js';
21
25
  export interface ProseRatioResult {
22
- /** Total catalog size (entries.length). */
26
+ /** Total entries considered (entries.length) — the denominator, always. */
23
27
  readonly totalRedLines: number;
24
28
  /** Count of entries with backing === 'cli-backed'. */
25
29
  readonly cliBacked: number;
26
30
  /** Count of entries with backing === 'partial'. */
27
31
  readonly partial: number;
28
- /** Count of entries with backing === 'prose-only' AND informational !== true. */
32
+ /** Count of entries with backing === 'prose-only'. THE numerator. */
29
33
  readonly proseOnly: number;
30
- /** Count of entries with informational === true (excluded from ratio). */
34
+ /**
35
+ * Breakdown only — the subset of `proseOnly` that carries
36
+ * `informational: true` (auto-discovered advisory phrases with no
37
+ * catalog template). Included in `proseOnly`; changing it changes
38
+ * nothing about `proseOnly` or `ratio`.
39
+ */
40
+ readonly discoveredProseOnly: number;
41
+ /** Count of entries with informational === true, whatever their backing. */
31
42
  readonly informational: number;
32
43
  /** proseOnly / totalRedLines. Returns 0 when totalRedLines === 0. */
33
44
  readonly ratio: number;