@getpipher/armory-fleet 0.5.1 → 0.5.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-fleet",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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
|
@@ -170,6 +170,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
170
170
|
const asyncRunLifecycle: AsyncRunnerDeps["runLifecycle"] = async (task, lifecycleName, opts) => {
|
|
171
171
|
const { runLifecycle } = await import("./lifecycle/run-lifecycle.ts");
|
|
172
172
|
const { spawnSubagent } = await import("./engine/spawnSubagent.ts");
|
|
173
|
+
// SPEC-5a §8.1 (Q4=A): bg runs must NOT compete with the foreground single-slot lock.
|
|
174
|
+
// The ConcurrencyPool gates bg-RUN concurrency (N-slot); within a run the lifecycle loop
|
|
175
|
+
// serializes phases, so a fresh per-run lock just satisfies spawnSubagent's tryAcquire API
|
|
176
|
+
// without ever contending (foreground holds deps.lock; bg holds its own). Without this, a
|
|
177
|
+
// foreground subagent holding deps.lock made every bg run's first-phase spawn fail fast
|
|
178
|
+
// (tryAcquire → "concurrency lock unexpectedly unavailable" → 6ms run:aborted).
|
|
179
|
+
const bgLock = createSingleSlotLock();
|
|
173
180
|
const lifecycleFullDeps: LifecycleRunDeps = {
|
|
174
181
|
...deps.lifecycleDeps,
|
|
175
182
|
genRunId: () => opts.runId, // override: use the async runner's runId
|
|
@@ -178,7 +185,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
178
185
|
spawn: async (o) => spawnSubagent({
|
|
179
186
|
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
180
187
|
skillsOverride: o.skills, backendOverride: o.backend,
|
|
181
|
-
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock:
|
|
188
|
+
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: bgLock,
|
|
182
189
|
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: opts.worktreePath, // child runs in the worktree
|
|
183
190
|
}),
|
|
184
191
|
};
|
|
@@ -203,7 +203,13 @@ export async function runLifecycle(task: string, lifecycleName: string, opts: Li
|
|
|
203
203
|
// A failed phase that's aborted = lifecycle failed (the work failed); a healthy phase
|
|
204
204
|
// aborted at a checkpoint = user-aborted (§12).
|
|
205
205
|
const status: LifecycleStatus = phaseRec.status === "failed" ? "failed" : "aborted";
|
|
206
|
-
|
|
206
|
+
// SPEC-5a fix: surface the failed phase's summary as the lifecycle error so the async
|
|
207
|
+
// runner's run:aborted journal event + notify show the real cause (spawnRes.error etc.),
|
|
208
|
+
// not just the bare status. Guard on non-empty summary so an empty summary still falls
|
|
209
|
+
// back to the status in the async runner (res.error ?? res.status). A healthy phase
|
|
210
|
+
// aborted at a checkpoint has no error.
|
|
211
|
+
const error = phaseRec.status === "failed" && phaseRec.summary ? phaseRec.summary : undefined;
|
|
212
|
+
return doneResult(runId, startedAt, status, lifecycleName, task, lifecycleBackend, opts.mode, phaseRecords, todoId, error);
|
|
207
213
|
}
|
|
208
214
|
// decision.action === "revise"
|
|
209
215
|
reviseCount++;
|
|
@@ -8,6 +8,13 @@ export interface PhaseArtifacts {
|
|
|
8
8
|
summary: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** SPEC-5a robustness: the error variant when the worktree git state is corrupted (e.g. child
|
|
12
|
+
* ran rm -rf .git). runLifecycle treats this as a failed phase (clean abort) instead of an
|
|
13
|
+
* unhandled throw. Matches the artifactDiscovery union in run-lifecycle.ts. */
|
|
14
|
+
export interface PhaseArtifactsError {
|
|
15
|
+
error: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
function sh(cmd: string, cwd: string): string {
|
|
12
19
|
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).toString();
|
|
13
20
|
}
|
|
@@ -22,11 +29,19 @@ export class DiffService {
|
|
|
22
29
|
*
|
|
23
30
|
* @param childFinalText the child's final text, truncated to MAX_SUMMARY chars as the prose summary.
|
|
24
31
|
*/
|
|
25
|
-
diffPhase(worktreePath: string, baseRef: string, childFinalText = ""): PhaseArtifacts {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
diffPhase(worktreePath: string, baseRef: string, childFinalText = ""): PhaseArtifacts | PhaseArtifactsError {
|
|
33
|
+
// SPEC-5a robustness: a child can corrupt its worktree's git state (e.g. rm -rf .git,
|
|
34
|
+
// git init over the worktree link). Catch git failures + return {error} so runLifecycle
|
|
35
|
+
// treats it as a failed phase (clean abort with surfaced cause) instead of an unhandled
|
|
36
|
+
// throw crashing the async runner. Empty diff (no changes) is NOT an error — returns paths: [].
|
|
37
|
+
let tracked: string[] = [];
|
|
38
|
+
let status = "";
|
|
39
|
+
try {
|
|
40
|
+
tracked = sh(`git diff --name-only ${baseRef} --`, worktreePath).split("\n").filter(Boolean);
|
|
41
|
+
status = sh("git status --porcelain", worktreePath);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return { error: `worktree diff failed: ${(e as Error).message.split("\n").filter(Boolean).pop() ?? (e as Error).message}` };
|
|
44
|
+
}
|
|
30
45
|
const untracked = status
|
|
31
46
|
.split("\n")
|
|
32
47
|
.filter((l) => l.startsWith("?? "))
|