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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `queue-to-branch` landing strategy.
|
|
3
|
+
*
|
|
4
|
+
* Adds the source stream to git-cascade's built-in merge queue targeting a
|
|
5
|
+
* named branch (default 'main' or configured via `strategyConfig.target`).
|
|
6
|
+
* The actual merge is drained by an integrator-capable agent using the
|
|
7
|
+
* `next_merge_request` / `merge_stream` / `mark_merge_complete` MCP tools.
|
|
8
|
+
*
|
|
9
|
+
* Strategy config:
|
|
10
|
+
* - `target`: "branch:<name>" | "stream:<id>" | "role:<role>" (default: "branch:main")
|
|
11
|
+
* - `priority`: number (lower = higher priority, default 100)
|
|
12
|
+
*
|
|
13
|
+
* Return value shape adapts to git-cascade's MergeResult:
|
|
14
|
+
* - `success: true` means the queue submission succeeded (not that merge happened).
|
|
15
|
+
*
|
|
16
|
+
* @module workspace/landing/queue-to-branch
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
LandingStrategy,
|
|
21
|
+
LandingContext,
|
|
22
|
+
MergeResult,
|
|
23
|
+
} from '../types-v3.js';
|
|
24
|
+
import { DefaultWorkspaceManager } from '../workspace-manager.js';
|
|
25
|
+
|
|
26
|
+
export class QueueToBranchStrategy implements LandingStrategy {
|
|
27
|
+
readonly name = 'queue-to-branch';
|
|
28
|
+
|
|
29
|
+
async land(ctx: LandingContext): Promise<MergeResult> {
|
|
30
|
+
const ws = ctx.workspaceManager as DefaultWorkspaceManager;
|
|
31
|
+
|
|
32
|
+
// Resolve the target branch name from strategyConfig.
|
|
33
|
+
const targetSpec = (ctx.strategyConfig?.target as string | undefined) ?? 'branch:main';
|
|
34
|
+
const targetBranch = this.resolveTargetBranch(targetSpec, ws, ctx);
|
|
35
|
+
|
|
36
|
+
if (!targetBranch) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: `queue-to-branch: could not resolve target from spec "${targetSpec}"`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const priority = (ctx.strategyConfig?.priority as number | undefined) ?? 100;
|
|
44
|
+
|
|
45
|
+
// Access the adapter through the WorkspaceManager's getMergeQueue shim.
|
|
46
|
+
// Direct call via the git-cascade adapter is made through a method on the
|
|
47
|
+
// manager that we expose for strategies.
|
|
48
|
+
try {
|
|
49
|
+
// Use the underlying adapter via the DefaultWorkspaceManager
|
|
50
|
+
const adapter = (ws as unknown as { adapter?: unknown }).adapter;
|
|
51
|
+
if (
|
|
52
|
+
adapter &&
|
|
53
|
+
typeof (adapter as { addToMergeQueue?: unknown }).addToMergeQueue === 'function'
|
|
54
|
+
) {
|
|
55
|
+
(
|
|
56
|
+
adapter as {
|
|
57
|
+
addToMergeQueue: (opts: {
|
|
58
|
+
streamId: string;
|
|
59
|
+
targetBranch: string;
|
|
60
|
+
priority?: number;
|
|
61
|
+
agentId: string;
|
|
62
|
+
}) => string;
|
|
63
|
+
}
|
|
64
|
+
).addToMergeQueue({
|
|
65
|
+
streamId: ctx.streamId,
|
|
66
|
+
targetBranch,
|
|
67
|
+
priority,
|
|
68
|
+
agentId: ctx.agentId,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: `queue-to-branch: enqueue failed — ${
|
|
75
|
+
err instanceof Error ? err.message : String(err)
|
|
76
|
+
}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { success: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private resolveTargetBranch(
|
|
84
|
+
spec: string,
|
|
85
|
+
ws: DefaultWorkspaceManager,
|
|
86
|
+
ctx: LandingContext
|
|
87
|
+
): string | null {
|
|
88
|
+
const [kind, value] = spec.split(':', 2);
|
|
89
|
+
switch (kind) {
|
|
90
|
+
case 'branch':
|
|
91
|
+
return value ?? 'main';
|
|
92
|
+
case 'stream': {
|
|
93
|
+
// Look up the stream; use its branch name. For now, we synthesize
|
|
94
|
+
// the default `stream/<id>` branch format git-cascade uses.
|
|
95
|
+
const allStreams = ws.listStreams();
|
|
96
|
+
const match = allStreams.find((s) => s.id === value);
|
|
97
|
+
if (!match) return null;
|
|
98
|
+
return `stream/${match.id}`;
|
|
99
|
+
}
|
|
100
|
+
case 'role':
|
|
101
|
+
// Role → agent lookup requires agentStore access; deferred. Use
|
|
102
|
+
// 'main' as fallback and log in a follow-up.
|
|
103
|
+
return 'main';
|
|
104
|
+
default:
|
|
105
|
+
return spec; // assume raw branch name
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* Coordinates merging of parallel worker branches into the integration branch.
|
|
5
5
|
* Workers submit completed work; Integrator processes sequentially.
|
|
6
6
|
*
|
|
7
|
+
* @deprecated Since v3 workspace redesign. Duplicates git-cascade's built-in
|
|
8
|
+
* `mergeQueue` module. New code uses `GitCascadeAdapter.addToMergeQueue`
|
|
9
|
+
* via `LandingStrategy` ("queue-to-branch"). Kept to preserve the legacy
|
|
10
|
+
* role-name dispatch path until teams migrate to `macro_agent.workspace`
|
|
11
|
+
* YAML. Scheduled for removal; see `docs/workspace-redesign-plan.md`
|
|
12
|
+
* Phases 6/8/9.
|
|
13
|
+
*
|
|
7
14
|
* @module workspace/merge-queue/merge-queue
|
|
8
15
|
* @implements [[s-bcqm]] Merge Queue section
|
|
9
16
|
*/
|
|
@@ -99,6 +106,9 @@ export interface MergeQueueConfig {
|
|
|
99
106
|
* MergeQueue implementation.
|
|
100
107
|
*
|
|
101
108
|
* Manages merge requests for coordinating parallel worker merges.
|
|
109
|
+
*
|
|
110
|
+
* @deprecated Use git-cascade's built-in queue via
|
|
111
|
+
* `GitCascadeAdapter.addToMergeQueue` / `getNextToMerge`. See module doc.
|
|
102
112
|
*/
|
|
103
113
|
export class MergeQueue implements MergeQueueInterface {
|
|
104
114
|
private readonly db: Database.Database;
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Types for the merge queue layer that coordinates parallel worker merges.
|
|
5
5
|
*
|
|
6
|
+
* @deprecated Since v3 workspace redesign. This module duplicates git-cascade's
|
|
7
|
+
* built-in `mergeQueue` (see `GitCascadeAdapter.addToMergeQueue`,
|
|
8
|
+
* `getNextToMerge`, etc.). New code should use `LandingStrategy`
|
|
9
|
+
* ("queue-to-branch") + git-cascade's queue directly. Kept in place to
|
|
10
|
+
* preserve the legacy role-name dispatch path (coordinator/worker/integrator)
|
|
11
|
+
* until teams migrate to `macro_agent.workspace` YAML. Scheduled for
|
|
12
|
+
* removal once self-driving is migrated. See `docs/workspace-redesign-plan.md`
|
|
13
|
+
* Phases 6/8/9.
|
|
14
|
+
*
|
|
6
15
|
* @module workspace/merge-queue/types
|
|
7
16
|
* @implements [[s-bcqm]] Merge Queue Schema section
|
|
8
17
|
*/
|
|
@@ -36,7 +45,7 @@ export interface MergeRequest {
|
|
|
36
45
|
/** Stream (integration branch) this MR targets */
|
|
37
46
|
streamId: string;
|
|
38
47
|
|
|
39
|
-
/**
|
|
48
|
+
/** git-cascade task ID this MR completes */
|
|
40
49
|
taskId: string;
|
|
41
50
|
|
|
42
51
|
/** Git branch containing the worker's changes */
|
|
@@ -83,7 +92,7 @@ export interface SubmitMergeRequestOptions {
|
|
|
83
92
|
/** Stream (integration branch) to merge into */
|
|
84
93
|
streamId: string;
|
|
85
94
|
|
|
86
|
-
/**
|
|
95
|
+
/** git-cascade task ID this completes */
|
|
87
96
|
taskId: string;
|
|
88
97
|
|
|
89
98
|
/** Git branch containing the worker's changes */
|
|
@@ -141,6 +150,11 @@ export type MergeQueueEventCallback = (event: MergeQueueEvent) => void;
|
|
|
141
150
|
* Coordinates merging of parallel worker branches into the integration branch.
|
|
142
151
|
* Workers submit completed work; Integrator processes sequentially.
|
|
143
152
|
*/
|
|
153
|
+
/**
|
|
154
|
+
* @deprecated Use git-cascade's built-in merge queue via
|
|
155
|
+
* `GitCascadeAdapter.addToMergeQueue` / `getNextToMerge` etc. See module
|
|
156
|
+
* doc for migration guidance.
|
|
157
|
+
*/
|
|
144
158
|
export interface MergeQueueInterface {
|
|
145
159
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
160
|
// Submit
|
|
@@ -18,7 +18,7 @@ import { execSync } from 'child_process';
|
|
|
18
18
|
import Database from 'better-sqlite3';
|
|
19
19
|
import { WorktreePool } from '../worktree-pool.js';
|
|
20
20
|
import { createWorkspaceManager, DefaultWorkspaceManager } from '../../workspace-manager.js';
|
|
21
|
-
import {
|
|
21
|
+
import { createGitCascadeAdapter, GitCascadeAdapter } from '../../git-cascade-adapter.js';
|
|
22
22
|
import type { PoolEvent, PoolStats } from '../types.js';
|
|
23
23
|
import type { WorkerWorkspace, IntegratorWorkspace, CoordinatorWorkspace } from '../../types.js';
|
|
24
24
|
|
|
@@ -694,12 +694,12 @@ describe('WorktreePool Integration', () => {
|
|
|
694
694
|
|
|
695
695
|
describe('WorkspaceManager integration', () => {
|
|
696
696
|
let db: Database.Database;
|
|
697
|
-
let adapter:
|
|
697
|
+
let adapter: GitCascadeAdapter;
|
|
698
698
|
let manager: DefaultWorkspaceManager;
|
|
699
699
|
|
|
700
700
|
beforeEach(() => {
|
|
701
701
|
db = new Database(dbPath);
|
|
702
|
-
adapter =
|
|
702
|
+
adapter = createGitCascadeAdapter({
|
|
703
703
|
enabled: true,
|
|
704
704
|
repoPath,
|
|
705
705
|
db,
|
|
@@ -944,12 +944,12 @@ describe('WorktreePool Integration', () => {
|
|
|
944
944
|
|
|
945
945
|
describe('full lifecycle with pool', () => {
|
|
946
946
|
let db: Database.Database;
|
|
947
|
-
let adapter:
|
|
947
|
+
let adapter: GitCascadeAdapter;
|
|
948
948
|
let manager: DefaultWorkspaceManager;
|
|
949
949
|
|
|
950
950
|
beforeEach(() => {
|
|
951
951
|
db = new Database(dbPath);
|
|
952
|
-
adapter =
|
|
952
|
+
adapter = createGitCascadeAdapter({
|
|
953
953
|
enabled: true,
|
|
954
954
|
repoPath,
|
|
955
955
|
db,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoResolveStrategy integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Creates a real merge conflict in a temp git repo and verifies the strategy
|
|
5
|
+
* replays the merge with `ours`/`theirs` and commits the resolution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { AutoResolveStrategy } from '../auto-resolve.js';
|
|
14
|
+
import type { WorkspaceManager } from '../../types.js';
|
|
15
|
+
import type { ConflictContext } from '../types.js';
|
|
16
|
+
|
|
17
|
+
function mockWorkspaceManager(): WorkspaceManager {
|
|
18
|
+
return {
|
|
19
|
+
resolveConflict: vi.fn(),
|
|
20
|
+
} as unknown as WorkspaceManager;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sh(cmd: string, cwd: string): string {
|
|
24
|
+
return execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8' }).trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('AutoResolveStrategy (real git)', () => {
|
|
28
|
+
let tempDir: string;
|
|
29
|
+
let repoPath: string;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'auto-resolve-'));
|
|
33
|
+
repoPath = path.join(tempDir, 'repo');
|
|
34
|
+
fs.mkdirSync(repoPath);
|
|
35
|
+
|
|
36
|
+
sh('git init -b main', repoPath);
|
|
37
|
+
sh('git config user.email "t@t.com"', repoPath);
|
|
38
|
+
sh('git config user.name "T"', repoPath);
|
|
39
|
+
|
|
40
|
+
// Base commit
|
|
41
|
+
fs.writeFileSync(path.join(repoPath, 'f.txt'), 'base\n');
|
|
42
|
+
sh('git add .', repoPath);
|
|
43
|
+
sh('git commit -m "base"', repoPath);
|
|
44
|
+
|
|
45
|
+
// Branch A (main) with change
|
|
46
|
+
fs.writeFileSync(path.join(repoPath, 'f.txt'), 'main-change\n');
|
|
47
|
+
sh('git add .', repoPath);
|
|
48
|
+
sh('git commit -m "main change"', repoPath);
|
|
49
|
+
|
|
50
|
+
// Branch B with conflicting change
|
|
51
|
+
sh('git checkout -b feature HEAD~1', repoPath);
|
|
52
|
+
fs.writeFileSync(path.join(repoPath, 'f.txt'), 'feature-change\n');
|
|
53
|
+
sh('git add .', repoPath);
|
|
54
|
+
sh('git commit -m "feature change"', repoPath);
|
|
55
|
+
|
|
56
|
+
// Checkout main + attempt merge (conflicts)
|
|
57
|
+
sh('git checkout main', repoPath);
|
|
58
|
+
try {
|
|
59
|
+
sh('git merge feature --no-edit', repoPath);
|
|
60
|
+
} catch {
|
|
61
|
+
// Expected — creates the conflict state
|
|
62
|
+
}
|
|
63
|
+
// Abort so AutoResolve starts from a clean state; it will re-trigger
|
|
64
|
+
// the merge via `git merge -X <strategy>`.
|
|
65
|
+
sh('git merge --abort', repoPath);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
if (fs.existsSync(tempDir)) {
|
|
70
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('resolves with -X ours — keeps main-change', async () => {
|
|
75
|
+
const strat = new AutoResolveStrategy();
|
|
76
|
+
const ws = mockWorkspaceManager();
|
|
77
|
+
|
|
78
|
+
const ctx: ConflictContext = {
|
|
79
|
+
conflictId: 'c-test',
|
|
80
|
+
streamId: 's-1',
|
|
81
|
+
paths: ['f.txt'],
|
|
82
|
+
operation: 'merge',
|
|
83
|
+
worktree: repoPath,
|
|
84
|
+
sourceCommit: 'feature',
|
|
85
|
+
recoveryDepth: 0,
|
|
86
|
+
strategyConfig: { strategy: 'ours' },
|
|
87
|
+
workspaceManager: ws,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = await strat.recover(ctx);
|
|
91
|
+
|
|
92
|
+
expect(result.kind).toBe('resolved');
|
|
93
|
+
if (result.kind === 'resolved') {
|
|
94
|
+
expect(result.resolutionCommit).toMatch(/^[0-9a-f]+$/);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify file has the 'ours' version
|
|
98
|
+
expect(fs.readFileSync(path.join(repoPath, 'f.txt'), 'utf-8')).toBe('main-change\n');
|
|
99
|
+
|
|
100
|
+
// WorkspaceManager.resolveConflict was notified
|
|
101
|
+
expect(ws.resolveConflict).toHaveBeenCalledWith(
|
|
102
|
+
expect.objectContaining({ conflictId: 'c-test' })
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('resolves with -X theirs — keeps feature-change', async () => {
|
|
107
|
+
const strat = new AutoResolveStrategy();
|
|
108
|
+
const ws = mockWorkspaceManager();
|
|
109
|
+
|
|
110
|
+
const ctx: ConflictContext = {
|
|
111
|
+
conflictId: 'c-test',
|
|
112
|
+
streamId: 's-1',
|
|
113
|
+
paths: ['f.txt'],
|
|
114
|
+
operation: 'merge',
|
|
115
|
+
worktree: repoPath,
|
|
116
|
+
sourceCommit: 'feature',
|
|
117
|
+
recoveryDepth: 0,
|
|
118
|
+
strategyConfig: { strategy: 'theirs' },
|
|
119
|
+
workspaceManager: ws,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const result = await strat.recover(ctx);
|
|
123
|
+
|
|
124
|
+
expect(result.kind).toBe('resolved');
|
|
125
|
+
expect(fs.readFileSync(path.join(repoPath, 'f.txt'), 'utf-8')).toBe('feature-change\n');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpawnResolverStrategy tests (Phase 7b).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
SpawnResolverStrategy,
|
|
8
|
+
createSpawnResolverStrategy,
|
|
9
|
+
} from '../spawn-resolver.js';
|
|
10
|
+
import type { ConflictContext } from '../types.js';
|
|
11
|
+
import type { WorkspaceManager } from '../../types.js';
|
|
12
|
+
import type { AgentManager } from '../../../agent/agent-manager.js';
|
|
13
|
+
|
|
14
|
+
type EventListener = (event: { type: string; data: Record<string, unknown> }) => void;
|
|
15
|
+
|
|
16
|
+
function mockManagers(): {
|
|
17
|
+
ws: WorkspaceManager;
|
|
18
|
+
am: AgentManager;
|
|
19
|
+
triggerResolved: (conflictId: string, commit: string) => void;
|
|
20
|
+
} {
|
|
21
|
+
const listeners = new Set<EventListener>();
|
|
22
|
+
|
|
23
|
+
const ws = {
|
|
24
|
+
onEvent: vi.fn((cb: EventListener) => {
|
|
25
|
+
listeners.add(cb);
|
|
26
|
+
return () => listeners.delete(cb);
|
|
27
|
+
}),
|
|
28
|
+
} as unknown as WorkspaceManager;
|
|
29
|
+
|
|
30
|
+
const am = {
|
|
31
|
+
spawn: vi.fn().mockResolvedValue({ id: 'resolver-agent-1' }),
|
|
32
|
+
} as unknown as AgentManager;
|
|
33
|
+
|
|
34
|
+
const triggerResolved = (conflictId: string, commit: string) => {
|
|
35
|
+
for (const listener of listeners) {
|
|
36
|
+
listener({
|
|
37
|
+
type: 'conflict:resolved',
|
|
38
|
+
data: { conflictId, resolutionCommit: commit },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { ws, am, triggerResolved };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mockContext(ws: WorkspaceManager, overrides: Partial<ConflictContext> = {}): ConflictContext {
|
|
47
|
+
return {
|
|
48
|
+
conflictId: 'c-1',
|
|
49
|
+
streamId: 'stream-1',
|
|
50
|
+
paths: ['a.ts'],
|
|
51
|
+
operation: 'merge',
|
|
52
|
+
recoveryDepth: 0,
|
|
53
|
+
workspaceManager: ws,
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('SpawnResolverStrategy', () => {
|
|
59
|
+
it('spawns a resolver with the configured role and awaits resolve', async () => {
|
|
60
|
+
const { ws, am, triggerResolved } = mockManagers();
|
|
61
|
+
const strat = createSpawnResolverStrategy({ agentManager: am });
|
|
62
|
+
|
|
63
|
+
const ctx = mockContext(ws, {
|
|
64
|
+
strategyConfig: { role: 'resolver', timeout_ms: 5000 },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const recoveryPromise = strat.recover(ctx);
|
|
68
|
+
|
|
69
|
+
// Allow spawn + onEvent registration to complete
|
|
70
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
71
|
+
|
|
72
|
+
expect(am.spawn).toHaveBeenCalledWith(
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
role: 'resolver',
|
|
75
|
+
capabilities: expect.arrayContaining(['workspace.resolve']),
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Simulate resolver agent calling resolve_conflict
|
|
80
|
+
triggerResolved('c-1', 'abc123');
|
|
81
|
+
|
|
82
|
+
const resolution = await recoveryPromise;
|
|
83
|
+
expect(resolution.kind).toBe('resolved');
|
|
84
|
+
if (resolution.kind === 'resolved') {
|
|
85
|
+
expect(resolution.resolutionCommit).toBe('abc123');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('escalates on timeout', async () => {
|
|
90
|
+
const { ws, am } = mockManagers();
|
|
91
|
+
const strat = createSpawnResolverStrategy({
|
|
92
|
+
agentManager: am,
|
|
93
|
+
defaultTimeoutMs: 50, // short timeout for test
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const ctx = mockContext(ws);
|
|
97
|
+
const resolution = await strat.recover(ctx);
|
|
98
|
+
|
|
99
|
+
expect(resolution.kind).toBe('escalated');
|
|
100
|
+
if (resolution.kind === 'escalated') {
|
|
101
|
+
expect(resolution.escalatedTo).toBe('human');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns retry-after when max_concurrent exceeded', async () => {
|
|
106
|
+
const { ws, am, triggerResolved } = mockManagers();
|
|
107
|
+
const strat = createSpawnResolverStrategy({
|
|
108
|
+
agentManager: am,
|
|
109
|
+
defaultMaxConcurrent: 1,
|
|
110
|
+
defaultTimeoutMs: 5000,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// First resolver is still pending
|
|
114
|
+
const first = strat.recover(mockContext(ws));
|
|
115
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
116
|
+
|
|
117
|
+
// Second try — exceeds max
|
|
118
|
+
const second = await strat.recover(mockContext(ws));
|
|
119
|
+
expect(second.kind).toBe('retry-after');
|
|
120
|
+
|
|
121
|
+
// Resolve the first so it cleans up
|
|
122
|
+
triggerResolved('c-1', 'commit');
|
|
123
|
+
await first;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns failed when spawn itself fails', async () => {
|
|
127
|
+
const { ws } = mockManagers();
|
|
128
|
+
const am = {
|
|
129
|
+
spawn: vi.fn().mockRejectedValue(new Error('spawn boom')),
|
|
130
|
+
} as unknown as AgentManager;
|
|
131
|
+
const strat = new SpawnResolverStrategy({ agentManager: am });
|
|
132
|
+
|
|
133
|
+
const resolution = await strat.recover(mockContext(ws));
|
|
134
|
+
expect(resolution.kind).toBe('failed');
|
|
135
|
+
if (resolution.kind === 'failed') {
|
|
136
|
+
expect(resolution.error).toMatch(/spawn boom/);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict recovery strategy tests (Phase 7).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
DeferStrategy,
|
|
8
|
+
AbandonStrategy,
|
|
9
|
+
EscalateStrategy,
|
|
10
|
+
AutoResolveStrategy,
|
|
11
|
+
buildBuiltinRecoveryRegistry,
|
|
12
|
+
} from '../index.js';
|
|
13
|
+
import type { ConflictContext } from '../types.js';
|
|
14
|
+
import type { WorkspaceManager } from '../../types.js';
|
|
15
|
+
|
|
16
|
+
function mockContext(overrides: Partial<ConflictContext> = {}): ConflictContext {
|
|
17
|
+
const ws = {
|
|
18
|
+
abandonStream: vi.fn(),
|
|
19
|
+
pauseStream: vi.fn(),
|
|
20
|
+
} as unknown as WorkspaceManager;
|
|
21
|
+
return {
|
|
22
|
+
conflictId: 'c-1',
|
|
23
|
+
streamId: 'stream-1',
|
|
24
|
+
paths: ['a.ts'],
|
|
25
|
+
operation: 'merge',
|
|
26
|
+
recoveryDepth: 0,
|
|
27
|
+
workspaceManager: ws,
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('conflict recovery strategies', () => {
|
|
33
|
+
describe('DeferStrategy', () => {
|
|
34
|
+
it('returns { kind: deferred }', async () => {
|
|
35
|
+
const strat = new DeferStrategy();
|
|
36
|
+
const res = await strat.recover(mockContext());
|
|
37
|
+
expect(res.kind).toBe('deferred');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('AbandonStrategy', () => {
|
|
42
|
+
it('abandons the stream and returns { kind: abandoned }', async () => {
|
|
43
|
+
const strat = new AbandonStrategy();
|
|
44
|
+
const ctx = mockContext();
|
|
45
|
+
const res = await strat.recover(ctx);
|
|
46
|
+
expect(res.kind).toBe('abandoned');
|
|
47
|
+
expect(ctx.workspaceManager.abandonStream).toHaveBeenCalledWith(
|
|
48
|
+
'stream-1',
|
|
49
|
+
expect.objectContaining({ reason: expect.stringContaining('c-1') })
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns { kind: failed } on abandon error', async () => {
|
|
54
|
+
const strat = new AbandonStrategy();
|
|
55
|
+
const ctx = mockContext();
|
|
56
|
+
(ctx.workspaceManager.abandonStream as any) = vi.fn(() => {
|
|
57
|
+
throw new Error('boom');
|
|
58
|
+
});
|
|
59
|
+
const res = await strat.recover(ctx);
|
|
60
|
+
expect(res.kind).toBe('failed');
|
|
61
|
+
if (res.kind === 'failed') {
|
|
62
|
+
expect(res.error).toContain('boom');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('EscalateStrategy', () => {
|
|
68
|
+
it('pauses the stream and returns { kind: escalated }', async () => {
|
|
69
|
+
const strat = new EscalateStrategy();
|
|
70
|
+
const ctx = mockContext();
|
|
71
|
+
const res = await strat.recover(ctx);
|
|
72
|
+
expect(res.kind).toBe('escalated');
|
|
73
|
+
expect(ctx.workspaceManager.pauseStream).toHaveBeenCalledWith(
|
|
74
|
+
'stream-1',
|
|
75
|
+
expect.any(String)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('uses notify config when provided', async () => {
|
|
80
|
+
const strat = new EscalateStrategy();
|
|
81
|
+
const ctx = mockContext({
|
|
82
|
+
strategyConfig: { notify: 'team:alpha' },
|
|
83
|
+
});
|
|
84
|
+
const res = await strat.recover(ctx);
|
|
85
|
+
expect(res.kind).toBe('escalated');
|
|
86
|
+
if (res.kind === 'escalated') {
|
|
87
|
+
expect(res.escalatedTo).toBe('team:alpha');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('AutoResolveStrategy', () => {
|
|
93
|
+
it('canHandle requires merge operation + worktree', () => {
|
|
94
|
+
const strat = new AutoResolveStrategy();
|
|
95
|
+
expect(
|
|
96
|
+
strat.canHandle!(mockContext({ operation: 'merge', worktree: '/tmp/wt' }))
|
|
97
|
+
).toBe(true);
|
|
98
|
+
expect(strat.canHandle!(mockContext({ operation: 'merge' }))).toBe(false);
|
|
99
|
+
expect(
|
|
100
|
+
strat.canHandle!(mockContext({ operation: 'rebase', worktree: '/tmp/wt' }))
|
|
101
|
+
).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns failed for non-merge operations', async () => {
|
|
105
|
+
const strat = new AutoResolveStrategy();
|
|
106
|
+
const res = await strat.recover(mockContext({ operation: 'rebase' }));
|
|
107
|
+
expect(res.kind).toBe('failed');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns failed without worktree', async () => {
|
|
111
|
+
const strat = new AutoResolveStrategy();
|
|
112
|
+
const res = await strat.recover(mockContext({ operation: 'merge' }));
|
|
113
|
+
expect(res.kind).toBe('failed');
|
|
114
|
+
if (res.kind === 'failed') {
|
|
115
|
+
expect(res.error).toMatch(/worktree/);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('rejects unsupported strategies', async () => {
|
|
120
|
+
const strat = new AutoResolveStrategy();
|
|
121
|
+
const res = await strat.recover(
|
|
122
|
+
mockContext({
|
|
123
|
+
operation: 'merge',
|
|
124
|
+
worktree: '/tmp/wt',
|
|
125
|
+
strategyConfig: { strategy: 'bogus' },
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
expect(res.kind).toBe('failed');
|
|
129
|
+
if (res.kind === 'failed') {
|
|
130
|
+
expect(res.error).toMatch(/unsupported strategy/);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('buildBuiltinRecoveryRegistry', () => {
|
|
136
|
+
it('includes 4 built-in strategies', () => {
|
|
137
|
+
const registry = buildBuiltinRecoveryRegistry();
|
|
138
|
+
expect(registry.has('defer')).toBe(true);
|
|
139
|
+
expect(registry.has('abandon')).toBe(true);
|
|
140
|
+
expect(registry.has('escalate')).toBe(true);
|
|
141
|
+
expect(registry.has('auto-resolve')).toBe(true);
|
|
142
|
+
expect(registry.has('spawn-resolver')).toBe(false); // Phase 7b
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|