peaks-loop 4.0.46 → 4.0.48
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 +54 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/agents/karpathy-reviewer.md +11 -10
- package/dist/cli/cli-helpers.d.ts +34 -0
- package/dist/cli/cli-helpers.js +57 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
- package/dist/cli/commands/code-runtime-commands.js +139 -16
- package/dist/cli/commands/compact-command.js +241 -1
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/container-commands.js +3 -3
- package/dist/cli/commands/core/skill-command.js +45 -10
- package/dist/cli/commands/cron-commands.js +2 -1
- package/dist/cli/commands/dashboard-long-run.js +6 -0
- package/dist/cli/commands/dispatch-commands.js +11 -1
- package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
- package/dist/cli/commands/e2e-verify.js +3 -3
- package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
- package/dist/cli/commands/hooks-commands.js +14 -5
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-commands.js +1 -0
- package/dist/cli/commands/loop-eval-commands.js +15 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +14 -1
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/reinject-command.d.ts +72 -0
- package/dist/cli/commands/reinject-command.js +174 -0
- package/dist/cli/commands/request-commands.js +14 -3
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/shadcn-commands.js +1 -0
- package/dist/cli/commands/slice-integrate-commands.js +5 -0
- package/dist/cli/commands/statusline-commands.js +44 -4
- package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
- package/dist/cli/commands/sub-agent/detached.js +47 -22
- package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
- package/dist/cli/commands/test-commands.js +2 -1
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/vm-commands.js +7 -7
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/commands/workspace/init-command.js +24 -2
- package/dist/cli/commands/worktree-lease-commands.js +4 -4
- package/dist/cli/index.js +10 -3
- package/dist/cli/program.js +5 -0
- package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
- package/dist/services/adapter/adapter-registry.js +1 -1
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +130 -65
- package/dist/services/artifacts/artifact-service.js +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +18 -8
- package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
- package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
- package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
- package/dist/services/audit-independent/perf-audit-service.js +27 -5
- package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
- package/dist/services/audit-independent/security-audit-service.js +28 -6
- package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
- package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
- package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +130 -1
- package/dist/services/code/auto-compact-lifecycle.js +180 -4
- package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
- package/dist/services/code/auto-compact-orchestrator.js +166 -34
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/code/orchestrator-can-do.d.ts +4 -2
- package/dist/services/code/orchestrator-can-do.js +37 -5
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
- package/dist/services/codegraph/codegraph-process-runner.js +3 -2
- package/dist/services/compact/request-transition-hook.js +5 -2
- package/dist/services/compact-history/compact-history-service.d.ts +75 -0
- package/dist/services/compact-history/compact-history-service.js +49 -0
- package/dist/services/config/config-restore.d.ts +12 -1
- package/dist/services/config/config-restore.js +35 -4
- package/dist/services/config/config-rollback.js +6 -1
- package/dist/services/config/config-safety.d.ts +52 -0
- package/dist/services/config/config-safety.js +75 -1
- package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
- package/dist/services/context/auto-compact-dispatcher.js +113 -40
- package/dist/services/context/auto-compact-reader.d.ts +68 -28
- package/dist/services/context/auto-compact-reader.js +155 -1
- package/dist/services/context/auto-compact-types.d.ts +89 -12
- package/dist/services/context/auto-compact-types.js +16 -32
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/context/harness-window-config.d.ts +412 -0
- package/dist/services/context/harness-window-config.js +607 -0
- package/dist/services/context/main-session-monitor.d.ts +27 -0
- package/dist/services/context/main-session-monitor.js +32 -1
- package/dist/services/context/post-compact-reinjection.d.ts +221 -0
- package/dist/services/context/post-compact-reinjection.js +491 -0
- package/dist/services/dispatch/merge-back-runner.js +5 -5
- package/dist/services/dispatch/service-shutdown.js +3 -3
- package/dist/services/doc/doc-generator.js +2 -1
- package/dist/services/env/shell-probe.js +1 -1
- package/dist/services/evidence/evidence-generator.js +86 -49
- package/dist/services/final-review/final-review-service.d.ts +9 -0
- package/dist/services/final-review/final-review-service.js +36 -12
- package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
- package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
- package/dist/services/hooks/auto-compact-hook-install.js +8 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
- package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
- package/dist/services/ide/ide-registry.d.ts +31 -0
- package/dist/services/ide/ide-registry.js +35 -0
- package/dist/services/ide/ide-types.d.ts +59 -0
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/lint/detect-eslint.js +2 -2
- package/dist/services/lint/eslint-runner.js +3 -1
- package/dist/services/loop/evaluator-dispatcher.js +2 -1
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
- package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
- package/dist/services/memory/project-memory-service/store/paths.js +15 -6
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/best-practice-auto-trigger.js +1 -0
- package/dist/services/prd/handoff-auto-regen.js +31 -27
- package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
- package/dist/services/prd/handoff-frontmatter.js +75 -0
- package/dist/services/prd/handoff-service.d.ts +41 -2
- package/dist/services/prd/handoff-service.js +81 -8
- package/dist/services/prd/handoff-types.d.ts +3 -2
- package/dist/services/prd/handoff-types.js +3 -2
- package/dist/services/qa/qa-business-review-state.js +9 -0
- package/dist/services/release/version-precheck-service.d.ts +2 -1
- package/dist/services/release/version-precheck-service.js +82 -12
- package/dist/services/runtime/vendor-adapter.d.ts +29 -4
- package/dist/services/runtime/vendors/claude-code.js +1 -1
- package/dist/services/runtime/vendors/codex.js +1 -1
- package/dist/services/runtime/vendors/copilot.js +1 -1
- package/dist/services/sc/sc-service.js +1 -1
- package/dist/services/scan/diff-scope-service.js +2 -2
- package/dist/services/scan/file-size-scan.js +2 -2
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/scan/orphan-service.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -2
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skillhub/tar-runtime.js +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +14 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +99 -3
- package/dist/services/skills/hooks-settings-service.d.ts +12 -0
- package/dist/services/skills/hooks-settings-service.js +91 -14
- package/dist/services/skills/session-start-hook-constants.d.ts +86 -0
- package/dist/services/skills/session-start-hook-constants.js +86 -0
- package/dist/services/skills/skill-presence-service.js +9 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +31 -12
- package/dist/services/slice/slice-decompose-runners.js +2 -1
- package/dist/services/slice/slice-review-state.js +8 -0
- package/dist/services/upgrade/upgrade-service.js +1 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
- package/dist/services/workflow/pipeline-verify-service.js +24 -23
- package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
- package/dist/services/workflow/workflow-skip-service.js +2 -1
- package/dist/services/workspace/claude-settings-template.d.ts +56 -8
- package/dist/services/workspace/claude-settings-template.js +98 -20
- package/dist/services/workspace/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-claude-settings-materializer.js +124 -9
- package/dist/services/workspace/workspace-service.js +8 -0
- package/dist/services/worktree/host-worktree-reconciler.js +1 -0
- package/dist/services/worktree/long-path-cleanup.js +3 -2
- package/dist/shared/process.js +1 -1
- package/package.json +6 -6
- package/scripts/install-skills.mjs +1 -0
- package/scripts/watch.mjs +3 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +8 -6
- package/skills/bee/peaks-qa/SKILL.md +7 -7
- package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
- package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
- package/skills/bee/peaks-rd/SKILL.md +10 -8
- package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
- package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +3 -3
- package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
- package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +13 -7
- package/skills/peaks-code/references/runbook.md +3 -2
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
- package/skills/peaks-content/SKILL.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-sop/SKILL.md +1 -1
- package/skills/peaks-status/SKILL.md +1 -1
- package/skills/peaks-test/SKILL.md +1 -1
|
@@ -4,9 +4,53 @@ import { fail, ok, getErrorMessage } from 'peaks-loop-shared/result';
|
|
|
4
4
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
5
5
|
import { writeCheckpoint } from '../../services/session/session-checkpoint-service.js';
|
|
6
6
|
import { getSessionIdCanonical } from '../../services/session/session-manager.js';
|
|
7
|
-
import { readCompactHistory, summarizeCompactHistory, } from '../../services/compact-history/compact-history-service.js';
|
|
7
|
+
import { computeWindowCalibration, readCompactHistory, summarizeCompactHistory, } from '../../services/compact-history/compact-history-service.js';
|
|
8
|
+
import { readHarnessWindowState, resolveHarnessWindowLocation } from '../../services/context/auto-compact-reader.js';
|
|
9
|
+
import { disableHarnessWindowSync, reenableHarnessWindowSync, resetHarnessWindow } from '../../services/context/harness-window-config.js';
|
|
8
10
|
import { buildRecommendEnvelopePure, dryRunCompact, suggestCompact } from '../../services/compact/suggest-service.js';
|
|
9
11
|
import { PHASES, SURVIVAL_TABLE, isPhase, lookupPhaseTransition } from '../../services/compact/decision-tables.js';
|
|
12
|
+
import { readSessionIdFromHookPayload, readTriggerFromHookPayload, settleCompactFromHarnessEvent } from '../../services/code/compact-event-settle.js';
|
|
13
|
+
/**
|
|
14
|
+
* Read the `PostCompact` hook payload from stdin.
|
|
15
|
+
*
|
|
16
|
+
* `PEAKS_HOOK_STDIN` is the test seam, verbatim the one `peaks gate enforce`
|
|
17
|
+
* uses (`gate-commands.ts`) — the CLI side owns `process.stdin`'s lifecycle, so
|
|
18
|
+
* the reader lives with the command rather than in the service.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` for every shape that is not a JSON object: empty stdin, a TTY
|
|
21
|
+
* (the `--json` diagnostic path run by a human), malformed JSON. A payload that
|
|
22
|
+
* cannot be read is not a failure — it is a payload with no `trigger`, which is
|
|
23
|
+
* an input this path is explicitly designed to accept.
|
|
24
|
+
*/
|
|
25
|
+
async function readHookPayload() {
|
|
26
|
+
const override = process.env.PEAKS_HOOK_STDIN;
|
|
27
|
+
let raw;
|
|
28
|
+
if (override !== undefined) {
|
|
29
|
+
raw = override;
|
|
30
|
+
}
|
|
31
|
+
else if (process.stdin.isTTY) {
|
|
32
|
+
raw = '';
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
raw = await new Promise((resolveStdin) => {
|
|
36
|
+
let data = '';
|
|
37
|
+
process.stdin.setEncoding('utf8');
|
|
38
|
+
process.stdin.on('data', (chunk) => {
|
|
39
|
+
data += chunk;
|
|
40
|
+
});
|
|
41
|
+
process.stdin.on('end', () => resolveStdin(data));
|
|
42
|
+
process.stdin.on('error', () => resolveStdin(data));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (raw.trim().length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
10
54
|
function splitList(value) {
|
|
11
55
|
if (!value)
|
|
12
56
|
return [];
|
|
@@ -288,9 +332,16 @@ export function registerCompactCommands(program, io) {
|
|
|
288
332
|
return;
|
|
289
333
|
}
|
|
290
334
|
const summary = summarizeCompactHistory(result.events);
|
|
335
|
+
// Slice 2026-09-13-auto-compact-trigger-ownership (T4): the
|
|
336
|
+
// intent-vs-observed record. `pairs[i].requestedTokens` is the token
|
|
337
|
+
// point peaks-loop asked for; `observedTokens` is what the next real
|
|
338
|
+
// session actually measured. Unmeasured pairs are reported as such —
|
|
339
|
+
// this instrument states what happened, it does not predict.
|
|
340
|
+
const windowCalibration = computeWindowCalibration(result.events);
|
|
291
341
|
printResult(io, ok('compact.history', {
|
|
292
342
|
sessionId: session.sid,
|
|
293
343
|
...summary,
|
|
344
|
+
windowCalibration,
|
|
294
345
|
events: result.events,
|
|
295
346
|
parseErrors: result.parseErrors,
|
|
296
347
|
historyPath: result.path,
|
|
@@ -301,4 +352,193 @@ export function registerCompactCommands(program, io) {
|
|
|
301
352
|
process.exitCode = 1;
|
|
302
353
|
}
|
|
303
354
|
}));
|
|
355
|
+
// 7. peaks compact harness-window [--reset | --disable | --reenable]
|
|
356
|
+
// (slice 2026-09-13-auto-compact-trigger-ownership, T1 + T2)
|
|
357
|
+
//
|
|
358
|
+
// The window peaks-loop divides by and the window the harness compacts
|
|
359
|
+
// against must be ONE number, or "95%" lands at two different token counts.
|
|
360
|
+
// This command is the visible half of that write: the default reports what
|
|
361
|
+
// is in force, `--reset` removes it, `--disable` stops managing it.
|
|
362
|
+
//
|
|
363
|
+
// There is deliberately NO `--sync` flag. Materializing the window requires
|
|
364
|
+
// the ratio's own denominator, which only a probe has (it is the only place
|
|
365
|
+
// the active model is known). A `--sync` that re-derived the window here
|
|
366
|
+
// would be the second, independent resolution this slice exists to delete —
|
|
367
|
+
// `peaks code context-now` and `peaks code auto-compact` already sync.
|
|
368
|
+
addJsonOption(compact
|
|
369
|
+
.command('harness-window')
|
|
370
|
+
.description('Report / roll back the auto-compact window peaks-loop writes into the ' +
|
|
371
|
+
'harness\'s own machine-local settings (the adapter-declared ' +
|
|
372
|
+
'autoCompactWindowEnvVar, e.g. CLAUDE_CODE_AUTO_COMPACT_WINDOW in ' +
|
|
373
|
+
'.claude/settings.local.json). peaks-loop computes its context ratio ' +
|
|
374
|
+
'against exactly this number, so the value it reports as "85%" and the ' +
|
|
375
|
+
'point the harness compacts at are the same. Context probes ' +
|
|
376
|
+
'(`peaks code context-now`, `peaks code auto-compact`) materialize it ' +
|
|
377
|
+
'automatically. Two different intentions, two verbs: --reset removes ' +
|
|
378
|
+
'the key AND opts the project out so later probes do not put it back ' +
|
|
379
|
+
'(when the file holds no peaks-loop row at all there is nothing to ' +
|
|
380
|
+
'roll back, so --reset writes nothing); --disable records the opt-out ' +
|
|
381
|
+
'only, leaving whatever value is already there untouched, so "stop ' +
|
|
382
|
+
'managing this key" works on a project peaks-loop has never written ' +
|
|
383
|
+
'to; --reenable undoes either opt-out.')
|
|
384
|
+
.option('--project <path>', 'project root (defaults to git root or cwd)')
|
|
385
|
+
.option('--reset', 'rollback: remove the window key and stop managing it')
|
|
386
|
+
.option('--disable', 'record the opt-out only (do not manage this key), without removing a value that is already there; expressible before the first write')
|
|
387
|
+
.option('--reenable', 'undo a --reset or --disable opt-out (peaks-loop manages it again)')
|
|
388
|
+
.action((options) => {
|
|
389
|
+
try {
|
|
390
|
+
const project = options.project !== undefined
|
|
391
|
+
? resolveCanonicalProjectRoot(options.project)
|
|
392
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
393
|
+
const location = resolveHarnessWindowLocation({ projectRoot: project, env: process.env });
|
|
394
|
+
if (location === null) {
|
|
395
|
+
printResult(io, ok('compact.harness-window', {
|
|
396
|
+
projectRoot: project,
|
|
397
|
+
managed: false,
|
|
398
|
+
message: 'The active IDE adapter declares no auto-compact window key, so peaks-loop cannot tie ' +
|
|
399
|
+
'the harness window to its own ratio. Nothing was written and there is nothing to roll back.',
|
|
400
|
+
}), options.json);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (options.reset === true) {
|
|
404
|
+
const result = resetHarnessWindow({ location, env: process.env });
|
|
405
|
+
printResult(io, ok('compact.harness-window', {
|
|
406
|
+
projectRoot: project,
|
|
407
|
+
action: result.action,
|
|
408
|
+
key: location.envVar,
|
|
409
|
+
settingsPath: result.settingsPath,
|
|
410
|
+
previousTokens: result.previousTokens,
|
|
411
|
+
}, [], [
|
|
412
|
+
result.action === 'removed'
|
|
413
|
+
? `Removed ${location.envVar} from ${result.settingsPath} and opted this project out, so later probes stop writing it. Undo with \`peaks compact harness-window --reenable\`.`
|
|
414
|
+
: `Nothing to remove — ${location.envVar} was already absent from ${result.settingsPath}, so nothing was written and no opt-out row was recorded: if a probe writes a window here later, run --reset again to remove it. To say "never manage this key here" WITHOUT waiting for that first write, run \`peaks compact harness-window --disable\`.`,
|
|
415
|
+
]), options.json);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (options.disable === true) {
|
|
419
|
+
const result = disableHarnessWindowSync({ location });
|
|
420
|
+
printResult(io, ok('compact.harness-window', {
|
|
421
|
+
projectRoot: project,
|
|
422
|
+
action: result.action,
|
|
423
|
+
key: location.envVar,
|
|
424
|
+
settingsPath: result.settingsPath,
|
|
425
|
+
}, [], [
|
|
426
|
+
result.action === 'disabled'
|
|
427
|
+
? `Recorded the opt-out in ${result.settingsPath}: peaks-loop will not write ${location.envVar} here, and left any value already in the file exactly as it was. Undo with \`peaks compact harness-window --reenable\`.`
|
|
428
|
+
: result.action === 'already-opted-out'
|
|
429
|
+
? `Already opted out — ${result.settingsPath} carries ${location.envVar}'s opt-out, so nothing was written. Undo with \`peaks compact harness-window --reenable\`.`
|
|
430
|
+
: result.action === 'refused-unsafe-project-root'
|
|
431
|
+
? `Nothing written: the resolved project root is the user's own home directory, so ${result.settingsPath} is their PERSONAL harness settings, not a project's. peaks-loop already refuses to write ${location.envVar} there, so an opt-out would change nothing except which refusal you see. Point --project at a project (a subdirectory of home is fine) and the opt-out lands there.`
|
|
432
|
+
: `Nothing written — ${result.settingsPath} is not a JSON object peaks-loop can safely edit, so the opt-out could not be recorded there.`,
|
|
433
|
+
]), options.json);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (options.reenable === true) {
|
|
437
|
+
const result = reenableHarnessWindowSync({ location });
|
|
438
|
+
printResult(io, ok('compact.harness-window', { projectRoot: project, action: result.action, key: location.envVar, settingsPath: result.settingsPath }, [], [
|
|
439
|
+
result.action === 'reenabled'
|
|
440
|
+
? 'Opt-out cleared; the next context probe will materialize the window again.'
|
|
441
|
+
: 'No opt-out was recorded for this project.',
|
|
442
|
+
]), options.json);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const state = readHarnessWindowState({ projectRoot: project, env: process.env });
|
|
446
|
+
printResult(io, ok('compact.harness-window', {
|
|
447
|
+
projectRoot: project,
|
|
448
|
+
managed: true,
|
|
449
|
+
key: location.envVar,
|
|
450
|
+
settingsPath: location.settingsPath,
|
|
451
|
+
tokens: state?.tokens ?? null,
|
|
452
|
+
raw: state?.raw ?? null,
|
|
453
|
+
source: state?.source ?? null,
|
|
454
|
+
optedOut: state?.optedOut ?? false,
|
|
455
|
+
// Provenance: whether this value is peaks-loop's own write or
|
|
456
|
+
// one the user set by hand. It decides whether the late 1M
|
|
457
|
+
// rescue may override it, so it is not just diagnostics.
|
|
458
|
+
peakWritten: state?.peakWritten ?? false,
|
|
459
|
+
}, [], [
|
|
460
|
+
state?.tokens === null || state?.tokens === undefined
|
|
461
|
+
? 'No window is set yet. The next `peaks code context-now` probe materializes the window it computes its ratio against.'
|
|
462
|
+
: `peaks-loop computes its context ratio against ${state.tokens} tokens; the harness fires near the end of that window. ${state.peakWritten
|
|
463
|
+
? 'peaks-loop wrote this value; it may raise it if a session outgrows it.'
|
|
464
|
+
: 'This value is not peaks-loop\'s own write, so peaks-loop will not raise it — it is treated as your setting.'}`,
|
|
465
|
+
'Rollback: `peaks compact harness-window --reset`.',
|
|
466
|
+
'Intent-vs-observed calibration: `peaks compact history --json` → windowCalibration.',
|
|
467
|
+
]), options.json);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
printResult(io, fail('compact.harness-window', 'COMPACT_HARNESS_WINDOW_FAILED', getErrorMessage(error), {}, ['Verify the project path is writable and a session is bound']), options.json);
|
|
471
|
+
process.exitCode = 1;
|
|
472
|
+
}
|
|
473
|
+
}));
|
|
474
|
+
// 8. peaks compact settle [--json]
|
|
475
|
+
// (slice 2026-09-13-compact-event-settle)
|
|
476
|
+
//
|
|
477
|
+
// The `PostCompact` hook's transport. Before this command, peaks-loop knew a
|
|
478
|
+
// compaction had landed only because a LATER probe measured a ratio that had
|
|
479
|
+
// fallen — an inference. This command lets the harness's own event settle the
|
|
480
|
+
// run, and records what the harness said the compaction WAS (`manual` /
|
|
481
|
+
// `auto`), which is the one question a ratio can never answer.
|
|
482
|
+
//
|
|
483
|
+
// STDOUT IS CONTEXT HERE. `PostCompact`'s output contract is truncated in the
|
|
484
|
+
// retrievable docs, so the safe assumption is the `SessionStart` one: stdout
|
|
485
|
+
// is added to the model's context. The hook path therefore prints NOTHING and
|
|
486
|
+
// exits 0 for every outcome — success, no open run, unresolvable session,
|
|
487
|
+
// unparseable stdin. An error sentence in the model's context is a false
|
|
488
|
+
// problem handed to it mid-slice. Same discipline as `session reinject`;
|
|
489
|
+
// `--json` is the diagnostic surface, and the hook never passes it.
|
|
490
|
+
addJsonOption(compact
|
|
491
|
+
.command('settle')
|
|
492
|
+
.description('PostCompact hook handler: settle the open compact run because the HARNESS reported a ' +
|
|
493
|
+
'compaction completed, and append an `observed` compact-history row carrying the ' +
|
|
494
|
+
'harness-reported trigger (manual | auto). Reads the hook JSON on stdin; prints nothing ' +
|
|
495
|
+
'and exits 0 when run by the hook. Pass --json for the diagnostic envelope.')
|
|
496
|
+
.option('--project <path>', 'project root (defaults to git root or cwd)')
|
|
497
|
+
.option('--session-id <sid>', 'override the active session id (defaults to the canonical binding)')).action(async (options) => {
|
|
498
|
+
// One parse, two fields: reading stdin twice would let the two readers
|
|
499
|
+
// disagree about the payload they were handed.
|
|
500
|
+
const payload = await readHookPayload();
|
|
501
|
+
const trigger = readTriggerFromHookPayload(payload);
|
|
502
|
+
const hookSessionId = readSessionIdFromHookPayload(payload);
|
|
503
|
+
// The hook path returns before any `process.exitCode` assignment below, so
|
|
504
|
+
// the harness never sees a non-zero exit from this command.
|
|
505
|
+
try {
|
|
506
|
+
const project = options.project !== undefined
|
|
507
|
+
? resolveCanonicalProjectRoot(options.project)
|
|
508
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
509
|
+
const session = resolveSessionId(project, options.sessionId);
|
|
510
|
+
if (session.error !== null) {
|
|
511
|
+
if (options.json !== true)
|
|
512
|
+
return;
|
|
513
|
+
printResult(io, fail('compact.settle', session.error.code, session.error.message, { projectRoot: project }, session.error.nextActions), true);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const result = settleCompactFromHarnessEvent({
|
|
517
|
+
projectRoot: project,
|
|
518
|
+
sessionId: session.sid,
|
|
519
|
+
trigger,
|
|
520
|
+
hookSessionId
|
|
521
|
+
});
|
|
522
|
+
if (options.json !== true)
|
|
523
|
+
return;
|
|
524
|
+
printResult(io, ok('compact.settle', { projectRoot: project, sessionId: session.sid, trigger: trigger ?? null, ...result }, [], [
|
|
525
|
+
result.settled
|
|
526
|
+
? result.lifecycleWritten
|
|
527
|
+
? result.historyWritten
|
|
528
|
+
? `Settled run ${result.runId}; appended a kind:'observed' row with pathway 'post-compact-hook'.`
|
|
529
|
+
: `Settled run ${result.runId}, but the kind:'observed' row could NOT be appended — check that .peaks/_runtime/<sid>/ is writable.`
|
|
530
|
+
: `The harness reported a compaction for run ${result.runId}, but the lifecycle record could NOT be written — that run is still open, so a later probe will settle it from its own measurement.${result.historyWritten
|
|
531
|
+
? " A kind:'observed' row was still appended for this event."
|
|
532
|
+
: " The kind:'observed' row could not be appended either."}`
|
|
533
|
+
: result.reason === 'different-session'
|
|
534
|
+
? `The hook payload names a different harness session, so this project's open compact run was left alone.`
|
|
535
|
+
: 'No compact run was open, so nothing was settled and no history row was appended.'
|
|
536
|
+
]), true);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (options.json !== true)
|
|
540
|
+
return;
|
|
541
|
+
printResult(io, fail('compact.settle', 'COMPACT_SETTLE_FAILED', getErrorMessage(error), {}, ['Verify the project path and session binding']), true);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
304
544
|
}
|
|
@@ -91,10 +91,11 @@ function registerConfigMigrationCommands(config, io) {
|
|
|
91
91
|
printResult(io, ok('config.rollback', { ...plan, applied: false }), options.json);
|
|
92
92
|
}
|
|
93
93
|
catch (error) {
|
|
94
|
+
// No `NO_BACKUP` branch: a missing `.bak` is no longer an error, it is
|
|
95
|
+
// an `available: false` SUCCESS envelope (see `config-rollback.ts`).
|
|
94
96
|
const message = getErrorMessage(error);
|
|
95
|
-
|
|
96
|
-
io.
|
|
97
|
-
printResult(io, fail('config.rollback', code, message, {}, ['Re-run peaks config migrate --apply to recreate the .bak']), options.json);
|
|
97
|
+
io.stderr(`CONFIG_ROLLBACK_FAILED: ${message}`);
|
|
98
|
+
printResult(io, fail('config.rollback', 'CONFIG_ROLLBACK_FAILED', message, {}, ['Re-run peaks config migrate --apply to recreate the .bak']), options.json);
|
|
98
99
|
process.exitCode = 1;
|
|
99
100
|
}
|
|
100
101
|
});
|
|
@@ -108,9 +109,14 @@ function registerConfigMigrationCommands(config, io) {
|
|
|
108
109
|
.option('--json', 'JSON envelope output')
|
|
109
110
|
.action((options) => {
|
|
110
111
|
try {
|
|
112
|
+
// `available` is on every envelope this command prints (see
|
|
113
|
+
// `config-restore.ts`): `false` ⇒ this machine has no `.bak`, exit 0,
|
|
114
|
+
// nothing to restore; `true` on a FAILURE envelope ⇒ the backup is
|
|
115
|
+
// there and the field/guard was the problem, exit 1. One key replaces
|
|
116
|
+
// the exit code as the "was there ever a backup?" signal.
|
|
111
117
|
if (options.list === true || !options.field) {
|
|
112
|
-
const fields = listAvailableFields();
|
|
113
|
-
printResult(io, ok('config.restore', { fields, applied: false }), options.json);
|
|
118
|
+
const { available, fields } = listAvailableFields();
|
|
119
|
+
printResult(io, ok('config.restore', { available, fields, applied: false }), options.json);
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
const apply = options.apply === true;
|
|
@@ -119,15 +125,15 @@ function registerConfigMigrationCommands(config, io) {
|
|
|
119
125
|
}
|
|
120
126
|
catch (error) {
|
|
121
127
|
const message = getErrorMessage(error);
|
|
128
|
+
// A missing `.bak` no longer reaches this block, so anything that does
|
|
129
|
+
// implies the backup exists — which is what `available: true` reports.
|
|
122
130
|
let code = 'CONFIG_RESTORE_FAILED';
|
|
123
|
-
if (message.startsWith('
|
|
124
|
-
code = 'NO_BACKUP';
|
|
125
|
-
else if (message.startsWith('RESTORE_GUARDED'))
|
|
131
|
+
if (message.startsWith('RESTORE_GUARDED'))
|
|
126
132
|
code = 'RESTORE_GUARDED';
|
|
127
133
|
else if (message.startsWith('FIELD_NOT_FOUND'))
|
|
128
134
|
code = 'FIELD_NOT_FOUND';
|
|
129
135
|
io.stderr(`${code}: ${message}`);
|
|
130
|
-
printResult(io, fail('config.restore', code, message, {}, ['Use --list to see available fields']), options.json);
|
|
136
|
+
printResult(io, fail('config.restore', code, message, { available: true, ...(options.field !== undefined ? { field: options.field } : {}) }, ['Use --list to see available fields']), options.json);
|
|
131
137
|
process.exitCode = 1;
|
|
132
138
|
}
|
|
133
139
|
});
|
|
@@ -37,7 +37,7 @@ function detectContainerRuntime(explicit) {
|
|
|
37
37
|
const tryOrder = explicit ? [explicit] : ['docker', 'podman'];
|
|
38
38
|
for (const r of tryOrder) {
|
|
39
39
|
try {
|
|
40
|
-
const version = execSync(`${r} --version`, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
40
|
+
const version = execSync(`${r} --version`, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', windowsHide: true }).trim();
|
|
41
41
|
return { ok: true, runtime: r, binary: `${r} (${version.split('\n')[0] ?? ''})` };
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
@@ -117,7 +117,7 @@ export function registerContainerCommand(program, io) {
|
|
|
117
117
|
// when the lease file is missing.
|
|
118
118
|
const cidFile = `${normalizePath(joinPathSession(projectRoot, sessionId))}/.${runtime.runtime}-cid-${leaseId}`;
|
|
119
119
|
try {
|
|
120
|
-
execSync(`${runtime.runtime} run --rm -d --cidfile "${cidFile}" --label "peaks.leaseId=${leaseId}" --label "peaks.rid=${options.rid}" -v "${mount}:/work" -w /work ${image} sleep infinity`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
|
|
120
|
+
execSync(`${runtime.runtime} run --rm -d --cidfile "${cidFile}" --label "peaks.leaseId=${leaseId}" --label "peaks.rid=${options.rid}" -v "${mount}:/work" -w /work ${image} sleep infinity`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
|
|
121
121
|
}
|
|
122
122
|
catch (err) {
|
|
123
123
|
printResult(io, fail('container.spawn', 'DOCKER_RUN_FAILED', getErrorMessage(err), { rid: options.rid, image, runtime: runtime.runtime, sessionId }, [
|
|
@@ -199,7 +199,7 @@ export function registerContainerCommand(program, io) {
|
|
|
199
199
|
try {
|
|
200
200
|
const releaseRuntime = detectContainerRuntime(undefined);
|
|
201
201
|
const runtimeCmd = releaseRuntime.ok ? releaseRuntime.runtime : 'docker';
|
|
202
|
-
execSync(`${runtimeCmd} rm --force "${lease.containerId}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
|
|
202
|
+
execSync(`${runtimeCmd} rm --force "${lease.containerId}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
|
|
203
203
|
}
|
|
204
204
|
catch {
|
|
205
205
|
dockerRmFailed = true;
|
|
@@ -307,17 +307,18 @@ export function registerSkillCommand(program, io) {
|
|
|
307
307
|
});
|
|
308
308
|
addJsonOption(skill
|
|
309
309
|
.command('presence:clear')
|
|
310
|
-
.description('
|
|
310
|
+
.description('Unlink the DEPRECATED pre-4.0.11 single-slot presence marker files (`.peaks/_runtime/active-skill.json`, `.peaks/.active-skill.json`). It does NOT terminalize a live presence lease — a workflow-bound lease terminalizes through `peaks workflow terminalize --workflow <id> --reason <reason>`, an ad-hoc lease only at session exit, and raw unlink is FORBIDDEN for both. The envelope reports what is still active.')
|
|
311
311
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action(async (options) => {
|
|
312
312
|
const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
313
|
+
// What this command actually is: a stale-marker cleanup for projects
|
|
314
|
+
// carrying a pre-4.0.11 single-slot file, and nothing more. The previous
|
|
315
|
+
// description claimed it "routes workflow leases through `workflow
|
|
316
|
+
// terminalize`" and that a "compat wrapper re-routes [it] to
|
|
317
|
+
// `terminalizePresenceLease` when a workflow binding is present" — there is
|
|
318
|
+
// no such wrapper, and no such routing happens here or in
|
|
319
|
+
// `clearSkillPresence`. A caller who read that line and ran this command
|
|
320
|
+
// expecting a live workflow lease to terminalize got a successful exit code
|
|
321
|
+
// and a still-running lease.
|
|
321
322
|
const removed = clearSkillPresence(options.project);
|
|
322
323
|
// Auto-update project context so future sessions have up-to-date history.
|
|
323
324
|
// Slice 2026-07-15-project-scan-bootstrap: generateProjectContext now also
|
|
@@ -329,7 +330,41 @@ export function registerSkillCommand(program, io) {
|
|
|
329
330
|
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
330
331
|
// non-fatal: context update failure should not block presence clear
|
|
331
332
|
}
|
|
332
|
-
|
|
333
|
+
// Report the state the project is actually in, not the state this command
|
|
334
|
+
// intended to reach. `clearSkillPresence` unlinks only the DEPRECATED
|
|
335
|
+
// single-slot marker files; the canonical sid-scoped lease is deliberately
|
|
336
|
+
// left alive for an AD-HOC lease (raw unlink is FORBIDDEN — only session
|
|
337
|
+
// exit may terminalize one; workflow leases route through `workflow
|
|
338
|
+
// terminalize`). So after a `presence:set` the very next `peaks skill
|
|
339
|
+
// presence` call still reads `active: true`. A hardcoded `active: false`
|
|
340
|
+
// therefore reported a state the command never reached, and the caller had
|
|
341
|
+
// no way to tell that from a real clear.
|
|
342
|
+
//
|
|
343
|
+
// Re-reading through `getSkillPresence` — the same projection the
|
|
344
|
+
// `skill presence` command serves — is the only derivation that cannot
|
|
345
|
+
// drift from what the next command prints: it is the same read path, on
|
|
346
|
+
// the same project root, evaluated after every write this command makes.
|
|
347
|
+
const active = getSkillPresence(projectRoot) !== null;
|
|
348
|
+
// `cleared` is the live question the caller is actually asking ("is it gone
|
|
349
|
+
// now?"), which `removed` could never answer: a legacy marker being unlinked
|
|
350
|
+
// says nothing about the lease the next command will read. When it is false
|
|
351
|
+
// the envelope says WHY, in the two routes that can terminalize a lease —
|
|
352
|
+
// without this the caller saw `active: true, removed: false` and had no way
|
|
353
|
+
// to tell "you ran the wrong command" from "this command has no opinion".
|
|
354
|
+
const cleared = !active;
|
|
355
|
+
printResult(io, ok('skill.presence:clear', {
|
|
356
|
+
active,
|
|
357
|
+
removed,
|
|
358
|
+
cleared,
|
|
359
|
+
...(cleared ? {} : { reason: 'live-lease-survives-presence-clear' }),
|
|
360
|
+
projectContextUpdated: true
|
|
361
|
+
}, [], cleared
|
|
362
|
+
? []
|
|
363
|
+
: [
|
|
364
|
+
'A workflow-bound lease terminalizes through `peaks workflow terminalize --workflow <id> --reason <reason>`.',
|
|
365
|
+
'An ad-hoc lease (`peaks skill presence:set`) is terminalizable only at session exit; `presence:clear` will not remove it, and raw unlink is FORBIDDEN.',
|
|
366
|
+
'Run `peaks skill presence --json` to read the lease that survived.'
|
|
367
|
+
]), options.json);
|
|
333
368
|
});
|
|
334
369
|
// Slice 4.0.8 (RD §4): manual lease GC primitive. LLM-coordinated;
|
|
335
370
|
// never a user-typed requirement. The user / LLM runner invokes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
4
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
4
5
|
import { read24hState } from '../../services/24h-mode/index.js';
|
|
5
6
|
import { getErrorMessage } from '../cli-helpers.js';
|
|
6
7
|
const SINCE_PATTERN = /^(\d+)([smhd])$/i;
|
|
@@ -24,6 +25,11 @@ function parseSince(raw) {
|
|
|
24
25
|
return { ok: true, ms: Math.min(WINDOW_CAP_MS, value * multiplier) };
|
|
25
26
|
}
|
|
26
27
|
function readSlices(projectRoot, sessionId) {
|
|
28
|
+
// Sid axis: `--session-id` reaches this join unmodified, and a traversal
|
|
29
|
+
// value resolved `metrics/slices.jsonl` outside every project root.
|
|
30
|
+
if (isUnsafePathInput(sessionId)) {
|
|
31
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
32
|
+
}
|
|
27
33
|
const path = join(projectRoot, '.peaks', '_runtime', sessionId, 'metrics', 'slices.jsonl');
|
|
28
34
|
if (!existsSync(path))
|
|
29
35
|
return 0;
|
|
@@ -127,7 +127,17 @@ export function registerDispatchCommand(parent, io) {
|
|
|
127
127
|
? { maxConcurrent }
|
|
128
128
|
: {}),
|
|
129
129
|
});
|
|
130
|
-
|
|
130
|
+
// The handler's `ok` is the launch outcome (a vendor CLI that is not
|
|
131
|
+
// installed is a failure, not a footnote) — the caller must not
|
|
132
|
+
// re-wrap it as `ok()` unconditionally, which is how `ok: true` with
|
|
133
|
+
// `pid: -1` reached the orchestrator.
|
|
134
|
+
if (result.ok) {
|
|
135
|
+
printResult(io, ok(result.command, result.data, result.warnings ?? [], result.nextActions ?? []), asJson);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
printResult(io, fail(result.command, 'DISPATCH_DETACHED_SPAWN_FAILED', `Could not launch the vendor CLI for a detached dispatch; nothing was started.`, result.data, result.nextActions ?? []), asJson);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
131
141
|
}
|
|
132
142
|
catch (error) {
|
|
133
143
|
printResult(io, fail('sub-agent.dispatch', 'DISPATCH_DETACHED_ERROR', getErrorMessage(error), {
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
+
import { isUnsafePathInput } from '../../../shared/path-safety.js';
|
|
10
11
|
export async function doctorInvokeFromCode(opts) {
|
|
12
|
+
// Sid axis: `opts.sid` is the sole path segment between the runtime root and
|
|
13
|
+
// the write below, and nothing upstream validated it.
|
|
14
|
+
if (isUnsafePathInput(opts.sid)) {
|
|
15
|
+
throw new Error(`Invalid session id: ${opts.sid} (must be a single path segment)`);
|
|
16
|
+
}
|
|
11
17
|
const dir = join('.peaks', '_runtime', opts.sid, 'doctor');
|
|
12
18
|
mkdirSync(dir, { recursive: true });
|
|
13
19
|
const proposalPath = join(dir, 'proposal.md');
|
|
@@ -17,7 +17,7 @@ async function probeChromiumBinary() {
|
|
|
17
17
|
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
18
18
|
for (const candidate of candidates) {
|
|
19
19
|
try {
|
|
20
|
-
const result = nodeSpawn(finder, [candidate], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
20
|
+
const result = nodeSpawn(finder, [candidate], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
21
21
|
const stdout = await new Promise((resolve, reject) => {
|
|
22
22
|
let buf = '';
|
|
23
23
|
result.stdout.on('data', (chunk) => { buf += chunk.toString('utf8'); });
|
|
@@ -33,7 +33,7 @@ async function probeChromiumBinary() {
|
|
|
33
33
|
const firstLine = stdout.split(/\r?\n/)[0]?.trim();
|
|
34
34
|
if (!firstLine)
|
|
35
35
|
continue;
|
|
36
|
-
const probe = nodeSpawn(firstLine, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
36
|
+
const probe = nodeSpawn(firstLine, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
37
37
|
const exitCode = await new Promise((resolve) => {
|
|
38
38
|
probe.on('close', (code) => resolve(code ?? 1));
|
|
39
39
|
probe.on('error', () => resolve(1));
|
|
@@ -114,7 +114,7 @@ async function runOneFixtureWithPlaywright(fixture, env) {
|
|
|
114
114
|
`--profile-directory=${env.profileName}`,
|
|
115
115
|
`--dump-dom`,
|
|
116
116
|
fixture.url,
|
|
117
|
-
], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
117
|
+
], { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
118
118
|
let html = '';
|
|
119
119
|
proc.stdout.on('data', (chunk) => { html += chunk.toString('utf8'); });
|
|
120
120
|
proc.on('error', () => settle({ pass: false, reason: 'chromium-spawn-failed' }));
|
|
@@ -7,7 +7,7 @@ import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
|
7
7
|
import { applyHookInstall, planHookInstall, readHookStatus, readInstalledEntriesFromSettings, removeHookInstall, listSuperpowersDenyEntries } from '../../services/skills/hooks-settings-service.js';
|
|
8
8
|
import { readJsonObjectFile } from '../../services/ide/shared/atomic-json.js';
|
|
9
9
|
import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
|
|
10
|
-
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
10
|
+
import { getAdapter, resolveIdeOptionHelp } from '../../services/ide/ide-registry.js';
|
|
11
11
|
/**
|
|
12
12
|
* This module's own directory — `<root>/src/cli/commands` in the source tree,
|
|
13
13
|
* `<root>/dist/cli/commands` in a build. Same reason as
|
|
@@ -153,7 +153,7 @@ export function registerHooksCommands(program, io) {
|
|
|
153
153
|
.description(`Install the peaks-managed gate-enforce hook entry into the adapter's settings.json. Slice #014: only the gate-enforce entry is installed; the legacy progress-start entry is no longer installed. Idempotent: re-runs are no-ops. Project scope by default.`)
|
|
154
154
|
.option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
|
|
155
155
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
156
|
-
.option('--ide <id>',
|
|
156
|
+
.option('--ide <id>', resolveIdeOptionHelp())
|
|
157
157
|
.option('--dry-run', 'show what would change without writing')
|
|
158
158
|
.option('--no-progress', 'skip the progress-start PreToolUse hook entry; install ONLY the gate-enforce entry')).action((options) => {
|
|
159
159
|
const scope = resolveScope(options);
|
|
@@ -282,7 +282,7 @@ export function registerHooksCommands(program, io) {
|
|
|
282
282
|
.description("Remove the peaks-managed gate-enforce hook entry from the target settings.json. Any legacy progress-start entry that a pre-#014 install left behind is also removed (sentinel-based scan). Third-party hooks are preserved.")
|
|
283
283
|
.option('--global', 'remove from the user-level ~/.claude/settings.json instead of the project')
|
|
284
284
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
285
|
-
.option('--ide <id>',
|
|
285
|
+
.option('--ide <id>', resolveIdeOptionHelp())).action((options) => {
|
|
286
286
|
const scope = resolveScope(options);
|
|
287
287
|
const projectRoot = resolveProjectRoot(scope, options.project);
|
|
288
288
|
const ide = resolveIdeForCommand(options, projectRoot);
|
|
@@ -310,7 +310,7 @@ export function registerHooksCommands(program, io) {
|
|
|
310
310
|
.description('Report which peaks-managed hook entries are installed.')
|
|
311
311
|
.option('--global', 'inspect the user-level ~/.claude/settings.json instead of the project')
|
|
312
312
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
313
|
-
.option('--ide <id>',
|
|
313
|
+
.option('--ide <id>', resolveIdeOptionHelp())).action((options) => {
|
|
314
314
|
const scope = resolveScope(options);
|
|
315
315
|
const projectRoot = resolveProjectRoot(scope, options.project);
|
|
316
316
|
const ide = resolveIdeForCommand(options, projectRoot);
|
|
@@ -339,6 +339,15 @@ export function registerHooksCommands(program, io) {
|
|
|
339
339
|
// through verbatim so the envelope reflects reality. The desired
|
|
340
340
|
// list is also included for parity with install / uninstall.
|
|
341
341
|
const onDiskDeny = readOnDiskDenyEntries(settings);
|
|
342
|
+
// `status` reports the state of the world; for an IDE that cannot host a
|
|
343
|
+
// hook the state of the world is "none, and none is possible". The exit
|
|
344
|
+
// code stays 0 (the query succeeded) and the payload carries the finding
|
|
345
|
+
// — `supportsHooks: false` on the data, plus a warning naming the IDE, so
|
|
346
|
+
// the negative fact is not confused with "supported IDE, nothing
|
|
347
|
+
// installed yet".
|
|
348
|
+
const warnings = status.supportsHooks
|
|
349
|
+
? []
|
|
350
|
+
: [`IDE '${ide}' cannot host peaks hooks (no HOOK_COMMAND_BY_IDE entry); nothing can be installed for it, which is why none is.`];
|
|
342
351
|
printResult(io, ok('hooks.status', {
|
|
343
352
|
...status,
|
|
344
353
|
ide,
|
|
@@ -348,7 +357,7 @@ export function registerHooksCommands(program, io) {
|
|
|
348
357
|
],
|
|
349
358
|
permissionsDenyEntries: listSuperpowersDenyEntries(),
|
|
350
359
|
permissionsDenyOnDisk: onDiskDeny
|
|
351
|
-
}), options.json);
|
|
360
|
+
}, warnings), options.json);
|
|
352
361
|
}
|
|
353
362
|
catch (error) {
|
|
354
363
|
const message = getErrorMessage(error);
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
6
6
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
7
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
7
8
|
import { JobStateStore } from '../../services/job/job-state-store.js';
|
|
8
9
|
import { JobOrchestrator } from '../../services/job/job-orchestrator.js';
|
|
9
10
|
import { writeJobProgress, readJobProgress, tryReadJobProgress } from '../../services/job/job-progress-store.js';
|
|
@@ -70,6 +71,13 @@ function resolveJobStateRoot(opts, jobId) {
|
|
|
70
71
|
if (!sessionId) {
|
|
71
72
|
throw new Error('NO_ACTIVE_SESSION: peaks job requires --session-id or an active peaks-code session via peaks workspace init');
|
|
72
73
|
}
|
|
74
|
+
// Sid axis — the sibling of the jobId guard in `JobStateStore.jobDir`. A
|
|
75
|
+
// caller-supplied `--session-id` reaches this join unmodified, so a
|
|
76
|
+
// traversal value lands `job/<id>/state.json` outside every project root
|
|
77
|
+
// while the envelope still reads `ok: true`.
|
|
78
|
+
if (isUnsafePathInput(sessionId)) {
|
|
79
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
80
|
+
}
|
|
73
81
|
const rootDir = join(project, '.peaks', '_runtime', sessionId, 'job');
|
|
74
82
|
if (jobId && !existsSync(join(rootDir, jobId, 'state.json'))) {
|
|
75
83
|
const other = findSessionHoldingJob(project, jobId);
|
|
@@ -77,6 +77,7 @@ export function registerLoopCommands(program, io) {
|
|
|
77
77
|
const stdout = execFileSync('node', ['bin/peaks.js', ...args], {
|
|
78
78
|
cwd: options.project,
|
|
79
79
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
80
|
+
windowsHide: true,
|
|
80
81
|
}).toString('utf-8');
|
|
81
82
|
printResult(io, ok('loop.distill', {
|
|
82
83
|
project: options.project,
|