@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.
- package/README.md +45 -0
- package/dist/index.js +155 -24
- 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
|
|
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
|
-
|
|
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.
|
|
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/
|
|
35
|
-
"@themoltnet/
|
|
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",
|