@versatly/workgraph 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,17 +1,22 @@
1
1
  import {
2
+ agent_exports,
3
+ autonomy_daemon_exports,
2
4
  bases_exports,
3
5
  board_exports,
4
6
  clawdapus_exports,
5
7
  command_center_exports,
8
+ diagnostics_exports,
6
9
  integration_exports,
7
10
  onboard_exports,
8
11
  search_qmd_adapter_exports,
9
12
  skill_exports,
10
13
  trigger_exports,
11
14
  workspace_exports
12
- } from "./chunk-E3QU5Y53.js";
15
+ } from "./chunk-OJ6KOGB2.js";
13
16
  import {
17
+ autonomy_exports,
14
18
  dispatch_exports,
19
+ gate_exports,
15
20
  graph_exports,
16
21
  ledger_exports,
17
22
  mcp_server_exports,
@@ -20,11 +25,14 @@ import {
20
25
  query_exports,
21
26
  registry_exports,
22
27
  store_exports,
23
- thread_exports
24
- } from "./chunk-65ZMX2WM.js";
28
+ thread_audit_exports,
29
+ thread_exports,
30
+ trigger_engine_exports
31
+ } from "./chunk-R2MLGBHB.js";
25
32
 
26
33
  // src/cli.ts
27
34
  import fs from "fs";
35
+ import os from "os";
28
36
  import path from "path";
29
37
  import { Command } from "commander";
30
38
  var DEFAULT_ACTOR = process.env.WORKGRAPH_AGENT || process.env.USER || "anonymous";
@@ -46,7 +54,7 @@ addWorkspaceOption(
46
54
  (targetPath, opts) => runCommand(
47
55
  opts,
48
56
  () => {
49
- const workspacePath = path.resolve(targetPath || resolveWorkspacePath(opts));
57
+ const workspacePath = resolveInitTargetPath(targetPath, opts);
50
58
  const result = workspace_exports.initWorkspace(workspacePath, {
51
59
  name: opts.name,
52
60
  createTypeDirs: opts.typeDirs,
@@ -168,13 +176,17 @@ addWorkspaceOption(
168
176
  )
169
177
  );
170
178
  addWorkspaceOption(
171
- threadCmd.command("claim <threadPath>").description("Claim a thread for this agent").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
179
+ threadCmd.command("claim <threadPath>").description("Claim a thread for this agent").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--lease-ttl-minutes <n>", "Claim lease TTL in minutes", "30").option("--json", "Emit structured JSON output")
172
180
  ).action(
173
181
  (threadPath, opts) => runCommand(
174
182
  opts,
175
183
  () => {
176
184
  const workspacePath = resolveWorkspacePath(opts);
177
- return { thread: thread_exports.claim(workspacePath, threadPath, opts.actor) };
185
+ return {
186
+ thread: thread_exports.claim(workspacePath, threadPath, opts.actor, {
187
+ leaseTtlMinutes: Number.parseFloat(String(opts.leaseTtlMinutes))
188
+ })
189
+ };
178
190
  },
179
191
  (result) => [`Claimed: ${result.thread.path}`, `Owner: ${String(result.thread.fields.owner)}`]
180
192
  )
@@ -203,6 +215,18 @@ addWorkspaceOption(
203
215
  (result) => [`Done: ${result.thread.path}`]
204
216
  )
205
217
  );
218
+ addWorkspaceOption(
219
+ threadCmd.command("reopen <threadPath>").description("Reopen a done/cancelled thread via compensating ledger op").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--reason <reason>", "Why the thread is being reopened").option("--json", "Emit structured JSON output")
220
+ ).action(
221
+ (threadPath, opts) => runCommand(
222
+ opts,
223
+ () => {
224
+ const workspacePath = resolveWorkspacePath(opts);
225
+ return { thread: thread_exports.reopen(workspacePath, threadPath, opts.actor, opts.reason) };
226
+ },
227
+ (result) => [`Reopened: ${result.thread.path}`, `Status: ${String(result.thread.fields.status)}`]
228
+ )
229
+ );
206
230
  addWorkspaceOption(
207
231
  threadCmd.command("block <threadPath>").description("Mark a thread blocked").requiredOption("-b, --blocked-by <dep>", "Dependency blocking this thread").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--reason <reason>", "Why it is blocked").option("--json", "Emit structured JSON output")
208
232
  ).action(
@@ -229,6 +253,63 @@ addWorkspaceOption(
229
253
  (result) => [`Unblocked: ${result.thread.path}`]
230
254
  )
231
255
  );
256
+ addWorkspaceOption(
257
+ threadCmd.command("heartbeat [threadPath]").description("Refresh thread claim lease heartbeat (one thread or all active claims for actor)").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--ttl-minutes <n>", "Lease TTL in minutes", "30").option("--json", "Emit structured JSON output")
258
+ ).action(
259
+ (threadPath, opts) => runCommand(
260
+ opts,
261
+ () => {
262
+ const workspacePath = resolveWorkspacePath(opts);
263
+ return thread_exports.heartbeatClaim(
264
+ workspacePath,
265
+ opts.actor,
266
+ threadPath,
267
+ {
268
+ ttlMinutes: Number.parseFloat(String(opts.ttlMinutes))
269
+ }
270
+ );
271
+ },
272
+ (result) => [
273
+ `Heartbeat actor: ${result.actor}`,
274
+ `Touched leases: ${result.touched.length}`,
275
+ ...result.touched.length > 0 ? result.touched.map((entry) => `- ${entry.threadPath} expires=${entry.expiresAt}`) : [],
276
+ ...result.skipped.length > 0 ? result.skipped.map((entry) => `SKIP ${entry.threadPath}: ${entry.reason}`) : []
277
+ ]
278
+ )
279
+ );
280
+ addWorkspaceOption(
281
+ threadCmd.command("reap-stale").description("Reopen/release stale claimed threads whose leases expired").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--limit <n>", "Max stale leases to reap this run").option("--json", "Emit structured JSON output")
282
+ ).action(
283
+ (opts) => runCommand(
284
+ opts,
285
+ () => {
286
+ const workspacePath = resolveWorkspacePath(opts);
287
+ return thread_exports.reapStaleClaims(workspacePath, opts.actor, {
288
+ limit: opts.limit ? Number.parseInt(String(opts.limit), 10) : void 0
289
+ });
290
+ },
291
+ (result) => [
292
+ `Reaper actor: ${result.actor}`,
293
+ `Scanned stale leases: ${result.scanned}`,
294
+ `Reaped: ${result.reaped.length}`,
295
+ ...result.reaped.length > 0 ? result.reaped.map((entry) => `- ${entry.threadPath} (prev=${entry.previousOwner})`) : [],
296
+ ...result.skipped.length > 0 ? result.skipped.map((entry) => `SKIP ${entry.threadPath}: ${entry.reason}`) : []
297
+ ]
298
+ )
299
+ );
300
+ addWorkspaceOption(
301
+ threadCmd.command("leases").description("List claim leases and staleness state").option("--json", "Emit structured JSON output")
302
+ ).action(
303
+ (opts) => runCommand(
304
+ opts,
305
+ () => {
306
+ const workspacePath = resolveWorkspacePath(opts);
307
+ const leases = thread_exports.listClaimLeaseStatus(workspacePath);
308
+ return { leases, count: leases.length };
309
+ },
310
+ (result) => result.leases.map((lease) => `${lease.stale ? "STALE" : "LIVE"} ${lease.owner} -> ${lease.target} expires=${lease.expiresAt}`)
311
+ )
312
+ );
232
313
  addWorkspaceOption(
233
314
  threadCmd.command("decompose <threadPath>").description("Break a thread into sub-threads").requiredOption("--sub <specs...>", 'Sub-thread specs as "title|goal"').option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
234
315
  ).action(
@@ -246,6 +327,58 @@ addWorkspaceOption(
246
327
  (result) => [`Created ${result.children.length} sub-thread(s).`]
247
328
  )
248
329
  );
330
+ var agentCmd = program.command("agent").description("Track agent presence heartbeats");
331
+ addWorkspaceOption(
332
+ agentCmd.command("heartbeat <name>").description("Create/update an agent presence heartbeat").option("-a, --actor <name>", "Actor writing the heartbeat", DEFAULT_ACTOR).option("--status <status>", "online | busy | offline", "online").option("--current-task <threadRef>", "Current task/thread slug for this agent").option("--capabilities <items>", "Comma-separated capability tags").option("--json", "Emit structured JSON output")
333
+ ).action(
334
+ (name, opts) => runCommand(
335
+ opts,
336
+ () => {
337
+ const workspacePath = resolveWorkspacePath(opts);
338
+ return {
339
+ presence: agent_exports.heartbeat(workspacePath, name, {
340
+ actor: opts.actor,
341
+ status: normalizeAgentPresenceStatus(opts.status),
342
+ currentTask: opts.currentTask,
343
+ capabilities: csv(opts.capabilities)
344
+ })
345
+ };
346
+ },
347
+ (result) => [
348
+ `Heartbeat: ${String(result.presence.fields.name)} [${String(result.presence.fields.status)}]`,
349
+ `Last seen: ${String(result.presence.fields.last_seen)}`,
350
+ `Current task: ${String(result.presence.fields.current_task ?? "none")}`
351
+ ]
352
+ )
353
+ );
354
+ addWorkspaceOption(
355
+ agentCmd.command("list").description("List known agent presence entries").option("--json", "Emit structured JSON output")
356
+ ).action(
357
+ (opts) => runCommand(
358
+ opts,
359
+ () => {
360
+ const workspacePath = resolveWorkspacePath(opts);
361
+ const agents = agent_exports.list(workspacePath);
362
+ return {
363
+ agents,
364
+ count: agents.length
365
+ };
366
+ },
367
+ (result) => {
368
+ if (result.agents.length === 0) return ["No agent presence entries found."];
369
+ return [
370
+ ...result.agents.map((entry) => {
371
+ const name = String(entry.fields.name ?? entry.path);
372
+ const status = String(entry.fields.status ?? "unknown");
373
+ const task = String(entry.fields.current_task ?? "none");
374
+ const lastSeen = String(entry.fields.last_seen ?? "unknown");
375
+ return `${name} [${status}] task=${task} last_seen=${lastSeen}`;
376
+ }),
377
+ `${result.count} agent(s)`
378
+ ];
379
+ }
380
+ )
381
+ );
249
382
  var primitiveCmd = program.command("primitive").description("Manage primitive type definitions and instances");
250
383
  addWorkspaceOption(
251
384
  primitiveCmd.command("define <name>").description("Define a new primitive type").requiredOption("-d, --description <desc>", "Type description").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--fields <specs...>", 'Field definitions as "name:type"').option("--dir <directory>", "Storage directory override").option("--json", "Emit structured JSON output")
@@ -283,6 +416,8 @@ addWorkspaceOption(
283
416
  ]
284
417
  )
285
418
  );
419
+ registerPrimitiveSchemaCommand("schema", "Show supported fields for a primitive type");
420
+ registerPrimitiveSchemaCommand("fields", "Alias for schema");
286
421
  var basesCmd = program.command("bases").description("Generate Obsidian .base files from primitive-registry.yaml");
287
422
  addWorkspaceOption(
288
423
  basesCmd.command("sync-registry").description("Sync .workgraph/primitive-registry.yaml from active registry").option("--json", "Emit structured JSON output")
@@ -337,6 +472,46 @@ addWorkspaceOption(
337
472
  (result) => result.types.map((t) => `${t.name} (${t.directory}/) ${t.builtIn ? "[built-in]" : ""}`)
338
473
  )
339
474
  );
475
+ function registerPrimitiveSchemaCommand(commandName, description) {
476
+ addWorkspaceOption(
477
+ primitiveCmd.command(`${commandName} <typeName>`).description(description).option("--json", "Emit structured JSON output")
478
+ ).action(
479
+ (typeName, opts) => runCommand(
480
+ opts,
481
+ () => {
482
+ const workspacePath = resolveWorkspacePath(opts);
483
+ const typeDef = registry_exports.getType(workspacePath, typeName);
484
+ if (!typeDef) {
485
+ throw new Error(`Unknown primitive type "${typeName}". Use \`workgraph primitive list\` to inspect available types.`);
486
+ }
487
+ const fields = Object.entries(typeDef.fields).map(([name, definition]) => ({
488
+ name,
489
+ type: definition.type,
490
+ required: definition.required === true,
491
+ default: definition.default,
492
+ enum: definition.enum ?? [],
493
+ description: definition.description ?? "",
494
+ template: definition.template ?? void 0,
495
+ pattern: definition.pattern ?? void 0,
496
+ refTypes: definition.refTypes ?? []
497
+ }));
498
+ return {
499
+ type: typeDef.name,
500
+ description: typeDef.description,
501
+ directory: typeDef.directory,
502
+ builtIn: typeDef.builtIn,
503
+ fields
504
+ };
505
+ },
506
+ (result) => [
507
+ `Type: ${result.type}`,
508
+ `Directory: ${result.directory}/`,
509
+ `Built-in: ${result.builtIn}`,
510
+ ...result.fields.map((field) => `- ${field.name}: ${field.type}${field.required ? " (required)" : ""}${field.description ? ` \u2014 ${field.description}` : ""}`)
511
+ ]
512
+ )
513
+ );
514
+ }
340
515
  addWorkspaceOption(
341
516
  primitiveCmd.command("create <type> <title>").description("Create an instance of any primitive type").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--set <fields...>", 'Set fields as "key=value"').option("--body <text>", "Markdown body content", "").option("--json", "Emit structured JSON output")
342
517
  ).action(
@@ -353,7 +528,7 @@ addWorkspaceOption(
353
528
  )
354
529
  );
355
530
  addWorkspaceOption(
356
- primitiveCmd.command("update <path>").description("Update an existing primitive instance").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--set <fields...>", 'Set fields as "key=value"').option("--body <text>", "Replace markdown body content").option("--body-file <path>", "Read markdown body content from file").option("--json", "Emit structured JSON output")
531
+ primitiveCmd.command("update <path>").description("Update an existing primitive instance").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--set <fields...>", 'Set fields as "key=value"').option("--etag <etag>", "Expected etag for optimistic concurrency").option("--body <text>", "Replace markdown body content").option("--body-file <path>", "Read markdown body content from file").option("--json", "Emit structured JSON output")
357
532
  ).action(
358
533
  (targetPath, opts) => runCommand(
359
534
  opts,
@@ -365,7 +540,9 @@ addWorkspaceOption(
365
540
  body = fs.readFileSync(path.resolve(opts.bodyFile), "utf-8");
366
541
  }
367
542
  return {
368
- instance: store_exports.update(workspacePath, targetPath, updates, body, opts.actor)
543
+ instance: store_exports.update(workspacePath, targetPath, updates, body, opts.actor, {
544
+ expectedEtag: opts.etag
545
+ })
369
546
  };
370
547
  },
371
548
  (result) => [`Updated ${result.instance.type}: ${result.instance.path}`]
@@ -653,6 +830,26 @@ addWorkspaceOption(
653
830
  ]
654
831
  )
655
832
  );
833
+ addWorkspaceOption(
834
+ ledgerCmd.command("reconcile").description("Audit thread files against ledger claims, leases, and dependency wiring").option("--fail-on-issues", "Exit non-zero when issues are found").option("--json", "Emit structured JSON output")
835
+ ).action(
836
+ (opts) => runCommand(
837
+ opts,
838
+ () => {
839
+ const workspacePath = resolveWorkspacePath(opts);
840
+ const report = thread_audit_exports.reconcileThreadState(workspacePath);
841
+ if (opts.failOnIssues && !report.ok) {
842
+ throw new Error(`Ledger reconcile found ${report.issues.length} issue(s).`);
843
+ }
844
+ return report;
845
+ },
846
+ (result) => [
847
+ `Reconcile ok: ${result.ok}`,
848
+ `Threads: ${result.totalThreads} Claims: ${result.totalClaims} Leases: ${result.totalLeases}`,
849
+ ...result.issues.length > 0 ? result.issues.map((issue) => `${issue.kind}: ${issue.path} \u2014 ${issue.message}`) : ["No reconcile issues found."]
850
+ ]
851
+ )
852
+ );
656
853
  addWorkspaceOption(
657
854
  ledgerCmd.command("seal").description("Rebuild ledger index + hash-chain state from ledger.jsonl").option("--json", "Emit structured JSON output")
658
855
  ).action(
@@ -674,6 +871,96 @@ addWorkspaceOption(
674
871
  ]
675
872
  )
676
873
  );
874
+ addWorkspaceOption(
875
+ program.command("doctor").description("Diagnose vault health, warnings, and repairable issues").option("--fix", "Auto-repair safe issues (orphan links, stale claims/runs)").option("--stale-after-minutes <n>", "Threshold for stale claims/runs in minutes", "60").option("-a, --actor <name>", "Actor used for --fix mutations", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
876
+ ).action(
877
+ (opts) => runCommand(
878
+ opts,
879
+ () => {
880
+ const workspacePath = resolveWorkspacePath(opts);
881
+ const staleAfterMinutes = Number.parseInt(String(opts.staleAfterMinutes), 10);
882
+ const safeStaleAfterMinutes = Number.isNaN(staleAfterMinutes) ? 60 : Math.max(1, staleAfterMinutes);
883
+ return diagnostics_exports.diagnoseVaultHealth(workspacePath, {
884
+ fix: !!opts.fix,
885
+ actor: opts.actor,
886
+ staleAfterMs: safeStaleAfterMinutes * 60 * 1e3
887
+ });
888
+ },
889
+ (result) => diagnostics_exports.renderDoctorReport(result)
890
+ )
891
+ );
892
+ addWorkspaceOption(
893
+ program.command("replay").description("Replay ledger events chronologically with typed filters").option("--type <type>", "create | update | transition").option("--actor <name>", "Filter by actor").option("--primitive <ref>", "Filter by primitive path/type substring").option("--since <iso>", "Filter events on/after ISO timestamp").option("--until <iso>", "Filter events on/before ISO timestamp").option("--no-color", "Disable colorized output").option("--json", "Emit structured JSON output")
894
+ ).action(
895
+ (opts) => runCommand(
896
+ opts,
897
+ () => {
898
+ const workspacePath = resolveWorkspacePath(opts);
899
+ return diagnostics_exports.replayLedger(workspacePath, {
900
+ type: opts.type,
901
+ actor: opts.actor,
902
+ primitive: opts.primitive,
903
+ since: opts.since,
904
+ until: opts.until
905
+ });
906
+ },
907
+ (result) => diagnostics_exports.renderReplayText(result, {
908
+ color: opts.color !== false && !wantsJson(opts)
909
+ })
910
+ )
911
+ );
912
+ addWorkspaceOption(
913
+ program.command("viz").description("Render an ASCII wiki-link graph of primitives in this vault").option("--focus <slugOrPath>", "Center the graph on a specific node").option("--depth <n>", "Traversal depth from each root", "2").option("--top <n>", "When large, show top N most-connected roots", "10").option("--no-color", "Disable colorized output").option("--json", "Emit structured JSON output")
914
+ ).action(
915
+ (opts) => runCommand(
916
+ opts,
917
+ () => {
918
+ const workspacePath = resolveWorkspacePath(opts);
919
+ const parsedDepth = Number.parseInt(String(opts.depth), 10);
920
+ const parsedTop = Number.parseInt(String(opts.top), 10);
921
+ return diagnostics_exports.visualizeVaultGraph(workspacePath, {
922
+ focus: opts.focus,
923
+ depth: Number.isNaN(parsedDepth) ? 2 : Math.max(1, parsedDepth),
924
+ top: Number.isNaN(parsedTop) ? 10 : Math.max(1, parsedTop),
925
+ color: opts.color !== false && !wantsJson(opts)
926
+ });
927
+ },
928
+ (result) => [
929
+ ...result.rendered.split("\n"),
930
+ "",
931
+ `Nodes: ${result.nodeCount}`,
932
+ `Edges: ${result.edgeCount}`,
933
+ ...result.focus ? [`Focus: ${result.focus}`] : []
934
+ ]
935
+ )
936
+ );
937
+ addWorkspaceOption(
938
+ program.command("stats").description("Show detailed vault statistics and graph/ledger health metrics").option("--json", "Emit structured JSON output")
939
+ ).action(
940
+ (opts) => runCommand(
941
+ opts,
942
+ () => {
943
+ const workspacePath = resolveWorkspacePath(opts);
944
+ return diagnostics_exports.computeVaultStats(workspacePath);
945
+ },
946
+ (result) => diagnostics_exports.renderStatsReport(result)
947
+ )
948
+ );
949
+ addWorkspaceOption(
950
+ program.command("changelog").description("Generate a human-readable changelog from ledger events").requiredOption("--since <date>", "Include entries on/after this date (ISO-8601)").option("--until <date>", "Include entries on/before this date (ISO-8601)").option("--json", "Emit structured JSON output")
951
+ ).action(
952
+ (opts) => runCommand(
953
+ opts,
954
+ () => {
955
+ const workspacePath = resolveWorkspacePath(opts);
956
+ return diagnostics_exports.generateLedgerChangelog(workspacePath, {
957
+ since: opts.since,
958
+ until: opts.until
959
+ });
960
+ },
961
+ (result) => diagnostics_exports.renderChangelogText(result)
962
+ )
963
+ );
677
964
  addWorkspaceOption(
678
965
  program.command("command-center").description("Generate a markdown command center from workgraph state").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("-o, --output <path>", "Output markdown path", "Command Center.md").option("-n, --recent <count>", "Recent ledger entries to include", "15").option("--json", "Emit structured JSON output")
679
966
  ).action(
@@ -886,6 +1173,114 @@ addWorkspaceOption(
886
1173
  ]
887
1174
  )
888
1175
  );
1176
+ addWorkspaceOption(
1177
+ graphCmd.command("neighborhood <slug>").description("Find connected primitives within N wiki-link hops").option("--depth <n>", "Traversal depth (default: 2)", "2").option("--refresh", "Refresh graph index before querying").option("--json", "Emit structured JSON output")
1178
+ ).action(
1179
+ (slug, opts) => runCommand(
1180
+ opts,
1181
+ () => {
1182
+ const workspacePath = resolveWorkspacePath(opts);
1183
+ return graph_exports.graphNeighborhoodQuery(workspacePath, slug, {
1184
+ depth: parseNonNegativeIntOption(opts.depth, "depth"),
1185
+ refresh: !!opts.refresh
1186
+ });
1187
+ },
1188
+ (result) => [
1189
+ `Center: ${result.center.path} (${result.center.exists ? "exists" : "missing"})`,
1190
+ `Depth: ${result.depth}`,
1191
+ `Connected nodes: ${result.connectedNodes.length}`,
1192
+ `Edges in neighborhood: ${result.edges.length}`
1193
+ ]
1194
+ )
1195
+ );
1196
+ addWorkspaceOption(
1197
+ graphCmd.command("impact <slug>").description("Analyze reverse-link impact for a primitive").option("--refresh", "Refresh graph index before querying").option("--json", "Emit structured JSON output")
1198
+ ).action(
1199
+ (slug, opts) => runCommand(
1200
+ opts,
1201
+ () => {
1202
+ const workspacePath = resolveWorkspacePath(opts);
1203
+ return graph_exports.graphImpactAnalysis(workspacePath, slug, {
1204
+ refresh: !!opts.refresh
1205
+ });
1206
+ },
1207
+ (result) => [
1208
+ `Target: ${result.target.path} (${result.target.exists ? "exists" : "missing"})`,
1209
+ `Total references: ${result.totalReferences}`,
1210
+ ...result.groups.map((group) => `${group.type}: ${group.referenceCount}`)
1211
+ ]
1212
+ )
1213
+ );
1214
+ addWorkspaceOption(
1215
+ graphCmd.command("context <slug>").description("Assemble token-budgeted markdown context from graph neighborhood").option("--budget <tokens>", "Approx token budget (chars/4)", "2000").option("--refresh", "Refresh graph index before querying").option("--json", "Emit structured JSON output")
1216
+ ).action(
1217
+ (slug, opts) => runCommand(
1218
+ opts,
1219
+ () => {
1220
+ const workspacePath = resolveWorkspacePath(opts);
1221
+ return graph_exports.graphContextAssembly(workspacePath, slug, {
1222
+ budgetTokens: parsePositiveIntOption(opts.budget, "budget"),
1223
+ refresh: !!opts.refresh
1224
+ });
1225
+ },
1226
+ (result) => [
1227
+ `Center: ${result.center.path}`,
1228
+ `Budget: ${result.budgetTokens} tokens`,
1229
+ `Used: ${result.usedTokens} tokens`,
1230
+ `Sections: ${result.sections.length}`,
1231
+ "",
1232
+ result.markdown
1233
+ ]
1234
+ )
1235
+ );
1236
+ addWorkspaceOption(
1237
+ graphCmd.command("edges <slug>").description("Show typed incoming/outgoing edges for one primitive").option("--refresh", "Refresh graph index before querying").option("--json", "Emit structured JSON output")
1238
+ ).action(
1239
+ (slug, opts) => runCommand(
1240
+ opts,
1241
+ () => {
1242
+ const workspacePath = resolveWorkspacePath(opts);
1243
+ return graph_exports.graphTypedEdges(workspacePath, slug, {
1244
+ refresh: !!opts.refresh
1245
+ });
1246
+ },
1247
+ (result) => [
1248
+ `Node: ${result.node.path} (${result.node.exists ? "exists" : "missing"})`,
1249
+ `Outgoing edges: ${result.outgoing.length}`,
1250
+ `Incoming edges: ${result.incoming.length}`,
1251
+ ...result.outgoing.map((edge) => `OUT ${edge.type} ${edge.from} -> ${edge.to}`),
1252
+ ...result.incoming.map((edge) => `IN ${edge.type} ${edge.from} -> ${edge.to}`)
1253
+ ]
1254
+ )
1255
+ );
1256
+ addWorkspaceOption(
1257
+ graphCmd.command("export <slug>").description("Export a markdown subgraph directory around a center primitive").option("--depth <n>", "Traversal depth (default: 2)", "2").option("--format <format>", "Export format (default: md)", "md").option("--output-dir <path>", "Output directory (default under .workgraph/graph-exports)").option("--refresh", "Refresh graph index before querying").option("--json", "Emit structured JSON output")
1258
+ ).action(
1259
+ (slug, opts) => runCommand(
1260
+ opts,
1261
+ () => {
1262
+ const workspacePath = resolveWorkspacePath(opts);
1263
+ const format = String(opts.format ?? "md").trim().toLowerCase();
1264
+ if (format !== "md") {
1265
+ throw new Error(`Invalid --format "${opts.format}". Supported formats: md.`);
1266
+ }
1267
+ return graph_exports.graphExportSubgraph(workspacePath, slug, {
1268
+ depth: parseNonNegativeIntOption(opts.depth, "depth"),
1269
+ format,
1270
+ outputDir: opts.outputDir,
1271
+ refresh: !!opts.refresh
1272
+ });
1273
+ },
1274
+ (result) => [
1275
+ `Exported subgraph: ${result.outputDirectory}`,
1276
+ `Center: ${result.center.path}`,
1277
+ `Depth: ${result.depth}`,
1278
+ `Nodes: ${result.exportedNodes.length}`,
1279
+ `Edges: ${result.exportedEdgeCount}`,
1280
+ `Manifest: ${result.manifestPath}`
1281
+ ]
1282
+ )
1283
+ );
889
1284
  addWorkspaceOption(
890
1285
  graphCmd.command("neighbors <nodePath>").description("Query incoming/outgoing wiki-link neighbors for one node").option("--refresh", "Refresh graph index before querying").option("--json", "Emit structured JSON output")
891
1286
  ).action(
@@ -952,6 +1347,33 @@ addWorkspaceOption(
952
1347
  (result) => result.parties.map((party) => `${party.id} [${party.roles.join(", ")}]`)
953
1348
  )
954
1349
  );
1350
+ var gateCmd = program.command("gate").description("Evaluate thread quality gates before claim");
1351
+ addWorkspaceOption(
1352
+ gateCmd.command("check <threadRef>").description("Check policy-gate status for one thread").option("--json", "Emit structured JSON output")
1353
+ ).action(
1354
+ (threadRef, opts) => runCommand(
1355
+ opts,
1356
+ () => {
1357
+ const workspacePath = resolveWorkspacePath(opts);
1358
+ return gate_exports.checkThreadGates(workspacePath, threadRef);
1359
+ },
1360
+ (result) => {
1361
+ const header = [`Gate check for ${result.threadPath}: ${result.allowed ? "PASSED" : "FAILED"}`];
1362
+ if (result.gates.length === 0) {
1363
+ return [...header, "No gates configured."];
1364
+ }
1365
+ const details = result.gates.map((gate) => {
1366
+ const failingRules = gate.rules.filter((rule) => !rule.ok);
1367
+ const gateLabel = gate.gatePath ?? gate.gateRef;
1368
+ if (failingRules.length === 0) {
1369
+ return `[pass] ${gateLabel}`;
1370
+ }
1371
+ return `[fail] ${gateLabel} :: ${failingRules.map((rule) => rule.message).join("; ")}`;
1372
+ });
1373
+ return [...header, ...details];
1374
+ }
1375
+ )
1376
+ );
955
1377
  var dispatchCmd = program.command("dispatch").description("Programmatic runtime dispatch contract");
956
1378
  addWorkspaceOption(
957
1379
  dispatchCmd.command("create <objective>").description("Create a new run dispatch request").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--adapter <name>", "Adapter name", "cursor-cloud").option("--idempotency-key <key>", "Idempotency key").option("--json", "Emit structured JSON output")
@@ -972,6 +1394,21 @@ addWorkspaceOption(
972
1394
  (result) => [`Run created: ${result.run.id} [${result.run.status}]`]
973
1395
  )
974
1396
  );
1397
+ addWorkspaceOption(
1398
+ dispatchCmd.command("claim <threadRef>").description("Claim a thread after passing quality gates").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
1399
+ ).action(
1400
+ (threadRef, opts) => runCommand(
1401
+ opts,
1402
+ () => {
1403
+ const workspacePath = resolveWorkspacePath(opts);
1404
+ return dispatch_exports.claimThread(workspacePath, threadRef, opts.actor);
1405
+ },
1406
+ (result) => [
1407
+ `Claimed thread: ${result.thread.path}`,
1408
+ `Gates checked: ${result.gateCheck.gates.length}`
1409
+ ]
1410
+ )
1411
+ );
975
1412
  addWorkspaceOption(
976
1413
  dispatchCmd.command("create-execute <objective>").description("Create and execute a run with autonomous multi-agent coordination").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--adapter <name>", "Adapter name", "cursor-cloud").option("--idempotency-key <key>", "Idempotency key").option("--agents <actors>", "Comma-separated agent identities for autonomous execution").option("--max-steps <n>", "Maximum scheduler steps", "200").option("--step-delay-ms <ms>", "Delay between scheduling steps", "25").option("--space <spaceRef>", "Restrict execution to one space").option("--no-checkpoint", "Skip automatic checkpoint generation after execution").option("--json", "Emit structured JSON output")
977
1414
  ).action(
@@ -1089,6 +1526,65 @@ addWorkspaceOption(
1089
1526
  (result) => [`Stopped run: ${result.run.id} [${result.run.status}]`]
1090
1527
  )
1091
1528
  );
1529
+ addWorkspaceOption(
1530
+ dispatchCmd.command("heartbeat <runId>").description("Heartbeat a running run lease and extend lease_expiry").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--lease-minutes <n>", "Lease extension in minutes").option("--json", "Emit structured JSON output")
1531
+ ).action(
1532
+ (runId, opts) => runCommand(
1533
+ opts,
1534
+ () => {
1535
+ const workspacePath = resolveWorkspacePath(opts);
1536
+ return {
1537
+ run: dispatch_exports.heartbeat(workspacePath, runId, {
1538
+ actor: opts.actor,
1539
+ leaseMinutes: opts.leaseMinutes ? Number.parseInt(String(opts.leaseMinutes), 10) : void 0
1540
+ })
1541
+ };
1542
+ },
1543
+ (result) => [
1544
+ `Heartbeated run: ${result.run.id}`,
1545
+ `Lease expires: ${String(result.run.leaseExpires ?? "none")}`,
1546
+ `Heartbeats: ${(result.run.heartbeats ?? []).length}`
1547
+ ]
1548
+ )
1549
+ );
1550
+ addWorkspaceOption(
1551
+ dispatchCmd.command("reconcile").description("Requeue runs with expired leases").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
1552
+ ).action(
1553
+ (opts) => runCommand(
1554
+ opts,
1555
+ () => {
1556
+ const workspacePath = resolveWorkspacePath(opts);
1557
+ return dispatch_exports.reconcileExpiredLeases(workspacePath, opts.actor);
1558
+ },
1559
+ (result) => [
1560
+ `Reconciled at: ${result.reconciledAt}`,
1561
+ `Inspected runs: ${result.inspectedRuns}`,
1562
+ `Requeued runs: ${result.requeuedRuns.length}`,
1563
+ ...result.requeuedRuns.map((run) => `- ${run.id}`)
1564
+ ]
1565
+ )
1566
+ );
1567
+ addWorkspaceOption(
1568
+ dispatchCmd.command("handoff <runId>").description("Create a structured run handoff to another agent").requiredOption("--to <agent>", "Target agent").requiredOption("--reason <text>", "Reason for handoff").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--adapter <name>", "Adapter override for handoff run").option("--json", "Emit structured JSON output")
1569
+ ).action(
1570
+ (runId, opts) => runCommand(
1571
+ opts,
1572
+ () => {
1573
+ const workspacePath = resolveWorkspacePath(opts);
1574
+ return dispatch_exports.handoffRun(workspacePath, runId, {
1575
+ actor: opts.actor,
1576
+ to: opts.to,
1577
+ reason: opts.reason,
1578
+ adapter: opts.adapter
1579
+ });
1580
+ },
1581
+ (result) => [
1582
+ `Handoff created: ${result.handoffRun.id} (from ${result.sourceRun.id})`,
1583
+ `Target agent: ${result.handoffRun.actor}`,
1584
+ `Objective: ${result.handoffRun.objective}`
1585
+ ]
1586
+ )
1587
+ );
1092
1588
  addWorkspaceOption(
1093
1589
  dispatchCmd.command("mark <runId>").description("Set run status transition explicitly").requiredOption("--status <status>", "running|succeeded|failed|cancelled").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--output <text>", "Optional output payload").option("--error <text>", "Optional error payload").option("--json", "Emit structured JSON output")
1094
1590
  ).action(
@@ -1142,6 +1638,28 @@ addWorkspaceOption(
1142
1638
  ]
1143
1639
  )
1144
1640
  );
1641
+ var triggerEngineCmd = triggerCmd.command("engine").description("Run trigger engine");
1642
+ addWorkspaceOption(
1643
+ triggerEngineCmd.command("run").description("Process trigger events once or continuously").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--watch", "Continuously process new events").option("--poll-ms <ms>", "Poll interval in watch mode", "2000").option("--max-cycles <n>", "Maximum cycles before exiting").option("--entry-limit <n>", "Maximum ledger entries per cycle").option("--agents <actors>", "Comma-separated agents used when executing runs").option("--max-steps <n>", "Maximum adapter scheduler steps", "200").option("--step-delay-ms <ms>", "Adapter scheduler delay", "25").option("--space <spaceRef>", "Restrict run execution to one space").option("--stale-claim-minutes <m>", "Drift warning threshold for stale claims", "30").option("--no-execute-runs", "Do not execute dispatched runs").option("--json", "Emit structured JSON output")
1644
+ ).action(
1645
+ (opts) => runCommand(
1646
+ opts,
1647
+ () => {
1648
+ const workspacePath = resolveWorkspacePath(opts);
1649
+ return trigger_engine_exports.runTriggerEngineCycle(workspacePath, {
1650
+ actor: opts.actor
1651
+ });
1652
+ },
1653
+ (result) => [
1654
+ `Evaluated: ${result.evaluated} triggers`,
1655
+ `Fired: ${result.fired}`,
1656
+ `Errors: ${result.errors}`,
1657
+ ...result.triggers.map(
1658
+ (t) => ` ${t.triggerPath}: ${t.fired ? "FIRED" : "skipped"} (${t.reason})${t.error ? ` error: ${t.error}` : ""}`
1659
+ )
1660
+ ]
1661
+ )
1662
+ );
1145
1663
  addWorkspaceOption(
1146
1664
  program.command("onboard").description("Guided agent-first workspace setup and starter artifacts").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--spaces <list>", "Comma-separated space names").option("--no-demo-threads", "Skip starter onboarding threads").option("--json", "Emit structured JSON output")
1147
1665
  ).action(
@@ -1204,12 +1722,124 @@ addWorkspaceOption(
1204
1722
  (result) => [`Updated onboarding: ${result.onboarding.path} [${String(result.onboarding.fields.status)}]`]
1205
1723
  )
1206
1724
  );
1725
+ var autonomyCmd = program.command("autonomy").description("Run long-lived autonomous collaboration loops");
1726
+ addWorkspaceOption(
1727
+ autonomyCmd.command("run").description("Run autonomy cycles (trigger engine + ready-thread execution)").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--adapter <name>", "Dispatch adapter name", "cursor-cloud").option("--agents <actors>", "Comma-separated autonomous worker identities").option("--watch", "Run continuously instead of stopping when idle").option("--poll-ms <ms>", "Cycle poll interval", "2000").option("--max-cycles <n>", "Maximum cycles before exit").option("--max-idle-cycles <n>", "Idle cycles before exit in non-watch mode", "2").option("--max-steps <n>", "Maximum adapter scheduler steps", "200").option("--step-delay-ms <ms>", "Adapter scheduler delay", "25").option("--space <spaceRef>", "Restrict autonomy to one space").option("--stale-claim-minutes <m>", "Drift warning threshold", "30").option("--heartbeat-file <path>", "Write daemon heartbeat JSON to this path").option("--no-execute-triggers", "Disable trigger engine actions").option("--no-execute-ready-threads", "Disable ready-thread dispatch execution").option("--json", "Emit structured JSON output")
1728
+ ).action(
1729
+ (opts) => runCommand(
1730
+ opts,
1731
+ async () => {
1732
+ const workspacePath = resolveWorkspacePath(opts);
1733
+ return autonomy_exports.runAutonomyLoop(workspacePath, {
1734
+ actor: opts.actor,
1735
+ adapter: opts.adapter,
1736
+ agents: csv(opts.agents),
1737
+ watch: !!opts.watch,
1738
+ pollMs: Number.parseInt(String(opts.pollMs), 10),
1739
+ maxCycles: opts.maxCycles ? Number.parseInt(String(opts.maxCycles), 10) : void 0,
1740
+ maxIdleCycles: Number.parseInt(String(opts.maxIdleCycles), 10),
1741
+ maxSteps: Number.parseInt(String(opts.maxSteps), 10),
1742
+ stepDelayMs: Number.parseInt(String(opts.stepDelayMs), 10),
1743
+ space: opts.space,
1744
+ staleClaimMinutes: Number.parseInt(String(opts.staleClaimMinutes), 10),
1745
+ heartbeatFile: opts.heartbeatFile,
1746
+ executeTriggers: opts.executeTriggers,
1747
+ executeReadyThreads: opts.executeReadyThreads
1748
+ });
1749
+ },
1750
+ (result) => [
1751
+ `Cycles: ${result.cycles.length}`,
1752
+ `Final ready threads: ${result.finalReadyThreads}`,
1753
+ `Final drift status: ${result.finalDriftOk ? "ok" : "issues"}`,
1754
+ ...result.cycles.map(
1755
+ (cycle) => `Cycle ${cycle.cycle}: ready=${cycle.readyThreads} trigger_actions=${cycle.triggerActions} run=${cycle.runStatus ?? "none"} drift_issues=${cycle.driftIssues}`
1756
+ )
1757
+ ]
1758
+ )
1759
+ );
1760
+ var autonomyDaemonCmd = autonomyCmd.command("daemon").description("Manage autonomy process lifecycle (pid + heartbeat + logs)");
1761
+ addWorkspaceOption(
1762
+ autonomyDaemonCmd.command("start").description("Start autonomy in detached daemon mode").option("-a, --actor <name>", "Actor", DEFAULT_ACTOR).option("--adapter <name>", "Dispatch adapter name", "cursor-cloud").option("--agents <actors>", "Comma-separated autonomous worker identities").option("--poll-ms <ms>", "Cycle poll interval", "2000").option("--max-cycles <n>", "Maximum cycles before daemon exits").option("--max-steps <n>", "Maximum adapter scheduler steps", "200").option("--step-delay-ms <ms>", "Adapter scheduler delay", "25").option("--space <spaceRef>", "Restrict autonomy to one space").option("--stale-claim-minutes <m>", "Drift warning threshold", "30").option("--log-path <path>", "Daemon log file path (workspace-relative)").option("--heartbeat-path <path>", "Heartbeat file path (workspace-relative)").option("--no-execute-triggers", "Disable trigger engine actions").option("--no-execute-ready-threads", "Disable ready-thread dispatch execution").option("--json", "Emit structured JSON output")
1763
+ ).action(
1764
+ (opts) => runCommand(
1765
+ opts,
1766
+ () => {
1767
+ const workspacePath = resolveWorkspacePath(opts);
1768
+ return autonomy_daemon_exports.startAutonomyDaemon(workspacePath, {
1769
+ cliEntrypointPath: process.argv[1] ?? path.resolve("bin/workgraph.js"),
1770
+ actor: opts.actor,
1771
+ adapter: opts.adapter,
1772
+ agents: csv(opts.agents),
1773
+ pollMs: Number.parseInt(String(opts.pollMs), 10),
1774
+ maxCycles: opts.maxCycles ? Number.parseInt(String(opts.maxCycles), 10) : void 0,
1775
+ maxSteps: Number.parseInt(String(opts.maxSteps), 10),
1776
+ stepDelayMs: Number.parseInt(String(opts.stepDelayMs), 10),
1777
+ space: opts.space,
1778
+ staleClaimMinutes: Number.parseInt(String(opts.staleClaimMinutes), 10),
1779
+ logPath: opts.logPath,
1780
+ heartbeatPath: opts.heartbeatPath,
1781
+ executeTriggers: opts.executeTriggers,
1782
+ executeReadyThreads: opts.executeReadyThreads
1783
+ });
1784
+ },
1785
+ (result) => [
1786
+ `Daemon running: ${result.running}`,
1787
+ ...result.pid ? [`PID: ${result.pid}`] : [],
1788
+ `PID file: ${result.pidPath}`,
1789
+ `Heartbeat: ${result.heartbeatPath}`,
1790
+ `Log: ${result.logPath}`
1791
+ ]
1792
+ )
1793
+ );
1794
+ addWorkspaceOption(
1795
+ autonomyDaemonCmd.command("status").description("Show autonomy daemon status").option("--json", "Emit structured JSON output")
1796
+ ).action(
1797
+ (opts) => runCommand(
1798
+ opts,
1799
+ () => {
1800
+ const workspacePath = resolveWorkspacePath(opts);
1801
+ return autonomy_daemon_exports.readAutonomyDaemonStatus(workspacePath);
1802
+ },
1803
+ (result) => [
1804
+ `Daemon running: ${result.running}`,
1805
+ ...result.pid ? [`PID: ${result.pid}`] : [],
1806
+ ...result.heartbeat ? [`Last heartbeat: ${result.heartbeat.ts}`] : ["Last heartbeat: none"],
1807
+ `PID file: ${result.pidPath}`,
1808
+ `Heartbeat: ${result.heartbeatPath}`,
1809
+ `Log: ${result.logPath}`
1810
+ ]
1811
+ )
1812
+ );
1813
+ addWorkspaceOption(
1814
+ autonomyDaemonCmd.command("stop").description("Stop autonomy daemon by PID").option("--signal <signal>", "Signal for graceful stop", "SIGTERM").option("--timeout-ms <ms>", "Graceful wait timeout", "5000").option("--json", "Emit structured JSON output")
1815
+ ).action(
1816
+ (opts) => runCommand(
1817
+ opts,
1818
+ async () => {
1819
+ const workspacePath = resolveWorkspacePath(opts);
1820
+ return autonomy_daemon_exports.stopAutonomyDaemon(workspacePath, {
1821
+ signal: String(opts.signal),
1822
+ timeoutMs: Number.parseInt(String(opts.timeoutMs), 10)
1823
+ });
1824
+ },
1825
+ (result) => [
1826
+ `Stopped: ${result.stopped}`,
1827
+ `Previously running: ${result.previouslyRunning}`,
1828
+ ...result.pid ? [`PID: ${result.pid}`] : [],
1829
+ `Daemon running now: ${result.status.running}`
1830
+ ]
1831
+ )
1832
+ );
1207
1833
  var mcpCmd = program.command("mcp").description("Run Workgraph MCP server");
1208
1834
  addWorkspaceOption(
1209
- mcpCmd.command("serve").description("Serve stdio MCP tools/resources for this workspace").option("-a, --actor <name>", "Default actor for MCP write tools", DEFAULT_ACTOR).option("--read-only", "Disable all MCP write tools")
1835
+ mcpCmd.command("serve").description("Serve stdio MCP tools/resources for this workspace").option("-a, --actor <name>", "Default actor for MCP write tools", DEFAULT_ACTOR).option("--read-only", "Disable all MCP write tools").option("--sse-port <port>", "Optional SSE event stream port").option("--sse-host <host>", "SSE bind host (default: 127.0.0.1)").option("--sse-path <path>", "SSE endpoint path (default: /events)").option("--sse-poll-ms <ms>", "Ledger poll interval for SSE stream (default: 250ms)").option("--sse-heartbeat-ms <ms>", "SSE heartbeat interval (default: 15000ms)")
1210
1836
  ).action(async (opts) => {
1211
1837
  const workspacePath = resolveWorkspacePath(opts);
1212
1838
  console.error(`Starting MCP server for workspace: ${workspacePath}`);
1839
+ const ssePort = opts.ssePort !== void 0 ? Number.parseInt(String(opts.ssePort), 10) : void 0;
1840
+ const ssePollMs = opts.ssePollMs !== void 0 ? Number.parseInt(String(opts.ssePollMs), 10) : void 0;
1841
+ const sseHeartbeatMs = opts.sseHeartbeatMs !== void 0 ? Number.parseInt(String(opts.sseHeartbeatMs), 10) : void 0;
1842
+ const sseEnabled = ssePort !== void 0 || opts.sseHost !== void 0 || opts.ssePath !== void 0;
1213
1843
  await mcp_server_exports.startWorkgraphMcpServer({
1214
1844
  workspacePath,
1215
1845
  defaultActor: opts.actor,
@@ -1218,15 +1848,51 @@ addWorkspaceOption(
1218
1848
  });
1219
1849
  await program.parseAsync();
1220
1850
  function addWorkspaceOption(command) {
1221
- return command.option("-w, --workspace <path>", "Workgraph workspace path").option("--vault <path>", "Alias for --workspace").option("--shared-vault <path>", "Shared vault path (e.g. mounted via Tailscale)");
1851
+ return command.option("-w, --workspace <path>", "Workgraph workspace path").option("--vault <path>", "Alias for --workspace").option("--shared-vault <path>", "Shared vault path (e.g. mounted via Tailscale)").option("--dry-run", "Execute against a temporary workspace copy and discard changes");
1222
1852
  }
1223
1853
  function resolveWorkspacePath(opts) {
1854
+ const originalWorkspacePath = resolveWorkspacePathBase(opts);
1855
+ if (!opts.dryRun) return originalWorkspacePath;
1856
+ if (opts.__dryRunWorkspace) return opts.__dryRunWorkspace;
1857
+ const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), "workgraph-dry-run-"));
1858
+ const sandboxWorkspace = path.join(sandboxRoot, "workspace");
1859
+ if (fs.existsSync(originalWorkspacePath)) {
1860
+ fs.cpSync(originalWorkspacePath, sandboxWorkspace, {
1861
+ recursive: true,
1862
+ force: true
1863
+ });
1864
+ } else {
1865
+ fs.mkdirSync(sandboxWorkspace, { recursive: true });
1866
+ }
1867
+ opts.__dryRunWorkspaceRoot = sandboxRoot;
1868
+ opts.__dryRunWorkspace = sandboxWorkspace;
1869
+ opts.__dryRunOriginal = originalWorkspacePath;
1870
+ return sandboxWorkspace;
1871
+ }
1872
+ function resolveWorkspacePathBase(opts) {
1224
1873
  const explicit = opts.workspace || opts.vault || opts.sharedVault;
1225
1874
  if (explicit) return path.resolve(explicit);
1226
1875
  if (process.env.WORKGRAPH_SHARED_VAULT) return path.resolve(process.env.WORKGRAPH_SHARED_VAULT);
1227
1876
  if (process.env.WORKGRAPH_PATH) return path.resolve(process.env.WORKGRAPH_PATH);
1228
1877
  return process.cwd();
1229
1878
  }
1879
+ function resolveInitTargetPath(targetPath, opts) {
1880
+ const requestedPath = path.resolve(targetPath || resolveWorkspacePathBase(opts));
1881
+ if (!opts.dryRun) return requestedPath;
1882
+ if (opts.__dryRunWorkspace) return opts.__dryRunWorkspace;
1883
+ const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), "workgraph-init-dry-run-"));
1884
+ const sandboxWorkspace = path.join(sandboxRoot, path.basename(requestedPath));
1885
+ if (fs.existsSync(requestedPath)) {
1886
+ fs.cpSync(requestedPath, sandboxWorkspace, {
1887
+ recursive: true,
1888
+ force: true
1889
+ });
1890
+ }
1891
+ opts.__dryRunWorkspaceRoot = sandboxRoot;
1892
+ opts.__dryRunWorkspace = sandboxWorkspace;
1893
+ opts.__dryRunOriginal = requestedPath;
1894
+ return sandboxWorkspace;
1895
+ }
1230
1896
  function parseSetPairs(pairs) {
1231
1897
  const fields = {};
1232
1898
  for (const pair of pairs) {
@@ -1279,6 +1945,20 @@ function parseScalar(value) {
1279
1945
  return value;
1280
1946
  }
1281
1947
  }
1948
+ function parsePositiveIntOption(value, name) {
1949
+ const parsed = Number.parseInt(String(value ?? ""), 10);
1950
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1951
+ throw new Error(`Invalid --${name} value "${String(value)}". Expected a positive integer.`);
1952
+ }
1953
+ return parsed;
1954
+ }
1955
+ function parseNonNegativeIntOption(value, name) {
1956
+ const parsed = Number.parseInt(String(value ?? ""), 10);
1957
+ if (!Number.isFinite(parsed) || parsed < 0) {
1958
+ throw new Error(`Invalid --${name} value "${String(value)}". Expected a non-negative integer.`);
1959
+ }
1960
+ return parsed;
1961
+ }
1282
1962
  function normalizeRunStatus(status) {
1283
1963
  const normalized = String(status).toLowerCase();
1284
1964
  if (normalized === "running" || normalized === "succeeded" || normalized === "failed" || normalized === "cancelled") {
@@ -1286,6 +1966,13 @@ function normalizeRunStatus(status) {
1286
1966
  }
1287
1967
  throw new Error(`Invalid run status "${status}". Expected running|succeeded|failed|cancelled.`);
1288
1968
  }
1969
+ function normalizeAgentPresenceStatus(status) {
1970
+ const normalized = String(status).toLowerCase();
1971
+ if (normalized === "online" || normalized === "busy" || normalized === "offline") {
1972
+ return normalized;
1973
+ }
1974
+ throw new Error(`Invalid agent status "${status}". Expected online|busy|offline.`);
1975
+ }
1289
1976
  function normalizeOnboardingStatus(status) {
1290
1977
  const normalized = String(status).toLowerCase();
1291
1978
  if (normalized === "active" || normalized === "paused" || normalized === "completed") {
@@ -1301,19 +1988,45 @@ function wantsJson(opts) {
1301
1988
  async function runCommand(opts, action, renderText) {
1302
1989
  try {
1303
1990
  const result = await action();
1991
+ const dryRunMetadata = opts.dryRun ? {
1992
+ dryRun: true,
1993
+ targetWorkspace: opts.__dryRunOriginal ?? resolveWorkspacePathBase(opts),
1994
+ sandboxWorkspace: opts.__dryRunWorkspace
1995
+ } : {};
1304
1996
  if (wantsJson(opts)) {
1305
- console.log(JSON.stringify({ ok: true, data: result }, null, 2));
1997
+ console.log(JSON.stringify({ ok: true, ...dryRunMetadata, data: result }, null, 2));
1306
1998
  return;
1307
1999
  }
2000
+ if (opts.dryRun) {
2001
+ console.log(
2002
+ [
2003
+ "[dry-run] Executed against sandbox workspace and discarded changes.",
2004
+ `Target: ${opts.__dryRunOriginal ?? resolveWorkspacePathBase(opts)}`,
2005
+ `Sandbox: ${opts.__dryRunWorkspace ?? "n/a"}`
2006
+ ].join(" ")
2007
+ );
2008
+ }
1308
2009
  const lines = renderText(result);
1309
2010
  for (const line of lines) console.log(line);
1310
2011
  } catch (error) {
1311
2012
  const message = error instanceof Error ? error.message : String(error);
1312
2013
  if (wantsJson(opts)) {
1313
- console.error(JSON.stringify({ ok: false, error: message }, null, 2));
2014
+ console.error(JSON.stringify({ ok: false, dryRun: !!opts.dryRun, error: message }, null, 2));
1314
2015
  } else {
1315
2016
  console.error(`Error: ${message}`);
1316
2017
  }
2018
+ cleanupDryRunSandbox(opts);
1317
2019
  process.exit(1);
2020
+ } finally {
2021
+ cleanupDryRunSandbox(opts);
2022
+ }
2023
+ }
2024
+ function cleanupDryRunSandbox(opts) {
2025
+ if (!opts.dryRun || !opts.__dryRunWorkspaceRoot) return;
2026
+ if (fs.existsSync(opts.__dryRunWorkspaceRoot)) {
2027
+ fs.rmSync(opts.__dryRunWorkspaceRoot, { recursive: true, force: true });
1318
2028
  }
2029
+ delete opts.__dryRunWorkspaceRoot;
2030
+ delete opts.__dryRunWorkspace;
2031
+ delete opts.__dryRunOriginal;
1319
2032
  }