rivet-design 0.8.7 → 0.9.2
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/bin/rivet.js +11 -0
- package/dist/config/evaluateFlags.d.ts +6 -2
- package/dist/config/evaluateFlags.d.ts.map +1 -1
- package/dist/config/evaluateFlags.js +4 -4
- package/dist/config/evaluateFlags.js.map +1 -1
- package/dist/config/featureFlagUserKey.d.ts +11 -0
- package/dist/config/featureFlagUserKey.d.ts.map +1 -0
- package/dist/config/featureFlagUserKey.js +21 -0
- package/dist/config/featureFlagUserKey.js.map +1 -0
- package/dist/config/flags.d.ts +6 -0
- package/dist/config/flags.d.ts.map +1 -1
- package/dist/config/flags.js +2 -0
- package/dist/config/flags.js.map +1 -1
- package/dist/index.js +198 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/agent-variants/SessionStore.d.ts +121 -0
- package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -0
- package/dist/mcp/agent-variants/SessionStore.js +480 -0
- package/dist/mcp/agent-variants/SessionStore.js.map +1 -0
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +187 -0
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -0
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js +482 -0
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -0
- package/dist/mcp/agent-variants/contracts.d.ts +373 -0
- package/dist/mcp/agent-variants/contracts.d.ts.map +1 -0
- package/dist/mcp/agent-variants/contracts.js +134 -0
- package/dist/mcp/agent-variants/contracts.js.map +1 -0
- package/dist/mcp/agent-variants/errors.d.ts +8 -0
- package/dist/mcp/agent-variants/errors.d.ts.map +1 -0
- package/dist/mcp/agent-variants/errors.js +30 -0
- package/dist/mcp/agent-variants/errors.js.map +1 -0
- package/dist/mcp/agent-variants/index.d.ts +11 -0
- package/dist/mcp/agent-variants/index.d.ts.map +1 -0
- package/dist/mcp/agent-variants/index.js +15 -0
- package/dist/mcp/agent-variants/index.js.map +1 -0
- package/dist/mcp/agent-variants/pendingChangesAdapter.d.ts +38 -0
- package/dist/mcp/agent-variants/pendingChangesAdapter.d.ts.map +1 -0
- package/dist/mcp/agent-variants/pendingChangesAdapter.js +79 -0
- package/dist/mcp/agent-variants/pendingChangesAdapter.js.map +1 -0
- package/dist/mcp/agent-variants/tools.d.ts +8 -0
- package/dist/mcp/agent-variants/tools.d.ts.map +1 -0
- package/dist/mcp/agent-variants/tools.js +221 -0
- package/dist/mcp/agent-variants/tools.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +51 -34
- package/dist/mcp/server.js.map +1 -1
- package/dist/proxy-middleware/proxy-config.d.ts +4 -1
- package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
- package/dist/proxy-middleware/proxy-config.js +34 -5
- package/dist/proxy-middleware/proxy-config.js.map +1 -1
- package/dist/routes/agentVariants.d.ts +16 -0
- package/dist/routes/agentVariants.d.ts.map +1 -0
- package/dist/routes/agentVariants.js +234 -0
- package/dist/routes/agentVariants.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -8
- package/dist/server.js.map +1 -1
- package/dist/services/FeatureFlagService.d.ts +9 -5
- package/dist/services/FeatureFlagService.d.ts.map +1 -1
- package/dist/services/FeatureFlagService.js +16 -7
- package/dist/services/FeatureFlagService.js.map +1 -1
- package/dist/services/SessionBridgeService.d.ts +22 -0
- package/dist/services/SessionBridgeService.d.ts.map +1 -1
- package/dist/services/SessionBridgeService.js +58 -1
- package/dist/services/SessionBridgeService.js.map +1 -1
- package/dist/services/TelemetryService.d.ts.map +1 -1
- package/dist/services/TelemetryService.js +6 -1
- package/dist/services/TelemetryService.js.map +1 -1
- package/dist/services/WorktreeManager.d.ts +42 -3
- package/dist/services/WorktreeManager.d.ts.map +1 -1
- package/dist/services/WorktreeManager.js +149 -6
- package/dist/services/WorktreeManager.js.map +1 -1
- package/dist/types/change-request-types.d.ts +36 -1
- package/dist/types/change-request-types.d.ts.map +1 -1
- package/dist/utils/devServerCommand.d.ts +19 -0
- package/dist/utils/devServerCommand.d.ts.map +1 -0
- package/dist/utils/devServerCommand.js +38 -0
- package/dist/utils/devServerCommand.js.map +1 -0
- package/dist/utils/skills/claude-skill.d.ts +2 -2
- package/dist/utils/skills/claude-skill.d.ts.map +1 -1
- package/dist/utils/skills/claude-skill.js +87 -4
- package/dist/utils/skills/claude-skill.js.map +1 -1
- package/dist/utils/skills/cursor-rules.d.ts +2 -2
- package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
- package/dist/utils/skills/cursor-rules.js +87 -4
- package/dist/utils/skills/cursor-rules.js.map +1 -1
- package/dist/utils/variantPreviewPort.d.ts +18 -0
- package/dist/utils/variantPreviewPort.d.ts.map +1 -0
- package/dist/utils/variantPreviewPort.js +28 -0
- package/dist/utils/variantPreviewPort.js.map +1 -0
- package/package.json +1 -1
- package/src/ui/dist/assets/{main-HqOyJspz.js → main-AsPCtLsx.js} +119 -119
- package/src/ui/dist/assets/main-BzmseUDd.css +1 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/main-DytMB3Wi.css +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { ChildProcess } from 'child_process';
|
|
2
|
+
import { AgentVariantsError } from './errors';
|
|
3
|
+
import { SessionStore, type ApprovedBriefSelection, type ApproveResult, type ProposeArgs, type ProposeResult, type ReportCompleteResult, type RequestWorkResult } from './SessionStore';
|
|
4
|
+
import type { Brief, Stage, TerminalSummary, WorkItemStatus } from './contracts';
|
|
5
|
+
import type { PendingChangesAdapter } from './pendingChangesAdapter';
|
|
6
|
+
/**
|
|
7
|
+
* Subset of WorktreeManager that the orchestrator depends on. Defined as an
|
|
8
|
+
* interface so unit tests can mock it without spinning up real git/processes.
|
|
9
|
+
*
|
|
10
|
+
* Each succeeded variant gets its own dev server so the user can cycle
|
|
11
|
+
* between live, running variants in the iframe via a proxy retarget.
|
|
12
|
+
*/
|
|
13
|
+
export interface WorktreeManagerLike {
|
|
14
|
+
createWorktrees(sessionId: string, count: number): Promise<string[]>;
|
|
15
|
+
getDiff(worktreePath: string): Promise<string>;
|
|
16
|
+
cleanupSession(sessionId: string): Promise<void>;
|
|
17
|
+
getFreePort(): Promise<number>;
|
|
18
|
+
startDevServer(worktreePath: string, port: number, cmd: string, args: string[], env: Record<string, string>): Promise<ChildProcess>;
|
|
19
|
+
stopDevServer(proc: ChildProcess, timeoutMs?: number): Promise<void>;
|
|
20
|
+
getProjectCwdInWorktree(worktreePath: string): Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Per-session resolved environment. Used to populate destinationPath on the
|
|
24
|
+
* variant envelope and (in fresh-project flows) the workspace target. Also
|
|
25
|
+
* feeds buildDevServerCommand so each variant's dev server gets the correct
|
|
26
|
+
* port flag for its framework.
|
|
27
|
+
*/
|
|
28
|
+
export interface ProjectEnvironment {
|
|
29
|
+
projectPath: string;
|
|
30
|
+
framework: string;
|
|
31
|
+
packageManager: string;
|
|
32
|
+
devCommand: string;
|
|
33
|
+
/** Builds (cmd, args, env) for spawning a dev server in a worktree on a
|
|
34
|
+
* specific port. Defaults to a generic { packageManager, [devCommand,
|
|
35
|
+
* '--port', port] } shape if not provided, but production wires this to
|
|
36
|
+
* buildDevServerCommand from utils/devServerCommand.ts. */
|
|
37
|
+
buildDevCommand?: (port: number) => {
|
|
38
|
+
cmd: string;
|
|
39
|
+
args: string[];
|
|
40
|
+
env: Record<string, string>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface OrchestratorDeps {
|
|
44
|
+
store: SessionStore;
|
|
45
|
+
worktreeManager: WorktreeManagerLike;
|
|
46
|
+
pendingChangesAdapter: PendingChangesAdapter;
|
|
47
|
+
resolveProjectEnvironment: (sessionId: string) => Promise<ProjectEnvironment>;
|
|
48
|
+
telemetry?: VariantsTelemetry;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Minimal telemetry surface the orchestrator depends on. Decoupled from
|
|
52
|
+
* TelemetryService so unit tests can pass a stub. Production wires this
|
|
53
|
+
* to the singleton TelemetryService instance.
|
|
54
|
+
*/
|
|
55
|
+
export interface VariantsTelemetry {
|
|
56
|
+
track(event: string, properties?: Record<string, unknown>): void;
|
|
57
|
+
}
|
|
58
|
+
/** Snapshot shape consumed by the iframe chip, both via SSE and the legacy
|
|
59
|
+
* GET /api/variants/active endpoint. Single source of truth lives on the
|
|
60
|
+
* orchestrator (`buildActiveSnapshot`) so both transports are consistent. */
|
|
61
|
+
export type ActiveSnapshot = {
|
|
62
|
+
active: false;
|
|
63
|
+
} | {
|
|
64
|
+
active: true;
|
|
65
|
+
sessionId: string;
|
|
66
|
+
stage: Stage;
|
|
67
|
+
briefs: Brief[];
|
|
68
|
+
progress: {
|
|
69
|
+
ready: number;
|
|
70
|
+
total: number;
|
|
71
|
+
};
|
|
72
|
+
summary: TerminalSummary | null;
|
|
73
|
+
variants: Array<{
|
|
74
|
+
workItemId: string;
|
|
75
|
+
status: import('./contracts').WorkItemStatus;
|
|
76
|
+
label?: string;
|
|
77
|
+
previewPort?: number;
|
|
78
|
+
worktreePath?: string;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Wraps SessionStore for the operations that have side effects: approve
|
|
83
|
+
* (provision worktrees), reportComplete (capture diff + auto-enqueue to
|
|
84
|
+
* pending changes), cancel (teardown). Pure read paths bypass and call
|
|
85
|
+
* SessionStore directly.
|
|
86
|
+
*
|
|
87
|
+
* This is the terminal-native variant of the orchestrator: the user picks
|
|
88
|
+
* a single brief upfront, so there's only ever one code-gen item per
|
|
89
|
+
* session and no live preview / proxy retarget. Once the agent reports
|
|
90
|
+
* the variant succeeded, the diff auto-enqueues to bridge — no separate
|
|
91
|
+
* pick step.
|
|
92
|
+
*/
|
|
93
|
+
export declare class AgentVariantsOrchestrator {
|
|
94
|
+
private readonly store;
|
|
95
|
+
private readonly worktrees;
|
|
96
|
+
private readonly adapter;
|
|
97
|
+
private readonly resolveEnv;
|
|
98
|
+
private readonly telemetry;
|
|
99
|
+
private readonly resources;
|
|
100
|
+
/** Most recent agent-variants sessionId — read by the iframe chip via
|
|
101
|
+
* GET /api/variants/active. Cleared on cancel or commit. */
|
|
102
|
+
private activeSessionId;
|
|
103
|
+
/** Push channel for the iframe chip's SSE subscription. Emits an
|
|
104
|
+
* ActiveSnapshot on every state change so the chip never has to poll. */
|
|
105
|
+
private readonly events;
|
|
106
|
+
constructor(deps: OrchestratorDeps);
|
|
107
|
+
propose(args: ProposeArgs): ProposeResult;
|
|
108
|
+
/** Returns the most recent agent-variants sessionId (or null). */
|
|
109
|
+
getActiveSessionId(): string | null;
|
|
110
|
+
/** Subscribe to active-snapshot pushes. Returns an unsubscribe fn. */
|
|
111
|
+
subscribeToEvents(listener: (snapshot: ActiveSnapshot) => void): () => void;
|
|
112
|
+
/** Build the snapshot the chip cares about. Reads from SessionStore +
|
|
113
|
+
* per-resource state; safe to call at any time. */
|
|
114
|
+
buildActiveSnapshot(): ActiveSnapshot;
|
|
115
|
+
private emitChange;
|
|
116
|
+
reportBriefs(args: {
|
|
117
|
+
sessionId: string;
|
|
118
|
+
workItemId: string;
|
|
119
|
+
attempt: number;
|
|
120
|
+
leaseId?: string;
|
|
121
|
+
briefs: Brief[];
|
|
122
|
+
}): {
|
|
123
|
+
briefs: Brief[];
|
|
124
|
+
};
|
|
125
|
+
requestWork(args: {
|
|
126
|
+
sessionId: string;
|
|
127
|
+
leaseOwner: string;
|
|
128
|
+
requestedLeaseCount?: number;
|
|
129
|
+
}): RequestWorkResult;
|
|
130
|
+
getStage(sessionId: string): Stage;
|
|
131
|
+
getBriefs(sessionId: string): Brief[];
|
|
132
|
+
getProgress(sessionId: string): {
|
|
133
|
+
ready: number;
|
|
134
|
+
total: number;
|
|
135
|
+
};
|
|
136
|
+
getSummary(sessionId: string): TerminalSummary;
|
|
137
|
+
hasSession(sessionId: string): boolean;
|
|
138
|
+
/** Resolve a worktree path for a code-gen work item, if provisioned. */
|
|
139
|
+
getWorktreePath(sessionId: string, workItemId: string): string | undefined;
|
|
140
|
+
/** Resolve the dev server port for a code-gen work item, if running. */
|
|
141
|
+
getDevServerPort(sessionId: string, workItemId: string): number | undefined;
|
|
142
|
+
approve(args: {
|
|
143
|
+
sessionId: string;
|
|
144
|
+
selections: ApprovedBriefSelection[];
|
|
145
|
+
}): Promise<ApproveResult>;
|
|
146
|
+
reportComplete(args: {
|
|
147
|
+
sessionId: string;
|
|
148
|
+
workItemId: string;
|
|
149
|
+
leaseId: string;
|
|
150
|
+
attempt: number;
|
|
151
|
+
status: WorkItemStatus;
|
|
152
|
+
output?: unknown;
|
|
153
|
+
error?: {
|
|
154
|
+
code: string;
|
|
155
|
+
message: string;
|
|
156
|
+
};
|
|
157
|
+
}): Promise<ReportCompleteResult>;
|
|
158
|
+
cancel(args: {
|
|
159
|
+
sessionId: string;
|
|
160
|
+
reason?: string;
|
|
161
|
+
}): Promise<{
|
|
162
|
+
stage: 'cancelled';
|
|
163
|
+
cleanupStatus: 'pending' | 'complete' | 'partial';
|
|
164
|
+
}>;
|
|
165
|
+
/**
|
|
166
|
+
* User has reviewed the rendered variants in chat and picked one. Look up
|
|
167
|
+
* the captured diff, build a VariantPickEnvelope, and enqueue to the
|
|
168
|
+
* pending-changes channel. Idempotent on (sessionId, variantId): repeating
|
|
169
|
+
* the call returns duplicate=true without re-enqueueing.
|
|
170
|
+
*/
|
|
171
|
+
commitVariant(args: {
|
|
172
|
+
sessionId: string;
|
|
173
|
+
variantId: string;
|
|
174
|
+
}): Promise<{
|
|
175
|
+
enqueued: boolean;
|
|
176
|
+
duplicate: boolean;
|
|
177
|
+
changedFilesCount: number;
|
|
178
|
+
}>;
|
|
179
|
+
/** Read the captured diff for a code-gen variant, if available. */
|
|
180
|
+
getVariantDiff(sessionId: string, variantId: string): string | undefined;
|
|
181
|
+
private provisionWorktrees;
|
|
182
|
+
private handleSucceededReport;
|
|
183
|
+
private teardownSession;
|
|
184
|
+
private ensureResources;
|
|
185
|
+
}
|
|
186
|
+
export type { AgentVariantsError };
|
|
187
|
+
//# sourceMappingURL=WorktreeOrchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorktreeOrchestrator.d.ts","sourceRoot":"","sources":["../../../src/mcp/agent-variants/WorktreeOrchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,KAAK,EAEL,KAAK,EAEL,eAAe,EAGf,cAAc,EACf,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAIrE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrE,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,cAAc,CACZ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC;IACzB,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAChE;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB;;;gEAG4D;IAC5D,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,MAAM,KACT;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CACnE;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,eAAe,EAAE,mBAAmB,CAAC;IACrC,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,yBAAyB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9E,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AA2BD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAClE;AAID;;8EAE8E;AAC9E,MAAM,MAAM,cAAc,GACtB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IACE,MAAM,EAAE,IAAI,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,aAAa,EAAE,cAAc,CAAC;QAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;CACJ,CAAC;AAUN;;;;;;;;;;;GAWG;AACH,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAEM;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE;iEAC6D;IAC7D,OAAO,CAAC,eAAe,CAAuB;IAC9C;8EAC0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;gBAEjC,IAAI,EAAE,gBAAgB;IAUlC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,aAAa;IAgBzC,kEAAkE;IAClE,kBAAkB,IAAI,MAAM,GAAG,IAAI;IAInC,sEAAsE;IACtE,iBAAiB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAO3E;wDACoD;IACpD,mBAAmB,IAAI,cAAc;IAiCrC,OAAO,CAAC,UAAU;IAQlB,YAAY,CAAC,IAAI,EAAE;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;KACjB,GAAG;QAAE,MAAM,EAAE,KAAK,EAAE,CAAA;KAAE;IAMvB,WAAW,CAAC,IAAI,EAAE;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC9B,GAAG,iBAAiB;IAkCrB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK;IAIlC,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,EAAE;IAIrC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAIhE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe;IAI9C,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAItC,wEAAwE;IACxE,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAK1E,wEAAwE;IACxE,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMrE,OAAO,CAAC,IAAI,EAAE;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,sBAAsB,EAAE,CAAC;KACtC,GAAG,OAAO,CAAC,aAAa,CAAC;IA6BpB,cAAc,CAAC,IAAI,EAAE;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,cAAc,CAAC;QACvB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,KAAK,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;KAC3C,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAoE3B,MAAM,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAClE,KAAK,EAAE,WAAW,CAAC;QACnB,aAAa,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;KACnD,CAAC;IAqBF;;;;;OAKG;IACG,aAAa,CAAC,IAAI,EAAE;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC;QACV,QAAQ,EAAE,OAAO,CAAC;QAClB,SAAS,EAAE,OAAO,CAAC;QACnB,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;IAiHF,mEAAmE;IACnE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;YAM1D,kBAAkB;YAgClB,qBAAqB;YAqErB,eAAe;IAsC7B,OAAO,CAAC,eAAe;CAexB;AAQD,YAAY,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentVariantsOrchestrator = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const logger_1 = require("../../utils/logger");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
const contracts_1 = require("./contracts");
|
|
8
|
+
const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
|
|
9
|
+
const NOOP_TELEMETRY = { track: () => undefined };
|
|
10
|
+
/**
|
|
11
|
+
* Wraps SessionStore for the operations that have side effects: approve
|
|
12
|
+
* (provision worktrees), reportComplete (capture diff + auto-enqueue to
|
|
13
|
+
* pending changes), cancel (teardown). Pure read paths bypass and call
|
|
14
|
+
* SessionStore directly.
|
|
15
|
+
*
|
|
16
|
+
* This is the terminal-native variant of the orchestrator: the user picks
|
|
17
|
+
* a single brief upfront, so there's only ever one code-gen item per
|
|
18
|
+
* session and no live preview / proxy retarget. Once the agent reports
|
|
19
|
+
* the variant succeeded, the diff auto-enqueues to bridge — no separate
|
|
20
|
+
* pick step.
|
|
21
|
+
*/
|
|
22
|
+
class AgentVariantsOrchestrator {
|
|
23
|
+
store;
|
|
24
|
+
worktrees;
|
|
25
|
+
adapter;
|
|
26
|
+
resolveEnv;
|
|
27
|
+
telemetry;
|
|
28
|
+
resources = new Map();
|
|
29
|
+
/** Most recent agent-variants sessionId — read by the iframe chip via
|
|
30
|
+
* GET /api/variants/active. Cleared on cancel or commit. */
|
|
31
|
+
activeSessionId = null;
|
|
32
|
+
/** Push channel for the iframe chip's SSE subscription. Emits an
|
|
33
|
+
* ActiveSnapshot on every state change so the chip never has to poll. */
|
|
34
|
+
events = new events_1.EventEmitter();
|
|
35
|
+
constructor(deps) {
|
|
36
|
+
this.store = deps.store;
|
|
37
|
+
this.worktrees = deps.worktreeManager;
|
|
38
|
+
this.adapter = deps.pendingChangesAdapter;
|
|
39
|
+
this.resolveEnv = deps.resolveProjectEnvironment;
|
|
40
|
+
this.telemetry = deps.telemetry ?? NOOP_TELEMETRY;
|
|
41
|
+
}
|
|
42
|
+
// --- Pure delegations (no side effects) ---------------------------------
|
|
43
|
+
propose(args) {
|
|
44
|
+
const result = this.store.propose(args);
|
|
45
|
+
this.activeSessionId = result.sessionId;
|
|
46
|
+
this.ensureResources(result.sessionId);
|
|
47
|
+
this.telemetry.track('agent_variants.session_started', {
|
|
48
|
+
source: 'mcp',
|
|
49
|
+
sessionId: result.sessionId,
|
|
50
|
+
count: args.count ?? 4,
|
|
51
|
+
hasTarget: Boolean(args.target),
|
|
52
|
+
targetType: args.target?.type ?? null,
|
|
53
|
+
projectContextKind: args.projectContext?.kind ?? 'existing',
|
|
54
|
+
});
|
|
55
|
+
this.emitChange();
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/** Returns the most recent agent-variants sessionId (or null). */
|
|
59
|
+
getActiveSessionId() {
|
|
60
|
+
return this.activeSessionId;
|
|
61
|
+
}
|
|
62
|
+
/** Subscribe to active-snapshot pushes. Returns an unsubscribe fn. */
|
|
63
|
+
subscribeToEvents(listener) {
|
|
64
|
+
this.events.on('change', listener);
|
|
65
|
+
return () => {
|
|
66
|
+
this.events.off('change', listener);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Build the snapshot the chip cares about. Reads from SessionStore +
|
|
70
|
+
* per-resource state; safe to call at any time. */
|
|
71
|
+
buildActiveSnapshot() {
|
|
72
|
+
const sessionId = this.activeSessionId;
|
|
73
|
+
if (!sessionId || !this.store.hasSession(sessionId)) {
|
|
74
|
+
return { active: false };
|
|
75
|
+
}
|
|
76
|
+
const stage = this.store.getStage(sessionId);
|
|
77
|
+
const briefs = this.store.getBriefs(sessionId);
|
|
78
|
+
const progress = this.store.getProgress(sessionId);
|
|
79
|
+
const summary = stage === 'ready' ||
|
|
80
|
+
stage === 'degraded' ||
|
|
81
|
+
stage === 'failed' ||
|
|
82
|
+
stage === 'cancelled'
|
|
83
|
+
? this.store.getSummary(sessionId)
|
|
84
|
+
: null;
|
|
85
|
+
const variants = (summary?.variants ?? []).map((v) => ({
|
|
86
|
+
workItemId: v.workItemId,
|
|
87
|
+
status: v.status,
|
|
88
|
+
label: v.label,
|
|
89
|
+
previewPort: this.getDevServerPort(sessionId, v.workItemId),
|
|
90
|
+
worktreePath: this.getWorktreePath(sessionId, v.workItemId),
|
|
91
|
+
}));
|
|
92
|
+
return {
|
|
93
|
+
active: true,
|
|
94
|
+
sessionId,
|
|
95
|
+
stage,
|
|
96
|
+
briefs,
|
|
97
|
+
progress,
|
|
98
|
+
summary,
|
|
99
|
+
variants,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
emitChange() {
|
|
103
|
+
try {
|
|
104
|
+
this.events.emit('change', this.buildActiveSnapshot());
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
log.warn('emitChange listener threw', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
reportBriefs(args) {
|
|
111
|
+
const result = this.store.reportBriefs(args);
|
|
112
|
+
this.emitChange();
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
requestWork(args) {
|
|
116
|
+
const result = this.store.requestWork(args);
|
|
117
|
+
// Record lease times so variant_completed can report lease→report
|
|
118
|
+
// duration. (We don't fire a separate code_gen_leased event — the
|
|
119
|
+
// funnel signal we care about is per-variant completion duration.)
|
|
120
|
+
const resources = this.resources.get(args.sessionId);
|
|
121
|
+
if (resources) {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
for (const item of result.leasedWorkItems) {
|
|
124
|
+
resources.leasedAt.set(item.id, now);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Decorate each leased item's input with the worktreePath provisioned
|
|
128
|
+
// by approve(). The skill instructs agents to write files to
|
|
129
|
+
// `input.worktreePath` — without this decoration that field would be
|
|
130
|
+
// missing and the agent would have no idea where to edit.
|
|
131
|
+
const decorated = {
|
|
132
|
+
...result,
|
|
133
|
+
leasedWorkItems: result.leasedWorkItems.map((item) => {
|
|
134
|
+
const worktreePath = this.getWorktreePath(args.sessionId, item.id);
|
|
135
|
+
const baseInput = item.input && typeof item.input === 'object'
|
|
136
|
+
? item.input
|
|
137
|
+
: {};
|
|
138
|
+
return {
|
|
139
|
+
...item,
|
|
140
|
+
input: { ...baseInput, worktreePath },
|
|
141
|
+
};
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
144
|
+
this.emitChange();
|
|
145
|
+
return decorated;
|
|
146
|
+
}
|
|
147
|
+
getStage(sessionId) {
|
|
148
|
+
return this.store.getStage(sessionId);
|
|
149
|
+
}
|
|
150
|
+
getBriefs(sessionId) {
|
|
151
|
+
return this.store.getBriefs(sessionId);
|
|
152
|
+
}
|
|
153
|
+
getProgress(sessionId) {
|
|
154
|
+
return this.store.getProgress(sessionId);
|
|
155
|
+
}
|
|
156
|
+
getSummary(sessionId) {
|
|
157
|
+
return this.store.getSummary(sessionId);
|
|
158
|
+
}
|
|
159
|
+
hasSession(sessionId) {
|
|
160
|
+
return this.store.hasSession(sessionId);
|
|
161
|
+
}
|
|
162
|
+
/** Resolve a worktree path for a code-gen work item, if provisioned. */
|
|
163
|
+
getWorktreePath(sessionId, workItemId) {
|
|
164
|
+
return this.resources.get(sessionId)?.worktrees.get(workItemId)
|
|
165
|
+
?.worktreePath;
|
|
166
|
+
}
|
|
167
|
+
/** Resolve the dev server port for a code-gen work item, if running. */
|
|
168
|
+
getDevServerPort(sessionId, workItemId) {
|
|
169
|
+
return this.resources.get(sessionId)?.worktrees.get(workItemId)?.port;
|
|
170
|
+
}
|
|
171
|
+
// --- Mutation wrappers (state + side effects) ---------------------------
|
|
172
|
+
async approve(args) {
|
|
173
|
+
const result = this.store.approve(args);
|
|
174
|
+
this.ensureResources(args.sessionId);
|
|
175
|
+
this.telemetry.track('agent_variants.approved', {
|
|
176
|
+
source: 'mcp',
|
|
177
|
+
sessionId: args.sessionId,
|
|
178
|
+
approvedCount: result.approvedCount,
|
|
179
|
+
totalBriefs: result.totalCount,
|
|
180
|
+
isFresh: Boolean(result.scaffoldBaseWorkItemId),
|
|
181
|
+
});
|
|
182
|
+
this.emitChange();
|
|
183
|
+
// Await provisioning before returning so request_work always sees a
|
|
184
|
+
// populated worktreePath on each leased item. Provisioning is fast
|
|
185
|
+
// (`git worktree add` per variant + node_modules symlink), and the
|
|
186
|
+
// agent has nothing useful to do between approve and request_work
|
|
187
|
+
// anyway — running them serially is the contract the skill assumes.
|
|
188
|
+
try {
|
|
189
|
+
await this.provisionWorktrees(args.sessionId, result);
|
|
190
|
+
this.emitChange();
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
log.error(`provisionWorktrees failed for session ${args.sessionId}`, err);
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
async reportComplete(args) {
|
|
199
|
+
const result = this.store.reportComplete(args);
|
|
200
|
+
this.emitChange();
|
|
201
|
+
// Per-variant telemetry on terminal item statuses (skip 'running'
|
|
202
|
+
// heartbeats and the brief work item — only code_gen / scaffold_base
|
|
203
|
+
// matter for the funnel).
|
|
204
|
+
if (args.status === 'succeeded' ||
|
|
205
|
+
args.status === 'failed' ||
|
|
206
|
+
args.status === 'cancelled') {
|
|
207
|
+
const resources = this.resources.get(args.sessionId);
|
|
208
|
+
const leasedAt = resources?.leasedAt.get(args.workItemId);
|
|
209
|
+
const durationMs = leasedAt !== undefined ? Date.now() - leasedAt : null;
|
|
210
|
+
this.telemetry.track('agent_variants.variant_completed', {
|
|
211
|
+
source: 'mcp',
|
|
212
|
+
sessionId: args.sessionId,
|
|
213
|
+
workItemId: args.workItemId,
|
|
214
|
+
status: args.status,
|
|
215
|
+
attempt: args.attempt,
|
|
216
|
+
durationMs,
|
|
217
|
+
hasError: Boolean(args.error),
|
|
218
|
+
errorCode: args.error?.code ?? null,
|
|
219
|
+
isScaffold: resources?.scaffoldBaseWorkItemId === args.workItemId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (contracts_1.TERMINAL_STAGES.has(result.stage)) {
|
|
223
|
+
const resources = this.resources.get(args.sessionId);
|
|
224
|
+
if (resources && resources.terminalAt === undefined) {
|
|
225
|
+
resources.terminalAt = Date.now();
|
|
226
|
+
const totalLatencyMs = resources.terminalAt - resources.startedAt;
|
|
227
|
+
const summary = result.summary ?? this.store.getSummary(args.sessionId);
|
|
228
|
+
this.telemetry.track('agent_variants.terminal_state', {
|
|
229
|
+
source: 'mcp',
|
|
230
|
+
sessionId: args.sessionId,
|
|
231
|
+
terminalStage: result.stage,
|
|
232
|
+
successCount: summary.successCount,
|
|
233
|
+
failureCount: summary.failureCount,
|
|
234
|
+
cancelledCount: summary.cancelledCount,
|
|
235
|
+
totalLatencyMs,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (args.status === 'succeeded') {
|
|
240
|
+
void this.handleSucceededReport(args.sessionId, args.workItemId).catch((err) => {
|
|
241
|
+
log.error(`handleSucceededReport failed for ${args.sessionId}/${args.workItemId}`, err);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// Teardown is deferred to commitVariant / cancel. Terminal stage just
|
|
245
|
+
// means all variants are done; the user still needs to review and pick
|
|
246
|
+
// one. Resources (worktrees + captured diffs) live until that pick lands
|
|
247
|
+
// on the bridge.
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
async cancel(args) {
|
|
251
|
+
const stageBefore = this.store.hasSession(args.sessionId)
|
|
252
|
+
? this.store.getStage(args.sessionId)
|
|
253
|
+
: null;
|
|
254
|
+
const result = this.store.cancel(args);
|
|
255
|
+
if (this.activeSessionId === args.sessionId) {
|
|
256
|
+
this.activeSessionId = null;
|
|
257
|
+
}
|
|
258
|
+
this.telemetry.track('agent_variants.cancelled', {
|
|
259
|
+
source: 'mcp',
|
|
260
|
+
sessionId: args.sessionId,
|
|
261
|
+
reason: args.reason ?? null,
|
|
262
|
+
fromStage: stageBefore,
|
|
263
|
+
});
|
|
264
|
+
this.emitChange();
|
|
265
|
+
void this.teardownSession(args.sessionId, 'cancel').catch((err) => {
|
|
266
|
+
log.error(`teardownSession failed for ${args.sessionId}`, err);
|
|
267
|
+
});
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* User has reviewed the rendered variants in chat and picked one. Look up
|
|
272
|
+
* the captured diff, build a VariantPickEnvelope, and enqueue to the
|
|
273
|
+
* pending-changes channel. Idempotent on (sessionId, variantId): repeating
|
|
274
|
+
* the call returns duplicate=true without re-enqueueing.
|
|
275
|
+
*/
|
|
276
|
+
async commitVariant(args) {
|
|
277
|
+
// Idempotent path — SessionStore.recordVariantPick is the source of truth
|
|
278
|
+
// for which variant the user picked. If the same variant is being
|
|
279
|
+
// committed again, replay the previously stored envelope's metadata
|
|
280
|
+
// without going back through resources (which may have been torn down).
|
|
281
|
+
const existingPick = this.store.getVariantPick(args.sessionId);
|
|
282
|
+
if (existingPick && existingPick.variantId === args.variantId) {
|
|
283
|
+
return {
|
|
284
|
+
enqueued: false,
|
|
285
|
+
duplicate: true,
|
|
286
|
+
changedFilesCount: existingPick.changedFilesCount,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const resources = this.resources.get(args.sessionId);
|
|
290
|
+
if (!resources) {
|
|
291
|
+
throw new errors_1.AgentVariantsError('SESSION_NOT_FOUND', `No live resources for session ${args.sessionId} (already torn down?)`);
|
|
292
|
+
}
|
|
293
|
+
const record = resources.worktrees.get(args.variantId);
|
|
294
|
+
if (!record) {
|
|
295
|
+
throw new errors_1.AgentVariantsError('WORK_ITEM_NOT_FOUND', `Unknown variantId ${args.variantId} for session ${args.sessionId}`);
|
|
296
|
+
}
|
|
297
|
+
if (record.diff === undefined) {
|
|
298
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Variant ${args.variantId} has no captured diff yet — wait for report_variant_complete(succeeded) first`);
|
|
299
|
+
}
|
|
300
|
+
const env = await this.resolveEnv(args.sessionId);
|
|
301
|
+
const input = this.store.getWorkItemInput(args.sessionId, args.variantId);
|
|
302
|
+
const projectContext = this.store.getProjectContext(args.sessionId);
|
|
303
|
+
const changedFilesCount = countDiffFiles(record.diff);
|
|
304
|
+
const payload = projectContext.kind === 'fresh'
|
|
305
|
+
? {
|
|
306
|
+
kind: 'new-project',
|
|
307
|
+
destinationPath: projectContext.workspacePath,
|
|
308
|
+
sourceWorktreePath: record.worktreePath,
|
|
309
|
+
changedFilesCount,
|
|
310
|
+
}
|
|
311
|
+
: {
|
|
312
|
+
kind: 'diff',
|
|
313
|
+
diff: record.diff,
|
|
314
|
+
target: input.target,
|
|
315
|
+
changedFilesCount,
|
|
316
|
+
};
|
|
317
|
+
const envelope = {
|
|
318
|
+
sourceSessionId: args.sessionId,
|
|
319
|
+
variantId: args.variantId,
|
|
320
|
+
variantLabel: input.briefLabel,
|
|
321
|
+
destinationPath: projectContext.kind === 'fresh'
|
|
322
|
+
? projectContext.workspacePath
|
|
323
|
+
: env.projectPath,
|
|
324
|
+
changedFilesCount,
|
|
325
|
+
payload,
|
|
326
|
+
};
|
|
327
|
+
// Record the pick in the SessionStore so future commitVariant calls with
|
|
328
|
+
// the same variantId are idempotent and a different variantId returns
|
|
329
|
+
// PENDING_CHANGE_CONFLICT (already picked).
|
|
330
|
+
this.store.recordVariantPick({
|
|
331
|
+
sessionId: args.sessionId,
|
|
332
|
+
envelope,
|
|
333
|
+
});
|
|
334
|
+
const enqueueResult = this.adapter.enqueue(envelope);
|
|
335
|
+
resources.committedVariantIds.add(args.variantId);
|
|
336
|
+
if (this.activeSessionId === args.sessionId) {
|
|
337
|
+
this.activeSessionId = null;
|
|
338
|
+
}
|
|
339
|
+
const dwellMsFromTerminal = resources.terminalAt
|
|
340
|
+
? Date.now() - resources.terminalAt
|
|
341
|
+
: null;
|
|
342
|
+
this.telemetry.track('agent_variants.committed', {
|
|
343
|
+
source: 'mcp',
|
|
344
|
+
sessionId: args.sessionId,
|
|
345
|
+
workItemId: args.variantId,
|
|
346
|
+
payloadKind: payload.kind,
|
|
347
|
+
changedFilesCount,
|
|
348
|
+
dwellMsFromTerminal,
|
|
349
|
+
isFresh: projectContext.kind === 'fresh',
|
|
350
|
+
});
|
|
351
|
+
this.emitChange();
|
|
352
|
+
log.info(`Variant ${args.variantId} (${input.briefLabel}) committed by user → enqueued=${enqueueResult.enqueued}`);
|
|
353
|
+
// Once a variant is committed, the unpicked worktrees are no longer
|
|
354
|
+
// useful. Schedule teardown in the background so the agent's
|
|
355
|
+
// commit_variant call returns immediately.
|
|
356
|
+
void this.teardownSession(args.sessionId, 'committed').catch((err) => {
|
|
357
|
+
log.warn(`teardownSession after commit failed for ${args.sessionId}`, err);
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
enqueued: enqueueResult.enqueued,
|
|
361
|
+
duplicate: enqueueResult.duplicate,
|
|
362
|
+
changedFilesCount,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/** Read the captured diff for a code-gen variant, if available. */
|
|
366
|
+
getVariantDiff(sessionId, variantId) {
|
|
367
|
+
return this.resources.get(sessionId)?.worktrees.get(variantId)?.diff;
|
|
368
|
+
}
|
|
369
|
+
// --- Side-effect implementations ----------------------------------------
|
|
370
|
+
async provisionWorktrees(sessionId, approveResult) {
|
|
371
|
+
const resources = this.ensureResources(sessionId);
|
|
372
|
+
const totalCount = approveResult.codeGenWorkItemIds.length +
|
|
373
|
+
(approveResult.scaffoldBaseWorkItemId ? 1 : 0);
|
|
374
|
+
if (totalCount === 0)
|
|
375
|
+
return;
|
|
376
|
+
log.info(`Provisioning ${totalCount} worktree(s) for session ${sessionId}`);
|
|
377
|
+
const paths = await this.worktrees.createWorktrees(sessionId, totalCount);
|
|
378
|
+
let cursor = 0;
|
|
379
|
+
if (approveResult.scaffoldBaseWorkItemId) {
|
|
380
|
+
resources.scaffoldBaseWorkItemId = approveResult.scaffoldBaseWorkItemId;
|
|
381
|
+
resources.worktrees.set(approveResult.scaffoldBaseWorkItemId, {
|
|
382
|
+
workItemId: approveResult.scaffoldBaseWorkItemId,
|
|
383
|
+
worktreePath: paths[cursor],
|
|
384
|
+
});
|
|
385
|
+
cursor += 1;
|
|
386
|
+
}
|
|
387
|
+
for (const id of approveResult.codeGenWorkItemIds) {
|
|
388
|
+
const path = paths[cursor];
|
|
389
|
+
cursor += 1;
|
|
390
|
+
resources.worktrees.set(id, { workItemId: id, worktreePath: path });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async handleSucceededReport(sessionId, workItemId) {
|
|
394
|
+
const resources = this.resources.get(sessionId);
|
|
395
|
+
const record = resources?.worktrees.get(workItemId);
|
|
396
|
+
if (!resources || !record) {
|
|
397
|
+
log.warn(`No worktree record for ${sessionId}/${workItemId}; skipping diff capture`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// scaffold_base sets up the base for dependent code_gen items. No diff
|
|
401
|
+
// to capture from it — its result is the disk state of the worktree.
|
|
402
|
+
if (resources.scaffoldBaseWorkItemId === workItemId) {
|
|
403
|
+
log.info(`scaffold_base ${workItemId} succeeded; awaiting dependent code-gen`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
record.diff = await this.worktrees.getDiff(record.worktreePath);
|
|
408
|
+
log.info(`Variant ${workItemId} diff captured (${countDiffFiles(record.diff)} files)`);
|
|
409
|
+
this.emitChange();
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
log.warn(`getDiff failed for ${record.worktreePath}`, err);
|
|
413
|
+
}
|
|
414
|
+
// Bring up a dev server in the variant's worktree so the user can cycle
|
|
415
|
+
// through live variants in the iframe via the chip. Failures here are
|
|
416
|
+
// logged but non-fatal — the user can still pick by reading the diff.
|
|
417
|
+
try {
|
|
418
|
+
const env = await this.resolveEnv(sessionId);
|
|
419
|
+
const port = await this.worktrees.getFreePort();
|
|
420
|
+
const cwd = await this.worktrees.getProjectCwdInWorktree(record.worktreePath);
|
|
421
|
+
const { cmd, args, env: spawnEnv } = env.buildDevCommand
|
|
422
|
+
? env.buildDevCommand(port)
|
|
423
|
+
: {
|
|
424
|
+
cmd: env.packageManager,
|
|
425
|
+
args: [env.devCommand, '--port', String(port)],
|
|
426
|
+
env: { PORT: String(port) },
|
|
427
|
+
};
|
|
428
|
+
const proc = await this.worktrees.startDevServer(cwd, port, cmd, args, spawnEnv);
|
|
429
|
+
record.port = port;
|
|
430
|
+
record.devServerProcess = proc;
|
|
431
|
+
this.emitChange();
|
|
432
|
+
log.info(`Variant ${workItemId} dev server up on port ${port} (worktree ${record.worktreePath}; cmd: ${cmd} ${args.join(' ')})`);
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
log.warn(`Failed to start dev server for variant ${workItemId}; live preview disabled for this variant`, err);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async teardownSession(sessionId, reason) {
|
|
439
|
+
const resources = this.resources.get(sessionId);
|
|
440
|
+
if (!resources)
|
|
441
|
+
return;
|
|
442
|
+
if (resources.cleanupStarted)
|
|
443
|
+
return;
|
|
444
|
+
resources.cleanupStarted = true;
|
|
445
|
+
log.info(`Tearing down session ${sessionId} (reason: ${reason})`);
|
|
446
|
+
// Stop dev servers in parallel; ignore individual failures.
|
|
447
|
+
const stops = [...resources.worktrees.values()]
|
|
448
|
+
.filter((r) => r.devServerProcess)
|
|
449
|
+
.map((r) => this.worktrees
|
|
450
|
+
.stopDevServer(r.devServerProcess)
|
|
451
|
+
.catch((err) => log.warn(`stopDevServer failed for ${sessionId}/${r.workItemId}`, err)));
|
|
452
|
+
await Promise.all(stops);
|
|
453
|
+
try {
|
|
454
|
+
await this.worktrees.cleanupSession(sessionId);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
log.warn(`cleanupSession (worktree removal) failed for ${sessionId}`, err);
|
|
458
|
+
}
|
|
459
|
+
this.resources.delete(sessionId);
|
|
460
|
+
}
|
|
461
|
+
ensureResources(sessionId) {
|
|
462
|
+
let r = this.resources.get(sessionId);
|
|
463
|
+
if (!r) {
|
|
464
|
+
r = {
|
|
465
|
+
sessionId,
|
|
466
|
+
worktrees: new Map(),
|
|
467
|
+
cleanupStarted: false,
|
|
468
|
+
committedVariantIds: new Set(),
|
|
469
|
+
startedAt: Date.now(),
|
|
470
|
+
leasedAt: new Map(),
|
|
471
|
+
};
|
|
472
|
+
this.resources.set(sessionId, r);
|
|
473
|
+
}
|
|
474
|
+
return r;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
exports.AgentVariantsOrchestrator = AgentVariantsOrchestrator;
|
|
478
|
+
function countDiffFiles(diff) {
|
|
479
|
+
// Each file in a unified diff starts with "diff --git ".
|
|
480
|
+
return (diff.match(/^diff --git /gm) ?? []).length;
|
|
481
|
+
}
|
|
482
|
+
//# sourceMappingURL=WorktreeOrchestrator.js.map
|