@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.
- package/README.md +96 -0
- package/dist/src/agent-config-validation.d.ts +9 -0
- package/dist/src/agent-config-validation.js +30 -0
- package/dist/src/api.d.ts +4 -0
- package/dist/src/api.js +48 -0
- package/dist/src/attachments.d.ts +45 -0
- package/dist/src/attachments.js +322 -0
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +1697 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +20 -0
- package/dist/src/cron.d.ts +11 -0
- package/dist/src/cron.js +65 -0
- package/dist/src/daemon.d.ts +36 -0
- package/dist/src/daemon.js +373 -0
- package/dist/src/engine.d.ts +32 -0
- package/dist/src/engine.js +1014 -0
- package/dist/src/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat.js +28 -0
- package/dist/src/host-api.d.ts +40 -0
- package/dist/src/host-api.js +59 -0
- package/dist/src/host-control.d.ts +48 -0
- package/dist/src/host-control.js +1279 -0
- package/dist/src/host-export.d.ts +50 -0
- package/dist/src/host-export.js +187 -0
- package/dist/src/host-feedback.d.ts +78 -0
- package/dist/src/host-feedback.js +178 -0
- package/dist/src/host-home.d.ts +13 -0
- package/dist/src/host-home.js +54 -0
- package/dist/src/host-ledger.d.ts +261 -0
- package/dist/src/host-ledger.js +554 -0
- package/dist/src/host-loop-events.d.ts +69 -0
- package/dist/src/host-loop-events.js +288 -0
- package/dist/src/host-permission.d.ts +36 -0
- package/dist/src/host-permission.js +180 -0
- package/dist/src/host-policy.d.ts +15 -0
- package/dist/src/host-policy.js +36 -0
- package/dist/src/host-run-executor.d.ts +13 -0
- package/dist/src/host-run-executor.js +221 -0
- package/dist/src/host-run-heartbeat.d.ts +40 -0
- package/dist/src/host-run-heartbeat.js +103 -0
- package/dist/src/host-run-layout.d.ts +17 -0
- package/dist/src/host-run-layout.js +387 -0
- package/dist/src/host-run-meta.d.ts +41 -0
- package/dist/src/host-run-meta.js +115 -0
- package/dist/src/host-run-spec.d.ts +149 -0
- package/dist/src/host-run-spec.js +465 -0
- package/dist/src/host-runs.d.ts +77 -0
- package/dist/src/host-runs.js +195 -0
- package/dist/src/host-sdk.d.ts +412 -0
- package/dist/src/host-sdk.js +628 -0
- package/dist/src/host-server.d.ts +26 -0
- package/dist/src/host-server.js +921 -0
- package/dist/src/host-timeline.d.ts +24 -0
- package/dist/src/host-timeline.js +161 -0
- package/dist/src/jsonl.d.ts +13 -0
- package/dist/src/jsonl.js +47 -0
- package/dist/src/lifecycle.d.ts +5 -0
- package/dist/src/lifecycle.js +18 -0
- package/dist/src/message-routing.d.ts +32 -0
- package/dist/src/message-routing.js +119 -0
- package/dist/src/paths.d.ts +19 -0
- package/dist/src/paths.js +26 -0
- package/dist/src/project-profile.d.ts +49 -0
- package/dist/src/project-profile.js +356 -0
- package/dist/src/remediation.d.ts +14 -0
- package/dist/src/remediation.js +114 -0
- package/dist/src/remote-devices.d.ts +41 -0
- package/dist/src/remote-devices.js +156 -0
- package/dist/src/remote-diagnostics.d.ts +39 -0
- package/dist/src/remote-diagnostics.js +199 -0
- package/dist/src/remote-ssh.d.ts +39 -0
- package/dist/src/remote-ssh.js +129 -0
- package/dist/src/run-stream.d.ts +57 -0
- package/dist/src/run-stream.js +119 -0
- package/dist/src/runner.d.ts +131 -0
- package/dist/src/runner.js +1161 -0
- package/dist/src/runtime-data.d.ts +68 -0
- package/dist/src/runtime-data.js +172 -0
- package/dist/src/service.d.ts +114 -0
- package/dist/src/service.js +631 -0
- package/dist/src/shared-skills.d.ts +26 -0
- package/dist/src/shared-skills.js +85 -0
- package/dist/src/shim.d.ts +1 -0
- package/dist/src/shim.js +64 -0
- package/dist/src/skill-check.d.ts +17 -0
- package/dist/src/skill-check.js +158 -0
- package/dist/src/sse.d.ts +9 -0
- package/dist/src/sse.js +36 -0
- package/dist/src/team-routing.d.ts +55 -0
- package/dist/src/team-routing.js +131 -0
- package/dist/src/team-workflow.d.ts +78 -0
- package/dist/src/team-workflow.js +253 -0
- package/dist/src/text.d.ts +7 -0
- package/dist/src/text.js +27 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.js +1 -0
- package/dist/src/usage.d.ts +116 -0
- package/dist/src/usage.js +350 -0
- package/dist/src/workspace.d.ts +9 -0
- package/dist/src/workspace.js +56 -0
- package/dist/src/worktree.d.ts +47 -0
- package/dist/src/worktree.js +201 -0
- 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
|
+
}
|