peaks-loop 4.0.11 → 4.0.13
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 +47 -0
- package/dist/cli/commands/code-commands.js +2 -0
- package/dist/cli/commands/code-orchestrator-can-do.d.ts +17 -0
- package/dist/cli/commands/code-orchestrator-can-do.js +75 -0
- package/dist/cli/commands/core/doctor-command.js +26 -3
- package/dist/cli/commands/workspace/reconcile-command.js +5 -2
- package/dist/services/code/orchestrator-can-do.d.ts +114 -0
- package/dist/services/code/orchestrator-can-do.js +205 -0
- package/dist/services/doctor/doctor-service/checks/multi-binary-drift.d.ts +61 -0
- package/dist/services/doctor/doctor-service/checks/multi-binary-drift.js +268 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +1 -1
- package/dist/services/doctor/doctor-service/checks/workspace-layout.d.ts +5 -2
- package/dist/services/doctor/doctor-service/checks/workspace-layout.js +70 -9
- package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/report/final-summary.d.ts +13 -5
- package/dist/services/doctor/doctor-service/report/final-summary.js +19 -9
- package/dist/services/doctor/doctor-service/types.d.ts +64 -0
- package/dist/services/doctor/doctor-service.d.ts +2 -1
- package/dist/services/doctor/doctor-service.js +1 -0
- package/dist/services/doctor/index.d.ts +1 -1
- package/dist/services/doctor/index.js +1 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +7 -5
- package/dist/services/hooks/presence-marker-detector.js +45 -85
- package/dist/services/migration/v2-10-to-v2-11-service.js +1 -3
- package/dist/services/sc/sc-service.d.ts +11 -7
- package/dist/services/sc/sc-service.js +29 -26
- package/dist/services/session/binding-store.d.ts +6 -6
- package/dist/services/skills/hooks-settings-service.d.ts +1 -1
- package/dist/services/skills/hooks-settings-service.js +1 -1
- package/dist/services/skills/skill-presence-service.d.ts +0 -1
- package/dist/services/skills/skill-presence-service.js +112 -102
- package/dist/services/skills/skill-statusline-renderer.d.ts +3 -1
- package/dist/services/skills/skill-statusline-renderer.js +37 -1
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/skills/skill-statusline-service.js +109 -53
- package/dist/services/skills/skill-statusline-sid-suffix.d.ts +55 -0
- package/dist/services/skills/skill-statusline-sid-suffix.js +65 -0
- package/package.json +4 -4
- package/skills/peaks-code/SKILL.md +6 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.13 — 2026-08-06 (statusline empty-render fix + sid-only marker + multi-binary drift guard)
|
|
4
|
+
|
|
5
|
+
**Statusline callerId fallback + active `[short-sid]` suffix** (slice `2026-08-05-statusline-empty-render-and-short-sid-suffix`, commit `4be37d08`):
|
|
6
|
+
- `src/services/skills/skill-statusline-service.ts` `readPresenceReadOnly` now retries with `callerId: null` when callerId-filtered resolution returns `source: 'none'`. Multi-tenant isolation invariant preserved (slice 4-B Case A regression test stays green).
|
|
7
|
+
- `src/services/skills/skill-statusline-renderer.ts` introduces `formatShortSid(sessionId) = sessionId.split('-').pop()` helper. Active state now renders `Peaks ● peaks-code → peaks-loop [3fe1be]`.
|
|
8
|
+
- New tests: `tests/unit/skills/skill-statusline-empty-render-and-short-sid-suffix.test.ts` (8 cases). Existing dual-skill test updated to accept the `[aaaa]` suffix.
|
|
9
|
+
- Fixes the "new session statusline empty despite active peaks-code lease" bug (root cause: outerSessionId drift between caller shell and lease's callerId).
|
|
10
|
+
|
|
11
|
+
**Statusline sid-only marker (idle/stale) + multi-binary drift guard** (slice `2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard`, commits `95654d48` + `34de6c22` repair):
|
|
12
|
+
- `skill-statusline-service.ts` adds `sessionId: string | null` to `StatusLineModel`. `skill-statusline-sid-suffix.ts` (new, 78 LOC) holds `computeRootSuffix` + `formatShortSid` (extracted from renderer for LOC cap; renderer 806 → 776).
|
|
13
|
+
- Idle and stale states now append ` [3fe1be]` after `peaks-loop` when session.json is bound. Invalid-presence stays unchanged (G2 — read-error signal must remain loud).
|
|
14
|
+
- New `peaks doctor check` plugin `build:multi-binary-drift` scans `process.env.PATH` for `peaks` binaries (cross-platform: `peaks` / `peaks.cmd` / `peaks.ps1`), resolves each to its `node_modules/peaks-loop/package.json`, dedupes by realpath, and emits `PEAKS_MULTI_BINARY_DRIFT` warning when ≥ 2 distinct versions coexist. Caught the `/c/nvm4w/nodejs 4.0.12 + /c/Users/smallMark/AppData/Roaming/npm 3.1.2` case that produced "Hook JSON output validation failed" on fresh IDE sessions.
|
|
15
|
+
- **Severity-aware `buildReport`** (the canonical reference for warn-only doctor checks): `DoctorCheck` now carries `severity: 'error' | 'warning'`. `buildReport` separates errors from warnings; `summary.ok = errors === 0`. Multi-binary drift emits `severity: 'warning'`. CLI exit code only flips on errors.
|
|
16
|
+
- Tests: `tests/unit/skills/skill-statusline-sid-only-marker.test.ts` (8 cases) + `tests/unit/doctor/multi-binary-drift-check.test.ts` (13 cases) + `tests/unit/doctor/final-summary-severity.test.ts` (7 cases) + `tests/unit/doctor/doctor-exit-code-warn-only.test.ts` (5 cases). 170/170 green on slice surface.
|
|
17
|
+
|
|
18
|
+
**Lockstep bump.** peaks-loop-shared `0.0.42 → 0.0.43` (CLI_VERSION re-stamped to 4.0.13); peaks-loop-mut `0.1.15 → 0.1.16`; peaks-loop-shared-channel `0.0.19 → 0.0.20`.
|
|
19
|
+
|
|
20
|
+
## 4.0.12 — 2026-08-05 (5-slice optimization bundle)
|
|
21
|
+
|
|
22
|
+
**publish.yml strict tag gate** (slice 1, commit `f60f7597`):
|
|
23
|
+
- New `gate-strict-tag-format` step inserted after `Checkout` (publish.yml lines 104-130). Runs `git describe --tags --exact-match HEAD` and validates against `^v[0-9]+\.[0-9]+\.[0-9]+$`; rejects `v1` / `v4.0` / `v4.0.11-rc1` / `v4.0.11+sha` shapes with `::error title=strict-tag-format::`.
|
|
24
|
+
- Drift guard: `tests/unit/publish-tag-strict.test.ts` (4 cases).
|
|
25
|
+
|
|
26
|
+
**PreToolUse:Bash hook JSON output validation** (slice 2, commit `eb13e44c`):
|
|
27
|
+
- Regression fix for the 2026-07-27 root cause. `.claude/settings.json` line 14 + `src/services/skills/hooks-settings-service.ts:86` both now append ` --json` to the `peaks gate enforce` hook command.
|
|
28
|
+
- Drift guard: `tests/unit/hooks/gate-enforce-template-json-flag.test.ts` (3 cases).
|
|
29
|
+
- PRD mis-reference correction noted: the actual hook template owner is `hooks-settings-service.ts`, not `claude-settings-template.ts` (the latter carries `peaks code gate-step-08`, a different hook path).
|
|
30
|
+
|
|
31
|
+
**Session-overload signal index** (slice 3, commit `6ef12bde`):
|
|
32
|
+
- New `skills/peaks-code/references/session-overload-signal-index.md` (60 lines): 7-signal lookup table (prompt size / auto-compact zone / sub-agent dispatch guard / statusline compact bar / in-flight batch deferral / compact stalled / sub-agent heartbeat) with thresholds + files + LLM actions + decision flowchart.
|
|
33
|
+
- SKILL.md Step N+2 section gets a 1-line "See also" pointer (+141 bytes; under 30000 cap).
|
|
34
|
+
- Codifies red line: LLM MUST NOT re-ask user about cost/length/context (cite `references/job-loop.md:59` red line #2).
|
|
35
|
+
|
|
36
|
+
**active-skill.json → sid-scoped lease projection** (slice 4, 3 sub-slices):
|
|
37
|
+
- Slice 4-A (`dc350c2c`): `skill-presence-service.ts` write path removed; `presence-marker-detector.ts` deprecated-constant comments added.
|
|
38
|
+
- Slice 4-B (`7699f70f`): `skill-statusline-service.ts` `readPresenceReadOnly` rewritten to canonical-only (drops `legacyPresence: true`, enumerates `listPresenceLeases`, deletes `PRESENCE_FILE` / `PRESENCE_FILE_LEGACY`). Fixes the "new session statusline empty" issue (root cause: outerSessionId drift between session.json and the global single-slot file).
|
|
39
|
+
- Slice 4-C (`1b08a62c`): doctor / sc / migration / hooks / skills / reconcile-command all migrated to canonical lease reads. New `STALE_SINGLE_SLOT_FILES` workspace-layout check surfaces orphan `.peaks/_runtime/active-skill.json` as a "stale single-slot presence" diagnostic (not an error). Drift guard: `tests/unit/workspace/active-skill-json-cleanup.test.ts` (3 cases).
|
|
40
|
+
- Multi-session on one project is now safe (each session writes its own `presence-<callerId>.json` under `.peaks/_runtime/<sid>/presence-index/`; the deprecated single-slot global file no longer races).
|
|
41
|
+
|
|
42
|
+
**Orchestrator-can-do probe CLI** (slice 5, commit `52736c82`):
|
|
43
|
+
- New `peaks code orchestrator-can-do --slice-spec <text> --json` command (the 2026-08-05 lesson converted into a downstream-inheritable CLI). Evaluates 4 boundary questions (source code? sub-agent available? requires user decision? context sustainable?) and returns `{ canDoInSession, blockers, warnings, suggestions, contextRatio, subAgentAvailable }`.
|
|
44
|
+
- SKILL.md gains a Step 0.51 paragraph pointing at it (LLM must run this probe before deciding to push a slice to the next session).
|
|
45
|
+
- 17 unit tests cover all 4 boundary branches.
|
|
46
|
+
- Anti-fake-green pattern: any future orchestrator capability judgment MUST call this CLI rather than re-derive the 4-question matrix from intuition.
|
|
47
|
+
|
|
48
|
+
**Lockstep bump.** peaks-loop-shared `0.0.41 → 0.0.42` (CLI_VERSION re-stamped to 4.0.12); peaks-loop-mut `0.1.14 → 0.1.15`; peaks-loop-shared-channel `0.0.18 → 0.0.19`.
|
|
49
|
+
|
|
3
50
|
## 4.0.11 — 2026-08-05 (BDD test-style + statusline bugs)
|
|
4
51
|
|
|
5
52
|
**BDD given-when-then test style** (rid-2026-08-05-bdd-test-style, 5 slice / 19 commit):
|
|
@@ -26,6 +26,7 @@ import { registerCodeRunCommand } from './code-run-command.js';
|
|
|
26
26
|
import { registerCodeModeGateCommands } from './code-mode-gate-commands.js';
|
|
27
27
|
import { registerCodeJobShapeCommands } from './code-job-shape-commands.js';
|
|
28
28
|
import { registerCodeRuntimeCommands } from './code-runtime-commands.js';
|
|
29
|
+
import { registerCodeOrchestratorCanDoCommand } from './code-orchestrator-can-do.js';
|
|
29
30
|
const STEP_ORDER = [
|
|
30
31
|
'load-memory',
|
|
31
32
|
'standards-preflight',
|
|
@@ -114,5 +115,6 @@ export function registerCodeCommands(program, io) {
|
|
|
114
115
|
registerCodeModeGateCommands(code, io);
|
|
115
116
|
registerCodeJobShapeCommands(code, io);
|
|
116
117
|
registerCodeRuntimeCommands(code, io);
|
|
118
|
+
registerCodeOrchestratorCanDoCommand(code, io);
|
|
117
119
|
registerCodeRunCommand(code, io);
|
|
118
120
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-05-orchestrator-can-do-probe — CLI shim for
|
|
3
|
+
* `peaks code orchestrator-can-do`.
|
|
4
|
+
*
|
|
5
|
+
* Thin entry point that wires the `orchestrator-can-do` subcommand
|
|
6
|
+
* to the parent `code` command. The actual probe logic lives in
|
|
7
|
+
* `../../services/code/orchestrator-can-do.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Encodes the 2026-08-05 lesson (`.peaks/memory/2026-08-05-peaks-code-
|
|
10
|
+
* orchestrator-capability-misjudgment.md`) — the peaks-code orchestrator
|
|
11
|
+
* MUST delegate source-code changes via sub-agent dispatch, not via
|
|
12
|
+
* direct Edit/Write. The probe returns a structured verdict so the LLM
|
|
13
|
+
* does not need to vibes-call "can this slice run in the current session".
|
|
14
|
+
*/
|
|
15
|
+
import type { Command } from 'commander';
|
|
16
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
17
|
+
export declare function registerCodeOrchestratorCanDoCommand(code: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-05-orchestrator-can-do-probe — CLI shim for
|
|
3
|
+
* `peaks code orchestrator-can-do`.
|
|
4
|
+
*
|
|
5
|
+
* Thin entry point that wires the `orchestrator-can-do` subcommand
|
|
6
|
+
* to the parent `code` command. The actual probe logic lives in
|
|
7
|
+
* `../../services/code/orchestrator-can-do.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Encodes the 2026-08-05 lesson (`.peaks/memory/2026-08-05-peaks-code-
|
|
10
|
+
* orchestrator-capability-misjudgment.md`) — the peaks-code orchestrator
|
|
11
|
+
* MUST delegate source-code changes via sub-agent dispatch, not via
|
|
12
|
+
* direct Edit/Write. The probe returns a structured verdict so the LLM
|
|
13
|
+
* does not need to vibes-call "can this slice run in the current session".
|
|
14
|
+
*/
|
|
15
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
16
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
17
|
+
import { evaluateOrchestratorCanDo, OrchestratorCanDoError, ORCHESTRATOR_PRECOMPACT_RATIO, ORCHESTRATOR_REDLINE_RATIO, } from '../../services/code/orchestrator-can-do.js';
|
|
18
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
19
|
+
export function registerCodeOrchestratorCanDoCommand(code, io) {
|
|
20
|
+
addJsonOption(code
|
|
21
|
+
.command('orchestrator-can-do')
|
|
22
|
+
.description('2026-08-05 lesson: probe whether the LLM orchestrator can execute a slice in the ' +
|
|
23
|
+
'current session. Evaluates 4 boundary questions (source-code touched? sub-agent ' +
|
|
24
|
+
'available? requires user decision? context sustainable?) and returns a structured ' +
|
|
25
|
+
'verdict with canDoInSession, blockers, warnings, and concrete next-action ' +
|
|
26
|
+
'suggestions. Default to canDoInSession=true unless hard blockers are present; ' +
|
|
27
|
+
'sub-agent dispatch (`peaks sub-agent dispatch rd`) is the canonical delegation path ' +
|
|
28
|
+
'for source-code changes.')
|
|
29
|
+
.requiredOption('--slice-spec <text>', 'short description of the slice (e.g. "modify src/services/foo.ts")')
|
|
30
|
+
.option('--project <path>', 'target project root (default: findProjectRoot(cwd))')
|
|
31
|
+
.option('--peaks-bin <path>', 'peaks binary path (test seam; default: peaks on PATH)')).action(async (opts) => {
|
|
32
|
+
try {
|
|
33
|
+
const projectRoot = opts.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
34
|
+
const peaksBin = opts.peaksBin ?? 'peaks';
|
|
35
|
+
const result = await evaluateOrchestratorCanDo({
|
|
36
|
+
sliceSpec: opts.sliceSpec,
|
|
37
|
+
projectRoot,
|
|
38
|
+
probeSubAgentAvailable: () => probeSubAgentAvailableWithBin(projectRoot, peaksBin),
|
|
39
|
+
probeContextRatio: () => probeContextRatioWithBin(projectRoot, peaksBin),
|
|
40
|
+
});
|
|
41
|
+
printResult(io, ok('code.orchestrator-can-do', result, [...result.warnings], [...result.suggestions, ...summaryLines(result)]), opts.json);
|
|
42
|
+
if (!result.canDoInSession)
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof OrchestratorCanDoError) {
|
|
47
|
+
printResult(io, fail('code.orchestrator-can-do', err.code, err.message, null, [
|
|
48
|
+
'Pass --slice-spec <text> describing what the slice should change',
|
|
49
|
+
]), opts.json);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
printResult(io, fail('code.orchestrator-can-do', 'PROBE_FAILED', getErrorMessage(err), null, [
|
|
54
|
+
'Verify --slice-spec is non-empty and --project is a valid path',
|
|
55
|
+
]), opts.json);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function summaryLines(result) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push(`verdict: ${result.canDoInSession ? 'canDoInSession=true' : 'canDoInSession=false (blockers present)'}; ` +
|
|
63
|
+
`q1=src[${result.q1SourceCodeTouched ? 'Y' : 'N'}] q2=subagent[${result.q2SubAgentAvailable ? 'Y' : 'N'}] ` +
|
|
64
|
+
`q3=user-decision[${result.q3RequiresUserDecision ? 'Y' : 'N'}] q4=ratio=${result.contextRatio.toFixed(2)} ` +
|
|
65
|
+
`(red-line ${ORCHESTRATOR_REDLINE_RATIO} / pre-compact ${ORCHESTRATOR_PRECOMPACT_RATIO})`);
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
async function probeSubAgentAvailableWithBin(projectRoot, peaksBin) {
|
|
69
|
+
const { probeSubAgentAvailable } = await import('../../services/code/orchestrator-can-do.js');
|
|
70
|
+
return probeSubAgentAvailable(projectRoot, peaksBin);
|
|
71
|
+
}
|
|
72
|
+
async function probeContextRatioWithBin(projectRoot, peaksBin) {
|
|
73
|
+
const { probeContextRatio } = await import('../../services/code/orchestrator-can-do.js');
|
|
74
|
+
return probeContextRatio(projectRoot, peaksBin);
|
|
75
|
+
}
|
|
@@ -212,6 +212,10 @@ export function registerDoctorCommand(program, io) {
|
|
|
212
212
|
const data = logsSection === null
|
|
213
213
|
? { ...report, staleBinding: staleBindingSection }
|
|
214
214
|
: { ...report, logs: logsSection, staleBinding: staleBindingSection };
|
|
215
|
+
// Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
216
|
+
// repair cycle: `report.summary.ok` already factors in the
|
|
217
|
+
// severity-aware aggregation in `buildReport` (warnings do NOT
|
|
218
|
+
// flip `ok`). Stale-binding is independent and still escalates.
|
|
215
219
|
const result = report.summary.ok && staleInstances.length === 0
|
|
216
220
|
? ok('doctor', data)
|
|
217
221
|
: fail('doctor', 'DOCTOR_FAILED', staleInstances.length > 0
|
|
@@ -224,9 +228,20 @@ export function registerDoctorCommand(program, io) {
|
|
|
224
228
|
}
|
|
225
229
|
else {
|
|
226
230
|
// Human-readable: one line per check, green/red indicators, no JSON.
|
|
231
|
+
// Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
232
|
+
// repair cycle: warn-only findings (severity: 'warning') surface
|
|
233
|
+
// as `! check.ok` with the `! ` (warning) glyph so the operator
|
|
234
|
+
// can tell them apart from real errors without reading JSON.
|
|
227
235
|
for (const check of report.checks) {
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
if (check.ok) {
|
|
237
|
+
io.stdout(` + ${check.message}`);
|
|
238
|
+
}
|
|
239
|
+
else if (check.severity === 'warning') {
|
|
240
|
+
io.stdout(` ! ${check.message}`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
io.stdout(` × ${check.message}`);
|
|
244
|
+
}
|
|
230
245
|
}
|
|
231
246
|
if (logsSection !== null) {
|
|
232
247
|
io.stdout('\n logs:');
|
|
@@ -252,11 +267,19 @@ export function registerDoctorCommand(program, io) {
|
|
|
252
267
|
io.stdout(' rerun with --cleanup-stale to drop them');
|
|
253
268
|
}
|
|
254
269
|
}
|
|
255
|
-
io.stdout(`\n ${report.summary.passed} passed, ${report.summary.failed} failed`);
|
|
270
|
+
io.stdout(`\n ${report.summary.passed} passed, ${report.summary.failed} failed${report.summary.warnings > 0 ? `, ${report.summary.warnings} warning(s)` : ''}`);
|
|
256
271
|
if (!report.summary.ok || staleInstances.length > 0) {
|
|
257
272
|
io.stderr(`\nDOCTOR_FAILED: ${staleInstances.length > 0 ? 'stale binding present' : `${report.summary.failed} check(s) failed`}.`);
|
|
258
273
|
}
|
|
274
|
+
else if (report.summary.warnings > 0) {
|
|
275
|
+
io.stdout(`\n ${report.summary.warnings} warning(s) present — exit code 0 (warn-only).`);
|
|
276
|
+
}
|
|
259
277
|
}
|
|
278
|
+
// Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
279
|
+
// repair cycle: `report.summary.ok` already factors in the
|
|
280
|
+
// severity-aware aggregation in `buildReport` (warnings do NOT
|
|
281
|
+
// flip `ok`). The exit-code gate remains `summary.ok &&
|
|
282
|
+
// !staleInstances` — no separate warning special-case needed here.
|
|
260
283
|
if (!report.summary.ok || staleInstances.length > 0) {
|
|
261
284
|
process.exitCode = 1;
|
|
262
285
|
}
|
|
@@ -26,9 +26,12 @@ export function registerWorkspaceReconcileCommand(workspace, io) {
|
|
|
26
26
|
'By default (no --apply) the command performs four actions:\n' +
|
|
27
27
|
' 1. Migrates legacy runtime files into .peaks/_runtime/: ' +
|
|
28
28
|
'.peaks/.session.json -> .peaks/_runtime/session.json, ' +
|
|
29
|
-
'.peaks/.active-skill.json -> .peaks/_runtime/active-skill.json, ' +
|
|
30
29
|
'.peaks/sop-state/ -> .peaks/_runtime/sop-state/ ' +
|
|
31
|
-
'(idempotent; no-op if already on the new layout)
|
|
30
|
+
'(idempotent; no-op if already on the new layout). ' +
|
|
31
|
+
'Single-slot presence files (.peaks/.active-skill.json and ' +
|
|
32
|
+
'.peaks/_runtime/active-skill.json) are no longer migrated; ' +
|
|
33
|
+
'they were removed in slice 4.0.11 and should be deleted ' +
|
|
34
|
+
'manually if present.\n' +
|
|
32
35
|
' 2. Re-points .peaks/_runtime/session.json to the canonical session ' +
|
|
33
36
|
'using a 4-tier heuristic: active-skill binding -> latest session.json mtime -> ' +
|
|
34
37
|
'latest any-file mtime -> dir-name sort.\n' +
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-05-orchestrator-can-do-probe — service layer for
|
|
3
|
+
* `peaks code orchestrator-can-do`.
|
|
4
|
+
*
|
|
5
|
+
* Encodes the 2026-08-05 lesson (`.peaks/memory/2026-08-05-peaks-code-
|
|
6
|
+
* orchestrator-capability-misjudgment.md`): the peaks-code orchestrator
|
|
7
|
+
* MUST NOT Edit/Write `src/` files directly, but MUST delegate via
|
|
8
|
+
* `peaks sub-agent dispatch`. The decision "can this slice run in the
|
|
9
|
+
* current session" must be a structured probe — not a vibes call.
|
|
10
|
+
*
|
|
11
|
+
* The probe answers 4 boundary questions:
|
|
12
|
+
* Q1 — Is the change to source code? (keywords: src/, *.ts, *.tsx,
|
|
13
|
+
* *.js, package.json, tsconfig, workflows/). If yes → NOT a
|
|
14
|
+
* blocker; orchestrator delegates to sub-agent.
|
|
15
|
+
* Q2 — Can a sub-agent be dispatched? (probe `peaks sub-agent
|
|
16
|
+
* dispatch --role rd --help`). If no → blocker.
|
|
17
|
+
* Q3 — Does the slice require user decisions? (keywords: design,
|
|
18
|
+
* decide, ?, 选择, 决定). If yes → soft warning, NOT a blocker
|
|
19
|
+
* (the LLM should AskUserQuestion, which is cheap).
|
|
20
|
+
* Q4 — Is context usage sustainable? (probe `peaks code context-now
|
|
21
|
+
* --json`). ratio ≥ 0.95 → blocker (red-line); ≥ 0.85 →
|
|
22
|
+
* blocker (auto-compact-now).
|
|
23
|
+
*
|
|
24
|
+
* Decision rule:
|
|
25
|
+
* canDoInSession === (blockers.length === 0)
|
|
26
|
+
*
|
|
27
|
+
* Concrete suggestion when canDoInSession=true and slice touches
|
|
28
|
+
* source code:
|
|
29
|
+
* `peaks sub-agent dispatch rd --prompt "<slice-spec>" --request-id
|
|
30
|
+
* <rid> --project . --batch-id <uuid>`
|
|
31
|
+
*
|
|
32
|
+
* Pure-function module. The CLI shim (code-orchestrator-can-do.ts)
|
|
33
|
+
* adapts the envelope into the program's `ResultEnvelope<T>` shape.
|
|
34
|
+
*/
|
|
35
|
+
/** Slice 2026-08-05-orchestrator-can-do-probe: red-line threshold. */
|
|
36
|
+
export declare const ORCHESTRATOR_REDLINE_RATIO = 0.95;
|
|
37
|
+
/** Slice 2026-08-05-orchestrator-can-do-probe: pre-compact threshold. */
|
|
38
|
+
export declare const ORCHESTRATOR_PRECOMPACT_RATIO = 0.85;
|
|
39
|
+
/** Source-code keywords that signal "do NOT Edit/Write directly". */
|
|
40
|
+
export declare const SOURCE_CODE_KEYWORDS: readonly string[];
|
|
41
|
+
/** Decision-marker keywords that signal "needs user AskUserQuestion". */
|
|
42
|
+
export declare const DECISION_KEYWORDS: readonly string[];
|
|
43
|
+
export interface ContextProbe {
|
|
44
|
+
/** 0.0–1.0; ≥0.85 = pre-compact; ≥0.95 = red-line. */
|
|
45
|
+
readonly ratio: number;
|
|
46
|
+
/** Source tag from `peaks code context-now`. */
|
|
47
|
+
readonly source: string;
|
|
48
|
+
}
|
|
49
|
+
export interface OrchestratorCanDoInput {
|
|
50
|
+
readonly sliceSpec: string;
|
|
51
|
+
readonly projectRoot: string;
|
|
52
|
+
/**
|
|
53
|
+
* Test seam — caller injects probe results. Production CLI builds
|
|
54
|
+
* these via `probeSubAgentAvailable` + `probeContextRatio`. When the
|
|
55
|
+
* test seam is set, the CLI's actual probes are skipped.
|
|
56
|
+
*/
|
|
57
|
+
readonly probeSubAgentAvailable?: () => Promise<boolean>;
|
|
58
|
+
readonly probeContextRatio?: () => Promise<ContextProbe>;
|
|
59
|
+
}
|
|
60
|
+
export interface OrchestratorCanDoResult {
|
|
61
|
+
/** canDoInSession === (blockers.length === 0). */
|
|
62
|
+
readonly canDoInSession: boolean;
|
|
63
|
+
readonly blockers: readonly string[];
|
|
64
|
+
readonly warnings: readonly string[];
|
|
65
|
+
readonly suggestions: readonly string[];
|
|
66
|
+
readonly contextRatio: number;
|
|
67
|
+
readonly subAgentAvailable: boolean;
|
|
68
|
+
/** Diagnostic — which of the 4 boundary questions fired. */
|
|
69
|
+
readonly q1SourceCodeTouched: boolean;
|
|
70
|
+
readonly q2SubAgentAvailable: boolean;
|
|
71
|
+
readonly q3RequiresUserDecision: boolean;
|
|
72
|
+
readonly q4ContextRatio: number;
|
|
73
|
+
}
|
|
74
|
+
export declare class OrchestratorCanDoError extends Error {
|
|
75
|
+
readonly code: 'MISSING_SLICE_SPEC' | 'PROBE_FAILED';
|
|
76
|
+
constructor(message: string, code: 'MISSING_SLICE_SPEC' | 'PROBE_FAILED');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Q1: does the slice touch source code? Pure keyword scan over
|
|
80
|
+
* the slice-spec string. Case-insensitive substring match.
|
|
81
|
+
*/
|
|
82
|
+
export declare function detectSourceCodeTouched(sliceSpec: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Q3: does the slice require user decisions? Pure keyword scan
|
|
85
|
+
* over the slice-spec string.
|
|
86
|
+
*/
|
|
87
|
+
export declare function detectRequiresUserDecision(sliceSpec: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Q2: probe `peaks sub-agent dispatch --role rd --help`. Returns
|
|
90
|
+
* true when the subprocess exits 0. Resolves to false on spawn
|
|
91
|
+
* failure or non-zero exit.
|
|
92
|
+
*/
|
|
93
|
+
export declare function probeSubAgentAvailable(projectRoot: string, peaksBin?: string): Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Q4: probe `peaks code context-now --json`. Parses the data.ratio
|
|
96
|
+
* field. Falls back to {ratio: 0, source: 'unavailable'} when the
|
|
97
|
+
* subprocess fails or returns malformed JSON.
|
|
98
|
+
*/
|
|
99
|
+
export declare function probeContextRatio(projectRoot: string, peaksBin?: string): Promise<ContextProbe>;
|
|
100
|
+
/**
|
|
101
|
+
* Build the structured OrchestratorCanDoResult. Pure over the 4 Q
|
|
102
|
+
* signals + sliceSpec. Decision rule is: canDoInSession === !blockers.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildOrchestratorCanDoResult(input: OrchestratorCanDoInput, signals: {
|
|
105
|
+
q1SourceCodeTouched: boolean;
|
|
106
|
+
q2SubAgentAvailable: boolean;
|
|
107
|
+
q3RequiresUserDecision: boolean;
|
|
108
|
+
q4ContextRatio: number;
|
|
109
|
+
}): OrchestratorCanDoResult;
|
|
110
|
+
/**
|
|
111
|
+
* Evaluate a slice-spec end-to-end. Probes Q2/Q4 via subprocess
|
|
112
|
+
* (overridable via test seams in `input`). Q1/Q3 are pure.
|
|
113
|
+
*/
|
|
114
|
+
export declare function evaluateOrchestratorCanDo(input: OrchestratorCanDoInput): Promise<OrchestratorCanDoResult>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-05-orchestrator-can-do-probe — service layer for
|
|
3
|
+
* `peaks code orchestrator-can-do`.
|
|
4
|
+
*
|
|
5
|
+
* Encodes the 2026-08-05 lesson (`.peaks/memory/2026-08-05-peaks-code-
|
|
6
|
+
* orchestrator-capability-misjudgment.md`): the peaks-code orchestrator
|
|
7
|
+
* MUST NOT Edit/Write `src/` files directly, but MUST delegate via
|
|
8
|
+
* `peaks sub-agent dispatch`. The decision "can this slice run in the
|
|
9
|
+
* current session" must be a structured probe — not a vibes call.
|
|
10
|
+
*
|
|
11
|
+
* The probe answers 4 boundary questions:
|
|
12
|
+
* Q1 — Is the change to source code? (keywords: src/, *.ts, *.tsx,
|
|
13
|
+
* *.js, package.json, tsconfig, workflows/). If yes → NOT a
|
|
14
|
+
* blocker; orchestrator delegates to sub-agent.
|
|
15
|
+
* Q2 — Can a sub-agent be dispatched? (probe `peaks sub-agent
|
|
16
|
+
* dispatch --role rd --help`). If no → blocker.
|
|
17
|
+
* Q3 — Does the slice require user decisions? (keywords: design,
|
|
18
|
+
* decide, ?, 选择, 决定). If yes → soft warning, NOT a blocker
|
|
19
|
+
* (the LLM should AskUserQuestion, which is cheap).
|
|
20
|
+
* Q4 — Is context usage sustainable? (probe `peaks code context-now
|
|
21
|
+
* --json`). ratio ≥ 0.95 → blocker (red-line); ≥ 0.85 →
|
|
22
|
+
* blocker (auto-compact-now).
|
|
23
|
+
*
|
|
24
|
+
* Decision rule:
|
|
25
|
+
* canDoInSession === (blockers.length === 0)
|
|
26
|
+
*
|
|
27
|
+
* Concrete suggestion when canDoInSession=true and slice touches
|
|
28
|
+
* source code:
|
|
29
|
+
* `peaks sub-agent dispatch rd --prompt "<slice-spec>" --request-id
|
|
30
|
+
* <rid> --project . --batch-id <uuid>`
|
|
31
|
+
*
|
|
32
|
+
* Pure-function module. The CLI shim (code-orchestrator-can-do.ts)
|
|
33
|
+
* adapts the envelope into the program's `ResultEnvelope<T>` shape.
|
|
34
|
+
*/
|
|
35
|
+
import { execFile } from 'node:child_process';
|
|
36
|
+
import { promisify } from 'node:util';
|
|
37
|
+
import { randomUUID } from 'node:crypto';
|
|
38
|
+
const execFileAsync = promisify(execFile);
|
|
39
|
+
/** Slice 2026-08-05-orchestrator-can-do-probe: red-line threshold. */
|
|
40
|
+
export const ORCHESTRATOR_REDLINE_RATIO = 0.95;
|
|
41
|
+
/** Slice 2026-08-05-orchestrator-can-do-probe: pre-compact threshold. */
|
|
42
|
+
export const ORCHESTRATOR_PRECOMPACT_RATIO = 0.85;
|
|
43
|
+
/** Source-code keywords that signal "do NOT Edit/Write directly". */
|
|
44
|
+
export const SOURCE_CODE_KEYWORDS = [
|
|
45
|
+
'src/',
|
|
46
|
+
'source/',
|
|
47
|
+
'.ts',
|
|
48
|
+
'.tsx',
|
|
49
|
+
'.js',
|
|
50
|
+
'.jsx',
|
|
51
|
+
'package.json',
|
|
52
|
+
'tsconfig',
|
|
53
|
+
'workflows/',
|
|
54
|
+
'.py',
|
|
55
|
+
'.go',
|
|
56
|
+
'.rs',
|
|
57
|
+
];
|
|
58
|
+
/** Decision-marker keywords that signal "needs user AskUserQuestion". */
|
|
59
|
+
export const DECISION_KEYWORDS = [
|
|
60
|
+
'design',
|
|
61
|
+
'decide',
|
|
62
|
+
'choose',
|
|
63
|
+
'?',
|
|
64
|
+
'选择',
|
|
65
|
+
'决定',
|
|
66
|
+
'design decision',
|
|
67
|
+
'user choice',
|
|
68
|
+
];
|
|
69
|
+
export class OrchestratorCanDoError extends Error {
|
|
70
|
+
code;
|
|
71
|
+
constructor(message, code) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.code = code;
|
|
74
|
+
this.name = 'OrchestratorCanDoError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Q1: does the slice touch source code? Pure keyword scan over
|
|
79
|
+
* the slice-spec string. Case-insensitive substring match.
|
|
80
|
+
*/
|
|
81
|
+
export function detectSourceCodeTouched(sliceSpec) {
|
|
82
|
+
const lower = sliceSpec.toLowerCase();
|
|
83
|
+
return SOURCE_CODE_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Q3: does the slice require user decisions? Pure keyword scan
|
|
87
|
+
* over the slice-spec string.
|
|
88
|
+
*/
|
|
89
|
+
export function detectRequiresUserDecision(sliceSpec) {
|
|
90
|
+
const lower = sliceSpec.toLowerCase();
|
|
91
|
+
return DECISION_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Q2: probe `peaks sub-agent dispatch --role rd --help`. Returns
|
|
95
|
+
* true when the subprocess exits 0. Resolves to false on spawn
|
|
96
|
+
* failure or non-zero exit.
|
|
97
|
+
*/
|
|
98
|
+
export async function probeSubAgentAvailable(projectRoot, peaksBin = 'peaks') {
|
|
99
|
+
try {
|
|
100
|
+
await execFileAsync(peaksBin, ['sub-agent', 'dispatch', '--role', 'rd', '--help'], {
|
|
101
|
+
cwd: projectRoot,
|
|
102
|
+
timeout: 5000,
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Q4: probe `peaks code context-now --json`. Parses the data.ratio
|
|
112
|
+
* field. Falls back to {ratio: 0, source: 'unavailable'} when the
|
|
113
|
+
* subprocess fails or returns malformed JSON.
|
|
114
|
+
*/
|
|
115
|
+
export async function probeContextRatio(projectRoot, peaksBin = 'peaks') {
|
|
116
|
+
try {
|
|
117
|
+
const { stdout } = await execFileAsync(peaksBin, ['code', 'context-now', '--project', projectRoot, '--json'], {
|
|
118
|
+
cwd: projectRoot,
|
|
119
|
+
timeout: 10000,
|
|
120
|
+
});
|
|
121
|
+
const parsed = JSON.parse(stdout);
|
|
122
|
+
const ratio = typeof parsed.data?.ratio === 'number' ? parsed.data.ratio : 0;
|
|
123
|
+
const source = typeof parsed.data?.source === 'string' ? parsed.data.source : 'unavailable';
|
|
124
|
+
return { ratio, source };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { ratio: 0, source: 'unavailable' };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build the structured OrchestratorCanDoResult. Pure over the 4 Q
|
|
132
|
+
* signals + sliceSpec. Decision rule is: canDoInSession === !blockers.
|
|
133
|
+
*/
|
|
134
|
+
export function buildOrchestratorCanDoResult(input, signals) {
|
|
135
|
+
const blockers = [];
|
|
136
|
+
const warnings = [];
|
|
137
|
+
const suggestions = [];
|
|
138
|
+
// Q2 — sub-agent availability is a hard precondition.
|
|
139
|
+
if (!signals.q2SubAgentAvailable) {
|
|
140
|
+
blockers.push('sub-agent dispatch unavailable (peaks sub-agent dispatch --help failed)');
|
|
141
|
+
suggestions.push('verify peaks CLI is on PATH; check `peaks --version`');
|
|
142
|
+
}
|
|
143
|
+
// Q4 — context ratio gate. ≥0.95 → red-line; ≥0.85 → pre-compact.
|
|
144
|
+
if (signals.q4ContextRatio >= ORCHESTRATOR_REDLINE_RATIO) {
|
|
145
|
+
blockers.push(`context red-line (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_REDLINE_RATIO}); auto-compact now or push to next session`);
|
|
146
|
+
suggestions.push('peaks compact auto --execute');
|
|
147
|
+
}
|
|
148
|
+
else if (signals.q4ContextRatio >= ORCHESTRATOR_PRECOMPACT_RATIO) {
|
|
149
|
+
blockers.push(`context near limit (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_PRECOMPACT_RATIO}); auto-compact or push to next session`);
|
|
150
|
+
suggestions.push('peaks compact auto --execute');
|
|
151
|
+
}
|
|
152
|
+
// Q3 — user-decision keywords → soft warning, NOT a blocker. The
|
|
153
|
+
// LLM should AskUserQuestion, which is cheap.
|
|
154
|
+
if (signals.q3RequiresUserDecision) {
|
|
155
|
+
warnings.push('slice-spec contains decision keywords; AskUserQuestion before proceeding');
|
|
156
|
+
}
|
|
157
|
+
// Q1 — source-code touched is NOT a blocker. The orchestrator
|
|
158
|
+
// delegates via sub-agent dispatch.
|
|
159
|
+
if (signals.q1SourceCodeTouched && signals.q2SubAgentAvailable) {
|
|
160
|
+
const batchId = randomUUID();
|
|
161
|
+
const rid = 'rid-' + Date.now().toString(36);
|
|
162
|
+
suggestions.push(`peaks sub-agent dispatch rd --prompt "<slice-spec>" --request-id ${rid} --project ${input.projectRoot} --batch-id ${batchId}`);
|
|
163
|
+
}
|
|
164
|
+
// When canDoInSession=true and there's no source code touched,
|
|
165
|
+
// surface a generic "do it" suggestion.
|
|
166
|
+
const canDoInSession = blockers.length === 0;
|
|
167
|
+
if (canDoInSession && !signals.q1SourceCodeTouched) {
|
|
168
|
+
suggestions.push(`non-source-code slice; orchestrator may handle in-session (e.g. via Write/Edit tools or directly)`);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
canDoInSession,
|
|
172
|
+
blockers,
|
|
173
|
+
warnings,
|
|
174
|
+
suggestions,
|
|
175
|
+
contextRatio: signals.q4ContextRatio,
|
|
176
|
+
subAgentAvailable: signals.q2SubAgentAvailable,
|
|
177
|
+
q1SourceCodeTouched: signals.q1SourceCodeTouched,
|
|
178
|
+
q2SubAgentAvailable: signals.q2SubAgentAvailable,
|
|
179
|
+
q3RequiresUserDecision: signals.q3RequiresUserDecision,
|
|
180
|
+
q4ContextRatio: signals.q4ContextRatio,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Evaluate a slice-spec end-to-end. Probes Q2/Q4 via subprocess
|
|
185
|
+
* (overridable via test seams in `input`). Q1/Q3 are pure.
|
|
186
|
+
*/
|
|
187
|
+
export async function evaluateOrchestratorCanDo(input) {
|
|
188
|
+
if (!input.sliceSpec || input.sliceSpec.trim().length === 0) {
|
|
189
|
+
throw new OrchestratorCanDoError('--slice-spec is required', 'MISSING_SLICE_SPEC');
|
|
190
|
+
}
|
|
191
|
+
const q1SourceCodeTouched = detectSourceCodeTouched(input.sliceSpec);
|
|
192
|
+
const q3RequiresUserDecision = detectRequiresUserDecision(input.sliceSpec);
|
|
193
|
+
const probeSubAgent = input.probeSubAgentAvailable ?? (() => probeSubAgentAvailable(input.projectRoot));
|
|
194
|
+
const probeContext = input.probeContextRatio ?? (() => probeContextRatio(input.projectRoot));
|
|
195
|
+
const [subAgentAvailable, ctxProbe] = await Promise.all([
|
|
196
|
+
probeSubAgent(),
|
|
197
|
+
probeContext(),
|
|
198
|
+
]);
|
|
199
|
+
return buildOrchestratorCanDoResult(input, {
|
|
200
|
+
q1SourceCodeTouched,
|
|
201
|
+
q2SubAgentAvailable: subAgentAvailable,
|
|
202
|
+
q3RequiresUserDecision,
|
|
203
|
+
q4ContextRatio: ctxProbe.ratio,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check: PATH-scoped `peaks-loop` binary drift
|
|
3
|
+
* (`build:multi-binary-drift`).
|
|
4
|
+
*
|
|
5
|
+
* Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
6
|
+
* (G3/G4). When more than one `peaks-loop` binary is discoverable on
|
|
7
|
+
* `process.env.PATH` AND the discovered versions disagree, the doctor
|
|
8
|
+
* emits a `PEAKS_MULTI_BINARY_DRIFT` warning. The user-reported
|
|
9
|
+
* production symptom — `peaks` resolving to an old version (e.g.
|
|
10
|
+
* 3.1.2 on `%AppData%\Roaming\npm`) while the freshly installed
|
|
11
|
+
* binary on another PATH entry (e.g. `C:\nvm4w\nodejs\peaks` 4.0.12)
|
|
12
|
+
* is what the user just bumped — produces "Hook JSON output
|
|
13
|
+
* validation failed" when the IDE statusline hook inherits the older
|
|
14
|
+
* binary via PATH ordering.
|
|
15
|
+
*
|
|
16
|
+
* Behavior contract (PRD AC6 / AC7 / AC8 / AC9):
|
|
17
|
+
* - AC6: ≥ 2 peaks-loop binaries with different versions → warning
|
|
18
|
+
* - AC7: warning severity only, doctor exit 0 (no other check
|
|
19
|
+
* flipped to error)
|
|
20
|
+
* - AC8: drift is scoped to peaks-loop (`package.json#name ===
|
|
21
|
+
* 'peaks-loop'`); sibling npm tools are NOT flagged
|
|
22
|
+
* - AC9: cross-platform — uses `process.env.PATH` + `path.delimiter`
|
|
23
|
+
* + binary naming `peaks` / `peaks.cmd` / `peaks.ps1`
|
|
24
|
+
*
|
|
25
|
+
* Pure `inspectMultiBinaryDrift` helper is exported so tests can drive
|
|
26
|
+
* the filesystem walk without monkey-patching `process.env.PATH` or
|
|
27
|
+
* `realpathSync`. Every probe call is wrapped in try/catch (read-only,
|
|
28
|
+
* must not throw across the doctor boundary).
|
|
29
|
+
*/
|
|
30
|
+
import type { DoctorCheckPlugin, MultiBinaryDriftInspection } from '../types.js';
|
|
31
|
+
/**
|
|
32
|
+
* Local record shape — same as the canonical
|
|
33
|
+
* `MultiBinaryDriftInspection.binaries[number]`. Re-declared so the
|
|
34
|
+
* helper signature carries the concrete shape (the canonical
|
|
35
|
+
* `MultiBinaryDriftInspection` widens `version` + `installDate` to
|
|
36
|
+
* `string | null` so external consumers do not depend on the
|
|
37
|
+
* field being nullable).
|
|
38
|
+
*/
|
|
39
|
+
export type PeaksBinaryRecord = {
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly version: string | null;
|
|
42
|
+
readonly installDate: string | null;
|
|
43
|
+
readonly realpath: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Pure helper. Inspects `process.env.PATH` (or the injected
|
|
47
|
+
* `pathEnv`) for `peaks-loop` binaries. Each found binary is walked
|
|
48
|
+
* to its package.json via `realpathSync` so symlinks / npm shims /
|
|
49
|
+
* `peaks.cmd` / `peaks.ps1` all collapse to the same dedupe key.
|
|
50
|
+
*
|
|
51
|
+
* Exported so tests can drive the filesystem walk without monkey-
|
|
52
|
+
* patching `process.env.PATH` or `realpathSync`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function inspectMultiBinaryDrift(opts?: {
|
|
55
|
+
pathEnv?: string;
|
|
56
|
+
envReader?: (key: string) => string | undefined;
|
|
57
|
+
binaryExists?: (candidate: string) => boolean;
|
|
58
|
+
binaryRealpath?: (candidate: string) => string;
|
|
59
|
+
packageJsonReader?: (path: string) => Buffer | string | null;
|
|
60
|
+
}): MultiBinaryDriftInspection;
|
|
61
|
+
export declare const check: DoctorCheckPlugin;
|