@os-eco/overstory-cli 0.9.4 → 0.11.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 +50 -19
- package/agents/builder.md +19 -9
- package/agents/coordinator.md +6 -6
- package/agents/lead.md +204 -87
- package/agents/merger.md +25 -14
- package/agents/reviewer.md +22 -16
- package/agents/scout.md +17 -12
- package/package.json +6 -3
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/headless-mail-injector.test.ts +448 -0
- package/src/agents/headless-mail-injector.ts +219 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +514 -14
- package/src/agents/hooks-deployer.ts +141 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/overlay.test.ts +60 -4
- package/src/agents/overlay.ts +63 -8
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/commands/agents.ts +9 -0
- package/src/commands/clean.ts +54 -0
- package/src/commands/coordinator.test.ts +254 -0
- package/src/commands/coordinator.ts +273 -8
- package/src/commands/dashboard.test.ts +188 -0
- package/src/commands/dashboard.ts +14 -4
- package/src/commands/doctor.ts +3 -1
- package/src/commands/group.test.ts +94 -0
- package/src/commands/group.ts +49 -20
- package/src/commands/init.test.ts +8 -0
- package/src/commands/init.ts +8 -1
- package/src/commands/log.test.ts +187 -11
- package/src/commands/log.ts +171 -71
- package/src/commands/mail.test.ts +162 -0
- package/src/commands/mail.ts +64 -9
- package/src/commands/merge.test.ts +230 -1
- package/src/commands/merge.ts +68 -12
- package/src/commands/nudge.test.ts +351 -4
- package/src/commands/nudge.ts +356 -34
- package/src/commands/run.test.ts +43 -7
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +408 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1323 -0
- package/src/commands/serve/rest.ts +708 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +565 -0
- package/src/commands/sling.test.ts +177 -1
- package/src/commands/sling.ts +243 -71
- package/src/commands/status.test.ts +9 -0
- package/src/commands/status.ts +12 -4
- package/src/commands/stop.test.ts +255 -1
- package/src/commands/stop.ts +107 -8
- package/src/commands/watch.test.ts +43 -0
- package/src/commands/watch.ts +153 -28
- package/src/config.ts +23 -0
- package/src/doctor/consistency.test.ts +106 -0
- package/src/doctor/consistency.ts +48 -1
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/types.ts +2 -1
- package/src/doctor/watchdog.ts +57 -1
- package/src/events/tailer.test.ts +234 -1
- package/src/events/tailer.ts +90 -0
- package/src/index.ts +57 -6
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.ts +29 -0
- package/src/logging/theme.ts +4 -0
- package/src/mail/client.ts +15 -2
- package/src/mail/store.test.ts +82 -0
- package/src/mail/store.ts +41 -4
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/resolver.ts +1 -1
- package/src/mulch/client.ts +3 -3
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/claude.test.ts +791 -1
- package/src/runtimes/claude.ts +323 -1
- package/src/runtimes/connections.test.ts +141 -1
- package/src/runtimes/connections.ts +73 -4
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/types.ts +10 -0
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/store.test.ts +657 -29
- package/src/sessions/store.ts +286 -23
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/types.ts +107 -2
- package/src/utils/pid.test.ts +85 -1
- package/src/utils/pid.ts +86 -1
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/watchdog/daemon.test.ts +1607 -376
- package/src/watchdog/daemon.ts +462 -88
- package/src/watchdog/health.test.ts +282 -0
- package/src/watchdog/health.ts +126 -27
- package/src/worktree/manager.test.ts +218 -1
- package/src/worktree/manager.ts +55 -0
- package/src/worktree/process.test.ts +71 -0
- package/src/worktree/process.ts +25 -5
- package/src/worktree/tmux.test.ts +28 -0
- package/src/worktree/tmux.ts +27 -3
- package/templates/CLAUDE.md.tmpl +19 -8
- package/templates/overlay.md.tmpl +5 -2
|
@@ -4,7 +4,7 @@ import { mkdtemp } from "node:fs/promises";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { resolveModel, resolveProviderEnv } from "../agents/manifest.ts";
|
|
7
|
-
import { HierarchyError } from "../errors.ts";
|
|
7
|
+
import { HierarchyError, ValidationError } from "../errors.ts";
|
|
8
8
|
import { ClaudeRuntime } from "../runtimes/claude.ts";
|
|
9
9
|
import { getRuntime } from "../runtimes/registry.ts";
|
|
10
10
|
import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
|
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
getSharedWritableDirs,
|
|
26
26
|
inferDomainsFromFiles,
|
|
27
27
|
isRunningAsRoot,
|
|
28
|
+
isTaskWorkable,
|
|
28
29
|
parentHasScouts,
|
|
30
|
+
parseSiblings,
|
|
31
|
+
resolveParentAgent,
|
|
32
|
+
resolveUseHeadless,
|
|
29
33
|
shouldShowScoutWarning,
|
|
30
34
|
validateHierarchy,
|
|
31
35
|
} from "./sling.ts";
|
|
@@ -766,6 +770,67 @@ function makeLeadSession(
|
|
|
766
770
|
return { agentName, taskId, capability };
|
|
767
771
|
}
|
|
768
772
|
|
|
773
|
+
describe("isTaskWorkable", () => {
|
|
774
|
+
test("accepts open and in_progress without recover", () => {
|
|
775
|
+
expect(isTaskWorkable("open", false)).toBe(true);
|
|
776
|
+
expect(isTaskWorkable("in_progress", false)).toBe(true);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("rejects closed and other terminal statuses without recover", () => {
|
|
780
|
+
expect(isTaskWorkable("closed", false)).toBe(false);
|
|
781
|
+
expect(isTaskWorkable("cancelled", false)).toBe(false);
|
|
782
|
+
expect(isTaskWorkable("done", false)).toBe(false);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("accepts any status when recover is true", () => {
|
|
786
|
+
expect(isTaskWorkable("closed", true)).toBe(true);
|
|
787
|
+
expect(isTaskWorkable("cancelled", true)).toBe(true);
|
|
788
|
+
expect(isTaskWorkable("open", true)).toBe(true);
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// --- resolveParentAgent (overstory-de3c) ---
|
|
793
|
+
//
|
|
794
|
+
// Witnessed bug: a coordinator/lead recovered a zombie spawn-per-turn worker
|
|
795
|
+
// via `ov sling --recover --name <existing>` without threading `--parent`.
|
|
796
|
+
// The pre-fix `parentAgent = opts.parent ?? null` overwrote the prior
|
|
797
|
+
// `parent_agent` row to null on upsert, so the runner could not emit
|
|
798
|
+
// `worker_died` on a resumed-turn parser stall — the lead waited forever.
|
|
799
|
+
// The fix: when --parent is not explicitly passed, fall back to the prior
|
|
800
|
+
// session row's parentAgent. Explicit caller intent (any string, including
|
|
801
|
+
// empty) always wins.
|
|
802
|
+
describe("resolveParentAgent", () => {
|
|
803
|
+
test("case A: explicit --parent wins over prior session linkage", () => {
|
|
804
|
+
const existing = { parentAgent: "old-lead" };
|
|
805
|
+
expect(resolveParentAgent("new-lead", existing)).toBe("new-lead");
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("case B: --parent omitted preserves prior session's parentAgent on re-spawn", () => {
|
|
809
|
+
// THE REGRESSION CHECK. Pre-fix this returned null, severing the link
|
|
810
|
+
// the runner needs to emit worker_died (overstory-de3c).
|
|
811
|
+
const existing = { parentAgent: "lead-r" };
|
|
812
|
+
expect(resolveParentAgent(undefined, existing)).toBe("lead-r");
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test("--parent omitted with no prior session yields null (fresh agent)", () => {
|
|
816
|
+
expect(resolveParentAgent(undefined, null)).toBeNull();
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test("--parent omitted with prior session whose parent is null yields null", () => {
|
|
820
|
+
// A coordinator-spawned root agent has parentAgent=null. Re-spawn must
|
|
821
|
+
// not synthesize a parent.
|
|
822
|
+
const existing = { parentAgent: null };
|
|
823
|
+
expect(resolveParentAgent(undefined, existing)).toBeNull();
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test("explicit --parent='' (empty string) is honored — caller intent wins", () => {
|
|
827
|
+
// Empty string is `defined` but `null`-y; we honor it as caller intent
|
|
828
|
+
// rather than silently falling back to the prior linkage.
|
|
829
|
+
const existing = { parentAgent: "lead-r" };
|
|
830
|
+
expect(resolveParentAgent("", existing)).toBe("");
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
769
834
|
describe("checkDuplicateLead", () => {
|
|
770
835
|
test("returns lead agent name when an active lead exists for the task", () => {
|
|
771
836
|
const sessions = [
|
|
@@ -1146,6 +1211,7 @@ function makeAutoDispatchOpts(overrides?: Partial<AutoDispatchOptions>): AutoDis
|
|
|
1146
1211
|
capability: "builder",
|
|
1147
1212
|
specPath: "/path/to/spec.md",
|
|
1148
1213
|
parentAgent: "lead-alpha",
|
|
1214
|
+
slingerName: null,
|
|
1149
1215
|
instructionPath: ".claude/CLAUDE.md",
|
|
1150
1216
|
...overrides,
|
|
1151
1217
|
};
|
|
@@ -1159,6 +1225,7 @@ describe("buildAutoDispatch", () => {
|
|
|
1159
1225
|
capability: "builder",
|
|
1160
1226
|
specPath: "/path/to/spec.md",
|
|
1161
1227
|
parentAgent: "lead-alpha",
|
|
1228
|
+
slingerName: null,
|
|
1162
1229
|
instructionPath: ".claude/CLAUDE.md",
|
|
1163
1230
|
});
|
|
1164
1231
|
expect(dispatch.from).toBe("lead-alpha");
|
|
@@ -1174,6 +1241,7 @@ describe("buildAutoDispatch", () => {
|
|
|
1174
1241
|
capability: "lead",
|
|
1175
1242
|
specPath: null,
|
|
1176
1243
|
parentAgent: null,
|
|
1244
|
+
slingerName: null,
|
|
1177
1245
|
instructionPath: ".claude/CLAUDE.md",
|
|
1178
1246
|
});
|
|
1179
1247
|
expect(dispatch.from).toBe("orchestrator");
|
|
@@ -1187,6 +1255,7 @@ describe("buildAutoDispatch", () => {
|
|
|
1187
1255
|
capability: "scout",
|
|
1188
1256
|
specPath: null,
|
|
1189
1257
|
parentAgent: "lead-alpha",
|
|
1258
|
+
slingerName: null,
|
|
1190
1259
|
instructionPath: ".claude/CLAUDE.md",
|
|
1191
1260
|
});
|
|
1192
1261
|
expect(dispatch.body).toContain("scout");
|
|
@@ -1199,11 +1268,33 @@ describe("buildAutoDispatch", () => {
|
|
|
1199
1268
|
capability: "builder",
|
|
1200
1269
|
specPath: "/abs/path/to/spec.md",
|
|
1201
1270
|
parentAgent: "lead-alpha",
|
|
1271
|
+
slingerName: null,
|
|
1202
1272
|
instructionPath: ".claude/CLAUDE.md",
|
|
1203
1273
|
});
|
|
1204
1274
|
expect(dispatch.body).toContain("/abs/path/to/spec.md");
|
|
1205
1275
|
});
|
|
1206
1276
|
|
|
1277
|
+
test("slinger takes precedence over parent agent for from field", () => {
|
|
1278
|
+
const dispatch = buildAutoDispatch(
|
|
1279
|
+
makeAutoDispatchOpts({ slingerName: "coordinator", parentAgent: "lead-alpha" }),
|
|
1280
|
+
);
|
|
1281
|
+
expect(dispatch.from).toBe("coordinator");
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
test("slinger fills in when parent agent is null", () => {
|
|
1285
|
+
const dispatch = buildAutoDispatch(
|
|
1286
|
+
makeAutoDispatchOpts({ slingerName: "coordinator", parentAgent: null }),
|
|
1287
|
+
);
|
|
1288
|
+
expect(dispatch.from).toBe("coordinator");
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test("falls back to orchestrator when both slinger and parent are null", () => {
|
|
1292
|
+
const dispatch = buildAutoDispatch(
|
|
1293
|
+
makeAutoDispatchOpts({ slingerName: null, parentAgent: null }),
|
|
1294
|
+
);
|
|
1295
|
+
expect(dispatch.from).toBe("orchestrator");
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1207
1298
|
test("subject contains task ID", () => {
|
|
1208
1299
|
const dispatch = buildAutoDispatch(makeAutoDispatchOpts({ taskId: "overstory-zz99" }));
|
|
1209
1300
|
expect(dispatch.subject).toContain("overstory-zz99");
|
|
@@ -1405,3 +1496,88 @@ describe("getCurrentBranch", () => {
|
|
|
1405
1496
|
}
|
|
1406
1497
|
});
|
|
1407
1498
|
});
|
|
1499
|
+
|
|
1500
|
+
describe("resolveUseHeadless", () => {
|
|
1501
|
+
const claudeLike = { id: "claude", buildDirectSpawn: () => [] as string[] };
|
|
1502
|
+
const claudeNoSpawn = { id: "claude" };
|
|
1503
|
+
const saplingLike = {
|
|
1504
|
+
id: "sapling",
|
|
1505
|
+
headless: true as const,
|
|
1506
|
+
buildDirectSpawn: () => [] as string[],
|
|
1507
|
+
};
|
|
1508
|
+
const codexLike = { id: "codex" };
|
|
1509
|
+
const baseConfig = {} as OverstoryConfig;
|
|
1510
|
+
const headlessByDefaultConfig = {
|
|
1511
|
+
runtime: { default: "claude", claudeHeadlessByDefault: true },
|
|
1512
|
+
} as unknown as OverstoryConfig;
|
|
1513
|
+
|
|
1514
|
+
test("statically headless runtime returns true regardless of flag", () => {
|
|
1515
|
+
expect(resolveUseHeadless(saplingLike, undefined, baseConfig)).toBe(true);
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
test("claude + no flag + base config returns false (default tmux)", () => {
|
|
1519
|
+
expect(resolveUseHeadless(claudeLike, undefined, baseConfig)).toBe(false);
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
test("claude + no flag + claudeHeadlessByDefault:true returns true", () => {
|
|
1523
|
+
expect(resolveUseHeadless(claudeLike, undefined, headlessByDefaultConfig)).toBe(true);
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
test("claude + flag:true + base config returns true", () => {
|
|
1527
|
+
expect(resolveUseHeadless(claudeLike, true, baseConfig)).toBe(true);
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
test("claude + flag:false + claudeHeadlessByDefault:true returns false (flag wins)", () => {
|
|
1531
|
+
expect(resolveUseHeadless(claudeLike, false, headlessByDefaultConfig)).toBe(false);
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
test("claude without buildDirectSpawn + flag:true throws ValidationError", () => {
|
|
1535
|
+
expect(() => resolveUseHeadless(claudeNoSpawn, true, baseConfig)).toThrow(ValidationError);
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
test("codex + claudeHeadlessByDefault:true returns false (config knob is Claude-only)", () => {
|
|
1539
|
+
expect(resolveUseHeadless(codexLike, undefined, headlessByDefaultConfig)).toBe(false);
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
test("codex + flag:true throws ValidationError (no buildDirectSpawn)", () => {
|
|
1543
|
+
expect(() => resolveUseHeadless(codexLike, true, baseConfig)).toThrow(ValidationError);
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
test("sapling + flag:false returns true (statically headless wins over flag)", () => {
|
|
1547
|
+
expect(resolveUseHeadless(saplingLike, false, baseConfig)).toBe(true);
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
describe("parseSiblings (overstory-f76a)", () => {
|
|
1552
|
+
test("undefined input returns empty array", () => {
|
|
1553
|
+
expect(parseSiblings(undefined)).toEqual([]);
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
test("empty string returns empty array", () => {
|
|
1557
|
+
expect(parseSiblings("")).toEqual([]);
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
test("single name returns one-element array", () => {
|
|
1561
|
+
expect(parseSiblings("sibling-a")).toEqual(["sibling-a"]);
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
test("comma-separated names are split and trimmed", () => {
|
|
1565
|
+
expect(parseSiblings("sibling-a, sibling-b ,sibling-c")).toEqual([
|
|
1566
|
+
"sibling-a",
|
|
1567
|
+
"sibling-b",
|
|
1568
|
+
"sibling-c",
|
|
1569
|
+
]);
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
test("blank entries between commas are dropped", () => {
|
|
1573
|
+
expect(parseSiblings("sibling-a,,sibling-b, ,sibling-c")).toEqual([
|
|
1574
|
+
"sibling-a",
|
|
1575
|
+
"sibling-b",
|
|
1576
|
+
"sibling-c",
|
|
1577
|
+
]);
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
test("whitespace-only input returns empty array", () => {
|
|
1581
|
+
expect(parseSiblings(" ")).toEqual([]);
|
|
1582
|
+
});
|
|
1583
|
+
});
|