@cat-factory/orchestration 0.143.1 → 0.145.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 (72) hide show
  1. package/dist/container/dependencies.d.ts +16 -1
  2. package/dist/container/dependencies.d.ts.map +1 -1
  3. package/dist/container/engine-collaborators.d.ts +1 -0
  4. package/dist/container/engine-collaborators.d.ts.map +1 -1
  5. package/dist/container/engine-collaborators.js +5 -1
  6. package/dist/container/engine-collaborators.js.map +1 -1
  7. package/dist/container/modules.d.ts +10 -1
  8. package/dist/container/modules.d.ts.map +1 -1
  9. package/dist/container/modules.js +26 -0
  10. package/dist/container/modules.js.map +1 -1
  11. package/dist/container/runtime.d.ts +1 -0
  12. package/dist/container/runtime.d.ts.map +1 -1
  13. package/dist/container/runtime.js +2 -1
  14. package/dist/container/runtime.js.map +1 -1
  15. package/dist/container.d.ts +8 -1
  16. package/dist/container.d.ts.map +1 -1
  17. package/dist/container.js +5 -2
  18. package/dist/container.js.map +1 -1
  19. package/dist/index.d.ts +2 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/modules/environments/EnvironmentTestService.d.ts +8 -1
  24. package/dist/modules/environments/EnvironmentTestService.d.ts.map +1 -1
  25. package/dist/modules/environments/EnvironmentTestService.js +9 -0
  26. package/dist/modules/environments/EnvironmentTestService.js.map +1 -1
  27. package/dist/modules/execution/ExecutionService.d.ts +19 -14
  28. package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
  29. package/dist/modules/execution/ExecutionService.js +39 -8
  30. package/dist/modules/execution/ExecutionService.js.map +1 -1
  31. package/dist/modules/execution/ExecutionServiceDependencies.d.ts +10 -1
  32. package/dist/modules/execution/ExecutionServiceDependencies.d.ts.map +1 -1
  33. package/dist/modules/execution/GateHelperDispatcher.d.ts +26 -0
  34. package/dist/modules/execution/GateHelperDispatcher.d.ts.map +1 -0
  35. package/dist/modules/execution/GateHelperDispatcher.js +66 -0
  36. package/dist/modules/execution/GateHelperDispatcher.js.map +1 -0
  37. package/dist/modules/execution/GateStepController.d.ts +39 -0
  38. package/dist/modules/execution/GateStepController.d.ts.map +1 -0
  39. package/dist/modules/execution/GateStepController.js +118 -0
  40. package/dist/modules/execution/GateStepController.js.map +1 -0
  41. package/dist/modules/execution/JudgeService.d.ts +42 -0
  42. package/dist/modules/execution/JudgeService.d.ts.map +1 -0
  43. package/dist/modules/execution/JudgeService.js +78 -0
  44. package/dist/modules/execution/JudgeService.js.map +1 -0
  45. package/dist/modules/execution/JudgeStepController.d.ts +116 -0
  46. package/dist/modules/execution/JudgeStepController.d.ts.map +1 -0
  47. package/dist/modules/execution/JudgeStepController.js +395 -0
  48. package/dist/modules/execution/JudgeStepController.js.map +1 -0
  49. package/dist/modules/execution/RunDispatcher.d.ts +41 -19
  50. package/dist/modules/execution/RunDispatcher.d.ts.map +1 -1
  51. package/dist/modules/execution/RunDispatcher.js +70 -185
  52. package/dist/modules/execution/RunDispatcher.js.map +1 -1
  53. package/dist/modules/execution/dispatcher-registries.d.ts +3 -1
  54. package/dist/modules/execution/dispatcher-registries.d.ts.map +1 -1
  55. package/dist/modules/execution/dispatcher-registries.js +13 -0
  56. package/dist/modules/execution/dispatcher-registries.js.map +1 -1
  57. package/dist/modules/execution/extension-contexts.d.ts +36 -0
  58. package/dist/modules/execution/extension-contexts.d.ts.map +1 -0
  59. package/dist/modules/execution/extension-contexts.js +38 -0
  60. package/dist/modules/execution/extension-contexts.js.map +1 -0
  61. package/dist/modules/execution/policy-types.d.ts +3 -0
  62. package/dist/modules/execution/policy-types.d.ts.map +1 -1
  63. package/dist/modules/execution/prReport.logic.d.ts.map +1 -1
  64. package/dist/modules/execution/prReport.logic.js +69 -0
  65. package/dist/modules/execution/prReport.logic.js.map +1 -1
  66. package/dist/modules/execution/reproductionProof.logic.d.ts.map +1 -1
  67. package/dist/modules/execution/reproductionProof.logic.js +19 -2
  68. package/dist/modules/execution/reproductionProof.logic.js.map +1 -1
  69. package/dist/modules/merge/RiskPolicyService.d.ts.map +1 -1
  70. package/dist/modules/merge/RiskPolicyService.js +6 -0
  71. package/dist/modules/merge/RiskPolicyService.js.map +1 -1
  72. package/package.json +11 -11
@@ -0,0 +1,42 @@
1
+ import type { JudgeAssessor, JudgeSubject, ModelProvider, ModelProviderResolver, ModelRef } from '@cat-factory/kernel';
2
+ import { type ResolveBlockRunContext } from '../../inlineScope.js';
3
+ /** What the inline assessor needs to resolve its model and reach the provider. */
4
+ export interface JudgeServiceDeps {
5
+ /** Resolve a ModelProvider for a workspace's credential scope (preferred). */
6
+ modelProviderResolver?: ModelProviderResolver;
7
+ /** Static provider (e.g. a fake in tests) used when no resolver is set. */
8
+ modelProvider?: ModelProvider;
9
+ /** Routing-default model ref when the block pins none. */
10
+ modelRef?: ModelRef;
11
+ /** Resolve a block's selected model id to a ref (the deployment-aware resolver). */
12
+ resolveBlockModel?: (modelId: string | undefined) => ModelRef | undefined;
13
+ /** Keep an ambient-eligible harness ref inline (local mode) instead of degrading it. */
14
+ runsInline?: (ref: ModelRef) => boolean;
15
+ /** Resolve the workspace's per-agent-kind default model id (block pins none). */
16
+ resolveWorkspaceModelDefault?: (workspaceId: string, agentKind: string, modelPresetId?: string) => Promise<string | undefined>;
17
+ /** Resolve the block's run/execution + initiator, folded into the inline model scope. */
18
+ resolveRunContext?: ResolveBlockRunContext;
19
+ }
20
+ export declare class JudgeService implements JudgeAssessor {
21
+ private readonly deps;
22
+ constructor(deps: JudgeServiceDeps);
23
+ /** Whether an assessment can run (a provider AND a routing default are wired). */
24
+ get enabled(): boolean;
25
+ /**
26
+ * Run ONE assessment against the rubric in force. Returns the raw extracted JSON value (the
27
+ * judge's registered parser owns the shape) plus the model that produced it.
28
+ *
29
+ * Throws {@link ValidationError} on an unresolved model, a failed generation, or a reply
30
+ * with no JSON object in it. The engine catches that and records a FAILING verdict rather
31
+ * than crashing the run — an assessment that could not be read must never be mistaken for a
32
+ * clean one.
33
+ */
34
+ assess(subject: JudgeSubject): Promise<{
35
+ verdict: unknown;
36
+ model: string;
37
+ }>;
38
+ private resolveModel;
39
+ /** Block pin > workspace per-kind default > routing default (subscription refs degrade inline). */
40
+ private modelFor;
41
+ }
42
+ //# sourceMappingURL=JudgeService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JudgeService.d.ts","sourceRoot":"","sources":["../../../src/modules/execution/JudgeService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACT,MAAM,qBAAqB,CAAA;AAa5B,OAAO,EAAE,KAAK,sBAAsB,EAAoB,MAAM,sBAAsB,CAAA;AAoBpF,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,qBAAqB,CAAC,EAAE,qBAAqB,CAAA;IAC7C,2EAA2E;IAC3E,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,KAAK,QAAQ,GAAG,SAAS,CAAA;IACzE,wFAAwF;IACxF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAA;IACvC,iFAAiF;IACjF,4BAA4B,CAAC,EAAE,CAC7B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,MAAM,KACnB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAChC,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,sBAAsB,CAAA;CAC3C;AAED,qBAAa,YAAa,YAAW,aAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,gBAAgB,EAAI;IAEvD,kFAAkF;IAClF,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;;;;;OAQG;IACG,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAmChF;YAEa,YAAY;IAa1B,mGAAmG;YACrF,QAAQ;CAgBvB"}
@@ -0,0 +1,78 @@
1
+ import { generateText } from 'ai';
2
+ import { extractJson, inlineModelRef, resolveScopedModelProvider, ValidationError, } from '@cat-factory/kernel';
3
+ import { catFactoryObservability, JUDGE_AGENT_KIND, JUDGE_SYSTEM_PROMPT, renderJudgePrompt, } from '@cat-factory/agents';
4
+ import { scopeForBlockRun } from '../../inlineScope.js';
5
+ export class JudgeService {
6
+ deps;
7
+ constructor(deps) {
8
+ this.deps = deps;
9
+ }
10
+ /** Whether an assessment can run (a provider AND a routing default are wired). */
11
+ get enabled() {
12
+ return (!!this.deps.modelProviderResolver || !!this.deps.modelProvider) && !!this.deps.modelRef;
13
+ }
14
+ /**
15
+ * Run ONE assessment against the rubric in force. Returns the raw extracted JSON value (the
16
+ * judge's registered parser owns the shape) plus the model that produced it.
17
+ *
18
+ * Throws {@link ValidationError} on an unresolved model, a failed generation, or a reply
19
+ * with no JSON object in it. The engine catches that and records a FAILING verdict rather
20
+ * than crashing the run — an assessment that could not be read must never be mistaken for a
21
+ * clean one.
22
+ */
23
+ async assess(subject) {
24
+ const { modelProvider, ref } = await this.resolveModel(subject.workspaceId, subject.block);
25
+ let text;
26
+ try {
27
+ const result = await generateText({
28
+ model: modelProvider.resolve(ref),
29
+ system: JUDGE_SYSTEM_PROMPT,
30
+ prompt: renderJudgePrompt(subject),
31
+ // Judgement should be reproducible for the same evidence: a rubric verdict that moves
32
+ // between identical runs is not a policy, it's noise.
33
+ temperature: 0,
34
+ maxOutputTokens: 2000,
35
+ providerOptions: catFactoryObservability({
36
+ agentKind: JUDGE_AGENT_KIND,
37
+ workspaceId: subject.workspaceId,
38
+ }),
39
+ });
40
+ text = result.text;
41
+ }
42
+ catch (e) {
43
+ throw new ValidationError(`The judge assessment (${ref.provider}:${ref.model}) failed: ${e instanceof Error ? e.message : String(e)}`);
44
+ }
45
+ const verdict = extractJson(text);
46
+ if (verdict === null || typeof verdict !== 'object') {
47
+ // An empty visible reply means the model answered only into its private reasoning
48
+ // channel (seen on some reasoning models); a non-JSON reply means it ignored the
49
+ // contract. Either way there is nothing to score.
50
+ throw new ValidationError(`The judge assessment (${ref.provider}:${ref.model}) returned no JSON verdict`);
51
+ }
52
+ return { verdict, model: `${ref.provider}:${ref.model}` };
53
+ }
54
+ async resolveModel(workspaceId, block) {
55
+ const scope = await scopeForBlockRun(workspaceId, block, this.deps.resolveRunContext);
56
+ const modelProvider = await resolveScopedModelProvider(scope, this.deps);
57
+ const ref = await this.modelFor(workspaceId, block);
58
+ if (!modelProvider || !ref) {
59
+ throw new ValidationError('No model is configured for the judge assessment');
60
+ }
61
+ return { modelProvider, ref };
62
+ }
63
+ /** Block pin > workspace per-kind default > routing default (subscription refs degrade inline). */
64
+ async modelFor(workspaceId, block) {
65
+ const fallback = this.deps.modelRef;
66
+ const runsInline = this.deps.runsInline;
67
+ const resolve = (ref) => inlineModelRef(ref, fallback ?? ref, runsInline ? { runsInline } : {});
68
+ const fromBlock = this.deps.resolveBlockModel?.(block.modelId);
69
+ if (fromBlock)
70
+ return resolve(fromBlock);
71
+ const defaultId = await this.deps.resolveWorkspaceModelDefault?.(workspaceId, JUDGE_AGENT_KIND, block.modelPresetId);
72
+ const fromDefault = this.deps.resolveBlockModel?.(defaultId);
73
+ if (fromDefault)
74
+ return resolve(fromDefault);
75
+ return fallback;
76
+ }
77
+ }
78
+ //# sourceMappingURL=JudgeService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JudgeService.js","sourceRoot":"","sources":["../../../src/modules/execution/JudgeService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import type { AgentRunResult, Block, Clock, ExecutionInstance, ExecutionRepository, JudgeAssessor, JudgeContext, JudgeDefinition, JudgeRegistry, JudgeStepState, PipelineStep, ResolveJudgeInput, RaiseNotificationInput, RiskPolicy, RunInitiatorScope, WorkRunner } from '@cat-factory/kernel';
2
+ import type { AdvanceResult } from './advance.js';
3
+ import type { FragmentBodyResolver } from './AgentContextBuilder.js';
4
+ import type { RunStateMachine } from './RunStateMachine.js';
5
+ import type { StepGraph } from './StepGraph.js';
6
+ /** What the judge driver needs beyond the shared run state-machine spine. */
7
+ export interface JudgeStepControllerDeps {
8
+ /** The app-owned registry the composition root injected (deployment-registered judges). */
9
+ judgeRegistry: JudgeRegistry;
10
+ /**
11
+ * The verdict producer. Absent / `enabled === false` ⇒ every judge step is a pass-through.
12
+ * The engine's default is the inline `JudgeService`; conformance injects a deterministic fake.
13
+ */
14
+ judgeAssessor?: JudgeAssessor;
15
+ executionRepository: ExecutionRepository;
16
+ /** The async instance/block spine (park/advance/persist/emit/notify/fail). */
17
+ stateMachine: RunStateMachine;
18
+ /** The pure step mutators (start/finish/reset/rewind a step). */
19
+ stepGraph: StepGraph;
20
+ workRunner: WorkRunner;
21
+ clock: Clock;
22
+ runInitiatorScope: RunInitiatorScope;
23
+ /** Raise a human-actionable card (the `judge_review` park notification). */
24
+ raiseNotification: (workspaceId: string, input: RaiseNotificationInput) => Promise<void>;
25
+ /** The task's resolved merge preset — the source of the threshold + bounce budget. */
26
+ resolveRiskPolicy: (workspaceId: string, block: Block) => Promise<Pick<RiskPolicy, 'judgeMinScore' | 'judgeMaxBounces'>>;
27
+ /**
28
+ * The prompt-fragment library, used for ONE thing: resolving a rubric's per-workspace
29
+ * override body. Absent (no library configured) ⇒ the registration's default rubric, which
30
+ * is exactly the behaviour a stock deployment gets.
31
+ */
32
+ fragmentResolver?: FragmentBodyResolver;
33
+ /** The engine's completion spine, so a passing judge finishes + advances like any step. */
34
+ recordStepResult: (workspaceId: string, instance: ExecutionInstance, step: PipelineStep, isFinalStep: boolean, result: AgentRunResult) => Promise<AdvanceResult>;
35
+ /** The judge context handed to each registered factory at registry-build time. */
36
+ makeJudgeContext: () => JudgeContext;
37
+ }
38
+ export declare class JudgeStepController {
39
+ private readonly deps;
40
+ private registryCache?;
41
+ constructor(deps: JudgeStepControllerDeps);
42
+ /**
43
+ * The registered judge for an agent kind, or undefined when the kind is not a judge. Built
44
+ * lazily (the factories close over the engine seams) and cached per instance — so, like the
45
+ * gate registry, a judge registered AFTER the first build is invisible to this instance:
46
+ * register at startup, before serving.
47
+ */
48
+ judgeFor(agentKind: string): JudgeDefinition | undefined;
49
+ /** Every registered judge, for the workspace-snapshot palette projection. */
50
+ all(): JudgeDefinition[];
51
+ /**
52
+ * The run's "active" judge state: prefer the step the run is currently on, else the latest
53
+ * step that carries judge state (a pipeline may place more than one judge). Mirrors
54
+ * `ForkDecisionController.getActive`; null when the run carries none.
55
+ */
56
+ getActive(workspaceId: string, executionId: string): Promise<JudgeStepState | null>;
57
+ /**
58
+ * The PASS-THROUGH branch: no assessor wired (tests / no model configured) or the judge's own
59
+ * prerequisite is unwired. Recorded as `skipped` WITH a note — a judge that quietly did nothing
60
+ * must not read like one that passed — then finished like any other step.
61
+ */
62
+ private passThrough;
63
+ /**
64
+ * Initialise `step.judge` on first entry. The threshold + bounce budget come from the task's
65
+ * merge preset ONCE and stay stable across bounce rounds — a preset edited mid-run must not
66
+ * move the bar the earlier rounds were judged against.
67
+ */
68
+ private initState;
69
+ /**
70
+ * Evaluate a judge step once: assess, dispose, and act. Shared by the fresh advance and the
71
+ * re-entry after a bounce (the producer re-ran and the work is ready to be re-judged).
72
+ */
73
+ evaluate(workspaceId: string, instance: ExecutionInstance, step: PipelineStep, block: Block, isFinalStep: boolean, judge: JudgeDefinition): Promise<AdvanceResult>;
74
+ /**
75
+ * Run the assessment and parse it with the registration's parser. A THROWN assessment (an
76
+ * unresolved model, a provider outage, an unparseable reply) becomes a score-0 verdict rather
77
+ * than an exception: the judge's whole job is to gate, so "I could not tell" must land on the
78
+ * cautious side of the threshold and reach a human, not abort the run with a stack trace or —
79
+ * worse — let the work through.
80
+ */
81
+ private assess;
82
+ /**
83
+ * Re-arm the producing step with the verdict's findings as rework feedback and re-run it
84
+ * through this judge. Uses `rerunProducerThrough` rather than a hand-rolled cursor rewind
85
+ * because that is what clears the stale container job handles on the intermediate steps (a
86
+ * re-dispatch would otherwise re-attach to an evicted job).
87
+ */
88
+ private bounce;
89
+ /** Park on a human decision and raise the `judge_review` card that makes it discoverable. */
90
+ private park;
91
+ /**
92
+ * Resolve a parked judge — the SINGLE entry point behind both the SPA controller and the
93
+ * public API's decisions surface, so the park's CAS/approval-id arbitration and the task's
94
+ * preset knobs apply identically whichever surface answers first.
95
+ *
96
+ * - `proceed` — advance past the judge despite the verdict (the human overrules the rubric);
97
+ * - `bounce` — spend a rework round on the producing step, even past the preset budget
98
+ * (the human is explicitly buying it);
99
+ * - `stop` — fail the run with the verdict.
100
+ */
101
+ resolveDecision(workspaceId: string, executionId: string, input: ResolveJudgeInput): Promise<JudgeStepState>;
102
+ /**
103
+ * The index of the step a bounce re-arms: the nearest preceding step whose kind the judge
104
+ * declares it grades, else the immediately preceding step. `-1` when the judge is the first
105
+ * step — which degrades a `bounce` to a `park` rather than advancing silently.
106
+ */
107
+ private bounceTargetIndex;
108
+ /**
109
+ * The rubric body in force: a workspace's prompt-library fragment override when one resolves
110
+ * for {@link JudgeRubric.fragmentId}, else the registration's default. Best-effort — a
111
+ * library outage must not turn a rubric gate into a hard failure, so it degrades to the
112
+ * default (which is a real rubric, not an empty one).
113
+ */
114
+ private resolveRubric;
115
+ }
116
+ //# sourceMappingURL=JudgeStepController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JudgeStepController.d.ts","sourceRoot":"","sources":["../../../src/modules/execution/JudgeStepController.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,395 @@
1
+ import { ConflictError, disposeJudgeVerdict, renderJudgeRework } from '@cat-factory/kernel';
2
+ import { parseJudgeVerdict } from '@cat-factory/contracts';
3
+ export class JudgeStepController {
4
+ deps;
5
+ registryCache;
6
+ constructor(deps) {
7
+ this.deps = deps;
8
+ }
9
+ /**
10
+ * The registered judge for an agent kind, or undefined when the kind is not a judge. Built
11
+ * lazily (the factories close over the engine seams) and cached per instance — so, like the
12
+ * gate registry, a judge registered AFTER the first build is invisible to this instance:
13
+ * register at startup, before serving.
14
+ */
15
+ judgeFor(agentKind) {
16
+ if (!this.registryCache) {
17
+ const map = new Map();
18
+ const ctx = this.deps.makeJudgeContext();
19
+ for (const { kind, factory } of this.deps.judgeRegistry.factories()) {
20
+ map.set(kind, factory(ctx));
21
+ }
22
+ this.registryCache = map;
23
+ }
24
+ return this.registryCache.get(agentKind);
25
+ }
26
+ /** Every registered judge, for the workspace-snapshot palette projection. */
27
+ all() {
28
+ return this.deps.judgeRegistry.factories().map(({ kind }) => this.judgeFor(kind));
29
+ }
30
+ /**
31
+ * The run's "active" judge state: prefer the step the run is currently on, else the latest
32
+ * step that carries judge state (a pipeline may place more than one judge). Mirrors
33
+ * `ForkDecisionController.getActive`; null when the run carries none.
34
+ */
35
+ async getActive(workspaceId, executionId) {
36
+ const instance = await this.deps.executionRepository.get(workspaceId, executionId);
37
+ if (!instance)
38
+ return null;
39
+ const current = instance.steps[instance.currentStep];
40
+ if (current?.judge)
41
+ return current.judge;
42
+ for (let i = instance.steps.length - 1; i >= 0; i--) {
43
+ const judge = instance.steps[i]?.judge;
44
+ if (judge)
45
+ return judge;
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * The PASS-THROUGH branch: no assessor wired (tests / no model configured) or the judge's own
51
+ * prerequisite is unwired. Recorded as `skipped` WITH a note — a judge that quietly did nothing
52
+ * must not read like one that passed — then finished like any other step.
53
+ */
54
+ passThrough(workspaceId, instance, step, isFinalStep, judge, noAssessor) {
55
+ const note = noAssessor
56
+ ? `The "${judge.rubric.name}" review was skipped (no assessment model is configured).`
57
+ : (judge.unwiredOutput ??
58
+ `The "${judge.rubric.name}" review was skipped (its provider is not configured).`);
59
+ step.judge = {
60
+ ...step.judge,
61
+ status: 'skipped',
62
+ rubricId: judge.rubric.id,
63
+ rubricName: judge.rubric.name,
64
+ rubricOverridden: step.judge?.rubricOverridden ?? false,
65
+ bounces: step.judge?.bounces ?? 0,
66
+ maxBounces: step.judge?.maxBounces ?? 0,
67
+ rounds: step.judge?.rounds ?? [],
68
+ note,
69
+ };
70
+ return this.deps.recordStepResult(workspaceId, instance, step, isFinalStep, { output: note });
71
+ }
72
+ /**
73
+ * Initialise `step.judge` on first entry. The threshold + bounce budget come from the task's
74
+ * merge preset ONCE and stay stable across bounce rounds — a preset edited mid-run must not
75
+ * move the bar the earlier rounds were judged against.
76
+ */
77
+ async initState(workspaceId, step, block, judge) {
78
+ if (step.judge) {
79
+ step.judge = { ...step.judge, status: 'evaluating' };
80
+ return step.judge;
81
+ }
82
+ const preset = await this.deps.resolveRiskPolicy(workspaceId, block);
83
+ step.judge = {
84
+ status: 'evaluating',
85
+ rubricId: judge.rubric.id,
86
+ rubricName: judge.rubric.name,
87
+ rubricOverridden: false,
88
+ threshold: judge.threshold ? judge.threshold(preset) : preset.judgeMinScore,
89
+ bounces: 0,
90
+ maxBounces: judge.attemptBudget ? judge.attemptBudget(preset) : preset.judgeMaxBounces,
91
+ rounds: [],
92
+ };
93
+ return step.judge;
94
+ }
95
+ /**
96
+ * Evaluate a judge step once: assess, dispose, and act. Shared by the fresh advance and the
97
+ * re-entry after a bounce (the producer re-ran and the work is ready to be re-judged).
98
+ */
99
+ async evaluate(workspaceId, instance, step, block, isFinalStep, judge) {
100
+ const assessor = this.deps.judgeAssessor;
101
+ if (!assessor?.enabled || judge.wired?.() === false) {
102
+ return this.passThrough(workspaceId, instance, step, isFinalStep, judge, !assessor?.enabled);
103
+ }
104
+ // The live state, kept as a local so the rest of the method reads it without re-narrowing
105
+ // `step.judge` (which stays the persisted source of truth and is re-assigned below).
106
+ let judgeState = await this.initState(workspaceId, step, block, judge);
107
+ const { body: rubric, overridden } = await this.resolveRubric(workspaceId, judge);
108
+ judgeState.rubricOverridden = overridden;
109
+ await this.deps.stateMachine.emitInstance(workspaceId, instance);
110
+ const stepIndex = instance.steps.indexOf(step);
111
+ const { verdict, model } = await this.assess(assessor, {
112
+ workspaceId,
113
+ block,
114
+ step,
115
+ rubric,
116
+ rubricName: judge.rubric.name,
117
+ priorOutputs: priorOutputsFor(instance, stepIndex),
118
+ previousFindings: judgeState.verdict ?? null,
119
+ }, judge, instance.initiatedBy ?? null);
120
+ const producerIndex = this.bounceTargetIndex(instance, stepIndex, judge);
121
+ const decision = disposeJudgeVerdict({
122
+ verdict,
123
+ threshold: judgeState.threshold ?? 0,
124
+ onFail: judge.onFail,
125
+ bounces: judgeState.bounces ?? 0,
126
+ maxBounces: judgeState.maxBounces ?? 0,
127
+ hasBounceTarget: producerIndex >= 0,
128
+ });
129
+ judgeState = {
130
+ ...judgeState,
131
+ verdict,
132
+ model,
133
+ disposition: decision.disposition,
134
+ note: decision.note ?? null,
135
+ rounds: [
136
+ ...(judgeState.rounds ?? []),
137
+ {
138
+ round: (judgeState.rounds?.length ?? 0) + 1,
139
+ at: this.deps.clock.now(),
140
+ verdict,
141
+ disposition: decision.disposition,
142
+ model,
143
+ },
144
+ ],
145
+ };
146
+ step.judge = judgeState;
147
+ switch (decision.disposition) {
148
+ case 'pass':
149
+ judgeState.status = 'passed';
150
+ return this.deps.recordStepResult(workspaceId, instance, step, isFinalStep, {
151
+ output: renderPassOutput(judge, verdict, judgeState.threshold ?? 0),
152
+ });
153
+ case 'fail':
154
+ judgeState.status = 'failed';
155
+ await this.deps.stateMachine.casPersist(workspaceId, instance);
156
+ await this.deps.stateMachine.emitInstance(workspaceId, instance);
157
+ return {
158
+ kind: 'job_failed',
159
+ error: `The "${judge.rubric.name}" review failed this work (${verdict.score.toFixed(2)} < ${(judgeState.threshold ?? 0).toFixed(2)}). ${verdict.summary}`.trim(),
160
+ };
161
+ case 'bounce':
162
+ return this.bounce(workspaceId, instance, {
163
+ step,
164
+ judge,
165
+ verdict,
166
+ producerIndex,
167
+ stepIndex,
168
+ });
169
+ default:
170
+ return this.park(workspaceId, instance, step, block, judge, verdict);
171
+ }
172
+ }
173
+ /**
174
+ * Run the assessment and parse it with the registration's parser. A THROWN assessment (an
175
+ * unresolved model, a provider outage, an unparseable reply) becomes a score-0 verdict rather
176
+ * than an exception: the judge's whole job is to gate, so "I could not tell" must land on the
177
+ * cautious side of the threshold and reach a human, not abort the run with a stack trace or —
178
+ * worse — let the work through.
179
+ */
180
+ async assess(assessor, subject, judge, initiatedBy) {
181
+ const parse = judge.parseVerdict ?? parseJudgeVerdict;
182
+ try {
183
+ const raw = await this.deps.runInitiatorScope(initiatedBy, () => assessor.assess(subject));
184
+ return { verdict: parse(raw.verdict), model: raw.model };
185
+ }
186
+ catch (e) {
187
+ const reason = e instanceof Error ? e.message : String(e);
188
+ return {
189
+ verdict: {
190
+ score: 0,
191
+ summary: `The "${judge.rubric.name}" assessment could not be completed: ${reason}`,
192
+ findings: [
193
+ {
194
+ title: 'The rubric assessment did not produce a readable verdict',
195
+ detail: reason,
196
+ severity: 'high',
197
+ },
198
+ ],
199
+ },
200
+ model: null,
201
+ };
202
+ }
203
+ }
204
+ /**
205
+ * Re-arm the producing step with the verdict's findings as rework feedback and re-run it
206
+ * through this judge. Uses `rerunProducerThrough` rather than a hand-rolled cursor rewind
207
+ * because that is what clears the stale container job handles on the intermediate steps (a
208
+ * re-dispatch would otherwise re-attach to an evicted job).
209
+ */
210
+ async bounce(workspaceId, instance, args) {
211
+ const { step, judge, verdict, producerIndex, stepIndex, extraFeedback } = args;
212
+ const judgeState = {
213
+ ...step.judge,
214
+ status: 'bouncing',
215
+ bounces: (step.judge?.bounces ?? 0) + 1,
216
+ };
217
+ this.deps.stepGraph.rerunProducerThrough(instance, producerIndex, stepIndex, {
218
+ previousProposal: instance.steps[producerIndex]?.output ?? '',
219
+ feedback: renderJudgeRework(verdict, judge.rubric.name, extraFeedback),
220
+ });
221
+ // `rerunProducerThrough` resets the judge step too; `judge` survives `resetStepForRerun`
222
+ // by design, but re-assign explicitly so the incremented bounce count is what persists.
223
+ step.judge = judgeState;
224
+ if (instance.status === 'blocked')
225
+ instance.status = 'running';
226
+ await this.deps.stateMachine.casPersist(workspaceId, instance);
227
+ await this.deps.stateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
228
+ await this.deps.stateMachine.emitInstance(workspaceId, instance);
229
+ return { kind: 'continue' };
230
+ }
231
+ /** Park on a human decision and raise the `judge_review` card that makes it discoverable. */
232
+ async park(workspaceId, instance, step, block, judge, verdict) {
233
+ step.judge = { ...step.judge, status: 'awaiting_decision' };
234
+ const threshold = step.judge.threshold ?? 0;
235
+ await this.deps.raiseNotification(workspaceId, {
236
+ type: 'judge_review',
237
+ blockId: block.id,
238
+ executionId: instance.id,
239
+ title: `"${judge.rubric.name}" review needs a decision for "${block.title}"`,
240
+ body: `The review scored this work ${verdict.score.toFixed(2)} against a threshold of ` +
241
+ `${threshold.toFixed(2)}. ${verdict.summary} ` +
242
+ `Proceed anyway, send it back for rework, or stop the run.`.trim(),
243
+ payload: {
244
+ pipelineName: instance.pipelineName,
245
+ ...(block.pullRequest?.url ? { prUrl: block.pullRequest.url } : {}),
246
+ },
247
+ });
248
+ return this.deps.stateMachine.parkStepOnDecision(workspaceId, instance, step);
249
+ }
250
+ /**
251
+ * Resolve a parked judge — the SINGLE entry point behind both the SPA controller and the
252
+ * public API's decisions surface, so the park's CAS/approval-id arbitration and the task's
253
+ * preset knobs apply identically whichever surface answers first.
254
+ *
255
+ * - `proceed` — advance past the judge despite the verdict (the human overrules the rubric);
256
+ * - `bounce` — spend a rework round on the producing step, even past the preset budget
257
+ * (the human is explicitly buying it);
258
+ * - `stop` — fail the run with the verdict.
259
+ */
260
+ async resolveDecision(workspaceId, executionId, input) {
261
+ const feedback = input.feedback?.trim() || undefined;
262
+ let approvalId = '';
263
+ let stepIndex = -1;
264
+ let bounceTo = -1;
265
+ let state;
266
+ let failure;
267
+ const instance = await this.deps.stateMachine.mutateInstance(workspaceId, executionId, (inst) => {
268
+ const index = inst.steps.findIndex((s) => s.state === 'waiting_decision' &&
269
+ s.approval?.status === 'pending' &&
270
+ s.judge?.status === 'awaiting_decision');
271
+ const step = index >= 0 ? inst.steps[index] : undefined;
272
+ if (!step?.approval || !step.judge) {
273
+ throw new ConflictError('The run is no longer awaiting a review decision');
274
+ }
275
+ const judge = this.judgeFor(step.agentKind);
276
+ if (!judge) {
277
+ throw new ConflictError(`No judge is registered for step kind '${step.agentKind}'`);
278
+ }
279
+ approvalId = step.approval.id;
280
+ stepIndex = index;
281
+ const resolution = {
282
+ choice: input.choice,
283
+ at: this.deps.clock.now(),
284
+ ...(feedback ? { feedback } : {}),
285
+ };
286
+ if (input.choice === 'bounce') {
287
+ bounceTo = this.bounceTargetIndex(inst, index, judge);
288
+ if (bounceTo < 0) {
289
+ throw new ConflictError('There is no preceding step to send this work back to');
290
+ }
291
+ // Recorded here so the re-armed run carries the human's intent; the actual rewind
292
+ // happens on the winning snapshot below (it mutates several steps).
293
+ step.judge = { ...step.judge, resolution, disposition: 'bounce' };
294
+ return;
295
+ }
296
+ if (input.choice === 'stop') {
297
+ step.judge = { ...step.judge, status: 'failed', resolution, disposition: 'fail' };
298
+ failure =
299
+ `The "${judge.rubric.name}" review was stopped by a human. ${step.judge.verdict?.summary ?? ''}`.trim();
300
+ state = step.judge;
301
+ return;
302
+ }
303
+ // `proceed`: the human overrules the rubric — stamp the judge passed and advance the
304
+ // run past it in the SAME mutation, exactly like an approved gate.
305
+ step.judge = {
306
+ ...step.judge,
307
+ status: 'passed',
308
+ resolution,
309
+ disposition: 'pass',
310
+ note: 'A human chose to proceed despite the verdict.',
311
+ };
312
+ step.output = `The "${judge.rubric.name}" review was overruled by a human; the run proceeded.`;
313
+ state = step.judge;
314
+ this.deps.stateMachine.advanceRunPastGate(inst, index);
315
+ });
316
+ if (input.choice === 'proceed') {
317
+ await this.deps.stateMachine.settleAdvancedGate(workspaceId, instance, stepIndex);
318
+ return state;
319
+ }
320
+ if (input.choice === 'stop') {
321
+ await this.deps.stateMachine.clearWaitingNotification(workspaceId, instance);
322
+ await this.deps.workRunner.signalDecision(workspaceId, instance.id, approvalId, 'rejected');
323
+ await this.deps.stateMachine.failRun(workspaceId, instance.id, failure, 'agent');
324
+ return state;
325
+ }
326
+ // `bounce`: re-arm the producer with the verdict + the human's extra guidance, then wake
327
+ // the driver. A human-bought round is NOT charged against the preset budget — they already
328
+ // decided; charging it would let the next automatic round be silently refused.
329
+ const step = instance.steps[stepIndex];
330
+ const judge = this.judgeFor(step.agentKind);
331
+ const verdict = step.judge?.verdict ?? { score: 0, summary: '', findings: [] };
332
+ this.deps.stepGraph.rerunProducerThrough(instance, bounceTo, stepIndex, {
333
+ previousProposal: instance.steps[bounceTo]?.output ?? '',
334
+ feedback: renderJudgeRework(verdict, judge.rubric.name, feedback),
335
+ });
336
+ step.judge = { ...step.judge, status: 'bouncing' };
337
+ if (instance.status === 'blocked')
338
+ instance.status = 'running';
339
+ await this.deps.stateMachine.casPersist(workspaceId, instance);
340
+ await this.deps.stateMachine.clearWaitingNotification(workspaceId, instance);
341
+ await this.deps.stateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
342
+ await this.deps.stateMachine.emitInstance(workspaceId, instance);
343
+ await this.deps.workRunner.signalDecision(workspaceId, instance.id, approvalId, 'approved');
344
+ return step.judge;
345
+ }
346
+ /**
347
+ * The index of the step a bounce re-arms: the nearest preceding step whose kind the judge
348
+ * declares it grades, else the immediately preceding step. `-1` when the judge is the first
349
+ * step — which degrades a `bounce` to a `park` rather than advancing silently.
350
+ */
351
+ bounceTargetIndex(instance, stepIndex, judge) {
352
+ const targets = judge.bounceTargets;
353
+ if (targets?.length) {
354
+ return this.deps.stepGraph.nearestStepIndexBefore(instance.steps, stepIndex, (s) => targets.includes(s.agentKind));
355
+ }
356
+ return stepIndex - 1;
357
+ }
358
+ /**
359
+ * The rubric body in force: a workspace's prompt-library fragment override when one resolves
360
+ * for {@link JudgeRubric.fragmentId}, else the registration's default. Best-effort — a
361
+ * library outage must not turn a rubric gate into a hard failure, so it degrades to the
362
+ * default (which is a real rubric, not an empty one).
363
+ */
364
+ async resolveRubric(workspaceId, judge) {
365
+ const fragmentId = judge.rubric.fragmentId;
366
+ if (!fragmentId || !this.deps.fragmentResolver)
367
+ return { body: judge.rubric.body, overridden: false };
368
+ try {
369
+ const resolved = await this.deps.fragmentResolver.resolveBodiesForRun(workspaceId, [
370
+ fragmentId,
371
+ ]);
372
+ const body = resolved.find((f) => f.id === fragmentId)?.body?.trim();
373
+ if (body)
374
+ return { body, overridden: true };
375
+ }
376
+ catch {
377
+ // Fall through to the default rubric.
378
+ }
379
+ return { body: judge.rubric.body, overridden: false };
380
+ }
381
+ }
382
+ /** The step outputs the judge reviews: every preceding step that produced one, oldest first. */
383
+ function priorOutputsFor(instance, stepIndex) {
384
+ return instance.steps
385
+ .slice(0, Math.max(stepIndex, 0))
386
+ .filter((s) => !!s.output?.trim())
387
+ .map((s) => ({ agentKind: s.agentKind, output: s.output }));
388
+ }
389
+ /** The step output a PASSING judge records (what it scored, and what it still noted). */
390
+ function renderPassOutput(judge, verdict, threshold) {
391
+ const nits = verdict.findings?.length ?? 0;
392
+ const suffix = nits > 0 ? ` ${nits} non-blocking finding(s) recorded.` : '';
393
+ return `The "${judge.rubric.name}" review passed (${verdict.score.toFixed(2)} ≥ ${threshold.toFixed(2)}).${suffix}`;
394
+ }
395
+ //# sourceMappingURL=JudgeStepController.js.map