peaks-loop 4.0.29 → 4.0.31

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 (75) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli/commands/_register.js +4 -0
  3. package/dist/cli/commands/dispatch-commands.d.ts +5 -41
  4. package/dist/cli/commands/dispatch-commands.js +35 -185
  5. package/dist/cli/commands/evidence-commands.d.ts +12 -0
  6. package/dist/cli/commands/evidence-commands.js +43 -0
  7. package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
  8. package/dist/cli/commands/fresh-context-commands.js +24 -0
  9. package/dist/cli/commands/request-commands.js +2 -96
  10. package/dist/cli/commands/request-format-helpers.d.ts +19 -0
  11. package/dist/cli/commands/request-format-helpers.js +102 -0
  12. package/dist/cli/commands/workspace/init-command.js +27 -14
  13. package/dist/cli/commands/worktree-auth-commands.js +2 -472
  14. package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
  15. package/dist/cli/commands/worktree-lease-commands.js +500 -0
  16. package/dist/services/artifacts/request-artifact-service.js +13 -3
  17. package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
  18. package/dist/services/code/auto-compact-lifecycle.js +235 -0
  19. package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
  20. package/dist/services/code/auto-compact-orchestrator.js +1 -222
  21. package/dist/services/codegraph/codegraph-autorefresh.js +44 -6
  22. package/dist/services/codegraph/codegraph-preflight-service.d.ts +5 -2
  23. package/dist/services/codegraph/codegraph-preflight-service.js +13 -6
  24. package/dist/services/codegraph/codegraph-service.d.ts +17 -2
  25. package/dist/services/codegraph/codegraph-service.js +27 -2
  26. package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
  27. package/dist/services/context/build-dispatch-system-prompt.js +38 -3
  28. package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
  29. package/dist/services/dispatch/dispatch-record-types.js +15 -0
  30. package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
  31. package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
  32. package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
  33. package/dist/services/dispatch/dispatch-record-writer.js +3 -245
  34. package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
  35. package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
  36. package/dist/services/dispatch/isolation-lease.d.ts +43 -0
  37. package/dist/services/dispatch/isolation-lease.js +129 -0
  38. package/dist/services/evidence/evidence-generator.d.ts +26 -0
  39. package/dist/services/evidence/evidence-generator.js +349 -0
  40. package/dist/services/fresh-context/config.d.ts +1 -0
  41. package/dist/services/fresh-context/config.js +13 -0
  42. package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
  43. package/dist/services/fresh-context/fresh-context-block.js +40 -0
  44. package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
  45. package/dist/services/fresh-context/trigger-scan.js +29 -0
  46. package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
  47. package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
  48. package/dist/services/skills/hooks-settings-service.d.ts +2 -64
  49. package/dist/services/skills/hooks-settings-service.js +2 -215
  50. package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
  51. package/dist/services/skills/skill-statusline-renderer.js +1 -184
  52. package/dist/services/skills/statusline-palette.d.ts +62 -0
  53. package/dist/services/skills/statusline-palette.js +190 -0
  54. package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
  55. package/dist/services/slice/slice-decompose-import-edges.js +102 -0
  56. package/dist/services/slice/slice-decompose-service.js +3 -198
  57. package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
  58. package/dist/services/slice/slice-decompose-tarjan.js +108 -0
  59. package/dist/services/standards/project-standards-service.d.ts +1 -9
  60. package/dist/services/standards/project-standards-service.js +3 -232
  61. package/dist/services/standards/standards-render.d.ts +23 -0
  62. package/dist/services/standards/standards-render.js +238 -0
  63. package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
  64. package/dist/services/standards/ui-library-dispatch-block.js +48 -0
  65. package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
  66. package/dist/services/workspace/reconcile-migrate.js +230 -0
  67. package/dist/services/workspace/reconcile-service.d.ts +0 -72
  68. package/dist/services/workspace/reconcile-service.js +3 -220
  69. package/dist/services/workspace/workspace-service.js +13 -2
  70. package/dist/shared/incrementing-number.d.ts +11 -0
  71. package/dist/shared/incrementing-number.js +16 -3
  72. package/package.json +5 -5
  73. package/skills/peaks-code/SKILL.md +6 -2
  74. package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
  75. package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
@@ -31,7 +31,7 @@
31
31
  // - CODEGRAPH_STRUCTURE_MAX_DIRS = 40 directory rows
32
32
  // - CODEGRAPH_STRUCTURE_MAX_ROOT_FILES = 12 bare root files
33
33
  import { mkdirSync } from 'node:fs';
34
- import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, defaultCodegraphInitGuard, executeCodegraphInvocation, writeCodegraphMarker, } from './codegraph-service.js';
34
+ import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, defaultCodegraphInitGuard, executeCodegraphInvocation, isCodegraphInitialized, writeCodegraphMarker, } from './codegraph-service.js';
35
35
  import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
36
36
  /** Cap for the directory histogram in the rendered structure block. */
37
37
  export const CODEGRAPH_STRUCTURE_MAX_DIRS = 40;
@@ -163,8 +163,11 @@ async function readStructure(projectRoot, runner) {
163
163
  *
164
164
  * Behavior matrix (acceptance criteria):
165
165
  * - `.codegraph/` absent → init + index (best-effort), then read.
166
- * - `.codegraph/` present with peaks-loop marker → skip init/index
167
- * (fresh), read directly. No redundant re-index on every dispatch.
166
+ * - `.codegraph/` present with peaks-loop marker AND `codegraph.db` →
167
+ * skip init/index (fresh), read directly. No redundant re-index on
168
+ * every dispatch.
169
+ * - `.codegraph/` present with peaks-loop marker but NO `codegraph.db`
170
+ * (dangling) → init + index (self-heal), then read.
168
171
  * - `.codegraph/` present WITHOUT marker (foreign schema) → fail-soft;
169
172
  * never clobber a foreign store.
170
173
  *
@@ -180,7 +183,10 @@ export async function buildCodegraphPreflightBlock(projectRoot, runner) {
180
183
  note: `codegraph unavailable: ${CODEGRAPH_DIR_NAME}/ exists with a non-peaks-loop schema and was not touched. Move or rename the foreign directory, then re-run \`peaks codegraph init\` to enable pre-dispatch structure reads.`,
181
184
  };
182
185
  }
183
- if (guard.status === 'fresh') {
186
+ // 'fresh' (no dir) OR a dangling peaks-loop dir (marker present, no
187
+ // codegraph.db) → run init + index. An initialized peaks-loop dir
188
+ // (`noop-already-peaks-loop` with codegraph.db) skips straight to read.
189
+ if (guard.status === 'fresh' || !isCodegraphInitialized(projectRoot)) {
184
190
  // 1. init (best-effort). Upstream creates the `.codegraph/` dir; we
185
191
  // stamp the peaks-loop marker afterwards so the NEXT dispatch hits
186
192
  // the noop (skip-when-fresh) branch.
@@ -220,7 +226,8 @@ export async function buildCodegraphPreflightBlock(projectRoot, runner) {
220
226
  return { available: false, note: `codegraph index unavailable: ${errorMessage(error)}` };
221
227
  }
222
228
  }
223
- // guard.status === 'noop-already-peaks-loop' (or we just initialized):
224
- // the schema exists — do NOT re-index, go straight to the bounded read.
229
+ // guard.status === 'noop-already-peaks-loop' with codegraph.db (or we
230
+ // just initialized): the schema exists — do NOT re-index, go straight
231
+ // to the bounded read.
225
232
  return readStructure(projectRoot, processRunner);
226
233
  }
@@ -55,6 +55,12 @@ export declare const CODEGRAPH_DIR_NAME = ".codegraph";
55
55
  * etc. all happily use the same directory name).
56
56
  */
57
57
  export declare const CODEGRAPH_MARKER_NAME = ".peaks-loop-marker";
58
+ /**
59
+ * Upstream SQLite schema filename. `isCodegraphInitialized` mirrors
60
+ * `@colbymchenry/codegraph`'s `isInitialized`, which requires this file
61
+ * (not just the `.codegraph/` directory) to exist.
62
+ */
63
+ export declare const CODEGRAPH_DB_NAME = "codegraph.db";
58
64
  export declare const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
59
65
  export type ResolvedCodegraphLocation = {
60
66
  readonly source: 'root';
@@ -88,10 +94,19 @@ export declare class CodegraphInitConflictError extends Error {
88
94
  constructor(message: string, codegraphDir: string);
89
95
  }
90
96
  export type CodegraphInitGuard = (projectRoot: string) => CodegraphInitGuardResult;
97
+ /**
98
+ * True when `<projectRoot>/.codegraph/codegraph.db` exists. Mirrors
99
+ * upstream `@colbymchenry/codegraph` `isInitialized`: the `.codegraph/`
100
+ * directory alone (or with only the peaks-loop marker) is NOT initialized.
101
+ * Pure fs probe; never throws.
102
+ */
103
+ export declare function isCodegraphInitialized(projectRoot: string): boolean;
91
104
  /**
92
105
  * Root-only init guard: probes `<projectRoot>/.codegraph/`. 'fresh'
93
- * is returned when the directory does not exist yet, so the next
94
- * `peaks codegraph init` creates the root `.codegraph/` directory.
106
+ * is returned when the directory does not exist yet OR when it exists
107
+ * with the peaks-loop marker but no `codegraph.db` (dangling), so the
108
+ * next `peaks codegraph init` creates the root `.codegraph/` directory
109
+ * and its SQLite schema.
95
110
  */
96
111
  export declare function defaultCodegraphInitGuard(projectRoot: string): CodegraphInitGuardResult;
97
112
  /**
@@ -180,6 +180,12 @@ export const CODEGRAPH_DIR_NAME = '.codegraph';
180
180
  * etc. all happily use the same directory name).
181
181
  */
182
182
  export const CODEGRAPH_MARKER_NAME = '.peaks-loop-marker';
183
+ /**
184
+ * Upstream SQLite schema filename. `isCodegraphInitialized` mirrors
185
+ * `@colbymchenry/codegraph`'s `isInitialized`, which requires this file
186
+ * (not just the `.codegraph/` directory) to exist.
187
+ */
188
+ export const CODEGRAPH_DB_NAME = 'codegraph.db';
183
189
  export const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
184
190
  /**
185
191
  * Root-only resolver: peaks-loop always manages
@@ -205,6 +211,15 @@ export class CodegraphInitConflictError extends Error {
205
211
  this.name = 'CodegraphInitConflictError';
206
212
  }
207
213
  }
214
+ /**
215
+ * True when `<projectRoot>/.codegraph/codegraph.db` exists. Mirrors
216
+ * upstream `@colbymchenry/codegraph` `isInitialized`: the `.codegraph/`
217
+ * directory alone (or with only the peaks-loop marker) is NOT initialized.
218
+ * Pure fs probe; never throws.
219
+ */
220
+ export function isCodegraphInitialized(projectRoot) {
221
+ return existsSync(join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_DB_NAME));
222
+ }
208
223
  /**
209
224
  * Inspect a candidate codegraph directory and return its guard
210
225
  * status. A file (or symlink-to-file) at the path counts as a
@@ -223,14 +238,24 @@ function inspectCandidateCodegraphDir(codegraphDir) {
223
238
  }
224
239
  const markerPath = join(codegraphDir, CODEGRAPH_MARKER_NAME);
225
240
  if (existsSync(markerPath)) {
241
+ // Marker present but no codegraph.db → the dangling state left by the
242
+ // pre-fix rid-CG-001 auto-stake (marker stamped without running
243
+ // upstream init). Report 'fresh' so consumers re-run upstream init
244
+ // (idempotent, creates the db) instead of treating the schema as
245
+ // fully initialized.
246
+ if (!existsSync(join(codegraphDir, CODEGRAPH_DB_NAME))) {
247
+ return { status: 'fresh', codegraphDir };
248
+ }
226
249
  return { status: 'noop-already-peaks-loop', codegraphDir };
227
250
  }
228
251
  return { status: 'conflict-foreign-schema', codegraphDir };
229
252
  }
230
253
  /**
231
254
  * Root-only init guard: probes `<projectRoot>/.codegraph/`. 'fresh'
232
- * is returned when the directory does not exist yet, so the next
233
- * `peaks codegraph init` creates the root `.codegraph/` directory.
255
+ * is returned when the directory does not exist yet OR when it exists
256
+ * with the peaks-loop marker but no `codegraph.db` (dangling), so the
257
+ * next `peaks codegraph init` creates the root `.codegraph/` directory
258
+ * and its SQLite schema.
234
259
  */
235
260
  export function defaultCodegraphInitGuard(projectRoot) {
236
261
  const codegraphDir = join(projectRoot, CODEGRAPH_DIR_NAME);
@@ -42,6 +42,32 @@ export interface DispatchPromptInput {
42
42
  * and the memory/task content.
43
43
  */
44
44
  codegraphBlock?: string | null;
45
+ /**
46
+ * Slice 2026-09-06-ui-lib-dispatch-priority: pre-composed
47
+ * `## Project stack` markdown block, rendered from the detected
48
+ * project context (component library + CSS framework + build tool) by
49
+ * the dispatch site BEFORE the sub-agent's task body is composed so
50
+ * the RD/UI sub-agent sees the library-first directive in-context.
51
+ *
52
+ * - `undefined` → no project-stack block (legacy callers unchanged).
53
+ * - `null` → the project has no detected component library; the
54
+ * composer renders nothing (byte-identical degradation).
55
+ * - `string` → the block, rendered verbatim between the codegraph
56
+ * block and the memory/task content.
57
+ */
58
+ projectStackBlock?: string | null;
59
+ /**
60
+ * Slice 2026-09-07-search-first-preflight: the orchestrator-synthesized
61
+ * `## Fresh context` block (≤5 binding directives from a Context7 →
62
+ * WebSearch preflight), read from `.peaks/_runtime/<sessionId>/fresh-context.md`.
63
+ *
64
+ * - `undefined` → no fresh-context block (legacy callers unchanged).
65
+ * - `null` → the block is unavailable (missing file / no `## Fresh context`
66
+ * heading); the composer renders nothing (byte-identical degradation).
67
+ * - `string` → the block, rendered verbatim after the project-stack block
68
+ * and before the memory/task content.
69
+ */
70
+ freshContextBlock?: string | null;
45
71
  }
46
72
  /**
47
73
  * Slice 2026-07-29-worktree-l1: Layer 1 of the 3-layer worktree governance
@@ -98,13 +98,15 @@ export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
98
98
  * refusal is in scope before any task-specific prose arrives.
99
99
  */
100
100
  export function buildDispatchSystemPrompt(input) {
101
- const { taskBody, memoryBlock, contextProbe, codegraphBlock } = input;
101
+ const { taskBody, memoryBlock, contextProbe, codegraphBlock, projectStackBlock, freshContextBlock } = input;
102
102
  const contextBlock = renderContextBlock(contextProbe ?? null);
103
103
  const codegraphText = renderCodegraphBlock(codegraphBlock);
104
+ const projectStackText = renderProjectStackBlock(projectStackBlock);
105
+ const freshContextText = renderFreshContextBlock(freshContextBlock);
104
106
  if (memoryBlock.available === true && typeof memoryBlock.block === 'string') {
105
- return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${memoryBlock.block}\n## Task\n${taskBody}`;
107
+ return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${memoryBlock.block}\n## Task\n${taskBody}`;
106
108
  }
107
- return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${taskBody}`;
109
+ return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${taskBody}`;
108
110
  }
109
111
  /**
110
112
  * Slice 2026-09-03-codegraph-preread: fixed degradation string emitted
@@ -133,6 +135,39 @@ function renderCodegraphBlock(codegraphBlock) {
133
135
  const text = codegraphBlock === null ? CODEGRAPH_UNAVAILABLE_BLOCK : codegraphBlock;
134
136
  return `${text.replace(/\s+$/, '')}\n\n`;
135
137
  }
138
+ /**
139
+ * Render the project-stack insertion for a dispatch prompt.
140
+ *
141
+ * - `undefined` / `null` → empty (the composer is byte-identical to the
142
+ * legacy shape — no dangling "Project stack" heading).
143
+ * - `string` → the pre-composed block from the dispatch site, verbatim.
144
+ *
145
+ * Mirrors `renderCodegraphBlock`: every non-empty variant is normalized
146
+ * to end on its own paragraph (`\n\n`) so the following block (project
147
+ * memory or task body) starts cleanly.
148
+ */
149
+ function renderProjectStackBlock(projectStackBlock) {
150
+ if (projectStackBlock === undefined || projectStackBlock === null)
151
+ return '';
152
+ return `${projectStackBlock.replace(/\s+$/, '')}\n\n`;
153
+ }
154
+ /**
155
+ * Render the fresh-context insertion for a dispatch prompt.
156
+ *
157
+ * - `undefined` / `null` → empty (the composer is byte-identical to the
158
+ * legacy shape — no dangling "Fresh context" heading).
159
+ * - `string` → the pre-composed block from the fresh-context preflight
160
+ * synthesis, verbatim.
161
+ *
162
+ * Mirrors `renderProjectStackBlock`: every non-empty variant is normalized
163
+ * to end on its own paragraph (`\n\n`) so the following block (project
164
+ * memory or task body) starts cleanly.
165
+ */
166
+ function renderFreshContextBlock(freshContextBlock) {
167
+ if (freshContextBlock === undefined || freshContextBlock === null)
168
+ return '';
169
+ return `${freshContextBlock.replace(/\s+$/, '')}\n\n`;
170
+ }
136
171
  /**
137
172
  * Slice 2026-07-29-context-evaluation-accuracy: emit a
138
173
  * `## Context window` block with the authoritative ratio so the
@@ -0,0 +1,278 @@
1
+ import type { SubAgentToolCall } from './sub-agent-dispatcher.js';
2
+ /** G6.3 Heartbeat entry — single update written by a running sub-agent. */
3
+ export interface Heartbeat {
4
+ readonly at: string;
5
+ readonly status: HeartbeatStatus;
6
+ readonly progress: number;
7
+ readonly note: string | null;
8
+ }
9
+ export type HeartbeatStatus = 'queued' | 'running' | 'finalizing' | 'done' | 'failed' | 'stale' | 'cancelled' | 'no-execution' | 'never-started' | 'unreadable';
10
+ export type DispatchRecordStatus = 'queued' | 'running' | 'finalizing' | 'done' | 'failed' | 'cancelled' | 'no-execution' | 'stale' | 'never-started' | 'unreadable';
11
+ export type DispatchOutcome = 'success' | 'failed' | 'timeout' | 'cancelled' | 'no-execution';
12
+ /** G2+G5+G6 dispatch record schema (AC-26 + AC-34). */
13
+ export interface DispatchRecord {
14
+ /**
15
+ * Slice 2026-07-29-worktree-l2-extended Part 4.C: schema v3 makes
16
+ * `leaseId` a structurally required field (was `leaseId?: string | null`
17
+ * in v2). The v3 upgrade is a "fill in" migration: every dispatch
18
+ * writer knows its lease id at construction time (Part 2.C's
19
+ * --isolation worktree spawns it; non-isolation dispatches stamp
20
+ * `null`). Readers tolerate both v2 and v3 on disk (see
21
+ * `upgradeRecord`); the `?` was a Part 4.A ergonomic concession
22
+ * to keep 4 unit-test literal sites from breaking the build. v3
23
+ * moves the optional off the type and adds the field to the
24
+ * 4 literal sites in one pass.
25
+ *
26
+ * Slice 2026-07-29-worktree-l2-extended Part 7: schema v3.1 adds
27
+ * `isolationStartedAt: string | null` for the L4 isolation
28
+ * bridge (Part 8 container POC). It's the ISO timestamp of
29
+ * when the isolation mode was set up (e.g. when the worktree
30
+ * was spawned). `null` means the dispatch did not request
31
+ * isolation. The `version` field stays at 3 because the
32
+ * v3 → v3.1 transition is additive; the new field is
33
+ * defaulted to `null` on read so v3 records upgrade cleanly.
34
+ */
35
+ /**
36
+ * Phase A Task 8: schema bumped to v4.1.0 (additive). The bump
37
+ * is purely additive — new fields (`mode`, `vendor`,
38
+ * `autoCompactEvents`, `tokenUsage`) all default safely on
39
+ * read for legacy v4.0.0 / v3.2 / v3.1 / v3 / v2 / v1 records.
40
+ * No existing field semantics changed.
41
+ */
42
+ readonly version: '4.1.0';
43
+ readonly createdAt: string;
44
+ readonly completedAt: string | null;
45
+ readonly outcome: DispatchOutcome;
46
+ readonly artifactPaths: readonly string[];
47
+ readonly disposed: boolean;
48
+ readonly disposedAt: string | null;
49
+ readonly role: string;
50
+ readonly requestId: string;
51
+ readonly sessionId: string;
52
+ readonly prompt: string;
53
+ readonly toolCall: SubAgentToolCall;
54
+ /** G5 batch id (AC-27) — uuid-like opaque token grouping one batch. */
55
+ readonly batchId: string;
56
+ /** G6 fields (AC-34) — backward compat: defaults on read. */
57
+ readonly heartbeats: readonly Heartbeat[];
58
+ readonly lastBeatAt: string | null;
59
+ readonly status: DispatchRecordStatus;
60
+ readonly stage: string | null;
61
+ /**
62
+ * Slice 2026-07-29-worktree-l2-extended Part 3.A + Part 4.C: the
63
+ * worktree lease id stamped on this dispatch (via `peaks sub-agent
64
+ * dispatch --isolation worktree`). The release hook (see
65
+ * markCompleted + `peaks sub-agent heartbeat --status done`)
66
+ * reads this field to auto-call `peaks worktree release` when
67
+ * the sub-agent finalizes. `null` means the dispatch did not
68
+ * request isolation and no release will fire. Persisted for
69
+ * audit + idempotency so a re-read of an old record still
70
+ * surfaces the lease id even if the on-disk lease file has since
71
+ * been gc'd.
72
+ *
73
+ * v3 (Part 4.C) makes this structurally required. v2 records
74
+ * missing the field upgrade to `null` on read (see
75
+ * `upgradeRecord`).
76
+ */
77
+ readonly leaseId: string | null;
78
+ /**
79
+ * Slice 2026-07-29-worktree-l2-extended Part 7: ISO timestamp of
80
+ * when the isolation mode was set up. For `--isolation worktree`
81
+ * this is the moment `peaks worktree spawn` returned; for
82
+ * `--isolation container` (Part 8) it's when the container
83
+ * runtime reported the container as running. `null` when the
84
+ * dispatch did not request isolation. Lets the dashboard
85
+ * compute isolation duration (now - isolationStartedAt) without
86
+ * cross-referencing the metrics stream.
87
+ */
88
+ readonly isolationStartedAt: string | null;
89
+ /**
90
+ * Slice 2026-08-01-subagent-merge-and-e2e (Task 7): v3.2 schema
91
+ * bump. One entry per pid the parent best-effort-killed during
92
+ * the service-shutdown phase of the merge-back pipeline (see
93
+ * src/services/dispatch/service-shutdown.ts). Empty array when
94
+ * the sub-agent did not register any services. The shape is the
95
+ * union of ServiceKillOutcome (skipped=false) and
96
+ * ServiceKillSkipped (skipped=true) — kept as a plain object
97
+ * here so the on-disk schema does not lock onto the helper's
98
+ * narrower union. The reader (merge-back-runner.ts) interprets
99
+ * each entry based on the `skipped` field.
100
+ */
101
+ readonly serviceKill: ReadonlyArray<{
102
+ readonly pid: number;
103
+ readonly name: string;
104
+ readonly signal: string;
105
+ readonly exitCode: number | null;
106
+ readonly skipped?: boolean;
107
+ readonly reason?: string;
108
+ }>;
109
+ /**
110
+ * Slice 2026-08-01-subagent-merge-and-e2e (Task 7): v3.2 schema
111
+ * bump. Counts how many merge attempts the parent session has
112
+ * made against this dispatch's branch. The conflict-replay
113
+ * orchestrator bumps this on each retry (bounded to ONE re-dispatch
114
+ * per merge attempt; multi-conflict cases escalate). Persisted so
115
+ * the dashboard can render the retry count without replaying the
116
+ * merge transcript.
117
+ */
118
+ readonly mergeBackAttempts: number;
119
+ /**
120
+ * Slice 4.0.8 RD §4 D4c (presence-lease-graph): the workflow id +
121
+ * graph node id + graphRef this dispatch is bound to. Persisted
122
+ * directly in the dispatch record (NOT in a sidecar) so the
123
+ * envelope-writer `markCompleted` can auto-transition the bound
124
+ * graph node to `envelope-received` with `ackStatus=pending` in
125
+ * one protected update.
126
+ *
127
+ * The schema bump from `3.2 → 4.0.0` is BREAKING in the sense
128
+ * that the literal type is narrowed. The optional `?` keeps back-
129
+ * compat for old records that pre-date the binding (the
130
+ * `upgradeRecord` reader defaults them to `null`).
131
+ */
132
+ readonly workflowId: string | null;
133
+ readonly graphNodeId: string | null;
134
+ readonly graphRef: string | null;
135
+ /**
136
+ * Phase A Task 8: dispatch execution mode. `in-process` is the
137
+ * current behavior (LLM-side runner, no separate OS process).
138
+ * `detached` is the new path: `peaks sub-agent dispatch --mode
139
+ * detached` spawns a real child OS process running a different
140
+ * LLM vendor (claude / codex / copilot) and reports back via
141
+ * the dispatch record. v4.1.0 is the additive bump; legacy v4.0.0
142
+ * records upgrade to `in-process` on read.
143
+ */
144
+ readonly mode: 'in-process' | 'detached';
145
+ /**
146
+ * Phase A Task 8: vendor id when `mode='detached'`. `null` when
147
+ * the dispatch is in-process. Reserved for future use; current
148
+ * detached sub-agents use `claude` but the schema also accepts
149
+ * `codex` and `copilot` for the vendor-neutral adapter layer.
150
+ */
151
+ readonly vendor: 'claude' | 'codex' | 'copilot' | null;
152
+ /**
153
+ * Phase A Task 8: G8 autoCompact events accumulated by the child
154
+ * LLM during a detached run. Each event records the threshold
155
+ * that fired (0.85 = first warning, 0.95 = second warning) plus
156
+ * token counts before/after. Empty for in-process dispatches
157
+ * and for legacy records upgraded on read.
158
+ */
159
+ readonly autoCompactEvents: ReadonlyArray<{
160
+ readonly at: number;
161
+ readonly threshold: '0.85' | '0.95';
162
+ readonly tokensBefore: number;
163
+ readonly tokensAfter: number;
164
+ readonly scratchFile?: string;
165
+ }>;
166
+ /**
167
+ * Phase A Task 8: G8 token-usage accounting for detached sub-agents.
168
+ * Detached runs have "unlimited spend but recorded" semantics —
169
+ * the cost is recorded for audit but not enforced. `null` when
170
+ * the dispatch is in-process (no detached accounting) or for
171
+ * legacy records upgraded on read.
172
+ */
173
+ readonly tokenUsage: {
174
+ readonly promptTokens: number;
175
+ readonly completionTokens: number;
176
+ readonly totalCostUsd?: number;
177
+ } | null;
178
+ }
179
+ /** Input for the initial write. */
180
+ export type WriteInitialDispatchInput = {
181
+ projectRoot: string;
182
+ sessionId: string;
183
+ requestId: string;
184
+ role: string;
185
+ prompt: string;
186
+ toolCall: SubAgentToolCall;
187
+ batchId: string;
188
+ /** Override the timestamp (testing). */
189
+ now?: () => Date;
190
+ /**
191
+ * Slice 2026-07-29-worktree-l2-extended Part 3.A: the worktree
192
+ * lease id this dispatch owns (set by `peaks sub-agent dispatch
193
+ * --isolation worktree`). Persisted so the finalize-time release
194
+ * hook in `markCompleted` can fire even after the dispatch
195
+ * process exits. Optional; absent when the dispatch did not
196
+ * request isolation.
197
+ */
198
+ leaseId?: string | null;
199
+ /**
200
+ * Slice 2026-07-29-worktree-l2-extended Part 7: ISO timestamp
201
+ * when the isolation mode was set up. Optional on the input
202
+ * (defaults to `null`); dispatch-commands.ts passes the spawn
203
+ * time when `--isolation` is requested.
204
+ */
205
+ isolationStartedAt?: string | null;
206
+ /**
207
+ * Slice 4.0.8: workflow graph binding for the dispatch. Defaults
208
+ * to `null` so a non-graph dispatch (legacy CLI flow, ad-hoc
209
+ * dispatch) still writes a v4.0.0 record.
210
+ */
211
+ workflowId?: string | null;
212
+ graphNodeId?: string | null;
213
+ graphRef?: string | null;
214
+ /**
215
+ * Phase A Task 8: dispatch execution mode. Default `'in-process'`
216
+ * preserves the current LLM-side runner behavior. `'detached'`
217
+ * triggers the new real-OS-process path via
218
+ * `peaks sub-agent dispatch --mode detached`.
219
+ */
220
+ mode?: 'in-process' | 'detached';
221
+ /**
222
+ * Phase A Task 8: vendor id when `mode='detached'`. Required by
223
+ * the adapter layer to know which CLI / runtime to spawn. The
224
+ * schema accepts the three vendors peaks-loop has adapters for
225
+ * (claude / codex / copilot). Ignored when `mode='in-process'`.
226
+ */
227
+ vendor?: 'claude' | 'codex' | 'copilot';
228
+ /**
229
+ * Phase A Task 8: G8 autoCompact events accumulated by the child
230
+ * LLM. Optional on input — most dispatches start with an empty
231
+ * array and the detached runner appends events as they fire.
232
+ */
233
+ autoCompactEvents?: Array<{
234
+ at: number;
235
+ threshold: '0.85' | '0.95';
236
+ tokensBefore: number;
237
+ tokensAfter: number;
238
+ scratchFile?: string;
239
+ }>;
240
+ /**
241
+ * Phase A Task 8: G8 token-usage accounting. Detached runs
242
+ * record spend for audit (unlimited, but persisted). Optional
243
+ * on input; the detached runner fills this in as it streams
244
+ * usage from the vendor API.
245
+ */
246
+ tokenUsage?: {
247
+ promptTokens: number;
248
+ completionTokens: number;
249
+ totalCostUsd?: number;
250
+ };
251
+ };
252
+ /** Heartbeat write input. */
253
+ export type AppendHeartbeatInput = {
254
+ recordPath: string;
255
+ status: HeartbeatStatus;
256
+ progress: number;
257
+ note?: string;
258
+ now?: () => Date;
259
+ };
260
+ /** Lifecycle transition input. */
261
+ export type LifecycleInput = {
262
+ recordPath: string;
263
+ outcome: DispatchOutcome;
264
+ status: DispatchRecordStatus;
265
+ artifactPaths?: readonly string[];
266
+ now?: () => Date;
267
+ /**
268
+ * Slice 2026-06-23-audit-4th #A4: trusted project root. Required
269
+ * so the active-dispatches index can be updated without deriving
270
+ * the root from the recordPath (the same anti-pattern that
271
+ * audit-3rd #1 fixed for heartbeat). The CLI / LLM-side runner
272
+ * passes this from `--project` or `process.cwd()`.
273
+ */
274
+ projectRoot?: string;
275
+ };
276
+ export declare const MAX_PROMPT_BYTES: number;
277
+ export declare const NOTE_MAX_CHARS = 200;
278
+ export declare const MS_PER_DAY: number;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * PRD-002b slice 2 — extract dispatch-record size budgets (max-prompt
3
+ * bytes, note truncation cap) + time-math primitives so the
4
+ * no-magic-numbers rule stops flagging the writer pipeline.
5
+ */
6
+ const BYTES_PER_KB = 1024;
7
+ const MAX_PROMPT_KB = 256;
8
+ export const MAX_PROMPT_BYTES = MAX_PROMPT_KB * BYTES_PER_KB;
9
+ export const NOTE_MAX_CHARS = 200;
10
+ const MS_PER_SECOND = 1_000;
11
+ const SECONDS_PER_MINUTE = 60;
12
+ const MINUTES_PER_HOUR = 60;
13
+ const HOURS_PER_DAY = 24;
14
+ export const MS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
15
+ const REDACTION_MAX_SCAN_DEPTH = 20;
@@ -0,0 +1,5 @@
1
+ import type { DispatchOutcome, DispatchRecord, DispatchRecordStatus } from './dispatch-record-types.js';
2
+ export declare function upgradeRecord(parsed: unknown): DispatchRecord;
3
+ declare function isDispatchStatus(v: unknown): v is DispatchRecordStatus;
4
+ declare function isOutcome(v: unknown): v is DispatchOutcome;
5
+ export { isDispatchStatus, isOutcome };