@themoltnet/pi-extension 0.16.1 → 0.16.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 +45 -0
  2. package/dist/index.js +155 -24
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -140,6 +140,7 @@ the base snapshot is used (Alpine + git + gh + MoltNet CLI + agent user).
140
140
  "cpus": 2,
141
141
  "memory": "6G"
142
142
  },
143
+ "resumeCommands": ["corepack enable"],
143
144
  "snapshot": {
144
145
  "allowedHosts": ["unofficial-builds.nodejs.org"],
145
146
  "overlaySize": "8G",
@@ -175,6 +176,25 @@ VM resource limits applied at runtime.
175
176
  | `cpus` | Number of virtual CPUs |
176
177
  | `memory` | RAM limit (e.g. `"6G"`) |
177
178
 
179
+ ### `resumeCommands`
180
+
181
+ Shell commands that run on every VM resume, after platform setup and before the
182
+ agent session starts.
183
+
184
+ Use this for per-session bootstrap that should not invalidate the snapshot
185
+ cache: mounting tmpfs, warming package-manager state, lightweight repo-local
186
+ setup.
187
+
188
+ Important properties:
189
+
190
+ - runs after TLS, DNS, and `git safe.directory` setup
191
+ - not part of the snapshot cache key
192
+ - each command runs in a fresh shell with `set -eu` and `set -o pipefail`
193
+ - first non-zero exit aborts VM resume
194
+
195
+ This split exists so repo-specific bootstrap can live in `sandbox.json` while
196
+ `pi-extension` stays consumer-agnostic.
197
+
178
198
  ### `vfs`
179
199
 
180
200
  VFS shadow configuration — hide host paths from the guest mount.
@@ -187,6 +207,31 @@ VFS shadow configuration — hide host paths from the guest mount.
187
207
  Use `shadow: ["node_modules"]` to hide host binaries (wrong platform) and let
188
208
  the guest install its own with `pnpm install`.
189
209
 
210
+ #### VFS caveat: shadowing is not a pnpm performance fix
211
+
212
+ For this repo we hit two distinct `/workspace` problems:
213
+
214
+ - the FUSE bridge makes file-write-heavy installs much slower than guest-local filesystems
215
+ - the `/workspace` VFS path drops `chmod()` calls, which breaks tools that create files and chmod them later
216
+
217
+ Dogfood trail:
218
+
219
+ - `47b67636-067a-4254-9098-38d00b4867bb` — `/workspace` install path measured at roughly 80x slower than guest tmpfs
220
+ - `62082ec9-0554-4bdc-9c64-9d89ece3fa40` — `chmod()` gap on the workspace mount
221
+ - `17f0ac6f-07f0-4e12-b5e5-d35a0fa2df6c` — first 100x pnpm recipe
222
+ - `2e4e25a9-ef4b-46bf-a55d-6c2b1159ee61` — follow-up fix for per-workspace `node_modules`
223
+
224
+ `vfs.shadow: ["node_modules"]` is still useful to hide host-built artifacts,
225
+ but it does not solve the hot-path problem by itself. For fast pnpm setup, move
226
+ both endpoints off the FUSE bridge:
227
+
228
+ - package store on guest-local disk, e.g. `NPM_CONFIG_STORE_DIR=/opt/pnpm-store`
229
+ - install target on guest tmpfs via `resumeCommands`
230
+
231
+ Current themoltnet `sandbox.json` does this by mounting tmpfs over the root and
232
+ per-workspace `node_modules` directories before running `pnpm install
233
+ --frozen-lockfile`.
234
+
190
235
  ### `env`
191
236
 
192
237
  Environment variable overrides applied to the guest VM. Use this to fix host
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
4
- import path, { join } from "node:path";
4
+ import path, { join, relative } from "node:path";
5
5
  import { DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
6
6
  import { createHash } from "node:crypto";
7
7
  import { readFile } from "node:fs/promises";
@@ -8824,22 +8824,6 @@ function validateRubricWeights(rubric) {
8824
8824
  if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
8825
8825
  return null;
8826
8826
  }
8827
- `
8828
- You are reviewing a GitHub pull request for **complexity** — how hard
8829
- this change is to review safely, NOT whether it's correct or whether
8830
- the feature is worthwhile. The diff has already been opened by the
8831
- producer; your job is to score reviewability.
8832
-
8833
- You may run \`gh pr diff <number>\`, \`gh pr view <number>\`, and read
8834
- files in the workspace. Don't run tests, don't push commits, don't
8835
- modify anything. The PR's GitHub URL is in the target metadata.
8836
-
8837
- When in doubt about a criterion, score conservatively (lower) and
8838
- explain what made the call ambiguous. Reviewers will read your
8839
- rationale; "looks fine" is not useful, "the change touches three
8840
- unrelated subsystems and the test coverage on the auth path is
8841
- unchanged" is.
8842
- `.trim();
8843
8827
  //#endregion
8844
8828
  //#region ../tasks/src/success-criteria.ts
8845
8829
  /**
@@ -9718,6 +9702,7 @@ var BUILT_IN_TASK_TYPES = {
9718
9702
  inputSchema: FulfillBriefInput,
9719
9703
  outputSchema: FulfillBriefOutput,
9720
9704
  outputKind: "artifact",
9705
+ workspaceMode: "dedicated_worktree",
9721
9706
  requiresReferences: false,
9722
9707
  validateOutput: requireVerificationWhenCriteriaPresent
9723
9708
  },
@@ -9726,6 +9711,7 @@ var BUILT_IN_TASK_TYPES = {
9726
9711
  inputSchema: AssessBriefInput,
9727
9712
  outputSchema: AssessBriefOutput,
9728
9713
  outputKind: "judgment",
9714
+ workspaceMode: "dedicated_worktree",
9729
9715
  requiresReferences: true,
9730
9716
  validateInput: validateJudgmentInput,
9731
9717
  validateInputAsync: validateAssessBriefInputAsync
@@ -9839,6 +9825,15 @@ function getTaskOutputSchema(taskType) {
9839
9825
  function taskTypeUsesSubagents(taskType) {
9840
9826
  return getTaskTypeEntry(taskType)?.usesSubagents === true;
9841
9827
  }
9828
+ /**
9829
+ * Filesystem isolation policy requested by the task type.
9830
+ *
9831
+ * Unknown task types and task types without an explicit policy default to the
9832
+ * legacy/shared behaviour.
9833
+ */
9834
+ function taskTypeWorkspaceMode(taskType) {
9835
+ return getTaskTypeEntry(taskType)?.workspaceMode ?? "shared_mount";
9836
+ }
9842
9837
  //#endregion
9843
9838
  //#region ../tasks/src/wire.ts
9844
9839
  /**
@@ -10326,6 +10321,15 @@ function buildAssessBriefUserPrompt(input, ctx) {
10326
10321
  rubric.preamble,
10327
10322
  ""
10328
10323
  ].join("\n") : "";
10324
+ const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
10325
+ "### Workspace",
10326
+ "",
10327
+ "This review attempt is running inside a dedicated disposable git",
10328
+ "worktree created for this task. If you need to check out the target",
10329
+ "branch or inspect refs locally, do it only inside this worktree.",
10330
+ ctx.workspace.branch ? `The current review branch is \`${ctx.workspace.branch}\`. You may replace it with the target branch locally if that helps your inspection.` : "The current checkout is disposable and will be cleaned up when the task ends.",
10331
+ ""
10332
+ ].join("\n") : "";
10329
10333
  return [
10330
10334
  "# Assess Brief Judge",
10331
10335
  "",
@@ -10366,6 +10370,7 @@ function buildAssessBriefUserPrompt(input, ctx) {
10366
10370
  " read it from the task you fetched in step 1 and pass",
10367
10371
  " `taskFilter: { correlationId: \"<id>\" }`.",
10368
10372
  "",
10373
+ workspaceSection,
10369
10374
  preambleSection,
10370
10375
  "## Criteria",
10371
10376
  "",
@@ -10625,6 +10630,14 @@ function buildFulfillBriefUserPrompt(input, ctx) {
10625
10630
  "from this branch naming scheme when correlationId is set.",
10626
10631
  ""
10627
10632
  ].join("\n") : "";
10633
+ const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
10634
+ "### Workspace",
10635
+ "",
10636
+ "This attempt is running inside a dedicated git worktree created",
10637
+ "for this task. Do not repurpose or switch the primary checkout.",
10638
+ ctx.workspace.branch ? `The current branch is \`${ctx.workspace.branch}\`. Stay on this branch unless the runtime instructor explicitly tells you otherwise.` : "Stay on the branch that was pre-provisioned for this task.",
10639
+ ""
10640
+ ].join("\n") : "";
10628
10641
  return [
10629
10642
  "# Fulfill Brief Agent",
10630
10643
  "",
@@ -10645,9 +10658,10 @@ function buildFulfillBriefUserPrompt(input, ctx) {
10645
10658
  criteriaSection,
10646
10659
  seedSection,
10647
10660
  correlationSection,
10661
+ workspaceSection,
10648
10662
  "### Workflow",
10649
10663
  "",
10650
- `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
10664
+ ctx.workspace?.mode === "dedicated_worktree" ? `1. Use the already-provisioned dedicated worktree branch${ctx.workspace.branch ? ` (\`${ctx.workspace.branch}\`)` : ""}; do not create or switch the primary checkout.` : `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
10651
10665
  "2. Understand the problem — read relevant code; do not speculate.",
10652
10666
  "3. Implement the change. Keep commits small and coherent.",
10653
10667
  "4. Add tests if applicable.",
@@ -11045,7 +11059,8 @@ function buildTaskUserPrompt(task, ctx) {
11045
11059
  return buildFulfillBriefUserPrompt(task.input, {
11046
11060
  diaryId: ctx.diaryId,
11047
11061
  taskId: ctx.taskId,
11048
- correlationId: task.correlationId
11062
+ correlationId: task.correlationId,
11063
+ workspace: ctx.workspace
11049
11064
  });
11050
11065
  case ASSESS_BRIEF_TYPE:
11051
11066
  if (!Value.Check(AssessBriefInput, task.input)) {
@@ -11054,7 +11069,8 @@ function buildTaskUserPrompt(task, ctx) {
11054
11069
  }
11055
11070
  return buildAssessBriefUserPrompt(task.input, {
11056
11071
  diaryId: ctx.diaryId,
11057
- taskId: ctx.taskId
11072
+ taskId: ctx.taskId,
11073
+ workspace: ctx.workspace
11058
11074
  });
11059
11075
  case CURATE_PACK_TYPE:
11060
11076
  if (!Value.Check(CuratePackInput, task.input)) {
@@ -15230,7 +15246,8 @@ async function executePiTask(claimedTask, reporter, opts) {
15230
15246
  const task = claimedTask.task;
15231
15247
  const attemptN = claimedTask.attemptN;
15232
15248
  const startTime = Date.now();
15233
- const mountPath = opts.mountPath ?? process.cwd();
15249
+ const workspace = prepareTaskWorkspace(task, opts.mountPath ?? process.cwd());
15250
+ const mountPath = workspace.mountPath;
15234
15251
  if (reporter.cancelSignal.aborted) return {
15235
15252
  taskId: task.id,
15236
15253
  attemptN,
@@ -15261,7 +15278,8 @@ async function executePiTask(claimedTask, reporter, opts) {
15261
15278
  "--relative-paths"
15262
15279
  ], { stdio: "pipe" });
15263
15280
  } catch {}
15264
- const managed = await resumeVm({
15281
+ let managed = null;
15282
+ managed = await resumeVm({
15265
15283
  checkpointPath,
15266
15284
  agentName: opts.agentName,
15267
15285
  mountPath,
@@ -15321,13 +15339,19 @@ async function executePiTask(claimedTask, reporter, opts) {
15321
15339
  taskType: task.taskType,
15322
15340
  teamId: task.teamId,
15323
15341
  provider: opts.provider,
15324
- model: opts.model
15342
+ model: opts.model,
15343
+ workspaceMode: workspace.mode,
15344
+ workspaceBranch: workspace.branch
15325
15345
  });
15326
15346
  let taskPrompt;
15327
15347
  try {
15328
15348
  taskPrompt = buildTaskUserPrompt(task, {
15329
15349
  diaryId,
15330
15350
  taskId: task.id,
15351
+ workspace: {
15352
+ mode: workspace.mode,
15353
+ branch: workspace.branch
15354
+ },
15331
15355
  extras: opts.promptExtras
15332
15356
  });
15333
15357
  } catch (err) {
@@ -15657,7 +15681,114 @@ async function executePiTask(claimedTask, reporter, opts) {
15657
15681
  console.error(`executePiTask: reporter.close() failed for task ${task.id} attempt ${attemptN}: ${detail}`);
15658
15682
  }
15659
15683
  }
15660
- await managed.vm.close();
15684
+ if (managed) await managed.vm.close();
15685
+ try {
15686
+ workspace.cleanup();
15687
+ } catch (err) {
15688
+ const detail = err instanceof Error ? err.message : String(err);
15689
+ console.error(`executePiTask: workspace cleanup failed for task ${task.id} attempt ${attemptN}: ${detail}`);
15690
+ }
15691
+ }
15692
+ }
15693
+ function resolveTaskWorktreeBranch(task) {
15694
+ if (taskTypeWorkspaceMode(task.taskType) !== "dedicated_worktree") return null;
15695
+ if (task.taskType === "fulfill_brief") {
15696
+ const input = task.input;
15697
+ const slug = slugifyBranchComponent(typeof input.title === "string" && input.title.trim().length > 0 ? input.title : typeof input.brief === "string" && input.brief.trim().length > 0 ? input.brief : task.taskType) || "task";
15698
+ if (task.correlationId) return `moltnet/${task.correlationId}/${slug}`;
15699
+ return `feat/${(typeof input.scopeHint === "string" && input.scopeHint.trim().length > 0 ? slugifyBranchComponent(input.scopeHint) : "task") || "task"}-${slug}`;
15700
+ }
15701
+ return `task/${slugifyBranchComponent(task.taskType) || "task"}-${task.id.slice(0, 8)}`;
15702
+ }
15703
+ function slugifyBranchComponent(input) {
15704
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
15705
+ }
15706
+ function prepareTaskWorkspace(task, requestedMountPath) {
15707
+ const branch = resolveTaskWorktreeBranch(task);
15708
+ if (!branch) return {
15709
+ mountPath: requestedMountPath,
15710
+ mode: "shared_mount",
15711
+ branch: null,
15712
+ cleanup: () => {}
15713
+ };
15714
+ const mainRepo = findMainWorktree();
15715
+ const worktreeDir = join(mainRepo, ".worktrees", `task-${task.id}`);
15716
+ removeExistingTaskWorktree(mainRepo, worktreeDir);
15717
+ const relMount = relative(mainRepo, requestedMountPath);
15718
+ const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
15719
+ const baseRef = resolveWorktreeBaseRef(mainRepo);
15720
+ execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
15721
+ "-C",
15722
+ mainRepo,
15723
+ "worktree",
15724
+ "add",
15725
+ worktreeDir,
15726
+ branch
15727
+ ] : [
15728
+ "-C",
15729
+ mainRepo,
15730
+ "worktree",
15731
+ "add",
15732
+ "-b",
15733
+ branch,
15734
+ worktreeDir,
15735
+ baseRef
15736
+ ], { stdio: "pipe" });
15737
+ return {
15738
+ mountPath,
15739
+ mode: "dedicated_worktree",
15740
+ branch,
15741
+ cleanup: () => {
15742
+ execFileSync("git", [
15743
+ "-C",
15744
+ mainRepo,
15745
+ "worktree",
15746
+ "remove",
15747
+ "--force",
15748
+ worktreeDir
15749
+ ], { stdio: "pipe" });
15750
+ }
15751
+ };
15752
+ }
15753
+ function removeExistingTaskWorktree(mainRepo, worktreeDir) {
15754
+ if (!existsSync(worktreeDir)) return;
15755
+ const list = execFileSync("git", [
15756
+ "-C",
15757
+ mainRepo,
15758
+ "worktree",
15759
+ "list",
15760
+ "--porcelain"
15761
+ ], {
15762
+ encoding: "utf8",
15763
+ stdio: "pipe"
15764
+ });
15765
+ const marker = `worktree ${worktreeDir}\n`;
15766
+ if (!list.includes(marker) && !list.endsWith(`worktree ${worktreeDir}`)) return;
15767
+ execFileSync("git", [
15768
+ "-C",
15769
+ mainRepo,
15770
+ "worktree",
15771
+ "remove",
15772
+ "--force",
15773
+ worktreeDir
15774
+ ], { stdio: "pipe" });
15775
+ }
15776
+ function resolveWorktreeBaseRef(mainRepo) {
15777
+ return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
15778
+ }
15779
+ function gitRefExists(mainRepo, ref) {
15780
+ try {
15781
+ execFileSync("git", [
15782
+ "-C",
15783
+ mainRepo,
15784
+ "show-ref",
15785
+ "--verify",
15786
+ "--quiet",
15787
+ ref
15788
+ ], { stdio: "pipe" });
15789
+ return true;
15790
+ } catch {
15791
+ return false;
15661
15792
  }
15662
15793
  }
15663
15794
  function emptyUsage(provider, model) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "type": "module",
5
5
  "description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
6
6
  "license": "MIT",
@@ -31,8 +31,8 @@
31
31
  "@earendil-works/gondolin": "^0.9.1",
32
32
  "@opentelemetry/api": "^1.9.0",
33
33
  "@sinclair/typebox": "^0.34.0",
34
- "@themoltnet/agent-runtime": "0.15.0",
35
- "@themoltnet/sdk": "0.102.0"
34
+ "@themoltnet/sdk": "0.102.0",
35
+ "@themoltnet/agent-runtime": "0.15.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": ">=0.74.0",