@linkedclaw/cli 0.1.3 → 0.1.6

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() {
@@ -4119,7 +4287,8 @@ var SessionSchema = external_exports.object({
4119
4287
  max_credits: external_exports.number().int(),
4120
4288
  agreed_quote: external_exports.record(external_exports.unknown()).nullable().optional(),
4121
4289
  manifest_id: external_exports.string().nullable().optional(),
4122
- arbitrator: external_exports.string().nullable().optional()
4290
+ arbitrator: external_exports.string().nullable().optional(),
4291
+ on_behalf_of_id: external_exports.string().nullable().optional()
4123
4292
  }).passthrough();
4124
4293
  var TrustScoreSchema = external_exports.object({
4125
4294
  score: external_exports.number().int(),
@@ -4256,7 +4425,8 @@ var ReceiptSchema = external_exports.object({
4256
4425
  requester_rating: external_exports.number().int().nullable().optional(),
4257
4426
  rating_comment: external_exports.string().nullable().optional(),
4258
4427
  verified: external_exports.boolean().optional(),
4259
- finalized: external_exports.boolean().optional()
4428
+ finalized: external_exports.boolean().optional(),
4429
+ on_behalf_of_id: external_exports.string().nullable().optional()
4260
4430
  }).passthrough();
4261
4431
  var RateReceiptResultSchema = external_exports.object({
4262
4432
  receipt_id: external_exports.string().optional(),
@@ -4402,17 +4572,50 @@ var CommonsLogEventSchema = external_exports.object({
4402
4572
  }).passthrough();
4403
4573
 
4404
4574
  // ../../sdk/consumer-ts/dist/client.js
4575
+ var GigTaskCreationError = class extends Error {
4576
+ mandate;
4577
+ cause;
4578
+ constructor(mandate, cause) {
4579
+ super(`Gig task creation failed after mandate ${mandate.mandate_id} was issued: ${cause.message}`);
4580
+ this.mandate = mandate;
4581
+ this.cause = cause;
4582
+ this.name = "GigTaskCreationError";
4583
+ }
4584
+ };
4405
4585
  var ConsumerClient = class {
4406
- baseUrl;
4586
+ // Substrate / network surface — identity, sessions, mandates, registration.
4587
+ networkUrl;
4588
+ // First-party service product surface — where Gig PA et al expose their
4589
+ // resource APIs. Distinct from networkUrl per LAYER_SYSTEM §8: services are
4590
+ // L5 service agents, not the network.
4591
+ serviceUrl;
4407
4592
  apiKey;
4408
4593
  fetchImpl;
4409
- constructor(baseUrl, apiKey, options) {
4410
- this.baseUrl = baseUrl.replace(/\/+$/, "");
4594
+ _gigPaAgentIdCache;
4595
+ constructor(networkUrl, apiKey, options) {
4596
+ this.networkUrl = networkUrl.replace(/\/+$/, "");
4597
+ this.serviceUrl = options?.serviceUrl !== void 0 ? options.serviceUrl.replace(/\/+$/, "") : void 0;
4411
4598
  this.apiKey = apiKey;
4412
4599
  this.fetchImpl = options?.fetch ?? fetch;
4413
4600
  }
4414
- async request(path, init, schema) {
4415
- const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
4601
+ async _resolveGigPaAgentId() {
4602
+ if (!this._gigPaAgentIdCache) {
4603
+ const data = await this.request("/api/v1/resolve/gig-pa-operator/gig-platform-agent", { method: "GET" }, AgentSchema);
4604
+ this._gigPaAgentIdCache = data.agent_id;
4605
+ }
4606
+ return this._gigPaAgentIdCache;
4607
+ }
4608
+ async request(path2, init, schema) {
4609
+ return this._call(this.networkUrl, path2, init, schema);
4610
+ }
4611
+ async serviceRequest(path2, init, schema) {
4612
+ if (this.serviceUrl === void 0) {
4613
+ throw new Error("ConsumerClient.serviceUrl is not set. Methods that talk to first-party service surfaces (e.g. Gig PA's /api/v1/gig-tasks/*) require a serviceUrl because services are L5 service agents, not part of the network. Pass { serviceUrl: <gig_pa_url> } in options.");
4614
+ }
4615
+ return this._call(this.serviceUrl, path2, init, schema);
4616
+ }
4617
+ async _call(baseUrl, path2, init, schema) {
4618
+ const res = await this.fetchImpl(`${baseUrl}${path2}`, {
4416
4619
  ...init,
4417
4620
  headers: {
4418
4621
  "Content-Type": "application/json",
@@ -4441,7 +4644,7 @@ var ConsumerClient = class {
4441
4644
  }
4442
4645
  // ───────── Identity / Auth ─────────
4443
4646
  /** @implements POST /api/v1/register */
4444
- static async register(baseUrl, params = {}, options) {
4647
+ static async register(networkUrl, params = {}, options) {
4445
4648
  const fetchImpl = options?.fetch ?? fetch;
4446
4649
  const body = { role: params.role ?? "requester" };
4447
4650
  if (params.email !== void 0)
@@ -4450,7 +4653,7 @@ var ConsumerClient = class {
4450
4653
  body.handle = params.handle;
4451
4654
  if (params.displayName !== void 0)
4452
4655
  body.display_name = params.displayName;
4453
- const res = await fetchImpl(`${baseUrl.replace(/\/+$/, "")}/api/v1/register`, {
4656
+ const res = await fetchImpl(`${networkUrl.replace(/\/+$/, "")}/api/v1/register`, {
4454
4657
  method: "POST",
4455
4658
  headers: {
4456
4659
  "Content-Type": "application/json"
@@ -4514,6 +4717,10 @@ var ConsumerClient = class {
4514
4717
  })
4515
4718
  }, InvokeResultSchema);
4516
4719
  }
4720
+ /** @implements POST /api/v1/agents/{agent_id}/messages */
4721
+ async deliverAgentMessage(agentId, payload) {
4722
+ return this.request(`/api/v1/agents/${encodeURIComponent(agentId)}/messages`, { method: "POST", body: JSON.stringify({ payload }) }, external_exports.record(external_exports.unknown()));
4723
+ }
4517
4724
  async getTrustScore(agentId, options) {
4518
4725
  return this.request(`/api/v1/agents/${agentId}/trust${this.queryString({
4519
4726
  capability: options?.capability
@@ -4547,6 +4754,16 @@ var ConsumerClient = class {
4547
4754
  async getSession(sessionId) {
4548
4755
  return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "GET" }, SessionSchema);
4549
4756
  }
4757
+ /** @implements POST /api/v1/sessions/{session_id}/activate
4758
+ *
4759
+ * Required by the requester after WS SESSION_ACCEPT to flip the DB-side
4760
+ * session record from `pending` to `active`. Without this call, subsequent
4761
+ * `POST /messages` will reject with 409 once cloud notices the WS dropped
4762
+ * (status flips `pending` → `interrupted` on heartbeat watchdog).
4763
+ */
4764
+ async activateSession(sessionId) {
4765
+ return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/activate`, { method: "POST", body: JSON.stringify({}) }, external_exports.record(external_exports.unknown()));
4766
+ }
4550
4767
  /** @implements POST /api/v1/sessions/{session_id}/end */
4551
4768
  async endSession(sessionId, options) {
4552
4769
  const body = {};
@@ -4568,40 +4785,88 @@ var ConsumerClient = class {
4568
4785
  })}`, { method: "GET" }, SessionEventListSchema);
4569
4786
  }
4570
4787
  // ───────── Gig PA (requester) ─────────
4571
- /** @implements GET /api/v1/gig_pa/tasks/ */
4788
+ /** @implements GET /api/v1/gig-tasks/ */
4572
4789
  async listGigTasks(options) {
4573
- return this.request(`/api/v1/gig_pa/tasks/${this.queryString({
4790
+ return this.serviceRequest(`/api/v1/gig-tasks/${this.queryString({
4574
4791
  status: options?.status,
4575
4792
  capability: options?.capability
4576
4793
  })}`, { method: "GET" }, external_exports.array(GigTaskSchema));
4577
4794
  }
4578
- /** @implements POST /api/v1/gig_pa/tasks/ */
4795
+ /** @implements POST /api/v1/gig-tasks/ */
4579
4796
  async createGigTask(body) {
4580
- return this.request("/api/v1/gig_pa/tasks/", { method: "POST", body: JSON.stringify(body) }, GigTaskSchema);
4797
+ return this.serviceRequest("/api/v1/gig-tasks/", { method: "POST", body: JSON.stringify(body) }, GigTaskSchema);
4798
+ }
4799
+ async createGigTaskWithMandate(options) {
4800
+ const { principal_agent_id, capability, instruction, target_providers, credits_per_provider, mandate_expires_at, gig_pa_agent_id, deadline, idempotency_key, mandate_id: preIssuedMandateId, ...taskExtra } = options;
4801
+ let issuedMandate;
4802
+ let resolvedMandateId = preIssuedMandateId;
4803
+ if (!resolvedMandateId) {
4804
+ const gigPaAgentId = gig_pa_agent_id ?? await this._resolveGigPaAgentId();
4805
+ const mandateHeaders = {};
4806
+ if (idempotency_key)
4807
+ mandateHeaders["Idempotency-Key"] = `${idempotency_key}:mandate`;
4808
+ issuedMandate = await this.request("/api/v1/mandates", {
4809
+ method: "POST",
4810
+ body: JSON.stringify({
4811
+ principal_agent_id,
4812
+ delegate_agent_id: gigPaAgentId,
4813
+ scope: ["session.open"],
4814
+ scope_params: { "session.open": { max_credits_per_op: credits_per_provider } },
4815
+ budget: target_providers * credits_per_provider,
4816
+ expires_at: mandate_expires_at
4817
+ }),
4818
+ headers: mandateHeaders
4819
+ }, MandateRecordSchema);
4820
+ resolvedMandateId = issuedMandate.mandate_id;
4821
+ }
4822
+ const taskHeaders = {};
4823
+ if (idempotency_key)
4824
+ taskHeaders["Idempotency-Key"] = idempotency_key;
4825
+ const taskBody = {
4826
+ capability,
4827
+ instruction,
4828
+ target_providers,
4829
+ credits_per_provider,
4830
+ mandate_id: resolvedMandateId,
4831
+ ...taskExtra
4832
+ };
4833
+ if (deadline)
4834
+ taskBody.deadline = deadline;
4835
+ let task;
4836
+ try {
4837
+ task = await this.serviceRequest("/api/v1/gig-tasks/", { method: "POST", body: JSON.stringify(taskBody), headers: taskHeaders }, GigTaskSchema);
4838
+ } catch (err) {
4839
+ if (issuedMandate && err instanceof Error) {
4840
+ throw new GigTaskCreationError(issuedMandate, err);
4841
+ }
4842
+ throw err;
4843
+ }
4844
+ const mandate = issuedMandate ?? { mandate_id: resolvedMandateId };
4845
+ return { task, mandate };
4581
4846
  }
4582
- /** @implements GET /api/v1/gig_pa/tasks/policy */
4847
+ /** @implements GET /api/v1/gig-tasks/policy */
4583
4848
  async getGigTaskPolicy() {
4584
- return this.request("/api/v1/gig_pa/tasks/policy", { method: "GET" }, GigTaskPolicySchema);
4849
+ return this.serviceRequest("/api/v1/gig-tasks/policy", { method: "GET" }, GigTaskPolicySchema);
4585
4850
  }
4586
- /** @implements GET /api/v1/gig_pa/tasks/{task_id} */
4851
+ /** @implements GET /api/v1/gig-tasks/{task_id} */
4587
4852
  async getGigTask(taskId) {
4588
- return this.request(`/api/v1/gig_pa/tasks/${encodeURIComponent(taskId)}`, { method: "GET" }, GigTaskSchema);
4853
+ return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}`, { method: "GET" }, GigTaskSchema);
4589
4854
  }
4590
- /** @implements POST /api/v1/gig_pa/tasks/{task_id}/results/{result_id}/verify */
4855
+ /** @implements POST /api/v1/gig-tasks/{task_id}/results/{result_id}/verify */
4591
4856
  async verifyGigTaskResult(taskId, resultId, body) {
4592
- return this.request(`/api/v1/gig_pa/tasks/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/verify`, { method: "POST", body: JSON.stringify(body ?? {}) }, GigTaskVerifyResultSchema);
4857
+ return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/verify`, { method: "POST", body: JSON.stringify(body ?? {}) }, GigTaskVerifyResultSchema);
4593
4858
  }
4594
- /** @implements POST /api/v1/gig_pa/tasks/{task_id}/results/{result_id}/review */
4859
+ /** @implements POST /api/v1/gig-tasks/{task_id}/results/{result_id}/review */
4595
4860
  async reviewGigTaskResult(taskId, resultId, body) {
4596
- return this.request(`/api/v1/gig_pa/tasks/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/review`, { method: "POST", body: JSON.stringify(body) }, GigTaskVerifyResultSchema);
4861
+ return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/review`, { method: "POST", body: JSON.stringify(body) }, GigTaskVerifyResultSchema);
4597
4862
  }
4598
- /** @implements POST /api/v1/gig_pa/tasks/{task_id}/release */
4863
+ /** @implements POST /api/v1/gig-tasks/{task_id}/release */
4599
4864
  async releaseGigTask(taskId) {
4600
- return this.request(`/api/v1/gig_pa/tasks/${encodeURIComponent(taskId)}/release`, { method: "POST", body: JSON.stringify({}) }, GigTaskActionResultSchema);
4865
+ return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/release`, { method: "POST", body: JSON.stringify({}) }, GigTaskActionResultSchema);
4601
4866
  }
4602
- /** @implements POST /api/v1/gig_pa/tasks/{task_id}/cancel */
4867
+ /** @implements POST /api/v1/gig-tasks/{task_id}/cancel */
4603
4868
  async cancelGigTask(taskId, reason) {
4604
- return this.request(`/api/v1/gig_pa/tasks/${encodeURIComponent(taskId)}/cancel`, { method: "POST", body: JSON.stringify(reason !== void 0 ? { reason } : {}) }, GigTaskActionResultSchema);
4869
+ return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/cancel`, { method: "POST", body: JSON.stringify(reason !== void 0 ? { reason } : {}) }, GigTaskActionResultSchema);
4605
4870
  }
4606
4871
  // ───────── Credits ─────────
4607
4872
  /** @implements GET /api/v1/credits */
@@ -4715,10 +4980,6 @@ var ConsumerClient = class {
4715
4980
  async revokeMandate(mandateId) {
4716
4981
  await this.request(`/api/v1/mandates/${encodeURIComponent(mandateId)}`, { method: "DELETE" }, external_exports.unknown());
4717
4982
  }
4718
- /** @implements POST /api/v1/mandates/transport */
4719
- async createTransportMandate(body) {
4720
- return this.request("/api/v1/mandates/transport", { method: "POST", body: JSON.stringify(body) }, MandateRecordSchema);
4721
- }
4722
4983
  // ───────── Observability ─────────
4723
4984
  /** @implements GET /api/v1/events/verify */
4724
4985
  async verifyEvents(options) {
@@ -4737,29 +4998,71 @@ var ConsumerClient = class {
4737
4998
  // ───────── Health (no auth required) ─────────
4738
4999
  /** @implements GET /health */
4739
5000
  async health() {
4740
- const res = await this.fetchImpl(`${this.baseUrl}/health`, { method: "GET" });
5001
+ const res = await this.fetchImpl(`${this.networkUrl}/health`, { method: "GET" });
4741
5002
  if (!res.ok)
4742
5003
  throw new Error(`LinkedClaw /health ${res.status}`);
4743
5004
  return HealthStatusSchema.parse(await res.json());
4744
5005
  }
4745
5006
  /** @implements GET /health/ready */
4746
5007
  async healthReady() {
4747
- const res = await this.fetchImpl(`${this.baseUrl}/health/ready`, { method: "GET" });
5008
+ const res = await this.fetchImpl(`${this.networkUrl}/health/ready`, { method: "GET" });
4748
5009
  if (!res.ok)
4749
5010
  throw new Error(`LinkedClaw /health/ready ${res.status}`);
4750
5011
  return HealthStatusSchema.parse(await res.json());
4751
5012
  }
4752
5013
  /** @implements GET /metrics */
4753
5014
  async metrics() {
4754
- const res = await this.fetchImpl(`${this.baseUrl}/metrics`, { method: "GET" });
5015
+ const res = await this.fetchImpl(`${this.networkUrl}/metrics`, { method: "GET" });
4755
5016
  if (!res.ok)
4756
5017
  throw new Error(`LinkedClaw /metrics ${res.status}`);
4757
5018
  return res.text();
4758
5019
  }
4759
5020
  };
4760
5021
 
5022
+ // ../../sdk/consumer-ts/dist/capabilitySchema.js
5023
+ var CapabilitySchemaError = class extends Error {
5024
+ constructor(message) {
5025
+ super(message);
5026
+ this.name = "CapabilitySchemaError";
5027
+ }
5028
+ };
5029
+ async function fetchCapabilitySchema(listing, capability, options) {
5030
+ const meta = listing.capabilities_meta?.[capability];
5031
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
5032
+ throw new CapabilitySchemaError(`no capabilities_meta entry for ${JSON.stringify(capability)}`);
5033
+ }
5034
+ const entry = meta;
5035
+ const schemaUrl = entry["schema_url"];
5036
+ if (!schemaUrl || typeof schemaUrl !== "string") {
5037
+ throw new CapabilitySchemaError(`no schema_url for capability ${JSON.stringify(capability)}`);
5038
+ }
5039
+ const schemaDigest = entry["schema_digest"];
5040
+ const fetchFn = options?.fetch ?? globalThis.fetch;
5041
+ const response = await fetchFn(schemaUrl);
5042
+ if (!response.ok) {
5043
+ throw new CapabilitySchemaError(`failed to fetch schema for ${JSON.stringify(capability)}: HTTP ${response.status}`);
5044
+ }
5045
+ const buffer = await response.arrayBuffer();
5046
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
5047
+ const actual = "sha256:" + Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
5048
+ if (actual !== schemaDigest) {
5049
+ throw new CapabilitySchemaError(`schema digest mismatch for ${JSON.stringify(capability)}: expected ${String(schemaDigest)}, got ${actual}`);
5050
+ }
5051
+ const text = new TextDecoder().decode(buffer);
5052
+ let parsed;
5053
+ try {
5054
+ parsed = JSON.parse(text);
5055
+ } catch {
5056
+ throw new CapabilitySchemaError(`schema body is not valid JSON for ${JSON.stringify(capability)}`);
5057
+ }
5058
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5059
+ throw new CapabilitySchemaError(`schema body is not a JSON object for ${JSON.stringify(capability)}`);
5060
+ }
5061
+ return parsed;
5062
+ }
5063
+
4761
5064
  // ../../sdk/consumer-ts/dist/index.js
4762
- var DEFAULT_RELAY_URL2 = "wss://api.linkedclaw.com/ws";
5065
+ var DEFAULT_RELAY_URL = "wss://api.linkedclaw.com/ws";
4763
5066
 
4764
5067
  // ../../sdk/provider-ts/dist/models.js
4765
5068
  var SessionSchema2 = external_exports.object({
@@ -5014,8 +5317,8 @@ var ProviderClient = class {
5014
5317
  this.apiKey = apiKey;
5015
5318
  this.fetchImpl = options?.fetch ?? fetch;
5016
5319
  }
5017
- async request(path, init, schema) {
5018
- const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
5320
+ async request(path2, init, schema) {
5321
+ const res = await this.fetchImpl(`${this.baseUrl}${path2}`, {
5019
5322
  ...init,
5020
5323
  headers: {
5021
5324
  "Content-Type": "application/json",
@@ -5417,12 +5720,18 @@ function strOrEmpty(frame, key) {
5417
5720
 
5418
5721
  // ../../sdk/consumer-runtime-ts/dist/index.js
5419
5722
  import WebSocket2 from "ws";
5420
- var ACP_TIMEOUT_MS = 3e4;
5723
+ var ACP_CONNECT_TIMEOUT_MS = 2e3;
5724
+ var SESSION_ACCEPT_TIMEOUT_MS = 3e4;
5421
5725
  var SessionRejectedError = class extends Error {
5422
5726
  constructor(reason) {
5423
5727
  super(`session rejected: ${reason}`);
5424
5728
  }
5425
5729
  };
5730
+ var TransportMissError = class extends Error {
5731
+ constructor(reason) {
5732
+ super(`transport miss: ${reason}`);
5733
+ }
5734
+ };
5426
5735
  var RequesterFlows = class {
5427
5736
  constructor(client) {
5428
5737
  this.client = client;
@@ -5432,8 +5741,11 @@ var RequesterFlows = class {
5432
5741
  return this.client.discover({ capability, ...extra });
5433
5742
  }
5434
5743
  /**
5435
- * Open a session. Performs HTTP create → ACP WS handshake (SESSION_CREATE/ACCEPT) → HTTP activate.
5436
- * Default 30s wait for SESSION_ACCEPT.
5744
+ * Open a session. HTTP create → WS handshake (SESSION_CREATE/ACCEPT) → done.
5745
+ *
5746
+ * Default transport is /ws — native providers register there
5747
+ * (`SkillConfig.try_acp=False` upstream default). Set `tryAcp: true` to try
5748
+ * /acp first; on opt-in, falls back to /ws when the recipient isn't on /acp.
5437
5749
  */
5438
5750
  async hire(params) {
5439
5751
  const session = await this.client.createSession({
@@ -5443,43 +5755,81 @@ var RequesterFlows = class {
5443
5755
  ...params.referredBy !== void 0 ? { referred_by: params.referredBy } : {}
5444
5756
  });
5445
5757
  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);
5758
+ const relayUrl = params.relayUrl ?? DEFAULT_RELAY_URL;
5759
+ try {
5760
+ if (params.tryAcp) {
5761
+ const acpUrl = relayUrl.replace(/\/ws$/, "/acp");
5762
+ try {
5763
+ await this.attemptHandshake(acpUrl, session.session_id, params, ACP_CONNECT_TIMEOUT_MS);
5764
+ } catch (err) {
5765
+ if (!(err instanceof TransportMissError)) throw err;
5766
+ await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
5767
+ }
5768
+ } else {
5769
+ await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
5770
+ }
5771
+ await this.client.activateSession(session.session_id);
5772
+ } catch (err) {
5773
+ await this.client.endSession(session.session_id, {}).catch(() => {
5774
+ });
5775
+ if (err instanceof TransportMissError) {
5776
+ throw new SessionRejectedError(`agent unreachable`);
5777
+ }
5778
+ throw err;
5779
+ }
5780
+ return { session, activated: true };
5781
+ }
5782
+ async attemptHandshake(url, sessionId, params, connectTimeoutMs) {
5783
+ const ws = new WebSocket2(url);
5449
5784
  try {
5450
- await new Promise((resolve, reject) => {
5451
- ws.once("open", () => resolve());
5452
- ws.once("error", reject);
5785
+ await new Promise((resolve3, reject) => {
5786
+ const timer = setTimeout(
5787
+ () => reject(new TransportMissError("connect timeout")),
5788
+ connectTimeoutMs
5789
+ );
5790
+ ws.once("open", () => {
5791
+ clearTimeout(timer);
5792
+ resolve3();
5793
+ });
5794
+ ws.once("error", (err) => {
5795
+ clearTimeout(timer);
5796
+ reject(new TransportMissError(`connect failed: ${err.message}`));
5797
+ });
5453
5798
  });
5454
5799
  ws.send(JSON.stringify({ type: MessageType.IDENTIFY, agent_id: params.agentId, token: params.apiKey }));
5455
5800
  ws.send(JSON.stringify({
5456
5801
  type: MessageType.SESSION_CREATE,
5457
- session_id: session.session_id,
5802
+ session_id: sessionId,
5458
5803
  recipient: params.providerAgentId,
5459
5804
  capability: params.capability
5460
5805
  }));
5461
- const reply = await new Promise((resolve, reject) => {
5462
- const timer = setTimeout(() => reject(new Error("ACP SESSION_ACCEPT timeout")), ACP_TIMEOUT_MS);
5806
+ const reply = await new Promise((resolve3, reject) => {
5807
+ const timer = setTimeout(
5808
+ () => reject(new Error("SESSION_ACCEPT timeout")),
5809
+ SESSION_ACCEPT_TIMEOUT_MS
5810
+ );
5463
5811
  ws.once("message", (data) => {
5464
5812
  clearTimeout(timer);
5465
- resolve(JSON.parse(data.toString()));
5813
+ resolve3(JSON.parse(data.toString()));
5466
5814
  });
5467
5815
  ws.once("error", (err) => {
5468
5816
  clearTimeout(timer);
5469
5817
  reject(err);
5470
5818
  });
5471
5819
  });
5472
- if (reply.type === MessageType.ERROR) throw new SessionRejectedError(reply.error ?? "relay error");
5820
+ if (reply.type === MessageType.ERROR) {
5821
+ const errMsg = reply.error ?? "relay error";
5822
+ if (errMsg.includes("not connected")) throw new TransportMissError(errMsg);
5823
+ throw new SessionRejectedError(errMsg);
5824
+ }
5473
5825
  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;
5826
+ if (reply.type !== MessageType.SESSION_ACCEPT) throw new Error(`unexpected reply: ${reply.type}`);
5827
+ } finally {
5828
+ try {
5829
+ ws.close();
5830
+ } catch {
5831
+ }
5480
5832
  }
5481
- ws.close();
5482
- return { session, activated: true };
5483
5833
  }
5484
5834
  send(sessionId, payload, seq) {
5485
5835
  const normalized = typeof payload === "string" ? { text: payload } : payload;
@@ -5490,6 +5840,52 @@ var RequesterFlows = class {
5490
5840
  }
5491
5841
  };
5492
5842
 
5843
+ // src/config.ts
5844
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
5845
+ import { homedir } from "os";
5846
+ import { join, dirname } from "path";
5847
+ import { load as yamlLoad, dump as yamlDump } from "js-yaml";
5848
+ var DEFAULT_CLOUD_URL = "https://api.linkedclaw.com";
5849
+ var DEFAULT_RELAY_URL2 = "wss://api.linkedclaw.com/ws";
5850
+ function configDir() {
5851
+ return process.env["LINKEDCLAW_CONFIG_DIR"] ?? join(homedir(), ".linkedclaw");
5852
+ }
5853
+ function configPath() {
5854
+ return join(configDir(), "config.yaml");
5855
+ }
5856
+ function readFileConfig(path2 = configPath()) {
5857
+ if (!existsSync(path2)) return {};
5858
+ const raw = readFileSync(path2, "utf8");
5859
+ const parsed = yamlLoad(raw);
5860
+ if (parsed === null || parsed === void 0) return {};
5861
+ if (typeof parsed !== "object") {
5862
+ throw new Error(`config file ${path2} is not a YAML object`);
5863
+ }
5864
+ return parsed;
5865
+ }
5866
+ function writeFileConfig(cfg, path2 = configPath()) {
5867
+ const dir = dirname(path2);
5868
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
5869
+ writeFileSync(path2, yamlDump(cfg), { mode: 384 });
5870
+ if (process.platform !== "win32") chmodSync(path2, 384);
5871
+ }
5872
+ function resolveConfig(overrides = {}) {
5873
+ const env = process.env;
5874
+ const file = readFileConfig();
5875
+ const cloudUrl = overrides.cloudUrl ?? env["LINKEDCLAW_CLOUD_URL"] ?? file.cloudUrl ?? DEFAULT_CLOUD_URL;
5876
+ const relayUrl = overrides.relayUrl ?? env["LINKEDCLAW_RELAY_URL"] ?? file.relayUrl ?? DEFAULT_RELAY_URL2;
5877
+ const servicesHostUrl = overrides.servicesHostUrl ?? env["LINKEDCLAW_SERVICES_HOST_URL"] ?? file.servicesHostUrl ?? cloudUrl;
5878
+ const apiKey = overrides.apiKey ?? env["LINKEDCLAW_API_KEY"] ?? file.apiKey;
5879
+ return {
5880
+ ...file,
5881
+ ...overrides,
5882
+ cloudUrl,
5883
+ relayUrl,
5884
+ servicesHostUrl,
5885
+ ...apiKey !== void 0 ? { apiKey } : {}
5886
+ };
5887
+ }
5888
+
5493
5889
  // src/errors.ts
5494
5890
  var LinkedClawError = class extends Error {
5495
5891
  code;
@@ -5507,11 +5903,11 @@ var NetworkError = class extends LinkedClawError {
5507
5903
  }
5508
5904
  };
5509
5905
  var ApiError = class extends LinkedClawError {
5510
- constructor(status, detail, path) {
5511
- super(`api_error_${status}`, `[${status}] ${path}: ${detail}`);
5906
+ constructor(status, detail, path2) {
5907
+ super(`api_error_${status}`, `[${status}] ${path2}: ${detail}`);
5512
5908
  this.status = status;
5513
5909
  this.detail = detail;
5514
- this.path = path;
5910
+ this.path = path2;
5515
5911
  this.name = "ApiError";
5516
5912
  }
5517
5913
  status;
@@ -5581,48 +5977,419 @@ async function readStdin() {
5581
5977
  return Buffer.concat(chunks).toString("utf8");
5582
5978
  }
5583
5979
 
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: ");
5980
+ // src/arena/api.ts
5981
+ function errorDetail(body) {
5982
+ if (body && typeof body === "object" && "detail" in body) {
5983
+ const detail = body.detail;
5984
+ return typeof detail === "string" ? detail : JSON.stringify(detail) ?? String(detail);
5985
+ }
5986
+ if (body == null) return "";
5987
+ return typeof body === "string" ? body : JSON.stringify(body);
5988
+ }
5989
+ function makeArenaApi(targetUrl, apiKey) {
5990
+ async function apiFetch(path2, opts = {}) {
5991
+ const url = targetUrl.replace(/\/$/, "") + path2;
5992
+ const res = await fetch(url, {
5993
+ ...opts,
5994
+ headers: {
5995
+ "Content-Type": "application/json",
5996
+ Authorization: `Bearer ${apiKey}`,
5997
+ "X-CSRF-Token": apiKey,
5998
+ ...opts.headers ?? {}
5591
5999
  }
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
6000
  });
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
- }
5612
- }
5613
- if (!opened) {
5614
- process.stderr.write(`Open this URL in a browser to register:
5615
- ${portalUrl}
5616
-
5617
- `);
6001
+ let body;
6002
+ try {
6003
+ body = await res.json();
6004
+ } catch {
6005
+ body = null;
6006
+ }
6007
+ if (!res.ok) {
6008
+ try {
6009
+ throw new ApiError(res.status, errorDetail(body), path2);
6010
+ } catch (err) {
6011
+ if (err instanceof ApiError) throw err;
6012
+ throw new LinkedClawError(`api_${res.status}`, `HTTP ${res.status}`);
5618
6013
  }
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
- };
6014
+ }
6015
+ return body;
6016
+ }
6017
+ return {
6018
+ async createTournamentArena(body, opts) {
6019
+ return apiFetch("/api/v1/arena/arenas", {
6020
+ method: "POST",
6021
+ headers: { "Idempotency-Key": opts.idempotencyKey },
6022
+ body: JSON.stringify(body)
6023
+ });
6024
+ },
6025
+ async register(body) {
6026
+ return apiFetch("/api/v1/arena/contestants/register", {
6027
+ method: "POST",
6028
+ body: JSON.stringify(body)
6029
+ });
6030
+ },
6031
+ async listOffers() {
6032
+ return apiFetch("/api/v1/arena/offers", { method: "GET" });
6033
+ },
6034
+ async acceptOffer(offerId) {
6035
+ return apiFetch(`/api/v1/arena/offers/${encodeURIComponent(offerId)}/accept`, {
6036
+ method: "POST",
6037
+ body: JSON.stringify({})
6038
+ });
6039
+ },
6040
+ async submit(arenaId, body) {
6041
+ const { submission_hash: _submissionHash, ...wireBody } = body;
6042
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/submissions`, {
6043
+ method: "POST",
6044
+ body: JSON.stringify(wireBody)
6045
+ });
6046
+ },
6047
+ async commitJuror(arenaId, body) {
6048
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/jurors/commit`, {
6049
+ method: "POST",
6050
+ body: JSON.stringify(body)
6051
+ });
6052
+ },
6053
+ async voteTask(arenaId, body) {
6054
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/juror-votes`, {
6055
+ method: "POST",
6056
+ body: JSON.stringify(body)
6057
+ });
6058
+ },
6059
+ async voteMatch(arenaId, matchId, body) {
6060
+ return apiFetch(
6061
+ `/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/matches/${encodeURIComponent(matchId)}/juror-votes`,
6062
+ { method: "POST", body: JSON.stringify(body) }
6063
+ );
6064
+ },
6065
+ async listArenas(opts = {}) {
6066
+ const suffix = opts.registered ? "?registered=true" : "";
6067
+ return apiFetch(`/api/v1/arena/arenas${suffix}`, { method: "GET" });
6068
+ },
6069
+ async getLeaderboard(arenaId) {
6070
+ return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/leaderboard`, {
6071
+ method: "GET"
6072
+ });
6073
+ },
6074
+ async getCategoryLeaderboard(category, mode = "match") {
6075
+ const q = new URLSearchParams({
6076
+ category_topic: category.topic,
6077
+ category_subtopic: category.subtopic,
6078
+ mode
6079
+ });
6080
+ return apiFetch(`/api/v1/arena/leaderboard?${q.toString()}`, {
6081
+ method: "GET"
6082
+ });
6083
+ }
6084
+ };
6085
+ }
6086
+
6087
+ // src/arena/hash.ts
6088
+ import { createHash } from "crypto";
6089
+ import { readFileSync as readFileSync2 } from "fs";
6090
+ function sha256Hex(bytes) {
6091
+ return createHash("sha256").update(bytes).digest("hex");
6092
+ }
6093
+ function sha256Digest(bytes) {
6094
+ return `sha256:${sha256Hex(bytes)}`;
6095
+ }
6096
+ function hashFile(path2) {
6097
+ const bytes = readFileSync2(path2);
6098
+ return { bytes, digest: sha256Digest(bytes) };
6099
+ }
6100
+
6101
+ // src/commands/arena.ts
6102
+ var FIRST_PARTY_ARENA_HANDLE = "gig-pa-operator";
6103
+ var FIRST_PARTY_ARENA_SLUG = "arena-v1";
6104
+ var ARENA_CAPABILITY = "arena.v1";
6105
+ function servicesHostBaseUrl(ctx) {
6106
+ return ctx.cfg.servicesHostUrl ?? process.env.LINKEDCLAW_SERVICES_HOST_URL ?? ctx.cfg.cloudUrl;
6107
+ }
6108
+ function endpointForListing(listing, ctx) {
6109
+ const endpoint = listing.external_endpoint;
6110
+ return typeof endpoint === "string" && endpoint.length > 0 ? endpoint : servicesHostBaseUrl(ctx);
6111
+ }
6112
+ function assertArenaPa(listing, source) {
6113
+ const caps = Array.isArray(listing.capabilities) ? listing.capabilities : [];
6114
+ if (!caps.includes(ARENA_CAPABILITY)) {
6115
+ throw new LinkedClawError(
6116
+ "arena_target_not_arena_pa",
6117
+ `${source} does not advertise ${ARENA_CAPABILITY}.`
6118
+ );
6119
+ }
6120
+ }
6121
+ async function resolveArenaTarget(ctx, opts) {
6122
+ if (opts.target && /^https?:\/\//i.test(opts.target)) {
6123
+ throw new LinkedClawError(
6124
+ "arena_target_must_be_agent_id",
6125
+ "--target now expects an arena.v1 agent id, not a URL."
6126
+ );
6127
+ }
6128
+ const listing = opts.target ? await ctx.consumer.getAgent(opts.target) : await ctx.consumer.resolveAgentHandle(FIRST_PARTY_ARENA_HANDLE, FIRST_PARTY_ARENA_SLUG);
6129
+ assertArenaPa(listing, opts.target ?? `${FIRST_PARTY_ARENA_HANDLE}/${FIRST_PARTY_ARENA_SLUG}`);
6130
+ return { agentId: listing.agent_id, baseUrl: endpointForListing(listing, ctx) };
6131
+ }
6132
+ async function buildArenaApi(opts) {
6133
+ const ctx = buildContext();
6134
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
6135
+ const target = await resolveArenaTarget(ctx, opts);
6136
+ return makeArenaApi(target.baseUrl, ctx.cfg.apiKey);
6137
+ }
6138
+ function parseCategory(opts) {
6139
+ if (!opts.categoryTopic) {
6140
+ throw new LinkedClawError("missing_category_topic", "--category-topic is required.");
6141
+ }
6142
+ if (!opts.categorySubtopic) {
6143
+ throw new LinkedClawError("missing_category_subtopic", "--category-subtopic is required.");
6144
+ }
6145
+ return { topic: opts.categoryTopic, subtopic: opts.categorySubtopic };
6146
+ }
6147
+ function parseSeq(value) {
6148
+ const seq = Number(value);
6149
+ if (!Number.isInteger(seq) || seq < 1) {
6150
+ throw new LinkedClawError("invalid_seq", "--seq must be a positive integer.");
6151
+ }
6152
+ return seq;
6153
+ }
6154
+ function parseScore(value) {
6155
+ const score = Number(value);
6156
+ if (!Number.isFinite(score) || score < 0 || score > 1) {
6157
+ throw new LinkedClawError("invalid_juror_score", "score must be a number between 0 and 1.");
6158
+ }
6159
+ return score;
6160
+ }
6161
+ function isPlainObject(value) {
6162
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6163
+ }
6164
+ function readTournamentManifest(path2) {
6165
+ if (path2 === "-" && process.stdin.isTTY) {
6166
+ throw new LinkedClawError(
6167
+ "arena_tournament_manifest_stdin_tty",
6168
+ "stdin is a TTY; pass a file path or pipe JSON via stdin (e.g. cat tournament.json | linkedclaw arena tournament create -)."
6169
+ );
6170
+ }
6171
+ let raw;
6172
+ try {
6173
+ raw = readFileSync3(path2 === "-" ? 0 : path2, "utf8");
6174
+ } catch (err) {
6175
+ const message = err instanceof Error ? err.message : String(err);
6176
+ throw new LinkedClawError(
6177
+ "arena_tournament_manifest_read_failed",
6178
+ path2 === "-" ? `could not read from stdin (use "-" to pipe JSON): ${message}` : message
6179
+ );
6180
+ }
6181
+ let parsed;
6182
+ try {
6183
+ parsed = JSON.parse(raw);
6184
+ } catch (err) {
6185
+ throw new LinkedClawError(
6186
+ "arena_tournament_manifest_json_invalid",
6187
+ err instanceof Error ? err.message : String(err)
6188
+ );
6189
+ }
6190
+ if (!isPlainObject(parsed)) {
6191
+ throw new LinkedClawError(
6192
+ "arena_tournament_manifest_shape_invalid",
6193
+ "manifest must be a JSON object."
6194
+ );
6195
+ }
6196
+ if (parsed.mode !== "tournament") {
6197
+ throw new LinkedClawError(
6198
+ "arena_tournament_manifest_mode_invalid",
6199
+ 'manifest mode must be exactly "tournament".'
6200
+ );
6201
+ }
6202
+ if (!isPlainObject(parsed.category) || !isPlainObject(parsed.config)) {
6203
+ throw new LinkedClawError(
6204
+ "arena_tournament_manifest_shape_invalid",
6205
+ "manifest must include category and config object fields."
6206
+ );
6207
+ }
6208
+ return {
6209
+ mode: "tournament",
6210
+ category: parsed.category,
6211
+ config: parsed.config
6212
+ };
6213
+ }
6214
+ function parseIdempotencyKey(value) {
6215
+ const idempotencyKey = value?.trim();
6216
+ if (!idempotencyKey) {
6217
+ throw new LinkedClawError(
6218
+ "arena_idempotency_key_required",
6219
+ "--idempotency-key must be a non-empty string."
6220
+ );
6221
+ }
6222
+ if (/[\r\n]/.test(idempotencyKey)) {
6223
+ throw new LinkedClawError(
6224
+ "arena_idempotency_key_invalid",
6225
+ "--idempotency-key must not contain newlines."
6226
+ );
6227
+ }
6228
+ return idempotencyKey;
6229
+ }
6230
+ function mergeSubmissionHash(response, submissionHash) {
6231
+ if (response && typeof response === "object" && "submission" in response && response.submission && typeof response.submission === "object") {
6232
+ const submission = response.submission;
6233
+ if (typeof submission.submission_hash === "string") return response;
6234
+ return { ...response, submission: { ...submission, submission_hash: submissionHash } };
6235
+ }
6236
+ if (response && typeof response === "object" && "submission_hash" in response) return response;
6237
+ return { response, submission_hash: submissionHash };
6238
+ }
6239
+ function registerArenaCommands(program2) {
6240
+ const arena = program2.command("arena").description("Arena PA commands");
6241
+ const tournament = arena.command("tournament").description("Arena tournament commands");
6242
+ 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) => {
6243
+ await runCommand(async () => {
6244
+ const idempotencyKey = parseIdempotencyKey(opts.idempotencyKey);
6245
+ return (await buildArenaApi(opts)).createTournamentArena(readTournamentManifest(manifestPath), {
6246
+ idempotencyKey
6247
+ });
6248
+ }, { human: opts.human });
6249
+ });
6250
+ 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(
6251
+ async (opts) => {
6252
+ await runCommand(async () => {
6253
+ const api = await buildArenaApi(opts);
6254
+ return api.register({
6255
+ contestant_agent_id: opts.agentId,
6256
+ mandate_id: opts.mandateId,
6257
+ category: parseCategory(opts)
6258
+ });
6259
+ }, { human: opts.human });
6260
+ }
6261
+ );
6262
+ 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) => {
6263
+ await runCommand(async () => (await buildArenaApi(opts)).listOffers(), { human: opts.human });
6264
+ });
6265
+ 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) => {
6266
+ await runCommand(async () => (await buildArenaApi(opts)).acceptOffer(offerId), { human: opts.human });
6267
+ });
6268
+ 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(
6269
+ async (arenaId, opts) => {
6270
+ await runCommand(async () => {
6271
+ if (opts.file && opts.body !== void 0) {
6272
+ throw new LinkedClawError("submission_source_conflict", "Use exactly one of --file or --body.");
6273
+ }
6274
+ if (!opts.file && opts.body === void 0) {
6275
+ throw new LinkedClawError("submission_source_required", "Use exactly one of --file or --body.");
6276
+ }
6277
+ let request;
6278
+ if (opts.file) {
6279
+ const { bytes, digest } = hashFile(opts.file);
6280
+ request = {
6281
+ offer_id: opts.offerId,
6282
+ raw_content: bytes.toString("utf8"),
6283
+ content_ref: opts.contentRef ?? opts.file,
6284
+ ...opts.matchId ? { match_id: opts.matchId } : {},
6285
+ seq: opts.seq,
6286
+ submission_hash: digest
6287
+ };
6288
+ } else {
6289
+ const body = opts.body ?? "";
6290
+ request = {
6291
+ offer_id: opts.offerId,
6292
+ raw_content: body,
6293
+ ...opts.contentRef ? { content_ref: opts.contentRef } : {},
6294
+ ...opts.matchId ? { match_id: opts.matchId } : {},
6295
+ seq: opts.seq,
6296
+ submission_hash: sha256Digest(Buffer.from(body, "utf8"))
6297
+ };
6298
+ }
6299
+ const response = await (await buildArenaApi(opts)).submit(arenaId, request);
6300
+ return mergeSubmissionHash(response, request.submission_hash);
6301
+ }, { human: opts.human });
6302
+ }
6303
+ );
6304
+ const vote = arena.command("vote").description("Arena juror voting commands");
6305
+ 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) => {
6306
+ await runCommand(async () => {
6307
+ const score = parseScore(scoreValue);
6308
+ return (await buildArenaApi(opts)).voteTask(arenaId, {
6309
+ submission_id: submissionId,
6310
+ score,
6311
+ ...opts.rationaleRef ? { rationale_ref: opts.rationaleRef } : {}
6312
+ });
6313
+ }, { human: opts.human });
6314
+ });
6315
+ 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) => {
6316
+ await runCommand(async () => {
6317
+ if (!["a", "b", "tie", "both_bad"].includes(outcome)) {
6318
+ throw new LinkedClawError(
6319
+ "invalid_juror_outcome",
6320
+ "outcome must be one of: a, b, tie, both_bad."
6321
+ );
6322
+ }
6323
+ return (await buildArenaApi(opts)).voteMatch(arenaId, matchId, {
6324
+ outcome,
6325
+ ...opts.rationaleRef ? { rationale_ref: opts.rationaleRef } : {}
6326
+ });
6327
+ }, { human: opts.human });
6328
+ });
6329
+ 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) => {
6330
+ await runCommand(async () => (await buildArenaApi(opts)).listArenas({ registered: opts.registered }), {
6331
+ human: opts.human
6332
+ });
6333
+ });
6334
+ 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) => {
6335
+ await runCommand(async () => {
6336
+ const api = await buildArenaApi(opts);
6337
+ if (arenaId) {
6338
+ return api.getLeaderboard(arenaId);
6339
+ }
6340
+ if (opts.mode !== "match") {
6341
+ throw new LinkedClawError(
6342
+ "unsupported_arena_leaderboard_mode",
6343
+ "--mode must be match when no arena_id is provided."
6344
+ );
6345
+ }
6346
+ return api.getCategoryLeaderboard(parseCategory(opts), opts.mode);
6347
+ }, { human: opts.human });
6348
+ });
6349
+ }
6350
+
6351
+ // src/commands/auth.ts
6352
+ function registerAuthCommands(program2) {
6353
+ 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) => {
6354
+ await runCommand(async () => {
6355
+ let apiKey = opts.apiKey;
6356
+ if (!apiKey) {
6357
+ apiKey = await readLine("Paste API key: ");
6358
+ }
6359
+ if (!apiKey) throw new Error("empty api key");
6360
+ const prev = readFileConfig();
6361
+ const next = { ...prev, apiKey, ...opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {} };
6362
+ writeFileConfig(next);
6363
+ return { ok: true, path: configPath() };
6364
+ });
6365
+ });
6366
+ 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) => {
6367
+ await runCommand(async () => {
6368
+ const prev = readFileConfig();
6369
+ const cloudUrl = opts.cloudUrl ?? prev.cloudUrl ?? process.env.LINKEDCLAW_CLOUD_URL ?? DEFAULT_CLOUD_URL;
6370
+ const portalUrl = cloudUrl.replace(/\/$/, "") + "/register";
6371
+ let opened = false;
6372
+ if (opts.browser !== false) {
6373
+ try {
6374
+ const open = (await import("open")).default;
6375
+ await open(portalUrl);
6376
+ opened = true;
6377
+ } catch {
6378
+ }
6379
+ }
6380
+ if (!opened) {
6381
+ process.stderr.write(`Open this URL in a browser to register:
6382
+ ${portalUrl}
6383
+
6384
+ `);
6385
+ }
6386
+ const apiKey = await readLine("Paste your API key (from portal Settings \u2192 API Keys): ");
6387
+ if (!apiKey) throw new Error("empty api key");
6388
+ const next = {
6389
+ ...prev,
6390
+ apiKey,
6391
+ cloudUrl
6392
+ };
5626
6393
  writeFileConfig(next);
5627
6394
  return { ok: true, path: configPath(), opened };
5628
6395
  });
@@ -5661,9 +6428,1060 @@ function tryParseJson(v) {
5661
6428
  }
5662
6429
  }
5663
6430
 
6431
+ // src/commands/converge.ts
6432
+ import { spawnSync } from "child_process";
6433
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
6434
+ import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
6435
+
6436
+ // src/converge/api.ts
6437
+ function makeFetchError(code, body) {
6438
+ const err = new LinkedClawError(`api_${code}`, `HTTP ${code}`);
6439
+ err.code = code;
6440
+ err.body = body;
6441
+ return err;
6442
+ }
6443
+ function makeConvergeApi(cloudUrl, apiKey) {
6444
+ async function apiFetch(path2, opts = {}) {
6445
+ const url = cloudUrl.replace(/\/$/, "") + path2;
6446
+ const res = await fetch(url, {
6447
+ ...opts,
6448
+ headers: {
6449
+ "Content-Type": "application/json",
6450
+ Authorization: `Bearer ${apiKey}`,
6451
+ ...opts.headers ?? {}
6452
+ }
6453
+ });
6454
+ let body;
6455
+ try {
6456
+ body = await res.json();
6457
+ } catch {
6458
+ body = null;
6459
+ }
6460
+ if (!res.ok) throw makeFetchError(res.status, body);
6461
+ return body;
6462
+ }
6463
+ return {
6464
+ async getDebate(debateId) {
6465
+ return apiFetch(`/api/v1/debates/${debateId}`);
6466
+ },
6467
+ async getCommonsLogEvents(cid, opts = {}) {
6468
+ const requested = opts.limit ?? 1e3;
6469
+ const PAGE = 1e3;
6470
+ const offsetStart = opts.offset ?? 0;
6471
+ let collected = [];
6472
+ let cursor = offsetStart;
6473
+ while (collected.length < requested) {
6474
+ const params = new URLSearchParams();
6475
+ params.set("offset", String(cursor));
6476
+ params.set("limit", String(Math.min(PAGE, requested - collected.length)));
6477
+ const page = await apiFetch(
6478
+ `/api/v1/commons-logs/${cid}/events?${params}`
6479
+ );
6480
+ const hoisted = page.events.map((e) => ({
6481
+ ...e,
6482
+ event_type: e.event_type ?? e.payload?.event_type ?? ""
6483
+ }));
6484
+ collected = collected.concat(hoisted);
6485
+ if (page.events.length === 0 || page.next_offset === cursor) break;
6486
+ cursor = page.next_offset;
6487
+ }
6488
+ return { events: collected, next_offset: cursor };
6489
+ },
6490
+ async discoverPaAgentId() {
6491
+ const result = await apiFetch(
6492
+ "/api/v1/agents?capability=convergence_synthesizer.v1"
6493
+ );
6494
+ const listings = Array.isArray(result) ? result : result.agents ?? [];
6495
+ if (listings.length === 0) {
6496
+ throw new LinkedClawError("pa_not_found", "No agent found with capability convergence_synthesizer.v1");
6497
+ }
6498
+ return listings[0].agent_id;
6499
+ },
6500
+ async findExistingMandate(principalAgentId, delegateAgentId, requiredScopes) {
6501
+ const result = await apiFetch(`/api/v1/mandates?kind=generalized`);
6502
+ const list = Array.isArray(result) ? result : result.mandates ?? [];
6503
+ const required = new Set(requiredScopes);
6504
+ const now = Date.now();
6505
+ for (const m of list) {
6506
+ if (m.principal_agent_id !== principalAgentId) continue;
6507
+ if (m.delegate_agent_id !== delegateAgentId) continue;
6508
+ if (m.revoked_at) continue;
6509
+ if (m.expires_at && new Date(m.expires_at).getTime() <= now) continue;
6510
+ if (![...required].every((s) => m.scope.includes(s))) continue;
6511
+ return m;
6512
+ }
6513
+ return null;
6514
+ },
6515
+ async issueMandate(principalAgentId, delegateAgentId, scopes, expiresAt) {
6516
+ return apiFetch("/api/v1/mandates", {
6517
+ method: "POST",
6518
+ body: JSON.stringify({
6519
+ principal_agent_id: principalAgentId,
6520
+ delegate_agent_id: delegateAgentId,
6521
+ scope: scopes,
6522
+ ...expiresAt ? { expires_at: expiresAt } : {}
6523
+ })
6524
+ });
6525
+ },
6526
+ async startRun(sourceDebateId) {
6527
+ return apiFetch("/api/v1/convergence/runs", {
6528
+ method: "POST",
6529
+ body: JSON.stringify({ source_debate_id: sourceDebateId })
6530
+ });
6531
+ },
6532
+ async getRun(runId) {
6533
+ return apiFetch(`/api/v1/convergence/runs/${runId}`);
6534
+ },
6535
+ async acceptOwnerB(runId) {
6536
+ return apiFetch(`/api/v1/convergence/runs/${runId}/owner_b_accept`, {
6537
+ method: "POST"
6538
+ });
6539
+ },
6540
+ async appendCommonsLog(cid, eventType, payload) {
6541
+ return apiFetch(`/api/v1/commons-logs/${cid}/append`, {
6542
+ method: "POST",
6543
+ body: JSON.stringify({ event_type: eventType, payload })
6544
+ });
6545
+ },
6546
+ async acceptCruxDecision(runId, cruxId, body) {
6547
+ return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/accept`, {
6548
+ method: "POST",
6549
+ body: JSON.stringify(body)
6550
+ });
6551
+ },
6552
+ async rejectCruxDecision(runId, cruxId, body) {
6553
+ return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/reject`, {
6554
+ method: "POST",
6555
+ body: JSON.stringify(body)
6556
+ });
6557
+ },
6558
+ async attestCruxDecision(runId, cruxId, body) {
6559
+ return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/attest`, {
6560
+ method: "POST",
6561
+ body: JSON.stringify(body)
6562
+ });
6563
+ }
6564
+ };
6565
+ }
6566
+
6567
+ // src/converge/hash.ts
6568
+ import { createHash as createHash2 } from "crypto";
6569
+ function encodeString(s) {
6570
+ let out = '"';
6571
+ for (let i = 0; i < s.length; i++) {
6572
+ const cp = s.charCodeAt(i);
6573
+ if (cp === 34) out += '\\"';
6574
+ else if (cp === 92) out += "\\\\";
6575
+ else if (cp === 8) out += "\\b";
6576
+ else if (cp === 9) out += "\\t";
6577
+ else if (cp === 10) out += "\\n";
6578
+ else if (cp === 12) out += "\\f";
6579
+ else if (cp === 13) out += "\\r";
6580
+ else if (cp < 32 || cp > 126) out += `\\u${cp.toString(16).padStart(4, "0")}`;
6581
+ else out += s[i];
6582
+ }
6583
+ return out + '"';
6584
+ }
6585
+ function canonicalize(value) {
6586
+ if (value === null) return "null";
6587
+ if (typeof value === "string") return encodeString(value);
6588
+ if (typeof value !== "object") return JSON.stringify(value);
6589
+ if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
6590
+ const keys = Object.keys(value).sort();
6591
+ return "{" + keys.map((k) => encodeString(k) + ":" + canonicalize(value[k])).join(",") + "}";
6592
+ }
6593
+ function sha256OfCanonicalJson(value) {
6594
+ const h = createHash2("sha256");
6595
+ h.update(canonicalize(value));
6596
+ return "sha256:" + h.digest("hex");
6597
+ }
6598
+
6599
+ // src/converge/lock.ts
6600
+ import { closeSync, openSync, unlinkSync, writeSync } from "fs";
6601
+ import { join as join2 } from "path";
6602
+ var LOCK_FILENAME = ".lock";
6603
+ function acquireLock(stagingDir) {
6604
+ const path2 = join2(stagingDir, LOCK_FILENAME);
6605
+ let fd;
6606
+ try {
6607
+ fd = openSync(path2, "wx");
6608
+ } catch (e) {
6609
+ if (e.code === "EEXIST") {
6610
+ throw new LinkedClawError(
6611
+ "lock_held",
6612
+ `Lock held at ${path2}. If no other run/accept is in progress, delete ${path2} to recover.`
6613
+ );
6614
+ }
6615
+ throw e;
6616
+ }
6617
+ writeSync(fd, JSON.stringify({ pid: process.pid }));
6618
+ closeSync(fd);
6619
+ return () => {
6620
+ try {
6621
+ unlinkSync(path2);
6622
+ } catch {
6623
+ }
6624
+ };
6625
+ }
6626
+
6627
+ // src/converge/staging.ts
6628
+ import { createHash as createHash3 } from "crypto";
6629
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync2 } from "fs";
6630
+ import { dirname as dirname2, join as join3 } from "path";
6631
+ import { load as yamlLoad2, dump as yamlDump2 } from "js-yaml";
6632
+ function stagingPathFor(stagingDir, cruxId) {
6633
+ return join3(stagingDir, `${cruxId}.md`);
6634
+ }
6635
+ function listCruxFiles(stagingDir) {
6636
+ if (!existsSync2(stagingDir)) return [];
6637
+ return readdirSync(stagingDir).filter(
6638
+ (f) => f.endsWith(".md") && !f.startsWith(".")
6639
+ );
6640
+ }
6641
+ function parseStaging(text) {
6642
+ if (!text.startsWith("---\n")) {
6643
+ throw new Error("Missing YAML frontmatter: document must start with ---\\n");
6644
+ }
6645
+ const endIdx = text.indexOf("\n---\n", 4);
6646
+ if (endIdx === -1) {
6647
+ throw new Error("Malformed frontmatter: no closing ---");
6648
+ }
6649
+ const yamlText = text.slice(4, endIdx);
6650
+ const body = text.slice(endIdx + 5);
6651
+ const raw = yamlLoad2(yamlText);
6652
+ if (!raw || typeof raw !== "object") {
6653
+ throw new Error("Frontmatter parsed to non-object");
6654
+ }
6655
+ const userResponse = typeof raw._user_response === "string" ? raw._user_response : "";
6656
+ delete raw._user_response;
6657
+ return {
6658
+ frontmatter: raw,
6659
+ userResponse,
6660
+ body
6661
+ };
6662
+ }
6663
+ function dumpStaging(doc) {
6664
+ const fmRaw = { ...doc.frontmatter };
6665
+ fmRaw._user_response = doc.userResponse ?? "";
6666
+ const yamlText = yamlDump2(fmRaw, { lineWidth: -1, sortKeys: false });
6667
+ return `---
6668
+ ${yamlText}---
6669
+ ${doc.body}`;
6670
+ }
6671
+ function readStaging(path2) {
6672
+ return parseStaging(readFileSync4(path2, "utf8"));
6673
+ }
6674
+ function writeStaging(path2, doc) {
6675
+ mkdirSync2(dirname2(path2), { recursive: true });
6676
+ writeFileSync2(path2, dumpStaging(doc), "utf8");
6677
+ }
6678
+ function computePaBodyHash(body) {
6679
+ return "sha256:" + createHash3("sha256").update(Buffer.from(body, "utf8")).digest("hex");
6680
+ }
6681
+
6682
+ // src/converge/workspace.ts
6683
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
6684
+ import { dirname as dirname3, isAbsolute, join as join4, resolve } from "path";
6685
+ import { load as yamlLoad3, dump as yamlDump3 } from "js-yaml";
6686
+ var META_FILENAME = ".run-meta.yaml";
6687
+ function readRunMeta(stagingDir) {
6688
+ const metaPath = join4(stagingDir, META_FILENAME);
6689
+ if (!existsSync3(metaPath)) return null;
6690
+ return yamlLoad3(readFileSync5(metaPath, "utf8"));
6691
+ }
6692
+ function writeRunMeta(stagingDir, meta) {
6693
+ mkdirSync3(stagingDir, { recursive: true });
6694
+ writeFileSync3(join4(stagingDir, META_FILENAME), yamlDump3(meta), "utf8");
6695
+ }
6696
+ function searchUpward(startDir, maxLevels = 5) {
6697
+ let dir = startDir;
6698
+ for (let i = 0; i < maxLevels; i++) {
6699
+ if (existsSync3(join4(dir, META_FILENAME))) return dir;
6700
+ const parent = dirname3(dir);
6701
+ if (parent === dir) break;
6702
+ dir = parent;
6703
+ }
6704
+ return null;
6705
+ }
6706
+ async function resolveWorkspace(opts) {
6707
+ const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
6708
+ let stagingDir;
6709
+ let meta = null;
6710
+ if (opts.stagingDir) {
6711
+ stagingDir = isAbsolute(opts.stagingDir) ? opts.stagingDir : resolve(cwd, opts.stagingDir);
6712
+ meta = readRunMeta(stagingDir);
6713
+ if (!meta) {
6714
+ throw new LinkedClawError(
6715
+ "meta_not_found",
6716
+ `No ${META_FILENAME} found in --staging-dir: ${stagingDir}`
6717
+ );
6718
+ }
6719
+ } else {
6720
+ const found = searchUpward(cwd);
6721
+ if (found) {
6722
+ stagingDir = found;
6723
+ meta = readRunMeta(found);
6724
+ }
6725
+ }
6726
+ if (!meta) {
6727
+ if (opts.runId) {
6728
+ throw new LinkedClawError(
6729
+ "meta_not_found",
6730
+ `--run-id given but no ${META_FILENAME} found (searched upward from ${cwd}). Provide --staging-dir to locate the run workspace.`
6731
+ );
6732
+ }
6733
+ throw new LinkedClawError(
6734
+ "meta_not_found",
6735
+ `No ${META_FILENAME} found (searched upward from ${cwd}). Run 'lc converge run <debate_id>' first.`
6736
+ );
6737
+ }
6738
+ if (opts.runId && opts.runId !== meta.run_id) {
6739
+ throw new LinkedClawError(
6740
+ "run_id_mismatch",
6741
+ `--run-id ${opts.runId} does not match run_id ${meta.run_id} in ${META_FILENAME}`
6742
+ );
6743
+ }
6744
+ const targetCorpus = isAbsolute(meta.target_corpus) ? meta.target_corpus : resolve(stagingDir, meta.target_corpus);
6745
+ return {
6746
+ runId: meta.run_id,
6747
+ sourceDebateId: meta.source_debate_id,
6748
+ paAgentId: meta.pa_agent_id,
6749
+ targetCorpus,
6750
+ stagingDir
6751
+ };
6752
+ }
6753
+
6754
+ // src/commands/converge.ts
6755
+ function resolveAbs(p) {
6756
+ return isAbsolute2(p) ? p : resolve2(process.cwd(), p);
6757
+ }
6758
+ async function getMyUserId(ctx) {
6759
+ const me = await ctx.consumer.getMe();
6760
+ if (!me.user_id) throw new LinkedClawError("no_user_id", "Could not determine user_id from /api/v1/me");
6761
+ return me.user_id;
6762
+ }
6763
+ function recomputeSourceCruxMapHash(events) {
6764
+ const ev = [...events].reverse().find((e) => e.event_type === "crux_map");
6765
+ if (!ev) return null;
6766
+ return sha256OfCanonicalJson(ev.payload.crux_map_data);
6767
+ }
6768
+ function recordedSourceHash(events) {
6769
+ const ev = events.find((e) => e.event_type === "run_started");
6770
+ if (!ev) return null;
6771
+ return typeof ev.payload.source_crux_map_hash === "string" ? ev.payload.source_crux_map_hash : null;
6772
+ }
6773
+ function buildPaBody(op) {
6774
+ const synthesis = typeof op.synthesis_text === "string" ? op.synthesis_text : "";
6775
+ const questions = Array.isArray(op.open_questions) ? op.open_questions : [];
6776
+ const qText = questions.map((q) => `- ${String(q)}`).join("\n");
6777
+ return `# Synthesis
6778
+
6779
+ ${synthesis}
6780
+
6781
+ # Open questions
6782
+
6783
+ ${qText || "(none)"}
6784
+ `;
6785
+ }
6786
+ function countPreviouslyClarifiedSections(body) {
6787
+ const m = body.match(/^# Previously clarified \(round \d+\)/gm);
6788
+ return m ? m.length : 0;
6789
+ }
6790
+ function slugify(s, maxLen = 64) {
6791
+ const base = s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/g, "");
6792
+ return base || "untitled";
6793
+ }
6794
+ function extractSynthesisSlug(body, maxLen = 32) {
6795
+ const synthIdx = body.indexOf("# Synthesis");
6796
+ const search = synthIdx >= 0 ? body.slice(synthIdx) : body;
6797
+ const lines = search.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
6798
+ const first = lines[0] ?? "";
6799
+ return slugify(first, maxLen);
6800
+ }
6801
+ function tryGitAdd(absPath) {
6802
+ try {
6803
+ const r = spawnSync("git", ["add", absPath], { encoding: "utf8" });
6804
+ if (r.error) return `git_add_failed: ${r.error.message}`;
6805
+ if (r.status !== 0) return `git_add_failed: ${(r.stderr || "").trim() || `exit ${r.status}`}`;
6806
+ return null;
6807
+ } catch (e) {
6808
+ return `git_add_failed: ${e.message}`;
6809
+ }
6810
+ }
6811
+ function assertSafeCruxId(cruxId) {
6812
+ if (!cruxId || cruxId.includes("/") || cruxId.includes("\\") || cruxId.includes("..") || cruxId.includes("\0")) {
6813
+ throw new LinkedClawError("invalid_crux_id", `Invalid crux_id for local file operation: ${cruxId}`);
6814
+ }
6815
+ }
6816
+ function assertInside(parentDir, childPath) {
6817
+ const parent = resolve2(parentDir);
6818
+ const child = resolve2(childPath);
6819
+ const rel = relative(parent, child);
6820
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute2(rel)) return;
6821
+ throw new LinkedClawError("path_escape", `Refusing local path outside ${parentDir}: ${childPath}`);
6822
+ }
6823
+ function safeStagingPathFor(stagingDir, cruxId) {
6824
+ assertSafeCruxId(cruxId);
6825
+ const path2 = stagingPathFor(stagingDir, cruxId);
6826
+ assertInside(stagingDir, path2);
6827
+ return path2;
6828
+ }
6829
+ function safeAcceptedPath(finalDir, cruxId, synthSlug) {
6830
+ assertSafeCruxId(cruxId);
6831
+ const path2 = join5(finalDir, `${cruxId}__${synthSlug}.md`);
6832
+ assertInside(finalDir, path2);
6833
+ return path2;
6834
+ }
6835
+ function computeDecisionBodyHash(synthesisText, citationsA, citationsB) {
6836
+ return sha256OfCanonicalJson({
6837
+ citations_a: citationsA,
6838
+ citations_b: citationsB,
6839
+ synthesis_text: synthesisText
6840
+ });
6841
+ }
6842
+ function eventKind(ev) {
6843
+ return typeof ev.payload.event_type === "string" ? ev.payload.event_type : ev.event_type;
6844
+ }
6845
+ function decisionEventTypeForAction(action) {
6846
+ if (action === "accept") return "accept_attestation";
6847
+ if (action === "reject") return "reject_attestation";
6848
+ return "attest_only";
6849
+ }
6850
+ function latestConvergenceMapEvent(events) {
6851
+ const operatorUserId = process.env.LINKEDCLAW_OPERATOR_USER_ID ?? "usr_operator";
6852
+ const reversed = [...events].reverse();
6853
+ const ev = reversed.find((e) => eventKind(e) === "convergence_map" && e.signed_by === operatorUserId);
6854
+ if (!ev) {
6855
+ throw new LinkedClawError(
6856
+ "convergence_map_not_found",
6857
+ `No PA-signed convergence_map event found for this run (expected signed_by=${operatorUserId}).`
6858
+ );
6859
+ }
6860
+ return ev;
6861
+ }
6862
+ function latestConvergenceMap(events) {
6863
+ return latestConvergenceMapEvent(events).payload;
6864
+ }
6865
+ function getCruxFromMap(map, cruxId) {
6866
+ const cruxes = Array.isArray(map.cruxes) ? map.cruxes : [];
6867
+ const crux = cruxes.find(
6868
+ (c) => c && typeof c === "object" && c.crux_id === cruxId
6869
+ );
6870
+ if (!crux || typeof crux !== "object") {
6871
+ throw new LinkedClawError("crux_not_found", `crux ${cruxId} not found in latest convergence_map.`);
6872
+ }
6873
+ return crux;
6874
+ }
6875
+ function citationsFromCrux(value) {
6876
+ if (!Array.isArray(value)) return [];
6877
+ return value.filter((v) => v != null && typeof v === "object" && !Array.isArray(v));
6878
+ }
6879
+ function classifyDecisionAttestation(action, outcome, bilateralMandateIntact, synthesisEdited = false) {
6880
+ if (action === "attest") return "user_attested_no_dialog";
6881
+ if (action === "reject") return "user_attested_with_network_context";
6882
+ if (outcome === "already_aligned") {
6883
+ throw new LinkedClawError(
6884
+ "attest_required",
6885
+ "outcome=already_aligned must be decided with `lc converge attest`."
6886
+ );
6887
+ }
6888
+ if ((outcome === "converged" || outcome === "partial_overlap") && bilateralMandateIntact && !synthesisEdited) {
6889
+ return "bilateral_convergence";
6890
+ }
6891
+ return "user_attested_with_network_context";
6892
+ }
6893
+ function decisionPayloadCitations(value) {
6894
+ return citationsFromCrux(value);
6895
+ }
6896
+ function stringFromPayload(payload, key) {
6897
+ const value = payload[key];
6898
+ if (typeof value !== "string" || value.length === 0) {
6899
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision missing ${key}.`);
6900
+ }
6901
+ return value;
6902
+ }
6903
+ function booleanFromPayload(payload, key) {
6904
+ const value = payload[key];
6905
+ if (typeof value !== "boolean") {
6906
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision missing ${key}.`);
6907
+ }
6908
+ return value;
6909
+ }
6910
+ function attestationFromPayload(payload) {
6911
+ const value = stringFromPayload(payload, "attestation");
6912
+ if (value !== "bilateral_convergence" && value !== "user_attested_with_network_context" && value !== "user_attested_no_dialog") {
6913
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision has invalid attestation: ${value}`);
6914
+ }
6915
+ return value;
6916
+ }
6917
+ function terminalOutcomeFromPayload(payload) {
6918
+ const value = stringFromPayload(payload, "terminal_outcome");
6919
+ if (value !== "converged" && value !== "partial_overlap" && value !== "needs_input" && value !== "irreconcilable" && value !== "already_aligned") {
6920
+ throw new LinkedClawError("decision_payload_invalid", `terminal decision has invalid outcome: ${value}`);
6921
+ }
6922
+ return value;
6923
+ }
6924
+ async function buildCruxDecisionRequest(api, ws, cruxId, action, opts = {}) {
6925
+ assertSafeCruxId(cruxId);
6926
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
6927
+ const map = latestConvergenceMap(events);
6928
+ const crux = getCruxFromMap(map, cruxId);
6929
+ const generationId = typeof crux.generation_id === "string" ? crux.generation_id : "";
6930
+ const sourceHash = typeof map.source_crux_map_hash === "string" ? map.source_crux_map_hash : "";
6931
+ const outcome = typeof crux.outcome === "string" ? crux.outcome : "";
6932
+ const latestSubDebateId = typeof crux.latest_sub_debate_id === "string" ? crux.latest_sub_debate_id : null;
6933
+ 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;
6934
+ const synthesisText = typeof crux.synthesis_text === "string" ? crux.synthesis_text : "";
6935
+ const citationsA = citationsFromCrux(crux.citations_a);
6936
+ const citationsB = citationsFromCrux(crux.citations_b);
6937
+ if (!generationId) throw new LinkedClawError("missing_generation_id", `crux ${cruxId} has no generation_id.`);
6938
+ if (!sourceHash) throw new LinkedClawError("missing_source_hash", "latest convergence_map has no source_crux_map_hash.");
6939
+ if (!outcome) throw new LinkedClawError("missing_outcome", `crux ${cruxId} has no outcome.`);
6940
+ if (action === "accept" && outcome === "already_aligned") {
6941
+ throw new LinkedClawError(
6942
+ "attest_required",
6943
+ "outcome=already_aligned must be decided with `lc converge attest`."
6944
+ );
6945
+ }
6946
+ if (!synthesisText) throw new LinkedClawError("missing_synthesis_text", `crux ${cruxId} has no synthesis_text.`);
6947
+ const paBodyHash = computeDecisionBodyHash(synthesisText, citationsA, citationsB);
6948
+ let acceptedSynthesisText = synthesisText;
6949
+ let synthesisEdited = false;
6950
+ if (action === "accept") {
6951
+ const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
6952
+ if (existsSync4(stagingPath)) {
6953
+ const doc = readStaging(stagingPath);
6954
+ synthesisEdited = computePaBodyHash(doc.body) !== doc.frontmatter.pa_body_hash;
6955
+ if (synthesisEdited) acceptedSynthesisText = doc.body;
6956
+ }
6957
+ }
6958
+ const acceptedBodyHash = computeDecisionBodyHash(acceptedSynthesisText, citationsA, citationsB);
6959
+ return {
6960
+ convergence_map_generation_id: generationId,
6961
+ source_crux_map_hash: sourceHash,
6962
+ latest_sub_debate_id: latestSubDebateId,
6963
+ terminal_outcome: outcome,
6964
+ bilateral_mandate_intact: bilateralMandateIntact,
6965
+ attestation: classifyDecisionAttestation(action, outcome, bilateralMandateIntact, synthesisEdited),
6966
+ synthesis_edited: synthesisEdited,
6967
+ pa_body_hash: paBodyHash,
6968
+ accepted_body_hash: acceptedBodyHash,
6969
+ synthesis_text: acceptedSynthesisText,
6970
+ citations_a: citationsA,
6971
+ citations_b: citationsB,
6972
+ ...opts.message ? { user_message: opts.message } : {}
6973
+ };
6974
+ }
6975
+ async function postCruxDecision(api, ws, cruxId, action, opts = {}) {
6976
+ assertSafeCruxId(cruxId);
6977
+ const body = await buildCruxDecisionRequest(api, ws, cruxId, action, opts);
6978
+ 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);
6979
+ return { event_id: resp.event_id, body };
6980
+ }
6981
+ async function materializeAcceptedCrux(ctx, api, ws, cruxId, payload, opts = {}) {
6982
+ const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
6983
+ if (!existsSync4(stagingPath)) return { warning: `staging_not_found: ${stagingPath}` };
6984
+ const doc = readStaging(stagingPath);
6985
+ const fm = doc.frontmatter;
6986
+ const sourceDebate = await api.getDebate(ws.sourceDebateId);
6987
+ const body = stringFromPayload(payload, "synthesis_text");
6988
+ const citationsA = decisionPayloadCitations(payload.citations_a);
6989
+ const citationsB = decisionPayloadCitations(payload.citations_b);
6990
+ const acceptedBodyHash = stringFromPayload(payload, "accepted_body_hash");
6991
+ const computedAcceptedBodyHash = computeDecisionBodyHash(body, citationsA, citationsB);
6992
+ if (acceptedBodyHash !== computedAcceptedBodyHash) {
6993
+ return { warning: `decision_body_hash_mismatch: ${cruxId}` };
6994
+ }
6995
+ const paBodyHash = stringFromPayload(payload, "pa_body_hash");
6996
+ const synthesisEdited = booleanFromPayload(payload, "synthesis_edited");
6997
+ const attestation = attestationFromPayload(payload);
6998
+ const terminalOutcome = terminalOutcomeFromPayload(payload);
6999
+ const generationId = stringFromPayload(payload, "convergence_map_generation_id");
7000
+ const me = await getMyUserId(ctx);
7001
+ const acceptedDoc = {
7002
+ frontmatter: {
7003
+ ...fm,
7004
+ generation_id: generationId,
7005
+ pa_body_hash: paBodyHash,
7006
+ outcome: terminalOutcome,
7007
+ bilateral_mandate_intact: booleanFromPayload(payload, "bilateral_mandate_intact"),
7008
+ citations_a: citationsA,
7009
+ citations_b: citationsB,
7010
+ provenance: {
7011
+ signed_off_by: me,
7012
+ signed_off_at: (/* @__PURE__ */ new Date()).toISOString(),
7013
+ accepted_generation_id: generationId,
7014
+ attestation,
7015
+ synthesis_edited: synthesisEdited,
7016
+ ...opts.message ? { user_message: opts.message } : {},
7017
+ ...synthesisEdited ? { pa_body_hash: paBodyHash, accepted_body_hash: acceptedBodyHash } : {}
7018
+ }
7019
+ },
7020
+ userResponse: "",
7021
+ body
7022
+ };
7023
+ const topicSlug = slugify(sourceDebate.topic ?? ws.sourceDebateId);
7024
+ const synthSlug = extractSynthesisSlug(body);
7025
+ const finalDir = join5(ws.targetCorpus, "converged", topicSlug);
7026
+ const finalPath = safeAcceptedPath(finalDir, cruxId, synthSlug);
7027
+ if (existsSync4(finalPath) && readStaging(finalPath).body !== acceptedDoc.body) {
7028
+ return { warning: `sync_conflict: ${finalPath}` };
7029
+ }
7030
+ mkdirSync4(finalDir, { recursive: true });
7031
+ if (!existsSync4(finalPath) || dumpStaging(readStaging(finalPath)) !== dumpStaging(acceptedDoc)) {
7032
+ writeStaging(finalPath, acceptedDoc);
7033
+ }
7034
+ unlinkSync2(stagingPath);
7035
+ const gitWarning = tryGitAdd(finalPath);
7036
+ return {
7037
+ accepted_path: finalPath,
7038
+ attestation,
7039
+ synthesis_edited: synthesisEdited,
7040
+ ...gitWarning ? { warning: gitWarning } : {}
7041
+ };
7042
+ }
7043
+ async function syncTerminalDecisions(ctx, api, ws, opts = {}) {
7044
+ if (opts.cruxId) assertSafeCruxId(opts.cruxId);
7045
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
7046
+ const warnings = [];
7047
+ const mapEvent = latestConvergenceMapEvent(events);
7048
+ const map = mapEvent.payload;
7049
+ const cruxGeneration = /* @__PURE__ */ new Map();
7050
+ for (const crux of Array.isArray(map.cruxes) ? map.cruxes : []) {
7051
+ if (!crux || typeof crux !== "object" || Array.isArray(crux)) continue;
7052
+ const c = crux;
7053
+ if (typeof c.crux_id === "string" && typeof c.generation_id === "string") {
7054
+ cruxGeneration.set(c.crux_id, c.generation_id);
7055
+ }
7056
+ }
7057
+ const terminal = /* @__PURE__ */ new Map();
7058
+ for (const ev of [...events].sort((a, b) => a.seq - b.seq)) {
7059
+ if (ev.seq <= mapEvent.seq) continue;
7060
+ if (ev.signed_by !== mapEvent.signed_by) continue;
7061
+ const eventType = eventKind(ev);
7062
+ if (!["accept_attestation", "reject_attestation", "attest_only"].includes(eventType)) continue;
7063
+ const cid = typeof ev.payload.crux_id === "string" ? ev.payload.crux_id : "";
7064
+ if (!cid || terminal.has(cid)) continue;
7065
+ try {
7066
+ assertSafeCruxId(cid);
7067
+ } catch (err) {
7068
+ warnings.push(`${cid}: ${err.message}`);
7069
+ continue;
7070
+ }
7071
+ if (ev.payload.convergence_map_generation_id !== cruxGeneration.get(cid)) continue;
7072
+ terminal.set(cid, { eventType, payload: ev.payload });
7073
+ }
7074
+ if (opts.injectedTerminal) {
7075
+ assertSafeCruxId(opts.injectedTerminal.cruxId);
7076
+ terminal.set(opts.injectedTerminal.cruxId, {
7077
+ eventType: opts.injectedTerminal.eventType,
7078
+ payload: opts.injectedTerminal.payload
7079
+ });
7080
+ }
7081
+ const materialized = [];
7082
+ const cleaned = [];
7083
+ const release = acquireLock(ws.stagingDir);
7084
+ try {
7085
+ for (const [cid, terminalEvent] of terminal.entries()) {
7086
+ if (opts.cruxId && cid !== opts.cruxId) continue;
7087
+ const stagingPath = safeStagingPathFor(ws.stagingDir, cid);
7088
+ const eventType = terminalEvent.eventType;
7089
+ if (eventType === "accept_attestation") {
7090
+ const result = await materializeAcceptedCrux(ctx, api, ws, cid, terminalEvent.payload, opts);
7091
+ if (result.accepted_path) materialized.push(result.accepted_path);
7092
+ if (result.warning) warnings.push(`${cid}: ${result.warning}`);
7093
+ continue;
7094
+ }
7095
+ if (existsSync4(stagingPath)) {
7096
+ unlinkSync2(stagingPath);
7097
+ cleaned.push(cid);
7098
+ }
7099
+ }
7100
+ } finally {
7101
+ release();
7102
+ }
7103
+ return { materialized, cleaned, warnings };
7104
+ }
7105
+ function registerConvergeCommands(program2) {
7106
+ const converge = program2.command("converge").description("Convergence: bilateral merger of two crux maps into shared corpus");
7107
+ 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(
7108
+ async (ref, opts) => {
7109
+ await runCommand(async () => {
7110
+ const ctx = buildContext();
7111
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7112
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7113
+ let ws;
7114
+ const resolvedStagingDir = opts.stagingDir ? resolveAbs(opts.stagingDir) : null;
7115
+ const metaExisting = resolvedStagingDir ? readRunMeta(resolvedStagingDir) : null;
7116
+ if (opts.accept) {
7117
+ if (!ref) {
7118
+ throw new LinkedClawError("missing_run_id", "--accept requires the run_id as a positional argument.");
7119
+ }
7120
+ const runId = ref;
7121
+ if (metaExisting && resolvedStagingDir) {
7122
+ const meta = metaExisting;
7123
+ ws = {
7124
+ runId: meta.run_id,
7125
+ sourceDebateId: meta.source_debate_id,
7126
+ paAgentId: meta.pa_agent_id,
7127
+ targetCorpus: meta.target_corpus,
7128
+ stagingDir: resolvedStagingDir
7129
+ };
7130
+ } else {
7131
+ if (!opts.targetCorpus) {
7132
+ throw new LinkedClawError(
7133
+ "missing_target_corpus",
7134
+ "Owner B --accept requires --target-corpus on first call."
7135
+ );
7136
+ }
7137
+ const targetCorpus = resolveAbs(opts.targetCorpus);
7138
+ let sourceDebateId;
7139
+ let paAgentId;
7140
+ let principalAgentId;
7141
+ if (opts.sourceDebateId) {
7142
+ sourceDebateId = opts.sourceDebateId;
7143
+ paAgentId = await api.discoverPaAgentId();
7144
+ const sourceDebate = await api.getDebate(sourceDebateId);
7145
+ principalAgentId = sourceDebate.agent_b_id;
7146
+ } else {
7147
+ const runMeta = await api.getRun(runId);
7148
+ sourceDebateId = runMeta.source_debate_id;
7149
+ paAgentId = runMeta.pa_agent_id;
7150
+ principalAgentId = runMeta.agent_b_id;
7151
+ }
7152
+ const existing = await api.findExistingMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7153
+ if (!existing) await api.issueMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7154
+ await api.acceptOwnerB(runId);
7155
+ const stagingDir = join5(targetCorpus, "converged", "staging", runId);
7156
+ mkdirSync4(stagingDir, { recursive: true });
7157
+ const meta = {
7158
+ run_id: runId,
7159
+ source_debate_id: sourceDebateId,
7160
+ pa_agent_id: paAgentId,
7161
+ target_corpus: targetCorpus,
7162
+ owner_role: "b"
7163
+ };
7164
+ writeRunMeta(stagingDir, meta);
7165
+ ws = { runId, sourceDebateId, paAgentId, targetCorpus, stagingDir };
7166
+ }
7167
+ } else if (!metaExisting) {
7168
+ if (!ref) {
7169
+ throw new LinkedClawError(
7170
+ "missing_source_debate_id",
7171
+ "First run requires a source_debate_id argument. Use --staging-dir to resume an existing run."
7172
+ );
7173
+ }
7174
+ if (!opts.targetCorpus) {
7175
+ throw new LinkedClawError("missing_target_corpus", "First call requires --target-corpus.");
7176
+ }
7177
+ const targetCorpus = resolveAbs(opts.targetCorpus);
7178
+ const paAgentId = await api.discoverPaAgentId();
7179
+ const sourceDebate = await api.getDebate(ref);
7180
+ const principalAgentId = sourceDebate.agent_a_id;
7181
+ const existing = await api.findExistingMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7182
+ if (!existing) await api.issueMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
7183
+ const { run_id } = await api.startRun(ref);
7184
+ const stagingDir = join5(targetCorpus, "converged", "staging", run_id);
7185
+ mkdirSync4(stagingDir, { recursive: true });
7186
+ const meta = {
7187
+ run_id,
7188
+ source_debate_id: ref,
7189
+ pa_agent_id: paAgentId,
7190
+ target_corpus: targetCorpus,
7191
+ owner_role: "a"
7192
+ };
7193
+ writeRunMeta(stagingDir, meta);
7194
+ ws = { runId: run_id, sourceDebateId: ref, paAgentId, targetCorpus, stagingDir };
7195
+ } else {
7196
+ ws = await resolveWorkspace({ stagingDir: opts.stagingDir });
7197
+ }
7198
+ const refreshStaging = async (cruxes, canonicalSourceHash2) => {
7199
+ for (const c of cruxes) {
7200
+ if (!c.latest_sub_debate_id) continue;
7201
+ const subD = await api.getDebate(c.latest_sub_debate_id);
7202
+ const subEvs = await api.getCommonsLogEvents(subD.commons_log_id, { limit: 2e3 });
7203
+ const outcomeEv = [...subEvs.events].reverse().find(
7204
+ (e) => e.payload.event_type === "convergence_outcome" || e.event_type === "convergence_outcome"
7205
+ );
7206
+ if (!outcomeEv) continue;
7207
+ const op = outcomeEv.payload;
7208
+ const body = buildPaBody(op);
7209
+ const newPaBodyHash = computePaBodyHash(body);
7210
+ const path2 = safeStagingPathFor(ws.stagingDir, c.crux_id);
7211
+ const existingDoc = existsSync4(path2) ? readStaging(path2) : null;
7212
+ if (existingDoc && existingDoc.frontmatter.pa_body_hash === newPaBodyHash) continue;
7213
+ const fm = {
7214
+ debate_id: ws.sourceDebateId,
7215
+ run_id: ws.runId,
7216
+ crux_id: c.crux_id,
7217
+ sub_debate_chain: c.sub_debate_chain,
7218
+ latest_sub_debate_id: c.latest_sub_debate_id,
7219
+ source_crux_map_hash: canonicalSourceHash2,
7220
+ generation_id: `gen_${ws.runId.slice(-8)}`,
7221
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
7222
+ pa_body_hash: newPaBodyHash,
7223
+ outcome: op.outcome ?? c.outcome,
7224
+ bilateral_mandate_intact: typeof op.bilateral_mandate_intact === "boolean" ? op.bilateral_mandate_intact : c.bilateral_mandate_intact ?? false,
7225
+ citations_a: Array.isArray(op.citations_a) ? op.citations_a : [],
7226
+ citations_b: Array.isArray(op.citations_b) ? op.citations_b : [],
7227
+ mod_progress_summary: op.final_progress_signal != null && typeof op.final_progress_signal === "object" && !Array.isArray(op.final_progress_signal) ? op.final_progress_signal : {},
7228
+ attested_by_user: existingDoc?.frontmatter.attested_by_user ?? false
7229
+ };
7230
+ writeStaging(path2, { frontmatter: fm, userResponse: existingDoc?.userResponse ?? "", body });
7231
+ }
7232
+ };
7233
+ let summary;
7234
+ let canonicalSourceHash = "";
7235
+ {
7236
+ const release = acquireLock(ws.stagingDir);
7237
+ try {
7238
+ for (const fn of listCruxFiles(ws.stagingDir)) {
7239
+ const path2 = join5(ws.stagingDir, fn);
7240
+ const doc = readStaging(path2);
7241
+ const text = (doc.userResponse || "").trim();
7242
+ if (!text) continue;
7243
+ const subDebateId = doc.frontmatter.latest_sub_debate_id;
7244
+ if (!subDebateId) continue;
7245
+ const subDebate = await api.getDebate(subDebateId);
7246
+ await api.appendCommonsLog(subDebate.commons_log_id, "owner_clarification", {
7247
+ event_type: "owner_clarification",
7248
+ content: text,
7249
+ in_response_to_event_id: null
7250
+ });
7251
+ const round = countPreviouslyClarifiedSections(doc.body) + 1;
7252
+ doc.userResponse = "";
7253
+ doc.body = doc.body.trimEnd() + `
7254
+
7255
+ # Previously clarified (round ${round})
7256
+
7257
+ ${text}
7258
+ `;
7259
+ writeStaging(path2, doc);
7260
+ }
7261
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
7262
+ summary = reduceRunState(ws, events);
7263
+ const sourceDebate = await api.getDebate(ws.sourceDebateId);
7264
+ const sourceEvents = await api.getCommonsLogEvents(sourceDebate.commons_log_id, {
7265
+ limit: 2e3
7266
+ });
7267
+ const liveSourceHash = recomputeSourceCruxMapHash(sourceEvents.events);
7268
+ const recordedHash = recordedSourceHash(events);
7269
+ if (recordedHash && liveSourceHash && liveSourceHash !== recordedHash && !opts.forceRegenerate) {
7270
+ throw new LinkedClawError(
7271
+ "source_crux_map_drift",
7272
+ `Source crux-map changed since run started (recorded=${recordedHash} live=${liveSourceHash}). Re-run with --force-regenerate.`
7273
+ );
7274
+ }
7275
+ canonicalSourceHash = recordedHash ?? liveSourceHash ?? "";
7276
+ await refreshStaging(summary.cruxes, canonicalSourceHash);
7277
+ } finally {
7278
+ release();
7279
+ }
7280
+ }
7281
+ if (opts.wait && !summary.terminal_emitted) {
7282
+ const deadline = Date.now() + opts.wait * 1e3;
7283
+ let polledEvents = null;
7284
+ while (Date.now() < deadline) {
7285
+ await new Promise((r) => setTimeout(r, 2e3));
7286
+ const { events: polled } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
7287
+ if (reduceRunState(ws, polled).terminal_emitted) {
7288
+ polledEvents = polled;
7289
+ break;
7290
+ }
7291
+ }
7292
+ if (polledEvents) {
7293
+ summary = reduceRunState(ws, polledEvents);
7294
+ const release2 = acquireLock(ws.stagingDir);
7295
+ try {
7296
+ await refreshStaging(summary.cruxes, canonicalSourceHash);
7297
+ } finally {
7298
+ release2();
7299
+ }
7300
+ }
7301
+ }
7302
+ return { run_id: ws.runId, summary, terminal_emitted: summary.terminal_emitted };
7303
+ });
7304
+ }
7305
+ );
7306
+ converge.command("clarify <sub_debate_id> <text>").description("Post owner_clarification.v1 directly to a sub-debate's Commons Log").action(async (subDebateId, text) => {
7307
+ await runCommand(async () => {
7308
+ const ctx = buildContext();
7309
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7310
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7311
+ const subDebate = await api.getDebate(subDebateId);
7312
+ const { seq } = await api.appendCommonsLog(subDebate.commons_log_id, "owner_clarification", {
7313
+ event_type: "owner_clarification",
7314
+ content: text,
7315
+ in_response_to_event_id: null
7316
+ });
7317
+ return { sub_debate_id: subDebateId, commons_log_id: subDebate.commons_log_id, seq };
7318
+ });
7319
+ });
7320
+ 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) => {
7321
+ await runCommand(async () => {
7322
+ const ctx = buildContext();
7323
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7324
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7325
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7326
+ const { event_id } = await postCruxDecision(api, ws, cruxId, "attest");
7327
+ return { run_id: ws.runId, crux_id: cruxId, action: "attest", event_id, synced: false };
7328
+ });
7329
+ });
7330
+ 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) => {
7331
+ await runCommand(async () => {
7332
+ const ctx = buildContext();
7333
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7334
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7335
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7336
+ const { event_id, body } = await postCruxDecision(api, ws, cruxId, "accept", { message: opts.message });
7337
+ if (opts.withSync) {
7338
+ const sync = await syncTerminalDecisions(ctx, api, ws, {
7339
+ cruxId,
7340
+ message: opts.message,
7341
+ injectedTerminal: {
7342
+ cruxId,
7343
+ eventType: decisionEventTypeForAction("accept"),
7344
+ payload: { event_type: decisionEventTypeForAction("accept"), crux_id: cruxId, ...body }
7345
+ }
7346
+ });
7347
+ return {
7348
+ run_id: ws.runId,
7349
+ crux_id: cruxId,
7350
+ action: "accept",
7351
+ event_id,
7352
+ synced: true,
7353
+ ...sync
7354
+ };
7355
+ }
7356
+ return { run_id: ws.runId, crux_id: cruxId, action: "accept", event_id, synced: false };
7357
+ });
7358
+ });
7359
+ 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) => {
7360
+ await runCommand(async () => {
7361
+ const ctx = buildContext();
7362
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7363
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7364
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7365
+ const { event_id } = await postCruxDecision(api, ws, cruxId, "reject");
7366
+ return { run_id: ws.runId, crux_id: cruxId, action: "reject", event_id, synced: false };
7367
+ });
7368
+ });
7369
+ 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) => {
7370
+ await runCommand(async () => {
7371
+ const ctx = buildContext();
7372
+ if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
7373
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7374
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7375
+ const result = await syncTerminalDecisions(ctx, api, ws, { cruxId: opts.cruxId });
7376
+ return { run_id: ws.runId, synced: true, ...result };
7377
+ });
7378
+ });
7379
+ converge.command("review").description("List staging cruxes; surface already_aligned cruxes prominently").option("--run-id <id>").option("--staging-dir <path>").action(async (opts) => {
7380
+ await runCommand(async () => {
7381
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7382
+ const files = listCruxFiles(ws.stagingDir);
7383
+ const cruxes = [];
7384
+ for (const fn of files) {
7385
+ const doc = readStaging(join5(ws.stagingDir, fn));
7386
+ const fm = doc.frontmatter;
7387
+ cruxes.push({
7388
+ crux_id: fm.crux_id,
7389
+ outcome: fm.outcome,
7390
+ bilateral_mandate_intact: fm.bilateral_mandate_intact,
7391
+ attested_by_user: fm.attested_by_user,
7392
+ latest_sub_debate_id: fm.latest_sub_debate_id,
7393
+ has_user_response: (doc.userResponse || "").trim().length > 0,
7394
+ next_action: fm.outcome === "already_aligned" && !fm.attested_by_user ? "attest" : fm.outcome === "needs_input" ? "clarify_or_accept" : "accept_or_reject"
7395
+ });
7396
+ }
7397
+ const alignedAwaitingAttest = cruxes.filter((c) => c.outcome === "already_aligned" && !c.attested_by_user);
7398
+ return {
7399
+ run_id: ws.runId,
7400
+ staging_dir: ws.stagingDir,
7401
+ cruxes,
7402
+ already_aligned_awaiting_attest: alignedAwaitingAttest.map((c) => c.crux_id)
7403
+ };
7404
+ });
7405
+ });
7406
+ 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) => {
7407
+ await runCommand(async () => {
7408
+ const ctx = buildContext();
7409
+ if (!ctx.cfg.apiKey) {
7410
+ throw new Error("missing apiKey \u2014 run `linkedclaw login` first");
7411
+ }
7412
+ const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
7413
+ const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
7414
+ const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 1e3 });
7415
+ if (opts.all) return { run_id: ws.runId, events };
7416
+ return reduceRunState(ws, events);
7417
+ });
7418
+ });
7419
+ }
7420
+ function reduceRunState(ws, events) {
7421
+ let started_at = null;
7422
+ let owner_b_accepted = false;
7423
+ let terminal_emitted = false;
7424
+ const cruxMap = /* @__PURE__ */ new Map();
7425
+ for (const ev of events) {
7426
+ const p = ev.payload;
7427
+ switch (ev.event_type) {
7428
+ case "run_started":
7429
+ started_at = ev.appended_at;
7430
+ break;
7431
+ case "owner_b_accepted":
7432
+ owner_b_accepted = true;
7433
+ break;
7434
+ case "sub_debate_dispatched": {
7435
+ const cruxId = p.crux_id;
7436
+ const subDebateId = p.sub_debate_id;
7437
+ if (!cruxMap.has(cruxId)) {
7438
+ cruxMap.set(cruxId, {
7439
+ crux_id: cruxId,
7440
+ latest_sub_debate_id: subDebateId,
7441
+ sub_debate_chain: [subDebateId],
7442
+ outcome: null,
7443
+ bilateral_mandate_intact: null
7444
+ });
7445
+ } else {
7446
+ const entry = cruxMap.get(cruxId);
7447
+ entry.latest_sub_debate_id = subDebateId;
7448
+ entry.sub_debate_chain.push(subDebateId);
7449
+ }
7450
+ break;
7451
+ }
7452
+ case "sub_debate_outcome_observed": {
7453
+ const cruxId = p.crux_id;
7454
+ const entry = cruxMap.get(cruxId);
7455
+ if (entry) {
7456
+ entry.outcome = p.outcome;
7457
+ if (typeof p.bilateral_mandate_intact === "boolean") {
7458
+ entry.bilateral_mandate_intact = p.bilateral_mandate_intact;
7459
+ }
7460
+ }
7461
+ break;
7462
+ }
7463
+ case "convergence_map":
7464
+ terminal_emitted = true;
7465
+ break;
7466
+ default:
7467
+ if (ev.event_type.startsWith("terminal_")) {
7468
+ terminal_emitted = true;
7469
+ }
7470
+ }
7471
+ }
7472
+ return {
7473
+ run_id: ws.runId,
7474
+ source_debate_id: ws.sourceDebateId,
7475
+ started_at,
7476
+ owner_b_accepted,
7477
+ cruxes: Array.from(cruxMap.values()),
7478
+ terminal_emitted
7479
+ };
7480
+ }
7481
+
5664
7482
  // src/commands/provider.ts
5665
- import { readFileSync as readFileSync2 } from "fs";
5666
- import { load as yamlLoad2 } from "js-yaml";
7483
+ import { readFileSync as readFileSync6 } from "fs";
7484
+ import { load as yamlLoad4 } from "js-yaml";
5667
7485
 
5668
7486
  // ../../sdk/provider-runtime-ts/dist/index.js
5669
7487
  import { EventEmitter } from "events";
@@ -5855,8 +7673,8 @@ var nodeWsConnector = async (url) => {
5855
7673
  const ws = new WebSocket3(url);
5856
7674
  const transport = {
5857
7675
  send: (data) => {
5858
- return new Promise((resolve, reject) => {
5859
- ws.send(data, (err) => err ? reject(err) : resolve());
7676
+ return new Promise((resolve3, reject) => {
7677
+ ws.send(data, (err) => err ? reject(err) : resolve3());
5860
7678
  });
5861
7679
  },
5862
7680
  close: (code, reason) => {
@@ -5872,8 +7690,8 @@ var nodeWsConnector = async (url) => {
5872
7690
  ws.on("error", (err) => fn(err));
5873
7691
  }
5874
7692
  };
5875
- const ready = new Promise((resolve, reject) => {
5876
- ws.once("open", () => resolve());
7693
+ const ready = new Promise((resolve3, reject) => {
7694
+ ws.once("open", () => resolve3());
5877
7695
  ws.once("error", (err) => reject(err));
5878
7696
  });
5879
7697
  return { transport, ready };
@@ -6308,12 +8126,12 @@ function normalizeReply(reply, nextSeq) {
6308
8126
  return { payload: reply, seq: nextSeq };
6309
8127
  }
6310
8128
  function withTimeout(p, ms, code) {
6311
- return new Promise((resolve, reject) => {
8129
+ return new Promise((resolve3, reject) => {
6312
8130
  const t = setTimeout(() => reject(new HandlerError(code, `timed out after ${ms}ms`)), ms);
6313
8131
  p.then(
6314
8132
  (v) => {
6315
8133
  clearTimeout(t);
6316
- resolve(v);
8134
+ resolve3(v);
6317
8135
  },
6318
8136
  (err) => {
6319
8137
  clearTimeout(t);
@@ -6327,7 +8145,7 @@ function escapeRegex(s) {
6327
8145
  }
6328
8146
 
6329
8147
  // src/handlers/subprocess.ts
6330
- import { spawn } from "child_process";
8148
+ import { spawn as spawn2 } from "child_process";
6331
8149
  import { randomUUID } from "crypto";
6332
8150
  import { createInterface } from "readline";
6333
8151
  var SubprocessHandler = class {
@@ -6339,7 +8157,7 @@ var SubprocessHandler = class {
6339
8157
  this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e5;
6340
8158
  const shell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
6341
8159
  const shellArgs = process.platform === "win32" ? ["/c", opts.cmd] : ["-c", opts.cmd];
6342
- this.child = spawn(shell, shellArgs, {
8160
+ this.child = spawn2(shell, shellArgs, {
6343
8161
  stdio: ["pipe", "pipe", "inherit"],
6344
8162
  cwd: opts.cwd,
6345
8163
  env: { ...process.env, ...opts.env }
@@ -6376,10 +8194,10 @@ var SubprocessHandler = class {
6376
8194
  async onInvoke(evt) {
6377
8195
  return this.request(evt);
6378
8196
  }
6379
- async onBroadcastOffer(evt) {
8197
+ async onGigTaskOffer(evt) {
6380
8198
  return this.request(evt);
6381
8199
  }
6382
- async onBroadcastExecute(evt) {
8200
+ async onGigTaskExecute(evt) {
6383
8201
  return this.request(evt);
6384
8202
  }
6385
8203
  // ───── shutdown ─────
@@ -6400,7 +8218,7 @@ var SubprocessHandler = class {
6400
8218
  request(frame) {
6401
8219
  const id = randomUUID();
6402
8220
  const line = JSON.stringify({ id, ...frame }) + "\n";
6403
- return new Promise((resolve, reject) => {
8221
+ return new Promise((resolve3, reject) => {
6404
8222
  const timer = setTimeout(() => {
6405
8223
  this.pending.delete(id);
6406
8224
  reject(new Error(`handler_timeout: no response for ${frame.type} after ${this.requestTimeoutMs}ms`));
@@ -6408,7 +8226,7 @@ var SubprocessHandler = class {
6408
8226
  if (typeof timer.unref === "function") {
6409
8227
  timer.unref();
6410
8228
  }
6411
- this.pending.set(id, { resolve, reject, timer });
8229
+ this.pending.set(id, { resolve: resolve3, reject, timer });
6412
8230
  this.stdin.write(line, (err) => {
6413
8231
  if (err) {
6414
8232
  this.pending.delete(id);
@@ -6456,9 +8274,9 @@ var SubprocessHandler = class {
6456
8274
  // src/commands/provider.ts
6457
8275
  function registerProviderCommands(program2) {
6458
8276
  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) => {
8277
+ 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
8278
  await runCommand(async () => {
6461
- const cfg = await loadProviderYaml(path);
8279
+ const cfg = await loadProviderYaml(path2);
6462
8280
  const { providerClient } = buildContext();
6463
8281
  const body = buildCreateAgentRequest(cfg);
6464
8282
  if (cfg.agentId) {
@@ -6474,7 +8292,7 @@ function registerProviderCommands(program2) {
6474
8292
  });
6475
8293
  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
8294
  await runCommand(async () => {
6477
- const raw = opts.body === "-" ? await readStdin() : readFileSync2(opts.body, "utf8");
8295
+ const raw = opts.body === "-" ? await readStdin() : readFileSync6(opts.body, "utf8");
6478
8296
  const body = JSON.parse(raw);
6479
8297
  const { providerClient } = buildContext();
6480
8298
  return providerClient.updateAgent(listingId, body);
@@ -6486,9 +8304,9 @@ function registerProviderCommands(program2) {
6486
8304
  return consumer.discover({ owner: "me" });
6487
8305
  }, { human: opts.human });
6488
8306
  });
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) => {
8307
+ 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
8308
  try {
6491
- const yamlCfg = await loadProviderYaml(path);
8309
+ const yamlCfg = await loadProviderYaml(path2);
6492
8310
  if (!yamlCfg.agentId) {
6493
8311
  process.stderr.write(
6494
8312
  JSON.stringify({ error: "provider_unconfigured", message: "agentId missing \u2014 run `linkedclaw provider register` first or set it in YAML" }) + "\n"
@@ -6516,7 +8334,7 @@ function registerProviderCommands(program2) {
6516
8334
  const handler = opts.handlerCmd ? new SubprocessHandler({ cmd: opts.handlerCmd }) : makeHttpHandler(opts.handlerHttp);
6517
8335
  const runtime = new ProviderRuntime({
6518
8336
  cloud: {
6519
- broadcasts: {
8337
+ gigTasks: {
6520
8338
  accept: (taskId, body) => providerClient.acceptGigTask(taskId, body),
6521
8339
  submit: (taskId, body) => providerClient.submitGigTask(taskId, body)
6522
8340
  }
@@ -6555,7 +8373,7 @@ function registerProviderCommands(program2) {
6555
8373
  process.exit(1);
6556
8374
  }
6557
8375
  });
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) => {
8376
+ 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
8377
  await runCommand(async () => {
6560
8378
  const { providerClient } = buildContext();
6561
8379
  const body = { agent_id: opts.agentId };
@@ -6563,20 +8381,20 @@ function registerProviderCommands(program2) {
6563
8381
  return providerClient.acceptGigTask(taskId, body);
6564
8382
  }, { human: opts.human });
6565
8383
  });
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) => {
8384
+ 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
8385
  await runCommand(async () => {
6568
- const raw = resultFile === "-" ? await readStdin() : readFileSync2(resultFile, "utf8");
8386
+ const raw = resultFile === "-" ? await readStdin() : readFileSync6(resultFile, "utf8");
6569
8387
  const body = JSON.parse(raw);
6570
8388
  const { providerClient } = buildContext();
6571
8389
  return providerClient.submitGigTask(taskId, body);
6572
8390
  }, { human: opts.human });
6573
8391
  });
6574
8392
  }
6575
- async function loadProviderYaml(path) {
6576
- const raw = path === "-" ? await readStdin() : readFileSync2(path, "utf8");
6577
- const parsed = yamlLoad2(raw);
8393
+ async function loadProviderYaml(path2) {
8394
+ const raw = path2 === "-" ? await readStdin() : readFileSync6(path2, "utf8");
8395
+ const parsed = yamlLoad4(raw);
6578
8396
  if (parsed === null || typeof parsed !== "object") {
6579
- throw new Error(`provider config ${path} is not a YAML object`);
8397
+ throw new Error(`provider config ${path2} is not a YAML object`);
6580
8398
  }
6581
8399
  return parsed;
6582
8400
  }
@@ -6617,10 +8435,10 @@ function makeHttpHandler(url) {
6617
8435
  async onInvoke(evt) {
6618
8436
  return await postEvent(url, evt);
6619
8437
  },
6620
- async onBroadcastOffer(evt) {
8438
+ async onGigTaskOffer(evt) {
6621
8439
  return await postEvent(url, evt);
6622
8440
  },
6623
- async onBroadcastExecute(evt) {
8441
+ async onGigTaskExecute(evt) {
6624
8442
  return await postEvent(url, evt);
6625
8443
  }
6626
8444
  };
@@ -6636,10 +8454,10 @@ async function postEvent(url, body) {
6636
8454
  }
6637
8455
 
6638
8456
  // src/commands/requester.ts
6639
- import { readFileSync as readFileSync3 } from "fs";
6640
- import { load as yamlLoad3 } from "js-yaml";
8457
+ import { readFileSync as readFileSync7 } from "fs";
8458
+ import { load as yamlLoad5 } from "js-yaml";
6641
8459
  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) => {
8460
+ 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
8461
  await runCommand(async () => {
6644
8462
  const { requesterFlows } = buildContext();
6645
8463
  return requesterFlows.search(capability, {
@@ -6734,24 +8552,24 @@ function registerRequesterCommands(program2) {
6734
8552
  return invokeAgent(consumer, agentId, body);
6735
8553
  }, { human: opts.human });
6736
8554
  });
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.'
8555
+ const gigTask = program2.command("gig-task").description("Gig Task commands");
8556
+ gigTask.command("create <manifest>").description(
8557
+ 'Create a gig task from a YAML/JSON manifest file. Use "-" for stdin. Required fields: capability, instruction, target_providers, credits_per_provider.'
6740
8558
  ).option("--human", "Human-readable output").action(async (manifestPath, opts) => {
6741
8559
  await runCommand(async () => {
6742
8560
  const { consumer } = buildContext();
6743
- const raw = manifestPath === "-" ? await readStdin() : readFileSync3(manifestPath, "utf8");
8561
+ const raw = manifestPath === "-" ? await readStdin() : readFileSync7(manifestPath, "utf8");
6744
8562
  const body = parseYamlOrJson(raw);
6745
8563
  return consumer.createGigTask(body);
6746
8564
  }, { human: opts.human });
6747
8565
  });
6748
- broadcast.command("get <bct_id>").description("Get a broadcast task by id").option("--human", "Human-readable output").action(async (taskId, opts) => {
8566
+ gigTask.command("get <bct_id>").description("Get a gig task by id").option("--human", "Human-readable output").action(async (taskId, opts) => {
6749
8567
  await runCommand(async () => {
6750
8568
  const { consumer } = buildContext();
6751
8569
  return consumer.getGigTask(taskId);
6752
8570
  }, { human: opts.human });
6753
8571
  });
6754
- broadcast.command("list").description("List broadcasts I own").option("--status <s>", "Filter by status").option("--human", "Human-readable output").action(async (opts) => {
8572
+ gigTask.command("list").description("List gig tasks I own").option("--status <s>", "Filter by status").option("--human", "Human-readable output").action(async (opts) => {
6755
8573
  await runCommand(async () => {
6756
8574
  const { consumer } = buildContext();
6757
8575
  return consumer.listGigTasks({
@@ -6759,13 +8577,13 @@ function registerRequesterCommands(program2) {
6759
8577
  });
6760
8578
  }, { human: opts.human });
6761
8579
  });
6762
- broadcast.command("available").description("List open broadcasts I could pick up (as provider)").option("--human", "Human-readable output").action(async (opts) => {
8580
+ gigTask.command("available").description("List open gig tasks I could pick up (as provider)").option("--human", "Human-readable output").action(async (opts) => {
6763
8581
  await runCommand(async () => {
6764
8582
  const { providerClient } = buildContext();
6765
8583
  return providerClient.listGigTasksAvailable();
6766
8584
  }, { human: opts.human });
6767
8585
  });
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) => {
8586
+ 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
8587
  await runCommand(async () => {
6770
8588
  const { providerClient } = buildContext();
6771
8589
  const body = { agent_id: opts.agentId };
@@ -6773,8 +8591,8 @@ function registerRequesterCommands(program2) {
6773
8591
  return providerClient.acceptGigTask(taskId, body);
6774
8592
  }, { human: opts.human });
6775
8593
  });
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)."
8594
+ gigTask.command("submit <bct_id>").description(
8595
+ "Submit gig task result (provider side). Body must include `result_data` (string) and may include `result_payload` (object) and `proof` (array)."
6778
8596
  ).requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)').option("--human", "Human-readable output").action(async (taskId, opts) => {
6779
8597
  await runCommand(async () => {
6780
8598
  const { providerClient } = buildContext();
@@ -6804,6 +8622,36 @@ function registerRequesterCommands(program2) {
6804
8622
  return consumer.getBalance();
6805
8623
  }, { human: opts.human });
6806
8624
  });
8625
+ 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) => {
8626
+ await runCommand(async () => {
8627
+ const { consumer } = buildContext();
8628
+ const agent = await consumer.getAgent(agentId);
8629
+ if (opts.capability) {
8630
+ const meta = agent.capabilities_meta?.[opts.capability];
8631
+ if (!meta) {
8632
+ throw new Error(
8633
+ `agent ${agentId} has no capabilities_meta entry for ${JSON.stringify(opts.capability)}`
8634
+ );
8635
+ }
8636
+ return meta;
8637
+ }
8638
+ return agent;
8639
+ }, { human: opts.human });
8640
+ });
8641
+ 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) => {
8642
+ await runCommand(async () => {
8643
+ const { consumer } = buildContext();
8644
+ const agent = await consumer.getAgent(agentId);
8645
+ try {
8646
+ return await fetchCapabilitySchema(agent, opts.capability);
8647
+ } catch (err) {
8648
+ if (err instanceof CapabilitySchemaError) {
8649
+ throw new Error(`schema fetch failed: ${err.message}`);
8650
+ }
8651
+ throw err;
8652
+ }
8653
+ }, { human: opts.human });
8654
+ });
6807
8655
  }
6808
8656
  async function runHireRepl(sessionId, ctx) {
6809
8657
  const readline = await import("readline/promises");
@@ -6882,7 +8730,7 @@ function parseJsonOrFail(raw, label) {
6882
8730
  function parseYamlOrJson(raw) {
6883
8731
  const trimmed = raw.trimStart();
6884
8732
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) return JSON.parse(raw);
6885
- return yamlLoad3(raw);
8733
+ return yamlLoad5(raw);
6886
8734
  }
6887
8735
  async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultIntention) {
6888
8736
  if (manifestOpt !== void 0 && manifestId !== void 0) {
@@ -6896,7 +8744,7 @@ async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultInt
6896
8744
  if (manifestOpt === "-") {
6897
8745
  raw = await readStdin();
6898
8746
  } else if (manifestOpt.startsWith("@")) {
6899
- raw = readFileSync3(manifestOpt.slice(1), "utf8");
8747
+ raw = readFileSync7(manifestOpt.slice(1), "utf8");
6900
8748
  } else {
6901
8749
  raw = manifestOpt;
6902
8750
  }
@@ -6910,12 +8758,16 @@ async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultInt
6910
8758
  }
6911
8759
 
6912
8760
  // src/bin.ts
6913
- var CLI_VERSION = "0.1.2";
8761
+ var pkgPath = join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
8762
+ var CLI_VERSION = JSON.parse(readFileSync8(pkgPath, "utf8")).version;
6914
8763
  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}`);
8764
+ 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
8765
  registerAuthCommands(program);
6917
8766
  registerRequesterCommands(program);
6918
8767
  registerProviderCommands(program);
8768
+ registerConvergeCommands(program);
8769
+ registerArenaCommands(program);
8770
+ registerAgentCommands(program);
6919
8771
  program.parseAsync(process.argv).catch((err) => {
6920
8772
  process.stderr.write(
6921
8773
  JSON.stringify({ error: "internal_error", message: err instanceof Error ? err.message : String(err) }) + "\n"