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
@@ -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;
@@ -1,21 +1,25 @@
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
  /** Default target: 5% (per A3.1). */
21
25
  export const DEFAULT_PROSE_RATIO_TARGET = 0.05;
@@ -24,18 +28,20 @@ export function computeProseRatio(entries, options = {}) {
24
28
  let cliBacked = 0;
25
29
  let partial = 0;
26
30
  let proseOnly = 0;
31
+ let discoveredProseOnly = 0;
27
32
  let informational = 0;
28
33
  for (const entry of entries) {
29
- if (entry.informational === true) {
34
+ if (entry.informational === true)
30
35
  informational += 1;
31
- continue;
32
- }
33
36
  if (entry.backing === 'cli-backed')
34
37
  cliBacked += 1;
35
38
  else if (entry.backing === 'partial')
36
39
  partial += 1;
37
- else if (entry.backing === 'prose-only')
40
+ else if (entry.backing === 'prose-only') {
38
41
  proseOnly += 1;
42
+ if (entry.informational === true)
43
+ discoveredProseOnly += 1;
44
+ }
39
45
  }
40
46
  const totalRedLines = entries.length;
41
47
  const ratio = totalRedLines === 0 ? 0 : proseOnly / totalRedLines;
@@ -44,6 +50,7 @@ export function computeProseRatio(entries, options = {}) {
44
50
  cliBacked,
45
51
  partial,
46
52
  proseOnly,
53
+ discoveredProseOnly,
47
54
  informational,
48
55
  ratio,
49
56
  target,
@@ -216,7 +216,7 @@ const PRD_ARTIFACT_HANDOFF = {
216
216
  };
217
217
  const RD_HANDOFF_CONTRACT = {
218
218
  id: 'rl-rd-handoff-contract-001',
219
- rule: 'peaks-rd SKILL.md must declare the QA-handoff BLOCKING contract (tech-doc + perf-baseline)',
219
+ rule: 'peaks-rd must not hand off to QA without a non-empty RD artifact under rd/requests/',
220
220
  markers: ['BLOCKING'],
221
221
  phrases: ['do not hand off to qa without', 'tech-doc', 'perf-baseline'],
222
222
  enforcerRef: 'src/services/audit/enforcers/lint-rd-handoff-coverage.ts',
@@ -16,6 +16,8 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { classifyFiles } from './classifier.js';
18
18
  import { classifyBackingBatch } from './backing-detector.js';
19
+ import { computeLiveEnforcers } from './enforcer-liveness.js';
20
+ import { RED_LINE_CATALOG } from './red-line-catalog.js';
19
21
  import { scanSkillsTree } from './scanners/skills-tree-scanner.js';
20
22
  import { scanRulesTree } from './scanners/rules-tree-scanner.js';
21
23
  import { scanOpenSpecTree } from './scanners/openspec-scanner.js';
@@ -28,6 +30,7 @@ import { readSkillFiles, lintSectionShape, lintSectionOrder, lintFrontmatterShap
28
30
  import { lintRefPathResolves, lintNoBrokenMkdir, lintNoPwdSymlinkJumps, lintNoRelativeArchivePaths, } from './enforcers/lint-reference-integrity.js';
29
31
  import { lintCliBackMandatorText, lintCliBackNoOrphanBlocking, lintCliBackNoOrphanMustNot, } from './enforcers/lint-cli-back.js';
30
32
  import { lintNoFluff, lintNoClosingPrompt, lintStatusHeader, } from './enforcers/lint-output-style.js';
33
+ import { lintRdHandoffContract, lintRdCoverageDiscipline, } from './enforcers/lint-rd-handoff-coverage.js';
31
34
  import { lintOpenSpecAcceptanceBullets, lintOpenSpecSpecReference,
32
35
  // (Removed in v2.11.0 Group A: `lintTechDocPresenceShape`)
33
36
  lintPeaksDoctorAcknowledged, } from './enforcers/lint-workflow-shape.js';
@@ -59,15 +62,16 @@ function tally(entries) {
59
62
  let partial = 0;
60
63
  let proseOnly = 0;
61
64
  for (const entry of entries) {
62
- // v2.12.1 catalog governance: informational entries (auto-discovered
63
- // prose phrases without a catalog template) are counted in the
64
- // total but excluded from `proseOnly` so the ratio (per spec §10.2
65
- // ≤ 5%) reflects the actionable backlog only.
65
+ // S3 of the 2026-09-15 diagnosis-remediation job: `informational` is a
66
+ // triage label, not a ratio input. The pre-S3 version skipped
67
+ // informational rows here too, which is how the audit came to report
68
+ // `proseOnly: 0` while carrying 44 `"backing": "prose-only"` rows in
69
+ // the same envelope. Every prose-only row is now counted.
66
70
  if (entry.backing === 'cli-backed')
67
71
  cliBacked++;
68
72
  else if (entry.backing === 'partial')
69
73
  partial++;
70
- else if (!entry.informational)
74
+ else if (entry.backing === 'prose-only')
71
75
  proseOnly++;
72
76
  }
73
77
  return {
@@ -87,7 +91,12 @@ export function runRedLinesAudit(input) {
87
91
  const openspec = scanOpenSpecTree({ projectRoot: input.projectRoot });
88
92
  const fileInputs = buildFileInputs(skills, rules, openspec);
89
93
  const classified = classifyFiles(fileInputs);
90
- const backed = classifyBackingBatch(classified.entries, input.projectRoot);
94
+ // A9: `cli-backed` requires a call site, not just a file on disk. The
95
+ // live set is computed once for the whole catalog; `null` means the
96
+ // project has no `src/` tree and liveness is undecidable, in which case
97
+ // the detector falls back to the pre-A9 "file exists" rule.
98
+ const liveness = computeLiveEnforcers(input.projectRoot, RED_LINE_CATALOG.map((entry) => entry.enforcerRef).filter((ref) => ref !== null));
99
+ const backed = classifyBackingBatch(classified.entries, input.projectRoot, liveness.live);
91
100
  // Sub-agent-sid enforcer (Task 2): dogfoods Slice 0.5 sid-naming-guard.
92
101
  const subAgentSids = findInvalidSubAgentSids(input.projectRoot);
93
102
  const runtimeSids = findInvalidRuntimeSids(input.projectRoot);
@@ -97,7 +106,22 @@ export function runRedLinesAudit(input) {
97
106
  ...openspec.warnings,
98
107
  ...classified.warnings.map((message) => ({ file: '(classifier)', message })),
99
108
  ...backed.warnings.map((message) => ({ file: '(backing-detector)', message })),
109
+ ...liveness.warnings.map((message) => ({ file: '(enforcer-liveness)', message })),
100
110
  ];
111
+ // A9: name every enforcer that was downgraded, so the drop in
112
+ // `cliBacked` is attributable rather than mysterious.
113
+ if (liveness.unknown) {
114
+ warnings.push({
115
+ file: '(enforcer-liveness)',
116
+ message: 'no src/ tree under the project root; enforcer liveness is undecidable, so `cli-backed` falls back to the pre-A9 "the enforcer file exists" rule',
117
+ });
118
+ }
119
+ for (const ref of backed.deadEnforcers) {
120
+ warnings.push({
121
+ file: ref,
122
+ message: 'enforcer file exists but nothing outside src/services/audit/enforcers/ imports it; red lines backed by it are counted as prose-only',
123
+ });
124
+ }
101
125
  if (subAgentSids.scanned && subAgentSids.invalid.length > 0) {
102
126
  for (const sid of subAgentSids.invalid) {
103
127
  warnings.push({
@@ -261,15 +285,31 @@ export function runRedLinesAudit(input) {
261
285
  const skillsRoot = join(input.projectRoot, 'skills');
262
286
  if (existsSync(skillsRoot)) {
263
287
  const skillNames = [];
288
+ const skippedRoots = [];
264
289
  for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
265
290
  if (!entry.isDirectory())
266
291
  continue;
267
292
  if (entry.name.startsWith('.'))
268
293
  continue;
269
- if (!existsSync(join(skillsRoot, entry.name, 'SKILL.md')))
294
+ if (!existsSync(join(skillsRoot, entry.name, 'SKILL.md'))) {
295
+ skippedRoots.push(entry.name);
270
296
  continue;
297
+ }
271
298
  skillNames.push(entry.name);
272
299
  }
300
+ // A11 scope limit, declared rather than left implicit: only skills
301
+ // with a top-level `SKILL.md` are linted. Nested roots such as
302
+ // `skills/bee/` hold real skills (`peaks-perf-audit`, `peaks-rd`, …)
303
+ // that the classifier DOES see (their markers appear as
304
+ // `rl-discovered-skills-bee-*` rows) but that every Theme A–G
305
+ // lint-style enforcer skips. A clean Theme A result therefore means
306
+ // "clean among the linted skills", not "clean everywhere".
307
+ for (const root of skippedRoots) {
308
+ warnings.push({
309
+ file: `skills/${root}`,
310
+ message: 'no top-level SKILL.md — this directory and every skill nested under it are skipped by all lint-style enforcers (including rl-section-hard-contracts-001); their red-line rows are unverified',
311
+ });
312
+ }
273
313
  const skillFiles = readSkillFiles(skillsRoot, skillNames);
274
314
  for (const skill of skillFiles) {
275
315
  const refsDir = join(skillsRoot, skill.name, 'references');
@@ -291,6 +331,10 @@ export function runRedLinesAudit(input) {
291
331
  ...lintNoFluff(skill),
292
332
  ...lintNoClosingPrompt(skill),
293
333
  ...lintPeaksDoctorAcknowledged(skill),
334
+ // A10: the RD handoff gate reads the artifact, not the SKILL.md
335
+ // sentence that promises one. See lint-rd-handoff-coverage.ts.
336
+ ...lintRdHandoffContract(skill, input.projectRoot),
337
+ ...lintRdCoverageDiscipline(skill),
294
338
  ];
295
339
  for (const hit of lintHits) {
296
340
  enforcerFindings.push({
@@ -0,0 +1,15 @@
1
+ import { type CapabilityBaselineRow } from '../capability-baseline/types.js';
2
+ import type { GuardContract, GuardRunResult } from '../capability-guard-runner/types.js';
3
+ import type { IndependentCheckResult } from './types.js';
4
+ export interface IndependentCheckInput {
5
+ readonly projectRoot: string;
6
+ readonly baselineRows: ReadonlyArray<CapabilityBaselineRow>;
7
+ readonly contracts: ReadonlyArray<GuardContract>;
8
+ readonly guardResults: ReadonlyArray<GuardRunResult>;
9
+ }
10
+ /**
11
+ * Run the credential-free independent evaluation. The verdict is `drifted`
12
+ * whenever a concrete deviation is observed — this function has no path that
13
+ * returns `consistent` without having checked.
14
+ */
15
+ export declare function runIndependentCheck(input: IndependentCheckInput): IndependentCheckResult;