@suwujs/king-ai 0.2.0

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 (104) hide show
  1. package/README.md +96 -0
  2. package/dist/src/agent-config-validation.d.ts +9 -0
  3. package/dist/src/agent-config-validation.js +30 -0
  4. package/dist/src/api.d.ts +4 -0
  5. package/dist/src/api.js +48 -0
  6. package/dist/src/attachments.d.ts +45 -0
  7. package/dist/src/attachments.js +322 -0
  8. package/dist/src/cli.d.ts +20 -0
  9. package/dist/src/cli.js +1697 -0
  10. package/dist/src/config.d.ts +3 -0
  11. package/dist/src/config.js +20 -0
  12. package/dist/src/cron.d.ts +11 -0
  13. package/dist/src/cron.js +65 -0
  14. package/dist/src/daemon.d.ts +36 -0
  15. package/dist/src/daemon.js +373 -0
  16. package/dist/src/engine.d.ts +32 -0
  17. package/dist/src/engine.js +1014 -0
  18. package/dist/src/heartbeat.d.ts +18 -0
  19. package/dist/src/heartbeat.js +28 -0
  20. package/dist/src/host-api.d.ts +40 -0
  21. package/dist/src/host-api.js +59 -0
  22. package/dist/src/host-control.d.ts +48 -0
  23. package/dist/src/host-control.js +1279 -0
  24. package/dist/src/host-export.d.ts +50 -0
  25. package/dist/src/host-export.js +187 -0
  26. package/dist/src/host-feedback.d.ts +78 -0
  27. package/dist/src/host-feedback.js +178 -0
  28. package/dist/src/host-home.d.ts +13 -0
  29. package/dist/src/host-home.js +54 -0
  30. package/dist/src/host-ledger.d.ts +261 -0
  31. package/dist/src/host-ledger.js +554 -0
  32. package/dist/src/host-loop-events.d.ts +69 -0
  33. package/dist/src/host-loop-events.js +288 -0
  34. package/dist/src/host-permission.d.ts +36 -0
  35. package/dist/src/host-permission.js +180 -0
  36. package/dist/src/host-policy.d.ts +15 -0
  37. package/dist/src/host-policy.js +36 -0
  38. package/dist/src/host-run-executor.d.ts +13 -0
  39. package/dist/src/host-run-executor.js +221 -0
  40. package/dist/src/host-run-heartbeat.d.ts +40 -0
  41. package/dist/src/host-run-heartbeat.js +103 -0
  42. package/dist/src/host-run-layout.d.ts +17 -0
  43. package/dist/src/host-run-layout.js +387 -0
  44. package/dist/src/host-run-meta.d.ts +41 -0
  45. package/dist/src/host-run-meta.js +115 -0
  46. package/dist/src/host-run-spec.d.ts +149 -0
  47. package/dist/src/host-run-spec.js +465 -0
  48. package/dist/src/host-runs.d.ts +77 -0
  49. package/dist/src/host-runs.js +195 -0
  50. package/dist/src/host-sdk.d.ts +412 -0
  51. package/dist/src/host-sdk.js +628 -0
  52. package/dist/src/host-server.d.ts +26 -0
  53. package/dist/src/host-server.js +921 -0
  54. package/dist/src/host-timeline.d.ts +24 -0
  55. package/dist/src/host-timeline.js +161 -0
  56. package/dist/src/jsonl.d.ts +13 -0
  57. package/dist/src/jsonl.js +47 -0
  58. package/dist/src/lifecycle.d.ts +5 -0
  59. package/dist/src/lifecycle.js +18 -0
  60. package/dist/src/message-routing.d.ts +32 -0
  61. package/dist/src/message-routing.js +119 -0
  62. package/dist/src/paths.d.ts +19 -0
  63. package/dist/src/paths.js +26 -0
  64. package/dist/src/project-profile.d.ts +49 -0
  65. package/dist/src/project-profile.js +356 -0
  66. package/dist/src/remediation.d.ts +14 -0
  67. package/dist/src/remediation.js +114 -0
  68. package/dist/src/remote-devices.d.ts +41 -0
  69. package/dist/src/remote-devices.js +156 -0
  70. package/dist/src/remote-diagnostics.d.ts +39 -0
  71. package/dist/src/remote-diagnostics.js +199 -0
  72. package/dist/src/remote-ssh.d.ts +39 -0
  73. package/dist/src/remote-ssh.js +129 -0
  74. package/dist/src/run-stream.d.ts +57 -0
  75. package/dist/src/run-stream.js +119 -0
  76. package/dist/src/runner.d.ts +131 -0
  77. package/dist/src/runner.js +1161 -0
  78. package/dist/src/runtime-data.d.ts +68 -0
  79. package/dist/src/runtime-data.js +172 -0
  80. package/dist/src/service.d.ts +114 -0
  81. package/dist/src/service.js +631 -0
  82. package/dist/src/shared-skills.d.ts +26 -0
  83. package/dist/src/shared-skills.js +85 -0
  84. package/dist/src/shim.d.ts +1 -0
  85. package/dist/src/shim.js +64 -0
  86. package/dist/src/skill-check.d.ts +17 -0
  87. package/dist/src/skill-check.js +158 -0
  88. package/dist/src/sse.d.ts +9 -0
  89. package/dist/src/sse.js +36 -0
  90. package/dist/src/team-routing.d.ts +55 -0
  91. package/dist/src/team-routing.js +131 -0
  92. package/dist/src/team-workflow.d.ts +78 -0
  93. package/dist/src/team-workflow.js +253 -0
  94. package/dist/src/text.d.ts +7 -0
  95. package/dist/src/text.js +27 -0
  96. package/dist/src/types.d.ts +98 -0
  97. package/dist/src/types.js +1 -0
  98. package/dist/src/usage.d.ts +116 -0
  99. package/dist/src/usage.js +350 -0
  100. package/dist/src/workspace.d.ts +9 -0
  101. package/dist/src/workspace.js +56 -0
  102. package/dist/src/worktree.d.ts +47 -0
  103. package/dist/src/worktree.js +201 -0
  104. package/package.json +63 -0
@@ -0,0 +1,1279 @@
1
+ import { collectDoctorResults, doctorExitCode, formatDoctorReport } from "./daemon.js";
2
+ import { cronMatches } from "./cron.js";
3
+ import { exportHostArtifacts, planHostExport } from "./host-export.js";
4
+ import { formatHostFeedback, formatHostFeedbackSummary, listHostFeedback, recordHostFeedback, summarizeHostFeedback } from "./host-feedback.js";
5
+ import { buildHostStatusSnapshot, formatHostStatusSnapshot } from "./host-api.js";
6
+ import { buildHostAgenda, compactHostLedger, createHostCapsule, createHostTask, createHostWorkflowCard, formatHostAgenda, formatHostCapsules, formatHostLedgerCompact, formatHostTasks, formatHostWorkflowCards, listHostCapsules, listHostTasks, listHostWorkflowCards, updateHostCapsule, updateHostTask, updateHostWorkflowCard } from "./host-ledger.js";
7
+ import { appendHostLoopEvent, formatHostLoopEvents, readHostLoopEvents, readHostLoopResults } from "./host-loop-events.js";
8
+ import { formatHostRunHeartbeat, hostRunHeartbeatPathForOutputDir, readHostRunHeartbeat, writeHostRunHeartbeat } from "./host-run-heartbeat.js";
9
+ import { formatHostRunMeta, hostRunMetaPathForOutputDir, readHostRunMeta, updateHostRunMeta } from "./host-run-meta.js";
10
+ import { prepareHostRunLayout } from "./host-run-layout.js";
11
+ import { executeNextHostRunRequest, formatHostRunExecuteResult } from "./host-run-executor.js";
12
+ import { checkHostCommandPolicy, formatHostPolicyCheck } from "./host-policy.js";
13
+ import { evaluateHostCommandPermission, resolveActorRole, resolveHumanApproval, resolveTeamSpec } from "./host-permission.js";
14
+ import { normalizeReviewVerdict, planHandoff, selectOwnerRole } from "./team-routing.js";
15
+ import { createHostLaunchPlan, toJsonSafeHostLaunchPlan } from "./host-run-spec.js";
16
+ import { formatHostRunRequestSummary, formatHostRunRequests, getHostRunRequest, listHostRunRequests, submitHostRunRequest, updateHostRunRequest } from "./host-runs.js";
17
+ import { appendHostTimelineEvent, formatHostTimeline, previewText, readHostTimeline, summarizeHostCommandJson } from "./host-timeline.js";
18
+ import { readRunningState } from "./service.js";
19
+ import { formatUsageExpenses, formatUsageSummary, listUsageExpenses, summarizeAgentUsage, tokenBudgetFromEnv, usagePricingFromEnv } from "./usage.js";
20
+ import { buildUsageRuntimeData } from "./runtime-data.js";
21
+ import { deleteRemoteDevice, findRemoteDevice, listRemoteDeviceSummaries, loadRemoteDevicesConfig, normalizeRemoteDevicesConfig, saveRemoteDevicesConfig, setDefaultRemoteDevice, summarizeRemoteDevice, upsertRemoteDevice } from "./remote-devices.js";
22
+ import { formatRemoteDevices, formatRemoteResult, remoteFindLogs, remoteLogs, remotePg, remoteProbe, remoteProfile, remoteRedis, remoteRun } from "./remote-diagnostics.js";
23
+ const COMMANDS = [
24
+ { name: "status", description: "Return the app-facing daemon status snapshot.", destructive: false },
25
+ { name: "usage", description: "Summarize local agent run and token usage.", destructive: false },
26
+ { name: "expenses", description: "List estimated local agent run expenses by agent.", destructive: false },
27
+ { name: "events", description: "Return recent daemon lifecycle events.", destructive: false },
28
+ { name: "timeline", description: "Return recent host command audit events.", destructive: false },
29
+ { name: "policy", description: "Check host command safety policy and confirmation requirements.", destructive: false },
30
+ { name: "doctor", description: "Probe local Claude/Codex engine availability.", destructive: false },
31
+ { name: "plan-run", description: "Normalize a host app run request into a reproducible run plan.", destructive: false },
32
+ { name: "preflight", description: "Check whether a host app run request is ready to launch.", destructive: false },
33
+ { name: "prepare-run-layout", description: "Materialize the local host run layout after explicit confirmation.", destructive: true },
34
+ { name: "submit-run", description: "Persist a pending host app run request for local execution.", destructive: false },
35
+ { name: "run-requests", description: "List pending host app run requests.", destructive: false },
36
+ { name: "run-request", description: "Return one host app run request by id.", destructive: false },
37
+ { name: "update-run", description: "Append a lifecycle status update for a host app run request.", destructive: false },
38
+ { name: "cancel-run", description: "Cancel a queued or active host app run request.", destructive: false },
39
+ { name: "execute-run", description: "Consume one pending host app run request with a safe local executor.", destructive: false },
40
+ { name: "task-create", description: "Append a local host task to the run ledger.", destructive: false },
41
+ { name: "task-list", description: "List local host tasks from the run ledger.", destructive: false },
42
+ { name: "task-update", description: "Append a local host task status or ownership update.", destructive: false },
43
+ { name: "agenda", description: "Build the dependency-aware host task agenda.", destructive: false },
44
+ { name: "capsule-create", description: "Append a repo work capsule to the run ledger.", destructive: false },
45
+ { name: "capsule-list", description: "List repo work capsules from the run ledger.", destructive: false },
46
+ { name: "capsule-update", description: "Append a repo work capsule update.", destructive: false },
47
+ { name: "workflow-create", description: "Append a first-class workflow object to the run ledger.", destructive: false },
48
+ { name: "workflow-list", description: "List first-class workflow objects from the run ledger.", destructive: false },
49
+ { name: "workflow-update", description: "Append a first-class workflow object update.", destructive: false },
50
+ { name: "initiative-create", description: "Append a long-running initiative workflow object.", destructive: false },
51
+ { name: "handoff-create", description: "Append a role handoff workflow object.", destructive: false },
52
+ { name: "review-create", description: "Append a review-request workflow object.", destructive: false },
53
+ { name: "decision-create", description: "Append a human or role decision workflow object.", destructive: false },
54
+ { name: "artifact-create", description: "Append an artifact workflow object.", destructive: false },
55
+ { name: "feedback-record", description: "Append run feedback and skill metric evidence.", destructive: false },
56
+ { name: "feedback-list", description: "List run feedback records.", destructive: false },
57
+ { name: "feedback-summary", description: "Summarize run feedback and skill metrics.", destructive: false },
58
+ { name: "cron-check", description: "Evaluate local cron schedules and emit matching run events.", destructive: false },
59
+ { name: "emit-run-event", description: "Append an app-facing event to a prepared host run output.", destructive: false },
60
+ { name: "watch-run", description: "Read King AI loop events from a prepared host run output.", destructive: false },
61
+ { name: "run-results", description: "Read King AI results.tsv rows from a prepared host run output.", destructive: false },
62
+ { name: "run-heartbeat", description: "Read a prepared or executing host run heartbeat file.", destructive: false },
63
+ { name: "run-meta", description: "Read prepared host run metadata from a run output.", destructive: false },
64
+ { name: "plan-export", description: "Preview host artifact and repo patch export outputs.", destructive: false },
65
+ { name: "export", description: "Write host artifact and repo patch exports to the output directory.", destructive: true },
66
+ { name: "compact-ledger", description: "Rewrite the append-only task/capsule/workflow ledgers into merged snapshots.", destructive: true },
67
+ { name: "remote-config-get", description: "Return the full remote test device JSON config.", destructive: false },
68
+ { name: "remote-config-save", description: "Replace the full remote test device JSON config.", destructive: false },
69
+ { name: "remote-list", description: "List configured remote test devices without secrets.", destructive: false },
70
+ { name: "remote-save-device", description: "Create or update a remote test device in the local config.", destructive: false },
71
+ { name: "remote-delete-device", description: "Delete a remote test device from the local config.", destructive: false },
72
+ { name: "remote-default-device", description: "Set the default remote test device.", destructive: false },
73
+ { name: "remote-probe", description: "Probe SSH connectivity and basic identity on a remote test device.", destructive: false },
74
+ { name: "remote-profile", description: "Collect a remote test device environment profile.", destructive: false },
75
+ { name: "remote-run", description: "Run a shell command on a remote test device.", destructive: false },
76
+ { name: "remote-logs", description: "Tail logs from a remote test device.", destructive: false },
77
+ { name: "remote-find-logs", description: "Search logs on a remote test device.", destructive: false },
78
+ { name: "remote-pg", description: "Run a PostgreSQL command on a remote test device.", destructive: false },
79
+ { name: "remote-redis", description: "Run a Redis command on a remote test device.", destructive: false }
80
+ ];
81
+ export function listHostCommands() {
82
+ return COMMANDS.map((entry) => ({ ...entry }));
83
+ }
84
+ export function normalizeHostCommandName(raw) {
85
+ const value = raw.trim().toLowerCase();
86
+ return COMMANDS.some((entry) => entry.name === value) ? value : null;
87
+ }
88
+ function formatEvents(events) {
89
+ if (events.length === 0)
90
+ return "no recent daemon events";
91
+ return events.map((event) => `${event.at} ${event.kind}${event.detail ? ` ${event.detail}` : ""}`).join("\n");
92
+ }
93
+ export async function runHostCommand(request, deps = {}) {
94
+ const started = Date.now();
95
+ const actorRole = resolveActorRole(request);
96
+ let result;
97
+ try {
98
+ result = await executeHostCommand(request, deps);
99
+ }
100
+ catch (err) {
101
+ const command = normalizeHostCommandName(request.command) ?? request.command;
102
+ if (deps.recordTimeline) {
103
+ const message = err instanceof Error ? err.message : String(err);
104
+ await appendHostTimelineEvent({
105
+ at: (deps.now ?? (() => new Date()))().toISOString(),
106
+ type: "host.command",
107
+ command,
108
+ ok: false,
109
+ exitCode: 1,
110
+ destructive: listHostCommands().some((entry) => entry.name === command && entry.destructive),
111
+ durationMs: Date.now() - started,
112
+ actorRole,
113
+ textPreview: previewText(message),
114
+ error: message
115
+ }, deps.timelinePath);
116
+ }
117
+ throw err;
118
+ }
119
+ if (deps.recordTimeline) {
120
+ await appendHostTimelineEvent({
121
+ at: (deps.now ?? (() => new Date()))().toISOString(),
122
+ type: "host.command",
123
+ command: result.command,
124
+ ok: result.ok,
125
+ exitCode: result.exitCode,
126
+ destructive: listHostCommands().some((entry) => entry.name === result.command && entry.destructive),
127
+ durationMs: Date.now() - started,
128
+ actorRole,
129
+ textPreview: previewText(result.text),
130
+ jsonSummary: summarizeHostCommandJson(result.command, result.json),
131
+ error: result.error
132
+ }, deps.timelinePath);
133
+ }
134
+ return result;
135
+ }
136
+ async function executeHostCommand(request, deps = {}) {
137
+ const command = normalizeHostCommandName(request.command);
138
+ if (!command) {
139
+ return {
140
+ ok: false,
141
+ command: request.command,
142
+ exitCode: 64,
143
+ text: `unsupported host command: ${request.command}`,
144
+ error: "unsupported host command"
145
+ };
146
+ }
147
+ if (command === "policy") {
148
+ const input = normalizePolicyInput(request.input);
149
+ const target = normalizeHostCommandName(input.command);
150
+ if (!target) {
151
+ return {
152
+ ok: false,
153
+ command,
154
+ exitCode: 64,
155
+ text: `unsupported host command: ${input.command}`,
156
+ error: "unsupported host command"
157
+ };
158
+ }
159
+ const policy = checkHostCommandPolicy(target, isDestructiveHostCommand(target), input);
160
+ return {
161
+ ok: policy.decision === "allow",
162
+ command,
163
+ exitCode: policy.decision === "allow" ? 0 : 1,
164
+ text: formatHostPolicyCheck(policy),
165
+ json: policy
166
+ };
167
+ }
168
+ const policy = checkHostCommandPolicy(command, isDestructiveHostCommand(command), request.input);
169
+ if (policy.decision !== "allow") {
170
+ return {
171
+ ok: false,
172
+ command,
173
+ exitCode: 75,
174
+ text: formatHostPolicyCheck(policy),
175
+ json: policy,
176
+ error: "host command confirmation required"
177
+ };
178
+ }
179
+ const permission = deps.enforcePermission === false
180
+ ? { enforced: false, action: null }
181
+ : evaluateHostCommandPermission(command, request.input, request, { teamSpec: deps.teamSpec });
182
+ if (permission.enforced && permission.decision === "deny") {
183
+ return {
184
+ ok: false,
185
+ command,
186
+ exitCode: 77,
187
+ text: `role governance blocked ${permission.role} from ${permission.action} via ${command}`,
188
+ json: { permission },
189
+ error: "host command blocked by role governance"
190
+ };
191
+ }
192
+ if (permission.enforced && permission.decision === "human-decision") {
193
+ const approval = resolveHumanApproval(request.input, permission.role);
194
+ if (!approval.approved) {
195
+ const decision = await createHostWorkflowCard({
196
+ ...ledgerPathInput(request.input),
197
+ kind: "decision",
198
+ title: `Human decision marker required: ${command} (${permission.action})`,
199
+ ownerRole: permission.role,
200
+ decisionBy: permission.rule?.requireReviewBy ?? "human",
201
+ detail: `Role ${permission.role} requested ${permission.action} via ${command}; ${approval.reason ?? "human decision marker required"}. Retry with humanApproved=true and approvedBy=<approver different from ${permission.role}>. Role governance is opt-in for trusted local automation; omit actorRole/KING_AI_TEAM_ROLE when this command should run unattended.`,
202
+ metadata: { blockedCommand: command, action: permission.action, requestedRole: permission.role, reason: approval.reason }
203
+ }, deps.now);
204
+ return {
205
+ ok: false,
206
+ command,
207
+ exitCode: 75,
208
+ text: `human decision marker required before ${command}; created decision ${decision.id} (${approval.reason ?? "approval required"})`,
209
+ json: { permission, decision },
210
+ error: "host command blocked by human-decision governance"
211
+ };
212
+ }
213
+ }
214
+ if (command === "timeline") {
215
+ const events = await readHostTimeline({
216
+ path: deps.timelinePath,
217
+ limit: normalizeTimelineInput(request.input).limit
218
+ });
219
+ return {
220
+ ok: true,
221
+ command,
222
+ exitCode: 0,
223
+ text: formatHostTimeline(events),
224
+ json: { events }
225
+ };
226
+ }
227
+ if (command.startsWith("remote-")) {
228
+ const config = await loadRemoteDevicesConfig();
229
+ if (command === "remote-config-get") {
230
+ const input = normalizeRemoteConfigGetInput(request.input);
231
+ const safeConfig = input.revealSecrets
232
+ ? config
233
+ : { ...config, devices: listRemoteDeviceSummaries(config) };
234
+ return {
235
+ ok: true,
236
+ command,
237
+ exitCode: 0,
238
+ text: input.revealSecrets ? "remote devices config loaded" : formatRemoteDevices(config),
239
+ json: { config: safeConfig, defaultDevice: config.defaultDevice, devices: listRemoteDeviceSummaries(config) }
240
+ };
241
+ }
242
+ if (command === "remote-config-save") {
243
+ const next = await saveRemoteDevicesConfig(normalizeRemoteDevicesConfig(request.input));
244
+ return {
245
+ ok: true,
246
+ command,
247
+ exitCode: 0,
248
+ text: `remote devices config saved: ${next.devices.length} device${next.devices.length === 1 ? "" : "s"}`,
249
+ json: { config: next, defaultDevice: next.defaultDevice, devices: listRemoteDeviceSummaries(next) }
250
+ };
251
+ }
252
+ if (command === "remote-list") {
253
+ return {
254
+ ok: true,
255
+ command,
256
+ exitCode: 0,
257
+ text: formatRemoteDevices(config),
258
+ json: { defaultDevice: config.defaultDevice, devices: listRemoteDeviceSummaries(config) }
259
+ };
260
+ }
261
+ if (command === "remote-save-device") {
262
+ const input = normalizeRemoteSaveDeviceInput(request.input);
263
+ const next = await saveRemoteDevicesConfig(upsertRemoteDevice(config, input));
264
+ const device = findRemoteDevice(next, input.id);
265
+ return {
266
+ ok: true,
267
+ command,
268
+ exitCode: 0,
269
+ text: `remote device saved: ${device.id}`,
270
+ json: { defaultDevice: next.defaultDevice, device: summarizeRemoteDevice(device), devices: listRemoteDeviceSummaries(next) }
271
+ };
272
+ }
273
+ if (command === "remote-delete-device") {
274
+ const input = normalizeRemoteDeviceIdInput(request.input);
275
+ const next = await saveRemoteDevicesConfig(deleteRemoteDevice(config, input.id));
276
+ return {
277
+ ok: true,
278
+ command,
279
+ exitCode: 0,
280
+ text: `remote device deleted: ${input.id}`,
281
+ json: { defaultDevice: next.defaultDevice, devices: listRemoteDeviceSummaries(next) }
282
+ };
283
+ }
284
+ if (command === "remote-default-device") {
285
+ const input = normalizeRemoteDeviceIdInput(request.input);
286
+ const next = await saveRemoteDevicesConfig(setDefaultRemoteDevice(config, input.id));
287
+ return {
288
+ ok: true,
289
+ command,
290
+ exitCode: 0,
291
+ text: `default remote device: ${input.id}`,
292
+ json: { defaultDevice: next.defaultDevice, devices: listRemoteDeviceSummaries(next) }
293
+ };
294
+ }
295
+ const remoteDeps = { config };
296
+ const result = command === "remote-probe" ? await remoteProbe(request.input, remoteDeps)
297
+ : command === "remote-profile" ? await remoteProfile(request.input, remoteDeps)
298
+ : command === "remote-run" ? await remoteRun(request.input, remoteDeps)
299
+ : command === "remote-logs" ? await remoteLogs(request.input, remoteDeps)
300
+ : command === "remote-find-logs" ? await remoteFindLogs(request.input, remoteDeps)
301
+ : command === "remote-pg" ? await remotePg(request.input, remoteDeps)
302
+ : await remoteRedis(request.input, remoteDeps);
303
+ return {
304
+ ok: result.ok,
305
+ command,
306
+ exitCode: result.exitCode ?? (result.ok ? 0 : 1),
307
+ text: formatRemoteResult(result),
308
+ json: result,
309
+ error: result.error
310
+ };
311
+ }
312
+ const readState = deps.readState ?? readRunningState;
313
+ const tokenBudget = deps.tokenBudget ?? tokenBudgetFromEnv;
314
+ const usagePricing = deps.usagePricing ?? usagePricingFromEnv;
315
+ if (command === "plan-run" || command === "preflight") {
316
+ const plan = createHostLaunchPlan(normalizeRunInput(request.input), process.env, normalizeAvailableEngines(deps.availableEngines?.()));
317
+ return {
318
+ ok: plan.ready,
319
+ command,
320
+ exitCode: plan.ready ? 0 : 1,
321
+ text: plan.launchSummary,
322
+ json: toJsonSafeHostLaunchPlan(plan)
323
+ };
324
+ }
325
+ if (command === "prepare-run-layout") {
326
+ try {
327
+ const result = await prepareHostRunLayout(normalizeRunLayoutInput(request.input), {
328
+ env: process.env,
329
+ availableEngines: normalizeAvailableEngines(deps.availableEngines?.())
330
+ });
331
+ return {
332
+ ok: result.launchPlan.ready,
333
+ command,
334
+ exitCode: result.launchPlan.ready ? 0 : 1,
335
+ text: result.summary,
336
+ json: result
337
+ };
338
+ }
339
+ catch (err) {
340
+ const message = err instanceof Error ? err.message : String(err);
341
+ return {
342
+ ok: false,
343
+ command,
344
+ exitCode: 1,
345
+ text: message,
346
+ error: message
347
+ };
348
+ }
349
+ }
350
+ if (command === "submit-run") {
351
+ const result = await submitHostRunRequest(withExecutorActorRole(normalizeSubmitRunInput(request.input), permission.role), {
352
+ path: deps.runsPath,
353
+ availableEngines: normalizeAvailableEngines(deps.availableEngines?.()),
354
+ now: deps.now
355
+ });
356
+ return {
357
+ ok: result.launchPlan.ready,
358
+ command,
359
+ exitCode: result.launchPlan.ready ? 0 : 1,
360
+ text: result.summary,
361
+ json: result
362
+ };
363
+ }
364
+ if (command === "run-requests") {
365
+ const requests = await listHostRunRequests(normalizeRunRequestsInput(request.input), deps.runsPath);
366
+ return {
367
+ ok: true,
368
+ command,
369
+ exitCode: 0,
370
+ text: formatHostRunRequests(requests),
371
+ json: { requests }
372
+ };
373
+ }
374
+ if (command === "run-request") {
375
+ const runRequest = await getHostRunRequest(normalizeRunRequestInput(request.input), deps.runsPath);
376
+ if (!runRequest) {
377
+ return {
378
+ ok: false,
379
+ command,
380
+ exitCode: 66,
381
+ text: "host run request not found",
382
+ error: "host run request not found"
383
+ };
384
+ }
385
+ return {
386
+ ok: true,
387
+ command,
388
+ exitCode: 0,
389
+ text: formatHostRunRequestSummary(runRequest),
390
+ json: { request: runRequest }
391
+ };
392
+ }
393
+ if (command === "update-run") {
394
+ const result = await updateHostRunRequest(normalizeUpdateRunInput(request.input), {
395
+ path: deps.runsPath,
396
+ now: deps.now
397
+ });
398
+ await syncRunRequestHeartbeat(result.request, deps.now);
399
+ await syncRunRequestMeta(result.request, deps.now);
400
+ await syncRunRequestLoopEvent(result.request, deps.now);
401
+ return {
402
+ ok: true,
403
+ command,
404
+ exitCode: 0,
405
+ text: result.summary,
406
+ json: result
407
+ };
408
+ }
409
+ if (command === "cancel-run") {
410
+ const result = await updateHostRunRequest(normalizeCancelRunInput(request.input), {
411
+ path: deps.runsPath,
412
+ now: deps.now
413
+ });
414
+ await syncRunRequestHeartbeat(result.request, deps.now);
415
+ await syncRunRequestMeta(result.request, deps.now);
416
+ await syncRunRequestLoopEvent(result.request, deps.now);
417
+ return {
418
+ ok: true,
419
+ command,
420
+ exitCode: 0,
421
+ text: result.summary,
422
+ json: result
423
+ };
424
+ }
425
+ if (command === "execute-run") {
426
+ const result = await executeNextHostRunRequest(normalizeExecuteRunInput(request.input), deps);
427
+ return {
428
+ ok: result.request ? result.request.status === "completed" : true,
429
+ command,
430
+ exitCode: result.request && result.request.status !== "completed" ? 1 : 0,
431
+ text: formatHostRunExecuteResult(result),
432
+ json: result
433
+ };
434
+ }
435
+ if (command === "task-create") {
436
+ const task = await createHostTask(normalizeTaskCreateInput(request.input), deps.now);
437
+ return {
438
+ ok: true,
439
+ command,
440
+ exitCode: 0,
441
+ text: formatHostTasks([task]),
442
+ json: { task }
443
+ };
444
+ }
445
+ if (command === "task-list") {
446
+ const tasks = await listHostTasks(normalizeTaskListInput(request.input));
447
+ return {
448
+ ok: true,
449
+ command,
450
+ exitCode: 0,
451
+ text: formatHostTasks(tasks),
452
+ json: { tasks }
453
+ };
454
+ }
455
+ if (command === "task-update") {
456
+ const task = await updateHostTask(normalizeTaskUpdateInput(request.input), deps.now);
457
+ return {
458
+ ok: true,
459
+ command,
460
+ exitCode: 0,
461
+ text: formatHostTasks([task]),
462
+ json: { task }
463
+ };
464
+ }
465
+ if (command === "agenda") {
466
+ const agenda = await buildHostAgenda(normalizeAgendaInput(request.input));
467
+ return {
468
+ ok: true,
469
+ command,
470
+ exitCode: 0,
471
+ text: formatHostAgenda(agenda),
472
+ json: agenda
473
+ };
474
+ }
475
+ if (command === "capsule-create") {
476
+ const capsule = await createHostCapsule(normalizeCapsuleCreateInput(request.input), deps.now);
477
+ return {
478
+ ok: true,
479
+ command,
480
+ exitCode: 0,
481
+ text: formatHostCapsules([capsule]),
482
+ json: { capsule }
483
+ };
484
+ }
485
+ if (command === "capsule-list") {
486
+ const capsules = await listHostCapsules(normalizeCapsuleListInput(request.input));
487
+ return {
488
+ ok: true,
489
+ command,
490
+ exitCode: 0,
491
+ text: formatHostCapsules(capsules),
492
+ json: { capsules }
493
+ };
494
+ }
495
+ if (command === "capsule-update") {
496
+ const capsule = await updateHostCapsule(normalizeCapsuleUpdateInput(request.input), deps.now);
497
+ return {
498
+ ok: true,
499
+ command,
500
+ exitCode: 0,
501
+ text: formatHostCapsules([capsule]),
502
+ json: { capsule }
503
+ };
504
+ }
505
+ if (command === "workflow-create") {
506
+ const card = await createHostWorkflowCard(applyCapabilityOwner(normalizeWorkflowCreateInput(request.input), deps), deps.now);
507
+ return {
508
+ ok: true,
509
+ command,
510
+ exitCode: 0,
511
+ text: formatHostWorkflowCards([card]),
512
+ json: { card }
513
+ };
514
+ }
515
+ if (command === "workflow-list") {
516
+ const cards = await listHostWorkflowCards(normalizeWorkflowListInput(request.input));
517
+ return {
518
+ ok: true,
519
+ command,
520
+ exitCode: 0,
521
+ text: formatHostWorkflowCards(cards),
522
+ json: { cards }
523
+ };
524
+ }
525
+ if (command === "workflow-update") {
526
+ const card = await updateHostWorkflowCard(normalizeWorkflowUpdateInput(request.input), deps.now);
527
+ const handoffs = await runAutoHandoff(card, request.input, deps);
528
+ return {
529
+ ok: true,
530
+ command,
531
+ exitCode: 0,
532
+ text: handoffs.length ? `${formatHostWorkflowCards([card])}\n${formatHandoffActions(handoffs)}` : formatHostWorkflowCards([card]),
533
+ json: handoffs.length ? { card, handoffs } : { card }
534
+ };
535
+ }
536
+ if (command === "initiative-create" || command === "handoff-create" || command === "review-create" || command === "decision-create" || command === "artifact-create") {
537
+ const card = await createHostWorkflowCard(normalizeKindedWorkflowCreateInput(command, request.input), deps.now);
538
+ return {
539
+ ok: true,
540
+ command,
541
+ exitCode: 0,
542
+ text: formatHostWorkflowCards([card]),
543
+ json: { card }
544
+ };
545
+ }
546
+ if (command === "feedback-record") {
547
+ const feedback = await recordHostFeedback(normalizeFeedbackRecordInput(request.input), deps.now);
548
+ return {
549
+ ok: true,
550
+ command,
551
+ exitCode: 0,
552
+ text: formatHostFeedback([feedback]),
553
+ json: { feedback }
554
+ };
555
+ }
556
+ if (command === "feedback-list") {
557
+ const feedback = await listHostFeedback(normalizeFeedbackListInput(request.input));
558
+ return {
559
+ ok: true,
560
+ command,
561
+ exitCode: 0,
562
+ text: formatHostFeedback(feedback),
563
+ json: { feedback }
564
+ };
565
+ }
566
+ if (command === "feedback-summary") {
567
+ const summary = await summarizeHostFeedback(normalizeFeedbackListInput(request.input));
568
+ return {
569
+ ok: true,
570
+ command,
571
+ exitCode: 0,
572
+ text: formatHostFeedbackSummary(summary),
573
+ json: summary
574
+ };
575
+ }
576
+ if (command === "cron-check") {
577
+ const result = await runHostCronCheck(normalizeCronCheckInput(request.input), deps.now);
578
+ return {
579
+ ok: true,
580
+ command,
581
+ exitCode: 0,
582
+ text: formatHostCronCheck(result),
583
+ json: result
584
+ };
585
+ }
586
+ if (command === "emit-run-event") {
587
+ const result = await emitHostRunEvent(await normalizeEmitRunEventInput(request.input, deps.runsPath), deps.now);
588
+ return {
589
+ ok: true,
590
+ command,
591
+ exitCode: 0,
592
+ text: `host run event emitted: ${result.file}\n${formatEmittedHostRunEvent(result.event)}`,
593
+ json: result
594
+ };
595
+ }
596
+ if (command === "watch-run") {
597
+ const result = await readHostLoopEvents(await normalizeLoopEventsInput(request.input, deps.runsPath));
598
+ return {
599
+ ok: true,
600
+ command,
601
+ exitCode: 0,
602
+ text: formatHostLoopEvents(result),
603
+ json: result
604
+ };
605
+ }
606
+ if (command === "run-results") {
607
+ const result = await readHostLoopResults(await normalizeLoopResultsInput(request.input, deps.runsPath));
608
+ return {
609
+ ok: true,
610
+ command,
611
+ exitCode: 0,
612
+ text: result.text ?? "",
613
+ json: result
614
+ };
615
+ }
616
+ if (command === "run-heartbeat") {
617
+ const result = await readHostRunHeartbeat(await normalizeRunHeartbeatInput(request.input, deps.runsPath));
618
+ return {
619
+ ok: result.exists && !!result.heartbeat,
620
+ command,
621
+ exitCode: result.exists && result.heartbeat ? 0 : 66,
622
+ text: formatHostRunHeartbeat(result),
623
+ json: result,
624
+ error: result.exists && result.heartbeat ? undefined : "host run heartbeat not found"
625
+ };
626
+ }
627
+ if (command === "run-meta") {
628
+ const result = await readHostRunMeta(await normalizeRunMetaInput(request.input, deps.runsPath));
629
+ return {
630
+ ok: result.exists && !!result.meta,
631
+ command,
632
+ exitCode: result.exists && result.meta ? 0 : 66,
633
+ text: formatHostRunMeta(result),
634
+ json: result,
635
+ error: result.exists && result.meta ? undefined : "host run meta not found"
636
+ };
637
+ }
638
+ if (command === "plan-export") {
639
+ const plan = await planHostExport(normalizeExportInput(request.input));
640
+ return {
641
+ ok: true,
642
+ command,
643
+ exitCode: 0,
644
+ text: plan.summary,
645
+ json: plan
646
+ };
647
+ }
648
+ if (command === "export") {
649
+ const result = await exportHostArtifacts(normalizeExportInput(request.input));
650
+ return {
651
+ ok: true,
652
+ command,
653
+ exitCode: 0,
654
+ text: `${result.summary}\nwritten files:\n${result.writtenFiles.map((file) => ` - ${file}`).join("\n") || " (none)"}`,
655
+ json: result
656
+ };
657
+ }
658
+ if (command === "compact-ledger") {
659
+ const result = await compactHostLedger(normalizeCompactLedgerInput(request.input));
660
+ return {
661
+ ok: true,
662
+ command,
663
+ exitCode: 0,
664
+ text: formatHostLedgerCompact(result),
665
+ json: result
666
+ };
667
+ }
668
+ if (command === "doctor") {
669
+ const results = await (deps.collectDoctorResults ?? collectDoctorResults)();
670
+ const exitCode = doctorExitCode(results);
671
+ return {
672
+ ok: exitCode === 0,
673
+ command,
674
+ exitCode,
675
+ text: formatDoctorReport(results),
676
+ json: { results, exitCode }
677
+ };
678
+ }
679
+ const state = await readState();
680
+ const snapshot = buildHostStatusSnapshot(state, tokenBudget(), usagePricing());
681
+ if (command === "status") {
682
+ return {
683
+ ok: snapshot.ok,
684
+ command,
685
+ exitCode: snapshot.ok ? 0 : 1,
686
+ text: formatHostStatusSnapshot(snapshot),
687
+ json: snapshot
688
+ };
689
+ }
690
+ if (command === "usage") {
691
+ const summary = summarizeAgentUsage(state?.agents ?? [], tokenBudget(), usagePricing());
692
+ return {
693
+ ok: true,
694
+ command,
695
+ exitCode: 0,
696
+ text: formatUsageSummary(summary),
697
+ json: {
698
+ ...summary,
699
+ runtimeData: buildUsageRuntimeData(state, { budget: tokenBudget(), pricingRules: usagePricing() })
700
+ }
701
+ };
702
+ }
703
+ if (command === "expenses") {
704
+ const summary = summarizeAgentUsage(state?.agents ?? [], tokenBudget(), usagePricing());
705
+ const expenses = listUsageExpenses(summary);
706
+ return {
707
+ ok: true,
708
+ command,
709
+ exitCode: 0,
710
+ text: formatUsageExpenses(expenses),
711
+ json: {
712
+ expenses,
713
+ usage: summary
714
+ }
715
+ };
716
+ }
717
+ return {
718
+ ok: true,
719
+ command,
720
+ exitCode: 0,
721
+ text: formatEvents(snapshot.events),
722
+ json: { events: snapshot.events }
723
+ };
724
+ }
725
+ function isDestructiveHostCommand(command) {
726
+ return COMMANDS.some((entry) => entry.name === command && entry.destructive);
727
+ }
728
+ function ledgerPathInput(value) {
729
+ if (!value || typeof value !== "object")
730
+ return {};
731
+ const record = value;
732
+ return {
733
+ outputDir: typeof record.outputDir === "string" ? record.outputDir : undefined,
734
+ workflowFile: typeof record.workflowFile === "string" ? record.workflowFile : undefined
735
+ };
736
+ }
737
+ function normalizePolicyInput(value) {
738
+ if (!value || typeof value !== "object")
739
+ throw new Error("policy input must include a command");
740
+ const input = value;
741
+ if (typeof input.command !== "string" || !input.command.trim())
742
+ throw new Error("policy input must include a command");
743
+ return {
744
+ command: input.command,
745
+ confirmed: input.confirmed,
746
+ confirmation: input.confirmation
747
+ };
748
+ }
749
+ function normalizeTimelineInput(value) {
750
+ if (!value)
751
+ return {};
752
+ if (typeof value !== "object")
753
+ throw new Error("timeline input must be an object");
754
+ const raw = value.limit;
755
+ if (raw === undefined)
756
+ return {};
757
+ const limit = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
758
+ if (!Number.isFinite(limit) || limit < 1)
759
+ throw new Error("timeline limit must be a positive integer");
760
+ return { limit };
761
+ }
762
+ function normalizeRemoteSaveDeviceInput(value) {
763
+ if (!value || typeof value !== "object")
764
+ throw new Error("remote-save-device input must include a device object");
765
+ const input = value;
766
+ if (typeof input.id !== "string" || !input.id.trim())
767
+ throw new Error("remote device id is required");
768
+ return { ...input, id: input.id.trim() };
769
+ }
770
+ function normalizeRemoteConfigGetInput(value) {
771
+ if (!value)
772
+ return { revealSecrets: false };
773
+ if (typeof value !== "object")
774
+ throw new Error("remote-config-get input must be an object");
775
+ return { revealSecrets: value.revealSecrets === true };
776
+ }
777
+ function normalizeRemoteDeviceIdInput(value) {
778
+ if (!value || typeof value !== "object")
779
+ throw new Error("remote device input must include an id");
780
+ const id = value.id;
781
+ if (typeof id !== "string" || !id.trim())
782
+ throw new Error("remote device id is required");
783
+ return { id: id.trim() };
784
+ }
785
+ function normalizeRunInput(value) {
786
+ if (value && typeof value === "object")
787
+ return value;
788
+ if (typeof value === "string")
789
+ return { goal: value };
790
+ throw new Error("plan-run input must include a goal");
791
+ }
792
+ function normalizeSubmitRunInput(value) {
793
+ if (value && typeof value === "object")
794
+ return value;
795
+ if (typeof value === "string")
796
+ return { goal: value };
797
+ throw new Error("submit-run input must include a goal");
798
+ }
799
+ function withExecutorActorRole(input, actorRole) {
800
+ if (!input.executor || input.executor.actorRole || !actorRole)
801
+ return input;
802
+ return {
803
+ ...input,
804
+ executor: {
805
+ ...input.executor,
806
+ actorRole
807
+ }
808
+ };
809
+ }
810
+ function normalizeRunLayoutInput(value) {
811
+ if (value && typeof value === "object")
812
+ return value;
813
+ if (typeof value === "string")
814
+ return { goal: value };
815
+ throw new Error("prepare-run-layout input must include a goal");
816
+ }
817
+ function normalizeRunRequestsInput(value) {
818
+ if (!value)
819
+ return {};
820
+ if (typeof value !== "object")
821
+ throw new Error("run-requests input must be an object");
822
+ return value;
823
+ }
824
+ function normalizeRunRequestInput(value) {
825
+ if (!value || typeof value !== "object")
826
+ throw new Error("run-request input must include an id");
827
+ return value;
828
+ }
829
+ function normalizeUpdateRunInput(value) {
830
+ if (!value || typeof value !== "object")
831
+ throw new Error("update-run input must include id and status");
832
+ return value;
833
+ }
834
+ function normalizeCancelRunInput(value) {
835
+ if (!value || typeof value !== "object")
836
+ throw new Error("cancel-run input must include an id");
837
+ const input = value;
838
+ return {
839
+ id: input.id,
840
+ status: "cancelled",
841
+ detail: input.detail
842
+ };
843
+ }
844
+ function normalizeExecuteRunInput(value) {
845
+ if (!value)
846
+ return {};
847
+ if (typeof value !== "object")
848
+ throw new Error("execute-run input must be an object");
849
+ return value;
850
+ }
851
+ function normalizeTaskCreateInput(value) {
852
+ if (!value || typeof value !== "object")
853
+ throw new Error("task-create input must include a title");
854
+ return value;
855
+ }
856
+ function normalizeTaskListInput(value) {
857
+ if (!value)
858
+ return {};
859
+ if (typeof value !== "object")
860
+ throw new Error("task-list input must be an object");
861
+ return value;
862
+ }
863
+ function normalizeTaskUpdateInput(value) {
864
+ if (!value || typeof value !== "object")
865
+ throw new Error("task-update input must include an id");
866
+ return value;
867
+ }
868
+ function normalizeAgendaInput(value) {
869
+ if (!value)
870
+ return {};
871
+ if (typeof value !== "object")
872
+ throw new Error("agenda input must be an object");
873
+ return value;
874
+ }
875
+ function normalizeCapsuleCreateInput(value) {
876
+ if (!value || typeof value !== "object")
877
+ throw new Error("capsule-create input must include a goal");
878
+ return value;
879
+ }
880
+ function normalizeCapsuleListInput(value) {
881
+ if (!value)
882
+ return {};
883
+ if (typeof value !== "object")
884
+ throw new Error("capsule-list input must be an object");
885
+ return value;
886
+ }
887
+ function normalizeCapsuleUpdateInput(value) {
888
+ if (!value || typeof value !== "object")
889
+ throw new Error("capsule-update input must include an id");
890
+ return value;
891
+ }
892
+ function normalizeWorkflowCreateInput(value) {
893
+ if (!value || typeof value !== "object")
894
+ throw new Error("workflow-create input must include kind and title");
895
+ return value;
896
+ }
897
+ function normalizeWorkflowListInput(value) {
898
+ if (!value)
899
+ return {};
900
+ if (typeof value !== "object")
901
+ throw new Error("workflow-list input must be an object");
902
+ return value;
903
+ }
904
+ function normalizeWorkflowUpdateInput(value) {
905
+ if (!value || typeof value !== "object")
906
+ throw new Error("workflow-update input must include an id");
907
+ return value;
908
+ }
909
+ function normalizeCompactLedgerInput(value) {
910
+ if (!value)
911
+ return {};
912
+ if (typeof value !== "object")
913
+ throw new Error("compact-ledger input must be an object");
914
+ return value;
915
+ }
916
+ /**
917
+ * Capability-first owner selection: when a workflow card is created without an explicit ownerRole but
918
+ * carries `requiredCapabilities`, route it to the best-matching team role per the routing policy.
919
+ */
920
+ function applyCapabilityOwner(input, deps) {
921
+ if (cleanInputString(input.ownerRole))
922
+ return input;
923
+ const capabilities = input.requiredCapabilities;
924
+ if (!Array.isArray(capabilities))
925
+ return input;
926
+ const required = capabilities.filter((entry) => typeof entry === "string");
927
+ if (required.length === 0)
928
+ return input;
929
+ const owner = selectOwnerRole(resolveTeamSpec({ teamSpec: deps.teamSpec }), required);
930
+ return owner ? { ...input, ownerRole: owner } : input;
931
+ }
932
+ /**
933
+ * Execute the team routing policy when a workflow card reaches `done`: materialize the follow-up
934
+ * review/decision/handoff cards the policy requires. Opt out per call with `{ handoff: false }` or per
935
+ * runner with `deps.autoHandoff === false`.
936
+ */
937
+ async function runAutoHandoff(card, rawInput, deps) {
938
+ if (deps.autoHandoff === false || handoffDisabled(rawInput) || card.status !== "done")
939
+ return [];
940
+ const source = card.sourceId ? await findWorkflowCard(card.sourceId, rawInput) : null;
941
+ const actions = planHandoff({
942
+ ...card,
943
+ sourceOwnerRole: source?.ownerRole
944
+ }, resolveTeamSpec({ teamSpec: deps.teamSpec }));
945
+ if (actions.length === 0)
946
+ return [];
947
+ const paths = ledgerPathInput(rawInput);
948
+ const existing = await listHostWorkflowCards(paths);
949
+ const created = [];
950
+ for (const action of actions) {
951
+ const reason = autoHandoffReason(action, card);
952
+ if (existing.some((entry) => entry.sourceId === card.id && entry.metadata?.autoHandoff === true && entry.metadata?.reason === reason))
953
+ continue;
954
+ const next = await createHostWorkflowCard({
955
+ ...paths,
956
+ kind: action.card.kind,
957
+ title: action.card.title,
958
+ status: action.card.status,
959
+ ownerRole: action.card.ownerRole,
960
+ reviewerRole: action.card.reviewerRole,
961
+ targetRole: action.card.targetRole,
962
+ dependsOn: action.card.dependsOn,
963
+ sourceId: action.card.sourceId,
964
+ decisionBy: action.card.decisionBy,
965
+ detail: action.card.detail,
966
+ handoffPolicy: action.card.handoffPolicy,
967
+ metadata: { autoHandoff: true, reason, sourceCard: card.id, reviewedCard: card.kind === "review" ? card.sourceId : undefined }
968
+ }, deps.now);
969
+ created.push({ reason: action.reason, card: next });
970
+ }
971
+ return created;
972
+ }
973
+ function handoffDisabled(input) {
974
+ return Boolean(input && typeof input === "object" && input.handoff === false);
975
+ }
976
+ async function findWorkflowCard(id, rawInput) {
977
+ return (await listHostWorkflowCards(ledgerPathInput(rawInput))).find((card) => card.id === id) ?? null;
978
+ }
979
+ function autoHandoffReason(action, sourceCard) {
980
+ return sourceCard.kind === "review" && normalizeReviewVerdict(sourceCard.result) === "changes_requested"
981
+ ? "changes-requested"
982
+ : action.reason;
983
+ }
984
+ function formatHandoffActions(handoffs) {
985
+ return handoffs
986
+ .map(({ reason, card }) => `auto-handoff (${reason}) -> ${card.id} ${card.kind} ${card.status}${card.ownerRole ? ` ownerRole=${card.ownerRole}` : ""}: ${card.title}`)
987
+ .join("\n");
988
+ }
989
+ function normalizeKindedWorkflowCreateInput(command, value) {
990
+ if (!value || typeof value !== "object")
991
+ throw new Error(`${command} input must include a title`);
992
+ const kind = command.replace(/-create$/, "");
993
+ if (kind !== "initiative" && kind !== "handoff" && kind !== "review" && kind !== "decision" && kind !== "artifact") {
994
+ throw new Error(`unsupported workflow create command: ${command}`);
995
+ }
996
+ return {
997
+ ...value,
998
+ kind,
999
+ status: kind === "decision" ? (value.status ?? "waiting_human") : value.status
1000
+ };
1001
+ }
1002
+ function normalizeFeedbackRecordInput(value) {
1003
+ if (!value || typeof value !== "object")
1004
+ throw new Error("feedback-record input must be an object");
1005
+ return value;
1006
+ }
1007
+ function normalizeFeedbackListInput(value) {
1008
+ if (!value)
1009
+ return {};
1010
+ if (typeof value !== "object")
1011
+ throw new Error("feedback input must be an object");
1012
+ return value;
1013
+ }
1014
+ function normalizeCronCheckInput(value) {
1015
+ if (!value || typeof value !== "object")
1016
+ throw new Error("cron-check input must include schedules");
1017
+ const input = value;
1018
+ if (!Array.isArray(input.schedules))
1019
+ throw new Error("cron-check schedules must be an array");
1020
+ return {
1021
+ outputDir: cleanInputString(input.outputDir),
1022
+ file: cleanInputString(input.file),
1023
+ runId: cleanInputString(input.runId),
1024
+ now: cleanInputString(input.now),
1025
+ schedules: input.schedules.map((entry) => {
1026
+ if (!entry || typeof entry !== "object")
1027
+ throw new Error("cron-check schedule entries must be objects");
1028
+ const schedule = entry;
1029
+ const cron = cleanInputString(schedule.cron);
1030
+ if (!cron)
1031
+ throw new Error("cron-check schedule cron is required");
1032
+ return {
1033
+ id: cleanInputString(schedule.id),
1034
+ cron,
1035
+ type: cleanInputString(schedule.type),
1036
+ agent: cleanInputString(schedule.agent),
1037
+ message: cleanInputString(schedule.message),
1038
+ payload: schedule.payload
1039
+ };
1040
+ })
1041
+ };
1042
+ }
1043
+ async function runHostCronCheck(input, now) {
1044
+ if (!input.file && !input.outputDir)
1045
+ throw new Error("cron-check requires a file or outputDir");
1046
+ const date = input.now ? new Date(input.now) : (now ?? (() => new Date()))();
1047
+ if (Number.isNaN(date.getTime()))
1048
+ throw new Error("cron-check now must be a valid date");
1049
+ const emitted = [];
1050
+ for (const schedule of input.schedules) {
1051
+ if (!cronMatches(schedule.cron, date))
1052
+ continue;
1053
+ const event = dropUndefined({
1054
+ type: schedule.type ?? "cron.tick",
1055
+ runId: input.runId,
1056
+ timestamp: date.toISOString(),
1057
+ agent: schedule.agent,
1058
+ message: schedule.message,
1059
+ payload: schedule.payload,
1060
+ source: "cron-check"
1061
+ });
1062
+ const file = await appendHostLoopEvent({
1063
+ file: input.file,
1064
+ outputDir: input.outputDir,
1065
+ event
1066
+ });
1067
+ emitted.push({ id: schedule.id, type: event.type ?? "cron.tick", file, event });
1068
+ }
1069
+ return {
1070
+ now: date.toISOString(),
1071
+ checked: input.schedules.length,
1072
+ matched: emitted.length,
1073
+ emitted
1074
+ };
1075
+ }
1076
+ function formatHostCronCheck(result) {
1077
+ const lines = [`cron-check: checked=${result.checked} matched=${result.matched} now=${result.now}`];
1078
+ for (const entry of result.emitted)
1079
+ lines.push(` - ${entry.id ?? entry.type}: ${entry.file}`);
1080
+ return lines.join("\n");
1081
+ }
1082
+ async function normalizeLoopEventsInput(value, runsPath) {
1083
+ if (!value)
1084
+ return {};
1085
+ if (typeof value !== "object")
1086
+ throw new Error("watch-run input must be an object");
1087
+ const input = value;
1088
+ const id = typeof input.id === "string" && input.id.trim() ? input.id.trim() : undefined;
1089
+ if (!input.file && !input.outputDir && id) {
1090
+ const request = await getHostRunRequest({ id }, runsPath);
1091
+ const outputDir = request?.spec.options?.outputDir;
1092
+ return {
1093
+ ...input,
1094
+ outputDir,
1095
+ runId: input.runId ?? id
1096
+ };
1097
+ }
1098
+ return input;
1099
+ }
1100
+ async function normalizeEmitRunEventInput(value, runsPath) {
1101
+ if (!value || typeof value !== "object")
1102
+ throw new Error("emit-run-event input must include an event object or type");
1103
+ const input = value;
1104
+ const id = typeof input.id === "string" && input.id.trim() ? input.id.trim() : undefined;
1105
+ let outputDir = cleanInputString(input.outputDir);
1106
+ if (!input.file && !outputDir && id) {
1107
+ const request = await getHostRunRequest({ id }, runsPath);
1108
+ outputDir = request?.spec.options?.outputDir;
1109
+ }
1110
+ const eventInput = input.event && typeof input.event === "object" ? input.event : {};
1111
+ const type = cleanInputString(eventInput.type) ?? cleanInputString(input.type);
1112
+ if (!type)
1113
+ throw new Error("emit-run-event type is required");
1114
+ return {
1115
+ ...input,
1116
+ id,
1117
+ outputDir,
1118
+ event: {
1119
+ ...eventInput,
1120
+ type,
1121
+ runId: cleanInputString(eventInput.runId) ?? id,
1122
+ loop: numberInput(eventInput.loop) ?? numberInput(input.loop),
1123
+ agent: cleanInputString(eventInput.agent) ?? cleanInputString(input.agent),
1124
+ classification: cleanInputString(eventInput.classification) ?? cleanInputString(input.classification),
1125
+ detail: cleanInputString(eventInput.detail) ?? cleanInputString(input.detail),
1126
+ message: cleanInputString(eventInput.message) ?? cleanInputString(input.message),
1127
+ payload: eventInput.payload ?? input.payload
1128
+ }
1129
+ };
1130
+ }
1131
+ async function emitHostRunEvent(input, now) {
1132
+ if (!input.file && !input.outputDir)
1133
+ throw new Error("emit-run-event requires a file, outputDir, or run id with outputDir");
1134
+ const event = dropUndefined({
1135
+ ...input.event,
1136
+ runId: typeof input.event.runId === "string" && input.event.runId.trim() ? input.event.runId.trim() : input.id,
1137
+ timestamp: typeof input.event.timestamp === "string" && input.event.timestamp.trim() ? input.event.timestamp.trim() : (now ?? (() => new Date()))().toISOString(),
1138
+ source: typeof input.event.source === "string" && input.event.source.trim() ? input.event.source.trim() : "host-app"
1139
+ });
1140
+ const file = await appendHostLoopEvent({
1141
+ file: input.file,
1142
+ outputDir: input.outputDir,
1143
+ event
1144
+ });
1145
+ return { file, event };
1146
+ }
1147
+ function formatEmittedHostRunEvent(event) {
1148
+ return `${event.timestamp ?? "no-time"} ${event.type ?? "unknown"} run=${event.runId ?? "?"}`;
1149
+ }
1150
+ async function normalizeLoopResultsInput(value, runsPath) {
1151
+ if (!value)
1152
+ return {};
1153
+ if (typeof value !== "object")
1154
+ throw new Error("run-results input must be an object");
1155
+ const input = value;
1156
+ const id = typeof input.id === "string" && input.id.trim() ? input.id.trim() : undefined;
1157
+ if (!input.file && !input.outputDir && !input.resultsFile && id) {
1158
+ const request = await getHostRunRequest({ id }, runsPath);
1159
+ const outputDir = request?.spec.options?.outputDir;
1160
+ return {
1161
+ ...input,
1162
+ outputDir
1163
+ };
1164
+ }
1165
+ return input;
1166
+ }
1167
+ async function normalizeRunHeartbeatInput(value, runsPath) {
1168
+ if (!value)
1169
+ return {};
1170
+ if (typeof value !== "object")
1171
+ throw new Error("run-heartbeat input must be an object");
1172
+ const input = value;
1173
+ const id = typeof input.id === "string" && input.id.trim() ? input.id.trim() : undefined;
1174
+ if (!input.file && !input.outputDir && id) {
1175
+ const request = await getHostRunRequest({ id }, runsPath);
1176
+ const outputDir = request?.spec.options?.outputDir;
1177
+ return {
1178
+ ...input,
1179
+ outputDir
1180
+ };
1181
+ }
1182
+ return input;
1183
+ }
1184
+ async function normalizeRunMetaInput(value, runsPath) {
1185
+ if (!value)
1186
+ return {};
1187
+ if (typeof value !== "object")
1188
+ throw new Error("run-meta input must be an object");
1189
+ const input = value;
1190
+ const id = typeof input.id === "string" && input.id.trim() ? input.id.trim() : undefined;
1191
+ if (!input.file && !input.outputDir && id) {
1192
+ const request = await getHostRunRequest({ id }, runsPath);
1193
+ const outputDir = request?.spec.options?.outputDir;
1194
+ return {
1195
+ ...input,
1196
+ outputDir
1197
+ };
1198
+ }
1199
+ return input;
1200
+ }
1201
+ async function syncRunRequestHeartbeat(request, now) {
1202
+ const outputDir = request.spec.options?.outputDir;
1203
+ if (!outputDir || request.status === "pending")
1204
+ return;
1205
+ await writeHostRunHeartbeat({
1206
+ path: hostRunHeartbeatPathForOutputDir(outputDir),
1207
+ runId: request.id,
1208
+ status: request.status,
1209
+ outputDir,
1210
+ pid: process.pid,
1211
+ detail: request.detail,
1212
+ command: request.result?.command,
1213
+ exitCode: request.result?.exitCode,
1214
+ loopCount: request.status === "running" ? 0 : 1,
1215
+ now
1216
+ });
1217
+ }
1218
+ async function syncRunRequestMeta(request, now) {
1219
+ const outputDir = request.spec.options?.outputDir;
1220
+ if (!outputDir || request.status === "pending")
1221
+ return;
1222
+ await updateHostRunMeta({
1223
+ file: hostRunMetaPathForOutputDir(outputDir),
1224
+ runId: request.id,
1225
+ status: request.status,
1226
+ actualLoops: request.status === "running" ? 0 : 1,
1227
+ detail: request.detail,
1228
+ command: request.result?.command,
1229
+ exitCode: request.result?.exitCode,
1230
+ now
1231
+ });
1232
+ }
1233
+ async function syncRunRequestLoopEvent(request, now) {
1234
+ const outputDir = request.spec.options?.outputDir;
1235
+ if (!outputDir || request.status === "pending")
1236
+ return;
1237
+ await appendHostLoopEvent({
1238
+ outputDir,
1239
+ event: {
1240
+ type: "run.status",
1241
+ runId: request.id,
1242
+ timestamp: (now ?? (() => new Date()))().toISOString(),
1243
+ status: request.status,
1244
+ detail: request.detail,
1245
+ command: request.result?.command,
1246
+ exitCode: request.result?.exitCode,
1247
+ loop: request.status === "running" ? 0 : 1,
1248
+ source: "update-run"
1249
+ }
1250
+ });
1251
+ }
1252
+ function normalizeExportInput(value) {
1253
+ if (!value)
1254
+ return {};
1255
+ if (value && typeof value === "object")
1256
+ return value;
1257
+ throw new Error("export input must be an object");
1258
+ }
1259
+ function cleanInputString(value) {
1260
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1261
+ }
1262
+ function numberInput(value) {
1263
+ if (typeof value === "number" && Number.isFinite(value))
1264
+ return value;
1265
+ if (typeof value === "string" && value.trim()) {
1266
+ const parsed = Number(value);
1267
+ if (Number.isFinite(parsed))
1268
+ return parsed;
1269
+ }
1270
+ return undefined;
1271
+ }
1272
+ function dropUndefined(value) {
1273
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
1274
+ }
1275
+ function normalizeAvailableEngines(value) {
1276
+ if (!Array.isArray(value))
1277
+ return undefined;
1278
+ return value.filter((entry) => entry === "claude" || entry === "codex");
1279
+ }