@amenophis1er/foreman 0.1.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/DESIGN.md +408 -0
- package/LICENSE +15 -0
- package/README.md +133 -0
- package/bin/foreman.mjs +58 -0
- package/package.json +68 -0
- package/scripts/prepare.mjs +48 -0
- package/skills/director/SKILL.md +65 -0
- package/src/anthropic-models.ts +54 -0
- package/src/ask.test.ts +88 -0
- package/src/ask.ts +95 -0
- package/src/attachments.test.ts +33 -0
- package/src/attachments.ts +60 -0
- package/src/cli.test.ts +27 -0
- package/src/cli.ts +297 -0
- package/src/codex.test.ts +328 -0
- package/src/codex.ts +196 -0
- package/src/cost-basis.test.ts +76 -0
- package/src/deck.test.ts +402 -0
- package/src/deck.ts +892 -0
- package/src/fork.test.ts +31 -0
- package/src/gateway/ledger.cjs +326 -0
- package/src/gateway/ledger.test.ts +255 -0
- package/src/gateway/llm-gateway.cjs +1411 -0
- package/src/gateway/llm-gateway.test.ts +478 -0
- package/src/gateway.test.ts +226 -0
- package/src/gateway.ts +309 -0
- package/src/instance.ts +124 -0
- package/src/models.test.ts +147 -0
- package/src/models.ts +158 -0
- package/src/notify/commands.test.ts +28 -0
- package/src/notify/commands.ts +73 -0
- package/src/notify/telegram.ts +259 -0
- package/src/notify.test.ts +343 -0
- package/src/notify.ts +495 -0
- package/src/ollama.test.ts +49 -0
- package/src/ollama.ts +49 -0
- package/src/openai-prices.test.ts +58 -0
- package/src/openai-prices.ts +106 -0
- package/src/orchestrator.test.ts +1147 -0
- package/src/orchestrator.ts +2325 -0
- package/src/planner.test.ts +60 -0
- package/src/planner.ts +505 -0
- package/src/policy.test.ts +411 -0
- package/src/policy.ts +599 -0
- package/src/preflight.ts +348 -0
- package/src/prices.test.ts +69 -0
- package/src/prices.ts +90 -0
- package/src/provider.test.ts +366 -0
- package/src/provider.ts +502 -0
- package/src/secrets.test.ts +143 -0
- package/src/secrets.ts +66 -0
- package/src/server.ts +1992 -0
- package/src/services.test.ts +53 -0
- package/src/services.ts +102 -0
- package/src/sse-events.test.ts +83 -0
- package/src/store.test.ts +119 -0
- package/src/store.ts +346 -0
- package/src/tailscale.test.ts +32 -0
- package/src/tailscale.ts +79 -0
- package/src/title.ts +138 -0
- package/src/types.ts +442 -0
- package/ui/dist/assets/index-LAj0Dy9p.css +1 -0
- package/ui/dist/assets/index-lcBy-uRZ.js +65 -0
- package/ui/dist/favicon.svg +8 -0
- package/ui/dist/index.html +14 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for Foreman's orchestrator, persistence, and HTTP layer.
|
|
3
|
+
*
|
|
4
|
+
* Everything the UI sees flows through {@link ForemanEvent}: each event is
|
|
5
|
+
* broadcast to connected SSE clients *and* appended to the run's event log,
|
|
6
|
+
* so replaying a log reproduces exactly what a live client observed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type RunStatus = 'running' | 'done' | 'error' | 'interrupted';
|
|
10
|
+
|
|
11
|
+
/** A linked project — a folder Foreman runs missions in. */
|
|
12
|
+
export interface Project {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Display name; defaults to the folder's basename. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Absolute path of the working directory. */
|
|
17
|
+
folder: string;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
/** Default budget suggested in the composer. */
|
|
20
|
+
defaultBudgetUsd: number;
|
|
21
|
+
/**
|
|
22
|
+
* Who serves and who pays. Absent means the legacy `claudeInstance` below
|
|
23
|
+
* (or the server default) — see providerOf() in provider.ts.
|
|
24
|
+
*/
|
|
25
|
+
provider?: ProviderRef;
|
|
26
|
+
/** @deprecated Pre-provider pin. Read through providerOf(); never written. */
|
|
27
|
+
claudeInstance?: ClaudeInstanceRef;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @deprecated The pre-provider shape, still read from records written before
|
|
32
|
+
* {@link ProviderRef} existed. Never written. See providerFromLegacy().
|
|
33
|
+
*/
|
|
34
|
+
export interface ClaudeInstanceRef {
|
|
35
|
+
/** CLAUDE_CONFIG_DIR for the agent. */
|
|
36
|
+
configDir?: string;
|
|
37
|
+
/** Claude Code executable; the SDK's bundled one when absent. */
|
|
38
|
+
executable?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Who pays for this project's missions.
|
|
41
|
+
* - `inherit` (default): whatever the server process authenticates as. An
|
|
42
|
+
* ANTHROPIC_API_KEY in the server environment outranks any stored login,
|
|
43
|
+
* so every project bills that key.
|
|
44
|
+
* - `own-login`: strip inherited API-key credentials from the agent's
|
|
45
|
+
* environment so the pinned config dir's own stored login pays. This is
|
|
46
|
+
* what makes per-project accounts work on a single shared server.
|
|
47
|
+
*/
|
|
48
|
+
billing?: 'inherit' | 'own-login';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Who serves the model, who pays for it, and where requests go — one choice,
|
|
53
|
+
* not three settings. See src/provider.ts and docs/provider-model.md; the
|
|
54
|
+
* union shape is what makes "subscription login + custom base URL", the
|
|
55
|
+
* combination that leaks a token, unrepresentable.
|
|
56
|
+
*/
|
|
57
|
+
export type ProviderRef =
|
|
58
|
+
/** An installed Claude Code, using whatever it is logged into. Ambient. */
|
|
59
|
+
| {
|
|
60
|
+
kind: 'claude-code';
|
|
61
|
+
/** CLAUDE_CONFIG_DIR; the server default when absent. */
|
|
62
|
+
configDir?: string;
|
|
63
|
+
/** Claude Code executable; the SDK's bundled one when absent. */
|
|
64
|
+
executable?: string;
|
|
65
|
+
/** Strip inherited key credentials so this install's own login pays. */
|
|
66
|
+
ownLogin?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** An Anthropic API key Foreman is told about by name, never by value. */
|
|
69
|
+
| {
|
|
70
|
+
kind: 'anthropic-api';
|
|
71
|
+
/** Stable id; names this provider's Foreman-owned config dir. */
|
|
72
|
+
id: string;
|
|
73
|
+
/** Server environment variable holding the key. Foreman stores no secrets. */
|
|
74
|
+
apiKeyEnv: string;
|
|
75
|
+
model?: string;
|
|
76
|
+
}
|
|
77
|
+
/** An installed Codex CLI, using the login `codex login` already stored. */
|
|
78
|
+
| {
|
|
79
|
+
kind: 'codex';
|
|
80
|
+
id: string;
|
|
81
|
+
/** CODEX_HOME; `~/.codex` when absent. */
|
|
82
|
+
codexHome?: string;
|
|
83
|
+
/** Codex inference backend; the public one when absent. */
|
|
84
|
+
upstreamUrl?: string;
|
|
85
|
+
model?: string;
|
|
86
|
+
}
|
|
87
|
+
/** Anything speaking OpenAI Chat Completions: Ollama, OpenRouter, vLLM, … */
|
|
88
|
+
| {
|
|
89
|
+
kind: 'openai-compatible';
|
|
90
|
+
id: string;
|
|
91
|
+
baseUrl: string;
|
|
92
|
+
/**
|
|
93
|
+
* Name of a server environment variable holding the key. The fallback
|
|
94
|
+
* when nothing is stored for this provider; omit it entirely to use a
|
|
95
|
+
* stored key alone.
|
|
96
|
+
*/
|
|
97
|
+
apiKeyEnv?: string;
|
|
98
|
+
/**
|
|
99
|
+
* This endpoint requires a credential. Distinguishes "a key is stored
|
|
100
|
+
* for it" from "it needs none at all" — a local Ollama is the latter,
|
|
101
|
+
* and inferring from the absence of `apiKeyEnv` would make every stored
|
|
102
|
+
* key look like no key.
|
|
103
|
+
*/
|
|
104
|
+
needsKey?: boolean;
|
|
105
|
+
/** Short name for badges — "Ollama", "OpenRouter". */
|
|
106
|
+
label?: string;
|
|
107
|
+
model?: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type WorkerStatus = 'running' | 'done' | 'error';
|
|
111
|
+
|
|
112
|
+
/** Persisted, replayable event envelope. */
|
|
113
|
+
export interface ForemanEvent {
|
|
114
|
+
/** Milliseconds since epoch, assigned at emit time. */
|
|
115
|
+
ts: number;
|
|
116
|
+
/** SSE event name; the UI switches on this. */
|
|
117
|
+
event: string;
|
|
118
|
+
/** JSON-serializable payload; shape depends on `event`. */
|
|
119
|
+
data: unknown;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* What a run's spend *is* — which is a different question from how much.
|
|
124
|
+
*
|
|
125
|
+
* A boolean `metered` used to carry this, and it collapsed two states that
|
|
126
|
+
* are not the same thing at all: a local model that costs nothing and an
|
|
127
|
+
* OpenAI key that costs real money Foreman holds no price table for both
|
|
128
|
+
* answered `false`, and rendered identically. "You are spending nothing" and
|
|
129
|
+
* "you are spending an amount I cannot tell you" are different sentences to
|
|
130
|
+
* put in front of someone, and only one of them is a reason to go and look at
|
|
131
|
+
* a vendor's dashboard.
|
|
132
|
+
*
|
|
133
|
+
* - `priced` — the dollar figure is the real one. Dollars are displayed,
|
|
134
|
+
* and the budget cap binds.
|
|
135
|
+
* - `free` — nothing is charged per token; the model runs on hardware
|
|
136
|
+
* the operator already owns. Tokens and turns are the only
|
|
137
|
+
* true units.
|
|
138
|
+
* - `unpriced` — real spend, of an amount Foreman cannot state: a paid
|
|
139
|
+
* endpoint with no price source, or a subscription allowance
|
|
140
|
+
* being consumed. Shown in tokens and turns like `free`, but
|
|
141
|
+
* said out loud to be untracked rather than passed off as
|
|
142
|
+
* costing nothing.
|
|
143
|
+
*
|
|
144
|
+
* Only `priced` may display or enforce dollars. `free` and `unpriced` differ
|
|
145
|
+
* in what they say and never in what they enforce — which is why this is
|
|
146
|
+
* three states and not four: the fourth distinction people reach for
|
|
147
|
+
* (subscription vs. pay-as-you-go) changes no behaviour here, and the
|
|
148
|
+
* provider label already carries it in words.
|
|
149
|
+
*/
|
|
150
|
+
export type CostBasis = 'priced' | 'free' | 'unpriced';
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A run's cost basis, including for runs that predate the field.
|
|
154
|
+
*
|
|
155
|
+
* The old `metered: false` meant "not priceable", which is the union of
|
|
156
|
+
* `free` and `unpriced` — so it cannot be split after the fact. This reads it
|
|
157
|
+
* as `unpriced`, because of the two possible errors, describing real spend as
|
|
158
|
+
* free is the one that costs somebody money.
|
|
159
|
+
*/
|
|
160
|
+
export function costBasisOf(meta: { costBasis?: CostBasis; metered?: boolean }): CostBasis {
|
|
161
|
+
if (meta.costBasis) return meta.costBasis;
|
|
162
|
+
return meta.metered === false ? 'unpriced' : 'priced';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* One cost basis for a run whose two roles may not share one.
|
|
167
|
+
*
|
|
168
|
+
* `priced` wins outright: if any part of the run bills real dollars, the
|
|
169
|
+
* dollar cap must still arm, because the alternative is an uncapped run
|
|
170
|
+
* spending genuine money. It is not a *precise* figure on a mixed run — the
|
|
171
|
+
* SDK prices the gateway role's tokens with Anthropic's table too, so the
|
|
172
|
+
* total overstates — but overstating a real bill is a safe error in a way
|
|
173
|
+
* that ignoring one is not. A per-role price table is what fixes it properly.
|
|
174
|
+
*
|
|
175
|
+
* Otherwise `unpriced` beats `free`, on the same principle that decides a
|
|
176
|
+
* single provider: never call a run free when part of it is not.
|
|
177
|
+
*/
|
|
178
|
+
export function combineBasis(a: CostBasis, b: CostBasis): CostBasis {
|
|
179
|
+
if (a === 'priced' || b === 'priced') return 'priced';
|
|
180
|
+
if (a === 'unpriced' || b === 'unpriced') return 'unpriced';
|
|
181
|
+
return 'free';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Whether dollars may be displayed or enforced at all. The one real branch. */
|
|
185
|
+
export function isPriced(meta: { costBasis?: CostBasis; metered?: boolean }): boolean {
|
|
186
|
+
return costBasisOf(meta) === 'priced';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Real token usage. `costUsd` is notional on a subscription plan and
|
|
191
|
+
* fictional through a gateway (the SDK prices foreign tokens with
|
|
192
|
+
* Anthropic's rate table) — token counts are what actually moved on any
|
|
193
|
+
* provider, so this is what an unmetered run's meter should show instead of
|
|
194
|
+
* a dollar figure nobody billed.
|
|
195
|
+
*/
|
|
196
|
+
export interface TokenUsage {
|
|
197
|
+
inputTokens: number;
|
|
198
|
+
outputTokens: number;
|
|
199
|
+
cacheReadTokens: number;
|
|
200
|
+
cacheWriteTokens: number;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Snapshot of one worker, persisted in run metadata. */
|
|
204
|
+
export interface WorkerMeta {
|
|
205
|
+
id: string;
|
|
206
|
+
status: WorkerStatus;
|
|
207
|
+
costUsd: number;
|
|
208
|
+
sessionId?: string;
|
|
209
|
+
/** First 500 chars of the task brief, for run-history display. */
|
|
210
|
+
task: string;
|
|
211
|
+
/**
|
|
212
|
+
* The worker's final report, kept on the record rather than handed back
|
|
213
|
+
* once and forgotten: spawning is asynchronous, so the director reads
|
|
214
|
+
* results through check_workers / wait_for_worker — possibly more than
|
|
215
|
+
* once, possibly after a restart — and a report that lived only in a
|
|
216
|
+
* returned promise would be gone by then.
|
|
217
|
+
*/
|
|
218
|
+
report?: string;
|
|
219
|
+
isError?: boolean;
|
|
220
|
+
startedAt?: number;
|
|
221
|
+
endedAt?: number;
|
|
222
|
+
/** Last SDK message seen; what "seconds since last activity" is measured from. */
|
|
223
|
+
lastActivityAt?: number;
|
|
224
|
+
toolCalls?: number;
|
|
225
|
+
/**
|
|
226
|
+
* Rolling window of the last few activity lines (tool name + a short arg
|
|
227
|
+
* hint, or the head of an assistant message). The director cannot see a
|
|
228
|
+
* worker's transcript; this is the glance that tells "reading tests" apart
|
|
229
|
+
* from "re-running the same failing command".
|
|
230
|
+
*/
|
|
231
|
+
recent?: string[];
|
|
232
|
+
/**
|
|
233
|
+
* The worker's own account of where it is, from its report_progress tool.
|
|
234
|
+
* Kept apart from `recent` because the two answer different questions:
|
|
235
|
+
* `recent` is what the harness saw the worker call, this is what the
|
|
236
|
+
* worker says it is doing and how far along it is — the only signal that
|
|
237
|
+
* tells "two of three files written, stuck on the third" from flailing.
|
|
238
|
+
* Latest report wins; `at` dates it so a stale one reads as stale.
|
|
239
|
+
*/
|
|
240
|
+
progress?: WorkerProgress;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** One report_progress call, as stored on the record and sent to the UI. */
|
|
244
|
+
export interface WorkerProgress {
|
|
245
|
+
/** One-line summary, capped at 200 chars by the tool. */
|
|
246
|
+
status: string;
|
|
247
|
+
done?: string[];
|
|
248
|
+
next?: string;
|
|
249
|
+
blocked?: string;
|
|
250
|
+
at: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Model for an agent: a harness alias (opus/sonnet/haiku), a full claude-*
|
|
255
|
+
* model id, or `undefined` to inherit the harness default.
|
|
256
|
+
*/
|
|
257
|
+
export type ModelChoice = string | undefined;
|
|
258
|
+
|
|
259
|
+
/** Per-tool decision applied before any prompt is considered. */
|
|
260
|
+
export type ToolPolicy = Record<string, 'allow' | 'ask' | 'deny'>;
|
|
261
|
+
|
|
262
|
+
/** Persisted run metadata (meta.json). Small, rewritten atomically on change. */
|
|
263
|
+
export interface RunMeta {
|
|
264
|
+
/** Effective tool policy, snapshotted from Settings at run start. */
|
|
265
|
+
toolPolicy?: ToolPolicy;
|
|
266
|
+
/** Whether read-only tools run silently (Settings; default true). */
|
|
267
|
+
autoAllowReadOnly?: boolean;
|
|
268
|
+
id: string;
|
|
269
|
+
/**
|
|
270
|
+
* Short generated name for the mission, from one cheap model call at start.
|
|
271
|
+
* Absent when the call failed or the run predates titling — the UI falls
|
|
272
|
+
* back to the brief, so nothing depends on this being here.
|
|
273
|
+
*/
|
|
274
|
+
title?: string;
|
|
275
|
+
/** Owning project; absent on runs recorded before projects existed. */
|
|
276
|
+
projectId?: string;
|
|
277
|
+
/** Model override for the director session. */
|
|
278
|
+
directorModel?: ModelChoice;
|
|
279
|
+
/** Model override for worker sessions (cost lever). */
|
|
280
|
+
workerModel?: ModelChoice;
|
|
281
|
+
/**
|
|
282
|
+
* Provider serving each role, when it differs from the run's own.
|
|
283
|
+
*
|
|
284
|
+
* A model carries the provider that serves it, so a run can put a capable
|
|
285
|
+
* director on one and cheap workers on another — the configuration the
|
|
286
|
+
* whole provider model exists to make possible. Absent means "the run's
|
|
287
|
+
* provider", which is every run recorded before this existed.
|
|
288
|
+
*/
|
|
289
|
+
directorProviderId?: string;
|
|
290
|
+
workerProviderId?: string;
|
|
291
|
+
/**
|
|
292
|
+
* Provider frozen at dispatch, so history and resume agree — and so a later
|
|
293
|
+
* settings change cannot move an in-flight or resumed run to another
|
|
294
|
+
* provider, or another bill.
|
|
295
|
+
*/
|
|
296
|
+
provider?: ProviderRef;
|
|
297
|
+
/** @deprecated Pre-provider pin, still read for runs recorded before providers. */
|
|
298
|
+
claudeInstance?: ClaudeInstanceRef;
|
|
299
|
+
/** Number of times this run was resumed after an interruption. */
|
|
300
|
+
resumes?: number;
|
|
301
|
+
/** Tools the human granted "always allow" for this run (survives resume). */
|
|
302
|
+
allowedTools?: string[];
|
|
303
|
+
/**
|
|
304
|
+
* Absolute directories the human opened for this run by approving "always"
|
|
305
|
+
* on a folder-boundary card (see `PendingPermission.escapedPath`). Paths
|
|
306
|
+
* under them count as inside the mission folder. Persisted beside
|
|
307
|
+
* `allowedTools` for the same reason: a resume must not re-ask what the
|
|
308
|
+
* human already answered.
|
|
309
|
+
*/
|
|
310
|
+
allowedRoots?: string[];
|
|
311
|
+
/** Dev servers the crew exposed through Foreman's proxy (see services.ts). */
|
|
312
|
+
services?: Array<{ port: number; label: string; path: string; since: number }>;
|
|
313
|
+
/**
|
|
314
|
+
* How long an approval card or director question may wait for the human
|
|
315
|
+
* before it is resolved with its unattended default (deny / "decide
|
|
316
|
+
* yourself"). Absent means the orchestrator's default; 0 disables the timer
|
|
317
|
+
* for a run someone intends to babysit.
|
|
318
|
+
*/
|
|
319
|
+
askTimeoutMs?: number;
|
|
320
|
+
/** Give agents a headless Playwright browser (navigate, click, screenshot). */
|
|
321
|
+
browserTools?: boolean;
|
|
322
|
+
folder: string;
|
|
323
|
+
mission: string;
|
|
324
|
+
budgetUsd: number;
|
|
325
|
+
/**
|
|
326
|
+
* What this run's spend *is* — see {@link CostBasis}. Absent on runs
|
|
327
|
+
* recorded before the split; read it through {@link costBasisOf}, never
|
|
328
|
+
* directly, so those runs keep answering.
|
|
329
|
+
*/
|
|
330
|
+
costBasis?: CostBasis;
|
|
331
|
+
/**
|
|
332
|
+
* @deprecated Superseded by {@link costBasis}, which distinguishes the two
|
|
333
|
+
* states this boolean collapsed. Still read for runs recorded before the
|
|
334
|
+
* split, and still written beside `costBasis` so a downgrade is survivable.
|
|
335
|
+
* Nothing new should branch on it.
|
|
336
|
+
*/
|
|
337
|
+
metered?: boolean;
|
|
338
|
+
/** Director turns before the run winds down. Universal; provider-independent. */
|
|
339
|
+
maxTurns?: number;
|
|
340
|
+
/** Wall-clock cap. Matters most exactly where dollars matter least. */
|
|
341
|
+
maxSeconds?: number;
|
|
342
|
+
/**
|
|
343
|
+
* How long a worker may produce nothing before it is treated as stalled and
|
|
344
|
+
* stopped. Absent means the orchestrator's default. Raise it for slow local
|
|
345
|
+
* models whose first token legitimately takes minutes.
|
|
346
|
+
*/
|
|
347
|
+
workerSilenceMs?: number;
|
|
348
|
+
status: RunStatus;
|
|
349
|
+
costUsd: number;
|
|
350
|
+
/**
|
|
351
|
+
* Where `costUsd` came from, kept apart so a resume adds to the right
|
|
352
|
+
* pile: `native` is the SDK's own figure for Anthropic-native roles,
|
|
353
|
+
* `rated` is gateway tokens priced from published or listed rates, and
|
|
354
|
+
* `ledger` is what a gateway upstream itself reported per response. When
|
|
355
|
+
* the upstream reports, its number outranks the rated one for the same
|
|
356
|
+
* tokens — the party that sends the bill wins.
|
|
357
|
+
*/
|
|
358
|
+
costParts?: { native: number; rated: number; ledger: number };
|
|
359
|
+
/**
|
|
360
|
+
* Token counts accumulated from every director and worker `result`
|
|
361
|
+
* message's `usage` object. Absent on runs recorded before this landed.
|
|
362
|
+
*/
|
|
363
|
+
usage?: TokenUsage;
|
|
364
|
+
/**
|
|
365
|
+
* Director turns taken so far, persisted so a resumed run keeps counting
|
|
366
|
+
* against {@link maxTurns} instead of restarting the cap from zero — the
|
|
367
|
+
* orchestrator's private counter is only ever accurate within one process.
|
|
368
|
+
*/
|
|
369
|
+
turns?: number;
|
|
370
|
+
createdAt: number;
|
|
371
|
+
endedAt?: number;
|
|
372
|
+
directorSessionId?: string;
|
|
373
|
+
workers: WorkerMeta[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Listing entry returned by GET /runs (meta without heavy fields). */
|
|
377
|
+
export type RunSummary = RunMeta;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* A project's planning conversation — the talk that happens before a mission.
|
|
381
|
+
*
|
|
382
|
+
* Only the session id and the running tally are kept here; the conversation
|
|
383
|
+
* itself is the append-only event log beside this file, exactly like a run's.
|
|
384
|
+
* There is no process behind it: each turn resumes the stored session and
|
|
385
|
+
* exits, so an idle conversation costs nothing but disk.
|
|
386
|
+
*/
|
|
387
|
+
export interface ChatMeta {
|
|
388
|
+
projectId: string;
|
|
389
|
+
/** Claude Code session to resume; absent until the first turn completes. */
|
|
390
|
+
sessionId?: string;
|
|
391
|
+
/** Everything this conversation has cost, for the whole of its life. */
|
|
392
|
+
costUsd: number;
|
|
393
|
+
createdAt: number;
|
|
394
|
+
updatedAt: number;
|
|
395
|
+
/** The most recent mission the planner proposed, if it has not been used. */
|
|
396
|
+
proposal?: MissionProposal;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** A mission the planner drafted, waiting for the human to start or discard. */
|
|
400
|
+
export interface MissionProposal {
|
|
401
|
+
id: string;
|
|
402
|
+
/** The brief, ready to go into the composer. */
|
|
403
|
+
mission: string;
|
|
404
|
+
/** Verifiable completion criteria, shown as a checklist. */
|
|
405
|
+
doneWhen: string[];
|
|
406
|
+
/** What the planner thinks it should cost. The human always sees it. */
|
|
407
|
+
budgetUsd: number;
|
|
408
|
+
/** Why this budget and this shape — one short paragraph. */
|
|
409
|
+
rationale?: string;
|
|
410
|
+
/**
|
|
411
|
+
* The planner judged the DONE WHEN criteria need a browser (a page must
|
|
412
|
+
* load, render, be console-clean or be screenshotted). The card starts with
|
|
413
|
+
* the switch on. Absent means "the planner did not say", not "no".
|
|
414
|
+
*/
|
|
415
|
+
browser?: boolean;
|
|
416
|
+
/**
|
|
417
|
+
* The planner's model recommendations, already validated against the
|
|
418
|
+
* machine's list — an id it could not have picked from the list is dropped
|
|
419
|
+
* and the role inherits. The card pre-selects these; the human can change
|
|
420
|
+
* them. `modelRationale` is the planner's one line on why, shown beside
|
|
421
|
+
* the pickers so the choice reads as a suggestion with a reason, not a
|
|
422
|
+
* setting that appeared.
|
|
423
|
+
*/
|
|
424
|
+
directorModel?: string;
|
|
425
|
+
workerModel?: string;
|
|
426
|
+
directorProviderId?: string;
|
|
427
|
+
workerProviderId?: string;
|
|
428
|
+
modelRationale?: string;
|
|
429
|
+
createdAt: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Result of a permission decision made by the human. */
|
|
433
|
+
export type PermissionDecision = 'allow' | 'allow_always' | 'deny';
|
|
434
|
+
|
|
435
|
+
/** Free-form UI settings blob (shape owned by the design system's modal). */
|
|
436
|
+
export type SettingsValues = Record<string, unknown>;
|
|
437
|
+
|
|
438
|
+
/** Persisted settings: global values + sparse per-project overlays. */
|
|
439
|
+
export interface SettingsFile {
|
|
440
|
+
global: SettingsValues;
|
|
441
|
+
projects: Record<string, SettingsValues>;
|
|
442
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap";:root{color-scheme:dark;--bg-app: #121417;--bg-panel: #1a1d22;--bg-card: #22262d;--bg-inset: #14161a;--bg-hover: #1e2228;--line: #2d323b;--line-strong: #3a4048;--ink-0: #e8e6df;--ink-1: #a7afb9;--ink-2: #6d7683;--brand: #e8b04b;--brand-ink: #14161a;--agent-director: var(--brand);--agent-worker: #9c86e8;--agent-worker-1: #9c86e8;--agent-worker-2: #7f9ff0;--agent-worker-3: #c48be0;--agent-worker-4: #b3a6f2;--agent-worker-5: #7a72d8;--agent-worker-6: #d19ac8;--status-good: #3fb950;--status-warning: #fab219;--status-serious: #ec835a;--status-critical: #e5484d;--brand-wash: rgba(232, 176, 75, .06);--brand-wash-strong: rgba(232, 176, 75, .12);--scrim: rgba(0, 0, 0, .55);--diff-add-bg: rgba(63, 185, 80, .12);--diff-del-bg: rgba(229, 72, 77, .12)}[data-theme=light]{color-scheme:light;--bg-app: #ecebe6;--bg-panel: #f6f5f1;--bg-card: #ffffff;--bg-inset: #e4e2db;--bg-hover: #f0efea;--line: #d9d6cd;--line-strong: #c2beb2;--ink-0: #1c1e22;--ink-1: #555c67;--ink-2: #7b828d;--brand: #c98f1f;--brand-ink: #ffffff;--agent-worker: #6f57d6;--agent-worker-1: #6f57d6;--agent-worker-2: #4a6fdc;--agent-worker-3: #a35bc9;--agent-worker-4: #8a78e0;--agent-worker-5: #5449b8;--agent-worker-6: #b3609e;--status-good: #1f8f3a;--status-warning: #b8790a;--status-serious: #c4592c;--status-critical: #c8323a;--brand-wash: rgba(201, 143, 31, .08);--brand-wash-strong: rgba(201, 143, 31, .16);--scrim: rgba(20, 22, 26, .45);--diff-add-bg: rgba(31, 143, 58, .12);--diff-del-bg: rgba(200, 50, 58, .12)}:root{--font-ui: "Source Sans 3", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;--ui-scale: 1;--fs-xs: .75rem;--fs-sm: .8125rem;--fs-md: .9375rem;--fs-lg: 1.0625rem;--fs-xl: 1.25rem;--lh: 1.45;--lh-prose: 1.55;--fw-regular: 400;--fw-medium: 500;--fw-semibold: 600;--ls-caps: .08em;--icon-sm: 14px;--icon-md: 16px;--icon-lg: 20px;--icon-stroke: 1.75}:root{--sp-1: 4px;--sp-2: 8px;--sp-3: 12px;--sp-4: 16px;--sp-5: 24px;--r-sm: 6px;--r-md: 10px;--r-pill: 999px;--rail-left: 15.625rem;--rail-right: 21.25rem;--bp-narrow: 900px;--bp-medium: 1180px;--fleet-max: 75rem;--composer-max: 45rem;--spine-max: 62rem;--card-min: 18.75rem;--card-min-h: 8.125rem;--modal-w: 30rem;--modal-w-wide: 32.5rem;--accent-w: 3px;--meter-h: 4px;--dot: 8px;--dot-sm: 6px;--focus: 0 0 0 2px var(--bg-app), 0 0 0 4px var(--brand)}:root{--dur-fast: .12s;--dur-meter: .3s;--dur-pulse: 1.6s;--ease: ease-in-out}@keyframes pulse{0%,to{opacity:1}50%{opacity:.55}}.pulse{animation:pulse var(--dur-pulse) var(--ease) infinite}@keyframes ticker-in{0%{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}.ticker{animation:ticker-in var(--dur-fast, .15s) var(--ease) both}:root{--surface-app: var(--bg-app);--surface-panel: var(--bg-panel);--surface-card: var(--bg-card);--surface-inset: var(--bg-inset);--surface-overlay: var(--bg-panel);--border-subtle: var(--line);--border-strong: var(--line-strong);--text-body: var(--ink-0);--text-secondary: var(--ink-1);--text-muted: var(--ink-2);--text-on-brand: var(--brand-ink);--text-accent: var(--brand);--text-danger: var(--status-critical);--intent-primary: var(--brand);--intent-affirm: var(--status-good);--intent-deny: var(--status-critical);--intent-caution: var(--status-warning);--intent-degraded: var(--status-serious);--state-idle: var(--ink-2);--state-running: var(--status-good);--state-done: var(--status-good);--state-interrupted: var(--status-serious);--state-error: var(--status-critical)}*{box-sizing:border-box}html,body,#root{height:100%}html{font-size:calc(100% * var(--ui-scale))}body{margin:0;background:var(--bg-app);color:var(--ink-0);font:var(--fs-md)/var(--lh) var(--font-ui)}button,input,textarea,select{font:inherit;color:inherit}:is(button,input,textarea,select):focus-visible{outline:none;box-shadow:var(--focus)}a{color:var(--brand);text-decoration:none}a:hover{text-decoration:underline}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:5px}::-webkit-scrollbar-track{background:transparent}
|