oxe-cc 0.9.2 → 1.0.0
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/.cursor/commands/oxe-retro.md +2 -2
- package/.cursor/commands/oxe-spec.md +2 -2
- package/.github/prompts/oxe-retro.prompt.md +2 -2
- package/.github/prompts/oxe-spec.prompt.md +2 -2
- package/README.md +1 -1
- package/bin/banner.txt +1 -1
- package/bin/lib/oxe-context-engine.cjs +1 -0
- package/bin/lib/oxe-dashboard.cjs +9 -7
- package/bin/lib/oxe-operational.cjs +569 -4
- package/bin/oxe-cc.js +141 -57
- package/commands/oxe/retro.md +2 -2
- package/commands/oxe/spec.md +2 -2
- package/lib/runtime/compiler/graph-compiler.d.ts +83 -0
- package/lib/runtime/compiler/graph-compiler.js +135 -0
- package/lib/runtime/compiler/index.d.ts +1 -0
- package/lib/runtime/compiler/index.js +17 -0
- package/lib/runtime/context/context-pack-builder.d.ts +36 -0
- package/lib/runtime/context/context-pack-builder.js +136 -0
- package/lib/runtime/context/index.d.ts +1 -0
- package/lib/runtime/context/index.js +17 -0
- package/lib/runtime/delivery/branch-manager.d.ts +19 -0
- package/lib/runtime/delivery/branch-manager.js +78 -0
- package/lib/runtime/delivery/ci-checks.d.ts +34 -0
- package/lib/runtime/delivery/ci-checks.js +209 -0
- package/lib/runtime/delivery/index.d.ts +3 -0
- package/lib/runtime/delivery/index.js +19 -0
- package/lib/runtime/delivery/pr-manager.d.ts +30 -0
- package/lib/runtime/delivery/pr-manager.js +82 -0
- package/lib/runtime/events/bus.d.ts +9 -0
- package/lib/runtime/events/bus.js +63 -0
- package/lib/runtime/events/catalog.d.ts +3 -0
- package/lib/runtime/events/catalog.js +30 -0
- package/lib/runtime/events/envelope.d.ts +13 -0
- package/lib/runtime/events/envelope.js +2 -0
- package/lib/runtime/events/index.d.ts +3 -0
- package/lib/runtime/events/index.js +19 -0
- package/lib/runtime/evidence/evidence-store.d.ts +22 -0
- package/lib/runtime/evidence/evidence-store.js +106 -0
- package/lib/runtime/evidence/index.d.ts +1 -0
- package/lib/runtime/evidence/index.js +17 -0
- package/lib/runtime/gate/gate-manager.d.ts +39 -0
- package/lib/runtime/gate/gate-manager.js +104 -0
- package/lib/runtime/gate/index.d.ts +1 -0
- package/lib/runtime/gate/index.js +17 -0
- package/lib/runtime/index.d.ts +16 -0
- package/lib/runtime/index.js +40 -0
- package/lib/runtime/models/attempt.d.ts +12 -0
- package/lib/runtime/models/attempt.js +2 -0
- package/lib/runtime/models/evidence.d.ts +9 -0
- package/lib/runtime/models/evidence.js +2 -0
- package/lib/runtime/models/gate-decision.d.ts +10 -0
- package/lib/runtime/models/gate-decision.js +2 -0
- package/lib/runtime/models/index.d.ts +8 -0
- package/lib/runtime/models/index.js +24 -0
- package/lib/runtime/models/run.d.ts +13 -0
- package/lib/runtime/models/run.js +2 -0
- package/lib/runtime/models/session.d.ts +10 -0
- package/lib/runtime/models/session.js +2 -0
- package/lib/runtime/models/verification-result.d.ts +9 -0
- package/lib/runtime/models/verification-result.js +2 -0
- package/lib/runtime/models/work-item.d.ts +15 -0
- package/lib/runtime/models/work-item.js +2 -0
- package/lib/runtime/models/workspace.d.ts +25 -0
- package/lib/runtime/models/workspace.js +2 -0
- package/lib/runtime/plugins/index.d.ts +2 -0
- package/lib/runtime/plugins/index.js +18 -0
- package/lib/runtime/plugins/plugin-abi.d.ts +76 -0
- package/lib/runtime/plugins/plugin-abi.js +2 -0
- package/lib/runtime/plugins/plugin-registry.d.ts +21 -0
- package/lib/runtime/plugins/plugin-registry.js +114 -0
- package/lib/runtime/policy/index.d.ts +1 -0
- package/lib/runtime/policy/index.js +17 -0
- package/lib/runtime/policy/policy-engine.d.ts +40 -0
- package/lib/runtime/policy/policy-engine.js +80 -0
- package/lib/runtime/projection/index.d.ts +1 -0
- package/lib/runtime/projection/index.js +17 -0
- package/lib/runtime/projection/projection-engine.d.ts +11 -0
- package/lib/runtime/projection/projection-engine.js +218 -0
- package/lib/runtime/reducers/debug-reducer.d.ts +10 -0
- package/lib/runtime/reducers/debug-reducer.js +30 -0
- package/lib/runtime/reducers/index.d.ts +2 -0
- package/lib/runtime/reducers/index.js +18 -0
- package/lib/runtime/reducers/run-state-reducer.d.ts +20 -0
- package/lib/runtime/reducers/run-state-reducer.js +110 -0
- package/lib/runtime/scheduler/index.d.ts +1 -0
- package/lib/runtime/scheduler/index.js +17 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +34 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.js +166 -0
- package/lib/runtime/scheduler/scheduler.d.ts +39 -0
- package/lib/runtime/scheduler/scheduler.js +196 -0
- package/lib/runtime/verification/index.d.ts +1 -0
- package/lib/runtime/verification/index.js +17 -0
- package/lib/runtime/verification/verification-compiler.d.ts +56 -0
- package/lib/runtime/verification/verification-compiler.js +147 -0
- package/lib/runtime/workspace/index.d.ts +5 -0
- package/lib/runtime/workspace/index.js +24 -0
- package/lib/runtime/workspace/strategies/ephemeral-container.d.ts +22 -0
- package/lib/runtime/workspace/strategies/ephemeral-container.js +109 -0
- package/lib/runtime/workspace/strategies/git-worktree.d.ts +12 -0
- package/lib/runtime/workspace/strategies/git-worktree.js +79 -0
- package/lib/runtime/workspace/strategies/inplace.d.ts +10 -0
- package/lib/runtime/workspace/strategies/inplace.js +37 -0
- package/lib/runtime/workspace/workspace-manager.d.ts +13 -0
- package/lib/runtime/workspace/workspace-manager.js +2 -0
- package/lib/sdk/index.cjs +24 -7
- package/lib/sdk/index.d.ts +17 -7
- package/oxe/templates/LESSONS-METRICS.template.json +13 -0
- package/oxe/workflows/references/robustness-elevation.md +295 -0
- package/oxe/workflows/references/workflow-runtime-contracts.json +32 -4
- package/oxe/workflows/retro.md +21 -0
- package/oxe/workflows/spec.md +50 -26
- package/oxe/workflows/verify.md +36 -0
- package/package.json +9 -3
- package/packages/runtime/package.json +17 -0
- package/packages/runtime/src/compiler/graph-compiler.ts +245 -0
- package/packages/runtime/src/compiler/index.ts +1 -0
- package/packages/runtime/src/context/context-pack-builder.ts +193 -0
- package/packages/runtime/src/context/index.ts +1 -0
- package/packages/runtime/src/delivery/branch-manager.ts +84 -0
- package/packages/runtime/src/delivery/ci-checks.ts +252 -0
- package/packages/runtime/src/delivery/index.ts +3 -0
- package/packages/runtime/src/delivery/pr-manager.ts +112 -0
- package/packages/runtime/src/events/bus.ts +92 -0
- package/packages/runtime/src/events/catalog.ts +29 -0
- package/packages/runtime/src/events/envelope.ts +14 -0
- package/packages/runtime/src/events/index.ts +3 -0
- package/packages/runtime/src/evidence/evidence-store.ts +130 -0
- package/packages/runtime/src/evidence/index.ts +1 -0
- package/packages/runtime/src/gate/gate-manager.ts +137 -0
- package/packages/runtime/src/gate/index.ts +1 -0
- package/packages/runtime/src/index.ts +32 -0
- package/packages/runtime/src/models/attempt.ts +19 -0
- package/packages/runtime/src/models/evidence.ts +21 -0
- package/packages/runtime/src/models/gate-decision.ts +21 -0
- package/packages/runtime/src/models/index.ts +8 -0
- package/packages/runtime/src/models/run.ts +24 -0
- package/packages/runtime/src/models/session.ts +11 -0
- package/packages/runtime/src/models/verification-result.ts +10 -0
- package/packages/runtime/src/models/work-item.ts +25 -0
- package/packages/runtime/src/models/workspace.ts +28 -0
- package/packages/runtime/src/plugins/index.ts +2 -0
- package/packages/runtime/src/plugins/plugin-abi.ts +95 -0
- package/packages/runtime/src/plugins/plugin-registry.ts +119 -0
- package/packages/runtime/src/policy/index.ts +1 -0
- package/packages/runtime/src/policy/policy-engine.ts +113 -0
- package/packages/runtime/src/projection/index.ts +1 -0
- package/packages/runtime/src/projection/projection-engine.ts +249 -0
- package/packages/runtime/src/reducers/debug-reducer.ts +36 -0
- package/packages/runtime/src/reducers/index.ts +2 -0
- package/packages/runtime/src/reducers/run-state-reducer.ts +127 -0
- package/packages/runtime/src/scheduler/index.ts +1 -0
- package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +231 -0
- package/packages/runtime/src/scheduler/scheduler.ts +281 -0
- package/packages/runtime/src/verification/index.ts +1 -0
- package/packages/runtime/src/verification/verification-compiler.ts +225 -0
- package/packages/runtime/src/workspace/index.ts +5 -0
- package/packages/runtime/src/workspace/strategies/ephemeral-container.ts +121 -0
- package/packages/runtime/src/workspace/strategies/git-worktree.ts +77 -0
- package/packages/runtime/src/workspace/strategies/inplace.ts +35 -0
- package/packages/runtime/src/workspace/workspace-manager.ts +15 -0
- package/packages/runtime/tsconfig.json +17 -0
- package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
- package/vscode-extension/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import type { EvidenceType } from '../models/evidence';
|
|
4
|
+
import type { VerificationStatus } from '../models/verification-result';
|
|
5
|
+
|
|
6
|
+
export type CheckType =
|
|
7
|
+
| 'unit'
|
|
8
|
+
| 'integration'
|
|
9
|
+
| 'contract'
|
|
10
|
+
| 'smoke'
|
|
11
|
+
| 'policy'
|
|
12
|
+
| 'security'
|
|
13
|
+
| 'ux_snapshot'
|
|
14
|
+
| 'performance_baseline'
|
|
15
|
+
| 'custom';
|
|
16
|
+
|
|
17
|
+
export interface AcceptanceCheck {
|
|
18
|
+
id: string;
|
|
19
|
+
type: CheckType;
|
|
20
|
+
command: string | null;
|
|
21
|
+
evidence_type_expected: EvidenceType;
|
|
22
|
+
acceptance_ref: string | null;
|
|
23
|
+
description: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AcceptanceCheckSuite {
|
|
27
|
+
checks: AcceptanceCheck[];
|
|
28
|
+
compiled_at: string;
|
|
29
|
+
spec_hash: string;
|
|
30
|
+
plan_hash: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CheckResult {
|
|
34
|
+
check_id: string;
|
|
35
|
+
acceptance_ref: string | null;
|
|
36
|
+
status: VerificationStatus;
|
|
37
|
+
stdout: string;
|
|
38
|
+
stderr: string;
|
|
39
|
+
exit_code: number | null;
|
|
40
|
+
duration_ms: number;
|
|
41
|
+
error: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mirror of ParsedSpec/ParsedPlan (same as in graph-compiler to avoid circular deps)
|
|
45
|
+
interface Criterion {
|
|
46
|
+
id: string;
|
|
47
|
+
criterion: string;
|
|
48
|
+
howToVerify: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ParsedSpecLike {
|
|
52
|
+
objective: string | null;
|
|
53
|
+
criteria: Criterion[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ParsedTaskLike {
|
|
57
|
+
id: string;
|
|
58
|
+
verifyCommand: string | null;
|
|
59
|
+
aceite: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ParsedPlanLike {
|
|
63
|
+
tasks: ParsedTaskLike[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inferCheckType(howToVerify: string): CheckType {
|
|
67
|
+
const v = howToVerify.toLowerCase();
|
|
68
|
+
if (v.includes('npm test') || v.includes('jest') || v.includes('vitest') || v.includes('node --test')) return 'unit';
|
|
69
|
+
if (v.includes('postman') || v.includes('newman') || v.includes('integration')) return 'integration';
|
|
70
|
+
if (v.includes('smoke') || v.includes('curl')) return 'smoke';
|
|
71
|
+
if (v.includes('eslint') || v.includes('lint') || v.includes('oxe-policy')) return 'policy';
|
|
72
|
+
if (v.includes('security') || v.includes('audit') || v.includes('trivy')) return 'security';
|
|
73
|
+
return 'custom';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inferEvidenceType(checkType: CheckType): EvidenceType {
|
|
77
|
+
switch (checkType) {
|
|
78
|
+
case 'unit': return 'junit_xml';
|
|
79
|
+
case 'integration': return 'api_output';
|
|
80
|
+
case 'security': return 'security_report';
|
|
81
|
+
case 'policy': return 'log';
|
|
82
|
+
default: return 'stdout';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function compile(
|
|
87
|
+
spec: ParsedSpecLike,
|
|
88
|
+
plan: ParsedPlanLike
|
|
89
|
+
): AcceptanceCheckSuite {
|
|
90
|
+
const checks: AcceptanceCheck[] = [];
|
|
91
|
+
const seenRefs = new Set<string>();
|
|
92
|
+
|
|
93
|
+
// Generate checks from spec criteria
|
|
94
|
+
for (const criterion of spec.criteria) {
|
|
95
|
+
// Find the verify command from the task that references this criterion
|
|
96
|
+
const task = plan.tasks.find((t) => t.aceite.includes(criterion.id));
|
|
97
|
+
const command = task?.verifyCommand ?? null;
|
|
98
|
+
const type = inferCheckType(criterion.howToVerify);
|
|
99
|
+
|
|
100
|
+
checks.push({
|
|
101
|
+
id: `check-${criterion.id.toLowerCase()}`,
|
|
102
|
+
type,
|
|
103
|
+
command: command ?? (criterion.howToVerify.startsWith('#') ? null : criterion.howToVerify),
|
|
104
|
+
evidence_type_expected: inferEvidenceType(type),
|
|
105
|
+
acceptance_ref: criterion.id,
|
|
106
|
+
description: criterion.criterion,
|
|
107
|
+
});
|
|
108
|
+
seenRefs.add(criterion.id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add checks for task verify commands not already covered
|
|
112
|
+
for (const task of plan.tasks) {
|
|
113
|
+
if (!task.verifyCommand) continue;
|
|
114
|
+
const uncovered = task.aceite.filter((ref) => !seenRefs.has(ref));
|
|
115
|
+
if (uncovered.length === 0 && checks.some((c) => c.command === task.verifyCommand)) continue;
|
|
116
|
+
|
|
117
|
+
checks.push({
|
|
118
|
+
id: `check-task-${task.id.toLowerCase()}`,
|
|
119
|
+
type: inferCheckType(task.verifyCommand),
|
|
120
|
+
command: task.verifyCommand,
|
|
121
|
+
evidence_type_expected: 'stdout',
|
|
122
|
+
acceptance_ref: uncovered[0] ?? null,
|
|
123
|
+
description: `Verify command for task ${task.id}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
checks,
|
|
129
|
+
compiled_at: new Date().toISOString(),
|
|
130
|
+
spec_hash: hashObject(spec),
|
|
131
|
+
plan_hash: hashObject(plan),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runCheck(
|
|
136
|
+
check: AcceptanceCheck,
|
|
137
|
+
cwd: string,
|
|
138
|
+
timeoutMs = 60_000
|
|
139
|
+
): Promise<CheckResult> {
|
|
140
|
+
if (!check.command) {
|
|
141
|
+
return {
|
|
142
|
+
check_id: check.id,
|
|
143
|
+
acceptance_ref: check.acceptance_ref,
|
|
144
|
+
status: 'skip',
|
|
145
|
+
stdout: '',
|
|
146
|
+
stderr: '',
|
|
147
|
+
exit_code: null,
|
|
148
|
+
duration_ms: 0,
|
|
149
|
+
error: null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const start = Date.now();
|
|
154
|
+
try {
|
|
155
|
+
// Split command into program + args (simple split; no shell expansion)
|
|
156
|
+
const parts = check.command.split(/\s+/);
|
|
157
|
+
const prog = parts[0];
|
|
158
|
+
const args = parts.slice(1);
|
|
159
|
+
|
|
160
|
+
const result = spawnSync(prog, args, {
|
|
161
|
+
cwd,
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
timeout: timeoutMs,
|
|
164
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const duration_ms = Date.now() - start;
|
|
168
|
+
const status: VerificationStatus = result.status === 0 ? 'pass' : 'fail';
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
check_id: check.id,
|
|
172
|
+
acceptance_ref: check.acceptance_ref,
|
|
173
|
+
status,
|
|
174
|
+
stdout: result.stdout ?? '',
|
|
175
|
+
stderr: result.stderr ?? '',
|
|
176
|
+
exit_code: result.status ?? null,
|
|
177
|
+
duration_ms,
|
|
178
|
+
error: result.error ? String(result.error) : null,
|
|
179
|
+
};
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return {
|
|
182
|
+
check_id: check.id,
|
|
183
|
+
acceptance_ref: check.acceptance_ref,
|
|
184
|
+
status: 'error',
|
|
185
|
+
stdout: '',
|
|
186
|
+
stderr: '',
|
|
187
|
+
exit_code: null,
|
|
188
|
+
duration_ms: Date.now() - start,
|
|
189
|
+
error: String(err),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function runSuite(
|
|
195
|
+
suite: AcceptanceCheckSuite,
|
|
196
|
+
cwd: string,
|
|
197
|
+
timeoutMs = 60_000
|
|
198
|
+
): Promise<CheckResult[]> {
|
|
199
|
+
const results: CheckResult[] = [];
|
|
200
|
+
for (const check of suite.checks) {
|
|
201
|
+
results.push(await runCheck(check, cwd, timeoutMs));
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function summarizeSuite(results: CheckResult[]): {
|
|
207
|
+
total: number;
|
|
208
|
+
pass: number;
|
|
209
|
+
fail: number;
|
|
210
|
+
skip: number;
|
|
211
|
+
error: number;
|
|
212
|
+
allPassed: boolean;
|
|
213
|
+
} {
|
|
214
|
+
const counts = { total: results.length, pass: 0, fail: 0, skip: 0, error: 0 };
|
|
215
|
+
for (const r of results) counts[r.status]++;
|
|
216
|
+
return { ...counts, allPassed: counts.fail === 0 && counts.error === 0 };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function hashObject(obj: unknown): string {
|
|
220
|
+
return crypto
|
|
221
|
+
.createHash('sha256')
|
|
222
|
+
.update(JSON.stringify(obj))
|
|
223
|
+
.digest('hex')
|
|
224
|
+
.slice(0, 12);
|
|
225
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './workspace-manager';
|
|
2
|
+
export { InplaceWorkspaceManager } from './strategies/inplace';
|
|
3
|
+
export { GitWorktreeManager } from './strategies/git-worktree';
|
|
4
|
+
export { EphemeralContainerManager } from './strategies/ephemeral-container';
|
|
5
|
+
export type { ContainerOptions } from './strategies/ephemeral-container';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'child_process';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import type { WorkspaceManager, WorkspaceRequest } from '../workspace-manager';
|
|
4
|
+
import type { WorkspaceLease, SnapshotRef } from '../../models/workspace';
|
|
5
|
+
import { GitWorktreeManager } from './git-worktree';
|
|
6
|
+
|
|
7
|
+
export interface ContainerOptions {
|
|
8
|
+
image: string;
|
|
9
|
+
mountPath: string;
|
|
10
|
+
extraEnv?: Record<string, string>;
|
|
11
|
+
/** Gracefully fall back to git_worktree if Docker is unavailable */
|
|
12
|
+
fallback?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isDockerAvailable(): boolean {
|
|
16
|
+
const result = spawnSync('docker', ['version', '--format', '{{.Server.Version}}'], {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
timeout: 5000,
|
|
19
|
+
});
|
|
20
|
+
return result.status === 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class EphemeralContainerManager implements WorkspaceManager {
|
|
24
|
+
private readonly fallbackManager: GitWorktreeManager;
|
|
25
|
+
private containerIds = new Map<string, string>();
|
|
26
|
+
private useFallback = false;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly projectRoot: string,
|
|
30
|
+
private readonly opts: ContainerOptions = { image: 'node:20-alpine', mountPath: '/workspace', fallback: true }
|
|
31
|
+
) {
|
|
32
|
+
this.fallbackManager = new GitWorktreeManager(projectRoot);
|
|
33
|
+
if (!isDockerAvailable()) {
|
|
34
|
+
if (opts.fallback !== false) {
|
|
35
|
+
this.useFallback = true;
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error('Docker is not available and fallback is disabled');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get usingFallback(): boolean { return this.useFallback; }
|
|
43
|
+
|
|
44
|
+
async allocate(req: WorkspaceRequest): Promise<WorkspaceLease> {
|
|
45
|
+
if (this.useFallback) return this.fallbackManager.allocate(req);
|
|
46
|
+
|
|
47
|
+
const wsId = `ws-container-${req.work_item_id}-a${req.attempt_number}`;
|
|
48
|
+
const envArgs = Object.entries(this.opts.extraEnv ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
|
|
49
|
+
|
|
50
|
+
const result = spawnSync('docker', [
|
|
51
|
+
'run', '-d',
|
|
52
|
+
'-v', `${this.projectRoot}:${this.opts.mountPath}`,
|
|
53
|
+
'-w', this.opts.mountPath,
|
|
54
|
+
...envArgs,
|
|
55
|
+
this.opts.image,
|
|
56
|
+
'sleep', '3600',
|
|
57
|
+
], { encoding: 'utf8' });
|
|
58
|
+
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
if (this.opts.fallback !== false) {
|
|
61
|
+
this.useFallback = true;
|
|
62
|
+
return this.fallbackManager.allocate(req);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`docker run failed: ${result.stderr}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const containerId = result.stdout.trim().slice(0, 12);
|
|
68
|
+
this.containerIds.set(wsId, containerId);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
workspace_id: wsId,
|
|
72
|
+
strategy: 'ephemeral_container',
|
|
73
|
+
branch: null,
|
|
74
|
+
base_commit: null,
|
|
75
|
+
root_path: `docker:${containerId}:${this.opts.mountPath}`,
|
|
76
|
+
ttl_minutes: 60,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async snapshot(id: string): Promise<SnapshotRef> {
|
|
81
|
+
if (this.useFallback) return this.fallbackManager.snapshot(id);
|
|
82
|
+
const containerId = this.containerIds.get(id);
|
|
83
|
+
if (!containerId) throw new Error(`Container for workspace ${id} not found`);
|
|
84
|
+
|
|
85
|
+
const tag = `oxe-snap-${crypto.randomBytes(4).toString('hex')}`;
|
|
86
|
+
execFileSync('docker', ['commit', containerId, tag]);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
snapshot_id: tag,
|
|
90
|
+
workspace_id: id,
|
|
91
|
+
commit: tag,
|
|
92
|
+
created_at: new Date().toISOString(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async reset(id: string, snapRef: SnapshotRef): Promise<void> {
|
|
97
|
+
if (this.useFallback) return this.fallbackManager.reset(id, snapRef);
|
|
98
|
+
const containerId = this.containerIds.get(id);
|
|
99
|
+
if (!containerId) return;
|
|
100
|
+
// Stop current container and start from snapshot
|
|
101
|
+
spawnSync('docker', ['stop', containerId]);
|
|
102
|
+
spawnSync('docker', ['rm', containerId]);
|
|
103
|
+
const result = spawnSync('docker', [
|
|
104
|
+
'run', '-d',
|
|
105
|
+
'-v', `${this.projectRoot}:${this.opts.mountPath}`,
|
|
106
|
+
snapRef.commit,
|
|
107
|
+
'sleep', '3600',
|
|
108
|
+
], { encoding: 'utf8' });
|
|
109
|
+
const newId = result.stdout.trim().slice(0, 12);
|
|
110
|
+
this.containerIds.set(id, newId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async dispose(id: string): Promise<void> {
|
|
114
|
+
if (this.useFallback) return this.fallbackManager.dispose(id);
|
|
115
|
+
const containerId = this.containerIds.get(id);
|
|
116
|
+
if (!containerId) return;
|
|
117
|
+
spawnSync('docker', ['stop', containerId], { encoding: 'utf8' });
|
|
118
|
+
spawnSync('docker', ['rm', containerId], { encoding: 'utf8' });
|
|
119
|
+
this.containerIds.delete(id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import type { WorkspaceManager, WorkspaceRequest } from '../workspace-manager';
|
|
6
|
+
import type { WorkspaceLease, SnapshotRef } from '../../models/workspace';
|
|
7
|
+
|
|
8
|
+
export class GitWorktreeManager implements WorkspaceManager {
|
|
9
|
+
private leases = new Map<string, WorkspaceLease>();
|
|
10
|
+
|
|
11
|
+
constructor(private readonly projectRoot: string) {}
|
|
12
|
+
|
|
13
|
+
async allocate(req: WorkspaceRequest): Promise<WorkspaceLease> {
|
|
14
|
+
const wsId = `ws-${req.work_item_id}-a${req.attempt_number}`;
|
|
15
|
+
const branch = `oxe/${req.work_item_id}-attempt${req.attempt_number}`;
|
|
16
|
+
const worktreePath = path.join(this.projectRoot, '.oxe', 'workspaces', wsId);
|
|
17
|
+
|
|
18
|
+
const baseCommit = this.git(['rev-parse', 'HEAD']).trim();
|
|
19
|
+
|
|
20
|
+
fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Create worktree on a new branch starting from HEAD
|
|
23
|
+
this.git(['worktree', 'add', worktreePath, '-b', branch]);
|
|
24
|
+
|
|
25
|
+
const lease: WorkspaceLease = {
|
|
26
|
+
workspace_id: wsId,
|
|
27
|
+
strategy: 'git_worktree',
|
|
28
|
+
branch,
|
|
29
|
+
base_commit: baseCommit,
|
|
30
|
+
root_path: worktreePath,
|
|
31
|
+
ttl_minutes: 45,
|
|
32
|
+
};
|
|
33
|
+
this.leases.set(wsId, lease);
|
|
34
|
+
return lease;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async snapshot(id: string): Promise<SnapshotRef> {
|
|
38
|
+
const lease = this.leases.get(id);
|
|
39
|
+
if (!lease || !lease.root_path) throw new Error(`Workspace ${id} not found`);
|
|
40
|
+
const commit = this.git(['rev-parse', 'HEAD'], lease.root_path).trim();
|
|
41
|
+
return {
|
|
42
|
+
snapshot_id: `snap-${crypto.randomBytes(4).toString('hex')}`,
|
|
43
|
+
workspace_id: id,
|
|
44
|
+
commit,
|
|
45
|
+
created_at: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async reset(id: string, snapRef: SnapshotRef): Promise<void> {
|
|
50
|
+
const lease = this.leases.get(id);
|
|
51
|
+
if (!lease) return;
|
|
52
|
+
this.git(['reset', '--hard', snapRef.commit], lease.root_path);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async dispose(id: string): Promise<void> {
|
|
56
|
+
const lease = this.leases.get(id);
|
|
57
|
+
if (!lease) return;
|
|
58
|
+
try {
|
|
59
|
+
this.git(['worktree', 'remove', lease.root_path, '--force']);
|
|
60
|
+
} catch {
|
|
61
|
+
// worktree may already be gone
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
if (lease.branch) this.git(['branch', '-D', lease.branch]);
|
|
65
|
+
} catch {
|
|
66
|
+
// branch may already be deleted
|
|
67
|
+
}
|
|
68
|
+
this.leases.delete(id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private git(args: string[], cwd?: string): string {
|
|
72
|
+
return execFileSync('git', args, {
|
|
73
|
+
cwd: cwd ?? this.projectRoot,
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import type { WorkspaceManager, WorkspaceRequest } from '../workspace-manager';
|
|
3
|
+
import type { WorkspaceLease, SnapshotRef } from '../../models/workspace';
|
|
4
|
+
|
|
5
|
+
export class InplaceWorkspaceManager implements WorkspaceManager {
|
|
6
|
+
constructor(private readonly projectRoot: string) {}
|
|
7
|
+
|
|
8
|
+
async allocate(req: WorkspaceRequest): Promise<WorkspaceLease> {
|
|
9
|
+
return {
|
|
10
|
+
workspace_id: `ws-inplace-${req.work_item_id}-a${req.attempt_number}`,
|
|
11
|
+
strategy: 'inplace',
|
|
12
|
+
branch: null,
|
|
13
|
+
base_commit: null,
|
|
14
|
+
root_path: this.projectRoot,
|
|
15
|
+
ttl_minutes: 60,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async snapshot(id: string): Promise<SnapshotRef> {
|
|
20
|
+
return {
|
|
21
|
+
snapshot_id: `snap-${crypto.randomBytes(4).toString('hex')}`,
|
|
22
|
+
workspace_id: id,
|
|
23
|
+
commit: 'HEAD',
|
|
24
|
+
created_at: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async reset(_id: string, _snapRef: SnapshotRef): Promise<void> {
|
|
29
|
+
// inplace: no filesystem isolation — reset is a no-op
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async dispose(_id: string): Promise<void> {
|
|
33
|
+
// inplace: nothing to tear down
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WorkspaceLease, SnapshotRef, WorkspaceStrategy } from '../models/workspace';
|
|
2
|
+
|
|
3
|
+
export interface WorkspaceRequest {
|
|
4
|
+
work_item_id: string;
|
|
5
|
+
attempt_number: number;
|
|
6
|
+
strategy: WorkspaceStrategy;
|
|
7
|
+
mutation_scope: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WorkspaceManager {
|
|
11
|
+
allocate(req: WorkspaceRequest): Promise<WorkspaceLease>;
|
|
12
|
+
snapshot(id: string): Promise<SnapshotRef>;
|
|
13
|
+
reset(id: string, snapRef: SnapshotRef): Promise<void>;
|
|
14
|
+
dispose(id: string): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": false,
|
|
10
|
+
"sourceMap": false,
|
|
11
|
+
"outDir": "../../lib/runtime",
|
|
12
|
+
"rootDir": "src",
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["tests/**", "node_modules/**", "dist-tests/**"]
|
|
17
|
+
}
|
|
Binary file
|