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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `abandon` conflict recovery strategy.
|
|
3
|
+
*
|
|
4
|
+
* Abandons the conflicted stream. Throwaway-exploration teams; CI-driven
|
|
5
|
+
* flows where broken work is discarded rather than resolved.
|
|
6
|
+
*
|
|
7
|
+
* @module workspace/recovery/abandon
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ConflictContext,
|
|
12
|
+
ConflictRecoveryStrategy,
|
|
13
|
+
ConflictResolution,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
export class AbandonStrategy implements ConflictRecoveryStrategy {
|
|
17
|
+
readonly name = 'abandon';
|
|
18
|
+
readonly mode = 'sync' as const;
|
|
19
|
+
|
|
20
|
+
async recover(ctx: ConflictContext): Promise<ConflictResolution> {
|
|
21
|
+
try {
|
|
22
|
+
// Mark the conflict resolved (method='abandoned') so the OpenHive hub
|
|
23
|
+
// moves cascade_conflicts.status from pending → resolved instead of
|
|
24
|
+
// showing it stuck pending forever.
|
|
25
|
+
try {
|
|
26
|
+
ctx.workspaceManager.resolveConflict({
|
|
27
|
+
conflictId: ctx.conflictId,
|
|
28
|
+
resolvedBy: 'system:abandon',
|
|
29
|
+
method: 'abandoned',
|
|
30
|
+
summary: `stream abandoned: ${ctx.streamId}`,
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
// Non-fatal — abandonStream below still runs
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ctx.workspaceManager.abandonStream(ctx.streamId, {
|
|
37
|
+
reason: `abandon strategy: conflict ${ctx.conflictId}`,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
kind: 'abandoned',
|
|
41
|
+
streamId: ctx.streamId,
|
|
42
|
+
reason: `conflict ${ctx.conflictId}`,
|
|
43
|
+
};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'failed',
|
|
47
|
+
error: err instanceof Error ? err.message : String(err),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|