ruvnet-brain 4.5.11 → 4.5.13

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.
@@ -0,0 +1,368 @@
1
+ // Policy-bound workflow controller. Agentic Kit owns DAG scheduling; guarded native adapters own execution.
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import crypto from 'node:crypto';
6
+ import { createRequire } from 'node:module';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { performance } from 'node:perf_hooks';
9
+ import { validateDispatchDecision } from './model-router-dispatch.mjs';
10
+ import { ContinuityJournal, drain } from '../plugin/scripts/continuity-journal.mjs';
11
+ import { EVENT_SCHEMA } from '../plugin/scripts/continuity-events.mjs';
12
+
13
+ const HASH = /^[a-f0-9]{64}$/;
14
+ const ID = /^[a-z][a-z0-9-]{0,79}$/;
15
+ const STOP = /permission|consent|auth|quota|allowance|policy|orphan|uncertain|unknown|cancel/i;
16
+ const sha = (value) => crypto.createHash('sha256').update(value).digest('hex');
17
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
18
+ const nativeReceipt = ({ workerId, host, configuredModel, observedModel, configuredEffort, observedEffort,
19
+ effortEvidence, sessionId, status, exitCategory, transcriptRefs, receiptRef }) =>
20
+ ({ workerId, host, configuredModel, observedModel, configuredEffort, observedEffort, effortEvidence,
21
+ sessionId, status, exitCategory, transcriptRefs, receiptRef });
22
+ const requireValue = (test, message) => { if (!test) throw new TypeError(message); };
23
+ const immutable = (value) => {
24
+ if (value && typeof value === 'object') { Object.values(value).forEach(immutable); Object.freeze(value); }
25
+ return value;
26
+ };
27
+ const canonical = (file) => typeof file === 'string' && path.isAbsolute(file) && fs.realpathSync(file) === file;
28
+
29
+ /** Resolve the installed global runner, never an npm download or a stock adapter. */
30
+ export async function loadManagedRunner({ globalRoot = path.join(os.homedir(), '.npm-global', 'lib', 'node_modules') } = {}) {
31
+ let entry;
32
+ try { entry = createRequire(import.meta.url).resolve('@pacphi/agentic-kit/package.json'); }
33
+ catch (error) {
34
+ if (error.code !== 'MODULE_NOT_FOUND') throw error;
35
+ // Older installations may supply only the global package. Never download a runner.
36
+ entry = createRequire(path.join(globalRoot, '_routing_resolver.cjs')).resolve('@pacphi/agentic-kit/package.json');
37
+ }
38
+ requireValue(JSON.parse(fs.readFileSync(entry, 'utf8')).name === '@pacphi/agentic-kit', 'Managed runner package mismatch');
39
+ const runner = await import(pathToFileURL(path.join(path.dirname(entry), 'src/lib/execution/runner.mjs')).href);
40
+ requireValue(typeof runner.executeRunPlan === 'function', 'Managed executeRunPlan unavailable');
41
+ return runner.executeRunPlan;
42
+ }
43
+
44
+ export function verifyContextRefs(refs) {
45
+ requireValue(Array.isArray(refs), 'Canonical context references required');
46
+ for (const ref of refs) {
47
+ requireValue(ref && canonical(ref.path) && HASH.test(ref.digest), 'Canonical context path and SHA-256 required');
48
+ requireValue(sha(fs.readFileSync(ref.path)) === ref.digest, 'Context reference changed');
49
+ }
50
+ }
51
+
52
+ /** Bind gate and review receipts to actual immutable artifact bytes. */
53
+ export function artifactDigest(refs) {
54
+ requireValue(Array.isArray(refs) && refs.length > 0, 'Actual artifact references required');
55
+ verifyContextRefs(refs);
56
+ requireValue(new Set(refs.map((ref) => ref.path)).size === refs.length, 'Duplicate artifact reference');
57
+ return sha(JSON.stringify([...refs].sort((a, b) => a.path.localeCompare(b.path))));
58
+ }
59
+
60
+ function tasksFor(request) {
61
+ return request.tasks ?? [{ id: 'work', instructions: request.originalPrompt, dependsOn: [],
62
+ ownership: { mode: 'read', worktree: request.projectRoot, paths: [] }, acceptanceChecks: request.acceptanceChecks }];
63
+ }
64
+
65
+ export function validateWorkflowRequest(request, now = Date.now()) {
66
+ requireValue(request && ID.test(request.id), 'Workflow ID required');
67
+ requireValue(typeof request.originalPrompt === 'string' && request.originalPrompt.trim(), 'Original request required');
68
+ requireValue(canonical(request.projectRoot), 'Canonical project root required');
69
+ verifyContextRefs(request.contextRefs);
70
+ requireValue(request.taskFacts && typeof request.taskFacts === 'object' && !Array.isArray(request.taskFacts), 'Explicit task facts required');
71
+ requireValue(!Object.hasOwn(request.taskFacts, 'verifiedTaskQualityFailure'), 'Quality failure facts must come from validated workflow evidence');
72
+ requireValue(request.permissions && request.permissions.apiBilling === false && typeof request.permissions.write === 'boolean', 'Explicit permissions and no API billing required');
73
+ requireValue(Number.isFinite(request.deadline) && request.deadline > now, 'Future absolute workflow deadline required');
74
+ for (const field of ['maxAttempts', 'maxConcurrent']) requireValue(Number.isInteger(request[field]) && request[field] > 0 && request[field] <= 64, `Invalid global ${field}`);
75
+ const tasks = tasksFor(request);
76
+ requireValue(Array.isArray(tasks) && tasks.length > 0 && tasks.length + 1 <= request.maxAttempts, 'Tasks exceed global attempt budget');
77
+ const ids = new Set();
78
+ for (const task of tasks) {
79
+ requireValue(task && ID.test(task.id) && !ids.has(task.id), 'Invalid or duplicate task ID'); ids.add(task.id);
80
+ requireValue(typeof task.instructions === 'string' && task.instructions.trim(), 'Task instructions required');
81
+ requireValue(Array.isArray(task.dependsOn ?? []) && new Set(task.dependsOn ?? []).size === (task.dependsOn ?? []).length, 'Invalid dependency list');
82
+ const own = task.ownership;
83
+ requireValue(own && ['read', 'write'].includes(own.mode) && canonical(own.worktree) && Array.isArray(own.paths), 'Explicit canonical ownership required');
84
+ requireValue(own.mode !== 'write' || request.permissions.write && own.paths.length > 0, 'Write authority and exact ownership required');
85
+ requireValue(own.paths.every((p) => typeof p === 'string' && p && !path.isAbsolute(p) && !p.split(/[\\/]/).some((part) => !part || part === '.' || part === '..')), 'Invalid owned relative path');
86
+ requireValue(Array.isArray(task.acceptanceChecks) && task.acceptanceChecks.length > 0 && task.acceptanceChecks.every((check) => check && ID.test(check.id)), 'Independently verifiable acceptance checks required');
87
+ }
88
+ const visiting = new Set(), visited = new Set();
89
+ const visit = (id) => {
90
+ requireValue(!visiting.has(id), 'Dependency cycle'); if (visited.has(id)) return;
91
+ visiting.add(id);
92
+ for (const dep of tasks.find((task) => task.id === id).dependsOn ?? []) { requireValue(ids.has(dep) && dep !== id, 'Unknown or self dependency'); visit(dep); }
93
+ visiting.delete(id); visited.add(id);
94
+ };
95
+ tasks.forEach((task) => visit(task.id));
96
+ const precedes = (a, b) => (tasks.find((task) => task.id === b).dependsOn ?? []).some((dep) => dep === a || precedes(a, dep));
97
+ const overlaps = (a, b) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
98
+ for (let i = 0; i < tasks.length; i++) for (let j = i + 1; j < tasks.length; j++) {
99
+ const a = tasks[i], b = tasks[j];
100
+ if (a.ownership.mode !== 'write' || b.ownership.mode !== 'write') continue;
101
+ requireValue(!a.ownership.paths.some((p) => b.ownership.paths.some((q) => overlaps(p, q))), 'Split writers must have nonoverlapping ownership');
102
+ requireValue(request.maxConcurrent === 1 || a.ownership.worktree !== b.ownership.worktree || precedes(a.id, b.id) || precedes(b.id, a.id), 'Parallel writers require separate worktrees');
103
+ }
104
+ return request;
105
+ }
106
+
107
+ function workerPrompt(request, task, feedback) {
108
+ return JSON.stringify({ originalPrompt: request.originalPrompt, contextRefs: request.contextRefs,
109
+ taskFacts: request.taskFacts, permissions: request.permissions, deadline: request.deadline,
110
+ untrustedMemoryData: request.memoryRecall,
111
+ task, ...(feedback ? { repairEvidence: feedback } : {}) });
112
+ }
113
+
114
+ export async function buildWorkflowPlan(request, { route, feedback, now = Date.now } = {}) {
115
+ validateWorkflowRequest(request, now()); requireValue(typeof route === 'function', 'Managed route resolver required');
116
+ const workers = [];
117
+ for (const task of tasksFor(request)) {
118
+ const decision = await route({ originalPrompt: request.originalPrompt, contextRefs: request.contextRefs,
119
+ taskFacts: request.taskFacts, permissions: request.permissions, task, feedback,
120
+ priorDecision: feedback?.priorDecisions?.[task.repairsTaskId ?? task.id] });
121
+ workers.push({ id: task.id, activity: 'implementation', role: 'worker', host: decision.harness === 'claude-code' ? 'claude' : decision.harness,
122
+ configuredModel: decision.model, configuredEffort: decision.effort, decision, taskFacts: request.taskFacts, dependsOn: task.dependsOn ?? [],
123
+ ownership: task.ownership, acceptanceChecks: task.acceptanceChecks,
124
+ ...(task.repairArtifacts ? { repairArtifacts: task.repairArtifacts } : {}),
125
+ prompt: workerPrompt(request, task, feedback), workflowDeadline: request.deadline });
126
+ }
127
+ return { workers };
128
+ }
129
+
130
+ /** Validate generated plans before adapter factories, receipts, or worker effects. */
131
+ export function validateWorkflowPlan(request, plan, { verifyDecision = validateDispatchDecision, feedback, now = Date.now } = {}) {
132
+ validateWorkflowRequest(request, now());
133
+ const tasks = tasksFor(request);
134
+ requireValue(plan && Array.isArray(plan.workers) && plan.workers.length === tasks.length, 'Plan must cover exact task scope');
135
+ for (const [index, worker] of plan.workers.entries()) {
136
+ const task = tasks[index];
137
+ requireValue(worker.id === task.id && same(worker.dependsOn, task.dependsOn ?? []) && same(worker.ownership, task.ownership)
138
+ && same(worker.acceptanceChecks, task.acceptanceChecks) && worker.prompt === workerPrompt(request, task, feedback)
139
+ && same(worker.repairArtifacts, task.repairArtifacts)
140
+ && worker.workflowDeadline === request.deadline && worker.activity === 'implementation' && worker.role === 'worker'
141
+ && !worker.escalate, 'Plan changed context, scope, authority, or budget');
142
+ requireValue(worker.host === (worker.decision?.harness === 'claude-code' ? 'claude' : worker.decision?.harness) && worker.configuredModel === worker.decision?.model && worker.configuredEffort === worker.decision?.effort && same(worker.taskFacts, request.taskFacts), 'Plan route mismatch');
143
+ verifyDecision(worker.decision);
144
+ }
145
+ return plan;
146
+ }
147
+
148
+ function blockedResult(result) {
149
+ return result.status !== 'succeeded'
150
+ || STOP.test(`${result.exitCategory} ${result.failure?.reason ?? ''}`)
151
+ || result.exitCategory !== 'success';
152
+ }
153
+
154
+ function blockedEvidence(value) {
155
+ return value?.status === 'blocked' || value?.uncertainEffects === true
156
+ || value?.exitCode === 126 || value?.exitCode === 127 || value?.timedOut === true || !!value?.signal
157
+ || STOP.test(`${value?.exitCategory ?? ''} ${value?.reason ?? ''}`)
158
+ || value?.passed === false && (/environment|unavailable|not.found|missing.executable|enoent|spawn.error|timeout/i.test(`${value?.exitCategory ?? ''} ${value?.reason ?? ''}`)
159
+ || /ERR_MODULE_NOT_FOUND|Cannot find (?:module|package)|command not found|No such file or directory|permission denied|authentication required|quota exhausted|ECONNREFUSED|EAI_AGAIN|ENOTFOUND/i.test(value?.output ?? ''))
160
+ || [...(value?.evidence ?? []), ...(value?.findings ?? [])].some((item) => item && typeof item === 'object' && blockedEvidence(item));
161
+ }
162
+
163
+ /** A repair is a new, explicitly scoped task; the original writing DAG is never replayed. */
164
+ export function validateRepairPlan(request, repair, now = Date.now()) {
165
+ requireValue(repair && repair.baseArtifactDigest === artifactDigest(repair.artifactRefs), 'Repair must bind existing artifact bytes');
166
+ requireValue(Array.isArray(repair.tasks) && repair.tasks.length > 0, 'Explicit repair tasks required');
167
+ const originals = tasksFor(request);
168
+ const seen = new Set();
169
+ for (const task of repair.tasks) {
170
+ const original = originals.find((entry) => entry.id === task.repairsTaskId);
171
+ requireValue(original && !originals.some((entry) => entry.id === task.id) && !seen.has(task.id), 'Repair must use a new task ID and name original scope'); seen.add(task.id);
172
+ const own = task.ownership, prior = original.ownership;
173
+ requireValue(own && own.worktree === prior.worktree && (own.mode === 'read' || prior.mode === 'write')
174
+ && own.paths.every((file) => prior.paths.some((scope) => file === scope || file.startsWith(`${scope}/`)))
175
+ && same(task.acceptanceChecks, original.acceptanceChecks), 'Repair expanded original ownership or acceptance scope');
176
+ const scopeRefs = repair.artifactRefs.filter((ref) => {
177
+ const relative = path.relative(own.worktree, ref.path);
178
+ return own.paths.some((scope) => relative === scope || relative.startsWith(`${scope}/`));
179
+ });
180
+ requireValue(own.mode !== 'write' || scopeRefs.length > 0, 'Writing repair must name existing artifacts in its exact scope');
181
+ requireValue(same(task.repairArtifacts, scopeRefs), 'Repair task must retain exact prior artifact references');
182
+ }
183
+ const executionRequest = { ...request, tasks: repair.tasks };
184
+ validateWorkflowRequest(executionRequest, now);
185
+ return executionRequest;
186
+ }
187
+
188
+ /** Per-launch caps are shared across all DAG branches, repairs, and independent review. */
189
+ function guardAdapters(adapters, request, budget, now) {
190
+ requireValue(adapters && typeof adapters === 'object', 'Explicit guarded adapters required; stock adapters forbidden');
191
+ const abort = budget.abort;
192
+ const preparedRefs = new WeakMap();
193
+ const entries = adapters instanceof Map ? [...adapters] : Object.entries(adapters);
194
+ return Object.fromEntries(entries.map(([host, adapter]) => {
195
+ const check = () => {
196
+ requireValue(!budget.blocked, 'Workflow policy blocked'); budget.assertTime();
197
+ verifyContextRefs(request.contextRefs);
198
+ };
199
+ const phase = (name) => async (...args) => {
200
+ check();
201
+ const refs = args[0]?.worker?.repairArtifacts ?? preparedRefs.get(args[0]) ?? [];
202
+ if (['readiness', 'prepare', 'launch'].includes(name) && refs.length) verifyContextRefs(refs);
203
+ const options = args.at(-1);
204
+ if (options && typeof options === 'object') args[args.length - 1] = { ...options,
205
+ signal: options.signal ? AbortSignal.any([options.signal, abort.signal]) : abort.signal,
206
+ timeoutMs: Math.min(options.timeoutMs ?? Infinity, budget.remainingMs()) };
207
+ if (name === 'launch') {
208
+ requireValue(budget.attemptsUsed < budget.maxAttempts, 'Global attempt budget exhausted');
209
+ budget.attemptsUsed++;
210
+ }
211
+ const result = await adapter[name](...args);
212
+ if (name === 'prepare' && result && typeof result === 'object') preparedRefs.set(result, refs);
213
+ budget.assertTime(); return result;
214
+ };
215
+ return [host, { id: adapter.id, readiness: phase('readiness'), prepare: phase('prepare'),
216
+ launch: phase('launch'), observe: phase('observe'), summarize: (...args) => adapter.summarize(...args),
217
+ cancel: (...args) => adapter.cancel(...args), cleanup: (...args) => adapter.cleanup(...args),
218
+ ...(adapter.handoffRequestFor ? { handoffRequestFor: (...args) => adapter.handoffRequestFor(...args) } : {}),
219
+ interpret: (...args) => {
220
+ const result = adapter.interpret(...args);
221
+ if (blockedResult(result)) {
222
+ budget.blocked = true;
223
+ // A safely retired native failure still yields its receipt; later launches are refused.
224
+ if (result.failure?.retrySafe !== true || STOP.test(`${result.exitCategory} ${result.failure?.reason ?? ''}`)) abort.abort();
225
+ }
226
+ return result;
227
+ } }];
228
+ }));
229
+ }
230
+
231
+ export function durableWorkflowReceipt(request, receipt) {
232
+ const journal = new ContinuityJournal({ projectRoot: request.projectRoot });
233
+ const event = { schema: EVENT_SCHEMA, kind: 'decision', id: sha(JSON.stringify(receipt)),
234
+ at: receipt.at, source: 'model-routing-controller', authoritative: true,
235
+ summary: `Routing workflow ${request.id}: ${receipt.status}`, detail: receipt };
236
+ journal.record([event]);
237
+ // Existing outbox is durable before the existing exact-readback AgentDB drainer runs.
238
+ const status = drain(journal, { budgetMs: Math.max(0, Math.min(1000, request.deadline - Date.now())), backoff: [] });
239
+ return { durable: true, agentDbCommitted: status.remaining === 0, ...status };
240
+ }
241
+
242
+ function validateAcceptance(request, acceptance) {
243
+ requireValue(acceptance && acceptance.artifactDigest === artifactDigest(acceptance.artifactRefs), 'Acceptance must bind actual artifact bytes');
244
+ requireValue(Array.isArray(acceptance.evidence), 'Actual acceptance evidence required');
245
+ const expected = tasksFor(request).flatMap((task) => task.acceptanceChecks.map((check) => `${task.id}:${check.id}`));
246
+ requireValue(new Set(expected).size === expected.length, 'Duplicate acceptance check ID');
247
+ requireValue(expected.every((id) => acceptance.evidence.some((e) => `${e.taskId}:${e.checkId}` === id
248
+ && typeof e.passed === 'boolean' && e.artifactDigest === acceptance.artifactDigest)), 'Acceptance coverage incomplete');
249
+ requireValue(typeof acceptance.passed === 'boolean' && acceptance.passed === acceptance.evidence.every((e) => e.passed === true), 'Acceptance aggregate contradicts actual check evidence');
250
+ return acceptance.passed;
251
+ }
252
+
253
+ async function bounded(budget, operation) {
254
+ const controller = new AbortController(); let timer, onAbort;
255
+ budget.assertTime();
256
+ try {
257
+ const result = await Promise.race([Promise.resolve().then(() => operation(AbortSignal.any([controller.signal, budget.abort.signal]))),
258
+ new Promise((_, reject) => { onAbort = () => reject(new Error('Workflow cancelled'));
259
+ budget.abort.signal.addEventListener('abort', onAbort, { once: true });
260
+ timer = setTimeout(() => { controller.abort(); reject(new Error('Absolute workflow deadline exceeded')); }, budget.remainingMs()); })]);
261
+ budget.assertTime(); return result;
262
+ } catch (error) { controller.abort(); throw error;
263
+ } finally { clearTimeout(timer); budget.abort.signal.removeEventListener('abort', onAbort); }
264
+ }
265
+
266
+ /** No automatic transport/settings mutation. Every completed result has real gates and independent review. */
267
+ export async function runRoutingWorkflow(input, { route, createAdapters, executePlan, checkAcceptance, review, planRepair,
268
+ recordReceipt = durableWorkflowReceipt, verifyDecision = validateDispatchDecision, now = Date.now, signal } = {}) {
269
+ const request = immutable(structuredClone(input)); validateWorkflowRequest(request, now());
270
+ for (const [name, fn] of Object.entries({ route, createAdapters, checkAcceptance, review, recordReceipt })) requireValue(typeof fn === 'function', `${name} boundary required`);
271
+ const budget = { deadline: request.deadline, maxAttempts: request.maxAttempts, attemptsUsed: 0, blocked: false, abort: new AbortController() };
272
+ const monotonicDeadline = performance.now() + (request.deadline - now());
273
+ const onAbort = () => { budget.blocked = true; budget.abort.abort(signal?.reason); };
274
+ signal?.addEventListener('abort', onAbort, { once: true }); if (signal?.aborted) onAbort();
275
+ budget.remainingMs = () => Math.max(0, Math.min(request.deadline - now(), monotonicDeadline - performance.now()));
276
+ budget.assertTime = () => { requireValue(!signal?.aborted, 'Workflow cancelled'); requireValue(budget.remainingMs() > 0, 'Absolute workflow deadline exceeded'); };
277
+ const runner = executePlan ?? await loadManagedRunner(); let results = [], acceptance, feedback, revision = 0;
278
+ let executionRequest = request, repairArtifactRefs = null, writerExecuted = false;
279
+ const executionReceipts = [];
280
+ const persist = async (status, extra = {}) => {
281
+ const receipt = { workflowId: request.id, status, at: new Date(now()).toISOString(),
282
+ originalPromptDigest: sha(request.originalPrompt), contextDigest: sha(JSON.stringify(request.contextRefs)),
283
+ attemptsUsed: budget.attemptsUsed, deadline: request.deadline, ...extra };
284
+ if (status === 'complete') budget.assertTime();
285
+ const recorded = status === 'blocked' ? await recordReceipt(request, receipt)
286
+ : await bounded(budget, (signal) => recordReceipt(request, receipt, { signal }));
287
+ requireValue(recorded?.durable === true, 'Workflow receipt durability not proven'); return receipt;
288
+ };
289
+ const execute = async (plan) => {
290
+ requireValue(!budget.blocked && budget.attemptsUsed < budget.maxAttempts, 'Global execution budget blocked'); budget.assertTime();
291
+ const adapters = guardAdapters(await bounded(budget, () => createAdapters({ request, plan, budget })), request, budget, now);
292
+ const results = await bounded(budget, () => runner(plan, { adapters, cwd: request.projectRoot, maxConcurrent: request.maxConcurrent,
293
+ timeoutMs: Math.max(1, budget.remainingMs()), escalate: false }));
294
+ try { budget.assertTime(); } catch (error) { error.executionResults = results; throw error; } return results;
295
+ };
296
+ try {
297
+ for (;;) {
298
+ if (repairArtifactRefs) verifyContextRefs(repairArtifactRefs);
299
+ const plan = immutable(await bounded(budget, () => buildWorkflowPlan(executionRequest, { route, feedback, now })));
300
+ validateWorkflowPlan(executionRequest, plan, { verifyDecision, feedback, now });
301
+ await persist('running', { revision: revision++ });
302
+ results = await execute(plan);
303
+ requireValue(Array.isArray(results) && results.length === plan.workers.length && results.every((r, i) => r.workerId === plan.workers[i].id), 'Execution result coverage invalid');
304
+ executionReceipts.push({ plan, results });
305
+ await persist('stage-finished', { nativeReceipts: results.map(nativeReceipt) });
306
+ writerExecuted ||= plan.workers.some((worker, index) => worker.ownership.mode === 'write'
307
+ && results[index].status !== 'blocked');
308
+ if (budget.blocked || results.some(blockedResult)) return {
309
+ ...await persist('blocked', { reason: 'native-execution-failed-without-quality-evidence' }), results, executionReceipts };
310
+ if (results.every((result) => result.status === 'succeeded')) {
311
+ acceptance = await bounded(budget, (signal) => checkAcceptance({ request, plan, results, executionReceipts, signal }));
312
+ if (blockedEvidence(acceptance)) return { ...await persist('blocked', { reason: 'acceptance-boundary-blocked' }), results, acceptance };
313
+ if (validateAcceptance(request, acceptance)) {
314
+ const reviewResults = new Map();
315
+ const executeReview = async (worker) => {
316
+ requireValue(worker?.role === 'reviewer' && worker.activity === 'review' && ID.test(worker.id)
317
+ && !executionReceipts.some((entry) => entry.plan.workers.some((w) => w.id === worker.id)) && worker.ownership?.mode === 'read'
318
+ && canonical(worker.ownership.worktree) && !worker.escalate
319
+ && worker.host === (worker.decision?.harness === 'claude-code' ? 'claude' : worker.decision?.harness) && worker.configuredModel === worker.decision?.model && worker.configuredEffort === worker.decision?.effort && same(worker.taskFacts, request.taskFacts),
320
+ 'Independent read-only reviewer required');
321
+ verifyDecision(worker.decision);
322
+ requireValue(worker.prompt === JSON.stringify({ originalPrompt: request.originalPrompt, contextRefs: request.contextRefs,
323
+ taskFacts: request.taskFacts, permissions: { ...request.permissions, write: false }, acceptance,
324
+ untrustedMemoryData: request.memoryRecall }), 'Review must retain exact request and artifact evidence');
325
+ const [result] = await execute({ workers: [worker] }); reviewResults.set(worker.id, result);
326
+ await persist('review-finished', { nativeReceipts: [nativeReceipt(result)] }); return result;
327
+ };
328
+ const verdict = await bounded(budget, (signal) => review({ request, plan, results, executionReceipts, acceptance, executeReview, signal }));
329
+ if (budget.blocked || blockedEvidence(verdict)) return { ...await persist('blocked', { reason: 'review-boundary-blocked' }), results, acceptance };
330
+ const reviewer = reviewResults.get(verdict?.reviewerWorkerId);
331
+ requireValue(verdict?.independent === true && reviewer?.status === 'succeeded'
332
+ && typeof reviewer.sessionId === 'string' && reviewer.sessionId.length > 0
333
+ && !executionReceipts.some((entry) => entry.results.some((result) => result.sessionId === reviewer.sessionId))
334
+ && verdict.artifactDigest === acceptance.artifactDigest && Array.isArray(verdict.findings)
335
+ && Array.isArray(verdict.evidence) && verdict.evidence.length > 0, 'Actual independent review of exact artifact required');
336
+ requireValue(artifactDigest(acceptance.artifactRefs) === acceptance.artifactDigest, 'Artifact changed after acceptance/review');
337
+ if (budget.blocked) return { ...await persist('blocked'), results };
338
+ if (verdict.passed === true && verdict.findings.length === 0) return { ...await persist('complete', {
339
+ artifactDigest: acceptance.artifactDigest, acceptanceEvidence: acceptance.evidence,
340
+ reviewerWorkerId: verdict.reviewerWorkerId, reviewEvidence: verdict.evidence }), results, executionReceipts, acceptance, review: verdict };
341
+ requireValue(verdict.findings.length > 0, 'Failed review requires specific quality defects');
342
+ feedback = { acceptance, review: verdict, verifiedTaskQualityFailure: true };
343
+ } else feedback = { acceptance, verifiedTaskQualityFailure: true };
344
+ } else feedback = { results };
345
+ feedback.priorDecisions = Object.fromEntries(plan.workers.map((worker) => {
346
+ const task = tasksFor(executionRequest).find((entry) => entry.id === worker.id);
347
+ return [task.repairsTaskId ?? task.id, worker.decision];
348
+ }));
349
+ if (writerExecuted || planRepair) {
350
+ if (typeof planRepair !== 'function') return { ...await persist('blocked', { reason: 'unsafe-writer-replay-refused' }), results, executionReceipts, acceptance };
351
+ const repair = await bounded(budget, (signal) => planRepair({ request, plan, results, acceptance, feedback,
352
+ executionReceipts, budget: { deadline: budget.deadline, maxAttempts: budget.maxAttempts, attemptsUsed: budget.attemptsUsed }, signal }));
353
+ executionRequest = immutable(validateRepairPlan(request, structuredClone(repair), now()));
354
+ repairArtifactRefs = immutable(structuredClone(repair.artifactRefs));
355
+ await persist('repair-planned', { baseArtifactDigest: repair.baseArtifactDigest, artifactRefs: repairArtifactRefs,
356
+ repairTasks: executionRequest.tasks.map(({ id, repairsTaskId, ownership }) => ({ id, repairsTaskId, ownership })) });
357
+ }
358
+ if (budget.blocked || budget.remainingMs() <= 0 || budget.attemptsUsed + tasksFor(executionRequest).length + 1 > budget.maxAttempts) {
359
+ return { ...await persist('blocked', { reason: 'budget-or-gates-unresolved' }), results, acceptance };
360
+ }
361
+ }
362
+ } catch (error) {
363
+ budget.blocked = true; budget.abort.abort();
364
+ const lateResults = error.executionResults ?? [];
365
+ return { ...await persist('blocked', { reason: 'workflow-boundary-failed', nativeReceipts: lateResults.map(nativeReceipt) }),
366
+ results, lateResults, executionReceipts, failure: String(error.message).slice(0, 240) };
367
+ } finally { signal?.removeEventListener('abort', onAbort); }
368
+ }
@@ -0,0 +1,44 @@
1
+ import { createAIDefence } from '@claude-flow/aidefence';
2
+
3
+ const severities = ['low', 'medium', 'high', 'critical'];
4
+ const types = ['prompt_injection', 'jailbreak', 'pii_exposure', 'instruction_override',
5
+ 'role_switching', 'context_manipulation', 'encoding_attack', 'unknown'];
6
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ function refusal(code, metadata) {
8
+ const error = new Error('Model routing defence refused this boundary.');
9
+ error.code = code; error.metadata = Object.freeze(metadata); return error;
10
+ }
11
+ function valid(result) {
12
+ return object(result) && typeof result.safe === 'boolean' && typeof result.piiFound === 'boolean'
13
+ && Number.isFinite(result.detectionTimeMs) && result.detectionTimeMs >= 0
14
+ && typeof result.inputHash === 'string' && result.inputHash.length > 0
15
+ && Array.isArray(result.threats) && result.safe === (result.threats.length === 0)
16
+ && result.threats.every(threat => object(threat) && severities.includes(threat.severity)
17
+ && types.includes(threat.type) && Number.isFinite(threat.confidence)
18
+ && threat.confidence >= 0 && threat.confidence <= 1
19
+ && ['id', 'pattern', 'description'].every(key => typeof threat[key] === 'string')
20
+ && threat.detectedAt instanceof Date && Number.isFinite(threat.detectedAt.getTime()));
21
+ }
22
+
23
+ /** Local in-memory detection only. PII is disclosed as metadata, not an automatic refusal rule. */
24
+ export function createModelRoutingDefence(factory = createAIDefence) {
25
+ let detector;
26
+ return async function assertText(text) {
27
+ let result;
28
+ try {
29
+ if (typeof text !== 'string') throw new Error();
30
+ detector ??= factory({ enableLearning: true, enablePIIDetection: true });
31
+ result = await detector.detect(text);
32
+ if (!valid(result)) throw new Error();
33
+ } catch {
34
+ throw refusal('MODEL_ROUTING_DEFENCE_UNAVAILABLE', { allowed: false, reason: 'detector-unavailable' });
35
+ }
36
+ const severity = result.threats.reduce((max, threat) => Math.max(max, severities.indexOf(threat.severity)), -1);
37
+ const metadata = { allowed: severity < 1, piiFound: result.piiFound,
38
+ threatCount: result.threats.length, severity: severity < 0 ? 'none' : severities[severity] };
39
+ if (!metadata.allowed) throw refusal('MODEL_ROUTING_DEFENCE_BLOCKED', metadata);
40
+ return Object.freeze(metadata);
41
+ };
42
+ }
43
+
44
+ export const assertModelRoutingText = createModelRoutingDefence();