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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rid `2026-09-13-compact-event-settle` — what runs when the HARNESS says a
|
|
3
|
+
* compaction completed.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM THIS EXISTS TO DELETE. Before this slice peaks-loop learned that
|
|
6
|
+
* a compaction had happened by INFERENCE: `src/services/code/auto-compact-orchestrator.ts`
|
|
7
|
+
* probes the context ratio on every run, and a ratio that has fallen back below
|
|
8
|
+
* the auto-fire threshold is taken as proof that a previously-dispatched
|
|
9
|
+
* compact landed. That is a guess with three failure modes, all silent — the
|
|
10
|
+
* probe may not measure at all (`conservative-fallback`), the next probe may
|
|
11
|
+
* not come for minutes (during which the ratio has already climbed back up),
|
|
12
|
+
* and a compaction that left the ratio high is invisible.
|
|
13
|
+
*
|
|
14
|
+
* `PostCompact` is the harness STATING it. This module turns that statement
|
|
15
|
+
* into the same lifecycle settlement the probe path produces, plus an
|
|
16
|
+
* `observed` row carrying the one fact the probe path can never produce: what
|
|
17
|
+
* the harness said the compaction WAS — `manual` or `auto`.
|
|
18
|
+
*
|
|
19
|
+
* FOUR THINGS THIS MODULE REFUSES TO DO, EACH FOR A REASON:
|
|
20
|
+
*
|
|
21
|
+
* 1. It does not invent a `trigger`. `PostCompact`'s payload schema is
|
|
22
|
+
* truncated in the retrievable docs, so a payload without the field is an
|
|
23
|
+
* expected input. Absent stays absent. Defaulting it to `'auto'` would
|
|
24
|
+
* answer "has this machine ever auto-compacted?" with a fabricated yes,
|
|
25
|
+
* which is worse than the current "no answer".
|
|
26
|
+
* 2. It does not settle when no run is open. A `PostCompact` on a session
|
|
27
|
+
* peaks-loop never dispatched for has nothing to attribute, and
|
|
28
|
+
* `CompactHistoryEvent.beforeRatio` is a required number that could only
|
|
29
|
+
* be filled with a ratio measured AFTER the compaction — a fabricated
|
|
30
|
+
* "before". See the RD tech-doc §D8: covering that case needs a
|
|
31
|
+
* `PreCompact` snapshot and is a separate slice, not a widened contract.
|
|
32
|
+
* 3. It does not let a stale measurement become an `afterRatio`. The probe's
|
|
33
|
+
* statusline source may still hold the pre-compact value at the instant
|
|
34
|
+
* this runs; `settleOpenLifecycleRunOnCompactEvent` drops anything that is
|
|
35
|
+
* not below the dispatch ratio.
|
|
36
|
+
* 4. It does not settle a run off an event that names a DIFFERENT harness
|
|
37
|
+
* session, and it does not label one `'main'` without checking. A payload
|
|
38
|
+
* whose `session_id` is another session's would otherwise close this
|
|
39
|
+
* project's open run and file the row as the main session's. The check is
|
|
40
|
+
* best-effort in one direction only: an absent `session_id`, or a project
|
|
41
|
+
* whose own harness session id cannot be resolved, is accepted rather than
|
|
42
|
+
* refused — a guard that cannot see the name it is checking against must
|
|
43
|
+
* not turn a missing field into a hook that never settles anything.
|
|
44
|
+
*
|
|
45
|
+
* NOTHING HERE THROWS. The caller is a hook on the harness's compaction path,
|
|
46
|
+
* where a failure must cost a telemetry row and nothing else.
|
|
47
|
+
*/
|
|
48
|
+
import { resolveAutoCompactProfile } from '../mode/mode-status-service.js';
|
|
49
|
+
import { readContextPercent } from '../context/auto-compact-reader.js';
|
|
50
|
+
import { resolveOuterSessionId } from '../session/binding-status-service.js';
|
|
51
|
+
import { appendCompactHistoryEvent } from './auto-compact-orchestrator.js';
|
|
52
|
+
import { settleOpenLifecycleRunOnCompactEvent } from './auto-compact-lifecycle.js';
|
|
53
|
+
/**
|
|
54
|
+
* Which layer settled the run. `post-compact-hook` = the harness's event;
|
|
55
|
+
* `post-compact-probe` (written by the orchestrator) = a later probe's
|
|
56
|
+
* measurement. The two are deliberately distinct strings so a reader of
|
|
57
|
+
* `compact-history.jsonl` can tell a NOTIFICATION from an INFERENCE — which is
|
|
58
|
+
* the entire instrument this slice exists to build.
|
|
59
|
+
*/
|
|
60
|
+
export const COMPACT_EVENT_PATHWAY = 'post-compact-hook';
|
|
61
|
+
/**
|
|
62
|
+
* Read `trigger` out of an already-parsed `PostCompact` payload.
|
|
63
|
+
*
|
|
64
|
+
* Anything that is not exactly `'manual'` or `'auto'` yields `undefined` — a
|
|
65
|
+
* missing field, a `null`, a number, another string, a payload that is not an
|
|
66
|
+
* object at all. This function never throws and never guesses (see this file's
|
|
67
|
+
* header, refusal 1).
|
|
68
|
+
*/
|
|
69
|
+
export function readTriggerFromHookPayload(payload) {
|
|
70
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
|
|
71
|
+
return undefined;
|
|
72
|
+
const raw = payload.trigger;
|
|
73
|
+
return raw === 'manual' || raw === 'auto' ? raw : undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read the harness session id out of an already-parsed `PostCompact` payload.
|
|
77
|
+
*
|
|
78
|
+
* Same contract as `readTriggerFromHookPayload`: a missing field, an empty
|
|
79
|
+
* string, a number, a non-object payload — every one of those is `undefined`,
|
|
80
|
+
* and "absent" is an expected input rather than an error (see this file's
|
|
81
|
+
* header, refusal 4, for what the caller does with it and what it does with
|
|
82
|
+
* `undefined`).
|
|
83
|
+
*/
|
|
84
|
+
export function readSessionIdFromHookPayload(payload) {
|
|
85
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
|
|
86
|
+
return undefined;
|
|
87
|
+
const raw = payload.session_id;
|
|
88
|
+
return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The post-compact ruler: the same probe the orchestrator uses, so this row
|
|
92
|
+
* divides by the same denominator the dispatch did.
|
|
93
|
+
*
|
|
94
|
+
* `ratio` is `null` when nothing could be measured — a `conservative-fallback`
|
|
95
|
+
* probe reports `0`, but that `0` means "unknown", and recording it here would
|
|
96
|
+
* publish a real, infinitely-deep drop. The rest of the reading (adapter, the
|
|
97
|
+
* window the ratio divides by) is still carried, because knowing WHICH adapter
|
|
98
|
+
* could not measure is diagnostic.
|
|
99
|
+
*/
|
|
100
|
+
export function measurePostCompact(input) {
|
|
101
|
+
const probe = readContextPercent({
|
|
102
|
+
projectRoot: input.projectRoot,
|
|
103
|
+
sessionId: input.sessionId,
|
|
104
|
+
outerSessionId: resolveOuterSessionId(input.projectRoot, input.sessionId, input.env),
|
|
105
|
+
env: input.env
|
|
106
|
+
});
|
|
107
|
+
const unmeasurable = probe.source === 'conservative-fallback';
|
|
108
|
+
return {
|
|
109
|
+
ratio: unmeasurable ? null : probe.ratio,
|
|
110
|
+
ide: probe.ide,
|
|
111
|
+
windowTokens: unmeasurable || typeof probe.capacityTokens !== 'number' ? null : probe.capacityTokens,
|
|
112
|
+
windowSource: unmeasurable ? null : (probe.capacitySource ?? null)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Settle whatever compact run is open in `sessionId`, because the harness just
|
|
117
|
+
* reported one.
|
|
118
|
+
*
|
|
119
|
+
* `measure` is the ruler, injected rather than called directly for one concrete
|
|
120
|
+
* reason: the production ruler prefers `~/.claude/statusline-state.json`, a real
|
|
121
|
+
* file whose contents differ on every machine that runs the test suite. A test
|
|
122
|
+
* asserting this row's `afterRatio` against the developer's own statusline would
|
|
123
|
+
* pass or fail by accident. It is called at most once — the IDE tag and the
|
|
124
|
+
* window come from the same reading, so they cannot disagree with each other.
|
|
125
|
+
*
|
|
126
|
+
* Returns `{ settled: false }` when there is no open run — the honest answer,
|
|
127
|
+
* not an error — and when the payload names another harness session. The two
|
|
128
|
+
* say different things in `reason`, because they are different facts.
|
|
129
|
+
*/
|
|
130
|
+
export function settleCompactFromHarnessEvent(input) {
|
|
131
|
+
const env = input.env ?? process.env;
|
|
132
|
+
const measure = input.measure ??
|
|
133
|
+
(() => measurePostCompact({ projectRoot: input.projectRoot, sessionId: input.sessionId, env }));
|
|
134
|
+
// Attribution first, and before the probe: an event that is not about this
|
|
135
|
+
// session must not spend a measurement, and must not touch this run at all.
|
|
136
|
+
// The comparison is harness-id to harness-id — the payload's `session_id` and
|
|
137
|
+
// this project's own outer session id as `resolveOuterSessionId` resolves it
|
|
138
|
+
// (env signal, then the binding's recorded id). It is never compared against
|
|
139
|
+
// the peaks-loop session id, which is a different namespace and would differ
|
|
140
|
+
// on every legitimate event.
|
|
141
|
+
if (input.hookSessionId !== undefined) {
|
|
142
|
+
const ownOuterSessionId = resolveOuterSessionId(input.projectRoot, input.sessionId, env);
|
|
143
|
+
if (ownOuterSessionId !== undefined && ownOuterSessionId !== input.hookSessionId) {
|
|
144
|
+
return { settled: false, reason: 'different-session' };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let measurement = null;
|
|
148
|
+
try {
|
|
149
|
+
measurement = measure();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// A ruler that broke is not a reason to refuse the harness's statement.
|
|
153
|
+
measurement = null;
|
|
154
|
+
}
|
|
155
|
+
const settled = settleOpenLifecycleRunOnCompactEvent({
|
|
156
|
+
projectRoot: input.projectRoot,
|
|
157
|
+
sessionId: input.sessionId,
|
|
158
|
+
measuredRatio: measurement?.ratio ?? null,
|
|
159
|
+
failLifecycleWrite: input.failLifecycleWrite
|
|
160
|
+
});
|
|
161
|
+
if (settled === null) {
|
|
162
|
+
return { settled: false, reason: 'nothing-to-settle' };
|
|
163
|
+
}
|
|
164
|
+
const event = {
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
kind: 'observed',
|
|
167
|
+
ts: new Date().toISOString(),
|
|
168
|
+
// The guard above refused every payload that named another session, so
|
|
169
|
+
// reaching here means the event named this one or named none. The row is
|
|
170
|
+
// therefore attributable to the main session, which is the only session
|
|
171
|
+
// this command settles.
|
|
172
|
+
target: 'main',
|
|
173
|
+
mode: resolveAutoCompactProfile(input.projectRoot),
|
|
174
|
+
ide: measurement?.ide ?? 'claude-code',
|
|
175
|
+
pathway: COMPACT_EVENT_PATHWAY,
|
|
176
|
+
beforeRatio: settled.triggerRatio,
|
|
177
|
+
...(settled.afterRatio !== null ? { afterRatio: settled.afterRatio } : {}),
|
|
178
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {}),
|
|
179
|
+
redLine: false,
|
|
180
|
+
ok: true,
|
|
181
|
+
checkpointPath: '',
|
|
182
|
+
dispatchMessage: `PostCompact fired: the harness reported a ${input.trigger ?? 'unreported-trigger'} compaction, ` +
|
|
183
|
+
`settling the run dispatched at ${(settled.triggerRatio * 100).toFixed(1)}%` +
|
|
184
|
+
(settled.afterRatio !== null
|
|
185
|
+
? ` (measured now at ${(settled.afterRatio * 100).toFixed(1)}%)`
|
|
186
|
+
: ' (no post-compact measurement available)'),
|
|
187
|
+
...(settled.afterRatio !== null
|
|
188
|
+
? { windowTokens: measurement?.windowTokens ?? null, windowSource: measurement?.windowSource ?? null }
|
|
189
|
+
: {})
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
appendCompactHistoryEvent({ projectRoot: input.projectRoot, sessionId: input.sessionId, event });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// The lifecycle run is already settled; losing the history row is the
|
|
196
|
+
// smaller loss, and a throwing hook is the larger one. Reported rather
|
|
197
|
+
// than silenced: "the run settled but the row is missing" and "the row is
|
|
198
|
+
// on disk" are different facts, and a caller that cannot tell them apart
|
|
199
|
+
// cannot diagnose a hook that has stopped recording anything.
|
|
200
|
+
return {
|
|
201
|
+
settled: true,
|
|
202
|
+
runId: settled.runId,
|
|
203
|
+
beforeRatio: settled.triggerRatio,
|
|
204
|
+
afterRatio: settled.afterRatio,
|
|
205
|
+
historyWritten: false,
|
|
206
|
+
lifecycleWritten: settled.lifecycleWritten,
|
|
207
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {})
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
settled: true,
|
|
212
|
+
runId: settled.runId,
|
|
213
|
+
beforeRatio: settled.triggerRatio,
|
|
214
|
+
afterRatio: settled.afterRatio,
|
|
215
|
+
historyWritten: true,
|
|
216
|
+
lifecycleWritten: settled.lifecycleWritten,
|
|
217
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {})
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* decide, ?, 选择, 决定). If yes → soft warning, NOT a blocker
|
|
19
19
|
* (the LLM should AskUserQuestion, which is cheap).
|
|
20
20
|
* Q4 — Is context usage sustainable? (probe `peaks code context-now
|
|
21
|
-
* --json`). ratio ≥ 0.95 →
|
|
22
|
-
*
|
|
21
|
+
* --json`). ratio ≥ 0.95 → WARNING (red-line, ask-and-wait);
|
|
22
|
+
* ≥ 0.85 → WARNING (pre-compact band). Never a blocker — see the
|
|
23
|
+
* comment on the Q4 branch in `buildOrchestratorCanDoResult`, and
|
|
24
|
+
* `evaluateCompactTrigger`, which is the face this one must agree with.
|
|
23
25
|
*
|
|
24
26
|
* Decision rule:
|
|
25
27
|
* canDoInSession === (blockers.length === 0)
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* decide, ?, 选择, 决定). If yes → soft warning, NOT a blocker
|
|
19
19
|
* (the LLM should AskUserQuestion, which is cheap).
|
|
20
20
|
* Q4 — Is context usage sustainable? (probe `peaks code context-now
|
|
21
|
-
* --json`). ratio ≥ 0.95 →
|
|
22
|
-
*
|
|
21
|
+
* --json`). ratio ≥ 0.95 → WARNING (red-line, ask-and-wait);
|
|
22
|
+
* ≥ 0.85 → WARNING (pre-compact band). Never a blocker — see the
|
|
23
|
+
* comment on the Q4 branch in `buildOrchestratorCanDoResult`, and
|
|
24
|
+
* `evaluateCompactTrigger`, which is the face this one must agree with.
|
|
23
25
|
*
|
|
24
26
|
* Decision rule:
|
|
25
27
|
* canDoInSession === (blockers.length === 0)
|
|
@@ -153,6 +155,7 @@ export async function probeSubAgentAvailable(projectRoot, peaksBin = DEFAULT_PEA
|
|
|
153
155
|
await execFileAsync(command, [...args, 'sub-agent', 'dispatch', '--role', 'rd', '--help'], {
|
|
154
156
|
cwd: projectRoot,
|
|
155
157
|
timeout: 5000,
|
|
158
|
+
windowsHide: true,
|
|
156
159
|
});
|
|
157
160
|
return true;
|
|
158
161
|
}
|
|
@@ -171,6 +174,7 @@ export async function probeContextRatio(projectRoot, peaksBin = DEFAULT_PEAKS_BI
|
|
|
171
174
|
const { stdout } = await execFileAsync(command, [...args, 'code', 'context-now', '--project', projectRoot, '--json'], {
|
|
172
175
|
cwd: projectRoot,
|
|
173
176
|
timeout: 10000,
|
|
177
|
+
windowsHide: true,
|
|
174
178
|
});
|
|
175
179
|
const parsed = JSON.parse(stdout);
|
|
176
180
|
const ratio = typeof parsed.data?.ratio === 'number' ? parsed.data.ratio : 0;
|
|
@@ -205,13 +209,41 @@ export function buildOrchestratorCanDoResult(input, signals) {
|
|
|
205
209
|
blockers.push('sub-agent dispatch unavailable (peaks sub-agent dispatch --help failed)');
|
|
206
210
|
suggestions.push('verify peaks CLI is on PATH; check `peaks --version`');
|
|
207
211
|
}
|
|
208
|
-
// Q4 — context ratio
|
|
212
|
+
// Q4 — context ratio. ≥0.95 → red-line; ≥0.85 → pre-compact. A WARNING, not
|
|
213
|
+
// a blocker, and the difference is deliberate (E3, rid 2026-09-13-defects-e).
|
|
214
|
+
//
|
|
215
|
+
// Why this is not a blocker any more, and why it is not an oversight:
|
|
216
|
+
//
|
|
217
|
+
// The peak this answers is "can this slice run in the current session".
|
|
218
|
+
// Context ratio cannot answer "no" to it. The reason is the one the
|
|
219
|
+
// T3 slice (2026-09-13-auto-compact-trigger-ownership) landed on the
|
|
220
|
+
// OTHER face of this same threshold: peaks-loop has no executor for a
|
|
221
|
+
// running session, so it cannot compact its way out of a high ratio —
|
|
222
|
+
// `evaluateCompactTrigger` therefore says of the red line "peaks-loop has
|
|
223
|
+
// asked the harness to compact and is WAITING for it — sub-agent dispatch
|
|
224
|
+
// is NOT blocked; carry on and re-probe". A probe that returned
|
|
225
|
+
// `canDoInSession: false` here would contradict that sentence for the same
|
|
226
|
+
// 0.95 on the same number, and its only programmatic consumer
|
|
227
|
+
// (`code-orchestrator-can-do.ts`) turns the false into exit code 1 — i.e.
|
|
228
|
+
// it would re-open, at the CLI layer, exactly the deadlock T3 deleted.
|
|
229
|
+
//
|
|
230
|
+
// Nor does the pre-compact zone (0.85–0.95) earn a blocker: the trigger's
|
|
231
|
+
// message there is "peaks-loop already fired the auto-compact pathway; the
|
|
232
|
+
// LLM does not need to act", which is the opposite of "you may not proceed".
|
|
233
|
+
//
|
|
234
|
+
// What a blocker would still need to be true: THE ORCHESTRATOR itself
|
|
235
|
+
// cannot continue. It can — the slice's edits are delegated to a sub-agent
|
|
236
|
+
// (Q1), and the orchestrator's own context is only spent co-ordinating.
|
|
237
|
+
//
|
|
238
|
+
// So the ratio is reported, warned about, and given a next action; the
|
|
239
|
+
// verdict is left to the blockers that really are un-survivable (a
|
|
240
|
+
// hard-blocked path family, an unreachable sub-agent dispatcher).
|
|
209
241
|
if (signals.q4ContextRatio >= ORCHESTRATOR_REDLINE_RATIO) {
|
|
210
|
-
|
|
242
|
+
warnings.push(`context red-line (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_REDLINE_RATIO}): peaks-loop has asked the harness to compact and is waiting for it; dispatch is NOT blocked — carry on and re-probe with \`peaks code context-now\``);
|
|
211
243
|
suggestions.push('peaks code auto-compact');
|
|
212
244
|
}
|
|
213
245
|
else if (signals.q4ContextRatio >= ORCHESTRATOR_PRECOMPACT_RATIO) {
|
|
214
|
-
|
|
246
|
+
warnings.push(`context near limit (ratio=${signals.q4ContextRatio.toFixed(2)} ≥ ${ORCHESTRATOR_PRECOMPACT_RATIO}): in the pre-compact band; peaks-loop fires the auto-compact pathway itself and dispatch is NOT blocked`);
|
|
215
247
|
suggestions.push('peaks code auto-compact');
|
|
216
248
|
}
|
|
217
249
|
// Q3 — user-decision keywords → soft warning, NOT a blocker. The
|
|
@@ -176,7 +176,8 @@ export const CODEGRAPH_CONFIG_FILENAME = 'config.json';
|
|
|
176
176
|
export function readTrackedFiles(projectRoot) {
|
|
177
177
|
const stdout = execFileSync('git', ['-C', projectRoot, 'ls-files'], {
|
|
178
178
|
encoding: 'utf8',
|
|
179
|
-
maxBuffer: 64 * 1024 * 1024
|
|
179
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
180
|
+
windowsHide: true
|
|
180
181
|
});
|
|
181
182
|
return stdout
|
|
182
183
|
.split('\n')
|
|
@@ -27,7 +27,7 @@ function terminateCodegraphProcess(childProcess) {
|
|
|
27
27
|
}
|
|
28
28
|
if (process.platform === 'win32') {
|
|
29
29
|
if (process.env.SystemRoot) {
|
|
30
|
-
spawn(join(process.env.SystemRoot, 'System32', 'taskkill.exe'), ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
|
|
30
|
+
spawn(join(process.env.SystemRoot, 'System32', 'taskkill.exe'), ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore', windowsHide: true });
|
|
31
31
|
}
|
|
32
32
|
else {
|
|
33
33
|
childProcess.kill();
|
|
@@ -47,7 +47,8 @@ export function defaultCodegraphProcessRunner(invocation) {
|
|
|
47
47
|
cwd: invocation.cwd,
|
|
48
48
|
detached: process.platform !== 'win32',
|
|
49
49
|
env: createCodegraphEnvironment(),
|
|
50
|
-
shell: false
|
|
50
|
+
shell: false,
|
|
51
|
+
windowsHide: true
|
|
51
52
|
});
|
|
52
53
|
const timeout = setTimeout(() => {
|
|
53
54
|
terminateCodegraphProcess(childProcess);
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The hook is INVOKED from `src/cli/commands/request-commands.ts`
|
|
17
17
|
* (RD→QA boundary) and from any future QA→final-review boundary. The
|
|
18
|
-
* 0.95 red line is
|
|
19
|
-
* orchestrator
|
|
18
|
+
* 0.95 red line is unchanged in THRESHOLD but not in behaviour: the
|
|
19
|
+
* auto-compact orchestrator asks the harness to compact and reports that
|
|
20
|
+
* it is waiting. It does not refuse sub-agent dispatch — peaks-loop has no
|
|
21
|
+
* executor for a running session, so a refusal gated nothing and deadlocked
|
|
22
|
+
* the runner (slice 2026-09-13-auto-compact-trigger-ownership).
|
|
20
23
|
*/
|
|
21
24
|
import { existsSync, readFileSync } from 'node:fs';
|
|
22
25
|
import { join } from 'node:path';
|
|
@@ -10,7 +10,82 @@ export interface CompactHistoryEvent {
|
|
|
10
10
|
readonly ok: boolean;
|
|
11
11
|
readonly checkpointPath: string;
|
|
12
12
|
readonly dispatchMessage: string;
|
|
13
|
+
/**
|
|
14
|
+
* Slice 2026-09-13-auto-compact-trigger-ownership (T4): the window this
|
|
15
|
+
* dispatch divided by. `beforeRatio * windowTokens` is the token point
|
|
16
|
+
* peaks-loop asked the harness to compact at.
|
|
17
|
+
*/
|
|
18
|
+
readonly windowTokens?: number | null;
|
|
19
|
+
/** Which layer produced `windowTokens` (env-override | harness-env | config | model-heuristic | default). */
|
|
20
|
+
readonly windowSource?: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* `dispatch` — peaks-loop asked for a compact (every row written before
|
|
23
|
+
* this slice is implicitly one of these). `observed` — a later probe
|
|
24
|
+
* MEASURED the ratio after a dispatched compact, proving one landed.
|
|
25
|
+
*/
|
|
26
|
+
readonly kind?: 'dispatch' | 'observed';
|
|
27
|
+
/** `observed` rows only: the measured post-compact ratio. */
|
|
28
|
+
readonly afterRatio?: number;
|
|
29
|
+
/**
|
|
30
|
+
* rid `2026-09-13-compact-event-settle`: what the HARNESS said caused the
|
|
31
|
+
* compaction — `manual` (a user ran `/compact`) or `auto` (the harness's own
|
|
32
|
+
* window fired). Written only by the `PostCompact` hook path, which is the
|
|
33
|
+
* only path a harness reports it on.
|
|
34
|
+
*
|
|
35
|
+
* OPTIONAL, AND ABSENT MEANS "NOT REPORTED" — never "manual", never "auto".
|
|
36
|
+
* `PostCompact`'s payload schema is truncated in the retrievable docs, so a
|
|
37
|
+
* payload without the field is an expected input, not an error; the whole
|
|
38
|
+
* reason this column exists is that "has this machine ever auto-compacted?"
|
|
39
|
+
* has no answer today, and a defaulted value would answer it falsely.
|
|
40
|
+
* Rows that predate the slice omit it, exactly like `windowTokens` / `kind`.
|
|
41
|
+
*/
|
|
42
|
+
readonly trigger?: 'manual' | 'auto';
|
|
13
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* One dispatch paired with the measurement that followed it — the
|
|
46
|
+
* intent-vs-observed record for the harness auto-compact window.
|
|
47
|
+
*
|
|
48
|
+
* Why a pair and not a number: the slice that owns this instrument could not
|
|
49
|
+
* run a real Claude Code session, and the harness's own documentation gives
|
|
50
|
+
* the env var as a WINDOW, not a trigger point, so "a window of X makes the
|
|
51
|
+
* harness fire at Y%" has no documented answer. Fabricating Y would be worse
|
|
52
|
+
* than not having it. Recording the intent (`requested*`) next to whatever
|
|
53
|
+
* the next real session actually measures (`observed*`) is the honest
|
|
54
|
+
* substitute: after one real session the gap becomes a measured fact.
|
|
55
|
+
*/
|
|
56
|
+
export interface WindowCalibrationPair {
|
|
57
|
+
readonly dispatchedAt: string;
|
|
58
|
+
readonly windowTokens: number | null;
|
|
59
|
+
readonly windowSource: string | null;
|
|
60
|
+
/** The ratio at which peaks-loop asked. */
|
|
61
|
+
readonly requestedRatio: number;
|
|
62
|
+
/** `requestedRatio * windowTokens` — token point asked for; null with no window. */
|
|
63
|
+
readonly requestedTokens: number | null;
|
|
64
|
+
/** Measured ratio of the first `observed` row after this dispatch. */
|
|
65
|
+
readonly observedRatio: number | null;
|
|
66
|
+
readonly observedTokens: number | null;
|
|
67
|
+
/**
|
|
68
|
+
* `observedTokens - requestedTokens`. Negative = the harness compacted
|
|
69
|
+
* BEFORE the point peaks-loop asked for (which is the harness's own
|
|
70
|
+
* trigger firing early). Null while unmeasured.
|
|
71
|
+
*/
|
|
72
|
+
readonly driftTokens: number | null;
|
|
73
|
+
readonly measured: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface WindowCalibration {
|
|
76
|
+
readonly pairs: ReadonlyArray<WindowCalibrationPair>;
|
|
77
|
+
/** Dispatches with no following measurement yet. */
|
|
78
|
+
readonly unmeasured: number;
|
|
79
|
+
/** The window in force on the most recent dispatch, for a one-line read. */
|
|
80
|
+
readonly lastWindowTokens: number | null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pair each dispatched compact with the measurement that followed it. Pure;
|
|
84
|
+
* tolerates a file written by an earlier release (no `windowTokens`, no
|
|
85
|
+
* `kind`) — those rows produce a pair with a null window rather than an
|
|
86
|
+
* error, because the history file is append-only and never rewritten.
|
|
87
|
+
*/
|
|
88
|
+
export declare function computeWindowCalibration(events: ReadonlyArray<CompactHistoryEvent>): WindowCalibration;
|
|
14
89
|
export type CompactHistoryReadResult = {
|
|
15
90
|
readonly kind: 'file-missing';
|
|
16
91
|
readonly path: string;
|
|
@@ -12,6 +12,55 @@
|
|
|
12
12
|
// read.
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
+
/**
|
|
16
|
+
* Pair each dispatched compact with the measurement that followed it. Pure;
|
|
17
|
+
* tolerates a file written by an earlier release (no `windowTokens`, no
|
|
18
|
+
* `kind`) — those rows produce a pair with a null window rather than an
|
|
19
|
+
* error, because the history file is append-only and never rewritten.
|
|
20
|
+
*/
|
|
21
|
+
export function computeWindowCalibration(events) {
|
|
22
|
+
const pairs = [];
|
|
23
|
+
for (const event of events) {
|
|
24
|
+
const kind = event.kind ?? 'dispatch';
|
|
25
|
+
if (kind === 'observed') {
|
|
26
|
+
// Attach to the most recent dispatch still waiting for a measurement.
|
|
27
|
+
for (let i = pairs.length - 1; i >= 0; i -= 1) {
|
|
28
|
+
const candidate = pairs[i];
|
|
29
|
+
if (candidate.observedRatio !== null || typeof event.afterRatio !== 'number')
|
|
30
|
+
continue;
|
|
31
|
+
const observedTokens = candidate.windowTokens === null ? null : event.afterRatio * candidate.windowTokens;
|
|
32
|
+
pairs[i] = {
|
|
33
|
+
...candidate,
|
|
34
|
+
observedRatio: event.afterRatio,
|
|
35
|
+
observedTokens,
|
|
36
|
+
driftTokens: observedTokens === null || candidate.requestedTokens === null
|
|
37
|
+
? null
|
|
38
|
+
: observedTokens - candidate.requestedTokens,
|
|
39
|
+
measured: true
|
|
40
|
+
};
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const windowTokens = typeof event.windowTokens === 'number' ? event.windowTokens : null;
|
|
46
|
+
pairs.push({
|
|
47
|
+
dispatchedAt: event.ts,
|
|
48
|
+
windowTokens,
|
|
49
|
+
windowSource: event.windowSource ?? null,
|
|
50
|
+
requestedRatio: event.beforeRatio,
|
|
51
|
+
requestedTokens: windowTokens === null ? null : event.beforeRatio * windowTokens,
|
|
52
|
+
observedRatio: null,
|
|
53
|
+
observedTokens: null,
|
|
54
|
+
driftTokens: null,
|
|
55
|
+
measured: false
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
pairs,
|
|
60
|
+
unmeasured: pairs.filter((p) => !p.measured).length,
|
|
61
|
+
lastWindowTokens: pairs.length === 0 ? null : pairs[pairs.length - 1].windowTokens
|
|
62
|
+
};
|
|
63
|
+
}
|
|
15
64
|
export function readCompactHistory(input) {
|
|
16
65
|
const path = join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'compact-history.jsonl');
|
|
17
66
|
if (!existsSync(path)) {
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
export interface RestoreListResult {
|
|
2
|
+
/**
|
|
3
|
+
* True when `~/.peaks/config.json.1.x.bak` exists — i.e. whether there is
|
|
4
|
+
* anything to restore from at all. Meaning is identical to
|
|
5
|
+
* `RollbackPlan.available`.
|
|
6
|
+
*/
|
|
7
|
+
available: boolean;
|
|
8
|
+
fields: string[];
|
|
9
|
+
}
|
|
1
10
|
export interface RestoreResult {
|
|
11
|
+
/** See `RestoreListResult.available`. */
|
|
12
|
+
available: boolean;
|
|
2
13
|
field: string;
|
|
3
14
|
applied: boolean;
|
|
4
15
|
sidecarPath?: string;
|
|
5
16
|
}
|
|
6
|
-
export declare function listAvailableFields():
|
|
17
|
+
export declare function listAvailableFields(): RestoreListResult;
|
|
7
18
|
export declare function restoreField(opts: {
|
|
8
19
|
field: string;
|
|
9
20
|
apply: boolean;
|
|
@@ -9,21 +9,52 @@ import { backupConfigPath } from './config-migration.js';
|
|
|
9
9
|
* can review before adopting. Fields in the deferred-design set
|
|
10
10
|
* (workspaces, providers, proxy) throw RESTORE_GUARDED so the user has
|
|
11
11
|
* to acknowledge explicitly.
|
|
12
|
+
*
|
|
13
|
+
* MISSING BACKUP IS NOT A FAILURE (rid 2026-09-13-two-decisions ①, user-
|
|
14
|
+
* decided). `~/.peaks/config.json.1.x.bak` only exists on a machine that ran
|
|
15
|
+
* `peaks config migrate --apply`; a machine that never did is in its NORMAL
|
|
16
|
+
* initial state, not in an error state. Both subcommands therefore report it
|
|
17
|
+
* as a SUCCESS carrying `available: false` rather than throwing — the sibling
|
|
18
|
+
* `rollback` already did, and `restore` exited 1 for the same state, so a
|
|
19
|
+
* script could not ask either one "was there ever a backup?" without knowing
|
|
20
|
+
* which subcommand it was talking to.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ THE COST, ACCEPTED BY THE USER: an existing script that read exit 1 as
|
|
23
|
+
* "never backed up" loses that signal. The replacement is `available` — it is
|
|
24
|
+
* on EVERY envelope this module produces, on the success path (`false` ⇒
|
|
25
|
+
* nothing to restore, exit 0) and on the failure path (`true` ⇒ a backup IS
|
|
26
|
+
* there and the field/guard was the problem, exit 1), so one JSON key answers
|
|
27
|
+
* the question that used to take the exit code, and it distinguishes "no
|
|
28
|
+
* backup" from "backup exists but the field is not in it".
|
|
12
29
|
*/
|
|
13
30
|
const GUARDED_FIELDS = new Set(['workspaces', 'providers', 'proxy']);
|
|
31
|
+
/**
|
|
32
|
+
* The parsed `.bak`, or `null` when there is none. `null` is the normal
|
|
33
|
+
* "nothing was ever migrated" state (see the module comment), not an error.
|
|
34
|
+
*
|
|
35
|
+
* A `.bak` that exists but does not parse still throws: `available` means
|
|
36
|
+
* "the backup file is present", and a present-but-malformed one is a real
|
|
37
|
+
* failure the caller must surface.
|
|
38
|
+
*/
|
|
14
39
|
function readBakContent() {
|
|
15
40
|
const bak = backupConfigPath();
|
|
16
41
|
if (!existsSync(bak)) {
|
|
17
|
-
|
|
42
|
+
return null;
|
|
18
43
|
}
|
|
19
44
|
return JSON.parse(readFileSync(bak, 'utf8'));
|
|
20
45
|
}
|
|
21
46
|
export function listAvailableFields() {
|
|
22
47
|
const bak = readBakContent();
|
|
23
|
-
|
|
48
|
+
if (bak === null) {
|
|
49
|
+
return { available: false, fields: [] };
|
|
50
|
+
}
|
|
51
|
+
return { available: true, fields: Object.keys(bak).filter((k) => k !== 'version') };
|
|
24
52
|
}
|
|
25
53
|
export function restoreField(opts) {
|
|
26
54
|
const bak = readBakContent();
|
|
55
|
+
if (bak === null) {
|
|
56
|
+
return { available: false, field: opts.field, applied: false };
|
|
57
|
+
}
|
|
27
58
|
if (!(opts.field in bak)) {
|
|
28
59
|
throw new Error(`FIELD_NOT_FOUND: ${opts.field} is not in config.json.1.x.bak`);
|
|
29
60
|
}
|
|
@@ -33,7 +64,7 @@ export function restoreField(opts) {
|
|
|
33
64
|
const home = homedir();
|
|
34
65
|
const sidecar = join(home, '.peaks', `config.json.restore-${opts.field}.json`);
|
|
35
66
|
if (!opts.apply) {
|
|
36
|
-
return { field: opts.field, applied: false };
|
|
67
|
+
return { available: true, field: opts.field, applied: false };
|
|
37
68
|
}
|
|
38
69
|
mkdirSync(join(home, '.peaks'), { recursive: true });
|
|
39
70
|
const payload = {
|
|
@@ -43,5 +74,5 @@ export function restoreField(opts) {
|
|
|
43
74
|
restoredAt: new Date().toISOString(),
|
|
44
75
|
};
|
|
45
76
|
writeFileSync(sidecar, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
46
|
-
return { field: opts.field, applied: true, sidecarPath: sidecar };
|
|
77
|
+
return { available: true, field: opts.field, applied: true, sidecarPath: sidecar };
|
|
47
78
|
}
|
|
@@ -15,7 +15,12 @@ export function planRollback() {
|
|
|
15
15
|
export function executeRollback(opts) {
|
|
16
16
|
const plan = planRollback();
|
|
17
17
|
if (!plan.available) {
|
|
18
|
-
|
|
18
|
+
// rid 2026-09-13-two-decisions ①: a machine that never migrated has no
|
|
19
|
+
// `.bak`, and that is its normal state — `--apply` on such a machine is
|
|
20
|
+
// "nothing to roll back", not a failure. Returned instead of thrown so the
|
|
21
|
+
// exit status and the `available` key agree on every path; see
|
|
22
|
+
// `config-restore.ts` for the full rationale and the accepted cost.
|
|
23
|
+
return { ...plan, applied: false };
|
|
19
24
|
}
|
|
20
25
|
if (!opts.apply) {
|
|
21
26
|
return { ...plan, applied: false };
|
|
@@ -51,6 +51,58 @@ export declare class InvalidProjectRootError extends Error {
|
|
|
51
51
|
constructor(reason: 'empty' | 'nul-byte' | 'non-existent' | 'non-canonical', input: string);
|
|
52
52
|
}
|
|
53
53
|
export declare function resolveCanonicalProjectRootStrict(startPath: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* True when `projectRoot` IS the user's home directory itself.
|
|
56
|
+
*
|
|
57
|
+
* Exact match, NOT `isInsidePath`: `~/my-project` is an ordinary project and
|
|
58
|
+
* must keep working — the guard exists for the one directory that is never a
|
|
59
|
+
* project.
|
|
60
|
+
*
|
|
61
|
+
* `projectRootsMatch` rather than `===` because the two strings reach this
|
|
62
|
+
* function from different sources: a `--project .` argument arrives in the
|
|
63
|
+
* caller's spelling while `homedir()` yields the OS form (`C:\Users\x` vs
|
|
64
|
+
* `C:/Users/x`, and differing case on win32/darwin).
|
|
65
|
+
*
|
|
66
|
+
* This is the ONE definition of "the user's home" in the codebase; the
|
|
67
|
+
* harness-window writer's own guard delegates here so the two cannot drift.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isUserHomeProjectRoot(projectRoot: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Thrown by `assertWritableProjectRoot` / `resolveWritableProjectRoot` when a
|
|
72
|
+
* write would land in the user's home directory. Distinct from
|
|
73
|
+
* `InvalidProjectRootError` (which is about a malformed/absent path): this path
|
|
74
|
+
* is perfectly valid — it is just not a project.
|
|
75
|
+
*/
|
|
76
|
+
export declare class UnsafeProjectRootError extends Error {
|
|
77
|
+
readonly projectRoot: string;
|
|
78
|
+
constructor(projectRoot: string);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Refuse a project root that IS the user's home directory.
|
|
82
|
+
*
|
|
83
|
+
* The hazard this closes: a fresh terminal starts in `$HOME`, and
|
|
84
|
+
* `peaks workspace init --project .` there resolved to `$HOME` (via
|
|
85
|
+
* `resolveCanonicalProjectRoot`'s fail-open fallback, which returns the start
|
|
86
|
+
* path when it can find no project marker) and then materialized a whole
|
|
87
|
+
* project tree INTO the user's home — `.claude/settings.local.json` above all,
|
|
88
|
+
* which is a live config file shared with every other project on the machine.
|
|
89
|
+
*
|
|
90
|
+
* Deliberately NOT enforced inside `resolveCanonicalProjectRoot`. That helper
|
|
91
|
+
* is called from 85 sites across 32 files, most of them READS (`peaks status`,
|
|
92
|
+
* `peaks dashboard`, `peaks memory`, the observability commands). Refusing
|
|
93
|
+
* there would break every one of them the moment a user runs a read command
|
|
94
|
+
* from their home directory, and its fail-open contract is documented and
|
|
95
|
+
* depended on. The hazard is a WRITE to `$HOME/.claude/`, so the guard belongs
|
|
96
|
+
* on write paths — this function, plus `resolveWritableProjectRoot` so a
|
|
97
|
+
* writer only has to make one call.
|
|
98
|
+
*/
|
|
99
|
+
export declare function assertWritableProjectRoot(projectRoot: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* `resolveCanonicalProjectRoot` for a path that is about to be WRITTEN to:
|
|
102
|
+
* same git-root-then-heuristic promotion, plus the home-directory refusal.
|
|
103
|
+
* Read paths keep using the fail-open helper.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveWritableProjectRoot(startPath: string): string;
|
|
54
106
|
export declare function getProjectConfigPath(projectRoot: string | null): string | null;
|
|
55
107
|
export declare function getProjectBootstrapConfigPath(projectRoot: string): string;
|
|
56
108
|
export declare function validateProjectBootstrapConfigPathForWrite(projectRoot: string, configPath: string): void;
|