macro-agent 0.1.7 → 0.1.10
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 +179 -38
- 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 +155 -6
- 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/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 +234 -71
- 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/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 +41 -0
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +34 -37
- package/dist/boot-v2.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/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-bridge.d.ts +44 -0
- package/dist/map/cascade-bridge.d.ts.map +1 -0
- package/dist/map/cascade-bridge.js +257 -0
- package/dist/map/cascade-bridge.js.map +1 -0
- package/dist/map/lifecycle-bridge.d.ts +1 -8
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +76 -22
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +47 -6
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +33 -4
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +20 -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 +908 -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 +185 -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 +111 -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 +110 -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 +145 -17
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +92 -13
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +373 -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/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 +4 -4
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -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/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 +167 -9
- package/src/acp/types.ts +10 -0
- package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
- package/src/agent/__tests__/agent-manager-v2.test.ts +71 -11
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +293 -77
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +87 -36
- package/src/cli/index.ts +61 -0
- package/src/cognitive/__tests__/macro-agent-backend.test.ts +47 -5
- package/src/cognitive/macro-agent-backend.ts +45 -29
- package/src/integrations/skilltree.ts +1 -0
- 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__/lifecycle-bridge.test.ts +165 -22
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-bridge.ts +301 -0
- package/src/map/lifecycle-bridge.ts +77 -27
- package/src/map/server.ts +47 -6
- package/src/map/sidecar.ts +31 -3
- package/src/map/types.ts +20 -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__/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 +1186 -0
- package/src/workspace/index.ts +11 -11
- package/src/workspace/landing/__tests__/strategies.test.ts +142 -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 +228 -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 +145 -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 +155 -0
- package/src/workspace/types.ts +191 -20
- package/src/workspace/workspace-manager.ts +474 -19
- package/src/workspace/yaml-schema.ts +216 -0
- package/src/workspace/dataplane-adapter.ts +0 -546
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# Conflict Recovery Design (Draft)
|
|
2
|
+
|
|
3
|
+
Parallel design to `docs/workspace-interfaces.md`. Defines how macro-agent handles merge/rebase/cascade conflicts produced by `WorkspaceManager` operations.
|
|
4
|
+
|
|
5
|
+
**Status**: draft for iteration. Not yet implemented.
|
|
6
|
+
|
|
7
|
+
**Relationship to other docs:**
|
|
8
|
+
- `git-cascade-integration-gaps.md` — narrative, established that conflict recovery is its own concern (§9 Open).
|
|
9
|
+
- `workspace-interfaces.md` — defines `ConflictRecord`, `WorkspaceManager.resolveConflict()`. This doc defines how recovery is *driven*.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Problem statement
|
|
14
|
+
|
|
15
|
+
Conflicts arise from four operations:
|
|
16
|
+
|
|
17
|
+
- `mergeStream(source → target)` — merge conflict between branches
|
|
18
|
+
- `syncWithParent(stream)` — rebase conflict when parent advanced
|
|
19
|
+
- `rebaseOntoStream(stream → target)` — same
|
|
20
|
+
- `cascadeRebase({ rootStreamId })` — one or more dependent streams fail to rebase
|
|
21
|
+
|
|
22
|
+
git-cascade records each via `createConflict()`; the stream is marked `conflicted`. Today, macro-agent has no handling — strategies use raw `execSync('git rebase')` which fails hard, and the event is lost.
|
|
23
|
+
|
|
24
|
+
The redesign needs a **pluggable recovery mechanism** that sits between detection (git-cascade's conflict model) and resolution (which might be git strategies, a spawned resolver agent, a human, or abandonment). The mechanism must work for all four sources.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. When conflicts happen — lifecycle
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Landing / sync / cascade operation
|
|
32
|
+
│
|
|
33
|
+
├─ Success: MergeResult.success = true
|
|
34
|
+
│ stream.committed event emitted
|
|
35
|
+
│
|
|
36
|
+
└─ Conflict: ConflictRecord created in git-cascade
|
|
37
|
+
stream marked 'conflicted'
|
|
38
|
+
stream.conflicted event emitted
|
|
39
|
+
MergeResult.success = false, conflictId returned
|
|
40
|
+
│
|
|
41
|
+
├─ LandingStrategy / operation caller receives result
|
|
42
|
+
│
|
|
43
|
+
└─ workspaceManager.recoverConflict({ conflictId, strategyName? })
|
|
44
|
+
│
|
|
45
|
+
└─ Recovery strategy dispatches
|
|
46
|
+
│
|
|
47
|
+
├─ Sync resolution → returns ConflictResolution immediately
|
|
48
|
+
└─ Async resolution → returns pending; events fire on completion
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Key separation**: LandingStrategy decides *how to land*; ConflictRecoveryStrategy decides *what to do when landing fails*. They're parallel concerns, not nested.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. Core types
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export interface ConflictContext {
|
|
59
|
+
conflictId: string;
|
|
60
|
+
streamId: StreamId; // the conflicted stream
|
|
61
|
+
sourceCommit?: string;
|
|
62
|
+
targetCommit?: string;
|
|
63
|
+
targetStreamId?: StreamId; // where we were trying to land (if applicable)
|
|
64
|
+
paths: string[]; // conflicting file paths
|
|
65
|
+
operation: "merge" | "sync" | "rebase" | "cascade";
|
|
66
|
+
landingAgentId?: AgentId; // who was trying to land (may be terminated)
|
|
67
|
+
recoveryDepth: number; // for bounded recursion
|
|
68
|
+
strategyConfig?: Record<string, unknown>;
|
|
69
|
+
workspaceManager: WorkspaceManager;
|
|
70
|
+
agentManager: AgentManager;
|
|
71
|
+
inboxAdapter: InboxAdapter;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type ConflictResolution =
|
|
75
|
+
| { kind: "resolved"; resolutionCommit: string }
|
|
76
|
+
| { kind: "deferred"; reason: string }
|
|
77
|
+
| { kind: "abandoned"; streamId: StreamId; reason: string }
|
|
78
|
+
| { kind: "escalated"; escalatedTo: AgentId | "human" }
|
|
79
|
+
| { kind: "retry-after"; backoffMs: number; reason: string }
|
|
80
|
+
| { kind: "failed"; error: string };
|
|
81
|
+
|
|
82
|
+
export type ConflictResolutionMode = "sync" | "async";
|
|
83
|
+
|
|
84
|
+
export interface ConflictRecoveryStrategy {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly mode: ConflictResolutionMode;
|
|
87
|
+
|
|
88
|
+
canHandle?(ctx: ConflictContext): boolean;
|
|
89
|
+
recover(ctx: ConflictContext): Promise<ConflictResolution>;
|
|
90
|
+
|
|
91
|
+
initialize?(): Promise<void>;
|
|
92
|
+
close?(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Rationale:**
|
|
97
|
+
- `recoveryDepth` prevents infinite recursion if a resolver creates new conflicts.
|
|
98
|
+
- `mode` is declarative — callers can decide whether to await or fire-and-forget based on it.
|
|
99
|
+
- `operation` tells strategies what produced the conflict (merge-specific resolution differs from rebase-specific).
|
|
100
|
+
- `landingAgentId` may be null or already terminated; strategies can't rely on it being alive.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 4. Built-in strategies
|
|
105
|
+
|
|
106
|
+
Five registered by default. Teams pick via YAML `on_conflict:` directive.
|
|
107
|
+
|
|
108
|
+
### 4.1 `auto-resolve` — sync, git-native strategies
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// strategyConfig: { strategy: "ours" | "theirs" | "union" }
|
|
112
|
+
// Runs raw git merge with -X <strategy>; commits the resolution.
|
|
113
|
+
// Returns: { kind: "resolved", resolutionCommit } or { kind: "failed", ... }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use cases: low-risk conflicts where one side is authoritative. Example: a reviewer branch that should always defer to coder's changes.
|
|
117
|
+
|
|
118
|
+
**Constraint**: Only applicable to `operation: "merge"`. Rebase conflicts need different handling.
|
|
119
|
+
|
|
120
|
+
### 4.2 `defer` — sync, no-op
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// Creates no new state; conflict record persists, stream stays 'conflicted'.
|
|
124
|
+
// Returns: { kind: "deferred", reason: "no strategy configured" }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use cases: the team doesn't want automated recovery. Something else (human, external process) will deal with it.
|
|
128
|
+
|
|
129
|
+
### 4.3 `spawn-resolver` — async, LLM-driven
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// strategyConfig: {
|
|
133
|
+
// role: string; // e.g., "resolver"
|
|
134
|
+
// max_concurrent?: number; // default 2
|
|
135
|
+
// timeout_ms?: number; // default 1800000 (30 min)
|
|
136
|
+
// prompt_template?: string; // optional prompt override
|
|
137
|
+
// }
|
|
138
|
+
//
|
|
139
|
+
// 1. Emit conflict.recovery.started event
|
|
140
|
+
// 2. AgentManager.spawn({ role, options: { conflictId, streamId, paths } })
|
|
141
|
+
// 3. Resolver's topology places it on the conflicted stream via
|
|
142
|
+
// workspace: new_stream, stream_lineage: track_existing_branch,
|
|
143
|
+
// track_branch: <conflicted_branch>.
|
|
144
|
+
// 4. Resolver has capabilities: [workspace.commit, workspace.resolve, workspace.read]
|
|
145
|
+
// 5. Resolver reads conflict markers, edits, commits, calls resolve_conflict MCP tool.
|
|
146
|
+
// 6. resolve_conflict → workspaceManager.resolveConflict({ conflictId, resolutionCommit })
|
|
147
|
+
// → emits conflict.resolved event
|
|
148
|
+
// → stream status back to 'active'
|
|
149
|
+
// 7. Original operation retried (if requested via config).
|
|
150
|
+
//
|
|
151
|
+
// Returns: { kind: "resolved", resolutionCommit } on success
|
|
152
|
+
// { kind: "escalated", escalatedTo: "human" } on timeout
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use cases: default for teams with LLM budget. Handles most real conflicts via LLM judgment.
|
|
156
|
+
|
|
157
|
+
**Recursion bound**: if resolver's `resolve_conflict` call produces a new conflict, `recoveryDepth` increments. Default max = 3; beyond that, falls back to `escalate`.
|
|
158
|
+
|
|
159
|
+
### 4.4 `abandon` — sync, give up
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Calls workspaceManager.abandonStream(streamId, { reason }).
|
|
163
|
+
// If operation was 'merge', source stream is abandoned; target untouched.
|
|
164
|
+
// If operation was 'sync' / 'rebase' / 'cascade', conflicted stream is abandoned
|
|
165
|
+
// (cascade: just the failing stream; siblings continue per cascade strategy).
|
|
166
|
+
// Returns: { kind: "abandoned", streamId, reason }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use cases: throwaway exploration streams; CI-driven teams where broken work is discarded.
|
|
170
|
+
|
|
171
|
+
### 4.5 `escalate` — async, human-in-the-loop
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// strategyConfig: { notify: AgentId[] | "team-root" | "external-channel" }
|
|
175
|
+
//
|
|
176
|
+
// 1. Create inbox message with high importance to configured recipients
|
|
177
|
+
// 2. Pause stream via workspaceManager.pauseStream(streamId, "awaiting human")
|
|
178
|
+
// 3. Emit conflict.recovery.escalated event
|
|
179
|
+
// 4. Wait indefinitely for external resolve_conflict call
|
|
180
|
+
// 5. On external resolution: resume stream, emit conflict.resolved
|
|
181
|
+
//
|
|
182
|
+
// Returns: { kind: "escalated", escalatedTo }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Use cases: high-stakes merges; teams with human reviewers; fallback from `spawn-resolver` timeout.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 5. Integration with LandingStrategy
|
|
190
|
+
|
|
191
|
+
Landing strategies detect conflicts but don't handle them directly. Contract:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// Inside a LandingStrategy.land(ctx):
|
|
195
|
+
|
|
196
|
+
const mergeResult = await ctx.workspaceManager.mergeStream({ ... });
|
|
197
|
+
if (!mergeResult.success && mergeResult.conflictId) {
|
|
198
|
+
// Do NOT call recoverConflict from inside the strategy.
|
|
199
|
+
// Return the conflict up; the caller (agent's done flow) handles it.
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
conflictId: mergeResult.conflictId,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Why not in-strategy recovery?** Three reasons:
|
|
208
|
+
1. Strategies can be registered without recovery knowledge. Separation of concerns.
|
|
209
|
+
2. Recovery policy is role-level (`on_conflict:` YAML), not strategy-level.
|
|
210
|
+
3. Async recovery means strategy would have to block on recovery resolution — fragile.
|
|
211
|
+
|
|
212
|
+
Instead, the agent's `done()` flow owns recovery dispatch:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// In AgentManagerV2's done handler (or land MCP tool handler):
|
|
216
|
+
|
|
217
|
+
const landingResult = await workspaceManager.land({ ... });
|
|
218
|
+
if (!landingResult.success && landingResult.conflictId) {
|
|
219
|
+
const roleConfig = topologyPolicy.getRoleConfig(agent.role);
|
|
220
|
+
const recoveryStrategy = roleConfig.on_conflict ?? teamDefault.conflict_recovery.default_strategy;
|
|
221
|
+
|
|
222
|
+
const resolution = await workspaceManager.recoverConflict({
|
|
223
|
+
conflictId: landingResult.conflictId,
|
|
224
|
+
strategyName: recoveryStrategy,
|
|
225
|
+
strategyConfig: roleConfig.conflict_recovery_config,
|
|
226
|
+
operation: "merge",
|
|
227
|
+
landingAgentId: agent.id,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Handle resolution result — may be async; if so, agent can terminate
|
|
231
|
+
// and let recovery run in background.
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 6. `WorkspaceManager` additions
|
|
238
|
+
|
|
239
|
+
Extend the interface from `workspace-interfaces.md` §5:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
interface WorkspaceManager {
|
|
243
|
+
// ... existing methods ...
|
|
244
|
+
|
|
245
|
+
// Conflict recovery registry
|
|
246
|
+
registerConflictRecoveryStrategy(strategy: ConflictRecoveryStrategy): void;
|
|
247
|
+
unregisterConflictRecoveryStrategy(name: string): void;
|
|
248
|
+
getConflictRecoveryStrategy(name: string): ConflictRecoveryStrategy | null;
|
|
249
|
+
|
|
250
|
+
// Recovery dispatch
|
|
251
|
+
recoverConflict(opts: {
|
|
252
|
+
conflictId: string;
|
|
253
|
+
strategyName?: string; // defaults to team default
|
|
254
|
+
strategyConfig?: Record<string, unknown>;
|
|
255
|
+
operation?: "merge" | "sync" | "rebase" | "cascade";
|
|
256
|
+
landingAgentId?: AgentId;
|
|
257
|
+
}): Promise<ConflictResolution>;
|
|
258
|
+
|
|
259
|
+
// Existing method, formalized
|
|
260
|
+
resolveConflict(opts: {
|
|
261
|
+
conflictId: string;
|
|
262
|
+
resolvedBy: Principal;
|
|
263
|
+
resolutionCommit?: string;
|
|
264
|
+
}): void; // Called by resolver agents via MCP tool.
|
|
265
|
+
// Emits conflict.resolved event.
|
|
266
|
+
|
|
267
|
+
// Listing / inspection
|
|
268
|
+
listConflicts(filter?: {
|
|
269
|
+
streamId?: StreamId;
|
|
270
|
+
resolved?: boolean;
|
|
271
|
+
}): ConflictRecord[];
|
|
272
|
+
|
|
273
|
+
getConflict(conflictId: string): ConflictRecord | null;
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
New events (extend `WorkspaceEvent` union):
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
| { kind: "conflict.created"; conflictId: string; streamId: StreamId; operation: string }
|
|
281
|
+
| { kind: "conflict.recovery.started"; conflictId: string; strategyName: string }
|
|
282
|
+
| { kind: "conflict.recovery.escalated"; conflictId: string; escalatedTo: string }
|
|
283
|
+
| { kind: "conflict.recovery.timed-out"; conflictId: string; strategyName: string }
|
|
284
|
+
| { kind: "conflict.resolved"; conflictId: string; resolutionCommit?: string; resolvedBy: Principal }
|
|
285
|
+
| { kind: "conflict.abandoned"; conflictId: string; reason: string }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 7. YAML configuration
|
|
291
|
+
|
|
292
|
+
```yaml
|
|
293
|
+
macro_agent:
|
|
294
|
+
workspace:
|
|
295
|
+
roles:
|
|
296
|
+
worker:
|
|
297
|
+
on_conflict: defer # strategy name
|
|
298
|
+
coder:
|
|
299
|
+
on_conflict: spawn-resolver
|
|
300
|
+
conflict_recovery_config:
|
|
301
|
+
role: resolver
|
|
302
|
+
timeout_ms: 1200000
|
|
303
|
+
resolver:
|
|
304
|
+
workspace: new_stream
|
|
305
|
+
stream_lineage: track_existing_branch
|
|
306
|
+
# track_branch is set at spawn time from conflict context
|
|
307
|
+
landing: none # resolver doesn't land itself
|
|
308
|
+
capabilities:
|
|
309
|
+
- workspace.commit
|
|
310
|
+
- workspace.resolve
|
|
311
|
+
- workspace.read
|
|
312
|
+
|
|
313
|
+
# Team-level defaults
|
|
314
|
+
conflict_recovery:
|
|
315
|
+
default_strategy: spawn-resolver # used when role has no on_conflict
|
|
316
|
+
default_config:
|
|
317
|
+
role: resolver
|
|
318
|
+
max_concurrent: 2
|
|
319
|
+
timeout_ms: 1800000
|
|
320
|
+
escalation_target: team-root # for escalate strategy
|
|
321
|
+
max_recovery_depth: 3 # recursion bound
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Resolution order for strategy selection:**
|
|
325
|
+
1. Role-level `on_conflict` (most specific)
|
|
326
|
+
2. Team-level `conflict_recovery.default_strategy`
|
|
327
|
+
3. Hardcoded fallback: `defer` (safest default)
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 8. MCP tool surface
|
|
332
|
+
|
|
333
|
+
New capability: `workspace.resolve`. Registered tools:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
// ── Capability: workspace.resolve ────────────────────────────────
|
|
337
|
+
export const resolveConflictToolInput = z.object({
|
|
338
|
+
conflictId: z.string(),
|
|
339
|
+
resolutionCommit: z.string().optional(), // inferred from current HEAD if omitted
|
|
340
|
+
});
|
|
341
|
+
// Handler: workspaceManager.resolveConflict({ conflictId, resolvedBy: agentId, resolutionCommit })
|
|
342
|
+
// → emits conflict.resolved event
|
|
343
|
+
// → returns { resolved: true, conflictId, resolutionCommit }
|
|
344
|
+
|
|
345
|
+
export const listConflictsToolInput = z.object({
|
|
346
|
+
streamId: z.string().optional(),
|
|
347
|
+
resolvedOnly: z.boolean().default(false),
|
|
348
|
+
});
|
|
349
|
+
// Handler: workspaceManager.listConflicts({ ... })
|
|
350
|
+
|
|
351
|
+
export const getConflictToolInput = z.object({
|
|
352
|
+
conflictId: z.string(),
|
|
353
|
+
});
|
|
354
|
+
// Handler: workspaceManager.getConflict(conflictId)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Capability → tool mapping:**
|
|
358
|
+
|
|
359
|
+
| Capability | Tools |
|
|
360
|
+
|---|---|
|
|
361
|
+
| `workspace.resolve` | `resolve_conflict`, `list_conflicts`, `get_conflict` |
|
|
362
|
+
|
|
363
|
+
Resolver roles need `workspace.resolve` + `workspace.commit` + `workspace.read`.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## 9. End-to-end flow: `spawn-resolver`
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
[Coder agent] lands via queue-to-branch strategy
|
|
371
|
+
└─ queue-to-branch.land(ctx):
|
|
372
|
+
workspaceManager.addToMergeQueue({ streamId: coderStream, targetBranch })
|
|
373
|
+
returns { success: true, queuedAt: ... }
|
|
374
|
+
|
|
375
|
+
[Integrator agent] drains queue
|
|
376
|
+
└─ integrator.processNextMergeRequest():
|
|
377
|
+
next = workspaceManager.getNextToMerge(targetBranch)
|
|
378
|
+
result = workspaceManager.mergeStream({ sourceStreamId: next.streamId, ... })
|
|
379
|
+
→ CONFLICT: result.success = false, result.conflictId = "c-abc"
|
|
380
|
+
workspaceManager.markMergeQueueReady(next.id, { status: "conflicted" })
|
|
381
|
+
|
|
382
|
+
[Integrator's landing flow sees conflict in result]
|
|
383
|
+
└─ done handler:
|
|
384
|
+
roleConfig.on_conflict = "spawn-resolver" (from YAML)
|
|
385
|
+
resolution = await workspaceManager.recoverConflict({
|
|
386
|
+
conflictId: "c-abc",
|
|
387
|
+
strategyName: "spawn-resolver",
|
|
388
|
+
strategyConfig: { role: "resolver", timeout_ms: 1200000 },
|
|
389
|
+
operation: "merge",
|
|
390
|
+
landingAgentId: integratorId,
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
[SpawnResolverStrategy.recover(ctx)]
|
|
394
|
+
emit conflict.recovery.started
|
|
395
|
+
agentId = await agentManager.spawn({
|
|
396
|
+
role: "resolver",
|
|
397
|
+
parent: ctx.landingAgentId,
|
|
398
|
+
task: `Resolve conflict ${ctx.conflictId} on stream ${ctx.streamId}`,
|
|
399
|
+
options: {
|
|
400
|
+
conflictId: ctx.conflictId,
|
|
401
|
+
streamId: ctx.streamId,
|
|
402
|
+
paths: ctx.paths,
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
// Topology places resolver on conflicted stream via track_existing_branch
|
|
406
|
+
// Resolver wakes up, reads conflict markers in worktree, edits files,
|
|
407
|
+
// calls `commit` MCP tool, then `resolve_conflict` MCP tool.
|
|
408
|
+
|
|
409
|
+
[Resolver agent]
|
|
410
|
+
calls commit({ message: "resolve: merge conflict in auth/middleware.ts" })
|
|
411
|
+
→ commitChanges returns { commit: "def456", changeId: "Change-I..." }
|
|
412
|
+
calls resolve_conflict({ conflictId: "c-abc" })
|
|
413
|
+
→ workspaceManager.resolveConflict({ conflictId, resolvedBy, resolutionCommit: "def456" })
|
|
414
|
+
→ emits conflict.resolved
|
|
415
|
+
→ stream status → 'active'
|
|
416
|
+
|
|
417
|
+
[SpawnResolverStrategy] subscribed to conflict.resolved
|
|
418
|
+
returns { kind: "resolved", resolutionCommit: "def456" }
|
|
419
|
+
|
|
420
|
+
[Integrator's done handler]
|
|
421
|
+
resolution.kind === "resolved"
|
|
422
|
+
→ retry mergeStream(coderStream → targetBranch) — this time it succeeds
|
|
423
|
+
→ mark queue entry merged
|
|
424
|
+
→ done
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Timing**: spawn-resolver is async from the caller's POV but the caller awaits the returned Promise. For truly fire-and-forget, caller doesn't await — just subscribes to `conflict.resolved` event.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## 10. Edge cases
|
|
432
|
+
|
|
433
|
+
**E1: Resolver creates a new conflict.**
|
|
434
|
+
Resolver's `commit` succeeds; but when the retry of the original merge runs, it conflicts again (e.g., rebase moved the target). `recoveryDepth` increments; strategy re-enters. Capped at `max_recovery_depth`; beyond that, falls back to `escalate`.
|
|
435
|
+
|
|
436
|
+
**E2: Multiple conflicts on the same stream.**
|
|
437
|
+
git-cascade allows multiple `ConflictRecord` entries per stream. Recovery serializes — `recoverConflict` for conflict B waits if conflict A is in-progress. Implementation: per-stream recovery lock.
|
|
438
|
+
|
|
439
|
+
**E3: Resolver crashes / times out.**
|
|
440
|
+
`spawn-resolver` has a `timeout_ms`. On timeout, strategy returns `{ kind: "escalated", escalatedTo: "human" }` — escalation happens via inbox message. Original agent sees escalation in its resolution result.
|
|
441
|
+
|
|
442
|
+
**E4: Landing agent already terminated when resolution completes.**
|
|
443
|
+
`landingAgentId` is advisory, not required. The retry logic in the agent's done handler is gone; so who retries? Options:
|
|
444
|
+
- (a) Recovery strategy doesn't trigger retry; caller subscribes to event and handles
|
|
445
|
+
- (b) Recovery strategy itself triggers retry via a "retry" phase
|
|
446
|
+
|
|
447
|
+
Proposal: **(a)**. The resolver's `resolve_conflict` tool does not retry the original operation. Downstream retry is a separate concern handled by the integrator/queue drainer, which should pick up the now-resolved stream on next iteration. Avoids complex state in strategies.
|
|
448
|
+
|
|
449
|
+
**E5: Conflict on `cascadeRebase`.**
|
|
450
|
+
Cascade produces N potential conflicts across dependents. `CascadeResult.failed[]` lists each with a conflictId. Caller iterates and calls `recoverConflict` per failed stream, typically in parallel. Cascade strategy `defer_conflicts` lets the cascade complete past failures; recovery kicks in per stream.
|
|
451
|
+
|
|
452
|
+
**E6: Abandoning a stream mid-recovery.**
|
|
453
|
+
If a human abandons a conflicted stream while recovery is in-progress, the resolver agent's subsequent `resolve_conflict` call fails with `stream_abandoned`. Resolver handles gracefully (reports failure, terminates). Recovery strategy emits `conflict.recovery.failed` with reason.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## 11. Open questions
|
|
458
|
+
|
|
459
|
+
- **Retry ownership** (E4). Proposed (a): caller subscribes to `conflict.resolved` and retries if it still cares. Downside: lost retries if no one subscribes. Alternative: retry hook on the original LandingStrategy. TBD.
|
|
460
|
+
- **Recovery for read-only operations.** `stream_status` tool returning stale data after a conflict — not a recovery concern, but affects UX. Documented as a non-goal.
|
|
461
|
+
- **Cross-team conflicts.** If two teams' streams conflict during merge to shared branch, which team's recovery policy applies? Propose: conflict is attached to the stream being merged; the *owning team's* policy runs. For federated scenarios, escalates to human by default.
|
|
462
|
+
- **Observability.** Recovery can take minutes; need a structured progress channel. Proposal: resolver agents emit `RECOVERY_PROGRESS` signals on a team channel; subscribers can watch. Optional, enabled via YAML.
|
|
463
|
+
- **Cost control for LLM-driven recovery.** Runaway recursion + expensive LLM calls are a risk. Hard cap: `max_recovery_depth` + `max_concurrent` per strategy config. Tokens should be tracked separately.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## 12. What's not defined here
|
|
468
|
+
|
|
469
|
+
- Implementation of resolver role prompt / skills.
|
|
470
|
+
- Conflict resolution heuristics (e.g., how an LLM resolver approaches different conflict types) — belongs in resolver role definition, not the strategy infrastructure.
|
|
471
|
+
- UI / dashboard for human-escalated conflicts.
|
|
472
|
+
- Cross-process conflict coordination (multi-instance macro-agent sharing a repo).
|