@fabric-harness/cli 0.5.0 → 0.6.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 +11 -5
- package/dist/bin/fabric-harness.js +897 -398
- package/dist/bin/fabric-harness.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from
|
|
3
|
-
import { randomUUID } from
|
|
4
|
-
import { promises as fs, watch } from
|
|
5
|
-
import path from
|
|
6
|
-
import { parseEnv } from
|
|
7
|
-
import { applyEnvModelProvider, buildWorkspace, compactSessionInStore, createConfiguredSessionStore,
|
|
8
|
-
import { FileSessionStore } from
|
|
9
|
-
import { EmptySandboxEnv, SchemaValidationError, createBuiltinTools, resolveModelProvider, toolsToModelSchemas } from
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { promises as fs, watch } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseEnv } from "node:util";
|
|
7
|
+
import { applyEnvModelProvider, buildWorkspace, cancelTaskInStore, compactSessionInStore, createConfiguredSessionStore, describeAgentFile, findWorkspaceRoot, getMetricsFromStore, getRequiredAgentDefinition, getTaskFromStore, inspectReplayFromStore, inspectSessionFromStore, listAgentSummaries, listApprovalStatesFromStore, listApprovalsFromStore, listBuilds, listCheckpointsFromStore, listSessionSummariesFromStore, listTasksFromStore, loadAgentModule, loadFabricHarnessConfig, resolveAgentPath, resolveApprovalInStore, runAgent, startDevServer, verifyAttestation, verifyProvenance, } from "@fabric-harness/node";
|
|
8
|
+
import { FileSessionStore } from "@fabric-harness/node";
|
|
9
|
+
import { EmptySandboxEnv, SchemaValidationError, createBuiltinTools, resolveModelProvider, toolsToModelSchemas, } from "@fabric-harness/sdk";
|
|
10
10
|
const ORIGINAL_ENV = { ...process.env };
|
|
11
|
-
import { createLocalTemporalActivities, createTemporalClient, startTemporalWorker } from
|
|
11
|
+
import { createLocalTemporalActivities, createTemporalClient, startTemporalWorker, } from "@fabric-harness/temporal";
|
|
12
12
|
const HELP = `Fabric Harness
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
@@ -19,7 +19,7 @@ Usage:
|
|
|
19
19
|
fabric-harness run <agent> [--target temporal-worker] [--model provider/model] [--id <id>] --prompt <text>
|
|
20
20
|
fabric-harness agents [--json]
|
|
21
21
|
fabric-harness describe <agent> [--json]
|
|
22
|
-
fabric-harness build [--target node|temporal-worker|docker|foundry-hosted-agent|cloudflare] [--out <dir>] [--sbom] [--sbom-required] [--provenance] [--attestation] [--sign-provenance] [--signing-key <path|env://VAR>] [--docker-build] [--docker-push] [--docker-tag <tag>] [--image-sbom] [--image-sbom-required]
|
|
22
|
+
fabric-harness build [--target node|temporal-worker|docker|foundry-hosted-agent|cloudflare|aks|aca] [--out <dir>] [--sbom] [--sbom-required] [--provenance] [--attestation] [--sign-provenance] [--signing-key <path|env://VAR>] [--docker-build] [--docker-push] [--docker-tag <tag>] [--image-sbom] [--image-sbom-required]
|
|
23
23
|
fabric-harness sessions
|
|
24
24
|
fabric-harness builds
|
|
25
25
|
fabric-harness verify-attestation <build-dir-or-attestation>
|
|
@@ -42,6 +42,7 @@ Usage:
|
|
|
42
42
|
fabric-harness reject <session-id> <approval-id> [--actor <id>] [--reason <text>]
|
|
43
43
|
fabric-harness dev [--target node|cloudflare] [--env <file>] [--host <host>] [--port <port>] [--auth-token-env <var>] [--max-body-bytes <n>] [--rate-limit-window-ms <n>] [--rate-limit-max <n>]
|
|
44
44
|
fabric-harness temporal-worker [--task-queue <name>] [--address <host:port>] [--env <file>]
|
|
45
|
+
fabric-harness init [--dir <path>] [--model <provider/model>] [--force]
|
|
45
46
|
|
|
46
47
|
Commands:
|
|
47
48
|
run <agent> Run a local .fabricharness agent handler
|
|
@@ -60,12 +61,13 @@ Commands:
|
|
|
60
61
|
task <id> <task-id> Show durable task status and checkpoints
|
|
61
62
|
cancel-task <id> Append a cancellation marker for a running task
|
|
62
63
|
compact <id> Add a compaction entry to a persisted session
|
|
63
|
-
replay <id> Show read-only active path and model context
|
|
64
|
+
replay <id> Show read-only active path and model context (--from <step-id>, --limit <n>)
|
|
64
65
|
approvals <id> List approval requests for a persisted session
|
|
65
66
|
approve <id> <app> Mark an approval request approved
|
|
66
67
|
reject <id> <app> Mark an approval request denied
|
|
67
68
|
dev Start local HTTP/SSE dev server
|
|
68
69
|
temporal-worker Start a local Temporal worker with Fabric activities
|
|
70
|
+
init Scaffold a .fabricharness/ workspace in the current dir
|
|
69
71
|
add List or print connector implementation recipes
|
|
70
72
|
|
|
71
73
|
Options:
|
|
@@ -111,126 +113,127 @@ function parseJsonPayload(value) {
|
|
|
111
113
|
}
|
|
112
114
|
function parsePayloadValue(value) {
|
|
113
115
|
const trimmed = value.trim();
|
|
114
|
-
if (trimmed ===
|
|
116
|
+
if (trimmed === "true")
|
|
115
117
|
return true;
|
|
116
|
-
if (trimmed ===
|
|
118
|
+
if (trimmed === "false")
|
|
117
119
|
return false;
|
|
118
|
-
if (trimmed ===
|
|
120
|
+
if (trimmed === "null")
|
|
119
121
|
return null;
|
|
120
122
|
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
121
123
|
return Number(trimmed);
|
|
122
|
-
if ((trimmed.startsWith(
|
|
124
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
125
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]")))
|
|
123
126
|
return parseJsonPayload(trimmed);
|
|
124
127
|
return value;
|
|
125
128
|
}
|
|
126
129
|
function applyPayloadField(fields, assignment) {
|
|
127
|
-
const equals = assignment.indexOf(
|
|
130
|
+
const equals = assignment.indexOf("=");
|
|
128
131
|
if (equals <= 0)
|
|
129
132
|
throw new Error(`Expected payload assignment key=value, got: ${assignment}`);
|
|
130
133
|
fields[assignment.slice(0, equals)] = parsePayloadValue(assignment.slice(equals + 1));
|
|
131
134
|
}
|
|
132
135
|
function parseRunArgs(args) {
|
|
133
136
|
const [agent, ...rest] = args;
|
|
134
|
-
if (!agent || agent.startsWith(
|
|
135
|
-
throw new Error(
|
|
137
|
+
if (!agent || agent.startsWith("-"))
|
|
138
|
+
throw new Error("Missing required <agent> argument for run.");
|
|
136
139
|
const parsed = { agent };
|
|
137
140
|
for (let index = 0; index < rest.length; index += 1) {
|
|
138
141
|
const arg = rest[index];
|
|
139
142
|
if (arg === undefined)
|
|
140
143
|
continue;
|
|
141
|
-
if (arg ===
|
|
144
|
+
if (arg === "--id") {
|
|
142
145
|
const value = rest[++index];
|
|
143
146
|
if (!value)
|
|
144
|
-
throw new Error(
|
|
147
|
+
throw new Error("Missing value for --id.");
|
|
145
148
|
parsed.id = value;
|
|
146
149
|
continue;
|
|
147
150
|
}
|
|
148
|
-
if (arg ===
|
|
151
|
+
if (arg === "--payload") {
|
|
149
152
|
const value = rest[++index];
|
|
150
153
|
if (!value)
|
|
151
|
-
throw new Error(
|
|
154
|
+
throw new Error("Missing value for --payload.");
|
|
152
155
|
parsed.payload = parseJsonPayload(value);
|
|
153
156
|
continue;
|
|
154
157
|
}
|
|
155
|
-
if (arg ===
|
|
158
|
+
if (arg === "--payload-file") {
|
|
156
159
|
const value = rest[++index];
|
|
157
160
|
if (!value)
|
|
158
|
-
throw new Error(
|
|
161
|
+
throw new Error("Missing value for --payload-file.");
|
|
159
162
|
parsed.payloadFile = value;
|
|
160
163
|
continue;
|
|
161
164
|
}
|
|
162
|
-
if (arg ===
|
|
165
|
+
if (arg === "--stdin") {
|
|
163
166
|
parsed.stdin = true;
|
|
164
167
|
continue;
|
|
165
168
|
}
|
|
166
|
-
if (arg ===
|
|
169
|
+
if (arg === "--set") {
|
|
167
170
|
const value = rest[++index];
|
|
168
171
|
if (!value)
|
|
169
|
-
throw new Error(
|
|
172
|
+
throw new Error("Missing value for --set.");
|
|
170
173
|
parsed.payloadFields ??= {};
|
|
171
174
|
applyPayloadField(parsed.payloadFields, value);
|
|
172
175
|
continue;
|
|
173
176
|
}
|
|
174
|
-
if (arg ===
|
|
177
|
+
if (arg === "--runtime") {
|
|
175
178
|
const value = rest[++index];
|
|
176
|
-
if (value !==
|
|
177
|
-
throw new Error(
|
|
179
|
+
if (value !== "inline" && value !== "temporal")
|
|
180
|
+
throw new Error("--runtime must be inline or temporal.");
|
|
178
181
|
parsed.runtime = value;
|
|
179
182
|
continue;
|
|
180
183
|
}
|
|
181
|
-
if (arg ===
|
|
184
|
+
if (arg === "--target") {
|
|
182
185
|
const value = rest[++index];
|
|
183
|
-
if (value !==
|
|
184
|
-
throw new Error(
|
|
186
|
+
if (value !== "node" && value !== "temporal-worker")
|
|
187
|
+
throw new Error("--target for run must be node or temporal-worker.");
|
|
185
188
|
parsed.target = value;
|
|
186
|
-
parsed.runtime = value ===
|
|
189
|
+
parsed.runtime = value === "temporal-worker" ? "temporal" : "inline";
|
|
187
190
|
continue;
|
|
188
191
|
}
|
|
189
|
-
if (arg ===
|
|
192
|
+
if (arg === "--prompt") {
|
|
190
193
|
const value = rest[++index];
|
|
191
194
|
if (!value)
|
|
192
|
-
throw new Error(
|
|
195
|
+
throw new Error("Missing value for --prompt.");
|
|
193
196
|
parsed.prompt = value;
|
|
194
197
|
continue;
|
|
195
198
|
}
|
|
196
|
-
if (arg ===
|
|
199
|
+
if (arg === "--model") {
|
|
197
200
|
const value = rest[++index];
|
|
198
201
|
if (!value)
|
|
199
|
-
throw new Error(
|
|
202
|
+
throw new Error("Missing value for --model.");
|
|
200
203
|
parsed.model = value;
|
|
201
204
|
continue;
|
|
202
205
|
}
|
|
203
|
-
if (arg ===
|
|
206
|
+
if (arg === "--cwd") {
|
|
204
207
|
const value = rest[++index];
|
|
205
208
|
if (!value)
|
|
206
|
-
throw new Error(
|
|
209
|
+
throw new Error("Missing value for --cwd.");
|
|
207
210
|
parsed.cwd = value;
|
|
208
211
|
continue;
|
|
209
212
|
}
|
|
210
|
-
if (arg ===
|
|
213
|
+
if (arg === "--env") {
|
|
211
214
|
const value = rest[++index];
|
|
212
215
|
if (!value)
|
|
213
|
-
throw new Error(
|
|
216
|
+
throw new Error("Missing value for --env.");
|
|
214
217
|
parsed.envFiles ??= [];
|
|
215
218
|
parsed.envFiles.push(value);
|
|
216
219
|
continue;
|
|
217
220
|
}
|
|
218
|
-
if (arg ===
|
|
221
|
+
if (arg === "--help" || arg === "-h") {
|
|
219
222
|
console.log(HELP);
|
|
220
223
|
process.exit(0);
|
|
221
224
|
}
|
|
222
|
-
if (arg.startsWith(
|
|
225
|
+
if (arg.startsWith("--")) {
|
|
223
226
|
const key = arg.slice(2);
|
|
224
227
|
if (!key)
|
|
225
228
|
throw new Error(`Unknown argument: ${arg}`);
|
|
226
229
|
const value = rest[++index];
|
|
227
|
-
if (value === undefined || value.startsWith(
|
|
230
|
+
if (value === undefined || value.startsWith("--"))
|
|
228
231
|
throw new Error(`Missing value for --${key}.`);
|
|
229
232
|
parsed.payloadFields ??= {};
|
|
230
233
|
parsed.payloadFields[key] = parsePayloadValue(value);
|
|
231
234
|
continue;
|
|
232
235
|
}
|
|
233
|
-
if (arg.includes(
|
|
236
|
+
if (arg.includes("=")) {
|
|
234
237
|
parsed.payloadFields ??= {};
|
|
235
238
|
applyPayloadField(parsed.payloadFields, arg);
|
|
236
239
|
continue;
|
|
@@ -240,7 +243,7 @@ function parseRunArgs(args) {
|
|
|
240
243
|
return parsed;
|
|
241
244
|
}
|
|
242
245
|
function printResult(result) {
|
|
243
|
-
if (typeof result ===
|
|
246
|
+
if (typeof result === "string") {
|
|
244
247
|
console.log(result);
|
|
245
248
|
return;
|
|
246
249
|
}
|
|
@@ -255,10 +258,10 @@ async function loadEnvFiles(envFiles, options = {}) {
|
|
|
255
258
|
const filePath = path.resolve(process.cwd(), envFile);
|
|
256
259
|
let contents;
|
|
257
260
|
try {
|
|
258
|
-
contents = await fs.readFile(filePath,
|
|
261
|
+
contents = await fs.readFile(filePath, "utf8");
|
|
259
262
|
}
|
|
260
263
|
catch (error) {
|
|
261
|
-
if (options.ignoreMissing && error.code ===
|
|
264
|
+
if (options.ignoreMissing && error.code === "ENOENT")
|
|
262
265
|
continue;
|
|
263
266
|
const message = error instanceof Error ? error.message : String(error);
|
|
264
267
|
throw new Error(`Failed to read --env file ${envFile}: ${message}`);
|
|
@@ -276,13 +279,15 @@ async function loadAutoEnvFiles() {
|
|
|
276
279
|
const cwd = process.cwd();
|
|
277
280
|
const workspaceRoot = await findWorkspaceRoot(cwd).catch(() => undefined);
|
|
278
281
|
const repoRoot = await findRepoRoot(cwd).catch(() => undefined);
|
|
279
|
-
const roots = [
|
|
282
|
+
const roots = [
|
|
283
|
+
...new Set([repoRoot, workspaceRoot].filter((value) => Boolean(value))),
|
|
284
|
+
];
|
|
280
285
|
const files = [];
|
|
281
286
|
for (const root of roots) {
|
|
282
|
-
files.push(path.join(root,
|
|
287
|
+
files.push(path.join(root, ".env"), path.join(root, ".env.local"));
|
|
283
288
|
}
|
|
284
289
|
if (workspaceRoot) {
|
|
285
|
-
files.push(path.join(workspaceRoot,
|
|
290
|
+
files.push(path.join(workspaceRoot, ".fabricharness", ".env"), path.join(workspaceRoot, ".fabricharness", ".env.local"));
|
|
286
291
|
}
|
|
287
292
|
await loadEnvFiles(files, { ignoreMissing: true });
|
|
288
293
|
}
|
|
@@ -290,7 +295,7 @@ async function findRepoRoot(start) {
|
|
|
290
295
|
let current = path.resolve(start);
|
|
291
296
|
for (;;) {
|
|
292
297
|
try {
|
|
293
|
-
await fs.access(path.join(current,
|
|
298
|
+
await fs.access(path.join(current, "pnpm-workspace.yaml"));
|
|
294
299
|
return current;
|
|
295
300
|
}
|
|
296
301
|
catch {
|
|
@@ -307,7 +312,7 @@ async function runCommand(args) {
|
|
|
307
312
|
await loadEnvFiles(parsedArgs.envFiles, { explicit: true });
|
|
308
313
|
const parsed = await resolvePayloadInputs(parsedArgs);
|
|
309
314
|
const resolved = await resolveRunDefaults(parsed);
|
|
310
|
-
if (resolved.runtime ===
|
|
315
|
+
if (resolved.runtime === "temporal")
|
|
311
316
|
return runTemporalPromptCommand(resolved);
|
|
312
317
|
const { cwd: sessionCwd, ...runOptions } = resolved;
|
|
313
318
|
const { result } = await runAgent({ ...runOptions, ...(sessionCwd ? { sessionCwd } : {}) });
|
|
@@ -317,13 +322,15 @@ async function resolvePayloadInputs(parsed) {
|
|
|
317
322
|
let payload = parsed.payload;
|
|
318
323
|
if (parsed.payloadFile) {
|
|
319
324
|
const filePath = path.resolve(process.cwd(), parsed.payloadFile);
|
|
320
|
-
payload = parseJsonPayload(await fs.readFile(filePath,
|
|
325
|
+
payload = parseJsonPayload(await fs.readFile(filePath, "utf8"));
|
|
321
326
|
}
|
|
322
327
|
if (parsed.stdin) {
|
|
323
328
|
payload = parseJsonPayload(await readStdin());
|
|
324
329
|
}
|
|
325
330
|
if (parsed.payloadFields && Object.keys(parsed.payloadFields).length > 0) {
|
|
326
|
-
const base = typeof payload ===
|
|
331
|
+
const base = typeof payload === "object" && payload !== null && !Array.isArray(payload)
|
|
332
|
+
? payload
|
|
333
|
+
: {};
|
|
327
334
|
payload = { ...base, ...parsed.payloadFields };
|
|
328
335
|
}
|
|
329
336
|
return { ...parsed, ...(payload !== undefined ? { payload } : {}) };
|
|
@@ -332,20 +339,24 @@ async function readStdin() {
|
|
|
332
339
|
const chunks = [];
|
|
333
340
|
for await (const chunk of process.stdin)
|
|
334
341
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
335
|
-
return Buffer.concat(chunks).toString(
|
|
342
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
336
343
|
}
|
|
337
344
|
async function resolveRunDefaults(parsed) {
|
|
338
345
|
const workspaceRoot = await findWorkspaceRoot();
|
|
339
346
|
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
340
347
|
const envTarget = process.env.FABRIC_TARGET ?? process.env.FABRIC_HARNESS_TARGET;
|
|
341
|
-
if (envTarget !== undefined && envTarget !==
|
|
342
|
-
throw new Error(
|
|
348
|
+
if (envTarget !== undefined && envTarget !== "node" && envTarget !== "temporal-worker")
|
|
349
|
+
throw new Error("FABRIC_TARGET/FABRIC_HARNESS_TARGET must be node or temporal-worker.");
|
|
343
350
|
const agentDefaults = await readAgentRunDefaults(workspaceRoot, parsed.agent);
|
|
344
351
|
const target = parsed.target ?? envTarget ?? config.run?.target ?? agentDefaults.target;
|
|
345
|
-
if (target !== undefined && target !==
|
|
346
|
-
throw new Error(
|
|
347
|
-
const runtime = parsed.runtime ?? (target ===
|
|
348
|
-
const model = parsed.model ??
|
|
352
|
+
if (target !== undefined && target !== "node" && target !== "temporal-worker")
|
|
353
|
+
throw new Error("Configured run.target must be node or temporal-worker.");
|
|
354
|
+
const runtime = parsed.runtime ?? (target === "temporal-worker" ? "temporal" : "inline");
|
|
355
|
+
const model = parsed.model ??
|
|
356
|
+
process.env.FABRIC_MODEL ??
|
|
357
|
+
config.run?.model ??
|
|
358
|
+
agentDefaults.model ??
|
|
359
|
+
(typeof config.agent?.model === "string" ? config.agent.model : undefined);
|
|
349
360
|
const id = parsed.id ?? `${config.run?.idPrefix ?? parsed.agent}-${randomUUID()}`;
|
|
350
361
|
const cwd = parsed.cwd ?? config.run?.cwd;
|
|
351
362
|
return {
|
|
@@ -372,37 +383,47 @@ async function readAgentRunDefaults(workspaceRoot, agentName) {
|
|
|
372
383
|
}
|
|
373
384
|
async function runTemporalPromptCommand(parsed) {
|
|
374
385
|
if (!parsed.id)
|
|
375
|
-
throw new Error(
|
|
386
|
+
throw new Error("--id is required with --runtime temporal.");
|
|
376
387
|
const workspaceRoot = await findWorkspaceRoot();
|
|
377
388
|
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
378
|
-
const taskQueue = process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ??
|
|
379
|
-
const address = process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ??
|
|
389
|
+
const taskQueue = process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? "fabric-harness";
|
|
390
|
+
const address = process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? "localhost:7233";
|
|
380
391
|
const namespace = process.env.FABRIC_TEMPORAL_NAMESPACE ?? config.temporal?.namespace;
|
|
381
392
|
const client = await createTemporalClient({
|
|
382
|
-
mode:
|
|
393
|
+
mode: "temporal",
|
|
383
394
|
address,
|
|
384
395
|
taskQueue,
|
|
385
396
|
...(namespace ? { namespace } : {}),
|
|
386
397
|
...(config.temporal?.apiKey ? { apiKey: config.temporal.apiKey } : {}),
|
|
387
398
|
...(config.temporal?.apiKeyEnv ? { apiKeyEnv: config.temporal.apiKeyEnv } : {}),
|
|
388
399
|
...(config.temporal?.tls ? { tls: config.temporal.tls } : {}),
|
|
389
|
-
workflowIdPrefix: config.temporal?.workflowIdPrefix ??
|
|
390
|
-
promptWorkflowMode: config.temporal?.promptWorkflowMode ??
|
|
400
|
+
workflowIdPrefix: config.temporal?.workflowIdPrefix ?? "fabric-cli",
|
|
401
|
+
promptWorkflowMode: config.temporal?.promptWorkflowMode ?? "hybrid",
|
|
391
402
|
});
|
|
392
403
|
try {
|
|
393
404
|
if (parsed.prompt) {
|
|
394
405
|
const runtime = client.createSessionRuntime(parsed.id);
|
|
395
|
-
const options = {
|
|
396
|
-
|
|
406
|
+
const options = {
|
|
407
|
+
...(parsed.model ? { model: parsed.model } : {}),
|
|
408
|
+
...(parsed.cwd ? { cwd: parsed.cwd } : {}),
|
|
409
|
+
};
|
|
410
|
+
printResult(await runtime.prompt({
|
|
411
|
+
text: parsed.prompt,
|
|
412
|
+
...(Object.keys(options).length > 0 ? { options } : {}),
|
|
413
|
+
}));
|
|
397
414
|
return;
|
|
398
415
|
}
|
|
399
416
|
const agentPath = await resolveAgentPath(workspaceRoot, parsed.agent);
|
|
400
417
|
const module = await loadAgentModule({ workspaceRoot, agentPath });
|
|
401
418
|
getRequiredAgentDefinition(module.default, agentPath);
|
|
402
419
|
const handler = module.default;
|
|
403
|
-
if (typeof handler !==
|
|
420
|
+
if (typeof handler !== "function")
|
|
404
421
|
throw new Error(`Agent module must default-export agent({...}) from @fabric-harness/sdk: ${agentPath}`);
|
|
405
|
-
const store = await createConfiguredSessionStore({
|
|
422
|
+
const store = await createConfiguredSessionStore({
|
|
423
|
+
workspaceRoot,
|
|
424
|
+
...(config.store ? { config: config.store } : {}),
|
|
425
|
+
env: process.env,
|
|
426
|
+
});
|
|
406
427
|
const context = createTemporalFabricContext(parsed, client.createSessionRuntime.bind(client), store);
|
|
407
428
|
printResult(await handler(context));
|
|
408
429
|
}
|
|
@@ -411,12 +432,14 @@ async function runTemporalPromptCommand(parsed) {
|
|
|
411
432
|
}
|
|
412
433
|
}
|
|
413
434
|
function createTemporalFabricContext(parsed, runtimeForSession, store) {
|
|
414
|
-
const payload = (typeof parsed.payload ===
|
|
435
|
+
const payload = (typeof parsed.payload === "object" && parsed.payload !== null && !Array.isArray(parsed.payload)
|
|
436
|
+
? parsed.payload
|
|
437
|
+
: {});
|
|
415
438
|
return {
|
|
416
439
|
payload,
|
|
417
440
|
init: async (initOptions = {}) => {
|
|
418
|
-
const agentId = initOptions.id ?? parsed.id ??
|
|
419
|
-
const model = parsed.model ?? (typeof initOptions.model ===
|
|
441
|
+
const agentId = initOptions.id ?? parsed.id ?? "temporal-agent";
|
|
442
|
+
const model = parsed.model ?? (typeof initOptions.model === "string" ? initOptions.model : undefined);
|
|
420
443
|
const defaultCwd = parsed.cwd ?? initOptions.cwd;
|
|
421
444
|
return {
|
|
422
445
|
id: agentId,
|
|
@@ -436,43 +459,154 @@ function createTemporalSession(sessionId, runtimeForSession, model, store, cwd)
|
|
|
436
459
|
};
|
|
437
460
|
return {
|
|
438
461
|
id: sessionId,
|
|
439
|
-
agent: {
|
|
462
|
+
agent: {
|
|
463
|
+
id: sessionId,
|
|
464
|
+
...(model ? { model } : {}),
|
|
465
|
+
session: async (_id, options) => createTemporalSession(sessionId, runtimeForSession, model, store, options?.cwd ?? cwd),
|
|
466
|
+
},
|
|
440
467
|
// Do not create a rejected promise here: many agents never access session.sandbox,
|
|
441
468
|
// but Node treats an eagerly rejected promise as unhandled. Direct sandbox effects
|
|
442
469
|
// in Temporal CLI mode should go through session.shell()/tools, which are routed
|
|
443
470
|
// to workflow activities. This placeholder prevents accidental unhandled rejection.
|
|
444
471
|
sandbox: Promise.resolve(new EmptySandboxEnv()),
|
|
445
|
-
prompt: (text, options) => {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
472
|
+
prompt: (text, options) => {
|
|
473
|
+
const merged = withDefaults(options);
|
|
474
|
+
return runtime.prompt({
|
|
475
|
+
text,
|
|
476
|
+
...(merged || model
|
|
477
|
+
? {
|
|
478
|
+
options: {
|
|
479
|
+
...(merged ?? {}),
|
|
480
|
+
...(model && merged?.model === undefined ? { model } : {}),
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
: {}),
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
stream: async function* (text, options) {
|
|
487
|
+
for (const event of [])
|
|
488
|
+
yield event;
|
|
489
|
+
const merged = withDefaults(options);
|
|
490
|
+
return await runtime.prompt({
|
|
491
|
+
text,
|
|
492
|
+
...(merged || model
|
|
493
|
+
? {
|
|
494
|
+
options: {
|
|
495
|
+
...(merged ?? {}),
|
|
496
|
+
...(model && merged?.model === undefined ? { model } : {}),
|
|
497
|
+
},
|
|
498
|
+
}
|
|
499
|
+
: {}),
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
skill: (name, options) => {
|
|
503
|
+
const merged = withDefaults(options);
|
|
504
|
+
return runtime.prompt({
|
|
505
|
+
text: `Run skill ${name}${options?.args ? ` with args ${JSON.stringify(options.args)}` : ""}`,
|
|
506
|
+
...(merged || model
|
|
507
|
+
? {
|
|
508
|
+
options: {
|
|
509
|
+
...(merged ?? {}),
|
|
510
|
+
...(model && merged?.model === undefined ? { model } : {}),
|
|
511
|
+
},
|
|
512
|
+
}
|
|
513
|
+
: {}),
|
|
514
|
+
});
|
|
515
|
+
},
|
|
516
|
+
task: (text, options) => {
|
|
517
|
+
const merged = withDefaults(options);
|
|
518
|
+
return runtime.task({
|
|
519
|
+
text,
|
|
520
|
+
...(options?.id ? { taskId: options.id } : {}),
|
|
521
|
+
...(merged || model
|
|
522
|
+
? {
|
|
523
|
+
options: {
|
|
524
|
+
...(merged ?? {}),
|
|
525
|
+
...(model && merged?.model === undefined ? { model } : {}),
|
|
526
|
+
},
|
|
527
|
+
}
|
|
528
|
+
: {}),
|
|
529
|
+
});
|
|
530
|
+
},
|
|
450
531
|
shell: (command, options) => {
|
|
451
532
|
const merged = withDefaults(options);
|
|
452
533
|
return runtime.shell({ command, ...(merged ? { options: merged } : {}) });
|
|
453
534
|
},
|
|
454
|
-
|
|
535
|
+
mount: async (mountAt, source, mountOptions) => {
|
|
536
|
+
// Temporal CLI session has a placeholder sandbox; mounts here are best-effort
|
|
537
|
+
// and do not survive across activity boundaries. For durable mounts, mount
|
|
538
|
+
// inside the agent run() body before delegating to Temporal.
|
|
539
|
+
const sandbox = new EmptySandboxEnv();
|
|
540
|
+
let files = 0;
|
|
541
|
+
let bytes = 0;
|
|
542
|
+
for await (const entry of source.entries()) {
|
|
543
|
+
files += 1;
|
|
544
|
+
bytes +=
|
|
545
|
+
typeof entry.content === "string"
|
|
546
|
+
? Buffer.byteLength(entry.content, "utf8")
|
|
547
|
+
: entry.content.byteLength;
|
|
548
|
+
void sandbox;
|
|
549
|
+
}
|
|
550
|
+
const mode = mountOptions?.mode ?? "read";
|
|
551
|
+
return { mountAt, files, bytes, mode, ...(source.name ? { source: source.name } : {}) };
|
|
552
|
+
},
|
|
553
|
+
approval: {
|
|
554
|
+
request: async () => {
|
|
555
|
+
// Custom approvals through the CLI Temporal shim are not supported; users
|
|
556
|
+
// should configure capability policy approvals or run with the SDK's
|
|
557
|
+
// direct API where session.approval.request() is wired through.
|
|
558
|
+
throw new Error('session.approval.request() is not yet supported in the CLI Temporal session shim. Use init({ runtime: "temporal", sessionRuntime }) directly.');
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
history: async () => ({
|
|
562
|
+
id: sessionId,
|
|
563
|
+
createdAt: new Date().toISOString(),
|
|
564
|
+
updatedAt: new Date().toISOString(),
|
|
565
|
+
entries: [],
|
|
566
|
+
events: [],
|
|
567
|
+
}),
|
|
455
568
|
checkpoint: {
|
|
456
569
|
create: (options) => runtime.checkpointCreate({ options }),
|
|
457
570
|
restore: (options) => runtime.checkpointRestore({ options }),
|
|
458
571
|
},
|
|
459
572
|
artifact: async (name, content, options) => {
|
|
460
573
|
if (!store.putArtifact)
|
|
461
|
-
throw new Error(
|
|
574
|
+
throw new Error("Configured session store does not support artifacts.");
|
|
462
575
|
const ref = await store.putArtifact(sessionId, name, content, options);
|
|
463
576
|
const timestamp = new Date().toISOString();
|
|
464
|
-
const entry = {
|
|
577
|
+
const entry = {
|
|
578
|
+
id: randomUUID(),
|
|
579
|
+
type: "artifact_created",
|
|
580
|
+
timestamp,
|
|
581
|
+
data: { artifact: ref },
|
|
582
|
+
};
|
|
465
583
|
if (store.appendEntry)
|
|
466
584
|
await store.appendEntry(sessionId, entry);
|
|
467
585
|
else {
|
|
468
586
|
const existing = await store.load(sessionId);
|
|
469
|
-
await store.save({
|
|
587
|
+
await store.save({
|
|
588
|
+
id: sessionId,
|
|
589
|
+
createdAt: existing?.createdAt ?? timestamp,
|
|
590
|
+
updatedAt: timestamp,
|
|
591
|
+
entries: [...(existing?.entries ?? []), entry],
|
|
592
|
+
events: existing?.events ?? [],
|
|
593
|
+
});
|
|
470
594
|
}
|
|
471
595
|
if (store.appendEvent)
|
|
472
|
-
await store.appendEvent(sessionId, {
|
|
596
|
+
await store.appendEvent(sessionId, {
|
|
597
|
+
id: randomUUID(),
|
|
598
|
+
type: "artifact_created",
|
|
599
|
+
timestamp,
|
|
600
|
+
sessionId,
|
|
601
|
+
data: { artifact: ref },
|
|
602
|
+
});
|
|
473
603
|
return ref;
|
|
474
604
|
},
|
|
475
|
-
compact: async () => ({
|
|
605
|
+
compact: async () => ({
|
|
606
|
+
entryId: "",
|
|
607
|
+
summary: "Compaction is executed by Temporal worker activities.",
|
|
608
|
+
compactedEntries: 0,
|
|
609
|
+
}),
|
|
476
610
|
};
|
|
477
611
|
}
|
|
478
612
|
async function temporalWorkerCommand(args) {
|
|
@@ -481,24 +615,24 @@ async function temporalWorkerCommand(args) {
|
|
|
481
615
|
let addressFlag;
|
|
482
616
|
for (let index = 0; index < args.length; index += 1) {
|
|
483
617
|
const arg = args[index];
|
|
484
|
-
if (arg ===
|
|
618
|
+
if (arg === "--env") {
|
|
485
619
|
const value = args[++index];
|
|
486
620
|
if (!value)
|
|
487
|
-
throw new Error(
|
|
621
|
+
throw new Error("Missing value for --env.");
|
|
488
622
|
envFiles.push(value);
|
|
489
623
|
continue;
|
|
490
624
|
}
|
|
491
|
-
if (arg ===
|
|
625
|
+
if (arg === "--task-queue") {
|
|
492
626
|
const value = args[++index];
|
|
493
627
|
if (!value)
|
|
494
|
-
throw new Error(
|
|
628
|
+
throw new Error("Missing value for --task-queue.");
|
|
495
629
|
taskQueueFlag = value;
|
|
496
630
|
continue;
|
|
497
631
|
}
|
|
498
|
-
if (arg ===
|
|
632
|
+
if (arg === "--address") {
|
|
499
633
|
const value = args[++index];
|
|
500
634
|
if (!value)
|
|
501
|
-
throw new Error(
|
|
635
|
+
throw new Error("Missing value for --address.");
|
|
502
636
|
addressFlag = value;
|
|
503
637
|
continue;
|
|
504
638
|
}
|
|
@@ -507,8 +641,14 @@ async function temporalWorkerCommand(args) {
|
|
|
507
641
|
await loadEnvFiles(envFiles, { explicit: true });
|
|
508
642
|
const workspaceRoot = await findWorkspaceRoot();
|
|
509
643
|
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
510
|
-
const taskQueue = taskQueueFlag ??
|
|
511
|
-
|
|
644
|
+
const taskQueue = taskQueueFlag ??
|
|
645
|
+
process.env.FABRIC_TEMPORAL_TASK_QUEUE ??
|
|
646
|
+
config.temporal?.taskQueue ??
|
|
647
|
+
"fabric-harness";
|
|
648
|
+
const address = addressFlag ??
|
|
649
|
+
process.env.FABRIC_TEMPORAL_ADDRESS ??
|
|
650
|
+
config.temporal?.address ??
|
|
651
|
+
"localhost:7233";
|
|
512
652
|
const namespace = process.env.FABRIC_TEMPORAL_NAMESPACE ?? config.temporal?.namespace;
|
|
513
653
|
const sandbox = new EmptySandboxEnv();
|
|
514
654
|
const store = new FileSessionStore({ workspaceRoot });
|
|
@@ -517,13 +657,13 @@ async function temporalWorkerCommand(args) {
|
|
|
517
657
|
store,
|
|
518
658
|
sandbox,
|
|
519
659
|
tools: createBuiltinTools(sandbox),
|
|
520
|
-
...(typeof modelOptions.model ===
|
|
660
|
+
...(typeof modelOptions.model === "string" ? { model: modelOptions.model } : {}),
|
|
521
661
|
...(modelOptions.modelProvider ? { modelProvider: modelOptions.modelProvider } : {}),
|
|
522
662
|
...(modelOptions.policy ? { policy: modelOptions.policy } : {}),
|
|
523
663
|
...(modelOptions.resolveSecret ? { resolveSecret: modelOptions.resolveSecret } : {}),
|
|
524
664
|
});
|
|
525
665
|
const worker = await startTemporalWorker({
|
|
526
|
-
mode:
|
|
666
|
+
mode: "temporal",
|
|
527
667
|
address,
|
|
528
668
|
taskQueue,
|
|
529
669
|
...(namespace ? { namespace } : {}),
|
|
@@ -537,10 +677,192 @@ async function temporalWorkerCommand(args) {
|
|
|
537
677
|
await worker.close();
|
|
538
678
|
process.exit(0);
|
|
539
679
|
};
|
|
540
|
-
process.on(
|
|
541
|
-
process.on(
|
|
680
|
+
process.on("SIGINT", () => void shutdown());
|
|
681
|
+
process.on("SIGTERM", () => void shutdown());
|
|
542
682
|
await worker.runPromise;
|
|
543
683
|
}
|
|
684
|
+
async function loadCliVersion() {
|
|
685
|
+
try {
|
|
686
|
+
const pkgUrl = new URL("../../package.json", import.meta.url);
|
|
687
|
+
const pkg = JSON.parse(await fs.readFile(pkgUrl, "utf8"));
|
|
688
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return "0.0.0";
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function initCommand(args) {
|
|
695
|
+
let dir = process.cwd();
|
|
696
|
+
let force = false;
|
|
697
|
+
let model = "openai/gpt-5.5";
|
|
698
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
699
|
+
const arg = args[i];
|
|
700
|
+
if (arg === "--dir") {
|
|
701
|
+
const value = args[++i];
|
|
702
|
+
if (!value)
|
|
703
|
+
throw new Error("Missing value for --dir.");
|
|
704
|
+
dir = path.resolve(value);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (arg === "--force") {
|
|
708
|
+
force = true;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (arg === "--model") {
|
|
712
|
+
const value = args[++i];
|
|
713
|
+
if (!value)
|
|
714
|
+
throw new Error("Missing value for --model.");
|
|
715
|
+
model = value;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (arg === "--help" || arg === "-h") {
|
|
719
|
+
console.log("Usage: fabric-harness init [--dir <path>] [--model <provider/model>] [--force]");
|
|
720
|
+
console.log("");
|
|
721
|
+
console.log("Scaffolds a .fabricharness/ workspace with a sample agent, role, skill, and config.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
725
|
+
}
|
|
726
|
+
const root = dir;
|
|
727
|
+
const harness = path.join(root, ".fabricharness");
|
|
728
|
+
const created = [];
|
|
729
|
+
const skipped = [];
|
|
730
|
+
async function writeIf(target, content) {
|
|
731
|
+
try {
|
|
732
|
+
await fs.access(target);
|
|
733
|
+
if (!force) {
|
|
734
|
+
skipped.push(path.relative(root, target));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
/* missing is fine */
|
|
740
|
+
}
|
|
741
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
742
|
+
await fs.writeFile(target, content);
|
|
743
|
+
created.push(path.relative(root, target));
|
|
744
|
+
}
|
|
745
|
+
await fs.mkdir(path.join(harness, "agents"), { recursive: true });
|
|
746
|
+
await fs.mkdir(path.join(harness, "roles"), { recursive: true });
|
|
747
|
+
await fs.mkdir(path.join(harness, "skills"), { recursive: true });
|
|
748
|
+
await writeIf(path.join(harness, "agents", "hello.ts"), [
|
|
749
|
+
`import { agent, schema } from '@fabric-harness/sdk';`,
|
|
750
|
+
"",
|
|
751
|
+
"export default agent({",
|
|
752
|
+
` name: 'hello',`,
|
|
753
|
+
` description: 'Greet a user.',`,
|
|
754
|
+
` input: schema.object({ name: schema.string().default('World') }),`,
|
|
755
|
+
" output: schema.string(),",
|
|
756
|
+
" triggers: { webhook: true },",
|
|
757
|
+
` model: ${JSON.stringify(model)},`,
|
|
758
|
+
" async run({ init, input }) {",
|
|
759
|
+
" const fabric = await init();",
|
|
760
|
+
" const session = await fabric.session();",
|
|
761
|
+
" return await session.prompt(`Say hello to ${input.name}.`);",
|
|
762
|
+
" },",
|
|
763
|
+
"});",
|
|
764
|
+
"",
|
|
765
|
+
].join("\n"));
|
|
766
|
+
await writeIf(path.join(harness, "roles", "engineer.md"), [
|
|
767
|
+
"---",
|
|
768
|
+
"description: Senior backend engineer focused on safe, minimal changes.",
|
|
769
|
+
"---",
|
|
770
|
+
"",
|
|
771
|
+
"You are a senior backend engineer.",
|
|
772
|
+
"Prefer small, well-tested changes. Do not make broad refactors unless required.",
|
|
773
|
+
"",
|
|
774
|
+
].join("\n"));
|
|
775
|
+
await writeIf(path.join(harness, "skills", "summarize", "SKILL.md"), [
|
|
776
|
+
"---",
|
|
777
|
+
"name: summarize",
|
|
778
|
+
"description: Summarize text concisely.",
|
|
779
|
+
"---",
|
|
780
|
+
"",
|
|
781
|
+
"Summarize the provided text in 2-3 sentences. Capture the main point and any",
|
|
782
|
+
"critical caveats. Avoid hedging.",
|
|
783
|
+
"",
|
|
784
|
+
].join("\n"));
|
|
785
|
+
await writeIf(path.join(harness, "config.ts"), [
|
|
786
|
+
"export default {",
|
|
787
|
+
" agent: {",
|
|
788
|
+
` model: ${JSON.stringify(model)},`,
|
|
789
|
+
" },",
|
|
790
|
+
" run: {",
|
|
791
|
+
" target: 'node',",
|
|
792
|
+
` model: ${JSON.stringify(model)},`,
|
|
793
|
+
" },",
|
|
794
|
+
" temporal: {",
|
|
795
|
+
" address: 'localhost:7233',",
|
|
796
|
+
" namespace: 'default',",
|
|
797
|
+
" taskQueue: 'fabric-harness',",
|
|
798
|
+
" },",
|
|
799
|
+
"};",
|
|
800
|
+
"",
|
|
801
|
+
].join("\n"));
|
|
802
|
+
// Only scaffold package.json/tsconfig.json when the directory isn't
|
|
803
|
+
// already a Node package — many users will run `fh init` inside an
|
|
804
|
+
// existing repo where these files already live.
|
|
805
|
+
const pkgDir = path
|
|
806
|
+
.basename(root)
|
|
807
|
+
.replace(/[^a-z0-9-]/gi, "-")
|
|
808
|
+
.toLowerCase() || "fabric-harness-app";
|
|
809
|
+
const cliVersion = await loadCliVersion();
|
|
810
|
+
const fabricRange = `^${cliVersion}`;
|
|
811
|
+
await writeIf(path.join(root, "package.json"), `${JSON.stringify({
|
|
812
|
+
name: pkgDir,
|
|
813
|
+
version: "0.0.0",
|
|
814
|
+
private: true,
|
|
815
|
+
type: "module",
|
|
816
|
+
scripts: {
|
|
817
|
+
build: "tsc -p tsconfig.json",
|
|
818
|
+
agents: "fabric-harness agents",
|
|
819
|
+
run: "fabric-harness run hello",
|
|
820
|
+
dev: "fabric-harness dev",
|
|
821
|
+
},
|
|
822
|
+
dependencies: {
|
|
823
|
+
"@fabric-harness/cli": fabricRange,
|
|
824
|
+
"@fabric-harness/sdk": fabricRange,
|
|
825
|
+
},
|
|
826
|
+
}, null, 2)}\n`);
|
|
827
|
+
await writeIf(path.join(root, "tsconfig.json"), `${JSON.stringify({
|
|
828
|
+
compilerOptions: {
|
|
829
|
+
target: "ES2022",
|
|
830
|
+
module: "ES2022",
|
|
831
|
+
moduleResolution: "bundler",
|
|
832
|
+
strict: true,
|
|
833
|
+
esModuleInterop: true,
|
|
834
|
+
skipLibCheck: true,
|
|
835
|
+
rootDir: ".",
|
|
836
|
+
outDir: "dist",
|
|
837
|
+
},
|
|
838
|
+
include: [".fabricharness/**/*.ts"],
|
|
839
|
+
}, null, 2)}\n`);
|
|
840
|
+
await writeIf(path.join(root, "AGENTS.md"), [
|
|
841
|
+
"# Agents",
|
|
842
|
+
"",
|
|
843
|
+
"This project uses [Fabric Harness](https://github.com/Fabric-Pro/fabric-harness).",
|
|
844
|
+
"",
|
|
845
|
+
"## Quickstart",
|
|
846
|
+
"",
|
|
847
|
+
"```sh",
|
|
848
|
+
"npm install",
|
|
849
|
+
"npx fabric-harness agents",
|
|
850
|
+
'npx fabric-harness run hello --payload \'{"name":"Ada"}\'',
|
|
851
|
+
"```",
|
|
852
|
+
"",
|
|
853
|
+
].join("\n"));
|
|
854
|
+
console.log(`Initialized Fabric Harness workspace at ${harness}`);
|
|
855
|
+
if (created.length)
|
|
856
|
+
console.log(`Created:\n${created.map((p) => ` ${p}`).join("\n")}`);
|
|
857
|
+
if (skipped.length)
|
|
858
|
+
console.log(`Skipped (already exist; pass --force to overwrite):\n${skipped.map((p) => ` ${p}`).join("\n")}`);
|
|
859
|
+
console.log("");
|
|
860
|
+
console.log("Next steps:");
|
|
861
|
+
console.log(" npm install # install fabric-harness");
|
|
862
|
+
console.log(" npx fabric-harness agents # list agents");
|
|
863
|
+
console.log(" npx fabric-harness run hello # run the sample agent");
|
|
864
|
+
console.log(" npx fabric-harness build --target node # build for production");
|
|
865
|
+
}
|
|
544
866
|
async function buildCommand(args) {
|
|
545
867
|
let outDir;
|
|
546
868
|
let target;
|
|
@@ -558,75 +880,96 @@ async function buildCommand(args) {
|
|
|
558
880
|
let signingKey;
|
|
559
881
|
for (let index = 0; index < args.length; index += 1) {
|
|
560
882
|
const arg = args[index];
|
|
561
|
-
if (arg ===
|
|
883
|
+
if (arg === "--out") {
|
|
562
884
|
outDir = args[++index];
|
|
563
885
|
continue;
|
|
564
886
|
}
|
|
565
|
-
if (arg ===
|
|
887
|
+
if (arg === "--target") {
|
|
566
888
|
const value = args[++index];
|
|
567
|
-
if (value !==
|
|
568
|
-
|
|
889
|
+
if (value !== "node" &&
|
|
890
|
+
value !== "temporal-worker" &&
|
|
891
|
+
value !== "docker" &&
|
|
892
|
+
value !== "foundry-hosted-agent" &&
|
|
893
|
+
value !== "cloudflare" &&
|
|
894
|
+
value !== "aks" &&
|
|
895
|
+
value !== "aca")
|
|
896
|
+
throw new Error("--target must be node, temporal-worker, docker, foundry-hosted-agent, cloudflare, aks, or aca.");
|
|
569
897
|
target = value;
|
|
570
898
|
continue;
|
|
571
899
|
}
|
|
572
|
-
if (arg ===
|
|
900
|
+
if (arg === "--no-clean") {
|
|
573
901
|
clean = false;
|
|
574
902
|
continue;
|
|
575
903
|
}
|
|
576
|
-
if (arg ===
|
|
904
|
+
if (arg === "--sbom") {
|
|
577
905
|
sbom = true;
|
|
578
906
|
continue;
|
|
579
907
|
}
|
|
580
|
-
if (arg ===
|
|
908
|
+
if (arg === "--sbom-required") {
|
|
581
909
|
sbom = true;
|
|
582
910
|
sbomRequired = true;
|
|
583
911
|
continue;
|
|
584
912
|
}
|
|
585
|
-
if (arg ===
|
|
913
|
+
if (arg === "--provenance") {
|
|
586
914
|
provenance = true;
|
|
587
915
|
continue;
|
|
588
916
|
}
|
|
589
|
-
if (arg ===
|
|
917
|
+
if (arg === "--attestation") {
|
|
590
918
|
attestation = true;
|
|
591
919
|
continue;
|
|
592
920
|
}
|
|
593
|
-
if (arg ===
|
|
921
|
+
if (arg === "--sign-provenance") {
|
|
594
922
|
signProvenance = true;
|
|
595
923
|
provenance = true;
|
|
596
924
|
continue;
|
|
597
925
|
}
|
|
598
|
-
if (arg ===
|
|
926
|
+
if (arg === "--signing-key") {
|
|
599
927
|
signingKey = args[++index];
|
|
600
928
|
if (!signingKey)
|
|
601
|
-
throw new Error(
|
|
929
|
+
throw new Error("Missing value for --signing-key.");
|
|
602
930
|
continue;
|
|
603
931
|
}
|
|
604
|
-
if (arg ===
|
|
932
|
+
if (arg === "--docker-build") {
|
|
605
933
|
dockerBuild = true;
|
|
606
934
|
continue;
|
|
607
935
|
}
|
|
608
|
-
if (arg ===
|
|
936
|
+
if (arg === "--docker-push") {
|
|
609
937
|
dockerPush = true;
|
|
610
938
|
continue;
|
|
611
939
|
}
|
|
612
|
-
if (arg ===
|
|
940
|
+
if (arg === "--docker-tag") {
|
|
613
941
|
dockerTag = args[++index];
|
|
614
942
|
if (!dockerTag)
|
|
615
|
-
throw new Error(
|
|
943
|
+
throw new Error("Missing value for --docker-tag.");
|
|
616
944
|
continue;
|
|
617
945
|
}
|
|
618
|
-
if (arg ===
|
|
946
|
+
if (arg === "--image-sbom") {
|
|
619
947
|
imageSbom = true;
|
|
620
948
|
continue;
|
|
621
949
|
}
|
|
622
|
-
if (arg ===
|
|
950
|
+
if (arg === "--image-sbom-required") {
|
|
623
951
|
imageSbom = true;
|
|
624
952
|
imageSbomRequired = true;
|
|
625
953
|
continue;
|
|
626
954
|
}
|
|
627
955
|
throw new Error(`Unknown argument: ${arg}`);
|
|
628
956
|
}
|
|
629
|
-
const result = await buildWorkspace({
|
|
957
|
+
const result = await buildWorkspace({
|
|
958
|
+
...(outDir ? { outDir } : {}),
|
|
959
|
+
...(target ? { target } : {}),
|
|
960
|
+
clean,
|
|
961
|
+
sbom,
|
|
962
|
+
sbomRequired,
|
|
963
|
+
provenance,
|
|
964
|
+
attestation,
|
|
965
|
+
signProvenance,
|
|
966
|
+
...(signingKey ? { signingKey } : {}),
|
|
967
|
+
dockerBuild,
|
|
968
|
+
dockerPush,
|
|
969
|
+
...(dockerTag ? { dockerTag } : {}),
|
|
970
|
+
imageSbom,
|
|
971
|
+
imageSbomRequired,
|
|
972
|
+
});
|
|
630
973
|
console.log(`Built Fabric Harness workspace: ${result.outDir}`);
|
|
631
974
|
console.log(`Manifest: ${result.manifestPath}`);
|
|
632
975
|
if (result.provenancePath)
|
|
@@ -643,12 +986,12 @@ async function buildCommand(args) {
|
|
|
643
986
|
console.log(`Image SBOM: ${result.imageSbomPath}`);
|
|
644
987
|
for (const warning of result.warnings)
|
|
645
988
|
console.warn(`Warning: ${warning}`);
|
|
646
|
-
console.log(`Agents: ${result.manifest.agents.map((agent) => agent.name).join(
|
|
989
|
+
console.log(`Agents: ${result.manifest.agents.map((agent) => agent.name).join(", ") || "none"}`);
|
|
647
990
|
}
|
|
648
991
|
async function devCommand(args) {
|
|
649
992
|
let host;
|
|
650
993
|
let port;
|
|
651
|
-
let target =
|
|
994
|
+
let target = "node";
|
|
652
995
|
let authTokenEnv;
|
|
653
996
|
let maxBodyBytes;
|
|
654
997
|
let rateLimitWindowMs;
|
|
@@ -656,67 +999,67 @@ async function devCommand(args) {
|
|
|
656
999
|
const envFiles = [];
|
|
657
1000
|
for (let index = 0; index < args.length; index += 1) {
|
|
658
1001
|
const arg = args[index];
|
|
659
|
-
if (arg ===
|
|
1002
|
+
if (arg === "--host") {
|
|
660
1003
|
host = args[++index];
|
|
661
1004
|
continue;
|
|
662
1005
|
}
|
|
663
|
-
if (arg ===
|
|
1006
|
+
if (arg === "--port") {
|
|
664
1007
|
const value = args[++index];
|
|
665
1008
|
port = value ? Number(value) : undefined;
|
|
666
1009
|
continue;
|
|
667
1010
|
}
|
|
668
|
-
if (arg ===
|
|
1011
|
+
if (arg === "--target") {
|
|
669
1012
|
const value = args[++index];
|
|
670
|
-
if (value !==
|
|
671
|
-
throw new Error(
|
|
1013
|
+
if (value !== "node" && value !== "cloudflare")
|
|
1014
|
+
throw new Error("--target for dev must be node or cloudflare.");
|
|
672
1015
|
target = value;
|
|
673
1016
|
continue;
|
|
674
1017
|
}
|
|
675
|
-
if (arg ===
|
|
1018
|
+
if (arg === "--env") {
|
|
676
1019
|
const value = args[++index];
|
|
677
1020
|
if (!value)
|
|
678
|
-
throw new Error(
|
|
1021
|
+
throw new Error("Missing value for --env.");
|
|
679
1022
|
envFiles.push(value);
|
|
680
1023
|
continue;
|
|
681
1024
|
}
|
|
682
|
-
if (arg ===
|
|
1025
|
+
if (arg === "--auth-token-env") {
|
|
683
1026
|
const value = args[++index];
|
|
684
1027
|
if (!value)
|
|
685
|
-
throw new Error(
|
|
1028
|
+
throw new Error("Missing value for --auth-token-env.");
|
|
686
1029
|
authTokenEnv = value;
|
|
687
1030
|
continue;
|
|
688
1031
|
}
|
|
689
|
-
if (arg ===
|
|
1032
|
+
if (arg === "--max-body-bytes") {
|
|
690
1033
|
const value = args[++index];
|
|
691
1034
|
if (!value)
|
|
692
|
-
throw new Error(
|
|
1035
|
+
throw new Error("Missing value for --max-body-bytes.");
|
|
693
1036
|
maxBodyBytes = Number(value);
|
|
694
1037
|
if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0)
|
|
695
|
-
throw new Error(
|
|
1038
|
+
throw new Error("--max-body-bytes must be a positive number.");
|
|
696
1039
|
continue;
|
|
697
1040
|
}
|
|
698
|
-
if (arg ===
|
|
1041
|
+
if (arg === "--rate-limit-window-ms") {
|
|
699
1042
|
const value = args[++index];
|
|
700
1043
|
if (!value)
|
|
701
|
-
throw new Error(
|
|
1044
|
+
throw new Error("Missing value for --rate-limit-window-ms.");
|
|
702
1045
|
rateLimitWindowMs = Number(value);
|
|
703
1046
|
if (!Number.isFinite(rateLimitWindowMs) || rateLimitWindowMs <= 0)
|
|
704
|
-
throw new Error(
|
|
1047
|
+
throw new Error("--rate-limit-window-ms must be a positive number.");
|
|
705
1048
|
continue;
|
|
706
1049
|
}
|
|
707
|
-
if (arg ===
|
|
1050
|
+
if (arg === "--rate-limit-max") {
|
|
708
1051
|
const value = args[++index];
|
|
709
1052
|
if (!value)
|
|
710
|
-
throw new Error(
|
|
1053
|
+
throw new Error("Missing value for --rate-limit-max.");
|
|
711
1054
|
rateLimitMax = Number(value);
|
|
712
1055
|
if (!Number.isFinite(rateLimitMax) || rateLimitMax <= 0)
|
|
713
|
-
throw new Error(
|
|
1056
|
+
throw new Error("--rate-limit-max must be a positive number.");
|
|
714
1057
|
continue;
|
|
715
1058
|
}
|
|
716
1059
|
throw new Error(`Unknown argument: ${arg}`);
|
|
717
1060
|
}
|
|
718
1061
|
await loadEnvFiles(envFiles, { explicit: true });
|
|
719
|
-
if (target ===
|
|
1062
|
+
if (target === "cloudflare") {
|
|
720
1063
|
const cloudflareOptions = {};
|
|
721
1064
|
if (host !== undefined)
|
|
722
1065
|
cloudflareOptions.host = host;
|
|
@@ -745,27 +1088,27 @@ async function devCommand(args) {
|
|
|
745
1088
|
await server.close();
|
|
746
1089
|
process.exit(0);
|
|
747
1090
|
};
|
|
748
|
-
process.on(
|
|
749
|
-
process.on(
|
|
1091
|
+
process.on("SIGINT", () => void shutdown());
|
|
1092
|
+
process.on("SIGTERM", () => void shutdown());
|
|
750
1093
|
console.log(`Fabric Harness dev server listening at ${server.url}`);
|
|
751
|
-
console.log(
|
|
752
|
-
console.log(
|
|
1094
|
+
console.log("POST /agents/:agent/:id, GET /sessions/:id, GET /sessions/:id/events");
|
|
1095
|
+
console.log("Watching .fabricharness for changes; agents/config reload on the next request.");
|
|
753
1096
|
}
|
|
754
1097
|
async function cloudflareDevCommand(options) {
|
|
755
1098
|
const workspaceRoot = await findWorkspaceRoot();
|
|
756
|
-
console.error(
|
|
757
|
-
const result = await buildWorkspace({ cwd: workspaceRoot, target:
|
|
1099
|
+
console.error("[fabric-harness] Building Cloudflare Worker artifact...");
|
|
1100
|
+
const result = await buildWorkspace({ cwd: workspaceRoot, target: "cloudflare" });
|
|
758
1101
|
console.error(`[fabric-harness] Cloudflare artifact: ${result.outDir}`);
|
|
759
|
-
console.error(
|
|
760
|
-
const args = [
|
|
1102
|
+
console.error("[fabric-harness] Starting Wrangler dev. Install wrangler/@cloudflare dependencies in the artifact if prompted.");
|
|
1103
|
+
const args = ["wrangler", "dev"];
|
|
761
1104
|
if (options.port !== undefined)
|
|
762
|
-
args.push(
|
|
1105
|
+
args.push("--port", String(options.port));
|
|
763
1106
|
if (options.host !== undefined)
|
|
764
|
-
args.push(
|
|
765
|
-
const npx = process.platform ===
|
|
1107
|
+
args.push("--ip", options.host);
|
|
1108
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
766
1109
|
const child = spawn(npx, args, {
|
|
767
1110
|
cwd: result.outDir,
|
|
768
|
-
stdio:
|
|
1111
|
+
stdio: "inherit",
|
|
769
1112
|
shell: false,
|
|
770
1113
|
env: process.env,
|
|
771
1114
|
});
|
|
@@ -776,13 +1119,13 @@ async function cloudflareDevCommand(options) {
|
|
|
776
1119
|
const structuralWatcher = startCloudflareStructuralWatcher(workspaceRoot);
|
|
777
1120
|
const shutdown = () => {
|
|
778
1121
|
structuralWatcher?.close();
|
|
779
|
-
child.kill(
|
|
1122
|
+
child.kill("SIGTERM");
|
|
780
1123
|
};
|
|
781
|
-
process.on(
|
|
782
|
-
process.on(
|
|
1124
|
+
process.on("SIGINT", shutdown);
|
|
1125
|
+
process.on("SIGTERM", shutdown);
|
|
783
1126
|
const code = await new Promise((resolve, reject) => {
|
|
784
|
-
child.on(
|
|
785
|
-
child.on(
|
|
1127
|
+
child.on("error", reject);
|
|
1128
|
+
child.on("close", resolve);
|
|
786
1129
|
});
|
|
787
1130
|
structuralWatcher?.close();
|
|
788
1131
|
if (code && code !== 0)
|
|
@@ -794,36 +1137,38 @@ async function cloudflareDevCommand(options) {
|
|
|
794
1137
|
* reloads workerd, so we don't need to restart it ourselves.
|
|
795
1138
|
*/
|
|
796
1139
|
function startCloudflareStructuralWatcher(workspaceRoot) {
|
|
797
|
-
const watchRoot = path.join(workspaceRoot,
|
|
1140
|
+
const watchRoot = path.join(workspaceRoot, ".fabricharness", "agents");
|
|
798
1141
|
let watcher;
|
|
799
1142
|
try {
|
|
800
1143
|
let timer;
|
|
801
|
-
let lastAgentSet =
|
|
1144
|
+
let lastAgentSet = "";
|
|
802
1145
|
const recomputeAndRebuild = async () => {
|
|
803
1146
|
const summaries = await listAgentSummaries(workspaceRoot).catch(() => []);
|
|
804
1147
|
const fingerprint = summaries
|
|
805
|
-
.map((a) => `${a.name}|${a.triggers?.webhook ?? false}|${a.triggers?.schedule ??
|
|
1148
|
+
.map((a) => `${a.name}|${a.triggers?.webhook ?? false}|${a.triggers?.schedule ?? ""}`)
|
|
806
1149
|
.sort()
|
|
807
|
-
.join(
|
|
1150
|
+
.join(",");
|
|
808
1151
|
if (fingerprint === lastAgentSet)
|
|
809
1152
|
return;
|
|
810
1153
|
lastAgentSet = fingerprint;
|
|
811
|
-
console.error(
|
|
1154
|
+
console.error("[fabric-harness] Agent set changed — regenerating Cloudflare entry.");
|
|
812
1155
|
try {
|
|
813
|
-
await buildWorkspace({ cwd: workspaceRoot, target:
|
|
814
|
-
console.error(
|
|
1156
|
+
await buildWorkspace({ cwd: workspaceRoot, target: "cloudflare" });
|
|
1157
|
+
console.error("[fabric-harness] Entry regenerated. Wrangler will reload.");
|
|
815
1158
|
}
|
|
816
1159
|
catch (err) {
|
|
817
|
-
console.error(
|
|
1160
|
+
console.error("[fabric-harness] Rebuild failed:", err instanceof Error ? err.message : err);
|
|
818
1161
|
}
|
|
819
1162
|
};
|
|
820
1163
|
// Prime the fingerprint without rebuilding (we just built before starting wrangler).
|
|
821
|
-
void listAgentSummaries(workspaceRoot)
|
|
1164
|
+
void listAgentSummaries(workspaceRoot)
|
|
1165
|
+
.then((summaries) => {
|
|
822
1166
|
lastAgentSet = summaries
|
|
823
|
-
.map((a) => `${a.name}|${a.triggers?.webhook ?? false}|${a.triggers?.schedule ??
|
|
1167
|
+
.map((a) => `${a.name}|${a.triggers?.webhook ?? false}|${a.triggers?.schedule ?? ""}`)
|
|
824
1168
|
.sort()
|
|
825
|
-
.join(
|
|
826
|
-
})
|
|
1169
|
+
.join(",");
|
|
1170
|
+
})
|
|
1171
|
+
.catch(() => undefined);
|
|
827
1172
|
watcher = watch(watchRoot, { recursive: true }, (_eventType, filename) => {
|
|
828
1173
|
if (!filename)
|
|
829
1174
|
return;
|
|
@@ -840,7 +1185,7 @@ function startCloudflareStructuralWatcher(workspaceRoot) {
|
|
|
840
1185
|
}
|
|
841
1186
|
}
|
|
842
1187
|
function startNodeDevWatcher(workspaceRoot, envFiles) {
|
|
843
|
-
const watchRoot = path.join(workspaceRoot,
|
|
1188
|
+
const watchRoot = path.join(workspaceRoot, ".fabricharness");
|
|
844
1189
|
try {
|
|
845
1190
|
const envFileSet = new Set(envFiles.map((file) => path.resolve(process.cwd(), file)));
|
|
846
1191
|
let timer;
|
|
@@ -848,7 +1193,7 @@ function startNodeDevWatcher(workspaceRoot, envFiles) {
|
|
|
848
1193
|
if (timer)
|
|
849
1194
|
clearTimeout(timer);
|
|
850
1195
|
timer = setTimeout(() => {
|
|
851
|
-
const changed = filename ? path.join(
|
|
1196
|
+
const changed = filename ? path.join(".fabricharness", String(filename)) : ".fabricharness";
|
|
852
1197
|
console.error(`[fabric-harness] Change detected: ${changed}. Reload will apply on next request.`);
|
|
853
1198
|
for (const envFile of envFileSet) {
|
|
854
1199
|
if (path.resolve(workspaceRoot, changed) === envFile)
|
|
@@ -862,19 +1207,43 @@ function startNodeDevWatcher(workspaceRoot, envFiles) {
|
|
|
862
1207
|
}
|
|
863
1208
|
}
|
|
864
1209
|
const CONNECTOR_RECIPES = {
|
|
865
|
-
daytona: {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1210
|
+
daytona: {
|
|
1211
|
+
file: "sandbox--daytona.md",
|
|
1212
|
+
category: "sandbox",
|
|
1213
|
+
description: "Daytona remote sandbox",
|
|
1214
|
+
},
|
|
1215
|
+
e2b: { file: "sandbox--e2b.md", category: "sandbox", description: "E2B remote sandbox" },
|
|
1216
|
+
modal: {
|
|
1217
|
+
file: "sandbox--modal.md",
|
|
1218
|
+
category: "sandbox",
|
|
1219
|
+
description: "Modal Labs remote sandbox",
|
|
1220
|
+
},
|
|
1221
|
+
vercel: { file: "sandbox--vercel.md", category: "sandbox", description: "Vercel Sandbox" },
|
|
1222
|
+
"github-mcp": { file: "mcp--github.md", category: "mcp", description: "GitHub MCP server" },
|
|
1223
|
+
"mintlify-mcp": {
|
|
1224
|
+
file: "mcp--mintlify.md",
|
|
1225
|
+
category: "mcp",
|
|
1226
|
+
description: "Mintlify hosted docs MCP",
|
|
1227
|
+
},
|
|
1228
|
+
"linear-mcp": { file: "mcp--linear.md", category: "mcp", description: "Linear hosted MCP" },
|
|
1229
|
+
"slack-mcp": {
|
|
1230
|
+
file: "mcp--slack.md",
|
|
1231
|
+
category: "mcp",
|
|
1232
|
+
description: "Slack MCP (hosted or local bridge)",
|
|
1233
|
+
},
|
|
1234
|
+
fumadocs: {
|
|
1235
|
+
file: "kb--fumadocs.md",
|
|
1236
|
+
category: "kb",
|
|
1237
|
+
description: "Mount a Fumadocs content tree as a knowledge base",
|
|
1238
|
+
},
|
|
1239
|
+
postgres: { file: "data--postgres.md", category: "data", description: "Postgres data connector" },
|
|
1240
|
+
notion: {
|
|
1241
|
+
file: "data--notion.md",
|
|
1242
|
+
category: "data",
|
|
1243
|
+
description: "Notion (hosted MCP or REST adapter)",
|
|
1244
|
+
},
|
|
876
1245
|
};
|
|
877
|
-
const RECIPE_REGISTRY_URL =
|
|
1246
|
+
const RECIPE_REGISTRY_URL = "https://raw.githubusercontent.com/Fabric-Pro/fabric-harness/main/connectors";
|
|
878
1247
|
const RECIPE_FREEFORM_TEMPLATE = `# Fabric Harness connector recipe (freeform)
|
|
879
1248
|
|
|
880
1249
|
Category: {{CATEGORY}}
|
|
@@ -915,18 +1284,18 @@ Return the full TS file content when done.
|
|
|
915
1284
|
function detectCodingAgentForHint() {
|
|
916
1285
|
// Common signals (env vars set by these agents when they shell out)
|
|
917
1286
|
const env = process.env;
|
|
918
|
-
if (env.CLAUDE_CODE ===
|
|
919
|
-
return { name:
|
|
920
|
-
if (env.CODEX_AGENT ===
|
|
921
|
-
return { name:
|
|
922
|
-
if (env.CURSOR_AGENT ===
|
|
923
|
-
return { name:
|
|
1287
|
+
if (env.CLAUDE_CODE === "1" || env.CLAUDECODE === "1" || env.CLAUDE_PROJECT_DIR)
|
|
1288
|
+
return { name: "claude", pipeHint: "claude" };
|
|
1289
|
+
if (env.CODEX_AGENT === "1" || env.CODEX_HOME)
|
|
1290
|
+
return { name: "codex", pipeHint: "codex" };
|
|
1291
|
+
if (env.CURSOR_AGENT === "1" || env.CURSOR_TRACE_ID)
|
|
1292
|
+
return { name: "cursor-agent", pipeHint: "cursor-agent" };
|
|
924
1293
|
if (env.AIDER_RUNNING)
|
|
925
|
-
return { name:
|
|
926
|
-
if (env.OPENCODE ===
|
|
927
|
-
return { name:
|
|
1294
|
+
return { name: "aider", pipeHint: "aider" };
|
|
1295
|
+
if (env.OPENCODE === "1")
|
|
1296
|
+
return { name: "opencode", pipeHint: "opencode" };
|
|
928
1297
|
// Default hint: most users have one of these installed; pick claude as the canonical example.
|
|
929
|
-
return { name:
|
|
1298
|
+
return { name: "your coding agent", pipeHint: "claude" };
|
|
930
1299
|
}
|
|
931
1300
|
async function fetchRemoteRecipe(file) {
|
|
932
1301
|
try {
|
|
@@ -944,7 +1313,7 @@ async function loadRecipeBody(file) {
|
|
|
944
1313
|
// Prefer local copy (developing locally / repo cloned) — falls back to remote.
|
|
945
1314
|
try {
|
|
946
1315
|
const localPath = await findConnectorRecipe(file);
|
|
947
|
-
return await fs.readFile(localPath,
|
|
1316
|
+
return await fs.readFile(localPath, "utf8");
|
|
948
1317
|
}
|
|
949
1318
|
catch {
|
|
950
1319
|
const remote = await fetchRemoteRecipe(file);
|
|
@@ -959,16 +1328,18 @@ async function addCommand(args) {
|
|
|
959
1328
|
let print = false;
|
|
960
1329
|
for (let i = 0; i < args.length; i += 1) {
|
|
961
1330
|
const arg = args[i];
|
|
962
|
-
if (arg ===
|
|
1331
|
+
if (arg === undefined)
|
|
1332
|
+
continue;
|
|
1333
|
+
if (arg === "--print") {
|
|
963
1334
|
print = true;
|
|
964
1335
|
continue;
|
|
965
1336
|
}
|
|
966
|
-
if (arg ===
|
|
1337
|
+
if (arg === "--category") {
|
|
967
1338
|
category = args[i + 1];
|
|
968
1339
|
i += 1;
|
|
969
1340
|
continue;
|
|
970
1341
|
}
|
|
971
|
-
if (!connector && !arg.startsWith(
|
|
1342
|
+
if (!connector && !arg.startsWith("-")) {
|
|
972
1343
|
connector = arg;
|
|
973
1344
|
continue;
|
|
974
1345
|
}
|
|
@@ -976,7 +1347,7 @@ async function addCommand(args) {
|
|
|
976
1347
|
}
|
|
977
1348
|
// No connector → list available recipes.
|
|
978
1349
|
if (!connector) {
|
|
979
|
-
console.log(
|
|
1350
|
+
console.log("Available connector recipes:\n");
|
|
980
1351
|
const byCategory = new Map();
|
|
981
1352
|
for (const [name, meta] of Object.entries(CONNECTOR_RECIPES)) {
|
|
982
1353
|
const list = byCategory.get(meta.category) ?? [];
|
|
@@ -994,18 +1365,18 @@ async function addCommand(args) {
|
|
|
994
1365
|
console.log(`Pipe a recipe to ${agent.name}:`);
|
|
995
1366
|
console.log(` fabric-harness add <name> | ${agent.pipeHint}`);
|
|
996
1367
|
console.log();
|
|
997
|
-
console.log(
|
|
1368
|
+
console.log("Or build a freeform recipe from a provider URL:");
|
|
998
1369
|
console.log(` fabric-harness add https://e2b.dev --category sandbox | ${agent.pipeHint}`);
|
|
999
1370
|
return;
|
|
1000
1371
|
}
|
|
1001
1372
|
// URL-form: build a freeform template instructing the agent to scaffold from docs.
|
|
1002
1373
|
if (/^https?:\/\//.test(connector)) {
|
|
1003
1374
|
if (!category)
|
|
1004
|
-
throw new Error(
|
|
1005
|
-
if (![
|
|
1375
|
+
throw new Error("--category is required when passing a URL (sandbox, mcp, kb, or data).");
|
|
1376
|
+
if (!["sandbox", "mcp", "kb", "data"].includes(category)) {
|
|
1006
1377
|
throw new Error(`Unknown category "${category}". Must be one of: sandbox, mcp, kb, data.`);
|
|
1007
1378
|
}
|
|
1008
|
-
const body = RECIPE_FREEFORM_TEMPLATE.replaceAll(
|
|
1379
|
+
const body = RECIPE_FREEFORM_TEMPLATE.replaceAll("{{URL}}", connector).replaceAll("{{CATEGORY}}", category);
|
|
1009
1380
|
if (!process.stdout.isTTY || print) {
|
|
1010
1381
|
process.stdout.write(body);
|
|
1011
1382
|
return;
|
|
@@ -1031,13 +1402,13 @@ async function addCommand(args) {
|
|
|
1031
1402
|
console.log(`Pipe to ${agent.name}:`);
|
|
1032
1403
|
console.log(` fabric-harness add ${connector} | ${agent.pipeHint}`);
|
|
1033
1404
|
console.log();
|
|
1034
|
-
console.log(
|
|
1405
|
+
console.log("Or print the markdown directly:");
|
|
1035
1406
|
console.log(` fabric-harness add ${connector} --print`);
|
|
1036
1407
|
}
|
|
1037
1408
|
async function findConnectorRecipe(file) {
|
|
1038
1409
|
let current = process.cwd();
|
|
1039
1410
|
while (true) {
|
|
1040
|
-
const candidate = path.join(current,
|
|
1411
|
+
const candidate = path.join(current, "connectors", file);
|
|
1041
1412
|
try {
|
|
1042
1413
|
await fs.access(candidate);
|
|
1043
1414
|
return candidate;
|
|
@@ -1050,7 +1421,7 @@ async function findConnectorRecipe(file) {
|
|
|
1050
1421
|
break;
|
|
1051
1422
|
current = parent;
|
|
1052
1423
|
}
|
|
1053
|
-
const fromCli = path.resolve(path.dirname(new URL(import.meta.url).pathname),
|
|
1424
|
+
const fromCli = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..", "..", "connectors", file);
|
|
1054
1425
|
try {
|
|
1055
1426
|
await fs.access(fromCli);
|
|
1056
1427
|
return fromCli;
|
|
@@ -1061,17 +1432,17 @@ async function findConnectorRecipe(file) {
|
|
|
1061
1432
|
}
|
|
1062
1433
|
async function verifyCommand(args, kind) {
|
|
1063
1434
|
const [target, ...rest] = args;
|
|
1064
|
-
if (!target || target.startsWith(
|
|
1435
|
+
if (!target || target.startsWith("-"))
|
|
1065
1436
|
throw new Error(`Missing required <build-dir-or-${kind}> argument.`);
|
|
1066
1437
|
if (rest.length > 0)
|
|
1067
1438
|
throw new Error(`Unknown argument: ${rest[0]}`);
|
|
1068
|
-
const result = kind ===
|
|
1439
|
+
const result = kind === "attestation" ? await verifyAttestation(target) : await verifyProvenance(target);
|
|
1069
1440
|
printResult(result);
|
|
1070
1441
|
}
|
|
1071
1442
|
async function inspectCommand(args) {
|
|
1072
1443
|
const [sessionId, ...rest] = args;
|
|
1073
|
-
if (!sessionId || sessionId.startsWith(
|
|
1074
|
-
throw new Error(
|
|
1444
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
1445
|
+
throw new Error("Missing required <session-id> argument for inspect.");
|
|
1075
1446
|
if (rest.length > 0)
|
|
1076
1447
|
throw new Error(`Unknown argument: ${rest[0]}`);
|
|
1077
1448
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
@@ -1081,34 +1452,76 @@ async function inspectCommand(args) {
|
|
|
1081
1452
|
printResult(data);
|
|
1082
1453
|
}
|
|
1083
1454
|
async function replayCommand(args) {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1455
|
+
let sessionId;
|
|
1456
|
+
let fromStep;
|
|
1457
|
+
let limit;
|
|
1458
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1459
|
+
const arg = args[i];
|
|
1460
|
+
if (arg === "--from") {
|
|
1461
|
+
const value = args[++i];
|
|
1462
|
+
if (!value)
|
|
1463
|
+
throw new Error("Missing value for --from.");
|
|
1464
|
+
fromStep = value;
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (arg === "--limit") {
|
|
1468
|
+
const value = args[++i];
|
|
1469
|
+
const n = Number(value);
|
|
1470
|
+
if (!value || !Number.isFinite(n) || n <= 0)
|
|
1471
|
+
throw new Error("--limit must be a positive integer.");
|
|
1472
|
+
limit = Math.floor(n);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (arg === "--help" || arg === "-h") {
|
|
1476
|
+
console.log("Usage: fabric-harness replay <session-id> [--from <step-id>] [--limit <n>]");
|
|
1477
|
+
console.log("");
|
|
1478
|
+
console.log("Shows the read-only active path and model context. With --from, starts at the matching step (entry id).");
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (!arg)
|
|
1482
|
+
continue;
|
|
1483
|
+
if (arg.startsWith("-"))
|
|
1484
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1485
|
+
if (sessionId)
|
|
1486
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1487
|
+
sessionId = arg;
|
|
1488
|
+
}
|
|
1489
|
+
if (!sessionId)
|
|
1490
|
+
throw new Error("Missing required <session-id> argument for replay.");
|
|
1089
1491
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1090
1492
|
const replay = await inspectReplayFromStore(store, sessionId);
|
|
1091
1493
|
if (!replay)
|
|
1092
1494
|
throw new Error(`Session not found: ${sessionId}`);
|
|
1495
|
+
let activePath = replay.activePath;
|
|
1496
|
+
if (fromStep) {
|
|
1497
|
+
const idx = activePath.findIndex((entry) => entry.id === fromStep);
|
|
1498
|
+
if (idx === -1)
|
|
1499
|
+
throw new Error(`Step not found in active path: ${fromStep}`);
|
|
1500
|
+
activePath = activePath.slice(idx);
|
|
1501
|
+
}
|
|
1502
|
+
if (limit !== undefined)
|
|
1503
|
+
activePath = activePath.slice(0, limit);
|
|
1093
1504
|
console.log(`Replay inspection for session ${replay.sessionId}`);
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1505
|
+
if (fromStep)
|
|
1506
|
+
console.log(`Filtered from step: ${fromStep}`);
|
|
1507
|
+
console.log("");
|
|
1508
|
+
console.log(`Active path entries: ${activePath.length}${fromStep || limit !== undefined ? ` (of ${replay.activePath.length} total)` : ""}`);
|
|
1509
|
+
for (const entry of activePath) {
|
|
1510
|
+
console.log(`${entry.timestamp} ${entry.type} ${entry.id}${entry.parentId ? ` <- ${entry.parentId}` : ""}`);
|
|
1511
|
+
}
|
|
1512
|
+
console.log("");
|
|
1100
1513
|
if (replay.latestCompaction) {
|
|
1101
1514
|
console.log(`Latest compaction: ${replay.latestCompaction.id}`);
|
|
1102
1515
|
console.log(formatInline(replay.latestCompaction.data ?? {}));
|
|
1103
1516
|
}
|
|
1104
1517
|
else {
|
|
1105
|
-
console.log(
|
|
1518
|
+
console.log("Latest compaction: none");
|
|
1106
1519
|
}
|
|
1107
|
-
console.log(
|
|
1520
|
+
console.log("");
|
|
1108
1521
|
console.log(`Model context messages: ${replay.modelMessages.length}`);
|
|
1109
1522
|
for (const message of replay.modelMessages) {
|
|
1110
|
-
const name = message.name ? ` name=${message.name}` :
|
|
1111
|
-
console.log(`${message.role}${name}: ${message.content.replace(/\s+/g,
|
|
1523
|
+
const name = message.name ? ` name=${message.name}` : "";
|
|
1524
|
+
console.log(`${message.role}${name}: ${message.content.replace(/\s+/g, " ").slice(0, 240)}`);
|
|
1112
1525
|
}
|
|
1113
1526
|
}
|
|
1114
1527
|
async function doctorCommand(args) {
|
|
@@ -1119,28 +1532,28 @@ async function doctorCommand(args) {
|
|
|
1119
1532
|
let jsonOutput = false;
|
|
1120
1533
|
for (let index = 0; index < args.length; index += 1) {
|
|
1121
1534
|
const arg = args[index];
|
|
1122
|
-
if (arg ===
|
|
1535
|
+
if (arg === "--target") {
|
|
1123
1536
|
const value = args[++index];
|
|
1124
|
-
if (value !==
|
|
1125
|
-
throw new Error(
|
|
1537
|
+
if (value !== "node" && value !== "temporal-worker")
|
|
1538
|
+
throw new Error("--target for doctor must be node or temporal-worker.");
|
|
1126
1539
|
target = value;
|
|
1127
1540
|
continue;
|
|
1128
1541
|
}
|
|
1129
|
-
if (arg ===
|
|
1542
|
+
if (arg === "--model") {
|
|
1130
1543
|
model = args[++index];
|
|
1131
1544
|
if (!model)
|
|
1132
|
-
throw new Error(
|
|
1545
|
+
throw new Error("Missing value for --model.");
|
|
1133
1546
|
continue;
|
|
1134
1547
|
}
|
|
1135
|
-
if (arg ===
|
|
1548
|
+
if (arg === "--tools") {
|
|
1136
1549
|
checkTools = true;
|
|
1137
1550
|
continue;
|
|
1138
1551
|
}
|
|
1139
|
-
if (arg ===
|
|
1552
|
+
if (arg === "--live") {
|
|
1140
1553
|
live = true;
|
|
1141
1554
|
continue;
|
|
1142
1555
|
}
|
|
1143
|
-
if (arg ===
|
|
1556
|
+
if (arg === "--json") {
|
|
1144
1557
|
jsonOutput = true;
|
|
1145
1558
|
continue;
|
|
1146
1559
|
}
|
|
@@ -1148,33 +1561,72 @@ async function doctorCommand(args) {
|
|
|
1148
1561
|
}
|
|
1149
1562
|
const workspaceRoot = await findWorkspaceRoot();
|
|
1150
1563
|
const checks = [];
|
|
1151
|
-
checks.push({ name:
|
|
1564
|
+
checks.push({ name: "workspace", ok: true, message: workspaceRoot });
|
|
1152
1565
|
let config = {};
|
|
1153
1566
|
try {
|
|
1154
1567
|
config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
1155
|
-
const configuredTarget = target ?? config.run?.target ??
|
|
1156
|
-
checks.push({ name:
|
|
1568
|
+
const configuredTarget = target ?? config.run?.target ?? "node";
|
|
1569
|
+
checks.push({ name: "run-target", ok: true, message: configuredTarget });
|
|
1157
1570
|
const doctorModel = model ?? process.env.FABRIC_MODEL ?? config.run?.model ?? config.agent?.model;
|
|
1158
|
-
const resolved = resolveModelProvider({
|
|
1159
|
-
|
|
1571
|
+
const resolved = resolveModelProvider({
|
|
1572
|
+
...(doctorModel !== undefined ? { model: doctorModel } : {}),
|
|
1573
|
+
...(config.agent?.providers !== undefined ? { providers: config.agent.providers } : {}),
|
|
1574
|
+
});
|
|
1575
|
+
checks.push({
|
|
1576
|
+
name: "model",
|
|
1577
|
+
ok: Boolean(resolved.provider),
|
|
1578
|
+
message: resolved.provider
|
|
1579
|
+
? `${resolved.providerName}/${resolved.modelId}`
|
|
1580
|
+
: "model disabled",
|
|
1581
|
+
});
|
|
1160
1582
|
for (const warning of resolved.warnings)
|
|
1161
|
-
checks.push({ name:
|
|
1583
|
+
checks.push({ name: "model-warning", ok: true, message: warning });
|
|
1162
1584
|
if (live && resolved.provider) {
|
|
1163
|
-
const response = await resolved.provider.generate({
|
|
1164
|
-
|
|
1585
|
+
const response = await resolved.provider.generate({
|
|
1586
|
+
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
1587
|
+
messages: [{ role: "user", content: "Say exactly: fabric harness doctor ok" }],
|
|
1588
|
+
});
|
|
1589
|
+
checks.push({
|
|
1590
|
+
name: "model-live",
|
|
1591
|
+
ok: Boolean(response.message?.content || response.toolCalls?.length),
|
|
1592
|
+
message: response.message?.content?.slice(0, 120) ??
|
|
1593
|
+
`${response.toolCalls?.length ?? 0} tool calls`,
|
|
1594
|
+
});
|
|
1165
1595
|
}
|
|
1166
1596
|
}
|
|
1167
1597
|
catch (error) {
|
|
1168
|
-
checks.push({
|
|
1598
|
+
checks.push({
|
|
1599
|
+
name: "model",
|
|
1600
|
+
ok: false,
|
|
1601
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1602
|
+
});
|
|
1169
1603
|
}
|
|
1170
1604
|
try {
|
|
1171
|
-
const store = await createConfiguredSessionStore({
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1605
|
+
const store = await createConfiguredSessionStore({
|
|
1606
|
+
workspaceRoot,
|
|
1607
|
+
...(config.store ? { config: config.store } : {}),
|
|
1608
|
+
env: process.env,
|
|
1609
|
+
});
|
|
1610
|
+
await store.save({
|
|
1611
|
+
id: "doctor-write-test",
|
|
1612
|
+
createdAt: new Date().toISOString(),
|
|
1613
|
+
updatedAt: new Date().toISOString(),
|
|
1614
|
+
entries: [],
|
|
1615
|
+
events: [],
|
|
1616
|
+
});
|
|
1617
|
+
const message = config.store?.backend
|
|
1618
|
+
? config.store.backend
|
|
1619
|
+
: store instanceof FileSessionStore
|
|
1620
|
+
? store.sessionsDir
|
|
1621
|
+
: "default";
|
|
1622
|
+
checks.push({ name: "session-store", ok: true, message });
|
|
1175
1623
|
}
|
|
1176
1624
|
catch (error) {
|
|
1177
|
-
checks.push({
|
|
1625
|
+
checks.push({
|
|
1626
|
+
name: "session-store",
|
|
1627
|
+
ok: false,
|
|
1628
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1629
|
+
});
|
|
1178
1630
|
}
|
|
1179
1631
|
if (checkTools) {
|
|
1180
1632
|
try {
|
|
@@ -1182,32 +1634,42 @@ async function doctorCommand(args) {
|
|
|
1182
1634
|
const schemas = toolsToModelSchemas(createBuiltinTools(sandbox));
|
|
1183
1635
|
for (const schema of schemas) {
|
|
1184
1636
|
const input = schema.inputSchema;
|
|
1185
|
-
if (input?.type !==
|
|
1637
|
+
if (input?.type !== "object" ||
|
|
1638
|
+
typeof input.properties !== "object" ||
|
|
1639
|
+
input.properties === null)
|
|
1186
1640
|
throw new Error(`Invalid schema for tool ${schema.name}`);
|
|
1187
1641
|
}
|
|
1188
|
-
checks.push({ name:
|
|
1642
|
+
checks.push({ name: "tools", ok: true, message: `${schemas.length} tool schemas valid` });
|
|
1189
1643
|
}
|
|
1190
1644
|
catch (error) {
|
|
1191
|
-
checks.push({
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1645
|
+
checks.push({
|
|
1646
|
+
name: "tools",
|
|
1647
|
+
ok: false,
|
|
1648
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if ((target ?? config.run?.target) === "temporal-worker") {
|
|
1653
|
+
const address = process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? "localhost:7233";
|
|
1654
|
+
const taskQueue = process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? "fabric-harness";
|
|
1655
|
+
checks.push({
|
|
1656
|
+
name: "temporal",
|
|
1657
|
+
ok: true,
|
|
1658
|
+
message: `configured address ${address}, taskQueue ${taskQueue}; start a worker/server for live validation`,
|
|
1659
|
+
});
|
|
1198
1660
|
}
|
|
1199
1661
|
if (jsonOutput) {
|
|
1200
1662
|
printResult({ ok: checks.every((check) => check.ok), checks });
|
|
1201
1663
|
return;
|
|
1202
1664
|
}
|
|
1203
1665
|
for (const check of checks)
|
|
1204
|
-
console.log(`${check.ok ?
|
|
1666
|
+
console.log(`${check.ok ? "✓" : "✗"} ${check.name}: ${check.message}`);
|
|
1205
1667
|
if (!checks.every((check) => check.ok))
|
|
1206
1668
|
process.exitCode = 1;
|
|
1207
1669
|
}
|
|
1208
1670
|
async function agentsCommand(args) {
|
|
1209
|
-
const jsonOutput = args.includes(
|
|
1210
|
-
const unknown = args.find((arg) => arg !==
|
|
1671
|
+
const jsonOutput = args.includes("--json");
|
|
1672
|
+
const unknown = args.find((arg) => arg !== "--json");
|
|
1211
1673
|
if (unknown)
|
|
1212
1674
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1213
1675
|
const workspaceRoot = await findWorkspaceRoot();
|
|
@@ -1217,18 +1679,18 @@ async function agentsCommand(args) {
|
|
|
1217
1679
|
return;
|
|
1218
1680
|
}
|
|
1219
1681
|
if (agents.length === 0) {
|
|
1220
|
-
console.log(
|
|
1682
|
+
console.log("No agents found.");
|
|
1221
1683
|
return;
|
|
1222
1684
|
}
|
|
1223
1685
|
for (const item of agents)
|
|
1224
|
-
console.log(`${item.name.padEnd(20)} ${item.description ??
|
|
1686
|
+
console.log(`${item.name.padEnd(20)} ${item.description ?? ""}`.trimEnd());
|
|
1225
1687
|
}
|
|
1226
1688
|
async function describeCommand(args) {
|
|
1227
1689
|
const [agentName, ...rest] = args;
|
|
1228
|
-
if (!agentName || agentName.startsWith(
|
|
1229
|
-
throw new Error(
|
|
1230
|
-
const jsonOutput = rest.includes(
|
|
1231
|
-
const unknown = rest.find((arg) => arg !==
|
|
1690
|
+
if (!agentName || agentName.startsWith("-"))
|
|
1691
|
+
throw new Error("Missing required <agent> argument for describe.");
|
|
1692
|
+
const jsonOutput = rest.includes("--json");
|
|
1693
|
+
const unknown = rest.find((arg) => arg !== "--json");
|
|
1232
1694
|
if (unknown)
|
|
1233
1695
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1234
1696
|
const workspaceRoot = await findWorkspaceRoot();
|
|
@@ -1246,19 +1708,19 @@ async function describeCommand(args) {
|
|
|
1246
1708
|
console.log(`Default model: ${description.model}`);
|
|
1247
1709
|
if (description.target)
|
|
1248
1710
|
console.log(`Default target: ${description.target}`);
|
|
1249
|
-
console.log(
|
|
1250
|
-
console.log(
|
|
1251
|
-
printSchema(description.inputSchema,
|
|
1252
|
-
console.log(
|
|
1253
|
-
console.log(
|
|
1254
|
-
printSchema(description.outputSchema,
|
|
1255
|
-
console.log(
|
|
1256
|
-
console.log(
|
|
1711
|
+
console.log("");
|
|
1712
|
+
console.log("Input:");
|
|
1713
|
+
printSchema(description.inputSchema, " ");
|
|
1714
|
+
console.log("");
|
|
1715
|
+
console.log("Output:");
|
|
1716
|
+
printSchema(description.outputSchema, " ");
|
|
1717
|
+
console.log("");
|
|
1718
|
+
console.log("Examples:");
|
|
1257
1719
|
const input = description.inputSchema;
|
|
1258
|
-
const firstField = input?.type ===
|
|
1720
|
+
const firstField = input?.type === "object" ? Object.keys(input.properties ?? {})[0] : undefined;
|
|
1259
1721
|
if (firstField)
|
|
1260
1722
|
console.log(` fabric-harness run ${agentName} --${firstField} "..."`);
|
|
1261
|
-
console.log(` fabric-harness run ${agentName} --payload '{"${firstField ??
|
|
1723
|
+
console.log(` fabric-harness run ${agentName} --payload '{"${firstField ?? "input"}":"..."}'`);
|
|
1262
1724
|
}
|
|
1263
1725
|
function printSchema(value, indent) {
|
|
1264
1726
|
if (!value) {
|
|
@@ -1266,7 +1728,7 @@ function printSchema(value, indent) {
|
|
|
1266
1728
|
return;
|
|
1267
1729
|
}
|
|
1268
1730
|
const schema = value;
|
|
1269
|
-
if (schema.type ===
|
|
1731
|
+
if (schema.type === "object") {
|
|
1270
1732
|
const required = new Set(schema.required ?? []);
|
|
1271
1733
|
const properties = schema.properties ?? {};
|
|
1272
1734
|
if (Object.keys(properties).length === 0) {
|
|
@@ -1275,38 +1737,46 @@ function printSchema(value, indent) {
|
|
|
1275
1737
|
}
|
|
1276
1738
|
for (const [key, property] of Object.entries(properties)) {
|
|
1277
1739
|
const field = property;
|
|
1278
|
-
const req = required.has(key) ?
|
|
1279
|
-
const enumText = field.enum ? ` enum=${field.enum.join(
|
|
1280
|
-
const desc = field.description ? ` - ${field.description}` :
|
|
1281
|
-
console.log(`${indent}${key.padEnd(16)} ${(field.type ??
|
|
1740
|
+
const req = required.has(key) ? "required" : "optional";
|
|
1741
|
+
const enumText = field.enum ? ` enum=${field.enum.join("|")}` : "";
|
|
1742
|
+
const desc = field.description ? ` - ${field.description}` : "";
|
|
1743
|
+
console.log(`${indent}${key.padEnd(16)} ${(field.type ?? "unknown").padEnd(8)} ${req}${enumText}${desc}`);
|
|
1282
1744
|
}
|
|
1283
1745
|
return;
|
|
1284
1746
|
}
|
|
1285
|
-
const enumText = schema.enum ? ` enum=${schema.enum.join(
|
|
1286
|
-
const desc = schema.description ? ` - ${schema.description}` :
|
|
1287
|
-
console.log(`${indent}${schema.type ??
|
|
1747
|
+
const enumText = schema.enum ? ` enum=${schema.enum.join("|")}` : "";
|
|
1748
|
+
const desc = schema.description ? ` - ${schema.description}` : "";
|
|
1749
|
+
console.log(`${indent}${schema.type ?? "unknown"}${enumText}${desc}`);
|
|
1288
1750
|
}
|
|
1289
1751
|
async function sessionsCommand(args) {
|
|
1290
1752
|
if (args.length > 0)
|
|
1291
1753
|
throw new Error(`Unknown argument: ${args[0]}`);
|
|
1292
1754
|
const workspaceRoot = await findWorkspaceRoot();
|
|
1293
1755
|
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
1294
|
-
const store = await createConfiguredSessionStore({
|
|
1756
|
+
const store = await createConfiguredSessionStore({
|
|
1757
|
+
workspaceRoot,
|
|
1758
|
+
...(config.store ? { config: config.store } : {}),
|
|
1759
|
+
env: process.env,
|
|
1760
|
+
});
|
|
1295
1761
|
const sessions = await listSessionSummariesFromStore(store);
|
|
1296
1762
|
if (sessions.length === 0) {
|
|
1297
|
-
console.log(
|
|
1763
|
+
console.log("No sessions found.");
|
|
1298
1764
|
return;
|
|
1299
1765
|
}
|
|
1300
1766
|
for (const session of sessions) {
|
|
1301
|
-
const agent = session.agentId ? ` agent=${session.agentId}` :
|
|
1302
|
-
const error = session.latestError ? ` error=${session.latestError}` :
|
|
1767
|
+
const agent = session.agentId ? ` agent=${session.agentId}` : "";
|
|
1768
|
+
const error = session.latestError ? ` error=${session.latestError}` : "";
|
|
1303
1769
|
console.log(`${session.updatedAt} ${session.id}${agent} entries=${session.entries} events=${session.events} approvals=${session.pendingApprovals}/${session.terminalApprovals}${error}`);
|
|
1304
1770
|
}
|
|
1305
1771
|
}
|
|
1306
1772
|
async function loadConfiguredStoreForWorkspace() {
|
|
1307
1773
|
const workspaceRoot = await findWorkspaceRoot();
|
|
1308
1774
|
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
1309
|
-
const store = await createConfiguredSessionStore({
|
|
1775
|
+
const store = await createConfiguredSessionStore({
|
|
1776
|
+
workspaceRoot,
|
|
1777
|
+
...(config.store ? { config: config.store } : {}),
|
|
1778
|
+
env: process.env,
|
|
1779
|
+
});
|
|
1310
1780
|
return { workspaceRoot, config, store };
|
|
1311
1781
|
}
|
|
1312
1782
|
async function buildsCommand(args) {
|
|
@@ -1315,66 +1785,70 @@ async function buildsCommand(args) {
|
|
|
1315
1785
|
const workspaceRoot = await findWorkspaceRoot();
|
|
1316
1786
|
const builds = await listBuilds(workspaceRoot);
|
|
1317
1787
|
if (builds.length === 0) {
|
|
1318
|
-
console.log(
|
|
1788
|
+
console.log("No builds found.");
|
|
1319
1789
|
return;
|
|
1320
1790
|
}
|
|
1321
1791
|
for (const build of builds) {
|
|
1322
|
-
const entrypoint = build.entrypoint ? ` entry=${build.entrypoint}` :
|
|
1792
|
+
const entrypoint = build.entrypoint ? ` entry=${build.entrypoint}` : "";
|
|
1323
1793
|
console.log(`${build.createdAt} ${build.target}${entrypoint} agents=${build.agents} roles=${build.roles} skills=${build.skills} files=${build.files}`);
|
|
1324
1794
|
}
|
|
1325
1795
|
}
|
|
1326
1796
|
async function approvalsCommand(args) {
|
|
1327
1797
|
const [sessionId, ...rest] = args;
|
|
1328
|
-
if (!sessionId || sessionId.startsWith(
|
|
1329
|
-
throw new Error(
|
|
1330
|
-
const pendingOnly = rest.includes(
|
|
1331
|
-
const stateView = rest.includes(
|
|
1332
|
-
const unknown = rest.find((arg) => arg !==
|
|
1798
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
1799
|
+
throw new Error("Missing required <session-id> argument for approvals.");
|
|
1800
|
+
const pendingOnly = rest.includes("--pending");
|
|
1801
|
+
const stateView = rest.includes("--state");
|
|
1802
|
+
const unknown = rest.find((arg) => arg !== "--pending" && arg !== "--state");
|
|
1333
1803
|
if (unknown)
|
|
1334
1804
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1335
1805
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1336
1806
|
if (stateView) {
|
|
1337
|
-
const states = (await listApprovalStatesFromStore(store, sessionId)).filter((approval) => !pendingOnly || approval.status ===
|
|
1807
|
+
const states = (await listApprovalStatesFromStore(store, sessionId)).filter((approval) => !pendingOnly || approval.status === "pending");
|
|
1338
1808
|
if (states.length === 0) {
|
|
1339
1809
|
console.log(`No approvals found for session ${sessionId}.`);
|
|
1340
1810
|
return;
|
|
1341
1811
|
}
|
|
1342
1812
|
for (const state of states) {
|
|
1343
|
-
const actors = state.votes
|
|
1344
|
-
|
|
1813
|
+
const actors = state.votes
|
|
1814
|
+
.map((vote) => `${typeof vote.actor === "string" ? vote.actor : vote.actor.id}:${vote.decision}`)
|
|
1815
|
+
.join(",") || "none";
|
|
1816
|
+
const resolved = state.resolvedAt ? ` resolved=${state.resolvedAt}` : "";
|
|
1345
1817
|
console.log(`${state.status} ${state.id} approvals=${state.approvedCount}/${state.requiredApprovals} denials=${state.deniedCount} votes=${actors}${resolved} subject=${state.request.subject}`);
|
|
1346
1818
|
}
|
|
1347
1819
|
return;
|
|
1348
1820
|
}
|
|
1349
|
-
const approvals = (await listApprovalsFromStore(store, sessionId)).filter((approval) => !pendingOnly || approval.status ===
|
|
1821
|
+
const approvals = (await listApprovalsFromStore(store, sessionId)).filter((approval) => !pendingOnly || approval.status === "pending");
|
|
1350
1822
|
if (approvals.length === 0) {
|
|
1351
1823
|
console.log(`No approvals found for session ${sessionId}.`);
|
|
1352
1824
|
return;
|
|
1353
1825
|
}
|
|
1354
1826
|
for (const approval of approvals) {
|
|
1355
|
-
const resolved = approval.resolvedAt ? ` resolved=${approval.resolvedAt}` :
|
|
1356
|
-
const risk = approval.risk ? ` risk=${approval.risk}` :
|
|
1357
|
-
const pattern = approval.matchedPattern ? ` pattern=${approval.matchedPattern}` :
|
|
1358
|
-
const env = approval.envKeys?.length ? ` env=${approval.envKeys.join(
|
|
1359
|
-
const paths = approval.affectedPaths?.length
|
|
1827
|
+
const resolved = approval.resolvedAt ? ` resolved=${approval.resolvedAt}` : "";
|
|
1828
|
+
const risk = approval.risk ? ` risk=${approval.risk}` : "";
|
|
1829
|
+
const pattern = approval.matchedPattern ? ` pattern=${approval.matchedPattern}` : "";
|
|
1830
|
+
const env = approval.envKeys?.length ? ` env=${approval.envKeys.join(",")}` : "";
|
|
1831
|
+
const paths = approval.affectedPaths?.length
|
|
1832
|
+
? ` paths=${approval.affectedPaths.join(",")}`
|
|
1833
|
+
: "";
|
|
1360
1834
|
console.log(`${approval.requestedAt} ${approval.status} ${approval.id} ${approval.kind}:${approval.subject}${risk}${pattern}${env}${paths}${resolved} reason=${approval.reason}`);
|
|
1361
1835
|
}
|
|
1362
1836
|
}
|
|
1363
1837
|
async function resolveApprovalCommand(args, decision) {
|
|
1364
1838
|
const [sessionId, approvalId, ...rest] = args;
|
|
1365
|
-
if (!sessionId || sessionId.startsWith(
|
|
1839
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
1366
1840
|
throw new Error(`Missing required <session-id> argument for ${decision}.`);
|
|
1367
|
-
if (!approvalId || approvalId.startsWith(
|
|
1841
|
+
if (!approvalId || approvalId.startsWith("-"))
|
|
1368
1842
|
throw new Error(`Missing required <approval-id> argument for ${decision}.`);
|
|
1369
1843
|
let reason;
|
|
1370
|
-
let actor =
|
|
1844
|
+
let actor = "cli";
|
|
1371
1845
|
for (let index = 0; index < rest.length; index += 1) {
|
|
1372
1846
|
const arg = rest[index];
|
|
1373
|
-
if (arg ===
|
|
1847
|
+
if (arg === "--reason") {
|
|
1374
1848
|
reason = rest[++index];
|
|
1375
1849
|
continue;
|
|
1376
1850
|
}
|
|
1377
|
-
if (arg ===
|
|
1851
|
+
if (arg === "--actor") {
|
|
1378
1852
|
actor = rest[++index] ?? actor;
|
|
1379
1853
|
continue;
|
|
1380
1854
|
}
|
|
@@ -1386,22 +1860,28 @@ async function resolveApprovalCommand(args, decision) {
|
|
|
1386
1860
|
printResult(resolved);
|
|
1387
1861
|
}
|
|
1388
1862
|
async function signalTemporalApprovalIfNeeded(workspaceRoot, sessionId, approval, decision, reason) {
|
|
1389
|
-
if (!approval.workflowId || (approval.status !==
|
|
1863
|
+
if (!approval.workflowId || (approval.status !== "approved" && approval.status !== "denied"))
|
|
1390
1864
|
return;
|
|
1391
1865
|
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
1392
1866
|
try {
|
|
1393
1867
|
const namespace = process.env.FABRIC_TEMPORAL_NAMESPACE ?? config.temporal?.namespace;
|
|
1394
1868
|
const client = await createTemporalClient({
|
|
1395
|
-
mode:
|
|
1396
|
-
address: process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ??
|
|
1397
|
-
taskQueue: process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ??
|
|
1869
|
+
mode: "temporal",
|
|
1870
|
+
address: process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? "localhost:7233",
|
|
1871
|
+
taskQueue: process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? "fabric-harness",
|
|
1398
1872
|
...(namespace ? { namespace } : {}),
|
|
1399
1873
|
...(config.temporal?.apiKey ? { apiKey: config.temporal.apiKey } : {}),
|
|
1400
1874
|
...(config.temporal?.apiKeyEnv ? { apiKeyEnv: config.temporal.apiKeyEnv } : {}),
|
|
1401
1875
|
...(config.temporal?.tls ? { tls: config.temporal.tls } : {}),
|
|
1402
1876
|
});
|
|
1403
1877
|
try {
|
|
1404
|
-
await client.signalWorkflowApproval(approval.workflowId, {
|
|
1878
|
+
await client.signalWorkflowApproval(approval.workflowId, {
|
|
1879
|
+
approvalId: approval.id,
|
|
1880
|
+
decision,
|
|
1881
|
+
...((reason ?? approval.resolutionReason)
|
|
1882
|
+
? { reason: reason ?? approval.resolutionReason }
|
|
1883
|
+
: {}),
|
|
1884
|
+
});
|
|
1405
1885
|
console.error(`Signaled Temporal workflow ${approval.workflowId} for approval ${approval.id}.`);
|
|
1406
1886
|
}
|
|
1407
1887
|
finally {
|
|
@@ -1414,8 +1894,8 @@ async function signalTemporalApprovalIfNeeded(workspaceRoot, sessionId, approval
|
|
|
1414
1894
|
}
|
|
1415
1895
|
async function checkpointsCommand(args) {
|
|
1416
1896
|
const [sessionId, ...rest] = args;
|
|
1417
|
-
if (!sessionId || sessionId.startsWith(
|
|
1418
|
-
throw new Error(
|
|
1897
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
1898
|
+
throw new Error("Missing required <session-id> argument for checkpoints.");
|
|
1419
1899
|
if (rest.length > 0)
|
|
1420
1900
|
throw new Error(`Unknown argument: ${rest[0]}`);
|
|
1421
1901
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
@@ -1425,24 +1905,24 @@ async function checkpointsCommand(args) {
|
|
|
1425
1905
|
return;
|
|
1426
1906
|
}
|
|
1427
1907
|
for (const checkpoint of checkpoints) {
|
|
1428
|
-
const snapshot = checkpoint.snapshotId ? ` snapshot=${checkpoint.snapshotId}` :
|
|
1908
|
+
const snapshot = checkpoint.snapshotId ? ` snapshot=${checkpoint.snapshotId}` : "";
|
|
1429
1909
|
console.log(`${checkpoint.timestamp} ${checkpoint.label} entry=${checkpoint.entryId}${snapshot} restored=${checkpoint.restoredCount}`);
|
|
1430
1910
|
}
|
|
1431
1911
|
}
|
|
1432
1912
|
async function compactCommand(args) {
|
|
1433
1913
|
const [sessionId, ...rest] = args;
|
|
1434
|
-
if (!sessionId || sessionId.startsWith(
|
|
1435
|
-
throw new Error(
|
|
1914
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
1915
|
+
throw new Error("Missing required <session-id> argument for compact.");
|
|
1436
1916
|
let keepRecentEntries;
|
|
1437
1917
|
let summary;
|
|
1438
1918
|
for (let index = 0; index < rest.length; index += 1) {
|
|
1439
1919
|
const arg = rest[index];
|
|
1440
|
-
if (arg ===
|
|
1920
|
+
if (arg === "--keep") {
|
|
1441
1921
|
const value = rest[++index];
|
|
1442
1922
|
keepRecentEntries = value ? Number(value) : undefined;
|
|
1443
1923
|
continue;
|
|
1444
1924
|
}
|
|
1445
|
-
if (arg ===
|
|
1925
|
+
if (arg === "--summary") {
|
|
1446
1926
|
summary = rest[++index];
|
|
1447
1927
|
continue;
|
|
1448
1928
|
}
|
|
@@ -1458,10 +1938,10 @@ async function compactCommand(args) {
|
|
|
1458
1938
|
}
|
|
1459
1939
|
async function logsCommand(args) {
|
|
1460
1940
|
const [sessionId, ...rest] = args;
|
|
1461
|
-
if (!sessionId || sessionId.startsWith(
|
|
1462
|
-
throw new Error(
|
|
1463
|
-
const showEvents = rest.includes(
|
|
1464
|
-
const unknown = rest.find((arg) => arg !==
|
|
1941
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
1942
|
+
throw new Error("Missing required <session-id> argument for logs.");
|
|
1943
|
+
const showEvents = rest.includes("--events");
|
|
1944
|
+
const unknown = rest.find((arg) => arg !== "--events");
|
|
1465
1945
|
if (unknown)
|
|
1466
1946
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1467
1947
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
@@ -1473,25 +1953,25 @@ async function logsCommand(args) {
|
|
|
1473
1953
|
console.log(`Updated: ${data.updatedAt}`);
|
|
1474
1954
|
if (data.agentId)
|
|
1475
1955
|
console.log(`Agent: ${data.agentId}`);
|
|
1476
|
-
console.log(
|
|
1477
|
-
const entries = showEvents ? data.events ?? [] : data.entries ?? [];
|
|
1956
|
+
console.log("");
|
|
1957
|
+
const entries = showEvents ? (data.events ?? []) : (data.entries ?? []);
|
|
1478
1958
|
if (entries.length === 0) {
|
|
1479
|
-
console.log(showEvents ?
|
|
1959
|
+
console.log(showEvents ? "No events." : "No structured entries.");
|
|
1480
1960
|
return;
|
|
1481
1961
|
}
|
|
1482
1962
|
for (const item of entries) {
|
|
1483
|
-
const timestamp =
|
|
1484
|
-
const type =
|
|
1485
|
-
const dataText =
|
|
1963
|
+
const timestamp = "timestamp" in item ? item.timestamp : "";
|
|
1964
|
+
const type = "type" in item ? item.type : "unknown";
|
|
1965
|
+
const dataText = "data" in item && item.data !== undefined ? ` ${formatInline(item.data)}` : "";
|
|
1486
1966
|
console.log(`${timestamp} ${type}${dataText}`);
|
|
1487
1967
|
}
|
|
1488
1968
|
}
|
|
1489
1969
|
async function artifactsCommand(args) {
|
|
1490
1970
|
const [sessionId, ...rest] = args;
|
|
1491
|
-
if (!sessionId || sessionId.startsWith(
|
|
1492
|
-
throw new Error(
|
|
1493
|
-
const json = rest.includes(
|
|
1494
|
-
const unknown = rest.find((arg) => arg !==
|
|
1971
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
1972
|
+
throw new Error("Missing required <session-id> argument for artifacts.");
|
|
1973
|
+
const json = rest.includes("--json");
|
|
1974
|
+
const unknown = rest.find((arg) => arg !== "--json");
|
|
1495
1975
|
if (unknown)
|
|
1496
1976
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1497
1977
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
@@ -1503,18 +1983,18 @@ async function artifactsCommand(args) {
|
|
|
1503
1983
|
return;
|
|
1504
1984
|
}
|
|
1505
1985
|
for (const artifact of artifacts)
|
|
1506
|
-
console.log(`${artifact.id} ${artifact.name} ${artifact.size} bytes ${artifact.contentType ??
|
|
1986
|
+
console.log(`${artifact.id} ${artifact.name} ${artifact.size} bytes ${artifact.contentType ?? "application/octet-stream"}`);
|
|
1507
1987
|
}
|
|
1508
1988
|
async function artifactCommand(args) {
|
|
1509
1989
|
const [subcommand, sessionId, artifactIdOrName, ...rest] = args;
|
|
1510
|
-
if (subcommand !==
|
|
1511
|
-
throw new Error(
|
|
1990
|
+
if (subcommand !== "get")
|
|
1991
|
+
throw new Error("Usage: fabric-harness artifact get <session-id> <artifact-id-or-name> [--out <path>]");
|
|
1512
1992
|
if (!sessionId || !artifactIdOrName)
|
|
1513
|
-
throw new Error(
|
|
1993
|
+
throw new Error("Usage: fabric-harness artifact get <session-id> <artifact-id-or-name> [--out <path>]");
|
|
1514
1994
|
let out;
|
|
1515
1995
|
for (let index = 0; index < rest.length; index += 1) {
|
|
1516
1996
|
const arg = rest[index];
|
|
1517
|
-
if (arg ===
|
|
1997
|
+
if (arg === "--out") {
|
|
1518
1998
|
out = rest[++index];
|
|
1519
1999
|
continue;
|
|
1520
2000
|
}
|
|
@@ -1535,10 +2015,10 @@ async function artifactCommand(args) {
|
|
|
1535
2015
|
}
|
|
1536
2016
|
async function metricsCommand(args) {
|
|
1537
2017
|
const [sessionId, ...rest] = args;
|
|
1538
|
-
if (!sessionId || sessionId.startsWith(
|
|
1539
|
-
throw new Error(
|
|
1540
|
-
const json = rest.includes(
|
|
1541
|
-
const unknown = rest.find((arg) => arg !==
|
|
2018
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
2019
|
+
throw new Error("Missing required <session-id> argument for metrics.");
|
|
2020
|
+
const json = rest.includes("--json");
|
|
2021
|
+
const unknown = rest.find((arg) => arg !== "--json");
|
|
1542
2022
|
if (unknown)
|
|
1543
2023
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1544
2024
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
@@ -1555,13 +2035,26 @@ async function metricsCommand(args) {
|
|
|
1555
2035
|
console.log(`Tools: calls=${metrics.toolCalls} durationMs=${metrics.toolDurationMs}`);
|
|
1556
2036
|
console.log(`Shell: commands=${metrics.shellCommands} durationMs=${metrics.shellDurationMs}`);
|
|
1557
2037
|
console.log(`Artifacts: ${metrics.artifacts}`);
|
|
2038
|
+
console.log(`Mounts: count=${metrics.mounts} files=${metrics.mountFiles} bytes=${metrics.mountBytes}`);
|
|
2039
|
+
if (metrics.perTool) {
|
|
2040
|
+
const top = Object.entries(metrics.perTool)
|
|
2041
|
+
.sort((a, b) => b[1] - a[1])
|
|
2042
|
+
.slice(0, 5);
|
|
2043
|
+
console.log(`Top tools: ${top.map(([name, count]) => `${name}=${count}`).join(" ")}`);
|
|
2044
|
+
}
|
|
2045
|
+
if (metrics.perSource) {
|
|
2046
|
+
const top = Object.entries(metrics.perSource)
|
|
2047
|
+
.sort((a, b) => b[1] - a[1])
|
|
2048
|
+
.slice(0, 5);
|
|
2049
|
+
console.log(`Top sources by bytes: ${top.map(([name, bytes]) => `${name}=${bytes}`).join(" ")}`);
|
|
2050
|
+
}
|
|
1558
2051
|
}
|
|
1559
2052
|
async function tasksCommand(args) {
|
|
1560
2053
|
const [sessionId, ...rest] = args;
|
|
1561
|
-
if (!sessionId || sessionId.startsWith(
|
|
1562
|
-
throw new Error(
|
|
1563
|
-
const json = rest.includes(
|
|
1564
|
-
const unknown = rest.find((arg) => arg !==
|
|
2054
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
2055
|
+
throw new Error("Missing required <session-id> argument for tasks.");
|
|
2056
|
+
const json = rest.includes("--json");
|
|
2057
|
+
const unknown = rest.find((arg) => arg !== "--json");
|
|
1565
2058
|
if (unknown)
|
|
1566
2059
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1567
2060
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
@@ -1573,8 +2066,8 @@ async function tasksCommand(args) {
|
|
|
1573
2066
|
return;
|
|
1574
2067
|
}
|
|
1575
2068
|
for (const task of tasks) {
|
|
1576
|
-
const checkpointText = task.checkpoints.length ? ` checkpoints=${task.checkpoints.length}` :
|
|
1577
|
-
const errorText = task.error ? ` error=${truncateInline(task.error)}` :
|
|
2069
|
+
const checkpointText = task.checkpoints.length ? ` checkpoints=${task.checkpoints.length}` : "";
|
|
2070
|
+
const errorText = task.error ? ` error=${truncateInline(task.error)}` : "";
|
|
1578
2071
|
console.log(`${task.id} ${task.status} started=${task.startedAt} updated=${task.updatedAt}${checkpointText}${errorText}`);
|
|
1579
2072
|
if (task.text)
|
|
1580
2073
|
console.log(` ${truncateInline(task.text)}`);
|
|
@@ -1582,12 +2075,12 @@ async function tasksCommand(args) {
|
|
|
1582
2075
|
}
|
|
1583
2076
|
async function taskCommand(args) {
|
|
1584
2077
|
const [sessionId, taskId, ...rest] = args;
|
|
1585
|
-
if (!sessionId || sessionId.startsWith(
|
|
1586
|
-
throw new Error(
|
|
1587
|
-
if (!taskId || taskId.startsWith(
|
|
1588
|
-
throw new Error(
|
|
1589
|
-
const json = rest.includes(
|
|
1590
|
-
const unknown = rest.find((arg) => arg !==
|
|
2078
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
2079
|
+
throw new Error("Missing required <session-id> argument for task.");
|
|
2080
|
+
if (!taskId || taskId.startsWith("-"))
|
|
2081
|
+
throw new Error("Missing required <task-id> argument for task.");
|
|
2082
|
+
const json = rest.includes("--json");
|
|
2083
|
+
const unknown = rest.find((arg) => arg !== "--json");
|
|
1591
2084
|
if (unknown)
|
|
1592
2085
|
throw new Error(`Unknown argument: ${unknown}`);
|
|
1593
2086
|
const { store } = await loadConfiguredStoreForWorkspace();
|
|
@@ -1614,26 +2107,26 @@ async function taskCommand(args) {
|
|
|
1614
2107
|
if (task.text)
|
|
1615
2108
|
console.log(`Text: ${task.text}`);
|
|
1616
2109
|
if (task.checkpoints.length) {
|
|
1617
|
-
console.log(
|
|
2110
|
+
console.log("Checkpoints:");
|
|
1618
2111
|
for (const checkpoint of task.checkpoints)
|
|
1619
|
-
console.log(` ${checkpoint.timestamp} ${checkpoint.phase ??
|
|
2112
|
+
console.log(` ${checkpoint.timestamp} ${checkpoint.phase ?? "checkpoint"} ${checkpoint.checkpointId ?? checkpoint.entryId}`);
|
|
1620
2113
|
}
|
|
1621
2114
|
}
|
|
1622
2115
|
async function cancelTaskCommand(args) {
|
|
1623
2116
|
const [sessionId, taskId, ...rest] = args;
|
|
1624
|
-
if (!sessionId || sessionId.startsWith(
|
|
1625
|
-
throw new Error(
|
|
1626
|
-
if (!taskId || taskId.startsWith(
|
|
1627
|
-
throw new Error(
|
|
1628
|
-
let actor =
|
|
2117
|
+
if (!sessionId || sessionId.startsWith("-"))
|
|
2118
|
+
throw new Error("Missing required <session-id> argument for cancel-task.");
|
|
2119
|
+
if (!taskId || taskId.startsWith("-"))
|
|
2120
|
+
throw new Error("Missing required <task-id> argument for cancel-task.");
|
|
2121
|
+
let actor = "cli";
|
|
1629
2122
|
let reason;
|
|
1630
2123
|
for (let index = 0; index < rest.length; index += 1) {
|
|
1631
2124
|
const arg = rest[index];
|
|
1632
|
-
if (arg ===
|
|
2125
|
+
if (arg === "--actor") {
|
|
1633
2126
|
actor = rest[++index] ?? actor;
|
|
1634
2127
|
continue;
|
|
1635
2128
|
}
|
|
1636
|
-
if (arg ===
|
|
2129
|
+
if (arg === "--reason") {
|
|
1637
2130
|
reason = rest[++index];
|
|
1638
2131
|
continue;
|
|
1639
2132
|
}
|
|
@@ -1649,77 +2142,83 @@ function truncateInline(text) {
|
|
|
1649
2142
|
function formatInline(value) {
|
|
1650
2143
|
const text = JSON.stringify(value);
|
|
1651
2144
|
if (!text)
|
|
1652
|
-
return
|
|
2145
|
+
return "";
|
|
1653
2146
|
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
1654
2147
|
}
|
|
1655
2148
|
async function main(argv) {
|
|
1656
2149
|
const [command, ...args] = argv;
|
|
1657
|
-
if (!command || command ===
|
|
2150
|
+
if (!command || command === "--help" || command === "-h") {
|
|
1658
2151
|
console.log(HELP);
|
|
1659
2152
|
return;
|
|
1660
2153
|
}
|
|
1661
2154
|
await loadAutoEnvFiles();
|
|
1662
|
-
if (command ===
|
|
2155
|
+
if (command === "run")
|
|
1663
2156
|
return runCommand(args);
|
|
1664
|
-
if (command ===
|
|
2157
|
+
if (command === "agents")
|
|
1665
2158
|
return agentsCommand(args);
|
|
1666
|
-
if (command ===
|
|
2159
|
+
if (command === "describe")
|
|
1667
2160
|
return describeCommand(args);
|
|
1668
|
-
if (command ===
|
|
2161
|
+
if (command === "build")
|
|
1669
2162
|
return buildCommand(args);
|
|
1670
|
-
if (command ===
|
|
2163
|
+
if (command === "sessions")
|
|
1671
2164
|
return sessionsCommand(args);
|
|
1672
|
-
if (command ===
|
|
2165
|
+
if (command === "builds")
|
|
1673
2166
|
return buildsCommand(args);
|
|
1674
|
-
if (command ===
|
|
1675
|
-
return verifyCommand(args,
|
|
1676
|
-
if (command ===
|
|
1677
|
-
return verifyCommand(args,
|
|
1678
|
-
if (command ===
|
|
2167
|
+
if (command === "verify-attestation")
|
|
2168
|
+
return verifyCommand(args, "attestation");
|
|
2169
|
+
if (command === "verify-provenance")
|
|
2170
|
+
return verifyCommand(args, "provenance");
|
|
2171
|
+
if (command === "doctor")
|
|
1679
2172
|
return doctorCommand(args);
|
|
1680
|
-
if (command ===
|
|
2173
|
+
if (command === "add")
|
|
1681
2174
|
return addCommand(args);
|
|
1682
|
-
if (command ===
|
|
2175
|
+
if (command === "inspect")
|
|
1683
2176
|
return inspectCommand(args);
|
|
1684
|
-
if (command ===
|
|
2177
|
+
if (command === "logs")
|
|
1685
2178
|
return logsCommand(args);
|
|
1686
|
-
if (command ===
|
|
2179
|
+
if (command === "checkpoints")
|
|
1687
2180
|
return checkpointsCommand(args);
|
|
1688
|
-
if (command ===
|
|
2181
|
+
if (command === "artifacts")
|
|
1689
2182
|
return artifactsCommand(args);
|
|
1690
|
-
if (command ===
|
|
2183
|
+
if (command === "artifact")
|
|
1691
2184
|
return artifactCommand(args);
|
|
1692
|
-
if (command ===
|
|
2185
|
+
if (command === "metrics")
|
|
1693
2186
|
return metricsCommand(args);
|
|
1694
|
-
if (command ===
|
|
2187
|
+
if (command === "tasks")
|
|
1695
2188
|
return tasksCommand(args);
|
|
1696
|
-
if (command ===
|
|
2189
|
+
if (command === "task")
|
|
1697
2190
|
return taskCommand(args);
|
|
1698
|
-
if (command ===
|
|
2191
|
+
if (command === "cancel-task")
|
|
1699
2192
|
return cancelTaskCommand(args);
|
|
1700
|
-
if (command ===
|
|
2193
|
+
if (command === "compact")
|
|
1701
2194
|
return compactCommand(args);
|
|
1702
|
-
if (command ===
|
|
2195
|
+
if (command === "replay")
|
|
1703
2196
|
return replayCommand(args);
|
|
1704
|
-
if (command ===
|
|
2197
|
+
if (command === "approvals")
|
|
1705
2198
|
return approvalsCommand(args);
|
|
1706
|
-
if (command ===
|
|
1707
|
-
return resolveApprovalCommand(args,
|
|
1708
|
-
if (command ===
|
|
1709
|
-
return resolveApprovalCommand(args,
|
|
1710
|
-
if (command ===
|
|
2199
|
+
if (command === "approve")
|
|
2200
|
+
return resolveApprovalCommand(args, "approved");
|
|
2201
|
+
if (command === "reject")
|
|
2202
|
+
return resolveApprovalCommand(args, "denied");
|
|
2203
|
+
if (command === "dev")
|
|
1711
2204
|
return devCommand(args);
|
|
1712
|
-
if (command ===
|
|
2205
|
+
if (command === "temporal-worker")
|
|
1713
2206
|
return temporalWorkerCommand(args);
|
|
2207
|
+
if (command === "init")
|
|
2208
|
+
return initCommand(args);
|
|
1714
2209
|
throw new Error(`Unknown command: ${command}`);
|
|
1715
2210
|
}
|
|
1716
2211
|
try {
|
|
1717
2212
|
await main(process.argv.slice(2));
|
|
1718
2213
|
}
|
|
1719
2214
|
catch (error) {
|
|
1720
|
-
const message = error instanceof SchemaValidationError
|
|
2215
|
+
const message = error instanceof SchemaValidationError
|
|
2216
|
+
? `Invalid payload or result:\n${error.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n")}`
|
|
2217
|
+
: error instanceof Error
|
|
2218
|
+
? error.message
|
|
2219
|
+
: String(error);
|
|
1721
2220
|
console.error(`fabric-harness: ${message}`);
|
|
1722
|
-
console.error(
|
|
2221
|
+
console.error("Run fabric-harness --help for usage.");
|
|
1723
2222
|
process.exit(1);
|
|
1724
2223
|
}
|
|
1725
2224
|
//# sourceMappingURL=fabric-harness.js.map
|