@themoltnet/pi-extension 0.16.1 → 0.17.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/README.md +45 -0
- package/dist/index.d.ts +54 -3
- package/dist/index.js +186 -30
- package/package.json +4 -4
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.d.ts
CHANGED
|
@@ -32,9 +32,11 @@ import { WriteOperations } from '@earendil-works/pi-coding-agent';
|
|
|
32
32
|
export declare function activateAgentEnv(agentEnv: Record<string, string | undefined>, repoRoot: string): void;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Construct an
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Construct an `AgentSession`. By default it is in-memory; callers may opt
|
|
36
|
+
* parent sessions into daemon-owned file persistence via `sessionPersistence`.
|
|
37
|
+
* The caller is responsible for eventually invoking `session.prompt(...)` and
|
|
38
|
+
* for tearing down — the helper does no lifecycle management beyond
|
|
39
|
+
* construction.
|
|
38
40
|
*/
|
|
39
41
|
export declare function buildAgentSession(args: BuildAgentSessionArgs): Promise<AgentSession>;
|
|
40
42
|
|
|
@@ -56,6 +58,13 @@ declare interface BuildAgentSessionArgs {
|
|
|
56
58
|
otelSpanAttrs: Record<string, string | number | boolean>;
|
|
57
59
|
/** Agent name for `gen_ai.agent.name` on the root span. */
|
|
58
60
|
agentName: string;
|
|
61
|
+
/**
|
|
62
|
+
* Parent sessions may persist their conversation history in a daemon-owned
|
|
63
|
+
* directory. Subagents should leave this unset and stay in-memory.
|
|
64
|
+
*/
|
|
65
|
+
sessionPersistence?: {
|
|
66
|
+
sessionDir: string;
|
|
67
|
+
};
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
declare interface ClaimedTask {
|
|
@@ -277,6 +286,12 @@ export declare interface ExecutePiTaskOptions {
|
|
|
277
286
|
* after HOST_EXEC_ALLOWED; an array limits auto-approval to matching rules.
|
|
278
287
|
*/
|
|
279
288
|
hostExecAutoApprove?: HostExecAutoApproveConfig;
|
|
289
|
+
/**
|
|
290
|
+
* Optional daemon-supplied execution plan. Keeps task semantics out of
|
|
291
|
+
* `pi-extension` while still letting callers opt into stable worktrees and
|
|
292
|
+
* file-backed Pi sessions for selected task classes.
|
|
293
|
+
*/
|
|
294
|
+
makeExecutionPlan?: PiTaskExecutionPlanFactory;
|
|
280
295
|
}
|
|
281
296
|
|
|
282
297
|
/**
|
|
@@ -415,6 +430,42 @@ export declare interface PiOtelOptions {
|
|
|
415
430
|
spanAttributes?: Record<string, string | number | boolean>;
|
|
416
431
|
}
|
|
417
432
|
|
|
433
|
+
export declare interface PiSessionPersistencePlan {
|
|
434
|
+
sessionDir: string;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export declare interface PiTaskExecutionPlan {
|
|
438
|
+
/**
|
|
439
|
+
* Daemon-local reuse key. When set alongside `workspaceScope: 'session'`,
|
|
440
|
+
* dedicated worktrees may be retained and reopened across related tasks.
|
|
441
|
+
*/
|
|
442
|
+
sessionKey: string | null;
|
|
443
|
+
/**
|
|
444
|
+
* Workspace identity selected by the daemon. `null` means the task should
|
|
445
|
+
* run against the shared mount path.
|
|
446
|
+
*/
|
|
447
|
+
workspaceId: string | null;
|
|
448
|
+
/**
|
|
449
|
+
* Branch to create or reopen for the workspace. `null` means no dedicated
|
|
450
|
+
* worktree is required.
|
|
451
|
+
*/
|
|
452
|
+
worktreeBranch: string | null;
|
|
453
|
+
/**
|
|
454
|
+
* Lifetime of the task workspace from the daemon's point of view.
|
|
455
|
+
* `attempt` = disposable; `session` = keep stable for the reuse key.
|
|
456
|
+
*/
|
|
457
|
+
workspaceScope: 'attempt' | 'session';
|
|
458
|
+
/**
|
|
459
|
+
* Optional location for file-backed Pi session history. When omitted,
|
|
460
|
+
* the executor keeps the conversation in memory for this attempt only.
|
|
461
|
+
*/
|
|
462
|
+
sessionPersistence?: PiSessionPersistencePlan | null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export declare type PiTaskExecutionPlanFactory = (claimedTask: ClaimedTask) => PiTaskExecutionPlan | null;
|
|
466
|
+
|
|
467
|
+
export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
|
|
468
|
+
|
|
418
469
|
/**
|
|
419
470
|
* Resume a VM from a checkpoint, inject credentials, configure egress +
|
|
420
471
|
* TLS. Returns the managed VM handle.
|
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";
|
|
@@ -8633,9 +8633,11 @@ var NO_SKILLS = () => ({
|
|
|
8633
8633
|
diagnostics: []
|
|
8634
8634
|
});
|
|
8635
8635
|
/**
|
|
8636
|
-
* Construct an
|
|
8637
|
-
*
|
|
8638
|
-
*
|
|
8636
|
+
* Construct an `AgentSession`. By default it is in-memory; callers may opt
|
|
8637
|
+
* parent sessions into daemon-owned file persistence via `sessionPersistence`.
|
|
8638
|
+
* The caller is responsible for eventually invoking `session.prompt(...)` and
|
|
8639
|
+
* for tearing down — the helper does no lifecycle management beyond
|
|
8640
|
+
* construction.
|
|
8639
8641
|
*/
|
|
8640
8642
|
async function buildAgentSession(args) {
|
|
8641
8643
|
const piOtelExtension = createPiOtelExtension({
|
|
@@ -8650,15 +8652,23 @@ async function buildAgentSession(args) {
|
|
|
8650
8652
|
skillsOverride: args.skillsOverride ?? NO_SKILLS
|
|
8651
8653
|
});
|
|
8652
8654
|
await resourceLoader.reload();
|
|
8655
|
+
const sessionManager = args.sessionPersistence ? await resolvePersistentSessionManager({
|
|
8656
|
+
cwd: args.mountPath,
|
|
8657
|
+
sessionDir: args.sessionPersistence.sessionDir
|
|
8658
|
+
}) : SessionManager.inMemory(args.mountPath);
|
|
8653
8659
|
return (await createAgentSession({
|
|
8654
8660
|
agentDir: args.piAuthDir,
|
|
8655
8661
|
cwd: args.mountPath,
|
|
8656
8662
|
model: args.modelHandle,
|
|
8657
8663
|
customTools: args.customTools,
|
|
8658
|
-
sessionManager
|
|
8664
|
+
sessionManager,
|
|
8659
8665
|
resourceLoader
|
|
8660
8666
|
})).session;
|
|
8661
8667
|
}
|
|
8668
|
+
async function resolvePersistentSessionManager(args) {
|
|
8669
|
+
await SessionManager.list(args.cwd, args.sessionDir);
|
|
8670
|
+
return SessionManager.continueRecent(args.cwd, args.sessionDir);
|
|
8671
|
+
}
|
|
8662
8672
|
//#endregion
|
|
8663
8673
|
//#region ../tasks/src/formats.ts
|
|
8664
8674
|
/**
|
|
@@ -8824,22 +8834,6 @@ function validateRubricWeights(rubric) {
|
|
|
8824
8834
|
if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
|
|
8825
8835
|
return null;
|
|
8826
8836
|
}
|
|
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
8837
|
//#endregion
|
|
8844
8838
|
//#region ../tasks/src/success-criteria.ts
|
|
8845
8839
|
/**
|
|
@@ -9718,6 +9712,10 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
9718
9712
|
inputSchema: FulfillBriefInput,
|
|
9719
9713
|
outputSchema: FulfillBriefOutput,
|
|
9720
9714
|
outputKind: "artifact",
|
|
9715
|
+
resumable: true,
|
|
9716
|
+
workspaceMode: "dedicated_worktree",
|
|
9717
|
+
workspaceScope: "session",
|
|
9718
|
+
sessionScope: "correlation",
|
|
9721
9719
|
requiresReferences: false,
|
|
9722
9720
|
validateOutput: requireVerificationWhenCriteriaPresent
|
|
9723
9721
|
},
|
|
@@ -9726,6 +9724,9 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
9726
9724
|
inputSchema: AssessBriefInput,
|
|
9727
9725
|
outputSchema: AssessBriefOutput,
|
|
9728
9726
|
outputKind: "judgment",
|
|
9727
|
+
workspaceMode: "dedicated_worktree",
|
|
9728
|
+
workspaceScope: "attempt",
|
|
9729
|
+
sessionScope: "none",
|
|
9729
9730
|
requiresReferences: true,
|
|
9730
9731
|
validateInput: validateJudgmentInput,
|
|
9731
9732
|
validateInputAsync: validateAssessBriefInputAsync
|
|
@@ -9735,6 +9736,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
9735
9736
|
inputSchema: CuratePackInput,
|
|
9736
9737
|
outputSchema: CuratePackOutput,
|
|
9737
9738
|
outputKind: "artifact",
|
|
9739
|
+
workspaceScope: "attempt",
|
|
9740
|
+
sessionScope: "none",
|
|
9738
9741
|
requiresReferences: false,
|
|
9739
9742
|
validateOutput: requireVerificationWhenCriteriaPresent
|
|
9740
9743
|
},
|
|
@@ -9743,6 +9746,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
9743
9746
|
inputSchema: RenderPackInput,
|
|
9744
9747
|
outputSchema: RenderPackOutput,
|
|
9745
9748
|
outputKind: "artifact",
|
|
9749
|
+
workspaceScope: "attempt",
|
|
9750
|
+
sessionScope: "none",
|
|
9746
9751
|
requiresReferences: false,
|
|
9747
9752
|
validateOutput: requireVerificationWhenCriteriaPresent,
|
|
9748
9753
|
validateInputAsync: validateRenderPackInputAsync
|
|
@@ -9752,6 +9757,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
9752
9757
|
inputSchema: JudgePackInput,
|
|
9753
9758
|
outputSchema: JudgePackOutput,
|
|
9754
9759
|
outputKind: "judgment",
|
|
9760
|
+
workspaceScope: "attempt",
|
|
9761
|
+
sessionScope: "none",
|
|
9755
9762
|
requiresReferences: true,
|
|
9756
9763
|
validateInput: validateJudgmentInput,
|
|
9757
9764
|
validateOutput: validateJudgePackOutput,
|
|
@@ -9762,6 +9769,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
9762
9769
|
inputSchema: RunEvalInput,
|
|
9763
9770
|
outputSchema: RunEvalOutput,
|
|
9764
9771
|
outputKind: "artifact",
|
|
9772
|
+
workspaceScope: "attempt",
|
|
9773
|
+
sessionScope: "custom",
|
|
9765
9774
|
requiresReferences: false,
|
|
9766
9775
|
validateOutput: validateRunEvalOutput
|
|
9767
9776
|
},
|
|
@@ -9770,6 +9779,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
9770
9779
|
inputSchema: JudgeEvalVariantInput,
|
|
9771
9780
|
outputSchema: JudgeEvalVariantOutput,
|
|
9772
9781
|
outputKind: "judgment",
|
|
9782
|
+
workspaceScope: "attempt",
|
|
9783
|
+
sessionScope: "custom",
|
|
9773
9784
|
requiresReferences: false,
|
|
9774
9785
|
validateInput: validateJudgeEvalVariantInput,
|
|
9775
9786
|
validateOutput: validateJudgeEvalVariantOutput,
|
|
@@ -10326,6 +10337,15 @@ function buildAssessBriefUserPrompt(input, ctx) {
|
|
|
10326
10337
|
rubric.preamble,
|
|
10327
10338
|
""
|
|
10328
10339
|
].join("\n") : "";
|
|
10340
|
+
const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
|
|
10341
|
+
"### Workspace",
|
|
10342
|
+
"",
|
|
10343
|
+
"This review attempt is running inside a dedicated disposable git",
|
|
10344
|
+
"worktree created for this task. If you need to check out the target",
|
|
10345
|
+
"branch or inspect refs locally, do it only inside this worktree.",
|
|
10346
|
+
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.",
|
|
10347
|
+
""
|
|
10348
|
+
].join("\n") : "";
|
|
10329
10349
|
return [
|
|
10330
10350
|
"# Assess Brief Judge",
|
|
10331
10351
|
"",
|
|
@@ -10366,6 +10386,7 @@ function buildAssessBriefUserPrompt(input, ctx) {
|
|
|
10366
10386
|
" read it from the task you fetched in step 1 and pass",
|
|
10367
10387
|
" `taskFilter: { correlationId: \"<id>\" }`.",
|
|
10368
10388
|
"",
|
|
10389
|
+
workspaceSection,
|
|
10369
10390
|
preambleSection,
|
|
10370
10391
|
"## Criteria",
|
|
10371
10392
|
"",
|
|
@@ -10625,6 +10646,14 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
10625
10646
|
"from this branch naming scheme when correlationId is set.",
|
|
10626
10647
|
""
|
|
10627
10648
|
].join("\n") : "";
|
|
10649
|
+
const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
|
|
10650
|
+
"### Workspace",
|
|
10651
|
+
"",
|
|
10652
|
+
"This attempt is running inside a dedicated git worktree created",
|
|
10653
|
+
"for this task. Do not repurpose or switch the primary checkout.",
|
|
10654
|
+
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.",
|
|
10655
|
+
""
|
|
10656
|
+
].join("\n") : "";
|
|
10628
10657
|
return [
|
|
10629
10658
|
"# Fulfill Brief Agent",
|
|
10630
10659
|
"",
|
|
@@ -10645,9 +10674,10 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
10645
10674
|
criteriaSection,
|
|
10646
10675
|
seedSection,
|
|
10647
10676
|
correlationSection,
|
|
10677
|
+
workspaceSection,
|
|
10648
10678
|
"### Workflow",
|
|
10649
10679
|
"",
|
|
10650
|
-
`1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
|
|
10680
|
+
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
10681
|
"2. Understand the problem — read relevant code; do not speculate.",
|
|
10652
10682
|
"3. Implement the change. Keep commits small and coherent.",
|
|
10653
10683
|
"4. Add tests if applicable.",
|
|
@@ -11045,7 +11075,8 @@ function buildTaskUserPrompt(task, ctx) {
|
|
|
11045
11075
|
return buildFulfillBriefUserPrompt(task.input, {
|
|
11046
11076
|
diaryId: ctx.diaryId,
|
|
11047
11077
|
taskId: ctx.taskId,
|
|
11048
|
-
correlationId: task.correlationId
|
|
11078
|
+
correlationId: task.correlationId,
|
|
11079
|
+
workspace: ctx.workspace
|
|
11049
11080
|
});
|
|
11050
11081
|
case ASSESS_BRIEF_TYPE:
|
|
11051
11082
|
if (!Value.Check(AssessBriefInput, task.input)) {
|
|
@@ -11054,7 +11085,8 @@ function buildTaskUserPrompt(task, ctx) {
|
|
|
11054
11085
|
}
|
|
11055
11086
|
return buildAssessBriefUserPrompt(task.input, {
|
|
11056
11087
|
diaryId: ctx.diaryId,
|
|
11057
|
-
taskId: ctx.taskId
|
|
11088
|
+
taskId: ctx.taskId,
|
|
11089
|
+
workspace: ctx.workspace
|
|
11058
11090
|
});
|
|
11059
11091
|
case CURATE_PACK_TYPE:
|
|
11060
11092
|
if (!Value.Check(CuratePackInput, task.input)) {
|
|
@@ -15182,6 +15214,113 @@ function resolveSubmitTools(taskType, opts = {}) {
|
|
|
15182
15214
|
};
|
|
15183
15215
|
}
|
|
15184
15216
|
//#endregion
|
|
15217
|
+
//#region src/runtime/task-workspace.ts
|
|
15218
|
+
function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
|
|
15219
|
+
const branch = executionPlan?.worktreeBranch ?? null;
|
|
15220
|
+
if (!branch) return {
|
|
15221
|
+
mountPath: requestedMountPath,
|
|
15222
|
+
mode: "shared_mount",
|
|
15223
|
+
branch: null,
|
|
15224
|
+
cleanup: () => {}
|
|
15225
|
+
};
|
|
15226
|
+
const mainRepo = findMainWorktree();
|
|
15227
|
+
const worktreeDir = resolveTaskWorktreePath(mainRepo, executionPlan?.workspaceId ?? `task-${task.id}`);
|
|
15228
|
+
const relMount = relative(mainRepo, requestedMountPath);
|
|
15229
|
+
const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
|
|
15230
|
+
const keepWorkspace = executionPlan?.workspaceScope === "session" && executionPlan.sessionKey !== null;
|
|
15231
|
+
if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch);
|
|
15232
|
+
else {
|
|
15233
|
+
removeExistingTaskWorktree(mainRepo, worktreeDir);
|
|
15234
|
+
addTaskWorktree(mainRepo, worktreeDir, branch);
|
|
15235
|
+
}
|
|
15236
|
+
return {
|
|
15237
|
+
mountPath,
|
|
15238
|
+
mode: "dedicated_worktree",
|
|
15239
|
+
branch,
|
|
15240
|
+
cleanup: keepWorkspace ? () => {} : () => {
|
|
15241
|
+
execFileSync("git", [
|
|
15242
|
+
"-C",
|
|
15243
|
+
mainRepo,
|
|
15244
|
+
"worktree",
|
|
15245
|
+
"remove",
|
|
15246
|
+
"--force",
|
|
15247
|
+
worktreeDir
|
|
15248
|
+
], { stdio: "pipe" });
|
|
15249
|
+
}
|
|
15250
|
+
};
|
|
15251
|
+
}
|
|
15252
|
+
function resolveTaskWorktreePath(mainRepo, workspaceId) {
|
|
15253
|
+
return join(mainRepo, ".worktrees", workspaceId);
|
|
15254
|
+
}
|
|
15255
|
+
function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch) {
|
|
15256
|
+
if (isRegisteredWorktree(mainRepo, worktreeDir)) return;
|
|
15257
|
+
if (existsSync(worktreeDir)) throw new Error(`Expected reusable worktree ${worktreeDir} to be git-managed, but it exists outside git worktree metadata.`);
|
|
15258
|
+
addTaskWorktree(mainRepo, worktreeDir, branch);
|
|
15259
|
+
}
|
|
15260
|
+
function addTaskWorktree(mainRepo, worktreeDir, branch) {
|
|
15261
|
+
const baseRef = resolveWorktreeBaseRef(mainRepo);
|
|
15262
|
+
execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
|
|
15263
|
+
"-C",
|
|
15264
|
+
mainRepo,
|
|
15265
|
+
"worktree",
|
|
15266
|
+
"add",
|
|
15267
|
+
worktreeDir,
|
|
15268
|
+
branch
|
|
15269
|
+
] : [
|
|
15270
|
+
"-C",
|
|
15271
|
+
mainRepo,
|
|
15272
|
+
"worktree",
|
|
15273
|
+
"add",
|
|
15274
|
+
"-b",
|
|
15275
|
+
branch,
|
|
15276
|
+
worktreeDir,
|
|
15277
|
+
baseRef
|
|
15278
|
+
], { stdio: "pipe" });
|
|
15279
|
+
}
|
|
15280
|
+
function removeExistingTaskWorktree(mainRepo, worktreeDir) {
|
|
15281
|
+
if (!existsSync(worktreeDir) || !isRegisteredWorktree(mainRepo, worktreeDir)) return;
|
|
15282
|
+
execFileSync("git", [
|
|
15283
|
+
"-C",
|
|
15284
|
+
mainRepo,
|
|
15285
|
+
"worktree",
|
|
15286
|
+
"remove",
|
|
15287
|
+
"--force",
|
|
15288
|
+
worktreeDir
|
|
15289
|
+
], { stdio: "pipe" });
|
|
15290
|
+
}
|
|
15291
|
+
function isRegisteredWorktree(mainRepo, worktreeDir) {
|
|
15292
|
+
const list = execFileSync("git", [
|
|
15293
|
+
"-C",
|
|
15294
|
+
mainRepo,
|
|
15295
|
+
"worktree",
|
|
15296
|
+
"list",
|
|
15297
|
+
"--porcelain"
|
|
15298
|
+
], {
|
|
15299
|
+
encoding: "utf8",
|
|
15300
|
+
stdio: "pipe"
|
|
15301
|
+
});
|
|
15302
|
+
const marker = `worktree ${worktreeDir}\n`;
|
|
15303
|
+
return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
|
|
15304
|
+
}
|
|
15305
|
+
function resolveWorktreeBaseRef(mainRepo) {
|
|
15306
|
+
return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
|
|
15307
|
+
}
|
|
15308
|
+
function gitRefExists(mainRepo, ref) {
|
|
15309
|
+
try {
|
|
15310
|
+
execFileSync("git", [
|
|
15311
|
+
"-C",
|
|
15312
|
+
mainRepo,
|
|
15313
|
+
"show-ref",
|
|
15314
|
+
"--verify",
|
|
15315
|
+
"--quiet",
|
|
15316
|
+
ref
|
|
15317
|
+
], { stdio: "pipe" });
|
|
15318
|
+
return true;
|
|
15319
|
+
} catch {
|
|
15320
|
+
return false;
|
|
15321
|
+
}
|
|
15322
|
+
}
|
|
15323
|
+
//#endregion
|
|
15185
15324
|
//#region src/runtime/execute-pi-task.ts
|
|
15186
15325
|
/**
|
|
15187
15326
|
* executePiTask — run a single Task attempt using pi-coding-agent inside a
|
|
@@ -15230,7 +15369,10 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15230
15369
|
const task = claimedTask.task;
|
|
15231
15370
|
const attemptN = claimedTask.attemptN;
|
|
15232
15371
|
const startTime = Date.now();
|
|
15233
|
-
const
|
|
15372
|
+
const requestedMountPath = opts.mountPath ?? process.cwd();
|
|
15373
|
+
const executionPlan = opts.makeExecutionPlan?.(claimedTask) ?? null;
|
|
15374
|
+
const workspace = prepareTaskWorkspace(task, requestedMountPath, executionPlan);
|
|
15375
|
+
const mountPath = workspace.mountPath;
|
|
15234
15376
|
if (reporter.cancelSignal.aborted) return {
|
|
15235
15377
|
taskId: task.id,
|
|
15236
15378
|
attemptN,
|
|
@@ -15261,7 +15403,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15261
15403
|
"--relative-paths"
|
|
15262
15404
|
], { stdio: "pipe" });
|
|
15263
15405
|
} catch {}
|
|
15264
|
-
|
|
15406
|
+
let managed = null;
|
|
15407
|
+
managed = await resumeVm({
|
|
15265
15408
|
checkpointPath,
|
|
15266
15409
|
agentName: opts.agentName,
|
|
15267
15410
|
mountPath,
|
|
@@ -15321,13 +15464,19 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15321
15464
|
taskType: task.taskType,
|
|
15322
15465
|
teamId: task.teamId,
|
|
15323
15466
|
provider: opts.provider,
|
|
15324
|
-
model: opts.model
|
|
15467
|
+
model: opts.model,
|
|
15468
|
+
workspaceMode: workspace.mode,
|
|
15469
|
+
workspaceBranch: workspace.branch
|
|
15325
15470
|
});
|
|
15326
15471
|
let taskPrompt;
|
|
15327
15472
|
try {
|
|
15328
15473
|
taskPrompt = buildTaskUserPrompt(task, {
|
|
15329
15474
|
diaryId,
|
|
15330
15475
|
taskId: task.id,
|
|
15476
|
+
workspace: {
|
|
15477
|
+
mode: workspace.mode,
|
|
15478
|
+
branch: workspace.branch
|
|
15479
|
+
},
|
|
15331
15480
|
extras: opts.promptExtras
|
|
15332
15481
|
});
|
|
15333
15482
|
} catch (err) {
|
|
@@ -15438,7 +15587,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15438
15587
|
"moltnet.task.id": task.id,
|
|
15439
15588
|
"moltnet.task.attempt": attemptN,
|
|
15440
15589
|
"moltnet.task.type": task.taskType
|
|
15441
|
-
}
|
|
15590
|
+
},
|
|
15591
|
+
sessionPersistence: executionPlan?.sessionPersistence ?? void 0
|
|
15442
15592
|
});
|
|
15443
15593
|
} catch (err) {
|
|
15444
15594
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -15657,7 +15807,13 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15657
15807
|
console.error(`executePiTask: reporter.close() failed for task ${task.id} attempt ${attemptN}: ${detail}`);
|
|
15658
15808
|
}
|
|
15659
15809
|
}
|
|
15660
|
-
await managed.vm.close();
|
|
15810
|
+
if (managed) await managed.vm.close();
|
|
15811
|
+
try {
|
|
15812
|
+
workspace.cleanup();
|
|
15813
|
+
} catch (err) {
|
|
15814
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
15815
|
+
console.error(`executePiTask: workspace cleanup failed for task ${task.id} attempt ${attemptN}: ${detail}`);
|
|
15816
|
+
}
|
|
15661
15817
|
}
|
|
15662
15818
|
}
|
|
15663
15819
|
function emptyUsage(provider, model) {
|
|
@@ -16039,4 +16195,4 @@ function moltnetExtension(pi) {
|
|
|
16039
16195
|
registerMoltnetReflectCommand(pi, state);
|
|
16040
16196
|
}
|
|
16041
16197
|
//#endregion
|
|
16042
|
-
export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, resumeVm, toGuestPath };
|
|
16198
|
+
export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, resolveTaskWorktreePath, resumeVm, toGuestPath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/pi-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
52
52
|
"@opentelemetry/sdk-metrics": "^2.5.1",
|
|
53
53
|
"@opentelemetry/sdk-trace-base": "^2.5.1",
|
|
54
|
-
"@types/node": "^
|
|
54
|
+
"@types/node": "^22.19.0",
|
|
55
55
|
"typescript": "^5.3.3",
|
|
56
56
|
"vite": "^8.0.0",
|
|
57
57
|
"vite-plugin-dts": "^4.5.4",
|