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.
- package/CHANGELOG.md +59 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/api-diff-commands.d.ts +16 -0
- package/dist/cli/commands/api-diff-commands.js +55 -0
- package/dist/cli/commands/audit-commands.d.ts +16 -3
- package/dist/cli/commands/audit-commands.js +84 -31
- package/dist/cli/commands/codegraph-commands.js +191 -6
- package/dist/cli/commands/final-review-commands.d.ts +34 -10
- package/dist/cli/commands/final-review-commands.js +130 -34
- package/dist/cli/commands/job-commands.js +4 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/share-commands.d.ts +49 -0
- package/dist/cli/commands/share-commands.js +114 -14
- package/dist/cli/commands/test-commands.d.ts +60 -3
- package/dist/cli/commands/test-commands.js +125 -7
- package/dist/services/audit/audit-goal-service.js +38 -3
- package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
- package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
- package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
- package/dist/services/codegraph/codegraph-service.d.ts +0 -1
- package/dist/services/codegraph/codegraph-service.js +5 -4
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
- package/dist/services/doctor/doctor-service/plugin-registry.js +4 -0
- package/dist/services/doctor/doctor-service/types.d.ts +47 -0
- package/dist/services/final-review/final-review-service.d.ts +154 -0
- package/dist/services/final-review/final-review-service.js +621 -7
- package/dist/services/final-review/index.d.ts +1 -1
- package/dist/services/final-review/index.js +1 -1
- package/dist/services/llm/anthropic-runner.d.ts +87 -0
- package/dist/services/llm/anthropic-runner.js +171 -0
- package/dist/services/llm/stub-runner.d.ts +11 -0
- package/dist/services/llm/stub-runner.js +33 -0
- package/dist/services/prd/handoff-auto-regen.js +0 -1
- package/dist/services/prd/handoff-service.d.ts +9 -1
- package/dist/services/prd/handoff-service.js +48 -6
- package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
- package/dist/services/scan/api-diff-openapi.d.ts +32 -0
- package/dist/services/scan/api-diff-openapi.js +359 -0
- package/dist/services/scan/api-diff-recorded.d.ts +96 -0
- package/dist/services/scan/api-diff-recorded.js +577 -0
- package/dist/services/scan/api-diff-service.d.ts +34 -0
- package/dist/services/scan/api-diff-service.js +407 -0
- package/dist/services/scan/api-diff-types.d.ts +116 -0
- package/dist/services/scan/api-diff-types.js +46 -0
- package/dist/services/scan/archetype-service.js +27 -1
- package/dist/services/scan/existing-system-service.js +17 -4
- package/dist/services/scan/hook-convention-service.d.ts +26 -0
- package/dist/services/scan/hook-convention-service.js +562 -0
- package/dist/services/scan/scan-types.d.ts +47 -0
- package/dist/services/session/caller-binding-service.d.ts +28 -0
- package/dist/services/session/caller-binding-service.js +10 -2
- package/dist/services/session/caller-id-types.d.ts +12 -2
- package/dist/services/session/index.d.ts +2 -2
- package/dist/services/session/index.js +2 -2
- package/dist/services/session/session-binding-bridge.js +11 -6
- package/dist/services/session/session-manager.d.ts +33 -1
- package/dist/services/session/session-manager.js +84 -25
- package/dist/services/skills/skill-presence-service.d.ts +17 -3
- package/dist/services/skills/skill-presence-service.js +23 -3
- package/package.json +7 -5
- package/skills/bee/peaks-rd/SKILL.md +11 -3
- package/skills/peaks-code/references/existing-system-extraction.md +5 -1
- package/skills/peaks-code/references/frontend-only-mode.md +48 -6
- package/skills/peaks-code/references/project-scan-checklist.md +20 -1
- package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
- 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;
|