peaks-loop 4.0.42 → 4.0.44

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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/_register.js +4 -0
  5. package/dist/cli/commands/api-diff-commands.d.ts +16 -0
  6. package/dist/cli/commands/api-diff-commands.js +55 -0
  7. package/dist/cli/commands/audit-commands.d.ts +16 -3
  8. package/dist/cli/commands/audit-commands.js +84 -31
  9. package/dist/cli/commands/codegraph-commands.js +191 -6
  10. package/dist/cli/commands/final-review-commands.d.ts +34 -10
  11. package/dist/cli/commands/final-review-commands.js +130 -34
  12. package/dist/cli/commands/job-commands.js +4 -2
  13. package/dist/cli/commands/scan-commands.js +1 -1
  14. package/dist/cli/commands/share-commands.d.ts +49 -0
  15. package/dist/cli/commands/share-commands.js +114 -14
  16. package/dist/cli/commands/test-commands.d.ts +60 -3
  17. package/dist/cli/commands/test-commands.js +125 -7
  18. package/dist/services/audit/audit-goal-service.js +38 -3
  19. package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
  20. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
  21. package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
  22. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
  23. package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
  24. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
  25. package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
  26. package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
  27. package/dist/services/codegraph/codegraph-service.d.ts +0 -1
  28. package/dist/services/codegraph/codegraph-service.js +5 -4
  29. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
  30. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
  31. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
  32. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
  33. package/dist/services/doctor/doctor-service/plugin-registry.js +4 -0
  34. package/dist/services/doctor/doctor-service/types.d.ts +47 -0
  35. package/dist/services/final-review/final-review-service.d.ts +154 -0
  36. package/dist/services/final-review/final-review-service.js +621 -7
  37. package/dist/services/final-review/index.d.ts +1 -1
  38. package/dist/services/final-review/index.js +1 -1
  39. package/dist/services/llm/anthropic-runner.d.ts +87 -0
  40. package/dist/services/llm/anthropic-runner.js +171 -0
  41. package/dist/services/llm/stub-runner.d.ts +11 -0
  42. package/dist/services/llm/stub-runner.js +33 -0
  43. package/dist/services/prd/handoff-auto-regen.js +0 -1
  44. package/dist/services/prd/handoff-service.d.ts +9 -1
  45. package/dist/services/prd/handoff-service.js +48 -6
  46. package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
  47. package/dist/services/scan/api-diff-openapi.d.ts +32 -0
  48. package/dist/services/scan/api-diff-openapi.js +359 -0
  49. package/dist/services/scan/api-diff-recorded.d.ts +96 -0
  50. package/dist/services/scan/api-diff-recorded.js +577 -0
  51. package/dist/services/scan/api-diff-service.d.ts +34 -0
  52. package/dist/services/scan/api-diff-service.js +407 -0
  53. package/dist/services/scan/api-diff-types.d.ts +116 -0
  54. package/dist/services/scan/api-diff-types.js +46 -0
  55. package/dist/services/scan/archetype-service.js +27 -1
  56. package/dist/services/scan/existing-system-service.js +17 -4
  57. package/dist/services/scan/hook-convention-service.d.ts +26 -0
  58. package/dist/services/scan/hook-convention-service.js +562 -0
  59. package/dist/services/scan/scan-types.d.ts +47 -0
  60. package/dist/services/session/caller-binding-service.d.ts +28 -0
  61. package/dist/services/session/caller-binding-service.js +10 -2
  62. package/dist/services/session/caller-id-types.d.ts +12 -2
  63. package/dist/services/session/index.d.ts +2 -2
  64. package/dist/services/session/index.js +2 -2
  65. package/dist/services/session/session-binding-bridge.js +11 -6
  66. package/dist/services/session/session-manager.d.ts +33 -1
  67. package/dist/services/session/session-manager.js +84 -25
  68. package/dist/services/skills/skill-presence-service.d.ts +17 -3
  69. package/dist/services/skills/skill-presence-service.js +23 -3
  70. package/package.json +7 -5
  71. package/skills/bee/peaks-rd/SKILL.md +11 -3
  72. package/skills/peaks-code/references/existing-system-extraction.md +5 -1
  73. package/skills/peaks-code/references/frontend-only-mode.md +48 -6
  74. package/skills/peaks-code/references/project-scan-checklist.md +20 -1
  75. package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
  76. package/skills/peaks-final-review/SKILL.md +43 -32
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Check: codegraph exclude integrity (`capability:codegraph-exclude-integrity`).
3
+ *
4
+ * The companion to `capability:codegraph`. That check answers "is the
5
+ * upstream package resolvable at the pinned version"; this one answers
6
+ * "does the project's `.codegraph/config.json` exclude rules silently
7
+ * drop source files git tracks".
8
+ *
9
+ * Why it exists: upstream ships a default `exclude` template matched by
10
+ * *directory name*, and `config.json`'s `exclude` array replaces those
11
+ * defaults wholesale. Any default rule whose directory name collides
12
+ * with a real source directory drops tracked files from the index while
13
+ * `peaks codegraph status` still printed `[OK] Index is up to date`.
14
+ *
15
+ * Read-only by construction — it consumes the same inspector
16
+ * `peaks codegraph status` gates on and never writes the config.
17
+ *
18
+ * Failure posture:
19
+ * - codegraph not initialized in the inspected root (no
20
+ * `.codegraph/config.json`) → `ok: true`; there is nothing to
21
+ * reconcile and a fresh clone must not fail the doctor.
22
+ * - a confirmed gap → `ok: false` (blocking; the index is provably
23
+ * incomplete and the fix is one command).
24
+ * - could not evaluate (not a git work tree, malformed config) →
25
+ * `ok: false, severity: 'warning'` so the doctor reports the blind
26
+ * spot without flipping the exit code on an unrelated failure.
27
+ */
28
+ import { getErrorMessage } from 'peaks-loop-shared/result';
29
+ import { inspectCodegraphExcludeIntegrity, isCodegraphExcludeConfigPresent } from '../../../codegraph/codegraph-exclude-integrity.js';
30
+ const CHECK_ID = 'capability:codegraph-exclude-integrity';
31
+ /** How many rules / offending files the message names before eliding. */
32
+ const MAX_NAMED = 5;
33
+ function defaultProbe() {
34
+ const projectRoot = process.cwd();
35
+ // No config → codegraph was never initialized here, so no exclude
36
+ // list is in play and there is nothing to report.
37
+ return isCodegraphExcludeConfigPresent(projectRoot)
38
+ ? inspectCodegraphExcludeIntegrity(projectRoot)
39
+ : null;
40
+ }
41
+ function renderGapMessage(excludedTrackedCount, trackedSourceCount, rulesToRemove, violations) {
42
+ const namedRules = rulesToRemove.slice(0, MAX_NAMED).join(', ');
43
+ const elidedRules = rulesToRemove.length > MAX_NAMED ? `, … (+${rulesToRemove.length - MAX_NAMED})` : '';
44
+ const namedFiles = violations
45
+ .slice(0, MAX_NAMED)
46
+ .map((violation) => `${violation.path} <- ${violation.matchedRule}`)
47
+ .join('; ');
48
+ const elidedFiles = violations.length > MAX_NAMED ? `; … (+${violations.length - MAX_NAMED})` : '';
49
+ return `codegraph index is incomplete: ${excludedTrackedCount} of ${trackedSourceCount} tracked source files are blocked by ${rulesToRemove.length} exclude rule(s) [${namedRules}${elidedRules}]. Blocked: ${namedFiles}${elidedFiles}. Run \`peaks codegraph repair-exclude --project <root>\` to drop them and rebuild the index.`;
50
+ }
51
+ function run({ options }) {
52
+ const probe = options.codegraphIntegrityProbe ?? defaultProbe;
53
+ let report;
54
+ try {
55
+ report = probe();
56
+ }
57
+ catch (error) {
58
+ return [{
59
+ id: CHECK_ID,
60
+ ok: false,
61
+ severity: 'warning',
62
+ message: `codegraph exclude integrity could not be evaluated: ${getErrorMessage(error)}`
63
+ }];
64
+ }
65
+ if (report === null) {
66
+ return [{
67
+ id: CHECK_ID,
68
+ ok: true,
69
+ message: 'codegraph is not initialized in this project (no .codegraph/config.json); the exclude list is not in play yet'
70
+ }];
71
+ }
72
+ if (!report.gap) {
73
+ return [{
74
+ id: CHECK_ID,
75
+ ok: true,
76
+ message: `codegraph exclude list drops no tracked source file (${report.trackedSourceCount} tracked source file(s) admitted by include)`
77
+ }];
78
+ }
79
+ return [{
80
+ id: CHECK_ID,
81
+ ok: false,
82
+ message: renderGapMessage(report.excludedTrackedCount, report.trackedSourceCount, report.rulesToRemove, report.violations)
83
+ }];
84
+ }
85
+ export const check = {
86
+ name: 'codegraph-exclude-integrity',
87
+ run
88
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Check: the third-party ECC plugin ships `hooks/hooks.json` keys that
3
+ * Claude Code's plugin hook schema ignores
4
+ * (`integration:ecc-hooks-schema-drift`).
5
+ *
6
+ * 2026-09-12 — Claude Code prints this at startup when the ECC plugin
7
+ * (github.com/affaan-m/ECC) is installed:
8
+ *
9
+ * ecc: hooks.json: unknown keys "$schema", "description" in
10
+ * hooks.PreToolUse[0], "id" in hooks.PreToolUse[0], ... and 42 more ignored
11
+ *
12
+ * Claude Code's plugin hook schema accepts exactly `{ matcher, hooks }`
13
+ * on a matcher group, and at the document root `hooks` plus an OPTIONAL
14
+ * top-level `description`. ECC ships a
15
+ * root `$schema` plus `description` AND `id` on each of its 23 matcher
16
+ * groups across 7 events — 47 ignored keys, matching the warning
17
+ * verbatim. The warning is cosmetic (the 23 hooks still load).
18
+ *
19
+ * peaks-loop does NOT write this file, and every ECC release checked
20
+ * (v2.2.0 / v2.2.1 / main) carries the same keys, so upgrading the
21
+ * plugin does not clear the warning. This check exists so a user who
22
+ * hits the startup line does not have to re-investigate it from
23
+ * scratch.
24
+ *
25
+ * Probing is split out of the check so the check itself stays a pure
26
+ * mapping over `EccHooksDriftProbeResult`. Tests inject the probe to
27
+ * keep the real `~/.claude/plugins/` tree out of fixtures.
28
+ */
29
+ import type { DoctorCheckPlugin, EccHooksDriftProbeResult } from '../types.js';
30
+ /**
31
+ * Unknown keys found in a plugin `hooks.json`. Mirrors the shape of
32
+ * Claude Code's own startup warning so the doctor message can name the
33
+ * same things.
34
+ */
35
+ export type EccHooksDriftFinding = {
36
+ /** Total ignored keys (`rootKeys` + every unknown key on every matcher group). */
37
+ readonly unknownKeyCount: number;
38
+ /** Unknown keys directly under the document root (e.g. `$schema`). */
39
+ readonly rootKeys: ReadonlyArray<string>;
40
+ /** Distinct unknown keys seen on matcher groups (e.g. `description`, `id`). */
41
+ readonly entryKeys: ReadonlyArray<string>;
42
+ /** Matcher groups carrying at least one unknown key. */
43
+ readonly entryCount: number;
44
+ };
45
+ /**
46
+ * Pure mapping over a parsed plugin `hooks.json` payload. Exported so
47
+ * tests drive the key scan without touching the real plugin tree.
48
+ */
49
+ export declare function findEccHooksSchemaDrift(payload: unknown): EccHooksDriftFinding;
50
+ /**
51
+ * Resolve the ECC plugin's install path from Claude Code's plugin
52
+ * manifest (`~/.claude/plugins/installed_plugins.json`), which is the
53
+ * only version-agnostic way to find the versioned cache directory
54
+ * (`…/plugins/cache/ecc/ecc/<version>/`). Exported so tests drive the
55
+ * lookup with an explicit manifest path.
56
+ */
57
+ export declare function readEccInstallPath(manifestPath: string): string | null;
58
+ /**
59
+ * Default probe: reads the ECC plugin manifest + its `hooks/hooks.json`.
60
+ * `homeDir` is injectable so tests can drive the real probe against a
61
+ * temp dir; the zero-arg call keeps using the real homedir, so the
62
+ * function stays assignable to `EccHooksDriftProbe`.
63
+ */
64
+ export declare function defaultEccHooksDriftProbe(homeDir?: string): EccHooksDriftProbeResult;
65
+ export declare const check: DoctorCheckPlugin;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Check: the third-party ECC plugin ships `hooks/hooks.json` keys that
3
+ * Claude Code's plugin hook schema ignores
4
+ * (`integration:ecc-hooks-schema-drift`).
5
+ *
6
+ * 2026-09-12 — Claude Code prints this at startup when the ECC plugin
7
+ * (github.com/affaan-m/ECC) is installed:
8
+ *
9
+ * ecc: hooks.json: unknown keys "$schema", "description" in
10
+ * hooks.PreToolUse[0], "id" in hooks.PreToolUse[0], ... and 42 more ignored
11
+ *
12
+ * Claude Code's plugin hook schema accepts exactly `{ matcher, hooks }`
13
+ * on a matcher group, and at the document root `hooks` plus an OPTIONAL
14
+ * top-level `description`. ECC ships a
15
+ * root `$schema` plus `description` AND `id` on each of its 23 matcher
16
+ * groups across 7 events — 47 ignored keys, matching the warning
17
+ * verbatim. The warning is cosmetic (the 23 hooks still load).
18
+ *
19
+ * peaks-loop does NOT write this file, and every ECC release checked
20
+ * (v2.2.0 / v2.2.1 / main) carries the same keys, so upgrading the
21
+ * plugin does not clear the warning. This check exists so a user who
22
+ * hits the startup line does not have to re-investigate it from
23
+ * scratch.
24
+ *
25
+ * Probing is split out of the check so the check itself stays a pure
26
+ * mapping over `EccHooksDriftProbeResult`. Tests inject the probe to
27
+ * keep the real `~/.claude/plugins/` tree out of fixtures.
28
+ */
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import { homedir } from 'node:os';
31
+ import { join } from 'node:path';
32
+ import { getErrorMessage } from 'peaks-loop-shared/result';
33
+ const CHECK_ID = 'integration:ecc-hooks-schema-drift';
34
+ /** Claude Code's plugin hook schema accepts exactly these keys on a matcher group. */
35
+ const ALLOWED_MATCHER_GROUP_KEYS = ['matcher', 'hooks'];
36
+ /**
37
+ * …and at the document root: `hooks`, plus an OPTIONAL top-level
38
+ * `description` (Claude Code's plugin hooks docs, "Reference scripts by
39
+ * path", document a top-level `description` for `hooks/hooks.json` and
40
+ * place it as a sibling of `hooks`). A top-level `description` is
41
+ * therefore legal — and is exactly the shape an upstream ECC fix would
42
+ * land on when it consolidates its 23 per-matcher descriptions into one.
43
+ */
44
+ const ALLOWED_ROOT_KEYS = ['hooks', 'description'];
45
+ const NO_DRIFT = {
46
+ unknownKeyCount: 0,
47
+ rootKeys: [],
48
+ entryKeys: [],
49
+ entryCount: 0
50
+ };
51
+ function isPlainObject(value) {
52
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
53
+ }
54
+ /**
55
+ * Pure mapping over a parsed plugin `hooks.json` payload. Exported so
56
+ * tests drive the key scan without touching the real plugin tree.
57
+ */
58
+ export function findEccHooksSchemaDrift(payload) {
59
+ if (!isPlainObject(payload))
60
+ return NO_DRIFT;
61
+ const rootKeys = Object.keys(payload).filter((key) => !ALLOWED_ROOT_KEYS.includes(key));
62
+ const hooks = payload.hooks;
63
+ if (!isPlainObject(hooks)) {
64
+ return { ...NO_DRIFT, unknownKeyCount: rootKeys.length, rootKeys };
65
+ }
66
+ const entryKeys = new Set();
67
+ let entryKeyTotal = 0;
68
+ let entryCount = 0;
69
+ for (const groups of Object.values(hooks)) {
70
+ if (!Array.isArray(groups))
71
+ continue;
72
+ for (const group of groups) {
73
+ if (!isPlainObject(group))
74
+ continue;
75
+ const unknown = Object.keys(group).filter((key) => !ALLOWED_MATCHER_GROUP_KEYS.includes(key));
76
+ if (unknown.length === 0)
77
+ continue;
78
+ entryCount += 1;
79
+ entryKeyTotal += unknown.length;
80
+ for (const key of unknown)
81
+ entryKeys.add(key);
82
+ }
83
+ }
84
+ return {
85
+ unknownKeyCount: rootKeys.length + entryKeyTotal,
86
+ rootKeys,
87
+ entryKeys: [...entryKeys].sort(),
88
+ entryCount
89
+ };
90
+ }
91
+ function readJsonIfPresent(path) {
92
+ if (!existsSync(path))
93
+ return null;
94
+ // Parse errors intentionally propagate to the check's own catch, which
95
+ // reports them as a `skipping check` message instead of swallowing them.
96
+ return JSON.parse(readFileSync(path, 'utf8'));
97
+ }
98
+ /**
99
+ * Resolve the ECC plugin's install path from Claude Code's plugin
100
+ * manifest (`~/.claude/plugins/installed_plugins.json`), which is the
101
+ * only version-agnostic way to find the versioned cache directory
102
+ * (`…/plugins/cache/ecc/ecc/<version>/`). Exported so tests drive the
103
+ * lookup with an explicit manifest path.
104
+ */
105
+ export function readEccInstallPath(manifestPath) {
106
+ const manifest = readJsonIfPresent(manifestPath);
107
+ if (!isPlainObject(manifest))
108
+ return null;
109
+ const plugins = manifest.plugins;
110
+ if (!isPlainObject(plugins))
111
+ return null;
112
+ for (const [name, records] of Object.entries(plugins)) {
113
+ if (!name.startsWith('ecc@'))
114
+ continue;
115
+ if (!Array.isArray(records))
116
+ continue;
117
+ for (const record of records) {
118
+ if (!isPlainObject(record))
119
+ continue;
120
+ const installPath = record.installPath;
121
+ if (typeof installPath === 'string' && installPath.length > 0)
122
+ return installPath;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ /**
128
+ * Default probe: reads the ECC plugin manifest + its `hooks/hooks.json`.
129
+ * `homeDir` is injectable so tests can drive the real probe against a
130
+ * temp dir; the zero-arg call keeps using the real homedir, so the
131
+ * function stays assignable to `EccHooksDriftProbe`.
132
+ */
133
+ export function defaultEccHooksDriftProbe(homeDir = homedir()) {
134
+ const manifestPath = join(homeDir, '.claude', 'plugins', 'installed_plugins.json');
135
+ const installPath = readEccInstallPath(manifestPath);
136
+ if (installPath === null)
137
+ return { hooksPath: null, hooks: null };
138
+ const hooksPath = join(installPath, 'hooks', 'hooks.json');
139
+ return { hooksPath, hooks: readJsonIfPresent(hooksPath) };
140
+ }
141
+ function run({ options }) {
142
+ const probe = options.eccHooksDriftProbe ?? defaultEccHooksDriftProbe;
143
+ try {
144
+ const { hooksPath, hooks } = probe();
145
+ if (hooksPath === null) {
146
+ return [{
147
+ id: CHECK_ID,
148
+ ok: true,
149
+ message: 'ECC plugin not installed (no `ecc@*` entry in ~/.claude/plugins/installed_plugins.json); no plugin hook schema drift to report'
150
+ }];
151
+ }
152
+ if (hooks === null) {
153
+ return [{
154
+ id: CHECK_ID,
155
+ ok: true,
156
+ message: `No readable ECC plugin hooks.json at ${hooksPath}; no plugin hook schema drift to report`
157
+ }];
158
+ }
159
+ const finding = findEccHooksSchemaDrift(hooks);
160
+ if (finding.unknownKeyCount === 0) {
161
+ return [{
162
+ id: CHECK_ID,
163
+ ok: true,
164
+ message: `ECC plugin hooks.json at ${hooksPath} carries only the keys Claude Code accepts (matcher/hooks per matcher group); the startup "unknown keys ... ignored" warning will not appear`
165
+ }];
166
+ }
167
+ const rootPart = finding.rootKeys.length === 0 ? '' : `at the root: ${finding.rootKeys.join(', ')}; `;
168
+ return [{
169
+ id: CHECK_ID,
170
+ ok: false,
171
+ severity: 'warning',
172
+ message: `ECC plugin hooks.json at ${hooksPath} carries ${finding.unknownKeyCount} key(s) that Claude Code's plugin hook schema ignores (${rootPart}on ${finding.entryCount} matcher group(s): ${finding.entryKeys.join(', ')}). Claude Code prints \`ecc: hooks.json: unknown keys ... ignored\` at startup; every hook still loads, so this warning is cosmetic. Source: the third-party ECC plugin (github.com/affaan-m/ECC) ships these keys in every release (v2.2.0 / v2.2.1 / main) — peaks-loop does NOT write this file. Fix: none inside peaks-loop; upstream ECC must drop them from its hooks/hooks.json (its scripts/ci/validate-hooks.js validates shape only, never a key allow-list, so ECC's own CI stays green). Upgrading ECC will not help.`
173
+ }];
174
+ }
175
+ catch (error) {
176
+ return [{
177
+ id: CHECK_ID,
178
+ ok: true,
179
+ message: `ECC hooks schema-drift probe failed (${getErrorMessage(error)}); skipping check`
180
+ }];
181
+ }
182
+ }
183
+ export const check = {
184
+ name: 'ecc-hooks-schema-drift',
185
+ run
186
+ };
@@ -42,10 +42,12 @@ import { check as workspaceInit } from './checks/workspace-init.js';
42
42
  import { check as statuslineInstall } from './checks/statusline-install.js';
43
43
  import { check as statuslineRuntime } from './checks/statusline-runtime.js';
44
44
  import { check as codegraphCapability } from './checks/codegraph-capability.js';
45
+ import { check as codegraphExcludeIntegrity } from './checks/codegraph-exclude-integrity.js';
45
46
  import { check as distSourceVersion } from './checks/dist-source-version.js';
46
47
  import { check as multiBinaryDrift } from './checks/multi-binary-drift.js';
47
48
  import { check as workspaceLayout } from './checks/workspace-layout.js';
48
49
  import { check as gateguardConflict } from './checks/gateguard-conflict.js';
50
+ import { check as eccHooksSchemaDrift } from './checks/ecc-hooks-schema-drift.js';
49
51
  import { check as checkIdSchema } from './checks/check-id-schema.js';
50
52
  import { check as l3OrphanSessions } from './checks/l3-orphan-sessions.js';
51
53
  import { check as l3MemoryHealth } from './checks/l3-memory-health.js';
@@ -69,10 +71,12 @@ export const PLUGINS = [
69
71
  statuslineInstall, // id "statusline:install"
70
72
  statuslineRuntime, // id "statusline:runtime"
71
73
  codegraphCapability, // id "capability:codegraph"
74
+ codegraphExcludeIntegrity, // id "capability:codegraph-exclude-integrity"
72
75
  distSourceVersion, // id "build:dist-version-matches-source"
73
76
  multiBinaryDrift, // id "build:multi-binary-drift"
74
77
  workspaceLayout, // id "build:workspace-layout-canonical"
75
78
  gateguardConflict, // id "integration:gateguard-peaks-conflict"
79
+ eccHooksSchemaDrift, // id "integration:ecc-hooks-schema-drift"
76
80
  checkIdSchema, // id "doctor-self:check-id-pattern"
77
81
  l3OrphanSessions, // id "L3:l3-orphan-sessions"
78
82
  l3MemoryHealth, // id "L3:l3-memory-health"
@@ -75,6 +75,25 @@ export type CodegraphCapabilityProbe = {
75
75
  */
76
76
  managedPath: CodegraphManagedPathInfo | null;
77
77
  };
78
+ /**
79
+ * Structural shape of the codegraph exclude-integrity report the
80
+ * `capability:codegraph-exclude-integrity` check gates on. Declared
81
+ * structurally (rather than imported from the codegraph service) to
82
+ * keep this type module dependency-free — the default probe returns a
83
+ * `CodegraphExcludeIntegrityReport`, which is assignable here.
84
+ */
85
+ export type CodegraphExcludeIntegrityProbe = {
86
+ readonly configPath: string;
87
+ readonly gap: boolean;
88
+ readonly trackedSourceCount: number;
89
+ readonly excludedTrackedCount: number;
90
+ readonly rulesToRemove: readonly string[];
91
+ /** One entry per (file, rule) pair. */
92
+ readonly violations: readonly {
93
+ readonly path: string;
94
+ readonly matchedRule: string;
95
+ }[];
96
+ };
78
97
  export type DistVersionComparison = {
79
98
  dist: string | null;
80
99
  source: string;
@@ -165,6 +184,24 @@ export type GateguardProbeResult = {
165
184
  projectSettings: unknown;
166
185
  };
167
186
  export type GateguardProbe = () => GateguardProbeResult;
187
+ /**
188
+ * 2026-09-12 — the third-party ECC plugin (github.com/affaan-m/ECC)
189
+ * ships `$schema` at the root of its `hooks/hooks.json` plus
190
+ * `description` + `id` on every matcher group. Claude Code's plugin
191
+ * hook schema accepts only `{ matcher, hooks }` per matcher group, and
192
+ * at the root `hooks` plus an OPTIONAL top-level `description`, so it
193
+ * prints an `unknown keys ... ignored`
194
+ * line at startup for the 47 extra keys (cosmetic — the hooks still
195
+ * load). The probe is injected so tests never read the real
196
+ * `~/.claude/plugins/` tree.
197
+ */
198
+ export type EccHooksDriftProbeResult = {
199
+ /** Absolute path to the ECC plugin's `hooks/hooks.json` (null when the plugin is not installed). */
200
+ hooksPath: string | null;
201
+ /** Parsed `hooks/hooks.json` payload (null when missing / unreadable). */
202
+ hooks: unknown;
203
+ };
204
+ export type EccHooksDriftProbe = () => EccHooksDriftProbeResult;
168
205
  /**
169
206
  * Subset of SkillPresence consumed by the doctor (slice-3b: the full
170
207
  * `SkillPresence` type lives in `src/services/skills/skill-presence-service.ts`;
@@ -211,6 +248,14 @@ export type DoctorOptions = {
211
248
  * `process.cwd()`.
212
249
  */
213
250
  codegraphManagedPathProbe?: () => CodegraphManagedPathInfo | null;
251
+ /**
252
+ * Optional override for the `capability:codegraph-exclude-integrity`
253
+ * check. Returns the integrity report, or `null` when codegraph is
254
+ * not initialized in the inspected root (nothing to reconcile). When
255
+ * omitted, the check inspects `process.cwd()`. Throwing is allowed
256
+ * and reported as a non-blocking warning.
257
+ */
258
+ codegraphIntegrityProbe?: () => CodegraphExcludeIntegrityProbe | null;
214
259
  skillPresenceProbe?: () => DoctorSkillPresence | null;
215
260
  skillPresenceFreshnessThresholdMs?: number;
216
261
  statusLineInstalledProbe?: () => boolean;
@@ -230,6 +275,8 @@ export type DoctorOptions = {
230
275
  workspaceLayoutProbe?: WorkspaceLayoutProbe;
231
276
  /** Injected for the integration:gateguard-peaks-conflict check (defaults to defaultGateguardProbe on disk). */
232
277
  gateguardProbe?: GateguardProbe;
278
+ /** Injected for the integration:ecc-hooks-schema-drift check (defaults to defaultEccHooksDriftProbe on disk). */
279
+ eccHooksDriftProbe?: EccHooksDriftProbe;
233
280
  /**
234
281
  * Slice 2026-06-13-repair-pre-existing-test-failures: injected
235
282
  * root for the L3:l3-memory-health check (defaults to
@@ -20,6 +20,160 @@ export declare class IncompleteFinalReviewError extends Error {
20
20
  readonly code: "INCOMPLETE_FINAL_REVIEW";
21
21
  constructor(message: string);
22
22
  }
23
+ /**
24
+ * N4 — the reply carried no text block at all.
25
+ *
26
+ * Measured 2/3 on this repo's own machine, and it is NOT truncation: the
27
+ * provider answered with a response whose `content` has no `text` block (a
28
+ * reasoning-only turn, a refusal, or a content filter), so there is no JSON to
29
+ * parse and no budget to raise — an operator sent to "raise the budget" for
30
+ * this failure would be sent the wrong way. It gets its own class, its own
31
+ * `code`, and a message that says so, so it is diagnosable instead of being
32
+ * flattened into "not valid JSON".
33
+ */
34
+ export declare class EmptyReviewReplyError extends Error {
35
+ readonly code: "EMPTY_FINAL_REVIEW_REPLY";
36
+ constructor(message: string);
37
+ }
38
+ /**
39
+ * How many times an empty reply is retried before it is reported. The failure
40
+ * was 2/3 on the observed machine — intermittent, not systematic — so a small
41
+ * bounded retry converts most of it into a completed review, while 3 attempts
42
+ * keeps a genuinely broken provider from being hammered.
43
+ */
44
+ export declare const MAX_EMPTY_REPLY_ATTEMPTS = 3;
45
+ /**
46
+ * Per-file evidence cap. Real evidence artifacts in this repo run 11–18 KB
47
+ * (`rd/tech-doc.md`, `rd/code-review.md`, `qa/*-findings-*.md`); 8 KB keeps the
48
+ * head of every file (header + verdict + first tables) without letting one
49
+ * verbose artifact crowd out the other sources. Enforced in BYTES against the
50
+ * raw buffer, so multi-byte (CJK) content cannot slip past the cap.
51
+ */
52
+ export declare const MAX_EVIDENCE_BYTES_PER_FILE: number;
53
+ /**
54
+ * Total evidence budget across all sources. 32 KB ≈ 8k tokens of input, which
55
+ * keeps the prompt far inside any modern context window. Sources that do not
56
+ * fit are reported as OMITTED — never dropped silently.
57
+ *
58
+ * This is an INPUT cap and stays fixed. The output ceiling that has to sit
59
+ * opposite it is derived per call by `outputBudgetForEvidence()` below — the
60
+ * two used to drift apart, and that drift was the defect.
61
+ */
62
+ export declare const MAX_EVIDENCE_BYTES_TOTAL: number;
63
+ /**
64
+ * Reserved floor per dimension — the anti-starvation guarantee.
65
+ *
66
+ * The allocator below used to be strictly first-come-first-served: each source
67
+ * took `min(perFileCap, budgetLeft)` in source order. With this repo's own
68
+ * 9-source evidence set (`2026-09-12-session-e37ef0`, measured) sources 1-4
69
+ * consumed the whole 32 KiB — 4 x 8,192 = 32,768, the cap to the byte — before
70
+ * source 5 was even opened. `existing-functionality-intact` is supplied ONLY by
71
+ * `rd/tech-doc.md` (6th) and `prd/handoff.md` (9th), so that one dimension
72
+ * reached the reviewer with zero evidence on every run and its verdict was
73
+ * structurally locked to `inconclusive` no matter how good the work was. A gate
74
+ * that is always red is noise, and an operator trained to ignore noise has no
75
+ * gate at all — the same harm as a gate that never fires, only quieter.
76
+ *
77
+ * So each dimension with at least one readable source on disk gets one floor
78
+ * reserved for the FIRST such source, and no source that is not that holder may
79
+ * spend it. The reservation is a floor, never a quota: it is released the
80
+ * instant its holder is served, and whatever the holder does not use flows back
81
+ * into the sequential allocation unchanged.
82
+ *
83
+ * Why 4 KiB: the per-file cap exists to keep the "header + verdict + first
84
+ * tables" — the part a reviewer actually cites. Measured on the same run's
85
+ * artifacts (9 files, 8,164-20,543 bytes each): every one of them states its
86
+ * verdict inside the first ~700 bytes. 4 KiB is ~5x that, so a floor holder is
87
+ * not there for depth — it is there so its dimension is not blind. Four
88
+ * dimensions x 4 KiB = 16 KiB of the 32 KiB cap, so at least half the budget
89
+ * still flows through the sequential path below.
90
+ */
91
+ export declare const MIN_EVIDENCE_BYTES_PER_DIMENSION: number;
92
+ /**
93
+ * Floor — also the value that shipped before this fix, so no evidence set can
94
+ * end up with a smaller budget than it had. ~3000 tokens is enough for the
95
+ * envelope skeleton plus a short paragraph per dimension.
96
+ */
97
+ export declare const MIN_OUTPUT_TOKENS = 3000;
98
+ /**
99
+ * Headroom for the part of the reply that is not the envelope.
100
+ *
101
+ * A Messages-API-compatible endpoint applies `max_tokens` to the WHOLE
102
+ * response, and a reasoning model spends it on hidden reasoning before it
103
+ * emits a single character of the 4-dim envelope. Measured on this repo's own
104
+ * machine (2026-09-12, rid `2026-09-12-codegraph-exclude-integrity`,
105
+ * `deepseek-flash[1M]` via `api.deepseek.com/anthropic`): `max_tokens=8192`
106
+ * came back with `output_tokens=8192` and only **574** visible characters —
107
+ * the entire budget went to reasoning. A bytes-per-token estimate of the
108
+ * visible output cannot see that cost, which is why the previous formula
109
+ * budgeted 7096 for a reply that needs 10108 — and why a 13240 budget still
110
+ * truncated on 2 of 10 real runs.
111
+ *
112
+ * 12288 (12 KiB) is sized so the largest evidence pack the input caps allow
113
+ * lands at 23480 (see the formula below) — about 1.8x the largest value ever
114
+ * OBSERVED to truncate (13240), which is the margin the observed variance
115
+ * asks for. The numbers are in the block comment above.
116
+ */
117
+ export declare const REASONING_HEADROOM_TOKENS: number;
118
+ /**
119
+ * Ceiling, 32_000: the value a real run on this machine was forced to in order
120
+ * to complete the envelope at all, and the largest this endpoint was observed
121
+ * to accept. 16384 was tried first and truncated 2/10 — a ceiling that is
122
+ * merely "above the last successful measurement" is not above the requirement,
123
+ * because the requirement moves with the model's reasoning spend.
124
+ *
125
+ * A model that caps output at 8192 will refuse this. That is still strictly
126
+ * better than shipping a budget measured to be too small, and the env lever
127
+ * below lets an operator pull it down without a code change.
128
+ */
129
+ export declare const MAX_OUTPUT_TOKENS = 32000;
130
+ /**
131
+ * Environment lever. The old failure message told the operator to "raise the
132
+ * budget" while the budget was a module constant with no CLI flag and no env
133
+ * var — an instruction that could not be carried out from any surface the
134
+ * operator has. This is that lever.
135
+ *
136
+ * The value is the output ceiling in tokens; it OVERRIDES the derivation below
137
+ * (it is not a bonus added to it). Unset/invalid/out-of-range handling is in
138
+ * `resolveOutputBudget`.
139
+ */
140
+ export declare const MAX_OUTPUT_TOKENS_ENV = "PEAKS_FINAL_REVIEW_MAX_OUTPUT_TOKENS";
141
+ /**
142
+ * Absolute upper bound the env lever may reach. An endpoint that accepts
143
+ * `max_tokens` at all accepts this; anything above it is a typo (a stray extra
144
+ * digit), not an intent, and clamping is safer than sending it.
145
+ */
146
+ export declare const HARD_MAX_OUTPUT_TOKENS = 64000;
147
+ /**
148
+ * Inlined bytes that buy one extra output token — 4:1.
149
+ *
150
+ * This term prices the visible envelope (4 x `summary` + `evidence[]` +
151
+ * `confidence` + `overallSummary`) against the evidence the model is required
152
+ * to cite. It was 8:1, which put the 32 KiB pack at 4096 tokens of visible
153
+ * output; the same pack has been observed to complete at 10108 and to truncate
154
+ * at 13240, so 8:1 was pricing the visible side BELOW its own measurement.
155
+ * 4:1 doubles it to 8192. It is still not treated as the whole budget — see
156
+ * `REASONING_HEADROOM_TOKENS`.
157
+ */
158
+ export declare const EVIDENCE_BYTES_PER_OUTPUT_TOKEN = 4;
159
+ /**
160
+ * Output ceiling for a call whose prompt carries `includedEvidenceBytes` bytes
161
+ * of inlined evidence. Pure, total, and clamped on both ends — the same
162
+ * evidence pack always yields the same budget.
163
+ */
164
+ export declare function outputBudgetForEvidence(includedEvidenceBytes: number): number;
165
+ /**
166
+ * The budget the call actually uses: the derived one, unless
167
+ * `PEAKS_FINAL_REVIEW_MAX_OUTPUT_TOKENS` overrides it.
168
+ *
169
+ * An override that is not a positive integer THROWS rather than being ignored:
170
+ * a silent fallback would leave an operator who passed a bad value with the
171
+ * exact experience this lever exists to remove — a budget they cannot move.
172
+ * Out-of-range values are clamped, not rejected, so a model needing more than
173
+ * `HARD_MAX_OUTPUT_TOKENS` (or a model needing less than `MIN_OUTPUT_TOKENS`)
174
+ * still gets a call made.
175
+ */
176
+ export declare function resolveOutputBudget(includedEvidenceBytes: number, env?: NodeJS.ProcessEnv): number;
23
177
  export declare function prepareFinalReview(rid: string, opts: PrepareFinalReviewOptions): Promise<FinalReviewOutput>;
24
178
  export declare function decideFifthDimension(input: {
25
179
  readonly audit: CapabilityAuditResult | null;