@zauso-ai/capstan-harness 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1069 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ const CAPSTAN_HARNESS_DIR = ".capstan/harness";
5
+ const RUNS_DIR = `${CAPSTAN_HARNESS_DIR}/runs`;
6
+ const EVENTS_PATH = `${CAPSTAN_HARNESS_DIR}/events.ndjson`;
7
+ const SUMMARIES_DIR = `${CAPSTAN_HARNESS_DIR}/summaries`;
8
+ const MEMORY_DIR = `${CAPSTAN_HARNESS_DIR}/memory`;
9
+ const DEFAULT_COMPACTION_TAIL = 5;
10
+ export async function createHarnessRun(appRoot, taskKey, input = {}, options = {}) {
11
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
12
+ const task = await readTaskDefinition(root, taskKey);
13
+ const at = new Date().toISOString();
14
+ const event = buildStartEvent(task, input, {
15
+ actor: options.actor ?? "agent",
16
+ at,
17
+ ...(options.note ? { note: options.note } : {})
18
+ });
19
+ const run = reduceHarnessEvent(undefined, event);
20
+ await persistHarnessRun(root, run);
21
+ await appendHarnessEvent(root, event);
22
+ return run;
23
+ }
24
+ export async function getHarnessRun(appRoot, runId, options = {}) {
25
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
26
+ return readHarnessRun(root, runId);
27
+ }
28
+ export async function listHarnessRuns(appRoot, options = {}) {
29
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
30
+ const runsDir = resolve(root, RUNS_DIR);
31
+ let entries;
32
+ try {
33
+ entries = await readdir(runsDir);
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ const runs = (await Promise.all(entries
39
+ .filter((entry) => entry.endsWith(".json"))
40
+ .map(async (entry) => {
41
+ try {
42
+ const source = await readFile(resolve(runsDir, entry), "utf8");
43
+ return JSON.parse(source);
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }))).filter((run) => Boolean(run));
49
+ return runs
50
+ .filter((run) => (options.taskKey ? run.taskKey === options.taskKey : true))
51
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
52
+ }
53
+ export async function pauseHarnessRun(appRoot, runId, options = {}) {
54
+ return mutateHarnessRun(appRoot, runId, {
55
+ type: "run_paused",
56
+ actor: options.actor ?? "human",
57
+ at: new Date().toISOString(),
58
+ ...(options.note ? { note: options.note } : {})
59
+ }, options.cwd);
60
+ }
61
+ export async function resumeHarnessRun(appRoot, runId, options = {}) {
62
+ return mutateHarnessRun(appRoot, runId, {
63
+ type: "run_resumed",
64
+ actor: options.actor ?? "agent",
65
+ at: new Date().toISOString(),
66
+ ...(options.note ? { note: options.note } : {})
67
+ }, options.cwd);
68
+ }
69
+ export async function requestHarnessApproval(appRoot, runId, options = {}) {
70
+ return mutateHarnessRun(appRoot, runId, {
71
+ type: "approval_requested",
72
+ actor: options.actor ?? "agent",
73
+ at: new Date().toISOString(),
74
+ ...(options.note ? { note: options.note } : {})
75
+ }, options.cwd);
76
+ }
77
+ export async function approveHarnessRun(appRoot, runId, options = {}) {
78
+ return mutateHarnessRun(appRoot, runId, {
79
+ type: "approval_granted",
80
+ actor: options.actor ?? "human",
81
+ at: new Date().toISOString(),
82
+ ...(options.note ? { note: options.note } : {})
83
+ }, options.cwd);
84
+ }
85
+ export async function requestHarnessInput(appRoot, runId, options = {}) {
86
+ return mutateHarnessRun(appRoot, runId, {
87
+ type: "input_requested",
88
+ actor: options.actor ?? "agent",
89
+ at: new Date().toISOString(),
90
+ ...(options.note ? { note: options.note } : {})
91
+ }, options.cwd);
92
+ }
93
+ export async function provideHarnessInput(appRoot, runId, input, options = {}) {
94
+ return mutateHarnessRun(appRoot, runId, {
95
+ type: "input_received",
96
+ actor: options.actor ?? "human",
97
+ at: new Date().toISOString(),
98
+ ...(options.note ? { note: options.note } : {}),
99
+ input
100
+ }, options.cwd);
101
+ }
102
+ export async function completeHarnessRun(appRoot, runId, output, options = {}) {
103
+ return mutateHarnessRun(appRoot, runId, {
104
+ type: "run_completed",
105
+ actor: options.actor ?? "agent",
106
+ at: new Date().toISOString(),
107
+ ...(options.note ? { note: options.note } : {}),
108
+ output
109
+ }, options.cwd);
110
+ }
111
+ export async function failHarnessRun(appRoot, runId, error, options = {}) {
112
+ return mutateHarnessRun(appRoot, runId, {
113
+ type: "run_failed",
114
+ actor: options.actor ?? "agent",
115
+ at: new Date().toISOString(),
116
+ error
117
+ }, options.cwd);
118
+ }
119
+ export async function retryHarnessRun(appRoot, runId, options = {}) {
120
+ return mutateHarnessRun(appRoot, runId, {
121
+ type: "run_retried",
122
+ actor: options.actor ?? "agent",
123
+ at: new Date().toISOString(),
124
+ ...(options.note ? { note: options.note } : {})
125
+ }, options.cwd);
126
+ }
127
+ export async function cancelHarnessRun(appRoot, runId, options = {}) {
128
+ return mutateHarnessRun(appRoot, runId, {
129
+ type: "run_cancelled",
130
+ actor: options.actor ?? "human",
131
+ at: new Date().toISOString(),
132
+ ...(options.note ? { note: options.note } : {})
133
+ }, options.cwd);
134
+ }
135
+ export async function listHarnessEvents(appRoot, options = {}) {
136
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
137
+ const eventsPath = resolve(root, EVENTS_PATH);
138
+ let source;
139
+ try {
140
+ source = await readFile(eventsPath, "utf8");
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ return source
146
+ .split("\n")
147
+ .map((line) => line.trim())
148
+ .filter(Boolean)
149
+ .map((line) => JSON.parse(line))
150
+ .filter((event) => (options.runId ? event.runId === options.runId : true))
151
+ .sort((left, right) => left.runId === right.runId
152
+ ? left.sequence - right.sequence
153
+ : left.at.localeCompare(right.at));
154
+ }
155
+ export async function replayHarnessRun(appRoot, runId, options = {}) {
156
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
157
+ const stored = await readHarnessRun(root, runId);
158
+ const events = await listHarnessEvents(root, {
159
+ runId
160
+ });
161
+ let replayed;
162
+ for (const event of events) {
163
+ replayed = reduceHarnessEvent(replayed, event);
164
+ }
165
+ return {
166
+ appRoot: root,
167
+ runId,
168
+ consistent: compareHarnessRuns(stored, replayed),
169
+ eventCount: events.length,
170
+ ...(stored ? { stored } : {}),
171
+ ...(replayed ? { replayed } : {})
172
+ };
173
+ }
174
+ export async function compactHarnessRun(appRoot, runId, options = {}) {
175
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
176
+ const stored = await readHarnessRunOrThrow(root, runId);
177
+ const events = await listHarnessEvents(root, { runId });
178
+ let replayed;
179
+ for (const event of events) {
180
+ replayed = reduceHarnessEvent(replayed, event);
181
+ }
182
+ const tailWindow = normalizeTailWindow(options.tail);
183
+ const summary = summarizeHarnessRun(root, stored, events, {
184
+ consistent: compareHarnessRuns(stored, replayed),
185
+ tailWindow
186
+ });
187
+ await persistHarnessSummary(root, summary);
188
+ return summary;
189
+ }
190
+ export async function getHarnessSummary(appRoot, runId, options = {}) {
191
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
192
+ const currentRun = await readHarnessRunOrThrow(root, runId);
193
+ if (options.refresh) {
194
+ return compactHarnessRun(root, runId, options);
195
+ }
196
+ const stored = await readHarnessSummary(root, runId);
197
+ if (stored && isHarnessSummaryFresh(currentRun, stored)) {
198
+ return stored;
199
+ }
200
+ return compactHarnessRun(root, runId, options);
201
+ }
202
+ export async function listHarnessSummaries(appRoot, options = {}) {
203
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
204
+ const summariesDir = resolve(root, SUMMARIES_DIR);
205
+ let entries;
206
+ try {
207
+ entries = await readdir(summariesDir);
208
+ }
209
+ catch {
210
+ return [];
211
+ }
212
+ const summaries = (await Promise.all(entries
213
+ .filter((entry) => entry.endsWith(".json"))
214
+ .map(async (entry) => {
215
+ try {
216
+ const source = await readFile(resolve(summariesDir, entry), "utf8");
217
+ return JSON.parse(source);
218
+ }
219
+ catch {
220
+ return undefined;
221
+ }
222
+ }))).filter((summary) => Boolean(summary));
223
+ const items = await Promise.all(summaries.map(async (summary) => {
224
+ const run = await readHarnessRun(root, summary.runId);
225
+ return {
226
+ runId: summary.runId,
227
+ taskKey: summary.taskKey,
228
+ taskTitle: summary.taskTitle,
229
+ status: summary.status,
230
+ attempt: summary.attempt,
231
+ consistent: summary.consistent,
232
+ eventCount: summary.eventCount,
233
+ compressedAt: summary.compressedAt,
234
+ fresh: run ? isHarnessSummaryFresh(run, summary) : false
235
+ };
236
+ }));
237
+ return items.sort((left, right) => right.compressedAt.localeCompare(left.compressedAt));
238
+ }
239
+ export async function createHarnessMemory(appRoot, runId, options = {}) {
240
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
241
+ const summary = await getHarnessSummary(root, runId, options);
242
+ const task = await readTaskDefinition(root, summary.taskKey);
243
+ const run = await readHarnessRunOrThrow(root, summary.runId);
244
+ const refreshedAt = new Date().toISOString();
245
+ const artifact = {
246
+ appRoot: root,
247
+ runId: summary.runId,
248
+ taskKey: summary.taskKey,
249
+ taskTitle: summary.taskTitle,
250
+ ...(task.description ? { taskDescription: task.description } : {}),
251
+ status: summary.status,
252
+ attempt: summary.attempt,
253
+ refreshedAt,
254
+ sourceRun: {
255
+ sequence: run.sequence,
256
+ lastEventId: run.lastEventId,
257
+ updatedAt: run.updatedAt
258
+ },
259
+ sourceSummary: {
260
+ compressedAt: summary.compressedAt,
261
+ tailWindow: summary.tailWindow
262
+ },
263
+ nextAction: decideHarnessNextAction(summary),
264
+ summaryPath: resolve(root, SUMMARIES_DIR, `${summary.runId}.json`),
265
+ inputKeys: summary.inputKeys,
266
+ outputKeys: summary.outputKeys,
267
+ operatorBrief: summary.operatorBrief,
268
+ suggestedCommands: buildHarnessSuggestedCommands(summary),
269
+ recentEvents: summary.recentEvents,
270
+ ...(summary.activeCheckpoint ? { activeCheckpoint: summary.activeCheckpoint } : {}),
271
+ ...(summary.error ? { error: summary.error } : {}),
272
+ prompt: buildHarnessMemoryPrompt(summary, task)
273
+ };
274
+ await persistHarnessMemory(root, artifact);
275
+ return artifact;
276
+ }
277
+ export async function getHarnessMemory(appRoot, runId, options = {}) {
278
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
279
+ const currentRun = await readHarnessRunOrThrow(root, runId);
280
+ if (options.refresh) {
281
+ return createHarnessMemory(root, runId, options);
282
+ }
283
+ const stored = await readHarnessMemory(root, runId);
284
+ if (stored && isHarnessMemoryFresh(currentRun, stored)) {
285
+ return stored;
286
+ }
287
+ return createHarnessMemory(root, runId, options);
288
+ }
289
+ export async function listHarnessMemories(appRoot, options = {}) {
290
+ const root = resolve(options.cwd ?? process.cwd(), appRoot);
291
+ const memoryDir = resolve(root, MEMORY_DIR);
292
+ let entries;
293
+ try {
294
+ entries = await readdir(memoryDir);
295
+ }
296
+ catch {
297
+ return [];
298
+ }
299
+ const memories = (await Promise.all(entries
300
+ .filter((entry) => entry.endsWith(".json"))
301
+ .map(async (entry) => {
302
+ try {
303
+ const source = await readFile(resolve(memoryDir, entry), "utf8");
304
+ return JSON.parse(source);
305
+ }
306
+ catch {
307
+ return undefined;
308
+ }
309
+ }))).filter((artifact) => Boolean(artifact));
310
+ const items = await Promise.all(memories.map(async (artifact) => {
311
+ const run = await readHarnessRun(root, artifact.runId);
312
+ return {
313
+ runId: artifact.runId,
314
+ taskKey: artifact.taskKey,
315
+ taskTitle: artifact.taskTitle,
316
+ status: artifact.status,
317
+ attempt: artifact.attempt,
318
+ refreshedAt: artifact.refreshedAt,
319
+ nextAction: artifact.nextAction,
320
+ fresh: run ? isHarnessMemoryFresh(run, artifact) : false
321
+ };
322
+ }));
323
+ return items.sort((left, right) => right.refreshedAt.localeCompare(left.refreshedAt));
324
+ }
325
+ export function serializeHarnessEvent(event) {
326
+ return `${JSON.stringify(event)}\n`;
327
+ }
328
+ export function renderHarnessRunText(run) {
329
+ const lines = [
330
+ "Capstan Harness Run",
331
+ `Run: ${run.id}`,
332
+ `Task: ${run.taskTitle} (${run.taskKey})`,
333
+ `Status: ${run.status}`,
334
+ `Attempt: ${run.attempt}`,
335
+ `Created At: ${run.createdAt}`,
336
+ `Updated At: ${run.updatedAt}`
337
+ ];
338
+ if (run.error) {
339
+ lines.push(`Error: ${run.error}`);
340
+ }
341
+ if (run.awaitingInput) {
342
+ lines.push(`Awaiting Input Since: ${run.awaitingInput.requestedAt}`);
343
+ if (run.awaitingInput.note) {
344
+ lines.push(`Awaiting Input Note: ${run.awaitingInput.note}`);
345
+ }
346
+ }
347
+ if (run.lastProvidedInput) {
348
+ lines.push(`Last Provided Input At: ${run.lastProvidedInput.at}`);
349
+ lines.push(`Last Provided Input Actor: ${run.lastProvidedInput.actor}`);
350
+ if (run.lastProvidedInput.note) {
351
+ lines.push(`Last Provided Input Note: ${run.lastProvidedInput.note}`);
352
+ }
353
+ }
354
+ if (typeof run.output !== "undefined") {
355
+ lines.push("Output:");
356
+ lines.push(JSON.stringify(run.output, null, 2));
357
+ }
358
+ return `${lines.join("\n")}\n`;
359
+ }
360
+ export function renderHarnessRunsText(runs) {
361
+ const lines = ["Capstan Harness Runs"];
362
+ if (!runs.length) {
363
+ lines.push("");
364
+ lines.push("No harness runs were found.");
365
+ return `${lines.join("\n")}\n`;
366
+ }
367
+ lines.push("");
368
+ for (const run of runs) {
369
+ lines.push(`- [${run.status}] ${run.taskTitle} (${run.taskKey}) · ${run.id} · attempt ${run.attempt}`);
370
+ }
371
+ return `${lines.join("\n")}\n`;
372
+ }
373
+ export function renderHarnessEventsText(events) {
374
+ const lines = ["Capstan Harness Events"];
375
+ if (!events.length) {
376
+ lines.push("");
377
+ lines.push("No harness events were found.");
378
+ return `${lines.join("\n")}\n`;
379
+ }
380
+ lines.push("");
381
+ for (const event of events) {
382
+ lines.push(`- #${event.sequence} [${event.status}] ${event.type} · run=${event.runId} · ${event.summary}`);
383
+ if (event.detail) {
384
+ lines.push(` detail: ${event.detail}`);
385
+ }
386
+ }
387
+ return `${lines.join("\n")}\n`;
388
+ }
389
+ export function renderHarnessReplayText(report) {
390
+ const lines = [
391
+ "Capstan Harness Replay",
392
+ `Run: ${report.runId}`,
393
+ `Consistent: ${report.consistent}`,
394
+ `Event Count: ${report.eventCount}`
395
+ ];
396
+ if (report.replayed) {
397
+ lines.push(`Replayed Status: ${report.replayed.status}`);
398
+ lines.push(`Replayed Attempt: ${report.replayed.attempt}`);
399
+ }
400
+ if (report.stored) {
401
+ lines.push(`Stored Status: ${report.stored.status}`);
402
+ lines.push(`Stored Attempt: ${report.stored.attempt}`);
403
+ }
404
+ return `${lines.join("\n")}\n`;
405
+ }
406
+ export function renderHarnessCompactionText(summary) {
407
+ const lines = [
408
+ "Capstan Harness Summary",
409
+ `Run: ${summary.runId}`,
410
+ `Task: ${summary.taskTitle} (${summary.taskKey})`,
411
+ `Status: ${summary.status}`,
412
+ `Attempt: ${summary.attempt}`,
413
+ `Consistent: ${summary.consistent}`,
414
+ `Event Count: ${summary.eventCount}`,
415
+ `Tail Window: ${summary.tailWindow}`,
416
+ `Compressed At: ${summary.compressedAt}`,
417
+ `Brief: ${summary.operatorBrief}`
418
+ ];
419
+ if (summary.activeCheckpoint) {
420
+ lines.push(`Active Checkpoint: ${summary.activeCheckpoint.type} requested at ${summary.activeCheckpoint.requestedAt}`);
421
+ if (summary.activeCheckpoint.note) {
422
+ lines.push(`Active Checkpoint Note: ${summary.activeCheckpoint.note}`);
423
+ }
424
+ }
425
+ if (summary.error) {
426
+ lines.push(`Error: ${summary.error}`);
427
+ }
428
+ if (summary.inputKeys.length) {
429
+ lines.push(`Input Keys: ${summary.inputKeys.join(", ")}`);
430
+ }
431
+ if (summary.outputKeys.length) {
432
+ lines.push(`Output Keys: ${summary.outputKeys.join(", ")}`);
433
+ }
434
+ if (summary.recentEvents.length) {
435
+ lines.push("Recent Events:");
436
+ for (const event of summary.recentEvents) {
437
+ lines.push(`- #${event.sequence} [${event.status}] ${event.type} · ${event.actor} · ${event.summary}`);
438
+ }
439
+ }
440
+ return `${lines.join("\n")}\n`;
441
+ }
442
+ export function renderHarnessSummariesText(summaries) {
443
+ const lines = ["Capstan Harness Summaries"];
444
+ if (!summaries.length) {
445
+ lines.push("");
446
+ lines.push("No harness summaries were found.");
447
+ return `${lines.join("\n")}\n`;
448
+ }
449
+ lines.push("");
450
+ for (const summary of summaries) {
451
+ lines.push(`- [${summary.status}] ${summary.taskTitle} (${summary.taskKey}) · ${summary.runId} · attempt ${summary.attempt} · consistent=${summary.consistent} · fresh=${summary.fresh}`);
452
+ }
453
+ return `${lines.join("\n")}\n`;
454
+ }
455
+ export function renderHarnessMemoryText(artifact) {
456
+ const lines = [
457
+ "Capstan Harness Memory",
458
+ `Run: ${artifact.runId}`,
459
+ `Task: ${artifact.taskTitle} (${artifact.taskKey})`,
460
+ `Status: ${artifact.status}`,
461
+ `Attempt: ${artifact.attempt}`,
462
+ `Refreshed At: ${artifact.refreshedAt}`,
463
+ `Next Action: ${artifact.nextAction}`,
464
+ `Operator Brief: ${artifact.operatorBrief}`
465
+ ];
466
+ if (artifact.taskDescription) {
467
+ lines.push(`Task Description: ${artifact.taskDescription}`);
468
+ }
469
+ if (artifact.activeCheckpoint) {
470
+ lines.push(`Active Checkpoint: ${artifact.activeCheckpoint.type} requested at ${artifact.activeCheckpoint.requestedAt}`);
471
+ if (artifact.activeCheckpoint.note) {
472
+ lines.push(`Active Checkpoint Note: ${artifact.activeCheckpoint.note}`);
473
+ }
474
+ }
475
+ if (artifact.error) {
476
+ lines.push(`Error: ${artifact.error}`);
477
+ }
478
+ if (artifact.suggestedCommands.length) {
479
+ lines.push("Suggested Commands:");
480
+ for (const command of artifact.suggestedCommands) {
481
+ lines.push(`- ${command}`);
482
+ }
483
+ }
484
+ lines.push("");
485
+ lines.push("Prompt:");
486
+ lines.push(artifact.prompt);
487
+ return `${lines.join("\n")}\n`;
488
+ }
489
+ export function renderHarnessMemoriesText(memories) {
490
+ const lines = ["Capstan Harness Memories"];
491
+ if (!memories.length) {
492
+ lines.push("");
493
+ lines.push("No harness memory artifacts were found.");
494
+ return `${lines.join("\n")}\n`;
495
+ }
496
+ lines.push("");
497
+ for (const artifact of memories) {
498
+ lines.push(`- [${artifact.status}] ${artifact.taskTitle} (${artifact.taskKey}) · ${artifact.runId} · attempt ${artifact.attempt} · next=${artifact.nextAction} · fresh=${artifact.fresh}`);
499
+ }
500
+ return `${lines.join("\n")}\n`;
501
+ }
502
+ export function summarizeHarnessRun(appRoot, run, events, options) {
503
+ const tailWindow = normalizeTailWindow(options.tailWindow);
504
+ const recentEvents = events.slice(-tailWindow).map((event) => ({
505
+ sequence: event.sequence,
506
+ type: event.type,
507
+ status: event.status,
508
+ actor: event.actor,
509
+ at: event.at,
510
+ summary: event.summary,
511
+ ...(event.detail ? { detail: event.detail } : {})
512
+ }));
513
+ const latestEvent = events.at(-1);
514
+ const checkpointHistory = summarizeHarnessCheckpoints(events);
515
+ const activeCheckpoint = checkpointHistory.find((checkpoint) => checkpoint.resolution === "pending");
516
+ const eventCounts = summarizeHarnessEventCounts(events);
517
+ const inputKeys = Object.keys(run.input).sort((left, right) => left.localeCompare(right));
518
+ const outputKeys = summarizeOutputKeys(run.output);
519
+ return {
520
+ appRoot,
521
+ runId: run.id,
522
+ taskKey: run.taskKey,
523
+ taskTitle: run.taskTitle,
524
+ status: run.status,
525
+ attempt: run.attempt,
526
+ consistent: options.consistent,
527
+ eventCount: events.length,
528
+ compressedAt: new Date().toISOString(),
529
+ tailWindow,
530
+ sourceRun: {
531
+ sequence: run.sequence,
532
+ lastEventId: run.lastEventId,
533
+ updatedAt: run.updatedAt
534
+ },
535
+ ...(latestEvent
536
+ ? {
537
+ boundary: {
538
+ sequence: latestEvent.sequence,
539
+ eventId: latestEvent.id,
540
+ type: latestEvent.type,
541
+ at: latestEvent.at
542
+ }
543
+ }
544
+ : {}),
545
+ inputKeys,
546
+ outputKeys,
547
+ recentEvents,
548
+ checkpointHistory,
549
+ ...(activeCheckpoint
550
+ ? {
551
+ activeCheckpoint: {
552
+ type: activeCheckpoint.type,
553
+ requestedAt: activeCheckpoint.requestedAt,
554
+ ...(activeCheckpoint.note ? { note: activeCheckpoint.note } : {})
555
+ }
556
+ }
557
+ : {}),
558
+ eventCounts,
559
+ operatorBrief: buildHarnessOperatorBrief(run, events, activeCheckpoint),
560
+ ...(run.error ? { error: run.error } : {})
561
+ };
562
+ }
563
+ export function reduceHarnessEvent(current, event) {
564
+ switch (event.type) {
565
+ case "run_started": {
566
+ if (current) {
567
+ throw new Error(`Run "${current.id}" has already been started.`);
568
+ }
569
+ const payload = ensureObjectPayload(event.payload);
570
+ const taskTitle = readStringPayload(payload, "taskTitle");
571
+ const attempt = readNumberPayload(payload, "attempt");
572
+ const input = ensureRecordPayload(payload, "input");
573
+ return {
574
+ id: event.runId,
575
+ taskKey: event.taskKey,
576
+ taskTitle,
577
+ status: "running",
578
+ attempt,
579
+ input,
580
+ createdAt: event.at,
581
+ updatedAt: event.at,
582
+ sequence: event.sequence,
583
+ lastEventId: event.id
584
+ };
585
+ }
586
+ case "run_paused":
587
+ return transitionHarnessRun(current, event, ["running"], "paused");
588
+ case "run_resumed":
589
+ return transitionHarnessRun(current, event, ["paused"], "running");
590
+ case "approval_requested":
591
+ return transitionHarnessRun(current, event, ["running", "paused"], "approval_required");
592
+ case "approval_granted":
593
+ return transitionHarnessRun(current, event, ["approval_required"], "running");
594
+ case "input_requested": {
595
+ const next = transitionHarnessRun(current, event, ["running", "paused"], "input_required");
596
+ return {
597
+ ...next,
598
+ awaitingInput: {
599
+ requestedAt: event.at,
600
+ ...(event.detail ? { note: event.detail } : {})
601
+ }
602
+ };
603
+ }
604
+ case "input_received": {
605
+ const next = transitionHarnessRun(current, event, ["input_required"], "running");
606
+ const providedInput = ensureRecordPayload(ensureObjectPayload(event.payload), "input");
607
+ const { awaitingInput: _awaitingInput, ...rest } = next;
608
+ return {
609
+ ...rest,
610
+ input: {
611
+ ...next.input,
612
+ ...providedInput
613
+ },
614
+ lastProvidedInput: {
615
+ at: event.at,
616
+ actor: event.actor,
617
+ ...(event.detail ? { note: event.detail } : {}),
618
+ payload: providedInput
619
+ }
620
+ };
621
+ }
622
+ case "run_completed": {
623
+ const next = transitionHarnessRun(current, event, ["running"], "completed");
624
+ return {
625
+ ...next,
626
+ output: event.payload
627
+ };
628
+ }
629
+ case "run_failed": {
630
+ const next = transitionHarnessRun(current, event, ["running", "paused", "approval_required"], "failed");
631
+ return {
632
+ ...next,
633
+ error: event.detail ?? "Harness run failed."
634
+ };
635
+ }
636
+ case "run_cancelled":
637
+ return transitionHarnessRun(current, event, ["running", "paused", "approval_required", "input_required"], "cancelled");
638
+ case "run_retried": {
639
+ const next = transitionHarnessRun(current, event, ["failed", "cancelled"], "running");
640
+ const { output: _output, error: _error, awaitingInput: _awaitingInput, ...rest } = next;
641
+ return {
642
+ ...rest,
643
+ attempt: next.attempt + 1
644
+ };
645
+ }
646
+ default:
647
+ throw new Error(`Unsupported harness event "${event.type}".`);
648
+ }
649
+ }
650
+ function transitionHarnessRun(current, event, allowedStatuses, nextStatus) {
651
+ if (!current) {
652
+ throw new Error(`Cannot apply "${event.type}" before a run has started.`);
653
+ }
654
+ if (!allowedStatuses.includes(current.status)) {
655
+ throw new Error(`Cannot apply "${event.type}" when run "${current.id}" is in status "${current.status}".`);
656
+ }
657
+ return {
658
+ ...current,
659
+ status: nextStatus,
660
+ updatedAt: event.at,
661
+ sequence: event.sequence,
662
+ lastEventId: event.id
663
+ };
664
+ }
665
+ async function mutateHarnessRun(appRoot, runId, input, cwd) {
666
+ const root = resolve(cwd ?? process.cwd(), appRoot);
667
+ const current = await readHarnessRunOrThrow(root, runId);
668
+ const event = buildHarnessEvent(current, input);
669
+ const next = reduceHarnessEvent(current, event);
670
+ await persistHarnessRun(root, next);
671
+ await appendHarnessEvent(root, event);
672
+ return next;
673
+ }
674
+ async function readTaskDefinition(appRoot, taskKey) {
675
+ const graph = await readAppGraph(appRoot);
676
+ const task = graph.tasks?.find((candidate) => candidate.key === taskKey);
677
+ if (!task) {
678
+ throw new Error(`Unknown task "${taskKey}".`);
679
+ }
680
+ return task;
681
+ }
682
+ async function readAppGraph(appRoot) {
683
+ const source = await readFile(resolve(appRoot, "capstan.app.json"), "utf8");
684
+ return JSON.parse(source);
685
+ }
686
+ async function ensureHarnessDirectories(appRoot) {
687
+ await mkdir(resolve(appRoot, RUNS_DIR), { recursive: true });
688
+ await mkdir(resolve(appRoot, dirname(EVENTS_PATH)), { recursive: true });
689
+ await mkdir(resolve(appRoot, SUMMARIES_DIR), { recursive: true });
690
+ await mkdir(resolve(appRoot, MEMORY_DIR), { recursive: true });
691
+ }
692
+ async function persistHarnessRun(appRoot, run) {
693
+ await ensureHarnessDirectories(appRoot);
694
+ await writeFile(resolve(appRoot, RUNS_DIR, `${run.id}.json`), `${JSON.stringify(run, null, 2)}\n`, "utf8");
695
+ }
696
+ async function appendHarnessEvent(appRoot, event) {
697
+ await ensureHarnessDirectories(appRoot);
698
+ await writeFile(resolve(appRoot, EVENTS_PATH), serializeHarnessEvent(event), {
699
+ encoding: "utf8",
700
+ flag: "a"
701
+ });
702
+ }
703
+ async function persistHarnessSummary(appRoot, summary) {
704
+ await ensureHarnessDirectories(appRoot);
705
+ await writeFile(resolve(appRoot, SUMMARIES_DIR, `${summary.runId}.json`), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
706
+ }
707
+ async function persistHarnessMemory(appRoot, artifact) {
708
+ await ensureHarnessDirectories(appRoot);
709
+ await writeFile(resolve(appRoot, MEMORY_DIR, `${artifact.runId}.json`), `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
710
+ }
711
+ async function readHarnessRun(appRoot, runId) {
712
+ try {
713
+ const source = await readFile(resolve(appRoot, RUNS_DIR, `${runId}.json`), "utf8");
714
+ return JSON.parse(source);
715
+ }
716
+ catch {
717
+ return undefined;
718
+ }
719
+ }
720
+ async function readHarnessSummary(appRoot, runId) {
721
+ try {
722
+ const source = await readFile(resolve(appRoot, SUMMARIES_DIR, `${runId}.json`), "utf8");
723
+ return JSON.parse(source);
724
+ }
725
+ catch {
726
+ return undefined;
727
+ }
728
+ }
729
+ async function readHarnessMemory(appRoot, runId) {
730
+ try {
731
+ const source = await readFile(resolve(appRoot, MEMORY_DIR, `${runId}.json`), "utf8");
732
+ return JSON.parse(source);
733
+ }
734
+ catch {
735
+ return undefined;
736
+ }
737
+ }
738
+ async function readHarnessRunOrThrow(appRoot, runId) {
739
+ const run = await readHarnessRun(appRoot, runId);
740
+ if (!run) {
741
+ throw new Error(`Unknown harness run "${runId}".`);
742
+ }
743
+ return run;
744
+ }
745
+ function buildStartEvent(task, input, options) {
746
+ return {
747
+ id: randomUUID(),
748
+ runId: `harness-run-${randomUUID()}`,
749
+ taskKey: task.key,
750
+ type: "run_started",
751
+ actor: options.actor,
752
+ sequence: 1,
753
+ at: options.at,
754
+ status: "running",
755
+ summary: `Started harness run for "${task.title}".`,
756
+ ...(options.note ? { detail: options.note } : {}),
757
+ payload: {
758
+ taskTitle: task.title,
759
+ attempt: 1,
760
+ input
761
+ }
762
+ };
763
+ }
764
+ function buildHarnessEvent(current, input) {
765
+ const base = {
766
+ id: randomUUID(),
767
+ runId: current.id,
768
+ taskKey: current.taskKey,
769
+ actor: input.actor,
770
+ sequence: current.sequence + 1,
771
+ at: input.at
772
+ };
773
+ switch (input.type) {
774
+ case "run_paused":
775
+ return {
776
+ ...base,
777
+ type: "run_paused",
778
+ status: "paused",
779
+ summary: `Paused harness run "${current.id}".`,
780
+ ...(input.note ? { detail: input.note } : {})
781
+ };
782
+ case "run_resumed":
783
+ return {
784
+ ...base,
785
+ type: "run_resumed",
786
+ status: "running",
787
+ summary: `Resumed harness run "${current.id}".`,
788
+ ...(input.note ? { detail: input.note } : {})
789
+ };
790
+ case "approval_requested":
791
+ return {
792
+ ...base,
793
+ type: "approval_requested",
794
+ status: "approval_required",
795
+ summary: `Requested approval for harness run "${current.id}".`,
796
+ ...(input.note ? { detail: input.note } : {})
797
+ };
798
+ case "approval_granted":
799
+ return {
800
+ ...base,
801
+ type: "approval_granted",
802
+ status: "running",
803
+ summary: `Approval granted for harness run "${current.id}".`,
804
+ ...(input.note ? { detail: input.note } : {})
805
+ };
806
+ case "input_requested":
807
+ return {
808
+ ...base,
809
+ type: "input_requested",
810
+ status: "input_required",
811
+ summary: `Requested additional input for harness run "${current.id}".`,
812
+ ...(input.note ? { detail: input.note } : {})
813
+ };
814
+ case "input_received":
815
+ return {
816
+ ...base,
817
+ type: "input_received",
818
+ status: "running",
819
+ summary: `Provided input for harness run "${current.id}".`,
820
+ ...(input.note ? { detail: input.note } : {}),
821
+ payload: {
822
+ input: input.input
823
+ }
824
+ };
825
+ case "run_completed":
826
+ return {
827
+ ...base,
828
+ type: "run_completed",
829
+ status: "completed",
830
+ summary: `Completed harness run "${current.id}".`,
831
+ ...(input.note ? { detail: input.note } : {}),
832
+ payload: input.output
833
+ };
834
+ case "run_failed":
835
+ return {
836
+ ...base,
837
+ type: "run_failed",
838
+ status: "failed",
839
+ summary: `Harness run "${current.id}" failed.`,
840
+ detail: input.error
841
+ };
842
+ case "run_cancelled":
843
+ return {
844
+ ...base,
845
+ type: "run_cancelled",
846
+ status: "cancelled",
847
+ summary: `Cancelled harness run "${current.id}".`,
848
+ ...(input.note ? { detail: input.note } : {})
849
+ };
850
+ case "run_retried":
851
+ return {
852
+ ...base,
853
+ type: "run_retried",
854
+ status: "running",
855
+ summary: `Retried harness run "${current.id}".`,
856
+ detail: input.note ?? `Attempt ${current.attempt + 1} started.`
857
+ };
858
+ default: {
859
+ const exhaustive = input;
860
+ throw new Error(`Unsupported harness transition ${String(exhaustive)}.`);
861
+ }
862
+ }
863
+ }
864
+ function compareHarnessRuns(stored, replayed) {
865
+ if (!stored || !replayed) {
866
+ return false;
867
+ }
868
+ return JSON.stringify(stored) === JSON.stringify(replayed);
869
+ }
870
+ function isHarnessSummaryFresh(current, summary) {
871
+ return (summary.sourceRun.sequence === current.sequence &&
872
+ summary.sourceRun.lastEventId === current.lastEventId &&
873
+ summary.status === current.status &&
874
+ summary.attempt === current.attempt);
875
+ }
876
+ function isHarnessMemoryFresh(current, artifact) {
877
+ return (artifact.sourceRun.sequence === current.sequence &&
878
+ artifact.sourceRun.lastEventId === current.lastEventId &&
879
+ artifact.status === current.status &&
880
+ artifact.attempt === current.attempt);
881
+ }
882
+ function decideHarnessNextAction(summary) {
883
+ switch (summary.status) {
884
+ case "running":
885
+ return "continue";
886
+ case "paused":
887
+ return "resume";
888
+ case "approval_required":
889
+ return "await_approval";
890
+ case "input_required":
891
+ return "await_input";
892
+ case "failed":
893
+ return "retry";
894
+ case "completed":
895
+ return "inspect_output";
896
+ case "cancelled":
897
+ return "review_cancellation";
898
+ }
899
+ }
900
+ function buildHarnessSuggestedCommands(summary) {
901
+ switch (summary.status) {
902
+ case "running":
903
+ return [
904
+ `capstan harness:get <app-dir> ${summary.runId} --json`,
905
+ `capstan harness:events <app-dir> --run ${summary.runId} --json`
906
+ ];
907
+ case "paused":
908
+ return [`capstan harness:resume <app-dir> ${summary.runId} --json`];
909
+ case "approval_required":
910
+ return [`capstan harness:approve <app-dir> ${summary.runId} --json`];
911
+ case "input_required":
912
+ return [
913
+ `capstan harness:provide-input <app-dir> ${summary.runId} --input ./input.json --json`
914
+ ];
915
+ case "failed":
916
+ return [`capstan harness:retry <app-dir> ${summary.runId} --json`];
917
+ case "completed":
918
+ return [
919
+ `capstan harness:get <app-dir> ${summary.runId} --json`,
920
+ `capstan harness:compact <app-dir> ${summary.runId} --json`
921
+ ];
922
+ case "cancelled":
923
+ return [
924
+ `capstan harness:retry <app-dir> ${summary.runId} --json`,
925
+ `capstan harness:events <app-dir> --run ${summary.runId} --json`
926
+ ];
927
+ }
928
+ }
929
+ function buildHarnessMemoryPrompt(summary, task) {
930
+ const lines = [
931
+ `You are resuming Capstan harness run "${summary.runId}".`,
932
+ `Task: ${summary.taskTitle} (${summary.taskKey})`,
933
+ `Status: ${summary.status}`,
934
+ `Attempt: ${summary.attempt}`,
935
+ `Operator brief: ${summary.operatorBrief}`
936
+ ];
937
+ if (task.description) {
938
+ lines.push(`Task description: ${task.description}`);
939
+ }
940
+ if (summary.activeCheckpoint) {
941
+ lines.push(`Active checkpoint: ${summary.activeCheckpoint.type} requested at ${summary.activeCheckpoint.requestedAt}.`);
942
+ if (summary.activeCheckpoint.note) {
943
+ lines.push(`Checkpoint note: ${summary.activeCheckpoint.note}`);
944
+ }
945
+ }
946
+ if (summary.inputKeys.length) {
947
+ lines.push(`Known input keys: ${summary.inputKeys.join(", ")}`);
948
+ }
949
+ if (summary.outputKeys.length) {
950
+ lines.push(`Known output keys: ${summary.outputKeys.join(", ")}`);
951
+ }
952
+ if (summary.error) {
953
+ lines.push(`Last error: ${summary.error}`);
954
+ }
955
+ if (summary.recentEvents.length) {
956
+ lines.push("Recent events:");
957
+ for (const event of summary.recentEvents) {
958
+ lines.push(`- #${event.sequence} ${event.type} [${event.status}] by ${event.actor}: ${event.summary}`);
959
+ }
960
+ }
961
+ return lines.join("\n");
962
+ }
963
+ function summarizeHarnessCheckpoints(events) {
964
+ const checkpoints = [];
965
+ for (const event of events) {
966
+ if (event.type === "approval_requested" || event.type === "input_requested") {
967
+ checkpoints.push({
968
+ type: event.type === "approval_requested" ? "approval" : "input",
969
+ requestedAt: event.at,
970
+ ...(event.detail ? { note: event.detail } : {}),
971
+ resolution: "pending"
972
+ });
973
+ continue;
974
+ }
975
+ const current = checkpoints.at(-1);
976
+ if (!current || current.resolution !== "pending") {
977
+ continue;
978
+ }
979
+ if (event.type === "approval_granted" && current.type === "approval") {
980
+ current.resolution = "granted";
981
+ current.resolvedAt = event.at;
982
+ current.resolvedBy = event.actor;
983
+ continue;
984
+ }
985
+ if (event.type === "input_received" && current.type === "input") {
986
+ current.resolution = "provided";
987
+ current.resolvedAt = event.at;
988
+ current.resolvedBy = event.actor;
989
+ continue;
990
+ }
991
+ if (event.type === "run_completed") {
992
+ current.resolution = "completed";
993
+ current.resolvedAt = event.at;
994
+ current.resolvedBy = event.actor;
995
+ continue;
996
+ }
997
+ if (event.type === "run_failed") {
998
+ current.resolution = "failed";
999
+ current.resolvedAt = event.at;
1000
+ current.resolvedBy = event.actor;
1001
+ continue;
1002
+ }
1003
+ if (event.type === "run_cancelled") {
1004
+ current.resolution = "cancelled";
1005
+ current.resolvedAt = event.at;
1006
+ current.resolvedBy = event.actor;
1007
+ }
1008
+ }
1009
+ return checkpoints;
1010
+ }
1011
+ function summarizeHarnessEventCounts(events) {
1012
+ const counts = {};
1013
+ for (const event of events) {
1014
+ counts[event.type] = (counts[event.type] ?? 0) + 1;
1015
+ }
1016
+ return counts;
1017
+ }
1018
+ function summarizeOutputKeys(output) {
1019
+ if (!output || typeof output !== "object" || Array.isArray(output)) {
1020
+ return [];
1021
+ }
1022
+ return Object.keys(output).sort((left, right) => left.localeCompare(right));
1023
+ }
1024
+ function buildHarnessOperatorBrief(run, events, activeCheckpoint) {
1025
+ const latestEvent = events.at(-1);
1026
+ const base = `Run "${run.id}" for "${run.taskTitle}" is ${run.status} on attempt ${run.attempt} after ${events.length} events.`;
1027
+ const latest = latestEvent
1028
+ ? ` Latest event: ${latestEvent.type} at ${latestEvent.at}.`
1029
+ : "";
1030
+ const checkpoint = activeCheckpoint
1031
+ ? ` Waiting on ${activeCheckpoint.type} input since ${activeCheckpoint.requestedAt}.`
1032
+ : "";
1033
+ const failure = run.error ? ` Last error: ${run.error}.` : "";
1034
+ return `${base}${latest}${checkpoint}${failure}`.trim();
1035
+ }
1036
+ function normalizeTailWindow(value) {
1037
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1038
+ return DEFAULT_COMPACTION_TAIL;
1039
+ }
1040
+ return Math.min(Math.max(Math.trunc(value), 1), 25);
1041
+ }
1042
+ function ensureObjectPayload(payload) {
1043
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1044
+ throw new Error("Harness event payload must be an object.");
1045
+ }
1046
+ return payload;
1047
+ }
1048
+ function ensureRecordPayload(payload, key) {
1049
+ const value = payload[key];
1050
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1051
+ throw new Error(`Harness event payload "${key}" must be an object.`);
1052
+ }
1053
+ return value;
1054
+ }
1055
+ function readStringPayload(payload, key) {
1056
+ const value = payload[key];
1057
+ if (typeof value !== "string" || !value.trim()) {
1058
+ throw new Error(`Harness event payload "${key}" must be a non-empty string.`);
1059
+ }
1060
+ return value;
1061
+ }
1062
+ function readNumberPayload(payload, key) {
1063
+ const value = payload[key];
1064
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1065
+ throw new Error(`Harness event payload "${key}" must be a finite number.`);
1066
+ }
1067
+ return value;
1068
+ }
1069
+ //# sourceMappingURL=index.js.map