harnery 0.8.0 → 0.9.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 (90) 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/doctor.d.ts.map +1 -1
  5. package/dist/commands/doctor.js +38 -0
  6. package/dist/commands/grep.d.ts +35 -2
  7. package/dist/commands/grep.d.ts.map +1 -1
  8. package/dist/commands/grep.js +427 -139
  9. package/dist/commands/init.d.ts +15 -5
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/init.js +125 -14
  12. package/dist/commands/workflow.d.ts +4 -0
  13. package/dist/commands/workflow.d.ts.map +1 -0
  14. package/dist/commands/workflow.js +89 -0
  15. package/dist/core/agents/cli.js +10 -3
  16. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  17. package/dist/core/agents/rules/claim-conflict.js +26 -1
  18. package/dist/core/agents/rules/stop-hook.d.ts +8 -0
  19. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  20. package/dist/core/agents/rules/stop-hook.js +8 -0
  21. package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
  22. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  23. package/dist/core/agents/state/heartbeat-projector.js +13 -2
  24. package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
  25. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  26. package/dist/core/agents/state/heartbeat-writer.js +21 -4
  27. package/dist/core/config.d.ts +9 -0
  28. package/dist/core/config.d.ts.map +1 -1
  29. package/dist/core/config.js +19 -0
  30. package/dist/core/hooks/cli.js +29 -3
  31. package/dist/core/hooks/events/schema.d.ts +4 -0
  32. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  33. package/dist/core/hooks/harness/events.d.ts +7 -0
  34. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  35. package/dist/core/hooks/harness/events.js +1 -0
  36. package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
  37. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
  38. package/dist/core/hooks/resolve/coord-root.js +28 -6
  39. package/dist/core/workflow/billing.d.ts +48 -0
  40. package/dist/core/workflow/billing.d.ts.map +1 -0
  41. package/dist/core/workflow/billing.js +102 -0
  42. package/dist/core/workflow/child-env.d.ts +30 -0
  43. package/dist/core/workflow/child-env.d.ts.map +1 -0
  44. package/dist/core/workflow/child-env.js +43 -0
  45. package/dist/core/workflow/engine.d.ts +21 -0
  46. package/dist/core/workflow/engine.d.ts.map +1 -0
  47. package/dist/core/workflow/engine.js +340 -0
  48. package/dist/core/workflow/harnesses.d.ts +17 -0
  49. package/dist/core/workflow/harnesses.d.ts.map +1 -0
  50. package/dist/core/workflow/harnesses.js +30 -0
  51. package/dist/core/workflow/spawn-claude.d.ts +22 -0
  52. package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
  53. package/dist/core/workflow/spawn-claude.js +82 -0
  54. package/dist/core/workflow/spawn-codex.d.ts +19 -0
  55. package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
  56. package/dist/core/workflow/spawn-codex.js +66 -0
  57. package/dist/core/workflow/spawn-cursor.d.ts +26 -0
  58. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
  59. package/dist/core/workflow/spawn-cursor.js +72 -0
  60. package/dist/core/workflow/types.d.ts +145 -0
  61. package/dist/core/workflow/types.d.ts.map +1 -0
  62. package/dist/core/workflow/types.js +9 -0
  63. package/dist/core/workflow/validate.d.ts +15 -0
  64. package/dist/core/workflow/validate.d.ts.map +1 -0
  65. package/dist/core/workflow/validate.js +70 -0
  66. package/package.json +1 -1
  67. package/src/commander.ts +2 -0
  68. package/src/commands/doctor.ts +45 -0
  69. package/src/commands/grep.ts +535 -142
  70. package/src/commands/init.ts +138 -17
  71. package/src/commands/workflow.ts +132 -0
  72. package/src/core/agents/cli.ts +11 -4
  73. package/src/core/agents/rules/claim-conflict.ts +26 -1
  74. package/src/core/agents/rules/stop-hook.ts +17 -0
  75. package/src/core/agents/state/heartbeat-projector.ts +13 -1
  76. package/src/core/agents/state/heartbeat-writer.ts +30 -5
  77. package/src/core/config.ts +23 -0
  78. package/src/core/hooks/cli.ts +30 -3
  79. package/src/core/hooks/events/schema.ts +4 -0
  80. package/src/core/hooks/harness/events.ts +8 -0
  81. package/src/core/hooks/resolve/coord-root.ts +28 -6
  82. package/src/core/workflow/billing.ts +146 -0
  83. package/src/core/workflow/child-env.ts +47 -0
  84. package/src/core/workflow/engine.ts +394 -0
  85. package/src/core/workflow/harnesses.ts +38 -0
  86. package/src/core/workflow/spawn-claude.ts +99 -0
  87. package/src/core/workflow/spawn-codex.ts +74 -0
  88. package/src/core/workflow/spawn-cursor.ts +89 -0
  89. package/src/core/workflow/types.ts +153 -0
  90. package/src/core/workflow/validate.ts +75 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Workflow engine contracts. A workflow is a small throwaway JS script with
3
+ * bounded, schema-gated stages that fan work out to headless harness-CLI
4
+ * subagents; the SCRIPT (deterministic code), not any model, decides routing
5
+ * between stages, and the run always terminates when the script returns.
6
+ *
7
+ * Design record: decision 0015 (portable coordination-aware workflows).
8
+ */
9
+
10
+ import type { BillingMode, BillingProber } from "./billing.ts";
11
+
12
+ /** JSON-schema *subset* accepted by stage gates (see validate.ts). */
13
+ export interface StageSchema {
14
+ type: "object" | "array" | "string" | "number" | "boolean";
15
+ /** type=object */
16
+ properties?: Record<string, StageSchema>;
17
+ required?: string[];
18
+ /** type=array */
19
+ items?: StageSchema;
20
+ /** any type: closed value set (compared with ===) */
21
+ enum?: Array<string | number | boolean>;
22
+ }
23
+
24
+ export interface AgentOpts {
25
+ /** Stage gate: when present, the agent's reply must strict-parse as JSON and
26
+ * validate; the engine retries with the validation error appended, up to
27
+ * `maxAttempts`. Without it, `agent()` resolves to the raw reply text. */
28
+ schema?: StageSchema;
29
+ /** Model slug passed through to the harness CLI (default: the CLI's default). */
30
+ model?: string;
31
+ /** Attempt ceiling for the schema-retry loop (default 2). */
32
+ maxAttempts?: number;
33
+ /** Subprocess timeout ms (default 300_000). */
34
+ timeoutMs?: number;
35
+ /** Harness-turn ceiling for the child (default 25; use 1 for pure
36
+ * classification stages — cheaper and faster). */
37
+ maxTurns?: number;
38
+ /** Display label in the journal (default: prompt head). */
39
+ label?: string;
40
+ /** Which harness runs this agent (default: the run's default harness).
41
+ * Mixed-harness workflows are legal: triage on one CLI, deep work on
42
+ * another. */
43
+ harness?: HarnessName;
44
+ }
45
+
46
+ export type HarnessName = "claude-code" | "codex" | "cursor";
47
+
48
+ /** What a spawn adapter returns for one subagent run. */
49
+ export interface SpawnResult {
50
+ ok: boolean;
51
+ /** The model's final reply text (envelope-unwrapped). */
52
+ text: string;
53
+ /** Child harness session id when the envelope carries one. */
54
+ sessionId?: string;
55
+ costUsd?: number;
56
+ durationMs: number;
57
+ /** Populated when ok=false. */
58
+ error?: string;
59
+ }
60
+
61
+ export interface SpawnRequest {
62
+ prompt: string;
63
+ model?: string;
64
+ timeoutMs: number;
65
+ maxTurns: number;
66
+ cwd: string;
67
+ /** Run id, stamped into the child env (HARNERY_WORKFLOW_RUN_ID) so the
68
+ * coord layer can associate child sessions with their workflow run. */
69
+ runId?: string;
70
+ /** Scrub all API-key vars from the child env so it can only authenticate
71
+ * via its stored (subscription) login. See billing.ts. */
72
+ subscriptionOnly?: boolean;
73
+ }
74
+
75
+ /** One headless-subagent runner. The engine is adapter-agnostic; claude-code
76
+ * ships first, codex/cursor land behind the same signature (plan Phase 4). */
77
+ export type Spawner = (req: SpawnRequest) => Promise<SpawnResult>;
78
+
79
+ /** The API surface injected into a workflow script's default export. Explicit
80
+ * injection (no ambient globals): keeps scripts portable and unit-testable. */
81
+ export interface WorkflowContext {
82
+ /** Spawn one subagent; resolves to validated JSON (schema) or reply text. */
83
+ agent: (prompt: string, opts?: AgentOpts) => Promise<unknown>;
84
+ /** Run thunks with bounded concurrency; a rejected thunk resolves to null. */
85
+ parallel: <T>(thunks: Array<() => Promise<T>>) => Promise<Array<T | null>>;
86
+ /** Declare the current stage (journal + progress grouping). */
87
+ stage: (title: string) => void;
88
+ /** Narrate progress (stderr + journal). */
89
+ log: (message: string) => void;
90
+ }
91
+
92
+ export interface WorkflowMeta {
93
+ name: string;
94
+ description?: string;
95
+ }
96
+
97
+ /** Loaded script shape: `export const meta` + `export default async (ctx) => …`. */
98
+ export interface WorkflowModule {
99
+ meta?: WorkflowMeta;
100
+ default: (ctx: WorkflowContext) => Promise<unknown>;
101
+ }
102
+
103
+ export interface EngineOpts {
104
+ /** Repo root whose .harnery/ receives the run journal. */
105
+ coordRoot: string;
106
+ /** Spawner registry keyed by harness. A single-harness caller registers one
107
+ * entry and names it in `defaultHarness`. */
108
+ spawners: Partial<Record<HarnessName, Spawner>>;
109
+ /** Harness used when an agent() call doesn't name one (default "claude-code"). */
110
+ defaultHarness?: HarnessName;
111
+ /** Resume: run id of a prior run whose journal supplies cached results.
112
+ * agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
113
+ * completed prior agent return the journaled result without spawning. */
114
+ resumeFrom?: string;
115
+ /** Total-agent ceiling for the run (default 50): the runaway backstop. */
116
+ maxAgents?: number;
117
+ /** Concurrent-subagent cap for parallel() (default 4). */
118
+ concurrency?: number;
119
+ /** Working directory children spawn in (default: coordRoot). */
120
+ cwd?: string;
121
+ /** Progress sink (default: process.stderr). */
122
+ onLog?: (line: string) => void;
123
+ /** Guarantee subscription billing: API-key vars are scrubbed from every
124
+ * child env, and a harness whose stored login is provably absent fails
125
+ * loud before spawning. */
126
+ subscriptionOnly?: boolean;
127
+ /** Permit the api-key-override billing state (an exported API key silently
128
+ * shadowing a stored subscription login), which the engine otherwise
129
+ * refuses. Deliberate key-only hosts don't need this — only the
130
+ * both-present case does. */
131
+ allowApiBilling?: boolean;
132
+ /** Billing-probe override for tests (default: the real probeBilling). */
133
+ probeBilling?: BillingProber;
134
+ }
135
+
136
+ export interface RunReport {
137
+ runId: string;
138
+ name: string;
139
+ /** What the script's default export returned. */
140
+ result: unknown;
141
+ agentsSpawned: number;
142
+ /** agent() calls satisfied from the resumeFrom journal without spawning. */
143
+ agentsCached: number;
144
+ costUsd: number;
145
+ durationMs: number;
146
+ journalPath: string;
147
+ /** Estimated tokens of repo instructions (CLAUDE.md/AGENTS.md at the child
148
+ * cwd) that EVERY child cache-writes on spawn — the fixed per-child context
149
+ * overhead a fan-out multiplies. bytes/4 heuristic; 0 when no such file. */
150
+ contextTokensPerChildEstimate: number;
151
+ /** Billing mode per harness actually used this run (probed on first use). */
152
+ billing: Array<{ harness: HarnessName; mode: BillingMode }>;
153
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Minimal validator for the StageSchema JSON-schema subset. Deliberately tiny:
3
+ * a full JSON Schema implementation would pull a dependency (ajv) for
4
+ * validation depth workflow gates don't need. Supported: type, properties,
5
+ * required, items, enum. Returns a list of human-readable problems (empty =
6
+ * valid) so the engine can feed failures back into the retry prompt verbatim.
7
+ */
8
+
9
+ import type { StageSchema } from "./types.ts";
10
+
11
+ export function validateAgainstSchema(value: unknown, schema: StageSchema, path = "$"): string[] {
12
+ const problems: string[] = [];
13
+
14
+ if (schema.enum) {
15
+ if (!schema.enum.some((v) => v === value)) {
16
+ problems.push(`${path}: expected one of ${JSON.stringify(schema.enum)}, got ${short(value)}`);
17
+ }
18
+ return problems; // enum is exhaustive; type check is implied by membership
19
+ }
20
+
21
+ switch (schema.type) {
22
+ case "object": {
23
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
24
+ return [`${path}: expected object, got ${short(value)}`];
25
+ }
26
+ const obj = value as Record<string, unknown>;
27
+ for (const key of schema.required ?? []) {
28
+ if (!(key in obj)) problems.push(`${path}.${key}: required property missing`);
29
+ }
30
+ for (const [key, sub] of Object.entries(schema.properties ?? {})) {
31
+ if (key in obj) problems.push(...validateAgainstSchema(obj[key], sub, `${path}.${key}`));
32
+ }
33
+ return problems;
34
+ }
35
+ case "array": {
36
+ if (!Array.isArray(value)) return [`${path}: expected array, got ${short(value)}`];
37
+ if (schema.items) {
38
+ for (const [i, item] of value.entries()) {
39
+ problems.push(
40
+ ...validateAgainstSchema(item, schema.items as StageSchema, `${path}[${i}]`),
41
+ );
42
+ }
43
+ }
44
+ return problems;
45
+ }
46
+ case "string":
47
+ case "number":
48
+ case "boolean": {
49
+ if (typeof value !== schema.type) {
50
+ problems.push(`${path}: expected ${schema.type}, got ${short(value)}`);
51
+ }
52
+ return problems;
53
+ }
54
+ default:
55
+ return [`${path}: unsupported schema type ${String((schema as { type?: unknown }).type)}`];
56
+ }
57
+ }
58
+
59
+ /** Strip accidental markdown code fences, then strict-parse JSON. */
60
+ export function parseStageOutput(text: string): { value?: unknown; error?: string } {
61
+ const stripped = text
62
+ .trim()
63
+ .replace(/^```(?:json)?\s*/i, "")
64
+ .replace(/\s*```$/, "");
65
+ try {
66
+ return { value: JSON.parse(stripped) };
67
+ } catch (err) {
68
+ return { error: `not valid JSON: ${(err as Error).message}` };
69
+ }
70
+ }
71
+
72
+ function short(value: unknown): string {
73
+ const s = JSON.stringify(value);
74
+ return s === undefined ? String(value) : s.length > 60 ? `${s.slice(0, 60)}…` : s;
75
+ }