harness-dispatch 0.5.0 → 0.6.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +5 -2
  3. package/dist/bin.d.ts.map +1 -1
  4. package/dist/bin.js +1 -175
  5. package/dist/bin.js.map +1 -1
  6. package/dist/config/route-fields.d.ts +72 -0
  7. package/dist/config/route-fields.d.ts.map +1 -0
  8. package/dist/config/route-fields.js +128 -0
  9. package/dist/config/route-fields.js.map +1 -0
  10. package/dist/config/validation.js +1 -1
  11. package/dist/config/validation.js.map +1 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +32 -171
  14. package/dist/config.js.map +1 -1
  15. package/dist/configure-yaml.d.ts +23 -0
  16. package/dist/configure-yaml.d.ts.map +1 -0
  17. package/dist/configure-yaml.js +193 -0
  18. package/dist/configure-yaml.js.map +1 -0
  19. package/dist/dispatchers/base.d.ts +3 -0
  20. package/dist/dispatchers/base.d.ts.map +1 -1
  21. package/dist/dispatchers/base.js.map +1 -1
  22. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  23. package/dist/dispatchers/generic-cli.js +4 -0
  24. package/dist/dispatchers/generic-cli.js.map +1 -1
  25. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  26. package/dist/dispatchers/openai-compatible.js +18 -0
  27. package/dist/dispatchers/openai-compatible.js.map +1 -1
  28. package/dist/dispatchers/shared/stream-subprocess.d.ts +11 -0
  29. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
  30. package/dist/dispatchers/shared/stream-subprocess.js +57 -35
  31. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
  32. package/dist/http/parse.d.ts +64 -0
  33. package/dist/http/parse.d.ts.map +1 -0
  34. package/dist/http/parse.js +230 -0
  35. package/dist/http/parse.js.map +1 -0
  36. package/dist/http/server.d.ts +2 -11
  37. package/dist/http/server.d.ts.map +1 -1
  38. package/dist/http/server.js +26 -232
  39. package/dist/http/server.js.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -2
  43. package/dist/index.js.map +1 -1
  44. package/dist/jobs/context.d.ts +17 -0
  45. package/dist/jobs/context.d.ts.map +1 -0
  46. package/dist/jobs/context.js +84 -0
  47. package/dist/jobs/context.js.map +1 -0
  48. package/dist/jobs/store.d.ts +80 -0
  49. package/dist/jobs/store.d.ts.map +1 -0
  50. package/dist/jobs/store.js +279 -0
  51. package/dist/jobs/store.js.map +1 -0
  52. package/dist/jobs/types.d.ts +135 -0
  53. package/dist/jobs/types.d.ts.map +1 -0
  54. package/dist/jobs/types.js +11 -0
  55. package/dist/jobs/types.js.map +1 -0
  56. package/dist/jobs.d.ts +113 -123
  57. package/dist/jobs.d.ts.map +1 -1
  58. package/dist/jobs.js +314 -309
  59. package/dist/jobs.js.map +1 -1
  60. package/dist/mcp/tool-schemas.d.ts +194 -0
  61. package/dist/mcp/tool-schemas.d.ts.map +1 -0
  62. package/dist/mcp/tool-schemas.js +311 -0
  63. package/dist/mcp/tool-schemas.js.map +1 -0
  64. package/dist/mcp/tools.d.ts +15 -59
  65. package/dist/mcp/tools.d.ts.map +1 -1
  66. package/dist/mcp/tools.js +63 -239
  67. package/dist/mcp/tools.js.map +1 -1
  68. package/dist/quota.d.ts +19 -1
  69. package/dist/quota.d.ts.map +1 -1
  70. package/dist/quota.js +39 -4
  71. package/dist/quota.js.map +1 -1
  72. package/dist/router.d.ts +4 -0
  73. package/dist/router.d.ts.map +1 -1
  74. package/dist/router.js +8 -0
  75. package/dist/router.js.map +1 -1
  76. package/dist/status.d.ts +13 -0
  77. package/dist/status.d.ts.map +1 -1
  78. package/dist/status.js +6 -0
  79. package/dist/status.js.map +1 -1
  80. package/dist/types.d.ts +24 -0
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/workspace-resolve.d.ts +98 -0
  83. package/dist/workspace-resolve.d.ts.map +1 -0
  84. package/dist/workspace-resolve.js +382 -0
  85. package/dist/workspace-resolve.js.map +1 -0
  86. package/dist/workspaces.d.ts +1 -0
  87. package/dist/workspaces.d.ts.map +1 -1
  88. package/dist/workspaces.js +67 -15
  89. package/dist/workspaces.js.map +1 -1
  90. package/package.json +5 -4
  91. package/dist/dashboard/live.d.ts +0 -49
  92. package/dist/dashboard/live.d.ts.map +0 -1
  93. package/dist/dashboard/live.js +0 -149
  94. package/dist/dashboard/live.js.map +0 -1
@@ -0,0 +1,279 @@
1
+ /**
2
+ * The job store: where a job lives on disk and how it is written.
3
+ *
4
+ * Split out of jobs.ts. These are the primitives every other part of the job
5
+ * system sits on — path resolution, atomic writes, status reads, retention —
6
+ * and they depend on nothing above them, so they lift out cleanly and can be
7
+ * reasoned about (and tested) without starting a dispatch.
8
+ *
9
+ * The atomicity here is load-bearing rather than incidental: a status file
10
+ * half-written when a reader arrives is indistinguishable from a crashed
11
+ * runner, which is why every write goes through tmp+rename with a retry for
12
+ * Windows EPERM.
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import { chmod, copyFile, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
16
+ import { randomUUID } from "node:crypto";
17
+ import path from "node:path";
18
+ import { setTimeout as delay } from "node:timers/promises";
19
+ import { stateRoot } from "../state-dir.js";
20
+ /**
21
+ * Dispatcher error strings are unbounded (a corrupted downstream config once
22
+ * produced a 173KB parse error). Full text always lands in stderr.log; the
23
+ * JSON surfaces returned over MCP carry a bounded copy.
24
+ */
25
+ const MAX_JSON_ERROR_CHARS = 4000;
26
+ /** Suggested delay before an agent checks `job_status` again. */
27
+ export const SUGGESTED_POLL_SECONDS = 300;
28
+ /**
29
+ * A "running" status whose updatedAt is older than this is a lie — the
30
+ * process that owned the run is gone (several missed heartbeats), so
31
+ * readers report the job as orphaned instead of keeping callers polling a
32
+ * corpse forever. Generous multiple of the heartbeat so an event-loop
33
+ * stall can't produce false orphans.
34
+ */
35
+ export const ORPHAN_THRESHOLD_MS = 90_000;
36
+ /**
37
+ * Compute-on-read orphan detection. Never writes the verdict back — the
38
+ * status file stays whatever the (dead) owner last wrote, so a future
39
+ * attach/recovery feature keeps its evidence intact.
40
+ */
41
+ export function withOrphanCheck(status) {
42
+ // Waiting for a concurrency slot is not death: nothing is heartbeating for
43
+ // it by design, so the staleness rule below would misreport every job that
44
+ // waits longer than 90s. drainSlotQueue() is what moves it forward.
45
+ if (status.slotQueued)
46
+ return status;
47
+ if (status.status !== "running" && status.status !== "queued")
48
+ return status;
49
+ const beat = Date.parse(status.updatedAt);
50
+ if (Number.isFinite(beat) && Date.now() - beat <= ORPHAN_THRESHOLD_MS)
51
+ return status;
52
+ return {
53
+ ...status,
54
+ status: "orphaned",
55
+ success: false,
56
+ error: "The dispatch server that started this job exited before the run finished — " +
57
+ "the background run died with it. Re-dispatch the task; this job will never complete.",
58
+ };
59
+ }
60
+ export function boundedError(error) {
61
+ if (error === undefined)
62
+ return undefined;
63
+ if (error.length <= MAX_JSON_ERROR_CHARS)
64
+ return error;
65
+ return (error.slice(0, MAX_JSON_ERROR_CHARS) +
66
+ ` … [truncated ${error.length - MAX_JSON_ERROR_CHARS} chars — full text in output/stderr.log]`);
67
+ }
68
+ export function pollInstructions(jobId) {
69
+ return (`Job runs in the background; CLI harnesses typically take 3-15 minutes. ` +
70
+ `Wait ~${Math.round(SUGGESTED_POLL_SECONDS / 60)} minutes (e.g. sleep), then call ` +
71
+ `job_status with jobId=${jobId}. While status is "running", partialOutput shows ` +
72
+ `progress; check again until status is "completed" or "failed". Results persist ` +
73
+ `on disk, so checking late loses nothing.`);
74
+ }
75
+ export function jobsRoot() {
76
+ return process.env.HARNESS_DISPATCH_JOBS_DIR ?? path.join(stateRoot(), "jobs");
77
+ }
78
+ const DEFAULT_JOB_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
79
+ let configuredJobMaxAgeMs;
80
+ /**
81
+ * Config-driven retention (`retention: { jobs_days: N }` in config.yaml) —
82
+ * set at runtime bootstrap and on every hot reload. Precedence:
83
+ * HARNESS_DISPATCH_JOB_MAX_AGE_MS env > config > 7-day default.
84
+ */
85
+ export function setJobRetentionDays(days) {
86
+ configuredJobMaxAgeMs =
87
+ days !== undefined && Number.isFinite(days) && days >= 0
88
+ ? days * 24 * 60 * 60 * 1000
89
+ : undefined;
90
+ }
91
+ export function jobMaxAgeMs() {
92
+ const raw = process.env.HARNESS_DISPATCH_JOB_MAX_AGE_MS;
93
+ const parsed = raw ? Number(raw) : NaN;
94
+ if (Number.isFinite(parsed) && parsed >= 0)
95
+ return parsed;
96
+ return configuredJobMaxAgeMs ?? DEFAULT_JOB_MAX_AGE_MS;
97
+ }
98
+ /**
99
+ * Nothing ever pruned old job directories — status.json/result.json/output
100
+ * logs and every snapshotted context file accumulated under jobsRoot()
101
+ * forever. Prune anything with no activity for the retention window
102
+ * (default 7 days, override via HARNESS_DISPATCH_JOB_MAX_AGE_MS) each time a
103
+ * new job is about to start. Job directory mtime is a reasonable proxy for
104
+ * "last activity": writeJson's tmp-then-rename touches the job dir on every
105
+ * status update, so a running (or freshly completed but unpolled) job keeps
106
+ * bumping it — only genuinely abandoned jobs go stale. Best effort: a prune
107
+ * failure must never block starting the job that was actually requested.
108
+ */
109
+ export async function pruneStaleJobs() {
110
+ const maxAgeMs = jobMaxAgeMs();
111
+ // 0 means KEEP FOREVER, not "prune immediately". The same config file
112
+ // establishes `max_concurrent_runs: 0` as "disable the bound", inviting the
113
+ // same reading here — and the old behaviour deleted RUNNING jobs out from
114
+ // under their runners (a job dir's mtime only moves on a 15s heartbeat, so
115
+ // at age 0 every beat gap was fatal): the runner's next write failed and
116
+ // the caller's jobId turned into "No such job".
117
+ if (maxAgeMs === 0)
118
+ return;
119
+ const root = jobsRoot();
120
+ let entries;
121
+ try {
122
+ entries = await readdir(root, { withFileTypes: true });
123
+ }
124
+ catch {
125
+ return;
126
+ }
127
+ const now = Date.now();
128
+ for (const entry of entries) {
129
+ if (!entry.isDirectory())
130
+ continue;
131
+ const jobDir = path.join(root, entry.name);
132
+ try {
133
+ const info = await stat(jobDir);
134
+ if (now - info.mtimeMs <= maxAgeMs)
135
+ continue;
136
+ // mtime is a proxy for activity; never delete a job that is
137
+ // demonstrably in flight. A live runner heartbeats status.json inside
138
+ // the orphan window, so running/queued with a fresh beat means "working
139
+ // right now", whatever retention says. An unreadable status file falls
140
+ // through to the mtime rule — that is the abandoned case.
141
+ try {
142
+ const status = JSON.parse(await readFile(path.join(jobDir, "status.json"), "utf8"));
143
+ const beat = Date.parse(status.updatedAt ?? "");
144
+ if ((status.status === "running" || status.status === "queued") &&
145
+ Number.isFinite(beat) &&
146
+ now - beat <= ORPHAN_THRESHOLD_MS) {
147
+ continue;
148
+ }
149
+ }
150
+ catch {
151
+ // Fall through to the mtime rule.
152
+ }
153
+ await rm(jobDir, { recursive: true, force: true });
154
+ }
155
+ catch {
156
+ // best effort — a locked/already-gone/permission-denied entry is skipped
157
+ }
158
+ }
159
+ }
160
+ export function timestamp() {
161
+ return new Date().toISOString();
162
+ }
163
+ export function safeBaseName(filePath) {
164
+ return path.basename(filePath).replace(/[^A-Za-z0-9_.-]/g, "_");
165
+ }
166
+ export async function writeJson(filePath, value) {
167
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID().slice(0, 8)}.tmp`;
168
+ await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
169
+ await renameWithRetry(tmpPath, filePath);
170
+ }
171
+ async function renameWithRetry(tmpPath, filePath) {
172
+ for (let attempt = 0; attempt < 8; attempt += 1) {
173
+ try {
174
+ await rename(tmpPath, filePath);
175
+ return;
176
+ }
177
+ catch (err) {
178
+ const code = typeof err === "object" && err !== null ? err.code : undefined;
179
+ if (code !== "EPERM" && code !== "EACCES" && code !== "EBUSY")
180
+ throw err;
181
+ await delay(25 * (attempt + 1));
182
+ }
183
+ }
184
+ await rename(tmpPath, filePath);
185
+ }
186
+ export async function readJson(filePath) {
187
+ return JSON.parse(await readFile(filePath, "utf8"));
188
+ }
189
+ export async function updateStatus(jobDir, status) {
190
+ await writeJson(path.join(jobDir, "status.json"), {
191
+ ...status,
192
+ updatedAt: timestamp(),
193
+ });
194
+ }
195
+ export async function snapshotFiles(jobDir, files) {
196
+ const out = [];
197
+ const filesDir = path.join(jobDir, "context", "files");
198
+ await mkdir(filesDir, { recursive: true, mode: 0o700 });
199
+ for (const [index, originalPath] of files.entries()) {
200
+ const item = { originalPath };
201
+ try {
202
+ const fileStat = await stat(originalPath);
203
+ if (!fileStat.isFile()) {
204
+ item.error = "not a regular file";
205
+ out.push(item);
206
+ continue;
207
+ }
208
+ const snapshotName = `${String(index + 1).padStart(3, "0")}-${safeBaseName(originalPath)}`;
209
+ const snapshotPath = path.join(filesDir, snapshotName);
210
+ await copyFile(originalPath, snapshotPath);
211
+ await chmod(snapshotPath, 0o600);
212
+ item.snapshotPath = snapshotPath;
213
+ item.sizeBytes = fileStat.size;
214
+ }
215
+ catch (err) {
216
+ item.error = err instanceof Error ? err.message : String(err);
217
+ }
218
+ out.push(item);
219
+ }
220
+ await writeJson(path.join(jobDir, "context", "files.json"), out);
221
+ return out;
222
+ }
223
+ /**
224
+ * The only jobId shape this module ever produces. Kept adjacent to
225
+ * `assertValidJobId` so the two cannot drift.
226
+ */
227
+ export function newJobId() {
228
+ return `job-${Date.now()}-${randomUUID().slice(0, 8)}`;
229
+ }
230
+ export const JOB_ID_RE = /^job-\d+-[0-9a-f]{8}$/;
231
+ /**
232
+ * Reject anything that isn't a jobId we generated, BEFORE it reaches
233
+ * path.join.
234
+ *
235
+ * The MCP schema validates this too, but the check belongs here as well:
236
+ * path.join(jobsRoot(), "../../etc/hosts") escapes the jobs root, and this
237
+ * function is reachable from more than one caller. Validating only at the
238
+ * schema would mean any future caller silently reintroduces the traversal.
239
+ */
240
+ export function assertValidJobId(jobId) {
241
+ if (!JOB_ID_RE.test(jobId)) {
242
+ throw new Error(`Invalid jobId ${JSON.stringify(jobId)} — expected job-<timestamp>-<8 hex chars>.`);
243
+ }
244
+ }
245
+ /**
246
+ * Cancellation is COOPERATIVE, by a marker file rather than a signal.
247
+ *
248
+ * Killing a pid is not available here: jobs run inside pooled supervisors
249
+ * (SUPERVISOR_POOL_SIZE), and one supervisor runs several jobs at once, so
250
+ * the only pid recorded against a job belongs to a process that is also
251
+ * running other people's work. Signalling it would cancel jobs nobody asked
252
+ * to cancel.
253
+ *
254
+ * So the canceller writes a marker and the RUN tears itself down: it drops
255
+ * out of its event stream, which triggers the dispatcher's own teardown
256
+ * (killTree on the agent CLI and its children) and releases the workspace
257
+ * lock through the same path a normal finish uses. The cost is that
258
+ * cancellation is not instant — it lands within one poll interval.
259
+ */
260
+ const CANCEL_MARKER = "cancel.json";
261
+ export async function requestCancel(jobDir, reason) {
262
+ await writeFile(path.join(jobDir, CANCEL_MARKER), JSON.stringify({ at: timestamp(), ...(reason !== undefined ? { reason } : {}) }), { encoding: "utf8", mode: 0o600 });
263
+ }
264
+ /** True once a cancel has been requested for this job. Cheap enough to poll. */
265
+ export function cancelRequested(jobDir) {
266
+ return existsSync(path.join(jobDir, CANCEL_MARKER));
267
+ }
268
+ /** The reason recorded with a cancel request, if one was given. */
269
+ export async function cancelReason(jobDir) {
270
+ try {
271
+ const raw = await readFile(path.join(jobDir, CANCEL_MARKER), "utf8");
272
+ const parsed = JSON.parse(raw);
273
+ return typeof parsed.reason === "string" ? parsed.reason : undefined;
274
+ }
275
+ catch {
276
+ return undefined;
277
+ }
278
+ }
279
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/jobs/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The shapes a job is written and read as.
3
+ *
4
+ * Split out of jobs.ts so the store, the supervisor and the dispatch path can
5
+ * share them without importing each other. These types are the actual
6
+ * contract between processes — a detached runner rebuilds its whole input
7
+ * from JobManifest, and every reader of a job's state goes through JobStatus
8
+ * — so they are the one part of the job system that must stay stable.
9
+ */
10
+ import type { DispatchResult, DispatcherEvent, RouteHints, RoutingDecision, WorkspacePolicy } from "../types.js";
11
+ import type { RuntimeHolder } from "../mcp/config-hot-reload.js";
12
+ export interface JobDeps {
13
+ holder: RuntimeHolder;
14
+ }
15
+ export interface StartJobInput {
16
+ prompt: string;
17
+ /**
18
+ * Earlier jobs whose results this one should be able to see.
19
+ *
20
+ * A delegate previously received a prompt and a file list and nothing else,
21
+ * so a second delegated step could not see what the first produced. The only
22
+ * way to chain work was for the orchestrator to read job A's output into its
23
+ * OWN context and re-summarise it into job B's prompt — spending exactly the
24
+ * context that delegating was meant to save, and losing detail in the
25
+ * retelling.
26
+ *
27
+ * Naming the prior jobs instead moves that transfer into the tool: their
28
+ * prompts and outputs are rendered into B's prompt directly, at full
29
+ * fidelity, without passing through the orchestrator.
30
+ */
31
+ contextJobs?: string[];
32
+ files?: string[];
33
+ workingDir?: string;
34
+ hints?: RouteHints;
35
+ workspacePolicy?: WorkspacePolicy;
36
+ service?: string;
37
+ /**
38
+ * The job this one is a retry of.
39
+ *
40
+ * Recorded so a chain of attempts is traceable after the fact — without it,
41
+ * three attempts at the same task look like three unrelated jobs, and the
42
+ * one that finally worked cannot be connected to the two that did not.
43
+ */
44
+ retryOf?: string;
45
+ /**
46
+ * Live dispatcher-event tap, used by the `dispatch` tool to forward MCP
47
+ * progress notifications during its inline grace window. Never serialized
48
+ * (the manifest lists its fields explicitly), never awaited, and a throw
49
+ * here must not fail the job.
50
+ */
51
+ onEvent?: (event: DispatcherEvent) => void;
52
+ }
53
+ export interface JobStatus {
54
+ jobId: string;
55
+ /**
56
+ * "orphaned" is never written to disk — it's computed on read: a job
57
+ * whose status file says "running" but whose heartbeat (updatedAt) has
58
+ * gone stale means the server process that owned the background run
59
+ * exited (session restart, crash) and the run died with it. Reporting it
60
+ * as still "running" forever is a lie that makes callers poll a corpse.
61
+ */
62
+ /**
63
+ * "cancelled" is terminal and deliberate: someone asked for this run to
64
+ * stop. It is distinct from "failed" because the run did not go wrong —
65
+ * treating a cancellation as a failure would charge the route's breaker and
66
+ * failure count for the caller changing their mind.
67
+ */
68
+ status: "queued" | "running" | "completed" | "failed" | "orphaned" | "cancelled";
69
+ createdAt: string;
70
+ updatedAt: string;
71
+ jobDir: string;
72
+ service?: string;
73
+ route?: string;
74
+ success?: boolean;
75
+ /** Bounded copy — full text in output/stderr.log. */
76
+ error?: string;
77
+ /**
78
+ * END-TO-END milliseconds for the run: routing, workspace preparation and
79
+ * any workspace-lock wait included. Deliberately NOT the same number as
80
+ * result.durationMs, which times the harness attempt alone — one job
81
+ * legitimately reports 116ms there and 15s here when it queued behind a
82
+ * shared_locked workspace. Both are real; they answer different questions.
83
+ */
84
+ durationMs?: number;
85
+ /** Suggested seconds to wait before polling action=get again. */
86
+ nextPollSeconds?: number;
87
+ /** Agent-facing guidance on how to collect the result. */
88
+ instructions?: string;
89
+ /** Set when workingDir was omitted and defaulted to the router's own cwd. */
90
+ warning?: string;
91
+ /**
92
+ * Queued because the machine is at `max_concurrent_runs`, NOT because its
93
+ * owner died. Load-bearing for orphan detection: a slot-queued job has no
94
+ * process heartbeating for it, so without this flag the 90s staleness rule
95
+ * would report a job that is merely waiting its turn as a dead one.
96
+ * Cleared the moment a slot frees and its runner is spawned.
97
+ */
98
+ slotQueued?: true;
99
+ }
100
+ export interface JobManifest {
101
+ jobId: string;
102
+ createdAt: string;
103
+ workingDir: string;
104
+ promptPath: string;
105
+ files: Array<{
106
+ originalPath: string;
107
+ snapshotPath?: string;
108
+ sizeBytes?: number;
109
+ error?: string;
110
+ }>;
111
+ hints?: RouteHints;
112
+ workspacePolicy?: WorkspacePolicy;
113
+ service?: string;
114
+ /** The job this one retries — see StartJobInput.retryOf. */
115
+ retryOf?: string;
116
+ /** Set when workingDir was omitted and defaulted to the router's own cwd. */
117
+ warning?: string;
118
+ }
119
+ export interface JobResultPayload {
120
+ jobId: string;
121
+ result: DispatchResult;
122
+ decision: RoutingDecision | null;
123
+ }
124
+ export interface StartedJob {
125
+ status: JobStatus;
126
+ /**
127
+ * Resolves once the run has reached a terminal state on disk (result.json
128
+ * or a failed/orphaned status). Never rejects. In detached mode this is a
129
+ * disk watcher on the job directory — the run itself lives in a separate
130
+ * job-runner process; in in-process mode (HARNESS_DISPATCH_INPROC_JOBS=1,
131
+ * used by unit tests with injected fakes) it is the runJob promise itself.
132
+ */
133
+ completion: Promise<void>;
134
+ }
135
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/jobs/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACjH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH;;;;;OAKG;IACH,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,CAAC;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,SAAS,CAAC;IAClB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The shapes a job is written and read as.
3
+ *
4
+ * Split out of jobs.ts so the store, the supervisor and the dispatch path can
5
+ * share them without importing each other. These types are the actual
6
+ * contract between processes — a detached runner rebuilds its whole input
7
+ * from JobManifest, and every reader of a job's state goes through JobStatus
8
+ * — so they are the one part of the job system that must stay stable.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/jobs/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}