@oisincoveney/pipeline 1.14.0 → 1.15.1

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 (49) hide show
  1. package/README.md +15 -2
  2. package/dist/config.d.ts +73 -38
  3. package/dist/config.js +92 -31
  4. package/dist/hooks.d.ts +60 -0
  5. package/dist/hooks.js +28 -0
  6. package/dist/index.js +5 -5
  7. package/dist/install-commands.js +2 -5
  8. package/dist/pipeline-init.js +28 -24
  9. package/dist/pipeline-runtime.d.ts +3 -213
  10. package/dist/pipeline-runtime.js +58 -1528
  11. package/dist/runner-job-contract.d.ts +21 -2
  12. package/dist/runner-job-contract.js +16 -0
  13. package/dist/runtime/agent-node/agent-node.js +271 -0
  14. package/dist/runtime/agent-node/index.js +2 -0
  15. package/dist/runtime/builtins/builtins.js +48 -0
  16. package/dist/runtime/builtins/index.js +2 -0
  17. package/dist/runtime/changed-files/changed-files.js +34 -0
  18. package/dist/runtime/changed-files/index.js +2 -0
  19. package/dist/runtime/command-executor/command-executor.js +53 -0
  20. package/dist/runtime/command-executor/index.js +2 -0
  21. package/dist/runtime/context/context.js +93 -0
  22. package/dist/runtime/context/index.js +2 -0
  23. package/dist/runtime/contracts/contracts.d.ts +229 -0
  24. package/dist/runtime/contracts/index.d.ts +1 -0
  25. package/dist/runtime/drain-merge/drain-merge.js +154 -0
  26. package/dist/runtime/drain-merge/index.js +2 -0
  27. package/dist/runtime/events/events.js +259 -0
  28. package/dist/runtime/events/index.js +2 -0
  29. package/dist/runtime/gates/gates.js +296 -0
  30. package/dist/runtime/gates/index.js +2 -0
  31. package/dist/runtime/hooks/hooks.js +261 -0
  32. package/dist/runtime/hooks/index.js +2 -0
  33. package/dist/runtime/json-validation/index.js +2 -0
  34. package/dist/runtime/json-validation/json-validation.js +82 -0
  35. package/dist/runtime/parallel-node/index.js +2 -0
  36. package/dist/runtime/parallel-node/parallel-node.js +105 -0
  37. package/dist/runtime/worktrees/index.js +2 -0
  38. package/dist/runtime/worktrees/worktrees.js +55 -0
  39. package/dist/runtime-machines/contracts.d.ts +1 -2
  40. package/dist/runtime-machines/node-machine.d.ts +1 -0
  41. package/dist/runtime-machines/workflow-machine.d.ts +1 -0
  42. package/dist/runtime-machines/workflow-machine.js +146 -93
  43. package/dist/schedule-planner.d.ts +0 -25
  44. package/dist/schedule-planner.js +95 -11
  45. package/dist/task-ref.js +12 -16
  46. package/dist/workflow-planner.d.ts +0 -1
  47. package/dist/workflow-planner.js +0 -1
  48. package/docs/config-architecture.md +3 -2
  49. package/package.json +5 -1
package/README.md CHANGED
@@ -188,7 +188,10 @@ default_workflow: default
188
188
 
189
189
  orchestrator:
190
190
  profile: orchestrator
191
- hooks: []
191
+
192
+ hooks:
193
+ functions: {}
194
+ on: {}
192
195
 
193
196
  workflows:
194
197
  default:
@@ -353,7 +356,7 @@ runners:
353
356
  - JSON Schema gates validate structure only. Use `verdict` and `acceptance`
354
357
  gates to enforce semantic pass/fail and per-criterion coverage.
355
358
  - Command hooks support host policy controls, sanitized environments, timeouts,
356
- output limits, and JSON payloads on stdin.
359
+ output limits, and JSON file input/result payloads.
357
360
 
358
361
  ## App-Facing API
359
362
 
@@ -377,6 +380,16 @@ import {
377
380
  } from "@oisincoveney/pipeline/runtime";
378
381
  ```
379
382
 
383
+ Hook modules can import the typed helper and result contract:
384
+
385
+ ```ts
386
+ import {
387
+ defineHook,
388
+ type HookContext,
389
+ type HookResult,
390
+ } from "@oisincoveney/pipeline/hooks";
391
+ ```
392
+
380
393
  Runner Job producers can import the shared payload contract:
381
394
 
382
395
  ```ts
package/dist/config.d.ts CHANGED
@@ -6,7 +6,7 @@ declare const RUNNERS_CONFIG_PATH = ".pipeline/runners.yaml";
6
6
  declare const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
7
7
  declare const RUNNER_TYPES: readonly ["codex", "opencode", "command"];
8
8
  declare const NODE_KINDS: readonly ["agent", "command", "builtin", "group", "parallel", "workflow"];
9
- declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "gate.failure"];
9
+ declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "node.finish", "gate.failure"];
10
10
  declare const GATE_KINDS: readonly ["acceptance", "artifact", "builtin", "changed_files", "command", "json_schema", "verdict"];
11
11
  declare const SCHEDULE_BASELINES: readonly ["epic", "pipe"];
12
12
  type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_MISSING" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
@@ -88,7 +88,6 @@ declare const workflowNodeBaseSchema: z.ZodObject<{
88
88
  field: z.ZodOptional<z.ZodString>;
89
89
  kind: z.ZodLiteral<"verdict">;
90
90
  }, z.core.$strict>], "kind">>>;
91
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
92
91
  id: z.ZodString;
93
92
  needs: z.ZodOptional<z.ZodArray<z.ZodString>>;
94
93
  retries: z.ZodOptional<z.ZodObject<{
@@ -146,7 +145,6 @@ declare const workflowSchema: z.ZodObject<{
146
145
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
147
146
  timeout_ms: z.ZodOptional<z.ZodNumber>;
148
147
  }, z.core.$strict>>;
149
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
150
148
  nodes: z.ZodArray<z.ZodType<WorkflowNode, unknown, z.core.$ZodTypeInternals<WorkflowNode, unknown>>>;
151
149
  }, z.core.$strict>;
152
150
  declare const configSchema: z.ZodObject<{
@@ -164,36 +162,75 @@ declare const configSchema: z.ZodObject<{
164
162
  }, z.core.$loose>>;
165
163
  schedule: z.ZodString;
166
164
  }, z.core.$strict>]>>>;
167
- hooks: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
168
- builtin: z.ZodOptional<z.ZodString>;
169
- command: z.ZodOptional<z.ZodArray<z.ZodString>>;
170
- enabled: z.ZodOptional<z.ZodBoolean>;
171
- env: z.ZodOptional<z.ZodObject<{
172
- passthrough: z.ZodOptional<z.ZodArray<z.ZodString>>;
173
- set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
165
+ hooks: z.ZodDefault<z.ZodObject<{
166
+ functions: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
167
+ kind: z.ZodLiteral<"module">;
168
+ module: z.ZodString;
169
+ permissions: z.ZodOptional<z.ZodObject<{
170
+ filesystem: z.ZodOptional<z.ZodEnum<{
171
+ "read-only": "read-only";
172
+ "workspace-write": "workspace-write";
173
+ }>>;
174
+ network: z.ZodOptional<z.ZodEnum<{
175
+ inherit: "inherit";
176
+ disabled: "disabled";
177
+ }>>;
178
+ }, z.core.$strict>>;
179
+ returns: z.ZodOptional<z.ZodObject<{
180
+ schema: z.ZodOptional<z.ZodString>;
181
+ }, z.core.$strict>>;
182
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
183
+ }, z.core.$strict>, z.ZodObject<{
184
+ command: z.ZodArray<z.ZodString>;
185
+ env: z.ZodOptional<z.ZodObject<{
186
+ passthrough: z.ZodOptional<z.ZodArray<z.ZodString>>;
187
+ set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
188
+ }, z.core.$strict>>;
189
+ kind: z.ZodLiteral<"command">;
190
+ output_limit_bytes: z.ZodOptional<z.ZodNumber>;
191
+ protocol: z.ZodDefault<z.ZodObject<{
192
+ input: z.ZodLiteral<"file">;
193
+ result: z.ZodLiteral<"file">;
194
+ }, z.core.$strict>>;
195
+ returns: z.ZodOptional<z.ZodObject<{
196
+ schema: z.ZodOptional<z.ZodString>;
197
+ }, z.core.$strict>>;
198
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
199
+ trusted: z.ZodOptional<z.ZodBoolean>;
200
+ }, z.core.$strict>], "kind">>>;
201
+ on: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
202
+ failure: z.ZodDefault<z.ZodEnum<{
203
+ fail: "fail";
204
+ ignore: "ignore";
205
+ }>>;
206
+ function: z.ZodString;
207
+ id: z.ZodString;
208
+ result: z.ZodOptional<z.ZodObject<{
209
+ pass_to: z.ZodOptional<z.ZodEnum<{
210
+ downstream: "downstream";
211
+ }>>;
212
+ publish: z.ZodOptional<z.ZodBoolean>;
213
+ save_as: z.ZodOptional<z.ZodString>;
214
+ }, z.core.$strict>>;
215
+ where: z.ZodOptional<z.ZodObject<{
216
+ gate: z.ZodOptional<z.ZodString>;
217
+ node: z.ZodOptional<z.ZodString>;
218
+ workflow: z.ZodOptional<z.ZodString>;
219
+ }, z.core.$strict>>;
220
+ with: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
221
+ }, z.core.$strict>>>>;
222
+ policy: z.ZodOptional<z.ZodObject<{
223
+ commands: z.ZodOptional<z.ZodEnum<{
224
+ allow: "allow";
225
+ deny: "deny";
226
+ "trusted-only": "trusted-only";
227
+ }>>;
228
+ modules: z.ZodOptional<z.ZodEnum<{
229
+ allow: "allow";
230
+ deny: "deny";
231
+ }>>;
174
232
  }, z.core.$strict>>;
175
- event: z.ZodEnum<{
176
- "workflow.start": "workflow.start";
177
- "workflow.success": "workflow.success";
178
- "workflow.failure": "workflow.failure";
179
- "workflow.complete": "workflow.complete";
180
- "node.start": "node.start";
181
- "node.success": "node.success";
182
- "node.error": "node.error";
183
- "gate.failure": "gate.failure";
184
- }>;
185
- kind: z.ZodEnum<{
186
- builtin: "builtin";
187
- command: "command";
188
- }>;
189
- output_limit_bytes: z.ZodOptional<z.ZodNumber>;
190
- payload: z.ZodOptional<z.ZodEnum<{
191
- stdin: "stdin";
192
- }>>;
193
- required: z.ZodOptional<z.ZodBoolean>;
194
- timeout_ms: z.ZodOptional<z.ZodNumber>;
195
- trusted: z.ZodOptional<z.ZodBoolean>;
196
- }, z.core.$strict>>>;
233
+ }, z.core.$strict>>;
197
234
  mcp_gateway: z.ZodOptional<z.ZodObject<{
198
235
  default_profile: z.ZodOptional<z.ZodString>;
199
236
  mode: z.ZodEnum<{
@@ -213,7 +250,6 @@ declare const configSchema: z.ZodObject<{
213
250
  url: z.ZodOptional<z.ZodString>;
214
251
  }, z.core.$strict>>>;
215
252
  orchestrator: z.ZodObject<{
216
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
217
253
  profile: z.ZodString;
218
254
  }, z.core.$strict>;
219
255
  profiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -241,10 +277,10 @@ declare const configSchema: z.ZodObject<{
241
277
  }, z.core.$strict>>;
242
278
  output: z.ZodOptional<z.ZodObject<{
243
279
  format: z.ZodEnum<{
280
+ json_schema: "json_schema";
244
281
  text: "text";
245
282
  json: "json";
246
283
  jsonl: "jsonl";
247
- json_schema: "json_schema";
248
284
  }>;
249
285
  repair: z.ZodOptional<z.ZodObject<{
250
286
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -257,7 +293,6 @@ declare const configSchema: z.ZodObject<{
257
293
  runner: z.ZodString;
258
294
  skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
259
295
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
260
- task: "task";
261
296
  read: "read";
262
297
  list: "list";
263
298
  grep: "grep";
@@ -265,6 +300,7 @@ declare const configSchema: z.ZodObject<{
265
300
  bash: "bash";
266
301
  edit: "edit";
267
302
  write: "write";
303
+ task: "task";
268
304
  }>>>;
269
305
  }, z.core.$strict>>>;
270
306
  rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -284,15 +320,14 @@ declare const configSchema: z.ZodObject<{
284
320
  disabled: "disabled";
285
321
  }>>>;
286
322
  output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
323
+ json_schema: "json_schema";
287
324
  text: "text";
288
325
  json: "json";
289
326
  jsonl: "jsonl";
290
- json_schema: "json_schema";
291
327
  }>>>;
292
328
  rules: z.ZodOptional<z.ZodBoolean>;
293
329
  skills: z.ZodOptional<z.ZodBoolean>;
294
330
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
295
- task: "task";
296
331
  read: "read";
297
332
  list: "list";
298
333
  grep: "grep";
@@ -300,6 +335,7 @@ declare const configSchema: z.ZodObject<{
300
335
  bash: "bash";
301
336
  edit: "edit";
302
337
  write: "write";
338
+ task: "task";
303
339
  }>>>;
304
340
  }, z.core.$strict>;
305
341
  command: z.ZodOptional<z.ZodString>;
@@ -334,7 +370,6 @@ declare const configSchema: z.ZodObject<{
334
370
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
335
371
  timeout_ms: z.ZodOptional<z.ZodNumber>;
336
372
  }, z.core.$strict>>;
337
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
338
373
  nodes: z.ZodArray<z.ZodType<WorkflowNode, unknown, z.core.$ZodTypeInternals<WorkflowNode, unknown>>>;
339
374
  }, z.core.$strict>>>;
340
375
  }, z.core.$strict>;
package/dist/config.js CHANGED
@@ -22,6 +22,7 @@ const HOOK_EVENTS = [
22
22
  "node.start",
23
23
  "node.success",
24
24
  "node.error",
25
+ "node.finish",
25
26
  "gate.failure"
26
27
  ];
27
28
  const TOOL_NAMES = [
@@ -229,27 +230,72 @@ const profileSchema = z.object({
229
230
  skills: z.array(z.string()).optional(),
230
231
  tools: z.array(z.enum(TOOL_NAMES)).optional()
231
232
  }).strict();
232
- const orchestratorSchema = z.object({
233
- hooks: z.array(z.string()).optional(),
234
- profile: z.string()
235
- }).strict();
233
+ const orchestratorSchema = z.object({ profile: z.string() }).strict();
236
234
  const hookEnvSchema = z.object({
237
235
  passthrough: z.array(z.string()).optional(),
238
236
  set: z.record(z.string(), z.string()).optional()
239
237
  }).strict();
240
- const hookSchema = z.object({
241
- builtin: z.string().optional(),
242
- command: z.array(z.string()).optional(),
243
- enabled: z.boolean().optional(),
238
+ const hookPermissionsSchema = z.object({
239
+ filesystem: z.enum(FILESYSTEM_MODES).optional(),
240
+ network: z.enum(NETWORK_MODES).optional()
241
+ }).strict();
242
+ const hookReturnsSchema = z.object({ schema: z.string().min(1).optional() }).strict();
243
+ const moduleHookFunctionSchema = z.object({
244
+ kind: z.literal("module"),
245
+ module: z.string().min(1),
246
+ permissions: hookPermissionsSchema.optional(),
247
+ returns: hookReturnsSchema.optional(),
248
+ timeout_ms: z.number().int().positive().optional()
249
+ }).strict();
250
+ const commandHookProtocolSchema = z.object({
251
+ input: z.literal("file"),
252
+ result: z.literal("file")
253
+ }).strict();
254
+ const commandHookFunctionSchema = z.object({
255
+ command: z.array(z.string()).min(1),
244
256
  env: hookEnvSchema.optional(),
245
- event: z.enum(HOOK_EVENTS),
246
- kind: z.enum(["command", "builtin"]),
257
+ kind: z.literal("command"),
247
258
  output_limit_bytes: z.number().int().positive().optional(),
248
- payload: z.enum(["stdin"]).optional(),
249
- required: z.boolean().optional(),
259
+ protocol: commandHookProtocolSchema.default({
260
+ input: "file",
261
+ result: "file"
262
+ }),
263
+ returns: hookReturnsSchema.optional(),
250
264
  timeout_ms: z.number().int().positive().optional(),
251
265
  trusted: z.boolean().optional()
252
266
  }).strict();
267
+ const hookFunctionSchema = z.discriminatedUnion("kind", [moduleHookFunctionSchema, commandHookFunctionSchema]);
268
+ const hookBindingWhereSchema = z.object({
269
+ gate: z.string().optional(),
270
+ node: z.string().optional(),
271
+ workflow: z.string().optional()
272
+ }).strict();
273
+ const hookBindingResultSchema = z.object({
274
+ pass_to: z.enum(["downstream"]).optional(),
275
+ publish: z.boolean().optional(),
276
+ save_as: z.string().min(1).optional()
277
+ }).strict();
278
+ const hookBindingSchema = z.object({
279
+ failure: z.enum(["fail", "ignore"]).default("ignore"),
280
+ function: z.string().min(1),
281
+ id: z.string().min(1),
282
+ result: hookBindingResultSchema.optional(),
283
+ where: hookBindingWhereSchema.optional(),
284
+ with: z.record(z.string(), z.unknown()).optional()
285
+ }).strict();
286
+ const hookPolicySchema = z.object({
287
+ commands: z.enum([
288
+ "allow",
289
+ "trusted-only",
290
+ "deny"
291
+ ]).optional(),
292
+ modules: z.enum(["allow", "deny"]).optional()
293
+ }).strict();
294
+ const hooksConfigSchema = z.object({
295
+ functions: strictRecord(hookFunctionSchema).default({}),
296
+ on: strictRecord(z.array(hookBindingSchema)).default({}),
297
+ policy: hookPolicySchema.optional()
298
+ }).strict();
253
299
  const taskContextResolverSchema = z.object({ type: z.string().min(1) }).passthrough();
254
300
  const nodeTaskContextSchema = z.object({
255
301
  acceptance_criteria: z.array(z.object({
@@ -274,7 +320,6 @@ const schedulePolicySchema = z.object({
274
320
  const workflowNodeBaseSchema = z.object({
275
321
  artifacts: z.array(artifactSchema).optional(),
276
322
  gates: z.array(gateSchema).optional(),
277
- hooks: z.array(z.string()).optional(),
278
323
  id: z.string(),
279
324
  needs: z.array(z.string()).optional(),
280
325
  retries: retriesSchema.optional(),
@@ -311,7 +356,6 @@ const workflowNodeSchema = z.lazy(() => z.discriminatedUnion("kind", [
311
356
  const workflowSchema = z.object({
312
357
  description: z.string().optional(),
313
358
  execution: workflowExecutionSchema.optional(),
314
- hooks: z.array(z.string()).optional(),
315
359
  nodes: z.array(workflowNodeSchema)
316
360
  }).strict();
317
361
  const runnersFileSchema = z.object({
@@ -329,7 +373,10 @@ const profilesFileSchema = z.object({
329
373
  const pipelineFileSchema = z.object({
330
374
  default_workflow: z.string(),
331
375
  entrypoints: strictRecord(entrypointSchema).default({}),
332
- hooks: strictRecord(hookSchema).default({}),
376
+ hooks: hooksConfigSchema.default({
377
+ functions: {},
378
+ on: {}
379
+ }),
333
380
  orchestrator: orchestratorSchema,
334
381
  schedules: strictRecord(schedulePolicySchema).default({}),
335
382
  task_context: taskContextResolverSchema.optional(),
@@ -339,7 +386,10 @@ const pipelineFileSchema = z.object({
339
386
  const configSchema = z.object({
340
387
  default_workflow: z.string(),
341
388
  entrypoints: strictRecord(entrypointSchema).default({}),
342
- hooks: strictRecord(hookSchema).default({}),
389
+ hooks: hooksConfigSchema.default({
390
+ functions: {},
391
+ on: {}
392
+ }),
343
393
  mcp_gateway: mcpGatewaySchema.optional(),
344
394
  mcp_servers: strictRecord(mcpServerSchema).default({}),
345
395
  orchestrator: orchestratorSchema,
@@ -504,11 +554,10 @@ function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
504
554
  validateRegistryIds("rules", config.rules, issues);
505
555
  validateRegistryIds("skills", config.skills, issues);
506
556
  validateRegistryIds("mcp_servers", config.mcp_servers, issues);
507
- validateRegistryIds("hooks", config.hooks, issues);
557
+ validateRegistryIds("hooks.functions", config.hooks.functions, issues);
508
558
  validateRegistryIds("workflows", config.workflows, issues);
509
559
  validateRegistryIds("entrypoints", config.entrypoints, issues);
510
- if (config.profiles[config.orchestrator.profile]) validateReferences("orchestrator.hooks", config.orchestrator.hooks, config.hooks, "hook", issues);
511
- else issues.push({
560
+ if (!config.profiles[config.orchestrator.profile]) issues.push({
512
561
  path: "orchestrator.profile",
513
562
  message: `orchestrator references missing profile '${config.orchestrator.profile}'`
514
563
  });
@@ -523,16 +572,7 @@ function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
523
572
  }
524
573
  validateProfile(profileId, profile, runner, config, issues, projectRoot, options);
525
574
  }
526
- for (const [hookId, hook] of Object.entries(config.hooks)) {
527
- if (hook.kind === "command" && !hook.command) issues.push({
528
- path: `hooks.${hookId}.command`,
529
- message: `command hook '${hookId}' must declare command`
530
- });
531
- if (hook.kind === "builtin" && !hook.builtin) issues.push({
532
- path: `hooks.${hookId}.builtin`,
533
- message: `builtin hook '${hookId}' must declare builtin`
534
- });
535
- }
575
+ validateHookConfig(config, issues, projectRoot, options);
536
576
  for (const [ruleId, rule] of Object.entries(config.rules)) validatePath(`rules.${ruleId}.path`, rule.path, projectRoot, issues, options);
537
577
  for (const [skillId, skill] of Object.entries(config.skills)) validatePath(`skills.${skillId}.path`, skill.path, projectRoot, issues, options);
538
578
  for (const [workflowId, workflow] of Object.entries(config.workflows)) validateWorkflow(workflowId, workflow, config, issues, projectRoot, options);
@@ -545,6 +585,29 @@ function validateRegistryIds(name, registry, issues) {
545
585
  message: `registry id '${id}' must match ${ID_RE.source}`
546
586
  });
547
587
  }
588
+ function validateHookConfig(config, issues, projectRoot, options = {}) {
589
+ const allowedEvents = new Set(HOOK_EVENTS);
590
+ for (const [functionId, hookFunction] of Object.entries(config.hooks.functions)) validatePath(`hooks.functions.${functionId}.returns.schema`, hookFunction.returns?.schema, projectRoot, issues, options);
591
+ for (const [event, bindings] of Object.entries(config.hooks.on)) {
592
+ if (!allowedEvents.has(event)) {
593
+ issues.push({
594
+ path: `hooks.on.${event}`,
595
+ message: `unsupported hook event '${event}'`
596
+ });
597
+ continue;
598
+ }
599
+ for (const [index, binding] of bindings.entries()) {
600
+ if (!ID_RE.test(binding.id)) issues.push({
601
+ path: `hooks.on.${event}.${index}.id`,
602
+ message: `hook binding id '${binding.id}' must match ${ID_RE.source}`
603
+ });
604
+ if (!config.hooks.functions[binding.function]) issues.push({
605
+ path: `hooks.on.${event}.${index}.function`,
606
+ message: `hook binding '${binding.id}' references missing function '${binding.function}'`
607
+ });
608
+ }
609
+ }
610
+ }
548
611
  function validateProfile(profileId, profile, runner, config, issues, projectRoot, options = {}) {
549
612
  validateActor(`profile '${profileId}'`, `profiles.${profileId}`, profile, runner, config, issues, projectRoot, options);
550
613
  validateListCapability(`profiles.${profileId}.output.format`, profile.output?.format ? [profile.output.format] : void 0, runner.capabilities.output_formats, "output format", issues);
@@ -580,7 +643,6 @@ function validateActor(label, path, actor, runner, config, issues, projectRoot,
580
643
  validateListCapability(`${path}.network.mode`, actor.network?.mode ? [actor.network.mode] : void 0, runner.capabilities.network, "network mode", issues);
581
644
  }
582
645
  function validateWorkflow(workflowId, workflow, config, issues, projectRoot, options = {}) {
583
- validateReferences(`workflows.${workflowId}.hooks`, workflow.hooks, config.hooks, "hook", issues);
584
646
  const nodeIds = /* @__PURE__ */ new Set();
585
647
  for (const node of workflow.nodes) {
586
648
  if (nodeIds.has(node.id)) issues.push({
@@ -603,7 +665,6 @@ function validateWorkflowNode(workflowId, node, nodeIds, config, issues) {
603
665
  path: `workflows.${workflowId}.nodes.${node.id}.needs`,
604
666
  message: `node '${node.id}' references missing dependency '${need}'`
605
667
  });
606
- validateReferences(`workflows.${workflowId}.nodes.${node.id}.hooks`, node.hooks, config.hooks, "hook", issues);
607
668
  validateWorkflowNodeKind(workflowId, node, config, issues);
608
669
  if (node.kind === "parallel") validateParallelWorkflowNode(workflowId, node, config, issues);
609
670
  }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/hooks.d.ts
4
+ declare const hookResultSchema: z.ZodObject<{
5
+ artifacts: z.ZodOptional<z.ZodArray<z.ZodObject<{
6
+ contentType: z.ZodOptional<z.ZodString>;
7
+ name: z.ZodString;
8
+ path: z.ZodString;
9
+ }, z.core.$strict>>>;
10
+ outputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
11
+ patch: z.ZodOptional<z.ZodObject<{
12
+ runLabels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
13
+ taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
+ }, z.core.$strict>>;
15
+ status: z.ZodEnum<{
16
+ fail: "fail";
17
+ pass: "pass";
18
+ skip: "skip";
19
+ }>;
20
+ summary: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strict>;
22
+ type HookResult = z.infer<typeof hookResultSchema>;
23
+ interface HookContext {
24
+ event: {
25
+ gateId?: string;
26
+ hookId: string;
27
+ nodeId?: string;
28
+ type: string;
29
+ workflowId: string;
30
+ };
31
+ failure?: {
32
+ evidence: string[];
33
+ gate: string;
34
+ nodeId?: string;
35
+ reason: string;
36
+ };
37
+ input: Record<string, unknown>;
38
+ node?: {
39
+ id: string;
40
+ };
41
+ results: Record<string, HookResult>;
42
+ task: string;
43
+ taskContext?: {
44
+ acceptanceCriteria?: Array<{
45
+ id: string;
46
+ text: string;
47
+ }>;
48
+ description?: string;
49
+ id?: string;
50
+ title?: string;
51
+ };
52
+ workflow: {
53
+ id: string;
54
+ };
55
+ }
56
+ type HookFunction = (context: HookContext) => HookResult | Promise<HookResult>;
57
+ declare function defineHook<T extends HookFunction>(hook: T): T;
58
+ declare function parseHookResult(value: unknown): HookResult;
59
+ //#endregion
60
+ export { HookContext, HookFunction, HookResult, defineHook, hookResultSchema, parseHookResult };
package/dist/hooks.js ADDED
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ //#region src/hooks.ts
3
+ const hookResultSchema = z.object({
4
+ artifacts: z.array(z.object({
5
+ contentType: z.string().optional(),
6
+ name: z.string().min(1),
7
+ path: z.string().min(1)
8
+ }).strict()).optional(),
9
+ outputs: z.record(z.string(), z.unknown()).optional(),
10
+ patch: z.object({
11
+ runLabels: z.record(z.string(), z.string()).optional(),
12
+ taskContext: z.record(z.string(), z.unknown()).optional()
13
+ }).strict().optional(),
14
+ status: z.enum([
15
+ "pass",
16
+ "fail",
17
+ "skip"
18
+ ]),
19
+ summary: z.string().optional()
20
+ }).strict();
21
+ function defineHook(hook) {
22
+ return hook;
23
+ }
24
+ function parseHookResult(value) {
25
+ return hookResultSchema.parse(value);
26
+ }
27
+ //#endregion
28
+ export { defineHook, hookResultSchema, parseHookResult };
package/dist/index.js CHANGED
@@ -110,6 +110,7 @@ function formatAgentProgress(event) {
110
110
  case "agent.finish": return `Agent finished: ${event.nodeId} runner=${event.runnerId ?? "unknown"} exit=${event.exitCode}`;
111
111
  case "hook.start": return `Hook starting: ${event.hookId} event=${event.event}${event.nodeId ? ` node=${event.nodeId}` : ""}`;
112
112
  case "hook.finish": return `Hook ${event.passed ? "passed" : "failed"}: ${event.hookId}${event.reason ? ` (${event.reason})` : ""}`;
113
+ case "hook.result": return `Hook result: ${event.hookId} ${event.status}${event.summary ? ` (${event.summary})` : ""}`;
113
114
  default: return null;
114
115
  }
115
116
  }
@@ -497,7 +498,6 @@ function formatSelectedWorkflowPlan(config, worktreePath, flags) {
497
498
  return formatWorkflowPlan(config, worktreePath, resolveWorkflowSelection(config, flags.workflow, flags.entrypoint));
498
499
  }
499
500
  function formatCompiledWorkflowPlan(config, worktreePath, plan) {
500
- const workflow = config.workflows[plan.workflowId];
501
501
  const lines = [`Workflow: ${plan.workflowId}`];
502
502
  lines.push(formatOrchestratorPlan(config, worktreePath));
503
503
  lines.push(`Batches: ${plan.parallelBatches.map((batch) => `[${batch.map((node) => node.id).join(", ")}]`).join(" -> ")}`);
@@ -505,7 +505,8 @@ function formatCompiledWorkflowPlan(config, worktreePath, plan) {
505
505
  if (node.kind === "parallel" && node.children?.length) lines.push(`${node.id}(parallel: ${node.children.map((child) => child.id).join(", ")})`);
506
506
  lines.push(formatWorkflowPlanNode(node, config, worktreePath));
507
507
  }
508
- if (workflow?.hooks?.length) lines.push(`Workflow hooks: ${workflow.hooks.join(", ")}`);
508
+ const workflowHooks = Object.entries(config.hooks.on).flatMap(([event, bindings]) => bindings.filter((binding) => binding.where?.workflow === plan.workflowId).map((binding) => `${event}:${binding.id}`));
509
+ if (workflowHooks.length > 0) lines.push(`Workflow hooks: ${workflowHooks.join(", ")}`);
509
510
  return lines.join("\n");
510
511
  }
511
512
  function formatWorkflowPlanNode(node, config, worktreePath) {
@@ -522,8 +523,7 @@ function formatWorkflowPlanNode(node, config, worktreePath) {
522
523
  launch ? `runner=${launch.runnerId}` : "",
523
524
  launch ? `strategy=${launch.strategy}` : "",
524
525
  node.gates?.length ? `gates=${node.gates.length}` : "gates=0",
525
- node.artifacts?.length ? `artifacts=${node.artifacts.map((artifact) => artifact.path).join(",")}` : "artifacts=none",
526
- node.hooks?.length ? `hooks=${node.hooks.join(",")}` : ""
526
+ node.artifacts?.length ? `artifacts=${node.artifacts.map((artifact) => artifact.path).join(",")}` : "artifacts=none"
527
527
  ].filter(Boolean).join(" ");
528
528
  }
529
529
  function resolveWorkflowSelection(config, workflowId, entrypointId) {
@@ -548,7 +548,7 @@ function formatOrchestratorPlan(config, worktreePath) {
548
548
  formatList("rules", orchestrator.rules),
549
549
  formatList("skills", orchestrator.skills),
550
550
  formatList("mcp_servers", orchestrator.mcp_servers),
551
- formatList("hooks", config.orchestrator.hooks)
551
+ formatList("hooks", Object.keys(config.hooks.functions))
552
552
  ].filter(Boolean).join(" ");
553
553
  }
554
554
  function formatList(label, items) {
@@ -58,10 +58,7 @@ function nativeProfileEntries(host, config) {
58
58
  function orchestratorProfile(config) {
59
59
  const profile = config.profiles[config.orchestrator.profile];
60
60
  if (!profile) throw new Error(`Orchestrator profile '${config.orchestrator.profile}' is not declared.`);
61
- return {
62
- ...profile,
63
- hooks: config.orchestrator.hooks
64
- };
61
+ return { ...profile };
65
62
  }
66
63
  function resolvedHostModel(config, host, profile) {
67
64
  const runner = config.runners[profile.runner];
@@ -132,7 +129,6 @@ function grants(actor) {
132
129
  `mcp_servers: ${(actor.mcp_servers ?? []).join(", ") || "none"}`,
133
130
  `filesystem: ${actor.filesystem?.mode ?? "default"}`,
134
131
  `network: ${actor.network?.mode ?? "default"}`,
135
- ..."hooks" in actor ? [`hooks: ${(actor.hooks ?? []).join(", ") || "none"}`] : [],
136
132
  ..."output" in actor ? [`output: ${actor.output?.format ?? "text"}`] : []
137
133
  ].join("\n");
138
134
  }
@@ -149,6 +145,7 @@ function orchestratorBlock(config) {
149
145
  return [
150
146
  "Configured orchestrator:",
151
147
  grants(orchestratorProfile(config)),
148
+ `hooks: ${Object.keys(config.hooks.functions).join(", ") || "none"}`,
152
149
  "",
153
150
  instructionsPointer(orchestratorProfile(config))
154
151
  ].join("\n");
@@ -34,31 +34,35 @@ entrypoints:
34
34
 
35
35
  orchestrator:
36
36
  profile: orchestrator
37
- hooks: [generated-defaults-audit]
38
37
 
39
38
  hooks:
40
- generated-defaults-audit:
41
- event: workflow.start
42
- kind: command
43
- command:
44
- - node
45
- - -e
46
- - |
47
- const fs = require("node:fs");
48
- const files = [".pipeline/profiles.yaml"].filter((file) => fs.existsSync(file));
49
- const text = files.map((file) => fs.readFileSync(file, "utf8")).join("\\n").toLowerCase();
50
- const banned = ["atlassian", "jira", "linear", "confluence", "compass", "sentry", "deepwiki"];
51
- const hits = banned.filter((item) => text.includes(item));
52
- const githubUrls = [...text.matchAll(/https:\\/\\/api\\.githubcopilot\\.com\\/mcp[^"'\\s]*/g)].map((match) => match[0]);
53
- const writeGithub = githubUrls.filter((url) => !url.includes("/readonly"));
54
- if (hits.length || writeGithub.length) {
55
- console.error(["Banned generated defaults detected.", hits.length ? "services=" + hits.join(",") : "", writeGithub.length ? "github=" + writeGithub.join(",") : ""].filter(Boolean).join(" "));
56
- process.exit(1);
57
- }
58
- required: true
59
- trusted: true
60
- timeout_ms: 5000
61
- output_limit_bytes: 4096
39
+ functions:
40
+ generated-defaults-audit:
41
+ kind: command
42
+ command:
43
+ - node
44
+ - -e
45
+ - |
46
+ const fs = require("node:fs");
47
+ const files = [".pipeline/profiles.yaml"].filter((file) => fs.existsSync(file));
48
+ const text = files.map((file) => fs.readFileSync(file, "utf8")).join("\\n").toLowerCase();
49
+ const banned = ["atlassian", "jira", "linear", "confluence", "compass", "sentry", "deepwiki"];
50
+ const hits = banned.filter((item) => text.includes(item));
51
+ const githubUrls = [...text.matchAll(/https:\\/\\/api\\.githubcopilot\\.com\\/mcp[^"'\\s]*/g)].map((match) => match[0]);
52
+ const writeGithub = githubUrls.filter((url) => !url.includes("/readonly"));
53
+ if (hits.length || writeGithub.length) {
54
+ console.error(["Banned generated defaults detected.", hits.length ? "services=" + hits.join(",") : "", writeGithub.length ? "github=" + writeGithub.join(",") : ""].filter(Boolean).join(" "));
55
+ process.exit(1);
56
+ }
57
+ fs.writeFileSync(process.env.PIPELINE_HOOK_RESULT, JSON.stringify({ status: "pass", summary: "Generated defaults audit passed" }));
58
+ trusted: true
59
+ timeout_ms: 5000
60
+ output_limit_bytes: 4096
61
+ on:
62
+ workflow.start:
63
+ - id: generated-defaults-audit
64
+ function: generated-defaults-audit
65
+ failure: fail
62
66
 
63
67
  schedules:
64
68
  pipe-schedule:
@@ -564,7 +568,7 @@ const EPIC_TRACK_ITEM_SCHEMA = z.object({
564
568
  title: z.string().optional()
565
569
  });
566
570
  function zodJsonSchema(schema) {
567
- return JSON.stringify(z.toJSONSchema(schema, { target: "draft-07" }), null, 2);
571
+ return JSON.stringify(z.toJSONSchema(schema, { target: "draft-2020-12" }), null, 2);
568
572
  }
569
573
  const RESEARCH_SCHEMA = zodJsonSchema(z.object({
570
574
  ac: STRING_ARRAY_SCHEMA,