@twelvehart/orcats 0.0.0-seed.0 → 0.2.3
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/LICENSE +48 -0
- package/NOTICE +5 -0
- package/README.md +399 -0
- package/bin/orcats +4 -0
- package/dist/backends/acp-client.d.ts +74 -0
- package/dist/backends/acp-client.d.ts.map +1 -0
- package/dist/backends/acp-run.d.ts +31 -0
- package/dist/backends/acp-run.d.ts.map +1 -0
- package/dist/backends/claude-run.d.ts +35 -0
- package/dist/backends/claude-run.d.ts.map +1 -0
- package/dist/backends/claude-stream-json.d.ts +15 -0
- package/dist/backends/claude-stream-json.d.ts.map +1 -0
- package/dist/backends/codex-jsonl.d.ts +34 -0
- package/dist/backends/codex-jsonl.d.ts.map +1 -0
- package/dist/backends/codex-run.d.ts +40 -0
- package/dist/backends/codex-run.d.ts.map +1 -0
- package/dist/backends/codex.d.ts +5 -0
- package/dist/backends/codex.d.ts.map +1 -0
- package/dist/backends/conversation-config.d.ts +12 -0
- package/dist/backends/conversation-config.d.ts.map +1 -0
- package/dist/backends/index.d.ts +14 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/opencode-run.d.ts +48 -0
- package/dist/backends/opencode-run.d.ts.map +1 -0
- package/dist/backends/opencode-sse.d.ts +35 -0
- package/dist/backends/opencode-sse.d.ts.map +1 -0
- package/dist/backends/pi-rpc.d.ts +21 -0
- package/dist/backends/pi-rpc.d.ts.map +1 -0
- package/dist/backends/pi-run.d.ts +21 -0
- package/dist/backends/pi-run.d.ts.map +1 -0
- package/dist/backends/select.d.ts +17 -0
- package/dist/backends/select.d.ts.map +1 -0
- package/dist/backends/subprocess-run.d.ts +68 -0
- package/dist/backends/subprocess-run.d.ts.map +1 -0
- package/dist/backends/types.d.ts +17 -0
- package/dist/backends/types.d.ts.map +1 -0
- package/dist/backends/unsupported.d.ts +9 -0
- package/dist/backends/unsupported.d.ts.map +1 -0
- package/dist/baseline/index.d.ts +56 -0
- package/dist/baseline/index.d.ts.map +1 -0
- package/dist/cli/args.d.ts +22 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/embedded.d.ts +7 -0
- package/dist/cli/embedded.d.ts.map +1 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/conversation/ask-user.d.ts +17 -0
- package/dist/conversation/ask-user.d.ts.map +1 -0
- package/dist/conversation/conversation.d.ts +46 -0
- package/dist/conversation/conversation.d.ts.map +1 -0
- package/dist/conversation/harness.d.ts +14 -0
- package/dist/conversation/harness.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +5 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/queue.d.ts +15 -0
- package/dist/conversation/queue.d.ts.map +1 -0
- package/dist/flow/accessors.d.ts +13 -0
- package/dist/flow/accessors.d.ts.map +1 -0
- package/dist/flow/approvals.d.ts +6 -0
- package/dist/flow/approvals.d.ts.map +1 -0
- package/dist/flow/args.d.ts +4 -0
- package/dist/flow/args.d.ts.map +1 -0
- package/dist/flow/context.d.ts +36 -0
- package/dist/flow/context.d.ts.map +1 -0
- package/dist/flow/index.d.ts +5 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/loop/builder/index.d.ts +17 -0
- package/dist/loop/builder/index.d.ts.map +1 -0
- package/dist/loop/builder/types.d.ts +97 -0
- package/dist/loop/builder/types.d.ts.map +1 -0
- package/dist/loop/context/compaction.d.ts +38 -0
- package/dist/loop/context/compaction.d.ts.map +1 -0
- package/dist/loop/context/index.d.ts +4 -0
- package/dist/loop/context/index.d.ts.map +1 -0
- package/dist/loop/context/offload.d.ts +34 -0
- package/dist/loop/context/offload.d.ts.map +1 -0
- package/dist/loop/context/types.d.ts +13 -0
- package/dist/loop/context/types.d.ts.map +1 -0
- package/dist/loop/engine/bridge.d.ts +21 -0
- package/dist/loop/engine/bridge.d.ts.map +1 -0
- package/dist/loop/engine/index.d.ts +73 -0
- package/dist/loop/engine/index.d.ts.map +1 -0
- package/dist/loop/execution.d.ts +68 -0
- package/dist/loop/execution.d.ts.map +1 -0
- package/dist/loop/fanout.d.ts +148 -0
- package/dist/loop/fanout.d.ts.map +1 -0
- package/dist/loop/firing.d.ts +54 -0
- package/dist/loop/firing.d.ts.map +1 -0
- package/dist/loop/graph/analyze.d.ts +8 -0
- package/dist/loop/graph/analyze.d.ts.map +1 -0
- package/dist/loop/graph/index.d.ts +3 -0
- package/dist/loop/graph/index.d.ts.map +1 -0
- package/dist/loop/graph/types.d.ts +35 -0
- package/dist/loop/graph/types.d.ts.map +1 -0
- package/dist/loop/index.d.ts +7 -0
- package/dist/loop/index.d.ts.map +1 -0
- package/dist/loop/io/index.d.ts +4 -0
- package/dist/loop/io/index.d.ts.map +1 -0
- package/dist/loop/io/linear.d.ts +142 -0
- package/dist/loop/io/linear.d.ts.map +1 -0
- package/dist/loop/io/sink.d.ts +47 -0
- package/dist/loop/io/sink.d.ts.map +1 -0
- package/dist/loop/io/source.d.ts +64 -0
- package/dist/loop/io/source.d.ts.map +1 -0
- package/dist/loop/serve.d.ts +97 -0
- package/dist/loop/serve.d.ts.map +1 -0
- package/dist/loop/state/index.d.ts +5 -0
- package/dist/loop/state/index.d.ts.map +1 -0
- package/dist/loop/state/manifest.d.ts +36 -0
- package/dist/loop/state/manifest.d.ts.map +1 -0
- package/dist/loop/state/port.d.ts +33 -0
- package/dist/loop/state/port.d.ts.map +1 -0
- package/dist/loop/state/snapshot.d.ts +15 -0
- package/dist/loop/state/snapshot.d.ts.map +1 -0
- package/dist/loop/state/sqlite.d.ts +31 -0
- package/dist/loop/state/sqlite.d.ts.map +1 -0
- package/dist/loop/termination-contract.d.ts +28 -0
- package/dist/loop/termination-contract.d.ts.map +1 -0
- package/dist/loop/termination.d.ts +33 -0
- package/dist/loop/termination.d.ts.map +1 -0
- package/dist/model/backend-config.d.ts +30 -0
- package/dist/model/backend-config.d.ts.map +1 -0
- package/dist/model/brand.d.ts +9 -0
- package/dist/model/brand.d.ts.map +1 -0
- package/dist/model/errors.d.ts +11 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/index.d.ts +8 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/json-schema.d.ts +4 -0
- package/dist/model/json-schema.d.ts.map +1 -0
- package/dist/model/result.d.ts +3 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/schemas.d.ts +435 -0
- package/dist/model/schemas.d.ts.map +1 -0
- package/dist/model/structured.d.ts +9 -0
- package/dist/model/structured.d.ts.map +1 -0
- package/dist/monitor/index.d.ts +139 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/plan/index.d.ts +3 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/persistent.d.ts +23 -0
- package/dist/plan/persistent.d.ts.map +1 -0
- package/dist/plan/verdict.d.ts +9 -0
- package/dist/plan/verdict.d.ts.map +1 -0
- package/dist/review/index.d.ts +4 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/loop.d.ts +117 -0
- package/dist/review/loop.d.ts.map +1 -0
- package/dist/review/reviewers.d.ts +13 -0
- package/dist/review/reviewers.d.ts.map +1 -0
- package/dist/review/tool.d.ts +20 -0
- package/dist/review/tool.d.ts.map +1 -0
- package/dist/run-output/index.d.ts +112 -0
- package/dist/run-output/index.d.ts.map +1 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/typecheck.d.ts +23 -0
- package/dist/runner/typecheck.d.ts.map +1 -0
- package/dist/test-utils/fakes.d.ts +76 -0
- package/dist/test-utils/fakes.d.ts.map +1 -0
- package/dist/test-utils/fixtures.d.ts +4 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +4 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/tier2.d.ts +8 -0
- package/dist/test-utils/tier2.d.ts.map +1 -0
- package/dist/tools/fs.d.ts +11 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/git.d.ts +10 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/github.d.ts +13 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/linear.d.ts +117 -0
- package/dist/tools/linear.d.ts.map +1 -0
- package/dist/tools/process.d.ts +40 -0
- package/dist/tools/process.d.ts.map +1 -0
- package/dist/tools/terminal.d.ts +23 -0
- package/dist/tools/terminal.d.ts.map +1 -0
- package/package.json +89 -9
- package/src/backends/acp-client.ts +324 -0
- package/src/backends/acp-run.ts +636 -0
- package/src/backends/claude-run.ts +226 -0
- package/src/backends/claude-stream-json.ts +273 -0
- package/src/backends/codex-jsonl.ts +339 -0
- package/src/backends/codex-run.ts +177 -0
- package/src/backends/codex.ts +84 -0
- package/src/backends/conversation-config.ts +26 -0
- package/src/backends/index.ts +13 -0
- package/src/backends/opencode-run.ts +432 -0
- package/src/backends/opencode-sse.ts +360 -0
- package/src/backends/pi-rpc.ts +242 -0
- package/src/backends/pi-run.ts +154 -0
- package/src/backends/select.ts +77 -0
- package/src/backends/subprocess-run.ts +305 -0
- package/src/backends/types.ts +29 -0
- package/src/backends/unsupported.ts +48 -0
- package/src/baseline/index.ts +437 -0
- package/src/cli/args.ts +109 -0
- package/src/cli/embedded.ts +206 -0
- package/src/cli/main.ts +332 -0
- package/src/cli/version.ts +1 -0
- package/src/conversation/ask-user.ts +105 -0
- package/src/conversation/conversation.ts +109 -0
- package/src/conversation/harness.ts +39 -0
- package/src/conversation/index.ts +4 -0
- package/src/conversation/queue.ts +86 -0
- package/src/flow/accessors.ts +43 -0
- package/src/flow/approvals.ts +10 -0
- package/src/flow/args.ts +26 -0
- package/src/flow/context.ts +106 -0
- package/src/flow/index.ts +4 -0
- package/src/index.ts +17 -0
- package/src/loop/builder/index.ts +346 -0
- package/src/loop/builder/types.ts +131 -0
- package/src/loop/context/compaction.ts +175 -0
- package/src/loop/context/index.ts +3 -0
- package/src/loop/context/offload.ts +79 -0
- package/src/loop/context/types.ts +17 -0
- package/src/loop/engine/bridge.ts +61 -0
- package/src/loop/engine/index.ts +166 -0
- package/src/loop/execution.ts +453 -0
- package/src/loop/fanout.ts +444 -0
- package/src/loop/firing.ts +188 -0
- package/src/loop/graph/analyze.ts +128 -0
- package/src/loop/graph/index.ts +2 -0
- package/src/loop/graph/types.ts +45 -0
- package/src/loop/index.ts +11 -0
- package/src/loop/io/index.ts +3 -0
- package/src/loop/io/linear.ts +714 -0
- package/src/loop/io/sink.ts +170 -0
- package/src/loop/io/source.ts +255 -0
- package/src/loop/serve.ts +307 -0
- package/src/loop/state/index.ts +4 -0
- package/src/loop/state/manifest.ts +56 -0
- package/src/loop/state/port.ts +43 -0
- package/src/loop/state/snapshot.ts +129 -0
- package/src/loop/state/sqlite.ts +265 -0
- package/src/loop/termination-contract.ts +89 -0
- package/src/loop/termination.ts +156 -0
- package/src/model/backend-config.ts +34 -0
- package/src/model/brand.ts +11 -0
- package/src/model/errors.ts +28 -0
- package/src/model/index.ts +7 -0
- package/src/model/json-schema.ts +14 -0
- package/src/model/result.ts +28 -0
- package/src/model/schemas.ts +218 -0
- package/src/model/structured.ts +26 -0
- package/src/monitor/index.ts +367 -0
- package/src/plan/index.ts +2 -0
- package/src/plan/persistent.ts +98 -0
- package/src/plan/verdict.ts +3 -0
- package/src/review/index.ts +3 -0
- package/src/review/loop.ts +365 -0
- package/src/review/reviewers.ts +92 -0
- package/src/review/tool.ts +41 -0
- package/src/run-output/index.ts +319 -0
- package/src/runner/index.ts +1 -0
- package/src/runner/typecheck.ts +72 -0
- package/src/test-utils/fakes.ts +260 -0
- package/src/test-utils/fixtures.ts +33 -0
- package/src/test-utils/index.ts +3 -0
- package/src/test-utils/tier2.ts +12 -0
- package/src/tools/fs.ts +44 -0
- package/src/tools/git.ts +34 -0
- package/src/tools/github.ts +25 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/linear.ts +428 -0
- package/src/tools/process.ts +156 -0
- package/src/tools/terminal.ts +116 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { Usage } from "../model/index.ts";
|
|
3
|
+
|
|
4
|
+
export type RunEvent =
|
|
5
|
+
| RunStartedEvent
|
|
6
|
+
| RunHeartbeatEvent
|
|
7
|
+
| RunFinishedEvent
|
|
8
|
+
| PreflightEvent
|
|
9
|
+
| StageEvent
|
|
10
|
+
| AgentActivityEvent
|
|
11
|
+
| CycleProgressEvent
|
|
12
|
+
| OutcomeEvent
|
|
13
|
+
| FailureEvent
|
|
14
|
+
| ArtifactEvent;
|
|
15
|
+
|
|
16
|
+
export interface RunStartedEvent {
|
|
17
|
+
readonly type: "run_started";
|
|
18
|
+
readonly runId?: string;
|
|
19
|
+
readonly label?: string;
|
|
20
|
+
readonly backend?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RunHeartbeatEvent {
|
|
24
|
+
readonly type: "run_heartbeat";
|
|
25
|
+
readonly label?: string;
|
|
26
|
+
readonly elapsedMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RunFinishedEvent {
|
|
30
|
+
readonly type: "run_finished";
|
|
31
|
+
readonly label?: string;
|
|
32
|
+
readonly status: "success" | "failed";
|
|
33
|
+
readonly stopReason?: string;
|
|
34
|
+
readonly iterations?: number;
|
|
35
|
+
readonly error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PreflightEvent {
|
|
39
|
+
readonly type: "preflight";
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly status: "started" | "passed" | "failed" | "skipped";
|
|
42
|
+
readonly reason?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StageEvent {
|
|
46
|
+
readonly type: "stage";
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly status: "started" | "running" | "completed" | "failed";
|
|
49
|
+
readonly durationMs?: number;
|
|
50
|
+
readonly message?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AgentActivityEvent {
|
|
54
|
+
readonly type: "agent_activity";
|
|
55
|
+
readonly activity: "started" | "tool_use" | "tool_result" | "assistant_summary" | "usage";
|
|
56
|
+
readonly backend?: string;
|
|
57
|
+
readonly name?: string;
|
|
58
|
+
readonly summary?: string;
|
|
59
|
+
readonly usage?: Usage;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CycleBranchEvent {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly status: string;
|
|
65
|
+
readonly usage?: Usage | "unknown";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CycleContextPressureEvent {
|
|
69
|
+
readonly offloadCount: number;
|
|
70
|
+
readonly compactionStages: readonly string[];
|
|
71
|
+
readonly tokensBefore: number;
|
|
72
|
+
readonly tokensAfter: number;
|
|
73
|
+
readonly observationCount: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CycleProgressEvent {
|
|
77
|
+
readonly type: "cycle_progress";
|
|
78
|
+
readonly iteration: number;
|
|
79
|
+
readonly measure?: number;
|
|
80
|
+
readonly delta?: number;
|
|
81
|
+
readonly stopStatus?: string;
|
|
82
|
+
readonly usage?: Usage | "unknown";
|
|
83
|
+
readonly branches?: readonly CycleBranchEvent[];
|
|
84
|
+
readonly contextPressure?: CycleContextPressureEvent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface OutcomeEvent {
|
|
88
|
+
readonly type: "outcome";
|
|
89
|
+
readonly file: string;
|
|
90
|
+
readonly verdict: string;
|
|
91
|
+
readonly durationMs?: number;
|
|
92
|
+
readonly reason?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface FailureEvent {
|
|
96
|
+
readonly type: "failure";
|
|
97
|
+
readonly file: string;
|
|
98
|
+
readonly category?: string;
|
|
99
|
+
readonly durationMs?: number;
|
|
100
|
+
readonly message: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ArtifactEvent {
|
|
104
|
+
readonly type: "artifact";
|
|
105
|
+
readonly artifact: string;
|
|
106
|
+
readonly path: string;
|
|
107
|
+
readonly label?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface RunEventSink {
|
|
111
|
+
handle(event: RunEvent): void | Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RunReporter {
|
|
115
|
+
emit(event: RunEvent): void;
|
|
116
|
+
events(): readonly RunEvent[];
|
|
117
|
+
flush(): Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface RunReporterOptions {
|
|
121
|
+
readonly sinks?: readonly RunEventSink[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RunNarrator {
|
|
125
|
+
summarize(events: readonly RunEvent[]): string | Promise<string>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface RunPresenterOptions {
|
|
129
|
+
readonly env?: Record<string, string | undefined>;
|
|
130
|
+
readonly isTTY?: boolean;
|
|
131
|
+
readonly narration?: "disabled" | "enabled";
|
|
132
|
+
readonly narrator?: RunNarrator;
|
|
133
|
+
readonly writeDiagnostic?: (text: string) => void;
|
|
134
|
+
readonly writeOutput?: (text: string) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const reporterStorage = new AsyncLocalStorage<RunReporter>();
|
|
138
|
+
const reporterStack: RunReporter[] = [];
|
|
139
|
+
|
|
140
|
+
export function activeRunReporter(): RunReporter | undefined {
|
|
141
|
+
return reporterStorage.getStore() ?? reporterStack.at(-1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function withRunReporter<T>(
|
|
145
|
+
reporter: RunReporter | undefined,
|
|
146
|
+
fn: () => Promise<T> | T,
|
|
147
|
+
): Promise<T> {
|
|
148
|
+
if (reporter === undefined) {
|
|
149
|
+
return await fn();
|
|
150
|
+
}
|
|
151
|
+
reporterStack.push(reporter);
|
|
152
|
+
try {
|
|
153
|
+
return await reporterStorage.run(reporter, async () => await fn());
|
|
154
|
+
} finally {
|
|
155
|
+
reporterStack.pop();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function createRunReporter(options: RunReporterOptions = {}): RunReporter {
|
|
160
|
+
const events: RunEvent[] = [];
|
|
161
|
+
const pending: Promise<void>[] = [];
|
|
162
|
+
const sinks = options.sinks ?? [];
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
emit(event) {
|
|
166
|
+
events.push(event);
|
|
167
|
+
for (const sink of sinks) {
|
|
168
|
+
try {
|
|
169
|
+
const result = sink.handle(event);
|
|
170
|
+
if (result instanceof Promise) {
|
|
171
|
+
pending.push(result.then(() => undefined, () => undefined));
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
events() {
|
|
179
|
+
return [...events];
|
|
180
|
+
},
|
|
181
|
+
async flush() {
|
|
182
|
+
await Promise.allSettled(pending.splice(0));
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createRunPresenter(options: RunPresenterOptions = {}): RunEventSink {
|
|
188
|
+
const env = options.env ?? process.env;
|
|
189
|
+
const isTTY = options.isTTY ?? process.stderr.isTTY;
|
|
190
|
+
const writeDiagnostic =
|
|
191
|
+
options.writeDiagnostic ??
|
|
192
|
+
((text: string) => {
|
|
193
|
+
process.stderr.write(text);
|
|
194
|
+
});
|
|
195
|
+
const recent: RunEvent[] = [];
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
async handle(event) {
|
|
199
|
+
recent.push(event);
|
|
200
|
+
writeDiagnostic(`${formatRunEvent(event, { env, isTTY })}\n`);
|
|
201
|
+
|
|
202
|
+
if (options.narration !== "enabled" || options.narrator === undefined || env.CI) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const summary = await options.narrator.summarize([...recent]);
|
|
208
|
+
if (summary.trim().length > 0) {
|
|
209
|
+
writeDiagnostic(`${prefix({ env, isTTY })} narrator: ${summary.trim()}\n`);
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
writeDiagnostic(`${prefix({ env, isTTY })} narration unavailable: ${describeError(error)}\n`);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function formatRunEvent(
|
|
219
|
+
event: RunEvent,
|
|
220
|
+
options: Pick<RunPresenterOptions, "env" | "isTTY"> = {},
|
|
221
|
+
): string {
|
|
222
|
+
const head = prefix(options);
|
|
223
|
+
switch (event.type) {
|
|
224
|
+
case "run_started": {
|
|
225
|
+
const label = event.label ?? event.runId ?? "run";
|
|
226
|
+
const backend = event.backend === undefined ? "" : ` (backend=${event.backend})`;
|
|
227
|
+
return `${head} run started: ${label}${backend}`;
|
|
228
|
+
}
|
|
229
|
+
case "run_heartbeat":
|
|
230
|
+
return `${head} run active: ${event.label ?? "work"}${durationSuffix(event.elapsedMs)}`;
|
|
231
|
+
case "run_finished": {
|
|
232
|
+
const label = event.label ?? "run";
|
|
233
|
+
if (event.status === "failed") {
|
|
234
|
+
return `${head} failed: ${label}${event.error === undefined ? "" : `: ${event.error}`}`;
|
|
235
|
+
}
|
|
236
|
+
const reason = event.stopReason === undefined ? "completed" : `stopped (${event.stopReason})`;
|
|
237
|
+
const iterations = event.iterations === undefined ? "" : ` after ${String(event.iterations)} iteration(s)`;
|
|
238
|
+
return `${head} done: ${label} ${reason}${iterations}`;
|
|
239
|
+
}
|
|
240
|
+
case "preflight":
|
|
241
|
+
return `${head} preflight ${event.name} ${event.status}${event.reason === undefined ? "" : `: ${event.reason}`}`;
|
|
242
|
+
case "stage":
|
|
243
|
+
return `${head} stage ${event.name} ${event.status}${durationSuffix(event.durationMs)}${event.message === undefined ? "" : `: ${event.message}`}`;
|
|
244
|
+
case "agent_activity":
|
|
245
|
+
return formatAgentActivity(head, event);
|
|
246
|
+
case "cycle_progress":
|
|
247
|
+
return formatCycleProgress(head, event);
|
|
248
|
+
case "outcome":
|
|
249
|
+
return `${head} outcome ${event.file} ${event.verdict}${durationSuffix(event.durationMs)}${event.reason === undefined ? "" : `: ${event.reason}`}`;
|
|
250
|
+
case "failure":
|
|
251
|
+
return `${head} failure ${event.file}${event.category === undefined ? "" : ` ${event.category}`}${durationSuffix(event.durationMs)}: ${event.message}`;
|
|
252
|
+
case "artifact":
|
|
253
|
+
return `${head} artifact ${event.label ?? event.artifact}: ${event.path}`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatAgentActivity(head: string, event: AgentActivityEvent): string {
|
|
258
|
+
const actor = event.backend === undefined ? "agent" : `agent ${event.backend}`;
|
|
259
|
+
switch (event.activity) {
|
|
260
|
+
case "started":
|
|
261
|
+
return `${head} ${actor} started`;
|
|
262
|
+
case "tool_use":
|
|
263
|
+
return `${head} ${actor} tool ${event.name ?? "unknown"}`;
|
|
264
|
+
case "tool_result":
|
|
265
|
+
return `${head} ${actor} tool result ${event.name ?? "unknown"}${event.summary === undefined ? "" : `: ${event.summary}`}`;
|
|
266
|
+
case "assistant_summary":
|
|
267
|
+
return `${head} ${actor}: ${event.summary ?? ""}`.trimEnd();
|
|
268
|
+
case "usage":
|
|
269
|
+
return `${head} ${actor} usage${event.usage === undefined ? "" : ` ${formatUsage(event.usage)}`}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatCycleProgress(head: string, event: CycleProgressEvent): string {
|
|
274
|
+
const fields = [`cycle ${String(event.iteration)}`];
|
|
275
|
+
if (event.measure !== undefined) fields.push(`measure=${String(event.measure)}`);
|
|
276
|
+
if (event.delta !== undefined) fields.push(`delta=${String(event.delta)}`);
|
|
277
|
+
if (event.stopStatus !== undefined) fields.push(`stop=${event.stopStatus}`);
|
|
278
|
+
if (event.usage !== undefined && event.usage !== "unknown") fields.push(formatUsage(event.usage));
|
|
279
|
+
if (event.usage === "unknown") fields.push("usage=unknown");
|
|
280
|
+
if (event.contextPressure !== undefined) {
|
|
281
|
+
fields.push(`context=${String(event.contextPressure.tokensBefore)}->${String(event.contextPressure.tokensAfter)}`);
|
|
282
|
+
}
|
|
283
|
+
return `${head} ${fields.join(" ")}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function prefix(options: Pick<RunPresenterOptions, "env" | "isTTY"> = {}): string {
|
|
287
|
+
const env = options.env ?? process.env;
|
|
288
|
+
const isTTY = options.isTTY ?? process.stderr.isTTY;
|
|
289
|
+
if (isTTY && !env.NO_COLOR && !env.CI) {
|
|
290
|
+
return "\u001b[36morca\u001b[0m |";
|
|
291
|
+
}
|
|
292
|
+
return "orcats |";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function durationSuffix(ms: number | undefined): string {
|
|
296
|
+
return ms === undefined ? "" : ` (${formatDuration(ms)})`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatDuration(ms: number): string {
|
|
300
|
+
if (ms < 1_000) return `${String(ms)}ms`;
|
|
301
|
+
const seconds = Math.floor(ms / 1_000);
|
|
302
|
+
if (seconds < 60) return `${String(seconds)}s`;
|
|
303
|
+
return `${String(Math.floor(seconds / 60))}m ${String(seconds % 60)}s`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatUsage(usage: Usage): string {
|
|
307
|
+
const reasoning = usage.reasoning === undefined ? "" : ` reasoning=${String(usage.reasoning)}`;
|
|
308
|
+
return `usage=input=${String(usage.input)} output=${String(usage.output)}${reasoning}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function describeError(error: unknown): string {
|
|
312
|
+
if (error instanceof Error) return error.message;
|
|
313
|
+
if (typeof error === "string") return error;
|
|
314
|
+
try {
|
|
315
|
+
return JSON.stringify(error);
|
|
316
|
+
} catch {
|
|
317
|
+
return String(error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./typecheck.ts";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { err, ok, type Result } from "neverthrow";
|
|
4
|
+
import type { RuntimeError } from "../model/index.ts";
|
|
5
|
+
import { runQuiet, type QuietProcResult } from "../tools/process.ts";
|
|
6
|
+
|
|
7
|
+
export interface CommandRunner {
|
|
8
|
+
(command: string, args: readonly string[], options: { cwd: string }): Promise<Result<QuietProcResult, RuntimeError>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TypecheckOptions {
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
readonly project?: string;
|
|
14
|
+
readonly skip?: boolean;
|
|
15
|
+
readonly runner?: CommandRunner;
|
|
16
|
+
readonly which?: (cwd: string) => string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TypecheckResult {
|
|
20
|
+
readonly skipped: boolean;
|
|
21
|
+
readonly stdout: string;
|
|
22
|
+
readonly stderr: string;
|
|
23
|
+
readonly reason?: "flag" | "tsc-not-found";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runTypecheck(
|
|
27
|
+
options: TypecheckOptions
|
|
28
|
+
): Promise<Result<TypecheckResult, RuntimeError>> {
|
|
29
|
+
if (options.skip) {
|
|
30
|
+
return ok({ skipped: true, reason: "flag", stdout: "", stderr: "" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const command = options.which ? options.which(options.cwd) : defaultWhichTsc(options.cwd, options.project);
|
|
34
|
+
if (command === null) {
|
|
35
|
+
return ok({ skipped: true, reason: "tsc-not-found", stdout: "", stderr: "" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const runner = options.runner ?? runQuiet;
|
|
39
|
+
const args = ["--noEmit"];
|
|
40
|
+
if (options.project) {
|
|
41
|
+
args.push("-p", options.project);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await runner(command, args, { cwd: options.cwd });
|
|
45
|
+
if (result.isErr()) {
|
|
46
|
+
const error = result.error;
|
|
47
|
+
if (error._tag === "CommandFailed") {
|
|
48
|
+
return err({
|
|
49
|
+
_tag: "TypecheckFailed",
|
|
50
|
+
stdout: error.stdout,
|
|
51
|
+
stderr: error.stderr,
|
|
52
|
+
exitCode: error.exitCode
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return err(error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return ok({ skipped: false, stdout: result.value.stdout, stderr: result.value.stderr });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultWhichTsc(cwd: string, project?: string): string | null {
|
|
62
|
+
if (!existsSync(resolve(cwd, project ?? "tsconfig.json"))) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const localTsc = join(cwd, "node_modules", ".bin", "tsc");
|
|
67
|
+
if (existsSync(localTsc)) {
|
|
68
|
+
return localTsc;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Bun.which("tsc");
|
|
72
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { ConversationEvent, LlmResult } from "../model/index.ts";
|
|
2
|
+
import { err, ok, type Result } from "neverthrow";
|
|
3
|
+
import type { RuntimeError } from "../model/index.ts";
|
|
4
|
+
import type { LlmBackend, LlmTool, AutonomousRequest } from "../backends/types.ts";
|
|
5
|
+
import { StreamConversation, type Conversation } from "../conversation/conversation.ts";
|
|
6
|
+
import { sessionId } from "../model/brand.ts";
|
|
7
|
+
import type { BackendTag } from "../model/schemas.ts";
|
|
8
|
+
import type { Sink, Source, SourceSubscription } from "../loop/io/index.ts";
|
|
9
|
+
import type {
|
|
10
|
+
LinearAgentActivityInput,
|
|
11
|
+
LinearAgentSessionUpdateInput,
|
|
12
|
+
LinearIssueUpdateInput,
|
|
13
|
+
LinearTool,
|
|
14
|
+
} from "../tools/index.ts";
|
|
15
|
+
|
|
16
|
+
export interface FakeSubprocess {
|
|
17
|
+
readonly lines: readonly string[];
|
|
18
|
+
readonly stderr?: string;
|
|
19
|
+
readonly exitCode?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FakeHttpSseTransport {
|
|
23
|
+
readonly events: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FakeRpcTransport<Request = unknown, Response = unknown> {
|
|
27
|
+
readonly requests: Request[];
|
|
28
|
+
readonly responses: Response[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FakeAgentRun {
|
|
32
|
+
readonly events: readonly ConversationEvent[];
|
|
33
|
+
readonly result: LlmResult;
|
|
34
|
+
readonly diff?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScriptedFakeAgentStep {
|
|
38
|
+
readonly events?: readonly ConversationEvent[];
|
|
39
|
+
readonly output?: string;
|
|
40
|
+
readonly diff?: string;
|
|
41
|
+
readonly failure?: RuntimeError;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ScriptedFakeAgent {
|
|
45
|
+
run(prompt: string): Promise<Result<FakeAgentRun, RuntimeError>>;
|
|
46
|
+
readonly prompts: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function scriptedSubprocess(lines: readonly string[]): FakeSubprocess {
|
|
50
|
+
return { lines };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function scriptedSse(events: readonly string[]): FakeHttpSseTransport {
|
|
54
|
+
return { events };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function scriptedRpc<Request, Response>(
|
|
58
|
+
responses: readonly Response[]
|
|
59
|
+
): FakeRpcTransport<Request, Response> {
|
|
60
|
+
return { requests: [], responses: [...responses] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function fakeAgentRun(events: readonly ConversationEvent[], result: LlmResult): FakeAgentRun {
|
|
64
|
+
return { events, result };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface FakeLlmBackend extends LlmBackend<"codex"> {
|
|
68
|
+
readonly calls: readonly string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function fakeBackend(outputs: readonly string[]): FakeLlmBackend {
|
|
72
|
+
const queue = [...outputs];
|
|
73
|
+
const calls: string[] = [];
|
|
74
|
+
return {
|
|
75
|
+
tag: "codex" as const,
|
|
76
|
+
calls,
|
|
77
|
+
autonomous<Output>(request: AutonomousRequest<Output, "codex">): Conversation<"codex"> {
|
|
78
|
+
calls.push(request.prompt);
|
|
79
|
+
const output = queue.shift();
|
|
80
|
+
if (output === undefined) {
|
|
81
|
+
throw new Error(`fakeBackend: scripted outputs exhausted (${String(calls.length)} calls made)`);
|
|
82
|
+
}
|
|
83
|
+
const callIndex = calls.length;
|
|
84
|
+
const conv = new StreamConversation<"codex">({ backend: "codex" });
|
|
85
|
+
void Promise.resolve().then(async () => {
|
|
86
|
+
await conv.emit({ type: "assistant_text_delta", text: output });
|
|
87
|
+
await conv.emit({ type: "assistant_turn_end" });
|
|
88
|
+
conv.succeed({
|
|
89
|
+
backend: "codex",
|
|
90
|
+
sessionId: sessionId("codex", `fake-session-${String(callIndex)}`),
|
|
91
|
+
output,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
return conv;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface EventRecorder<E = ConversationEvent> {
|
|
100
|
+
readonly push: (event: E) => void;
|
|
101
|
+
readonly events: () => readonly E[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function eventRecorder<E = ConversationEvent>(): EventRecorder<E> {
|
|
105
|
+
const captured: E[] = [];
|
|
106
|
+
return {
|
|
107
|
+
push: (event) => { captured.push(event); },
|
|
108
|
+
events: () => captured,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createFakeLlmTool(fake: FakeLlmBackend): LlmTool {
|
|
113
|
+
return {
|
|
114
|
+
autonomous<B extends BackendTag, Output>(_backend: LlmBackend<B>, req: AutonomousRequest<Output, B>) {
|
|
115
|
+
return fake.autonomous(req as unknown as AutonomousRequest<Output, "codex">) as unknown as Conversation<B>;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function scriptedFakeAgent(steps: readonly ScriptedFakeAgentStep[]): ScriptedFakeAgent {
|
|
121
|
+
const prompts: string[] = [];
|
|
122
|
+
const queue = [...steps];
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
prompts,
|
|
126
|
+
run(prompt) {
|
|
127
|
+
prompts.push(prompt);
|
|
128
|
+
const step = queue.shift();
|
|
129
|
+
if (!step) {
|
|
130
|
+
return Promise.resolve(err({
|
|
131
|
+
_tag: "BackendFailed",
|
|
132
|
+
backend: "codex",
|
|
133
|
+
message: "scripted fake agent exhausted"
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
if (step.failure) {
|
|
137
|
+
return Promise.resolve(err(step.failure));
|
|
138
|
+
}
|
|
139
|
+
const run: FakeAgentRun = {
|
|
140
|
+
events: step.events ?? [],
|
|
141
|
+
result: {
|
|
142
|
+
backend: "codex",
|
|
143
|
+
sessionId: "fake-agent-session",
|
|
144
|
+
output: step.output ?? ""
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
return Promise.resolve(ok({
|
|
148
|
+
...run,
|
|
149
|
+
...(step.diff === undefined ? {} : { diff: step.diff })
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Fake Source / Sink (loop-io spec): in-memory, so a loop runs end to end with no real ---
|
|
156
|
+
// --- trigger or output IO. The fake sink captures emitted outputs; the fake source fires on demand. ---
|
|
157
|
+
|
|
158
|
+
export interface FakeSource<E> extends Source<E> {
|
|
159
|
+
readonly kind: "manual";
|
|
160
|
+
/** Push a trigger event to the started handler. No-op until started / after stop. */
|
|
161
|
+
fire(event: E): void;
|
|
162
|
+
/** Whether a handler is currently subscribed. */
|
|
163
|
+
isStarted(): boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function fakeSource<E = void>(): FakeSource<E> {
|
|
167
|
+
let handler: ((event: E) => void) | undefined;
|
|
168
|
+
return {
|
|
169
|
+
kind: "manual",
|
|
170
|
+
isStarted: () => handler !== undefined,
|
|
171
|
+
fire(event) {
|
|
172
|
+
handler?.(event);
|
|
173
|
+
},
|
|
174
|
+
start(received) {
|
|
175
|
+
handler = received;
|
|
176
|
+
const sub: SourceSubscription = {
|
|
177
|
+
stop() {
|
|
178
|
+
handler = undefined;
|
|
179
|
+
return Promise.resolve(ok(undefined));
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return Promise.resolve(ok(sub));
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface FakeSink<A> extends Sink<A> {
|
|
188
|
+
readonly kind: "stdout";
|
|
189
|
+
/** Outputs captured by each successful emit, in order. */
|
|
190
|
+
emitted(): readonly A[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** A capturing Sink. With `failWith`, every emit returns `err(RuntimeError)` and captures nothing. */
|
|
194
|
+
export function fakeSink<A = unknown>(options: { failWith?: RuntimeError } = {}): FakeSink<A> {
|
|
195
|
+
const captured: A[] = [];
|
|
196
|
+
const { failWith } = options;
|
|
197
|
+
return {
|
|
198
|
+
kind: "stdout",
|
|
199
|
+
emitted: () => captured,
|
|
200
|
+
emit(output) {
|
|
201
|
+
if (failWith !== undefined) {
|
|
202
|
+
return Promise.resolve(err(failWith));
|
|
203
|
+
}
|
|
204
|
+
captured.push(output);
|
|
205
|
+
return Promise.resolve(ok(undefined));
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface FakeLinearTool extends LinearTool {
|
|
211
|
+
readonly issueComments: readonly { readonly issueId: string; readonly body: string }[];
|
|
212
|
+
readonly issueUpdates: readonly LinearIssueUpdateInput[];
|
|
213
|
+
readonly agentActivities: readonly LinearAgentActivityInput[];
|
|
214
|
+
readonly agentSessionUpdates: readonly LinearAgentSessionUpdateInput[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createFakeLinearTool(): FakeLinearTool {
|
|
218
|
+
const issueComments: { readonly issueId: string; readonly body: string }[] = [];
|
|
219
|
+
const issueUpdates: LinearIssueUpdateInput[] = [];
|
|
220
|
+
const agentActivities: LinearAgentActivityInput[] = [];
|
|
221
|
+
const agentSessionUpdates: LinearAgentSessionUpdateInput[] = [];
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
issueComments,
|
|
225
|
+
issueUpdates,
|
|
226
|
+
agentActivities,
|
|
227
|
+
agentSessionUpdates,
|
|
228
|
+
fetchIssue(input) {
|
|
229
|
+
return Promise.resolve(ok({ id: input.issueId }));
|
|
230
|
+
},
|
|
231
|
+
updateIssue(input) {
|
|
232
|
+
issueUpdates.push(input);
|
|
233
|
+
return Promise.resolve(ok({ id: input.issueId, title: input.title ?? null }));
|
|
234
|
+
},
|
|
235
|
+
createIssueComment(input) {
|
|
236
|
+
issueComments.push(input);
|
|
237
|
+
return Promise.resolve(ok({ id: `comment-${String(issueComments.length)}`, body: input.body }));
|
|
238
|
+
},
|
|
239
|
+
createAgentActivity(input) {
|
|
240
|
+
agentActivities.push(input);
|
|
241
|
+
return Promise.resolve(ok({
|
|
242
|
+
id: `activity-${String(agentActivities.length)}`,
|
|
243
|
+
body: input.body,
|
|
244
|
+
...(input.type === undefined ? {} : { type: input.type }),
|
|
245
|
+
}));
|
|
246
|
+
},
|
|
247
|
+
updateAgentSession(input) {
|
|
248
|
+
agentSessionUpdates.push(input);
|
|
249
|
+
return Promise.resolve(ok({
|
|
250
|
+
id: input.agentSessionId,
|
|
251
|
+
...(input.plan === undefined ? {} : { plan: input.plan }),
|
|
252
|
+
...(input.externalUrl === undefined ? {} : { externalUrl: input.externalUrl }),
|
|
253
|
+
...(input.externalUrls === undefined ? {} : { externalUrls: input.externalUrls }),
|
|
254
|
+
}));
|
|
255
|
+
},
|
|
256
|
+
getTeamWorkflowStates() {
|
|
257
|
+
return Promise.resolve(ok([]));
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { expect } from "bun:test";
|
|
4
|
+
|
|
5
|
+
export async function readJsonFixture<T = unknown>(path: string): Promise<T> {
|
|
6
|
+
return JSON.parse(await readFile(path, "utf8")) as T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function assertExactJson(actual: unknown, expected: unknown): void {
|
|
10
|
+
expect(isDeepStrictEqual(actual, expected)).toBe(true);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stableJson(value: unknown): string {
|
|
14
|
+
return `${JSON.stringify(value, Object.keys(flattenKeys(value)).sort(), 2)}\n`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function flattenKeys(value: unknown, keys: Record<string, true> = {}): Record<string, true> {
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
for (const item of value) {
|
|
20
|
+
flattenKeys(item, keys);
|
|
21
|
+
}
|
|
22
|
+
return keys;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (value !== null && typeof value === "object") {
|
|
26
|
+
for (const [key, child] of Object.entries(value)) {
|
|
27
|
+
keys[key] = true;
|
|
28
|
+
flattenKeys(child, keys);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return keys;
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
export interface Tier2Golden {
|
|
4
|
+
readonly commits: readonly string[];
|
|
5
|
+
readonly planFiles: Readonly<Record<string, string>>;
|
|
6
|
+
readonly terminal: readonly string[];
|
|
7
|
+
readonly events: readonly unknown[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function assertTier2Golden(actual: Tier2Golden, expected: Tier2Golden): void {
|
|
11
|
+
expect(actual).toEqual(expected);
|
|
12
|
+
}
|