harnery 0.27.0 → 0.29.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/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/work.d.ts +2 -0
- package/dist/commands/work.d.ts.map +1 -1
- package/dist/commands/work.js +74 -5
- package/dist/core/harnesses/types.d.ts +5 -0
- package/dist/core/harnesses/types.d.ts.map +1 -1
- package/dist/core/work/index.d.ts +1 -1
- package/dist/core/work/index.d.ts.map +1 -1
- package/dist/core/work/index.js +1 -1
- package/dist/core/work/runner.d.ts.map +1 -1
- package/dist/core/work/runner.js +8 -1
- package/dist/core/work/state.d.ts +24 -1
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +108 -14
- package/dist/core/workflow/approvals.d.ts.map +1 -1
- package/dist/core/workflow/approvals.js +3 -6
- package/dist/core/workflow/attempt-context.d.ts.map +1 -1
- package/dist/core/workflow/attempt-context.js +38 -1
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +20 -19
- package/dist/core/workflow/journal.d.ts +30 -0
- package/dist/core/workflow/journal.d.ts.map +1 -0
- package/dist/core/workflow/journal.js +70 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +8 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +8 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +8 -0
- package/dist/core/workflow/types.d.ts +11 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +101 -20
- package/dist/core/workflow/workspaces/state.d.ts +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +9 -30
- package/dist/lib/browser/client.d.ts +50 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +103 -0
- package/dist/lib/exec.d.ts +4 -0
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +6 -1
- package/package.json +1 -1
- package/src/commands/browse.ts +33 -1
- package/src/commands/work.ts +109 -14
- package/src/core/harnesses/types.ts +5 -0
- package/src/core/work/index.ts +1 -0
- package/src/core/work/runner.ts +8 -0
- package/src/core/work/state.ts +177 -27
- package/src/core/workflow/approvals.ts +3 -6
- package/src/core/workflow/attempt-context.ts +47 -1
- package/src/core/workflow/engine.ts +48 -33
- package/src/core/workflow/journal.ts +90 -0
- package/src/core/workflow/spawn-claude.ts +8 -0
- package/src/core/workflow/spawn-codex.ts +8 -0
- package/src/core/workflow/spawn-cursor.ts +8 -0
- package/src/core/workflow/types.ts +12 -0
- package/src/core/workflow/workspaces/local-git.ts +112 -22
- package/src/core/workflow/workspaces/state.ts +13 -33
- package/src/lib/browser/client.ts +129 -0
- package/src/lib/exec.ts +10 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAiJ1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAiQN"}
|
package/dist/commands/browse.js
CHANGED
|
@@ -55,6 +55,7 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
55
55
|
.option("--press <key>", "Press a key after navigation/fill (e.g., Enter)")
|
|
56
56
|
.option("--wait-for <selector>", "Wait for this selector before capturing output")
|
|
57
57
|
.option("--evaluate <js>", "Run JS in the page context after navigation; result printed to stdout")
|
|
58
|
+
.option("--capture-evaluate <js>", "In trio mode, evaluate JS inside the exact screenshot viewport immediately before capture; result is written as captureEval")
|
|
58
59
|
.option("--batch <steps>", "Run multiple steps in one session, semicolon-separated. Each step is one of: " +
|
|
59
60
|
"`click <selector>`, `fill <selector=>value>`, `press <key>`, `wait <selector|ms>`, `eval <js>`, `reload`. " +
|
|
60
61
|
'Example: `--batch "click button; wait 1500; reload; wait 3000"`. `reload` preserves sessionStorage + cookies, which is how to repro sessionStorage-restored UI state.')
|
|
@@ -312,6 +313,9 @@ async function runBrowse(url, opts, context) {
|
|
|
312
313
|
devOverlay = await captureDevOverlay(browser.currentPage);
|
|
313
314
|
}
|
|
314
315
|
if (printMode) {
|
|
316
|
+
if (opts.captureEvaluate) {
|
|
317
|
+
throw new Error("--capture-evaluate requires trio mode so its result can be bound to a screenshot.");
|
|
318
|
+
}
|
|
315
319
|
if (opts.baseline || opts.diff) {
|
|
316
320
|
throw new Error("--baseline / --diff require trio mode (screenshot file). Remove --snapshot/--html/--json or capture the screenshot first.");
|
|
317
321
|
}
|
|
@@ -551,11 +555,28 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
551
555
|
const skipScreenshot = opts.screenshot === false || opts.domOnly === true;
|
|
552
556
|
let pngPath;
|
|
553
557
|
let pngBytes;
|
|
558
|
+
let captureEvalResult;
|
|
559
|
+
let captureViewport;
|
|
560
|
+
let captureEvidence;
|
|
554
561
|
if (!skipScreenshot) {
|
|
555
562
|
pngPath = `${prefix}.png`;
|
|
556
|
-
|
|
563
|
+
if (opts.captureEvaluate) {
|
|
564
|
+
const captured = await browser.screenshotWithEvaluation(pngPath, opts.captureEvaluate, {
|
|
565
|
+
fullPage: opts.fullPage !== false,
|
|
566
|
+
});
|
|
567
|
+
pngBytes = captured.bytes;
|
|
568
|
+
captureEvalResult = captured.evaluation;
|
|
569
|
+
captureViewport = captured.viewport;
|
|
570
|
+
captureEvidence = captured.evidence;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
pngBytes = await browser.screenshot(pngPath, { fullPage: opts.fullPage !== false });
|
|
574
|
+
}
|
|
557
575
|
written.push(pngPath);
|
|
558
576
|
}
|
|
577
|
+
else if (opts.captureEvaluate) {
|
|
578
|
+
throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot / --dom-only.");
|
|
579
|
+
}
|
|
559
580
|
const htmlPath = `${prefix}.html`;
|
|
560
581
|
const html = await browser.htmlContent(opts.selector);
|
|
561
582
|
writeFileSync(htmlPath, html);
|
|
@@ -577,6 +598,11 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
577
598
|
envelope.screenshotBytes = pngBytes;
|
|
578
599
|
if (opts.evaluate)
|
|
579
600
|
envelope.eval = evalResult;
|
|
601
|
+
if (opts.captureEvaluate) {
|
|
602
|
+
envelope.captureEval = captureEvalResult;
|
|
603
|
+
envelope.captureViewport = captureViewport;
|
|
604
|
+
envelope.captureEvidence = captureEvidence;
|
|
605
|
+
}
|
|
580
606
|
if (opts.networkHar)
|
|
581
607
|
envelope.har = resolve(opts.networkHar);
|
|
582
608
|
if (visibility)
|
package/dist/commands/work.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { EmitContext } from "../commander.js";
|
|
3
|
+
import { type WorkAttempt } from "../core/work/index.js";
|
|
3
4
|
export declare function registerWorkCommand(program: Command, emit: EmitContext): void;
|
|
5
|
+
export declare function renderAttemptRow(attempt: WorkAttempt): string;
|
|
4
6
|
//# sourceMappingURL=work.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work.d.ts","sourceRoot":"","sources":["../../src/commands/work.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"work.d.ts","sourceRoot":"","sources":["../../src/commands/work.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAUnD,OAAO,EAUL,KAAK,WAAW,EAEjB,MAAM,uBAAuB,CAAC;AAuC/B,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA8F7E;AA0OD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAM7D"}
|
package/dist/commands/work.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { workflowSubscriptionOnly } from "../core/config.js";
|
|
2
3
|
import { createBuiltinHarnessRegistry, harnessProofInputs, probeBinaryVersion, } from "../core/harnesses/index.js";
|
|
3
4
|
import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
|
|
4
5
|
import { loadPolicyFile } from "../core/policy/index.js";
|
|
5
6
|
import { acceptWorkItem, cancelWorkItem, createWorkItem, listWorkItems, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, runWorkItem, } from "../core/work/index.js";
|
|
6
7
|
import { WorkflowParkedError } from "../core/workflow/index.js";
|
|
8
|
+
const MAX_ATTEMPT_JOURNAL_ERROR_RENDER = 240;
|
|
7
9
|
export function registerWorkCommand(program, emit) {
|
|
8
10
|
const registry = createBuiltinHarnessRegistry();
|
|
9
11
|
const harnesses = registry.ids();
|
|
@@ -110,6 +112,7 @@ function registerRunCommand(work, name, retry, registry, harnesses, emit) {
|
|
|
110
112
|
.option("--allow-api-billing", "Permit API-key override billing")
|
|
111
113
|
.option("--policy <file>", "Host policy JSON/JSONC")
|
|
112
114
|
.option("--isolation <mode>", "shared | worktree | sandbox | remote")
|
|
115
|
+
.option("--workspace-root <dir>", "Writable root the isolated workspace is allocated under")
|
|
113
116
|
.option("--approval-to <address>", "Address durable ASK requests")
|
|
114
117
|
.option("--actor <name>", "Actor recorded in attempt receipts")
|
|
115
118
|
.option("--json", "Emit the workflow report or parked result as JSON")
|
|
@@ -118,7 +121,21 @@ function registerRunCommand(work, name, retry, registry, harnesses, emit) {
|
|
|
118
121
|
if (opts.harness && !registry.get(opts.harness)) {
|
|
119
122
|
throw new Error(`unknown harness ${JSON.stringify(opts.harness)}`);
|
|
120
123
|
}
|
|
124
|
+
if (opts.workspaceRoot && opts.isolation !== "worktree") {
|
|
125
|
+
throw new Error("--workspace-root requires --isolation worktree");
|
|
126
|
+
}
|
|
121
127
|
try {
|
|
128
|
+
// An isolated attempt needs a provider AND the writable root it may
|
|
129
|
+
// allocate under; the engine refuses to guess either. Without them a
|
|
130
|
+
// worktree request degrades to shared, which the proof records as
|
|
131
|
+
// requested vs effective isolation and the reporting below surfaces.
|
|
132
|
+
const { createLocalGitWorktreeProvider } = await import("../core/workflow/index.js");
|
|
133
|
+
const workspace = opts.workspaceRoot
|
|
134
|
+
? {
|
|
135
|
+
provider: createLocalGitWorktreeProvider({ coordRoot }),
|
|
136
|
+
writableRoots: [resolve(opts.workspaceRoot)],
|
|
137
|
+
}
|
|
138
|
+
: undefined;
|
|
122
139
|
const report = await runWorkItem({
|
|
123
140
|
coordRoot,
|
|
124
141
|
workId,
|
|
@@ -137,6 +154,7 @@ function registerRunCommand(work, name, retry, registry, harnesses, emit) {
|
|
|
137
154
|
approvalMode: "park",
|
|
138
155
|
approvalAddressee: opts.approvalTo,
|
|
139
156
|
isolation: opts.isolation,
|
|
157
|
+
workspace,
|
|
140
158
|
networkAccess: "enabled",
|
|
141
159
|
},
|
|
142
160
|
});
|
|
@@ -145,7 +163,16 @@ function registerRunCommand(work, name, retry, registry, harnesses, emit) {
|
|
|
145
163
|
emit.data(report);
|
|
146
164
|
}
|
|
147
165
|
else {
|
|
148
|
-
|
|
166
|
+
// A requested isolation the run could not honour is recorded in the
|
|
167
|
+
// proof as requested vs effective, which nobody reads on a green run.
|
|
168
|
+
// Say it here instead, so a silent downgrade to shared cannot pass for
|
|
169
|
+
// an isolated attempt.
|
|
170
|
+
const degraded = opts.isolation && opts.isolation !== "shared" && !report.workspaceBinding
|
|
171
|
+
? `note: ran shared; ${opts.isolation} isolation was requested but not allocated` +
|
|
172
|
+
`${opts.workspaceRoot ? "" : " (no --workspace-root given)"}\n`
|
|
173
|
+
: "";
|
|
174
|
+
emit.text(`work ${workId}: run ${report.runId} finished\n${degraded}` +
|
|
175
|
+
`proof: ${report.proofPath}\n` +
|
|
149
176
|
`next: harn work reconcile ${workId}\n`);
|
|
150
177
|
}
|
|
151
178
|
}
|
|
@@ -182,14 +209,43 @@ function registerGovernanceCommand(work, name, emit) {
|
|
|
182
209
|
: "Explicitly reopen terminal or blocked work for a future attempt.")
|
|
183
210
|
.option("--actor <name>", "Actor recorded in the governance receipt")
|
|
184
211
|
.option("--reason <text>", "Bounded decision reason")
|
|
185
|
-
.option("--json", "Emit the complete work record as JSON")
|
|
186
|
-
|
|
212
|
+
.option("--json", "Emit the complete work record as JSON");
|
|
213
|
+
const command = work.commands.at(-1);
|
|
214
|
+
if (name === "reopen") {
|
|
215
|
+
command.option("--finding <text>", "Correction the next attempt must address (repeatable)", collectValue, []);
|
|
216
|
+
}
|
|
217
|
+
if (name === "accept") {
|
|
218
|
+
command.option("--dispose <id=outcome[:reason]>", "Dispose of an open finding: fixed, or deferred:<reason> (repeatable)", collectValue, []);
|
|
219
|
+
}
|
|
220
|
+
command.action((workId, opts) => {
|
|
187
221
|
withWorkRoot(emit, (coordRoot) => {
|
|
188
222
|
const fn = name === "accept" ? acceptWorkItem : name === "cancel" ? cancelWorkItem : reopenWorkItem;
|
|
189
|
-
emitWork(fn(coordRoot, workId,
|
|
223
|
+
emitWork(fn(coordRoot, workId, {
|
|
224
|
+
...opts,
|
|
225
|
+
...(opts.finding?.length ? { findings: opts.finding } : {}),
|
|
226
|
+
...(opts.dispose?.length ? { dispositions: opts.dispose.map(parseDisposition) } : {}),
|
|
227
|
+
}), opts.json, emit, true);
|
|
190
228
|
});
|
|
191
229
|
});
|
|
192
230
|
}
|
|
231
|
+
function collectValue(value, previous) {
|
|
232
|
+
return [...previous, value];
|
|
233
|
+
}
|
|
234
|
+
/** `<id>=fixed` or `<id>=deferred:<reason>`. Deferring without a reason is
|
|
235
|
+
* rejected downstream, so a lapsed finding cannot look like a decision. */
|
|
236
|
+
function parseDisposition(input) {
|
|
237
|
+
const split = input.indexOf("=");
|
|
238
|
+
if (split <= 0)
|
|
239
|
+
throw new Error(`invalid --dispose ${JSON.stringify(input)}; expected <id>=<outcome>`);
|
|
240
|
+
const id = input.slice(0, split).trim();
|
|
241
|
+
const rest = input.slice(split + 1).trim();
|
|
242
|
+
const [outcome, ...reasonParts] = rest.split(":");
|
|
243
|
+
if (outcome !== "fixed" && outcome !== "deferred") {
|
|
244
|
+
throw new Error(`invalid --dispose outcome ${JSON.stringify(outcome)}; expected fixed or deferred`);
|
|
245
|
+
}
|
|
246
|
+
const reason = reasonParts.join(":").trim();
|
|
247
|
+
return { id, outcome, ...(reason ? { reason } : {}) };
|
|
248
|
+
}
|
|
193
249
|
function emitWork(record, json, emit, detail = false) {
|
|
194
250
|
if (json) {
|
|
195
251
|
emit.config({ format: "json" });
|
|
@@ -215,7 +271,7 @@ function emitWork(record, json, emit, detail = false) {
|
|
|
215
271
|
lines.push("acceptance:", ...record.intent.acceptance.map((item) => ` - ${item}`));
|
|
216
272
|
}
|
|
217
273
|
if (projection.attempts.length) {
|
|
218
|
-
lines.push("attempts:", ...projection.attempts.map(
|
|
274
|
+
lines.push("attempts:", ...projection.attempts.map(renderAttemptRow));
|
|
219
275
|
}
|
|
220
276
|
if (projection.approval_id)
|
|
221
277
|
lines.push(`approval: ${projection.approval_id}`);
|
|
@@ -223,6 +279,19 @@ function emitWork(record, json, emit, detail = false) {
|
|
|
223
279
|
lines.push(`proof: ${projection.proof_path}`);
|
|
224
280
|
emit.text(`${lines.join("\n")}\n`);
|
|
225
281
|
}
|
|
282
|
+
export function renderAttemptRow(attempt) {
|
|
283
|
+
const statusDetail = attempt.status === "journal_unreadable"
|
|
284
|
+
? `: ${renderAttemptJournalError(attempt.journal_error)}`
|
|
285
|
+
: "";
|
|
286
|
+
return ` ${attempt.number}. ${attempt.run_id} ${attempt.status}${statusDetail}`;
|
|
287
|
+
}
|
|
288
|
+
function renderAttemptJournalError(error) {
|
|
289
|
+
const normalized = (error ?? "").replace(/\s+/g, " ").trim();
|
|
290
|
+
const value = normalized || "unknown journal read error";
|
|
291
|
+
if (value.length <= MAX_ATTEMPT_JOURNAL_ERROR_RENDER)
|
|
292
|
+
return value;
|
|
293
|
+
return `${value.slice(0, MAX_ATTEMPT_JOURNAL_ERROR_RENDER - 3).trimEnd()}...`;
|
|
294
|
+
}
|
|
226
295
|
function renderWorkRow(record) {
|
|
227
296
|
const projection = record.projection;
|
|
228
297
|
return `${record.intent.id} ${projection.state.padEnd(17)} ${projection.attempts_used}/${record.intent.max_attempts} ${projection.next_action.padEnd(21)} ${record.intent.title}`;
|
|
@@ -56,6 +56,11 @@ export interface HarnessRawResult {
|
|
|
56
56
|
exitCode: number;
|
|
57
57
|
durationMs: number;
|
|
58
58
|
resultFileText?: string;
|
|
59
|
+
/** The host killed this child for exceeding its timeout. A vendor CLI that
|
|
60
|
+
* handles the signal cleanly still exits 0, so exit code alone cannot tell a
|
|
61
|
+
* kill from a finish; without this a timed-out child reads as a success with
|
|
62
|
+
* an empty answer. */
|
|
63
|
+
timedOut?: boolean;
|
|
59
64
|
}
|
|
60
65
|
export interface HarnessBenchFixture {
|
|
61
66
|
raw: HarnessRawResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE/E;uDACuD;AACvD,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAAC;AAEpF,eAAO,MAAM,6BAA6B,2TAoBhC,CAAC;AAEX,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAC;AAExF,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,iBAAiB,CAAC;IAC3B;iFAC6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,0BAA0B,EAAE,eAAe,CAAC,CAAC;AAEtF;;;0DAG0D;AAC1D,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC,WAAW,GAAG,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9D,iEAAiE;IACjE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,SAAS,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,gBAAgB,CAAC;IAClC,SAAS,EAAE,UAAU,CAAC;IACtB,WAAW,EAAE,QAAQ,GAAG,KAAK,GAAG,OAAO,CAAC;IACxC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,YAAY,EAAE,mBAAmB,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C;uEACmE;IACnE,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAED;oEACoE;AACpE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE/E;uDACuD;AACvD,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAAC;AAEpF,eAAO,MAAM,6BAA6B,2TAoBhC,CAAC;AAEX,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAC;AAExF,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,iBAAiB,CAAC;IAC3B;iFAC6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,0BAA0B,EAAE,eAAe,CAAC,CAAC;AAEtF;;;0DAG0D;AAC1D,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC,WAAW,GAAG,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9D,iEAAiE;IACjE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,SAAS,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,gBAAgB,CAAC;IAClC,SAAS,EAAE,UAAU,CAAC;IACtB,WAAW,EAAE,QAAQ,GAAG,KAAK,GAAG,OAAO,CAAC;IACxC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,YAAY,EAAE,mBAAmB,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C;uEACmE;IACnE,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAED;oEACoE;AACpE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;0BAGsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,MAAM,CAAC,GAAG;QAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;wDAEwD;AACxD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,iBAAiB,CAAC;IACnF,eAAe,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,WAAW,CAAC;IACxD,OAAO,EAAE,mBAAmB,CAAC;CAC9B"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { type RunWorkItemInput, runWorkItem } from "./runner.js";
|
|
2
|
-
export { acceptWorkItem, assertWorkId, type CreateWorkItemInput, cancelWorkItem, createWorkItem, listWorkItems, newWorkId, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, type WorkAttempt, type WorkAttemptTrigger, type WorkEvent, type WorkEventType, type WorkIntent, type WorkNextAction, type WorkProjection, type WorkRecord, type WorkState, } from "./state.js";
|
|
2
|
+
export { acceptWorkItem, assertWorkId, type CreateWorkItemInput, cancelWorkItem, createWorkItem, listWorkItems, newWorkId, openOperatorFindings, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, type WorkAttempt, type WorkAttemptTrigger, type WorkEvent, type WorkEventType, type WorkIntent, type WorkNextAction, type WorkProjection, type WorkRecord, type WorkState, } from "./state.js";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/work/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,mBAAmB,EACxB,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/work/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,mBAAmB,EACxB,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
|
package/dist/core/work/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { runWorkItem } from "./runner.js";
|
|
2
|
-
export { acceptWorkItem, assertWorkId, cancelWorkItem, createWorkItem, listWorkItems, newWorkId, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, } from "./state.js";
|
|
2
|
+
export { acceptWorkItem, assertWorkId, cancelWorkItem, createWorkItem, listWorkItems, newWorkId, openOperatorFindings, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, } from "./state.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/work/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,SAAS,EAGV,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/work/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,SAAS,EAGV,MAAM,sBAAsB,CAAC;AAiB9B,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,IAAI,CACV,UAAU,EACV,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,YAAY,GAAG,aAAa,GAAG,gBAAgB,CACxF,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CA4F7E"}
|
package/dist/core/work/runner.js
CHANGED
|
@@ -2,7 +2,7 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { runWorkflow } from "../workflow/engine.js";
|
|
3
3
|
import { readWorkflowProof } from "../workflow/proof.js";
|
|
4
4
|
import { WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION, WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION, } from "../workflow/types.js";
|
|
5
|
-
import { acquireWorkLease, appendReconcileIfChanged, appendWorkEvent, assertWorkflowUnchanged, boundedActor, newWorkflowRunId, readWorkItemIgnoringLease, } from "./state.js";
|
|
5
|
+
import { acquireWorkLease, appendReconcileIfChanged, appendWorkEvent, assertWorkflowUnchanged, boundedActor, newWorkflowRunId, openOperatorFindings, readWorkItemIgnoringLease, } from "./state.js";
|
|
6
6
|
export async function runWorkItem(input) {
|
|
7
7
|
const coordRoot = resolve(input.coordRoot);
|
|
8
8
|
const release = acquireWorkLease(coordRoot, input.workId);
|
|
@@ -49,11 +49,18 @@ export async function runWorkItem(input) {
|
|
|
49
49
|
}
|
|
50
50
|
prior = priorContext(coordRoot, latest);
|
|
51
51
|
}
|
|
52
|
+
const open = openOperatorFindings(coordRoot, input.workId);
|
|
53
|
+
const findings = open.findings.map((finding) => ({
|
|
54
|
+
id: finding.id,
|
|
55
|
+
actor: open.actor,
|
|
56
|
+
statement: finding.statement,
|
|
57
|
+
}));
|
|
52
58
|
const attemptContext = {
|
|
53
59
|
schema_version: WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION,
|
|
54
60
|
number: attempt,
|
|
55
61
|
trigger,
|
|
56
62
|
...(prior ? { prior } : {}),
|
|
63
|
+
...(findings.length === 0 ? {} : { findings }),
|
|
57
64
|
};
|
|
58
65
|
const runId = newWorkflowRunId();
|
|
59
66
|
appendWorkEvent(coordRoot, input.workId, {
|
|
@@ -37,17 +37,32 @@ export interface WorkEvent {
|
|
|
37
37
|
proof_sha256?: string;
|
|
38
38
|
terminal_attestation_sha256?: string;
|
|
39
39
|
accepted_unknowns?: string[];
|
|
40
|
+
/** Operator corrections raised with a reopen. Authored by a human against a
|
|
41
|
+
* prior attempt, carried into the next attempt, and open until disposed. */
|
|
42
|
+
findings?: WorkOperatorFinding[];
|
|
43
|
+
/** How an operator disposed of each open finding at acceptance. */
|
|
44
|
+
dispositions?: WorkFindingDisposition[];
|
|
40
45
|
state?: WorkState;
|
|
41
46
|
next_action?: WorkNextAction;
|
|
42
47
|
}
|
|
48
|
+
export interface WorkOperatorFinding {
|
|
49
|
+
id: string;
|
|
50
|
+
statement: string;
|
|
51
|
+
}
|
|
52
|
+
export interface WorkFindingDisposition {
|
|
53
|
+
id: string;
|
|
54
|
+
outcome: "fixed" | "deferred";
|
|
55
|
+
reason?: string;
|
|
56
|
+
}
|
|
43
57
|
export interface WorkAttempt {
|
|
44
58
|
number: number;
|
|
45
59
|
run_id: string;
|
|
46
60
|
started_at: string;
|
|
47
61
|
trigger?: WorkAttemptTrigger;
|
|
48
|
-
status: "running" | "parked" | "succeeded" | "failed" | "lost";
|
|
62
|
+
status: "running" | "parked" | "succeeded" | "failed" | "lost" | "journal_unreadable";
|
|
49
63
|
approval_id?: string;
|
|
50
64
|
proof_path?: string;
|
|
65
|
+
journal_error?: string;
|
|
51
66
|
}
|
|
52
67
|
export interface WorkProjection {
|
|
53
68
|
id: string;
|
|
@@ -90,6 +105,7 @@ export declare function acceptWorkItem(coordRoot: string, workId: string, input?
|
|
|
90
105
|
actor?: string;
|
|
91
106
|
reason?: string;
|
|
92
107
|
acceptedUnknowns?: readonly string[];
|
|
108
|
+
dispositions?: readonly WorkFindingDisposition[];
|
|
93
109
|
}): WorkRecord;
|
|
94
110
|
export declare function cancelWorkItem(coordRoot: string, workId: string, input?: {
|
|
95
111
|
actor?: string;
|
|
@@ -98,7 +114,14 @@ export declare function cancelWorkItem(coordRoot: string, workId: string, input?
|
|
|
98
114
|
export declare function reopenWorkItem(coordRoot: string, workId: string, input?: {
|
|
99
115
|
actor?: string;
|
|
100
116
|
reason?: string;
|
|
117
|
+
findings?: readonly string[];
|
|
101
118
|
}): WorkRecord;
|
|
119
|
+
/** Findings raised by the most recent reopen that no later acceptance disposed
|
|
120
|
+
* of. Empty when the work was never reopened with findings. */
|
|
121
|
+
export declare function openOperatorFindings(coordRoot: string, workId: string): {
|
|
122
|
+
actor: string;
|
|
123
|
+
findings: WorkOperatorFinding[];
|
|
124
|
+
};
|
|
102
125
|
export declare function assertWorkId(workId: string): void;
|
|
103
126
|
export declare function newWorkId(): string;
|
|
104
127
|
export declare function newWorkflowRunId(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/work/state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/work/state.ts"],"names":[],"mappings":"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"}
|
package/dist/core/work/state.js
CHANGED
|
@@ -4,6 +4,7 @@ import { hostname } from "node:os";
|
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { readWorkflowApproval } from "../workflow/approvals.js";
|
|
6
6
|
import { stableDigest } from "../workflow/durable-record.js";
|
|
7
|
+
import { WORKFLOW_JOURNAL_EVENT_BYTES, workflowJournalPath } from "../workflow/journal.js";
|
|
7
8
|
import { readWorkflowProof } from "../workflow/proof.js";
|
|
8
9
|
import { readWorkflowRunManifest, workflowScriptDigest } from "../workflow/run-state.js";
|
|
9
10
|
import { isWorkspaceBoundExecutionEvidence } from "../workflow/workspaces/validate.js";
|
|
@@ -142,6 +143,24 @@ export function cancelWorkItem(coordRoot, workId, input = {}) {
|
|
|
142
143
|
export function reopenWorkItem(coordRoot, workId, input = {}) {
|
|
143
144
|
return appendGovernanceEvent(coordRoot, workId, "work.reopened", ["blocked", "in_review", "succeeded", "cancelled"], input);
|
|
144
145
|
}
|
|
146
|
+
/** Findings raised by the most recent reopen that no later acceptance disposed
|
|
147
|
+
* of. Empty when the work was never reopened with findings. */
|
|
148
|
+
export function openOperatorFindings(coordRoot, workId) {
|
|
149
|
+
const events = readWorkEvents(coordRoot, workId);
|
|
150
|
+
let actor = "operator";
|
|
151
|
+
let open = [];
|
|
152
|
+
for (const event of events) {
|
|
153
|
+
if (event.event === "work.reopened" && event.findings?.length) {
|
|
154
|
+
actor = event.actor;
|
|
155
|
+
open = event.findings.map((finding) => ({ ...finding }));
|
|
156
|
+
}
|
|
157
|
+
if (event.event === "work.accepted" && event.dispositions?.length) {
|
|
158
|
+
const disposed = new Set(event.dispositions.map((entry) => entry.id));
|
|
159
|
+
open = open.filter((finding) => !disposed.has(finding.id));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { actor, findings: open };
|
|
163
|
+
}
|
|
145
164
|
export function assertWorkId(workId) {
|
|
146
165
|
if (!WORK_ID.test(workId))
|
|
147
166
|
throw new Error(`invalid work id ${JSON.stringify(workId)}`);
|
|
@@ -248,6 +267,14 @@ function deriveWorkProjection(coordRoot, intent, events, ignoreLease = false) {
|
|
|
248
267
|
};
|
|
249
268
|
}
|
|
250
269
|
const attemptsRemaining = intent.max_attempts - attempts.length;
|
|
270
|
+
if (latest.status === "journal_unreadable") {
|
|
271
|
+
return {
|
|
272
|
+
...base,
|
|
273
|
+
state: "blocked",
|
|
274
|
+
reason: `workflow attempt ${latest.number} journal is unreadable: ${latest.journal_error ?? "unknown journal read error"}`,
|
|
275
|
+
next_action: attemptsRemaining > 0 ? "retry" : "none",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
251
278
|
return {
|
|
252
279
|
...base,
|
|
253
280
|
state: "blocked",
|
|
@@ -309,22 +336,34 @@ function inspectAttempt(coordRoot, event, intent, priorRunId) {
|
|
|
309
336
|
: "failed";
|
|
310
337
|
return attempt;
|
|
311
338
|
}
|
|
312
|
-
const journalPath =
|
|
339
|
+
const journalPath = workflowJournalPath(coordRoot, runId);
|
|
313
340
|
if (existsSync(journalPath)) {
|
|
314
341
|
let parked;
|
|
315
342
|
let resumed = false;
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
parked
|
|
320
|
-
|
|
343
|
+
try {
|
|
344
|
+
for (const line of readBoundedLines(journalPath, MAX_EVENTS_BYTES, WORKFLOW_JOURNAL_EVENT_BYTES, "workflow journal")) {
|
|
345
|
+
const value = parseObject(line, `workflow run ${runId} journal`);
|
|
346
|
+
if (value.event === "run.parked" && typeof value.approval_id === "string") {
|
|
347
|
+
parked = value.approval_id;
|
|
348
|
+
resumed = false;
|
|
349
|
+
}
|
|
350
|
+
if (value.event === "run.resume")
|
|
351
|
+
resumed = true;
|
|
352
|
+
}
|
|
353
|
+
if (parked && !resumed) {
|
|
354
|
+
attempt.status = "parked";
|
|
355
|
+
attempt.approval_id = parked;
|
|
356
|
+
return attempt;
|
|
321
357
|
}
|
|
322
|
-
if (value.event === "run.resume")
|
|
323
|
-
resumed = true;
|
|
324
358
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (workflowResumeLeaseIsLive(coordRoot, runId)) {
|
|
361
|
+
attempt.status = "running";
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
attempt.status = "journal_unreadable";
|
|
365
|
+
attempt.journal_error = boundedJournalError(error.message);
|
|
366
|
+
}
|
|
328
367
|
return attempt;
|
|
329
368
|
}
|
|
330
369
|
}
|
|
@@ -354,11 +393,17 @@ function appendGovernanceEvent(coordRoot, workId, event, allowed, input) {
|
|
|
354
393
|
const acceptance = event === "work.accepted"
|
|
355
394
|
? acceptanceAuthority(coordRoot, current, input.acceptedUnknowns ?? [])
|
|
356
395
|
: undefined;
|
|
396
|
+
const dispositions = event === "work.accepted"
|
|
397
|
+
? findingAuthority(coordRoot, workId, input.dispositions ?? [])
|
|
398
|
+
: undefined;
|
|
399
|
+
const findings = event === "work.reopened" ? normalizeRaisedFindings(input.findings) : undefined;
|
|
357
400
|
appendWorkEventUnderLease(coordRoot, workId, {
|
|
358
401
|
event,
|
|
359
402
|
actor: boundedActor(input.actor),
|
|
360
403
|
reason: boundedOptional(input.reason, "work reason", MAX_REASON),
|
|
361
404
|
...acceptance,
|
|
405
|
+
...(findings ? { findings } : {}),
|
|
406
|
+
...(dispositions ? { dispositions } : {}),
|
|
362
407
|
});
|
|
363
408
|
}
|
|
364
409
|
finally {
|
|
@@ -367,6 +412,51 @@ function appendGovernanceEvent(coordRoot, workId, event, allowed, input) {
|
|
|
367
412
|
}
|
|
368
413
|
return reconcileWorkItem(coordRoot, workId, input.actor);
|
|
369
414
|
}
|
|
415
|
+
/** Acceptance fails closed while an operator finding is open. The operator who
|
|
416
|
+
* raised it decides whether it was addressed; the team cannot mark its own. */
|
|
417
|
+
function findingAuthority(coordRoot, workId, dispositions) {
|
|
418
|
+
const open = openOperatorFindings(coordRoot, workId).findings;
|
|
419
|
+
if (open.length === 0) {
|
|
420
|
+
if (dispositions.length > 0) {
|
|
421
|
+
throw new Error(`work item ${workId} has no open operator finding to dispose`);
|
|
422
|
+
}
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
const byId = new Map(dispositions.map((entry) => [entry.id, entry]));
|
|
426
|
+
const undisposed = open.filter((finding) => !byId.has(finding.id));
|
|
427
|
+
if (undisposed.length > 0) {
|
|
428
|
+
throw new Error(`work item ${workId} cannot be accepted while operator findings are undisposed: ${undisposed
|
|
429
|
+
.map((finding) => finding.id)
|
|
430
|
+
.join(", ")}`);
|
|
431
|
+
}
|
|
432
|
+
const known = new Set(open.map((finding) => finding.id));
|
|
433
|
+
return dispositions.map((entry) => {
|
|
434
|
+
if (!known.has(entry.id)) {
|
|
435
|
+
throw new Error(`work item ${workId} has no open operator finding ${entry.id}`);
|
|
436
|
+
}
|
|
437
|
+
if (entry.outcome !== "fixed" && entry.outcome !== "deferred") {
|
|
438
|
+
throw new Error(`operator finding ${entry.id} disposition must be fixed or deferred`);
|
|
439
|
+
}
|
|
440
|
+
if (entry.outcome === "deferred" && !entry.reason?.trim()) {
|
|
441
|
+
throw new Error(`deferring operator finding ${entry.id} requires a reason`);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
id: entry.id,
|
|
445
|
+
outcome: entry.outcome,
|
|
446
|
+
...(entry.reason ? { reason: boundedString(entry.reason, "disposition reason", 500) } : {}),
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function normalizeRaisedFindings(statements) {
|
|
451
|
+
if (!statements || statements.length === 0)
|
|
452
|
+
return undefined;
|
|
453
|
+
if (statements.length > 20)
|
|
454
|
+
throw new Error("a reopen may raise at most 20 findings");
|
|
455
|
+
return statements.map((statement, index) => ({
|
|
456
|
+
id: `f${index + 1}`,
|
|
457
|
+
statement: boundedString(statement, `finding[${index}] statement`, 1_000),
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
370
460
|
function acceptanceAuthority(coordRoot, current, acceptedUnknowns) {
|
|
371
461
|
const latest = current.projection.attempts.at(-1);
|
|
372
462
|
if (!latest?.proof_path)
|
|
@@ -484,7 +574,7 @@ function readWorkEvents(coordRoot, workId) {
|
|
|
484
574
|
const path = join(workDir(coordRoot, workId), "events.jsonl");
|
|
485
575
|
if (!existsSync(path))
|
|
486
576
|
return [];
|
|
487
|
-
const events = readBoundedLines(path, MAX_EVENTS_BYTES, `work item ${workId} events`).map((line, index) => {
|
|
577
|
+
const events = readBoundedLines(path, MAX_EVENTS_BYTES, MAX_EVENT_BYTES, `work item ${workId} events`).map((line, index) => {
|
|
488
578
|
const event = parseObject(line, `work item ${workId} event ${index + 1}`);
|
|
489
579
|
validateWorkEvent(event, workId, index + 1);
|
|
490
580
|
return event;
|
|
@@ -756,7 +846,7 @@ function writePrivateJson(path, value, maxBytes) {
|
|
|
756
846
|
writeFileSync(path, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
757
847
|
chmodSync(path, 0o600);
|
|
758
848
|
}
|
|
759
|
-
function readBoundedLines(path, maxBytes, field) {
|
|
849
|
+
function readBoundedLines(path, maxBytes, maxRecordBytes, field) {
|
|
760
850
|
const size = statSync(path).size;
|
|
761
851
|
if (size > maxBytes)
|
|
762
852
|
throw new Error(`${field} exceeds ${maxBytes} bytes`);
|
|
@@ -767,11 +857,15 @@ function readBoundedLines(path, maxBytes, field) {
|
|
|
767
857
|
if (lines.length > MAX_EVENTS)
|
|
768
858
|
throw new Error(`${field} exceeds ${MAX_EVENTS} records`);
|
|
769
859
|
for (const line of lines) {
|
|
770
|
-
if (Buffer.byteLength(line) >
|
|
860
|
+
if (Buffer.byteLength(line) > maxRecordBytes)
|
|
771
861
|
throw new Error(`${field} contains an oversized record`);
|
|
772
862
|
}
|
|
773
863
|
return lines;
|
|
774
864
|
}
|
|
865
|
+
function boundedJournalError(message) {
|
|
866
|
+
const normalized = message.trim() || "unknown journal read error";
|
|
867
|
+
return normalized.length > MAX_REASON ? normalized.slice(0, MAX_REASON) : normalized;
|
|
868
|
+
}
|
|
775
869
|
function parseObject(input, field) {
|
|
776
870
|
try {
|
|
777
871
|
const value = JSON.parse(input);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/approvals.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/approvals.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { chmodSync, existsSync, linkSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { appendWorkflowJournalEvent } from "./journal.js";
|
|
4
5
|
export const WORKFLOW_APPROVAL_SCHEMA_VERSION = 1;
|
|
5
6
|
const RECORD_LIMIT_BYTES = 128 * 1024;
|
|
6
7
|
const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
@@ -136,19 +137,15 @@ function appendResolutionJournal(coordRoot, approval) {
|
|
|
136
137
|
const journalPath = join(coordRoot, ".harnery", "workflows", approval.request.run_id, "journal.jsonl");
|
|
137
138
|
if (!existsSync(journalPath) || !approval.decision)
|
|
138
139
|
return;
|
|
139
|
-
|
|
140
|
-
schema_version: 1,
|
|
141
|
-
run_id: approval.request.run_id,
|
|
140
|
+
appendWorkflowJournalEvent(coordRoot, approval.request.run_id, "approval.resolved", {
|
|
142
141
|
ts: approval.decision.decided_at,
|
|
143
|
-
event: "approval.resolved",
|
|
144
142
|
stage: "",
|
|
145
143
|
approval_id: approval.request.id,
|
|
146
144
|
decision_id: approval.request.decision_id,
|
|
147
145
|
verdict: approval.decision.verdict,
|
|
148
146
|
actor: approval.decision.actor,
|
|
149
147
|
reason: approval.decision.reason ?? null,
|
|
150
|
-
};
|
|
151
|
-
writeFileSync(journalPath, `${JSON.stringify(event)}\n`, { encoding: "utf8", flag: "a" });
|
|
148
|
+
});
|
|
152
149
|
}
|
|
153
150
|
function approvalRequestDigest(policySha256, request, evaluation) {
|
|
154
151
|
return createHash("sha256")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attempt-context.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/attempt-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,
|
|
1
|
+
{"version":3,"file":"attempt-context.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/attempt-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,EAKvB,MAAM,YAAY,CAAC;AAyBpB,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAiCtF;AAmCD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC,sBAAsB,CAAC,CAc7F;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,sBAAsB,CAOjG"}
|