@themoltnet/agent-daemon 0.10.1 → 0.10.2

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.
Files changed (3) hide show
  1. package/README.md +35 -5
  2. package/dist/main.js +84 -34
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -158,9 +158,14 @@ This matters for evals in particular. `run_eval` tasks declare their intended
158
158
  workspace shape in `input.execution.workspace`: `none` becomes a
159
159
  `scratch_mount`, `shared_mount` uses the daemon mount, and
160
160
  `dedicated_worktree` uses an isolated checkout. Downstream
161
- `judge_eval_attempt` tasks can attach to that exact producer workspace, so any
162
- repo-specific `resumeCommands` that should not run in scratch mode must be
163
- guarded with `when.workspaceMode`.
161
+ `judge_eval_attempt` tasks only resolve against a still-live producer
162
+ session/workspace slot. If the producer slot has already expired and been
163
+ reaped, the judge fails with `producer_context_missing`. When the judge does
164
+ claim in time, it immediately forks the producer session and copies the
165
+ producer workspace into judge-owned scratch state so the running judge no
166
+ longer depends on the producer slot after claim time. Repo-specific
167
+ `resumeCommands` that should not run in scratch mode must still be guarded
168
+ with `when.workspaceMode`.
164
169
 
165
170
  ### 1. Start the local stack
166
171
 
@@ -243,9 +248,32 @@ Leave it running. It idles until a task lands in its queue.
243
248
 
244
249
  ### 4. Create a task
245
250
 
246
- In another terminal, with `.moltnet/local-dev/env` sourced:
251
+ In another terminal, with `.moltnet/local-dev/env` sourced. Pick the CLI
252
+ form (recommended — schema-validates locally, no Node dependency in the
253
+ imposer path) or the template-driven TS form (legacy path that supports
254
+ `{{placeholder}}` substitution via `--set`).
255
+
256
+ ::: code-group
257
+
258
+ ```bash [CLI (recommended)]
259
+ BRIEF="Create a feature branch named feat/smoke-hello, write \
260
+ /workspace/demo/out/hello.txt with the single line 'hi from local-dev', \
261
+ commit the file with a signed diary entry per the runtime instructor, \
262
+ and report the branch name and commit sha in the final \
263
+ FulfillBriefOutput JSON. There is no remote to push to — leave \
264
+ pullRequestUrl null."
265
+
266
+ jq -n --arg brief "$BRIEF" \
267
+ --arg title "Smoke: hello file in a feature branch" \
268
+ '{brief: $brief, title: $title, scopeHint: "feature"}' \
269
+ | moltnet task create \
270
+ --task-type fulfill_brief \
271
+ --team-id "$MOLTNET_TEAM_ID" \
272
+ --diary-id "$MOLTNET_DIARY_ID" \
273
+ --credentials "$PWD/.moltnet/local-dev/moltnet.json"
274
+ ```
247
275
 
248
- ```bash
276
+ ```bash [tsx (legacy)]
249
277
  pnpm exec tsx tools/src/tasks/create-task.ts \
250
278
  --agent local-dev \
251
279
  --task-file examples/tasks/api/fulfill-brief.create.template.json \
@@ -255,6 +283,8 @@ pnpm exec tsx tools/src/tasks/create-task.ts \
255
283
  --set brief="Create a feature branch named feat/smoke-hello, write /workspace/demo/out/hello.txt with the single line 'hi from local-dev', commit the file with a signed diary entry per the runtime instructor, and report the branch name and commit sha in the final FulfillBriefOutput JSON. There is no remote to push to — leave pullRequestUrl null."
256
284
  ```
257
285
 
286
+ :::
287
+
258
288
  > **Why a real coding brief**: `fulfill_brief` requires the agent to emit a
259
289
  > structured `FulfillBriefOutput` JSON
260
290
  > (`{ branch, commits, pullRequestUrl, diaryEntryIds, summary }`) as its
package/dist/main.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import crypto, { createHash } from "crypto";
3
- import path, { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { parseArgs, parseEnv, promisify } from "node:util";
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
5
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
6
6
  import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
7
7
  import { pino, transport } from "pino";
8
8
  import { readFile } from "node:fs/promises";
@@ -4510,7 +4510,8 @@ var PrReviewSubject = Type$2.Object({
4510
4510
  var PrReviewInput = Type$2.Object({
4511
4511
  subject: PrReviewSubject,
4512
4512
  taskPrompt: Type$2.Optional(Type$2.String({ minLength: 1 })),
4513
- successCriteria: SuccessCriteria
4513
+ successCriteria: SuccessCriteria,
4514
+ context: Type$2.Optional(TaskContext)
4514
4515
  }, {
4515
4516
  $id: "PrReviewInput",
4516
4517
  additionalProperties: false
@@ -6794,7 +6795,7 @@ function buildJudgeEvalAttemptUserPrompt(input, ctx) {
6794
6795
  "If the accepted attempt output lists `artifacts[].path`, treat those",
6795
6796
  "paths as relative to the current workspace root unless the output",
6796
6797
  "explicitly says otherwise.",
6797
- ctx.workspace.mode === "dedicated_worktree" ? `This attachment is a dedicated producer worktree${ctx.workspace.branch ? ` on branch \`${ctx.workspace.branch}\`` : ""}.` : ctx.workspace.mode === "scratch_mount" ? "This attachment is the producer scratch workspace mounted with shadow writes for safe inspection." : "This attachment is the producer shared workspace mounted with shadow writes for safe inspection.",
6798
+ ctx.workspace.mode === "dedicated_worktree" ? `This attachment is a dedicated producer worktree${ctx.workspace.branch ? ` on branch \`${ctx.workspace.branch}\`` : ""}.` : ctx.workspace.mode === "scratch_mount" ? "This workspace is a fresh judge-owned scratch copy of the producer workspace." : "This attachment is the producer shared workspace mounted with shadow writes for safe inspection.",
6798
6799
  ""
6799
6800
  ].join("\n") : "";
6800
6801
  return [
@@ -16293,6 +16294,8 @@ function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
16293
16294
  });
16294
16295
  mkdirSync(scratchDir, { recursive: true });
16295
16296
  }
16297
+ const workspaceSeed = executionPlan?.workspaceSeed ?? null;
16298
+ if (workspaceSeed) copyDirectoryContents(workspaceSeed.copyFromPath, scratchDir);
16296
16299
  return {
16297
16300
  mountPath: scratchDir,
16298
16301
  cwdPath: scratchDir,
@@ -16414,6 +16417,68 @@ function gitRefExists(mainRepo, ref) {
16414
16417
  return false;
16415
16418
  }
16416
16419
  }
16420
+ function copyDirectoryContents(sourceDir, targetDir) {
16421
+ if (!existsSync(sourceDir)) throw new Error(`Workspace seed source is missing: ${sourceDir}`);
16422
+ if (existsSync(join(sourceDir, ".git"))) initializeScratchGitRepo(sourceDir, targetDir);
16423
+ const resolvedTargetDir = realpathSync(targetDir);
16424
+ for (const entry of readdirSync(sourceDir)) {
16425
+ const sourceEntry = join(sourceDir, entry);
16426
+ if (shouldSkipSeedEntry(sourceEntry, entry, resolvedTargetDir)) continue;
16427
+ cpSync(sourceEntry, join(targetDir, entry), { recursive: true });
16428
+ }
16429
+ }
16430
+ function initializeScratchGitRepo(sourceDir, targetDir) {
16431
+ execFileSync("git", [
16432
+ "-C",
16433
+ targetDir,
16434
+ "init"
16435
+ ], { stdio: "pipe" });
16436
+ let headCommit = null;
16437
+ try {
16438
+ headCommit = execFileSync("git", [
16439
+ "-C",
16440
+ sourceDir,
16441
+ "rev-parse",
16442
+ "HEAD"
16443
+ ], {
16444
+ encoding: "utf8",
16445
+ stdio: "pipe"
16446
+ }).trim();
16447
+ } catch {
16448
+ headCommit = null;
16449
+ }
16450
+ if (!headCommit) return;
16451
+ execFileSync("git", [
16452
+ "-C",
16453
+ targetDir,
16454
+ "remote",
16455
+ "add",
16456
+ "origin",
16457
+ sourceDir
16458
+ ], { stdio: "pipe" });
16459
+ execFileSync("git", [
16460
+ "-C",
16461
+ targetDir,
16462
+ "fetch",
16463
+ "--quiet",
16464
+ "--depth=1",
16465
+ "origin",
16466
+ headCommit
16467
+ ], { stdio: "pipe" });
16468
+ execFileSync("git", [
16469
+ "-C",
16470
+ targetDir,
16471
+ "checkout",
16472
+ "--quiet",
16473
+ "--detach",
16474
+ "FETCH_HEAD"
16475
+ ], { stdio: "pipe" });
16476
+ }
16477
+ function shouldSkipSeedEntry(sourceEntry, entryName, resolvedTargetDir) {
16478
+ if (entryName === ".git") return true;
16479
+ const resolvedSourceEntry = realpathSync(sourceEntry);
16480
+ return resolvedTargetDir === resolvedSourceEntry || resolvedTargetDir.startsWith(`${resolvedSourceEntry}${sep}`);
16481
+ }
16417
16482
  //#endregion
16418
16483
  //#region ../../libs/pi-extension/src/runtime/execute-pi-task.ts
16419
16484
  /**
@@ -16619,7 +16684,8 @@ async function executePiTask(claimedTask, reporter, opts) {
16619
16684
  workspace: {
16620
16685
  mode: activeWorkspace.mode,
16621
16686
  branch: activeWorkspace.branch,
16622
- attached: executionPlan?.workspaceAttachment !== void 0
16687
+ attached: executionPlan?.workspaceAttachment !== void 0 || executionPlan?.workspaceSeed?.source === "producer",
16688
+ source: executionPlan?.workspaceSeed?.source === "producer" ? "producer_copy" : executionPlan?.workspaceAttachment !== void 0 ? "producer_attachment" : void 0
16623
16689
  },
16624
16690
  extras: opts.promptExtras
16625
16691
  });
@@ -17690,19 +17756,18 @@ function maybeAttachProducerContext(claimedTask, basePlan, stateDirs, slotRegist
17690
17756
  const targetTaskId = typeof claimedTask.task.input.targetTaskId === "string" ? claimedTask.task.input.targetTaskId : null;
17691
17757
  const targetAttemptN = typeof claimedTask.task.input.targetAttemptN === "number" ? claimedTask.task.input.targetAttemptN : null;
17692
17758
  if (!targetTaskId || !targetAttemptN) throw new ProducerContextResolutionError("judge_eval_attempt is missing targetTaskId/targetAttemptN");
17693
- const producer = slotRegistry.findLatestProducerSlotByTaskAttempt(targetTaskId, targetAttemptN);
17694
- if (!producer) throw new ProducerContextResolutionError(`No persisted producer daemon slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
17695
- const sourceSessionPath = resolveProducerSessionPath(producer);
17759
+ const producerContext = slotRegistry.findLatestProducerSlotByTaskAttempt(targetTaskId, targetAttemptN);
17760
+ if (!producerContext) throw new ProducerContextResolutionError(`No live producer daemon slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
17761
+ const sourceSessionPath = resolveProducerSessionPath(producerContext);
17696
17762
  if (!sourceSessionPath) throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
17697
- const attachedWorkspace = resolveProducerWorkspaceAttachment(producer, stateDirs);
17763
+ const copiedWorkspaceSource = resolveProducerWorkspaceCopySource(producerContext, stateDirs);
17698
17764
  return {
17699
17765
  ...basePlan,
17700
- workspaceMode: attachedWorkspace.mode,
17701
- worktreeBranch: attachedWorkspace.branch,
17702
- workspaceAttachment: {
17703
- mountPath: attachedWorkspace.mountPath,
17704
- cwdPath: attachedWorkspace.cwdPath,
17705
- shadowWrites: "tmpfs"
17766
+ workspaceMode: "scratch_mount",
17767
+ worktreeBranch: null,
17768
+ workspaceSeed: {
17769
+ copyFromPath: copiedWorkspaceSource,
17770
+ source: "producer"
17706
17771
  },
17707
17772
  sessionPersistence: {
17708
17773
  sessionDir: `${stateDirs.piSessionsDir}/judge-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`,
@@ -17718,32 +17783,17 @@ function resolveProducerSessionPath(producer) {
17718
17783
  const latest = resolveLatestPiSessionPath(sessionDir);
17719
17784
  return latest && existsSync(latest) ? latest : null;
17720
17785
  }
17721
- function resolveProducerWorkspaceAttachment(producer, stateDirs) {
17786
+ function resolveProducerWorkspaceCopySource(producer, stateDirs) {
17722
17787
  const workspacePath = producer.workspace?.worktreePath ?? null;
17723
17788
  if (workspacePath) {
17724
- if (existsSync(workspacePath)) return {
17725
- mountPath: workspacePath,
17726
- cwdPath: workspacePath,
17727
- mode: producer.workspace?.worktreeBranch ? "dedicated_worktree" : "scratch_mount",
17728
- branch: producer.workspace?.worktreeBranch ?? null
17729
- };
17789
+ if (existsSync(workspacePath)) return workspacePath;
17730
17790
  const recoveredPath = recoverScratchWorkspacePath(producer, stateDirs);
17731
- if (recoveredPath) return {
17732
- mountPath: recoveredPath,
17733
- cwdPath: recoveredPath,
17734
- mode: "scratch_mount",
17735
- branch: null
17736
- };
17791
+ if (recoveredPath) return recoveredPath;
17737
17792
  throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
17738
17793
  }
17739
17794
  const sharedMountRoot = dirname(dirname(stateDirs.rootDir));
17740
17795
  if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
17741
- return {
17742
- mountPath: sharedMountRoot,
17743
- cwdPath: sharedMountRoot,
17744
- mode: "shared_mount",
17745
- branch: null
17746
- };
17796
+ return sharedMountRoot;
17747
17797
  }
17748
17798
  function recoverScratchWorkspacePath(producer, stateDirs) {
17749
17799
  if (producer.workspace?.worktreeBranch) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
@@ -33,8 +33,8 @@
33
33
  "@opentelemetry/semantic-conventions": "^1.39.0",
34
34
  "pino": "^10.3.1",
35
35
  "pino-pretty": "^13.1.3",
36
- "@themoltnet/agent-runtime": "0.18.0",
37
- "@themoltnet/pi-extension": "0.19.1",
36
+ "@themoltnet/agent-runtime": "0.18.1",
37
+ "@themoltnet/pi-extension": "0.19.2",
38
38
  "@themoltnet/sdk": "0.103.0"
39
39
  },
40
40
  "devDependencies": {
@@ -42,10 +42,10 @@
42
42
  "typescript": "^5.3.3",
43
43
  "vite": "^8.0.0",
44
44
  "vitest": "^3.0.0",
45
- "@moltnet/database": "0.1.0",
46
- "@moltnet/tasks": "0.1.0",
47
45
  "@moltnet/bootstrap": "0.1.0",
48
- "@moltnet/crypto-service": "0.1.0"
46
+ "@moltnet/database": "0.1.0",
47
+ "@moltnet/crypto-service": "0.1.0",
48
+ "@moltnet/tasks": "0.1.0"
49
49
  },
50
50
  "nx": {
51
51
  "tags": [