macro-agent 0.1.8 → 0.1.11
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/CLAUDE.md +263 -33
- package/README.md +781 -131
- package/dist/acp/claude-code-replay.d.ts +11 -0
- package/dist/acp/claude-code-replay.d.ts.map +1 -0
- package/dist/acp/claude-code-replay.js +190 -0
- package/dist/acp/claude-code-replay.js.map +1 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +192 -7
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +9 -0
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/adapters/tasks-adapter.d.ts.map +1 -1
- package/dist/adapters/tasks-adapter.js +3 -0
- package/dist/adapters/tasks-adapter.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.d.ts +21 -0
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +308 -54
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +12 -0
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/agent-store.d.ts +10 -0
- package/dist/agent/agent-store.d.ts.map +1 -1
- package/dist/agent/agent-store.js +22 -0
- package/dist/agent/agent-store.js.map +1 -1
- package/dist/agent/types.d.ts +15 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/boot-v2.d.ts +129 -1
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +359 -8
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/acp.js +4 -0
- package/dist/cli/acp.js.map +1 -1
- package/dist/cli/index.js +56 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
- package/dist/cognitive/macro-agent-backend.js +40 -22
- package/dist/cognitive/macro-agent-backend.js.map +1 -1
- package/dist/integrations/skilltree.d.ts.map +1 -1
- package/dist/integrations/skilltree.js +1 -0
- package/dist/integrations/skilltree.js.map +1 -1
- package/dist/lifecycle/cascade.d.ts +25 -2
- package/dist/lifecycle/cascade.d.ts.map +1 -1
- package/dist/lifecycle/cascade.js +70 -2
- package/dist/lifecycle/cascade.js.map +1 -1
- package/dist/lifecycle/cleanup.d.ts +33 -2
- package/dist/lifecycle/cleanup.d.ts.map +1 -1
- package/dist/lifecycle/cleanup.js +28 -6
- package/dist/lifecycle/cleanup.js.map +1 -1
- package/dist/lifecycle/handlers-v2.d.ts +7 -0
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +28 -2
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/lifecycle/types.d.ts +11 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/map/acp-bridge.d.ts +9 -0
- package/dist/map/acp-bridge.d.ts.map +1 -1
- package/dist/map/acp-bridge.js +15 -2
- package/dist/map/acp-bridge.js.map +1 -1
- package/dist/map/cascade-action-handler.d.ts +24 -0
- package/dist/map/cascade-action-handler.d.ts.map +1 -0
- package/dist/map/cascade-action-handler.js +170 -0
- package/dist/map/cascade-action-handler.js.map +1 -0
- package/dist/map/cascade-bridge.d.ts +44 -0
- package/dist/map/cascade-bridge.d.ts.map +1 -0
- package/dist/map/cascade-bridge.js +294 -0
- package/dist/map/cascade-bridge.js.map +1 -0
- package/dist/map/coordination-handler.d.ts.map +1 -1
- package/dist/map/coordination-handler.js +12 -1
- package/dist/map/coordination-handler.js.map +1 -1
- package/dist/map/lifecycle-bridge.d.ts +1 -1
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +58 -23
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +219 -7
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +49 -2
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +22 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +8 -0
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/teams/team-manager-v2.d.ts.map +1 -1
- package/dist/teams/team-manager-v2.js +26 -0
- package/dist/teams/team-manager-v2.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +16 -3
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/dist/workspace/config.d.ts +10 -10
- package/dist/workspace/config.d.ts.map +1 -1
- package/dist/workspace/config.js +4 -4
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/git-cascade-adapter.d.ts +510 -0
- package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
- package/dist/workspace/git-cascade-adapter.js +934 -0
- package/dist/workspace/git-cascade-adapter.js.map +1 -0
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +4 -4
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/landing/direct-push.d.ts +20 -0
- package/dist/workspace/landing/direct-push.d.ts.map +1 -0
- package/dist/workspace/landing/direct-push.js +74 -0
- package/dist/workspace/landing/direct-push.js.map +1 -0
- package/dist/workspace/landing/index.d.ts +29 -0
- package/dist/workspace/landing/index.d.ts.map +1 -0
- package/dist/workspace/landing/index.js +37 -0
- package/dist/workspace/landing/index.js.map +1 -0
- package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
- package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
- package/dist/workspace/landing/merge-to-parent.js +186 -0
- package/dist/workspace/landing/merge-to-parent.js.map +1 -0
- package/dist/workspace/landing/optimistic-push.d.ts +16 -0
- package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
- package/dist/workspace/landing/optimistic-push.js +27 -0
- package/dist/workspace/landing/optimistic-push.js.map +1 -0
- package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
- package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
- package/dist/workspace/landing/queue-to-branch.js +79 -0
- package/dist/workspace/landing/queue-to-branch.js.map +1 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
- package/dist/workspace/merge-queue/merge-queue.js +10 -0
- package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
- package/dist/workspace/merge-queue/types.d.ts +16 -2
- package/dist/workspace/merge-queue/types.d.ts.map +1 -1
- package/dist/workspace/merge-queue/types.js +9 -0
- package/dist/workspace/merge-queue/types.js.map +1 -1
- package/dist/workspace/pool/types.d.ts +1 -0
- package/dist/workspace/pool/types.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.js +1 -0
- package/dist/workspace/pool/worktree-pool.js.map +1 -1
- package/dist/workspace/recovery/abandon.d.ts +15 -0
- package/dist/workspace/recovery/abandon.d.ts.map +1 -0
- package/dist/workspace/recovery/abandon.js +45 -0
- package/dist/workspace/recovery/abandon.js.map +1 -0
- package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
- package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
- package/dist/workspace/recovery/auto-resolve.js +99 -0
- package/dist/workspace/recovery/auto-resolve.js.map +1 -0
- package/dist/workspace/recovery/defer.d.ts +15 -0
- package/dist/workspace/recovery/defer.d.ts.map +1 -0
- package/dist/workspace/recovery/defer.js +16 -0
- package/dist/workspace/recovery/defer.js.map +1 -0
- package/dist/workspace/recovery/escalate.d.ts +16 -0
- package/dist/workspace/recovery/escalate.d.ts.map +1 -0
- package/dist/workspace/recovery/escalate.js +24 -0
- package/dist/workspace/recovery/escalate.js.map +1 -0
- package/dist/workspace/recovery/index.d.ts +32 -0
- package/dist/workspace/recovery/index.d.ts.map +1 -0
- package/dist/workspace/recovery/index.js +45 -0
- package/dist/workspace/recovery/index.js.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.js +118 -0
- package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
- package/dist/workspace/recovery/types.d.ts +63 -0
- package/dist/workspace/recovery/types.d.ts.map +1 -0
- package/dist/workspace/recovery/types.js +12 -0
- package/dist/workspace/recovery/types.js.map +1 -0
- package/dist/workspace/topology/index.d.ts +9 -0
- package/dist/workspace/topology/index.d.ts.map +1 -0
- package/dist/workspace/topology/index.js +8 -0
- package/dist/workspace/topology/index.js.map +1 -0
- package/dist/workspace/topology/no-workspace.d.ts +18 -0
- package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
- package/dist/workspace/topology/no-workspace.js +25 -0
- package/dist/workspace/topology/no-workspace.js.map +1 -0
- package/dist/workspace/topology/types.d.ts +97 -0
- package/dist/workspace/topology/types.d.ts.map +1 -0
- package/dist/workspace/topology/types.js +20 -0
- package/dist/workspace/topology/types.js.map +1 -0
- package/dist/workspace/topology/yaml-driven.d.ts +69 -0
- package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
- package/dist/workspace/topology/yaml-driven.js +273 -0
- package/dist/workspace/topology/yaml-driven.js.map +1 -0
- package/dist/workspace/types-v3.d.ts +117 -0
- package/dist/workspace/types-v3.d.ts.map +1 -0
- package/dist/workspace/types-v3.js +20 -0
- package/dist/workspace/types-v3.js.map +1 -0
- package/dist/workspace/types.d.ts +162 -17
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +101 -13
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +416 -13
- package/dist/workspace/workspace-manager.js.map +1 -1
- package/dist/workspace/yaml-schema.d.ts +254 -0
- package/dist/workspace/yaml-schema.d.ts.map +1 -0
- package/dist/workspace/yaml-schema.js +170 -0
- package/dist/workspace/yaml-schema.js.map +1 -0
- package/docs/conflict-recovery.md +472 -0
- package/docs/design/task-dispatcher.md +880 -0
- package/docs/git-cascade-integration-gaps.md +678 -0
- package/docs/workspace-interfaces.md +731 -0
- package/docs/workspace-redesign-plan.md +302 -0
- package/package.json +6 -5
- package/src/__tests__/boot-v2.test.ts +435 -0
- package/src/__tests__/e2e/acp-over-map.e2e.test.ts +92 -0
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
- package/src/__tests__/e2e/bootstrap.e2e.test.ts +319 -0
- package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
- package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
- package/src/__tests__/e2e/dispatch-coordination.e2e.test.ts +495 -0
- package/src/__tests__/e2e/dispatch-live.e2e.test.ts +564 -0
- package/src/__tests__/e2e/dispatch-opentasks.e2e.test.ts +496 -0
- package/src/__tests__/e2e/dispatch-phase2-live.e2e.test.ts +456 -0
- package/src/__tests__/e2e/dispatch-phase2.e2e.test.ts +386 -0
- package/src/__tests__/e2e/dispatch.e2e.test.ts +376 -0
- package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
- package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
- package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
- package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
- package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
- package/src/acp/__tests__/macro-agent.test.ts +39 -1
- package/src/acp/claude-code-replay.ts +208 -0
- package/src/acp/macro-agent.ts +203 -10
- package/src/acp/types.ts +10 -0
- package/src/adapters/__tests__/tasks-adapter.test.ts +1 -0
- package/src/adapters/tasks-adapter.ts +3 -0
- package/src/adapters/types.ts +1 -0
- package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
- package/src/agent/__tests__/agent-manager-v2.test.ts +66 -0
- package/src/agent/__tests__/agent-store.test.ts +52 -0
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +372 -59
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/agent-store.ts +24 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +589 -35
- package/src/cli/acp.ts +4 -0
- package/src/cli/index.ts +61 -0
- package/src/cognitive/macro-agent-backend.ts +45 -29
- package/src/integrations/skilltree.ts +1 -0
- package/src/lifecycle/__tests__/cascade-consolidation.test.ts +240 -0
- package/src/lifecycle/cascade.ts +77 -2
- package/src/lifecycle/cleanup.ts +52 -3
- package/src/lifecycle/handlers-v2.ts +40 -3
- package/src/lifecycle/types.ts +12 -0
- package/src/map/__tests__/cascade-bridge.test.ts +229 -0
- package/src/map/__tests__/emit-event.test.ts +71 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-action-handler.ts +205 -0
- package/src/map/cascade-bridge.ts +339 -0
- package/src/map/coordination-handler.ts +13 -1
- package/src/map/lifecycle-bridge.ts +52 -17
- package/src/map/server.ts +225 -7
- package/src/map/sidecar.ts +48 -1
- package/src/map/types.ts +23 -0
- package/src/mcp/tools/done-v2.ts +9 -0
- package/src/teams/team-manager-v2.ts +37 -0
- package/src/teams/team-runtime-v2.ts +23 -3
- package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
- package/src/workspace/__tests__/land-dispatch.test.ts +214 -0
- package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
- package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
- package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
- package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
- package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
- package/src/workspace/config.ts +11 -11
- package/src/workspace/git-cascade-adapter.ts +1213 -0
- package/src/workspace/index.ts +11 -11
- package/src/workspace/landing/__tests__/strategies.test.ts +184 -0
- package/src/workspace/landing/direct-push.ts +91 -0
- package/src/workspace/landing/index.ts +40 -0
- package/src/workspace/landing/merge-to-parent.ts +229 -0
- package/src/workspace/landing/optimistic-push.ts +36 -0
- package/src/workspace/landing/queue-to-branch.ts +108 -0
- package/src/workspace/merge-queue/merge-queue.ts +10 -0
- package/src/workspace/merge-queue/types.ts +16 -2
- package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
- package/src/workspace/pool/types.ts +1 -0
- package/src/workspace/pool/worktree-pool.ts +1 -0
- package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
- package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
- package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
- package/src/workspace/recovery/abandon.ts +51 -0
- package/src/workspace/recovery/auto-resolve.ts +119 -0
- package/src/workspace/recovery/defer.ts +23 -0
- package/src/workspace/recovery/escalate.ts +30 -0
- package/src/workspace/recovery/index.ts +58 -0
- package/src/workspace/recovery/spawn-resolver.ts +152 -0
- package/src/workspace/recovery/types.ts +54 -0
- package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
- package/src/workspace/topology/index.ts +18 -0
- package/src/workspace/topology/no-workspace.ts +39 -0
- package/src/workspace/topology/types.ts +116 -0
- package/src/workspace/topology/yaml-driven.ts +316 -0
- package/src/workspace/types-v3.ts +162 -0
- package/src/workspace/types.ts +211 -20
- package/src/workspace/workspace-manager.ts +533 -19
- package/src/workspace/yaml-schema.ts +216 -0
- package/dist/workspace/dataplane-adapter.d.ts +0 -260
- package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
- package/dist/workspace/dataplane-adapter.js +0 -416
- package/dist/workspace/dataplane-adapter.js.map +0 -1
- package/src/workspace/dataplane-adapter.ts +0 -546
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Dispatch + OpenTasks Integration E2E Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the full dispatch lifecycle with REAL opentasks daemon AND
|
|
5
|
+
* REAL Claude Code agents. No mocking of task data or agent processes.
|
|
6
|
+
*
|
|
7
|
+
* End-to-end flow:
|
|
8
|
+
* opentasks daemon → tasks created → dispatch strategy polls →
|
|
9
|
+
* claims task → spawns real agent → agent works → done() →
|
|
10
|
+
* lifecycle listener → task transitioned → reconciliation
|
|
11
|
+
*
|
|
12
|
+
* REQUIRES: RUN_FULL_AGENT_TESTS=true
|
|
13
|
+
*
|
|
14
|
+
* Run with:
|
|
15
|
+
* RUN_FULL_AGENT_TESTS=true npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/dispatch-opentasks.e2e.test.ts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import * as os from "node:os";
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { bootV2, type MacroAgentSystemV2 } from "../../boot-v2.js";
|
|
24
|
+
import {
|
|
25
|
+
ensureOpentasksDaemon,
|
|
26
|
+
type DaemonHandle,
|
|
27
|
+
} from "../../adapters/opentasks-daemon.js";
|
|
28
|
+
import {
|
|
29
|
+
createTaskDispatcher,
|
|
30
|
+
type TaskDispatcher,
|
|
31
|
+
type DispatchAgentRuntime,
|
|
32
|
+
type DispatchTaskSource,
|
|
33
|
+
} from "swarm-dispatch";
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────
|
|
36
|
+
// Configuration
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
|
|
40
|
+
const describeFn = RUN_FULL_AGENT ? describe : describe.skip;
|
|
41
|
+
|
|
42
|
+
const TIMEOUT = {
|
|
43
|
+
SETUP: 30_000,
|
|
44
|
+
DISPATCH: 120_000,
|
|
45
|
+
MULTI: 180_000,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────
|
|
49
|
+
// Helpers
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function createTempDir(prefix = "dispatch-ot"): string {
|
|
53
|
+
const suffix = Math.random().toString(36).slice(2, 8);
|
|
54
|
+
const dir = path.join(os.tmpdir(), `${prefix}-${suffix}`);
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function initGitRepo(dir: string): void {
|
|
60
|
+
execFileSync("git", ["init"], { cwd: dir, stdio: "pipe" });
|
|
61
|
+
execFileSync("git", ["config", "user.email", "test@e2e.dev"], {
|
|
62
|
+
cwd: dir,
|
|
63
|
+
stdio: "pipe",
|
|
64
|
+
});
|
|
65
|
+
execFileSync("git", ["config", "user.name", "E2E Test"], {
|
|
66
|
+
cwd: dir,
|
|
67
|
+
stdio: "pipe",
|
|
68
|
+
});
|
|
69
|
+
execFileSync("git", ["config", "commit.gpgsign", "false"], {
|
|
70
|
+
cwd: dir,
|
|
71
|
+
stdio: "pipe",
|
|
72
|
+
});
|
|
73
|
+
fs.writeFileSync(path.join(dir, "README.md"), "# Dispatch E2E\n");
|
|
74
|
+
execFileSync("git", ["add", "."], { cwd: dir, stdio: "pipe" });
|
|
75
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd: dir, stdio: "pipe" });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cleanupDir(dir: string): void {
|
|
79
|
+
try {
|
|
80
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
} catch {
|
|
82
|
+
// best-effort
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function log(msg: string): void {
|
|
87
|
+
console.log(`[DISPATCH-OT] ${msg}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sleep(ms: number): Promise<void> {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────
|
|
95
|
+
// Test Suite
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describeFn("Task Dispatch + OpenTasks E2E", () => {
|
|
99
|
+
// Per-test: each test gets its own daemon for isolation
|
|
100
|
+
let daemonDir: string;
|
|
101
|
+
let registryDir: string;
|
|
102
|
+
let daemonHandle: DaemonHandle;
|
|
103
|
+
let canStartDaemon = false;
|
|
104
|
+
|
|
105
|
+
let system: MacroAgentSystemV2;
|
|
106
|
+
let dispatcher: TaskDispatcher;
|
|
107
|
+
let testRepoDir: string;
|
|
108
|
+
|
|
109
|
+
function createSourceAdapter(tasksAdapter: typeof system.tasksAdapter): DispatchTaskSource {
|
|
110
|
+
return {
|
|
111
|
+
queryReady: (opts) => tasksAdapter.queryReady(opts),
|
|
112
|
+
claim: async (taskId, claimantId) => {
|
|
113
|
+
try {
|
|
114
|
+
await tasksAdapter.assignTask(taskId, claimantId);
|
|
115
|
+
return { success: true as const };
|
|
116
|
+
} catch { return { success: false as const }; }
|
|
117
|
+
},
|
|
118
|
+
release: async (taskId) => tasksAdapter.unclaimTask(taskId),
|
|
119
|
+
transition: async (taskId, action) => tasksAdapter.transitionTask(taskId, action),
|
|
120
|
+
getTask: async (taskId) => tasksAdapter.getTask(taskId),
|
|
121
|
+
listInProgress: async () => tasksAdapter.listTasks({ status: "in_progress" }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createRuntimeAdapter(agentManager: typeof system.agentManager): DispatchAgentRuntime {
|
|
126
|
+
return {
|
|
127
|
+
spawn: async (opts) => {
|
|
128
|
+
const spawned = await agentManager.spawn({
|
|
129
|
+
task: opts.prompt, task_id: opts.taskId, role: opts.role, parent: null,
|
|
130
|
+
});
|
|
131
|
+
return { id: spawned.id };
|
|
132
|
+
},
|
|
133
|
+
terminate: async (agentId) => agentManager.terminate(agentId, "cancelled"),
|
|
134
|
+
onStopped: (cb) => agentManager.onLifecycleEvent((event) => {
|
|
135
|
+
if (event.type === "stopped") cb(event.agent.id, event.reason);
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
beforeEach(async () => {
|
|
141
|
+
try {
|
|
142
|
+
daemonDir = createTempDir("ot-dmn");
|
|
143
|
+
registryDir = createTempDir("ot-reg");
|
|
144
|
+
initGitRepo(daemonDir);
|
|
145
|
+
|
|
146
|
+
daemonHandle = await ensureOpentasksDaemon(daemonDir, {
|
|
147
|
+
timeoutMs: 15_000,
|
|
148
|
+
registryPath: path.join(registryDir, "registry.json"),
|
|
149
|
+
});
|
|
150
|
+
canStartDaemon = true;
|
|
151
|
+
log(`Daemon started at ${daemonHandle.socketPath}`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.warn(
|
|
154
|
+
`[dispatch-opentasks] Skipping: daemon could not start: ${
|
|
155
|
+
err instanceof Error ? err.message : String(err)
|
|
156
|
+
}`
|
|
157
|
+
);
|
|
158
|
+
canStartDaemon = false;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
testRepoDir = createTempDir("dispatch-repo");
|
|
163
|
+
initGitRepo(testRepoDir);
|
|
164
|
+
|
|
165
|
+
const baseDir = path.join(testRepoDir, ".macro-agent");
|
|
166
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
167
|
+
|
|
168
|
+
system = await bootV2({
|
|
169
|
+
cwd: testRepoDir,
|
|
170
|
+
baseDir,
|
|
171
|
+
defaultPermissionMode: "auto-approve",
|
|
172
|
+
inbox: { socketPath: path.join(baseDir, "inbox.sock") },
|
|
173
|
+
tasks: { socketPath: daemonHandle.socketPath },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
dispatcher = createTaskDispatcher(
|
|
177
|
+
createSourceAdapter(system.tasksAdapter),
|
|
178
|
+
createRuntimeAdapter(system.agentManager),
|
|
179
|
+
{
|
|
180
|
+
claimantId: `test:${process.pid}:dispatch-ot`,
|
|
181
|
+
pollIntervalMs: 600_000,
|
|
182
|
+
defaultRole: "worker",
|
|
183
|
+
concurrency: { global: 3 },
|
|
184
|
+
retry: { maxRetries: 3, baseDelayMs: 1_000, maxDelayMs: 60_000 },
|
|
185
|
+
reconcile: { enabled: true, intervalMs: 600_000 },
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
await dispatcher.start();
|
|
189
|
+
|
|
190
|
+
log("System booted with real opentasks + swarm-dispatch");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
afterEach(async () => {
|
|
194
|
+
if (dispatcher) await dispatcher.stop();
|
|
195
|
+
if (system) {
|
|
196
|
+
try {
|
|
197
|
+
const running = system.agentManager.list({ state: "running" } as any);
|
|
198
|
+
for (const agent of running) {
|
|
199
|
+
try {
|
|
200
|
+
await system.agentManager.terminate(agent.id, "cancelled");
|
|
201
|
+
} catch { /* best effort */ }
|
|
202
|
+
}
|
|
203
|
+
await system.shutdown();
|
|
204
|
+
} catch { /* best effort */ }
|
|
205
|
+
}
|
|
206
|
+
if (daemonHandle) {
|
|
207
|
+
try { await daemonHandle.stop(); } catch { /* best effort */ }
|
|
208
|
+
}
|
|
209
|
+
if (testRepoDir) cleanupDir(testRepoDir);
|
|
210
|
+
if (daemonDir) cleanupDir(daemonDir);
|
|
211
|
+
if (registryDir) cleanupDir(registryDir);
|
|
212
|
+
log("Cleanup complete");
|
|
213
|
+
}, 30_000);
|
|
214
|
+
|
|
215
|
+
// ── Full loop: create task → dispatch → agent completes ────
|
|
216
|
+
|
|
217
|
+
it(
|
|
218
|
+
"creates a real task, dispatches to a real agent, agent completes",
|
|
219
|
+
async () => {
|
|
220
|
+
if (!canStartDaemon) return;
|
|
221
|
+
|
|
222
|
+
// 1. Create a task in the real opentasks daemon
|
|
223
|
+
log("Creating task in opentasks...");
|
|
224
|
+
const taskId = await system.tasksAdapter.createTask({
|
|
225
|
+
title: "Write a haiku",
|
|
226
|
+
content:
|
|
227
|
+
'Create a file called haiku.txt with a haiku about code. ' +
|
|
228
|
+
'Then call the "done" MCP tool with status="completed" and summary="Created haiku.txt".',
|
|
229
|
+
tags: ["auto", "e2e"],
|
|
230
|
+
priority: 3,
|
|
231
|
+
});
|
|
232
|
+
log(`Task created: ${taskId}`);
|
|
233
|
+
|
|
234
|
+
// Verify task is queryable
|
|
235
|
+
const readyBefore = await system.tasksAdapter.queryReady();
|
|
236
|
+
log(`Ready tasks before dispatch: ${readyBefore.length}`);
|
|
237
|
+
expect(readyBefore.some((t) => t.id === taskId)).toBe(true);
|
|
238
|
+
|
|
239
|
+
// 2. Trigger dispatch
|
|
240
|
+
log("Triggering dispatch...");
|
|
241
|
+
await dispatcher.dispatchNow();
|
|
242
|
+
|
|
243
|
+
await sleep(3_000);
|
|
244
|
+
|
|
245
|
+
// 3. Verify agent was spawned
|
|
246
|
+
log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
|
|
247
|
+
expect(dispatcher.tracker.activeCount()).toBeGreaterThanOrEqual(1);
|
|
248
|
+
|
|
249
|
+
const active = dispatcher.tracker.listActive();
|
|
250
|
+
const dispatch = active.find((d) => d.taskId === taskId);
|
|
251
|
+
expect(dispatch).toBeDefined();
|
|
252
|
+
|
|
253
|
+
const agentId = dispatch!.agentId;
|
|
254
|
+
log(`Dispatched agent: ${agentId}`);
|
|
255
|
+
|
|
256
|
+
const agentRecord = system.agentStore.getAgent(agentId);
|
|
257
|
+
expect(agentRecord).not.toBeNull();
|
|
258
|
+
expect(agentRecord!.state).toBe("running");
|
|
259
|
+
expect(agentRecord!.parent_id).toBeNull(); // Parentless
|
|
260
|
+
|
|
261
|
+
// 4. Prompt agent to complete its work
|
|
262
|
+
log("Prompting agent to complete task...");
|
|
263
|
+
const result = await system.agentManager.promptUntilDone(
|
|
264
|
+
agentId,
|
|
265
|
+
'Complete your task: create haiku.txt with a haiku about code, then call done(status="completed", summary="Created haiku.txt").',
|
|
266
|
+
{
|
|
267
|
+
maxFollowUps: 3,
|
|
268
|
+
onUpdate: (update: any) => {
|
|
269
|
+
if (update.sessionUpdate === "tool_call") {
|
|
270
|
+
log(` [tool_call] ${update.title ?? "unknown"}`);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
log(`promptUntilDone: doneCalled=${result.doneCalled}, status=${result.doneStatus}`);
|
|
277
|
+
|
|
278
|
+
// Wait for lifecycle listener
|
|
279
|
+
await sleep(3_000);
|
|
280
|
+
|
|
281
|
+
// 5. Verify completion
|
|
282
|
+
if (result.doneCalled && result.doneStatus === "completed") {
|
|
283
|
+
log("Agent called done(completed)");
|
|
284
|
+
|
|
285
|
+
// Task should be removed from tracker
|
|
286
|
+
expect(dispatcher.tracker.isTracked(taskId)).toBe(false);
|
|
287
|
+
|
|
288
|
+
// Agent should be stopped
|
|
289
|
+
const finalAgent = system.agentStore.getAgent(agentId);
|
|
290
|
+
expect(finalAgent?.state).toBe("stopped");
|
|
291
|
+
|
|
292
|
+
// Verify haiku.txt was created
|
|
293
|
+
const haikuPath = path.join(testRepoDir, "haiku.txt");
|
|
294
|
+
if (fs.existsSync(haikuPath)) {
|
|
295
|
+
const content = fs.readFileSync(haikuPath, "utf-8");
|
|
296
|
+
log(`haiku.txt: ${content.trim().substring(0, 80)}`);
|
|
297
|
+
expect(content.length).toBeGreaterThan(0);
|
|
298
|
+
} else {
|
|
299
|
+
log("haiku.txt not found (agent may have written elsewhere)");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Verify the task was transitioned in opentasks
|
|
303
|
+
// (The lifecycle listener calls transitionTask("complete"))
|
|
304
|
+
const taskAfter = await system.tasksAdapter.getTask(taskId);
|
|
305
|
+
log(`Task status after completion: ${taskAfter.status}`);
|
|
306
|
+
|
|
307
|
+
// Ready list should no longer contain this task
|
|
308
|
+
const readyAfter = await system.tasksAdapter.queryReady();
|
|
309
|
+
expect(readyAfter.some((t) => t.id === taskId)).toBe(false);
|
|
310
|
+
} else {
|
|
311
|
+
log("Agent did not call done(completed) — LLM behavior variance");
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
TIMEOUT.MULTI
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// ── Dispatch with dependencies: blocked task not dispatched ──
|
|
318
|
+
|
|
319
|
+
it(
|
|
320
|
+
"does not dispatch blocked tasks, dispatches when unblocked",
|
|
321
|
+
async () => {
|
|
322
|
+
if (!canStartDaemon) return;
|
|
323
|
+
|
|
324
|
+
// Create two tasks: taskB blocked by taskA
|
|
325
|
+
log("Creating tasks with dependency...");
|
|
326
|
+
const taskAId = await system.tasksAdapter.createTask({
|
|
327
|
+
title: "Prerequisite task A",
|
|
328
|
+
content: 'Say "done" and call done(status="completed").',
|
|
329
|
+
tags: ["auto"],
|
|
330
|
+
});
|
|
331
|
+
const taskBId = await system.tasksAdapter.createTask({
|
|
332
|
+
title: "Dependent task B",
|
|
333
|
+
content: 'Say "done" and call done(status="completed").',
|
|
334
|
+
tags: ["auto"],
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// taskA blocks taskB
|
|
338
|
+
await system.tasksAdapter.addBlocker(taskBId, taskAId);
|
|
339
|
+
log(`Task ${taskAId} blocks ${taskBId}`);
|
|
340
|
+
|
|
341
|
+
// Verify only taskA is ready (taskB is blocked)
|
|
342
|
+
const readyBefore = await system.tasksAdapter.queryReady();
|
|
343
|
+
const readyIds = readyBefore.map((t) => t.id);
|
|
344
|
+
log(`Ready before: ${readyIds.join(", ")}`);
|
|
345
|
+
expect(readyIds).toContain(taskAId);
|
|
346
|
+
expect(readyIds).not.toContain(taskBId);
|
|
347
|
+
|
|
348
|
+
// Dispatch — should only pick up taskA
|
|
349
|
+
log("Triggering dispatch...");
|
|
350
|
+
await dispatcher.dispatchNow();
|
|
351
|
+
|
|
352
|
+
await sleep(3_000);
|
|
353
|
+
|
|
354
|
+
log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
|
|
355
|
+
const active = dispatcher.tracker.listActive();
|
|
356
|
+
const dispatchedIds = active.map((d) => d.taskId);
|
|
357
|
+
log(`Dispatched: ${dispatchedIds.join(", ")}`);
|
|
358
|
+
|
|
359
|
+
// taskA should be dispatched, taskB should not
|
|
360
|
+
expect(dispatchedIds).toContain(taskAId);
|
|
361
|
+
expect(dispatchedIds).not.toContain(taskBId);
|
|
362
|
+
|
|
363
|
+
// Complete taskA to unblock taskB
|
|
364
|
+
log("Completing taskA to unblock taskB...");
|
|
365
|
+
const agentAId = active.find((d) => d.taskId === taskAId)!.agentId;
|
|
366
|
+
await system.agentManager.terminate(agentAId, "completed");
|
|
367
|
+
await sleep(1_000);
|
|
368
|
+
|
|
369
|
+
// Remove the blocker
|
|
370
|
+
await system.tasksAdapter.removeBlocker(taskBId, taskAId);
|
|
371
|
+
|
|
372
|
+
// Verify taskB is now ready
|
|
373
|
+
const readyAfter = await system.tasksAdapter.queryReady();
|
|
374
|
+
const readyAfterIds = readyAfter.map((t) => t.id);
|
|
375
|
+
log(`Ready after unblock: ${readyAfterIds.join(", ")}`);
|
|
376
|
+
expect(readyAfterIds).toContain(taskBId);
|
|
377
|
+
|
|
378
|
+
// Dispatch again — should pick up taskB
|
|
379
|
+
await dispatcher.dispatchNow();
|
|
380
|
+
await sleep(3_000);
|
|
381
|
+
|
|
382
|
+
const activeAfter = dispatcher.tracker.listActive();
|
|
383
|
+
const dispatchedAfter = activeAfter.map((d) => d.taskId);
|
|
384
|
+
log(`Dispatched after unblock: ${dispatchedAfter.join(", ")}`);
|
|
385
|
+
expect(dispatchedAfter).toContain(taskBId);
|
|
386
|
+
},
|
|
387
|
+
TIMEOUT.MULTI
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// ── Multiple tasks dispatched concurrently ─────────────────
|
|
391
|
+
|
|
392
|
+
it(
|
|
393
|
+
"dispatches multiple ready tasks from opentasks concurrently",
|
|
394
|
+
async () => {
|
|
395
|
+
if (!canStartDaemon) return;
|
|
396
|
+
|
|
397
|
+
log("Creating 3 independent tasks...");
|
|
398
|
+
const ids: string[] = [];
|
|
399
|
+
for (let i = 0; i < 3; i++) {
|
|
400
|
+
const id = await system.tasksAdapter.createTask({
|
|
401
|
+
title: `Concurrent task ${i + 1}`,
|
|
402
|
+
content: "Wait for instructions.",
|
|
403
|
+
tags: ["auto", "concurrent"],
|
|
404
|
+
priority: 3,
|
|
405
|
+
});
|
|
406
|
+
ids.push(id);
|
|
407
|
+
}
|
|
408
|
+
log(`Created: ${ids.join(", ")}`);
|
|
409
|
+
|
|
410
|
+
// Verify all are ready
|
|
411
|
+
const ready = await system.tasksAdapter.queryReady();
|
|
412
|
+
const readyIds = ready.map((t) => t.id);
|
|
413
|
+
log(`Ready: ${readyIds.join(", ")}`);
|
|
414
|
+
for (const id of ids) {
|
|
415
|
+
expect(readyIds).toContain(id);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Dispatch
|
|
419
|
+
log("Triggering dispatch...");
|
|
420
|
+
await dispatcher.dispatchNow();
|
|
421
|
+
|
|
422
|
+
await sleep(3_000);
|
|
423
|
+
|
|
424
|
+
log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
|
|
425
|
+
expect(dispatcher.tracker.activeCount()).toBeGreaterThanOrEqual(3);
|
|
426
|
+
|
|
427
|
+
const active = dispatcher.tracker.listActive();
|
|
428
|
+
const dispatchedIds = active.map((d) => d.taskId);
|
|
429
|
+
log(`Dispatched: ${dispatchedIds.join(", ")}`);
|
|
430
|
+
|
|
431
|
+
// All 3 should be dispatched (global limit is 3)
|
|
432
|
+
for (const id of ids) {
|
|
433
|
+
expect(dispatchedIds).toContain(id);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Verify all agents are running
|
|
437
|
+
for (const record of active) {
|
|
438
|
+
if (!ids.includes(record.taskId)) continue;
|
|
439
|
+
const agent = system.agentStore.getAgent(record.agentId);
|
|
440
|
+
expect(agent).not.toBeNull();
|
|
441
|
+
expect(agent!.state).toBe("running");
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
TIMEOUT.DISPATCH
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// ── Reconcile detects externally completed task ────────────
|
|
448
|
+
|
|
449
|
+
it(
|
|
450
|
+
"reconciliation detects task completed externally in opentasks",
|
|
451
|
+
async () => {
|
|
452
|
+
if (!canStartDaemon) return;
|
|
453
|
+
|
|
454
|
+
// Create and dispatch a task
|
|
455
|
+
log("Creating task...");
|
|
456
|
+
const taskId = await system.tasksAdapter.createTask({
|
|
457
|
+
title: "Task to close externally",
|
|
458
|
+
content: "Wait for instructions.",
|
|
459
|
+
tags: ["auto"],
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
log("Dispatching...");
|
|
463
|
+
await dispatcher.dispatchNow();
|
|
464
|
+
await sleep(3_000);
|
|
465
|
+
|
|
466
|
+
log(`Tracker active: ${dispatcher.tracker.activeCount()}`);
|
|
467
|
+
expect(dispatcher.tracker.activeCount()).toBe(1);
|
|
468
|
+
|
|
469
|
+
const agentId = dispatcher.tracker.listActive().find((d) => d.taskId === taskId)!.agentId;
|
|
470
|
+
log(`Agent: ${agentId}`);
|
|
471
|
+
|
|
472
|
+
// Simulate external completion: transition task to closed directly
|
|
473
|
+
log("Closing task externally via opentasks...");
|
|
474
|
+
await system.tasksAdapter.transitionTask(taskId, "start");
|
|
475
|
+
await system.tasksAdapter.transitionTask(taskId, "complete");
|
|
476
|
+
|
|
477
|
+
const taskAfterClose = await system.tasksAdapter.getTask(taskId);
|
|
478
|
+
log(`Task status after external close: ${taskAfterClose.status}`);
|
|
479
|
+
expect(taskAfterClose.status).toBe("closed");
|
|
480
|
+
|
|
481
|
+
log("Triggering reconciliation...");
|
|
482
|
+
await dispatcher.reconcileNow();
|
|
483
|
+
await sleep(3_000);
|
|
484
|
+
|
|
485
|
+
// Task should be removed from tracker
|
|
486
|
+
log(`Tracker active after reconcile: ${dispatcher.tracker.activeCount()}`);
|
|
487
|
+
expect(dispatcher.tracker.isTracked(taskId)).toBe(false);
|
|
488
|
+
|
|
489
|
+
// Agent should be stopped
|
|
490
|
+
const agentAfter = system.agentStore.getAgent(agentId);
|
|
491
|
+
log(`Agent state after reconcile: ${agentAfter?.state}`);
|
|
492
|
+
expect(agentAfter?.state).toBe("stopped");
|
|
493
|
+
},
|
|
494
|
+
TIMEOUT.DISPATCH
|
|
495
|
+
);
|
|
496
|
+
});
|