peaks-loop 4.0.47 → 4.0.49
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 +44 -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.js +48 -8
- package/dist/cli/commands/compact-command.js +110 -0
- package/dist/cli/commands/config-commands.js +15 -9
- 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/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +4 -4
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-eval-commands.js +31 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +12 -0
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/request-commands.js +8 -0
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/slice-integrate-commands.js +22 -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/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/index.js +5 -45
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +140 -65
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +77 -46
- 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/enforcers/active-skill-resolver.js +14 -1
- 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/code/auto-compact-lifecycle.d.ts +194 -0
- package/dist/services/code/auto-compact-lifecycle.js +229 -11
- package/dist/services/code/auto-compact-orchestrator.js +118 -7
- package/dist/services/code/compact-event-settle.d.ts +134 -0
- package/dist/services/code/compact-event-settle.js +240 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -0
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- 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/context/auto-compact-types.d.ts +20 -2
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/evidence/evidence-generator.js +86 -49
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- 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/ide/ide-registry.d.ts +19 -0
- package/dist/services/ide/ide-registry.js +21 -0
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +10 -3
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -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 +124 -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 +23 -0
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +6 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +61 -2
- package/dist/services/skills/hooks-settings-service.js +14 -4
- package/dist/services/skills/session-start-hook-constants.d.ts +45 -0
- package/dist/services/skills/session-start-hook-constants.js +45 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +29 -11
- package/dist/services/slice/slice-review-state.js +23 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +221 -103
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +47 -33
- package/dist/services/workflow/pipeline-verify-types.d.ts +15 -6
- 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/workspace-claude-settings-materializer.js +78 -7
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +6 -6
- package/skills/bee/peaks-prd/SKILL.md +7 -5
- package/skills/bee/peaks-qa/SKILL.md +5 -5
- 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 +8 -6
- 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/peaks-code/SKILL.md +1 -1
- 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/workflow-gates-and-types.md +8 -6
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
if (!settled.lifecycleWritten) {
|
|
165
|
+
// Repair R9 (AC2): a settle whose lifecycle write FAILED has not settled the
|
|
166
|
+
// run. The record is still resting at `armed` / `compacting`, so the next
|
|
167
|
+
// arrival — another `PostCompact`, or a probe — finds the SAME run open and
|
|
168
|
+
// settles it again. Appending the row anyway writes one per arrival for a
|
|
169
|
+
// compaction the lifecycle store never recorded, each one carrying an
|
|
170
|
+
// `afterRatio` that reads as a settled measurement; and when the store does
|
|
171
|
+
// recover, the retry appends a SECOND row for the same compaction, which
|
|
172
|
+
// `computeWindowCalibration` is free to attach to a later dispatch pair.
|
|
173
|
+
// Nothing is lost by deferring: `probe.ratio` is re-measured at the retry,
|
|
174
|
+
// and the retry is the settlement this row is about.
|
|
175
|
+
return {
|
|
176
|
+
settled: true,
|
|
177
|
+
runId: settled.runId,
|
|
178
|
+
beforeRatio: settled.triggerRatio,
|
|
179
|
+
afterRatio: settled.afterRatio,
|
|
180
|
+
historyWritten: false,
|
|
181
|
+
lifecycleWritten: false,
|
|
182
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {})
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const event = {
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
kind: 'observed',
|
|
188
|
+
ts: new Date().toISOString(),
|
|
189
|
+
// The guard above refused every payload that named another session, so
|
|
190
|
+
// reaching here means the event named this one or named none. The row is
|
|
191
|
+
// therefore attributable to the main session, which is the only session
|
|
192
|
+
// this command settles.
|
|
193
|
+
target: 'main',
|
|
194
|
+
mode: resolveAutoCompactProfile(input.projectRoot),
|
|
195
|
+
ide: measurement?.ide ?? 'claude-code',
|
|
196
|
+
pathway: COMPACT_EVENT_PATHWAY,
|
|
197
|
+
beforeRatio: settled.triggerRatio,
|
|
198
|
+
...(settled.afterRatio !== null ? { afterRatio: settled.afterRatio } : {}),
|
|
199
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {}),
|
|
200
|
+
redLine: false,
|
|
201
|
+
ok: true,
|
|
202
|
+
checkpointPath: '',
|
|
203
|
+
dispatchMessage: `PostCompact fired: the harness reported a ${input.trigger ?? 'unreported-trigger'} compaction, ` +
|
|
204
|
+
`settling the run dispatched at ${(settled.triggerRatio * 100).toFixed(1)}%` +
|
|
205
|
+
(settled.afterRatio !== null
|
|
206
|
+
? ` (measured now at ${(settled.afterRatio * 100).toFixed(1)}%)`
|
|
207
|
+
: ' (no post-compact measurement available)'),
|
|
208
|
+
...(settled.afterRatio !== null
|
|
209
|
+
? { windowTokens: measurement?.windowTokens ?? null, windowSource: measurement?.windowSource ?? null }
|
|
210
|
+
: {})
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
appendCompactHistoryEvent({ projectRoot: input.projectRoot, sessionId: input.sessionId, event });
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// The lifecycle run is already settled; losing the history row is the
|
|
217
|
+
// smaller loss, and a throwing hook is the larger one. Reported rather
|
|
218
|
+
// than silenced: "the run settled but the row is missing" and "the row is
|
|
219
|
+
// on disk" are different facts, and a caller that cannot tell them apart
|
|
220
|
+
// cannot diagnose a hook that has stopped recording anything.
|
|
221
|
+
return {
|
|
222
|
+
settled: true,
|
|
223
|
+
runId: settled.runId,
|
|
224
|
+
beforeRatio: settled.triggerRatio,
|
|
225
|
+
afterRatio: settled.afterRatio,
|
|
226
|
+
historyWritten: false,
|
|
227
|
+
lifecycleWritten: settled.lifecycleWritten,
|
|
228
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {})
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
settled: true,
|
|
233
|
+
runId: settled.runId,
|
|
234
|
+
beforeRatio: settled.triggerRatio,
|
|
235
|
+
afterRatio: settled.afterRatio,
|
|
236
|
+
historyWritten: true,
|
|
237
|
+
lifecycleWritten: settled.lifecycleWritten,
|
|
238
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {})
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -26,6 +26,20 @@ export interface CompactHistoryEvent {
|
|
|
26
26
|
readonly kind?: 'dispatch' | 'observed';
|
|
27
27
|
/** `observed` rows only: the measured post-compact ratio. */
|
|
28
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';
|
|
29
43
|
}
|
|
30
44
|
/**
|
|
31
45
|
* One dispatch paired with the measurement that followed it — the
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Decision priority (explicit, no implicit fall-through):
|
|
11
11
|
// 1. lifecycle missing → fall back to legacy (pending → queued,
|
|
12
|
-
// recent history → completed WITHOUT an invented
|
|
13
|
-
// else none)
|
|
12
|
+
// a recent `observed` history row → completed WITHOUT an invented
|
|
13
|
+
// after-ratio, else none)
|
|
14
14
|
// 2. lifecycle invalid → 'invalid' (NEVER fall back to legacy
|
|
15
15
|
// — a corrupted lifecycle is not a green progress bar)
|
|
16
16
|
// 3. lifecycle valid → map stage to filledCells via the
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
// ratios. After-ratio is only rendered when the lifecycle record
|
|
38
38
|
// carries a real one; otherwise the bar shows a stable "no
|
|
39
39
|
// measurement" hint.
|
|
40
|
-
import { existsSync, readFileSync
|
|
40
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
41
41
|
import { getSessionDir } from '../session/getSessionDir.js';
|
|
42
42
|
import { AUTO_COMPACT_RED_LINE_RATIO } from '../context/auto-compact-types.js';
|
|
43
|
+
import { readCompactHistory } from '../compact-history/compact-history-service.js';
|
|
43
44
|
import { readCompactLifecycle, } from './compact-lifecycle-store.js';
|
|
44
45
|
/**
|
|
45
46
|
* Concrete first-version stale timeout. Adjustable after real timing
|
|
@@ -58,7 +59,12 @@ const DEFAULT_STALE_AFTER_MS = 120_000;
|
|
|
58
59
|
* to not flap on subsequent reads. Adjustable after real timing feedback.
|
|
59
60
|
*/
|
|
60
61
|
export const COMPLETED_EXPIRY_MS = 10_000;
|
|
61
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* How recently the last history row must itself have been written for the
|
|
64
|
+
* legacy path to report it as "just compacted". Measured against the row's own
|
|
65
|
+
* `ts` — the fact — rather than the file's mtime, which is only a proxy for it
|
|
66
|
+
* and is also moved by rows that record no compaction (repair R9).
|
|
67
|
+
*/
|
|
62
68
|
const LEGACY_JUST_COMPACTED_WINDOW_MS = 30_000;
|
|
63
69
|
// PRD-002b slice 2 — extract cell-table magic numbers (4/6/8) into named
|
|
64
70
|
// consts so the no-magic-numbers lint rule stops flagging the typed
|
|
@@ -142,6 +148,7 @@ export function decideCompactStatusline(input) {
|
|
|
142
148
|
// lifecycle.kind === 'missing' → fall back to legacy files.
|
|
143
149
|
return decideLegacyFallback({
|
|
144
150
|
projectRoot: input.projectRoot,
|
|
151
|
+
sessionId: input.sessionId,
|
|
145
152
|
sessionDir,
|
|
146
153
|
now: input.now,
|
|
147
154
|
});
|
|
@@ -212,26 +219,53 @@ function decideLegacyFallback(input) {
|
|
|
212
219
|
// fall through to history check
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
|
-
// Priority 2 within legacy: a
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
222
|
+
// Priority 2 within legacy: a history row TESTIFIES that a compaction was
|
|
223
|
+
// witnessed, recently enough to still be the one being reported.
|
|
224
|
+
//
|
|
225
|
+
// Repair R9 (AC4). This used to read the file's mtime — freshness taken as
|
|
226
|
+
// evidence that "a compact just landed". Freshness is evidence of neither:
|
|
227
|
+
// the same file takes a `dispatch` row every time peaks-loop ASKS for a
|
|
228
|
+
// compact, and an ask is an intent, not an outcome. One real session
|
|
229
|
+
// (2026-09-13, ~15.5 h) holds 1075 dispatch rows and ZERO compactions
|
|
230
|
+
// (`auto-compact-orchestrator.ts`), so a fresh file was the NORMAL state of a
|
|
231
|
+
// session in which nothing had compacted at all — and each of those asks
|
|
232
|
+
// painted this 8-cell "completed" bar.
|
|
233
|
+
//
|
|
234
|
+
// So the row's own testimony is read instead, through the same reader the CLI
|
|
235
|
+
// uses. Only `kind: 'observed'` means a compaction was witnessed (a row with
|
|
236
|
+
// no `kind` is a dispatch — every pre-`kind` row is one), and the row's `ts`
|
|
237
|
+
// is the moment compared, not the filesystem's.
|
|
238
|
+
//
|
|
239
|
+
// Repair R11. The testimony is looked for in EVERY row inside the window, not
|
|
240
|
+
// only in the last one. Reading the last row alone made the indicator depend
|
|
241
|
+
// on the ASK: a `dispatch` row lands on every probe, so an `observed` row
|
|
242
|
+
// stopped counting the moment peaks-loop asked again — `[observed, dispatch]`
|
|
243
|
+
// one second apart answered `none` for a compaction that had just been
|
|
244
|
+
// witnessed — i.e. it deleted the very indicator the mtime read had shown,
|
|
245
|
+
// the one the replacement was meant to keep honest. A `dispatch` row alone
|
|
246
|
+
// still testifies
|
|
247
|
+
// to nothing (the direction R9 closed): what is required is an `observed` row
|
|
248
|
+
// inside the window, wherever in the file it sits.
|
|
249
|
+
try {
|
|
250
|
+
// The reader answers `file-missing` / `empty` itself; the `catch` is for the
|
|
251
|
+
// read itself, which it does not guard.
|
|
252
|
+
const read = readCompactHistory({ projectRoot: input.projectRoot, sessionId: input.sessionId });
|
|
253
|
+
const witnessed = read.kind === 'ok' &&
|
|
254
|
+
read.events.some((row) => row.kind === 'observed' && now - Date.parse(row.ts) <= LEGACY_JUST_COMPACTED_WINDOW_MS);
|
|
255
|
+
if (witnessed) {
|
|
256
|
+
// CRITICAL: no invented after-ratio. The row may carry a measured
|
|
257
|
+
// afterRatio, but this is the legacy path and we honour the
|
|
258
|
+
// "no measurement" default.
|
|
259
|
+
return {
|
|
260
|
+
kind: 'completed',
|
|
261
|
+
filledCells: 8,
|
|
262
|
+
detail: historyPath,
|
|
263
|
+
};
|
|
233
264
|
}
|
|
234
265
|
}
|
|
266
|
+
catch {
|
|
267
|
+
// fall through to idle
|
|
268
|
+
}
|
|
235
269
|
return { kind: 'none', filledCells: 0 };
|
|
236
270
|
}
|
|
237
271
|
/**
|
|
@@ -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 };
|
|
@@ -128,13 +128,31 @@ export type AutoCompactResult = {
|
|
|
128
128
|
readonly nextActions: readonly string[];
|
|
129
129
|
} | {
|
|
130
130
|
readonly ok: true;
|
|
131
|
-
readonly code: 'AUTO_COMPACT_SKIP' | 'AUTO_COMPACT_WAIT';
|
|
131
|
+
readonly code: 'AUTO_COMPACT_SKIP' | 'AUTO_COMPACT_WAIT' | 'AUTO_COMPACT_ALREADY_ARMED' | 'AUTO_COMPACT_UNRESOLVED_SESSION';
|
|
132
132
|
readonly message: string;
|
|
133
133
|
readonly data: {
|
|
134
134
|
readonly sessionId: string;
|
|
135
135
|
readonly ratio: number;
|
|
136
136
|
readonly source: string;
|
|
137
|
-
|
|
137
|
+
/**
|
|
138
|
+
* `unresolved-session` (repair R6): the session id named no session
|
|
139
|
+
* directory, so the compact backoff's question — "is an attempt already
|
|
140
|
+
* outstanding?" — could not be asked. It is NOT `already-armed` (no run
|
|
141
|
+
* was found; the record could not be read at all) and NOT an admit: the
|
|
142
|
+
* dispatch is left undone because an unanswerable question is not a
|
|
143
|
+
* "no", and admitting on one is how a gate ends up reading a string
|
|
144
|
+
* instead of the artifact the string names.
|
|
145
|
+
*/
|
|
146
|
+
readonly decision: 'below-threshold' | 'in-flight-batch' | 'already-armed' | 'unresolved-session';
|
|
147
|
+
/**
|
|
148
|
+
* rid `2026-09-14-compact-dispatch-backoff`, `already-armed` only: the
|
|
149
|
+
* ratio the open run was dispatched at, and its id. `ratio` above is the
|
|
150
|
+
* LIVE reading — the pair is what keeps "it is still high" legible after
|
|
151
|
+
* the backoff drops the per-probe rows: the ask is at `armedAtRatio`, and
|
|
152
|
+
* the context is now at `ratio`, above it and not yet compacted.
|
|
153
|
+
*/
|
|
154
|
+
readonly armedAtRatio?: number;
|
|
155
|
+
readonly armedRunId?: string;
|
|
138
156
|
/**
|
|
139
157
|
* Slice 2026-09-13-auto-compact-trigger-ownership: what syncing the
|
|
140
158
|
* harness auto-compact window did on this probe. `null` = the active
|