@themoltnet/agent-daemon 0.6.1 → 0.6.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 +191 -0
- package/dist/main.js +155 -24
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -105,6 +105,197 @@ If any of the GitHub-side writes fails (rate limit, missing `gh`, network
|
|
|
105
105
|
blip, …) the daemon logs and continues — the other anchors are
|
|
106
106
|
independent and at least one usually survives.
|
|
107
107
|
|
|
108
|
+
## Local development & smoke testing
|
|
109
|
+
|
|
110
|
+
End-to-end smoke test of the daemon against a local Docker stack. Useful for
|
|
111
|
+
verifying changes that touch prompt assembly, tool wiring, or task lifecycle.
|
|
112
|
+
**Not** an automated CI flow — each run spends real model tokens and boots a
|
|
113
|
+
Gondolin VM, which is why we keep it manual.
|
|
114
|
+
|
|
115
|
+
### Prerequisites
|
|
116
|
+
|
|
117
|
+
- Docker running.
|
|
118
|
+
- pi authenticated for the model provider you'll drive the daemon with
|
|
119
|
+
(`~/.pi/agent/auth.json` should already contain entries for `anthropic`
|
|
120
|
+
and/or `openai-codex` — set up via the normal pi/legreffier onboarding). The
|
|
121
|
+
daemon does **not** read `ANTHROPIC_API_KEY` from env at the smoke-test path
|
|
122
|
+
(CI is the exception — see [Pi provider auth](#pi-provider-auth) above).
|
|
123
|
+
- `ssh-keygen` on `PATH`.
|
|
124
|
+
- A `sandbox.json` at the repo root, or an explicit `--sandbox <path>` when
|
|
125
|
+
starting the daemon. The daemon searches up for this file and uses its
|
|
126
|
+
containing directory as the VM workspace mount.
|
|
127
|
+
|
|
128
|
+
For `themoltnet`, prefer the checked-in repo `sandbox.json` as-is — it carries
|
|
129
|
+
the current pnpm/VFS workaround. A minimal `sandbox.json` for another repo:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"hostExec": {
|
|
134
|
+
"autoApprove": [
|
|
135
|
+
{
|
|
136
|
+
"argsExcludes": ["--mirror", "--all", "--tags"],
|
|
137
|
+
"argsPrefix": ["push"],
|
|
138
|
+
"executable": "git"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
That's only a starting point. `vfs.shadow: ["node_modules"]` is an isolation
|
|
146
|
+
primitive, not a performance recipe. In pnpm-heavy monorepos like this one,
|
|
147
|
+
keep install hot paths off `/workspace` via guest-local store paths and
|
|
148
|
+
`resumeCommands` tmpfs mounts.
|
|
149
|
+
|
|
150
|
+
### 1. Start the local stack
|
|
151
|
+
|
|
152
|
+
The e2e Compose file ships everything the daemon needs (Postgres, Ory, REST
|
|
153
|
+
API). Run from the **main repo root** — `docker compose` looks for
|
|
154
|
+
`.env.local` next to the compose file:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cd <repo-root>
|
|
158
|
+
COMPOSE_DISABLE_ENV_FILE=true \
|
|
159
|
+
docker compose -f docker-compose.e2e.yaml up -d --build
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The REST API binds to **port 8080** (not 8000):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
docker compose -f docker-compose.e2e.yaml ps rest-api
|
|
166
|
+
# ... 0.0.0.0:8080->8080/tcp ...
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
There is no `/_health` route mounted currently; once the container shows
|
|
170
|
+
`(healthy)` per `docker compose ps`, move on.
|
|
171
|
+
|
|
172
|
+
### 2. Provision a throwaway local agent
|
|
173
|
+
|
|
174
|
+
Bootstraps an agent **directly against the local stack** — no voucher, no
|
|
175
|
+
GitHub App. Writes `.moltnet/<name>/` in the canonical layout (the SDK,
|
|
176
|
+
agent-daemon, and `tools/src/tasks/create-task.ts` all consume the same
|
|
177
|
+
files).
|
|
178
|
+
|
|
179
|
+
> Run this from the worktree (or repo) where you want `.moltnet/<name>/` to
|
|
180
|
+
> live — that's also where you'll run the daemon.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Source the local env so DATABASE_URL and ORY_*_URL are available.
|
|
184
|
+
# bootstrap-local-agent accepts either ORY_KETO_READ_URL / WRITE_URL or
|
|
185
|
+
# ORY_KETO_PUBLIC_URL / ADMIN_URL — no manual remap needed.
|
|
186
|
+
set -a; source <repo-root>/.env.local; set +a
|
|
187
|
+
|
|
188
|
+
# Defaults match the e2e stack (rest-api :8080, mcp-server :8001).
|
|
189
|
+
pnpm exec tsx tools/src/tasks/bootstrap-local-agent.ts --name local-dev
|
|
190
|
+
|
|
191
|
+
# Convenience: source the generated env file.
|
|
192
|
+
source .moltnet/local-dev/env
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The script prints a JSON summary including the agent's identity, team id,
|
|
196
|
+
and private diary id.
|
|
197
|
+
|
|
198
|
+
> The bootstrapped agent has no GitHub App. That's fine for any task that
|
|
199
|
+
> doesn't touch `gh`. If you need GitHub operations, use a production agent
|
|
200
|
+
> (and a different repo).
|
|
201
|
+
|
|
202
|
+
### 3. Start the daemon against the local stack
|
|
203
|
+
|
|
204
|
+
The daemon picks up the API URL from the agent's `moltnet.json`. It is a
|
|
205
|
+
workspace package, not a global CLI — invoke it via `pnpm --filter`:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pnpm --filter @themoltnet/agent-daemon dev poll \
|
|
209
|
+
--agent local-dev \
|
|
210
|
+
--team "$MOLTNET_TEAM_ID" \
|
|
211
|
+
--task-types fulfill_brief \
|
|
212
|
+
--provider openai-codex \
|
|
213
|
+
--model gpt-5.4-codex \
|
|
214
|
+
--debug
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- If you're starting from a directory without `sandbox.json` at or above it,
|
|
218
|
+
pass `--sandbox <repo-root>/sandbox.json`.
|
|
219
|
+
- `--task-types fulfill_brief` scopes the queue. Omit to accept any
|
|
220
|
+
registered type.
|
|
221
|
+
- Pick provider/model that matches your pi auth credits. Common choices:
|
|
222
|
+
`--provider openai-codex --model gpt-5.4-codex`, or
|
|
223
|
+
`--provider anthropic --model claude-sonnet-4-6`.
|
|
224
|
+
- `dev` (= `tsx watch src/main.ts`) is fine for local. Use `cli` for a
|
|
225
|
+
one-shot run without watch.
|
|
226
|
+
|
|
227
|
+
Leave it running. It idles until a task lands in its queue.
|
|
228
|
+
|
|
229
|
+
### 4. Create a task
|
|
230
|
+
|
|
231
|
+
In another terminal, with `.moltnet/local-dev/env` sourced:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
pnpm exec tsx tools/src/tasks/create-task.ts \
|
|
235
|
+
--agent local-dev \
|
|
236
|
+
--task-file examples/tasks/api/fulfill-brief.create.template.json \
|
|
237
|
+
--set diaryId="$MOLTNET_DIARY_ID" \
|
|
238
|
+
--set teamId="$MOLTNET_TEAM_ID" \
|
|
239
|
+
--set title="Smoke: hello file in a feature branch" \
|
|
240
|
+
--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."
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
> **Why a real coding brief**: `fulfill_brief` requires the agent to emit a
|
|
244
|
+
> structured `FulfillBriefOutput` JSON
|
|
245
|
+
> (`{ branch, commits, pullRequestUrl, diaryEntryIds, summary }`) as its
|
|
246
|
+
> final message. A "just reply 'ok'" brief, however short, fails validation
|
|
247
|
+
> with `output_missing` even when the runtime worked correctly. Pick a task
|
|
248
|
+
> that fits the shape.
|
|
249
|
+
|
|
250
|
+
Watch the daemon logs and the diary:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
moltnet entry list --diary-id "$MOLTNET_DIARY_ID" --limit 10 \
|
|
254
|
+
--credentials "$PWD/.moltnet/local-dev/moltnet.json"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### What to verify
|
|
258
|
+
|
|
259
|
+
After the task completes, every entry produced **during the attempt** should:
|
|
260
|
+
|
|
261
|
+
- Live in `task.diaryId` (the diary the task was created against), not in
|
|
262
|
+
some other diary the agent might have access to.
|
|
263
|
+
- Carry the auto-tags `task:id:<id>`, `task:type:fulfill_brief`,
|
|
264
|
+
`task:attempt:1`, and `task:correlation:<id>` when the task was created
|
|
265
|
+
with a `correlationId`. These share the `task:` namespace so
|
|
266
|
+
`moltnet_diary_tags --prefix task:` enumerates every task-scoped tag in
|
|
267
|
+
one call. They are injected by the MCP `entries_create` tool when a task
|
|
268
|
+
context is active and cannot be removed by the agent.
|
|
269
|
+
|
|
270
|
+
### Cleanup
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Stop the daemon (Ctrl+C).
|
|
274
|
+
|
|
275
|
+
# Tear down the stack and discard the database.
|
|
276
|
+
COMPOSE_DISABLE_ENV_FILE=true \
|
|
277
|
+
docker compose -f docker-compose.e2e.yaml down -v
|
|
278
|
+
|
|
279
|
+
# Drop the local agent dir if you don't need it again.
|
|
280
|
+
rm -rf .moltnet/local-dev
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Re-running
|
|
284
|
+
|
|
285
|
+
`bootstrap-local-agent` refuses to overwrite an existing agent dir. Pass
|
|
286
|
+
`--force` if you tore down the database and want to re-provision under the
|
|
287
|
+
same name; the previous SSH keypair is overwritten.
|
|
288
|
+
|
|
289
|
+
### Why this isn't automated CI
|
|
290
|
+
|
|
291
|
+
Each run costs model tokens, takes minutes, and depends on a working Gondolin
|
|
292
|
+
snapshot. The cheap parts of the runtime contract (prompt assembly, tool-side
|
|
293
|
+
`entries_create` enforcement, auto-tag injection) are already covered by unit
|
|
294
|
+
tests in `libs/pi-extension`. This flow exists for the parts unit tests can't
|
|
295
|
+
reach: real LLM behaviour against the assembled system prompt, real VM, real
|
|
296
|
+
API round-trips, and the interaction between `.moltnet/<agent>/` identity
|
|
297
|
+
material and the active `sandbox.json`.
|
|
298
|
+
|
|
108
299
|
## License
|
|
109
300
|
|
|
110
301
|
AGPL-3.0-only.
|
package/dist/main.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } fr
|
|
|
6
6
|
import { pino, transport } from "pino";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { createHash as createHash$1 } from "node:crypto";
|
|
9
|
-
import path, { dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
|
+
import path, { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { execFile, execFileSync } from "node:child_process";
|
|
12
12
|
import { DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createEditToolDefinition, createReadToolDefinition, createSyntheticSourceInfo, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
@@ -3037,22 +3037,6 @@ function validateRubricWeights(rubric) {
|
|
|
3037
3037
|
if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
|
|
3038
3038
|
return null;
|
|
3039
3039
|
}
|
|
3040
|
-
`
|
|
3041
|
-
You are reviewing a GitHub pull request for **complexity** — how hard
|
|
3042
|
-
this change is to review safely, NOT whether it's correct or whether
|
|
3043
|
-
the feature is worthwhile. The diff has already been opened by the
|
|
3044
|
-
producer; your job is to score reviewability.
|
|
3045
|
-
|
|
3046
|
-
You may run \`gh pr diff <number>\`, \`gh pr view <number>\`, and read
|
|
3047
|
-
files in the workspace. Don't run tests, don't push commits, don't
|
|
3048
|
-
modify anything. The PR's GitHub URL is in the target metadata.
|
|
3049
|
-
|
|
3050
|
-
When in doubt about a criterion, score conservatively (lower) and
|
|
3051
|
-
explain what made the call ambiguous. Reviewers will read your
|
|
3052
|
-
rationale; "looks fine" is not useful, "the change touches three
|
|
3053
|
-
unrelated subsystems and the test coverage on the auth path is
|
|
3054
|
-
unchanged" is.
|
|
3055
|
-
`.trim();
|
|
3056
3040
|
//#endregion
|
|
3057
3041
|
//#region ../../libs/tasks/src/success-criteria.ts
|
|
3058
3042
|
/**
|
|
@@ -4772,6 +4756,7 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4772
4756
|
inputSchema: FulfillBriefInput,
|
|
4773
4757
|
outputSchema: FulfillBriefOutput,
|
|
4774
4758
|
outputKind: "artifact",
|
|
4759
|
+
workspaceMode: "dedicated_worktree",
|
|
4775
4760
|
requiresReferences: false,
|
|
4776
4761
|
validateOutput: requireVerificationWhenCriteriaPresent
|
|
4777
4762
|
},
|
|
@@ -4780,6 +4765,7 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4780
4765
|
inputSchema: AssessBriefInput,
|
|
4781
4766
|
outputSchema: AssessBriefOutput,
|
|
4782
4767
|
outputKind: "judgment",
|
|
4768
|
+
workspaceMode: "dedicated_worktree",
|
|
4783
4769
|
requiresReferences: true,
|
|
4784
4770
|
validateInput: validateJudgmentInput,
|
|
4785
4771
|
validateInputAsync: validateAssessBriefInputAsync
|
|
@@ -5737,6 +5723,15 @@ function getTaskOutputSchema(taskType) {
|
|
|
5737
5723
|
function taskTypeUsesSubagents(taskType) {
|
|
5738
5724
|
return getTaskTypeEntry(taskType)?.usesSubagents === true;
|
|
5739
5725
|
}
|
|
5726
|
+
/**
|
|
5727
|
+
* Filesystem isolation policy requested by the task type.
|
|
5728
|
+
*
|
|
5729
|
+
* Unknown task types and task types without an explicit policy default to the
|
|
5730
|
+
* legacy/shared behaviour.
|
|
5731
|
+
*/
|
|
5732
|
+
function taskTypeWorkspaceMode(taskType) {
|
|
5733
|
+
return getTaskTypeEntry(taskType)?.workspaceMode ?? "shared_mount";
|
|
5734
|
+
}
|
|
5740
5735
|
//#endregion
|
|
5741
5736
|
//#region ../../libs/tasks/src/wire.ts
|
|
5742
5737
|
/**
|
|
@@ -6362,6 +6357,15 @@ function buildAssessBriefUserPrompt(input, ctx) {
|
|
|
6362
6357
|
rubric.preamble,
|
|
6363
6358
|
""
|
|
6364
6359
|
].join("\n") : "";
|
|
6360
|
+
const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
|
|
6361
|
+
"### Workspace",
|
|
6362
|
+
"",
|
|
6363
|
+
"This review attempt is running inside a dedicated disposable git",
|
|
6364
|
+
"worktree created for this task. If you need to check out the target",
|
|
6365
|
+
"branch or inspect refs locally, do it only inside this worktree.",
|
|
6366
|
+
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.",
|
|
6367
|
+
""
|
|
6368
|
+
].join("\n") : "";
|
|
6365
6369
|
return [
|
|
6366
6370
|
"# Assess Brief Judge",
|
|
6367
6371
|
"",
|
|
@@ -6402,6 +6406,7 @@ function buildAssessBriefUserPrompt(input, ctx) {
|
|
|
6402
6406
|
" read it from the task you fetched in step 1 and pass",
|
|
6403
6407
|
" `taskFilter: { correlationId: \"<id>\" }`.",
|
|
6404
6408
|
"",
|
|
6409
|
+
workspaceSection,
|
|
6405
6410
|
preambleSection,
|
|
6406
6411
|
"## Criteria",
|
|
6407
6412
|
"",
|
|
@@ -6661,6 +6666,14 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
6661
6666
|
"from this branch naming scheme when correlationId is set.",
|
|
6662
6667
|
""
|
|
6663
6668
|
].join("\n") : "";
|
|
6669
|
+
const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
|
|
6670
|
+
"### Workspace",
|
|
6671
|
+
"",
|
|
6672
|
+
"This attempt is running inside a dedicated git worktree created",
|
|
6673
|
+
"for this task. Do not repurpose or switch the primary checkout.",
|
|
6674
|
+
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.",
|
|
6675
|
+
""
|
|
6676
|
+
].join("\n") : "";
|
|
6664
6677
|
return [
|
|
6665
6678
|
"# Fulfill Brief Agent",
|
|
6666
6679
|
"",
|
|
@@ -6681,9 +6694,10 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
6681
6694
|
criteriaSection,
|
|
6682
6695
|
seedSection,
|
|
6683
6696
|
correlationSection,
|
|
6697
|
+
workspaceSection,
|
|
6684
6698
|
"### Workflow",
|
|
6685
6699
|
"",
|
|
6686
|
-
`1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
|
|
6700
|
+
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>\`).`,
|
|
6687
6701
|
"2. Understand the problem — read relevant code; do not speculate.",
|
|
6688
6702
|
"3. Implement the change. Keep commits small and coherent.",
|
|
6689
6703
|
"4. Add tests if applicable.",
|
|
@@ -7081,7 +7095,8 @@ function buildTaskUserPrompt(task, ctx) {
|
|
|
7081
7095
|
return buildFulfillBriefUserPrompt(task.input, {
|
|
7082
7096
|
diaryId: ctx.diaryId,
|
|
7083
7097
|
taskId: ctx.taskId,
|
|
7084
|
-
correlationId: task.correlationId
|
|
7098
|
+
correlationId: task.correlationId,
|
|
7099
|
+
workspace: ctx.workspace
|
|
7085
7100
|
});
|
|
7086
7101
|
case ASSESS_BRIEF_TYPE:
|
|
7087
7102
|
if (!Check(AssessBriefInput, task.input)) {
|
|
@@ -7090,7 +7105,8 @@ function buildTaskUserPrompt(task, ctx) {
|
|
|
7090
7105
|
}
|
|
7091
7106
|
return buildAssessBriefUserPrompt(task.input, {
|
|
7092
7107
|
diaryId: ctx.diaryId,
|
|
7093
|
-
taskId: ctx.taskId
|
|
7108
|
+
taskId: ctx.taskId,
|
|
7109
|
+
workspace: ctx.workspace
|
|
7094
7110
|
});
|
|
7095
7111
|
case CURATE_PACK_TYPE:
|
|
7096
7112
|
if (!Check(CuratePackInput, task.input)) {
|
|
@@ -15997,7 +16013,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15997
16013
|
const task = claimedTask.task;
|
|
15998
16014
|
const attemptN = claimedTask.attemptN;
|
|
15999
16015
|
const startTime = Date.now();
|
|
16000
|
-
const
|
|
16016
|
+
const workspace = prepareTaskWorkspace(task, opts.mountPath ?? process.cwd());
|
|
16017
|
+
const mountPath = workspace.mountPath;
|
|
16001
16018
|
if (reporter.cancelSignal.aborted) return {
|
|
16002
16019
|
taskId: task.id,
|
|
16003
16020
|
attemptN,
|
|
@@ -16028,7 +16045,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16028
16045
|
"--relative-paths"
|
|
16029
16046
|
], { stdio: "pipe" });
|
|
16030
16047
|
} catch {}
|
|
16031
|
-
|
|
16048
|
+
let managed = null;
|
|
16049
|
+
managed = await resumeVm({
|
|
16032
16050
|
checkpointPath,
|
|
16033
16051
|
agentName: opts.agentName,
|
|
16034
16052
|
mountPath,
|
|
@@ -16088,13 +16106,19 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16088
16106
|
taskType: task.taskType,
|
|
16089
16107
|
teamId: task.teamId,
|
|
16090
16108
|
provider: opts.provider,
|
|
16091
|
-
model: opts.model
|
|
16109
|
+
model: opts.model,
|
|
16110
|
+
workspaceMode: workspace.mode,
|
|
16111
|
+
workspaceBranch: workspace.branch
|
|
16092
16112
|
});
|
|
16093
16113
|
let taskPrompt;
|
|
16094
16114
|
try {
|
|
16095
16115
|
taskPrompt = buildTaskUserPrompt(task, {
|
|
16096
16116
|
diaryId,
|
|
16097
16117
|
taskId: task.id,
|
|
16118
|
+
workspace: {
|
|
16119
|
+
mode: workspace.mode,
|
|
16120
|
+
branch: workspace.branch
|
|
16121
|
+
},
|
|
16098
16122
|
extras: opts.promptExtras
|
|
16099
16123
|
});
|
|
16100
16124
|
} catch (err) {
|
|
@@ -16424,7 +16448,114 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16424
16448
|
console.error(`executePiTask: reporter.close() failed for task ${task.id} attempt ${attemptN}: ${detail}`);
|
|
16425
16449
|
}
|
|
16426
16450
|
}
|
|
16427
|
-
await managed.vm.close();
|
|
16451
|
+
if (managed) await managed.vm.close();
|
|
16452
|
+
try {
|
|
16453
|
+
workspace.cleanup();
|
|
16454
|
+
} catch (err) {
|
|
16455
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
16456
|
+
console.error(`executePiTask: workspace cleanup failed for task ${task.id} attempt ${attemptN}: ${detail}`);
|
|
16457
|
+
}
|
|
16458
|
+
}
|
|
16459
|
+
}
|
|
16460
|
+
function resolveTaskWorktreeBranch(task) {
|
|
16461
|
+
if (taskTypeWorkspaceMode(task.taskType) !== "dedicated_worktree") return null;
|
|
16462
|
+
if (task.taskType === "fulfill_brief") {
|
|
16463
|
+
const input = task.input;
|
|
16464
|
+
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";
|
|
16465
|
+
if (task.correlationId) return `moltnet/${task.correlationId}/${slug}`;
|
|
16466
|
+
return `feat/${(typeof input.scopeHint === "string" && input.scopeHint.trim().length > 0 ? slugifyBranchComponent(input.scopeHint) : "task") || "task"}-${slug}`;
|
|
16467
|
+
}
|
|
16468
|
+
return `task/${slugifyBranchComponent(task.taskType) || "task"}-${task.id.slice(0, 8)}`;
|
|
16469
|
+
}
|
|
16470
|
+
function slugifyBranchComponent(input) {
|
|
16471
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
16472
|
+
}
|
|
16473
|
+
function prepareTaskWorkspace(task, requestedMountPath) {
|
|
16474
|
+
const branch = resolveTaskWorktreeBranch(task);
|
|
16475
|
+
if (!branch) return {
|
|
16476
|
+
mountPath: requestedMountPath,
|
|
16477
|
+
mode: "shared_mount",
|
|
16478
|
+
branch: null,
|
|
16479
|
+
cleanup: () => {}
|
|
16480
|
+
};
|
|
16481
|
+
const mainRepo = findMainWorktree();
|
|
16482
|
+
const worktreeDir = join(mainRepo, ".worktrees", `task-${task.id}`);
|
|
16483
|
+
removeExistingTaskWorktree(mainRepo, worktreeDir);
|
|
16484
|
+
const relMount = relative(mainRepo, requestedMountPath);
|
|
16485
|
+
const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
|
|
16486
|
+
const baseRef = resolveWorktreeBaseRef(mainRepo);
|
|
16487
|
+
execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
|
|
16488
|
+
"-C",
|
|
16489
|
+
mainRepo,
|
|
16490
|
+
"worktree",
|
|
16491
|
+
"add",
|
|
16492
|
+
worktreeDir,
|
|
16493
|
+
branch
|
|
16494
|
+
] : [
|
|
16495
|
+
"-C",
|
|
16496
|
+
mainRepo,
|
|
16497
|
+
"worktree",
|
|
16498
|
+
"add",
|
|
16499
|
+
"-b",
|
|
16500
|
+
branch,
|
|
16501
|
+
worktreeDir,
|
|
16502
|
+
baseRef
|
|
16503
|
+
], { stdio: "pipe" });
|
|
16504
|
+
return {
|
|
16505
|
+
mountPath,
|
|
16506
|
+
mode: "dedicated_worktree",
|
|
16507
|
+
branch,
|
|
16508
|
+
cleanup: () => {
|
|
16509
|
+
execFileSync("git", [
|
|
16510
|
+
"-C",
|
|
16511
|
+
mainRepo,
|
|
16512
|
+
"worktree",
|
|
16513
|
+
"remove",
|
|
16514
|
+
"--force",
|
|
16515
|
+
worktreeDir
|
|
16516
|
+
], { stdio: "pipe" });
|
|
16517
|
+
}
|
|
16518
|
+
};
|
|
16519
|
+
}
|
|
16520
|
+
function removeExistingTaskWorktree(mainRepo, worktreeDir) {
|
|
16521
|
+
if (!existsSync(worktreeDir)) return;
|
|
16522
|
+
const list = execFileSync("git", [
|
|
16523
|
+
"-C",
|
|
16524
|
+
mainRepo,
|
|
16525
|
+
"worktree",
|
|
16526
|
+
"list",
|
|
16527
|
+
"--porcelain"
|
|
16528
|
+
], {
|
|
16529
|
+
encoding: "utf8",
|
|
16530
|
+
stdio: "pipe"
|
|
16531
|
+
});
|
|
16532
|
+
const marker = `worktree ${worktreeDir}\n`;
|
|
16533
|
+
if (!list.includes(marker) && !list.endsWith(`worktree ${worktreeDir}`)) return;
|
|
16534
|
+
execFileSync("git", [
|
|
16535
|
+
"-C",
|
|
16536
|
+
mainRepo,
|
|
16537
|
+
"worktree",
|
|
16538
|
+
"remove",
|
|
16539
|
+
"--force",
|
|
16540
|
+
worktreeDir
|
|
16541
|
+
], { stdio: "pipe" });
|
|
16542
|
+
}
|
|
16543
|
+
function resolveWorktreeBaseRef(mainRepo) {
|
|
16544
|
+
return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
|
|
16545
|
+
}
|
|
16546
|
+
function gitRefExists(mainRepo, ref) {
|
|
16547
|
+
try {
|
|
16548
|
+
execFileSync("git", [
|
|
16549
|
+
"-C",
|
|
16550
|
+
mainRepo,
|
|
16551
|
+
"show-ref",
|
|
16552
|
+
"--verify",
|
|
16553
|
+
"--quiet",
|
|
16554
|
+
ref
|
|
16555
|
+
], { stdio: "pipe" });
|
|
16556
|
+
return true;
|
|
16557
|
+
} catch {
|
|
16558
|
+
return false;
|
|
16428
16559
|
}
|
|
16429
16560
|
}
|
|
16430
16561
|
function emptyUsage(provider, model) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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,9 +33,9 @@
|
|
|
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.15.1",
|
|
36
37
|
"@themoltnet/sdk": "0.102.0",
|
|
37
|
-
"@themoltnet/pi-extension": "0.16.
|
|
38
|
-
"@themoltnet/agent-runtime": "0.15.0"
|
|
38
|
+
"@themoltnet/pi-extension": "0.16.2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsx": "^4.7.0",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vite": "^8.0.0",
|
|
44
44
|
"vitest": "^3.0.0",
|
|
45
45
|
"@moltnet/crypto-service": "0.1.0",
|
|
46
|
-
"@moltnet/
|
|
46
|
+
"@moltnet/bootstrap": "0.1.0",
|
|
47
47
|
"@moltnet/database": "0.1.0",
|
|
48
|
-
"@moltnet/
|
|
48
|
+
"@moltnet/tasks": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"nx": {
|
|
51
51
|
"tags": [
|