peaks-loop 4.0.47 → 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 +24 -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 +112 -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/hooks-commands.js +4 -4
- package/dist/cli/commands/job-commands.js +8 -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 +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 +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/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 +130 -65
- 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/code/auto-compact-lifecycle.d.ts +119 -0
- package/dist/services/code/auto-compact-lifecycle.js +169 -0
- package/dist/services/code/auto-compact-orchestrator.js +13 -2
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -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/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/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-state-store.js +7 -0
- 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 +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/scan/karpathy-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/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 +8 -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/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/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
|
@@ -275,3 +275,172 @@ export function settleOpenLifecycleRun(input) {
|
|
|
275
275
|
emit('completed', true);
|
|
276
276
|
return { runId: prior.runId, triggerRatio: prior.triggerRatio, afterRatio: input.measuredRatio };
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* rid `2026-09-13-compact-event-settle`: close out an open compact run because
|
|
280
|
+
* the HARNESS said one completed — `PostCompact` — rather than because a later
|
|
281
|
+
* probe noticed the ratio had fallen.
|
|
282
|
+
*
|
|
283
|
+
* WHY THIS IS A SECOND FUNCTION AND NOT A FLAG ON THE ONE ABOVE. The function
|
|
284
|
+
* above is defined by two MEASUREMENT gates: it refuses when nothing could be
|
|
285
|
+
* measured, and refuses when the number it got has not dropped far enough. Both
|
|
286
|
+
* are correct for a probe, whose ratio is an INFERENCE about whether something
|
|
287
|
+
* happened. Handed a harness event, both are wrong in the same direction —
|
|
288
|
+
* the harness has already stated that the compaction happened, so a probe that
|
|
289
|
+
* could not measure, or measured something larger, contradicts nothing. The
|
|
290
|
+
* event is the evidence; the ratio is a consequence.
|
|
291
|
+
*
|
|
292
|
+
* What survives from the probe path is the ATTRIBUTION gate, and only that:
|
|
293
|
+
* there must be an open run (`compacting` / `armed`) for this event to be
|
|
294
|
+
* about. A `PostCompact` on a session where peaks-loop never dispatched has
|
|
295
|
+
* nothing to settle — objectively, the run the event would complete does not
|
|
296
|
+
* exist. (`queued` / `preparing` are excluded for the probe path's reason: a
|
|
297
|
+
* run that died before dispatch never had a compaction to complete.)
|
|
298
|
+
*
|
|
299
|
+
* `afterRatio` is recorded ONLY when it is a genuine DROP below the ratio the
|
|
300
|
+
* dispatch was made at. Immediately after a compaction, `readContextPercent`
|
|
301
|
+
* prefers the statusline file, which may still hold the PRE-compact value; the
|
|
302
|
+
* one thing this row must not do is launder that stale reading into an
|
|
303
|
+
* `afterRatio` and publish "the context did not shrink" as a measurement. A
|
|
304
|
+
* `null` here means "no honest post-compact number was available at the moment
|
|
305
|
+
* the event fired" — and that is NOT self-healing: the record is left at
|
|
306
|
+
* `completed` with no number, `computeWindowCalibration` skips `observed` rows
|
|
307
|
+
* that carry none, and the probe path refuses a run that is no longer open. The
|
|
308
|
+
* pair is then closed by `fillEventSettledMeasurement` below — but only on the
|
|
309
|
+
* probes that reach it, which is not all of them: that call sits in the
|
|
310
|
+
* BELOW-THRESHOLD branch of `runAutoCompact` (`auto-compact-orchestrator.ts:567`
|
|
311
|
+
* guards it, `:589` calls it). A probe that instead commits to compacting does
|
|
312
|
+
* not merely defer the measurement: `advance('queued')` writes a fresh run to
|
|
313
|
+
* the same one-record-per-session store (`auto-compact-orchestrator.ts:668`), so
|
|
314
|
+
* the `completed`-without-`afterRatio` record this pair was owed is gone and the
|
|
315
|
+
* pair stays unmeasured. That loss is inherent rather than an oversight — once a
|
|
316
|
+
* second compaction has happened, no later ratio can be attributed to the first,
|
|
317
|
+
* so there is nothing honest left to fill — and it is visible as `unmeasured` in
|
|
318
|
+
* `peaks compact history` (QA residual R9). A fabricated number is not
|
|
319
|
+
* recoverable at all, which is why the stale reading is dropped rather than
|
|
320
|
+
* corrected.
|
|
321
|
+
*
|
|
322
|
+
* `verifying` is deliberately NOT emitted: its documented meaning is "we hold a
|
|
323
|
+
* measurement and are checking it", and on this path there may be no
|
|
324
|
+
* measurement at all. Emitting it would move the same untruth from the history
|
|
325
|
+
* row into the lifecycle record.
|
|
326
|
+
*
|
|
327
|
+
* Returns the settled facts, or `null` when there was nothing to settle. `null`
|
|
328
|
+
* means exactly ONE thing here — there was no OPEN run for this event to be
|
|
329
|
+
* about. A run that was open but whose record could not be written is not
|
|
330
|
+
* `null`: it returns the facts read off that run with `lifecycleWritten: false`,
|
|
331
|
+
* because a failed write is not an absent run, and a caller that cannot tell the
|
|
332
|
+
* two apart ends up telling the user a falsehood (see `compact-event-settle.ts`).
|
|
333
|
+
*/
|
|
334
|
+
export function settleOpenLifecycleRunOnCompactEvent(input) {
|
|
335
|
+
const prior = readOpenCompactLifecycle({
|
|
336
|
+
projectRoot: input.projectRoot,
|
|
337
|
+
sessionId: input.sessionId
|
|
338
|
+
});
|
|
339
|
+
if (prior === null)
|
|
340
|
+
return null;
|
|
341
|
+
if (prior.stage !== 'compacting' && prior.stage !== 'armed')
|
|
342
|
+
return null;
|
|
343
|
+
const afterRatio = input.measuredRatio !== null && input.measuredRatio < prior.triggerRatio ? input.measuredRatio : null;
|
|
344
|
+
const record = {
|
|
345
|
+
schemaVersion: 1,
|
|
346
|
+
runId: prior.runId,
|
|
347
|
+
stage: 'completed',
|
|
348
|
+
updatedAt: new Date().toISOString(),
|
|
349
|
+
triggerRatio: prior.triggerRatio,
|
|
350
|
+
redLine: prior.redLine,
|
|
351
|
+
...(afterRatio !== null ? { afterRatio } : {})
|
|
352
|
+
};
|
|
353
|
+
const facts = { runId: prior.runId, triggerRatio: prior.triggerRatio, afterRatio };
|
|
354
|
+
try {
|
|
355
|
+
if (input.failLifecycleWrite)
|
|
356
|
+
throw new Error('lifecycle store unavailable');
|
|
357
|
+
writeCompactLifecycle({
|
|
358
|
+
projectRoot: input.projectRoot,
|
|
359
|
+
sessionId: input.sessionId,
|
|
360
|
+
record
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Deliberately NOT `null`. The run WAS open and the write FAILED; returning
|
|
365
|
+
// the same value as "no run is open" is the conflation the repo's own lint
|
|
366
|
+
// names at this exact line (`catch-return-null — caller cannot distinguish
|
|
367
|
+
// failure from success`), and it reached the user as "No compact run was
|
|
368
|
+
// open", which is false.
|
|
369
|
+
return { ...facts, lifecycleWritten: false };
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
input.onLifecycleStage?.('completed', record);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Observer failures are not ours to propagate.
|
|
376
|
+
}
|
|
377
|
+
return { ...facts, lifecycleWritten: true };
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* rid `2026-09-13-compact-event-settle` (repair R1): supply the measurement a
|
|
381
|
+
* harness-settled run was left owing.
|
|
382
|
+
*
|
|
383
|
+
* The function above deliberately refuses to launder a post-compact reading
|
|
384
|
+
* that has not dropped — and right after a compaction that refusal is the
|
|
385
|
+
* NORMAL case, because the statusline still holds the pre-compact value. The
|
|
386
|
+
* run is then closed at `completed` with no `afterRatio`, so the dispatch's
|
|
387
|
+
* calibration pair never closes and "intent vs observed" stays blank for
|
|
388
|
+
* exactly the compactions this slice exists to witness. This function is what
|
|
389
|
+
* makes the function above's promise payable.
|
|
390
|
+
*
|
|
391
|
+
* WHY NOT WIDEN `settleOpenLifecycleRun`. That one re-emits `verifying` before
|
|
392
|
+
* `completed`, which on an already-`completed` record is a backwards stage
|
|
393
|
+
* transition with no observer to serve. This is not a settlement — the run IS
|
|
394
|
+
* settled; only the number is owed. So no stage is rewritten here.
|
|
395
|
+
*
|
|
396
|
+
* WRITING `afterRatio` ONTO THE RECORD IS THE IDEMPOTENCE TOKEN: every later
|
|
397
|
+
* probe finds it present and returns `null`, so however many probes follow, one
|
|
398
|
+
* compaction yields exactly one late measurement.
|
|
399
|
+
*
|
|
400
|
+
* THE DROP GATE IS THE EVENT PATH'S OWN (`measuredRatio < triggerRatio`), not
|
|
401
|
+
* the probe path's `autoFireThreshold`. `afterRatio` has to mean "below the
|
|
402
|
+
* ratio this run was dispatched at" — the rule the event path already enforces
|
|
403
|
+
* — or a run dispatched under the threshold (a forced or banded dispatch) would
|
|
404
|
+
* let a NON-drop through the one path that can still write a `completed` record.
|
|
405
|
+
* `conservative-fallback` is refused for the probe path's reason: its `0` is
|
|
406
|
+
* the absence of a measurement, not an empty context.
|
|
407
|
+
*
|
|
408
|
+
* Returns the filled record, or `null` when no run is owed a measurement.
|
|
409
|
+
*/
|
|
410
|
+
export function fillEventSettledMeasurement(input) {
|
|
411
|
+
if (input.source === 'conservative-fallback')
|
|
412
|
+
return null;
|
|
413
|
+
const prior = readOpenCompactLifecycle({
|
|
414
|
+
projectRoot: input.projectRoot,
|
|
415
|
+
sessionId: input.sessionId
|
|
416
|
+
});
|
|
417
|
+
if (prior === null)
|
|
418
|
+
return null;
|
|
419
|
+
// Exactly one shape is owed a number: the one the EVENT path leaves behind.
|
|
420
|
+
// `settleOpenLifecycleRun` never writes it (it always carries `afterRatio`),
|
|
421
|
+
// and a `failed` run never dispatched, so it has no row to pair with.
|
|
422
|
+
if (prior.stage !== 'completed' || prior.afterRatio !== undefined)
|
|
423
|
+
return null;
|
|
424
|
+
if (input.measuredRatio >= prior.triggerRatio)
|
|
425
|
+
return null;
|
|
426
|
+
const record = {
|
|
427
|
+
schemaVersion: 1,
|
|
428
|
+
runId: prior.runId,
|
|
429
|
+
stage: 'completed',
|
|
430
|
+
updatedAt: new Date().toISOString(),
|
|
431
|
+
triggerRatio: prior.triggerRatio,
|
|
432
|
+
redLine: prior.redLine,
|
|
433
|
+
afterRatio: input.measuredRatio
|
|
434
|
+
};
|
|
435
|
+
try {
|
|
436
|
+
writeCompactLifecycle({
|
|
437
|
+
projectRoot: input.projectRoot,
|
|
438
|
+
sessionId: input.sessionId,
|
|
439
|
+
record
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
return { runId: prior.runId, triggerRatio: prior.triggerRatio, afterRatio: input.measuredRatio };
|
|
446
|
+
}
|
|
@@ -42,7 +42,7 @@ import { describeHarnessWindowSync, harnessWindowSyncWarning } from '../context/
|
|
|
42
42
|
import { AUTO_COMPACT_PRE_COMPACT_RATIO, DEPRECATED_ENVELOPE_FIELDS } from '../context/auto-compact-types.js';
|
|
43
43
|
import { describeMode, thresholdFor } from './auto-compact-modes.js';
|
|
44
44
|
import { resolveAutoCompactProfile } from '../mode/mode-status-service.js';
|
|
45
|
-
import { CompactLifecyclePublisher, newCompactRunId, resolveDispatchedStage, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
|
|
45
|
+
import { CompactLifecyclePublisher, fillEventSettledMeasurement, newCompactRunId, resolveDispatchedStage, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
|
|
46
46
|
const PRE_COMPACT_REASON = 'pre-compact-auto';
|
|
47
47
|
/**
|
|
48
48
|
* Map a context ratio to a `CompactTrigger` action. Pure; the side
|
|
@@ -389,7 +389,18 @@ export async function runAutoCompact(input) {
|
|
|
389
389
|
source: probe.source,
|
|
390
390
|
autoFireThreshold: thresholdFor(mode, 'autoFire'),
|
|
391
391
|
onLifecycleStage: input.onLifecycleStage
|
|
392
|
-
})
|
|
392
|
+
}) ??
|
|
393
|
+
// Repair R1 (`2026-09-13-compact-event-settle`): the HARNESS event may
|
|
394
|
+
// already have closed this run WITHOUT an honest post-compact number —
|
|
395
|
+
// in which case the call above finds nothing open, and without this the
|
|
396
|
+
// calibration pair stays blank for exactly the compactions the event path
|
|
397
|
+
// exists to witness. Fills the number the event owed.
|
|
398
|
+
fillEventSettledMeasurement({
|
|
399
|
+
projectRoot: input.projectRoot,
|
|
400
|
+
sessionId,
|
|
401
|
+
measuredRatio: probe.ratio,
|
|
402
|
+
source: probe.source
|
|
403
|
+
});
|
|
393
404
|
// Slice 2026-09-13-auto-compact-trigger-ownership (T4): a settle means a
|
|
394
405
|
// dispatched compact demonstrably landed. Append an `observed` row
|
|
395
406
|
// carrying the measured ratio, so `peaks compact history` can show
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** The harness's own report of what caused a compaction. */
|
|
2
|
+
export type CompactTrigger = 'manual' | 'auto';
|
|
3
|
+
/**
|
|
4
|
+
* Which layer settled the run. `post-compact-hook` = the harness's event;
|
|
5
|
+
* `post-compact-probe` (written by the orchestrator) = a later probe's
|
|
6
|
+
* measurement. The two are deliberately distinct strings so a reader of
|
|
7
|
+
* `compact-history.jsonl` can tell a NOTIFICATION from an INFERENCE — which is
|
|
8
|
+
* the entire instrument this slice exists to build.
|
|
9
|
+
*/
|
|
10
|
+
export declare const COMPACT_EVENT_PATHWAY = "post-compact-hook";
|
|
11
|
+
/**
|
|
12
|
+
* One post-compact reading: the ratio, the window it was divided by, and the
|
|
13
|
+
* adapter that produced it — all from a SINGLE probe call, so the three cannot
|
|
14
|
+
* disagree with each other.
|
|
15
|
+
*/
|
|
16
|
+
export type PostCompactMeasurement = {
|
|
17
|
+
/** `null` = nothing could be measured. Never `0`, which means "unknown". */
|
|
18
|
+
readonly ratio: number | null;
|
|
19
|
+
readonly ide: string;
|
|
20
|
+
readonly windowTokens: number | null;
|
|
21
|
+
readonly windowSource: string | null;
|
|
22
|
+
};
|
|
23
|
+
export type CompactSettleResult = {
|
|
24
|
+
readonly settled: true;
|
|
25
|
+
readonly runId: string;
|
|
26
|
+
readonly beforeRatio: number;
|
|
27
|
+
/** The measured post-compact ratio, or `null` when none was trustworthy. */
|
|
28
|
+
readonly afterRatio: number | null;
|
|
29
|
+
/** Present only when the harness reported one. */
|
|
30
|
+
readonly trigger?: CompactTrigger;
|
|
31
|
+
/**
|
|
32
|
+
* False when the run settled but the `observed` row could not be
|
|
33
|
+
* appended. The settlement is the load-bearing half and is never undone
|
|
34
|
+
* for this, but the two outcomes are not the same fact.
|
|
35
|
+
*/
|
|
36
|
+
readonly historyWritten: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* False when the lifecycle record could not be written. The run is then
|
|
39
|
+
* still open, so a later probe will settle it from its own measurement —
|
|
40
|
+
* but the `observed` row is appended anyway, because the harness's
|
|
41
|
+
* statement is evidence independent of that write. The two failures are
|
|
42
|
+
* reported separately rather than collapsed: a caller that sees only one
|
|
43
|
+
* of them cannot say which half of the settlement landed.
|
|
44
|
+
*/
|
|
45
|
+
readonly lifecycleWritten: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
readonly settled: false;
|
|
48
|
+
/**
|
|
49
|
+
* `nothing-to-settle` — there was no open run. `different-session` — the
|
|
50
|
+
* payload named a harness session that is not this project's, so the open
|
|
51
|
+
* run was left alone. The second is a refusal of ATTRIBUTION, not an
|
|
52
|
+
* absence of it, and a reader told "nothing was open" would be told
|
|
53
|
+
* something false.
|
|
54
|
+
*/
|
|
55
|
+
readonly reason: 'nothing-to-settle' | 'different-session';
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Read `trigger` out of an already-parsed `PostCompact` payload.
|
|
59
|
+
*
|
|
60
|
+
* Anything that is not exactly `'manual'` or `'auto'` yields `undefined` — a
|
|
61
|
+
* missing field, a `null`, a number, another string, a payload that is not an
|
|
62
|
+
* object at all. This function never throws and never guesses (see this file's
|
|
63
|
+
* header, refusal 1).
|
|
64
|
+
*/
|
|
65
|
+
export declare function readTriggerFromHookPayload(payload: unknown): CompactTrigger | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Read the harness session id out of an already-parsed `PostCompact` payload.
|
|
68
|
+
*
|
|
69
|
+
* Same contract as `readTriggerFromHookPayload`: a missing field, an empty
|
|
70
|
+
* string, a number, a non-object payload — every one of those is `undefined`,
|
|
71
|
+
* and "absent" is an expected input rather than an error (see this file's
|
|
72
|
+
* header, refusal 4, for what the caller does with it and what it does with
|
|
73
|
+
* `undefined`).
|
|
74
|
+
*/
|
|
75
|
+
export declare function readSessionIdFromHookPayload(payload: unknown): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* The post-compact ruler: the same probe the orchestrator uses, so this row
|
|
78
|
+
* divides by the same denominator the dispatch did.
|
|
79
|
+
*
|
|
80
|
+
* `ratio` is `null` when nothing could be measured — a `conservative-fallback`
|
|
81
|
+
* probe reports `0`, but that `0` means "unknown", and recording it here would
|
|
82
|
+
* publish a real, infinitely-deep drop. The rest of the reading (adapter, the
|
|
83
|
+
* window the ratio divides by) is still carried, because knowing WHICH adapter
|
|
84
|
+
* could not measure is diagnostic.
|
|
85
|
+
*/
|
|
86
|
+
export declare function measurePostCompact(input: {
|
|
87
|
+
readonly projectRoot: string;
|
|
88
|
+
readonly sessionId: string;
|
|
89
|
+
readonly env: NodeJS.ProcessEnv;
|
|
90
|
+
}): PostCompactMeasurement;
|
|
91
|
+
/**
|
|
92
|
+
* Settle whatever compact run is open in `sessionId`, because the harness just
|
|
93
|
+
* reported one.
|
|
94
|
+
*
|
|
95
|
+
* `measure` is the ruler, injected rather than called directly for one concrete
|
|
96
|
+
* reason: the production ruler prefers `~/.claude/statusline-state.json`, a real
|
|
97
|
+
* file whose contents differ on every machine that runs the test suite. A test
|
|
98
|
+
* asserting this row's `afterRatio` against the developer's own statusline would
|
|
99
|
+
* pass or fail by accident. It is called at most once — the IDE tag and the
|
|
100
|
+
* window come from the same reading, so they cannot disagree with each other.
|
|
101
|
+
*
|
|
102
|
+
* Returns `{ settled: false }` when there is no open run — the honest answer,
|
|
103
|
+
* not an error — and when the payload names another harness session. The two
|
|
104
|
+
* say different things in `reason`, because they are different facts.
|
|
105
|
+
*/
|
|
106
|
+
export declare function settleCompactFromHarnessEvent(input: {
|
|
107
|
+
readonly projectRoot: string;
|
|
108
|
+
readonly sessionId: string;
|
|
109
|
+
readonly trigger?: CompactTrigger | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* The harness session id the payload named, when it named one (see
|
|
112
|
+
* `readSessionIdFromHookPayload`). Refused when it is present AND this
|
|
113
|
+
* project's own harness session id resolves AND the two differ. Absent, or
|
|
114
|
+
* unresolvable on this side, is accepted: a guard that cannot see what it is
|
|
115
|
+
* checking against must not become a hook that never settles anything.
|
|
116
|
+
*/
|
|
117
|
+
readonly hookSessionId?: string | undefined;
|
|
118
|
+
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
119
|
+
readonly measure?: (() => PostCompactMeasurement | null) | undefined;
|
|
120
|
+
/** Failure injection for the lifecycle write — the seam `CompactLifecyclePublisher` already takes. */
|
|
121
|
+
readonly failLifecycleWrite?: boolean | undefined;
|
|
122
|
+
}): CompactSettleResult;
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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
|
}
|