@linkedclaw/cli 0.1.3 → 0.1.5

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/bin.js CHANGED
@@ -7,50 +7,218 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/bin.ts
9
9
  import { Command } from "commander";
10
+ import { readFileSync as readFileSync8 } from "fs";
11
+ import { fileURLToPath } from "url";
12
+ import { dirname as dirname4, join as join6 } from "path";
10
13
 
11
- // src/config.ts
12
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
13
- import { homedir } from "os";
14
- import { join, dirname } from "path";
15
- import { load as yamlLoad, dump as yamlDump } from "js-yaml";
16
- var DEFAULT_CLOUD_URL = "https://api.linkedclaw.com";
17
- var DEFAULT_RELAY_URL = "wss://api.linkedclaw.com/ws";
18
- function configDir() {
19
- return process.env["LINKEDCLAW_CONFIG_DIR"] ?? join(homedir(), ".linkedclaw");
14
+ // src/commands/agent.ts
15
+ import { spawn } from "child_process";
16
+ import { accessSync, constants, statSync } from "fs";
17
+ import path from "path";
18
+ function registerAgentCommands(program2) {
19
+ const agent = program2.command("agent").description("Owner-agent runtime commands");
20
+ agent.command("run").description("Run a long-lived owner agent from a local config").option("--config <path>", "Owner-agent config path").option("--watch <debate_id:commons_log_id>", "Watch a debate Commons Log; repeatable", collect, []).option("--once", "Process pending local tasks once, then exit").option("--python-command <cmd>", "Python executable to use").action((opts) => runOwnerAgent(opts));
21
+ agent.command("rotate-mandate").description("Issue a replacement owner-agent mandate, update local config, then revoke the old one").option("--config <path>", "Owner-agent config path").option("--old-mandate-id <id>", "Mandate id to replace; defaults to the configured transport mandate").option("--expires-at <iso>", "Replacement mandate expiry timestamp").option("--python-command <cmd>", "Python executable to use").action((opts) => rotateOwnerAgentMandate(opts));
20
22
  }
21
- function configPath() {
22
- return join(configDir(), "config.yaml");
23
+ function collect(value, previous) {
24
+ return [...previous, value];
23
25
  }
24
- function readFileConfig(path = configPath()) {
25
- if (!existsSync(path)) return {};
26
- const raw = readFileSync(path, "utf8");
27
- const parsed = yamlLoad(raw);
28
- if (parsed === null || parsed === void 0) return {};
29
- if (typeof parsed !== "object") {
30
- throw new Error(`config file ${path} is not a YAML object`);
26
+ function runOwnerAgent(opts) {
27
+ let pythonCommand;
28
+ let watches;
29
+ let configPath2;
30
+ try {
31
+ pythonCommand = resolvePythonCommand(opts.pythonCommand ?? process.env.LINKEDCLAW_OWNER_AGENT_PYTHON ?? "python3");
32
+ watches = (opts.watch ?? []).map(validateWatch);
33
+ const rawConfigPath = opts.config ?? process.env.LINKEDCLAW_OWNER_AGENT_CONFIG;
34
+ configPath2 = rawConfigPath === void 0 ? void 0 : validateConfigPath(rawConfigPath);
35
+ } catch (err) {
36
+ process.stderr.write(
37
+ JSON.stringify({
38
+ error: "invalid_agent_run_option",
39
+ message: err instanceof Error ? err.message : String(err)
40
+ }) + "\n"
41
+ );
42
+ process.exitCode = 1;
43
+ return;
31
44
  }
32
- return parsed;
45
+ const args = ["-m", "linkedclaw.owner_agent.cli", "run"];
46
+ if (configPath2 !== void 0) {
47
+ args.push("--config", configPath2);
48
+ }
49
+ for (const watch of watches) {
50
+ args.push("--watch", watch);
51
+ }
52
+ if (opts.once) {
53
+ args.push("--once");
54
+ }
55
+ spawnOwnerAgentPython(args, pythonCommand);
33
56
  }
34
- function writeFileConfig(cfg, path = configPath()) {
35
- const dir = dirname(path);
36
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
37
- writeFileSync(path, yamlDump(cfg), { mode: 384 });
38
- if (process.platform !== "win32") chmodSync(path, 384);
57
+ function rotateOwnerAgentMandate(opts) {
58
+ let pythonCommand;
59
+ let configPath2;
60
+ try {
61
+ pythonCommand = resolvePythonCommand(opts.pythonCommand ?? process.env.LINKEDCLAW_OWNER_AGENT_PYTHON ?? "python3");
62
+ const rawConfigPath = opts.config ?? process.env.LINKEDCLAW_OWNER_AGENT_CONFIG;
63
+ if (rawConfigPath === void 0) {
64
+ throw new Error("Config path required; pass --config or set LINKEDCLAW_OWNER_AGENT_CONFIG");
65
+ }
66
+ configPath2 = validateConfigPath(rawConfigPath);
67
+ if (opts.oldMandateId !== void 0) {
68
+ validateNonEmptyNoNul(opts.oldMandateId, "--old-mandate-id");
69
+ }
70
+ if (opts.expiresAt !== void 0) {
71
+ validateNonEmptyNoNul(opts.expiresAt, "--expires-at");
72
+ }
73
+ } catch (err) {
74
+ process.stderr.write(
75
+ JSON.stringify({
76
+ error: "invalid_agent_rotate_mandate_option",
77
+ message: err instanceof Error ? err.message : String(err)
78
+ }) + "\n"
79
+ );
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ const args = [
84
+ "-m",
85
+ "linkedclaw.owner_agent.cli",
86
+ "rotate-mandate",
87
+ "--config",
88
+ configPath2
89
+ ];
90
+ if (opts.oldMandateId !== void 0) {
91
+ args.push("--old-mandate-id", opts.oldMandateId);
92
+ }
93
+ if (opts.expiresAt !== void 0) {
94
+ args.push("--expires-at", opts.expiresAt);
95
+ }
96
+ spawnOwnerAgentPython(args, pythonCommand);
39
97
  }
40
- function resolveConfig(overrides = {}) {
41
- const env = process.env;
42
- const file = readFileConfig();
43
- const cloudUrl = overrides.cloudUrl ?? env["LINKEDCLAW_CLOUD_URL"] ?? file.cloudUrl ?? DEFAULT_CLOUD_URL;
44
- const relayUrl = overrides.relayUrl ?? env["LINKEDCLAW_RELAY_URL"] ?? file.relayUrl ?? DEFAULT_RELAY_URL;
45
- const apiKey = overrides.apiKey ?? env["LINKEDCLAW_API_KEY"] ?? file.apiKey;
46
- return {
47
- ...file,
48
- ...overrides,
49
- cloudUrl,
50
- relayUrl,
51
- ...apiKey !== void 0 ? { apiKey } : {}
98
+ function spawnOwnerAgentPython(args, pythonCommand) {
99
+ const child = spawn(pythonCommand, args, { stdio: "inherit" });
100
+ const forwardSignal = (signal) => {
101
+ if (!child.killed) {
102
+ child.kill(signal);
103
+ }
52
104
  };
105
+ const onSigterm = () => forwardSignal("SIGTERM");
106
+ const onSigint = () => forwardSignal("SIGINT");
107
+ const cleanupSignals = () => {
108
+ process.off("SIGTERM", onSigterm);
109
+ process.off("SIGINT", onSigint);
110
+ };
111
+ process.on("SIGTERM", onSigterm);
112
+ process.on("SIGINT", onSigint);
113
+ child.on("error", (err) => {
114
+ cleanupSignals();
115
+ process.stderr.write(
116
+ JSON.stringify({ error: "owner_agent_python_unavailable", message: err.message }) + "\n"
117
+ );
118
+ process.exitCode = 1;
119
+ });
120
+ child.on("exit", (code, signal) => {
121
+ cleanupSignals();
122
+ process.exitCode = signal ? 1 : code ?? 1;
123
+ });
124
+ }
125
+ function validateNonEmptyNoNul(value, label) {
126
+ if (value.length === 0 || value.trim().length === 0) {
127
+ throw new Error(`${label} must be non-empty`);
128
+ }
129
+ if (value.includes("\0")) {
130
+ throw new Error(`${label} must not contain NUL bytes`);
131
+ }
132
+ }
133
+ function validateWatch(value) {
134
+ const separator = value.indexOf(":");
135
+ if (separator <= 0 || separator === value.length - 1 || value.indexOf(":", separator + 1) !== -1) {
136
+ throw new Error("--watch must use debate_id:commons_log_id with both ids non-empty");
137
+ }
138
+ const debateId = value.slice(0, separator).trim();
139
+ const commonsLogId = value.slice(separator + 1).trim();
140
+ if (!debateId || !commonsLogId) {
141
+ throw new Error("--watch must use debate_id:commons_log_id with both ids non-empty");
142
+ }
143
+ return `${debateId}:${commonsLogId}`;
144
+ }
145
+ function validateConfigPath(value) {
146
+ if (value.length === 0 || value.trim().length === 0) {
147
+ throw new Error("Config path must be a non-empty file path");
148
+ }
149
+ if (value.includes("\0")) {
150
+ throw new Error("Config path must not contain NUL bytes");
151
+ }
152
+ try {
153
+ if (!statSync(value).isFile()) {
154
+ throw new Error(`Config path is not a regular file: ${value}`);
155
+ }
156
+ } catch (err) {
157
+ if (err instanceof Error && err.message.startsWith("Config path is not a regular file:")) {
158
+ throw err;
159
+ }
160
+ throw new Error(`Config path does not exist or cannot be read: ${value}`);
161
+ }
162
+ return value;
163
+ }
164
+ function resolvePythonCommand(command) {
165
+ const trimmed = command.trim();
166
+ if (!trimmed) {
167
+ throw new Error("Python command must be a non-empty executable name or absolute path");
168
+ }
169
+ if (trimmed.includes("\0") || /[\s;&|<>`$\n\r]/.test(trimmed)) {
170
+ throw new Error("Python command must be an executable only; arguments and shell metacharacters are rejected");
171
+ }
172
+ const isAbsolute3 = path.isAbsolute(trimmed) || path.win32.isAbsolute(trimmed);
173
+ const hasPathSeparator = trimmed.includes("/") || trimmed.includes("\\");
174
+ if (hasPathSeparator && !isAbsolute3) {
175
+ throw new Error("Python command path must be absolute");
176
+ }
177
+ if (isAbsolute3) {
178
+ assertExecutable(trimmed);
179
+ return trimmed;
180
+ }
181
+ if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) {
182
+ throw new Error("Python command must be a bare executable name or absolute path");
183
+ }
184
+ const resolved = findExecutable(trimmed);
185
+ if (resolved === null) {
186
+ throw new Error(`Python command not found on PATH: ${trimmed}`);
187
+ }
188
+ return resolved;
189
+ }
190
+ function findExecutable(command) {
191
+ const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
192
+ const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
193
+ for (const dir of pathEntries) {
194
+ for (const ext of extensions) {
195
+ const candidate = path.join(dir, command.toLowerCase().endsWith(ext.toLowerCase()) ? command : `${command}${ext}`);
196
+ if (isExecutable(candidate)) {
197
+ return candidate;
198
+ }
199
+ }
200
+ }
201
+ return null;
53
202
  }
203
+ function assertExecutable(file) {
204
+ if (!isExecutable(file)) {
205
+ throw new Error(`Python command is not an executable file: ${file}`);
206
+ }
207
+ }
208
+ function isExecutable(file) {
209
+ try {
210
+ if (!statSync(file).isFile()) {
211
+ return false;
212
+ }
213
+ accessSync(file, constants.X_OK);
214
+ return true;
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+
220
+ // src/commands/arena.ts
221
+ import { readFileSync as readFileSync3 } from "fs";
54
222
 
55
223
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
56
224
  var external_exports = {};
@@ -530,8 +698,8 @@ function getErrorMap() {
530
698
 
531
699
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
532
700
  var makeIssue = (params) => {
533
- const { data, path, errorMaps, issueData } = params;
534
- const fullPath = [...path, ...issueData.path || []];
701
+ const { data, path: path2, errorMaps, issueData } = params;
702
+ const fullPath = [...path2, ...issueData.path || []];
535
703
  const fullIssue = {
536
704
  ...issueData,
537
705
  path: fullPath
@@ -647,11 +815,11 @@ var errorUtil;
647
815
 
648
816
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
649
817
  var ParseInputLazyPath = class {
650
- constructor(parent, value, path, key) {
818
+ constructor(parent, value, path2, key) {
651
819
  this._cachedPath = [];
652
820
  this.parent = parent;
653
821
  this.data = value;
654
- this._path = path;
822
+ this._path = path2;
655
823
  this._key = key;
656
824
  }
657
825
  get path() {
@@ -4411,8 +4579,8 @@ var ConsumerClient = class {
4411
4579
  this.apiKey = apiKey;
4412
4580
  this.fetchImpl = options?.fetch ?? fetch;
4413
4581
  }
4414
- async request(path, init, schema) {
4415
- const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
4582
+ async request(path2, init, schema) {
4583
+ const res = await this.fetchImpl(`${this.baseUrl}${path2}`, {
4416
4584
  ...init,
4417
4585
  headers: {
4418
4586
  "Content-Type": "application/json",
@@ -4758,8 +4926,50 @@ var ConsumerClient = class {
4758
4926
  }
4759
4927
  };
4760
4928
 
4929
+ // ../../sdk/consumer-ts/dist/capabilitySchema.js
4930
+ var CapabilitySchemaError = class extends Error {
4931
+ constructor(message) {
4932
+ super(message);
4933
+ this.name = "CapabilitySchemaError";
4934
+ }
4935
+ };
4936
+ async function fetchCapabilitySchema(listing, capability, options) {
4937
+ const meta = listing.capabilities_meta?.[capability];
4938
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
4939
+ throw new CapabilitySchemaError(`no capabilities_meta entry for ${JSON.stringify(capability)}`);
4940
+ }
4941
+ const entry = meta;
4942
+ const schemaUrl = entry["schema_url"];
4943
+ if (!schemaUrl || typeof schemaUrl !== "string") {
4944
+ throw new CapabilitySchemaError(`no schema_url for capability ${JSON.stringify(capability)}`);
4945
+ }
4946
+ const schemaDigest = entry["schema_digest"];
4947
+ const fetchFn = options?.fetch ?? globalThis.fetch;
4948
+ const response = await fetchFn(schemaUrl);
4949
+ if (!response.ok) {
4950
+ throw new CapabilitySchemaError(`failed to fetch schema for ${JSON.stringify(capability)}: HTTP ${response.status}`);
4951
+ }
4952
+ const buffer = await response.arrayBuffer();
4953
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
4954
+ const actual = "sha256:" + Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
4955
+ if (actual !== schemaDigest) {
4956
+ throw new CapabilitySchemaError(`schema digest mismatch for ${JSON.stringify(capability)}: expected ${String(schemaDigest)}, got ${actual}`);
4957
+ }
4958
+ const text = new TextDecoder().decode(buffer);
4959
+ let parsed;
4960
+ try {
4961
+ parsed = JSON.parse(text);
4962
+ } catch {
4963
+ throw new CapabilitySchemaError(`schema body is not valid JSON for ${JSON.stringify(capability)}`);
4964
+ }
4965
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4966
+ throw new CapabilitySchemaError(`schema body is not a JSON object for ${JSON.stringify(capability)}`);
4967
+ }
4968
+ return parsed;
4969
+ }
4970
+
4761
4971
  // ../../sdk/consumer-ts/dist/index.js
4762
- var DEFAULT_RELAY_URL2 = "wss://api.linkedclaw.com/ws";
4972
+ var DEFAULT_RELAY_URL = "wss://api.linkedclaw.com/ws";
4763
4973
 
4764
4974
  // ../../sdk/provider-ts/dist/models.js
4765
4975
  var SessionSchema2 = external_exports.object({
@@ -5014,8 +5224,8 @@ var ProviderClient = class {
5014
5224
  this.apiKey = apiKey;
5015
5225
  this.fetchImpl = options?.fetch ?? fetch;
5016
5226
  }
5017
- async request(path, init, schema) {
5018
- const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
5227
+ async request(path2, init, schema) {
5228
+ const res = await this.fetchImpl(`${this.baseUrl}${path2}`, {
5019
5229
  ...init,
5020
5230
  headers: {
5021
5231
  "Content-Type": "application/json",
@@ -5417,12 +5627,18 @@ function strOrEmpty(frame, key) {
5417
5627
 
5418
5628
  // ../../sdk/consumer-runtime-ts/dist/index.js
5419
5629
  import WebSocket2 from "ws";
5420
- var ACP_TIMEOUT_MS = 3e4;
5630
+ var ACP_CONNECT_TIMEOUT_MS = 2e3;
5631
+ var SESSION_ACCEPT_TIMEOUT_MS = 3e4;
5421
5632
  var SessionRejectedError = class extends Error {
5422
5633
  constructor(reason) {
5423
5634
  super(`session rejected: ${reason}`);
5424
5635
  }
5425
5636
  };
5637
+ var TransportMissError = class extends Error {
5638
+ constructor(reason) {
5639
+ super(`transport miss: ${reason}`);
5640
+ }
5641
+ };
5426
5642
  var RequesterFlows = class {
5427
5643
  constructor(client) {
5428
5644
  this.client = client;
@@ -5432,8 +5648,11 @@ var RequesterFlows = class {
5432
5648
  return this.client.discover({ capability, ...extra });
5433
5649
  }
5434
5650
  /**
5435
- * Open a session. Performs HTTP create → ACP WS handshake (SESSION_CREATE/ACCEPT) → HTTP activate.
5436
- * Default 30s wait for SESSION_ACCEPT.
5651
+ * Open a session. HTTP create → WS handshake (SESSION_CREATE/ACCEPT) → done.
5652
+ *
5653
+ * Default transport is /ws — native providers register there
5654
+ * (`SkillConfig.try_acp=False` upstream default). Set `tryAcp: true` to try
5655
+ * /acp first; on opt-in, falls back to /ws when the recipient isn't on /acp.
5437
5656
  */
5438
5657
  async hire(params) {
5439
5658
  const session = await this.client.createSession({
@@ -5443,43 +5662,80 @@ var RequesterFlows = class {
5443
5662
  ...params.referredBy !== void 0 ? { referred_by: params.referredBy } : {}
5444
5663
  });
5445
5664
  if (params.autoActivate === false) return { session, activated: false };
5446
- const relayUrl = params.relayUrl ?? DEFAULT_RELAY_URL2;
5447
- const acpUrl = relayUrl.replace(/\/ws$/, "/acp");
5448
- const ws = new WebSocket2(acpUrl);
5665
+ const relayUrl = params.relayUrl ?? DEFAULT_RELAY_URL;
5666
+ try {
5667
+ if (params.tryAcp) {
5668
+ const acpUrl = relayUrl.replace(/\/ws$/, "/acp");
5669
+ try {
5670
+ await this.attemptHandshake(acpUrl, session.session_id, params, ACP_CONNECT_TIMEOUT_MS);
5671
+ } catch (err) {
5672
+ if (!(err instanceof TransportMissError)) throw err;
5673
+ await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
5674
+ }
5675
+ } else {
5676
+ await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
5677
+ }
5678
+ } catch (err) {
5679
+ await this.client.endSession(session.session_id, {}).catch(() => {
5680
+ });
5681
+ if (err instanceof TransportMissError) {
5682
+ throw new SessionRejectedError(`agent unreachable`);
5683
+ }
5684
+ throw err;
5685
+ }
5686
+ return { session, activated: true };
5687
+ }
5688
+ async attemptHandshake(url, sessionId, params, connectTimeoutMs) {
5689
+ const ws = new WebSocket2(url);
5449
5690
  try {
5450
- await new Promise((resolve, reject) => {
5451
- ws.once("open", () => resolve());
5452
- ws.once("error", reject);
5691
+ await new Promise((resolve3, reject) => {
5692
+ const timer = setTimeout(
5693
+ () => reject(new TransportMissError("connect timeout")),
5694
+ connectTimeoutMs
5695
+ );
5696
+ ws.once("open", () => {
5697
+ clearTimeout(timer);
5698
+ resolve3();
5699
+ });
5700
+ ws.once("error", (err) => {
5701
+ clearTimeout(timer);
5702
+ reject(new TransportMissError(`connect failed: ${err.message}`));
5703
+ });
5453
5704
  });
5454
5705
  ws.send(JSON.stringify({ type: MessageType.IDENTIFY, agent_id: params.agentId, token: params.apiKey }));
5455
5706
  ws.send(JSON.stringify({
5456
5707
  type: MessageType.SESSION_CREATE,
5457
- session_id: session.session_id,
5708
+ session_id: sessionId,
5458
5709
  recipient: params.providerAgentId,
5459
5710
  capability: params.capability
5460
5711
  }));
5461
- const reply = await new Promise((resolve, reject) => {
5462
- const timer = setTimeout(() => reject(new Error("ACP SESSION_ACCEPT timeout")), ACP_TIMEOUT_MS);
5712
+ const reply = await new Promise((resolve3, reject) => {
5713
+ const timer = setTimeout(
5714
+ () => reject(new Error("SESSION_ACCEPT timeout")),
5715
+ SESSION_ACCEPT_TIMEOUT_MS
5716
+ );
5463
5717
  ws.once("message", (data) => {
5464
5718
  clearTimeout(timer);
5465
- resolve(JSON.parse(data.toString()));
5719
+ resolve3(JSON.parse(data.toString()));
5466
5720
  });
5467
5721
  ws.once("error", (err) => {
5468
5722
  clearTimeout(timer);
5469
5723
  reject(err);
5470
5724
  });
5471
5725
  });
5472
- if (reply.type === MessageType.ERROR) throw new SessionRejectedError(reply.error ?? "relay error");
5726
+ if (reply.type === MessageType.ERROR) {
5727
+ const errMsg = reply.error ?? "relay error";
5728
+ if (errMsg.includes("not connected")) throw new TransportMissError(errMsg);
5729
+ throw new SessionRejectedError(errMsg);
5730
+ }
5473
5731
  if (reply.type === MessageType.SESSION_REJECT) throw new SessionRejectedError(reply.reason ?? "rejected");
5474
- if (reply.type !== MessageType.SESSION_ACCEPT) throw new Error(`unexpected ACP reply: ${reply.type}`);
5475
- } catch (err) {
5476
- ws.close();
5477
- await this.client.endSession(session.session_id, {}).catch(() => {
5478
- });
5479
- throw err;
5732
+ if (reply.type !== MessageType.SESSION_ACCEPT) throw new Error(`unexpected reply: ${reply.type}`);
5733
+ } finally {
5734
+ try {
5735
+ ws.close();
5736
+ } catch {
5737
+ }
5480
5738
  }
5481
- ws.close();
5482
- return { session, activated: true };
5483
5739
  }
5484
5740
  send(sessionId, payload, seq) {
5485
5741
  const normalized = typeof payload === "string" ? { text: payload } : payload;
@@ -5490,6 +5746,52 @@ var RequesterFlows = class {
5490
5746
  }
5491
5747
  };
5492
5748
 
5749
+ // src/config.ts
5750
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
5751
+ import { homedir } from "os";
5752
+ import { join, dirname } from "path";
5753
+ import { load as yamlLoad, dump as yamlDump } from "js-yaml";
5754
+ var DEFAULT_CLOUD_URL = "https://api.linkedclaw.com";
5755
+ var DEFAULT_RELAY_URL2 = "wss://api.linkedclaw.com/ws";
5756
+ function configDir() {
5757
+ return process.env["LINKEDCLAW_CONFIG_DIR"] ?? join(homedir(), ".linkedclaw");
5758
+ }
5759
+ function configPath() {
5760
+ return join(configDir(), "config.yaml");
5761
+ }
5762
+ function readFileConfig(path2 = configPath()) {
5763
+ if (!existsSync(path2)) return {};
5764
+ const raw = readFileSync(path2, "utf8");
5765
+ const parsed = yamlLoad(raw);
5766
+ if (parsed === null || parsed === void 0) return {};
5767
+ if (typeof parsed !== "object") {
5768
+ throw new Error(`config file ${path2} is not a YAML object`);
5769
+ }
5770
+ return parsed;
5771
+ }
5772
+ function writeFileConfig(cfg, path2 = configPath()) {
5773
+ const dir = dirname(path2);
5774
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
5775
+ writeFileSync(path2, yamlDump(cfg), { mode: 384 });
5776
+ if (process.platform !== "win32") chmodSync(path2, 384);
5777
+ }
5778
+ function resolveConfig(overrides = {}) {
5779
+ const env = process.env;
5780
+ const file = readFileConfig();
5781
+ const cloudUrl = overrides.cloudUrl ?? env["LINKEDCLAW_CLOUD_URL"] ?? file.cloudUrl ?? DEFAULT_CLOUD_URL;
5782
+ const relayUrl = overrides.relayUrl ?? env["LINKEDCLAW_RELAY_URL"] ?? file.relayUrl ?? DEFAULT_RELAY_URL2;
5783
+ const servicesHostUrl = overrides.servicesHostUrl ?? env["LINKEDCLAW_SERVICES_HOST_URL"] ?? file.servicesHostUrl ?? cloudUrl;
5784
+ const apiKey = overrides.apiKey ?? env["LINKEDCLAW_API_KEY"] ?? file.apiKey;
5785
+ return {
5786
+ ...file,
5787
+ ...overrides,
5788
+ cloudUrl,
5789
+ relayUrl,
5790
+ servicesHostUrl,
5791
+ ...apiKey !== void 0 ? { apiKey } : {}
5792
+ };
5793
+ }
5794
+
5493
5795
  // src/errors.ts
5494
5796
  var LinkedClawError = class extends Error {
5495
5797
  code;
@@ -5507,11 +5809,11 @@ var NetworkError = class extends LinkedClawError {
5507
5809
  }
5508
5810
  };
5509
5811
  var ApiError = class extends LinkedClawError {
5510
- constructor(status, detail, path) {
5511
- super(`api_error_${status}`, `[${status}] ${path}: ${detail}`);
5812
+ constructor(status, detail, path2) {
5813
+ super(`api_error_${status}`, `[${status}] ${path2}: ${detail}`);
5512
5814
  this.status = status;
5513
5815
  this.detail = detail;
5514
- this.path = path;
5816
+ this.path = path2;
5515
5817
  this.name = "ApiError";
5516
5818
  }
5517
5819
  status;
@@ -5581,57 +5883,428 @@ async function readStdin() {
5581
5883
  return Buffer.concat(chunks).toString("utf8");
5582
5884
  }
5583
5885
 
5584
- // src/commands/auth.ts
5585
- function registerAuthCommands(program2) {
5586
- program2.command("login").description("Store API key in ~/.linkedclaw/config.yaml").option("--api-key <key>", "API key (otherwise read from stdin)").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
5587
- await runCommand(async () => {
5588
- let apiKey = opts.apiKey;
5589
- if (!apiKey) {
5590
- apiKey = await readLine("Paste API key: ");
5886
+ // src/arena/api.ts
5887
+ function errorDetail(body) {
5888
+ if (body && typeof body === "object" && "detail" in body) {
5889
+ const detail = body.detail;
5890
+ return typeof detail === "string" ? detail : JSON.stringify(detail) ?? String(detail);
5891
+ }
5892
+ if (body == null) return "";
5893
+ return typeof body === "string" ? body : JSON.stringify(body);
5894
+ }
5895
+ function makeArenaApi(targetUrl, apiKey) {
5896
+ async function apiFetch(path2, opts = {}) {
5897
+ const url = targetUrl.replace(/\/$/, "") + path2;
5898
+ const res = await fetch(url, {
5899
+ ...opts,
5900
+ headers: {
5901
+ "Content-Type": "application/json",
5902
+ Authorization: `Bearer ${apiKey}`,
5903
+ "X-CSRF-Token": apiKey,
5904
+ ...opts.headers ?? {}
5591
5905
  }
5592
- if (!apiKey) throw new Error("empty api key");
5593
- const prev = readFileConfig();
5594
- const next = { ...prev, apiKey, ...opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {} };
5595
- writeFileConfig(next);
5596
- return { ok: true, path: configPath() };
5597
5906
  });
5598
- });
5599
- program2.command("register").description("Open browser to create a LinkedClaw account, then paste your API key").option("--no-browser", "Print URL instead of attempting to open the browser").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
5600
- await runCommand(async () => {
5601
- const prev = readFileConfig();
5602
- const cloudUrl = opts.cloudUrl ?? prev.cloudUrl ?? process.env.LINKEDCLAW_CLOUD_URL ?? DEFAULT_CLOUD_URL;
5603
- const portalUrl = cloudUrl.replace(/\/$/, "") + "/register";
5604
- let opened = false;
5605
- if (opts.browser !== false) {
5606
- try {
5607
- const open = (await import("open")).default;
5608
- await open(portalUrl);
5609
- opened = true;
5610
- } catch {
5611
- }
5907
+ let body;
5908
+ try {
5909
+ body = await res.json();
5910
+ } catch {
5911
+ body = null;
5912
+ }
5913
+ if (!res.ok) {
5914
+ try {
5915
+ throw new ApiError(res.status, errorDetail(body), path2);
5916
+ } catch (err) {
5917
+ if (err instanceof ApiError) throw err;
5918
+ throw new LinkedClawError(`api_${res.status}`, `HTTP ${res.status}`);
5612
5919
  }
5613
- if (!opened) {
5614
- process.stderr.write(`Open this URL in a browser to register:
5615
- ${portalUrl}
5920
+ }
5921
+ return body;
5922
+ }
5923
+ return {
5924
+ async createTournamentArena(body, opts) {
5925
+ return apiFetch("/api/v1/arena/arenas", {
5926
+ method: "POST",
5927
+ headers: { "Idempotency-Key": opts.idempotencyKey },
5928
+ body: JSON.stringify(body)
5929
+ });
5930
+ },
5931
+ async register(body) {
5932
+ return apiFetch("/api/v1/arena/contestants/register", {
5933
+ method: "POST",
5934
+ body: JSON.stringify(body)
5935
+ });
5936
+ },
5937
+ async listOffers() {
5938
+ return apiFetch("/api/v1/arena/offers", { method: "GET" });
5939
+ },
5940
+ async acceptOffer(offerId) {
5941
+ return apiFetch(`/api/v1/arena/offers/${encodeURIComponent(offerId)}/accept`, {
5942
+ method: "POST",
5943
+ body: JSON.stringify({})
5944
+ });
5945
+ },
5946
+ async submit(arenaId, body) {
5947
+ const { submission_hash: _submissionHash, ...wireBody } = body;
5948
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/submissions`, {
5949
+ method: "POST",
5950
+ body: JSON.stringify(wireBody)
5951
+ });
5952
+ },
5953
+ async commitJuror(arenaId, body) {
5954
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/jurors/commit`, {
5955
+ method: "POST",
5956
+ body: JSON.stringify(body)
5957
+ });
5958
+ },
5959
+ async voteTask(arenaId, body) {
5960
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/juror-votes`, {
5961
+ method: "POST",
5962
+ body: JSON.stringify(body)
5963
+ });
5964
+ },
5965
+ async voteMatch(arenaId, matchId, body) {
5966
+ return apiFetch(
5967
+ `/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/matches/${encodeURIComponent(matchId)}/juror-votes`,
5968
+ { method: "POST", body: JSON.stringify(body) }
5969
+ );
5970
+ },
5971
+ async listArenas(opts = {}) {
5972
+ const suffix = opts.registered ? "?registered=true" : "";
5973
+ return apiFetch(`/api/v1/arena/arenas${suffix}`, { method: "GET" });
5974
+ },
5975
+ async getLeaderboard(arenaId) {
5976
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/leaderboard`, {
5977
+ method: "GET"
5978
+ });
5979
+ },
5980
+ async getCategoryLeaderboard(category, mode = "match") {
5981
+ const q = new URLSearchParams({
5982
+ category_topic: category.topic,
5983
+ category_subtopic: category.subtopic,
5984
+ mode
5985
+ });
5986
+ return apiFetch(`/api/v1/arena/leaderboard?${q.toString()}`, {
5987
+ method: "GET"
5988
+ });
5989
+ }
5990
+ };
5991
+ }
5616
5992
 
5617
- `);
5618
- }
5619
- const apiKey = await readLine("Paste your API key (from portal Settings \u2192 API Keys): ");
5620
- if (!apiKey) throw new Error("empty api key");
5621
- const next = {
5622
- ...prev,
5623
- apiKey,
5624
- cloudUrl
5625
- };
5626
- writeFileConfig(next);
5627
- return { ok: true, path: configPath(), opened };
5628
- });
5629
- });
5630
- program2.command("whoami").description("Print current user info").option("--human", "Human-readable output").action(async (opts) => {
5631
- await runCommand(async () => {
5632
- const { consumer } = buildContext();
5633
- return consumer.getMe();
5634
- }, { human: opts.human });
5993
+ // src/arena/hash.ts
5994
+ import { createHash } from "crypto";
5995
+ import { readFileSync as readFileSync2 } from "fs";
5996
+ function sha256Hex(bytes) {
5997
+ return createHash("sha256").update(bytes).digest("hex");
5998
+ }
5999
+ function sha256Digest(bytes) {
6000
+ return `sha256:${sha256Hex(bytes)}`;
6001
+ }
6002
+ function hashFile(path2) {
6003
+ const bytes = readFileSync2(path2);
6004
+ return { bytes, digest: sha256Digest(bytes) };
6005
+ }
6006
+
6007
+ // src/commands/arena.ts
6008
+ var FIRST_PARTY_ARENA_HANDLE = "gig-pa-operator";
6009
+ var FIRST_PARTY_ARENA_SLUG = "arena-v1";
6010
+ var ARENA_CAPABILITY = "arena.v1";
6011
+ function servicesHostBaseUrl(ctx) {
6012
+ return ctx.cfg.servicesHostUrl ?? process.env.LINKEDCLAW_SERVICES_HOST_URL ?? ctx.cfg.cloudUrl;
6013
+ }
6014
+ function endpointForListing(listing, ctx) {
6015
+ const endpoint = listing.external_endpoint;
6016
+ return typeof endpoint === "string" && endpoint.length > 0 ? endpoint : servicesHostBaseUrl(ctx);
6017
+ }
6018
+ function assertArenaPa(listing, source) {
6019
+ const caps = Array.isArray(listing.capabilities) ? listing.capabilities : [];
6020
+ if (!caps.includes(ARENA_CAPABILITY)) {
6021
+ throw new LinkedClawError(
6022
+ "arena_target_not_arena_pa",
6023
+ `${source} does not advertise ${ARENA_CAPABILITY}.`
6024
+ );
6025
+ }
6026
+ }
6027
+ async function resolveArenaTarget(ctx, opts) {
6028
+ if (opts.target && /^https?:\/\//i.test(opts.target)) {
6029
+ throw new LinkedClawError(
6030
+ "arena_target_must_be_agent_id",
6031
+ "--target now expects an arena.v1 agent id, not a URL."
6032
+ );
6033
+ }
6034
+ const listing = opts.target ? await ctx.consumer.getAgent(opts.target) : await ctx.consumer.resolveAgentHandle(FIRST_PARTY_ARENA_HANDLE, FIRST_PARTY_ARENA_SLUG);
6035
+ assertArenaPa(listing, opts.target ?? `${FIRST_PARTY_ARENA_HANDLE}/${FIRST_PARTY_ARENA_SLUG}`);
6036
+ return { agentId: listing.agent_id, baseUrl: endpointForListing(listing, ctx) };
6037
+ }
6038
+ async function buildArenaApi(opts) {
6039
+ const ctx = buildContext();
6040
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
6041
+ const target = await resolveArenaTarget(ctx, opts);
6042
+ return makeArenaApi(target.baseUrl, ctx.cfg.apiKey);
6043
+ }
6044
+ function parseCategory(opts) {
6045
+ if (!opts.categoryTopic) {
6046
+ throw new LinkedClawError("missing_category_topic", "--category-topic is required.");
6047
+ }
6048
+ if (!opts.categorySubtopic) {
6049
+ throw new LinkedClawError("missing_category_subtopic", "--category-subtopic is required.");
6050
+ }
6051
+ return { topic: opts.categoryTopic, subtopic: opts.categorySubtopic };
6052
+ }
6053
+ function parseSeq(value) {
6054
+ const seq = Number(value);
6055
+ if (!Number.isInteger(seq) || seq < 1) {
6056
+ throw new LinkedClawError("invalid_seq", "--seq must be a positive integer.");
6057
+ }
6058
+ return seq;
6059
+ }
6060
+ function parseScore(value) {
6061
+ const score = Number(value);
6062
+ if (!Number.isFinite(score) || score < 0 || score > 1) {
6063
+ throw new LinkedClawError("invalid_juror_score", "score must be a number between 0 and 1.");
6064
+ }
6065
+ return score;
6066
+ }
6067
+ function isPlainObject(value) {
6068
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6069
+ }
6070
+ function readTournamentManifest(path2) {
6071
+ if (path2 === "-" && process.stdin.isTTY) {
6072
+ throw new LinkedClawError(
6073
+ "arena_tournament_manifest_stdin_tty",
6074
+ "stdin is a TTY; pass a file path or pipe JSON via stdin (e.g. cat tournament.json | linkedclaw arena tournament create -)."
6075
+ );
6076
+ }
6077
+ let raw;
6078
+ try {
6079
+ raw = readFileSync3(path2 === "-" ? 0 : path2, "utf8");
6080
+ } catch (err) {
6081
+ const message = err instanceof Error ? err.message : String(err);
6082
+ throw new LinkedClawError(
6083
+ "arena_tournament_manifest_read_failed",
6084
+ path2 === "-" ? `could not read from stdin (use "-" to pipe JSON): ${message}` : message
6085
+ );
6086
+ }
6087
+ let parsed;
6088
+ try {
6089
+ parsed = JSON.parse(raw);
6090
+ } catch (err) {
6091
+ throw new LinkedClawError(
6092
+ "arena_tournament_manifest_json_invalid",
6093
+ err instanceof Error ? err.message : String(err)
6094
+ );
6095
+ }
6096
+ if (!isPlainObject(parsed)) {
6097
+ throw new LinkedClawError(
6098
+ "arena_tournament_manifest_shape_invalid",
6099
+ "manifest must be a JSON object."
6100
+ );
6101
+ }
6102
+ if (parsed.mode !== "tournament") {
6103
+ throw new LinkedClawError(
6104
+ "arena_tournament_manifest_mode_invalid",
6105
+ 'manifest mode must be exactly "tournament".'
6106
+ );
6107
+ }
6108
+ if (!isPlainObject(parsed.category) || !isPlainObject(parsed.config)) {
6109
+ throw new LinkedClawError(
6110
+ "arena_tournament_manifest_shape_invalid",
6111
+ "manifest must include category and config object fields."
6112
+ );
6113
+ }
6114
+ return {
6115
+ mode: "tournament",
6116
+ category: parsed.category,
6117
+ config: parsed.config
6118
+ };
6119
+ }
6120
+ function parseIdempotencyKey(value) {
6121
+ const idempotencyKey = value?.trim();
6122
+ if (!idempotencyKey) {
6123
+ throw new LinkedClawError(
6124
+ "arena_idempotency_key_required",
6125
+ "--idempotency-key must be a non-empty string."
6126
+ );
6127
+ }
6128
+ if (/[\r\n]/.test(idempotencyKey)) {
6129
+ throw new LinkedClawError(
6130
+ "arena_idempotency_key_invalid",
6131
+ "--idempotency-key must not contain newlines."
6132
+ );
6133
+ }
6134
+ return idempotencyKey;
6135
+ }
6136
+ function mergeSubmissionHash(response, submissionHash) {
6137
+ if (response && typeof response === "object" && "submission" in response && response.submission && typeof response.submission === "object") {
6138
+ const submission = response.submission;
6139
+ if (typeof submission.submission_hash === "string") return response;
6140
+ return { ...response, submission: { ...submission, submission_hash: submissionHash } };
6141
+ }
6142
+ if (response && typeof response === "object" && "submission_hash" in response) return response;
6143
+ return { response, submission_hash: submissionHash };
6144
+ }
6145
+ function registerArenaCommands(program2) {
6146
+ const arena = program2.command("arena").description("Arena PA commands");
6147
+ const tournament = arena.command("tournament").description("Arena tournament commands");
6148
+ tournament.command("create <manifest.json>").description("Create a tournament Arena from an exact JSON manifest").option("--idempotency-key <key>", "Required replay key for tournament creation").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (manifestPath, opts) => {
6149
+ await runCommand(async () => {
6150
+ const idempotencyKey = parseIdempotencyKey(opts.idempotencyKey);
6151
+ return (await buildArenaApi(opts)).createTournamentArena(readTournamentManifest(manifestPath), {
6152
+ idempotencyKey
6153
+ });
6154
+ }, { human: opts.human });
6155
+ });
6156
+ arena.command("register").description("Register a contestant agent for Arena offers").requiredOption("--agent-id <agt_id>").requiredOption("--mandate-id <mandate_id>").requiredOption("--category-topic <topic>").requiredOption("--category-subtopic <subtopic>").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(
6157
+ async (opts) => {
6158
+ await runCommand(async () => {
6159
+ const api = await buildArenaApi(opts);
6160
+ return api.register({
6161
+ contestant_agent_id: opts.agentId,
6162
+ mandate_id: opts.mandateId,
6163
+ category: parseCategory(opts)
6164
+ });
6165
+ }, { human: opts.human });
6166
+ }
6167
+ );
6168
+ arena.command("offers").description("List durable Arena offers for this owner").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (opts) => {
6169
+ await runCommand(async () => (await buildArenaApi(opts)).listOffers(), { human: opts.human });
6170
+ });
6171
+ arena.command("accept <offer_id>").description("Accept an Arena offer").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (offerId, opts) => {
6172
+ await runCommand(async () => (await buildArenaApi(opts)).acceptOffer(offerId), { human: opts.human });
6173
+ });
6174
+ arena.command("submit <arena_id>").description("Submit a text or file answer to an Arena").requiredOption("--offer-id <offer_id>").option("--match-id <match_id>", "Pending match id for match-mode submissions").option("--file <path>").option("--body <text>").option("--content-ref <ref>").option("--seq <n>", "Submission sequence number", parseSeq, 1).option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(
6175
+ async (arenaId, opts) => {
6176
+ await runCommand(async () => {
6177
+ if (opts.file && opts.body !== void 0) {
6178
+ throw new LinkedClawError("submission_source_conflict", "Use exactly one of --file or --body.");
6179
+ }
6180
+ if (!opts.file && opts.body === void 0) {
6181
+ throw new LinkedClawError("submission_source_required", "Use exactly one of --file or --body.");
6182
+ }
6183
+ let request;
6184
+ if (opts.file) {
6185
+ const { bytes, digest } = hashFile(opts.file);
6186
+ request = {
6187
+ offer_id: opts.offerId,
6188
+ raw_content: bytes.toString("utf8"),
6189
+ content_ref: opts.contentRef ?? opts.file,
6190
+ ...opts.matchId ? { match_id: opts.matchId } : {},
6191
+ seq: opts.seq,
6192
+ submission_hash: digest
6193
+ };
6194
+ } else {
6195
+ const body = opts.body ?? "";
6196
+ request = {
6197
+ offer_id: opts.offerId,
6198
+ raw_content: body,
6199
+ ...opts.contentRef ? { content_ref: opts.contentRef } : {},
6200
+ ...opts.matchId ? { match_id: opts.matchId } : {},
6201
+ seq: opts.seq,
6202
+ submission_hash: sha256Digest(Buffer.from(body, "utf8"))
6203
+ };
6204
+ }
6205
+ const response = await (await buildArenaApi(opts)).submit(arenaId, request);
6206
+ return mergeSubmissionHash(response, request.submission_hash);
6207
+ }, { human: opts.human });
6208
+ }
6209
+ );
6210
+ const vote = arena.command("vote").description("Arena juror voting commands");
6211
+ vote.command("task <arena_id> <submission_id> <score>").description("Submit a task-submission juror score").option("--rationale-ref <ref>").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (arenaId, submissionId, scoreValue, opts) => {
6212
+ await runCommand(async () => {
6213
+ const score = parseScore(scoreValue);
6214
+ return (await buildArenaApi(opts)).voteTask(arenaId, {
6215
+ submission_id: submissionId,
6216
+ score,
6217
+ ...opts.rationaleRef ? { rationale_ref: opts.rationaleRef } : {}
6218
+ });
6219
+ }, { human: opts.human });
6220
+ });
6221
+ vote.command("match <arena_id> <match_id> <outcome>").description("Submit a match-mode juror outcome").option("--rationale-ref <ref>").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (arenaId, matchId, outcome, opts) => {
6222
+ await runCommand(async () => {
6223
+ if (!["a", "b", "tie", "both_bad"].includes(outcome)) {
6224
+ throw new LinkedClawError(
6225
+ "invalid_juror_outcome",
6226
+ "outcome must be one of: a, b, tie, both_bad."
6227
+ );
6228
+ }
6229
+ return (await buildArenaApi(opts)).voteMatch(arenaId, matchId, {
6230
+ outcome,
6231
+ ...opts.rationaleRef ? { rationale_ref: opts.rationaleRef } : {}
6232
+ });
6233
+ }, { human: opts.human });
6234
+ });
6235
+ arena.command("list").description("List visible Arenas").option("--registered", "Only arenas where this owner is registered or submitted").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (opts) => {
6236
+ await runCommand(async () => (await buildArenaApi(opts)).listArenas({ registered: opts.registered }), {
6237
+ human: opts.human
6238
+ });
6239
+ });
6240
+ arena.command("leaderboard [arena_id]").description("Read an Arena leaderboard").option("--category-topic <topic>").option("--category-subtopic <subtopic>").option("--mode <mode>", "Leaderboard mode", "match").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (arenaId, opts) => {
6241
+ await runCommand(async () => {
6242
+ const api = await buildArenaApi(opts);
6243
+ if (arenaId) {
6244
+ return api.getLeaderboard(arenaId);
6245
+ }
6246
+ if (opts.mode !== "match") {
6247
+ throw new LinkedClawError(
6248
+ "unsupported_arena_leaderboard_mode",
6249
+ "--mode must be match when no arena_id is provided."
6250
+ );
6251
+ }
6252
+ return api.getCategoryLeaderboard(parseCategory(opts), opts.mode);
6253
+ }, { human: opts.human });
6254
+ });
6255
+ }
6256
+
6257
+ // src/commands/auth.ts
6258
+ function registerAuthCommands(program2) {
6259
+ program2.command("login").description("Store API key in ~/.linkedclaw/config.yaml").option("--api-key <key>", "API key (otherwise read from stdin)").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
6260
+ await runCommand(async () => {
6261
+ let apiKey = opts.apiKey;
6262
+ if (!apiKey) {
6263
+ apiKey = await readLine("Paste API key: ");
6264
+ }
6265
+ if (!apiKey) throw new Error("empty api key");
6266
+ const prev = readFileConfig();
6267
+ const next = { ...prev, apiKey, ...opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {} };
6268
+ writeFileConfig(next);
6269
+ return { ok: true, path: configPath() };
6270
+ });
6271
+ });
6272
+ program2.command("register").description("Open browser to create a LinkedClaw account, then paste your API key").option("--no-browser", "Print URL instead of attempting to open the browser").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
6273
+ await runCommand(async () => {
6274
+ const prev = readFileConfig();
6275
+ const cloudUrl = opts.cloudUrl ?? prev.cloudUrl ?? process.env.LINKEDCLAW_CLOUD_URL ?? DEFAULT_CLOUD_URL;
6276
+ const portalUrl = cloudUrl.replace(/\/$/, "") + "/register";
6277
+ let opened = false;
6278
+ if (opts.browser !== false) {
6279
+ try {
6280
+ const open = (await import("open")).default;
6281
+ await open(portalUrl);
6282
+ opened = true;
6283
+ } catch {
6284
+ }
6285
+ }
6286
+ if (!opened) {
6287
+ process.stderr.write(`Open this URL in a browser to register:
6288
+ ${portalUrl}
6289
+
6290
+ `);
6291
+ }
6292
+ const apiKey = await readLine("Paste your API key (from portal Settings \u2192 API Keys): ");
6293
+ if (!apiKey) throw new Error("empty api key");
6294
+ const next = {
6295
+ ...prev,
6296
+ apiKey,
6297
+ cloudUrl
6298
+ };
6299
+ writeFileConfig(next);
6300
+ return { ok: true, path: configPath(), opened };
6301
+ });
6302
+ });
6303
+ program2.command("whoami").description("Print current user info").option("--human", "Human-readable output").action(async (opts) => {
6304
+ await runCommand(async () => {
6305
+ const { consumer } = buildContext();
6306
+ return consumer.getMe();
6307
+ }, { human: opts.human });
5635
6308
  });
5636
6309
  const config = program2.command("config").description("Inspect or edit local config file");
5637
6310
  config.command("show").description("Print the config file contents (api key redacted)").action(async () => {
@@ -5661,9 +6334,1060 @@ function tryParseJson(v) {
5661
6334
  }
5662
6335
  }
5663
6336
 
6337
+ // src/commands/converge.ts
6338
+ import { spawnSync } from "child_process";
6339
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
6340
+ import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
6341
+
6342
+ // src/converge/api.ts
6343
+ function makeFetchError(code, body) {
6344
+ const err = new LinkedClawError(`api_${code}`, `HTTP ${code}`);
6345
+ err.code = code;
6346
+ err.body = body;
6347
+ return err;
6348
+ }
6349
+ function makeConvergeApi(cloudUrl, apiKey) {
6350
+ async function apiFetch(path2, opts = {}) {
6351
+ const url = cloudUrl.replace(/\/$/, "") + path2;
6352
+ const res = await fetch(url, {
6353
+ ...opts,
6354
+ headers: {
6355
+ "Content-Type": "application/json",
6356
+ Authorization: `Bearer ${apiKey}`,
6357
+ ...opts.headers ?? {}
6358
+ }
6359
+ });
6360
+ let body;
6361
+ try {
6362
+ body = await res.json();
6363
+ } catch {
6364
+ body = null;
6365
+ }
6366
+ if (!res.ok) throw makeFetchError(res.status, body);
6367
+ return body;
6368
+ }
6369
+ return {
6370
+ async getDebate(debateId) {
6371
+ return apiFetch(`/api/v1/debates/${debateId}`);
6372
+ },
6373
+ async getCommonsLogEvents(cid, opts = {}) {
6374
+ const requested = opts.limit ?? 1e3;
6375
+ const PAGE = 1e3;
6376
+ const offsetStart = opts.offset ?? 0;
6377
+ let collected = [];
6378
+ let cursor = offsetStart;
6379
+ while (collected.length < requested) {
6380
+ const params = new URLSearchParams();
6381
+ params.set("offset", String(cursor));
6382
+ params.set("limit", String(Math.min(PAGE, requested - collected.length)));
6383
+ const page = await apiFetch(
6384
+ `/api/v1/commons-logs/${cid}/events?${params}`
6385
+ );
6386
+ const hoisted = page.events.map((e) => ({
6387
+ ...e,
6388
+ event_type: e.event_type ?? e.payload?.event_type ?? ""
6389
+ }));
6390
+ collected = collected.concat(hoisted);
6391
+ if (page.events.length === 0 || page.next_offset === cursor) break;
6392
+ cursor = page.next_offset;
6393
+ }
6394
+ return { events: collected, next_offset: cursor };
6395
+ },
6396
+ async discoverPaAgentId() {
6397
+ const result = await apiFetch(
6398
+ "/api/v1/agents?capability=convergence_synthesizer.v1"
6399
+ );
6400
+ const listings = Array.isArray(result) ? result : result.agents ?? [];
6401
+ if (listings.length === 0) {
6402
+ throw new LinkedClawError("pa_not_found", "No agent found with capability convergence_synthesizer.v1");
6403
+ }
6404
+ return listings[0].agent_id;
6405
+ },
6406
+ async findExistingMandate(principalAgentId, delegateAgentId, requiredScopes) {
6407
+ const result = await apiFetch(`/api/v1/mandates?kind=generalized`);
6408
+ const list = Array.isArray(result) ? result : result.mandates ?? [];
6409
+ const required = new Set(requiredScopes);
6410
+ const now = Date.now();
6411
+ for (const m of list) {
6412
+ if (m.principal_agent_id !== principalAgentId) continue;
6413
+ if (m.delegate_agent_id !== delegateAgentId) continue;
6414
+ if (m.revoked_at) continue;
6415
+ if (m.expires_at && new Date(m.expires_at).getTime() <= now) continue;
6416
+ if (![...required].every((s) => m.scope.includes(s))) continue;
6417
+ return m;
6418
+ }
6419
+ return null;
6420
+ },
6421
+ async issueMandate(principalAgentId, delegateAgentId, scopes, expiresAt) {
6422
+ return apiFetch("/api/v1/mandates", {
6423
+ method: "POST",
6424
+ body: JSON.stringify({
6425
+ principal_agent_id: principalAgentId,
6426
+ delegate_agent_id: delegateAgentId,
6427
+ scope: scopes,
6428
+ ...expiresAt ? { expires_at: expiresAt } : {}
6429
+ })
6430
+ });
6431
+ },
6432
+ async startRun(sourceDebateId) {
6433
+ return apiFetch("/api/v1/convergence/runs", {
6434
+ method: "POST",
6435
+ body: JSON.stringify({ source_debate_id: sourceDebateId })
6436
+ });
6437
+ },
6438
+ async getRun(runId) {
6439
+ return apiFetch(`/api/v1/convergence/runs/${runId}`);
6440
+ },
6441
+ async acceptOwnerB(runId) {
6442
+ return apiFetch(`/api/v1/convergence/runs/${runId}/owner_b_accept`, {
6443
+ method: "POST"
6444
+ });
6445
+ },
6446
+ async appendCommonsLog(cid, eventType, payload) {
6447
+ return apiFetch(`/api/v1/commons-logs/${cid}/append`, {
6448
+ method: "POST",
6449
+ body: JSON.stringify({ event_type: eventType, payload })
6450
+ });
6451
+ },
6452
+ async acceptCruxDecision(runId, cruxId, body) {
6453
+ return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/accept`, {
6454
+ method: "POST",
6455
+ body: JSON.stringify(body)
6456
+ });
6457
+ },
6458
+ async rejectCruxDecision(runId, cruxId, body) {
6459
+ return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/reject`, {
6460
+ method: "POST",
6461
+ body: JSON.stringify(body)
6462
+ });
6463
+ },
6464
+ async attestCruxDecision(runId, cruxId, body) {
6465
+ return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/attest`, {
6466
+ method: "POST",
6467
+ body: JSON.stringify(body)
6468
+ });
6469
+ }
6470
+ };
6471
+ }
6472
+
6473
+ // src/converge/hash.ts
6474
+ import { createHash as createHash2 } from "crypto";
6475
+ function encodeString(s) {
6476
+ let out = '"';
6477
+ for (let i = 0; i < s.length; i++) {
6478
+ const cp = s.charCodeAt(i);
6479
+ if (cp === 34) out += '\\"';
6480
+ else if (cp === 92) out += "\\\\";
6481
+ else if (cp === 8) out += "\\b";
6482
+ else if (cp === 9) out += "\\t";
6483
+ else if (cp === 10) out += "\\n";
6484
+ else if (cp === 12) out += "\\f";
6485
+ else if (cp === 13) out += "\\r";
6486
+ else if (cp < 32 || cp > 126) out += `\\u${cp.toString(16).padStart(4, "0")}`;
6487
+ else out += s[i];
6488
+ }
6489
+ return out + '"';
6490
+ }
6491
+ function canonicalize(value) {
6492
+ if (value === null) return "null";
6493
+ if (typeof value === "string") return encodeString(value);
6494
+ if (typeof value !== "object") return JSON.stringify(value);
6495
+ if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
6496
+ const keys = Object.keys(value).sort();
6497
+ return "{" + keys.map((k) => encodeString(k) + ":" + canonicalize(value[k])).join(",") + "}";
6498
+ }
6499
+ function sha256OfCanonicalJson(value) {
6500
+ const h = createHash2("sha256");
6501
+ h.update(canonicalize(value));
6502
+ return "sha256:" + h.digest("hex");
6503
+ }
6504
+
6505
+ // src/converge/lock.ts
6506
+ import { closeSync, openSync, unlinkSync, writeSync } from "fs";
6507
+ import { join as join2 } from "path";
6508
+ var LOCK_FILENAME = ".lock";
6509
+ function acquireLock(stagingDir) {
6510
+ const path2 = join2(stagingDir, LOCK_FILENAME);
6511
+ let fd;
6512
+ try {
6513
+ fd = openSync(path2, "wx");
6514
+ } catch (e) {
6515
+ if (e.code === "EEXIST") {
6516
+ throw new LinkedClawError(
6517
+ "lock_held",
6518
+ `Lock held at ${path2}. If no other run/accept is in progress, delete ${path2} to recover.`
6519
+ );
6520
+ }
6521
+ throw e;
6522
+ }
6523
+ writeSync(fd, JSON.stringify({ pid: process.pid }));
6524
+ closeSync(fd);
6525
+ return () => {
6526
+ try {
6527
+ unlinkSync(path2);
6528
+ } catch {
6529
+ }
6530
+ };
6531
+ }
6532
+
6533
+ // src/converge/staging.ts
6534
+ import { createHash as createHash3 } from "crypto";
6535
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync2 } from "fs";
6536
+ import { dirname as dirname2, join as join3 } from "path";
6537
+ import { load as yamlLoad2, dump as yamlDump2 } from "js-yaml";
6538
+ function stagingPathFor(stagingDir, cruxId) {
6539
+ return join3(stagingDir, `${cruxId}.md`);
6540
+ }
6541
+ function listCruxFiles(stagingDir) {
6542
+ if (!existsSync2(stagingDir)) return [];
6543
+ return readdirSync(stagingDir).filter(
6544
+ (f) => f.endsWith(".md") && !f.startsWith(".")
6545
+ );
6546
+ }
6547
+ function parseStaging(text) {
6548
+ if (!text.startsWith("---\n")) {
6549
+ throw new Error("Missing YAML frontmatter: document must start with ---\\n");
6550
+ }
6551
+ const endIdx = text.indexOf("\n---\n", 4);
6552
+ if (endIdx === -1) {
6553
+ throw new Error("Malformed frontmatter: no closing ---");
6554
+ }
6555
+ const yamlText = text.slice(4, endIdx);
6556
+ const body = text.slice(endIdx + 5);
6557
+ const raw = yamlLoad2(yamlText);
6558
+ if (!raw || typeof raw !== "object") {
6559
+ throw new Error("Frontmatter parsed to non-object");
6560
+ }
6561
+ const userResponse = typeof raw._user_response === "string" ? raw._user_response : "";
6562
+ delete raw._user_response;
6563
+ return {
6564
+ frontmatter: raw,
6565
+ userResponse,
6566
+ body
6567
+ };
6568
+ }
6569
+ function dumpStaging(doc) {
6570
+ const fmRaw = { ...doc.frontmatter };
6571
+ fmRaw._user_response = doc.userResponse ?? "";
6572
+ const yamlText = yamlDump2(fmRaw, { lineWidth: -1, sortKeys: false });
6573
+ return `---
6574
+ ${yamlText}---
6575
+ ${doc.body}`;
6576
+ }
6577
+ function readStaging(path2) {
6578
+ return parseStaging(readFileSync4(path2, "utf8"));
6579
+ }
6580
+ function writeStaging(path2, doc) {
6581
+ mkdirSync2(dirname2(path2), { recursive: true });
6582
+ writeFileSync2(path2, dumpStaging(doc), "utf8");
6583
+ }
6584
+ function computePaBodyHash(body) {
6585
+ return "sha256:" + createHash3("sha256").update(Buffer.from(body, "utf8")).digest("hex");
6586
+ }
6587
+
6588
+ // src/converge/workspace.ts
6589
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
6590
+ import { dirname as dirname3, isAbsolute, join as join4, resolve } from "path";
6591
+ import { load as yamlLoad3, dump as yamlDump3 } from "js-yaml";
6592
+ var META_FILENAME = ".run-meta.yaml";
6593
+ function readRunMeta(stagingDir) {
6594
+ const metaPath = join4(stagingDir, META_FILENAME);
6595
+ if (!existsSync3(metaPath)) return null;
6596
+ return yamlLoad3(readFileSync5(metaPath, "utf8"));
6597
+ }
6598
+ function writeRunMeta(stagingDir, meta) {
6599
+ mkdirSync3(stagingDir, { recursive: true });
6600
+ writeFileSync3(join4(stagingDir, META_FILENAME), yamlDump3(meta), "utf8");
6601
+ }
6602
+ function searchUpward(startDir, maxLevels = 5) {
6603
+ let dir = startDir;
6604
+ for (let i = 0; i < maxLevels; i++) {
6605
+ if (existsSync3(join4(dir, META_FILENAME))) return dir;
6606
+ const parent = dirname3(dir);
6607
+ if (parent === dir) break;
6608
+ dir = parent;
6609
+ }
6610
+ return null;
6611
+ }
6612
+ async function resolveWorkspace(opts) {
6613
+ const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
6614
+ let stagingDir;
6615
+ let meta = null;
6616
+ if (opts.stagingDir) {
6617
+ stagingDir = isAbsolute(opts.stagingDir) ? opts.stagingDir : resolve(cwd, opts.stagingDir);
6618
+ meta = readRunMeta(stagingDir);
6619
+ if (!meta) {
6620
+ throw new LinkedClawError(
6621
+ "meta_not_found",
6622
+ `No ${META_FILENAME} found in --staging-dir: ${stagingDir}`
6623
+ );
6624
+ }
6625
+ } else {
6626
+ const found = searchUpward(cwd);
6627
+ if (found) {
6628
+ stagingDir = found;
6629
+ meta = readRunMeta(found);
6630
+ }
6631
+ }
6632
+ if (!meta) {
6633
+ if (opts.runId) {
6634
+ throw new LinkedClawError(
6635
+ "meta_not_found",
6636
+ `--run-id given but no ${META_FILENAME} found (searched upward from ${cwd}). Provide --staging-dir to locate the run workspace.`
6637
+ );
6638
+ }
6639
+ throw new LinkedClawError(
6640
+ "meta_not_found",
6641
+ `No ${META_FILENAME} found (searched upward from ${cwd}). Run 'lc converge run <debate_id>' first.`
6642
+ );
6643
+ }
6644
+ if (opts.runId && opts.runId !== meta.run_id) {
6645
+ throw new LinkedClawError(
6646
+ "run_id_mismatch",
6647
+ `--run-id ${opts.runId} does not match run_id ${meta.run_id} in ${META_FILENAME}`
6648
+ );
6649
+ }
6650
+ const targetCorpus = isAbsolute(meta.target_corpus) ? meta.target_corpus : resolve(stagingDir, meta.target_corpus);
6651
+ return {
6652
+ runId: meta.run_id,
6653
+ sourceDebateId: meta.source_debate_id,
6654
+ paAgentId: meta.pa_agent_id,
6655
+ targetCorpus,
6656
+ stagingDir
6657
+ };
6658
+ }
6659
+
6660
+ // src/commands/converge.ts
6661
+ function resolveAbs(p) {
6662
+ return isAbsolute2(p) ? p : resolve2(process.cwd(), p);
6663
+ }
6664
+ async function getMyUserId(ctx) {
6665
+ const me = await ctx.consumer.getMe();
6666
+ if (!me.user_id) throw new LinkedClawError("no_user_id", "Could not determine user_id from /api/v1/me");
6667
+ return me.user_id;
6668
+ }
6669
+ function recomputeSourceCruxMapHash(events) {
6670
+ const ev = [...events].reverse().find((e) => e.event_type === "crux_map");
6671
+ if (!ev) return null;
6672
+ return sha256OfCanonicalJson(ev.payload.crux_map_data);
6673
+ }
6674
+ function recordedSourceHash(events) {
6675
+ const ev = events.find((e) => e.event_type === "run_started");
6676
+ if (!ev) return null;
6677
+ return typeof ev.payload.source_crux_map_hash === "string" ? ev.payload.source_crux_map_hash : null;
6678
+ }
6679
+ function buildPaBody(op) {
6680
+ const synthesis = typeof op.synthesis_text === "string" ? op.synthesis_text : "";
6681
+ const questions = Array.isArray(op.open_questions) ? op.open_questions : [];
6682
+ const qText = questions.map((q) => `- ${String(q)}`).join("\n");
6683
+ return `# Synthesis
6684
+
6685
+ ${synthesis}
6686
+
6687
+ # Open questions
6688
+
6689
+ ${qText || "(none)"}
6690
+ `;
6691
+ }
6692
+ function countPreviouslyClarifiedSections(body) {
6693
+ const m = body.match(/^# Previously clarified \(round \d+\)/gm);
6694
+ return m ? m.length : 0;
6695
+ }
6696
+ function slugify(s, maxLen = 64) {
6697
+ const base = s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/g, "");
6698
+ return base || "untitled";
6699
+ }
6700
+ function extractSynthesisSlug(body, maxLen = 32) {
6701
+ const synthIdx = body.indexOf("# Synthesis");
6702
+ const search = synthIdx >= 0 ? body.slice(synthIdx) : body;
6703
+ const lines = search.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
6704
+ const first = lines[0] ?? "";
6705
+ return slugify(first, maxLen);
6706
+ }
6707
+ function tryGitAdd(absPath) {
6708
+ try {
6709
+ const r = spawnSync("git", ["add", absPath], { encoding: "utf8" });
6710
+ if (r.error) return `git_add_failed: ${r.error.message}`;
6711
+ if (r.status !== 0) return `git_add_failed: ${(r.stderr || "").trim() || `exit ${r.status}`}`;
6712
+ return null;
6713
+ } catch (e) {
6714
+ return `git_add_failed: ${e.message}`;
6715
+ }
6716
+ }
6717
+ function assertSafeCruxId(cruxId) {
6718
+ if (!cruxId || cruxId.includes("/") || cruxId.includes("\\") || cruxId.includes("..") || cruxId.includes("\0")) {
6719
+ throw new LinkedClawError("invalid_crux_id", `Invalid crux_id for local file operation: ${cruxId}`);
6720
+ }
6721
+ }
6722
+ function assertInside(parentDir, childPath) {
6723
+ const parent = resolve2(parentDir);
6724
+ const child = resolve2(childPath);
6725
+ const rel = relative(parent, child);
6726
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute2(rel)) return;
6727
+ throw new LinkedClawError("path_escape", `Refusing local path outside ${parentDir}: ${childPath}`);
6728
+ }
6729
+ function safeStagingPathFor(stagingDir, cruxId) {
6730
+ assertSafeCruxId(cruxId);
6731
+ const path2 = stagingPathFor(stagingDir, cruxId);
6732
+ assertInside(stagingDir, path2);
6733
+ return path2;
6734
+ }
6735
+ function safeAcceptedPath(finalDir, cruxId, synthSlug) {
6736
+ assertSafeCruxId(cruxId);
6737
+ const path2 = join5(finalDir, `${cruxId}__${synthSlug}.md`);
6738
+ assertInside(finalDir, path2);
6739
+ return path2;
6740
+ }
6741
+ function computeDecisionBodyHash(synthesisText, citationsA, citationsB) {
6742
+ return sha256OfCanonicalJson({
6743
+ citations_a: citationsA,
6744
+ citations_b: citationsB,
6745
+ synthesis_text: synthesisText
6746
+ });
6747
+ }
6748
+ function eventKind(ev) {
6749
+ return typeof ev.payload.event_type === "string" ? ev.payload.event_type : ev.event_type;
6750
+ }
6751
+ function decisionEventTypeForAction(action) {
6752
+ if (action === "accept") return "accept_attestation";
6753
+ if (action === "reject") return "reject_attestation";
6754
+ return "attest_only";
6755
+ }
6756
+ function latestConvergenceMapEvent(events) {
6757
+ const operatorUserId = process.env.LINKEDCLAW_OPERATOR_USER_ID ?? "usr_operator";
6758
+ const reversed = [...events].reverse();
6759
+ const ev = reversed.find((e) => eventKind(e) === "convergence_map" && e.signed_by === operatorUserId);
6760
+ if (!ev) {
6761
+ throw new LinkedClawError(
6762
+ "convergence_map_not_found",
6763
+ `No PA-signed convergence_map event found for this run (expected signed_by=${operatorUserId}).`
6764
+ );
6765
+ }
6766
+ return ev;
6767
+ }
6768
+ function latestConvergenceMap(events) {
6769
+ return latestConvergenceMapEvent(events).payload;
6770
+ }
6771
+ function getCruxFromMap(map, cruxId) {
6772
+ const cruxes = Array.isArray(map.cruxes) ? map.cruxes : [];
6773
+ const crux = cruxes.find(
6774
+ (c) => c && typeof c === "object" && c.crux_id === cruxId
6775
+ );
6776
+ if (!crux || typeof crux !== "object") {
6777
+ throw new LinkedClawError("crux_not_found", `crux ${cruxId} not found in latest convergence_map.`);
6778
+ }
6779
+ return crux;
6780
+ }
6781
+ function citationsFromCrux(value) {
6782
+ if (!Array.isArray(value)) return [];
6783
+ return value.filter((v) => v != null && typeof v === "object" && !Array.isArray(v));
6784
+ }
6785
+ function classifyDecisionAttestation(action, outcome, bilateralMandateIntact, synthesisEdited = false) {
6786
+ if (action === "attest") return "user_attested_no_dialog";
6787
+ if (action === "reject") return "user_attested_with_network_context";
6788
+ if (outcome === "already_aligned") {
6789
+ throw new LinkedClawError(
6790
+ "attest_required",
6791
+ "outcome=already_aligned must be decided with `lc converge attest`."
6792
+ );
6793
+ }
6794
+ if ((outcome === "converged" || outcome === "partial_overlap") && bilateralMandateIntact && !synthesisEdited) {
6795
+ return "bilateral_convergence";
6796
+ }
6797
+ return "user_attested_with_network_context";
6798
+ }
6799
+ function decisionPayloadCitations(value) {
6800
+ return citationsFromCrux(value);
6801
+ }
6802
+ function stringFromPayload(payload, key) {
6803
+ const value = payload[key];
6804
+ if (typeof value !== "string" || value.length === 0) {
6805
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision missing ${key}.`);
6806
+ }
6807
+ return value;
6808
+ }
6809
+ function booleanFromPayload(payload, key) {
6810
+ const value = payload[key];
6811
+ if (typeof value !== "boolean") {
6812
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision missing ${key}.`);
6813
+ }
6814
+ return value;
6815
+ }
6816
+ function attestationFromPayload(payload) {
6817
+ const value = stringFromPayload(payload, "attestation");
6818
+ if (value !== "bilateral_convergence" && value !== "user_attested_with_network_context" && value !== "user_attested_no_dialog") {
6819
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision has invalid attestation: ${value}`);
6820
+ }
6821
+ return value;
6822
+ }
6823
+ function terminalOutcomeFromPayload(payload) {
6824
+ const value = stringFromPayload(payload, "terminal_outcome");
6825
+ if (value !== "converged" && value !== "partial_overlap" && value !== "needs_input" && value !== "irreconcilable" && value !== "already_aligned") {
6826
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision has invalid outcome: ${value}`);
6827
+ }
6828
+ return value;
6829
+ }
6830
+ async function buildCruxDecisionRequest(api, ws, cruxId, action, opts = {}) {
6831
+ assertSafeCruxId(cruxId);
6832
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
6833
+ const map = latestConvergenceMap(events);
6834
+ const crux = getCruxFromMap(map, cruxId);
6835
+ const generationId = typeof crux.generation_id === "string" ? crux.generation_id : "";
6836
+ const sourceHash = typeof map.source_crux_map_hash === "string" ? map.source_crux_map_hash : "";
6837
+ const outcome = typeof crux.outcome === "string" ? crux.outcome : "";
6838
+ const latestSubDebateId = typeof crux.latest_sub_debate_id === "string" ? crux.latest_sub_debate_id : null;
6839
+ const bilateralMandateIntact = typeof crux.bilateral_mandate_intact_at_outcome === "boolean" ? crux.bilateral_mandate_intact_at_outcome : typeof crux.bilateral_mandate_intact === "boolean" ? crux.bilateral_mandate_intact : false;
6840
+ const synthesisText = typeof crux.synthesis_text === "string" ? crux.synthesis_text : "";
6841
+ const citationsA = citationsFromCrux(crux.citations_a);
6842
+ const citationsB = citationsFromCrux(crux.citations_b);
6843
+ if (!generationId) throw new LinkedClawError("missing_generation_id", `crux ${cruxId} has no generation_id.`);
6844
+ if (!sourceHash) throw new LinkedClawError("missing_source_hash", "latest convergence_map has no source_crux_map_hash.");
6845
+ if (!outcome) throw new LinkedClawError("missing_outcome", `crux ${cruxId} has no outcome.`);
6846
+ if (action === "accept" && outcome === "already_aligned") {
6847
+ throw new LinkedClawError(
6848
+ "attest_required",
6849
+ "outcome=already_aligned must be decided with `lc converge attest`."
6850
+ );
6851
+ }
6852
+ if (!synthesisText) throw new LinkedClawError("missing_synthesis_text", `crux ${cruxId} has no synthesis_text.`);
6853
+ const paBodyHash = computeDecisionBodyHash(synthesisText, citationsA, citationsB);
6854
+ let acceptedSynthesisText = synthesisText;
6855
+ let synthesisEdited = false;
6856
+ if (action === "accept") {
6857
+ const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
6858
+ if (existsSync4(stagingPath)) {
6859
+ const doc = readStaging(stagingPath);
6860
+ synthesisEdited = computePaBodyHash(doc.body) !== doc.frontmatter.pa_body_hash;
6861
+ if (synthesisEdited) acceptedSynthesisText = doc.body;
6862
+ }
6863
+ }
6864
+ const acceptedBodyHash = computeDecisionBodyHash(acceptedSynthesisText, citationsA, citationsB);
6865
+ return {
6866
+ convergence_map_generation_id: generationId,
6867
+ source_crux_map_hash: sourceHash,
6868
+ latest_sub_debate_id: latestSubDebateId,
6869
+ terminal_outcome: outcome,
6870
+ bilateral_mandate_intact: bilateralMandateIntact,
6871
+ attestation: classifyDecisionAttestation(action, outcome, bilateralMandateIntact, synthesisEdited),
6872
+ synthesis_edited: synthesisEdited,
6873
+ pa_body_hash: paBodyHash,
6874
+ accepted_body_hash: acceptedBodyHash,
6875
+ synthesis_text: acceptedSynthesisText,
6876
+ citations_a: citationsA,
6877
+ citations_b: citationsB,
6878
+ ...opts.message ? { user_message: opts.message } : {}
6879
+ };
6880
+ }
6881
+ async function postCruxDecision(api, ws, cruxId, action, opts = {}) {
6882
+ assertSafeCruxId(cruxId);
6883
+ const body = await buildCruxDecisionRequest(api, ws, cruxId, action, opts);
6884
+ const resp = action === "accept" ? await api.acceptCruxDecision(ws.runId, cruxId, body) : action === "reject" ? await api.rejectCruxDecision(ws.runId, cruxId, body) : await api.attestCruxDecision(ws.runId, cruxId, body);
6885
+ return { event_id: resp.event_id, body };
6886
+ }
6887
+ async function materializeAcceptedCrux(ctx, api, ws, cruxId, payload, opts = {}) {
6888
+ const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
6889
+ if (!existsSync4(stagingPath)) return { warning: `staging_not_found: ${stagingPath}` };
6890
+ const doc = readStaging(stagingPath);
6891
+ const fm = doc.frontmatter;
6892
+ const sourceDebate = await api.getDebate(ws.sourceDebateId);
6893
+ const body = stringFromPayload(payload, "synthesis_text");
6894
+ const citationsA = decisionPayloadCitations(payload.citations_a);
6895
+ const citationsB = decisionPayloadCitations(payload.citations_b);
6896
+ const acceptedBodyHash = stringFromPayload(payload, "accepted_body_hash");
6897
+ const computedAcceptedBodyHash = computeDecisionBodyHash(body, citationsA, citationsB);
6898
+ if (acceptedBodyHash !== computedAcceptedBodyHash) {
6899
+ return { warning: `decision_body_hash_mismatch: ${cruxId}` };
6900
+ }
6901
+ const paBodyHash = stringFromPayload(payload, "pa_body_hash");
6902
+ const synthesisEdited = booleanFromPayload(payload, "synthesis_edited");
6903
+ const attestation = attestationFromPayload(payload);
6904
+ const terminalOutcome = terminalOutcomeFromPayload(payload);
6905
+ const generationId = stringFromPayload(payload, "convergence_map_generation_id");
6906
+ const me = await getMyUserId(ctx);
6907
+ const acceptedDoc = {
6908
+ frontmatter: {
6909
+ ...fm,
6910
+ generation_id: generationId,
6911
+ pa_body_hash: paBodyHash,
6912
+ outcome: terminalOutcome,
6913
+ bilateral_mandate_intact: booleanFromPayload(payload, "bilateral_mandate_intact"),
6914
+ citations_a: citationsA,
6915
+ citations_b: citationsB,
6916
+ provenance: {
6917
+ signed_off_by: me,
6918
+ signed_off_at: (/* @__PURE__ */ new Date()).toISOString(),
6919
+ accepted_generation_id: generationId,
6920
+ attestation,
6921
+ synthesis_edited: synthesisEdited,
6922
+ ...opts.message ? { user_message: opts.message } : {},
6923
+ ...synthesisEdited ? { pa_body_hash: paBodyHash, accepted_body_hash: acceptedBodyHash } : {}
6924
+ }
6925
+ },
6926
+ userResponse: "",
6927
+ body
6928
+ };
6929
+ const topicSlug = slugify(sourceDebate.topic ?? ws.sourceDebateId);
6930
+ const synthSlug = extractSynthesisSlug(body);
6931
+ const finalDir = join5(ws.targetCorpus, "converged", topicSlug);
6932
+ const finalPath = safeAcceptedPath(finalDir, cruxId, synthSlug);
6933
+ if (existsSync4(finalPath) && readStaging(finalPath).body !== acceptedDoc.body) {
6934
+ return { warning: `sync_conflict: ${finalPath}` };
6935
+ }
6936
+ mkdirSync4(finalDir, { recursive: true });
6937
+ if (!existsSync4(finalPath) || dumpStaging(readStaging(finalPath)) !== dumpStaging(acceptedDoc)) {
6938
+ writeStaging(finalPath, acceptedDoc);
6939
+ }
6940
+ unlinkSync2(stagingPath);
6941
+ const gitWarning = tryGitAdd(finalPath);
6942
+ return {
6943
+ accepted_path: finalPath,
6944
+ attestation,
6945
+ synthesis_edited: synthesisEdited,
6946
+ ...gitWarning ? { warning: gitWarning } : {}
6947
+ };
6948
+ }
6949
+ async function syncTerminalDecisions(ctx, api, ws, opts = {}) {
6950
+ if (opts.cruxId) assertSafeCruxId(opts.cruxId);
6951
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
6952
+ const warnings = [];
6953
+ const mapEvent = latestConvergenceMapEvent(events);
6954
+ const map = mapEvent.payload;
6955
+ const cruxGeneration = /* @__PURE__ */ new Map();
6956
+ for (const crux of Array.isArray(map.cruxes) ? map.cruxes : []) {
6957
+ if (!crux || typeof crux !== "object" || Array.isArray(crux)) continue;
6958
+ const c = crux;
6959
+ if (typeof c.crux_id === "string" && typeof c.generation_id === "string") {
6960
+ cruxGeneration.set(c.crux_id, c.generation_id);
6961
+ }
6962
+ }
6963
+ const terminal = /* @__PURE__ */ new Map();
6964
+ for (const ev of [...events].sort((a, b) => a.seq - b.seq)) {
6965
+ if (ev.seq <= mapEvent.seq) continue;
6966
+ if (ev.signed_by !== mapEvent.signed_by) continue;
6967
+ const eventType = eventKind(ev);
6968
+ if (!["accept_attestation", "reject_attestation", "attest_only"].includes(eventType)) continue;
6969
+ const cid = typeof ev.payload.crux_id === "string" ? ev.payload.crux_id : "";
6970
+ if (!cid || terminal.has(cid)) continue;
6971
+ try {
6972
+ assertSafeCruxId(cid);
6973
+ } catch (err) {
6974
+ warnings.push(`${cid}: ${err.message}`);
6975
+ continue;
6976
+ }
6977
+ if (ev.payload.convergence_map_generation_id !== cruxGeneration.get(cid)) continue;
6978
+ terminal.set(cid, { eventType, payload: ev.payload });
6979
+ }
6980
+ if (opts.injectedTerminal) {
6981
+ assertSafeCruxId(opts.injectedTerminal.cruxId);
6982
+ terminal.set(opts.injectedTerminal.cruxId, {
6983
+ eventType: opts.injectedTerminal.eventType,
6984
+ payload: opts.injectedTerminal.payload
6985
+ });
6986
+ }
6987
+ const materialized = [];
6988
+ const cleaned = [];
6989
+ const release = acquireLock(ws.stagingDir);
6990
+ try {
6991
+ for (const [cid, terminalEvent] of terminal.entries()) {
6992
+ if (opts.cruxId && cid !== opts.cruxId) continue;
6993
+ const stagingPath = safeStagingPathFor(ws.stagingDir, cid);
6994
+ const eventType = terminalEvent.eventType;
6995
+ if (eventType === "accept_attestation") {
6996
+ const result = await materializeAcceptedCrux(ctx, api, ws, cid, terminalEvent.payload, opts);
6997
+ if (result.accepted_path) materialized.push(result.accepted_path);
6998
+ if (result.warning) warnings.push(`${cid}: ${result.warning}`);
6999
+ continue;
7000
+ }
7001
+ if (existsSync4(stagingPath)) {
7002
+ unlinkSync2(stagingPath);
7003
+ cleaned.push(cid);
7004
+ }
7005
+ }
7006
+ } finally {
7007
+ release();
7008
+ }
7009
+ return { materialized, cleaned, warnings };
7010
+ }
7011
+ function registerConvergeCommands(program2) {
7012
+ const converge = program2.command("converge").description("Convergence: bilateral merger of two crux maps into shared corpus");
7013
+ converge.command("run [ref]").description("Start a convergence run (Owner A) or accept (Owner B with --accept), then sync staging").option("--target-corpus <path>", "Absolute path to the target corpus directory").option("--staging-dir <path>", "Override staging directory").option("--accept", "Owner B: accept an existing run by run_id").option("--source-debate-id <id>", "Owner B fallback when /convergence/runs/{run_id} is unavailable").option("--force-regenerate", "Bypass source-hash drift check and regenerate staging files").option("--wait <secs>", "Poll until terminal_emitted or timeout", parseInt).action(
7014
+ async (ref, opts) => {
7015
+ await runCommand(async () => {
7016
+ const ctx = buildContext();
7017
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7018
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7019
+ let ws;
7020
+ const resolvedStagingDir = opts.stagingDir ? resolveAbs(opts.stagingDir) : null;
7021
+ const metaExisting = resolvedStagingDir ? readRunMeta(resolvedStagingDir) : null;
7022
+ if (opts.accept) {
7023
+ if (!ref) {
7024
+ throw new LinkedClawError("missing_run_id", "--accept requires the run_id as a positional argument.");
7025
+ }
7026
+ const runId = ref;
7027
+ if (metaExisting && resolvedStagingDir) {
7028
+ const meta = metaExisting;
7029
+ ws = {
7030
+ runId: meta.run_id,
7031
+ sourceDebateId: meta.source_debate_id,
7032
+ paAgentId: meta.pa_agent_id,
7033
+ targetCorpus: meta.target_corpus,
7034
+ stagingDir: resolvedStagingDir
7035
+ };
7036
+ } else {
7037
+ if (!opts.targetCorpus) {
7038
+ throw new LinkedClawError(
7039
+ "missing_target_corpus",
7040
+ "Owner B --accept requires --target-corpus on first call."
7041
+ );
7042
+ }
7043
+ const targetCorpus = resolveAbs(opts.targetCorpus);
7044
+ let sourceDebateId;
7045
+ let paAgentId;
7046
+ let principalAgentId;
7047
+ if (opts.sourceDebateId) {
7048
+ sourceDebateId = opts.sourceDebateId;
7049
+ paAgentId = await api.discoverPaAgentId();
7050
+ const sourceDebate = await api.getDebate(sourceDebateId);
7051
+ principalAgentId = sourceDebate.agent_b_id;
7052
+ } else {
7053
+ const runMeta = await api.getRun(runId);
7054
+ sourceDebateId = runMeta.source_debate_id;
7055
+ paAgentId = runMeta.pa_agent_id;
7056
+ principalAgentId = runMeta.agent_b_id;
7057
+ }
7058
+ const existing = await api.findExistingMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7059
+ if (!existing) await api.issueMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7060
+ await api.acceptOwnerB(runId);
7061
+ const stagingDir = join5(targetCorpus, "converged", "staging", runId);
7062
+ mkdirSync4(stagingDir, { recursive: true });
7063
+ const meta = {
7064
+ run_id: runId,
7065
+ source_debate_id: sourceDebateId,
7066
+ pa_agent_id: paAgentId,
7067
+ target_corpus: targetCorpus,
7068
+ owner_role: "b"
7069
+ };
7070
+ writeRunMeta(stagingDir, meta);
7071
+ ws = { runId, sourceDebateId, paAgentId, targetCorpus, stagingDir };
7072
+ }
7073
+ } else if (!metaExisting) {
7074
+ if (!ref) {
7075
+ throw new LinkedClawError(
7076
+ "missing_source_debate_id",
7077
+ "First run requires a source_debate_id argument. Use --staging-dir to resume an existing run."
7078
+ );
7079
+ }
7080
+ if (!opts.targetCorpus) {
7081
+ throw new LinkedClawError("missing_target_corpus", "First call requires --target-corpus.");
7082
+ }
7083
+ const targetCorpus = resolveAbs(opts.targetCorpus);
7084
+ const paAgentId = await api.discoverPaAgentId();
7085
+ const sourceDebate = await api.getDebate(ref);
7086
+ const principalAgentId = sourceDebate.agent_a_id;
7087
+ const existing = await api.findExistingMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7088
+ if (!existing) await api.issueMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7089
+ const { run_id } = await api.startRun(ref);
7090
+ const stagingDir = join5(targetCorpus, "converged", "staging", run_id);
7091
+ mkdirSync4(stagingDir, { recursive: true });
7092
+ const meta = {
7093
+ run_id,
7094
+ source_debate_id: ref,
7095
+ pa_agent_id: paAgentId,
7096
+ target_corpus: targetCorpus,
7097
+ owner_role: "a"
7098
+ };
7099
+ writeRunMeta(stagingDir, meta);
7100
+ ws = { runId: run_id, sourceDebateId: ref, paAgentId, targetCorpus, stagingDir };
7101
+ } else {
7102
+ ws = await resolveWorkspace({ stagingDir: opts.stagingDir });
7103
+ }
7104
+ const refreshStaging = async (cruxes, canonicalSourceHash2) => {
7105
+ for (const c of cruxes) {
7106
+ if (!c.latest_sub_debate_id) continue;
7107
+ const subD = await api.getDebate(c.latest_sub_debate_id);
7108
+ const subEvs = await api.getCommonsLogEvents(subD.commons_log_id, { limit: 2e3 });
7109
+ const outcomeEv = [...subEvs.events].reverse().find(
7110
+ (e) => e.payload.event_type === "convergence_outcome" || e.event_type === "convergence_outcome"
7111
+ );
7112
+ if (!outcomeEv) continue;
7113
+ const op = outcomeEv.payload;
7114
+ const body = buildPaBody(op);
7115
+ const newPaBodyHash = computePaBodyHash(body);
7116
+ const path2 = safeStagingPathFor(ws.stagingDir, c.crux_id);
7117
+ const existingDoc = existsSync4(path2) ? readStaging(path2) : null;
7118
+ if (existingDoc && existingDoc.frontmatter.pa_body_hash === newPaBodyHash) continue;
7119
+ const fm = {
7120
+ debate_id: ws.sourceDebateId,
7121
+ run_id: ws.runId,
7122
+ crux_id: c.crux_id,
7123
+ sub_debate_chain: c.sub_debate_chain,
7124
+ latest_sub_debate_id: c.latest_sub_debate_id,
7125
+ source_crux_map_hash: canonicalSourceHash2,
7126
+ generation_id: `gen_${ws.runId.slice(-8)}`,
7127
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
7128
+ pa_body_hash: newPaBodyHash,
7129
+ outcome: op.outcome ?? c.outcome,
7130
+ bilateral_mandate_intact: typeof op.bilateral_mandate_intact === "boolean" ? op.bilateral_mandate_intact : c.bilateral_mandate_intact ?? false,
7131
+ citations_a: Array.isArray(op.citations_a) ? op.citations_a : [],
7132
+ citations_b: Array.isArray(op.citations_b) ? op.citations_b : [],
7133
+ mod_progress_summary: op.final_progress_signal != null && typeof op.final_progress_signal === "object" && !Array.isArray(op.final_progress_signal) ? op.final_progress_signal : {},
7134
+ attested_by_user: existingDoc?.frontmatter.attested_by_user ?? false
7135
+ };
7136
+ writeStaging(path2, { frontmatter: fm, userResponse: existingDoc?.userResponse ?? "", body });
7137
+ }
7138
+ };
7139
+ let summary;
7140
+ let canonicalSourceHash = "";
7141
+ {
7142
+ const release = acquireLock(ws.stagingDir);
7143
+ try {
7144
+ for (const fn of listCruxFiles(ws.stagingDir)) {
7145
+ const path2 = join5(ws.stagingDir, fn);
7146
+ const doc = readStaging(path2);
7147
+ const text = (doc.userResponse || "").trim();
7148
+ if (!text) continue;
7149
+ const subDebateId = doc.frontmatter.latest_sub_debate_id;
7150
+ if (!subDebateId) continue;
7151
+ const subDebate = await api.getDebate(subDebateId);
7152
+ await api.appendCommonsLog(subDebate.commons_log_id, "owner_clarification", {
7153
+ event_type: "owner_clarification",
7154
+ content: text,
7155
+ in_response_to_event_id: null
7156
+ });
7157
+ const round = countPreviouslyClarifiedSections(doc.body) + 1;
7158
+ doc.userResponse = "";
7159
+ doc.body = doc.body.trimEnd() + `
7160
+
7161
+ # Previously clarified (round ${round})
7162
+
7163
+ ${text}
7164
+ `;
7165
+ writeStaging(path2, doc);
7166
+ }
7167
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
7168
+ summary = reduceRunState(ws, events);
7169
+ const sourceDebate = await api.getDebate(ws.sourceDebateId);
7170
+ const sourceEvents = await api.getCommonsLogEvents(sourceDebate.commons_log_id, {
7171
+ limit: 2e3
7172
+ });
7173
+ const liveSourceHash = recomputeSourceCruxMapHash(sourceEvents.events);
7174
+ const recordedHash = recordedSourceHash(events);
7175
+ if (recordedHash && liveSourceHash && liveSourceHash !== recordedHash && !opts.forceRegenerate) {
7176
+ throw new LinkedClawError(
7177
+ "source_crux_map_drift",
7178
+ `Source crux-map changed since run started (recorded=${recordedHash} live=${liveSourceHash}). Re-run with --force-regenerate.`
7179
+ );
7180
+ }
7181
+ canonicalSourceHash = recordedHash ?? liveSourceHash ?? "";
7182
+ await refreshStaging(summary.cruxes, canonicalSourceHash);
7183
+ } finally {
7184
+ release();
7185
+ }
7186
+ }
7187
+ if (opts.wait && !summary.terminal_emitted) {
7188
+ const deadline = Date.now() + opts.wait * 1e3;
7189
+ let polledEvents = null;
7190
+ while (Date.now() < deadline) {
7191
+ await new Promise((r) => setTimeout(r, 2e3));
7192
+ const { events: polled } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
7193
+ if (reduceRunState(ws, polled).terminal_emitted) {
7194
+ polledEvents = polled;
7195
+ break;
7196
+ }
7197
+ }
7198
+ if (polledEvents) {
7199
+ summary = reduceRunState(ws, polledEvents);
7200
+ const release2 = acquireLock(ws.stagingDir);
7201
+ try {
7202
+ await refreshStaging(summary.cruxes, canonicalSourceHash);
7203
+ } finally {
7204
+ release2();
7205
+ }
7206
+ }
7207
+ }
7208
+ return { run_id: ws.runId, summary, terminal_emitted: summary.terminal_emitted };
7209
+ });
7210
+ }
7211
+ );
7212
+ converge.command("clarify <sub_debate_id> <text>").description("Post owner_clarification.v1 directly to a sub-debate's Commons Log").action(async (subDebateId, text) => {
7213
+ await runCommand(async () => {
7214
+ const ctx = buildContext();
7215
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7216
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7217
+ const subDebate = await api.getDebate(subDebateId);
7218
+ const { seq } = await api.appendCommonsLog(subDebate.commons_log_id, "owner_clarification", {
7219
+ event_type: "owner_clarification",
7220
+ content: text,
7221
+ in_response_to_event_id: null
7222
+ });
7223
+ return { sub_debate_id: subDebateId, commons_log_id: subDebate.commons_log_id, seq };
7224
+ });
7225
+ });
7226
+ converge.command("attest <crux_id>").description("POST an attest_only decision to the Convergence PA").option("--run-id <id>").option("--staging-dir <path>").action(async (cruxId, opts) => {
7227
+ await runCommand(async () => {
7228
+ const ctx = buildContext();
7229
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7230
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7231
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7232
+ const { event_id } = await postCruxDecision(api, ws, cruxId, "attest");
7233
+ return { run_id: ws.runId, crux_id: cruxId, action: "attest", event_id, synced: false };
7234
+ });
7235
+ });
7236
+ converge.command("accept <crux_id>").description("POST an accept decision to the Convergence PA").option("--run-id <id>").option("--staging-dir <path>").option("--message <text>", "Optional user_message recorded in provenance").option("--with-sync", "Compatibility: after the PA accepts, materialize local corpus files").action(async (cruxId, opts) => {
7237
+ await runCommand(async () => {
7238
+ const ctx = buildContext();
7239
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7240
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7241
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7242
+ const { event_id, body } = await postCruxDecision(api, ws, cruxId, "accept", { message: opts.message });
7243
+ if (opts.withSync) {
7244
+ const sync = await syncTerminalDecisions(ctx, api, ws, {
7245
+ cruxId,
7246
+ message: opts.message,
7247
+ injectedTerminal: {
7248
+ cruxId,
7249
+ eventType: decisionEventTypeForAction("accept"),
7250
+ payload: { event_type: decisionEventTypeForAction("accept"), crux_id: cruxId, ...body }
7251
+ }
7252
+ });
7253
+ return {
7254
+ run_id: ws.runId,
7255
+ crux_id: cruxId,
7256
+ action: "accept",
7257
+ event_id,
7258
+ synced: true,
7259
+ ...sync
7260
+ };
7261
+ }
7262
+ return { run_id: ws.runId, crux_id: cruxId, action: "accept", event_id, synced: false };
7263
+ });
7264
+ });
7265
+ converge.command("reject <crux_id>").description("POST a reject decision to the Convergence PA").option("--run-id <id>").option("--staging-dir <path>").action(async (cruxId, opts) => {
7266
+ await runCommand(async () => {
7267
+ const ctx = buildContext();
7268
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7269
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7270
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7271
+ const { event_id } = await postCruxDecision(api, ws, cruxId, "reject");
7272
+ return { run_id: ws.runId, crux_id: cruxId, action: "reject", event_id, synced: false };
7273
+ });
7274
+ });
7275
+ converge.command("sync").description("Materialize PA decision events into local corpus/staging files").option("--run-id <id>").option("--staging-dir <path>").option("--crux-id <id>", "Limit sync to one crux").action(async (opts) => {
7276
+ await runCommand(async () => {
7277
+ const ctx = buildContext();
7278
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7279
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7280
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7281
+ const result = await syncTerminalDecisions(ctx, api, ws, { cruxId: opts.cruxId });
7282
+ return { run_id: ws.runId, synced: true, ...result };
7283
+ });
7284
+ });
7285
+ converge.command("review").description("List staging cruxes; surface already_aligned cruxes prominently").option("--run-id <id>").option("--staging-dir <path>").action(async (opts) => {
7286
+ await runCommand(async () => {
7287
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7288
+ const files = listCruxFiles(ws.stagingDir);
7289
+ const cruxes = [];
7290
+ for (const fn of files) {
7291
+ const doc = readStaging(join5(ws.stagingDir, fn));
7292
+ const fm = doc.frontmatter;
7293
+ cruxes.push({
7294
+ crux_id: fm.crux_id,
7295
+ outcome: fm.outcome,
7296
+ bilateral_mandate_intact: fm.bilateral_mandate_intact,
7297
+ attested_by_user: fm.attested_by_user,
7298
+ latest_sub_debate_id: fm.latest_sub_debate_id,
7299
+ has_user_response: (doc.userResponse || "").trim().length > 0,
7300
+ next_action: fm.outcome === "already_aligned" && !fm.attested_by_user ? "attest" : fm.outcome === "needs_input" ? "clarify_or_accept" : "accept_or_reject"
7301
+ });
7302
+ }
7303
+ const alignedAwaitingAttest = cruxes.filter((c) => c.outcome === "already_aligned" && !c.attested_by_user);
7304
+ return {
7305
+ run_id: ws.runId,
7306
+ staging_dir: ws.stagingDir,
7307
+ cruxes,
7308
+ already_aligned_awaiting_attest: alignedAwaitingAttest.map((c) => c.crux_id)
7309
+ };
7310
+ });
7311
+ });
7312
+ converge.command("status").description("Show reduced run state from the convergence run-log").option("--run-id <id>").option("--staging-dir <path>").option("--all", "List every run-log event (no reduction)").action(async (opts) => {
7313
+ await runCommand(async () => {
7314
+ const ctx = buildContext();
7315
+ if (!ctx.cfg.apiKey) {
7316
+ throw new Error("missing apiKey \u2014 run `linkedclaw login` first");
7317
+ }
7318
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7319
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7320
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 1e3 });
7321
+ if (opts.all) return { run_id: ws.runId, events };
7322
+ return reduceRunState(ws, events);
7323
+ });
7324
+ });
7325
+ }
7326
+ function reduceRunState(ws, events) {
7327
+ let started_at = null;
7328
+ let owner_b_accepted = false;
7329
+ let terminal_emitted = false;
7330
+ const cruxMap = /* @__PURE__ */ new Map();
7331
+ for (const ev of events) {
7332
+ const p = ev.payload;
7333
+ switch (ev.event_type) {
7334
+ case "run_started":
7335
+ started_at = ev.appended_at;
7336
+ break;
7337
+ case "owner_b_accepted":
7338
+ owner_b_accepted = true;
7339
+ break;
7340
+ case "sub_debate_dispatched": {
7341
+ const cruxId = p.crux_id;
7342
+ const subDebateId = p.sub_debate_id;
7343
+ if (!cruxMap.has(cruxId)) {
7344
+ cruxMap.set(cruxId, {
7345
+ crux_id: cruxId,
7346
+ latest_sub_debate_id: subDebateId,
7347
+ sub_debate_chain: [subDebateId],
7348
+ outcome: null,
7349
+ bilateral_mandate_intact: null
7350
+ });
7351
+ } else {
7352
+ const entry = cruxMap.get(cruxId);
7353
+ entry.latest_sub_debate_id = subDebateId;
7354
+ entry.sub_debate_chain.push(subDebateId);
7355
+ }
7356
+ break;
7357
+ }
7358
+ case "sub_debate_outcome_observed": {
7359
+ const cruxId = p.crux_id;
7360
+ const entry = cruxMap.get(cruxId);
7361
+ if (entry) {
7362
+ entry.outcome = p.outcome;
7363
+ if (typeof p.bilateral_mandate_intact === "boolean") {
7364
+ entry.bilateral_mandate_intact = p.bilateral_mandate_intact;
7365
+ }
7366
+ }
7367
+ break;
7368
+ }
7369
+ case "convergence_map":
7370
+ terminal_emitted = true;
7371
+ break;
7372
+ default:
7373
+ if (ev.event_type.startsWith("terminal_")) {
7374
+ terminal_emitted = true;
7375
+ }
7376
+ }
7377
+ }
7378
+ return {
7379
+ run_id: ws.runId,
7380
+ source_debate_id: ws.sourceDebateId,
7381
+ started_at,
7382
+ owner_b_accepted,
7383
+ cruxes: Array.from(cruxMap.values()),
7384
+ terminal_emitted
7385
+ };
7386
+ }
7387
+
5664
7388
  // src/commands/provider.ts
5665
- import { readFileSync as readFileSync2 } from "fs";
5666
- import { load as yamlLoad2 } from "js-yaml";
7389
+ import { readFileSync as readFileSync6 } from "fs";
7390
+ import { load as yamlLoad4 } from "js-yaml";
5667
7391
 
5668
7392
  // ../../sdk/provider-runtime-ts/dist/index.js
5669
7393
  import { EventEmitter } from "events";
@@ -5855,8 +7579,8 @@ var nodeWsConnector = async (url) => {
5855
7579
  const ws = new WebSocket3(url);
5856
7580
  const transport = {
5857
7581
  send: (data) => {
5858
- return new Promise((resolve, reject) => {
5859
- ws.send(data, (err) => err ? reject(err) : resolve());
7582
+ return new Promise((resolve3, reject) => {
7583
+ ws.send(data, (err) => err ? reject(err) : resolve3());
5860
7584
  });
5861
7585
  },
5862
7586
  close: (code, reason) => {
@@ -5872,8 +7596,8 @@ var nodeWsConnector = async (url) => {
5872
7596
  ws.on("error", (err) => fn(err));
5873
7597
  }
5874
7598
  };
5875
- const ready = new Promise((resolve, reject) => {
5876
- ws.once("open", () => resolve());
7599
+ const ready = new Promise((resolve3, reject) => {
7600
+ ws.once("open", () => resolve3());
5877
7601
  ws.once("error", (err) => reject(err));
5878
7602
  });
5879
7603
  return { transport, ready };
@@ -6308,12 +8032,12 @@ function normalizeReply(reply, nextSeq) {
6308
8032
  return { payload: reply, seq: nextSeq };
6309
8033
  }
6310
8034
  function withTimeout(p, ms, code) {
6311
- return new Promise((resolve, reject) => {
8035
+ return new Promise((resolve3, reject) => {
6312
8036
  const t = setTimeout(() => reject(new HandlerError(code, `timed out after ${ms}ms`)), ms);
6313
8037
  p.then(
6314
8038
  (v) => {
6315
8039
  clearTimeout(t);
6316
- resolve(v);
8040
+ resolve3(v);
6317
8041
  },
6318
8042
  (err) => {
6319
8043
  clearTimeout(t);
@@ -6327,7 +8051,7 @@ function escapeRegex(s) {
6327
8051
  }
6328
8052
 
6329
8053
  // src/handlers/subprocess.ts
6330
- import { spawn } from "child_process";
8054
+ import { spawn as spawn2 } from "child_process";
6331
8055
  import { randomUUID } from "crypto";
6332
8056
  import { createInterface } from "readline";
6333
8057
  var SubprocessHandler = class {
@@ -6339,7 +8063,7 @@ var SubprocessHandler = class {
6339
8063
  this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e5;
6340
8064
  const shell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
6341
8065
  const shellArgs = process.platform === "win32" ? ["/c", opts.cmd] : ["-c", opts.cmd];
6342
- this.child = spawn(shell, shellArgs, {
8066
+ this.child = spawn2(shell, shellArgs, {
6343
8067
  stdio: ["pipe", "pipe", "inherit"],
6344
8068
  cwd: opts.cwd,
6345
8069
  env: { ...process.env, ...opts.env }
@@ -6376,10 +8100,10 @@ var SubprocessHandler = class {
6376
8100
  async onInvoke(evt) {
6377
8101
  return this.request(evt);
6378
8102
  }
6379
- async onBroadcastOffer(evt) {
8103
+ async onGigTaskOffer(evt) {
6380
8104
  return this.request(evt);
6381
8105
  }
6382
- async onBroadcastExecute(evt) {
8106
+ async onGigTaskExecute(evt) {
6383
8107
  return this.request(evt);
6384
8108
  }
6385
8109
  // ───── shutdown ─────
@@ -6400,7 +8124,7 @@ var SubprocessHandler = class {
6400
8124
  request(frame) {
6401
8125
  const id = randomUUID();
6402
8126
  const line = JSON.stringify({ id, ...frame }) + "\n";
6403
- return new Promise((resolve, reject) => {
8127
+ return new Promise((resolve3, reject) => {
6404
8128
  const timer = setTimeout(() => {
6405
8129
  this.pending.delete(id);
6406
8130
  reject(new Error(`handler_timeout: no response for ${frame.type} after ${this.requestTimeoutMs}ms`));
@@ -6408,7 +8132,7 @@ var SubprocessHandler = class {
6408
8132
  if (typeof timer.unref === "function") {
6409
8133
  timer.unref();
6410
8134
  }
6411
- this.pending.set(id, { resolve, reject, timer });
8135
+ this.pending.set(id, { resolve: resolve3, reject, timer });
6412
8136
  this.stdin.write(line, (err) => {
6413
8137
  if (err) {
6414
8138
  this.pending.delete(id);
@@ -6456,9 +8180,9 @@ var SubprocessHandler = class {
6456
8180
  // src/commands/provider.ts
6457
8181
  function registerProviderCommands(program2) {
6458
8182
  const provider = program2.command("provider").description("Provider-side commands");
6459
- provider.command("register <config>").description('Register (create/update) an agent listing from a provider YAML file. Use "-" for stdin.').option("--human", "Human-readable output").action(async (path, opts) => {
8183
+ provider.command("register <config>").description('Register (create/update) an agent listing from a provider YAML file. Use "-" for stdin.').option("--human", "Human-readable output").action(async (path2, opts) => {
6460
8184
  await runCommand(async () => {
6461
- const cfg = await loadProviderYaml(path);
8185
+ const cfg = await loadProviderYaml(path2);
6462
8186
  const { providerClient } = buildContext();
6463
8187
  const body = buildCreateAgentRequest(cfg);
6464
8188
  if (cfg.agentId) {
@@ -6474,7 +8198,7 @@ function registerProviderCommands(program2) {
6474
8198
  });
6475
8199
  provider.command("update <listing_id>").description("Patch an existing agent listing. Body = JSON file or stdin.").requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)').option("--human", "Human-readable output").action(async (listingId, opts) => {
6476
8200
  await runCommand(async () => {
6477
- const raw = opts.body === "-" ? await readStdin() : readFileSync2(opts.body, "utf8");
8201
+ const raw = opts.body === "-" ? await readStdin() : readFileSync6(opts.body, "utf8");
6478
8202
  const body = JSON.parse(raw);
6479
8203
  const { providerClient } = buildContext();
6480
8204
  return providerClient.updateAgent(listingId, body);
@@ -6486,9 +8210,9 @@ function registerProviderCommands(program2) {
6486
8210
  return consumer.discover({ owner: "me" });
6487
8211
  }, { human: opts.human });
6488
8212
  });
6489
- provider.command("run <config>").description("Run a provider daemon: connect WS, dispatch events to handler subprocess").option("--handler-cmd <cmd>", "Shell command to spawn as the handler child process").option("--handler-http <url>", "HTTP webhook URL to POST each event to (alternative to --handler-cmd)").option("--human", "Human-readable status output").action(async (path, opts) => {
8213
+ provider.command("run <config>").description("Run a provider daemon: connect WS, dispatch events to handler subprocess").option("--handler-cmd <cmd>", "Shell command to spawn as the handler child process").option("--handler-http <url>", "HTTP webhook URL to POST each event to (alternative to --handler-cmd)").option("--human", "Human-readable status output").action(async (path2, opts) => {
6490
8214
  try {
6491
- const yamlCfg = await loadProviderYaml(path);
8215
+ const yamlCfg = await loadProviderYaml(path2);
6492
8216
  if (!yamlCfg.agentId) {
6493
8217
  process.stderr.write(
6494
8218
  JSON.stringify({ error: "provider_unconfigured", message: "agentId missing \u2014 run `linkedclaw provider register` first or set it in YAML" }) + "\n"
@@ -6516,7 +8240,7 @@ function registerProviderCommands(program2) {
6516
8240
  const handler = opts.handlerCmd ? new SubprocessHandler({ cmd: opts.handlerCmd }) : makeHttpHandler(opts.handlerHttp);
6517
8241
  const runtime = new ProviderRuntime({
6518
8242
  cloud: {
6519
- broadcasts: {
8243
+ gigTasks: {
6520
8244
  accept: (taskId, body) => providerClient.acceptGigTask(taskId, body),
6521
8245
  submit: (taskId, body) => providerClient.submitGigTask(taskId, body)
6522
8246
  }
@@ -6555,7 +8279,7 @@ function registerProviderCommands(program2) {
6555
8279
  process.exit(1);
6556
8280
  }
6557
8281
  });
6558
- provider.command("pick <bct_id>").description("Manually accept a broadcast task (provider side)").requiredOption("--agent-id <agt_id>", "Which of your agents is accepting").option("--slot-key <key>", "Slot key for sliced broadcasts").option("--human", "Human-readable output").action(async (taskId, opts) => {
8282
+ provider.command("pick <bct_id>").description("Manually accept a gig task (provider side)").requiredOption("--agent-id <agt_id>", "Which of your agents is accepting").option("--slot-key <key>", "Slot key for sliced gig tasks").option("--human", "Human-readable output").action(async (taskId, opts) => {
6559
8283
  await runCommand(async () => {
6560
8284
  const { providerClient } = buildContext();
6561
8285
  const body = { agent_id: opts.agentId };
@@ -6563,20 +8287,20 @@ function registerProviderCommands(program2) {
6563
8287
  return providerClient.acceptGigTask(taskId, body);
6564
8288
  }, { human: opts.human });
6565
8289
  });
6566
- provider.command("submit <bct_id> <result_file>").description('Submit a broadcast result. result_file = JSON path or "-" for stdin.').option("--human", "Human-readable output").action(async (taskId, resultFile, opts) => {
8290
+ provider.command("submit <bct_id> <result_file>").description('Submit a gig task result. result_file = JSON path or "-" for stdin.').option("--human", "Human-readable output").action(async (taskId, resultFile, opts) => {
6567
8291
  await runCommand(async () => {
6568
- const raw = resultFile === "-" ? await readStdin() : readFileSync2(resultFile, "utf8");
8292
+ const raw = resultFile === "-" ? await readStdin() : readFileSync6(resultFile, "utf8");
6569
8293
  const body = JSON.parse(raw);
6570
8294
  const { providerClient } = buildContext();
6571
8295
  return providerClient.submitGigTask(taskId, body);
6572
8296
  }, { human: opts.human });
6573
8297
  });
6574
8298
  }
6575
- async function loadProviderYaml(path) {
6576
- const raw = path === "-" ? await readStdin() : readFileSync2(path, "utf8");
6577
- const parsed = yamlLoad2(raw);
8299
+ async function loadProviderYaml(path2) {
8300
+ const raw = path2 === "-" ? await readStdin() : readFileSync6(path2, "utf8");
8301
+ const parsed = yamlLoad4(raw);
6578
8302
  if (parsed === null || typeof parsed !== "object") {
6579
- throw new Error(`provider config ${path} is not a YAML object`);
8303
+ throw new Error(`provider config ${path2} is not a YAML object`);
6580
8304
  }
6581
8305
  return parsed;
6582
8306
  }
@@ -6617,10 +8341,10 @@ function makeHttpHandler(url) {
6617
8341
  async onInvoke(evt) {
6618
8342
  return await postEvent(url, evt);
6619
8343
  },
6620
- async onBroadcastOffer(evt) {
8344
+ async onGigTaskOffer(evt) {
6621
8345
  return await postEvent(url, evt);
6622
8346
  },
6623
- async onBroadcastExecute(evt) {
8347
+ async onGigTaskExecute(evt) {
6624
8348
  return await postEvent(url, evt);
6625
8349
  }
6626
8350
  };
@@ -6636,10 +8360,10 @@ async function postEvent(url, body) {
6636
8360
  }
6637
8361
 
6638
8362
  // src/commands/requester.ts
6639
- import { readFileSync as readFileSync3 } from "fs";
6640
- import { load as yamlLoad3 } from "js-yaml";
8363
+ import { readFileSync as readFileSync7 } from "fs";
8364
+ import { load as yamlLoad5 } from "js-yaml";
6641
8365
  function registerRequesterCommands(program2) {
6642
- program2.command("search <capability>").description("Search public agent listings by capability").option("--owner <owner>", '"me" or a "usr_..." id').option("--status <status>", "filter by status (online/offline/disabled)").option("--sort <sort>", "newest | price_asc | price_desc | trust").option("--human", "Human-readable output").action(async (capability, opts) => {
8366
+ program2.command("search <capability>").description("Search public agent listings by capability").option("--owner <owner>", '"me" or a "usr_..." id').option("--status <status>", "filter by status (online/offline/disabled)").option("--sort <sort>", "newest | trust (default)").option("--human", "Human-readable output").action(async (capability, opts) => {
6643
8367
  await runCommand(async () => {
6644
8368
  const { requesterFlows } = buildContext();
6645
8369
  return requesterFlows.search(capability, {
@@ -6734,24 +8458,24 @@ function registerRequesterCommands(program2) {
6734
8458
  return invokeAgent(consumer, agentId, body);
6735
8459
  }, { human: opts.human });
6736
8460
  });
6737
- const broadcast = program2.command("broadcast").description("Broadcast task commands");
6738
- broadcast.command("create <manifest>").description(
6739
- 'Create a broadcast task from a YAML/JSON manifest file. Use "-" for stdin. Required fields: capability, instruction, target_providers, credits_per_provider.'
8461
+ const gigTask = program2.command("gig-task").description("Gig Task commands");
8462
+ gigTask.command("create <manifest>").description(
8463
+ 'Create a gig task from a YAML/JSON manifest file. Use "-" for stdin. Required fields: capability, instruction, target_providers, credits_per_provider.'
6740
8464
  ).option("--human", "Human-readable output").action(async (manifestPath, opts) => {
6741
8465
  await runCommand(async () => {
6742
8466
  const { consumer } = buildContext();
6743
- const raw = manifestPath === "-" ? await readStdin() : readFileSync3(manifestPath, "utf8");
8467
+ const raw = manifestPath === "-" ? await readStdin() : readFileSync7(manifestPath, "utf8");
6744
8468
  const body = parseYamlOrJson(raw);
6745
8469
  return consumer.createGigTask(body);
6746
8470
  }, { human: opts.human });
6747
8471
  });
6748
- broadcast.command("get <bct_id>").description("Get a broadcast task by id").option("--human", "Human-readable output").action(async (taskId, opts) => {
8472
+ gigTask.command("get <bct_id>").description("Get a gig task by id").option("--human", "Human-readable output").action(async (taskId, opts) => {
6749
8473
  await runCommand(async () => {
6750
8474
  const { consumer } = buildContext();
6751
8475
  return consumer.getGigTask(taskId);
6752
8476
  }, { human: opts.human });
6753
8477
  });
6754
- broadcast.command("list").description("List broadcasts I own").option("--status <s>", "Filter by status").option("--human", "Human-readable output").action(async (opts) => {
8478
+ gigTask.command("list").description("List gig tasks I own").option("--status <s>", "Filter by status").option("--human", "Human-readable output").action(async (opts) => {
6755
8479
  await runCommand(async () => {
6756
8480
  const { consumer } = buildContext();
6757
8481
  return consumer.listGigTasks({
@@ -6759,13 +8483,13 @@ function registerRequesterCommands(program2) {
6759
8483
  });
6760
8484
  }, { human: opts.human });
6761
8485
  });
6762
- broadcast.command("available").description("List open broadcasts I could pick up (as provider)").option("--human", "Human-readable output").action(async (opts) => {
8486
+ gigTask.command("available").description("List open gig tasks I could pick up (as provider)").option("--human", "Human-readable output").action(async (opts) => {
6763
8487
  await runCommand(async () => {
6764
8488
  const { providerClient } = buildContext();
6765
8489
  return providerClient.listGigTasksAvailable();
6766
8490
  }, { human: opts.human });
6767
8491
  });
6768
- broadcast.command("accept <bct_id>").description("Accept a broadcast (provider side) \u2014 returns a result_id").requiredOption("--agent-id <agt_id>", "Which of your agents is accepting").option("--slot-key <key>", "Slot key for sliced broadcasts").option("--human", "Human-readable output").action(async (taskId, opts) => {
8492
+ gigTask.command("accept <bct_id>").description("Accept a gig task (provider side) \u2014 returns a result_id").requiredOption("--agent-id <agt_id>", "Which of your agents is accepting").option("--slot-key <key>", "Slot key for sliced gig tasks").option("--human", "Human-readable output").action(async (taskId, opts) => {
6769
8493
  await runCommand(async () => {
6770
8494
  const { providerClient } = buildContext();
6771
8495
  const body = { agent_id: opts.agentId };
@@ -6773,8 +8497,8 @@ function registerRequesterCommands(program2) {
6773
8497
  return providerClient.acceptGigTask(taskId, body);
6774
8498
  }, { human: opts.human });
6775
8499
  });
6776
- broadcast.command("submit <bct_id>").description(
6777
- "Submit broadcast result (provider side). Body must include `result_data` (string) and may include `result_payload` (object) and `proof` (array)."
8500
+ gigTask.command("submit <bct_id>").description(
8501
+ "Submit gig task result (provider side). Body must include `result_data` (string) and may include `result_payload` (object) and `proof` (array)."
6778
8502
  ).requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)').option("--human", "Human-readable output").action(async (taskId, opts) => {
6779
8503
  await runCommand(async () => {
6780
8504
  const { providerClient } = buildContext();
@@ -6804,6 +8528,36 @@ function registerRequesterCommands(program2) {
6804
8528
  return consumer.getBalance();
6805
8529
  }, { human: opts.human });
6806
8530
  });
8531
+ program2.command("show <agent_id>").description("Show full agent listing including capabilities_meta").option("--capability <name>", "Print only this capability's meta entry").option("--human", "Human-readable output").action(async (agentId, opts) => {
8532
+ await runCommand(async () => {
8533
+ const { consumer } = buildContext();
8534
+ const agent = await consumer.getAgent(agentId);
8535
+ if (opts.capability) {
8536
+ const meta = agent.capabilities_meta?.[opts.capability];
8537
+ if (!meta) {
8538
+ throw new Error(
8539
+ `agent ${agentId} has no capabilities_meta entry for ${JSON.stringify(opts.capability)}`
8540
+ );
8541
+ }
8542
+ return meta;
8543
+ }
8544
+ return agent;
8545
+ }, { human: opts.human });
8546
+ });
8547
+ program2.command("schema <agent_id>").description("Fetch + sha256-verify a capability's input JSON Schema").requiredOption("--capability <name>", "Capability name").option("--human", "Human-readable output").action(async (agentId, opts) => {
8548
+ await runCommand(async () => {
8549
+ const { consumer } = buildContext();
8550
+ const agent = await consumer.getAgent(agentId);
8551
+ try {
8552
+ return await fetchCapabilitySchema(agent, opts.capability);
8553
+ } catch (err) {
8554
+ if (err instanceof CapabilitySchemaError) {
8555
+ throw new Error(`schema fetch failed: ${err.message}`);
8556
+ }
8557
+ throw err;
8558
+ }
8559
+ }, { human: opts.human });
8560
+ });
6807
8561
  }
6808
8562
  async function runHireRepl(sessionId, ctx) {
6809
8563
  const readline = await import("readline/promises");
@@ -6882,7 +8636,7 @@ function parseJsonOrFail(raw, label) {
6882
8636
  function parseYamlOrJson(raw) {
6883
8637
  const trimmed = raw.trimStart();
6884
8638
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) return JSON.parse(raw);
6885
- return yamlLoad3(raw);
8639
+ return yamlLoad5(raw);
6886
8640
  }
6887
8641
  async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultIntention) {
6888
8642
  if (manifestOpt !== void 0 && manifestId !== void 0) {
@@ -6896,7 +8650,7 @@ async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultInt
6896
8650
  if (manifestOpt === "-") {
6897
8651
  raw = await readStdin();
6898
8652
  } else if (manifestOpt.startsWith("@")) {
6899
- raw = readFileSync3(manifestOpt.slice(1), "utf8");
8653
+ raw = readFileSync7(manifestOpt.slice(1), "utf8");
6900
8654
  } else {
6901
8655
  raw = manifestOpt;
6902
8656
  }
@@ -6910,12 +8664,16 @@ async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultInt
6910
8664
  }
6911
8665
 
6912
8666
  // src/bin.ts
6913
- var CLI_VERSION = "0.1.2";
8667
+ var pkgPath = join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
8668
+ var CLI_VERSION = JSON.parse(readFileSync8(pkgPath, "utf8")).version;
6914
8669
  var program = new Command();
6915
- program.name("linkedclaw").description("Official LinkedClaw CLI \u2014 any agent can shell out to hire providers, invoke, or broadcast").version(`cli ${CLI_VERSION}`);
8670
+ program.name("linkedclaw").description("Official LinkedClaw CLI \u2014 any agent can shell out to hire providers, invoke, or gig task").version(`cli ${CLI_VERSION}`);
6916
8671
  registerAuthCommands(program);
6917
8672
  registerRequesterCommands(program);
6918
8673
  registerProviderCommands(program);
8674
+ registerConvergeCommands(program);
8675
+ registerArenaCommands(program);
8676
+ registerAgentCommands(program);
6919
8677
  program.parseAsync(process.argv).catch((err) => {
6920
8678
  process.stderr.write(
6921
8679
  JSON.stringify({ error: "internal_error", message: err instanceof Error ? err.message : String(err) }) + "\n"