@themoltnet/pi-extension 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,7 +13,6 @@ import { RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, crea
13
13
  import { parseEnv } from "node:util";
14
14
  import { FormatRegistry, Type as Type$1 } from "@sinclair/typebox";
15
15
  import { Value } from "@sinclair/typebox/value";
16
- import { TypeCompiler } from "@sinclair/typebox/compiler";
17
16
  //#region ../api-client/src/generated/core/bodySerializer.gen.ts
18
17
  var jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
19
18
  Object.entries({
@@ -2039,6 +2038,276 @@ var getLegreffierOnboardingStatus = (options) => (options.client ?? client).get(
2039
2038
  ...options
2040
2039
  });
2041
2040
  /**
2041
+ * List tasks for a team with optional filters.
2042
+ */
2043
+ var listTasks = (options) => (options.client ?? client).get({
2044
+ security: [
2045
+ {
2046
+ scheme: "bearer",
2047
+ type: "http"
2048
+ },
2049
+ {
2050
+ name: "X-Moltnet-Session-Token",
2051
+ type: "apiKey"
2052
+ },
2053
+ {
2054
+ in: "cookie",
2055
+ name: "ory_kratos_session",
2056
+ type: "apiKey"
2057
+ }
2058
+ ],
2059
+ url: "/tasks",
2060
+ ...options
2061
+ });
2062
+ /**
2063
+ * Create and enqueue a new task.
2064
+ */
2065
+ var createTask = (options) => (options.client ?? client).post({
2066
+ security: [
2067
+ {
2068
+ scheme: "bearer",
2069
+ type: "http"
2070
+ },
2071
+ {
2072
+ name: "X-Moltnet-Session-Token",
2073
+ type: "apiKey"
2074
+ },
2075
+ {
2076
+ in: "cookie",
2077
+ name: "ory_kratos_session",
2078
+ type: "apiKey"
2079
+ }
2080
+ ],
2081
+ url: "/tasks",
2082
+ ...options,
2083
+ headers: {
2084
+ "Content-Type": "application/json",
2085
+ ...options.headers
2086
+ }
2087
+ });
2088
+ /**
2089
+ * Get a task by ID.
2090
+ */
2091
+ var getTask = (options) => (options.client ?? client).get({
2092
+ security: [
2093
+ {
2094
+ scheme: "bearer",
2095
+ type: "http"
2096
+ },
2097
+ {
2098
+ name: "X-Moltnet-Session-Token",
2099
+ type: "apiKey"
2100
+ },
2101
+ {
2102
+ in: "cookie",
2103
+ name: "ory_kratos_session",
2104
+ type: "apiKey"
2105
+ }
2106
+ ],
2107
+ url: "/tasks/{id}",
2108
+ ...options
2109
+ });
2110
+ /**
2111
+ * Claim a queued task and start an attempt.
2112
+ */
2113
+ var claimTask = (options) => (options.client ?? client).post({
2114
+ security: [
2115
+ {
2116
+ scheme: "bearer",
2117
+ type: "http"
2118
+ },
2119
+ {
2120
+ name: "X-Moltnet-Session-Token",
2121
+ type: "apiKey"
2122
+ },
2123
+ {
2124
+ in: "cookie",
2125
+ name: "ory_kratos_session",
2126
+ type: "apiKey"
2127
+ }
2128
+ ],
2129
+ url: "/tasks/{id}/claim",
2130
+ ...options,
2131
+ headers: {
2132
+ "Content-Type": "application/json",
2133
+ ...options.headers
2134
+ }
2135
+ });
2136
+ /**
2137
+ * Send a heartbeat to keep the attempt lease alive.
2138
+ */
2139
+ var taskHeartbeat = (options) => (options.client ?? client).post({
2140
+ security: [
2141
+ {
2142
+ scheme: "bearer",
2143
+ type: "http"
2144
+ },
2145
+ {
2146
+ name: "X-Moltnet-Session-Token",
2147
+ type: "apiKey"
2148
+ },
2149
+ {
2150
+ in: "cookie",
2151
+ name: "ory_kratos_session",
2152
+ type: "apiKey"
2153
+ }
2154
+ ],
2155
+ url: "/tasks/{id}/attempts/{n}/heartbeat",
2156
+ ...options,
2157
+ headers: {
2158
+ "Content-Type": "application/json",
2159
+ ...options.headers
2160
+ }
2161
+ });
2162
+ /**
2163
+ * Mark an attempt as completed with output.
2164
+ */
2165
+ var completeTask = (options) => (options.client ?? client).post({
2166
+ security: [
2167
+ {
2168
+ scheme: "bearer",
2169
+ type: "http"
2170
+ },
2171
+ {
2172
+ name: "X-Moltnet-Session-Token",
2173
+ type: "apiKey"
2174
+ },
2175
+ {
2176
+ in: "cookie",
2177
+ name: "ory_kratos_session",
2178
+ type: "apiKey"
2179
+ }
2180
+ ],
2181
+ url: "/tasks/{id}/attempts/{n}/complete",
2182
+ ...options,
2183
+ headers: {
2184
+ "Content-Type": "application/json",
2185
+ ...options.headers
2186
+ }
2187
+ });
2188
+ /**
2189
+ * Mark an attempt as failed with error details.
2190
+ */
2191
+ var failTask = (options) => (options.client ?? client).post({
2192
+ security: [
2193
+ {
2194
+ scheme: "bearer",
2195
+ type: "http"
2196
+ },
2197
+ {
2198
+ name: "X-Moltnet-Session-Token",
2199
+ type: "apiKey"
2200
+ },
2201
+ {
2202
+ in: "cookie",
2203
+ name: "ory_kratos_session",
2204
+ type: "apiKey"
2205
+ }
2206
+ ],
2207
+ url: "/tasks/{id}/attempts/{n}/fail",
2208
+ ...options,
2209
+ headers: {
2210
+ "Content-Type": "application/json",
2211
+ ...options.headers
2212
+ }
2213
+ });
2214
+ /**
2215
+ * Cancel a task.
2216
+ */
2217
+ var cancelTask = (options) => (options.client ?? client).post({
2218
+ security: [
2219
+ {
2220
+ scheme: "bearer",
2221
+ type: "http"
2222
+ },
2223
+ {
2224
+ name: "X-Moltnet-Session-Token",
2225
+ type: "apiKey"
2226
+ },
2227
+ {
2228
+ in: "cookie",
2229
+ name: "ory_kratos_session",
2230
+ type: "apiKey"
2231
+ }
2232
+ ],
2233
+ url: "/tasks/{id}/cancel",
2234
+ ...options,
2235
+ headers: {
2236
+ "Content-Type": "application/json",
2237
+ ...options.headers
2238
+ }
2239
+ });
2240
+ /**
2241
+ * List all attempts for a task.
2242
+ */
2243
+ var listTaskAttempts = (options) => (options.client ?? client).get({
2244
+ security: [
2245
+ {
2246
+ scheme: "bearer",
2247
+ type: "http"
2248
+ },
2249
+ {
2250
+ name: "X-Moltnet-Session-Token",
2251
+ type: "apiKey"
2252
+ },
2253
+ {
2254
+ in: "cookie",
2255
+ name: "ory_kratos_session",
2256
+ type: "apiKey"
2257
+ }
2258
+ ],
2259
+ url: "/tasks/{id}/attempts",
2260
+ ...options
2261
+ });
2262
+ /**
2263
+ * List messages for a task attempt.
2264
+ */
2265
+ var listTaskMessages = (options) => (options.client ?? client).get({
2266
+ security: [
2267
+ {
2268
+ scheme: "bearer",
2269
+ type: "http"
2270
+ },
2271
+ {
2272
+ name: "X-Moltnet-Session-Token",
2273
+ type: "apiKey"
2274
+ },
2275
+ {
2276
+ in: "cookie",
2277
+ name: "ory_kratos_session",
2278
+ type: "apiKey"
2279
+ }
2280
+ ],
2281
+ url: "/tasks/{id}/attempts/{n}/messages",
2282
+ ...options
2283
+ });
2284
+ /**
2285
+ * Append messages to a task attempt.
2286
+ */
2287
+ var appendTaskMessages = (options) => (options.client ?? client).post({
2288
+ security: [
2289
+ {
2290
+ scheme: "bearer",
2291
+ type: "http"
2292
+ },
2293
+ {
2294
+ name: "X-Moltnet-Session-Token",
2295
+ type: "apiKey"
2296
+ },
2297
+ {
2298
+ in: "cookie",
2299
+ name: "ory_kratos_session",
2300
+ type: "apiKey"
2301
+ }
2302
+ ],
2303
+ url: "/tasks/{id}/attempts/{n}/messages",
2304
+ ...options,
2305
+ headers: {
2306
+ "Content-Type": "application/json",
2307
+ ...options.headers
2308
+ }
2309
+ });
2310
+ /**
2042
2311
  * List all problem types used in API error responses (RFC 9457).
2043
2312
  */
2044
2313
  var listProblemTypes = (options) => (options?.client ?? client).get({
@@ -3059,7 +3328,7 @@ function decode$3(string, alphabetIdx, bitsPerChar, name) {
3059
3328
  if (bits >= bitsPerChar || (255 & buffer << 8 - bits) !== 0) throw new SyntaxError("Unexpected end of data");
3060
3329
  return out;
3061
3330
  }
3062
- function encode$1(data, alphabet, bitsPerChar) {
3331
+ function encode$2(data, alphabet, bitsPerChar) {
3063
3332
  const pad = alphabet[alphabet.length - 1] === "=";
3064
3333
  const mask = (1 << bitsPerChar) - 1;
3065
3334
  let out = "";
@@ -3091,7 +3360,7 @@ function rfc4648({ name, prefix, bitsPerChar, alphabet }) {
3091
3360
  prefix,
3092
3361
  name,
3093
3362
  encode(input) {
3094
- return encode$1(input, alphabet, bitsPerChar);
3363
+ return encode$2(input, alphabet, bitsPerChar);
3095
3364
  },
3096
3365
  decode(input) {
3097
3366
  return decode$3(input, alphabetIdx, bitsPerChar, name);
@@ -3180,14 +3449,14 @@ baseX({
3180
3449
  });
3181
3450
  //#endregion
3182
3451
  //#region ../../node_modules/.pnpm/multiformats@13.4.2/node_modules/multiformats/dist/src/vendor/varint.js
3183
- var encode_1 = encode;
3452
+ var encode_1 = encode$1;
3184
3453
  var MSB = 128, MSBALL = -128, INT = Math.pow(2, 31);
3185
3454
  /**
3186
3455
  * @param {number} num
3187
3456
  * @param {number[]} out
3188
3457
  * @param {number} offset
3189
3458
  */
3190
- function encode(num, out, offset) {
3459
+ function encode$1(num, out, offset) {
3191
3460
  out = out || [];
3192
3461
  offset = offset || 0;
3193
3462
  var oldOffset = offset;
@@ -3200,7 +3469,7 @@ function encode(num, out, offset) {
3200
3469
  num >>>= 7;
3201
3470
  }
3202
3471
  out[offset] = num | 0;
3203
- encode.bytes = offset - oldOffset + 1;
3472
+ encode$1.bytes = offset - oldOffset + 1;
3204
3473
  return out;
3205
3474
  }
3206
3475
  var decode$2 = read;
@@ -4069,8 +4338,13 @@ etc.sha512Sync = (...m) => {
4069
4338
  m.forEach((msg) => hash.update(msg));
4070
4339
  return hash.digest();
4071
4340
  };
4072
- new TextEncoder();
4341
+ //#endregion
4342
+ //#region ../../node_modules/.pnpm/multiformats@13.4.2/node_modules/multiformats/dist/src/codecs/json.js
4343
+ var textEncoder$1 = new TextEncoder();
4073
4344
  new TextDecoder();
4345
+ function encode(node) {
4346
+ return textEncoder$1.encode(JSON.stringify(node));
4347
+ }
4074
4348
  //#endregion
4075
4349
  //#region ../../node_modules/.pnpm/multiformats@13.4.2/node_modules/multiformats/dist/src/hashes/hasher.js
4076
4350
  var DEFAULT_MIN_DIGEST_LENGTH = 20;
@@ -4117,7 +4391,9 @@ function createDigest(digest, code, truncate) {
4117
4391
  }
4118
4392
  return create(code, digest);
4119
4393
  }
4120
- from({
4394
+ //#endregion
4395
+ //#region ../../node_modules/.pnpm/multiformats@13.4.2/node_modules/multiformats/dist/src/hashes/sha2.js
4396
+ var sha256 = from({
4121
4397
  name: "sha2-256",
4122
4398
  code: 18,
4123
4399
  encode: (input) => coerce(crypto.createHash("sha256").update(input).digest())
@@ -4128,6 +4404,20 @@ from({
4128
4404
  encode: (input) => coerce(crypto.createHash("sha512").update(input).digest())
4129
4405
  });
4130
4406
  //#endregion
4407
+ //#region ../crypto-service/src/json-cid.ts
4408
+ /**
4409
+ * Generic JSON CID — CIDv1 for arbitrary JSON-serialisable values.
4410
+ *
4411
+ * Uses the dag-json codec and sha2-256, producing a base32lower CIDv1.
4412
+ * Suitable for content-addressing task inputs, schema objects, and other
4413
+ * JSON payloads that don't need diary-entry canonical normalisation.
4414
+ */
4415
+ async function computeJsonCid(value) {
4416
+ const bytes = encode(value);
4417
+ const hash = await sha256.digest(bytes);
4418
+ return CID.create(1, 512, hash).toString();
4419
+ }
4420
+ //#endregion
4131
4421
  //#region ../../node_modules/.pnpm/cborg@4.5.8/node_modules/cborg/lib/is.js
4132
4422
  var objectTypeNames = [
4133
4423
  "Object",
@@ -6302,18 +6592,124 @@ function createSigningRequestsNamespace(context) {
6302
6592
  };
6303
6593
  }
6304
6594
  //#endregion
6305
- //#region ../sdk/src/namespaces/teams.ts
6306
- function createTeamsNamespace(context) {
6595
+ //#region ../sdk/src/namespaces/tasks.ts
6596
+ function createTasksNamespace(context) {
6307
6597
  const { client, auth } = context;
6308
6598
  return {
6309
- async list() {
6310
- return unwrapResult(await listTeams({
6599
+ async list(query) {
6600
+ return unwrapResult(await listTasks({
6311
6601
  client,
6312
- auth
6602
+ auth,
6603
+ query
6313
6604
  }));
6314
6605
  },
6315
- async get(id) {
6316
- return unwrapResult(await getTeam({
6606
+ async create(body) {
6607
+ return unwrapResult(await createTask({
6608
+ client,
6609
+ auth,
6610
+ body
6611
+ }));
6612
+ },
6613
+ async get(id) {
6614
+ return unwrapResult(await getTask({
6615
+ client,
6616
+ auth,
6617
+ path: { id }
6618
+ }));
6619
+ },
6620
+ async claim(id, body) {
6621
+ return unwrapResult(await claimTask({
6622
+ client,
6623
+ auth,
6624
+ path: { id },
6625
+ body
6626
+ }));
6627
+ },
6628
+ async heartbeat(id, n, body) {
6629
+ return unwrapResult(await taskHeartbeat({
6630
+ client,
6631
+ auth,
6632
+ path: {
6633
+ id,
6634
+ n
6635
+ },
6636
+ body
6637
+ }));
6638
+ },
6639
+ async complete(id, n, body) {
6640
+ return unwrapResult(await completeTask({
6641
+ client,
6642
+ auth,
6643
+ path: {
6644
+ id,
6645
+ n
6646
+ },
6647
+ body
6648
+ }));
6649
+ },
6650
+ async fail(id, n, body) {
6651
+ return unwrapResult(await failTask({
6652
+ client,
6653
+ auth,
6654
+ path: {
6655
+ id,
6656
+ n
6657
+ },
6658
+ body
6659
+ }));
6660
+ },
6661
+ async cancel(id, body) {
6662
+ return unwrapResult(await cancelTask({
6663
+ client,
6664
+ auth,
6665
+ path: { id },
6666
+ body
6667
+ }));
6668
+ },
6669
+ async listAttempts(id) {
6670
+ return unwrapResult(await listTaskAttempts({
6671
+ client,
6672
+ auth,
6673
+ path: { id }
6674
+ }));
6675
+ },
6676
+ async listMessages(id, n, query) {
6677
+ return unwrapResult(await listTaskMessages({
6678
+ client,
6679
+ auth,
6680
+ path: {
6681
+ id,
6682
+ n
6683
+ },
6684
+ query
6685
+ }));
6686
+ },
6687
+ async appendMessages(id, n, body) {
6688
+ return unwrapResult(await appendTaskMessages({
6689
+ client,
6690
+ auth,
6691
+ path: {
6692
+ id,
6693
+ n
6694
+ },
6695
+ body
6696
+ }));
6697
+ }
6698
+ };
6699
+ }
6700
+ //#endregion
6701
+ //#region ../sdk/src/namespaces/teams.ts
6702
+ function createTeamsNamespace(context) {
6703
+ const { client, auth } = context;
6704
+ return {
6705
+ async list() {
6706
+ return unwrapResult(await listTeams({
6707
+ client,
6708
+ auth
6709
+ }));
6710
+ },
6711
+ async get(id) {
6712
+ return unwrapResult(await getTeam({
6317
6713
  client,
6318
6714
  auth,
6319
6715
  path: { id }
@@ -6433,6 +6829,7 @@ function createAgent(options) {
6433
6829
  legreffier: createLegreffierNamespace(context),
6434
6830
  problems: createProblemsNamespace(context),
6435
6831
  teams: createTeamsNamespace(context),
6832
+ tasks: createTasksNamespace(context),
6436
6833
  client,
6437
6834
  getToken: () => tokenManager.getToken()
6438
6835
  };
@@ -7171,6 +7568,24 @@ function renderPhase6Markdown(pack) {
7171
7568
  * These tools run on the host (not in the VM) via the MoltNet SDK,
7172
7569
  * so agent credentials never touch the VM filesystem.
7173
7570
  */
7571
+ /**
7572
+ * Baseline env keys forwarded to host-exec child processes.
7573
+ * Callers can extend this set at sandbox startup via `MoltNetToolsConfig.hostExecBaseEnv`.
7574
+ */
7575
+ var HOST_EXEC_DEFAULT_BASE_ENV = new Set([
7576
+ "PATH",
7577
+ "HOME",
7578
+ "LANG",
7579
+ "LC_ALL",
7580
+ "TMPDIR",
7581
+ "GIT_CONFIG_GLOBAL",
7582
+ "MOLTNET_CREDENTIALS_PATH",
7583
+ "GIT_AUTHOR_NAME",
7584
+ "GIT_AUTHOR_EMAIL",
7585
+ "GIT_COMMITTER_NAME",
7586
+ "GIT_COMMITTER_EMAIL",
7587
+ "SSH_AUTH_SOCK"
7588
+ ]);
7174
7589
  function ensureConnected(config) {
7175
7590
  const agent = config.getAgent();
7176
7591
  const diaryId = config.getDiaryId();
@@ -7184,303 +7599,548 @@ function ensureConnected(config) {
7184
7599
  * Create all MoltNet tool definitions, ready to pass to `pi.registerTool()`.
7185
7600
  */
7186
7601
  function createMoltNetTools(config) {
7187
- return [
7188
- defineTool({
7189
- name: "moltnet_pack_get",
7190
- label: "Get MoltNet Pack",
7191
- description: "Get a context pack by ID. Optionally expand included entries.",
7192
- parameters: Type.Object({
7193
- packId: Type.String({ description: "Context pack ID" }),
7194
- expandEntries: Type.Optional(Type.Boolean({ description: "Include full expanded entries" }))
7195
- }),
7196
- async execute(_id, params) {
7197
- const { agent } = ensureConnected(config);
7198
- const pack = await agent.packs.get(params.packId, { expand: params.expandEntries ? "entries" : void 0 });
7199
- return {
7200
- content: [{
7201
- type: "text",
7202
- text: JSON.stringify(pack, null, 2)
7203
- }],
7204
- details: {}
7205
- };
7206
- }
7602
+ const getPack = defineTool({
7603
+ name: "moltnet_pack_get",
7604
+ label: "Get MoltNet Pack",
7605
+ description: "Get a context pack by ID. Optionally expand included entries.",
7606
+ parameters: Type.Object({
7607
+ packId: Type.String({ description: "Context pack ID" }),
7608
+ expandEntries: Type.Optional(Type.Boolean({ description: "Include full expanded entries" }))
7207
7609
  }),
7208
- defineTool({
7209
- name: "moltnet_pack_create",
7210
- label: "Create MoltNet Pack",
7211
- description: "Persist a curated context pack. Entries are caller-ranked (lower rank = more prominent). Recipe/prompt/selection_rationale belong in params. Defaults to pinned=false — packs in the attribution pipeline are ephemeral unless the caller explicitly opts in.",
7212
- parameters: Type.Object({
7213
- entries: Type.Array(Type.Object({
7214
- entryId: Type.String({ description: "Diary entry UUID" }),
7215
- rank: Type.Number({ description: "Rank (1..N, lower = more prominent)" })
7216
- }), { description: "Selected entries with their ranks" }),
7217
- params: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Free-form recipe parameters (recipe name, prompt, selection rationale, etc.)" })),
7218
- tokenBudget: Type.Optional(Type.Number({ description: "Soft token budget recorded on the pack (optional)" })),
7219
- pinned: Type.Optional(Type.Boolean({ description: "Pin the pack against retention policy (default false)" }))
7220
- }),
7221
- async execute(_id, params) {
7222
- const { agent, diaryId } = ensureConnected(config);
7223
- const pack = await agent.packs.create(diaryId, {
7224
- packType: "custom",
7225
- params: params.params ?? {},
7226
- entries: params.entries,
7227
- tokenBudget: params.tokenBudget,
7228
- pinned: params.pinned ?? false
7229
- });
7230
- return {
7231
- content: [{
7232
- type: "text",
7233
- text: JSON.stringify(pack, null, 2)
7234
- }],
7235
- details: {}
7236
- };
7237
- }
7610
+ async execute(_id, params) {
7611
+ const { agent } = ensureConnected(config);
7612
+ const pack = await agent.packs.get(params.packId, { expand: params.expandEntries ? "entries" : void 0 });
7613
+ return {
7614
+ content: [{
7615
+ type: "text",
7616
+ text: JSON.stringify(pack, null, 2)
7617
+ }],
7618
+ details: {}
7619
+ };
7620
+ }
7621
+ });
7622
+ const createPack = defineTool({
7623
+ name: "moltnet_pack_create",
7624
+ label: "Create MoltNet Pack",
7625
+ description: "Persist a curated context pack. Entries are caller-ranked (lower rank = more prominent). Recipe/prompt/selection_rationale belong in params. Defaults to pinned=false — packs in the attribution pipeline are ephemeral unless the caller explicitly opts in.",
7626
+ parameters: Type.Object({
7627
+ entries: Type.Array(Type.Object({
7628
+ entryId: Type.String({ description: "Diary entry UUID" }),
7629
+ rank: Type.Number({ description: "Rank (1..N, lower = more prominent)" })
7630
+ }), { description: "Selected entries with their ranks" }),
7631
+ params: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Free-form recipe parameters (recipe name, prompt, selection rationale, etc.)" })),
7632
+ tokenBudget: Type.Optional(Type.Number({ description: "Soft token budget recorded on the pack (optional)" })),
7633
+ pinned: Type.Optional(Type.Boolean({ description: "Pin the pack against retention policy (default false)" }))
7238
7634
  }),
7239
- defineTool({
7240
- name: "moltnet_pack_provenance",
7241
- label: "Get MoltNet Pack Provenance",
7242
- description: "Get the provenance graph for a context pack by ID or CID.",
7243
- parameters: Type.Object({
7244
- packId: Type.Optional(Type.String({ description: "Context pack ID" })),
7245
- packCid: Type.Optional(Type.String({ description: "Context pack CID" })),
7246
- depth: Type.Optional(Type.Number({ description: "Supersession ancestry depth to include (default 2)" }))
7247
- }),
7248
- async execute(_id, params) {
7249
- const { agent } = ensureConnected(config);
7250
- if (!params.packId && !params.packCid) throw new Error("Provide either packId or packCid");
7251
- if (params.packId && params.packCid) throw new Error("Provide only one of packId or packCid");
7252
- const graph = params.packId ? await agent.packs.getProvenance(params.packId, { depth: params.depth ?? 2 }) : await agent.packs.getProvenanceByCid(params.packCid, { depth: params.depth ?? 2 });
7253
- const payload = {
7254
- metadata: graph.metadata,
7255
- counts: {
7256
- nodes: graph.nodes.length,
7257
- edges: graph.edges.length
7258
- },
7259
- graph
7260
- };
7261
- return {
7262
- content: [{
7263
- type: "text",
7264
- text: JSON.stringify(payload, null, 2)
7265
- }],
7266
- details: {}
7267
- };
7268
- }
7635
+ async execute(_id, params) {
7636
+ const { agent, diaryId } = ensureConnected(config);
7637
+ const pack = await agent.packs.create(diaryId, {
7638
+ packType: "custom",
7639
+ params: params.params ?? {},
7640
+ entries: params.entries,
7641
+ tokenBudget: params.tokenBudget,
7642
+ pinned: params.pinned ?? false
7643
+ });
7644
+ return {
7645
+ content: [{
7646
+ type: "text",
7647
+ text: JSON.stringify(pack, null, 2)
7648
+ }],
7649
+ details: {}
7650
+ };
7651
+ }
7652
+ });
7653
+ const getPackProvenance = defineTool({
7654
+ name: "moltnet_pack_provenance",
7655
+ label: "Get MoltNet Pack Provenance",
7656
+ description: "Get the provenance graph for a context pack by ID or CID.",
7657
+ parameters: Type.Object({
7658
+ packId: Type.Optional(Type.String({ description: "Context pack ID" })),
7659
+ packCid: Type.Optional(Type.String({ description: "Context pack CID" })),
7660
+ depth: Type.Optional(Type.Number({ description: "Supersession ancestry depth to include (default 2)" }))
7269
7661
  }),
7270
- defineTool({
7271
- name: "moltnet_pack_render",
7272
- label: "Render MoltNet Pack",
7273
- description: "Fetch a pack with entries, transform it into docs, then preview or persist the rendered pack.",
7274
- parameters: Type.Object({
7275
- packId: Type.String({ description: "Context pack ID" }),
7276
- renderMethod: Type.Optional(Type.String({ description: "Render method label. Defaults to pi:pack-to-docs-v1" })),
7277
- markdown: Type.Optional(Type.String({ description: "Optional caller-authored markdown override" })),
7278
- preview: Type.Optional(Type.Boolean({ description: "Preview without persisting (default false)" })),
7279
- pinned: Type.Optional(Type.Boolean({ description: "Persist the rendered pack as pinned (default false)" }))
7280
- }),
7281
- async execute(_id, params) {
7282
- const { agent } = ensureConnected(config);
7283
- const renderMethod = params.renderMethod ?? "pi:pack-to-docs-v1";
7284
- let renderedMarkdown = params.markdown;
7285
- if (!renderedMarkdown && !renderMethod.startsWith("server:")) renderedMarkdown = renderPhase6Markdown(await agent.packs.get(params.packId, { expand: "entries" }));
7286
- const result = params.preview ?? false ? await agent.packs.previewRendered(params.packId, {
7287
- renderMethod,
7288
- renderedMarkdown
7289
- }) : await agent.packs.render(params.packId, {
7290
- renderMethod,
7291
- renderedMarkdown,
7292
- pinned: params.pinned
7293
- });
7294
- return {
7295
- content: [{
7296
- type: "text",
7297
- text: JSON.stringify(result, null, 2)
7298
- }],
7299
- details: {}
7300
- };
7301
- }
7662
+ async execute(_id, params) {
7663
+ const { agent } = ensureConnected(config);
7664
+ if (!params.packId && !params.packCid) throw new Error("Provide either packId or packCid");
7665
+ if (params.packId && params.packCid) throw new Error("Provide only one of packId or packCid");
7666
+ const graph = params.packId ? await agent.packs.getProvenance(params.packId, { depth: params.depth ?? 2 }) : await agent.packs.getProvenanceByCid(params.packCid, { depth: params.depth ?? 2 });
7667
+ const payload = {
7668
+ metadata: graph.metadata,
7669
+ counts: {
7670
+ nodes: graph.nodes.length,
7671
+ edges: graph.edges.length
7672
+ },
7673
+ graph
7674
+ };
7675
+ return {
7676
+ content: [{
7677
+ type: "text",
7678
+ text: JSON.stringify(payload, null, 2)
7679
+ }],
7680
+ details: {}
7681
+ };
7682
+ }
7683
+ });
7684
+ const renderPack = defineTool({
7685
+ name: "moltnet_pack_render",
7686
+ label: "Render MoltNet Pack",
7687
+ description: "Fetch a pack with entries, transform it into docs, then preview or persist the rendered pack.",
7688
+ parameters: Type.Object({
7689
+ packId: Type.String({ description: "Context pack ID" }),
7690
+ renderMethod: Type.Optional(Type.String({ description: "Render method label. Defaults to pi:pack-to-docs-v1" })),
7691
+ markdown: Type.Optional(Type.String({ description: "Optional caller-authored markdown override" })),
7692
+ preview: Type.Optional(Type.Boolean({ description: "Preview without persisting (default false)" })),
7693
+ pinned: Type.Optional(Type.Boolean({ description: "Persist the rendered pack as pinned (default false)" }))
7302
7694
  }),
7303
- defineTool({
7304
- name: "moltnet_rendered_pack_list",
7305
- label: "List MoltNet Rendered Packs",
7306
- description: "List rendered packs for the current MoltNet diary, optionally filtered by source pack or render method.",
7307
- parameters: Type.Object({
7308
- sourcePackId: Type.Optional(Type.String({ description: "Filter by source pack ID" })),
7309
- renderMethod: Type.Optional(Type.String({ description: "Filter by render method" })),
7310
- limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
7311
- offset: Type.Optional(Type.Number({ description: "Offset for pagination (default 0)" }))
7312
- }),
7313
- async execute(_id, params) {
7314
- const { agent, diaryId } = ensureConnected(config);
7315
- const rendered = await agent.packs.listRendered(diaryId, {
7316
- sourcePackId: params.sourcePackId,
7317
- renderMethod: params.renderMethod,
7318
- limit: params.limit ?? 10,
7319
- offset: params.offset ?? 0
7320
- });
7321
- return {
7322
- content: [{
7323
- type: "text",
7324
- text: JSON.stringify(rendered, null, 2)
7325
- }],
7326
- details: {}
7327
- };
7328
- }
7695
+ async execute(_id, params) {
7696
+ const { agent } = ensureConnected(config);
7697
+ const renderMethod = params.renderMethod ?? "pi:pack-to-docs-v1";
7698
+ let renderedMarkdown = params.markdown;
7699
+ if (!renderedMarkdown && !renderMethod.startsWith("server:")) renderedMarkdown = renderPhase6Markdown(await agent.packs.get(params.packId, { expand: "entries" }));
7700
+ const result = params.preview ?? false ? await agent.packs.previewRendered(params.packId, {
7701
+ renderMethod,
7702
+ renderedMarkdown
7703
+ }) : await agent.packs.render(params.packId, {
7704
+ renderMethod,
7705
+ renderedMarkdown,
7706
+ pinned: params.pinned
7707
+ });
7708
+ return {
7709
+ content: [{
7710
+ type: "text",
7711
+ text: JSON.stringify(result, null, 2)
7712
+ }],
7713
+ details: {}
7714
+ };
7715
+ }
7716
+ });
7717
+ const listRenderedPacks = defineTool({
7718
+ name: "moltnet_rendered_pack_list",
7719
+ label: "List MoltNet Rendered Packs",
7720
+ description: "List rendered packs for the current MoltNet diary, optionally filtered by source pack or render method.",
7721
+ parameters: Type.Object({
7722
+ sourcePackId: Type.Optional(Type.String({ description: "Filter by source pack ID" })),
7723
+ renderMethod: Type.Optional(Type.String({ description: "Filter by render method" })),
7724
+ limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
7725
+ offset: Type.Optional(Type.Number({ description: "Offset for pagination (default 0)" }))
7329
7726
  }),
7330
- defineTool({
7331
- name: "moltnet_rendered_pack_get",
7332
- label: "Get MoltNet Rendered Pack",
7333
- description: "Get a rendered pack by ID.",
7334
- parameters: Type.Object({ renderedPackId: Type.String({ description: "Rendered pack ID" }) }),
7335
- async execute(_id, params) {
7336
- const { agent } = ensureConnected(config);
7727
+ async execute(_id, params) {
7728
+ const { agent, diaryId } = ensureConnected(config);
7729
+ const rendered = await agent.packs.listRendered(diaryId, {
7730
+ sourcePackId: params.sourcePackId,
7731
+ renderMethod: params.renderMethod,
7732
+ limit: params.limit ?? 10,
7733
+ offset: params.offset ?? 0
7734
+ });
7735
+ return {
7736
+ content: [{
7737
+ type: "text",
7738
+ text: JSON.stringify(rendered, null, 2)
7739
+ }],
7740
+ details: {}
7741
+ };
7742
+ }
7743
+ });
7744
+ const getRenderedPack = defineTool({
7745
+ name: "moltnet_rendered_pack_get",
7746
+ label: "Get MoltNet Rendered Pack",
7747
+ description: "Get a rendered pack by ID.",
7748
+ parameters: Type.Object({ renderedPackId: Type.String({ description: "Rendered pack ID" }) }),
7749
+ async execute(_id, params) {
7750
+ const { agent } = ensureConnected(config);
7751
+ const rendered = await agent.packs.getRendered(params.renderedPackId);
7752
+ return {
7753
+ content: [{
7754
+ type: "text",
7755
+ text: JSON.stringify(rendered, null, 2)
7756
+ }],
7757
+ details: {}
7758
+ };
7759
+ }
7760
+ });
7761
+ const verifyRenderedPack = defineTool({
7762
+ name: "moltnet_rendered_pack_verify",
7763
+ label: "Verify MoltNet Rendered Pack",
7764
+ description: "Create a verification workflow for a rendered pack and return the verification ID and nonce.",
7765
+ parameters: Type.Object({
7766
+ renderedPackId: Type.String({ description: "Rendered pack ID" }),
7767
+ nonce: Type.Optional(Type.String({ description: "Caller-supplied idempotency nonce. Generated automatically if omitted." }))
7768
+ }),
7769
+ async execute(_id, params) {
7770
+ const { agent } = ensureConnected(config);
7771
+ const nonce = params.nonce ?? randomUUID();
7772
+ const verification = await agent.packs.verifyRendered(params.renderedPackId, { nonce });
7773
+ return {
7774
+ content: [{
7775
+ type: "text",
7776
+ text: JSON.stringify({
7777
+ ...verification,
7778
+ nonce
7779
+ }, null, 2)
7780
+ }],
7781
+ details: {}
7782
+ };
7783
+ }
7784
+ });
7785
+ const judgeRenderedPack = defineTool({
7786
+ name: "moltnet_rendered_pack_judge",
7787
+ label: "Judge MoltNet Rendered Pack",
7788
+ description: "Run the fidelity judge against a rendered pack. Local mode (no nonce): fetch the rendered pack + its source pack with entries, judge locally, return scores. Proctored mode (nonce): claim the verification payload from the API, judge, and submit scores with a Pi judge-recipe CID.",
7789
+ parameters: Type.Object({
7790
+ renderedPackId: Type.String({ description: "Rendered pack ID" }),
7791
+ nonce: Type.Optional(Type.String({ description: "Verification nonce from moltnet_rendered_pack_verify. If set, runs proctored mode and submits scores. If omitted, runs local mode and does not submit." })),
7792
+ rubric: Type.Optional(Type.String({ description: "Custom rubric override (local mode only). Defaults to the built-in rubric when omitted." }))
7793
+ }),
7794
+ async execute(_id, params, _signal, _onUpdate, ctx) {
7795
+ const { agent } = ensureConnected(config);
7796
+ const model = ctx?.model;
7797
+ if (!model) throw new Error("No active model in pi session — cannot run the fidelity judge.");
7798
+ let sourceEntriesMd;
7799
+ let renderedContent;
7800
+ let rubric;
7801
+ if (params.nonce) {
7802
+ if (params.rubric) throw new Error("`rubric` is only supported in local mode (omit `nonce`).");
7803
+ const claim = await agent.packs.claimVerification(params.renderedPackId);
7804
+ sourceEntriesMd = buildSourceEntriesMarkdown(claim.sourceEntries);
7805
+ renderedContent = claim.renderedContent;
7806
+ rubric = claim.rubric?.trim() ? claim.rubric : DEFAULT_RUBRIC;
7807
+ } else {
7337
7808
  const rendered = await agent.packs.getRendered(params.renderedPackId);
7338
- return {
7339
- content: [{
7340
- type: "text",
7341
- text: JSON.stringify(rendered, null, 2)
7342
- }],
7343
- details: {}
7344
- };
7809
+ if (!rendered.content?.trim()) throw new Error(`rendered pack ${params.renderedPackId} has empty content`);
7810
+ const sourcePack = await agent.packs.get(rendered.sourcePackId, { expand: "entries" });
7811
+ if (!sourcePack.entries || sourcePack.entries.length === 0) throw new Error(`source pack ${rendered.sourcePackId} has no entries`);
7812
+ sourceEntriesMd = buildSourceEntriesMarkdown(sourcePack.entries.map((entry) => ({
7813
+ title: entry.entry.title,
7814
+ content: entry.entry.content
7815
+ })));
7816
+ renderedContent = rendered.content;
7817
+ rubric = params.rubric?.trim() ? params.rubric : DEFAULT_RUBRIC;
7818
+ }
7819
+ let scores;
7820
+ try {
7821
+ scores = await runFidelityJudge({
7822
+ model,
7823
+ sourceEntries: sourceEntriesMd,
7824
+ renderedContent,
7825
+ rubric
7826
+ });
7827
+ } catch (err) {
7828
+ throw new Error(`judge failed: ${err.message ?? String(err)}`);
7345
7829
  }
7830
+ if (!params.nonce) return {
7831
+ content: [{
7832
+ type: "text",
7833
+ text: JSON.stringify({
7834
+ mode: "local",
7835
+ renderedPackId: params.renderedPackId,
7836
+ scores
7837
+ }, null, 2)
7838
+ }],
7839
+ details: {}
7840
+ };
7841
+ const recipe = computePiJudgeRecipeCid({
7842
+ judgePrompt: JUDGE_SYSTEM_PROMPT,
7843
+ rubric,
7844
+ promptAsset: JUDGE_PROMPT_ASSET_PATH,
7845
+ rubricAsset: RUBRIC_ASSET_PATH
7846
+ });
7847
+ const providerName = model.provider ?? "pi";
7848
+ const modelId = model.id ?? "unknown";
7849
+ const submit = await agent.packs.submitVerification(params.renderedPackId, {
7850
+ nonce: params.nonce,
7851
+ coverage: scores.coverage,
7852
+ grounding: scores.grounding,
7853
+ faithfulness: scores.faithfulness,
7854
+ transcript: scores.reasoning,
7855
+ judgeModel: modelId,
7856
+ judgeProvider: providerName,
7857
+ judgeBinaryCid: recipe.cid
7858
+ });
7859
+ return {
7860
+ content: [{
7861
+ type: "text",
7862
+ text: JSON.stringify({
7863
+ mode: "proctored",
7864
+ renderedPackId: params.renderedPackId,
7865
+ scores,
7866
+ submission: submit,
7867
+ judgeRecipeCid: recipe.cid,
7868
+ judgeRecipeManifest: recipe.manifest
7869
+ }, null, 2)
7870
+ }],
7871
+ details: {}
7872
+ };
7873
+ }
7874
+ });
7875
+ const diaryTags = defineTool({
7876
+ name: "moltnet_diary_tags",
7877
+ label: "List MoltNet Diary Tags",
7878
+ description: "Inventory tags on the current diary with entry counts. Cheap reconnaissance before committing to a search or list — use it to discover scope prefixes and cluster sizes. Optional prefix/minCount/entryTypes filters narrow the result.",
7879
+ parameters: Type.Object({
7880
+ prefix: Type.Optional(Type.String({ description: "Filter to tags starting with this prefix (e.g. \"scope:\")" })),
7881
+ minCount: Type.Optional(Type.Number({ description: "Exclude tags with fewer than this many entries" })),
7882
+ entryTypes: Type.Optional(Type.Array(Type.Union([
7883
+ Type.Literal("episodic"),
7884
+ Type.Literal("semantic"),
7885
+ Type.Literal("procedural"),
7886
+ Type.Literal("reflection"),
7887
+ Type.Literal("identity"),
7888
+ Type.Literal("soul")
7889
+ ]), { description: "Scope the tag count to these entry types" }))
7346
7890
  }),
7347
- defineTool({
7348
- name: "moltnet_rendered_pack_verify",
7349
- label: "Verify MoltNet Rendered Pack",
7350
- description: "Create a verification workflow for a rendered pack and return the verification ID and nonce.",
7351
- parameters: Type.Object({
7352
- renderedPackId: Type.String({ description: "Rendered pack ID" }),
7353
- nonce: Type.Optional(Type.String({ description: "Caller-supplied idempotency nonce. Generated automatically if omitted." }))
7354
- }),
7355
- async execute(_id, params) {
7356
- const { agent } = ensureConnected(config);
7357
- const nonce = params.nonce ?? randomUUID();
7358
- const verification = await agent.packs.verifyRendered(params.renderedPackId, { nonce });
7359
- return {
7360
- content: [{
7361
- type: "text",
7362
- text: JSON.stringify({
7363
- ...verification,
7364
- nonce
7365
- }, null, 2)
7366
- }],
7367
- details: {}
7368
- };
7891
+ async execute(_id, params) {
7892
+ const { agent, diaryId } = ensureConnected(config);
7893
+ const result = await agent.diaries.tags(diaryId, {
7894
+ prefix: params.prefix,
7895
+ minCount: params.minCount,
7896
+ entryTypes: params.entryTypes
7897
+ });
7898
+ return {
7899
+ content: [{
7900
+ type: "text",
7901
+ text: JSON.stringify(result, null, 2)
7902
+ }],
7903
+ details: {}
7904
+ };
7905
+ }
7906
+ });
7907
+ const listEntries = defineTool({
7908
+ name: "moltnet_list_entries",
7909
+ label: "List MoltNet Diary Entries",
7910
+ description: "List entries from the MoltNet diary. When `entryIds` is provided, batch-fetches those specific entries (max 50) and returns full fields including entryType, contentSignature, and contentHash for signature checks. Otherwise returns recent entries with a content preview.",
7911
+ parameters: Type.Object({
7912
+ limit: Type.Optional(Type.Number({ description: "Max entries to return (default 10)" })),
7913
+ tag: Type.Optional(Type.String({ description: "Filter by tag (optional)" })),
7914
+ entryIds: Type.Optional(Type.Array(Type.String(), {
7915
+ description: "Batch-fetch specific entries by UUID (max 50). Overrides `limit` and `tag` for selection.",
7916
+ maxItems: 50
7917
+ }))
7918
+ }),
7919
+ async execute(_id, params) {
7920
+ const { agent, diaryId } = ensureConnected(config);
7921
+ const query = {
7922
+ orderBy: "createdAt",
7923
+ order: "desc"
7924
+ };
7925
+ const batchMode = !!params.entryIds?.length;
7926
+ if (batchMode) query.ids = params.entryIds;
7927
+ else {
7928
+ query.limit = params.limit ?? 10;
7929
+ if (params.tag) query.tag = params.tag;
7369
7930
  }
7931
+ const entries = await agent.entries.list(diaryId, query);
7932
+ return {
7933
+ content: [{
7934
+ type: "text",
7935
+ text: JSON.stringify(entries.items?.map((e) => batchMode ? {
7936
+ id: e.id,
7937
+ title: e.title,
7938
+ entryType: e.entryType,
7939
+ tags: e.tags,
7940
+ importance: e.importance,
7941
+ contentHash: e.contentHash,
7942
+ contentSignature: e.contentSignature,
7943
+ signingNonce: e.signingNonce,
7944
+ createdAt: e.createdAt
7945
+ } : {
7946
+ id: e.id,
7947
+ title: e.title,
7948
+ tags: e.tags,
7949
+ importance: e.importance,
7950
+ createdAt: e.createdAt,
7951
+ contentPreview: typeof e.content === "string" ? e.content.slice(0, 200) : void 0
7952
+ }), null, 2)
7953
+ }],
7954
+ details: {}
7955
+ };
7956
+ }
7957
+ });
7958
+ const getEntry = defineTool({
7959
+ name: "moltnet_get_entry",
7960
+ label: "Get MoltNet Diary Entry",
7961
+ description: "Get the full content of a specific diary entry by ID.",
7962
+ parameters: Type.Object({ entryId: Type.String({ description: "The entry ID to fetch" }) }),
7963
+ async execute(_id, params) {
7964
+ const { agent } = ensureConnected(config);
7965
+ const entry = await agent.entries.get(params.entryId);
7966
+ return {
7967
+ content: [{
7968
+ type: "text",
7969
+ text: JSON.stringify({
7970
+ id: entry.id,
7971
+ title: entry.title,
7972
+ content: entry.content,
7973
+ tags: entry.tags,
7974
+ importance: entry.importance,
7975
+ createdAt: entry.createdAt
7976
+ }, null, 2)
7977
+ }],
7978
+ details: {}
7979
+ };
7980
+ }
7981
+ });
7982
+ const searchEntries = defineTool({
7983
+ name: "moltnet_search_entries",
7984
+ label: "Search MoltNet Diary Entries",
7985
+ description: "Search diary entries by semantic query. Uses vector similarity to find relevant entries.",
7986
+ parameters: Type.Object({
7987
+ query: Type.String({ description: "Natural language search query" }),
7988
+ limit: Type.Optional(Type.Number({ description: "Max results (default 5)" }))
7370
7989
  }),
7990
+ async execute(_id, params) {
7991
+ const { agent, diaryId } = ensureConnected(config);
7992
+ const results = await agent.entries.search({
7993
+ diaryId,
7994
+ query: params.query,
7995
+ limit: params.limit ?? 5
7996
+ });
7997
+ return {
7998
+ content: [{
7999
+ type: "text",
8000
+ text: JSON.stringify(results.results?.map((e) => ({
8001
+ id: e.id,
8002
+ title: e.title,
8003
+ tags: e.tags,
8004
+ importance: e.importance,
8005
+ contentPreview: typeof e.content === "string" ? e.content.slice(0, 200) : void 0
8006
+ })), null, 2)
8007
+ }],
8008
+ details: {}
8009
+ };
8010
+ }
8011
+ });
8012
+ const createEntry = defineTool({
8013
+ name: "moltnet_create_entry",
8014
+ label: "Create MoltNet Diary Entry",
8015
+ description: "Create a new diary entry to record decisions, findings, incidents, or reflections.",
8016
+ parameters: Type.Object({
8017
+ title: Type.String({ description: "Entry title (concise, descriptive)" }),
8018
+ content: Type.String({ description: "Entry content (markdown)" }),
8019
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorization" })),
8020
+ importance: Type.Optional(Type.Number({ description: "Importance 1-10 (default 5)" }))
8021
+ }),
8022
+ async execute(_id, params) {
8023
+ const { agent, diaryId } = ensureConnected(config);
8024
+ const entry = await agent.entries.create(diaryId, {
8025
+ title: params.title,
8026
+ content: params.content,
8027
+ tags: params.tags ?? [],
8028
+ importance: params.importance ?? 5
8029
+ });
8030
+ return {
8031
+ content: [{
8032
+ type: "text",
8033
+ text: JSON.stringify({
8034
+ id: entry.id,
8035
+ title: entry.title,
8036
+ createdAt: entry.createdAt
8037
+ }, null, 2)
8038
+ }],
8039
+ details: {}
8040
+ };
8041
+ }
8042
+ });
8043
+ const reviewSessionErrors = defineTool({
8044
+ name: "moltnet_review_session_errors",
8045
+ label: "Review Session Tool Errors",
8046
+ description: "Review tool failures buffered during this session (isError=true results). Use this to decide whether any failures are worth persisting as a diary entry via moltnet_create_entry. Most failures are transient (denied prompts, empty greps, mid-iteration typecheck errors) and should NOT be written to the diary — only persist incidents that represent a real finding (root cause identified, non-obvious workaround, recurring pattern). Pass clear=true to drop the buffer after reviewing.",
8047
+ parameters: Type.Object({ clear: Type.Optional(Type.Boolean({ description: "If true, empty the buffer after returning it. Use once you have decided whether to persist." })) }),
8048
+ async execute(_id, params) {
8049
+ const errors = config.getSessionErrors();
8050
+ const payload = {
8051
+ count: errors.length,
8052
+ errors: errors.map((e) => ({
8053
+ toolName: e.toolName,
8054
+ toolCallId: e.toolCallId,
8055
+ timestamp: new Date(e.timestamp).toISOString(),
8056
+ input: e.input,
8057
+ error: e.error
8058
+ }))
8059
+ };
8060
+ if (params.clear) config.clearSessionErrors();
8061
+ return {
8062
+ content: [{
8063
+ type: "text",
8064
+ text: JSON.stringify(payload, null, 2)
8065
+ }],
8066
+ details: {}
8067
+ };
8068
+ }
8069
+ });
8070
+ const HOST_EXEC_ALLOWED = new Set([
8071
+ "git",
8072
+ "gh",
8073
+ "moltnet"
8074
+ ]);
8075
+ const hostExecBaseEnv = config.hostExecBaseEnv ?? HOST_EXEC_DEFAULT_BASE_ENV;
8076
+ const HOST_EXEC_TIMEOUT_MS = 6e4;
8077
+ return [
8078
+ getPack,
8079
+ createPack,
8080
+ getPackProvenance,
8081
+ renderPack,
8082
+ listRenderedPacks,
8083
+ getRenderedPack,
8084
+ verifyRenderedPack,
8085
+ judgeRenderedPack,
8086
+ diaryTags,
8087
+ listEntries,
8088
+ getEntry,
8089
+ searchEntries,
8090
+ createEntry,
8091
+ reviewSessionErrors,
7371
8092
  defineTool({
7372
- name: "moltnet_rendered_pack_judge",
7373
- label: "Judge MoltNet Rendered Pack",
7374
- description: "Run the fidelity judge against a rendered pack. Local mode (no nonce): fetch the rendered pack + its source pack with entries, judge locally, return scores. Proctored mode (nonce): claim the verification payload from the API, judge, and submit scores with a Pi judge-recipe CID.",
8093
+ name: "moltnet_host_exec",
8094
+ label: "Run command on host (escape hatch — requires user approval)",
8095
+ description: "Runs a command on the HOST machine, outside the sandbox VM. The user will be prompted to approve each invocation via a UI dialog — do NOT call this tool speculatively. Use ONLY when a sandboxed operation is impossible e.g. `git push`, `gh pr create`.\n\nAllowed executables: git, gh, moltnet. Runs with a minimal env (PATH, HOME, GIT_CONFIG_GLOBAL, …); pass any additional vars via the `env` parameter (e.g. GH_TOKEN). Every invocation is logged as an auditable host execution.",
7375
8096
  parameters: Type.Object({
7376
- renderedPackId: Type.String({ description: "Rendered pack ID" }),
7377
- nonce: Type.Optional(Type.String({ description: "Verification nonce from moltnet_rendered_pack_verify. If set, runs proctored mode and submits scores. If omitted, runs local mode and does not submit." })),
7378
- rubric: Type.Optional(Type.String({ description: "Custom rubric override (local mode only). Defaults to the built-in rubric when omitted." }))
8097
+ executable: Type.String({ description: "Executable to run (git | gh | moltnet)" }),
8098
+ args: Type.Array(Type.String(), { description: "Arguments to pass to the executable" }),
8099
+ env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Additional environment variables for this invocation (e.g. { \"GH_TOKEN\": \"...\" }). Merged on top of the minimal base env." }))
7379
8100
  }),
7380
8101
  async execute(_id, params, _signal, _onUpdate, ctx) {
7381
- const { agent } = ensureConnected(config);
7382
- const model = ctx?.model;
7383
- if (!model) throw new Error("No active model in pi session — cannot run the fidelity judge.");
7384
- let sourceEntriesMd;
7385
- let renderedContent;
7386
- let rubric;
7387
- if (params.nonce) {
7388
- if (params.rubric) throw new Error("`rubric` is only supported in local mode (omit `nonce`).");
7389
- const claim = await agent.packs.claimVerification(params.renderedPackId);
7390
- sourceEntriesMd = buildSourceEntriesMarkdown(claim.sourceEntries);
7391
- renderedContent = claim.renderedContent;
7392
- rubric = claim.rubric?.trim() ? claim.rubric : DEFAULT_RUBRIC;
7393
- } else {
7394
- const rendered = await agent.packs.getRendered(params.renderedPackId);
7395
- if (!rendered.content?.trim()) throw new Error(`rendered pack ${params.renderedPackId} has empty content`);
7396
- const sourcePack = await agent.packs.get(rendered.sourcePackId, { expand: "entries" });
7397
- if (!sourcePack.entries || sourcePack.entries.length === 0) throw new Error(`source pack ${rendered.sourcePackId} has no entries`);
7398
- sourceEntriesMd = buildSourceEntriesMarkdown(sourcePack.entries.map((entry) => ({
7399
- title: entry.entry.title,
7400
- content: entry.entry.content
7401
- })));
7402
- renderedContent = rendered.content;
7403
- rubric = params.rubric?.trim() ? params.rubric : DEFAULT_RUBRIC;
8102
+ if (!HOST_EXEC_ALLOWED.has(params.executable)) throw new Error(`host_exec: '${params.executable}' is not in the allowed list (${[...HOST_EXEC_ALLOWED].join(", ")}). Extend HOST_EXEC_ALLOWED only after explicit security review.`);
8103
+ if (ctx?.ui) {
8104
+ const cmdDisplay = [params.executable, ...params.args].join(" ");
8105
+ if (!await ctx.ui.confirm("Allow host command?", `The agent wants to run on your machine:\n\n ${cmdDisplay}\n\nAllow?`)) throw new Error(`host_exec: user declined approval for: ${cmdDisplay}`);
8106
+ }
8107
+ const cwd = config.getHostCwd?.() ?? process.cwd();
8108
+ const baseEnv = {};
8109
+ for (const key of hostExecBaseEnv) {
8110
+ const val = process.env[key];
8111
+ if (val !== void 0) baseEnv[key] = val;
7404
8112
  }
7405
- let scores;
8113
+ const mergedEnv = {
8114
+ ...baseEnv,
8115
+ ...params.env ?? {}
8116
+ };
8117
+ let stdout;
8118
+ let stderr = "";
7406
8119
  try {
7407
- scores = await runFidelityJudge({
7408
- model,
7409
- sourceEntries: sourceEntriesMd,
7410
- renderedContent,
7411
- rubric
8120
+ stdout = execFileSync(params.executable, params.args, {
8121
+ encoding: "utf8",
8122
+ cwd,
8123
+ env: mergedEnv,
8124
+ stdio: [
8125
+ "pipe",
8126
+ "pipe",
8127
+ "pipe"
8128
+ ],
8129
+ timeout: HOST_EXEC_TIMEOUT_MS
7412
8130
  });
7413
8131
  } catch (err) {
7414
- throw new Error(`judge failed: ${err.message ?? String(err)}`);
8132
+ const e = err;
8133
+ stdout = e.stdout ?? "";
8134
+ stderr = e.stderr ?? e.message ?? String(err);
7415
8135
  }
7416
- if (!params.nonce) return {
7417
- content: [{
7418
- type: "text",
7419
- text: JSON.stringify({
7420
- mode: "local",
7421
- renderedPackId: params.renderedPackId,
7422
- scores
7423
- }, null, 2)
7424
- }],
7425
- details: {}
7426
- };
7427
- const recipe = computePiJudgeRecipeCid({
7428
- judgePrompt: JUDGE_SYSTEM_PROMPT,
7429
- rubric,
7430
- promptAsset: JUDGE_PROMPT_ASSET_PATH,
7431
- rubricAsset: RUBRIC_ASSET_PATH
7432
- });
7433
- const providerName = model.provider ?? "pi";
7434
- const modelId = model.id ?? "unknown";
7435
- const submit = await agent.packs.submitVerification(params.renderedPackId, {
7436
- nonce: params.nonce,
7437
- coverage: scores.coverage,
7438
- grounding: scores.grounding,
7439
- faithfulness: scores.faithfulness,
7440
- transcript: scores.reasoning,
7441
- judgeModel: modelId,
7442
- judgeProvider: providerName,
7443
- judgeBinaryCid: recipe.cid
7444
- });
7445
- return {
7446
- content: [{
7447
- type: "text",
7448
- text: JSON.stringify({
7449
- mode: "proctored",
7450
- renderedPackId: params.renderedPackId,
7451
- scores,
7452
- submission: submit,
7453
- judgeRecipeCid: recipe.cid,
7454
- judgeRecipeManifest: recipe.manifest
7455
- }, null, 2)
7456
- }],
7457
- details: {}
8136
+ const result = {
8137
+ host_exec: true,
8138
+ executable: params.executable,
8139
+ args: params.args,
8140
+ cwd,
8141
+ stdout: stdout.trimEnd(),
8142
+ stderr: stderr.trimEnd() || void 0
7458
8143
  };
7459
- }
7460
- }),
7461
- defineTool({
7462
- name: "moltnet_diary_tags",
7463
- label: "List MoltNet Diary Tags",
7464
- description: "Inventory tags on the current diary with entry counts. Cheap reconnaissance before committing to a search or list — use it to discover scope prefixes and cluster sizes. Optional prefix/minCount/entryTypes filters narrow the result.",
7465
- parameters: Type.Object({
7466
- prefix: Type.Optional(Type.String({ description: "Filter to tags starting with this prefix (e.g. \"scope:\")" })),
7467
- minCount: Type.Optional(Type.Number({ description: "Exclude tags with fewer than this many entries" })),
7468
- entryTypes: Type.Optional(Type.Array(Type.Union([
7469
- Type.Literal("episodic"),
7470
- Type.Literal("semantic"),
7471
- Type.Literal("procedural"),
7472
- Type.Literal("reflection"),
7473
- Type.Literal("identity"),
7474
- Type.Literal("soul")
7475
- ]), { description: "Scope the tag count to these entry types" }))
7476
- }),
7477
- async execute(_id, params) {
7478
- const { agent, diaryId } = ensureConnected(config);
7479
- const result = await agent.diaries.tags(diaryId, {
7480
- prefix: params.prefix,
7481
- minCount: params.minCount,
7482
- entryTypes: params.entryTypes
7483
- });
7484
8144
  return {
7485
8145
  content: [{
7486
8146
  type: "text",
@@ -7489,169 +8149,6 @@ function createMoltNetTools(config) {
7489
8149
  details: {}
7490
8150
  };
7491
8151
  }
7492
- }),
7493
- defineTool({
7494
- name: "moltnet_list_entries",
7495
- label: "List MoltNet Diary Entries",
7496
- description: "List entries from the MoltNet diary. When `entryIds` is provided, batch-fetches those specific entries (max 50) and returns full fields including entryType, contentSignature, and contentHash for signature checks. Otherwise returns recent entries with a content preview.",
7497
- parameters: Type.Object({
7498
- limit: Type.Optional(Type.Number({ description: "Max entries to return (default 10)" })),
7499
- tag: Type.Optional(Type.String({ description: "Filter by tag (optional)" })),
7500
- entryIds: Type.Optional(Type.Array(Type.String(), {
7501
- description: "Batch-fetch specific entries by UUID (max 50). Overrides `limit` and `tag` for selection.",
7502
- maxItems: 50
7503
- }))
7504
- }),
7505
- async execute(_id, params) {
7506
- const { agent, diaryId } = ensureConnected(config);
7507
- const query = {
7508
- orderBy: "createdAt",
7509
- order: "desc"
7510
- };
7511
- const batchMode = !!params.entryIds?.length;
7512
- if (batchMode) query.ids = params.entryIds;
7513
- else {
7514
- query.limit = params.limit ?? 10;
7515
- if (params.tag) query.tag = params.tag;
7516
- }
7517
- const entries = await agent.entries.list(diaryId, query);
7518
- return {
7519
- content: [{
7520
- type: "text",
7521
- text: JSON.stringify(entries.items?.map((e) => batchMode ? {
7522
- id: e.id,
7523
- title: e.title,
7524
- entryType: e.entryType,
7525
- tags: e.tags,
7526
- importance: e.importance,
7527
- contentHash: e.contentHash,
7528
- contentSignature: e.contentSignature,
7529
- signingNonce: e.signingNonce,
7530
- createdAt: e.createdAt
7531
- } : {
7532
- id: e.id,
7533
- title: e.title,
7534
- tags: e.tags,
7535
- importance: e.importance,
7536
- createdAt: e.createdAt,
7537
- contentPreview: typeof e.content === "string" ? e.content.slice(0, 200) : void 0
7538
- }), null, 2)
7539
- }],
7540
- details: {}
7541
- };
7542
- }
7543
- }),
7544
- defineTool({
7545
- name: "moltnet_get_entry",
7546
- label: "Get MoltNet Diary Entry",
7547
- description: "Get the full content of a specific diary entry by ID.",
7548
- parameters: Type.Object({ entryId: Type.String({ description: "The entry ID to fetch" }) }),
7549
- async execute(_id, params) {
7550
- const { agent } = ensureConnected(config);
7551
- const entry = await agent.entries.get(params.entryId);
7552
- return {
7553
- content: [{
7554
- type: "text",
7555
- text: JSON.stringify({
7556
- id: entry.id,
7557
- title: entry.title,
7558
- content: entry.content,
7559
- tags: entry.tags,
7560
- importance: entry.importance,
7561
- createdAt: entry.createdAt
7562
- }, null, 2)
7563
- }],
7564
- details: {}
7565
- };
7566
- }
7567
- }),
7568
- defineTool({
7569
- name: "moltnet_search_entries",
7570
- label: "Search MoltNet Diary Entries",
7571
- description: "Search diary entries by semantic query. Uses vector similarity to find relevant entries.",
7572
- parameters: Type.Object({
7573
- query: Type.String({ description: "Natural language search query" }),
7574
- limit: Type.Optional(Type.Number({ description: "Max results (default 5)" }))
7575
- }),
7576
- async execute(_id, params) {
7577
- const { agent, diaryId } = ensureConnected(config);
7578
- const results = await agent.entries.search({
7579
- diaryId,
7580
- query: params.query,
7581
- limit: params.limit ?? 5
7582
- });
7583
- return {
7584
- content: [{
7585
- type: "text",
7586
- text: JSON.stringify(results.results?.map((e) => ({
7587
- id: e.id,
7588
- title: e.title,
7589
- tags: e.tags,
7590
- importance: e.importance,
7591
- contentPreview: typeof e.content === "string" ? e.content.slice(0, 200) : void 0
7592
- })), null, 2)
7593
- }],
7594
- details: {}
7595
- };
7596
- }
7597
- }),
7598
- defineTool({
7599
- name: "moltnet_create_entry",
7600
- label: "Create MoltNet Diary Entry",
7601
- description: "Create a new diary entry to record decisions, findings, incidents, or reflections.",
7602
- parameters: Type.Object({
7603
- title: Type.String({ description: "Entry title (concise, descriptive)" }),
7604
- content: Type.String({ description: "Entry content (markdown)" }),
7605
- tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorization" })),
7606
- importance: Type.Optional(Type.Number({ description: "Importance 1-10 (default 5)" }))
7607
- }),
7608
- async execute(_id, params) {
7609
- const { agent, diaryId } = ensureConnected(config);
7610
- const entry = await agent.entries.create(diaryId, {
7611
- title: params.title,
7612
- content: params.content,
7613
- tags: params.tags ?? [],
7614
- importance: params.importance ?? 5
7615
- });
7616
- return {
7617
- content: [{
7618
- type: "text",
7619
- text: JSON.stringify({
7620
- id: entry.id,
7621
- title: entry.title,
7622
- createdAt: entry.createdAt
7623
- }, null, 2)
7624
- }],
7625
- details: {}
7626
- };
7627
- }
7628
- }),
7629
- defineTool({
7630
- name: "moltnet_review_session_errors",
7631
- label: "Review Session Tool Errors",
7632
- description: "Review tool failures buffered during this session (isError=true results). Use this to decide whether any failures are worth persisting as a diary entry via moltnet_create_entry. Most failures are transient (denied prompts, empty greps, mid-iteration typecheck errors) and should NOT be written to the diary — only persist incidents that represent a real finding (root cause identified, non-obvious workaround, recurring pattern). Pass clear=true to drop the buffer after reviewing.",
7633
- parameters: Type.Object({ clear: Type.Optional(Type.Boolean({ description: "If true, empty the buffer after returning it. Use once you have decided whether to persist." })) }),
7634
- async execute(_id, params) {
7635
- const errors = config.getSessionErrors();
7636
- const payload = {
7637
- count: errors.length,
7638
- errors: errors.map((e) => ({
7639
- toolName: e.toolName,
7640
- toolCallId: e.toolCallId,
7641
- timestamp: new Date(e.timestamp).toISOString(),
7642
- input: e.input,
7643
- error: e.error
7644
- }))
7645
- };
7646
- if (params.clear) config.clearSessionErrors();
7647
- return {
7648
- content: [{
7649
- type: "text",
7650
- text: JSON.stringify(payload, null, 2)
7651
- }],
7652
- details: {}
7653
- };
7654
- }
7655
8152
  })
7656
8153
  ];
7657
8154
  }
@@ -7987,10 +8484,6 @@ function createGondolinBashOps(vm, localCwd) {
7987
8484
  }
7988
8485
  //#endregion
7989
8486
  //#region src/vm-manager.ts
7990
- /**
7991
- * VM lifecycle manager: resume checkpoint, inject credentials, configure
7992
- * egress, fix TLS, and provide clean shutdown.
7993
- */
7994
8487
  var GUEST_WORKSPACE$1 = "/workspace";
7995
8488
  /**
7996
8489
  * Resolve the main worktree root (where .moltnet/ lives — it's untracked,
@@ -8021,6 +8514,15 @@ function loadCredentials(agentDir) {
8021
8514
  const sshPrivateKey = existsSync(path.join(sshDir, "id_ed25519")) ? readFileSync(path.join(sshDir, "id_ed25519"), "utf8") : null;
8022
8515
  const sshPublicKey = existsSync(path.join(sshDir, "id_ed25519.pub")) ? readFileSync(path.join(sshDir, "id_ed25519.pub"), "utf8") : null;
8023
8516
  const allowedSigners = existsSync(path.join(sshDir, "allowed_signers")) ? readFileSync(path.join(sshDir, "allowed_signers"), "utf8") : null;
8517
+ let githubAppPem = null;
8518
+ let githubAppPemFilename = null;
8519
+ const pemPath = JSON.parse(moltnetJson).github?.private_key_path;
8520
+ if (pemPath) if (!existsSync(pemPath)) process.stderr.write(`[pi-extension] Warning: github.private_key_path not found at ${pemPath} — moltnet github token will fail inside the guest
8521
+ `);
8522
+ else {
8523
+ githubAppPem = readFileSync(pemPath, "utf8");
8524
+ githubAppPemFilename = path.basename(pemPath);
8525
+ }
8024
8526
  return {
8025
8527
  moltnetJson,
8026
8528
  agentEnvRaw,
@@ -8029,7 +8531,9 @@ function loadCredentials(agentDir) {
8029
8531
  gitconfig,
8030
8532
  sshPrivateKey,
8031
8533
  sshPublicKey,
8032
- allowedSigners
8534
+ allowedSigners,
8535
+ githubAppPem,
8536
+ githubAppPemFilename
8033
8537
  };
8034
8538
  }
8035
8539
  /**
@@ -8076,13 +8580,15 @@ async function resumeVm(config) {
8076
8580
  apiHost,
8077
8581
  ...config.extraAllowedHosts ?? []
8078
8582
  ] });
8583
+ const vmAgentDir = `/home/agent/.moltnet/${config.agentName}`;
8079
8584
  const vmAgentEnv = {};
8080
8585
  for (const [k, v] of Object.entries(creds.agentEnv)) {
8081
8586
  if (v === void 0 || v === "") continue;
8082
- if (k === "GIT_CONFIG_GLOBAL") vmAgentEnv[k] = `/home/agent/.moltnet/${config.agentName}/gitconfig`;
8083
- else if (k.endsWith("_PRIVATE_KEY_PATH")) vmAgentEnv[k] = `/home/agent/.moltnet/${config.agentName}/${path.basename(v)}`;
8587
+ if (k === "GIT_CONFIG_GLOBAL") vmAgentEnv[k] = `${vmAgentDir}/gitconfig`;
8588
+ else if (k.endsWith("_PRIVATE_KEY_PATH")) vmAgentEnv[k] = `${vmAgentDir}/${path.basename(v)}`;
8084
8589
  else vmAgentEnv[k] = v;
8085
8590
  }
8591
+ vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
8086
8592
  const vfsConfig = config.sandboxConfig?.vfs;
8087
8593
  let workspaceProvider = new RealFSProvider(config.mountPath);
8088
8594
  if (vfsConfig?.shadow?.length) {
@@ -8117,11 +8623,11 @@ async function resumeVm(config) {
8117
8623
  '`);
8118
8624
  await vm.exec(`sh -c 'echo "nameserver 8.8.8.8
8119
8625
  nameserver 1.1.1.1" > /etc/resolv.conf'`);
8120
- const vmAgentDir = `/home/agent/.moltnet/${config.agentName}`;
8121
8626
  const vmSshDir = `${vmAgentDir}/ssh`;
8122
8627
  await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
8123
8628
  await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
8124
- await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, creds.moltnetJson, { mode: 384 });
8629
+ const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
8630
+ await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
8125
8631
  await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
8126
8632
  if (creds.gitconfig) {
8127
8633
  const vmSigningKey = `${vmSshDir}/id_ed25519`;
@@ -8132,6 +8638,7 @@ nameserver 1.1.1.1" > /etc/resolv.conf'`);
8132
8638
  if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
8133
8639
  if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
8134
8640
  if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
8641
+ if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
8135
8642
  await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
8136
8643
  return {
8137
8644
  vm,
@@ -8142,6 +8649,43 @@ nameserver 1.1.1.1" > /etc/resolv.conf'`);
8142
8649
  };
8143
8650
  }
8144
8651
  /**
8652
+ * Rewrite host-absolute paths inside moltnet.json to VM-local equivalents.
8653
+ *
8654
+ * Fields rewritten:
8655
+ * ssh.private_key_path → <vmSshDir>/<basename of original>
8656
+ * ssh.public_key_path → <vmSshDir>/<basename of original>
8657
+ * git.config_path → <vmAgentDir>/gitconfig
8658
+ * github.private_key_path → <vmAgentDir>/<pemFilename> (if present)
8659
+ *
8660
+ * All other fields are passed through unchanged.
8661
+ * Throws if moltnetJson is not valid JSON — callers must not inject a broken
8662
+ * moltnet.json into the guest.
8663
+ */
8664
+ function rewriteMoltnetJsonPaths(moltnetJson, vmAgentDir, vmSshDir, githubAppPemFilename) {
8665
+ const config = JSON.parse(moltnetJson);
8666
+ if (config.ssh && typeof config.ssh === "object") {
8667
+ const ssh = config.ssh;
8668
+ const origPrivate = typeof ssh.private_key_path === "string" ? ssh.private_key_path : null;
8669
+ const origPublic = typeof ssh.public_key_path === "string" ? ssh.public_key_path : null;
8670
+ config.ssh = {
8671
+ ...ssh,
8672
+ ...origPrivate !== null && { private_key_path: `${vmSshDir}/${path.basename(origPrivate)}` },
8673
+ ...origPublic !== null && { public_key_path: `${vmSshDir}/${path.basename(origPublic)}` }
8674
+ };
8675
+ }
8676
+ if (config.git && typeof config.git === "object") {
8677
+ const git = { ...config.git };
8678
+ git.config_path = `${vmAgentDir}/gitconfig`;
8679
+ config.git = git;
8680
+ }
8681
+ if (githubAppPemFilename && config.github && typeof config.github === "object") {
8682
+ const github = { ...config.github };
8683
+ github.private_key_path = `${vmAgentDir}/${githubAppPemFilename}`;
8684
+ config.github = github;
8685
+ }
8686
+ return JSON.stringify(config);
8687
+ }
8688
+ /**
8145
8689
  * Ensure `[worktree] useRelativePaths = true` is set in the given
8146
8690
  * gitconfig text. If the section exists, rewrite the key; otherwise
8147
8691
  * append a new section.
@@ -8179,9 +8723,9 @@ if (!FormatRegistry.Has("date-time")) FormatRegistry.Set("date-time", (v) => !Nu
8179
8723
  * own signed rows and CIDv1 lookup. The schema below is designed to
8180
8724
  * carry forward unchanged — only storage and addressing differ.
8181
8725
  *
8182
- * Until Phase 2 lands, `rubric_id` + `version` + `content_hash` are
8726
+ * Until Phase 2 lands, `rubricId` + `version` + `contentHash` are
8183
8727
  * informational fields the author fills in; no uniqueness is enforced.
8184
- * `content_hash` is optional in Phase 1 because the *task*'s input_cid
8728
+ * `contentHash` is optional in Phase 1 because the *task*'s input_cid
8185
8729
  * is the authoritative commitment.
8186
8730
  */
8187
8731
  /**
@@ -8217,12 +8761,12 @@ var RubricCriterion = Type$1.Object({
8217
8761
  * (stored row `body`); only the addressing mechanism differs.
8218
8762
  */
8219
8763
  var Rubric = Type$1.Object({
8220
- rubric_id: Type$1.String({ minLength: 1 }),
8764
+ rubricId: Type$1.String({ minLength: 1 }),
8221
8765
  version: Type$1.String({ minLength: 1 }),
8222
8766
  preamble: Type$1.Optional(Type$1.String()),
8223
8767
  criteria: Type$1.Array(RubricCriterion, { minItems: 1 }),
8224
8768
  scope: Type$1.Optional(Type$1.String()),
8225
- content_hash: Type$1.Optional(Type$1.String())
8769
+ contentHash: Type$1.Optional(Type$1.String())
8226
8770
  }, {
8227
8771
  $id: "Rubric",
8228
8772
  additionalProperties: false
@@ -8278,25 +8822,25 @@ var AssessBriefCriterion = Type$1.Object({
8278
8822
  additionalProperties: false
8279
8823
  });
8280
8824
  var AssessBriefInput = Type$1.Object({
8281
- target_task_id: Type$1.String({ format: "uuid" }),
8825
+ targetTaskId: Type$1.String({ format: "uuid" }),
8282
8826
  criteria: Type$1.Array(AssessBriefCriterion, { minItems: 1 }),
8283
- rubric_preamble: Type$1.Optional(Type$1.String())
8827
+ rubricPreamble: Type$1.Optional(Type$1.String())
8284
8828
  }, {
8285
8829
  $id: "AssessBriefInput",
8286
8830
  additionalProperties: false
8287
8831
  });
8288
8832
  /** One score line. */
8289
8833
  var AssessBriefScore = Type$1.Object({
8290
- criterion_id: Type$1.String({ minLength: 1 }),
8834
+ criterionId: Type$1.String({ minLength: 1 }),
8291
8835
  score: Type$1.Number({
8292
8836
  minimum: 0,
8293
8837
  maximum: 1
8294
8838
  }),
8295
8839
  rationale: Type$1.Optional(Type$1.String()),
8296
8840
  evidence: Type$1.Optional(Type$1.Object({
8297
- commits_verified: Type$1.Number(),
8298
- commits_total: Type$1.Number(),
8299
- signature_failures: Type$1.Array(Type$1.String())
8841
+ commitsVerified: Type$1.Number(),
8842
+ commitsTotal: Type$1.Number(),
8843
+ signatureFailures: Type$1.Array(Type$1.String())
8300
8844
  }, { additionalProperties: false }))
8301
8845
  }, {
8302
8846
  $id: "AssessBriefScore",
@@ -8309,7 +8853,7 @@ var AssessBriefOutput = Type$1.Object({
8309
8853
  maximum: 1
8310
8854
  }),
8311
8855
  verdict: Type$1.String({ minLength: 1 }),
8312
- judge_model: Type$1.Optional(Type$1.String())
8856
+ judgeModel: Type$1.Optional(Type$1.String())
8313
8857
  }, {
8314
8858
  $id: "AssessBriefOutput",
8315
8859
  additionalProperties: false
@@ -8340,15 +8884,15 @@ var EntryTypeFilter = Type$1.Union([
8340
8884
  Type$1.Literal("reflection")
8341
8885
  ]);
8342
8886
  var CuratePackInput = Type$1.Object({
8343
- diary_id: Type$1.String({ format: "uuid" }),
8344
- task_prompt: Type$1.String({ minLength: 1 }),
8345
- entry_types: Type$1.Optional(Type$1.Array(EntryTypeFilter, { minItems: 1 })),
8346
- tag_filters: Type$1.Optional(Type$1.Object({
8887
+ diaryId: Type$1.String({ format: "uuid" }),
8888
+ taskPrompt: Type$1.String({ minLength: 1 }),
8889
+ entryTypes: Type$1.Optional(Type$1.Array(EntryTypeFilter, { minItems: 1 })),
8890
+ tagFilters: Type$1.Optional(Type$1.Object({
8347
8891
  include: Type$1.Optional(Type$1.Array(Type$1.String())),
8348
8892
  exclude: Type$1.Optional(Type$1.Array(Type$1.String())),
8349
8893
  prefix: Type$1.Optional(Type$1.String())
8350
8894
  }, { additionalProperties: false })),
8351
- token_budget: Type$1.Optional(Type$1.Number({ minimum: 500 })),
8895
+ tokenBudget: Type$1.Optional(Type$1.Number({ minimum: 500 })),
8352
8896
  recipe: Type$1.Optional(Type$1.Union([Type$1.Literal("topic-focused-v1"), Type$1.Literal("scope-inventory-v1")]))
8353
8897
  }, {
8354
8898
  $id: "CuratePackInput",
@@ -8360,18 +8904,18 @@ var CuratePackInput = Type$1.Object({
8360
8904
  * is the receipt.
8361
8905
  */
8362
8906
  var CuratePackOutput = Type$1.Object({
8363
- pack_id: Type$1.String({ format: "uuid" }),
8364
- pack_cid: Type$1.String({ minLength: 1 }),
8907
+ packId: Type$1.String({ format: "uuid" }),
8908
+ packCid: Type$1.String({ minLength: 1 }),
8365
8909
  entries: Type$1.Array(Type$1.Object({
8366
- entry_id: Type$1.String({ format: "uuid" }),
8910
+ entryId: Type$1.String({ format: "uuid" }),
8367
8911
  rank: Type$1.Number({ minimum: 1 }),
8368
8912
  rationale: Type$1.String({ minLength: 1 })
8369
8913
  }, { additionalProperties: false }), { minItems: 1 }),
8370
- recipe_params: Type$1.Record(Type$1.String(), Type$1.Unknown()),
8914
+ recipeParams: Type$1.Record(Type$1.String(), Type$1.Unknown()),
8371
8915
  checkpoints: Type$1.Optional(Type$1.Array(Type$1.Object({
8372
8916
  phase: Type$1.String({ minLength: 1 }),
8373
- candidate_ids: Type$1.Array(Type$1.String({ format: "uuid" })),
8374
- dropped_ids: Type$1.Optional(Type$1.Array(Type$1.String({ format: "uuid" }))),
8917
+ candidateIds: Type$1.Array(Type$1.String({ format: "uuid" })),
8918
+ droppedIds: Type$1.Optional(Type$1.Array(Type$1.String({ format: "uuid" }))),
8375
8919
  notes: Type$1.String({ minLength: 1 })
8376
8920
  }, { additionalProperties: false }))),
8377
8921
  summary: Type$1.String({ minLength: 1 })
@@ -8392,9 +8936,9 @@ var FULFILL_BRIEF_TYPE = "fulfill_brief";
8392
8936
  var FulfillBriefInput = Type$1.Object({
8393
8937
  brief: Type$1.String({ minLength: 1 }),
8394
8938
  title: Type$1.Optional(Type$1.String()),
8395
- acceptance_criteria: Type$1.Optional(Type$1.Array(Type$1.String())),
8396
- seed_files: Type$1.Optional(Type$1.Array(Type$1.String())),
8397
- scope_hint: Type$1.Optional(Type$1.String())
8939
+ acceptanceCriteria: Type$1.Optional(Type$1.Array(Type$1.String())),
8940
+ seedFiles: Type$1.Optional(Type$1.Array(Type$1.String())),
8941
+ scopeHint: Type$1.Optional(Type$1.String())
8398
8942
  }, {
8399
8943
  $id: "FulfillBriefInput",
8400
8944
  additionalProperties: false
@@ -8408,10 +8952,10 @@ var FulfillBriefOutput = Type$1.Object({
8408
8952
  commits: Type$1.Array(Type$1.Object({
8409
8953
  sha: Type$1.String({ minLength: 7 }),
8410
8954
  message: Type$1.String(),
8411
- diary_entry_id: Type$1.Union([Type$1.String({ format: "uuid" }), Type$1.Null()])
8955
+ diaryEntryId: Type$1.Union([Type$1.String({ format: "uuid" }), Type$1.Null()])
8412
8956
  }, { additionalProperties: false })),
8413
- pull_request_url: Type$1.Union([Type$1.String(), Type$1.Null()]),
8414
- diary_entry_ids: Type$1.Array(Type$1.String({ format: "uuid" })),
8957
+ pullRequestUrl: Type$1.Union([Type$1.String(), Type$1.Null()]),
8958
+ diaryEntryIds: Type$1.Array(Type$1.String({ format: "uuid" })),
8415
8959
  summary: Type$1.String({ minLength: 1 })
8416
8960
  }, {
8417
8961
  $id: "FulfillBriefOutput",
@@ -8442,8 +8986,8 @@ var FulfillBriefOutput = Type$1.Object({
8442
8986
  */
8443
8987
  var JUDGE_PACK_TYPE = "judge_pack";
8444
8988
  var JudgePackInput = Type$1.Object({
8445
- rendered_pack_id: Type$1.String({ format: "uuid" }),
8446
- source_pack_id: Type$1.String({ format: "uuid" }),
8989
+ renderedPackId: Type$1.String({ format: "uuid" }),
8990
+ sourcePackId: Type$1.String({ format: "uuid" }),
8447
8991
  rubric: Rubric
8448
8992
  }, {
8449
8993
  $id: "JudgePackInput",
@@ -8451,7 +8995,7 @@ var JudgePackInput = Type$1.Object({
8451
8995
  });
8452
8996
  /** One scored criterion. Mirrors `AssessBriefScore`. */
8453
8997
  var JudgePackScore = Type$1.Object({
8454
- criterion_id: Type$1.String({ minLength: 1 }),
8998
+ criterionId: Type$1.String({ minLength: 1 }),
8455
8999
  score: Type$1.Number({
8456
9000
  minimum: 0,
8457
9001
  maximum: 1
@@ -8469,8 +9013,8 @@ var JudgePackOutput = Type$1.Object({
8469
9013
  maximum: 1
8470
9014
  }),
8471
9015
  verdict: Type$1.String({ minLength: 1 }),
8472
- judge_model: Type$1.Optional(Type$1.String()),
8473
- renderer_binary_cid: Type$1.Optional(Type$1.Union([Type$1.String(), Type$1.Null()]))
9016
+ judgeModel: Type$1.Optional(Type$1.String()),
9017
+ rendererBinaryCid: Type$1.Optional(Type$1.Union([Type$1.String(), Type$1.Null()]))
8474
9018
  }, {
8475
9019
  $id: "JudgePackOutput",
8476
9020
  additionalProperties: false
@@ -8494,7 +9038,7 @@ var JudgePackOutput = Type$1.Object({
8494
9038
  */
8495
9039
  var RENDER_PACK_TYPE = "render_pack";
8496
9040
  var RenderPackInput = Type$1.Object({
8497
- pack_id: Type$1.String({ format: "uuid" }),
9041
+ packId: Type$1.String({ format: "uuid" }),
8498
9042
  persist: Type$1.Optional(Type$1.Boolean()),
8499
9043
  pinned: Type$1.Optional(Type$1.Boolean())
8500
9044
  }, {
@@ -8502,11 +9046,11 @@ var RenderPackInput = Type$1.Object({
8502
9046
  additionalProperties: false
8503
9047
  });
8504
9048
  var RenderPackOutput = Type$1.Object({
8505
- rendered_pack_id: Type$1.Union([Type$1.String({ format: "uuid" }), Type$1.Null()]),
8506
- rendered_cid: Type$1.String({ minLength: 1 }),
8507
- render_method: Type$1.String({ minLength: 1 }),
8508
- byte_size: Type$1.Number({ minimum: 0 }),
8509
- entries_rendered: Type$1.Number({ minimum: 0 }),
9049
+ renderedPackId: Type$1.Union([Type$1.String({ format: "uuid" }), Type$1.Null()]),
9050
+ renderedCid: Type$1.String({ minLength: 1 }),
9051
+ renderMethod: Type$1.String({ minLength: 1 }),
9052
+ byteSize: Type$1.Number({ minimum: 0 }),
9053
+ entriesRendered: Type$1.Number({ minimum: 0 }),
8510
9054
  summary: Type$1.String({ minLength: 1 })
8511
9055
  }, {
8512
9056
  $id: "RenderPackOutput",
@@ -8578,6 +9122,30 @@ new Proxy({}, { get(_, prop) {
8578
9122
  return getTaskTypeRegistry().get(prop);
8579
9123
  } });
8580
9124
  //#endregion
9125
+ //#region ../tasks/src/validation.ts
9126
+ function getTaskTypeEntry(taskType) {
9127
+ const taskTypes = BUILT_IN_TASK_TYPES;
9128
+ if (!Object.prototype.hasOwnProperty.call(taskTypes, taskType)) return;
9129
+ return taskTypes[taskType];
9130
+ }
9131
+ function formatField(prefix, path) {
9132
+ return path ? `${prefix}${path}` : prefix;
9133
+ }
9134
+ function schemaErrors(prefix, schema, value) {
9135
+ return [...Value.Errors(schema, value)].map((error) => ({
9136
+ field: formatField(prefix, error.path),
9137
+ message: error.message
9138
+ }));
9139
+ }
9140
+ function validateTaskOutput(taskType, output) {
9141
+ const entry = getTaskTypeEntry(taskType);
9142
+ if (!entry) return [{
9143
+ field: "taskType",
9144
+ message: `Unknown task type: ${taskType}`
9145
+ }];
9146
+ return schemaErrors("output", entry.outputSchema, output);
9147
+ }
9148
+ //#endregion
8581
9149
  //#region ../tasks/src/wire.ts
8582
9150
  /**
8583
9151
  * Wire-format types for the MoltNet Task model.
@@ -8589,7 +9157,7 @@ new Proxy({}, { get(_, prop) {
8589
9157
  * - `TaskReporter` output records (PR 0)
8590
9158
  *
8591
9159
  * Invariant: every property on `Task` is type-neutral (applies to all
8592
- * `task_type`s). Type-specific payloads live inside `input` / `output`
9160
+ * `taskType`s). Type-specific payloads live inside `input` / `output`
8593
9161
  * JSONB, validated against schemas registered under `task_types`.
8594
9162
  *
8595
9163
  * Identity rule:
@@ -8632,8 +9200,8 @@ var IsoTimestamp = Type$1.String({ format: "date-time" });
8632
9200
  * Embedded in `tasks.references` JSONB array.
8633
9201
  */
8634
9202
  var TaskRef = Type$1.Object({
8635
- task_id: Type$1.Union([Uuid, Type$1.Null()]),
8636
- output_cid: Cid,
9203
+ taskId: Type$1.Union([Uuid, Type$1.Null()]),
9204
+ outputCid: Cid,
8637
9205
  role: Type$1.Union([
8638
9206
  Type$1.Literal("judged_work"),
8639
9207
  Type$1.Literal("reviewed_diff"),
@@ -8662,11 +9230,11 @@ var TaskRef = Type$1.Object({
8662
9230
  * `TaskOutput.usage` for convenience.
8663
9231
  */
8664
9232
  var TaskUsage = Type$1.Object({
8665
- input_tokens: Type$1.Integer({ minimum: 0 }),
8666
- output_tokens: Type$1.Integer({ minimum: 0 }),
8667
- cache_read_tokens: Type$1.Optional(Type$1.Integer({ minimum: 0 })),
8668
- cache_write_tokens: Type$1.Optional(Type$1.Integer({ minimum: 0 })),
8669
- tool_calls: Type$1.Optional(Type$1.Integer({ minimum: 0 })),
9233
+ inputTokens: Type$1.Integer({ minimum: 0 }),
9234
+ outputTokens: Type$1.Integer({ minimum: 0 }),
9235
+ cacheReadTokens: Type$1.Optional(Type$1.Integer({ minimum: 0 })),
9236
+ cacheWriteTokens: Type$1.Optional(Type$1.Integer({ minimum: 0 })),
9237
+ toolCalls: Type$1.Optional(Type$1.Integer({ minimum: 0 })),
8670
9238
  model: Type$1.Optional(Type$1.String()),
8671
9239
  provider: Type$1.Optional(Type$1.String())
8672
9240
  }, {
@@ -8686,62 +9254,65 @@ var TaskError = Type$1.Object({
8686
9254
  additionalProperties: false
8687
9255
  });
8688
9256
  Type$1.Object({
8689
- agent_id: Type$1.Union([Uuid, Type$1.Null()]),
8690
- human_id: Type$1.Union([Uuid, Type$1.Null()])
9257
+ agentId: Type$1.Union([Uuid, Type$1.Null()]),
9258
+ humanId: Type$1.Union([Uuid, Type$1.Null()])
8691
9259
  }, {
8692
9260
  $id: "ActorPair",
8693
9261
  additionalProperties: false
8694
9262
  });
8695
9263
  Type$1.Object({
8696
9264
  id: Uuid,
8697
- task_type: Type$1.String({ minLength: 1 }),
8698
- team_id: Uuid,
8699
- diary_id: Type$1.Union([Uuid, Type$1.Null()]),
8700
- output_kind: OutputKind,
9265
+ taskType: Type$1.String({ minLength: 1 }),
9266
+ teamId: Uuid,
9267
+ diaryId: Type$1.Union([Uuid, Type$1.Null()]),
9268
+ outputKind: OutputKind,
8701
9269
  input: Type$1.Record(Type$1.String(), Type$1.Unknown()),
8702
- input_schema_cid: Cid,
8703
- input_cid: Cid,
8704
- criteria_cid: Type$1.Union([Cid, Type$1.Null()]),
9270
+ inputSchemaCid: Cid,
9271
+ inputCid: Cid,
9272
+ criteriaCid: Type$1.Union([Cid, Type$1.Null()]),
8705
9273
  references: Type$1.Array(TaskRef),
8706
- correlation_id: Type$1.Union([Uuid, Type$1.Null()]),
8707
- imposed_by_agent_id: Type$1.Union([Uuid, Type$1.Null()]),
8708
- imposed_by_human_id: Type$1.Union([Uuid, Type$1.Null()]),
8709
- accepted_attempt_n: Type$1.Union([Type$1.Number(), Type$1.Null()]),
9274
+ correlationId: Type$1.Union([Uuid, Type$1.Null()]),
9275
+ imposedByAgentId: Type$1.Union([Uuid, Type$1.Null()]),
9276
+ imposedByHumanId: Type$1.Union([Uuid, Type$1.Null()]),
9277
+ acceptedAttemptN: Type$1.Union([Type$1.Number(), Type$1.Null()]),
8710
9278
  status: TaskStatus,
8711
- queued_at: IsoTimestamp,
8712
- completed_at: Type$1.Union([IsoTimestamp, Type$1.Null()]),
8713
- expires_at: Type$1.Union([IsoTimestamp, Type$1.Null()]),
8714
- cancelled_by_agent_id: Type$1.Union([Uuid, Type$1.Null()]),
8715
- cancelled_by_human_id: Type$1.Union([Uuid, Type$1.Null()]),
8716
- cancel_reason: Type$1.Union([Type$1.String(), Type$1.Null()]),
8717
- max_attempts: Type$1.Number({ minimum: 1 })
9279
+ queuedAt: IsoTimestamp,
9280
+ completedAt: Type$1.Union([IsoTimestamp, Type$1.Null()]),
9281
+ expiresAt: Type$1.Union([IsoTimestamp, Type$1.Null()]),
9282
+ cancelledByAgentId: Type$1.Union([Uuid, Type$1.Null()]),
9283
+ cancelledByHumanId: Type$1.Union([Uuid, Type$1.Null()]),
9284
+ cancelReason: Type$1.Union([Type$1.String(), Type$1.Null()]),
9285
+ maxAttempts: Type$1.Number({ minimum: 1 })
8718
9286
  }, {
8719
9287
  $id: "Task",
8720
9288
  additionalProperties: false
8721
9289
  });
8722
9290
  Type$1.Object({
8723
- task_id: Uuid,
8724
- attempt_n: Type$1.Number({ minimum: 1 }),
8725
- claimed_by_agent_id: Uuid,
8726
- runtime_id: Type$1.Union([Uuid, Type$1.Null()]),
8727
- claimed_at: IsoTimestamp,
8728
- started_at: Type$1.Union([IsoTimestamp, Type$1.Null()]),
8729
- completed_at: Type$1.Union([IsoTimestamp, Type$1.Null()]),
9291
+ taskId: Uuid,
9292
+ attemptN: Type$1.Number({ minimum: 1 }),
9293
+ claimedByAgentId: Uuid,
9294
+ runtimeId: Type$1.Union([Uuid, Type$1.Null()]),
9295
+ claimedAt: IsoTimestamp,
9296
+ startedAt: Type$1.Union([IsoTimestamp, Type$1.Null()]),
9297
+ completedAt: Type$1.Union([IsoTimestamp, Type$1.Null()]),
8730
9298
  status: TaskAttemptStatus,
8731
9299
  output: Type$1.Union([Type$1.Record(Type$1.String(), Type$1.Unknown()), Type$1.Null()]),
8732
- output_cid: Type$1.Union([Cid, Type$1.Null()]),
9300
+ outputCid: Type$1.Union([Cid, Type$1.Null()]),
8733
9301
  error: Type$1.Union([TaskError, Type$1.Null()]),
8734
9302
  usage: Type$1.Union([TaskUsage, Type$1.Null()]),
8735
- content_signature: Type$1.Union([Type$1.String(), Type$1.Null()]),
8736
- signed_at: Type$1.Union([IsoTimestamp, Type$1.Null()])
9303
+ contentSignature: Type$1.Union([Type$1.String(), Type$1.Null()]),
9304
+ signedAt: Type$1.Union([IsoTimestamp, Type$1.Null()])
8737
9305
  }, {
8738
9306
  $id: "TaskAttempt",
8739
9307
  additionalProperties: false
8740
9308
  });
8741
9309
  Type$1.Object({
8742
- task_id: Uuid,
8743
- attempt_n: Type$1.Number({ minimum: 1 }),
8744
- seq: Type$1.Number({ minimum: 0 }),
9310
+ taskId: Uuid,
9311
+ attemptN: Type$1.Number({ minimum: 1 }),
9312
+ seq: Type$1.Number({
9313
+ minimum: 0,
9314
+ description: "Monotonically increasing integer assigned by the server. Use as the afterSeq cursor on the list-messages endpoint to poll for new messages without re-fetching earlier ones."
9315
+ }),
8745
9316
  timestamp: IsoTimestamp,
8746
9317
  kind: TaskMessageKind,
8747
9318
  payload: Type$1.Record(Type$1.String(), Type$1.Unknown())
@@ -8750,34 +9321,34 @@ Type$1.Object({
8750
9321
  additionalProperties: false
8751
9322
  });
8752
9323
  Type$1.Object({
8753
- task_id: Uuid,
8754
- attempt_n: Type$1.Number({ minimum: 1 }),
9324
+ taskId: Uuid,
9325
+ attemptN: Type$1.Number({ minimum: 1 }),
8755
9326
  status: Type$1.Union([
8756
9327
  Type$1.Literal("completed"),
8757
9328
  Type$1.Literal("failed"),
8758
9329
  Type$1.Literal("cancelled")
8759
9330
  ]),
8760
9331
  output: Type$1.Union([Type$1.Record(Type$1.String(), Type$1.Unknown()), Type$1.Null()]),
8761
- output_cid: Type$1.Union([Cid, Type$1.Null()]),
9332
+ outputCid: Type$1.Union([Cid, Type$1.Null()]),
8762
9333
  usage: TaskUsage,
8763
- duration_ms: Type$1.Number({ minimum: 0 }),
9334
+ durationMs: Type$1.Number({ minimum: 0 }),
8764
9335
  error: Type$1.Optional(TaskError),
8765
- content_signature: Type$1.Optional(Type$1.String())
9336
+ contentSignature: Type$1.Optional(Type$1.String())
8766
9337
  }, {
8767
9338
  $id: "TaskOutput",
8768
9339
  additionalProperties: false
8769
9340
  });
8770
9341
  Type$1.Object({
8771
- runtime_id: Uuid,
8772
- agent_id: Uuid,
9342
+ runtimeId: Uuid,
9343
+ agentId: Uuid,
8773
9344
  timestamp: IsoTimestamp,
8774
9345
  status: Type$1.Union([
8775
9346
  Type$1.Literal("idle"),
8776
9347
  Type$1.Literal("busy"),
8777
9348
  Type$1.Literal("draining")
8778
9349
  ]),
8779
- active_task_ids: Type$1.Array(Uuid),
8780
- supported_task_types: Type$1.Array(Type$1.String())
9350
+ activeTaskIds: Type$1.Array(Uuid),
9351
+ supportedTaskTypes: Type$1.Array(Type$1.String())
8781
9352
  }, {
8782
9353
  $id: "RuntimeHeartbeat",
8783
9354
  additionalProperties: false
@@ -8798,10 +9369,10 @@ function buildAssessBriefPrompt(input, ctx) {
8798
9369
  ...ctx.target.diaryEntryIds.map((id) => `- ${id}`),
8799
9370
  ""
8800
9371
  ].join("\n") : "";
8801
- const preambleSection = input.rubric_preamble ? [
9372
+ const preambleSection = input.rubricPreamble ? [
8802
9373
  "### Rubric preamble",
8803
9374
  "",
8804
- input.rubric_preamble,
9375
+ input.rubricPreamble,
8805
9376
  ""
8806
9377
  ].join("\n") : "";
8807
9378
  return [
@@ -8832,12 +9403,12 @@ function buildAssessBriefPrompt(input, ctx) {
8832
9403
  "",
8833
9404
  "- `llm_judged`: score 0..1 continuous. `rationale` REQUIRED (2–4 sentences).",
8834
9405
  "- `boolean`: score exactly 0 or 1. `rationale` optional.",
8835
- "- `deterministic_signature_check`: run `moltnet entry verify` on every diary entry listed above AND `git verify-commit` on every commit. Score 1 iff ALL signatures are valid; otherwise 0. Populate `evidence.commits_verified`, `evidence.commits_total`, `evidence.signature_failures`.",
9406
+ "- `deterministic_signature_check`: run `moltnet entry verify` on every diary entry listed above AND `git verify-commit` on every commit. Score 1 iff ALL signatures are valid; otherwise 0. Populate `evidence.commitsVerified`, `evidence.commitsTotal`, `evidence.signatureFailures`.",
8836
9407
  "",
8837
9408
  "### Final output",
8838
9409
  "",
8839
9410
  "Emit a JSON object matching `AssessBriefOutput`:",
8840
- " { \"scores\": [{criterion_id, score, rationale?, evidence?}], \"composite\", \"verdict\", \"judge_model\"? }",
9411
+ " { \"scores\": [{criterionId, score, rationale?, evidence?}], \"composite\", \"verdict\", \"judgeModel\"? }",
8841
9412
  "`composite` = Σ(weight_i × score_i) recomputed. The runtime will reject a mismatch.",
8842
9413
  "Write a signed diary entry (tags: \"judgment\", \"assess_brief\") capturing the rationale before emitting the JSON."
8843
9414
  ].filter(Boolean).join("\n");
@@ -8859,7 +9430,7 @@ function buildAssessBriefPrompt(input, ctx) {
8859
9430
  * N isolated `createAgentSession` children (one per tag cluster or
8860
9431
  * entry_type axis the curator picks after recon), each with a narrow
8861
9432
  * tool subset and a turn cap, and returns compressed summaries. Parent
8862
- * curator keeps a warm context and only sees {candidate_ids, notes}
9433
+ * curator keeps a warm context and only sees {candidateIds, notes}
8863
9434
  * per probe — mirrors the fan-out pattern pi-mono SDK example #13
8864
9435
  * (session runtime) + #05 (custom tools) makes possible. Until that
8865
9436
  * lands, the `checkpoints[]` output field is the fallback: curator
@@ -8867,7 +9438,7 @@ function buildAssessBriefPrompt(input, ctx) {
8867
9438
  * resume without replaying the tool history.
8868
9439
  */
8869
9440
  function buildCuratePackPrompt(input, ctx) {
8870
- const { diary_id: diaryId, task_prompt: taskPrompt, entry_types: entryTypes, tag_filters: tagFilters, token_budget: tokenBudget, recipe } = input;
9441
+ const { diaryId, taskPrompt, entryTypes, tagFilters, tokenBudget, recipe } = input;
8871
9442
  const effectiveEntryTypes = entryTypes ?? [
8872
9443
  "semantic",
8873
9444
  "episodic",
@@ -8954,7 +9525,7 @@ function buildCuratePackPrompt(input, ctx) {
8954
9525
  "",
8955
9526
  "The tool returns a JSON payload whose top-level fields are `packId` and",
8956
9527
  "`packCid` (NOT `id`). Copy those exact UUID/CID strings verbatim into",
8957
- "`pack_id` and `pack_cid` in your final output — do not substitute an",
9528
+ "`packId` and `packCid` in your final output — do not substitute an",
8958
9529
  "entry id, do not reformat, do not fabricate a UUID.",
8959
9530
  "",
8960
9531
  "## Hard constraints",
@@ -8971,14 +9542,14 @@ function buildCuratePackPrompt(input, ctx) {
8971
9542
  "Write to stdout a JSON object matching `CuratePackOutput`:",
8972
9543
  "```",
8973
9544
  "{",
8974
- " \"pack_id\": \"<uuid>\",",
8975
- " \"pack_cid\": \"<cid>\",",
9545
+ " \"packId\": \"<uuid>\",",
9546
+ " \"packCid\": \"<cid>\",",
8976
9547
  " \"entries\": [",
8977
- " { \"entry_id\": \"<uuid>\", \"rank\": 1, \"rationale\": \"<why>\" }",
9548
+ " { \"entryId\": \"<uuid>\", \"rank\": 1, \"rationale\": \"<why>\" }",
8978
9549
  " ],",
8979
- " \"recipe_params\": { \"recipe\": \"...\", \"prompt\": \"...\", ... },",
9550
+ " \"recipeParams\": { \"recipe\": \"...\", \"prompt\": \"...\", ... },",
8980
9551
  " \"checkpoints\": [",
8981
- " { \"phase\": \"recon\", \"candidate_ids\": [...], \"dropped_ids\": [...], \"notes\": \"...\" }",
9552
+ " { \"phase\": \"recon\", \"candidateIds\": [...], \"droppedIds\": [...], \"notes\": \"...\" }",
8982
9553
  " ],",
8983
9554
  " \"summary\": \"<2-4 sentences: what you looked for, how you narrowed, what defines the final set>\"",
8984
9555
  "}",
@@ -8996,7 +9567,7 @@ function buildCuratePackPrompt(input, ctx) {
8996
9567
  * is told to inspect them itself.
8997
9568
  */
8998
9569
  function buildFulfillBriefPrompt(input, ctx) {
8999
- const { brief, title, acceptance_criteria: acceptanceCriteria, seed_files: seedFiles, scope_hint: scopeHint } = input;
9570
+ const { brief, title, acceptanceCriteria, seedFiles, scopeHint } = input;
9000
9571
  const criteriaSection = acceptanceCriteria?.length ? [
9001
9572
  "### Acceptance criteria",
9002
9573
  "",
@@ -9045,14 +9616,14 @@ function buildFulfillBriefPrompt(input, ctx) {
9045
9616
  "### Final output",
9046
9617
  "",
9047
9618
  "When done, write to stdout a JSON object with shape matching `FulfillBriefOutput`:",
9048
- " { \"branch\", \"commits\": [{sha, message, diary_entry_id}], \"pull_request_url\", \"diary_entry_ids\", \"summary\" }",
9619
+ " { \"branch\", \"commits\": [{sha, message, diaryEntryId}], \"pullRequestUrl\", \"diaryEntryIds\", \"summary\" }",
9049
9620
  "The runtime parses this as the structured task output. Failing to emit it is a failure."
9050
9621
  ].filter(Boolean).join("\n");
9051
9622
  }
9052
9623
  //#endregion
9053
9624
  //#region ../agent-runtime/src/prompts/judge-pack.ts
9054
9625
  function buildJudgePackPrompt(input, ctx) {
9055
- const { rendered_pack_id: renderedPackId, source_pack_id: sourcePackId, rubric } = input;
9626
+ const { renderedPackId, sourcePackId, rubric } = input;
9056
9627
  const criteriaList = rubric.criteria.map((c, i) => `${i + 1}. **${c.id}** (weight ${c.weight}, scoring: \`${c.scoring}\`) — ${c.description}`).join("\n");
9057
9628
  const preambleSection = rubric.preamble ? [
9058
9629
  "### Rubric preamble",
@@ -9075,7 +9646,7 @@ function buildJudgePackPrompt(input, ctx) {
9075
9646
  "",
9076
9647
  `- **Rendered pack**: \`${renderedPackId}\``,
9077
9648
  `- **Source pack**: \`${sourcePackId}\``,
9078
- `- **Rubric**: \`${rubric.rubric_id}\` v${rubric.version}`,
9649
+ `- **Rubric**: \`${rubric.rubricId}\` v${rubric.version}`,
9079
9650
  "",
9080
9651
  preambleSection,
9081
9652
  "## Workflow",
@@ -9114,12 +9685,12 @@ function buildJudgePackPrompt(input, ctx) {
9114
9685
  " non-empty `contentSignature`. If `required_signed_total` is 0,",
9115
9686
  " score = 1. Populate `evidence` with `{ entries_verified,",
9116
9687
  " entries_total, required_signed_total, required_signed_ok,",
9117
- " signature_failures: [entry_ids] }` where `signature_failures` lists",
9688
+ " signatureFailures: [entryIds] }` where `signatureFailures` lists",
9118
9689
  " ONLY the REQUIRED-SIGNED entries that lack a signature.",
9119
9690
  "- `deterministic_coverage_check`: for every source entry, check",
9120
- " whether its `entry_id` (or a stable reference like title + CID",
9691
+ " whether its `entryId` (or a stable reference like title + CID",
9121
9692
  " prefix) appears in the rendered `content`. Score 1 iff coverage is",
9122
- " complete; otherwise 0. Populate `evidence` with `{ covered, total, missing: [entry_ids] }`.",
9693
+ " complete; otherwise 0. Populate `evidence` with `{ covered, total, missing: [entryIds] }`.",
9123
9694
  "",
9124
9695
  "## Constraints",
9125
9696
  "",
@@ -9133,17 +9704,17 @@ function buildJudgePackPrompt(input, ctx) {
9133
9704
  "Write to stdout a JSON object matching `JudgePackOutput`:",
9134
9705
  "```",
9135
9706
  "{",
9136
- " \"scores\": [{\"criterion_id\": \"...\", \"score\": 0.0, \"rationale\": \"...\", \"evidence\": {...}}],",
9707
+ " \"scores\": [{\"criterionId\": \"...\", \"score\": 0.0, \"rationale\": \"...\", \"evidence\": {...}}],",
9137
9708
  " \"composite\": <sum-of-weighted-scores>,",
9138
9709
  " \"verdict\": \"<1-3 sentence overall>\",",
9139
- " \"judge_model\": \"<provider:model>\",",
9140
- " \"renderer_binary_cid\": \"<cid-string-only-if-available>\"",
9710
+ " \"judgeModel\": \"<provider:model>\",",
9711
+ " \"rendererBinaryCid\": \"<cid-string-only-if-available>\"",
9141
9712
  "}",
9142
9713
  "```",
9143
- "Omit `renderer_binary_cid` entirely when no binary CID is exposed by",
9714
+ "Omit `rendererBinaryCid` entirely when no binary CID is exposed by",
9144
9715
  "`moltnet_rendered_pack_get`. Do NOT emit `null` — the field is optional",
9145
9716
  "and absence is the correct representation when unavailable.",
9146
- `Write a signed diary entry (tags: \`judgment\`, \`judge_pack\`, \`rubric:${rubric.rubric_id}\`) capturing the rationale before`,
9717
+ `Write a signed diary entry (tags: \`judgment\`, \`judge_pack\`, \`rubric:${rubric.rubricId}\`) capturing the rationale before`,
9147
9718
  "emitting the JSON."
9148
9719
  ].filter((l) => l !== null).join("\n");
9149
9720
  }
@@ -9154,7 +9725,7 @@ function buildJudgePackPrompt(input, ctx) {
9154
9725
  * wraps `moltnet_pack_render` and emits the receipt.
9155
9726
  */
9156
9727
  function buildRenderPackPrompt(input, ctx) {
9157
- const { pack_id: packId, persist = true, pinned = false } = input;
9728
+ const { packId, persist = true, pinned = false } = input;
9158
9729
  return [
9159
9730
  "# Render Pack Agent",
9160
9731
  "",
@@ -9194,11 +9765,11 @@ function buildRenderPackPrompt(input, ctx) {
9194
9765
  "Write to stdout a JSON object matching `RenderPackOutput`:",
9195
9766
  "```",
9196
9767
  "{",
9197
- " \"rendered_pack_id\": \"<uuid-or-null>\",",
9198
- " \"rendered_cid\": \"<cid>\",",
9199
- " \"render_method\": \"<label>\",",
9200
- " \"byte_size\": <int>,",
9201
- " \"entries_rendered\": <int>,",
9768
+ " \"renderedPackId\": \"<uuid-or-null>\",",
9769
+ " \"renderedCid\": \"<cid>\",",
9770
+ " \"renderMethod\": \"<label>\",",
9771
+ " \"byteSize\": <int>,",
9772
+ " \"entriesRendered\": <int>,",
9202
9773
  " \"summary\": \"<1-3 sentence recap>\"",
9203
9774
  "}",
9204
9775
  "```",
@@ -9208,11 +9779,11 @@ function buildRenderPackPrompt(input, ctx) {
9208
9779
  //#endregion
9209
9780
  //#region ../agent-runtime/src/prompts/index.ts
9210
9781
  /**
9211
- * Resolve the correct prompt builder for `task.task_type` and invoke it.
9782
+ * Resolve the correct prompt builder for `task.taskType` and invoke it.
9212
9783
  * Throws if the type is unknown or the input fails TypeBox validation.
9213
9784
  */
9214
9785
  function buildPromptForTask(task, ctx) {
9215
- switch (task.task_type) {
9786
+ switch (task.taskType) {
9216
9787
  case FULFILL_BRIEF_TYPE:
9217
9788
  if (!Value.Check(FulfillBriefInput, task.input)) {
9218
9789
  const errors = [...Value.Errors(FulfillBriefInput, task.input)];
@@ -9262,9 +9833,101 @@ function buildPromptForTask(task, ctx) {
9262
9833
  diaryId: ctx.diaryId,
9263
9834
  taskId: ctx.taskId
9264
9835
  });
9265
- default: throw new Error(`No prompt builder registered for task_type="${task.task_type}"`);
9836
+ default: throw new Error(`No prompt builder registered for taskType="${task.taskType}"`);
9837
+ }
9838
+ }
9839
+ //#endregion
9840
+ //#region src/runtime/task-output.ts
9841
+ async function parseStructuredTaskOutput(assistantText, taskType) {
9842
+ const extracted = extractJsonObject(assistantText);
9843
+ if (!extracted) return {
9844
+ output: null,
9845
+ outputCid: null,
9846
+ error: {
9847
+ code: "output_missing",
9848
+ message: "Agent did not emit a parseable JSON object as its final message."
9849
+ }
9850
+ };
9851
+ const errors = validateTaskOutput(taskType, extracted);
9852
+ if (errors.length > 0) {
9853
+ const details = errors.slice(0, 3).map((error) => `${error.field}: ${error.message}`);
9854
+ const [firstError] = errors;
9855
+ return {
9856
+ output: null,
9857
+ outputCid: null,
9858
+ error: {
9859
+ code: firstError?.field === "taskType" ? "unknown_task_type" : "output_validation_failed",
9860
+ message: `Output failed schema validation: ${details.join("; ")}`
9861
+ }
9862
+ };
9863
+ }
9864
+ try {
9865
+ return {
9866
+ output: extracted,
9867
+ outputCid: await computeJsonCid(extracted),
9868
+ error: null
9869
+ };
9870
+ } catch (error) {
9871
+ return {
9872
+ output: null,
9873
+ outputCid: null,
9874
+ error: {
9875
+ code: "output_cid_compute_failed",
9876
+ message: `Validated output could not be canonicalized: ${error instanceof Error ? error.message : String(error)}`
9877
+ }
9878
+ };
9266
9879
  }
9267
9880
  }
9881
+ /**
9882
+ * Find the last balanced top-level JSON object in `text` and parse it.
9883
+ * Tolerates markdown fences and leading prose. Returns null if parsing fails.
9884
+ */
9885
+ function extractJsonObject(text) {
9886
+ if (!text) return null;
9887
+ const fenceMatch = /```(?:json)?\s*([\s\S]*?)```/gi;
9888
+ const candidates = [];
9889
+ for (const m of text.matchAll(fenceMatch)) candidates.push(m[1]);
9890
+ const scanForObject = (s) => {
9891
+ let depth = 0;
9892
+ let start = -1;
9893
+ let lastComplete = null;
9894
+ let inString = false;
9895
+ let escape = false;
9896
+ for (let i = 0; i < s.length; i++) {
9897
+ const ch = s[i];
9898
+ if (inString) {
9899
+ if (escape) escape = false;
9900
+ else if (ch === "\\") escape = true;
9901
+ else if (ch === "\"") inString = false;
9902
+ continue;
9903
+ }
9904
+ if (ch === "\"") {
9905
+ inString = true;
9906
+ continue;
9907
+ }
9908
+ if (ch === "{") {
9909
+ if (depth === 0) start = i;
9910
+ depth++;
9911
+ } else if (ch === "}") {
9912
+ depth--;
9913
+ if (depth === 0 && start !== -1) {
9914
+ lastComplete = s.slice(start, i + 1);
9915
+ start = -1;
9916
+ }
9917
+ }
9918
+ }
9919
+ return lastComplete;
9920
+ };
9921
+ candidates.push(text);
9922
+ for (let i = candidates.length - 1; i >= 0; i--) {
9923
+ const obj = scanForObject(candidates[i]);
9924
+ if (!obj) continue;
9925
+ try {
9926
+ return JSON.parse(obj);
9927
+ } catch {}
9928
+ }
9929
+ return null;
9930
+ }
9268
9931
  //#endregion
9269
9932
  //#region src/runtime/execute-pi-task.ts
9270
9933
  /**
@@ -9290,14 +9953,14 @@ function buildPromptForTask(task, ctx) {
9290
9953
  */
9291
9954
  function createPiTaskExecutor(opts) {
9292
9955
  let cachedCheckpoint = opts.checkpointPath ?? null;
9293
- return async (task, reporter) => {
9956
+ return async (claimedTask, reporter) => {
9294
9957
  if (!cachedCheckpoint) cachedCheckpoint = await ensureSnapshot({
9295
9958
  config: opts.sandboxConfig?.snapshot,
9296
9959
  onProgress: opts.onSnapshotProgress ?? ((m) => {
9297
9960
  process.stderr.write(`[snapshot] ${m}\n`);
9298
9961
  })
9299
9962
  });
9300
- return executePiTask(task, reporter, {
9963
+ return executePiTask(claimedTask, reporter, {
9301
9964
  ...opts,
9302
9965
  checkpointPath: cachedCheckpoint
9303
9966
  });
@@ -9309,8 +9972,9 @@ function createPiTaskExecutor(opts) {
9309
9972
  * a `TaskOutput` (failures surface as `status: 'failed'`); throws only on
9310
9973
  * unrecoverable setup errors.
9311
9974
  */
9312
- async function executePiTask(task, reporter, opts) {
9313
- const attemptN = opts.attemptN ?? 1;
9975
+ async function executePiTask(claimedTask, reporter, opts) {
9976
+ const task = claimedTask.task;
9977
+ const attemptN = claimedTask.attemptN;
9314
9978
  const startTime = Date.now();
9315
9979
  const mountPath = opts.mountPath ?? process.cwd();
9316
9980
  const checkpointPath = opts.checkpointPath ?? await ensureSnapshot({
@@ -9336,17 +10000,18 @@ async function executePiTask(task, reporter, opts) {
9336
10000
  extraAllowedHosts: opts.extraAllowedHosts,
9337
10001
  sandboxConfig: opts.sandboxConfig
9338
10002
  });
9339
- const diaryId = task.diary_id ?? "";
10003
+ const diaryId = task.diaryId ?? "";
9340
10004
  let reporterOpen = false;
9341
10005
  let session = null;
9342
- const makeFailedOutput = (code, message, usage = emptyUsage(opts.provider, opts.model)) => ({
9343
- task_id: task.id,
9344
- attempt_n: attemptN,
10006
+ const finalUsage = emptyUsage(opts.provider, opts.model);
10007
+ const makeFailedOutput = (code, message, usage = finalUsage) => ({
10008
+ taskId: task.id,
10009
+ attemptN,
9345
10010
  status: "failed",
9346
10011
  output: null,
9347
- output_cid: null,
10012
+ outputCid: null,
9348
10013
  usage,
9349
- duration_ms: Date.now() - startTime,
10014
+ durationMs: Date.now() - startTime,
9350
10015
  error: {
9351
10016
  code,
9352
10017
  message,
@@ -9367,8 +10032,8 @@ async function executePiTask(task, reporter, opts) {
9367
10032
  });
9368
10033
  await emit("info", {
9369
10034
  event: "execute_start",
9370
- task_type: task.task_type,
9371
- team_id: task.team_id,
10035
+ taskType: task.taskType,
10036
+ teamId: task.teamId,
9372
10037
  provider: opts.provider,
9373
10038
  model: opts.model
9374
10039
  });
@@ -9399,7 +10064,9 @@ async function executePiTask(task, reporter, opts) {
9399
10064
  getAgent: () => moltnetAgent,
9400
10065
  getDiaryId: () => diaryId,
9401
10066
  getSessionErrors: () => [],
9402
- clearSessionErrors: () => {}
10067
+ clearSessionErrors: () => {},
10068
+ getHostCwd: () => mountPath,
10069
+ hostExecBaseEnv: new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(managed.credentials.agentEnv)])
9403
10070
  });
9404
10071
  const piAuthDir = join(homedir(), ".pi", "agent");
9405
10072
  const modelHandle = getModel(opts.provider, opts.model);
@@ -9427,7 +10094,7 @@ async function executePiTask(task, reporter, opts) {
9427
10094
  let llmAbort = false;
9428
10095
  let assistantText = "";
9429
10096
  let reporterError = null;
9430
- const usage = emptyUsage(opts.provider, opts.model);
10097
+ const usage = finalUsage;
9431
10098
  const recordingPromise = [];
9432
10099
  const track = (p) => {
9433
10100
  recordingPromise.push(p.catch((err) => {
@@ -9457,12 +10124,12 @@ async function executePiTask(task, reporter, opts) {
9457
10124
  else if (event.type === "turn_end") {
9458
10125
  const msg = event.message;
9459
10126
  if (msg?.role === "assistant" && msg.usage) {
9460
- usage.input_tokens += Math.max(0, msg.usage.input ?? 0);
9461
- usage.output_tokens += Math.max(0, msg.usage.output ?? 0);
10127
+ usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
10128
+ usage.outputTokens += Math.max(0, msg.usage.output ?? 0);
9462
10129
  const cr = Math.max(0, msg.usage.cacheRead ?? 0);
9463
10130
  const cw = Math.max(0, msg.usage.cacheWrite ?? 0);
9464
- if (cr) usage.cache_read_tokens = (usage.cache_read_tokens ?? 0) + cr;
9465
- if (cw) usage.cache_write_tokens = (usage.cache_write_tokens ?? 0) + cw;
10131
+ if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
10132
+ if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
9466
10133
  }
9467
10134
  track(emit("turn_end", { stop_reason: msg?.stopReason ?? "end_turn" }));
9468
10135
  llmAbort = msg?.stopReason === "error";
@@ -9484,28 +10151,13 @@ async function executePiTask(task, reporter, opts) {
9484
10151
  }
9485
10152
  await Promise.all(recordingPromise);
9486
10153
  let parsedOutput = null;
10154
+ let parsedOutputCid = null;
9487
10155
  let parseError = null;
9488
10156
  if (!runError && !llmAbort) {
9489
- const extracted = extractJsonObject(assistantText);
9490
- if (!extracted) parseError = {
9491
- code: "output_missing",
9492
- message: "Agent did not emit a parseable JSON object as its final message."
9493
- };
9494
- else {
9495
- const entry = BUILT_IN_TASK_TYPES[task.task_type];
9496
- if (!entry) parseError = {
9497
- code: "unknown_task_type",
9498
- message: `No output schema registered for task_type=${task.task_type}`
9499
- };
9500
- else {
9501
- const check = TypeCompiler.Compile(entry.outputSchema);
9502
- if (check.Check(extracted)) parsedOutput = extracted;
9503
- else parseError = {
9504
- code: "output_validation_failed",
9505
- message: `Output failed schema validation: ${[...check.Errors(extracted)].slice(0, 3).map((e) => `${e.path}: ${e.message}`).join("; ")}`
9506
- };
9507
- }
9508
- }
10157
+ const parsed = await parseStructuredTaskOutput(assistantText, task.taskType);
10158
+ parsedOutput = parsed.output;
10159
+ parsedOutputCid = parsed.outputCid;
10160
+ parseError = parsed.error;
9509
10161
  if (parseError) await emit("error", {
9510
10162
  message: parseError.message,
9511
10163
  phase: "output_validation"
@@ -9515,13 +10167,13 @@ async function executePiTask(task, reporter, opts) {
9515
10167
  const errorCode = runError?.code ?? parseError?.code ?? reporterError?.code ?? (llmAbort ? "llm_api_error" : void 0);
9516
10168
  const errorMessage = runError?.message ?? parseError?.message ?? reporterError?.message ?? (llmAbort ? "LLM API error during turn" : void 0);
9517
10169
  return {
9518
- task_id: task.id,
9519
- attempt_n: attemptN,
10170
+ taskId: task.id,
10171
+ attemptN,
9520
10172
  status,
9521
10173
  output: parsedOutput,
9522
- output_cid: null,
10174
+ outputCid: parsedOutputCid,
9523
10175
  usage,
9524
- duration_ms: Date.now() - startTime,
10176
+ durationMs: Date.now() - startTime,
9525
10177
  ...errorCode && errorMessage ? { error: {
9526
10178
  code: errorCode,
9527
10179
  message: errorMessage,
@@ -9536,7 +10188,7 @@ async function executePiTask(task, reporter, opts) {
9536
10188
  } catch {}
9537
10189
  if (reporterOpen) {
9538
10190
  try {
9539
- await reporter.finalize(emptyUsage(opts.provider, opts.model));
10191
+ await reporter.finalize(finalUsage);
9540
10192
  } catch {}
9541
10193
  try {
9542
10194
  await reporter.close();
@@ -9547,8 +10199,8 @@ async function executePiTask(task, reporter, opts) {
9547
10199
  }
9548
10200
  function emptyUsage(provider, model) {
9549
10201
  return {
9550
- input_tokens: 0,
9551
- output_tokens: 0,
10202
+ inputTokens: 0,
10203
+ outputTokens: 0,
9552
10204
  provider,
9553
10205
  model
9554
10206
  };
@@ -9582,79 +10234,16 @@ function truncateForWire(value) {
9582
10234
  };
9583
10235
  }
9584
10236
  }
9585
- /**
9586
- * Find the last balanced top-level JSON object in `text` and parse it.
9587
- * Tolerates markdown fences and leading prose. Returns null if parsing fails.
9588
- */
9589
- function extractJsonObject(text) {
9590
- if (!text) return null;
9591
- const fenceMatch = /```(?:json)?\s*([\s\S]*?)```/gi;
9592
- const candidates = [];
9593
- for (const m of text.matchAll(fenceMatch)) candidates.push(m[1]);
9594
- const scanForObject = (s) => {
9595
- let depth = 0;
9596
- let start = -1;
9597
- let lastComplete = null;
9598
- let inString = false;
9599
- let escape = false;
9600
- for (let i = 0; i < s.length; i++) {
9601
- const ch = s[i];
9602
- if (inString) {
9603
- if (escape) escape = false;
9604
- else if (ch === "\\") escape = true;
9605
- else if (ch === "\"") inString = false;
9606
- continue;
9607
- }
9608
- if (ch === "\"") {
9609
- inString = true;
9610
- continue;
9611
- }
9612
- if (ch === "{") {
9613
- if (depth === 0) start = i;
9614
- depth++;
9615
- } else if (ch === "}") {
9616
- depth--;
9617
- if (depth === 0 && start !== -1) {
9618
- lastComplete = s.slice(start, i + 1);
9619
- start = -1;
9620
- }
9621
- }
9622
- }
9623
- return lastComplete;
9624
- };
9625
- candidates.push(text);
9626
- for (let i = candidates.length - 1; i >= 0; i--) {
9627
- const obj = scanForObject(candidates[i]);
9628
- if (!obj) continue;
9629
- try {
9630
- return JSON.parse(obj);
9631
- } catch {}
9632
- }
9633
- return null;
9634
- }
9635
10237
  //#endregion
9636
10238
  //#region src/index.ts
9637
10239
  /**
9638
10240
  * @themoltnet/pi-extension — MoltNet pi extension
9639
10241
  *
9640
- * Sandboxes tool execution in a Gondolin VM with:
9641
- * - Auto-built and cached VM snapshots
9642
- * - Credential injection (pi OAuth + MoltNet identity)
9643
- * - Egress policy (only LLM provider + MoltNet API)
9644
- * - Tool redirection (read/write/edit/bash → VM)
9645
- * - MoltNet custom tools (diary entries — run on host via SDK)
9646
- * - Optional git worktree per session
10242
+ * Runs pi coding-agent sessions inside a Gondolin VM with the agent's
10243
+ * MoltNet identity fully available inside the sandbox.
9647
10244
  *
9648
- * Usage:
9649
- * pi -e @themoltnet/pi-extension
9650
- * pi -e @themoltnet/pi-extension --agent legreffier
9651
- * pi -e @themoltnet/pi-extension --worktree-branch feat/my-task
9652
- * pi -e @themoltnet/pi-extension --sandbox-config ./sandbox.json
9653
- *
9654
- * Sandbox config resolution (first match):
9655
- * 1. --sandbox-config flag (explicit path to JSON)
9656
- * 2. sandbox.json in cwd (convention)
9657
- * 3. Base only (git, gh, moltnet CLI, agent user)
10245
+ * See README.md for credential injection flow, tool split, sandbox.json
10246
+ * reference, and headless/programmatic usage.
9658
10247
  */
9659
10248
  var GUEST_WORKSPACE = "/workspace";
9660
10249
  function moltnetExtension(pi) {
@@ -9692,6 +10281,7 @@ function moltnetExtension(pi) {
9692
10281
  let worktreePath = null;
9693
10282
  let moltnetAgent = null;
9694
10283
  let diaryId = null;
10284
+ let hostExecBaseEnv = HOST_EXEC_DEFAULT_BASE_ENV;
9695
10285
  async function ensureVm(ctx) {
9696
10286
  if (vm) return vm;
9697
10287
  if (vmStarting) return vmStarting;
@@ -9745,6 +10335,7 @@ function moltnetExtension(pi) {
9745
10335
  activateAgentEnv(managed.credentials.agentEnv, mainRepo);
9746
10336
  moltnetAgent = await connect({ configDir: managed.agentDir });
9747
10337
  diaryId = managed.credentials.agentEnv.MOLTNET_DIARY_ID ?? null;
10338
+ hostExecBaseEnv = new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(managed.credentials.agentEnv)]);
9748
10339
  vm = managed.vm;
9749
10340
  const label = worktreePath ? `${mountPath} → ${GUEST_WORKSPACE}` : `${localCwd} → ${GUEST_WORKSPACE}`;
9750
10341
  ctx?.ui.setStatus("sandbox", ctx.ui.theme.fg("accent", `Sandbox: running (${label})`));
@@ -9807,7 +10398,9 @@ function moltnetExtension(pi) {
9807
10398
  getSessionErrors: () => sessionErrors,
9808
10399
  clearSessionErrors: () => {
9809
10400
  sessionErrors.length = 0;
9810
- }
10401
+ },
10402
+ getHostCwd: () => worktreePath ?? localCwd,
10403
+ hostExecBaseEnv
9811
10404
  });
9812
10405
  for (const tool of moltnetTools) pi.registerTool(tool);
9813
10406
  const sessionStartTime = Date.now();
@@ -9913,4 +10506,4 @@ function moltnetExtension(pi) {
9913
10506
  registerMoltnetReflectCommand(pi, state);
9914
10507
  }
9915
10508
  //#endregion
9916
- export { activateAgentEnv, buildPiJudgeRecipeManifest, computePiJudgeRecipeCid, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiTaskExecutor, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, loadCredentials, resolvePiJudgeRecipeVersions, resumeVm, toGuestPath };
10509
+ export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildPiJudgeRecipeManifest, computePiJudgeRecipeCid, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiTaskExecutor, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, loadCredentials, resolvePiJudgeRecipeVersions, resumeVm, toGuestPath };