harnery 0.14.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 (45) hide show
  1. package/README.md +1 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +2 -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/core/supervisor/index.d.ts +3 -0
  8. package/dist/core/supervisor/index.d.ts.map +1 -0
  9. package/dist/core/supervisor/index.js +2 -0
  10. package/dist/core/supervisor/read.d.ts +2 -0
  11. package/dist/core/supervisor/read.d.ts.map +1 -0
  12. package/dist/core/supervisor/read.js +1 -0
  13. package/dist/core/supervisor/runner.d.ts +34 -0
  14. package/dist/core/supervisor/runner.d.ts.map +1 -0
  15. package/dist/core/supervisor/runner.js +198 -0
  16. package/dist/core/supervisor/state.d.ts +69 -0
  17. package/dist/core/supervisor/state.d.ts.map +1 -0
  18. package/dist/core/supervisor/state.js +413 -0
  19. package/dist/core/workflow/engine.d.ts.map +1 -1
  20. package/dist/core/workflow/engine.js +21 -8
  21. package/dist/core/workflow/index.d.ts +1 -1
  22. package/dist/core/workflow/index.d.ts.map +1 -1
  23. package/dist/core/workflow/proof.d.ts.map +1 -1
  24. package/dist/core/workflow/proof.js +1 -0
  25. package/dist/core/workflow/run-state.d.ts +2 -0
  26. package/dist/core/workflow/run-state.d.ts.map +1 -1
  27. package/dist/core/workflow/run-state.js +12 -0
  28. package/dist/core/workflow/specialists.d.ts +7 -0
  29. package/dist/core/workflow/specialists.d.ts.map +1 -0
  30. package/dist/core/workflow/specialists.js +78 -0
  31. package/dist/core/workflow/types.d.ts +17 -0
  32. package/dist/core/workflow/types.d.ts.map +1 -1
  33. package/package.json +11 -1
  34. package/src/commander.ts +2 -0
  35. package/src/commands/supervisor.ts +315 -0
  36. package/src/core/supervisor/index.ts +24 -0
  37. package/src/core/supervisor/read.ts +14 -0
  38. package/src/core/supervisor/runner.ts +278 -0
  39. package/src/core/supervisor/state.ts +552 -0
  40. package/src/core/workflow/engine.ts +21 -8
  41. package/src/core/workflow/index.ts +1 -0
  42. package/src/core/workflow/proof.ts +1 -0
  43. package/src/core/workflow/run-state.ts +17 -0
  44. package/src/core/workflow/specialists.ts +97 -0
  45. package/src/core/workflow/types.ts +18 -0
@@ -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
 
@@ -48,6 +50,7 @@ export interface WorkflowRunManifest {
48
50
  isolation: PolicyIsolation;
49
51
  network_access: PolicyNetworkAccess;
50
52
  policy?: NormalizedPolicy;
53
+ specialists?: Record<string, WorkflowSpecialistProfile>;
51
54
  };
52
55
  }
53
56
 
@@ -253,10 +256,24 @@ function validExecution(value: WorkflowRunManifest["execution"]): boolean {
253
256
  value.approval_addressee.length <= 200 &&
254
257
  ["shared", "worktree", "sandbox", "remote"].includes(value.isolation) &&
255
258
  ["enabled", "disabled", "unknown"].includes(value.network_access) &&
259
+ validSpecialists(value.specialists) &&
256
260
  validFrozenPolicy(value.policy, value.cwd)
257
261
  );
258
262
  }
259
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
+
260
277
  function validFrozenPolicy(policy: NormalizedPolicy | undefined, cwd: string): boolean {
261
278
  if (policy === undefined) return true;
262
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";
@@ -210,6 +211,9 @@ export interface StageSchema {
210
211
  }
211
212
 
212
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;
213
217
  /** Stage gate: when present, the agent's reply must strict-parse as JSON and
214
218
  * validate; the engine retries with the validation error appended, up to
215
219
  * `maxAttempts`. Without it, `agent()` resolves to the raw reply text. */
@@ -239,6 +243,18 @@ export interface AgentOpts {
239
243
  * a package-owned union first. */
240
244
  export type HarnessName = string;
241
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
+
242
258
  /** What a spawn adapter returns for one subagent run. */
243
259
  export interface SpawnResult {
244
260
  ok: boolean;
@@ -309,6 +325,8 @@ export interface EngineOpts {
309
325
  spawners: Readonly<Record<HarnessName, Spawner | undefined>>;
310
326
  /** Harness used when an agent() call doesn't name one (default "claude-code"). */
311
327
  defaultHarness?: HarnessName;
328
+ /** Named specialist roles available to agent(..., { specialist }). */
329
+ specialists?: Readonly<Record<string, WorkflowSpecialistProfile>>;
312
330
  /** Resume: run id of a prior run whose journal supplies cached results.
313
331
  * agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
314
332
  * completed prior agent return the journaled result without spawning. */