@pellux/goodvibes-sdk 0.33.16 → 0.33.18

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 (44) hide show
  1. package/dist/contracts/artifacts/operator-contract.json +1 -1
  2. package/dist/events/workflows.d.ts +1 -1
  3. package/dist/events/workflows.d.ts.map +1 -1
  4. package/dist/platform/agents/completion-report.d.ts +3 -3
  5. package/dist/platform/agents/completion-report.d.ts.map +1 -1
  6. package/dist/platform/agents/completion-report.js +1 -1
  7. package/dist/platform/agents/index.d.ts +1 -0
  8. package/dist/platform/agents/index.d.ts.map +1 -1
  9. package/dist/platform/agents/index.js +1 -0
  10. package/dist/platform/agents/wrfc-controller.d.ts +14 -6
  11. package/dist/platform/agents/wrfc-controller.d.ts.map +1 -1
  12. package/dist/platform/agents/wrfc-controller.js +316 -100
  13. package/dist/platform/agents/wrfc-external-adapter.d.ts +45 -0
  14. package/dist/platform/agents/wrfc-external-adapter.d.ts.map +1 -0
  15. package/dist/platform/agents/wrfc-external-adapter.js +18 -0
  16. package/dist/platform/agents/wrfc-prompt-addenda.d.ts.map +1 -1
  17. package/dist/platform/agents/wrfc-prompt-addenda.js +9 -0
  18. package/dist/platform/agents/wrfc-reporting.d.ts +2 -2
  19. package/dist/platform/agents/wrfc-reporting.d.ts.map +1 -1
  20. package/dist/platform/agents/wrfc-reporting.js +22 -13
  21. package/dist/platform/agents/wrfc-types.d.ts +35 -5
  22. package/dist/platform/agents/wrfc-types.d.ts.map +1 -1
  23. package/dist/platform/agents/wrfc-workmap.d.ts +7 -1
  24. package/dist/platform/agents/wrfc-workmap.d.ts.map +1 -1
  25. package/dist/platform/knowledge/semantic/gap-repair.d.ts +1 -0
  26. package/dist/platform/knowledge/semantic/gap-repair.d.ts.map +1 -1
  27. package/dist/platform/knowledge/semantic/gap-repair.js +1 -0
  28. package/dist/platform/knowledge/store-config.d.ts +3 -0
  29. package/dist/platform/knowledge/store-config.d.ts.map +1 -1
  30. package/dist/platform/knowledge/store-config.js +6 -2
  31. package/dist/platform/knowledge/store-schema.d.ts +2 -1
  32. package/dist/platform/knowledge/store-schema.d.ts.map +1 -1
  33. package/dist/platform/knowledge/store-schema.js +3 -2
  34. package/dist/platform/runtime/services.d.ts.map +1 -1
  35. package/dist/platform/runtime/services.js +24 -7
  36. package/dist/platform/tools/agent/index.d.ts.map +1 -1
  37. package/dist/platform/tools/agent/index.js +6 -2
  38. package/dist/platform/tools/agent/manager.d.ts +3 -0
  39. package/dist/platform/tools/agent/manager.d.ts.map +1 -1
  40. package/dist/platform/tools/agent/manager.js +43 -6
  41. package/dist/platform/tools/agent/schema.d.ts +2 -0
  42. package/dist/platform/tools/agent/schema.d.ts.map +1 -1
  43. package/dist/platform/version.js +1 -1
  44. package/package.json +9 -9
@@ -8,7 +8,7 @@ import { AgentWorktree } from './worktree.js';
8
8
  import { completePlanItemsForAgent } from './wrfc-plan-sync.js';
9
9
  import { logger } from '../utils/logger.js';
10
10
  import { summarizeError } from '../utils/error-display.js';
11
- import { emitWorkflowChainFailed, emitWorkflowFixAttempted, emitWorkflowReviewCompleted, } from '../runtime/emitters/index.js';
11
+ import { emitAgentCompleted, emitAgentFailed, emitWorkflowChainFailed, emitWorkflowFixAttempted, emitWorkflowReviewCompleted, } from '../runtime/emitters/index.js';
12
12
  import { getWrfcAutoCommit, getWrfcMaxFixAttempts, getWrfcScoreThreshold, } from './wrfc-config.js';
13
13
  import { buildEngineerConstraintAddendum, } from './wrfc-prompt-addenda.js';
14
14
  import { completeWrfcOrchestrationNode, createWrfcWorkflowContext, emitWrfcAutoCommitted, emitWrfcCascadeAbort, emitWrfcChainCreated, emitWrfcChainPassed, emitWrfcConstraintsEnumerated, emitWrfcGraphCreated, emitWrfcStateChanged, failWrfcOrchestrationNode, startWrfcOrchestrationNode, } from './wrfc-runtime-events.js';
@@ -20,7 +20,7 @@ const VALID_TRANSITIONS = {
20
20
  reviewing: ['fixing', 'awaiting_gates', 'failed'],
21
21
  fixing: ['reviewing', 'failed'],
22
22
  awaiting_gates: ['gating', 'failed'],
23
- gating: ['passed', 'failed', 'committing'],
23
+ gating: ['passed', 'failed', 'committing', 'fixing'],
24
24
  committing: ['passed', 'failed'],
25
25
  };
26
26
  const MAX_ACTIVE_CHAINS = 6;
@@ -30,9 +30,6 @@ export class WrfcController {
30
30
  chainQueue = [];
31
31
  unsubscribers = [];
32
32
  activeChainCount = 0;
33
- pendingParentChainIds = new Map();
34
- /** Constraints to inherit when a gate-retry child chain is created via the pending path. */
35
- pendingParentConstraints = new Map();
36
33
  sessionId;
37
34
  workmap;
38
35
  projectRoot;
@@ -42,6 +39,7 @@ export class WrfcController {
42
39
  agentManager;
43
40
  configManager;
44
41
  createWorktree;
42
+ selectChildRoute;
45
43
  constructor(runtimeBus, messageBus, deps) {
46
44
  this.runtimeBus = runtimeBus;
47
45
  this.messageBus = messageBus;
@@ -49,22 +47,23 @@ export class WrfcController {
49
47
  this.configManager = deps.configManager;
50
48
  this.projectRoot = deps.projectRoot;
51
49
  this.createWorktree = deps.createWorktree ?? (() => new AgentWorktree(this.projectRoot));
50
+ this.selectChildRoute = deps.selectChildRoute ?? null;
52
51
  this.sessionId = crypto.randomUUID().slice(0, 8);
53
52
  this.workmap = new WrfcWorkmap(this.projectRoot, this.sessionId, { surfaceRoot: deps.surfaceRoot });
54
53
  this.setupListeners();
55
54
  }
56
- createChain(engineerRecord) {
55
+ createChain(ownerRecord) {
57
56
  logger.info('WrfcController.createChain: called', {
58
- agentId: engineerRecord.id,
59
- task: engineerRecord.task.slice(0, 60),
57
+ agentId: ownerRecord.id,
58
+ task: ownerRecord.task.slice(0, 60),
60
59
  activeChainCount: this.activeChainCount,
61
60
  });
62
- const chain = this.createBaseChain(engineerRecord);
61
+ const chain = this.createBaseChain(ownerRecord);
63
62
  if (this.activeChainCount >= MAX_ACTIVE_CHAINS) {
64
- this.chainQueue.push({ record: engineerRecord, queuedAt: Date.now() });
63
+ this.chainQueue.push({ record: ownerRecord, queuedAt: Date.now() });
65
64
  logger.debug('WrfcController.createChain: at cap, queued', {
66
65
  chainId: chain.id,
67
- agentId: engineerRecord.id,
66
+ agentId: ownerRecord.id,
68
67
  activeCount: this.activeChainCount,
69
68
  queueLength: this.chainQueue.length,
70
69
  });
@@ -72,7 +71,7 @@ export class WrfcController {
72
71
  return chain;
73
72
  }
74
73
  this.startEngineeringChain(chain, true);
75
- logger.debug('WrfcController.createChain', { chainId: chain.id, agentId: engineerRecord.id });
74
+ logger.debug('WrfcController.createChain', { chainId: chain.id, agentId: ownerRecord.id });
76
75
  return chain;
77
76
  }
78
77
  getSessionId() { return this.sessionId; }
@@ -89,6 +88,39 @@ export class WrfcController {
89
88
  }
90
89
  getChain(chainId) { return this.chains.get(chainId) ?? null; }
91
90
  listChains() { return Array.from(this.chains.values()); }
91
+ resumeChain(chainId) {
92
+ const chain = this.chains.get(chainId);
93
+ if (!chain || chain.state === 'passed' || chain.state === 'failed')
94
+ return false;
95
+ if (this.hasRunningChild(chain)) {
96
+ this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain already has an active child agent');
97
+ return true;
98
+ }
99
+ if (chain.state !== 'pending') {
100
+ this.appendOwnerDecision(chain, 'resume_skipped', `WRFC chain state ${chain.state} cannot be resumed without an active phase result`);
101
+ return true;
102
+ }
103
+ if (this.activeChainCount >= MAX_ACTIVE_CHAINS) {
104
+ if (!this.chainQueue.some((queued) => queued.record.id === chain.ownerAgentId)) {
105
+ const owner = this.agentManager.getStatus(chain.ownerAgentId);
106
+ if (owner)
107
+ this.chainQueue.push({ record: owner, queuedAt: Date.now() });
108
+ }
109
+ this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain is queued because active chain capacity is full');
110
+ return true;
111
+ }
112
+ this.appendOwnerDecision(chain, 'resume_started', 'WRFC owner resumed pending chain');
113
+ this.startEngineeringChain(chain, false);
114
+ return true;
115
+ }
116
+ resumeAllActiveChains() {
117
+ let resumed = 0;
118
+ for (const chain of this.chains.values()) {
119
+ if (this.resumeChain(chain.id))
120
+ resumed += 1;
121
+ }
122
+ return resumed;
123
+ }
92
124
  dispose() {
93
125
  for (const unsub of this.unsubscribers)
94
126
  unsub();
@@ -121,12 +153,21 @@ export class WrfcController {
121
153
  const unsubError = this.runtimeBus.on('AGENT_FAILED', ({ payload }) => {
122
154
  this.onAgentFailed(payload.agentId, payload.error);
123
155
  });
124
- this.unsubscribers.push(unsubComplete, unsubError);
156
+ const unsubCancelled = this.runtimeBus.on('AGENT_CANCELLED', ({ payload }) => {
157
+ this.onAgentCancelled(payload.agentId, payload.reason);
158
+ });
159
+ this.unsubscribers.push(unsubComplete, unsubError, unsubCancelled);
125
160
  }
126
161
  async onAgentComplete(agentId) {
127
162
  const chain = this.findChainByAgentId(agentId);
128
163
  if (!chain)
129
164
  return;
165
+ if (agentId === chain.ownerAgentId) {
166
+ if (chain.ownerTerminalEmitted)
167
+ return;
168
+ this.failChain(chain, 'WRFC owner agent completed before the chain reached a terminal state');
169
+ return;
170
+ }
130
171
  const record = this.agentManager.getStatus(agentId);
131
172
  const rawOutput = record?.fullOutput ?? '';
132
173
  logger.debug('WrfcController.onAgentComplete', {
@@ -165,13 +206,25 @@ export class WrfcController {
165
206
  const chain = this.findChainByAgentId(agentId);
166
207
  if (!chain)
167
208
  return;
209
+ if (agentId === chain.ownerAgentId && (chain.state === 'passed' || chain.state === 'failed'))
210
+ return;
168
211
  this.failChain(chain, errorMessage ?? `Agent ${agentId} failed`);
169
212
  }
213
+ onAgentCancelled(agentId, reason) {
214
+ const chain = this.findChainByAgentId(agentId);
215
+ if (!chain || chain.state === 'passed' || chain.state === 'failed')
216
+ return;
217
+ if (agentId === chain.ownerAgentId) {
218
+ this.cancelChain(chain, reason ?? 'WRFC owner agent cancelled');
219
+ return;
220
+ }
221
+ this.failChain(chain, reason ?? `Agent ${agentId} cancelled`);
222
+ }
170
223
  startReview(chain, report) {
171
224
  this.transition(chain, 'reviewing');
172
225
  // Prepend any synthetic issues from the controller (e.g. fixer constraint-continuity
173
226
  // violations) to the review task body, then clear them so they fire only once.
174
- let reviewTask = buildReviewTask(chain.id, report, getWrfcScoreThreshold(this.configManager), chain.constraints);
227
+ let reviewTask = buildReviewTask(chain.id, chain.task, report, getWrfcScoreThreshold(this.configManager), chain.constraints);
175
228
  if (chain.syntheticIssues && chain.syntheticIssues.length > 0) {
176
229
  const syntheticBlock = [
177
230
  `## Synthetic issues from controller`,
@@ -181,8 +234,9 @@ export class WrfcController {
181
234
  reviewTask = syntheticBlock + '\n\n---\n\n' + reviewTask;
182
235
  chain.syntheticIssues = [];
183
236
  }
184
- const reviewerRecord = this.spawnWrfcAgent(chain, 'reviewer', reviewTask, true);
237
+ const reviewerRecord = this.spawnWrfcAgent(chain, 'reviewer', 'reviewer', reviewTask, true);
185
238
  chain.reviewerAgentId = reviewerRecord.id;
239
+ reviewerRecord.wrfcRole = 'reviewer';
186
240
  chain.allAgentIds.push(reviewerRecord.id);
187
241
  reviewerRecord.wrfcId = chain.id;
188
242
  this.messageBus.registerAgent({
@@ -195,6 +249,11 @@ export class WrfcController {
195
249
  chainId: chain.id,
196
250
  reviewerAgentId: reviewerRecord.id,
197
251
  });
252
+ this.appendOwnerDecision(chain, 'spawn_reviewer', this.withRouteReason('Review full current result against the original WRFC ask', reviewerRecord), {
253
+ agentId: reviewerRecord.id,
254
+ role: 'reviewer',
255
+ record: reviewerRecord,
256
+ });
198
257
  }
199
258
  async processReview(chain, review) {
200
259
  const threshold = getWrfcScoreThreshold(this.configManager);
@@ -249,6 +308,11 @@ export class WrfcController {
249
308
  });
250
309
  chain.reviewScores.push(review.score);
251
310
  if (passed) {
311
+ this.appendOwnerDecision(chain, 'review_passed', `Review score ${review.score}/10 met threshold ${threshold}/10`, {
312
+ agentId: chain.reviewerAgentId,
313
+ role: 'reviewer',
314
+ reviewScore: review.score,
315
+ });
252
316
  this.transition(chain, 'awaiting_gates');
253
317
  await this.checkAndRunGatesForAll();
254
318
  return;
@@ -269,6 +333,11 @@ export class WrfcController {
269
333
  this.failChain(chain, failureReason);
270
334
  return;
271
335
  }
336
+ this.appendOwnerDecision(chain, 'review_failed', `Review score ${review.score}/10 did not pass full-scope WRFC review`, {
337
+ agentId: chain.reviewerAgentId,
338
+ role: 'reviewer',
339
+ reviewScore: review.score,
340
+ });
272
341
  this.startFix(chain, review);
273
342
  }
274
343
  startFix(chain, review) {
@@ -282,8 +351,9 @@ export class WrfcController {
282
351
  maxAttempts,
283
352
  ...(targetConstraintIds.length > 0 ? { targetConstraintIds } : {}),
284
353
  });
285
- const fixerRecord = this.spawnWrfcAgent(chain, 'engineer', buildFixTask(chain.id, review, getWrfcScoreThreshold(this.configManager), chain.fixAttempts, chain.constraints, review.constraintFindings ?? []), true);
354
+ const fixerRecord = this.spawnWrfcAgent(chain, 'fixer', 'engineer', buildFixTask(chain.id, chain.task, review, getWrfcScoreThreshold(this.configManager), chain.fixAttempts, chain.constraints, review.constraintFindings ?? []), true);
286
355
  chain.fixerAgentId = fixerRecord.id;
356
+ fixerRecord.wrfcRole = 'fixer';
287
357
  chain.allAgentIds.push(fixerRecord.id);
288
358
  fixerRecord.wrfcId = chain.id;
289
359
  this.messageBus.registerAgent({
@@ -304,6 +374,11 @@ export class WrfcController {
304
374
  fixerAgentId: fixerRecord.id,
305
375
  attempt: chain.fixAttempts,
306
376
  });
377
+ this.appendOwnerDecision(chain, 'spawn_fixer', this.withRouteReason('Fix review findings while preserving the full original WRFC ask', fixerRecord), {
378
+ agentId: fixerRecord.id,
379
+ role: 'fixer',
380
+ record: fixerRecord,
381
+ });
307
382
  }
308
383
  async runGates(chain) {
309
384
  this.transition(chain, 'gating');
@@ -382,6 +457,7 @@ export class WrfcController {
382
457
  if (allPassed) {
383
458
  this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_passed' });
384
459
  chain.gatesPassed = true;
460
+ this.appendOwnerDecision(chain, 'gate_passed', 'All configured WRFC quality gates passed');
385
461
  if (autoCommit) {
386
462
  await this.autoCommit(chain);
387
463
  }
@@ -391,50 +467,54 @@ export class WrfcController {
391
467
  return;
392
468
  }
393
469
  const failedGates = results.filter((result) => !result.passed);
394
- const fingerprint = failedGates.map((result) => `${result.gate}:${result.output.slice(0, 200)}`).join('|');
395
470
  const maxGateRetries = getWrfcMaxFixAttempts(this.configManager);
396
- chain.gateFailureFingerprint = fingerprint;
397
- this.completeChainAsPassed(chain);
398
- if (chain.gateRetryDepth >= maxGateRetries) {
471
+ this.appendOwnerDecision(chain, 'gate_failed', `${failedGates.length} quality gate(s) failed and require same-chain fixing`);
472
+ if (chain.fixAttempts >= maxGateRetries) {
399
473
  logger.error('WrfcController.processGateResults: gate retry limit reached, manual intervention required', {
400
474
  chainId: chain.id,
401
- gateRetryDepth: chain.gateRetryDepth,
475
+ fixAttempts: chain.fixAttempts,
402
476
  maxGateRetries,
403
477
  });
404
- emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Gate failures exceeded max retries (${chain.gateRetryDepth}/${maxGateRetries}). Manual intervention required.`);
478
+ emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Gate failures exceeded max retries (${chain.fixAttempts}/${maxGateRetries}). Manual intervention required.`);
479
+ this.failChain(chain, `Gate failures exceeded max retries (${chain.fixAttempts}/${maxGateRetries})`);
405
480
  return;
406
481
  }
407
- const followUpTask = buildGateFailureTask(chain.id, chain.task, failedGates, chain.constraints);
408
- const followUpRecord = this.spawnWrfcAgent(chain, 'engineer', followUpTask, false);
409
- const followUpChain = this.findChainByAgentId(followUpRecord.id);
410
- if (followUpChain) {
411
- followUpChain.parentChainId = chain.id;
412
- // Inherit constraints from the parent chain as source: 'inherited'. The
413
- // inherited list is authoritative; a child engineer that drops or adds ids
414
- // surfaces a synthetic review issue instead of changing scope.
415
- if (chain.constraints.length > 0) {
416
- followUpChain.constraints = chain.constraints.map((c) => ({
417
- id: c.id,
418
- text: c.text,
419
- source: 'inherited',
420
- }));
421
- followUpChain.constraintsEnumerated = true;
422
- }
423
- }
424
- else {
425
- this.pendingParentChainIds.set(followUpRecord.id, chain.id);
426
- // Store parent constraints for inheritance when the child chain is registered later.
427
- if (chain.constraints.length > 0) {
428
- this.pendingParentConstraints.set(followUpRecord.id, chain.constraints.map((c) => ({
429
- id: c.id,
430
- text: c.text,
431
- source: 'inherited',
432
- })));
433
- }
434
- }
435
- logger.debug('WrfcController.processGateResults: gate failure — spawned follow-up agent', {
436
- parentChainId: chain.id,
437
- followUpAgentId: followUpRecord.id,
482
+ chain.fixAttempts += 1;
483
+ this.transition(chain, 'fixing');
484
+ emitWorkflowFixAttempted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
485
+ chainId: chain.id,
486
+ attempt: chain.fixAttempts,
487
+ maxAttempts: maxGateRetries,
488
+ ...(chain.constraints.length > 0 ? { targetConstraintIds: chain.constraints.map((constraint) => constraint.id) } : {}),
489
+ });
490
+ const gateFixTask = buildGateFailureTask(chain.id, chain.task, failedGates, chain.constraints);
491
+ const fixerRecord = this.spawnWrfcAgent(chain, 'fixer', 'engineer', gateFixTask, true);
492
+ fixerRecord.wrfcRole = 'fixer';
493
+ chain.fixerAgentId = fixerRecord.id;
494
+ chain.allAgentIds.push(fixerRecord.id);
495
+ fixerRecord.wrfcId = chain.id;
496
+ this.messageBus.registerAgent({
497
+ agentId: fixerRecord.id,
498
+ role: 'fixer',
499
+ wrfcId: chain.id,
500
+ });
501
+ chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `fix:${chain.fixAttempts}:gates`, 'fixer', `Gate fix attempt ${chain.fixAttempts}`, fixerRecord.id);
502
+ this.workmap.append({
503
+ ts: new Date().toISOString(),
504
+ wrfcId: chain.id,
505
+ event: 'fix_started',
506
+ agentId: fixerRecord.id,
507
+ attempt: chain.fixAttempts,
508
+ gate: failedGates.map((gate) => gate.gate).join(', '),
509
+ });
510
+ logger.debug('WrfcController.processGateResults: gate failure — spawned same-chain fixer', {
511
+ chainId: chain.id,
512
+ fixerAgentId: fixerRecord.id,
513
+ });
514
+ this.appendOwnerDecision(chain, 'spawn_gate_fixer', this.withRouteReason('Fix failed quality gates in the same WRFC owner chain', fixerRecord), {
515
+ agentId: fixerRecord.id,
516
+ role: 'fixer',
517
+ record: fixerRecord,
438
518
  });
439
519
  }
440
520
  scheduleChainCleanup(chain) {
@@ -513,7 +593,7 @@ export class WrfcController {
513
593
  }
514
594
  failChain(chain, reason) {
515
595
  if (chain.state === 'pending') {
516
- this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.engineerAgentId);
596
+ this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.ownerAgentId);
517
597
  }
518
598
  const wasActive = chain.state !== 'passed' && chain.state !== 'failed' && chain.state !== 'pending';
519
599
  this.failCurrentNode(chain, reason);
@@ -528,12 +608,69 @@ export class WrfcController {
528
608
  }
529
609
  chain.error = reason;
530
610
  chain.completedAt = Date.now();
611
+ this.cancelRunningChildren(chain);
612
+ this.appendOwnerDecision(chain, 'chain_failed', reason, {
613
+ agentId: chain.ownerAgentId,
614
+ });
615
+ this.completeOwnerAgent(chain, 'failed', reason);
531
616
  this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_failed', reason });
532
617
  emitWorkflowChainFailed(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), { chainId: chain.id, reason });
533
618
  logger.error('WrfcController.failChain', { chainId: chain.id, reason });
534
619
  this.scheduleChainCleanup(chain);
535
620
  this.safeDequeueNext();
536
621
  }
622
+ cancelRunningChildren(chain) {
623
+ for (const agentId of chain.allAgentIds) {
624
+ if (agentId === chain.ownerAgentId)
625
+ continue;
626
+ const record = this.agentManager.getStatus(agentId);
627
+ if (record?.status === 'pending' || record?.status === 'running') {
628
+ this.agentManager.cancel(agentId);
629
+ }
630
+ }
631
+ }
632
+ hasRunningChild(chain) {
633
+ return chain.allAgentIds.some((agentId) => {
634
+ if (agentId === chain.ownerAgentId)
635
+ return false;
636
+ const record = this.agentManager.getStatus(agentId);
637
+ return record?.status === 'pending' || record?.status === 'running';
638
+ });
639
+ }
640
+ cancelChain(chain, reason) {
641
+ if (chain.state === 'pending') {
642
+ this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.ownerAgentId);
643
+ }
644
+ const wasActive = chain.state !== 'pending';
645
+ this.failCurrentNode(chain, reason);
646
+ try {
647
+ this.transition(chain, 'failed');
648
+ }
649
+ catch {
650
+ chain.state = 'failed';
651
+ }
652
+ if (wasActive) {
653
+ this.activeChainCount = Math.max(0, this.activeChainCount - 1);
654
+ }
655
+ chain.error = reason;
656
+ chain.completedAt = Date.now();
657
+ chain.ownerTerminalEmitted = true;
658
+ this.appendOwnerDecision(chain, 'chain_cancelled', reason, {
659
+ agentId: chain.ownerAgentId,
660
+ });
661
+ const owner = this.agentManager.getStatus(chain.ownerAgentId);
662
+ if (owner && (owner.status === 'pending' || owner.status === 'running')) {
663
+ owner.status = 'cancelled';
664
+ owner.completedAt = Date.now();
665
+ owner.progress = reason;
666
+ }
667
+ this.cancelRunningChildren(chain);
668
+ this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_failed', reason });
669
+ emitWorkflowChainFailed(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), { chainId: chain.id, reason });
670
+ logger.warn('WrfcController.cancelChain', { chainId: chain.id, reason });
671
+ this.scheduleChainCleanup(chain);
672
+ this.safeDequeueNext();
673
+ }
537
674
  async dequeueNext() {
538
675
  if (this.chainQueue.length === 0 || this.activeChainCount >= MAX_ACTIVE_CHAINS)
539
676
  return;
@@ -565,6 +702,38 @@ export class WrfcController {
565
702
  return null;
566
703
  }
567
704
  generateWrfcId() { return `wrfc-${crypto.randomUUID().slice(0, 8)}`; }
705
+ generateDecisionId() { return `wrfc-decision-${crypto.randomUUID().slice(0, 8)}`; }
706
+ appendOwnerDecision(chain, action, reason, details = {}) {
707
+ const record = details.record ?? (details.agentId ? this.agentManager.getStatus(details.agentId) ?? undefined : undefined);
708
+ const decision = {
709
+ id: this.generateDecisionId(),
710
+ ts: new Date().toISOString(),
711
+ action,
712
+ state: chain.state,
713
+ reason,
714
+ ...(details.agentId ? { agentId: details.agentId } : {}),
715
+ ...(details.role ? { role: details.role } : {}),
716
+ ...(record?.model ? { model: record.model } : {}),
717
+ ...(record?.provider ? { provider: record.provider } : {}),
718
+ ...(record?.reasoningEffort ? { reasoningEffort: record.reasoningEffort } : {}),
719
+ ...(typeof details.reviewScore === 'number' ? { reviewScore: details.reviewScore } : {}),
720
+ };
721
+ chain.ownerDecisions.push(decision);
722
+ this.workmap.append({
723
+ ts: decision.ts,
724
+ wrfcId: chain.id,
725
+ event: 'owner_decision',
726
+ action,
727
+ state: chain.state,
728
+ reason,
729
+ ...(decision.agentId ? { agentId: decision.agentId } : {}),
730
+ ...(decision.role ? { role: decision.role } : {}),
731
+ ...(decision.model ? { model: decision.model } : {}),
732
+ ...(decision.provider ? { provider: decision.provider } : {}),
733
+ ...(decision.reasoningEffort ? { reasoningEffort: decision.reasoningEffort } : {}),
734
+ ...(typeof decision.reviewScore === 'number' ? { score: decision.reviewScore } : {}),
735
+ });
736
+ }
568
737
  completeCurrentNode(chain, summary) {
569
738
  if (!chain.currentNodeId)
570
739
  return;
@@ -577,63 +746,59 @@ export class WrfcController {
577
746
  failWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, chain.currentNodeId, error);
578
747
  chain.currentNodeId = undefined;
579
748
  }
580
- createBaseChain(engineerRecord) {
581
- // Inject the engineer constraint addendum before the runner reads the system prompt.
582
- // createBaseChain is called synchronously inside manager.spawn() before
583
- // executor.runAgent(record), so the field is visible to the runner.
584
- engineerRecord.systemPromptAddendum = '\n\n---\n\n' + buildEngineerConstraintAddendum();
749
+ createBaseChain(ownerRecord) {
585
750
  const chain = {
586
751
  id: this.generateWrfcId(),
587
752
  state: 'pending',
588
- task: engineerRecord.task,
589
- engineerAgentId: engineerRecord.id,
590
- allAgentIds: [engineerRecord.id],
753
+ task: ownerRecord.task,
754
+ ownerAgentId: ownerRecord.id,
755
+ allAgentIds: [ownerRecord.id],
591
756
  fixAttempts: 0,
592
757
  reviewCycles: 0,
593
- gateRetryDepth: 0,
594
758
  reviewScores: [],
759
+ ownerDecisions: [],
760
+ ownerTerminalEmitted: false,
595
761
  constraints: [],
596
762
  constraintsEnumerated: false,
597
763
  createdAt: Date.now(),
598
764
  };
599
765
  this.chains.set(chain.id, chain);
600
- emitWrfcGraphCreated(this.runtimeBus, this.sessionId, chain.id, `WRFC: ${engineerRecord.task}`);
601
- engineerRecord.wrfcId = chain.id;
766
+ emitWrfcGraphCreated(this.runtimeBus, this.sessionId, chain.id, `WRFC: ${ownerRecord.task}`);
767
+ ownerRecord.wrfcId = chain.id;
768
+ ownerRecord.wrfcRole = 'owner';
769
+ ownerRecord.progress = 'WRFC owner supervising child agents';
602
770
  this.messageBus.registerAgent({
603
- agentId: engineerRecord.id,
604
- template: engineerRecord.template,
771
+ agentId: ownerRecord.id,
772
+ template: ownerRecord.template,
605
773
  wrfcId: chain.id,
606
774
  });
607
- this.attachPendingParentChain(chain, engineerRecord.id);
775
+ this.appendOwnerDecision(chain, 'chain_created', 'WRFC owner created for original ask', {
776
+ agentId: ownerRecord.id,
777
+ });
608
778
  return chain;
609
779
  }
610
- attachPendingParentChain(chain, agentId) {
611
- const pendingParentId = this.pendingParentChainIds.get(agentId);
612
- if (!pendingParentId)
613
- return;
614
- chain.parentChainId = pendingParentId;
615
- const parent = this.chains.get(pendingParentId);
616
- if (parent) {
617
- chain.gateRetryDepth = parent.gateRetryDepth + (parent.gateFailureFingerprint ? 1 : 0);
618
- }
619
- this.pendingParentChainIds.delete(agentId);
620
- // Inherit constraints from parent when they were queued via the pending path.
621
- // The inherited list is authoritative; child output is checked for continuity
622
- // on completion instead of being allowed to change scope.
623
- const inherited = this.pendingParentConstraints.get(agentId);
624
- if (inherited && inherited.length > 0) {
625
- chain.constraints = inherited;
626
- chain.constraintsEnumerated = true;
627
- }
628
- this.pendingParentConstraints.delete(agentId);
629
- }
630
780
  startEngineeringChain(chain, emitCreated) {
631
781
  this.activeChainCount += 1;
632
782
  this.transition(chain, 'engineering');
633
- chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `engineer:${chain.fixAttempts}`, 'engineer', 'Engineer implementation', chain.engineerAgentId);
783
+ const engineerRecord = this.spawnWrfcAgent(chain, 'engineer', 'engineer', chain.task, true);
784
+ engineerRecord.wrfcRole = 'engineer';
785
+ chain.engineerAgentId = engineerRecord.id;
786
+ chain.allAgentIds.push(engineerRecord.id);
787
+ engineerRecord.wrfcId = chain.id;
788
+ this.messageBus.registerAgent({
789
+ agentId: engineerRecord.id,
790
+ role: 'engineer',
791
+ wrfcId: chain.id,
792
+ });
793
+ chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `engineer:${chain.fixAttempts}`, 'engineer', 'Engineer implementation', engineerRecord.id);
634
794
  if (emitCreated) {
635
795
  emitWrfcChainCreated(this.runtimeBus, this.sessionId, chain.id, chain.task);
636
796
  }
797
+ this.appendOwnerDecision(chain, 'spawn_engineer', this.withRouteReason('Start WRFC implementation child for the original ask', engineerRecord), {
798
+ agentId: engineerRecord.id,
799
+ role: 'engineer',
800
+ record: engineerRecord,
801
+ });
637
802
  }
638
803
  handleEngineerCompletion(chain, agentId, report) {
639
804
  this.completeCurrentNode(chain, report.summary);
@@ -683,31 +848,82 @@ export class WrfcController {
683
848
  }
684
849
  this.startReview(chain, report);
685
850
  }
686
- spawnWrfcAgent(chain, template, task, dangerouslyDisableWrfc) {
687
- const sourceAgent = [chain.fixerAgentId, chain.engineerAgentId]
688
- .filter((value) => typeof value === 'string')
689
- .map((agentId) => this.agentManager.getStatus(agentId))
690
- .find((record) => record != null) ?? null;
691
- return this.agentManager.spawn({
851
+ spawnWrfcAgent(chain, role, template, task, dangerouslyDisableWrfc) {
852
+ const owner = this.agentManager.getStatus(chain.ownerAgentId);
853
+ const selectedRoute = this.selectChildRoute?.({ chain, role, task, ownerAgent: owner }) ?? null;
854
+ const model = selectedRoute?.model ?? owner?.model;
855
+ const provider = selectedRoute?.provider ?? owner?.provider;
856
+ const fallbackModels = selectedRoute?.fallbackModels ?? owner?.fallbackModels;
857
+ const routing = selectedRoute?.routing ?? owner?.routing;
858
+ const reasoningEffort = selectedRoute?.reasoningEffort ?? owner?.reasoningEffort;
859
+ const record = this.agentManager.spawn({
692
860
  mode: 'spawn',
693
861
  task,
694
862
  template,
695
- ...(sourceAgent?.model ? { model: sourceAgent.model } : {}),
696
- ...(sourceAgent?.provider ? { provider: sourceAgent.provider } : {}),
697
- ...(sourceAgent?.fallbackModels?.length ? { fallbackModels: [...sourceAgent.fallbackModels] } : {}),
698
- ...(sourceAgent?.routing ? { routing: sourceAgent.routing } : {}),
699
- ...(sourceAgent?.reasoningEffort ? { reasoningEffort: sourceAgent.reasoningEffort } : {}),
863
+ parentAgentId: chain.ownerAgentId,
864
+ ...(model ? { model } : {}),
865
+ ...(provider ? { provider } : {}),
866
+ ...(fallbackModels?.length ? { fallbackModels: [...fallbackModels] } : {}),
867
+ ...(routing ? { routing } : {}),
868
+ ...(reasoningEffort ? { reasoningEffort } : {}),
869
+ ...(template === 'engineer' ? { systemPromptAddendum: '\n\n---\n\n' + buildEngineerConstraintAddendum() } : {}),
700
870
  ...(dangerouslyDisableWrfc ? { dangerously_disable_wrfc: true } : {}),
701
871
  });
872
+ record.wrfcId = chain.id;
873
+ if (selectedRoute?.reason) {
874
+ record.wrfcRouteReason = selectedRoute.reason;
875
+ }
876
+ return record;
877
+ }
878
+ withRouteReason(baseReason, record) {
879
+ return record.wrfcRouteReason ? `${baseReason}; route: ${record.wrfcRouteReason}` : baseReason;
702
880
  }
703
881
  completeChainAsPassed(chain) {
704
882
  this.activeChainCount = Math.max(0, this.activeChainCount - 1);
705
883
  this.transition(chain, 'passed');
706
884
  chain.completedAt = Date.now();
885
+ this.appendOwnerDecision(chain, 'chain_passed', 'WRFC full-scope review and quality gates passed', {
886
+ agentId: chain.ownerAgentId,
887
+ });
888
+ this.completeOwnerAgent(chain, 'completed', `WRFC chain ${chain.id} passed`);
707
889
  emitWrfcChainPassed(this.runtimeBus, this.sessionId, chain.id);
708
890
  this.scheduleChainCleanup(chain);
709
891
  this.safeDequeueNext();
710
892
  }
893
+ completeOwnerAgent(chain, status, message) {
894
+ if (chain.ownerTerminalEmitted)
895
+ return;
896
+ const owner = this.agentManager.getStatus(chain.ownerAgentId);
897
+ if (!owner)
898
+ return;
899
+ owner.status = status;
900
+ owner.completedAt = Date.now();
901
+ owner.progress = message;
902
+ owner.fullOutput = message;
903
+ chain.ownerTerminalEmitted = true;
904
+ const context = {
905
+ sessionId: this.sessionId,
906
+ traceId: `${this.sessionId}:wrfc-owner:${chain.id}:${status}`,
907
+ source: 'wrfc-controller',
908
+ agentId: owner.id,
909
+ };
910
+ if (status === 'completed') {
911
+ emitAgentCompleted(this.runtimeBus, context, {
912
+ agentId: owner.id,
913
+ durationMs: Math.max(0, owner.completedAt - owner.startedAt),
914
+ output: message,
915
+ toolCallsMade: owner.toolCallCount,
916
+ });
917
+ }
918
+ else {
919
+ owner.error = message;
920
+ emitAgentFailed(this.runtimeBus, context, {
921
+ agentId: owner.id,
922
+ durationMs: Math.max(0, owner.completedAt - owner.startedAt),
923
+ error: message,
924
+ });
925
+ }
926
+ }
711
927
  safeDequeueNext() {
712
928
  this.dequeueNext().catch((error) => {
713
929
  logger.error('WrfcController.dequeueNext unhandled error', { error: summarizeError(error) });