peaks-loop 4.0.48 → 4.0.50
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/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/compact-command.js +1 -3
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -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 +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- 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/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- 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 +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- package/skills/peaks-code/references/command-migration.md +0 -3
|
@@ -36,13 +36,14 @@
|
|
|
36
36
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
37
37
|
import { dirname, join } from 'node:path';
|
|
38
38
|
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
39
|
+
import { getSessionDir } from '../session/getSessionDir.js';
|
|
39
40
|
import { resolveOuterSessionId } from '../session/binding-status-service.js';
|
|
40
41
|
import { resolveCanonicalProjectRoot } from '../config/config-service.js';
|
|
41
42
|
import { describeHarnessWindowSync, harnessWindowSyncWarning } from '../context/harness-window-config.js';
|
|
42
43
|
import { AUTO_COMPACT_PRE_COMPACT_RATIO, DEPRECATED_ENVELOPE_FIELDS } from '../context/auto-compact-types.js';
|
|
43
44
|
import { describeMode, thresholdFor } from './auto-compact-modes.js';
|
|
44
45
|
import { resolveAutoCompactProfile } from '../mode/mode-status-service.js';
|
|
45
|
-
import { CompactLifecyclePublisher, fillEventSettledMeasurement, newCompactRunId, resolveDispatchedStage, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
|
|
46
|
+
import { CompactLifecyclePublisher, fillEventSettledMeasurement, newCompactRunId, readOpenDispatchRun, resolveDispatchedStage, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
|
|
46
47
|
const PRE_COMPACT_REASON = 'pre-compact-auto';
|
|
47
48
|
/**
|
|
48
49
|
* Map a context ratio to a `CompactTrigger` action. Pure; the side
|
|
@@ -195,7 +196,10 @@ export function buildConvergencePlan(input) {
|
|
|
195
196
|
* transcript).
|
|
196
197
|
*/
|
|
197
198
|
function appendAutoDecisionLog(input) {
|
|
198
|
-
|
|
199
|
+
// Repair R6: same hand-rolled join as `writePreCompactCheckpoint`. All four
|
|
200
|
+
// session-scoped paths in this file now go through the axis builder; none
|
|
201
|
+
// composes the join itself, so none can resolve outside the project root.
|
|
202
|
+
const dir = join(getSessionDir(input.projectRoot, input.sessionId), 'txt');
|
|
199
203
|
if (!existsSync(dir))
|
|
200
204
|
mkdirSync(dir, { recursive: true });
|
|
201
205
|
const logPath = join(dir, 'auto-decisions.md');
|
|
@@ -235,7 +239,7 @@ function appendAutoDecisionLog(input) {
|
|
|
235
239
|
* state re-injection. Not a capability peaks-loop has today.
|
|
236
240
|
*/
|
|
237
241
|
function writeMainSessionCompactIntent(input) {
|
|
238
|
-
const dir = join(input.projectRoot,
|
|
242
|
+
const dir = join(getSessionDir(input.projectRoot, input.sessionId), 'txt');
|
|
239
243
|
if (!existsSync(dir))
|
|
240
244
|
mkdirSync(dir, { recursive: true });
|
|
241
245
|
const path = join(dir, 'auto-compact-pending.json');
|
|
@@ -255,7 +259,12 @@ function writeMainSessionCompactIntent(input) {
|
|
|
255
259
|
* checkpoint` so D7's post-compact-detect picks it up unchanged.
|
|
256
260
|
*/
|
|
257
261
|
function writePreCompactCheckpoint(input) {
|
|
258
|
-
|
|
262
|
+
// Repair R6: this used to hand-roll the session join, which is the one shape
|
|
263
|
+
// `getSessionDir`'s own header records as NOT covered by the guard — an
|
|
264
|
+
// unsafe id resolved outside the project root and the checkpoint was written
|
|
265
|
+
// there. The gate above now refuses an unresolvable id before this point, but
|
|
266
|
+
// `--force` outranks that gate, so this join has to hold on its own.
|
|
267
|
+
const dir = join(getSessionDir(input.projectRoot, input.sessionId), 'checkpoints');
|
|
259
268
|
if (!existsSync(dir))
|
|
260
269
|
mkdirSync(dir, { recursive: true });
|
|
261
270
|
const prefix = input.redLine === true ? 'red-line-' : 'pre-compact-';
|
|
@@ -382,25 +391,37 @@ export async function runAutoCompact(input) {
|
|
|
382
391
|
// still open at `compacting`. Nothing is written when there is no
|
|
383
392
|
// open run, when the ratio is still high, or when the probe could
|
|
384
393
|
// not measure at all.
|
|
385
|
-
const
|
|
394
|
+
const settleRead = settleOpenLifecycleRun({
|
|
386
395
|
projectRoot: input.projectRoot,
|
|
387
396
|
sessionId,
|
|
388
397
|
measuredRatio: probe.ratio,
|
|
389
398
|
source: probe.source,
|
|
390
399
|
autoFireThreshold: thresholdFor(mode, 'autoFire'),
|
|
391
|
-
onLifecycleStage: input.onLifecycleStage
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
onLifecycleStage: input.onLifecycleStage,
|
|
401
|
+
failLifecycleWrite: input.testHooks?.failLifecycleWrite
|
|
402
|
+
});
|
|
403
|
+
// Repair R6 (AC3): a settle whose lifecycle write FAILED has not settled the
|
|
404
|
+
// run — the record is still resting at `armed` / `compacting`, so the next
|
|
405
|
+
// probe finds the same run open and settles it again. Appending the
|
|
406
|
+
// "observed compaction point" row anyway writes one row per probe for a
|
|
407
|
+
// compaction the lifecycle store never recorded: an unbounded append driven
|
|
408
|
+
// by a write that did not happen, and a history row whose paired lifecycle
|
|
409
|
+
// record does not exist. Nothing is lost by deferring the row — `probe.ratio`
|
|
410
|
+
// is re-measured on every probe, so the retry carries a fresh number.
|
|
411
|
+
const settled = settleRead !== null && !settleRead.lifecycleWritten
|
|
412
|
+
? null
|
|
413
|
+
: (settleRead ??
|
|
414
|
+
// Repair R1 (`2026-09-13-compact-event-settle`): the HARNESS event may
|
|
415
|
+
// already have closed this run WITHOUT an honest post-compact number —
|
|
416
|
+
// in which case the call above finds nothing open, and without this the
|
|
417
|
+
// calibration pair stays blank for exactly the compactions the event path
|
|
418
|
+
// exists to witness. Fills the number the event owed.
|
|
419
|
+
fillEventSettledMeasurement({
|
|
420
|
+
projectRoot: input.projectRoot,
|
|
421
|
+
sessionId,
|
|
422
|
+
measuredRatio: probe.ratio,
|
|
423
|
+
source: probe.source
|
|
424
|
+
}));
|
|
404
425
|
// Slice 2026-09-13-auto-compact-trigger-ownership (T4): a settle means a
|
|
405
426
|
// dispatched compact demonstrably landed. Append an `observed` row
|
|
406
427
|
// carrying the measured ratio, so `peaks compact history` can show
|
|
@@ -460,6 +481,85 @@ export async function runAutoCompact(input) {
|
|
|
460
481
|
}
|
|
461
482
|
const isRedLine = decision.reason === 'red-line';
|
|
462
483
|
const now = input.now ?? new Date();
|
|
484
|
+
// rid `2026-09-14-compact-dispatch-backoff`: ONE dispatch per compact
|
|
485
|
+
// attempt.
|
|
486
|
+
//
|
|
487
|
+
// The decision above fires whenever the ratio is at or over the auto-fire
|
|
488
|
+
// threshold, and the ratio does not come back down on its own — nothing in
|
|
489
|
+
// peaks-loop can compact a running session, and the harness fires only at
|
|
490
|
+
// its own red line. So "shouldCompact" was true on every probe, forever: one
|
|
491
|
+
// real session's `compact-history.jsonl` holds 1075 dispatch rows, 444
|
|
492
|
+
// checkpoints and ZERO compactions over 15.5 h — and the file was still
|
|
493
|
+
// growing while this slice ran. Re-dispatching bought nothing, because the
|
|
494
|
+
// `ide-native` dispatch only installs a PreToolUse hook and installing it
|
|
495
|
+
// again is a documented no-op: 1074 of those 1075 rows say `already
|
|
496
|
+
// installed` in their own `dispatchMessage`. It produced rows, not
|
|
497
|
+
// compactions.
|
|
498
|
+
//
|
|
499
|
+
// The lifecycle store already knows whether an attempt is outstanding (see
|
|
500
|
+
// `readOpenDispatchRun` — `armed`/`compacting` = dispatched, `completed`/
|
|
501
|
+
// `failed` = over), so the gate needs no new state and no threshold: it is
|
|
502
|
+
// keyed on the STAGE of the open run, never on a ratio, which is why it
|
|
503
|
+
// survives any future realignment of the 0.65/0.70/0.85/0.95 table.
|
|
504
|
+
//
|
|
505
|
+
// What is NOT suppressed: the probe still measures and still reports. The
|
|
506
|
+
// envelope carries the LIVE ratio plus the ratio the open ask was made at,
|
|
507
|
+
// so "it crossed and it is still high, unanswered" stays legible on every
|
|
508
|
+
// turn — the difference between a quiet signal and a silenced one.
|
|
509
|
+
//
|
|
510
|
+
// `force` (the `--force` test seam, "force compact at any ratio") outranks
|
|
511
|
+
// the inference: an explicit instruction must not be silently reduced to a
|
|
512
|
+
// no-op, which would make the published flag a lie.
|
|
513
|
+
// Repair R6: `readOpenDispatchRun` answers in THREE states, not two. `none`
|
|
514
|
+
// admits the dispatch; `unresolvable` means the backoff's question could not
|
|
515
|
+
// be asked — the session id names no session directory, so no lifecycle
|
|
516
|
+
// record can exist under it and the gate used to read that as "nothing is
|
|
517
|
+
// outstanding". Measured on ONE directory before this: a legal sid found the
|
|
518
|
+
// open `armed` run and suppressed the dispatch; `./<legal sid>`, which joins
|
|
519
|
+
// to the identical path, returned `null` and dispatched. A gate that reads a
|
|
520
|
+
// string instead of the artifact the string names is the defect the whole
|
|
521
|
+
// id axis exists to close, so an unanswerable question is NOT an admit: the
|
|
522
|
+
// conservative direction is to leave the dispatch undone and say why.
|
|
523
|
+
const openRun = readOpenDispatchRun({ projectRoot: input.projectRoot, sessionId });
|
|
524
|
+
if (openRun.kind === 'unresolvable' && input.force !== true) {
|
|
525
|
+
return {
|
|
526
|
+
ok: true,
|
|
527
|
+
code: 'AUTO_COMPACT_UNRESOLVED_SESSION',
|
|
528
|
+
message: `Context at ${(probe.ratio * 100).toFixed(1)}%; a compact may be warranted, but this session's ` +
|
|
529
|
+
`directory could not be resolved (${openRun.reason}), so whether a compact was already dispatched ` +
|
|
530
|
+
`for this crossing cannot be answered. Not dispatching: an unanswered question is not a 'no'. ` +
|
|
531
|
+
`The session id comes from the active binding or from the caller.`,
|
|
532
|
+
data: {
|
|
533
|
+
sessionId,
|
|
534
|
+
ratio: probe.ratio,
|
|
535
|
+
source: probe.source,
|
|
536
|
+
decision: 'unresolved-session',
|
|
537
|
+
harnessWindow
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (openRun.kind === 'open' && input.force !== true) {
|
|
542
|
+
return {
|
|
543
|
+
ok: true,
|
|
544
|
+
code: 'AUTO_COMPACT_ALREADY_ARMED',
|
|
545
|
+
message: `Context at ${(probe.ratio * 100).toFixed(1)}% — still above the ` +
|
|
546
|
+
`${(thresholdFor(mode, 'autoFire') * 100).toFixed(0)}% auto-fire threshold (mode=${mode}); ` +
|
|
547
|
+
`a compact was already dispatched for this crossing at ${(openRun.triggerRatio * 100).toFixed(1)}% ` +
|
|
548
|
+
`(run ${openRun.runId}, resting at '${openRun.stage}') and nothing has compacted since. ` +
|
|
549
|
+
`Not dispatching again: the trigger is already registered, so a second dispatch would install the ` +
|
|
550
|
+
`same hook and add a checkpoint and a history row without adding a capability. ` +
|
|
551
|
+
`Re-probe with \`peaks code context-now\`.`,
|
|
552
|
+
data: {
|
|
553
|
+
sessionId,
|
|
554
|
+
ratio: probe.ratio,
|
|
555
|
+
source: probe.source,
|
|
556
|
+
decision: 'already-armed',
|
|
557
|
+
armedAtRatio: openRun.triggerRatio,
|
|
558
|
+
armedRunId: openRun.runId,
|
|
559
|
+
harnessWindow
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
463
563
|
// Slice 2026-08-01-compact-lifecycle (Task 5): the decision has now
|
|
464
564
|
// committed to compacting, so the run is `queued`. One runId per
|
|
465
565
|
// attempt; every later transition carries it forward.
|
|
@@ -665,7 +765,7 @@ export async function runAutoCompact(input) {
|
|
|
665
765
|
};
|
|
666
766
|
}
|
|
667
767
|
export function appendCompactHistoryEvent(input) {
|
|
668
|
-
const dir =
|
|
768
|
+
const dir = getSessionDir(input.projectRoot, input.sessionId);
|
|
669
769
|
if (!existsSync(dir))
|
|
670
770
|
mkdirSync(dir, { recursive: true });
|
|
671
771
|
const path = join(dir, 'compact-history.jsonl');
|
|
@@ -29,18 +29,30 @@ export type CompactSettleResult = {
|
|
|
29
29
|
/** Present only when the harness reported one. */
|
|
30
30
|
readonly trigger?: CompactTrigger;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Whether the `observed` row for this event is on disk. `false` means
|
|
33
|
+
* there is none, for one of two DIFFERENT reasons, told apart by
|
|
34
|
+
* `lifecycleWritten`:
|
|
35
|
+
*
|
|
36
|
+
* - `lifecycleWritten: true` — the append was attempted and threw.
|
|
37
|
+
* - `lifecycleWritten: false` — the append was deliberately not made,
|
|
38
|
+
* because the settle it would describe is one the store never
|
|
39
|
+
* recorded (see `lifecycleWritten`).
|
|
35
40
|
*/
|
|
36
41
|
readonly historyWritten: boolean;
|
|
37
42
|
/**
|
|
38
43
|
* False when the lifecycle record could not be written. The run is then
|
|
39
|
-
* still open,
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
+
* still open, and this path does NOT append the `observed` row: the row
|
|
45
|
+
* would assert a settled measurement for a run the store still holds at
|
|
46
|
+
* `armed` / `compacting`, and each arrival would add another one. The
|
|
47
|
+
* row is deferred rather than lost — the run stays open, so the retry
|
|
48
|
+
* that does land owns the row and measures the ratio at that moment.
|
|
49
|
+
* This is repair R6's answer on the probe path
|
|
50
|
+
* (`auto-compact-orchestrator.ts`), applied here so the two settle
|
|
51
|
+
* paths agree.
|
|
52
|
+
*
|
|
53
|
+
* The two halves are reported separately rather than collapsed: a caller
|
|
54
|
+
* that sees only one of them cannot say which half of the settlement
|
|
55
|
+
* landed.
|
|
44
56
|
*/
|
|
45
57
|
readonly lifecycleWritten: boolean;
|
|
46
58
|
} | {
|
|
@@ -161,6 +161,27 @@ export function settleCompactFromHarnessEvent(input) {
|
|
|
161
161
|
if (settled === null) {
|
|
162
162
|
return { settled: false, reason: 'nothing-to-settle' };
|
|
163
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
|
+
}
|
|
164
185
|
const event = {
|
|
165
186
|
schemaVersion: 1,
|
|
166
187
|
kind: 'observed',
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { getSkillPresence } from '../skills/skill-presence-service.js';
|
|
27
|
+
import { isExpectedFsMiss } from '../../shared/fs-utils.js';
|
|
27
28
|
import { emitObservabilityEvent } from '../observability/observability-service.js';
|
|
28
29
|
import { isCodeMode } from './mode-gate.js';
|
|
29
30
|
export const POST_COMPACT_RESUME_REASONS = [
|
|
@@ -65,12 +66,19 @@ function safeReadCheckpoint(absPath) {
|
|
|
65
66
|
const content = mutable;
|
|
66
67
|
return { path: absPath, mtime: stat.mtime, content };
|
|
67
68
|
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
catch (err) {
|
|
70
|
+
// P1 site (S6, 2026-09-15). The TODO(g2) marker this replaces said the
|
|
71
|
+
// catch "narrows to IO errors only" — it did not. It rethrew the two JS
|
|
72
|
+
// error classes it happened to name and swallowed everything else,
|
|
73
|
+
// including the ReferenceError an ESM `require()` bug produces on the one
|
|
74
|
+
// platform this file's whole defence exists for. Corrupt JSON stays loud
|
|
75
|
+
// (we cannot read a checkpoint we cannot parse); every non-fs-miss error
|
|
76
|
+
// now propagates. See `isExpectedFsMiss`.
|
|
71
77
|
if (err instanceof SyntaxError)
|
|
72
|
-
throw err;
|
|
73
|
-
|
|
78
|
+
throw err;
|
|
79
|
+
if (!isExpectedFsMiss(err))
|
|
80
|
+
throw err;
|
|
81
|
+
return null;
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
function readLatestTodayCheckpoint(runtimeDir, now) {
|
|
@@ -227,12 +235,13 @@ function readActiveSkillName(projectRoot) {
|
|
|
227
235
|
const presence = getSkillPresence(projectRoot);
|
|
228
236
|
return presence?.skill;
|
|
229
237
|
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
catch (err) {
|
|
239
|
+
// P1 site (S6, 2026-09-15) — same correction as `safeReadCheckpoint`
|
|
240
|
+
// above: a non-fs-miss failure reading the active skill is a bug, not an
|
|
241
|
+
// absence, and no longer resolves to `undefined` (i.e. to "no skill").
|
|
242
|
+
if (!isExpectedFsMiss(err))
|
|
243
|
+
throw err;
|
|
244
|
+
return undefined;
|
|
236
245
|
}
|
|
237
246
|
}
|
|
238
247
|
/**
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { existsSync, readFileSync } from 'node:fs';
|
|
36
36
|
import { join } from 'node:path';
|
|
37
37
|
import { readJobShapeDecision, JobShapeDecisionError, JOB_SHAPE_NOT_DECIDED } from './job-shape-decision.js';
|
|
38
|
+
import { isExpectedFsMiss } from '../../shared/fs-utils.js';
|
|
38
39
|
export const STEP_08_GATE_FILE_NAME = 'job-shape.json';
|
|
39
40
|
export const STEP_08_PROGRESS_FILE_NAME = 'progress.json';
|
|
40
41
|
/**
|
|
@@ -85,12 +86,16 @@ function readProgressIfAny(projectRoot, sessionId, jid) {
|
|
|
85
86
|
}
|
|
86
87
|
return null;
|
|
87
88
|
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
catch (err) {
|
|
90
|
+
// P1 site (S6, 2026-09-15) — same correction as `readPromptFromLastPromptFile`
|
|
91
|
+
// below: corrupt progress.json stays loud, and a failure that is not an
|
|
92
|
+
// fs miss (module-load ReferenceError, TypeError from a bad field, …)
|
|
93
|
+
// propagates instead of reading as "no progress yet".
|
|
91
94
|
if (err instanceof SyntaxError)
|
|
92
|
-
throw err;
|
|
93
|
-
|
|
95
|
+
throw err;
|
|
96
|
+
if (!isExpectedFsMiss(err))
|
|
97
|
+
throw err;
|
|
98
|
+
return null;
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
function readPromptFromLastPromptFile(projectRoot, sessionId) {
|
|
@@ -100,7 +105,17 @@ function readPromptFromLastPromptFile(projectRoot, sessionId) {
|
|
|
100
105
|
try {
|
|
101
106
|
return readFileSync(path, 'utf8');
|
|
102
107
|
}
|
|
103
|
-
catch {
|
|
108
|
+
catch (err) {
|
|
109
|
+
// P1 site (S6, 2026-09-15). This catch was bare — it bound nothing and
|
|
110
|
+
// swallowed every error class. The sibling reads in this file had been
|
|
111
|
+
// narrowed; this one had not, so the ONE path that feeds the step-08
|
|
112
|
+
// backup regex was also the one that could not tell "no prompt file" from
|
|
113
|
+
// "the module is broken". On the platform this gate exists for, that is
|
|
114
|
+
// the difference between blocking a disavowal prompt and silently
|
|
115
|
+
// allowing it. Missing/unreadable text is an empty prompt; anything else
|
|
116
|
+
// propagates.
|
|
117
|
+
if (!isExpectedFsMiss(err))
|
|
118
|
+
throw err;
|
|
104
119
|
return '';
|
|
105
120
|
}
|
|
106
121
|
}
|
|
@@ -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
|
/**
|
|
@@ -173,15 +173,17 @@ export function resolveCanonicalProjectRootStrict(startPath) {
|
|
|
173
173
|
catch {
|
|
174
174
|
throw new InvalidProjectRootError('non-existent', startPath);
|
|
175
175
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
// Use the realpath form as the canonical input. macOS exposes
|
|
177
|
+
// `/var/folders/...` as a symlink to `/private/var/folders/...`, so any
|
|
178
|
+
// path returned by `os.tmpdir()` (and therefore any `mkdtempSync(...)`
|
|
179
|
+
// result) has `realStart !== start`. Rejecting that case as
|
|
180
|
+
// "non-canonical" would block the legitimate SessionStart hook path
|
|
181
|
+
// and the test fixtures under `tests/unit/hooks/`. The security
|
|
182
|
+
// property is preserved: `realpathSync` collapses any attacker-
|
|
183
|
+
// controlled symlink chain, so `realStart` IS the canonical form
|
|
184
|
+
// regardless of which prefix the caller used.
|
|
185
|
+
const canonical = resolveCanonicalProjectRoot(realStart);
|
|
186
|
+
if (canonical === realStart) {
|
|
185
187
|
return canonical;
|
|
186
188
|
}
|
|
187
189
|
// Canonicalization moved the path (e.g. to a git root) — accept it
|
|
@@ -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
|