@getpipher/armory-fleet 0.11.0 → 0.11.1
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/package.json +1 -1
- package/src/index.ts +8 -5
- package/src/runtime/async-runner.ts +79 -25
- package/src/runtime/resume.ts +21 -15
- package/src/runtime/run-journal.ts +2 -2
- package/src/scheduling/scheduler.ts +4 -0
- package/src/tools/subagent.ts +8 -2
- package/src/worktree/worktree-service.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-fleet",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The armory suite's subagent orchestrator for the pi coding agent — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -212,17 +212,20 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
212
212
|
// foreground subagent holding deps.lock made every bg run's first-phase spawn fail fast
|
|
213
213
|
// (tryAcquire → "concurrency lock unexpectedly unavailable" → 6ms run:aborted).
|
|
214
214
|
const bgLock = createSingleSlotLock();
|
|
215
|
+
// v0.11.1: isolated runs use worktree-diff artifact discovery + the worktree as spawn cwd;
|
|
216
|
+
// in-place runs (worktreePath undefined) use the prompt-baked parser + the session cwd.
|
|
217
|
+
const isolated = !!opts.worktreePath;
|
|
215
218
|
const lifecycleFullDeps: LifecycleRunDeps = {
|
|
216
219
|
...deps.lifecycleDeps,
|
|
217
220
|
genRunId: () => opts.runId, // override: use the async runner's runId
|
|
218
|
-
|
|
219
|
-
artifactDiscovery: ({ finalText, cwd, baseRef }) => (deps.asyncRunner as AsyncRunnerDeps).diff.diffPhase(cwd, baseRef, finalText),
|
|
221
|
+
...(isolated ? { artifactDiscovery: ({ finalText, cwd, baseRef }: { finalText: string; cwd: string; baseRef: string }) => (deps.asyncRunner as AsyncRunnerDeps).diff.diffPhase(cwd, baseRef, finalText) } : {}),
|
|
220
222
|
spawn: async (o) => spawnSubagent({
|
|
221
223
|
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
222
224
|
skillsOverride: o.skills, backendOverride: o.backend,
|
|
223
225
|
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: bgLock,
|
|
224
|
-
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel,
|
|
225
|
-
|
|
226
|
+
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel,
|
|
227
|
+
parentCwd: isolated ? opts.worktreePath! : deps.parentCwd,
|
|
228
|
+
runLog: deps.runLog, tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
226
229
|
}),
|
|
227
230
|
};
|
|
228
231
|
const res = await runLifecycle(task, lifecycleName, { deps: lifecycleFullDeps, mode: opts.mode, worktreePath: opts.worktreePath, baseRef: "HEAD", onCheckpoint: async (p) => p.status === "failed" ? { action: "abort" } : { action: "continue" } });
|
|
@@ -297,7 +300,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
297
300
|
lockPath: join(dir, "schedules.lock"),
|
|
298
301
|
onFire: (spec) => {
|
|
299
302
|
if (!deps.asyncRunner) return;
|
|
300
|
-
runBackground(spec.task, { deps: deps.asyncRunner, lifecycle: spec.lifecycle ?? "default", mode: spec.auto ? "auto" : "checkpointed" });
|
|
303
|
+
runBackground(spec.task, { deps: deps.asyncRunner, lifecycle: spec.lifecycle ?? "default", mode: spec.auto ? "auto" : "checkpointed", isolation: spec.isolation });
|
|
301
304
|
},
|
|
302
305
|
});
|
|
303
306
|
deps.scheduler.start();
|
|
@@ -23,8 +23,8 @@ export interface FakeLifecycleResult {
|
|
|
23
23
|
|
|
24
24
|
export interface RunLifecycleOpts {
|
|
25
25
|
runId: string;
|
|
26
|
-
worktreePath
|
|
27
|
-
branch
|
|
26
|
+
worktreePath?: string;
|
|
27
|
+
branch?: string;
|
|
28
28
|
mode: "auto" | "checkpointed";
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -49,12 +49,22 @@ export interface RunBackgroundOpts {
|
|
|
49
49
|
deps: AsyncRunnerDeps;
|
|
50
50
|
lifecycle: string;
|
|
51
51
|
mode: "auto" | "checkpointed";
|
|
52
|
+
/** v0.11.1: edit isolation for background runs. Default "auto" (worktree when cwd is a git repo, in-place otherwise). */
|
|
53
|
+
isolation?: Isolation;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
/** The success shape (back-compat: existing external refs to RunBackgroundHandle still typecheck). */
|
|
57
|
+
export interface RunBackgroundHandle { runId: string; status: "background"; }
|
|
58
|
+
|
|
59
|
+
/** v0.11.1: a background dispatch either starts (runId + background) or fails synchronously (error, no runId). */
|
|
60
|
+
export type RunBackgroundResult =
|
|
61
|
+
| { runId: string; status: "background" }
|
|
62
|
+
| { status: "failed"; error: string };
|
|
63
|
+
|
|
64
|
+
export type Isolation = "worktree" | "none" | "auto";
|
|
65
|
+
|
|
66
|
+
/** Per-session dedup flag for the auto-fallback in-place notify (resets on process restart = new session). */
|
|
67
|
+
let inPlaceFallbackWarned = false;
|
|
58
68
|
|
|
59
69
|
function emitProgress(deps: AsyncRunnerDeps, runId: string, partial: Partial<import("../panel/rows.ts").BgRunStatus> & { status: import("../panel/rows.ts").BgStatus; phase: string; phaseIndex: number; phaseTotal: number }): void {
|
|
60
70
|
if (!deps.onProgress) return;
|
|
@@ -72,54 +82,98 @@ function sh(cmd: string, cwd: string): void {
|
|
|
72
82
|
execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
85
|
+
/** The git-agnostic core: pool slot → journal → runLifecycle in ctx.cwd (or a worktree, when `isolated` is set) → inbox + notify.
|
|
86
|
+
* Fire-and-forget. When `isolated` is present, journals the worktree field, commits on completion, and removes the worktree. */
|
|
87
|
+
function runBackgroundInPlace(runId: string, task: string, opts: RunBackgroundOpts, isolated?: { worktreePath: string; branch: string }): void {
|
|
76
88
|
const { deps } = opts;
|
|
77
|
-
const runId = deps.genRunId();
|
|
78
|
-
const baseRef = "HEAD";
|
|
79
|
-
|
|
80
|
-
// Fire-and-forget: the pool gates concurrency; the journal records the run.
|
|
81
89
|
void deps.pool.withSlot(async () => {
|
|
82
|
-
let wt: { path: string; branch: string } | null = null;
|
|
83
90
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
const ev0: JournalEvent = isolated
|
|
92
|
+
? { type: "run:started", runId, task, lifecycle: opts.lifecycle, worktree: { path: isolated.worktreePath, branch: isolated.branch }, mode: opts.mode, ts: Date.now() }
|
|
93
|
+
: { type: "run:started", runId, task, lifecycle: opts.lifecycle, mode: opts.mode, ts: Date.now() };
|
|
86
94
|
deps.journal.append(runId, ev0);
|
|
87
95
|
emitProgress(deps, runId, { status: "running", phase: "", phaseIndex: 0, phaseTotal: 0, lifecycle: opts.lifecycle, mode: opts.mode, task });
|
|
88
96
|
|
|
89
|
-
const res = await deps.runLifecycle(task, opts.lifecycle, { runId, worktreePath:
|
|
97
|
+
const res = await deps.runLifecycle(task, opts.lifecycle, { runId, worktreePath: isolated?.worktreePath, branch: isolated?.branch, mode: opts.mode });
|
|
90
98
|
|
|
91
99
|
if (res.status === "completed") {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
if (isolated) {
|
|
101
|
+
try { sh("git add -A && git commit -m 'fleet run complete'", isolated.worktreePath); } catch { /* nothing to commit */ }
|
|
102
|
+
}
|
|
103
|
+
deps.journal.append(runId, isolated
|
|
104
|
+
? { type: "run:completed", runId, branch: isolated.branch, ts: Date.now() }
|
|
105
|
+
: { type: "run:completed", runId, ts: Date.now() });
|
|
95
106
|
const total = res.phases.length;
|
|
96
|
-
const lastIdx = total;
|
|
97
|
-
emitProgress(deps, runId, { status: "completed", phase: res.phases[total - 1]?.name ?? "finish", phaseIndex: lastIdx, phaseTotal: total, lifecycle: opts.lifecycle, mode: opts.mode, task, branch:
|
|
107
|
+
const lastIdx = total;
|
|
108
|
+
emitProgress(deps, runId, { status: "completed", phase: res.phases[total - 1]?.name ?? "finish", phaseIndex: lastIdx, phaseTotal: total, lifecycle: opts.lifecycle, mode: opts.mode, task, ...(isolated ? { branch: isolated.branch } : {}) });
|
|
98
109
|
const lastPhase = res.phases[res.phases.length - 1];
|
|
99
110
|
const result: RunResult = {
|
|
100
111
|
runId, task, status: "completed",
|
|
101
112
|
summary: lastPhase?.summary ?? "",
|
|
102
113
|
paths: res.phases.flatMap((p) => p.paths),
|
|
103
|
-
|
|
114
|
+
completedAt: Date.now(),
|
|
115
|
+
...(isolated ? { branch: isolated.branch } : {}),
|
|
104
116
|
};
|
|
105
117
|
deps.inbox.push(result);
|
|
106
118
|
deps.notify(`fleet run ${runId} completed`, "info");
|
|
107
|
-
|
|
108
|
-
deps.worktree.removeWorktree(runId);
|
|
119
|
+
if (isolated) deps.worktree.removeWorktree(runId);
|
|
109
120
|
} else {
|
|
110
121
|
deps.journal.append(runId, { type: "run:aborted", runId, reason: res.error ?? res.status, ts: Date.now() });
|
|
111
|
-
deps.worktree.remove(runId);
|
|
122
|
+
if (isolated) deps.worktree.remove(runId);
|
|
112
123
|
emitProgress(deps, runId, { status: "failed", phase: "", phaseIndex: 0, phaseTotal: res.phases.length, lifecycle: opts.lifecycle, mode: opts.mode, task });
|
|
113
124
|
deps.notify(`fleet run ${runId} ${res.status}: ${res.error ?? ""}`, "warning");
|
|
114
125
|
}
|
|
115
126
|
} catch (e) {
|
|
116
127
|
const msg = (e as Error).message;
|
|
117
128
|
deps.journal.append(runId, { type: "run:aborted", runId, reason: msg, ts: Date.now() });
|
|
118
|
-
if (
|
|
129
|
+
if (isolated) deps.worktree.remove(runId);
|
|
119
130
|
deps.notify(`fleet run ${runId} failed: ${msg}`, "error");
|
|
120
131
|
emitProgress(deps, runId, { status: "failed", phase: "", phaseIndex: 0, phaseTotal: 0, lifecycle: opts.lifecycle, mode: opts.mode, task });
|
|
121
132
|
}
|
|
122
133
|
});
|
|
134
|
+
}
|
|
123
135
|
|
|
136
|
+
/** The worktree wrapper: SYNCHRONOUS pre-flight + worktree create, then core-with-isolation. */
|
|
137
|
+
function runBackgroundIsolated(task: string, opts: RunBackgroundOpts): RunBackgroundResult {
|
|
138
|
+
const { deps } = opts;
|
|
139
|
+
if (!deps.worktree.isGitRepo()) {
|
|
140
|
+
return { status: "failed", error: "isolation: 'worktree' requires a git repo; cwd is not one — use isolation: 'none' or run in a git repo" };
|
|
141
|
+
}
|
|
142
|
+
const runId = deps.genRunId();
|
|
143
|
+
const baseRef = "HEAD";
|
|
144
|
+
let wt: { path: string; branch: string };
|
|
145
|
+
try {
|
|
146
|
+
wt = deps.worktree.create(runId, baseRef);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return { status: "failed", error: (e as Error).message };
|
|
149
|
+
}
|
|
150
|
+
runBackgroundInPlace(runId, task, opts, { worktreePath: wt.path, branch: wt.branch });
|
|
124
151
|
return { runId, status: "background" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The auto router: isolated when cwd is a git repo, in-place + one per-session notify when not. */
|
|
155
|
+
function runBackgroundAuto(task: string, opts: RunBackgroundOpts): RunBackgroundResult {
|
|
156
|
+
const { deps } = opts;
|
|
157
|
+
if (deps.worktree.isGitRepo()) {
|
|
158
|
+
return runBackgroundIsolated(task, opts);
|
|
159
|
+
}
|
|
160
|
+
if (!inPlaceFallbackWarned) {
|
|
161
|
+
inPlaceFallbackWarned = true;
|
|
162
|
+
deps.notify("background run in-place (no worktree isolation — parallel edits may conflict)", "warning");
|
|
163
|
+
}
|
|
164
|
+
const runId = deps.genRunId();
|
|
165
|
+
runBackgroundInPlace(runId, task, opts);
|
|
166
|
+
return { runId, status: "background" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Public dispatcher (keeps the `runBackground` name + RunBackgroundHandle success shape for back-compat). */
|
|
170
|
+
export function runBackground(task: string, opts: RunBackgroundOpts): RunBackgroundResult {
|
|
171
|
+
const isolation = opts.isolation ?? "auto";
|
|
172
|
+
if (isolation === "worktree") return runBackgroundIsolated(task, opts);
|
|
173
|
+
if (isolation === "none") {
|
|
174
|
+
const runId = opts.deps.genRunId();
|
|
175
|
+
runBackgroundInPlace(runId, task, opts);
|
|
176
|
+
return { runId, status: "background" };
|
|
177
|
+
}
|
|
178
|
+
return runBackgroundAuto(task, opts);
|
|
125
179
|
}
|
package/src/runtime/resume.ts
CHANGED
|
@@ -8,8 +8,10 @@ export interface ResumeCandidate {
|
|
|
8
8
|
runId: string;
|
|
9
9
|
task: string;
|
|
10
10
|
lifecycle: string;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** Present only for isolated (worktree) runs. */
|
|
12
|
+
worktreePath?: string;
|
|
13
|
+
/** Present only for isolated (worktree) runs. */
|
|
14
|
+
branch?: string;
|
|
13
15
|
lastPhase: string | null;
|
|
14
16
|
canResume: boolean;
|
|
15
17
|
}
|
|
@@ -26,23 +28,27 @@ export function scanResumeCandidates(_projectDir: string, opts: ScanResumeOpts):
|
|
|
26
28
|
for (const runId of ids) {
|
|
27
29
|
const events = journal.replay(runId);
|
|
28
30
|
const started = events.find((e) => e.type === "run:started") as
|
|
29
|
-
| (JournalEvent & { type: "run:started" }) | undefined;
|
|
31
|
+
| (JournalEvent & { type: "run:started"; worktree?: { path: string; branch: string } }) | undefined;
|
|
30
32
|
if (!started) continue;
|
|
31
33
|
const phaseEvents = events.filter((e) => e.type === "phase:completed" || e.type === "phase:started" || e.type === "phase:failed") as Array<{ phase: string }>;
|
|
32
34
|
const lastPhase = phaseEvents.length > 0 ? phaseEvents[phaseEvents.length - 1]!.phase : null;
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
if (started.worktree) {
|
|
37
|
+
// isolated run: resume iff the worktree still exists
|
|
38
|
+
const wtExists = opts.worktree.exists(runId);
|
|
39
|
+
if (!wtExists) {
|
|
40
|
+
journal.append(runId, { type: "run:aborted", runId, reason: "worktree-missing", ts: Date.now() });
|
|
41
|
+
}
|
|
42
|
+
cands.push({
|
|
43
|
+
runId, task: started.task, lifecycle: started.lifecycle,
|
|
44
|
+
worktreePath: started.worktree.path, branch: started.worktree.branch,
|
|
45
|
+
lastPhase, canResume: wtExists,
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
// v0.11.1: in-place interrupted run — no worktree to clean; abort (partial edits may remain in cwd).
|
|
49
|
+
journal.append(runId, { type: "run:aborted", runId, reason: "in-place interrupted (partial edits may remain in cwd)", ts: Date.now() });
|
|
50
|
+
cands.push({ runId, task: started.task, lifecycle: started.lifecycle, lastPhase, canResume: false });
|
|
36
51
|
}
|
|
37
|
-
cands.push({
|
|
38
|
-
runId,
|
|
39
|
-
task: started.task,
|
|
40
|
-
lifecycle: started.lifecycle,
|
|
41
|
-
worktreePath: started.worktree.path,
|
|
42
|
-
branch: started.worktree.branch,
|
|
43
|
-
lastPhase,
|
|
44
|
-
canResume: wtExists,
|
|
45
|
-
});
|
|
46
52
|
}
|
|
47
53
|
return cands;
|
|
48
54
|
}
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
|
-
export interface RunStartedEvent { type: "run:started"; runId: string; task: string; lifecycle: string; worktree
|
|
7
|
+
export interface RunStartedEvent { type: "run:started"; runId: string; task: string; lifecycle: string; worktree?: { path: string; branch: string }; mode: "auto" | "checkpointed"; ts: number; }
|
|
8
8
|
export interface PhaseStartedEvent { type: "phase:started"; phase: string; ts: number; }
|
|
9
9
|
export interface PhaseCompletedEvent { type: "phase:completed"; phase: string; summary: string; paths: string[]; ts: number; }
|
|
10
10
|
export interface PhaseFailedEvent { type: "phase:failed"; phase: string; error: string; ts: number; }
|
|
11
11
|
export interface CheckpointEvent { type: "checkpoint"; phase: string; decision: "continue" | "revise" | "abort"; ts: number; }
|
|
12
|
-
export interface RunCompletedEvent { type: "run:completed"; runId: string; branch
|
|
12
|
+
export interface RunCompletedEvent { type: "run:completed"; runId: string; branch?: string; ts: number; }
|
|
13
13
|
export interface RunAbortedEvent { type: "run:aborted"; runId: string; reason: string; ts: number; }
|
|
14
14
|
|
|
15
15
|
export type JournalEvent =
|
|
@@ -11,6 +11,8 @@ export interface ScheduleSpec {
|
|
|
11
11
|
expression: string;
|
|
12
12
|
lifecycle?: string; // default "default"
|
|
13
13
|
auto?: boolean;
|
|
14
|
+
/** v0.11.1: edit isolation for the background run on fire. Default "auto". */
|
|
15
|
+
isolation?: "worktree" | "none" | "auto";
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface Schedule extends ScheduleSpec {
|
|
@@ -71,6 +73,7 @@ export class Scheduler {
|
|
|
71
73
|
expression: spec.expression,
|
|
72
74
|
lifecycle: spec.lifecycle ?? "default",
|
|
73
75
|
auto: spec.auto ?? true,
|
|
76
|
+
isolation: spec.isolation,
|
|
74
77
|
paused: false,
|
|
75
78
|
};
|
|
76
79
|
this.schedules.set(id, { spec: stored, expr, timer: null });
|
|
@@ -86,6 +89,7 @@ export class Scheduler {
|
|
|
86
89
|
expression: e.spec.expression,
|
|
87
90
|
lifecycle: e.spec.lifecycle,
|
|
88
91
|
auto: e.spec.auto,
|
|
92
|
+
isolation: e.spec.isolation,
|
|
89
93
|
paused: e.spec.paused,
|
|
90
94
|
nextFire: e.spec.paused ? null : e.expr.nextFire(new Date()),
|
|
91
95
|
}));
|
package/src/tools/subagent.ts
CHANGED
|
@@ -22,6 +22,11 @@ export const subagentParams = Type.Object({
|
|
|
22
22
|
lifecycle: Type.Optional(Type.String({ description: "Run a multi-phase superpowers lifecycle by name (e.g. 'default') instead of a single delegate. Tool-driven lifecycles run end-to-end (auto) — checkpoints are a /fleet panel feature." })),
|
|
23
23
|
auto: Type.Optional(Type.Boolean({ description: "Only relevant with `lifecycle`. Tool-driven is always auto; this flag is forward-compat. Panel-driven uses --auto on /fleet-implement." })),
|
|
24
24
|
background: Type.Optional(Type.Boolean({ description: "Fire without awaiting. The run goes to the async/bg pool on an isolated git worktree; this returns { runId, status: 'background' } immediately. Foreground (default) awaits the result." })),
|
|
25
|
+
isolation: Type.Optional(Type.Union([
|
|
26
|
+
Type.Literal("worktree"),
|
|
27
|
+
Type.Literal("none"),
|
|
28
|
+
Type.Literal("auto"),
|
|
29
|
+
], { description: "Edit isolation for background runs. 'worktree' = git worktree (requires a git repo; fails sync if not). 'none' = in-place in cwd (no isolation; parallel edits may conflict). 'auto' (default) = worktree when cwd is a git repo, in-place otherwise." })),
|
|
25
30
|
schedule: Type.Optional(Type.String({ description: 'Schedule the run instead of firing now: a cron string ("0 9 * * 1-5"), an interval ("30m"/"2h"), or a one-shot ISO datetime ("2026-07-25T14:00"). Returns { scheduleId, nextFire }. Session-scoped (fires only while pi is open); no catch-up.' })),
|
|
26
31
|
maxTurns: Type.Optional(Type.Number({ description: 'Per-run turn budget (default 20). Raise for complex multi-step tasks (e.g. 40) so the subagent doesn\'t hit the budget mid-task; lower for trivial lookups.' })),
|
|
27
32
|
});
|
|
@@ -80,13 +85,14 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
80
85
|
}
|
|
81
86
|
if (params.schedule) {
|
|
82
87
|
if (!deps.scheduler) return { isError: true, content: [{ type: "text" as const, text: "scheduling not configured (scheduler missing)" }] };
|
|
83
|
-
const id = deps.scheduler.register({ task: params.task, expression: params.schedule, lifecycle: params.lifecycle ?? "default", auto: params.auto ?? true });
|
|
88
|
+
const id = deps.scheduler.register({ task: params.task, expression: params.schedule, lifecycle: params.lifecycle ?? "default", auto: params.auto ?? true, isolation: params.isolation });
|
|
84
89
|
const entry = deps.scheduler.list().find((s) => s.id === id);
|
|
85
90
|
return { content: [{ type: "text" as const, text: `scheduled: ${id} · next fire: ${entry?.nextFire?.toISOString() ?? "(paused)"}` }], details: { scheduleId: id, nextFire: entry?.nextFire ?? null } };
|
|
86
91
|
}
|
|
87
92
|
if (params.background) {
|
|
88
93
|
if (!deps.asyncRunner) return { isError: true, content: [{ type: "text" as const, text: "background runs not configured (asyncRunner missing)" }] };
|
|
89
|
-
const handle = runBackground(params.task, { deps: deps.asyncRunner, lifecycle: params.lifecycle ?? "default", mode: "auto" });
|
|
94
|
+
const handle = runBackground(params.task, { deps: deps.asyncRunner, lifecycle: params.lifecycle ?? "default", mode: "auto", isolation: params.isolation });
|
|
95
|
+
if (handle.status === "failed") return { isError: true, content: [{ type: "text" as const, text: handle.error }] };
|
|
90
96
|
return { content: [{ type: "text" as const, text: `background run: ${handle.runId}` }], details: handle };
|
|
91
97
|
}
|
|
92
98
|
if (params.lifecycle) {
|
|
@@ -40,6 +40,16 @@ export class WorktreeService {
|
|
|
40
40
|
return existsSync(this.pathFor(runId));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** v0.11.1: is `rootDir` (or `dir`) inside a git repo? Cheap sync pre-flight for isolation routing. */
|
|
44
|
+
isGitRepo(dir: string = this.rootDir): boolean {
|
|
45
|
+
try {
|
|
46
|
+
sh("git rev-parse --show-toplevel", dir);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
create(runId: string, baseRef = "HEAD"): WorktreeRef {
|
|
44
54
|
if (this.exists(runId)) {
|
|
45
55
|
throw new Error(`worktree for run ${runId} already exists at ${this.pathFor(runId)}`);
|