peaks-loop 4.0.29 → 4.0.30
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 +14 -0
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/dispatch-commands.d.ts +5 -41
- package/dist/cli/commands/dispatch-commands.js +35 -185
- package/dist/cli/commands/evidence-commands.d.ts +12 -0
- package/dist/cli/commands/evidence-commands.js +43 -0
- package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
- package/dist/cli/commands/fresh-context-commands.js +24 -0
- package/dist/cli/commands/request-commands.js +2 -96
- package/dist/cli/commands/request-format-helpers.d.ts +19 -0
- package/dist/cli/commands/request-format-helpers.js +102 -0
- package/dist/cli/commands/worktree-auth-commands.js +2 -472
- package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
- package/dist/cli/commands/worktree-lease-commands.js +500 -0
- package/dist/services/artifacts/request-artifact-service.js +13 -3
- package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
- package/dist/services/code/auto-compact-lifecycle.js +235 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +1 -222
- package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +38 -3
- package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
- package/dist/services/dispatch/dispatch-record-types.js +15 -0
- package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
- package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
- package/dist/services/dispatch/dispatch-record-writer.js +3 -245
- package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
- package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
- package/dist/services/dispatch/isolation-lease.d.ts +43 -0
- package/dist/services/dispatch/isolation-lease.js +129 -0
- package/dist/services/evidence/evidence-generator.d.ts +26 -0
- package/dist/services/evidence/evidence-generator.js +349 -0
- package/dist/services/fresh-context/config.d.ts +1 -0
- package/dist/services/fresh-context/config.js +13 -0
- package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
- package/dist/services/fresh-context/fresh-context-block.js +40 -0
- package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
- package/dist/services/fresh-context/trigger-scan.js +29 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
- package/dist/services/skills/hooks-settings-service.d.ts +2 -64
- package/dist/services/skills/hooks-settings-service.js +2 -215
- package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
- package/dist/services/skills/skill-statusline-renderer.js +1 -184
- package/dist/services/skills/statusline-palette.d.ts +62 -0
- package/dist/services/skills/statusline-palette.js +190 -0
- package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
- package/dist/services/slice/slice-decompose-import-edges.js +102 -0
- package/dist/services/slice/slice-decompose-service.js +3 -198
- package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
- package/dist/services/slice/slice-decompose-tarjan.js +108 -0
- package/dist/services/standards/project-standards-service.d.ts +1 -9
- package/dist/services/standards/project-standards-service.js +3 -232
- package/dist/services/standards/standards-render.d.ts +23 -0
- package/dist/services/standards/standards-render.js +238 -0
- package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
- package/dist/services/standards/ui-library-dispatch-block.js +48 -0
- package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
- package/dist/services/workspace/reconcile-migrate.js +230 -0
- package/dist/services/workspace/reconcile-service.d.ts +0 -72
- package/dist/services/workspace/reconcile-service.js +3 -220
- package/dist/services/workspace/workspace-service.js +13 -2
- package/dist/shared/incrementing-number.d.ts +11 -0
- package/dist/shared/incrementing-number.js +16 -3
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +6 -2
- package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
- package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
|
@@ -1,279 +1,7 @@
|
|
|
1
|
-
import type { SubAgentToolCall } from './sub-agent-dispatcher.js';
|
|
2
1
|
import { type StageLabel } from './stage-enum.js';
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
readonly status: HeartbeatStatus;
|
|
7
|
-
readonly progress: number;
|
|
8
|
-
readonly note: string | null;
|
|
9
|
-
}
|
|
10
|
-
export type HeartbeatStatus = 'queued' | 'running' | 'finalizing' | 'done' | 'failed' | 'stale' | 'cancelled' | 'no-execution' | 'never-started' | 'unreadable';
|
|
11
|
-
export type DispatchRecordStatus = 'queued' | 'running' | 'finalizing' | 'done' | 'failed' | 'cancelled' | 'no-execution' | 'stale' | 'never-started' | 'unreadable';
|
|
12
|
-
export type DispatchOutcome = 'success' | 'failed' | 'timeout' | 'cancelled' | 'no-execution';
|
|
13
|
-
/** G2+G5+G6 dispatch record schema (AC-26 + AC-34). */
|
|
14
|
-
export interface DispatchRecord {
|
|
15
|
-
/**
|
|
16
|
-
* Slice 2026-07-29-worktree-l2-extended Part 4.C: schema v3 makes
|
|
17
|
-
* `leaseId` a structurally required field (was `leaseId?: string | null`
|
|
18
|
-
* in v2). The v3 upgrade is a "fill in" migration: every dispatch
|
|
19
|
-
* writer knows its lease id at construction time (Part 2.C's
|
|
20
|
-
* --isolation worktree spawns it; non-isolation dispatches stamp
|
|
21
|
-
* `null`). Readers tolerate both v2 and v3 on disk (see
|
|
22
|
-
* `upgradeRecord`); the `?` was a Part 4.A ergonomic concession
|
|
23
|
-
* to keep 4 unit-test literal sites from breaking the build. v3
|
|
24
|
-
* moves the optional off the type and adds the field to the
|
|
25
|
-
* 4 literal sites in one pass.
|
|
26
|
-
*
|
|
27
|
-
* Slice 2026-07-29-worktree-l2-extended Part 7: schema v3.1 adds
|
|
28
|
-
* `isolationStartedAt: string | null` for the L4 isolation
|
|
29
|
-
* bridge (Part 8 container POC). It's the ISO timestamp of
|
|
30
|
-
* when the isolation mode was set up (e.g. when the worktree
|
|
31
|
-
* was spawned). `null` means the dispatch did not request
|
|
32
|
-
* isolation. The `version` field stays at 3 because the
|
|
33
|
-
* v3 → v3.1 transition is additive; the new field is
|
|
34
|
-
* defaulted to `null` on read so v3 records upgrade cleanly.
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* Phase A Task 8: schema bumped to v4.1.0 (additive). The bump
|
|
38
|
-
* is purely additive — new fields (`mode`, `vendor`,
|
|
39
|
-
* `autoCompactEvents`, `tokenUsage`) all default safely on
|
|
40
|
-
* read for legacy v4.0.0 / v3.2 / v3.1 / v3 / v2 / v1 records.
|
|
41
|
-
* No existing field semantics changed.
|
|
42
|
-
*/
|
|
43
|
-
readonly version: '4.1.0';
|
|
44
|
-
readonly createdAt: string;
|
|
45
|
-
readonly completedAt: string | null;
|
|
46
|
-
readonly outcome: DispatchOutcome;
|
|
47
|
-
readonly artifactPaths: readonly string[];
|
|
48
|
-
readonly disposed: boolean;
|
|
49
|
-
readonly disposedAt: string | null;
|
|
50
|
-
readonly role: string;
|
|
51
|
-
readonly requestId: string;
|
|
52
|
-
readonly sessionId: string;
|
|
53
|
-
readonly prompt: string;
|
|
54
|
-
readonly toolCall: SubAgentToolCall;
|
|
55
|
-
/** G5 batch id (AC-27) — uuid-like opaque token grouping one batch. */
|
|
56
|
-
readonly batchId: string;
|
|
57
|
-
/** G6 fields (AC-34) — backward compat: defaults on read. */
|
|
58
|
-
readonly heartbeats: readonly Heartbeat[];
|
|
59
|
-
readonly lastBeatAt: string | null;
|
|
60
|
-
readonly status: DispatchRecordStatus;
|
|
61
|
-
readonly stage: string | null;
|
|
62
|
-
/**
|
|
63
|
-
* Slice 2026-07-29-worktree-l2-extended Part 3.A + Part 4.C: the
|
|
64
|
-
* worktree lease id stamped on this dispatch (via `peaks sub-agent
|
|
65
|
-
* dispatch --isolation worktree`). The release hook (see
|
|
66
|
-
* markCompleted + `peaks sub-agent heartbeat --status done`)
|
|
67
|
-
* reads this field to auto-call `peaks worktree release` when
|
|
68
|
-
* the sub-agent finalizes. `null` means the dispatch did not
|
|
69
|
-
* request isolation and no release will fire. Persisted for
|
|
70
|
-
* audit + idempotency so a re-read of an old record still
|
|
71
|
-
* surfaces the lease id even if the on-disk lease file has since
|
|
72
|
-
* been gc'd.
|
|
73
|
-
*
|
|
74
|
-
* v3 (Part 4.C) makes this structurally required. v2 records
|
|
75
|
-
* missing the field upgrade to `null` on read (see
|
|
76
|
-
* `upgradeRecord`).
|
|
77
|
-
*/
|
|
78
|
-
readonly leaseId: string | null;
|
|
79
|
-
/**
|
|
80
|
-
* Slice 2026-07-29-worktree-l2-extended Part 7: ISO timestamp of
|
|
81
|
-
* when the isolation mode was set up. For `--isolation worktree`
|
|
82
|
-
* this is the moment `peaks worktree spawn` returned; for
|
|
83
|
-
* `--isolation container` (Part 8) it's when the container
|
|
84
|
-
* runtime reported the container as running. `null` when the
|
|
85
|
-
* dispatch did not request isolation. Lets the dashboard
|
|
86
|
-
* compute isolation duration (now - isolationStartedAt) without
|
|
87
|
-
* cross-referencing the metrics stream.
|
|
88
|
-
*/
|
|
89
|
-
readonly isolationStartedAt: string | null;
|
|
90
|
-
/**
|
|
91
|
-
* Slice 2026-08-01-subagent-merge-and-e2e (Task 7): v3.2 schema
|
|
92
|
-
* bump. One entry per pid the parent best-effort-killed during
|
|
93
|
-
* the service-shutdown phase of the merge-back pipeline (see
|
|
94
|
-
* src/services/dispatch/service-shutdown.ts). Empty array when
|
|
95
|
-
* the sub-agent did not register any services. The shape is the
|
|
96
|
-
* union of ServiceKillOutcome (skipped=false) and
|
|
97
|
-
* ServiceKillSkipped (skipped=true) — kept as a plain object
|
|
98
|
-
* here so the on-disk schema does not lock onto the helper's
|
|
99
|
-
* narrower union. The reader (merge-back-runner.ts) interprets
|
|
100
|
-
* each entry based on the `skipped` field.
|
|
101
|
-
*/
|
|
102
|
-
readonly serviceKill: ReadonlyArray<{
|
|
103
|
-
readonly pid: number;
|
|
104
|
-
readonly name: string;
|
|
105
|
-
readonly signal: string;
|
|
106
|
-
readonly exitCode: number | null;
|
|
107
|
-
readonly skipped?: boolean;
|
|
108
|
-
readonly reason?: string;
|
|
109
|
-
}>;
|
|
110
|
-
/**
|
|
111
|
-
* Slice 2026-08-01-subagent-merge-and-e2e (Task 7): v3.2 schema
|
|
112
|
-
* bump. Counts how many merge attempts the parent session has
|
|
113
|
-
* made against this dispatch's branch. The conflict-replay
|
|
114
|
-
* orchestrator bumps this on each retry (bounded to ONE re-dispatch
|
|
115
|
-
* per merge attempt; multi-conflict cases escalate). Persisted so
|
|
116
|
-
* the dashboard can render the retry count without replaying the
|
|
117
|
-
* merge transcript.
|
|
118
|
-
*/
|
|
119
|
-
readonly mergeBackAttempts: number;
|
|
120
|
-
/**
|
|
121
|
-
* Slice 4.0.8 RD §4 D4c (presence-lease-graph): the workflow id +
|
|
122
|
-
* graph node id + graphRef this dispatch is bound to. Persisted
|
|
123
|
-
* directly in the dispatch record (NOT in a sidecar) so the
|
|
124
|
-
* envelope-writer `markCompleted` can auto-transition the bound
|
|
125
|
-
* graph node to `envelope-received` with `ackStatus=pending` in
|
|
126
|
-
* one protected update.
|
|
127
|
-
*
|
|
128
|
-
* The schema bump from `3.2 → 4.0.0` is BREAKING in the sense
|
|
129
|
-
* that the literal type is narrowed. The optional `?` keeps back-
|
|
130
|
-
* compat for old records that pre-date the binding (the
|
|
131
|
-
* `upgradeRecord` reader defaults them to `null`).
|
|
132
|
-
*/
|
|
133
|
-
readonly workflowId: string | null;
|
|
134
|
-
readonly graphNodeId: string | null;
|
|
135
|
-
readonly graphRef: string | null;
|
|
136
|
-
/**
|
|
137
|
-
* Phase A Task 8: dispatch execution mode. `in-process` is the
|
|
138
|
-
* current behavior (LLM-side runner, no separate OS process).
|
|
139
|
-
* `detached` is the new path: `peaks sub-agent dispatch --mode
|
|
140
|
-
* detached` spawns a real child OS process running a different
|
|
141
|
-
* LLM vendor (claude / codex / copilot) and reports back via
|
|
142
|
-
* the dispatch record. v4.1.0 is the additive bump; legacy v4.0.0
|
|
143
|
-
* records upgrade to `in-process` on read.
|
|
144
|
-
*/
|
|
145
|
-
readonly mode: 'in-process' | 'detached';
|
|
146
|
-
/**
|
|
147
|
-
* Phase A Task 8: vendor id when `mode='detached'`. `null` when
|
|
148
|
-
* the dispatch is in-process. Reserved for future use; current
|
|
149
|
-
* detached sub-agents use `claude` but the schema also accepts
|
|
150
|
-
* `codex` and `copilot` for the vendor-neutral adapter layer.
|
|
151
|
-
*/
|
|
152
|
-
readonly vendor: 'claude' | 'codex' | 'copilot' | null;
|
|
153
|
-
/**
|
|
154
|
-
* Phase A Task 8: G8 autoCompact events accumulated by the child
|
|
155
|
-
* LLM during a detached run. Each event records the threshold
|
|
156
|
-
* that fired (0.85 = first warning, 0.95 = second warning) plus
|
|
157
|
-
* token counts before/after. Empty for in-process dispatches
|
|
158
|
-
* and for legacy records upgraded on read.
|
|
159
|
-
*/
|
|
160
|
-
readonly autoCompactEvents: ReadonlyArray<{
|
|
161
|
-
readonly at: number;
|
|
162
|
-
readonly threshold: '0.85' | '0.95';
|
|
163
|
-
readonly tokensBefore: number;
|
|
164
|
-
readonly tokensAfter: number;
|
|
165
|
-
readonly scratchFile?: string;
|
|
166
|
-
}>;
|
|
167
|
-
/**
|
|
168
|
-
* Phase A Task 8: G8 token-usage accounting for detached sub-agents.
|
|
169
|
-
* Detached runs have "unlimited spend but recorded" semantics —
|
|
170
|
-
* the cost is recorded for audit but not enforced. `null` when
|
|
171
|
-
* the dispatch is in-process (no detached accounting) or for
|
|
172
|
-
* legacy records upgraded on read.
|
|
173
|
-
*/
|
|
174
|
-
readonly tokenUsage: {
|
|
175
|
-
readonly promptTokens: number;
|
|
176
|
-
readonly completionTokens: number;
|
|
177
|
-
readonly totalCostUsd?: number;
|
|
178
|
-
} | null;
|
|
179
|
-
}
|
|
180
|
-
/** Input for the initial write. */
|
|
181
|
-
export type WriteInitialDispatchInput = {
|
|
182
|
-
projectRoot: string;
|
|
183
|
-
sessionId: string;
|
|
184
|
-
requestId: string;
|
|
185
|
-
role: string;
|
|
186
|
-
prompt: string;
|
|
187
|
-
toolCall: SubAgentToolCall;
|
|
188
|
-
batchId: string;
|
|
189
|
-
/** Override the timestamp (testing). */
|
|
190
|
-
now?: () => Date;
|
|
191
|
-
/**
|
|
192
|
-
* Slice 2026-07-29-worktree-l2-extended Part 3.A: the worktree
|
|
193
|
-
* lease id this dispatch owns (set by `peaks sub-agent dispatch
|
|
194
|
-
* --isolation worktree`). Persisted so the finalize-time release
|
|
195
|
-
* hook in `markCompleted` can fire even after the dispatch
|
|
196
|
-
* process exits. Optional; absent when the dispatch did not
|
|
197
|
-
* request isolation.
|
|
198
|
-
*/
|
|
199
|
-
leaseId?: string | null;
|
|
200
|
-
/**
|
|
201
|
-
* Slice 2026-07-29-worktree-l2-extended Part 7: ISO timestamp
|
|
202
|
-
* when the isolation mode was set up. Optional on the input
|
|
203
|
-
* (defaults to `null`); dispatch-commands.ts passes the spawn
|
|
204
|
-
* time when `--isolation` is requested.
|
|
205
|
-
*/
|
|
206
|
-
isolationStartedAt?: string | null;
|
|
207
|
-
/**
|
|
208
|
-
* Slice 4.0.8: workflow graph binding for the dispatch. Defaults
|
|
209
|
-
* to `null` so a non-graph dispatch (legacy CLI flow, ad-hoc
|
|
210
|
-
* dispatch) still writes a v4.0.0 record.
|
|
211
|
-
*/
|
|
212
|
-
workflowId?: string | null;
|
|
213
|
-
graphNodeId?: string | null;
|
|
214
|
-
graphRef?: string | null;
|
|
215
|
-
/**
|
|
216
|
-
* Phase A Task 8: dispatch execution mode. Default `'in-process'`
|
|
217
|
-
* preserves the current LLM-side runner behavior. `'detached'`
|
|
218
|
-
* triggers the new real-OS-process path via
|
|
219
|
-
* `peaks sub-agent dispatch --mode detached`.
|
|
220
|
-
*/
|
|
221
|
-
mode?: 'in-process' | 'detached';
|
|
222
|
-
/**
|
|
223
|
-
* Phase A Task 8: vendor id when `mode='detached'`. Required by
|
|
224
|
-
* the adapter layer to know which CLI / runtime to spawn. The
|
|
225
|
-
* schema accepts the three vendors peaks-loop has adapters for
|
|
226
|
-
* (claude / codex / copilot). Ignored when `mode='in-process'`.
|
|
227
|
-
*/
|
|
228
|
-
vendor?: 'claude' | 'codex' | 'copilot';
|
|
229
|
-
/**
|
|
230
|
-
* Phase A Task 8: G8 autoCompact events accumulated by the child
|
|
231
|
-
* LLM. Optional on input — most dispatches start with an empty
|
|
232
|
-
* array and the detached runner appends events as they fire.
|
|
233
|
-
*/
|
|
234
|
-
autoCompactEvents?: Array<{
|
|
235
|
-
at: number;
|
|
236
|
-
threshold: '0.85' | '0.95';
|
|
237
|
-
tokensBefore: number;
|
|
238
|
-
tokensAfter: number;
|
|
239
|
-
scratchFile?: string;
|
|
240
|
-
}>;
|
|
241
|
-
/**
|
|
242
|
-
* Phase A Task 8: G8 token-usage accounting. Detached runs
|
|
243
|
-
* record spend for audit (unlimited, but persisted). Optional
|
|
244
|
-
* on input; the detached runner fills this in as it streams
|
|
245
|
-
* usage from the vendor API.
|
|
246
|
-
*/
|
|
247
|
-
tokenUsage?: {
|
|
248
|
-
promptTokens: number;
|
|
249
|
-
completionTokens: number;
|
|
250
|
-
totalCostUsd?: number;
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
/** Heartbeat write input. */
|
|
254
|
-
export type AppendHeartbeatInput = {
|
|
255
|
-
recordPath: string;
|
|
256
|
-
status: HeartbeatStatus;
|
|
257
|
-
progress: number;
|
|
258
|
-
note?: string;
|
|
259
|
-
now?: () => Date;
|
|
260
|
-
};
|
|
261
|
-
/** Lifecycle transition input. */
|
|
262
|
-
export type LifecycleInput = {
|
|
263
|
-
recordPath: string;
|
|
264
|
-
outcome: DispatchOutcome;
|
|
265
|
-
status: DispatchRecordStatus;
|
|
266
|
-
artifactPaths?: readonly string[];
|
|
267
|
-
now?: () => Date;
|
|
268
|
-
/**
|
|
269
|
-
* Slice 2026-06-23-audit-4th #A4: trusted project root. Required
|
|
270
|
-
* so the active-dispatches index can be updated without deriving
|
|
271
|
-
* the root from the recordPath (the same anti-pattern that
|
|
272
|
-
* audit-3rd #1 fixed for heartbeat). The CLI / LLM-side runner
|
|
273
|
-
* passes this from `--project` or `process.cwd()`.
|
|
274
|
-
*/
|
|
275
|
-
projectRoot?: string;
|
|
276
|
-
};
|
|
2
|
+
import type { AppendHeartbeatInput, DispatchRecord, Heartbeat, LifecycleInput, WriteInitialDispatchInput } from './dispatch-record-types.js';
|
|
3
|
+
export type { AppendHeartbeatInput, DispatchOutcome, DispatchRecord, DispatchRecordStatus, Heartbeat, HeartbeatStatus, LifecycleInput, WriteInitialDispatchInput } from './dispatch-record-types.js';
|
|
4
|
+
export { isDispatchStatus, isOutcome } from './dispatch-record-upgrade.js';
|
|
277
5
|
/** Write a new dispatch record (G2 + G5 + G6). Returns the absolute path. */
|
|
278
6
|
export declare function writeInitialDispatchRecord(input: WriteInitialDispatchInput): {
|
|
279
7
|
path: string;
|
|
@@ -386,6 +114,3 @@ export declare function setStage(input: {
|
|
|
386
114
|
export declare function readRecord(recordPath: string): DispatchRecord;
|
|
387
115
|
/** Read multiple records from a list of paths. Tolerates missing files. */
|
|
388
116
|
export declare function readRecords(paths: readonly string[]): DispatchRecord[];
|
|
389
|
-
declare function isDispatchStatus(v: unknown): v is DispatchRecordStatus;
|
|
390
|
-
declare function isOutcome(v: unknown): v is DispatchOutcome;
|
|
391
|
-
export { isDispatchStatus, isOutcome };
|
|
@@ -30,21 +30,9 @@ import { assertSafeDispatchRecordPath, dispatchRecordPath } from '../security/sa
|
|
|
30
30
|
import { withFileLockSync } from 'peaks-loop-shared-channel';
|
|
31
31
|
import { isStageLabel } from './stage-enum.js';
|
|
32
32
|
import { emitLeaseEvent } from '../observability/observability-service.js';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* no-magic-numbers rule stops flagging the writer pipeline.
|
|
37
|
-
*/
|
|
38
|
-
const BYTES_PER_KB = 1024;
|
|
39
|
-
const MAX_PROMPT_KB = 256;
|
|
40
|
-
const MAX_PROMPT_BYTES = MAX_PROMPT_KB * BYTES_PER_KB;
|
|
41
|
-
const NOTE_MAX_CHARS = 200;
|
|
42
|
-
const MS_PER_SECOND = 1_000;
|
|
43
|
-
const SECONDS_PER_MINUTE = 60;
|
|
44
|
-
const MINUTES_PER_HOUR = 60;
|
|
45
|
-
const HOURS_PER_DAY = 24;
|
|
46
|
-
const MS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
|
|
47
|
-
const REDACTION_MAX_SCAN_DEPTH = 20;
|
|
33
|
+
import { upgradeRecord } from './dispatch-record-upgrade.js';
|
|
34
|
+
import { MAX_PROMPT_BYTES, MS_PER_DAY, NOTE_MAX_CHARS } from './dispatch-record-types.js';
|
|
35
|
+
export { isDispatchStatus, isOutcome } from './dispatch-record-upgrade.js';
|
|
48
36
|
/** Write a new dispatch record (G2 + G5 + G6). Returns the absolute path. */
|
|
49
37
|
export function writeInitialDispatchRecord(input) {
|
|
50
38
|
const { projectRoot, sessionId, requestId, role, prompt, toolCall, batchId } = input;
|
|
@@ -674,236 +662,6 @@ export function readRecords(paths) {
|
|
|
674
662
|
}
|
|
675
663
|
return out;
|
|
676
664
|
}
|
|
677
|
-
function upgradeRecord(parsed) {
|
|
678
|
-
if (!isObject(parsed)) {
|
|
679
|
-
throw new Error('Dispatch record root must be an object');
|
|
680
|
-
}
|
|
681
|
-
const obj = parsed;
|
|
682
|
-
// Slice 4.0.8: 3.2 → 4.0.0 schema bump. Phase A Task 8: 4.0.0 → 4.1.0
|
|
683
|
-
// (additive). The literal type narrows to '4.1.0' but legacy v4.0.0 /
|
|
684
|
-
// v3.2 / v3.1 / 3 / 2 / 1 records are accepted transparently and
|
|
685
|
-
// upgraded on read.
|
|
686
|
-
const rawVersion = obj.version;
|
|
687
|
-
if (rawVersion !== '4.1.0' && rawVersion !== '4.0.0' && rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
|
|
688
|
-
throw new Error(`Dispatch record version mismatch: expected '4.1.0', '4.0.0', '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
|
|
689
|
-
'The v1 → v4.1.0 migration is in-file; records from much older or newer builds must be regenerated.');
|
|
690
|
-
}
|
|
691
|
-
const legacy = parseUpgradeRecordLegacyFields(obj);
|
|
692
|
-
const migration = parseUpgradeRecordMigrationFields(obj);
|
|
693
|
-
return {
|
|
694
|
-
version: '4.1.0',
|
|
695
|
-
createdAt: legacy.createdAt,
|
|
696
|
-
completedAt: legacy.completedAt,
|
|
697
|
-
outcome: legacy.outcome,
|
|
698
|
-
artifactPaths: legacy.artifactPaths,
|
|
699
|
-
disposed: legacy.disposed,
|
|
700
|
-
disposedAt: legacy.disposedAt,
|
|
701
|
-
role: legacy.role,
|
|
702
|
-
requestId: legacy.requestId,
|
|
703
|
-
sessionId: legacy.sessionId,
|
|
704
|
-
prompt: legacy.prompt,
|
|
705
|
-
toolCall: legacy.toolCall,
|
|
706
|
-
batchId: legacy.batchId,
|
|
707
|
-
heartbeats: legacy.heartbeats,
|
|
708
|
-
lastBeatAt: legacy.lastBeatAt,
|
|
709
|
-
status: legacy.status,
|
|
710
|
-
stage: migration.stage,
|
|
711
|
-
leaseId: migration.leaseId,
|
|
712
|
-
isolationStartedAt: migration.isolationStartedAt,
|
|
713
|
-
serviceKill: migration.serviceKill,
|
|
714
|
-
mergeBackAttempts: migration.mergeBackAttempts,
|
|
715
|
-
workflowId: migration.workflowId,
|
|
716
|
-
graphNodeId: migration.graphNodeId,
|
|
717
|
-
graphRef: migration.graphRef,
|
|
718
|
-
// Phase A Task 8: detached sub-agent fields. Legacy records
|
|
719
|
-
// (pre-4.1.0) default mode='in-process', vendor=null,
|
|
720
|
-
// autoCompactEvents=[], tokenUsage=null. See
|
|
721
|
-
// parseUpgradeRecordMigrationFields for the per-field
|
|
722
|
-
// validation rules.
|
|
723
|
-
mode: migration.mode,
|
|
724
|
-
vendor: migration.vendor,
|
|
725
|
-
autoCompactEvents: migration.autoCompactEvents,
|
|
726
|
-
tokenUsage: migration.tokenUsage
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Parse the v1..v3.2 core fields of a legacy dispatch record.
|
|
731
|
-
* PRD-002b slice 6: extracted from `upgradeRecord` so the reader
|
|
732
|
-
* stays under the `max-lines-per-function: 50` ESLint ceiling.
|
|
733
|
-
* Behavior is byte-identical to the previous inline block.
|
|
734
|
-
*/
|
|
735
|
-
function parseUpgradeRecordLegacyFields(obj) {
|
|
736
|
-
const role = stringField(obj, 'role');
|
|
737
|
-
const requestId = stringField(obj, 'requestId');
|
|
738
|
-
const sessionId = stringField(obj, 'sessionId');
|
|
739
|
-
const prompt = stringField(obj, 'prompt');
|
|
740
|
-
// Slice 2026-06-23-audit-4th #C2: preserve toolCallVersion on read.
|
|
741
|
-
// Pre-versioning records default to '2.0.0' (the pre-#C2 implicit
|
|
742
|
-
// shape; matches the version stamped by every current dispatcher).
|
|
743
|
-
const rawToolCall = obj.toolCall;
|
|
744
|
-
if (!isObject(rawToolCall) || typeof rawToolCall.name !== 'string') {
|
|
745
|
-
throw new Error('Dispatch record toolCall must be { name, args }');
|
|
746
|
-
}
|
|
747
|
-
const toolCall = {
|
|
748
|
-
name: rawToolCall.name,
|
|
749
|
-
args: (isObject(rawToolCall.args) ? rawToolCall.args : {}),
|
|
750
|
-
...(typeof rawToolCall.toolCallVersion === 'string' ? { toolCallVersion: rawToolCall.toolCallVersion } : { toolCallVersion: '2.0.0' })
|
|
751
|
-
};
|
|
752
|
-
const createdAt = stringField(obj, 'createdAt');
|
|
753
|
-
const heartbeats = Array.isArray(obj.heartbeats)
|
|
754
|
-
? obj.heartbeats.filter(isValidHeartbeat)
|
|
755
|
-
: [];
|
|
756
|
-
const lastBeatAt = typeof obj.lastBeatAt === 'string' ? obj.lastBeatAt : null;
|
|
757
|
-
// Slice 2026-07-29-dispatch-stall-governance / S1 (UQ-1) — `no-execution`
|
|
758
|
-
// keeps its natural "dispatched, never executed" reading; an unparseable
|
|
759
|
-
// status field now resolves to a *distinct* `unreadable` label so the
|
|
760
|
-
// caller can tell "corrupt record" apart from "record written, no first
|
|
761
|
-
// heartbeat" (which is the new `never-started` state).
|
|
762
|
-
const status = isDispatchStatus(obj.status)
|
|
763
|
-
? obj.status
|
|
764
|
-
: 'unreadable';
|
|
765
|
-
const completedAt = typeof obj.completedAt === 'string' ? obj.completedAt : null;
|
|
766
|
-
const outcome = isOutcome(obj.outcome) ? obj.outcome : 'no-execution';
|
|
767
|
-
const artifactPaths = Array.isArray(obj.artifactPaths)
|
|
768
|
-
? obj.artifactPaths.filter((p) => typeof p === 'string')
|
|
769
|
-
: [];
|
|
770
|
-
const disposed = obj.disposed === true;
|
|
771
|
-
const disposedAt = typeof obj.disposedAt === 'string' ? obj.disposedAt : null;
|
|
772
|
-
const batchId = typeof obj.batchId === 'string' && obj.batchId.length > 0
|
|
773
|
-
? obj.batchId
|
|
774
|
-
: 'legacy-batch';
|
|
775
|
-
return {
|
|
776
|
-
role,
|
|
777
|
-
requestId,
|
|
778
|
-
sessionId,
|
|
779
|
-
prompt,
|
|
780
|
-
toolCall,
|
|
781
|
-
createdAt,
|
|
782
|
-
heartbeats,
|
|
783
|
-
lastBeatAt,
|
|
784
|
-
status,
|
|
785
|
-
completedAt,
|
|
786
|
-
outcome,
|
|
787
|
-
artifactPaths,
|
|
788
|
-
disposed,
|
|
789
|
-
disposedAt,
|
|
790
|
-
batchId
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* Parse the post-v3 migration fields of a legacy dispatch record.
|
|
795
|
-
* PRD-002b slice 6: extracted from `upgradeRecord` so the reader
|
|
796
|
-
* stays under the `max-lines-per-function: 50` ESLint ceiling.
|
|
797
|
-
* Behavior is byte-identical to the previous inline block.
|
|
798
|
-
*/
|
|
799
|
-
function parseUpgradeRecordMigrationFields(obj) {
|
|
800
|
-
return {
|
|
801
|
-
// Slice 2026-07-29-dispatch-stall-governance / S5 (AC-5.1 / PB-2)
|
|
802
|
-
// — legacy records (pre-slice) had no `stage` field. The reader
|
|
803
|
-
// defaults to `null` so the watch surface can tell "no stage ever
|
|
804
|
-
// emitted" apart from "stage: ''" (which is itself a *valid*
|
|
805
|
-
// round-trip through the writer — an empty stage is rejected by
|
|
806
|
-
// `setStage`, but a record that round-tripped through a non-strict
|
|
807
|
-
// tool would land here).
|
|
808
|
-
stage: typeof obj.stage === 'string' && obj.stage.length > 0 ? obj.stage : null,
|
|
809
|
-
// Slice 2026-07-29-worktree-l2-extended Part 3.A: legacy records
|
|
810
|
-
// have no `leaseId`; default to `null` so the auto-release hook
|
|
811
|
-
// in `markCompleted` is a clean no-op for them.
|
|
812
|
-
leaseId: typeof obj.leaseId === 'string' && /^[a-f0-9]{16}$/.test(obj.leaseId)
|
|
813
|
-
? obj.leaseId
|
|
814
|
-
: null,
|
|
815
|
-
// Slice 2026-07-29-worktree-l2-extended Part 7: v3 → v3.1
|
|
816
|
-
// migration. Legacy records have no `isolationStartedAt`; default
|
|
817
|
-
// to `null`. v3.1 readers can treat the field as opt-in.
|
|
818
|
-
isolationStartedAt: typeof obj.isolationStartedAt === 'string' && obj.isolationStartedAt.length > 0
|
|
819
|
-
? obj.isolationStartedAt
|
|
820
|
-
: null,
|
|
821
|
-
// Slice 2026-08-01-subagent-merge-and-e2e (Task 7): v3.1 → v3.2
|
|
822
|
-
// migration. Legacy v3.1 records have no `serviceKill` or
|
|
823
|
-
// `mergeBackAttempts` fields. Default to [] and 0 so the
|
|
824
|
-
// merge-back-runner (Task 9) can read either schema on disk.
|
|
825
|
-
serviceKill: Array.isArray(obj.serviceKill)
|
|
826
|
-
? (obj.serviceKill.filter((e) => {
|
|
827
|
-
if (typeof e !== 'object' || e === null)
|
|
828
|
-
return false;
|
|
829
|
-
const o = e;
|
|
830
|
-
return typeof o.pid === 'number' && typeof o.name === 'string' && typeof o.signal === 'string' && (o.exitCode === null || typeof o.exitCode === 'number');
|
|
831
|
-
}))
|
|
832
|
-
: [],
|
|
833
|
-
mergeBackAttempts: typeof obj.mergeBackAttempts === 'number' && Number.isFinite(obj.mergeBackAttempts) && obj.mergeBackAttempts >= 0
|
|
834
|
-
? Math.floor(obj.mergeBackAttempts)
|
|
835
|
-
: 0,
|
|
836
|
-
// Slice 4.0.8: 3.2 → 4.0.0 migration. v3.2 records on disk
|
|
837
|
-
// pre-date the workflow-graph binding; default all three
|
|
838
|
-
// fields to `null` so a legacy record upgrades transparently.
|
|
839
|
-
workflowId: typeof obj.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.workflowId) ? obj.workflowId : null,
|
|
840
|
-
graphNodeId: typeof obj.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.graphNodeId) ? obj.graphNodeId : null,
|
|
841
|
-
graphRef: typeof obj.graphRef === 'string' ? obj.graphRef : null,
|
|
842
|
-
// Phase A Task 8: 4.0.0 → 4.1.0 migration. Pre-4.1.0 records
|
|
843
|
-
// have no mode / vendor / autoCompactEvents / tokenUsage
|
|
844
|
-
// fields. Default to safe in-process / null / [] / null so
|
|
845
|
-
// legacy records upgrade transparently without breaking
|
|
846
|
-
// consumers (e.g. the dashboard, the merge-back-runner).
|
|
847
|
-
mode: obj.mode === 'detached' ? 'detached' : 'in-process',
|
|
848
|
-
vendor: obj.vendor === 'claude' || obj.vendor === 'codex' || obj.vendor === 'copilot' ? obj.vendor : null,
|
|
849
|
-
autoCompactEvents: Array.isArray(obj.autoCompactEvents)
|
|
850
|
-
? obj.autoCompactEvents.filter((e) => typeof e?.at === 'number' &&
|
|
851
|
-
(e?.threshold === '0.85' || e?.threshold === '0.95') &&
|
|
852
|
-
typeof e?.tokensBefore === 'number' &&
|
|
853
|
-
typeof e?.tokensAfter === 'number')
|
|
854
|
-
: [],
|
|
855
|
-
tokenUsage: typeof obj.tokenUsage === 'object' && obj.tokenUsage !== null && typeof obj.tokenUsage.promptTokens === 'number' && typeof obj.tokenUsage.completionTokens === 'number'
|
|
856
|
-
? {
|
|
857
|
-
promptTokens: obj.tokenUsage.promptTokens,
|
|
858
|
-
completionTokens: obj.tokenUsage.completionTokens,
|
|
859
|
-
...(typeof obj.tokenUsage.totalCostUsd === 'number'
|
|
860
|
-
? { totalCostUsd: obj.tokenUsage.totalCostUsd }
|
|
861
|
-
: {}),
|
|
862
|
-
}
|
|
863
|
-
: null
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
function isObject(v) {
|
|
867
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
868
|
-
}
|
|
869
|
-
function stringField(obj, key) {
|
|
870
|
-
const v = obj[key];
|
|
871
|
-
if (typeof v !== 'string') {
|
|
872
|
-
throw new Error(`Dispatch record field '${key}' must be a string (got ${typeof v})`);
|
|
873
|
-
}
|
|
874
|
-
return v;
|
|
875
|
-
}
|
|
876
|
-
function isValidHeartbeat(v) {
|
|
877
|
-
if (!isObject(v))
|
|
878
|
-
return false;
|
|
879
|
-
return (typeof v.at === 'string' &&
|
|
880
|
-
isHeartbeatStatus(v.status) &&
|
|
881
|
-
typeof v.progress === 'number' &&
|
|
882
|
-
(v.note === null || typeof v.note === 'string'));
|
|
883
|
-
}
|
|
884
|
-
function isHeartbeatStatus(v) {
|
|
885
|
-
return (v === 'queued' || v === 'running' || v === 'finalizing' ||
|
|
886
|
-
v === 'done' || v === 'failed' || v === 'stale' ||
|
|
887
|
-
// Slice 2026-07-29-dispatch-stall-governance / S2 — accept the
|
|
888
|
-
// S1 terminal members so a sub-agent can report `cancelled`,
|
|
889
|
-
// `no-execution`, `never-started`, or `unreadable` through the
|
|
890
|
-
// heartbeat CLI.
|
|
891
|
-
v === 'cancelled' || v === 'no-execution' ||
|
|
892
|
-
v === 'never-started' || v === 'unreadable');
|
|
893
|
-
}
|
|
894
|
-
function isDispatchStatus(v) {
|
|
895
|
-
return (v === 'queued' || v === 'running' || v === 'finalizing' ||
|
|
896
|
-
v === 'done' || v === 'failed' || v === 'cancelled' ||
|
|
897
|
-
v === 'no-execution' || v === 'stale' ||
|
|
898
|
-
// Slice 2026-07-29-dispatch-stall-governance / S1 — accept the two
|
|
899
|
-
// new terminal members from the startup-timeout service.
|
|
900
|
-
v === 'never-started' || v === 'unreadable');
|
|
901
|
-
}
|
|
902
|
-
function isOutcome(v) {
|
|
903
|
-
return (v === 'success' || v === 'failed' || v === 'timeout' ||
|
|
904
|
-
v === 'cancelled' || v === 'no-execution');
|
|
905
|
-
}
|
|
906
|
-
export { isDispatchStatus, isOutcome };
|
|
907
665
|
function writeAtomic(path, record) {
|
|
908
666
|
const dir = dirname(path);
|
|
909
667
|
// Slice 2026-06-23-audit-3rd #11: skip mkdirSync when the dir already
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
|
|
3
|
+
* §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
|
|
4
|
+
* `git ls-files <glob>` against `projectRoot` and returns the matching
|
|
5
|
+
* tracked file paths (relative to projectRoot). Empty array when no
|
|
6
|
+
* files match (e.g. untracked new file, wrong glob, not a git repo).
|
|
7
|
+
*
|
|
8
|
+
* Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
|
|
9
|
+
* is "the file the sub-agent claims to have written must ACTUALLY be
|
|
10
|
+
* tracked by git" — `git ls-files` enforces that contract; `fs.glob`
|
|
11
|
+
* would happily return untracked-but-on-disk files (false-positive
|
|
12
|
+
* for the fake-green gate).
|
|
13
|
+
*
|
|
14
|
+
* Failure modes (best-effort, never throws):
|
|
15
|
+
* - git not on PATH → empty array (`ENOENT` swallowed)
|
|
16
|
+
* - not a git repo → empty array (git exits non-zero)
|
|
17
|
+
* - glob matches zero tracked files → empty array
|
|
18
|
+
*
|
|
19
|
+
* Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
|
|
20
|
+
* The export is intentional — the helper has zero side effects and
|
|
21
|
+
* keeps the dispatch action handler small.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runGitLsFiles(projectRoot: string, glob: string): readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* `dispatchSubAgent` is the thin programmatic wrapper the integration
|
|
26
|
+
* test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
|
|
27
|
+
* It enforces --graph-node required BEFORE any record write so a
|
|
28
|
+
* caller can't bypass the CLI's requiredOption guard. Throws a typed
|
|
29
|
+
* error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function dispatchSubAgent(input: {
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
role: string;
|
|
34
|
+
prompt: string;
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
graphNode?: string;
|
|
37
|
+
workflowId?: string;
|
|
38
|
+
graphRef?: string;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
role: string;
|
|
41
|
+
toolCall: unknown;
|
|
42
|
+
dispatchRecordPath: string | null;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
|
|
3
|
+
* §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
|
|
4
|
+
* `git ls-files <glob>` against `projectRoot` and returns the matching
|
|
5
|
+
* tracked file paths (relative to projectRoot). Empty array when no
|
|
6
|
+
* files match (e.g. untracked new file, wrong glob, not a git repo).
|
|
7
|
+
*
|
|
8
|
+
* Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
|
|
9
|
+
* is "the file the sub-agent claims to have written must ACTUALLY be
|
|
10
|
+
* tracked by git" — `git ls-files` enforces that contract; `fs.glob`
|
|
11
|
+
* would happily return untracked-but-on-disk files (false-positive
|
|
12
|
+
* for the fake-green gate).
|
|
13
|
+
*
|
|
14
|
+
* Failure modes (best-effort, never throws):
|
|
15
|
+
* - git not on PATH → empty array (`ENOENT` swallowed)
|
|
16
|
+
* - not a git repo → empty array (git exits non-zero)
|
|
17
|
+
* - glob matches zero tracked files → empty array
|
|
18
|
+
*
|
|
19
|
+
* Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
|
|
20
|
+
* The export is intentional — the helper has zero side effects and
|
|
21
|
+
* keeps the dispatch action handler small.
|
|
22
|
+
*/
|
|
23
|
+
export function runGitLsFiles(projectRoot, glob) {
|
|
24
|
+
try {
|
|
25
|
+
const { execFileSync } = require('node:child_process');
|
|
26
|
+
const stdout = execFileSync('git', ['ls-files', '--', glob], { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
27
|
+
return stdout.split('\n').filter((line) => line.length > 0);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/* ---------- Slice 4.0.8 RD §4 D4c: programmatic dispatcher ---------- */
|
|
34
|
+
/**
|
|
35
|
+
* `dispatchSubAgent` is the thin programmatic wrapper the integration
|
|
36
|
+
* test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
|
|
37
|
+
* It enforces --graph-node required BEFORE any record write so a
|
|
38
|
+
* caller can't bypass the CLI's requiredOption guard. Throws a typed
|
|
39
|
+
* error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
|
|
40
|
+
*/
|
|
41
|
+
export async function dispatchSubAgent(input) {
|
|
42
|
+
if (typeof input.graphNode !== 'string' || input.graphNode.length === 0) {
|
|
43
|
+
const err = new Error('PEAKS_GRAPH_NODE_REQUIRED: --graph-node is required (RD §4 D4c)');
|
|
44
|
+
err.code = 'PEAKS_GRAPH_NODE_REQUIRED';
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
// The integration test only checks the rejection path; the success
|
|
48
|
+
// path is exercised by the existing CLI command. Return a minimal
|
|
49
|
+
// stub so any future programmatic caller has a stable surface.
|
|
50
|
+
return {
|
|
51
|
+
role: input.role,
|
|
52
|
+
toolCall: null,
|
|
53
|
+
dispatchRecordPath: null,
|
|
54
|
+
};
|
|
55
|
+
}
|