@fabric-harness/cli 0.4.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.
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
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, compactSessionInStore, createConfiguredSessionStore, findWorkspaceRoot, inspectReplayFromStore, inspectSessionFromStore, describeAgentFile, getRequiredAgentDefinition, listAgentSummaries, listBuilds, listSessionSummariesFromStore, listApprovalStatesFromStore, listApprovalsFromStore, listCheckpointsFromStore, listTasksFromStore, loadAgentModule, loadFabricHarnessConfig, resolveAgentPath, resolveApprovalInStore, resolveSessionApproval, runAgent, startDevServer, cancelTaskInStore, getMetricsFromStore, getTaskFromStore, verifyAttestation, verifyProvenance, } from '@fabric-harness/node';
8
- import { FileSessionStore } from '@fabric-harness/node';
9
- import { EmptySandboxEnv, SchemaValidationError, createBuiltinTools, resolveModelProvider, toolsToModelSchemas } from '@fabric-harness/sdk';
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 '@fabric-harness/temporal';
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 === 'true')
116
+ if (trimmed === "true")
115
117
  return true;
116
- if (trimmed === 'false')
118
+ if (trimmed === "false")
117
119
  return false;
118
- if (trimmed === 'null')
120
+ if (trimmed === "null")
119
121
  return null;
120
122
  if (/^-?\d+(\.\d+)?$/.test(trimmed))
121
123
  return Number(trimmed);
122
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')))
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('Missing required <agent> argument for run.');
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 === '--id') {
144
+ if (arg === "--id") {
142
145
  const value = rest[++index];
143
146
  if (!value)
144
- throw new Error('Missing value for --id.');
147
+ throw new Error("Missing value for --id.");
145
148
  parsed.id = value;
146
149
  continue;
147
150
  }
148
- if (arg === '--payload') {
151
+ if (arg === "--payload") {
149
152
  const value = rest[++index];
150
153
  if (!value)
151
- throw new Error('Missing value for --payload.');
154
+ throw new Error("Missing value for --payload.");
152
155
  parsed.payload = parseJsonPayload(value);
153
156
  continue;
154
157
  }
155
- if (arg === '--payload-file') {
158
+ if (arg === "--payload-file") {
156
159
  const value = rest[++index];
157
160
  if (!value)
158
- throw new Error('Missing value for --payload-file.');
161
+ throw new Error("Missing value for --payload-file.");
159
162
  parsed.payloadFile = value;
160
163
  continue;
161
164
  }
162
- if (arg === '--stdin') {
165
+ if (arg === "--stdin") {
163
166
  parsed.stdin = true;
164
167
  continue;
165
168
  }
166
- if (arg === '--set') {
169
+ if (arg === "--set") {
167
170
  const value = rest[++index];
168
171
  if (!value)
169
- throw new Error('Missing value for --set.');
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 === '--runtime') {
177
+ if (arg === "--runtime") {
175
178
  const value = rest[++index];
176
- if (value !== 'inline' && value !== 'temporal')
177
- throw new Error('--runtime must be inline or temporal.');
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 === '--target') {
184
+ if (arg === "--target") {
182
185
  const value = rest[++index];
183
- if (value !== 'node' && value !== 'temporal-worker')
184
- throw new Error('--target for run must be node or temporal-worker.');
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 === 'temporal-worker' ? 'temporal' : 'inline';
189
+ parsed.runtime = value === "temporal-worker" ? "temporal" : "inline";
187
190
  continue;
188
191
  }
189
- if (arg === '--prompt') {
192
+ if (arg === "--prompt") {
190
193
  const value = rest[++index];
191
194
  if (!value)
192
- throw new Error('Missing value for --prompt.');
195
+ throw new Error("Missing value for --prompt.");
193
196
  parsed.prompt = value;
194
197
  continue;
195
198
  }
196
- if (arg === '--model') {
199
+ if (arg === "--model") {
197
200
  const value = rest[++index];
198
201
  if (!value)
199
- throw new Error('Missing value for --model.');
202
+ throw new Error("Missing value for --model.");
200
203
  parsed.model = value;
201
204
  continue;
202
205
  }
203
- if (arg === '--cwd') {
206
+ if (arg === "--cwd") {
204
207
  const value = rest[++index];
205
208
  if (!value)
206
- throw new Error('Missing value for --cwd.');
209
+ throw new Error("Missing value for --cwd.");
207
210
  parsed.cwd = value;
208
211
  continue;
209
212
  }
210
- if (arg === '--env') {
213
+ if (arg === "--env") {
211
214
  const value = rest[++index];
212
215
  if (!value)
213
- throw new Error('Missing value for --env.');
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 === '--help' || arg === '-h') {
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 === 'string') {
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, 'utf8');
261
+ contents = await fs.readFile(filePath, "utf8");
259
262
  }
260
263
  catch (error) {
261
- if (options.ignoreMissing && error.code === 'ENOENT')
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 = [...new Set([repoRoot, workspaceRoot].filter((value) => Boolean(value)))];
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, '.env'), path.join(root, '.env.local'));
287
+ files.push(path.join(root, ".env"), path.join(root, ".env.local"));
283
288
  }
284
289
  if (workspaceRoot) {
285
- files.push(path.join(workspaceRoot, '.fabricharness', '.env'), path.join(workspaceRoot, '.fabricharness', '.env.local'));
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, 'pnpm-workspace.yaml'));
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 === 'temporal')
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, 'utf8'));
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 === 'object' && payload !== null && !Array.isArray(payload) ? 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('utf8');
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 !== 'node' && envTarget !== 'temporal-worker')
342
- throw new Error('FABRIC_TARGET/FABRIC_HARNESS_TARGET must be node or temporal-worker.');
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 !== 'node' && target !== 'temporal-worker')
346
- throw new Error('Configured run.target must be node or temporal-worker.');
347
- const runtime = parsed.runtime ?? (target === 'temporal-worker' ? 'temporal' : 'inline');
348
- const model = parsed.model ?? process.env.FABRIC_MODEL ?? config.run?.model ?? agentDefaults.model ?? (typeof config.agent?.model === 'string' ? config.agent.model : undefined);
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('--id is required with --runtime temporal.');
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 ?? 'fabric-harness';
379
- const address = process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233';
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: 'temporal',
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 ?? 'fabric-cli',
390
- promptWorkflowMode: config.temporal?.promptWorkflowMode ?? 'hybrid',
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 = { ...(parsed.model ? { model: parsed.model } : {}), ...(parsed.cwd ? { cwd: parsed.cwd } : {}) };
396
- printResult(await runtime.prompt({ text: parsed.prompt, ...(Object.keys(options).length > 0 ? { options } : {}) }));
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 !== 'function')
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({ workspaceRoot, ...(config.store ? { config: config.store } : {}), env: process.env });
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 === 'object' && parsed.payload !== null && !Array.isArray(parsed.payload) ? 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 ?? 'temporal-agent';
419
- const model = parsed.model ?? (typeof initOptions.model === 'string' ? initOptions.model : undefined);
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: { id: sessionId, ...(model ? { model } : {}), session: async (_id, options) => createTemporalSession(sessionId, runtimeForSession, model, store, options?.cwd ?? cwd) },
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) => { const merged = withDefaults(options); return runtime.prompt({ text, ...(merged || model ? { options: { ...(merged ?? {}), ...(model && merged?.model === undefined ? { model } : {}) } } : {}) }); },
446
- stream: async function* (text, options) { for (const event of [])
447
- yield event; const merged = withDefaults(options); return await runtime.prompt({ text, ...(merged || model ? { options: { ...(merged ?? {}), ...(model && merged?.model === undefined ? { model } : {}) } } : {}) }); },
448
- skill: (name, options) => { const merged = withDefaults(options); return runtime.prompt({ text: `Run skill ${name}${options?.args ? ` with args ${JSON.stringify(options.args)}` : ''}`, ...(merged || model ? { options: { ...(merged ?? {}), ...(model && merged?.model === undefined ? { model } : {}) } } : {}) }); },
449
- task: (text, options) => { const merged = withDefaults(options); return runtime.task({ text, ...(options?.id ? { taskId: options.id } : {}), ...(merged || model ? { options: { ...(merged ?? {}), ...(model && merged?.model === undefined ? { model } : {}) } } : {}) }); },
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
- history: async () => ({ id: sessionId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), entries: [], events: [] }),
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('Configured session store does not support artifacts.');
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 = { id: randomUUID(), type: 'artifact_created', timestamp, data: { artifact: ref } };
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({ id: sessionId, createdAt: existing?.createdAt ?? timestamp, updatedAt: timestamp, entries: [...(existing?.entries ?? []), entry], events: existing?.events ?? [] });
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, { id: randomUUID(), type: 'artifact_created', timestamp, sessionId, data: { artifact: ref } });
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 () => ({ entryId: '', summary: 'Compaction is executed by Temporal worker activities.', compactedEntries: 0 }),
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 === '--env') {
618
+ if (arg === "--env") {
485
619
  const value = args[++index];
486
620
  if (!value)
487
- throw new Error('Missing value for --env.');
621
+ throw new Error("Missing value for --env.");
488
622
  envFiles.push(value);
489
623
  continue;
490
624
  }
491
- if (arg === '--task-queue') {
625
+ if (arg === "--task-queue") {
492
626
  const value = args[++index];
493
627
  if (!value)
494
- throw new Error('Missing value for --task-queue.');
628
+ throw new Error("Missing value for --task-queue.");
495
629
  taskQueueFlag = value;
496
630
  continue;
497
631
  }
498
- if (arg === '--address') {
632
+ if (arg === "--address") {
499
633
  const value = args[++index];
500
634
  if (!value)
501
- throw new Error('Missing value for --address.');
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 ?? process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? 'fabric-harness';
511
- const address = addressFlag ?? process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233';
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 === 'string' ? { model: 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: 'temporal',
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('SIGINT', () => void shutdown());
541
- process.on('SIGTERM', () => void shutdown());
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 === '--out') {
883
+ if (arg === "--out") {
562
884
  outDir = args[++index];
563
885
  continue;
564
886
  }
565
- if (arg === '--target') {
887
+ if (arg === "--target") {
566
888
  const value = args[++index];
567
- if (value !== 'node' && value !== 'temporal-worker' && value !== 'docker' && value !== 'foundry-hosted-agent' && value !== 'cloudflare')
568
- throw new Error('--target must be node, temporal-worker, docker, foundry-hosted-agent, or cloudflare.');
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 === '--no-clean') {
900
+ if (arg === "--no-clean") {
573
901
  clean = false;
574
902
  continue;
575
903
  }
576
- if (arg === '--sbom') {
904
+ if (arg === "--sbom") {
577
905
  sbom = true;
578
906
  continue;
579
907
  }
580
- if (arg === '--sbom-required') {
908
+ if (arg === "--sbom-required") {
581
909
  sbom = true;
582
910
  sbomRequired = true;
583
911
  continue;
584
912
  }
585
- if (arg === '--provenance') {
913
+ if (arg === "--provenance") {
586
914
  provenance = true;
587
915
  continue;
588
916
  }
589
- if (arg === '--attestation') {
917
+ if (arg === "--attestation") {
590
918
  attestation = true;
591
919
  continue;
592
920
  }
593
- if (arg === '--sign-provenance') {
921
+ if (arg === "--sign-provenance") {
594
922
  signProvenance = true;
595
923
  provenance = true;
596
924
  continue;
597
925
  }
598
- if (arg === '--signing-key') {
926
+ if (arg === "--signing-key") {
599
927
  signingKey = args[++index];
600
928
  if (!signingKey)
601
- throw new Error('Missing value for --signing-key.');
929
+ throw new Error("Missing value for --signing-key.");
602
930
  continue;
603
931
  }
604
- if (arg === '--docker-build') {
932
+ if (arg === "--docker-build") {
605
933
  dockerBuild = true;
606
934
  continue;
607
935
  }
608
- if (arg === '--docker-push') {
936
+ if (arg === "--docker-push") {
609
937
  dockerPush = true;
610
938
  continue;
611
939
  }
612
- if (arg === '--docker-tag') {
940
+ if (arg === "--docker-tag") {
613
941
  dockerTag = args[++index];
614
942
  if (!dockerTag)
615
- throw new Error('Missing value for --docker-tag.');
943
+ throw new Error("Missing value for --docker-tag.");
616
944
  continue;
617
945
  }
618
- if (arg === '--image-sbom') {
946
+ if (arg === "--image-sbom") {
619
947
  imageSbom = true;
620
948
  continue;
621
949
  }
622
- if (arg === '--image-sbom-required') {
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({ ...(outDir ? { outDir } : {}), ...(target ? { target } : {}), clean, sbom, sbomRequired, provenance, attestation, signProvenance, ...(signingKey ? { signingKey } : {}), dockerBuild, dockerPush, ...(dockerTag ? { dockerTag } : {}), imageSbom, imageSbomRequired });
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(', ') || 'none'}`);
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 = 'node';
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 === '--host') {
1002
+ if (arg === "--host") {
660
1003
  host = args[++index];
661
1004
  continue;
662
1005
  }
663
- if (arg === '--port') {
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 === '--target') {
1011
+ if (arg === "--target") {
669
1012
  const value = args[++index];
670
- if (value !== 'node' && value !== 'cloudflare')
671
- throw new Error('--target for dev must be node or cloudflare.');
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 === '--env') {
1018
+ if (arg === "--env") {
676
1019
  const value = args[++index];
677
1020
  if (!value)
678
- throw new Error('Missing value for --env.');
1021
+ throw new Error("Missing value for --env.");
679
1022
  envFiles.push(value);
680
1023
  continue;
681
1024
  }
682
- if (arg === '--auth-token-env') {
1025
+ if (arg === "--auth-token-env") {
683
1026
  const value = args[++index];
684
1027
  if (!value)
685
- throw new Error('Missing value for --auth-token-env.');
1028
+ throw new Error("Missing value for --auth-token-env.");
686
1029
  authTokenEnv = value;
687
1030
  continue;
688
1031
  }
689
- if (arg === '--max-body-bytes') {
1032
+ if (arg === "--max-body-bytes") {
690
1033
  const value = args[++index];
691
1034
  if (!value)
692
- throw new Error('Missing value for --max-body-bytes.');
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('--max-body-bytes must be a positive number.');
1038
+ throw new Error("--max-body-bytes must be a positive number.");
696
1039
  continue;
697
1040
  }
698
- if (arg === '--rate-limit-window-ms') {
1041
+ if (arg === "--rate-limit-window-ms") {
699
1042
  const value = args[++index];
700
1043
  if (!value)
701
- throw new Error('Missing value for --rate-limit-window-ms.');
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('--rate-limit-window-ms must be a positive number.');
1047
+ throw new Error("--rate-limit-window-ms must be a positive number.");
705
1048
  continue;
706
1049
  }
707
- if (arg === '--rate-limit-max') {
1050
+ if (arg === "--rate-limit-max") {
708
1051
  const value = args[++index];
709
1052
  if (!value)
710
- throw new Error('Missing value for --rate-limit-max.');
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('--rate-limit-max must be a positive number.');
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 === 'cloudflare') {
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('SIGINT', () => void shutdown());
749
- process.on('SIGTERM', () => void shutdown());
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('POST /agents/:agent/:id, GET /sessions/:id, GET /sessions/:id/events');
752
- console.log('Watching .fabricharness for changes; agents/config reload on the next request.');
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('[fabric-harness] Building Cloudflare Worker artifact...');
757
- const result = await buildWorkspace({ cwd: workspaceRoot, target: 'cloudflare' });
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('[fabric-harness] Starting Wrangler dev. Install wrangler/@cloudflare dependencies in the artifact if prompted.');
760
- const args = ['wrangler', 'dev'];
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('--port', String(options.port));
1105
+ args.push("--port", String(options.port));
763
1106
  if (options.host !== undefined)
764
- args.push('--ip', options.host);
765
- const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
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: 'inherit',
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('SIGTERM');
1122
+ child.kill("SIGTERM");
780
1123
  };
781
- process.on('SIGINT', shutdown);
782
- process.on('SIGTERM', shutdown);
1124
+ process.on("SIGINT", shutdown);
1125
+ process.on("SIGTERM", shutdown);
783
1126
  const code = await new Promise((resolve, reject) => {
784
- child.on('error', reject);
785
- child.on('close', resolve);
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, '.fabricharness', 'agents');
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('[fabric-harness] Agent set changed — regenerating Cloudflare entry.');
1154
+ console.error("[fabric-harness] Agent set changed — regenerating Cloudflare entry.");
812
1155
  try {
813
- await buildWorkspace({ cwd: workspaceRoot, target: 'cloudflare' });
814
- console.error('[fabric-harness] Entry regenerated. Wrangler will reload.');
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('[fabric-harness] Rebuild failed:', err instanceof Error ? err.message : err);
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).then((summaries) => {
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
- }).catch(() => undefined);
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, '.fabricharness');
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('.fabricharness', String(filename)) : '.fabricharness';
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: { file: 'sandbox--daytona.md', category: 'sandbox', description: 'Daytona remote sandbox' },
866
- e2b: { file: 'sandbox--e2b.md', category: 'sandbox', description: 'E2B remote sandbox' },
867
- modal: { file: 'sandbox--modal.md', category: 'sandbox', description: 'Modal Labs remote sandbox' },
868
- vercel: { file: 'sandbox--vercel.md', category: 'sandbox', description: 'Vercel Sandbox' },
869
- 'github-mcp': { file: 'mcp--github.md', category: 'mcp', description: 'GitHub MCP server' },
870
- 'mintlify-mcp': { file: 'mcp--mintlify.md', category: 'mcp', description: 'Mintlify hosted docs MCP' },
871
- 'linear-mcp': { file: 'mcp--linear.md', category: 'mcp', description: 'Linear hosted MCP' },
872
- 'slack-mcp': { file: 'mcp--slack.md', category: 'mcp', description: 'Slack MCP (hosted or local bridge)' },
873
- fumadocs: { file: 'kb--fumadocs.md', category: 'kb', description: 'Mount a Fumadocs content tree as a knowledge base' },
874
- postgres: { file: 'data--postgres.md', category: 'data', description: 'Postgres data connector' },
875
- notion: { file: 'data--notion.md', category: 'data', description: 'Notion (hosted MCP or REST adapter)' },
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 = 'https://raw.githubusercontent.com/Fabric-Pro/fabric-harness/main/connectors';
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 === '1' || env.CLAUDECODE === '1' || env.CLAUDE_PROJECT_DIR)
919
- return { name: 'claude', pipeHint: 'claude' };
920
- if (env.CODEX_AGENT === '1' || env.CODEX_HOME)
921
- return { name: 'codex', pipeHint: 'codex' };
922
- if (env.CURSOR_AGENT === '1' || env.CURSOR_TRACE_ID)
923
- return { name: 'cursor-agent', pipeHint: 'cursor-agent' };
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: 'aider', pipeHint: 'aider' };
926
- if (env.OPENCODE === '1')
927
- return { name: 'opencode', pipeHint: 'opencode' };
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: 'your coding agent', pipeHint: 'claude' };
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, 'utf8');
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 === '--print') {
1331
+ if (arg === undefined)
1332
+ continue;
1333
+ if (arg === "--print") {
963
1334
  print = true;
964
1335
  continue;
965
1336
  }
966
- if (arg === '--category') {
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('Available connector recipes:\n');
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('Or build a freeform recipe from a provider URL:');
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('--category is required when passing a URL (sandbox, mcp, kb, or data).');
1005
- if (!['sandbox', 'mcp', 'kb', 'data'].includes(category)) {
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('{{URL}}', connector).replaceAll('{{CATEGORY}}', category);
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(`Or print the markdown directly:`);
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, 'connectors', file);
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), '..', '..', '..', 'connectors', file);
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 === 'attestation' ? await verifyAttestation(target) : await verifyProvenance(target);
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('Missing required <session-id> argument for inspect.');
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
- const [sessionId, ...rest] = args;
1085
- if (!sessionId || sessionId.startsWith('-'))
1086
- throw new Error('Missing required <session-id> argument for replay.');
1087
- if (rest.length > 0)
1088
- throw new Error(`Unknown argument: ${rest[0]}`);
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
- console.log('');
1095
- console.log(`Active path entries: ${replay.activePath.length}`);
1096
- for (const entry of replay.activePath) {
1097
- console.log(`${entry.timestamp} ${entry.type} ${entry.id}${entry.parentId ? ` <- ${entry.parentId}` : ''}`);
1098
- }
1099
- console.log('');
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('Latest compaction: none');
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, ' ').slice(0, 240)}`);
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 === '--target') {
1535
+ if (arg === "--target") {
1123
1536
  const value = args[++index];
1124
- if (value !== 'node' && value !== 'temporal-worker')
1125
- throw new Error('--target for doctor must be node or temporal-worker.');
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 === '--model') {
1542
+ if (arg === "--model") {
1130
1543
  model = args[++index];
1131
1544
  if (!model)
1132
- throw new Error('Missing value for --model.');
1545
+ throw new Error("Missing value for --model.");
1133
1546
  continue;
1134
1547
  }
1135
- if (arg === '--tools') {
1548
+ if (arg === "--tools") {
1136
1549
  checkTools = true;
1137
1550
  continue;
1138
1551
  }
1139
- if (arg === '--live') {
1552
+ if (arg === "--live") {
1140
1553
  live = true;
1141
1554
  continue;
1142
1555
  }
1143
- if (arg === '--json') {
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: 'workspace', ok: true, message: workspaceRoot });
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 ?? 'node';
1156
- checks.push({ name: 'run-target', ok: true, message: configuredTarget });
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({ ...(doctorModel !== undefined ? { model: doctorModel } : {}), ...(config.agent?.providers !== undefined ? { providers: config.agent.providers } : {}) });
1159
- checks.push({ name: 'model', ok: Boolean(resolved.provider), message: resolved.provider ? `${resolved.providerName}/${resolved.modelId}` : 'model disabled' });
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: 'model-warning', ok: true, message: warning });
1583
+ checks.push({ name: "model-warning", ok: true, message: warning });
1162
1584
  if (live && resolved.provider) {
1163
- const response = await resolved.provider.generate({ ...(resolved.model !== undefined ? { model: resolved.model } : {}), messages: [{ role: 'user', content: 'Say exactly: fabric harness doctor ok' }] });
1164
- checks.push({ name: 'model-live', ok: Boolean(response.message?.content || response.toolCalls?.length), message: response.message?.content?.slice(0, 120) ?? `${response.toolCalls?.length ?? 0} tool calls` });
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({ name: 'model', ok: false, message: error instanceof Error ? error.message : String(error) });
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({ workspaceRoot, ...(config.store ? { config: config.store } : {}), env: process.env });
1172
- await store.save({ id: 'doctor-write-test', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), entries: [], events: [] });
1173
- const message = config.store?.backend ? config.store.backend : store instanceof FileSessionStore ? store.sessionsDir : 'default';
1174
- checks.push({ name: 'session-store', ok: true, message });
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({ name: 'session-store', ok: false, message: error instanceof Error ? error.message : String(error) });
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 !== 'object' || typeof input.properties !== 'object' || input.properties === null)
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: 'tools', ok: true, message: `${schemas.length} tool schemas valid` });
1642
+ checks.push({ name: "tools", ok: true, message: `${schemas.length} tool schemas valid` });
1189
1643
  }
1190
1644
  catch (error) {
1191
- checks.push({ name: 'tools', ok: false, message: error instanceof Error ? error.message : String(error) });
1192
- }
1193
- }
1194
- if ((target ?? config.run?.target) === 'temporal-worker') {
1195
- const address = process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233';
1196
- const taskQueue = process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? 'fabric-harness';
1197
- checks.push({ name: 'temporal', ok: true, message: `configured address ${address}, taskQueue ${taskQueue}; start a worker/server for live validation` });
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 ? '' : ''} ${check.name}: ${check.message}`);
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('--json');
1210
- const unknown = args.find((arg) => arg !== '--json');
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('No agents found.');
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 ?? ''}`.trimEnd());
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('Missing required <agent> argument for describe.');
1230
- const jsonOutput = rest.includes('--json');
1231
- const unknown = rest.find((arg) => arg !== '--json');
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('Input:');
1251
- printSchema(description.inputSchema, ' ');
1252
- console.log('');
1253
- console.log('Output:');
1254
- printSchema(description.outputSchema, ' ');
1255
- console.log('');
1256
- console.log('Examples:');
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 === 'object' ? Object.keys(input.properties ?? {})[0] : undefined;
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 ?? 'input'}":"..."}'`);
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 === 'object') {
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) ? 'required' : 'optional';
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 ?? 'unknown').padEnd(8)} ${req}${enumText}${desc}`);
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 ?? 'unknown'}${enumText}${desc}`);
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({ workspaceRoot, ...(config.store ? { config: config.store } : {}), env: process.env });
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('No sessions found.');
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({ workspaceRoot, ...(config.store ? { config: config.store } : {}), env: process.env });
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('No builds found.');
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('Missing required <session-id> argument for approvals.');
1330
- const pendingOnly = rest.includes('--pending');
1331
- const stateView = rest.includes('--state');
1332
- const unknown = rest.find((arg) => arg !== '--pending' && arg !== '--state');
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 === 'pending');
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.map((vote) => `${typeof vote.actor === 'string' ? vote.actor : vote.actor.id}:${vote.decision}`).join(',') || 'none';
1344
- const resolved = state.resolvedAt ? ` resolved=${state.resolvedAt}` : '';
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 === 'pending');
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 ? ` paths=${approval.affectedPaths.join(',')}` : '';
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 = 'cli';
1844
+ let actor = "cli";
1371
1845
  for (let index = 0; index < rest.length; index += 1) {
1372
1846
  const arg = rest[index];
1373
- if (arg === '--reason') {
1847
+ if (arg === "--reason") {
1374
1848
  reason = rest[++index];
1375
1849
  continue;
1376
1850
  }
1377
- if (arg === '--actor') {
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 !== 'approved' && approval.status !== 'denied'))
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: 'temporal',
1396
- address: process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233',
1397
- taskQueue: process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? 'fabric-harness',
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, { approvalId: approval.id, decision, ...(reason ?? approval.resolutionReason ? { reason: reason ?? approval.resolutionReason } : {}) });
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('Missing required <session-id> argument for checkpoints.');
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('Missing required <session-id> argument for compact.');
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 === '--keep') {
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 === '--summary') {
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('Missing required <session-id> argument for logs.');
1463
- const showEvents = rest.includes('--events');
1464
- const unknown = rest.find((arg) => arg !== '--events');
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 ? 'No events.' : 'No structured entries.');
1959
+ console.log(showEvents ? "No events." : "No structured entries.");
1480
1960
  return;
1481
1961
  }
1482
1962
  for (const item of entries) {
1483
- const timestamp = 'timestamp' in item ? item.timestamp : '';
1484
- const type = 'type' in item ? item.type : 'unknown';
1485
- const dataText = 'data' in item && item.data !== undefined ? ` ${formatInline(item.data)}` : '';
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('Missing required <session-id> argument for artifacts.');
1493
- const json = rest.includes('--json');
1494
- const unknown = rest.find((arg) => arg !== '--json');
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 ?? 'application/octet-stream'}`);
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 !== 'get')
1511
- throw new Error('Usage: fabric-harness artifact get <session-id> <artifact-id-or-name> [--out <path>]');
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('Usage: fabric-harness artifact get <session-id> <artifact-id-or-name> [--out <path>]');
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 === '--out') {
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('Missing required <session-id> argument for metrics.');
1540
- const json = rest.includes('--json');
1541
- const unknown = rest.find((arg) => arg !== '--json');
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('Missing required <session-id> argument for tasks.');
1563
- const json = rest.includes('--json');
1564
- const unknown = rest.find((arg) => arg !== '--json');
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('Missing required <session-id> argument for task.');
1587
- if (!taskId || taskId.startsWith('-'))
1588
- throw new Error('Missing required <task-id> argument for task.');
1589
- const json = rest.includes('--json');
1590
- const unknown = rest.find((arg) => arg !== '--json');
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('Checkpoints:');
2110
+ console.log("Checkpoints:");
1618
2111
  for (const checkpoint of task.checkpoints)
1619
- console.log(` ${checkpoint.timestamp} ${checkpoint.phase ?? 'checkpoint'} ${checkpoint.checkpointId ?? checkpoint.entryId}`);
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('Missing required <session-id> argument for cancel-task.');
1626
- if (!taskId || taskId.startsWith('-'))
1627
- throw new Error('Missing required <task-id> argument for cancel-task.');
1628
- let actor = 'cli';
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 === '--actor') {
2125
+ if (arg === "--actor") {
1633
2126
  actor = rest[++index] ?? actor;
1634
2127
  continue;
1635
2128
  }
1636
- if (arg === '--reason') {
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 === '--help' || command === '-h') {
2150
+ if (!command || command === "--help" || command === "-h") {
1658
2151
  console.log(HELP);
1659
2152
  return;
1660
2153
  }
1661
2154
  await loadAutoEnvFiles();
1662
- if (command === 'run')
2155
+ if (command === "run")
1663
2156
  return runCommand(args);
1664
- if (command === 'agents')
2157
+ if (command === "agents")
1665
2158
  return agentsCommand(args);
1666
- if (command === 'describe')
2159
+ if (command === "describe")
1667
2160
  return describeCommand(args);
1668
- if (command === 'build')
2161
+ if (command === "build")
1669
2162
  return buildCommand(args);
1670
- if (command === 'sessions')
2163
+ if (command === "sessions")
1671
2164
  return sessionsCommand(args);
1672
- if (command === 'builds')
2165
+ if (command === "builds")
1673
2166
  return buildsCommand(args);
1674
- if (command === 'verify-attestation')
1675
- return verifyCommand(args, 'attestation');
1676
- if (command === 'verify-provenance')
1677
- return verifyCommand(args, 'provenance');
1678
- if (command === 'doctor')
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 === 'add')
2173
+ if (command === "add")
1681
2174
  return addCommand(args);
1682
- if (command === 'inspect')
2175
+ if (command === "inspect")
1683
2176
  return inspectCommand(args);
1684
- if (command === 'logs')
2177
+ if (command === "logs")
1685
2178
  return logsCommand(args);
1686
- if (command === 'checkpoints')
2179
+ if (command === "checkpoints")
1687
2180
  return checkpointsCommand(args);
1688
- if (command === 'artifacts')
2181
+ if (command === "artifacts")
1689
2182
  return artifactsCommand(args);
1690
- if (command === 'artifact')
2183
+ if (command === "artifact")
1691
2184
  return artifactCommand(args);
1692
- if (command === 'metrics')
2185
+ if (command === "metrics")
1693
2186
  return metricsCommand(args);
1694
- if (command === 'tasks')
2187
+ if (command === "tasks")
1695
2188
  return tasksCommand(args);
1696
- if (command === 'task')
2189
+ if (command === "task")
1697
2190
  return taskCommand(args);
1698
- if (command === 'cancel-task')
2191
+ if (command === "cancel-task")
1699
2192
  return cancelTaskCommand(args);
1700
- if (command === 'compact')
2193
+ if (command === "compact")
1701
2194
  return compactCommand(args);
1702
- if (command === 'replay')
2195
+ if (command === "replay")
1703
2196
  return replayCommand(args);
1704
- if (command === 'approvals')
2197
+ if (command === "approvals")
1705
2198
  return approvalsCommand(args);
1706
- if (command === 'approve')
1707
- return resolveApprovalCommand(args, 'approved');
1708
- if (command === 'reject')
1709
- return resolveApprovalCommand(args, 'denied');
1710
- if (command === 'dev')
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 === 'temporal-worker')
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 ? `Invalid payload or result:\n${error.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n')}` : error instanceof Error ? error.message : String(error);
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('Run fabric-harness --help for usage.');
2221
+ console.error("Run fabric-harness --help for usage.");
1723
2222
  process.exit(1);
1724
2223
  }
1725
2224
  //# sourceMappingURL=fabric-harness.js.map