harnery 0.19.1 → 0.21.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/README.md +1 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +21 -1
- package/dist/core/supervisor/index.d.ts +1 -1
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/index.js +1 -1
- package/dist/core/supervisor/plan-api.d.ts +7 -0
- package/dist/core/supervisor/plan-api.d.ts.map +1 -1
- package/dist/core/supervisor/plan-api.js +17 -1
- package/dist/core/supervisor/plan-read.d.ts.map +1 -1
- package/dist/core/supervisor/plan-read.js +4 -0
- package/dist/core/supervisor/plan-types.d.ts +2 -2
- package/dist/core/supervisor/plan-types.d.ts.map +1 -1
- package/dist/core/supervisor/planning.d.ts +7 -0
- package/dist/core/supervisor/planning.d.ts.map +1 -1
- package/dist/core/supervisor/planning.js +38 -0
- package/dist/core/supervisor/state.d.ts +2 -1
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +4 -1
- package/dist/core/work/runner.d.ts +1 -1
- package/dist/core/work/runner.d.ts.map +1 -1
- package/dist/core/work/runner.js +8 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +24 -4
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +29 -3
- package/dist/core/workflow/index.d.ts +2 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/proof.d.ts +2 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +8 -1
- package/dist/core/workflow/run-state.d.ts +2 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +5 -0
- package/dist/core/workflow/types.d.ts +16 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +1 -0
- package/dist/core/workflow/work-context.d.ts +5 -0
- package/dist/core/workflow/work-context.d.ts.map +1 -0
- package/dist/core/workflow/work-context.js +58 -0
- package/package.json +1 -1
- package/src/commands/supervisor.ts +32 -0
- package/src/core/supervisor/index.ts +5 -1
- package/src/core/supervisor/plan-api.ts +27 -1
- package/src/core/supervisor/plan-read.ts +4 -0
- package/src/core/supervisor/plan-types.ts +2 -0
- package/src/core/supervisor/planning.ts +52 -0
- package/src/core/supervisor/state.ts +7 -1
- package/src/core/work/runner.ts +9 -1
- package/src/core/work/state.ts +35 -4
- package/src/core/workflow/engine.ts +33 -2
- package/src/core/workflow/index.ts +5 -1
- package/src/core/workflow/proof.ts +12 -1
- package/src/core/workflow/run-state.ts +7 -1
- package/src/core/workflow/types.ts +17 -0
- package/src/core/workflow/work-context.ts +66 -0
|
@@ -64,9 +64,11 @@ import type {
|
|
|
64
64
|
WorkflowEvidenceRecord,
|
|
65
65
|
WorkflowModule,
|
|
66
66
|
WorkflowProof,
|
|
67
|
+
WorkflowWorkContext,
|
|
67
68
|
} from "./types.ts";
|
|
68
69
|
import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
|
|
69
70
|
import { parseStageOutput, validateAgainstSchema } from "./validate.ts";
|
|
71
|
+
import { freezeWorkflowWorkContext } from "./work-context.ts";
|
|
70
72
|
|
|
71
73
|
const DEFAULT_MAX_AGENTS = 50;
|
|
72
74
|
const DEFAULT_CONCURRENCY = 4;
|
|
@@ -110,6 +112,19 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
110
112
|
if (opts.workItemId && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(opts.workItemId)) {
|
|
111
113
|
throw new Error(`invalid work item id ${JSON.stringify(opts.workItemId)}`);
|
|
112
114
|
}
|
|
115
|
+
if (opts.resumeRunId && opts.workContext !== undefined) {
|
|
116
|
+
throw new Error("parked workflow resume uses the frozen manifest work context");
|
|
117
|
+
}
|
|
118
|
+
const requestedWorkContext =
|
|
119
|
+
opts.workContext === undefined ? undefined : freezeWorkflowWorkContext(opts.workContext);
|
|
120
|
+
if (requestedWorkContext && !opts.workItemId) {
|
|
121
|
+
throw new Error("workflow work context requires a work item id");
|
|
122
|
+
}
|
|
123
|
+
if (requestedWorkContext && requestedWorkContext.id !== opts.workItemId) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`workflow work context ${requestedWorkContext.id} does not match work item ${opts.workItemId}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
113
128
|
if (opts.resumeRunId && opts.resumeFrom) {
|
|
114
129
|
throw new Error("resumeRunId and resumeFrom are mutually exclusive");
|
|
115
130
|
}
|
|
@@ -123,6 +138,9 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
123
138
|
) {
|
|
124
139
|
throw new Error(`workflow run ${opts.resumeRunId} belongs to a different work item`);
|
|
125
140
|
}
|
|
141
|
+
const workContext = resumeState?.manifest.work_context
|
|
142
|
+
? freezeWorkflowWorkContext(resumeState.manifest.work_context)
|
|
143
|
+
: requestedWorkContext;
|
|
126
144
|
const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
|
|
127
145
|
if (resumeState) {
|
|
128
146
|
if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
|
|
@@ -139,7 +157,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
139
157
|
// the run between the optimistic read and lease acquisition.
|
|
140
158
|
assertWorkflowRunResumable(opts.coordRoot, resumeState.manifest.run_id);
|
|
141
159
|
}
|
|
142
|
-
return await executeWorkflow(scriptPath, absScript, opts, resumeState);
|
|
160
|
+
return await executeWorkflow(scriptPath, absScript, opts, resumeState, workContext);
|
|
143
161
|
} finally {
|
|
144
162
|
releaseResumeLease?.();
|
|
145
163
|
}
|
|
@@ -150,6 +168,7 @@ async function executeWorkflow(
|
|
|
150
168
|
absScript: string,
|
|
151
169
|
opts: EngineOpts,
|
|
152
170
|
resumeState: ReturnType<typeof assertWorkflowRunResumable> | undefined,
|
|
171
|
+
workContext: Readonly<WorkflowWorkContext> | undefined,
|
|
153
172
|
): Promise<RunReport> {
|
|
154
173
|
const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
|
|
155
174
|
if (typeof mod.default !== "function") {
|
|
@@ -198,6 +217,7 @@ async function executeWorkflow(
|
|
|
198
217
|
schema_version: 1,
|
|
199
218
|
run_id: runId,
|
|
200
219
|
work_item_id: opts.workItemId,
|
|
220
|
+
work_context: workContext,
|
|
201
221
|
name,
|
|
202
222
|
started_at: startedAt,
|
|
203
223
|
script: { path: absScript, sha256: workflowScriptDigest(absScript) },
|
|
@@ -762,7 +782,15 @@ async function executeWorkflow(
|
|
|
762
782
|
});
|
|
763
783
|
};
|
|
764
784
|
|
|
765
|
-
const ctx: WorkflowContext = {
|
|
785
|
+
const ctx: WorkflowContext = {
|
|
786
|
+
work: workContext,
|
|
787
|
+
agent,
|
|
788
|
+
parallel,
|
|
789
|
+
stage,
|
|
790
|
+
log,
|
|
791
|
+
evidence,
|
|
792
|
+
authorize,
|
|
793
|
+
};
|
|
766
794
|
try {
|
|
767
795
|
if (resumeState) {
|
|
768
796
|
journal("run.resume", {
|
|
@@ -774,6 +802,7 @@ async function executeWorkflow(
|
|
|
774
802
|
journal("run.start", {
|
|
775
803
|
name,
|
|
776
804
|
work_item_id: opts.workItemId ?? null,
|
|
805
|
+
work_context: workContext ?? null,
|
|
777
806
|
script: absScript,
|
|
778
807
|
objective: meta.objective ?? null,
|
|
779
808
|
acceptance: meta.acceptance,
|
|
@@ -800,6 +829,7 @@ async function executeWorkflow(
|
|
|
800
829
|
proof = buildWorkflowProof({
|
|
801
830
|
runId,
|
|
802
831
|
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
832
|
+
workContext,
|
|
803
833
|
meta,
|
|
804
834
|
status: "succeeded",
|
|
805
835
|
startedAt,
|
|
@@ -866,6 +896,7 @@ async function executeWorkflow(
|
|
|
866
896
|
const proof = buildWorkflowProof({
|
|
867
897
|
runId,
|
|
868
898
|
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
899
|
+
workContext,
|
|
869
900
|
meta,
|
|
870
901
|
status: "failed",
|
|
871
902
|
startedAt,
|
|
@@ -71,5 +71,9 @@ export type {
|
|
|
71
71
|
WorkflowRepoSnapshot,
|
|
72
72
|
WorkflowRunStatus,
|
|
73
73
|
WorkflowSpecialistProfile,
|
|
74
|
+
WorkflowWorkContext,
|
|
75
|
+
} from "./types.ts";
|
|
76
|
+
export {
|
|
77
|
+
WORKFLOW_PROOF_SCHEMA_VERSION,
|
|
78
|
+
WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION,
|
|
74
79
|
} from "./types.ts";
|
|
75
|
-
export { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
|
|
@@ -34,8 +34,10 @@ import type {
|
|
|
34
34
|
WorkflowProofUnknown,
|
|
35
35
|
WorkflowRepoEvidence,
|
|
36
36
|
WorkflowRepoSnapshot,
|
|
37
|
+
WorkflowWorkContext,
|
|
37
38
|
} from "./types.ts";
|
|
38
39
|
import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
|
|
40
|
+
import { isCanonicalWorkflowWorkContext } from "./work-context.ts";
|
|
39
41
|
|
|
40
42
|
const MAX_ACCEPTANCE_CRITERIA = 50;
|
|
41
43
|
const MAX_EVIDENCE_RECORDS = 200;
|
|
@@ -59,6 +61,7 @@ export interface NormalizedWorkflowMeta {
|
|
|
59
61
|
export interface BuildWorkflowProofInput {
|
|
60
62
|
runId: string;
|
|
61
63
|
workItemId?: string;
|
|
64
|
+
workContext?: Readonly<WorkflowWorkContext>;
|
|
62
65
|
meta: NormalizedWorkflowMeta;
|
|
63
66
|
status: "succeeded" | "failed";
|
|
64
67
|
startedAt: string;
|
|
@@ -221,6 +224,7 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
|
|
|
221
224
|
started_at: input.startedAt,
|
|
222
225
|
ended_at: input.endedAt,
|
|
223
226
|
duration_ms: input.durationMs,
|
|
227
|
+
work_context: input.workContext,
|
|
224
228
|
objective: input.meta.objective,
|
|
225
229
|
error: clippedOptional(input.error, MAX_SUMMARY_CHARS),
|
|
226
230
|
result: input.result === undefined ? undefined : digestResult(input.result),
|
|
@@ -303,7 +307,14 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
|
|
|
303
307
|
} catch (error) {
|
|
304
308
|
throw new Error(`cannot parse workflow proof at ${path}: ${(error as Error).message}`);
|
|
305
309
|
}
|
|
306
|
-
if (
|
|
310
|
+
if (
|
|
311
|
+
proof.schema_version !== WORKFLOW_PROOF_SCHEMA_VERSION ||
|
|
312
|
+
proof.run?.id !== runId ||
|
|
313
|
+
(proof.run.work_context !== undefined &&
|
|
314
|
+
(!proof.run.work_item_id ||
|
|
315
|
+
proof.run.work_context.id !== proof.run.work_item_id ||
|
|
316
|
+
!isCanonicalWorkflowWorkContext(proof.run.work_context)))
|
|
317
|
+
) {
|
|
307
318
|
throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
|
|
308
319
|
}
|
|
309
320
|
return proof;
|
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
} from "../policy/index.ts";
|
|
24
24
|
import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.ts";
|
|
25
25
|
import { normalizeWorkflowSpecialists } from "./specialists.ts";
|
|
26
|
-
import type { WorkflowSpecialistProfile } from "./types.ts";
|
|
26
|
+
import type { WorkflowSpecialistProfile, WorkflowWorkContext } from "./types.ts";
|
|
27
|
+
import { isCanonicalWorkflowWorkContext } from "./work-context.ts";
|
|
27
28
|
|
|
28
29
|
export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 1 as const;
|
|
29
30
|
|
|
@@ -34,6 +35,7 @@ export interface WorkflowRunManifest {
|
|
|
34
35
|
schema_version: typeof WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION;
|
|
35
36
|
run_id: string;
|
|
36
37
|
work_item_id?: string;
|
|
38
|
+
work_context?: WorkflowWorkContext;
|
|
37
39
|
name: string;
|
|
38
40
|
started_at: string;
|
|
39
41
|
script: { path: string; sha256: string };
|
|
@@ -103,6 +105,10 @@ export function readWorkflowRunManifest(coordRoot: string, runId: string): Workf
|
|
|
103
105
|
manifest.run_id !== runId ||
|
|
104
106
|
(manifest.work_item_id !== undefined &&
|
|
105
107
|
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(manifest.work_item_id)) ||
|
|
108
|
+
(manifest.work_context !== undefined &&
|
|
109
|
+
(!manifest.work_item_id ||
|
|
110
|
+
manifest.work_context.id !== manifest.work_item_id ||
|
|
111
|
+
!isCanonicalWorkflowWorkContext(manifest.work_context))) ||
|
|
106
112
|
typeof manifest.name !== "string" ||
|
|
107
113
|
manifest.name.length === 0 ||
|
|
108
114
|
manifest.name.length > 200 ||
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
import type { BillingMode, BillingProber } from "./billing.ts";
|
|
21
21
|
|
|
22
22
|
export const WORKFLOW_PROOF_SCHEMA_VERSION = 1 as const;
|
|
23
|
+
export const WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION = 1 as const;
|
|
23
24
|
|
|
24
25
|
export type EvidenceKind = "test" | "command" | "artifact" | "change" | "review" | "observation";
|
|
25
26
|
export type EvidenceStatus = "passed" | "failed" | "observed" | "unknown";
|
|
@@ -155,6 +156,7 @@ export interface WorkflowProof {
|
|
|
155
156
|
started_at: string;
|
|
156
157
|
ended_at: string;
|
|
157
158
|
duration_ms: number;
|
|
159
|
+
work_context?: WorkflowWorkContext;
|
|
158
160
|
objective?: string;
|
|
159
161
|
error?: string;
|
|
160
162
|
result?: ResultDigest;
|
|
@@ -299,6 +301,9 @@ export type Spawner = (req: SpawnRequest) => Promise<SpawnResult>;
|
|
|
299
301
|
/** The API surface injected into a workflow script's default export. Explicit
|
|
300
302
|
* injection (no ambient globals): keeps scripts portable and unit-testable. */
|
|
301
303
|
export interface WorkflowContext {
|
|
304
|
+
/** Frozen durable-work assignment for work-linked runs. Standalone and
|
|
305
|
+
* legacy-resumed workflows have no work context. */
|
|
306
|
+
work?: Readonly<WorkflowWorkContext>;
|
|
302
307
|
/** Spawn one subagent; resolves to validated JSON (schema) or reply text. */
|
|
303
308
|
agent: (prompt: string, opts?: AgentOpts) => Promise<unknown>;
|
|
304
309
|
/** Run thunks with bounded concurrency; a rejected thunk resolves to null. */
|
|
@@ -326,6 +331,15 @@ export interface WorkflowModule {
|
|
|
326
331
|
default: (ctx: WorkflowContext) => Promise<unknown>;
|
|
327
332
|
}
|
|
328
333
|
|
|
334
|
+
/** Minimal immutable durable-work input exposed to reusable workflow code. */
|
|
335
|
+
export interface WorkflowWorkContext {
|
|
336
|
+
readonly schema_version: typeof WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION;
|
|
337
|
+
readonly id: string;
|
|
338
|
+
readonly title: string;
|
|
339
|
+
readonly objective: string;
|
|
340
|
+
readonly acceptance: readonly string[];
|
|
341
|
+
}
|
|
342
|
+
|
|
329
343
|
export interface EngineOpts {
|
|
330
344
|
/** Repo root whose .harnery/ receives the run journal. */
|
|
331
345
|
coordRoot: string;
|
|
@@ -348,6 +362,9 @@ export interface EngineOpts {
|
|
|
348
362
|
runId?: string;
|
|
349
363
|
/** Durable objective this execution attempt belongs to. */
|
|
350
364
|
workItemId?: string;
|
|
365
|
+
/** Frozen assignment data supplied by a durable-work host. Requires the
|
|
366
|
+
* matching `workItemId`; parked resume always uses the manifest copy. */
|
|
367
|
+
workContext?: WorkflowWorkContext;
|
|
351
368
|
/** Total-agent ceiling for the run (default 50): the runaway backstop. */
|
|
352
369
|
maxAgents?: number;
|
|
353
370
|
/** Concurrent-subagent cap for parallel() (default 4). */
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { WorkflowWorkContext } from "./types.ts";
|
|
2
|
+
import { WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
const WORK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
5
|
+
const MAX_TITLE = 200;
|
|
6
|
+
const MAX_OBJECTIVE = 4_000;
|
|
7
|
+
const MAX_ACCEPTANCE = 50;
|
|
8
|
+
const MAX_ACCEPTANCE_ITEM = 500;
|
|
9
|
+
const FIELDS = new Set(["schema_version", "id", "title", "objective", "acceptance"]);
|
|
10
|
+
|
|
11
|
+
export function normalizeWorkflowWorkContext(value: unknown): WorkflowWorkContext {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
13
|
+
throw new Error("workflow work context must be an object");
|
|
14
|
+
}
|
|
15
|
+
const input = value as Record<string, unknown>;
|
|
16
|
+
const extra = Object.keys(input).filter((key) => !FIELDS.has(key));
|
|
17
|
+
if (extra.length > 0) {
|
|
18
|
+
throw new Error(`workflow work context has unknown field ${JSON.stringify(extra[0])}`);
|
|
19
|
+
}
|
|
20
|
+
if (input.schema_version !== WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION) {
|
|
21
|
+
throw new Error("workflow work context has an unsupported schema");
|
|
22
|
+
}
|
|
23
|
+
const id = requiredString(input.id, "workflow work context id", 100);
|
|
24
|
+
if (!WORK_ID.test(id)) {
|
|
25
|
+
throw new Error(`invalid workflow work context id ${JSON.stringify(id)}`);
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(input.acceptance) || input.acceptance.length > MAX_ACCEPTANCE) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`workflow work context acceptance must contain at most ${MAX_ACCEPTANCE} criteria`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const acceptance = input.acceptance.map((criterion, index) =>
|
|
33
|
+
requiredString(criterion, `workflow work context acceptance[${index}]`, MAX_ACCEPTANCE_ITEM),
|
|
34
|
+
);
|
|
35
|
+
return {
|
|
36
|
+
schema_version: WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION,
|
|
37
|
+
id,
|
|
38
|
+
title: requiredString(input.title, "workflow work context title", MAX_TITLE),
|
|
39
|
+
objective: requiredString(input.objective, "workflow work context objective", MAX_OBJECTIVE),
|
|
40
|
+
acceptance,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function freezeWorkflowWorkContext(value: unknown): Readonly<WorkflowWorkContext> {
|
|
45
|
+
const normalized = normalizeWorkflowWorkContext(value);
|
|
46
|
+
Object.freeze(normalized.acceptance);
|
|
47
|
+
return Object.freeze(normalized);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isCanonicalWorkflowWorkContext(value: unknown): value is WorkflowWorkContext {
|
|
51
|
+
try {
|
|
52
|
+
const normalized = normalizeWorkflowWorkContext(value);
|
|
53
|
+
return JSON.stringify(normalized) === JSON.stringify(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requiredString(value: unknown, field: string, max: number): string {
|
|
60
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
61
|
+
throw new Error(`${field} is required`);
|
|
62
|
+
}
|
|
63
|
+
const normalized = value.trim();
|
|
64
|
+
if (normalized.length > max) throw new Error(`${field} exceeds ${max} characters`);
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|