harnery 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +4 -0
  4. package/dist/commands/supervisor.d.ts +4 -0
  5. package/dist/commands/supervisor.d.ts.map +1 -0
  6. package/dist/commands/supervisor.js +238 -0
  7. package/dist/commands/work.d.ts +4 -0
  8. package/dist/commands/work.d.ts.map +1 -0
  9. package/dist/commands/work.js +281 -0
  10. package/dist/core/supervisor/index.d.ts +3 -0
  11. package/dist/core/supervisor/index.d.ts.map +1 -0
  12. package/dist/core/supervisor/index.js +2 -0
  13. package/dist/core/supervisor/read.d.ts +2 -0
  14. package/dist/core/supervisor/read.d.ts.map +1 -0
  15. package/dist/core/supervisor/read.js +1 -0
  16. package/dist/core/supervisor/runner.d.ts +34 -0
  17. package/dist/core/supervisor/runner.d.ts.map +1 -0
  18. package/dist/core/supervisor/runner.js +198 -0
  19. package/dist/core/supervisor/state.d.ts +69 -0
  20. package/dist/core/supervisor/state.d.ts.map +1 -0
  21. package/dist/core/supervisor/state.js +413 -0
  22. package/dist/core/work/index.d.ts +3 -0
  23. package/dist/core/work/index.d.ts.map +1 -0
  24. package/dist/core/work/index.js +2 -0
  25. package/dist/core/work/read.d.ts +2 -0
  26. package/dist/core/work/read.d.ts.map +1 -0
  27. package/dist/core/work/read.js +1 -0
  28. package/dist/core/work/runner.d.ts +10 -0
  29. package/dist/core/work/runner.d.ts.map +1 -0
  30. package/dist/core/work/runner.js +63 -0
  31. package/dist/core/work/state.d.ts +109 -0
  32. package/dist/core/work/state.d.ts.map +1 -0
  33. package/dist/core/work/state.js +698 -0
  34. package/dist/core/workflow/engine.d.ts.map +1 -1
  35. package/dist/core/workflow/engine.js +41 -9
  36. package/dist/core/workflow/index.d.ts +1 -1
  37. package/dist/core/workflow/index.d.ts.map +1 -1
  38. package/dist/core/workflow/proof.d.ts +1 -0
  39. package/dist/core/workflow/proof.d.ts.map +1 -1
  40. package/dist/core/workflow/proof.js +2 -0
  41. package/dist/core/workflow/run-state.d.ts +3 -0
  42. package/dist/core/workflow/run-state.d.ts.map +1 -1
  43. package/dist/core/workflow/run-state.js +14 -0
  44. package/dist/core/workflow/specialists.d.ts +7 -0
  45. package/dist/core/workflow/specialists.d.ts.map +1 -0
  46. package/dist/core/workflow/specialists.js +78 -0
  47. package/dist/core/workflow/types.d.ts +24 -0
  48. package/dist/core/workflow/types.d.ts.map +1 -1
  49. package/package.json +21 -1
  50. package/src/commander.ts +4 -0
  51. package/src/commands/supervisor.ts +315 -0
  52. package/src/commands/work.ts +367 -0
  53. package/src/core/supervisor/index.ts +24 -0
  54. package/src/core/supervisor/read.ts +14 -0
  55. package/src/core/supervisor/runner.ts +278 -0
  56. package/src/core/supervisor/state.ts +552 -0
  57. package/src/core/work/index.ts +24 -0
  58. package/src/core/work/read.ts +15 -0
  59. package/src/core/work/runner.ts +81 -0
  60. package/src/core/work/state.ts +900 -0
  61. package/src/core/workflow/engine.ts +42 -9
  62. package/src/core/workflow/index.ts +1 -0
  63. package/src/core/workflow/proof.ts +3 -0
  64. package/src/core/workflow/run-state.ts +20 -0
  65. package/src/core/workflow/specialists.ts +97 -0
  66. package/src/core/workflow/types.ts +25 -0
@@ -35,7 +35,7 @@ import {
35
35
  policyDigest,
36
36
  summarizePolicyRequest,
37
37
  } from "../policy/index.ts";
38
- import { createWorkflowApproval } from "./approvals.ts";
38
+ import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.ts";
39
39
  import { type BillingProbe, probeBilling } from "./billing.ts";
40
40
  import {
41
41
  buildWorkflowProof,
@@ -51,6 +51,7 @@ import {
51
51
  workflowScriptDigest,
52
52
  writeWorkflowRunManifest,
53
53
  } from "./run-state.ts";
54
+ import { normalizeWorkflowSpecialists, resolveSpecialistAssignment } from "./specialists.ts";
54
55
  import type {
55
56
  AgentOpts,
56
57
  EngineOpts,
@@ -101,12 +102,26 @@ export class WorkflowParkedError extends Error {
101
102
  }
102
103
 
103
104
  export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise<RunReport> {
105
+ if (opts.runId && opts.resumeRunId) {
106
+ throw new Error("runId and resumeRunId are mutually exclusive");
107
+ }
108
+ if (opts.runId) assertWorkflowRunId(opts.runId);
109
+ if (opts.workItemId && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(opts.workItemId)) {
110
+ throw new Error(`invalid work item id ${JSON.stringify(opts.workItemId)}`);
111
+ }
104
112
  if (opts.resumeRunId && opts.resumeFrom) {
105
113
  throw new Error("resumeRunId and resumeFrom are mutually exclusive");
106
114
  }
107
115
  const resumeState = opts.resumeRunId
108
116
  ? assertWorkflowRunResumable(opts.coordRoot, opts.resumeRunId)
109
117
  : undefined;
118
+ if (
119
+ resumeState !== undefined &&
120
+ opts.workItemId !== undefined &&
121
+ resumeState.manifest.work_item_id !== opts.workItemId
122
+ ) {
123
+ throw new Error(`workflow run ${opts.resumeRunId} belongs to a different work item`);
124
+ }
110
125
  const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
111
126
  if (resumeState) {
112
127
  if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
@@ -145,6 +160,7 @@ async function executeWorkflow(
145
160
 
146
161
  const runId =
147
162
  opts.resumeRunId ??
163
+ opts.runId ??
148
164
  `wf-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
149
165
  const runDir = join(opts.coordRoot, ".harnery", "workflows", runId);
150
166
  mkdirSync(runDir, { recursive: true });
@@ -160,6 +176,8 @@ async function executeWorkflow(
160
176
  const log = opts.onLog ?? ((line: string) => process.stderr.write(`${line}\n`));
161
177
  const defaultHarness: HarnessName =
162
178
  frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
179
+ const specialists =
180
+ frozen?.specialists ?? (resumeState ? {} : normalizeWorkflowSpecialists(opts.specialists));
163
181
  const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
164
182
  const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
165
183
  const policy =
@@ -178,6 +196,7 @@ async function executeWorkflow(
178
196
  manifest: {
179
197
  schema_version: 1,
180
198
  run_id: runId,
199
+ work_item_id: opts.workItemId,
181
200
  name,
182
201
  started_at: startedAt,
183
202
  script: { path: absScript, sha256: workflowScriptDigest(absScript) },
@@ -194,6 +213,7 @@ async function executeWorkflow(
194
213
  isolation,
195
214
  network_access: networkAccess,
196
215
  policy: policy ? (policy as NormalizedPolicy) : undefined,
216
+ specialists,
197
217
  },
198
218
  },
199
219
  });
@@ -399,7 +419,10 @@ async function executeWorkflow(
399
419
  return decision;
400
420
  };
401
421
 
402
- const agent = async (prompt: string, agentOpts: AgentOpts = {}): Promise<unknown> => {
422
+ const agent = async (prompt: string, requestedOpts: AgentOpts = {}): Promise<unknown> => {
423
+ const assignment = resolveSpecialistAssignment(specialists, prompt, requestedOpts);
424
+ const agentOpts = assignment.opts;
425
+ const assignmentPrompt = assignment.prompt;
403
426
  let reservedForDispatch = 0;
404
427
  let spawnCountClaimed = false;
405
428
  const harness = agentOpts.harness ?? defaultHarness;
@@ -417,6 +440,7 @@ async function executeWorkflow(
417
440
  id,
418
441
  label: proofLabel,
419
442
  stage: currentStage || undefined,
443
+ specialist: agentOpts.specialist,
420
444
  harness,
421
445
  model: agentOpts.model,
422
446
  status: "failed",
@@ -427,7 +451,7 @@ async function executeWorkflow(
427
451
 
428
452
  // Call identity for resume: same stage + harness + model + effort + turns + schema
429
453
  // + ORIGINAL prompt → same key. Retry-mutated prompts never enter the key.
430
- const key = agentCallKey(currentStage, harness, agentOpts, prompt);
454
+ const key = agentCallKey(currentStage, harness, agentOpts, assignmentPrompt);
431
455
  const cached = resumeCache.get(key);
432
456
  if (cached) {
433
457
  // Exact-run replay skips dispatch authorization because no dispatch
@@ -442,6 +466,7 @@ async function executeWorkflow(
442
466
  label,
443
467
  key,
444
468
  harness,
469
+ specialist: agentOpts.specialist ?? null,
445
470
  model: agentOpts.model ?? null,
446
471
  kind: cached.kind,
447
472
  });
@@ -462,7 +487,7 @@ async function executeWorkflow(
462
487
  max_attempts: maxAttempts,
463
488
  max_turns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
464
489
  timeout_ms: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
465
- prompt_bytes: Buffer.byteLength(prompt),
490
+ prompt_bytes: Buffer.byteLength(assignmentPrompt),
466
491
  isolation,
467
492
  network_access: networkAccess,
468
493
  current_cost_usd: round4(costUsd + reservedCostUsd),
@@ -578,12 +603,13 @@ async function executeWorkflow(
578
603
  label,
579
604
  key,
580
605
  harness,
606
+ specialist: agentOpts.specialist ?? null,
581
607
  model: agentOpts.model ?? null,
582
608
  effort: agentOpts.effort ?? null,
583
609
  });
584
610
  log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${harness}] ${label}`);
585
611
 
586
- let attemptPrompt = prompt;
612
+ let attemptPrompt = assignmentPrompt;
587
613
  let last: SpawnResult | null = null;
588
614
  let agentCostUsd = 0;
589
615
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -655,7 +681,7 @@ async function executeWorkflow(
655
681
  // Feed the validation failure back verbatim — the retry prompt carries
656
682
  // exactly what was wrong, which is what makes bounded retry converge.
657
683
  attemptPrompt =
658
- `${prompt}\n\nYour previous reply failed validation:\n` +
684
+ `${assignmentPrompt}\n\nYour previous reply failed validation:\n` +
659
685
  `${problems.map((p) => ` - ${p}`).join("\n")}\n` +
660
686
  `Reply with ONLY the corrected JSON object. No prose, no code fences.`;
661
687
  }
@@ -739,11 +765,13 @@ async function executeWorkflow(
739
765
  } else {
740
766
  journal("run.start", {
741
767
  name,
768
+ work_item_id: opts.workItemId ?? null,
742
769
  script: absScript,
743
770
  objective: meta.objective ?? null,
744
771
  acceptance: meta.acceptance,
745
772
  max_agents: maxAgents,
746
773
  concurrency,
774
+ specialists: Object.keys(specialists),
747
775
  policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
748
776
  isolation,
749
777
  network_access: networkAccess,
@@ -763,6 +791,7 @@ async function executeWorkflow(
763
791
  try {
764
792
  proof = buildWorkflowProof({
765
793
  runId,
794
+ workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
766
795
  meta,
767
796
  status: "succeeded",
768
797
  startedAt,
@@ -795,6 +824,7 @@ async function executeWorkflow(
795
824
  }
796
825
  const report: RunReport = {
797
826
  runId,
827
+ workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
798
828
  name,
799
829
  result,
800
830
  agentsSpawned,
@@ -827,6 +857,7 @@ async function executeWorkflow(
827
857
  try {
828
858
  const proof = buildWorkflowProof({
829
859
  runId,
860
+ workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
830
861
  meta,
831
862
  status: "failed",
832
863
  startedAt,
@@ -869,15 +900,17 @@ function agentCallKey(
869
900
  agentOpts: AgentOpts,
870
901
  prompt: string,
871
902
  ): string {
872
- const basis = JSON.stringify([
903
+ const parts: unknown[] = [
873
904
  stage,
874
905
  harness,
875
906
  agentOpts.model ?? null,
876
907
  agentOpts.effort ?? null,
877
908
  agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
878
909
  agentOpts.schema ?? null,
879
- prompt,
880
- ]);
910
+ ];
911
+ if (agentOpts.specialist) parts.push(agentOpts.specialist);
912
+ parts.push(prompt);
913
+ const basis = JSON.stringify(parts);
881
914
  return createHash("sha256").update(basis).digest("hex").slice(0, 16);
882
915
  }
883
916
 
@@ -70,5 +70,6 @@ export type {
70
70
  WorkflowRepoEvidence,
71
71
  WorkflowRepoSnapshot,
72
72
  WorkflowRunStatus,
73
+ WorkflowSpecialistProfile,
73
74
  } from "./types.ts";
74
75
  export { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
@@ -58,6 +58,7 @@ export interface NormalizedWorkflowMeta {
58
58
 
59
59
  export interface BuildWorkflowProofInput {
60
60
  runId: string;
61
+ workItemId?: string;
61
62
  meta: NormalizedWorkflowMeta;
62
63
  status: "succeeded" | "failed";
63
64
  startedAt: string;
@@ -202,6 +203,7 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
202
203
  const agents = input.agents.map((agent) => ({
203
204
  ...agent,
204
205
  label: clipped(agent.label, MAX_LABEL_CHARS),
206
+ specialist: clippedOptional(agent.specialist, MAX_LABEL_CHARS),
205
207
  model: clippedOptional(agent.model, MAX_LABEL_CHARS),
206
208
  session_id: clippedOptional(agent.session_id, MAX_REF_CHARS),
207
209
  error: clippedOptional(agent.error, MAX_SUMMARY_CHARS),
@@ -213,6 +215,7 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
213
215
  schema_version: WORKFLOW_PROOF_SCHEMA_VERSION,
214
216
  run: {
215
217
  id: input.runId,
218
+ work_item_id: input.workItemId,
216
219
  name: input.meta.name,
217
220
  status: input.status,
218
221
  started_at: input.startedAt,
@@ -22,6 +22,8 @@ import {
22
22
  policyDigest,
23
23
  } from "../policy/index.ts";
24
24
  import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.ts";
25
+ import { normalizeWorkflowSpecialists } from "./specialists.ts";
26
+ import type { WorkflowSpecialistProfile } from "./types.ts";
25
27
 
26
28
  export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 1 as const;
27
29
 
@@ -31,6 +33,7 @@ const FOREIGN_LEASE_STALE_MS = 24 * 60 * 60 * 1_000;
31
33
  export interface WorkflowRunManifest {
32
34
  schema_version: typeof WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION;
33
35
  run_id: string;
36
+ work_item_id?: string;
34
37
  name: string;
35
38
  started_at: string;
36
39
  script: { path: string; sha256: string };
@@ -47,6 +50,7 @@ export interface WorkflowRunManifest {
47
50
  isolation: PolicyIsolation;
48
51
  network_access: PolicyNetworkAccess;
49
52
  policy?: NormalizedPolicy;
53
+ specialists?: Record<string, WorkflowSpecialistProfile>;
50
54
  };
51
55
  }
52
56
 
@@ -97,6 +101,8 @@ export function readWorkflowRunManifest(coordRoot: string, runId: string): Workf
97
101
  if (
98
102
  manifest.schema_version !== WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION ||
99
103
  manifest.run_id !== runId ||
104
+ (manifest.work_item_id !== undefined &&
105
+ !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(manifest.work_item_id)) ||
100
106
  typeof manifest.name !== "string" ||
101
107
  manifest.name.length === 0 ||
102
108
  manifest.name.length > 200 ||
@@ -250,10 +256,24 @@ function validExecution(value: WorkflowRunManifest["execution"]): boolean {
250
256
  value.approval_addressee.length <= 200 &&
251
257
  ["shared", "worktree", "sandbox", "remote"].includes(value.isolation) &&
252
258
  ["enabled", "disabled", "unknown"].includes(value.network_access) &&
259
+ validSpecialists(value.specialists) &&
253
260
  validFrozenPolicy(value.policy, value.cwd)
254
261
  );
255
262
  }
256
263
 
264
+ function validSpecialists(
265
+ specialists: Record<string, WorkflowSpecialistProfile> | undefined,
266
+ ): boolean {
267
+ if (specialists === undefined) return true;
268
+ try {
269
+ return (
270
+ JSON.stringify(normalizeWorkflowSpecialists(specialists)) === JSON.stringify(specialists)
271
+ );
272
+ } catch {
273
+ return false;
274
+ }
275
+ }
276
+
257
277
  function validFrozenPolicy(policy: NormalizedPolicy | undefined, cwd: string): boolean {
258
278
  if (policy === undefined) return true;
259
279
  try {
@@ -0,0 +1,97 @@
1
+ import type { AgentOpts, WorkflowSpecialistProfile } from "./types.ts";
2
+
3
+ const SPECIALIST_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4
+ const MAX_SPECIALISTS = 32;
5
+ const MAX_INSTRUCTIONS = 4_000;
6
+ const MAX_OPTION = 200;
7
+
8
+ export function normalizeWorkflowSpecialists(
9
+ input: Readonly<Record<string, WorkflowSpecialistProfile>> | undefined,
10
+ ): Record<string, WorkflowSpecialistProfile> {
11
+ if (input === undefined) return {};
12
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
13
+ throw new Error("workflow specialists must be an object keyed by role id");
14
+ }
15
+ const entries = Object.entries(input);
16
+ if (entries.length > MAX_SPECIALISTS) {
17
+ throw new Error(`workflow specialists exceed ${MAX_SPECIALISTS} roles`);
18
+ }
19
+ const normalized: Record<string, WorkflowSpecialistProfile> = {};
20
+ for (const [id, profile] of entries.sort(([left], [right]) => left.localeCompare(right))) {
21
+ if (!SPECIALIST_ID.test(id))
22
+ throw new Error(`invalid workflow specialist id ${JSON.stringify(id)}`);
23
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
24
+ throw new Error(`workflow specialist ${id} must be an object`);
25
+ }
26
+ normalized[id] = {
27
+ instructions: bounded(
28
+ profile.instructions,
29
+ `workflow specialist ${id} instructions`,
30
+ MAX_INSTRUCTIONS,
31
+ ),
32
+ harness: optional(profile.harness, `workflow specialist ${id} harness`),
33
+ model: optional(profile.model, `workflow specialist ${id} model`),
34
+ effort: optional(profile.effort, `workflow specialist ${id} effort`),
35
+ maxAttempts: positiveOptional(
36
+ profile.maxAttempts,
37
+ `workflow specialist ${id} maxAttempts`,
38
+ 10,
39
+ ),
40
+ timeoutMs: positiveOptional(
41
+ profile.timeoutMs,
42
+ `workflow specialist ${id} timeoutMs`,
43
+ 24 * 60 * 60 * 1_000,
44
+ ),
45
+ maxTurns: positiveOptional(profile.maxTurns, `workflow specialist ${id} maxTurns`, 1_000),
46
+ };
47
+ }
48
+ return normalized;
49
+ }
50
+
51
+ export function resolveSpecialistAssignment(
52
+ profiles: Readonly<Record<string, WorkflowSpecialistProfile>>,
53
+ prompt: string,
54
+ opts: AgentOpts,
55
+ ): { prompt: string; opts: AgentOpts } {
56
+ if (!opts.specialist) return { prompt, opts };
57
+ if (!SPECIALIST_ID.test(opts.specialist)) {
58
+ throw new Error(`invalid workflow specialist id ${JSON.stringify(opts.specialist)}`);
59
+ }
60
+ const profile = profiles[opts.specialist];
61
+ if (!profile)
62
+ throw new Error(`workflow specialist ${JSON.stringify(opts.specialist)} is not configured`);
63
+ return {
64
+ prompt: `${profile.instructions}\n\nAssignment:\n${prompt}`,
65
+ opts: {
66
+ specialist: opts.specialist,
67
+ harness: opts.harness ?? profile.harness,
68
+ model: opts.model ?? profile.model,
69
+ effort: opts.effort ?? profile.effort,
70
+ maxAttempts: opts.maxAttempts ?? profile.maxAttempts,
71
+ timeoutMs: opts.timeoutMs ?? profile.timeoutMs,
72
+ maxTurns: opts.maxTurns ?? profile.maxTurns,
73
+ label: opts.label,
74
+ schema: opts.schema,
75
+ },
76
+ };
77
+ }
78
+
79
+ function bounded(value: unknown, field: string, max: number): string {
80
+ if (typeof value !== "string") throw new Error(`${field} must be a string`);
81
+ const normalized = value.trim();
82
+ if (!normalized) throw new Error(`${field} must not be empty`);
83
+ if (normalized.length > max) throw new Error(`${field} exceeds ${max} characters`);
84
+ return normalized;
85
+ }
86
+
87
+ function optional(value: unknown, field: string): string | undefined {
88
+ return value === undefined ? undefined : bounded(value, field, MAX_OPTION);
89
+ }
90
+
91
+ function positiveOptional(value: unknown, field: string, max: number): number | undefined {
92
+ if (value === undefined) return undefined;
93
+ if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > max) {
94
+ throw new Error(`${field} must be an integer from 1 to ${max}`);
95
+ }
96
+ return value as number;
97
+ }
@@ -82,6 +82,7 @@ export interface WorkflowAgentProof {
82
82
  id: string;
83
83
  label: string;
84
84
  stage?: string;
85
+ specialist?: string;
85
86
  harness: HarnessName;
86
87
  model?: string;
87
88
  status: "succeeded" | "failed" | "cached";
@@ -147,6 +148,8 @@ export interface WorkflowProof {
147
148
  schema_version: typeof WORKFLOW_PROOF_SCHEMA_VERSION;
148
149
  run: {
149
150
  id: string;
151
+ /** Durable objective this execution attempt belongs to, when linked. */
152
+ work_item_id?: string;
150
153
  name: string;
151
154
  status: WorkflowRunStatus;
152
155
  started_at: string;
@@ -208,6 +211,9 @@ export interface StageSchema {
208
211
  }
209
212
 
210
213
  export interface AgentOpts {
214
+ /** Frozen specialist profile whose instructions and defaults wrap this
215
+ * assignment. The host supplies profiles through EngineOpts.specialists. */
216
+ specialist?: string;
211
217
  /** Stage gate: when present, the agent's reply must strict-parse as JSON and
212
218
  * validate; the engine retries with the validation error appended, up to
213
219
  * `maxAttempts`. Without it, `agent()` resolves to the raw reply text. */
@@ -237,6 +243,18 @@ export interface AgentOpts {
237
243
  * a package-owned union first. */
238
244
  export type HarnessName = string;
239
245
 
246
+ /** Durable role defaults supplied by a goal supervisor or embedding host.
247
+ * Profiles are frozen into a workflow run manifest before the first spawn. */
248
+ export interface WorkflowSpecialistProfile {
249
+ instructions: string;
250
+ harness?: HarnessName;
251
+ model?: string;
252
+ effort?: string;
253
+ maxAttempts?: number;
254
+ timeoutMs?: number;
255
+ maxTurns?: number;
256
+ }
257
+
240
258
  /** What a spawn adapter returns for one subagent run. */
241
259
  export interface SpawnResult {
242
260
  ok: boolean;
@@ -307,6 +325,8 @@ export interface EngineOpts {
307
325
  spawners: Readonly<Record<HarnessName, Spawner | undefined>>;
308
326
  /** Harness used when an agent() call doesn't name one (default "claude-code"). */
309
327
  defaultHarness?: HarnessName;
328
+ /** Named specialist roles available to agent(..., { specialist }). */
329
+ specialists?: Readonly<Record<string, WorkflowSpecialistProfile>>;
310
330
  /** Resume: run id of a prior run whose journal supplies cached results.
311
331
  * agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
312
332
  * completed prior agent return the journaled result without spawning. */
@@ -315,6 +335,10 @@ export interface EngineOpts {
315
335
  * approval has been resolved. The frozen run manifest supplies execution
316
336
  * options and the original repository-before snapshot. */
317
337
  resumeRunId?: string;
338
+ /** Stable id for a new run allocated by a durable-work host. */
339
+ runId?: string;
340
+ /** Durable objective this execution attempt belongs to. */
341
+ workItemId?: string;
318
342
  /** Total-agent ceiling for the run (default 50): the runaway backstop. */
319
343
  maxAgents?: number;
320
344
  /** Concurrent-subagent cap for parallel() (default 4). */
@@ -357,6 +381,7 @@ export interface EngineOpts {
357
381
 
358
382
  export interface RunReport {
359
383
  runId: string;
384
+ workItemId?: string;
360
385
  name: string;
361
386
  /** What the script's default export returned. */
362
387
  result: unknown;