peaks-loop 4.0.15 → 4.0.17
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 +82 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +47 -27
- package/dist/cli/commands/_register.js +4 -2
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/code-review-commands.d.ts +2 -30
- package/dist/cli/commands/code-review-commands.js +43 -65
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/lint-commands.d.ts +21 -0
- package/dist/cli/commands/lint-commands.js +146 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/security-audit-commands.d.ts +1 -1
- package/dist/cli/commands/security-audit-commands.js +1 -1
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/code-review/ecc-bridge.d.ts +2 -7
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-service.d.ts +2 -19
- package/dist/services/config/config-service.js +2 -56
- package/dist/services/config/config-types.d.ts +4 -24
- package/dist/services/config/config-types.js +1 -10
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.d.ts +10 -0
- package/dist/services/lint/detect-eslint.js +62 -0
- package/dist/services/lint/detect-ocr-18.d.ts +10 -0
- package/dist/services/lint/detect-ocr-18.js +40 -0
- package/dist/services/lint/eslint-runner.d.ts +57 -0
- package/dist/services/lint/eslint-runner.js +370 -0
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
- package/dist/services/lint/ocr-multilang-adapter.js +143 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +27 -1
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +8 -12
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
- package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
- package/skills/peaks-code/SKILL.md +47 -1
- package/dist/services/code-review/ocr-service.d.ts +0 -129
- package/dist/services/code-review/ocr-service.js +0 -372
- package/skills/bee/peaks-rd/references/ocr-integration.md +0 -229
|
@@ -74,7 +74,14 @@ export async function runDefaultStatuslineRender(options, io) {
|
|
|
74
74
|
const seeded = options.project
|
|
75
75
|
? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
|
|
76
76
|
: stdin;
|
|
77
|
-
|
|
77
|
+
// Slice 2026-08-07-statusline-flake: accept `--now <epoch-ms>` to pin
|
|
78
|
+
// `Date.now()` for deterministic lifecycle-window checks under full-
|
|
79
|
+
// suite concurrency. The integration test passes a test-time `now`
|
|
80
|
+
// pinned at the moment `seedLifecycle` was called so the completed-
|
|
81
|
+
// expiry window stays in-range regardless of how long the spawned
|
|
82
|
+
// subprocess sat descheduled. Production callers omit the flag.
|
|
83
|
+
const now = options.now !== undefined ? Number(options.now) : Date.now();
|
|
84
|
+
const model = buildStatusLineModel(seeded, now);
|
|
78
85
|
const capability = resolveStatusLineCapability({
|
|
79
86
|
env: process.env,
|
|
80
87
|
isTTY: Boolean(process.stdout.isTTY),
|
|
@@ -98,7 +105,8 @@ export function registerStatusLineCommands(program, io) {
|
|
|
98
105
|
const statusline = addJsonOption(program
|
|
99
106
|
.command('statusline')
|
|
100
107
|
.description('Render the Peaks skill status line for the current session, or manage the adapter-driven statusLine entry. Run with no subcommand to render; with a subcommand (install | uninstall | status) to manage.')
|
|
101
|
-
.option('--project <path>', 'project root path (used to label the status line when stdin is absent; applies to the default render path)')
|
|
108
|
+
.option('--project <path>', 'project root path (used to label the status line when stdin is absent; applies to the default render path)')
|
|
109
|
+
.option('--now <ms>', 'override current time (epoch ms) — for testing / deterministic rendering of the lifecycle expiry window'));
|
|
102
110
|
// Default behavior: when the user types `peaks statusline` with no
|
|
103
111
|
// subcommand, render the status line. Without this default action,
|
|
104
112
|
// commander falls back to printing usage (Bug-02, ice-cola surface check
|
|
@@ -225,6 +233,7 @@ export function registerStatusLineCommands(program, io) {
|
|
|
225
233
|
'--session-id to target a non-canonical session explicitly.')
|
|
226
234
|
.option('--project <path>', 'project root (auto-detected from cwd when omitted)')
|
|
227
235
|
.option('--session-id <sid>', 'override the active session id (defaults to the canonical binding; supported for QA / internal tooling)')
|
|
236
|
+
.option('--now <ms>', 'override current time (epoch ms) — for testing / deterministic rendering of the lifecycle expiry window')
|
|
228
237
|
.action((options, command) => {
|
|
229
238
|
// Re-resolve options via `command.optsWithGlobals()` (Commander 12.x
|
|
230
239
|
// compatibility): when the parent `statusline` command has its own
|
|
@@ -243,10 +252,15 @@ export function registerStatusLineCommands(program, io) {
|
|
|
243
252
|
? merged.project
|
|
244
253
|
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
245
254
|
const sid = merged.sessionId ?? getSessionIdCanonical(projectRoot) ?? null;
|
|
255
|
+
// Slice 2026-08-07-statusline-flake: honor `--now` (epoch ms)
|
|
256
|
+
// when present so deterministic lifecycle-window checks stay
|
|
257
|
+
// in-range under full-suite concurrency. See `--now` doc on
|
|
258
|
+
// the parent `statusline` command.
|
|
259
|
+
const now = merged.now !== undefined ? Number(merged.now) : Date.now();
|
|
246
260
|
const state = decideCompactStatusline({
|
|
247
261
|
projectRoot,
|
|
248
262
|
sessionId: sid,
|
|
249
|
-
now
|
|
263
|
+
now,
|
|
250
264
|
});
|
|
251
265
|
const label = renderCompactStatusline(state);
|
|
252
266
|
// For non-JSON mode we print the LABEL DIRECTLY to stdout
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* (B2 contract preservation rule from the rid-009 closure record).
|
|
19
19
|
*/
|
|
20
20
|
import type { Command } from 'commander';
|
|
21
|
-
import type
|
|
21
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
22
22
|
import type { WorkspaceConfig } from '../../services/config/config-types.js';
|
|
23
23
|
import { type TechChangeStatus, type WorkspaceConfig as _WorkspaceConfig } from '../../services/tech/tech-change-id-service.js';
|
|
24
24
|
export type { WorkspaceConfig };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_REQUEST_TYPE, isRequestType, VALID_REQUEST_TYPES, type PrerequisiteCheckResult, type RequestType } from './artifact-prerequisites.js';
|
|
2
2
|
export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType, type RequestType };
|
|
3
|
-
import type
|
|
3
|
+
import { type RequestArtifactRole } from './artifact-templates.js';
|
|
4
4
|
export type { RequestArtifactRole } from './artifact-templates.js';
|
|
5
5
|
export { formatHandoffPath, formatCommitBoundaryPath, formatSkillUsageLessonsPath } from './artifact-templates.js';
|
|
6
6
|
export type CreateRequestArtifactOptions = {
|
|
@@ -110,7 +110,7 @@ export type TransitionRequestArtifactResult = RequestArtifactSummary & {
|
|
|
110
110
|
content: string;
|
|
111
111
|
bypassedPrerequisites?: PrerequisiteCheckResult;
|
|
112
112
|
};
|
|
113
|
-
import type
|
|
113
|
+
import { type RequestArtifactState } from './request-artifact-state-helpers.js';
|
|
114
114
|
export { allowedStatesForRole, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock } from './request-artifact-state-helpers.js';
|
|
115
115
|
export type { RequestArtifactState } from './request-artifact-state-helpers.js';
|
|
116
116
|
export declare function transitionRequestArtifact(options: TransitionRequestArtifactOptions): Promise<TransitionRequestArtifactResult | null>;
|
|
@@ -16,6 +16,15 @@ import { requireUserConfirmation } from '../mode/mode-enforcement.js';
|
|
|
16
16
|
import { scanFileSize } from '../scan/file-size-scan.js';
|
|
17
17
|
import { emitObservabilityEvent } from '../observability/observability-service.js';
|
|
18
18
|
export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType };
|
|
19
|
+
// Re-export the pure-function template surface so external callers (CLI,
|
|
20
|
+
// tests) keep importing from this module unchanged. The render templates,
|
|
21
|
+
// handoff-path helpers, and `RequestArtifactRole` type all live in the
|
|
22
|
+
// sibling `artifact-templates.ts` module — see slice
|
|
23
|
+
// 2026-06-24-handoff-path-canonicalization for the rationale.
|
|
24
|
+
//
|
|
25
|
+
// `import type` is required so the type is in scope as a bare identifier
|
|
26
|
+
// within this file (the value re-export alone does not bring types into
|
|
27
|
+
// the local namespace under `isolatedModules`).
|
|
19
28
|
import { renderTemplate } from './artifact-templates.js';
|
|
20
29
|
export { formatHandoffPath, formatCommitBoundaryPath, formatSkillUsageLessonsPath } from './artifact-templates.js';
|
|
21
30
|
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -324,7 +333,7 @@ async function readRequestArtifact(projectRoot, scope, role, found) {
|
|
|
324
333
|
// the sibling `request-artifact-state-helpers.ts` module — see
|
|
325
334
|
// v2.18.3 file-split for the rationale. Function signatures and
|
|
326
335
|
// behaviour are unchanged (verbatim move).
|
|
327
|
-
import { ALLOWED_STATES_PER_ROLE, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock } from './request-artifact-state-helpers.js';
|
|
336
|
+
import { ALLOWED_STATES_PER_ROLE, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock, } from './request-artifact-state-helpers.js';
|
|
328
337
|
export { allowedStatesForRole, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock } from './request-artifact-state-helpers.js';
|
|
329
338
|
export async function transitionRequestArtifact(options) {
|
|
330
339
|
if (!VALID_ROLES.has(options.role)) {
|
|
@@ -35,6 +35,20 @@ const ABSOLUTE_PATH_WINDOWS = /[A-Z]:\\[\w\\]/;
|
|
|
35
35
|
const ABSOLUTE_PATH_UNIX = /\/usr\/(?:local|bin|opt)\b/;
|
|
36
36
|
const MAGIC_NUMBER = /\b(\d{3,})\b/;
|
|
37
37
|
const LOAD_STRATEGY_PATTERN = /loadStrategy:\s*(always|on-demand)/i;
|
|
38
|
+
/**
|
|
39
|
+
* PRD-002b slice 2 — extract cross-cutting thresholds used by
|
|
40
|
+
* multiple enforcers (file-size cap, magic-number digit floor).
|
|
41
|
+
* Per-rule spec literals (≤800 lines, ≤12 H2 headings, ≤10 magic
|
|
42
|
+
* digits, top-N aggregation cap) are kept inline because they ARE
|
|
43
|
+
* the rule spec; extracting them would change the rule semantics to
|
|
44
|
+
* depend on a constant with no semantic gain. Names here describe
|
|
45
|
+
* the role only.
|
|
46
|
+
*/
|
|
47
|
+
const KB_PER_FILE = 1024;
|
|
48
|
+
const LOAD_STRATEGY_FILE_SIZE_KB = 5;
|
|
49
|
+
const OVERVIEW_TOP_SEARCH_LINES = 30;
|
|
50
|
+
const OVERVIEW_MIN_LINE_COUNT = 200;
|
|
51
|
+
const LOAD_STRATEGY_PROBE_LINES = 20;
|
|
38
52
|
function findLine(lines, pattern) {
|
|
39
53
|
for (let i = 0; i < lines.length; i += 1) {
|
|
40
54
|
if (pattern.test(lines[i] ?? ''))
|
|
@@ -129,9 +143,9 @@ export function lintH2CountLe12(ref) {
|
|
|
129
143
|
return [hit('rl-ref-h2-count-le-12-001', 'at most 12 `## <heading>` per reference', ref.path, 1, `(h2 count ${matches.length} > 12)`)];
|
|
130
144
|
}
|
|
131
145
|
export function lintOverviewNearTop(ref) {
|
|
132
|
-
if (ref.lines.length <=
|
|
146
|
+
if (ref.lines.length <= OVERVIEW_MIN_LINE_COUNT)
|
|
133
147
|
return [];
|
|
134
|
-
if (findLine(ref.lines.slice(0,
|
|
148
|
+
if (findLine(ref.lines.slice(0, OVERVIEW_TOP_SEARCH_LINES), OVERVIEW_HEADING) !== -1)
|
|
135
149
|
return [];
|
|
136
150
|
return [hit('rl-ref-overview-section-near-top-001', 'long references (>200 lines) must have `## Overview` within the first 30 lines', ref.path, 1, '(missing `## Overview` near top of long reference)')];
|
|
137
151
|
}
|
|
@@ -149,7 +163,7 @@ export function lintLoadStrategyAlwaysCacheable(ref) {
|
|
|
149
163
|
// Heuristic: a loadStrategy: always reference must not have a
|
|
150
164
|
// top-level shell command. This is a soft check — we look at
|
|
151
165
|
// the first 10 non-frontmatter lines for I/O patterns.
|
|
152
|
-
const lines = ref.lines.slice(0,
|
|
166
|
+
const lines = ref.lines.slice(0, LOAD_STRATEGY_PROBE_LINES);
|
|
153
167
|
for (const line of lines) {
|
|
154
168
|
if (/^\s*(npm|pnpm|yarn|npx|git|curl|wget|docker)\s+/.test(line)) {
|
|
155
169
|
return [hit('rl-ref-loadstrategy-always-cacheable-001', 'loadStrategy: always references must not run I/O at top of file', ref.path, 1, `(I/O pattern at top of file: ${line.trim()})`)];
|
|
@@ -249,7 +263,7 @@ export function lintSkillCitesEveryReference(ref, skill) {
|
|
|
249
263
|
}
|
|
250
264
|
export function lintLoadStrategyMatchesSize(ref) {
|
|
251
265
|
const sizeBytes = Buffer.byteLength(ref.body, 'utf8');
|
|
252
|
-
if (sizeBytes <=
|
|
266
|
+
if (sizeBytes <= LOAD_STRATEGY_FILE_SIZE_KB * KB_PER_FILE)
|
|
253
267
|
return [];
|
|
254
268
|
// >5KB file should declare loadStrategy: on-demand (always
|
|
255
269
|
// is a context-budget bug).
|
|
@@ -36,6 +36,14 @@ import { describeMode, thresholdFor } from './auto-compact-modes.js';
|
|
|
36
36
|
import { read24hState } from '../24h-mode/store.js';
|
|
37
37
|
import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusline/compact-lifecycle-store.js';
|
|
38
38
|
const PRE_COMPACT_REASON = 'pre-compact-auto';
|
|
39
|
+
/**
|
|
40
|
+
* PRD-002b slice 2 — extract the few cross-cutting magic numbers
|
|
41
|
+
* that actually appear at runtime call sites in this orchestrator.
|
|
42
|
+
* Threshold ratios (0.85 / 0.95) live in `auto-compact-types.ts`
|
|
43
|
+
* and are NOT extracted here because they ARE the contract; comment
|
|
44
|
+
* + spec prose references must keep the literal value visible.
|
|
45
|
+
*/
|
|
46
|
+
const COLLAPSED_ERROR_MAX_CHARS = 160;
|
|
39
47
|
/**
|
|
40
48
|
* Map a context ratio to a `CompactTrigger` action. Pure; the side
|
|
41
49
|
* effects (checkpoint + IDE dispatch) live in `runAutoCompact`. Two
|
|
@@ -409,7 +417,7 @@ function summarizeLifecycleError(error) {
|
|
|
409
417
|
raw = 'unknown error';
|
|
410
418
|
const firstLine = raw.split('\n')[0] ?? '';
|
|
411
419
|
const collapsed = firstLine.replace(/\s+/g, ' ').trim();
|
|
412
|
-
return collapsed.length === 0 ? 'unknown error' : (collapsed.length >
|
|
420
|
+
return collapsed.length === 0 ? 'unknown error' : (collapsed.length > COLLAPSED_ERROR_MAX_CHARS ? collapsed.slice(0, COLLAPSED_ERROR_MAX_CHARS) : collapsed);
|
|
413
421
|
}
|
|
414
422
|
/**
|
|
415
423
|
* Slice 2026-08-01-compact-lifecycle (Task 5, Step 4): close out an
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G6 — in-process batch heartbeat poller.
|
|
3
|
+
*
|
|
4
|
+
* The Dispatcher (peaks-code main loop) starts one poller per batch.
|
|
5
|
+
* The poller ticks every 10 s, reads `heartbeats[]` + `lastBeatAt` from
|
|
6
|
+
* each record in the batch, and emits:
|
|
7
|
+
*
|
|
8
|
+
* - a `dispatcherStatus` envelope to the renderer's `onStatus` callback
|
|
9
|
+
* (which formats the single-line status per G6.5)
|
|
10
|
+
* - a `stale` envelope to `onStale` if a record crosses the 5-min
|
|
11
|
+
* threshold (G6.2 / AC-35)
|
|
12
|
+
*
|
|
13
|
+
* The poller does **not**:
|
|
14
|
+
* - cancel, kill, or send SIGTERM to a sub-agent (RL-15)
|
|
15
|
+
* - modify `outcome` (only the aggregate `status` is flipped to 'stale')
|
|
16
|
+
* - block the LLM call (it is fire-and-forget; the parent batch-sync
|
|
17
|
+
* wait is governed by the LLM platform, not by this poller)
|
|
18
|
+
*
|
|
19
|
+
* The poller stops when:
|
|
20
|
+
* - the parent calls `stop()` (batch finished)
|
|
21
|
+
* - all records are terminal (`done` / `failed` / `cancelled` / `no-execution`)
|
|
22
|
+
*
|
|
23
|
+
* Pure JS event loop — no native deps, no IPC. Designed to be replaced
|
|
24
|
+
* by a real OS-level watcher in a future slice without changing the
|
|
25
|
+
* callback contract.
|
|
26
|
+
*/
|
|
1
27
|
import { readRecords } from '../dispatch/dispatch-record-writer.js';
|
|
2
28
|
import { renderStatusLine, summarize, viewSubAgent } from './status-line-renderer.js';
|
|
3
29
|
export const DEFAULT_POLL_INTERVAL_MS = 10_000;
|
|
@@ -38,6 +38,19 @@ export declare const ORCHESTRATOR_REDLINE_RATIO = 0.95;
|
|
|
38
38
|
export declare const ORCHESTRATOR_PRECOMPACT_RATIO = 0.85;
|
|
39
39
|
/** Source-code keywords that signal "do NOT Edit/Write directly". */
|
|
40
40
|
export declare const SOURCE_CODE_KEYWORDS: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Slice 2026-08-06-codegate-vendor-neutral — hard-blocked path families.
|
|
43
|
+
* When ANY of these substrings appears in the slice-spec, the orchestrator
|
|
44
|
+
* MUST NOT Edit/Write directly; the probe returns `canDoInSession: false`
|
|
45
|
+
* with `blockers: ["requires-sub-agent-dispatch"]` to force
|
|
46
|
+
* `peaks sub-agent dispatch rd`. The hook (`pre-tool-code-gate.sh`)
|
|
47
|
+
* enforces the same deny at the PreToolUse layer.
|
|
48
|
+
*
|
|
49
|
+
* The allow-list (.peaks/**, .peaks/_runtime/**, skill files, docs/**)
|
|
50
|
+
* is checked by the hook, NOT by this probe — the probe is content-side
|
|
51
|
+
* (slice-spec text) and the hook is file-side (resolved path on Edit/Write).
|
|
52
|
+
*/
|
|
53
|
+
export declare const HARD_BLOCKED_PATH_FAMILIES: readonly string[];
|
|
41
54
|
/** Decision-marker keywords that signal "needs user AskUserQuestion". */
|
|
42
55
|
export declare const DECISION_KEYWORDS: readonly string[];
|
|
43
56
|
export interface ContextProbe {
|
|
@@ -67,6 +80,8 @@ export interface OrchestratorCanDoResult {
|
|
|
67
80
|
readonly subAgentAvailable: boolean;
|
|
68
81
|
/** Diagnostic — which of the 4 boundary questions fired. */
|
|
69
82
|
readonly q1SourceCodeTouched: boolean;
|
|
83
|
+
/** Slice 2026-08-06-codegate-vendor-neutral: did the slice-spec mention a hard-blocked path family (src/, tests/unit/, ...)? When true the probe refuses direct execution. */
|
|
84
|
+
readonly q1HardBlockedPath: boolean;
|
|
70
85
|
readonly q2SubAgentAvailable: boolean;
|
|
71
86
|
readonly q3RequiresUserDecision: boolean;
|
|
72
87
|
readonly q4ContextRatio: number;
|
|
@@ -80,6 +95,14 @@ export declare class OrchestratorCanDoError extends Error {
|
|
|
80
95
|
* the slice-spec string. Case-insensitive substring match.
|
|
81
96
|
*/
|
|
82
97
|
export declare function detectSourceCodeTouched(sliceSpec: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Slice 2026-08-06-codegate-vendor-neutral — does the slice-spec mention
|
|
100
|
+
* any hard-blocked path family? Pure substring match. When true, the
|
|
101
|
+
* probe returns `canDoInSession: false` with `requires-sub-agent-dispatch`.
|
|
102
|
+
* This is the LLM-side complement to the `pre-tool-code-gate.sh` hook
|
|
103
|
+
* (which checks the actual Edit/Write/MultiEdit target path).
|
|
104
|
+
*/
|
|
105
|
+
export declare function detectHardBlockedPath(sliceSpec: string): boolean;
|
|
83
106
|
/**
|
|
84
107
|
* Q3: does the slice require user decisions? Pure keyword scan
|
|
85
108
|
* over the slice-spec string.
|
|
@@ -103,6 +126,7 @@ export declare function probeContextRatio(projectRoot: string, peaksBin?: string
|
|
|
103
126
|
*/
|
|
104
127
|
export declare function buildOrchestratorCanDoResult(input: OrchestratorCanDoInput, signals: {
|
|
105
128
|
q1SourceCodeTouched: boolean;
|
|
129
|
+
q1HardBlockedPath: boolean;
|
|
106
130
|
q2SubAgentAvailable: boolean;
|
|
107
131
|
q3RequiresUserDecision: boolean;
|
|
108
132
|
q4ContextRatio: number;
|
|
@@ -55,6 +55,26 @@ export const SOURCE_CODE_KEYWORDS = [
|
|
|
55
55
|
'.go',
|
|
56
56
|
'.rs',
|
|
57
57
|
];
|
|
58
|
+
/**
|
|
59
|
+
* Slice 2026-08-06-codegate-vendor-neutral — hard-blocked path families.
|
|
60
|
+
* When ANY of these substrings appears in the slice-spec, the orchestrator
|
|
61
|
+
* MUST NOT Edit/Write directly; the probe returns `canDoInSession: false`
|
|
62
|
+
* with `blockers: ["requires-sub-agent-dispatch"]` to force
|
|
63
|
+
* `peaks sub-agent dispatch rd`. The hook (`pre-tool-code-gate.sh`)
|
|
64
|
+
* enforces the same deny at the PreToolUse layer.
|
|
65
|
+
*
|
|
66
|
+
* The allow-list (.peaks/**, .peaks/_runtime/**, skill files, docs/**)
|
|
67
|
+
* is checked by the hook, NOT by this probe — the probe is content-side
|
|
68
|
+
* (slice-spec text) and the hook is file-side (resolved path on Edit/Write).
|
|
69
|
+
*/
|
|
70
|
+
export const HARD_BLOCKED_PATH_FAMILIES = [
|
|
71
|
+
'src/',
|
|
72
|
+
'tests/unit/',
|
|
73
|
+
'tests/integration/',
|
|
74
|
+
'config/',
|
|
75
|
+
'bin/',
|
|
76
|
+
'scripts/',
|
|
77
|
+
];
|
|
58
78
|
/** Decision-marker keywords that signal "needs user AskUserQuestion". */
|
|
59
79
|
export const DECISION_KEYWORDS = [
|
|
60
80
|
'design',
|
|
@@ -82,6 +102,17 @@ export function detectSourceCodeTouched(sliceSpec) {
|
|
|
82
102
|
const lower = sliceSpec.toLowerCase();
|
|
83
103
|
return SOURCE_CODE_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
|
|
84
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Slice 2026-08-06-codegate-vendor-neutral — does the slice-spec mention
|
|
107
|
+
* any hard-blocked path family? Pure substring match. When true, the
|
|
108
|
+
* probe returns `canDoInSession: false` with `requires-sub-agent-dispatch`.
|
|
109
|
+
* This is the LLM-side complement to the `pre-tool-code-gate.sh` hook
|
|
110
|
+
* (which checks the actual Edit/Write/MultiEdit target path).
|
|
111
|
+
*/
|
|
112
|
+
export function detectHardBlockedPath(sliceSpec) {
|
|
113
|
+
const lower = sliceSpec.toLowerCase();
|
|
114
|
+
return HARD_BLOCKED_PATH_FAMILIES.some((fam) => lower.includes(fam.toLowerCase()));
|
|
115
|
+
}
|
|
85
116
|
/**
|
|
86
117
|
* Q3: does the slice require user decisions? Pure keyword scan
|
|
87
118
|
* over the slice-spec string.
|
|
@@ -135,6 +166,17 @@ export function buildOrchestratorCanDoResult(input, signals) {
|
|
|
135
166
|
const blockers = [];
|
|
136
167
|
const warnings = [];
|
|
137
168
|
const suggestions = [];
|
|
169
|
+
// Slice 2026-08-06-codegate-vendor-neutral — Q1 HARD BLOCKER. When
|
|
170
|
+
// the slice-spec mentions any hard-blocked path family
|
|
171
|
+
// (src/, tests/unit/, tests/integration/, config/, bin/, scripts/),
|
|
172
|
+
// the orchestrator MUST refuse direct execution and force sub-agent
|
|
173
|
+
// dispatch. This is the LLM-side complement to the
|
|
174
|
+
// `pre-tool-code-gate.sh` PreToolUse hook.
|
|
175
|
+
if (signals.q1HardBlockedPath) {
|
|
176
|
+
blockers.push('requires-sub-agent-dispatch: slice-spec mentions a hard-blocked path family ' +
|
|
177
|
+
'(src/, tests/unit/, tests/integration/, config/, bin/, scripts/); ' +
|
|
178
|
+
'orchestrator MUST NOT Edit/Write these directly. Use peaks sub-agent dispatch rd.');
|
|
179
|
+
}
|
|
138
180
|
// Q2 — sub-agent availability is a hard precondition.
|
|
139
181
|
if (!signals.q2SubAgentAvailable) {
|
|
140
182
|
blockers.push('sub-agent dispatch unavailable (peaks sub-agent dispatch --help failed)');
|
|
@@ -154,8 +196,9 @@ export function buildOrchestratorCanDoResult(input, signals) {
|
|
|
154
196
|
if (signals.q3RequiresUserDecision) {
|
|
155
197
|
warnings.push('slice-spec contains decision keywords; AskUserQuestion before proceeding');
|
|
156
198
|
}
|
|
157
|
-
// Q1 — source-code touched is
|
|
158
|
-
//
|
|
199
|
+
// Q1 (soft) — source-code touched is a sub-agent-dispatch hint. When
|
|
200
|
+
// not already hard-blocked (above), surface the dispatch verb. When
|
|
201
|
+
// already hard-blocked the blocker line carries the same instruction.
|
|
159
202
|
if (signals.q1SourceCodeTouched && signals.q2SubAgentAvailable) {
|
|
160
203
|
const batchId = randomUUID();
|
|
161
204
|
const rid = 'rid-' + Date.now().toString(36);
|
|
@@ -175,6 +218,7 @@ export function buildOrchestratorCanDoResult(input, signals) {
|
|
|
175
218
|
contextRatio: signals.q4ContextRatio,
|
|
176
219
|
subAgentAvailable: signals.q2SubAgentAvailable,
|
|
177
220
|
q1SourceCodeTouched: signals.q1SourceCodeTouched,
|
|
221
|
+
q1HardBlockedPath: signals.q1HardBlockedPath,
|
|
178
222
|
q2SubAgentAvailable: signals.q2SubAgentAvailable,
|
|
179
223
|
q3RequiresUserDecision: signals.q3RequiresUserDecision,
|
|
180
224
|
q4ContextRatio: signals.q4ContextRatio,
|
|
@@ -189,6 +233,7 @@ export async function evaluateOrchestratorCanDo(input) {
|
|
|
189
233
|
throw new OrchestratorCanDoError('--slice-spec is required', 'MISSING_SLICE_SPEC');
|
|
190
234
|
}
|
|
191
235
|
const q1SourceCodeTouched = detectSourceCodeTouched(input.sliceSpec);
|
|
236
|
+
const q1HardBlockedPath = detectHardBlockedPath(input.sliceSpec);
|
|
192
237
|
const q3RequiresUserDecision = detectRequiresUserDecision(input.sliceSpec);
|
|
193
238
|
const probeSubAgent = input.probeSubAgentAvailable ?? (() => probeSubAgentAvailable(input.projectRoot));
|
|
194
239
|
const probeContext = input.probeContextRatio ?? (() => probeContextRatio(input.projectRoot));
|
|
@@ -198,6 +243,7 @@ export async function evaluateOrchestratorCanDo(input) {
|
|
|
198
243
|
]);
|
|
199
244
|
return buildOrchestratorCanDoResult(input, {
|
|
200
245
|
q1SourceCodeTouched,
|
|
246
|
+
q1HardBlockedPath,
|
|
201
247
|
q2SubAgentAvailable: subAgentAvailable,
|
|
202
248
|
q3RequiresUserDecision,
|
|
203
249
|
q4ContextRatio: ctxProbe.ratio,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* IO: readdir + readFile on `.peaks/_runtime/<sid>/checkpoints/`.
|
|
22
22
|
* No writes (caller logs the decision via the auto-decisions channel).
|
|
23
23
|
*/
|
|
24
|
-
import type
|
|
24
|
+
import { type SkillPresenceMode } from '../skills/skill-presence-service.js';
|
|
25
25
|
import { type CodeMode } from './mode-gate.js';
|
|
26
26
|
export type PostCompactResumeReason = 'post-compact-match' | 'sid-unbound' | 'runtime-dir-missing' | 'no-checkpoint-today' | 'no-mode-field' | 'stale-checkpoint' | 'multiple-checkpoints-ambiguous' | 'active-skill-mismatch' | 'checkpoint-read-failed';
|
|
27
27
|
export declare const POST_COMPACT_RESUME_REASONS: readonly PostCompactResumeReason[];
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
* `rd/code-review.md` markdown format that `peaks request transition`
|
|
6
6
|
* Gate B3 already reads (`mustContain: ['## Findings', 'CRITICAL']`).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* (uses peaks-loop's own Agent tool surface), ocr is the external
|
|
10
|
-
* subprocess option. Both produce a second-opinion code review that
|
|
11
|
-
* the parent RD loop aggregates alongside its own inline review.
|
|
8
|
+
* ECC is the in-skill agent option and the production Gate B3 path.
|
|
12
9
|
*
|
|
13
10
|
* === Why a bridge (not a direct swap) ===
|
|
14
11
|
*
|
|
@@ -54,9 +51,7 @@ export interface EccEnvelope {
|
|
|
54
51
|
readonly gateAction: EccGateAction;
|
|
55
52
|
}
|
|
56
53
|
/**
|
|
57
|
-
* 5-state
|
|
58
|
-
* (ready / package-missing / binary-missing / config-missing /
|
|
59
|
-
* detection-failed). ECC's analogue:
|
|
54
|
+
* 5-state ECC detection result:
|
|
60
55
|
* - `ready` — ECC plugin is installed and reachable
|
|
61
56
|
* - `plugin-missing` — `everything-claude-code` not in skill list
|
|
62
57
|
* - `agent-missing` — plugin present but `code-review` agent absent
|
|
@@ -56,17 +56,24 @@ const DEFAULT_STALE_AFTER_MS = 120_000;
|
|
|
56
56
|
export const COMPLETED_EXPIRY_MS = 10_000;
|
|
57
57
|
/** Legacy mtime window for the "just compacted" indicator. */
|
|
58
58
|
const LEGACY_JUST_COMPACTED_WINDOW_MS = 30_000;
|
|
59
|
+
// PRD-002b slice 2 — extract cell-table magic numbers (4/6/8) into named
|
|
60
|
+
// consts so the no-magic-numbers lint rule stops flagging the typed
|
|
61
|
+
// literal-union cell-table values. Values match the documented contract.
|
|
62
|
+
const STAGE_CELL_COMPACTING = 4;
|
|
63
|
+
const STAGE_CELL_VERIFYING = 6;
|
|
64
|
+
const STAGE_CELL_COMPLETED = 8;
|
|
65
|
+
/* eslint-disable no-magic-numbers -- cell-table contract uses literal-type unions (`0 | 2 | 4 | 6 | 8`); these are type-position discriminators, not runtime values. Magic-number extraction is done at the runtime call sites via STAGE_CELL_* constants above. */
|
|
59
66
|
const CELL_BY_STAGE = new Map([
|
|
60
67
|
['queued', 0],
|
|
61
68
|
['preparing', 2],
|
|
62
|
-
['compacting',
|
|
63
|
-
['verifying',
|
|
64
|
-
['completed',
|
|
65
|
-
['failed',
|
|
69
|
+
['compacting', STAGE_CELL_COMPACTING],
|
|
70
|
+
['verifying', STAGE_CELL_VERIFYING],
|
|
71
|
+
['completed', STAGE_CELL_COMPLETED],
|
|
72
|
+
['failed', STAGE_CELL_COMPACTING],
|
|
66
73
|
]);
|
|
67
74
|
const FILLED = '█';
|
|
68
75
|
const EMPTY = '░';
|
|
69
|
-
const BAR_WIDTH =
|
|
76
|
+
const BAR_WIDTH = STAGE_CELL_COMPLETED;
|
|
70
77
|
const NO_AFTER_RATIO_HINT = 'after-ratio not recorded';
|
|
71
78
|
function renderBar(filledCells) {
|
|
72
79
|
return `[${FILLED.repeat(filledCells)}${EMPTY.repeat(BAR_WIDTH - filledCells)}]`;
|
|
@@ -74,6 +81,7 @@ function renderBar(filledCells) {
|
|
|
74
81
|
function renderLegacyBar(filledCells) {
|
|
75
82
|
return renderBar(filledCells);
|
|
76
83
|
}
|
|
84
|
+
/* eslint-enable no-magic-numbers */
|
|
77
85
|
export function decideCompactStatusline(input) {
|
|
78
86
|
const staleAfterMs = input.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
79
87
|
const completedExpiryMs = input.completedExpiryMs ?? COMPLETED_EXPIRY_MS;
|
|
@@ -137,7 +145,7 @@ function stateFromLifecycle(record) {
|
|
|
137
145
|
const failedAt = record.failedAt ?? 'compacting';
|
|
138
146
|
const state = {
|
|
139
147
|
kind: 'failed',
|
|
140
|
-
filledCells: CELL_BY_STAGE.get(failedAt) ??
|
|
148
|
+
filledCells: CELL_BY_STAGE.get(failedAt) ?? STAGE_CELL_COMPACTING,
|
|
141
149
|
triggerRatio: record.triggerRatio,
|
|
142
150
|
redLine: record.redLine,
|
|
143
151
|
failedAt,
|
|
@@ -160,7 +168,7 @@ function stateFromLifecycle(record) {
|
|
|
160
168
|
return base;
|
|
161
169
|
}
|
|
162
170
|
function stateFromStalled(record) {
|
|
163
|
-
const filledCells = CELL_BY_STAGE.get(record.stage) ??
|
|
171
|
+
const filledCells = CELL_BY_STAGE.get(record.stage) ?? STAGE_CELL_COMPACTING;
|
|
164
172
|
const detailText = record.stage === 'failed'
|
|
165
173
|
? record.errorSummary
|
|
166
174
|
: `no heartbeat for ${record.stage} stage`;
|
|
@@ -236,9 +244,9 @@ export function renderCompactStatusline(state) {
|
|
|
236
244
|
case 'preparing':
|
|
237
245
|
return `compact ${renderBar(2)}`;
|
|
238
246
|
case 'compacting':
|
|
239
|
-
return `compact ${renderBar(
|
|
247
|
+
return `compact ${renderBar(STAGE_CELL_COMPACTING)}`;
|
|
240
248
|
case 'verifying':
|
|
241
|
-
return `compact ${renderBar(
|
|
249
|
+
return `compact ${renderBar(STAGE_CELL_VERIFYING)}`;
|
|
242
250
|
case 'completed':
|
|
243
251
|
return formatCompleted(state);
|
|
244
252
|
case 'failed':
|
|
@@ -250,7 +258,7 @@ export function renderCompactStatusline(state) {
|
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
260
|
function formatCompleted(state) {
|
|
253
|
-
const bar = renderBar(
|
|
261
|
+
const bar = renderBar(STAGE_CELL_COMPLETED);
|
|
254
262
|
if (typeof state.afterRatio === 'number') {
|
|
255
263
|
return `compact ${bar} → ${state.afterRatio.toFixed(2)}`;
|
|
256
264
|
}
|
|
@@ -258,7 +266,7 @@ function formatCompleted(state) {
|
|
|
258
266
|
}
|
|
259
267
|
function formatFailed(state) {
|
|
260
268
|
const filledAt = state.failedAt ?? 'compacting';
|
|
261
|
-
const cells = CELL_BY_STAGE.get(filledAt) ??
|
|
269
|
+
const cells = CELL_BY_STAGE.get(filledAt) ?? STAGE_CELL_COMPACTING;
|
|
262
270
|
const bar = renderBar(cells);
|
|
263
271
|
const detail = state.detail ? ` — ${state.detail}` : '';
|
|
264
272
|
return `compact ${bar} failed at ${filledAt}${detail}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ConfigGetOptions, type ConfigLayer, type ConfigSetOptions, type ConfigV2, type PeaksConfig, type TokenRef, type WorkspaceConfig } from './config-types.js';
|
|
2
2
|
export { resolveProjectRootForConfig, resolveCanonicalProjectRoot } from './config-safety.js';
|
|
3
3
|
/**
|
|
4
4
|
* Load the slim 2.0 `~/.peaks/config.json` file. Returns the parsed
|
|
@@ -22,23 +22,6 @@ export type RedactedConfigValue = string | number | boolean | null | RedactedCon
|
|
|
22
22
|
[key: string]: RedactedConfigValue;
|
|
23
23
|
};
|
|
24
24
|
export declare function redactConfigSecrets(value: unknown, path?: string): RedactedConfigValue;
|
|
25
|
-
/**
|
|
26
|
-
* Read the ocr LLM endpoint config from the user-layer
|
|
27
|
-
* `~/.peaks/config.json`. The user populates this themselves by
|
|
28
|
-
* pasting the `peaks code-review config-template` output (or by
|
|
29
|
-
* running `peaks config set --key ocr.llm.url --value '...'`).
|
|
30
|
-
* peaks-loop never auto-writes these values.
|
|
31
|
-
*/
|
|
32
|
-
export declare function getOcrConfig(): OcrConfig;
|
|
33
|
-
/**
|
|
34
|
-
* Return the resolved `OcrLlmConfig` block (`peaksConfig.ocr.llm`)
|
|
35
|
-
* or `null` when the user has not populated the user config. The
|
|
36
|
-
* 5-state OCR detector uses this as the source of truth; when the
|
|
37
|
-
* returned block is missing required fields it produces a
|
|
38
|
-
* `config-missing` state with a templated `nextActions` payload
|
|
39
|
-
* the user can paste into their config.
|
|
40
|
-
*/
|
|
41
|
-
export declare function getOcrLlmConfig(): OcrLlmConfig | null;
|
|
42
25
|
export declare function bootstrapProjectLanguageConfig(projectRoot: string, language: string): void;
|
|
43
26
|
export declare function readConfig(projectRoot?: string | null): PeaksConfig;
|
|
44
27
|
export declare function writeConfig(partial: Partial<PeaksConfig>, layer?: ConfigLayer): void;
|
|
@@ -53,6 +36,6 @@ export declare function getWorkspaceConfigForPath(path?: string): WorkspaceConfi
|
|
|
53
36
|
export declare function ensureWorkspaceConfigForPath(path?: string): WorkspaceConfig | null;
|
|
54
37
|
export declare function getWorkspaceConfigForCurrentPath(): WorkspaceConfig | null;
|
|
55
38
|
export declare function ensureWorkspaceConfigForCurrentPath(): WorkspaceConfig | null;
|
|
56
|
-
export type {
|
|
39
|
+
export type { TokenRef, WorkspaceConfig, PeaksConfig, ConfigLayer };
|
|
57
40
|
export { getUserConfigPath } from './config-safety.js';
|
|
58
41
|
export { globalConfigPath } from './config-migration.js';
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
3
|
-
import { DEFAULT_CONFIG } from './config-types.js';
|
|
3
|
+
import { DEFAULT_CONFIG, isConfigV2 } from './config-types.js';
|
|
4
4
|
import { stablePath } from '../../shared/path-utils.js';
|
|
5
5
|
import { findProjectRoot, getProjectBootstrapConfigPath, getProjectConfigPath, getUserConfigPath, isInsidePath, readConfigFileSafely, resolveProjectRootForConfig, validateArtifactWorkspaceMarkerPath, validateArtifactWorkspaceRoot, validateProjectBootstrapConfigPathForWrite, validateUserConfigPathForWrite, writeConfigFileSafely, writeProjectConfigFile, writeUserConfigFile } from './config-safety.js';
|
|
6
6
|
import { globalConfigPath, CONFIG_SCHEMA_VERSION_V2 } from './config-migration.js';
|
|
7
|
-
import {
|
|
8
|
-
import { providersConfigPath, proxyConfigPath, readSidecarJson, sidecarExists, workspacesConfigPath, writeSidecarJson } from './sidecar-store.js';
|
|
9
|
-
import { SIDECAR_SCHEMA_VERSION } from './sidecar-store.js';
|
|
7
|
+
import { SIDECAR_SCHEMA_VERSION, providersConfigPath, proxyConfigPath, readSidecarJson, sidecarExists, workspacesConfigPath, writeSidecarJson } from './sidecar-store.js';
|
|
10
8
|
// Re-export resolveProjectRootForConfig and resolveCanonicalProjectRoot for external consumers
|
|
11
9
|
export { resolveProjectRootForConfig, resolveCanonicalProjectRoot } from './config-safety.js';
|
|
12
10
|
/**
|
|
@@ -412,58 +410,6 @@ export function redactConfigSecrets(value, path = '') {
|
|
|
412
410
|
return [key, redactConfigSecrets(entry, nextPath)];
|
|
413
411
|
}));
|
|
414
412
|
}
|
|
415
|
-
const OCR_AUTH_HEADERS = new Set(['authorization', 'x-api-key', 'bearer']);
|
|
416
|
-
function toOcrLlmConfig(value) {
|
|
417
|
-
if (!isRecord(value))
|
|
418
|
-
return {};
|
|
419
|
-
const url = typeof value.url === 'string' && value.url.trim().length > 0 ? value.url.trim() : undefined;
|
|
420
|
-
const authToken = typeof value.authToken === 'string' && value.authToken.length > 0 ? value.authToken : undefined;
|
|
421
|
-
const model = typeof value.model === 'string' && value.model.trim().length > 0 ? value.model.trim() : undefined;
|
|
422
|
-
const useAnthropic = typeof value.useAnthropic === 'boolean' ? value.useAnthropic : undefined;
|
|
423
|
-
const rawAuthHeader = typeof value.authHeader === 'string' ? value.authHeader : undefined;
|
|
424
|
-
const authHeader = rawAuthHeader !== undefined && OCR_AUTH_HEADERS.has(rawAuthHeader)
|
|
425
|
-
? rawAuthHeader
|
|
426
|
-
: undefined;
|
|
427
|
-
return {
|
|
428
|
-
...(url !== undefined ? { url } : {}),
|
|
429
|
-
...(authToken !== undefined ? { authToken } : {}),
|
|
430
|
-
...(model !== undefined ? { model } : {}),
|
|
431
|
-
...(useAnthropic !== undefined ? { useAnthropic } : {}),
|
|
432
|
-
...(authHeader !== undefined ? { authHeader } : {})
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
function toOcrConfig(value) {
|
|
436
|
-
if (!isRecord(value))
|
|
437
|
-
return {};
|
|
438
|
-
return {
|
|
439
|
-
...(isRecord(value.llm) ? { llm: toOcrLlmConfig(value.llm) } : {})
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Read the ocr LLM endpoint config from the user-layer
|
|
444
|
-
* `~/.peaks/config.json`. The user populates this themselves by
|
|
445
|
-
* pasting the `peaks code-review config-template` output (or by
|
|
446
|
-
* running `peaks config set --key ocr.llm.url --value '...'`).
|
|
447
|
-
* peaks-loop never auto-writes these values.
|
|
448
|
-
*/
|
|
449
|
-
export function getOcrConfig() {
|
|
450
|
-
const userConfig = readUserJsonFile() ?? {};
|
|
451
|
-
return toOcrConfig(userConfig.ocr);
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Return the resolved `OcrLlmConfig` block (`peaksConfig.ocr.llm`)
|
|
455
|
-
* or `null` when the user has not populated the user config. The
|
|
456
|
-
* 5-state OCR detector uses this as the source of truth; when the
|
|
457
|
-
* returned block is missing required fields it produces a
|
|
458
|
-
* `config-missing` state with a templated `nextActions` payload
|
|
459
|
-
* the user can paste into their config.
|
|
460
|
-
*/
|
|
461
|
-
export function getOcrLlmConfig() {
|
|
462
|
-
const ocr = getOcrConfig();
|
|
463
|
-
if (!ocr.llm)
|
|
464
|
-
return null;
|
|
465
|
-
return ocr.llm;
|
|
466
|
-
}
|
|
467
413
|
function inferHumanLanguage(value) {
|
|
468
414
|
const normalized = value.trim();
|
|
469
415
|
if (!normalized) {
|
|
@@ -47,38 +47,18 @@ export type WorkspaceConfig = {
|
|
|
47
47
|
artifactStorage?: ArtifactStorageConfig;
|
|
48
48
|
installedCapabilityIds: string[];
|
|
49
49
|
};
|
|
50
|
-
export type OcrAuthHeader = 'authorization' | 'x-api-key' | 'bearer';
|
|
51
|
-
export type OcrLlmConfig = {
|
|
52
|
-
url?: string;
|
|
53
|
-
authToken?: string;
|
|
54
|
-
model?: string;
|
|
55
|
-
useAnthropic?: boolean;
|
|
56
|
-
authHeader?: OcrAuthHeader;
|
|
57
|
-
};
|
|
58
|
-
export type OcrConfig = {
|
|
59
|
-
llm?: OcrLlmConfig;
|
|
60
|
-
};
|
|
61
50
|
/**
|
|
62
|
-
* 2.0.1 slim `~/.peaks/config.json` schema.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* toggles) live in sidecar files under the same `~/.peaks/`
|
|
66
|
-
* directory or in per-project `preferences.json`.
|
|
67
|
-
*
|
|
68
|
-
* The slim shape is enforced on disk by `loadGlobalConfig`; any
|
|
69
|
-
* unknown field on read is silently stripped and the file is
|
|
70
|
-
* rewritten with the slim shape, so a hand-written or partially-
|
|
71
|
-
* migrated file cannot grow stale fields.
|
|
51
|
+
* 2.0.1 slim `~/.peaks/config.json` schema. Runtime settings live in
|
|
52
|
+
* sidecar files under the same `~/.peaks/` directory or in per-project
|
|
53
|
+
* `preferences.json`.
|
|
72
54
|
*
|
|
73
55
|
* NOTE: legacy fields below are kept on the type as `@deprecated`
|
|
74
56
|
* so existing consumers (`config-service.ts`, `workflow-commands.ts`,
|
|
75
57
|
* etc.) continue to compile during the migration window. They are
|
|
76
|
-
* written to / read from sidecar files at runtime
|
|
77
|
-
* `~/.peaks/config.json` only persists `version` + `ocr`.
|
|
58
|
+
* written to / read from sidecar files at runtime.
|
|
78
59
|
*/
|
|
79
60
|
export type PeaksConfig = {
|
|
80
61
|
version: string;
|
|
81
|
-
ocr?: OcrConfig;
|
|
82
62
|
/** @deprecated Moved to `~/.peaks/providers.json` (provider-service.ts) */
|
|
83
63
|
providers?: ModelProviderConfig;
|
|
84
64
|
/** @deprecated Moved to `~/.peaks/proxy.json` (proxy-service.ts) */
|