peaks-loop 4.0.41 → 4.0.43

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 (52) 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/_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/job-commands.js +4 -2
  10. package/dist/cli/commands/scan-commands.js +1 -1
  11. package/dist/cli/commands/test-commands.d.ts +60 -3
  12. package/dist/cli/commands/test-commands.js +125 -7
  13. package/dist/services/audit/audit-goal-service.js +38 -3
  14. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
  15. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
  16. package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
  17. package/dist/services/doctor/doctor-service/types.d.ts +20 -0
  18. package/dist/services/hooks/write-gate.js +32 -9
  19. package/dist/services/llm/anthropic-runner.d.ts +87 -0
  20. package/dist/services/llm/anthropic-runner.js +171 -0
  21. package/dist/services/llm/stub-runner.d.ts +11 -0
  22. package/dist/services/llm/stub-runner.js +33 -0
  23. package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
  24. package/dist/services/scan/api-diff-openapi.d.ts +32 -0
  25. package/dist/services/scan/api-diff-openapi.js +359 -0
  26. package/dist/services/scan/api-diff-recorded.d.ts +96 -0
  27. package/dist/services/scan/api-diff-recorded.js +577 -0
  28. package/dist/services/scan/api-diff-service.d.ts +34 -0
  29. package/dist/services/scan/api-diff-service.js +407 -0
  30. package/dist/services/scan/api-diff-types.d.ts +116 -0
  31. package/dist/services/scan/api-diff-types.js +46 -0
  32. package/dist/services/scan/archetype-service.js +27 -1
  33. package/dist/services/scan/existing-system-service.js +17 -4
  34. package/dist/services/scan/hook-convention-service.d.ts +26 -0
  35. package/dist/services/scan/hook-convention-service.js +562 -0
  36. package/dist/services/scan/scan-types.d.ts +47 -0
  37. package/dist/services/session/caller-binding-service.d.ts +28 -0
  38. package/dist/services/session/caller-binding-service.js +10 -2
  39. package/dist/services/session/caller-id-types.d.ts +12 -2
  40. package/dist/services/session/index.d.ts +2 -2
  41. package/dist/services/session/index.js +2 -2
  42. package/dist/services/session/session-binding-bridge.js +11 -6
  43. package/dist/services/session/session-manager.d.ts +33 -1
  44. package/dist/services/session/session-manager.js +84 -25
  45. package/dist/services/skills/skill-presence-service.d.ts +17 -3
  46. package/dist/services/skills/skill-presence-service.js +23 -3
  47. package/package.json +5 -5
  48. package/skills/bee/peaks-rd/SKILL.md +11 -3
  49. package/skills/peaks-code/references/existing-system-extraction.md +5 -1
  50. package/skills/peaks-code/references/frontend-only-mode.md +48 -6
  51. package/skills/peaks-code/references/project-scan-checklist.md +20 -1
  52. package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The first real `LlmRunner` in peaks-loop: it binds a caller to the
3
+ * Anthropic Messages API shape at `<ANTHROPIC_BASE_URL>/v1/messages`,
4
+ * which is the same endpoint the running session is already using.
5
+ *
6
+ * Why this exists: `peaks audit goal` is the entry gate for every
7
+ * peaks-* workflow, and until this file landed the CLI could only answer
8
+ * with a fixed `scaffold-only` envelope — a gate that gated nothing.
9
+ *
10
+ * No SDK and no new runtime dependency: Node 18+ global `fetch` covers it.
11
+ * Every transport concern (auth scheme, timeout, text extraction) lives
12
+ * here so the CLI layer never speaks HTTP itself, and so tests can inject
13
+ * a fake transport instead of reaching the network.
14
+ */
15
+ import type { LlmRunner } from '../audit/audit-goal-service.js';
16
+ /**
17
+ * The subset of `Response` this client touches. Declared narrowly so tests
18
+ * can inject a plain object and stay off the network.
19
+ */
20
+ export interface LlmHttpResponse {
21
+ readonly ok: boolean;
22
+ readonly status: number;
23
+ json(): Promise<unknown>;
24
+ text(): Promise<string>;
25
+ }
26
+ export interface LlmFetchInit {
27
+ readonly method: string;
28
+ readonly headers: Record<string, string>;
29
+ readonly body: string;
30
+ readonly signal: AbortSignal;
31
+ }
32
+ export type FetchLike = (url: string, init: LlmFetchInit) => Promise<LlmHttpResponse>;
33
+ /** Which header carries the credential. */
34
+ export type AnthropicAuthScheme = 'bearer' | 'x-api-key';
35
+ export interface AnthropicConfig {
36
+ readonly baseUrl: string;
37
+ readonly authToken: string;
38
+ readonly authScheme: AnthropicAuthScheme;
39
+ readonly model: string;
40
+ }
41
+ /**
42
+ * Thrown when the environment cannot name a usable LLM. `code` distinguishes
43
+ * an absent credential from an absent model so the CLI can name the env var
44
+ * to set instead of degrading into a scaffold.
45
+ */
46
+ export declare class LlmBindingError extends Error {
47
+ readonly code: 'LLM_CREDENTIAL_MISSING' | 'LLM_MODEL_MISSING';
48
+ /**
49
+ * The environment variables that were absent, verbatim.
50
+ *
51
+ * `message` also names them, but `fail()` runs every envelope message
52
+ * through `redactSensitiveErrorMessage`, whose catch-all pattern matches
53
+ * the words `token` / `api_key` and would strip them out of this very
54
+ * message. Callers must surface `missingEnv` on a channel the redactor
55
+ * does not touch (envelope `data` / `nextActions`) so the operator is told
56
+ * exactly what to set.
57
+ */
58
+ readonly missingEnv: readonly string[];
59
+ constructor(code: 'LLM_CREDENTIAL_MISSING' | 'LLM_MODEL_MISSING', message: string, missingEnv: readonly string[]);
60
+ }
61
+ /** Thrown when a bound LLM could not be reached, answered non-2xx, or answered without text. */
62
+ export declare class LlmRequestError extends Error {
63
+ readonly code: "LLM_REQUEST_FAILED";
64
+ constructor(message: string);
65
+ }
66
+ /**
67
+ * Resolve the session's LLM from the environment.
68
+ *
69
+ * Precedence:
70
+ * - credential: `ANTHROPIC_AUTH_TOKEN` first (it is what a Claude-Code
71
+ * session exports for a gateway), else `ANTHROPIC_API_KEY`.
72
+ * - auth header: `Authorization: Bearer` for `ANTHROPIC_AUTH_TOKEN`,
73
+ * `x-api-key` for `ANTHROPIC_API_KEY` — each matches its own convention.
74
+ * - model: `ANTHROPIC_MODEL`, else `CLAUDE_CODE_SUBAGENT_MODEL`.
75
+ * - base URL: `ANTHROPIC_BASE_URL`, else the public Anthropic endpoint.
76
+ * Expected WITHOUT a trailing `/v1` — this function appends `/v1/messages`.
77
+ *
78
+ * Throws `LlmBindingError` rather than defaulting: a gate that quietly
79
+ * falls back is the bug this file was written to remove.
80
+ */
81
+ export declare function resolveAnthropicConfig(env?: NodeJS.ProcessEnv): AnthropicConfig;
82
+ export interface AnthropicRunnerOptions {
83
+ /** Injected transport. Tests pass a fake so no test performs a network call. */
84
+ readonly fetchImpl?: FetchLike;
85
+ readonly timeoutMs?: number;
86
+ }
87
+ export declare function createAnthropicRunner(config: AnthropicConfig, options?: AnthropicRunnerOptions): LlmRunner;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The first real `LlmRunner` in peaks-loop: it binds a caller to the
3
+ * Anthropic Messages API shape at `<ANTHROPIC_BASE_URL>/v1/messages`,
4
+ * which is the same endpoint the running session is already using.
5
+ *
6
+ * Why this exists: `peaks audit goal` is the entry gate for every
7
+ * peaks-* workflow, and until this file landed the CLI could only answer
8
+ * with a fixed `scaffold-only` envelope — a gate that gated nothing.
9
+ *
10
+ * No SDK and no new runtime dependency: Node 18+ global `fetch` covers it.
11
+ * Every transport concern (auth scheme, timeout, text extraction) lives
12
+ * here so the CLI layer never speaks HTTP itself, and so tests can inject
13
+ * a fake transport instead of reaching the network.
14
+ */
15
+ import { getErrorMessage } from 'peaks-loop-shared/result';
16
+ /** Public Anthropic endpoint. `ANTHROPIC_BASE_URL` overrides it (gateways, local proxies). */
17
+ const DEFAULT_BASE_URL = 'https://api.anthropic.com';
18
+ /** One call is bounded: an unbounded gate is a hung gate. */
19
+ const DEFAULT_TIMEOUT_MS = 120_000;
20
+ /** Enough of an error body to name the cause without dumping a payload into a message. */
21
+ const MAX_BODY_SNIPPET = 200;
22
+ /**
23
+ * Wrapped rather than aliased: `fetch` takes a wider `RequestInit`, which
24
+ * does not satisfy `LlmFetchInit` under `strictFunctionTypes`.
25
+ */
26
+ const defaultFetch = (url, init) => fetch(url, init);
27
+ /**
28
+ * Thrown when the environment cannot name a usable LLM. `code` distinguishes
29
+ * an absent credential from an absent model so the CLI can name the env var
30
+ * to set instead of degrading into a scaffold.
31
+ */
32
+ export class LlmBindingError extends Error {
33
+ code;
34
+ /**
35
+ * The environment variables that were absent, verbatim.
36
+ *
37
+ * `message` also names them, but `fail()` runs every envelope message
38
+ * through `redactSensitiveErrorMessage`, whose catch-all pattern matches
39
+ * the words `token` / `api_key` and would strip them out of this very
40
+ * message. Callers must surface `missingEnv` on a channel the redactor
41
+ * does not touch (envelope `data` / `nextActions`) so the operator is told
42
+ * exactly what to set.
43
+ */
44
+ missingEnv;
45
+ constructor(code, message, missingEnv) {
46
+ super(message);
47
+ this.name = 'LlmBindingError';
48
+ this.code = code;
49
+ this.missingEnv = missingEnv;
50
+ }
51
+ }
52
+ /** Thrown when a bound LLM could not be reached, answered non-2xx, or answered without text. */
53
+ export class LlmRequestError extends Error {
54
+ code = 'LLM_REQUEST_FAILED';
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = 'LlmRequestError';
58
+ }
59
+ }
60
+ /** A blank value is as unusable as an absent one. */
61
+ function readEnv(env, name) {
62
+ const value = env[name]?.trim();
63
+ return value ? value : undefined;
64
+ }
65
+ /**
66
+ * Resolve the session's LLM from the environment.
67
+ *
68
+ * Precedence:
69
+ * - credential: `ANTHROPIC_AUTH_TOKEN` first (it is what a Claude-Code
70
+ * session exports for a gateway), else `ANTHROPIC_API_KEY`.
71
+ * - auth header: `Authorization: Bearer` for `ANTHROPIC_AUTH_TOKEN`,
72
+ * `x-api-key` for `ANTHROPIC_API_KEY` — each matches its own convention.
73
+ * - model: `ANTHROPIC_MODEL`, else `CLAUDE_CODE_SUBAGENT_MODEL`.
74
+ * - base URL: `ANTHROPIC_BASE_URL`, else the public Anthropic endpoint.
75
+ * Expected WITHOUT a trailing `/v1` — this function appends `/v1/messages`.
76
+ *
77
+ * Throws `LlmBindingError` rather than defaulting: a gate that quietly
78
+ * falls back is the bug this file was written to remove.
79
+ */
80
+ export function resolveAnthropicConfig(env = process.env) {
81
+ const authToken = readEnv(env, 'ANTHROPIC_AUTH_TOKEN');
82
+ const apiKey = readEnv(env, 'ANTHROPIC_API_KEY');
83
+ const credential = authToken ?? apiKey;
84
+ if (!credential) {
85
+ throw new LlmBindingError('LLM_CREDENTIAL_MISSING', 'No LLM credential in the environment: set ANTHROPIC_AUTH_TOKEN (or ANTHROPIC_API_KEY) so the audit gate can reach the LLM this session already uses.', ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY']);
86
+ }
87
+ const model = readEnv(env, 'ANTHROPIC_MODEL') ?? readEnv(env, 'CLAUDE_CODE_SUBAGENT_MODEL');
88
+ if (!model) {
89
+ throw new LlmBindingError('LLM_MODEL_MISSING', 'No LLM model in the environment: set ANTHROPIC_MODEL (or CLAUDE_CODE_SUBAGENT_MODEL) so the audit gate knows which model to bind to.', ['ANTHROPIC_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL']);
90
+ }
91
+ return {
92
+ baseUrl: (readEnv(env, 'ANTHROPIC_BASE_URL') ?? DEFAULT_BASE_URL).replace(/\/+$/, ''),
93
+ authToken: credential,
94
+ authScheme: authToken ? 'bearer' : 'x-api-key',
95
+ model
96
+ };
97
+ }
98
+ function isTextBlock(block) {
99
+ return block.type === 'text' && typeof block.text === 'string';
100
+ }
101
+ /** `fetch` rejects on abort with a DOMException whose `name` says why. */
102
+ function isAbort(error) {
103
+ return error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError');
104
+ }
105
+ export function createAnthropicRunner(config, options = {}) {
106
+ const fetchImpl = options.fetchImpl ?? defaultFetch;
107
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
108
+ const url = `${config.baseUrl}/v1/messages`;
109
+ return {
110
+ async call(systemPrompt, userPrompt, opts) {
111
+ const body = JSON.stringify({
112
+ model: config.model,
113
+ max_tokens: opts.maxTokens,
114
+ system: systemPrompt,
115
+ messages: [{ role: 'user', content: userPrompt }]
116
+ });
117
+ let response;
118
+ try {
119
+ response = await fetchImpl(url, {
120
+ method: 'POST',
121
+ headers: {
122
+ 'content-type': 'application/json',
123
+ 'anthropic-version': '2023-06-01',
124
+ ...(config.authScheme === 'bearer'
125
+ ? { authorization: `Bearer ${config.authToken}` }
126
+ : { 'x-api-key': config.authToken })
127
+ },
128
+ body,
129
+ signal: AbortSignal.timeout(timeoutMs)
130
+ });
131
+ }
132
+ catch (error) {
133
+ if (isAbort(error)) {
134
+ throw new LlmRequestError(`LLM request to ${url} timed out after ${timeoutMs}ms`);
135
+ }
136
+ throw new LlmRequestError(`LLM request to ${url} failed: ${getErrorMessage(error)}`);
137
+ }
138
+ if (!response.ok) {
139
+ throw new LlmRequestError(`LLM request to ${url} failed: HTTP ${response.status}${await bodySnippet(response)}`);
140
+ }
141
+ let payload;
142
+ try {
143
+ payload = (await response.json());
144
+ }
145
+ catch (error) {
146
+ throw new LlmRequestError(`LLM reply from ${url} was not valid JSON: ${getErrorMessage(error)}`);
147
+ }
148
+ const blocks = Array.isArray(payload.content) ? payload.content : [];
149
+ const output = blocks.filter(isTextBlock).map((block) => block.text).join('');
150
+ if (!output) {
151
+ throw new LlmRequestError(`LLM reply from ${url} carried no text block`);
152
+ }
153
+ return {
154
+ output,
155
+ tokens: {
156
+ input: payload.usage?.input_tokens ?? 0,
157
+ output: payload.usage?.output_tokens ?? 0
158
+ }
159
+ };
160
+ }
161
+ };
162
+ }
163
+ async function bodySnippet(response) {
164
+ try {
165
+ const text = (await response.text()).trim();
166
+ return text ? `: ${text.slice(0, MAX_BODY_SNIPPET)}` : '';
167
+ }
168
+ catch {
169
+ return '';
170
+ }
171
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Offline `LlmRunner` behind `peaks audit goal --llm-provider stub`.
3
+ *
4
+ * It exists so CI and unit tests can exercise the CLI route — including
5
+ * `auditGoal()`'s 6-dimension validation — without a network call. It
6
+ * performs NO audit: every finding below is a placeholder, which is why
7
+ * the CLI reports this run as `scaffold-only` / `providerBinding: 'stub'`
8
+ * and never as an audit.
9
+ */
10
+ import type { LlmRunner } from '../audit/audit-goal-service.js';
11
+ export declare function createStubRunner(): LlmRunner;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Offline `LlmRunner` behind `peaks audit goal --llm-provider stub`.
3
+ *
4
+ * It exists so CI and unit tests can exercise the CLI route — including
5
+ * `auditGoal()`'s 6-dimension validation — without a network call. It
6
+ * performs NO audit: every finding below is a placeholder, which is why
7
+ * the CLI reports this run as `scaffold-only` / `providerBinding: 'stub'`
8
+ * and never as an audit.
9
+ */
10
+ const NOT_AUDITED = 'Stub provider: this placeholder is not an audit finding.';
11
+ const STUB_REPLY = JSON.stringify({
12
+ summary: 'Stub provider: no audit was performed.',
13
+ audit: [
14
+ { dimension: 'correctness', finding: NOT_AUDITED, severity: 'info' },
15
+ { dimension: 'completeness', finding: NOT_AUDITED, severity: 'info' },
16
+ { dimension: 'scope', finding: NOT_AUDITED, severity: 'info' },
17
+ { dimension: 'risks', finding: NOT_AUDITED, severity: 'info' },
18
+ { dimension: 'alternatives', finding: NOT_AUDITED, severity: 'info' },
19
+ { dimension: 'constraints', finding: NOT_AUDITED, severity: 'info' }
20
+ ],
21
+ proposedGoal: 'Stub provider: no goal proposed.',
22
+ successCriteria: ['Stub provider: no acceptance criteria produced.'],
23
+ roughEffort: 'small',
24
+ confidence: 'low',
25
+ rationale: 'Stub provider: the six dimensions above are placeholders so the CLI route can be exercised without a network call. Treating this as an audit would defeat the gate.'
26
+ });
27
+ export function createStubRunner() {
28
+ return {
29
+ async call() {
30
+ return { output: STUB_REPLY, tokens: { input: 0, output: 0 } };
31
+ }
32
+ };
33
+ }
@@ -218,15 +218,15 @@ function buildZeroToOneProjectScan() {
218
218
  '|---|---|',
219
219
  '| Type | `unknown` |',
220
220
  '| Confidence | `low` |',
221
- '| Reason | 0-1 project, no package.json or source files |',
222
- '| Frontend-only | `false` |',
223
221
  '',
224
222
  '## Project mode',
225
223
  '',
226
224
  '| Field | Value |',
227
225
  '|---|---|',
228
- '| Mode | unknown |',
229
- '| Reason | 0-1 bootstrap — refresh after the first source file lands |',
226
+ '| Integration mode | unknown |',
227
+ '| Integration mode reason | 0-1 bootstrap — refresh after the first source file lands |',
228
+ '| Frontend-only | `false` |',
229
+ '| Reason | 0-1 project, no package.json or source files |',
230
230
  '',
231
231
  '## Tech stack',
232
232
  '',
@@ -315,14 +315,14 @@ async function buildExistingProjectScan(args) {
315
315
  '|---|---|',
316
316
  `| Type | \`${archetypeReport.archetype}\` |`,
317
317
  `| Confidence | \`${archetypeReport.confidence}\` |`,
318
- `| Frontend-only | \`${String(archetypeReport.frontendOnly)}\` |`,
319
- `| Reason | ${archetypeReport.frontendOnlyReason} |`,
320
318
  '',
321
319
  '## Project mode',
322
320
  '',
323
321
  '| Field | Value |',
324
322
  '|---|---|',
325
- `| Mode | ${archetypeReport.frontendOnly ? 'frontend-only' : 'full-stack-or-unknown'} |`,
323
+ `| Integration mode | ${archetypeReport.integrationMode} |`,
324
+ `| Integration mode reason | ${archetypeReport.integrationModeReason} |`,
325
+ `| Frontend-only | \`${String(archetypeReport.frontendOnly)}\` |`,
326
326
  `| Reason | ${archetypeReport.frontendOnlyReason} |`,
327
327
  '',
328
328
  '## Tech stack',
@@ -0,0 +1,32 @@
1
+ /**
2
+ * S1 / rid=api-diff-report — OpenAPI 3.x parsing for `peaks scan api-diff`.
3
+ *
4
+ * `.json` via `JSON.parse`; `.yaml` / `.yml` via the `yaml` runtime dependency.
5
+ * A document that is not OpenAPI 3.x, or that declares no operations, raises
6
+ * `ApiDiffInputError` — the command must never print an empty-but-successful
7
+ * diff.
8
+ *
9
+ * THE SAME INVERTED COMPLETENESS RULE THE RECORDED SIDE GETS APPLIES HERE
10
+ * (QA final gate): `collectFields` returns a *partial* field set just as easily
11
+ * as a complete one, and the two are indistinguishable downstream. Any schema
12
+ * construct this reader cannot fully account for marks its LOCATION as
13
+ * incomplete, which suppresses every exact line for that location. Partial is
14
+ * never presented as complete.
15
+ */
16
+ import { type ParsedDocument } from './api-diff-types.js';
17
+ export declare function parseOpenApiDocument(file: string, doc: Record<string, unknown>): ParsedDocument;
18
+ /** Reads and parses `.json` / `.yaml` / `.yml`. Throws `ApiDiffInputError` on anything else. */
19
+ export declare function loadApiDocument(file: string): ParsedDocument;
20
+ /**
21
+ * Normalizes type text so `Array<string>`, `string[]`, `string | null` and
22
+ * `string|null` compare equal.
23
+ *
24
+ * Quote style is unified because it is a RENDERING artifact, not a change: the
25
+ * document renders enums via `JSON.stringify` (`"a"`) while hand-written
26
+ * recorded types use single quotes (`'a'`), so the same project written with
27
+ * double quotes emitted nothing and with single quotes emitted an exact
28
+ * CHANGED. Duplicate union members are collapsed for the same reason — a
29
+ * recorded `a?: X` normalises to `X | undefined`, which against a document's
30
+ * `X | undefined` used to become `X | undefined | undefined`.
31
+ */
32
+ export declare function normalizeType(text: string): string;