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