@getpipher/armory-fleet 0.5.0 → 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++;
|
|
@@ -117,6 +117,9 @@ export class Scheduler {
|
|
|
117
117
|
|
|
118
118
|
start(): boolean {
|
|
119
119
|
if (this.running) return true;
|
|
120
|
+
// Ensure the lock file's parent dir exists (fresh projects have no `.pi/fleet/` yet;
|
|
121
|
+
// persist() only runs on register(), so start() must self-sufficient the dir first).
|
|
122
|
+
mkdirSync(dirname(this.opts.lockPath), { recursive: true });
|
|
120
123
|
if (!this.pidLock.acquire(this.opts.lockPath)) return false;
|
|
121
124
|
this.running = true;
|
|
122
125
|
for (const id of this.schedules.keys()) this.arm(id);
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
- **Version:** 1.1.1 (latest 1.x — dependency-free; later versions pull in `luxon`)
|
|
6
6
|
- **License:** MIT (see upstream LICENSE)
|
|
7
7
|
- **Vendored on:** 2026-07-24
|
|
8
|
-
- **Vendored surface:** `lib/` (4 files: `parser.js`, `expression.js`, `date.js`, `number.js` — CommonJS, all-relative `require()`s, zero runtime deps)
|
|
9
|
-
- **Frozen:** do NOT edit files under `lib/`. To upgrade, replace `lib/` + update this NOTICE (version + date). Note: v2+ adds `luxon` as a runtime dep — vendoring those would require also vendoring luxon; v1.1.1 is intentionally dep-free.
|
|
8
|
+
- **Vendored surface:** `lib/` (4 files: `parser.js`, `expression.js`, `date.js`, `number.js` — CommonJS, all-relative `require()`s, zero runtime deps) + `package.json` (`{"type":"commonjs"}`) scoping the dir as CJS so Node's loader treats the `.js` files as CommonJS despite the package root's `"type": "module"`.
|
|
9
|
+
- **Frozen:** do NOT edit files under `lib/`. To upgrade, replace `lib/` + update this NOTICE (version + date). Note: v2+ adds `luxon` as a runtime dep — vendoring those would require also vendoring luxon; v1.1.1 is intentionally dep-free. The `package.json` scope file is NOT part of the upstream lib — it's our CJS-in-ESM interop shim; keep it across upgrades.
|
|
10
10
|
|
|
11
11
|
## Why vendored (per SPEC-5a §9, Q9=A)
|
|
12
12
|
cron expression parsing is commodity plumbing (DST, month-length, DOW/DOM OR-semantics, Feb 29).
|
|
@@ -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("?? "))
|