peaks-loop 4.0.7 → 4.0.9

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.
Files changed (156) hide show
  1. package/CHANGELOG.md +58 -288
  2. package/dist/cli/commands/_register.js +2 -0
  3. package/dist/cli/commands/baseline-commands.d.ts +3 -0
  4. package/dist/cli/commands/baseline-commands.js +146 -0
  5. package/dist/cli/commands/code-job-shape-commands.js +8 -11
  6. package/dist/cli/commands/code-mode-gate-commands.js +1 -43
  7. package/dist/cli/commands/code-runtime-commands.js +36 -14
  8. package/dist/cli/commands/core/skill-command.js +75 -13
  9. package/dist/cli/commands/core/standards-command.js +5 -50
  10. package/dist/cli/commands/dispatch-commands.d.ts +20 -0
  11. package/dist/cli/commands/dispatch-commands.js +39 -38
  12. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  13. package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
  14. package/dist/cli/commands/heartbeat-commands.js +40 -0
  15. package/dist/cli/commands/hook-handle.js +5 -2
  16. package/dist/cli/commands/hooks-commands.js +9 -26
  17. package/dist/cli/commands/loop-eval-commands.js +5 -2
  18. package/dist/cli/commands/statusline-commands.js +1 -1
  19. package/dist/cli/commands/sub-agent-commands.js +0 -2
  20. package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
  21. package/dist/cli/commands/workflow-commands.js +8 -0
  22. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
  23. package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
  24. package/dist/cli/commands/workspace/init-command.js +19 -0
  25. package/dist/services/audit/backing-detector.d.ts +2 -19
  26. package/dist/services/audit/backing-detector.js +7 -66
  27. package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
  28. package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
  29. package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
  30. package/dist/services/capability-audit-service/cross-check.d.ts +8 -0
  31. package/dist/services/capability-audit-service/cross-check.js +11 -0
  32. package/dist/services/capability-audit-service/index.d.ts +3 -0
  33. package/dist/services/capability-audit-service/index.js +2 -0
  34. package/dist/services/capability-audit-service/runner.d.ts +27 -0
  35. package/dist/services/capability-audit-service/runner.js +40 -0
  36. package/dist/services/capability-audit-service/staleness.d.ts +1 -0
  37. package/dist/services/capability-audit-service/staleness.js +7 -0
  38. package/dist/services/capability-audit-service/types.d.ts +24 -0
  39. package/dist/services/capability-audit-service/types.js +1 -0
  40. package/dist/services/capability-baseline/store.d.ts +33 -0
  41. package/dist/services/capability-baseline/store.js +80 -0
  42. package/dist/services/capability-baseline/types.d.ts +45 -0
  43. package/dist/services/capability-baseline/types.js +5 -0
  44. package/dist/services/capability-baseline/validator.d.ts +19 -0
  45. package/dist/services/capability-baseline/validator.js +38 -0
  46. package/dist/services/capability-guard-runner/contracts/J01.d.ts +2 -0
  47. package/dist/services/capability-guard-runner/contracts/J01.js +48 -0
  48. package/dist/services/capability-guard-runner/contracts/J02.d.ts +2 -0
  49. package/dist/services/capability-guard-runner/contracts/J02.js +34 -0
  50. package/dist/services/capability-guard-runner/contracts/J03.d.ts +2 -0
  51. package/dist/services/capability-guard-runner/contracts/J03.js +24 -0
  52. package/dist/services/capability-guard-runner/contracts/J04.d.ts +2 -0
  53. package/dist/services/capability-guard-runner/contracts/J04.js +40 -0
  54. package/dist/services/capability-guard-runner/contracts/J05.d.ts +2 -0
  55. package/dist/services/capability-guard-runner/contracts/J05.js +21 -0
  56. package/dist/services/capability-guard-runner/contracts/J06.d.ts +2 -0
  57. package/dist/services/capability-guard-runner/contracts/J06.js +43 -0
  58. package/dist/services/capability-guard-runner/contracts/J07.d.ts +2 -0
  59. package/dist/services/capability-guard-runner/contracts/J07.js +53 -0
  60. package/dist/services/capability-guard-runner/contracts/J08.d.ts +2 -0
  61. package/dist/services/capability-guard-runner/contracts/J08.js +43 -0
  62. package/dist/services/capability-guard-runner/contracts/J09.d.ts +2 -0
  63. package/dist/services/capability-guard-runner/contracts/J09.js +44 -0
  64. package/dist/services/capability-guard-runner/contracts/J10.d.ts +2 -0
  65. package/dist/services/capability-guard-runner/contracts/J10.js +40 -0
  66. package/dist/services/capability-guard-runner/contracts/J11.d.ts +2 -0
  67. package/dist/services/capability-guard-runner/contracts/J11.js +39 -0
  68. package/dist/services/capability-guard-runner/contracts/J12.d.ts +2 -0
  69. package/dist/services/capability-guard-runner/contracts/J12.js +34 -0
  70. package/dist/services/capability-guard-runner/contracts/J13.d.ts +2 -0
  71. package/dist/services/capability-guard-runner/contracts/J13.js +45 -0
  72. package/dist/services/capability-guard-runner/contracts/J14.d.ts +2 -0
  73. package/dist/services/capability-guard-runner/contracts/J14.js +36 -0
  74. package/dist/services/capability-guard-runner/contracts/J15.d.ts +2 -0
  75. package/dist/services/capability-guard-runner/contracts/J15.js +40 -0
  76. package/dist/services/capability-guard-runner/diff.d.ts +2 -0
  77. package/dist/services/capability-guard-runner/diff.js +7 -0
  78. package/dist/services/capability-guard-runner/runner.d.ts +9 -0
  79. package/dist/services/capability-guard-runner/runner.js +22 -0
  80. package/dist/services/capability-guard-runner/types.d.ts +39 -0
  81. package/dist/services/capability-guard-runner/types.js +1 -0
  82. package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
  83. package/dist/services/code/auto-compact-orchestrator.js +29 -9
  84. package/dist/services/context/build-dispatch-system-prompt.js +0 -18
  85. package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
  86. package/dist/services/dispatch/dispatch-record-writer.js +85 -15
  87. package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
  88. package/dist/services/dispatch/test-tool-detection.js +0 -22
  89. package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
  90. package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
  91. package/dist/services/evolution/evolution-types.d.ts +16 -16
  92. package/dist/services/final-review/final-review-service.d.ts +8 -0
  93. package/dist/services/final-review/final-review-service.js +14 -0
  94. package/dist/services/final-review/final-review-types.d.ts +1 -1
  95. package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
  96. package/dist/services/hooks/presence-marker-detector.js +48 -0
  97. package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
  98. package/dist/services/ide/adapters/codex-adapter.js +20 -1
  99. package/dist/services/ide/adapters/cursor-adapter.js +21 -1
  100. package/dist/services/ide/adapters/hermes-adapter.js +20 -1
  101. package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
  102. package/dist/services/ide/adapters/qoder-adapter.js +20 -1
  103. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
  104. package/dist/services/ide/adapters/trae-adapter.js +21 -1
  105. package/dist/services/ide/adapters/zcode-adapter.js +19 -0
  106. package/dist/services/ide/ide-types.d.ts +15 -0
  107. package/dist/services/job/job-progress-store.d.ts +2 -2
  108. package/dist/services/job/job-types.d.ts +26 -26
  109. package/dist/services/observability/observability-service.d.ts +4 -4
  110. package/dist/services/scan/archetype-service.js +2 -52
  111. package/dist/services/sediment/json-schema.d.ts +4 -4
  112. package/dist/services/session/caller-binding-service.d.ts +21 -0
  113. package/dist/services/session/caller-binding-service.js +35 -0
  114. package/dist/services/session/caller-id-types.d.ts +62 -13
  115. package/dist/services/session/caller-id-types.js +25 -13
  116. package/dist/services/session/index.d.ts +4 -2
  117. package/dist/services/session/index.js +8 -2
  118. package/dist/services/session/platform-fallbacks.d.ts +25 -17
  119. package/dist/services/session/platform-fallbacks.js +26 -28
  120. package/dist/services/session/resolve-caller-id.d.ts +64 -29
  121. package/dist/services/session/resolve-caller-id.js +122 -48
  122. package/dist/services/session/session-manager.d.ts +0 -29
  123. package/dist/services/session/session-manager.js +0 -33
  124. package/dist/services/session/session-resume-service.d.ts +4 -0
  125. package/dist/services/session/session-resume-service.js +4 -0
  126. package/dist/services/skills/hooks-settings-service.d.ts +0 -41
  127. package/dist/services/skills/hooks-settings-service.js +5 -61
  128. package/dist/services/skills/presence-lease-service.d.ts +73 -0
  129. package/dist/services/skills/presence-lease-service.js +358 -0
  130. package/dist/services/skills/presence-lease-types.d.ts +75 -0
  131. package/dist/services/skills/presence-lease-types.js +16 -0
  132. package/dist/services/skills/skill-presence-service.js +86 -22
  133. package/dist/services/skills/skill-statusline-renderer.d.ts +46 -56
  134. package/dist/services/skills/skill-statusline-renderer.js +312 -100
  135. package/dist/services/slice/slice-check-service.js +0 -59
  136. package/dist/services/standards/project-standards-service.js +0 -44
  137. package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
  138. package/dist/services/workflow/workflow-graph-store.js +278 -0
  139. package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
  140. package/dist/services/workflow/workflow-graph-types.js +67 -0
  141. package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
  142. package/dist/services/workflow/workflow-inflight-probe.js +86 -0
  143. package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
  144. package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
  145. package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
  146. package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
  147. package/dist/services/workspace/reconcile-service.d.ts +31 -0
  148. package/dist/services/workspace/reconcile-service.js +132 -1
  149. package/package.json +5 -4
  150. package/skills/peaks-code/SKILL.md +5 -43
  151. package/skills/peaks-code/references/completion-handoff.md +1 -1
  152. package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
  153. package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
  154. package/dist/cli/commands/sub-agent-exec-command.js +0 -132
  155. package/dist/services/standards/find-skills-integration.d.ts +0 -59
  156. package/dist/services/standards/find-skills-integration.js +0 -225
@@ -0,0 +1,22 @@
1
+ function assertBaselineRef(contract) {
2
+ if (!contract.source.baselineRow || !contract.source.invariant) {
3
+ throw new Error('GUARD_CONTRACT_MISSING_BASELINE_REF: contract must reference a baseline row and invariant');
4
+ }
5
+ }
6
+ export async function runGuard(contract, ctx) {
7
+ assertBaselineRef(contract);
8
+ return contract.execute(ctx);
9
+ }
10
+ export async function runAllGuards(contracts, ctx) {
11
+ const results = [];
12
+ for (const c of contracts) {
13
+ results.push(await runGuard(c, ctx));
14
+ }
15
+ return {
16
+ pass: results.filter((r) => r.status === 'pass').length,
17
+ fail: results.filter((r) => r.status === 'fail').length,
18
+ skipped: results.filter((r) => r.status === 'skipped').length,
19
+ total: results.length,
20
+ results
21
+ };
22
+ }
@@ -0,0 +1,39 @@
1
+ import type { JourneyId } from '../capability-baseline/types.js';
2
+ export type ContractKind = 'cli-envelope' | 'workflow-trace' | 'hook-assertion' | 'cli-output-golden' | 'asset-roundtrip' | 'concurrency-lease' | 'sop-register' | 'spec-coverage' | 'envelope-arg-shapes';
3
+ export interface GuardContract {
4
+ readonly journeyId: JourneyId;
5
+ readonly kind: ContractKind;
6
+ readonly source: {
7
+ readonly baselineRow: JourneyId;
8
+ readonly invariant: string;
9
+ };
10
+ readonly execute: (ctx: GuardContext) => Promise<GuardRunResult>;
11
+ readonly evidence: {
12
+ readonly kind: ContractKind;
13
+ readonly artifact: string;
14
+ };
15
+ }
16
+ export interface GuardContext {
17
+ readonly projectRoot: string;
18
+ readonly sessionId: string;
19
+ readonly contract: GuardContract;
20
+ readonly baselineInvariant: string;
21
+ }
22
+ export type GuardStatus = 'pass' | 'fail' | 'skipped';
23
+ export interface GuardDiff {
24
+ readonly before: string;
25
+ readonly after: string;
26
+ readonly reason: string;
27
+ }
28
+ export interface GuardRunResult {
29
+ readonly journeyId: JourneyId;
30
+ readonly contract: ContractKind;
31
+ readonly status: GuardStatus;
32
+ readonly diff?: GuardDiff;
33
+ readonly artifactPath: string;
34
+ }
35
+ export type GuardErrorCode = 'GUARD_CONTRACT_MISSING_BASELINE_REF' | 'GUARD_DIFF_DETECTED' | 'GUARD_TEST_FLAKY';
36
+ export interface GuardError {
37
+ readonly code: GuardErrorCode;
38
+ readonly message: string;
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -5,8 +5,26 @@ import { type CompactLifecycleRecord, type CompactLifecycleStage } from '../comp
5
5
  export interface AutoCompactInput {
6
6
  /** Project root for context (default cwd). */
7
7
  readonly projectRoot: string;
8
- /** Caller-provided in-flight batch probe (default false). */
8
+ /**
9
+ * Caller-provided in-flight batch probe (default false). Slice
10
+ * 4.0.8 (D4d): production callers should pass a `probeInflightBatch`
11
+ * function (see below). The legacy `inFlightBatch` boolean
12
+ * remains as a TEST-ONLY seam — the CLI gates it behind
13
+ * `process.env.PEAKS_TEST_SEAM === '1'` so a production run
14
+ * never consults the boolean as truth.
15
+ */
9
16
  readonly inFlightBatch?: InFlightBatchProbe | undefined;
17
+ /**
18
+ * Slice 4.0.8 (D4d): graph-only in-flight probe. When supplied,
19
+ * the orchestrator calls this function and uses the boolean
20
+ * result as the production `inFlightBatch` signal. The legacy
21
+ * boolean parameter is suppressed in production callers — the
22
+ * CLI's `--in-flight-batch` flag is gated behind
23
+ * `PEAKS_TEST_SEAM === '1'`. The pure decision function
24
+ * `evaluateAutoCompactDecision` retains the boolean signature
25
+ * for the test seam + the red-line override.
26
+ */
27
+ readonly probeInflightBatch?: (() => boolean) | undefined;
10
28
  /**
11
29
  * Force execute even when ratio < threshold (test seam). In
12
30
  * production this is always `false` — peaks-loop drives compact
@@ -96,7 +114,20 @@ export declare function evaluateCompactTrigger(ratio: number, mode?: AutoCompact
96
114
  */
97
115
  export declare function evaluateAutoCompactDecision(input: {
98
116
  ratio: number;
99
- inFlightBatch?: InFlightBatchProbe | undefined;
117
+ /**
118
+ * Caller-provided in-flight batch signal. Accepts either a plain
119
+ * boolean (convenience / repro seam) or the full `InFlightBatchProbe`
120
+ * shape (production callers — graph-probe-backed). When the boolean
121
+ * form is `true`, the normalized probe carries
122
+ * `hasInFlightBatch: true`. Slice 4.0.8 hotfix.
123
+ */
124
+ inFlightBatch?: boolean | InFlightBatchProbe | undefined;
125
+ /**
126
+ * Legacy camelCase alias for `inFlightBatch`. Production repro
127
+ * inputs use `inflightBatch: true|false`; the orchestrator
128
+ * normalizes it to the same internal shape. Slice 4.0.8 hotfix.
129
+ */
130
+ inflightBatch?: boolean | InFlightBatchProbe | undefined;
100
131
  force?: boolean | undefined;
101
132
  bypassRedLine?: boolean | undefined;
102
133
  mode?: AutoCompactMode | undefined;
@@ -111,6 +142,8 @@ export declare function evaluateAutoCompactDecision(input: {
111
142
  shouldCompact: boolean;
112
143
  reason: 'below-threshold' | 'in-flight-batch' | 'pre-compact' | 'red-line';
113
144
  trigger: CompactTrigger;
145
+ /** Typed verdict enum (RD §4 24h-mode contract). Slice 4.0.8 hotfix. */
146
+ action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' | 'defer';
114
147
  };
115
148
  /**
116
149
  * Build the convergence plan that D7's post-compact-detect will read
@@ -108,19 +108,29 @@ export function evaluateCompactTrigger(ratio, mode = 'standard') {
108
108
  */
109
109
  export function evaluateAutoCompactDecision(input) {
110
110
  const trigger = evaluateCompactTrigger(input.ratio, input.mode ?? 'standard');
111
- if (trigger.kind === 'none' || trigger.kind === 'soft-warn') {
112
- return { shouldCompact: false, reason: 'below-threshold', trigger };
111
+ // Normalize both `inFlightBatch` and `inflightBatch` (boolean | probe)
112
+ // into a single `InFlightBatchProbe` shape. The probe reads
113
+ // `hasInFlightBatch`; the boolean reads truthiness.
114
+ const rawProbe = input.inFlightBatch ?? input.inflightBatch;
115
+ const probe = typeof rawProbe === 'boolean'
116
+ ? { hasInFlightBatch: rawProbe }
117
+ : rawProbe;
118
+ if (trigger.kind === 'none') {
119
+ return { shouldCompact: false, reason: 'below-threshold', trigger, action: 'ok' };
120
+ }
121
+ if (trigger.kind === 'soft-warn') {
122
+ return { shouldCompact: false, reason: 'below-threshold', trigger, action: 'soft-warn' };
113
123
  }
114
124
  if (trigger.kind === 'red-line') {
115
125
  // Red line: ignore in-flight batch — synchronous dispatch wins.
116
- return { shouldCompact: true, reason: 'red-line', trigger };
126
+ return { shouldCompact: true, reason: 'red-line', trigger, action: 'red-line' };
117
127
  }
118
128
  // pre-compact zone (0.85 ≤ ratio < 0.95): honor D6.e in-flight deferral.
119
- if (input.inFlightBatch?.hasInFlightBatch === true) {
120
- return { shouldCompact: false, reason: 'in-flight-batch', trigger };
129
+ if (probe?.hasInFlightBatch === true) {
130
+ return { shouldCompact: false, reason: 'in-flight-batch', trigger, action: 'defer' };
121
131
  }
122
132
  if (input.force) {
123
- return { shouldCompact: true, reason: 'pre-compact', trigger };
133
+ return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
124
134
  }
125
135
  // Slice 2026-07-31-rid-mac-transcript-estimate-trigger: transcript-estimate
126
136
  // is the ONLY signal available on Mac Claude Code (no env-var, no statusline
@@ -131,9 +141,9 @@ export function evaluateAutoCompactDecision(input) {
131
141
  // source is present (`claude-code-env` would have been P1, `statusline-poll`
132
142
  // P2, `user-overridden` P4) — Mac's only signal is `transcript-estimate`.
133
143
  if (input.source === 'transcript-estimate' && input.ratio >= AUTO_COMPACT_PRE_COMPACT_RATIO)
134
- return { shouldCompact: true, reason: 'pre-compact', trigger };
144
+ return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
135
145
  // Default: peaks-loop drives pre-compact autonomously.
136
- return { shouldCompact: true, reason: 'pre-compact', trigger };
146
+ return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
137
147
  }
138
148
  /**
139
149
  * Build the convergence plan that D7's post-compact-detect will read
@@ -514,7 +524,17 @@ export async function runAutoCompact(input) {
514
524
  });
515
525
  const decision = evaluateAutoCompactDecision({
516
526
  ratio: probe.ratio,
517
- inFlightBatch: input.inFlightBatch,
527
+ // Slice 4.0.8 (D4d): production `inFlightBatch` MUST come from
528
+ // the graph probe (see `workflow-inflight-probe.ts`). The
529
+ // legacy boolean is preserved as a TEST-ONLY seam: when
530
+ // `probeInflightBatch` is supplied, we call it and pass the
531
+ // result as a `InFlightBatchProbe` so the pure decision function
532
+ // sees a graph-backed value. When only the boolean is
533
+ // supplied, we treat it as the test seam (the CLI gates it
534
+ // behind `PEAKS_TEST_SEAM === '1'`).
535
+ inFlightBatch: input.probeInflightBatch !== undefined
536
+ ? { hasInFlightBatch: input.probeInflightBatch() }
537
+ : input.inFlightBatch,
518
538
  force: input.force,
519
539
  bypassRedLine: input.bypassRedLine,
520
540
  mode,
@@ -123,24 +123,6 @@ function renderContextBlock(probe) {
123
123
  if (probe !== null && probe !== undefined) {
124
124
  const usedPct = (probe.ratio * 100).toFixed(1);
125
125
  const freePct = ((1 - probe.ratio) * 100).toFixed(1);
126
- // Slice 4.0.7-dogfood-PR-11 (ice-cola surface probe 2026-08-02):
127
- // when the source is conservative-fallback, the ratio is
128
- // hard-coded to 0 (no signal available). The pre-rid prompt
129
- // rendered "0.0% used (100.0% free)" without flagging that
130
- // this is a fallback, so the sub-agent thought it had 100%
131
- // free and never re-checked. The fix short-circuits to the
132
- // "no probe available" message when the source is not one of
133
- // the trusted values emitted by peaks-loop's own readers.
134
- const KNOWN_SOURCES = new Set(['token-counted', 'transcript-estimate', 'statusline']);
135
- if (!KNOWN_SOURCES.has(probe.source)) {
136
- return `## Context window (no probe available)
137
-
138
- The orchestrator captured a probe with source: \`${probe.source}\` (ratio ${usedPct}%) but the dispatch layer does not trust this source. The pre-rid dispatch prompt rendered this as "0.0% used (100.0% free)" without flagging the fallback, which caused sub-agents to think they had 100% free and never re-check. **Treat the displayed ratio as unverified.**
139
-
140
- If you need to evaluate context pressure, run \`peaks code context-now --project <root>\` and trust its \`ratio\` field. Do not estimate from message length.
141
-
142
- `;
143
- }
144
126
  const action = probe.ratio >= 0.95
145
127
  ? 'RED-LINE — call `peaks compact auto --execute` immediately.'
146
128
  : probe.ratio >= 0.85
@@ -43,7 +43,7 @@ export interface DispatchRecord {
43
43
  * truth for "this record is v3.1-form"; readers check
44
44
  * `version === '3.1'` for forward-compatible dispatching.
45
45
  */
46
- readonly version: '3.2';
46
+ readonly version: '4.0.0';
47
47
  readonly createdAt: string;
48
48
  readonly completedAt: string | null;
49
49
  readonly outcome: DispatchOutcome;
@@ -120,6 +120,22 @@ export interface DispatchRecord {
120
120
  * merge transcript.
121
121
  */
122
122
  readonly mergeBackAttempts: number;
123
+ /**
124
+ * Slice 4.0.8 RD §4 D4c (presence-lease-graph): the workflow id +
125
+ * graph node id + graphRef this dispatch is bound to. Persisted
126
+ * directly in the dispatch record (NOT in a sidecar) so the
127
+ * envelope-writer `markCompleted` can auto-transition the bound
128
+ * graph node to `envelope-received` with `ackStatus=pending` in
129
+ * one protected update.
130
+ *
131
+ * The schema bump from `3.2 → 4.0.0` is BREAKING in the sense
132
+ * that the literal type is narrowed. The optional `?` keeps back-
133
+ * compat for old records that pre-date the binding (the
134
+ * `upgradeRecord` reader defaults them to `null`).
135
+ */
136
+ readonly workflowId: string | null;
137
+ readonly graphNodeId: string | null;
138
+ readonly graphRef: string | null;
123
139
  }
124
140
  /** Input for the initial write. */
125
141
  export type WriteInitialDispatchInput = {
@@ -148,6 +164,14 @@ export type WriteInitialDispatchInput = {
148
164
  * time when `--isolation` is requested.
149
165
  */
150
166
  isolationStartedAt?: string | null;
167
+ /**
168
+ * Slice 4.0.8: workflow graph binding for the dispatch. Defaults
169
+ * to `null` so a non-graph dispatch (legacy CLI flow, ad-hoc
170
+ * dispatch) still writes a v4.0.0 record.
171
+ */
172
+ workflowId?: string | null;
173
+ graphNodeId?: string | null;
174
+ graphRef?: string | null;
151
175
  };
152
176
  /** Heartbeat write input. */
153
177
  export type AppendHeartbeatInput = {
@@ -44,7 +44,7 @@ export function writeInitialDispatchRecord(input) {
44
44
  const path = dispatchRecordPath(projectRoot, sessionId, requestId, now());
45
45
  const safePath = assertSafeDispatchRecordPath(path, projectRoot);
46
46
  const record = {
47
- version: '3.2',
47
+ version: '4.0.0',
48
48
  createdAt: now().toISOString(),
49
49
  completedAt: null,
50
50
  outcome: 'no-execution',
@@ -91,7 +91,14 @@ export function writeInitialDispatchRecord(input) {
91
91
  // New records start with empty serviceKill and zero attempts;
92
92
  // the merge-back-runner (Task 9) populates them in place.
93
93
  serviceKill: [],
94
- mergeBackAttempts: 0
94
+ mergeBackAttempts: 0,
95
+ // Slice 4.0.8: workflow graph binding (RD §4 D4c). Defaults to
96
+ // `null` for legacy / ad-hoc dispatches that do not bind a graph
97
+ // node; v4.0.0 schema is structural (required field), so a `null`
98
+ // is the explicit "no binding" state.
99
+ workflowId: typeof input.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.workflowId) ? input.workflowId : null,
100
+ graphNodeId: typeof input.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.graphNodeId) ? input.graphNodeId : null,
101
+ graphRef: typeof input.graphRef === 'string' && input.graphRef.length > 0 ? input.graphRef : null,
95
102
  };
96
103
  writeAtomic(safePath, record);
97
104
  // Slice 2026-06-23-audit-4th #A4: register the path in the
@@ -425,6 +432,68 @@ export function markCompleted(input) {
425
432
  writeAtomic(input.recordPath, next);
426
433
  return { record: next };
427
434
  });
435
+ // Slice 4.0.8 RD §4 D4c: after validating the artifact/envelope
436
+ // association with `dispatchRef`, automatically transition the
437
+ // bound graph node to `envelope-received` with
438
+ // `ackStatus=pending`. This is the auto-transition the parent
439
+ // ack protocol relies on — the canonical dispatch chain is
440
+ // `prepare -> dispatched -> running -> envelope-received -> consumed-by-parent`.
441
+ // The transition runs through `workflow-node-lifecycle.writeEnvelope`
442
+ // so the same typed error contract (PEAKS_ENVELOPE_GRAPH_MISMATCH,
443
+ // PEAKS_GRAPH_REF_BROKEN, etc.) is reused. Failures are swallowed
444
+ // (best-effort; the dispatch record itself is the source of truth
445
+ // and the transition is observable through the graph store).
446
+ if (result.record.workflowId !== null && result.record.graphNodeId !== null && result.record.graphRef !== null) {
447
+ try {
448
+ // Lazy ESM dynamic import: the graph lifecycle service is not
449
+ // on the dispatch hot path; it would be wasteful to import it
450
+ // for non-graph-bound dispatches. ESM dynamic import returns
451
+ // a promise; we do not await (best-effort) — the dispatch
452
+ // record write is the load-bearing artifact, and the graph
453
+ // node transition is observable to the next CLI call.
454
+ void (async () => {
455
+ try {
456
+ const lifecycleMod = await import('../workflow/workflow-node-lifecycle.js');
457
+ const storeMod = await import('../workflow/workflow-graph-store.js');
458
+ const sessionRoot = (() => {
459
+ try {
460
+ return storeMod.graphPathFor({
461
+ projectRoot: input.projectRoot ?? '',
462
+ sessionId: result.record.sessionId,
463
+ graphRef: result.record.graphRef ?? '',
464
+ workflowId: result.record.workflowId ?? '',
465
+ });
466
+ }
467
+ catch {
468
+ return null;
469
+ }
470
+ })();
471
+ if (sessionRoot === null)
472
+ return;
473
+ const graph = storeMod.readGraph({
474
+ projectRoot: input.projectRoot ?? '',
475
+ sessionId: result.record.sessionId,
476
+ graphRef: result.record.graphRef ?? '',
477
+ workflowId: result.record.workflowId ?? '',
478
+ });
479
+ const node = graph.nodes.find((n) => n.id === result.record.graphNodeId);
480
+ if (node === undefined)
481
+ return;
482
+ // Use the dispatch record's path as the dispatchRef. The
483
+ // record itself is the load-bearing artifact; the
484
+ // graph-node transition is a derived side-effect.
485
+ const dispatchRef = input.recordPath;
486
+ lifecycleMod.writeEnvelope({
487
+ graphNode: node,
488
+ dispatchRef,
489
+ envelopeDispatchRef: dispatchRef,
490
+ });
491
+ }
492
+ catch { /* best-effort graph transition */ }
493
+ })();
494
+ }
495
+ catch { /* best-effort */ }
496
+ }
428
497
  // Slice 2026-06-23-audit-4th #A4: update the active-dispatches
429
498
  // index. Best-effort (the on-disk record is the source of truth);
430
499
  // we only attempt the update when the trusted projectRoot is
@@ -549,18 +618,13 @@ function upgradeRecord(parsed) {
549
618
  throw new Error('Dispatch record root must be an object');
550
619
  }
551
620
  const obj = parsed;
552
- // Slice 2026-07-29-rid-prose-only-sweep Part 34: the version
553
- // field is now the literal '3.1'. Legacy v2 and v3 records
554
- // (version: 2 or version: 3) are accepted transparently and
555
- // upgraded to v3.1 on read. The forward-compat check rejects
556
- // records with an unknown version string (which would
557
- // indicate either a much-older build or a much-newer one
558
- // with a schema we have not implemented). v1 records
559
- // (pre-audit-trail) must be regenerated.
621
+ // Slice 4.0.8: 3.2 → 4.0.0 schema bump. The literal type narrows
622
+ // to '4.0.0' but legacy v3.2 / v3.1 / 3 / 2 / 1 records are
623
+ // accepted transparently and upgraded on read.
560
624
  const rawVersion = obj.version;
561
- if (rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
562
- throw new Error(`Dispatch record version mismatch: expected '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
563
- 'The v1 → v3.2 migration is in-file; records from much older or newer builds must be regenerated.');
625
+ if (rawVersion !== '4.0.0' && rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
626
+ throw new Error(`Dispatch record version mismatch: expected '4.0.0', '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
627
+ 'The v1 → v4.0.0 migration is in-file; records from much older or newer builds must be regenerated.');
564
628
  }
565
629
  const role = stringField(obj, 'role');
566
630
  const requestId = stringField(obj, 'requestId');
@@ -602,7 +666,7 @@ function upgradeRecord(parsed) {
602
666
  ? obj.batchId
603
667
  : 'legacy-batch';
604
668
  return {
605
- version: '3.2',
669
+ version: '4.0.0',
606
670
  createdAt,
607
671
  completedAt,
608
672
  outcome,
@@ -652,7 +716,13 @@ function upgradeRecord(parsed) {
652
716
  : [],
653
717
  mergeBackAttempts: typeof obj.mergeBackAttempts === 'number' && Number.isFinite(obj.mergeBackAttempts) && obj.mergeBackAttempts >= 0
654
718
  ? Math.floor(obj.mergeBackAttempts)
655
- : 0
719
+ : 0,
720
+ // Slice 4.0.8: 3.2 → 4.0.0 migration. v3.2 records on disk
721
+ // pre-date the workflow-graph binding; default all three
722
+ // fields to `null` so a legacy record upgrades transparently.
723
+ workflowId: typeof obj.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.workflowId) ? obj.workflowId : null,
724
+ graphNodeId: typeof obj.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.graphNodeId) ? obj.graphNodeId : null,
725
+ graphRef: typeof obj.graphRef === 'string' ? obj.graphRef : null
656
726
  };
657
727
  }
658
728
  function isObject(v) {
@@ -34,20 +34,6 @@ export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandat
34
34
  * (e.g. projectRoot for context) without churning every call site.
35
35
  */
36
36
  export declare function formatTestToolDetection(): string;
37
- /**
38
- * Slice 4.0.7-dogfood-PR-9: optional monorepo block prepended when
39
- * `<projectRoot>/pnpm-workspace.yaml` exists. The pre-rid static
40
- * template did not mention monorepos at all, so sub-agents on a
41
- * monorepo project (e.g. ice-cola, 4 packages) ran `pnpm test` which
42
- * fanned out to every sub-package — a 36-minute wall clock for what
43
- * was a single-slice change. The block tells the sub-agent to scope
44
- * to a single sub-package via `pnpm -r --filter @scope/name test`
45
- * and to detect the layout via `peaks test --json` first.
46
- *
47
- * The block is empty when the project is not a monorepo, so the
48
- * single-package case is byte-identical to the pre-rid prompt.
49
- */
50
- export declare const MONOREPO_TEST_TOOL_BLOCK = "## Monorepo Test Tool Detection (only when pnpm-workspace.yaml exists)\n\nWhen `<projectRoot>/pnpm-workspace.yaml` is present, the project is a pnpm monorepo. The root `package.json#scripts.test` is typically `pnpm -r test` (recursive across all packages) and `pnpm test` from the root will fan out to every sub-package. **Do not run that.** Scope to a single sub-package:\n\n- detect the active sub-package by looking for `packages/<name>/package.json` (or via `pnpm -r --filter ${YOUR_PACKAGE_NAME} test`)\n- run `pnpm --filter @<scope>/<name> test <pattern>` for one sub-package only\n- if a top-level test script is genuinely needed across multiple packages, prefix with `PEAKS_FULL_TEST=1` AND justify in the final report\n\nA repo without `pnpm-workspace.yaml` has no monorepo block; the pre-rid static template applies unchanged.";
51
37
  /**
52
38
  * Slice 2026-07-29-dispatch-stall-governance / S5 (AC-4.1 / AC-4.2) —
53
39
  * pure scope classifier. Returns a typed result so callers (the
@@ -54,28 +54,6 @@ If unsure which framework the consumer project uses, run \`peaks test --json\` f
54
54
  export function formatTestToolDetection() {
55
55
  return TEST_TOOL_DETECTION_BLOCK;
56
56
  }
57
- /**
58
- * Slice 4.0.7-dogfood-PR-9: optional monorepo block prepended when
59
- * `<projectRoot>/pnpm-workspace.yaml` exists. The pre-rid static
60
- * template did not mention monorepos at all, so sub-agents on a
61
- * monorepo project (e.g. ice-cola, 4 packages) ran `pnpm test` which
62
- * fanned out to every sub-package — a 36-minute wall clock for what
63
- * was a single-slice change. The block tells the sub-agent to scope
64
- * to a single sub-package via `pnpm -r --filter @scope/name test`
65
- * and to detect the layout via `peaks test --json` first.
66
- *
67
- * The block is empty when the project is not a monorepo, so the
68
- * single-package case is byte-identical to the pre-rid prompt.
69
- */
70
- export const MONOREPO_TEST_TOOL_BLOCK = `## Monorepo Test Tool Detection (only when pnpm-workspace.yaml exists)
71
-
72
- When \`<projectRoot>/pnpm-workspace.yaml\` is present, the project is a pnpm monorepo. The root \`package.json#scripts.test\` is typically \`pnpm -r test\` (recursive across all packages) and \`pnpm test\` from the root will fan out to every sub-package. **Do not run that.** Scope to a single sub-package:
73
-
74
- - detect the active sub-package by looking for \`packages/<name>/package.json\` (or via \`pnpm -r --filter \${YOUR_PACKAGE_NAME} test\`)
75
- - run \`pnpm --filter @<scope>/<name> test <pattern>\` for one sub-package only
76
- - if a top-level test script is genuinely needed across multiple packages, prefix with \`PEAKS_FULL_TEST=1\` AND justify in the final report
77
-
78
- A repo without \`pnpm-workspace.yaml\` has no monorepo block; the pre-rid static template applies unchanged.`;
79
57
  export function classifyTestCommand(command, env = process.env) {
80
58
  if (typeof command !== 'string') {
81
59
  return {
@@ -2,6 +2,14 @@
2
2
  * Check: skill presence current + freshness
3
3
  * (`skill-presence:current` and `skill-presence:freshness`).
4
4
  *
5
+ * Slice 4.0.8 (RD §3 + §4 D3): the diagnostic also surfaces
6
+ * stale / lost / broken-graph leases by running a `dry-run` GC
7
+ * summary through `gcStalePresenceLeases`. The summary carries
8
+ * `removed` / `retained` counts plus per-lease warnings (typed
9
+ * `PEAKS_GRAPH_REF_BROKEN`, `PEAKS_GRAPH_CORRUPTED`); the doctor
10
+ * surfaces them so the operator can run `peaks workspace reconcile`
11
+ * to repair.
12
+ *
5
13
  * Two checks sharing one read of the `presence` context field.
6
14
  * - `skill-presence:current` is informational: passes when the
7
15
  * presence is wired, with a message describing the active
@@ -2,6 +2,14 @@
2
2
  * Check: skill presence current + freshness
3
3
  * (`skill-presence:current` and `skill-presence:freshness`).
4
4
  *
5
+ * Slice 4.0.8 (RD §3 + §4 D3): the diagnostic also surfaces
6
+ * stale / lost / broken-graph leases by running a `dry-run` GC
7
+ * summary through `gcStalePresenceLeases`. The summary carries
8
+ * `removed` / `retained` counts plus per-lease warnings (typed
9
+ * `PEAKS_GRAPH_REF_BROKEN`, `PEAKS_GRAPH_CORRUPTED`); the doctor
10
+ * surfaces them so the operator can run `peaks workspace reconcile`
11
+ * to repair.
12
+ *
5
13
  * Two checks sharing one read of the `presence` context field.
6
14
  * - `skill-presence:current` is informational: passes when the
7
15
  * presence is wired, with a message describing the active
@@ -56,7 +64,7 @@ function run({ options, presence }) {
56
64
  checks.push({
57
65
  id: 'skill-presence:freshness',
58
66
  ok: false,
59
- message: `Skill presence ${presence.skill} is stale (set ${presence.setAt}, ~${ageHours}h ago); run peaks skill presence:clear if the role has ended`
67
+ message: `Skill presence ${presence.skill} is stale (set ${presence.setAt}, ~${ageHours}h ago); run peaks skill presence:clear if the role has ended, or \`peaks skill lease gc\` to drain the canonical lease`
60
68
  });
61
69
  }
62
70
  else {