opencode-swarm 6.22.3 → 6.22.4

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.
package/dist/index.js CHANGED
@@ -47321,406 +47321,32 @@ function maskToolOutput(msg, _threshold) {
47321
47321
  }
47322
47322
  // src/hooks/delegation-gate.ts
47323
47323
  init_schema();
47324
- function extractTaskLine(text) {
47325
- const match = text.match(/TASK:\s*(.+?)(?:\n|$)/i);
47326
- return match ? match[1].trim() : null;
47327
- }
47328
- function createDelegationGateHook(config3) {
47329
- const enabled = config3.hooks?.delegation_gate !== false;
47330
- const delegationMaxChars = config3.hooks?.delegation_max_chars ?? 4000;
47331
- if (!enabled) {
47332
- return {
47333
- messagesTransform: async (_input, _output) => {},
47334
- toolAfter: async () => {}
47335
- };
47336
- }
47337
- const toolAfter = async (input, _output) => {
47338
- if (!input.sessionID)
47339
- return;
47340
- const session = swarmState.agentSessions.get(input.sessionID);
47341
- if (!session)
47342
- return;
47343
- const normalized = input.tool.replace(/^[^:]+[:.]/, "");
47344
- if (normalized === "Task" || normalized === "task") {
47345
- const delegationChain = swarmState.delegationChains.get(input.sessionID);
47346
- if (delegationChain && delegationChain.length > 0) {
47347
- let lastCoderIndex = -1;
47348
- for (let i2 = delegationChain.length - 1;i2 >= 0; i2--) {
47349
- const target = stripKnownSwarmPrefix(delegationChain[i2].to);
47350
- if (target.includes("coder")) {
47351
- lastCoderIndex = i2;
47352
- break;
47353
- }
47354
- }
47355
- if (lastCoderIndex === -1)
47356
- return;
47357
- const afterCoder = delegationChain.slice(lastCoderIndex);
47358
- let hasReviewer = false;
47359
- let hasTestEngineer = false;
47360
- for (const delegation of afterCoder) {
47361
- const target = stripKnownSwarmPrefix(delegation.to);
47362
- if (target === "reviewer")
47363
- hasReviewer = true;
47364
- if (target === "test_engineer")
47365
- hasTestEngineer = true;
47366
- }
47367
- if (hasReviewer && hasTestEngineer) {
47368
- session.qaSkipCount = 0;
47369
- session.qaSkipTaskIds = [];
47370
- }
47371
- if (hasReviewer && session.taskWorkflowStates) {
47372
- for (const [taskId, state] of session.taskWorkflowStates) {
47373
- if (state === "coder_delegated" || state === "pre_check_passed") {
47374
- try {
47375
- advanceTaskState(session, taskId, "reviewer_run");
47376
- } catch {}
47377
- }
47378
- }
47379
- }
47380
- if (hasReviewer && hasTestEngineer && session.taskWorkflowStates) {
47381
- for (const [taskId, state] of session.taskWorkflowStates) {
47382
- if (state === "reviewer_run") {
47383
- try {
47384
- advanceTaskState(session, taskId, "tests_run");
47385
- } catch {}
47386
- }
47387
- }
47388
- }
47389
- }
47390
- }
47391
- };
47392
- return {
47393
- messagesTransform: async (_input, output) => {
47394
- const messages = output.messages;
47395
- if (!messages || messages.length === 0)
47396
- return;
47397
- let lastUserMessageIndex = -1;
47398
- for (let i2 = messages.length - 1;i2 >= 0; i2--) {
47399
- if (messages[i2]?.info?.role === "user") {
47400
- lastUserMessageIndex = i2;
47401
- break;
47402
- }
47403
- }
47404
- if (lastUserMessageIndex === -1)
47405
- return;
47406
- const lastUserMessage = messages[lastUserMessageIndex];
47407
- if (!lastUserMessage?.parts)
47408
- return;
47409
- const agent = lastUserMessage.info?.agent;
47410
- const strippedAgent = agent ? stripKnownSwarmPrefix(agent) : undefined;
47411
- if (strippedAgent && strippedAgent !== "architect")
47412
- return;
47413
- const textPartIndex = lastUserMessage.parts.findIndex((p) => p?.type === "text" && p.text !== undefined);
47414
- if (textPartIndex === -1)
47415
- return;
47416
- const textPart = lastUserMessage.parts[textPartIndex];
47417
- const text = textPart.text ?? "";
47418
- const taskDisclosureSessionID = lastUserMessage.info?.sessionID;
47419
- if (taskDisclosureSessionID) {
47420
- const taskSession = ensureAgentSession(taskDisclosureSessionID);
47421
- const currentTaskIdForWindow = taskSession.currentTaskId;
47422
- if (currentTaskIdForWindow) {
47423
- const taskLineRegex = /^[ \t]*-[ \t]*(?:\[[ x]\][ \t]+)?(\d+\.\d+(?:\.\d+)*)[:. ].*/gm;
47424
- const taskLines = [];
47425
- taskLineRegex.lastIndex = 0;
47426
- let regexMatch = taskLineRegex.exec(text);
47427
- while (regexMatch !== null) {
47428
- taskLines.push({
47429
- line: regexMatch[0],
47430
- taskId: regexMatch[1],
47431
- index: regexMatch.index
47432
- });
47433
- regexMatch = taskLineRegex.exec(text);
47434
- }
47435
- if (taskLines.length > 5) {
47436
- const currentIdx = taskLines.findIndex((t) => t.taskId === currentTaskIdForWindow);
47437
- const windowStart = Math.max(0, currentIdx - 2);
47438
- const windowEnd = Math.min(taskLines.length - 1, currentIdx + 3);
47439
- const visibleTasks = taskLines.slice(windowStart, windowEnd + 1);
47440
- const hiddenBefore = windowStart;
47441
- const hiddenAfter = taskLines.length - 1 - windowEnd;
47442
- const totalTasks = taskLines.length;
47443
- const visibleCount = visibleTasks.length;
47444
- const firstTaskIndex = taskLines[0].index;
47445
- const lastTask = taskLines[taskLines.length - 1];
47446
- const lastTaskEnd = lastTask.index + lastTask.line.length;
47447
- const before = text.slice(0, firstTaskIndex);
47448
- const after = text.slice(lastTaskEnd);
47449
- const visibleLines = visibleTasks.map((t) => t.line).join(`
47450
- `);
47451
- const trimComment = `[Task window: showing ${visibleCount} of ${totalTasks} tasks]`;
47452
- const trimmedMiddle = (hiddenBefore > 0 ? `[...${hiddenBefore} tasks hidden...]
47453
- ` : "") + visibleLines + (hiddenAfter > 0 ? `
47454
- [...${hiddenAfter} tasks hidden...]` : "");
47455
- textPart.text = `${before}${trimmedMiddle}
47456
- ${trimComment}${after}`;
47457
- }
47458
- }
47459
- }
47460
- const sessionID = lastUserMessage.info?.sessionID;
47461
- const taskIdMatch = text.match(/TASK:\s*(.+?)(?:\n|$)/i);
47462
- const currentTaskId = taskIdMatch ? taskIdMatch[1].trim() : null;
47463
- const coderDelegationPattern = /(?:^|\n)\s*(?:\w+_)?coder\s*\n\s*TASK:/i;
47464
- const isCoderDelegation = coderDelegationPattern.test(text);
47465
- const priorCoderTaskId = sessionID ? ensureAgentSession(sessionID).lastCoderDelegationTaskId ?? null : null;
47466
- if (sessionID && isCoderDelegation && currentTaskId) {
47467
- const session = ensureAgentSession(sessionID);
47468
- session.lastCoderDelegationTaskId = currentTaskId;
47469
- const fileDirPattern = /^FILE:\s*(.+)$/gm;
47470
- const declaredFiles = [];
47471
- for (const match of text.matchAll(fileDirPattern)) {
47472
- const filePath = match[1].trim();
47473
- if (filePath.length > 0 && !declaredFiles.includes(filePath)) {
47474
- declaredFiles.push(filePath);
47475
- }
47476
- }
47477
- session.declaredCoderScope = declaredFiles.length > 0 ? declaredFiles : null;
47478
- try {
47479
- advanceTaskState(session, currentTaskId, "coder_delegated");
47480
- } catch (err2) {
47481
- console.warn(`[delegation-gate] state machine warn: ${err2 instanceof Error ? err2.message : String(err2)}`);
47482
- }
47483
- }
47484
- if (sessionID && !isCoderDelegation && currentTaskId) {
47485
- const session = ensureAgentSession(sessionID);
47486
- if (session.architectWriteCount > 0 && session.lastCoderDelegationTaskId !== currentTaskId) {
47487
- const warningText2 = `\u26A0\uFE0F DELEGATION VIOLATION: Code modifications detected for task ${currentTaskId} with zero coder delegations.
47488
- Rule 1: DELEGATE all coding to coder. You do NOT write code.`;
47489
- textPart.text = `${warningText2}
47490
-
47491
- ${text}`;
47492
- }
47493
- }
47494
- {
47495
- const deliberationSessionID = lastUserMessage.info?.sessionID;
47496
- if (deliberationSessionID) {
47497
- if (!/^[a-zA-Z0-9_-]{1,128}$/.test(deliberationSessionID)) {} else {
47498
- const deliberationSession = ensureAgentSession(deliberationSessionID);
47499
- const lastGate = deliberationSession.lastGateOutcome;
47500
- let preamble;
47501
- if (lastGate) {
47502
- const gateResult = lastGate.passed ? "PASSED" : "FAILED";
47503
- const sanitizedGate = lastGate.gate.replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 64);
47504
- const sanitizedTaskId = lastGate.taskId.replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 32);
47505
- preamble = `[Last gate: ${sanitizedGate} ${gateResult} for task ${sanitizedTaskId}]
47506
- [DELIBERATE: Before proceeding \u2014 what is the SINGLE next task? What gates must it pass?]`;
47507
- } else {
47508
- preamble = `[DELIBERATE: Identify the first task from the plan. What gates must it pass before marking complete?]`;
47509
- }
47510
- const currentText = textPart.text ?? "";
47511
- textPart.text = `${preamble}
47512
-
47513
- ${currentText}`;
47514
- }
47515
- }
47516
- }
47517
- if (!isCoderDelegation)
47518
- return;
47519
- const warnings = [];
47520
- if (text.length > delegationMaxChars) {
47521
- warnings.push(`Delegation exceeds recommended size (${text.length} chars, limit ${delegationMaxChars}). Consider splitting into smaller tasks.`);
47522
- }
47523
- const fileMatches = text.match(/^FILE:/gm);
47524
- if (fileMatches && fileMatches.length > 1) {
47525
- warnings.push(`Multiple FILE: directives detected (${fileMatches.length}). Each coder task should target ONE file.`);
47526
- }
47527
- const taskMatches = text.match(/^TASK:/gm);
47528
- if (taskMatches && taskMatches.length > 1) {
47529
- warnings.push(`Multiple TASK: sections detected (${taskMatches.length}). Send ONE task per coder call.`);
47530
- }
47531
- const batchingPattern = /\b(?:and also|then also|additionally|as well as|along with|while you'?re at it)[.,]?\b/gi;
47532
- const batchingMatches = text.match(batchingPattern);
47533
- if (batchingMatches && batchingMatches.length > 0) {
47534
- warnings.push(`Batching language detected (${batchingMatches.join(", ")}). Break compound objectives into separate coder calls.`);
47535
- }
47536
- const taskLine = extractTaskLine(text);
47537
- if (taskLine) {
47538
- const andPattern = /\s+and\s+(update|add|remove|modify|refactor|implement|create|delete|fix|change|build|deploy|write|test|move|rename|extend|extract|convert|migrate|upgrade|replace)\b/i;
47539
- if (andPattern.test(taskLine)) {
47540
- warnings.push('TASK line contains "and" connecting separate actions');
47541
- }
47542
- }
47543
- if (sessionID) {
47544
- const delegationChain = swarmState.delegationChains.get(sessionID);
47545
- if (delegationChain && delegationChain.length >= 2) {
47546
- const coderIndices = [];
47547
- for (let i2 = delegationChain.length - 1;i2 >= 0; i2--) {
47548
- if (stripKnownSwarmPrefix(delegationChain[i2].to).includes("coder")) {
47549
- coderIndices.unshift(i2);
47550
- if (coderIndices.length === 2)
47551
- break;
47552
- }
47553
- }
47554
- if (coderIndices.length === 2) {
47555
- const prevCoderIndex = coderIndices[0];
47556
- const betweenCoders = delegationChain.slice(prevCoderIndex + 1);
47557
- const hasReviewer = betweenCoders.some((d) => stripKnownSwarmPrefix(d.to) === "reviewer");
47558
- const hasTestEngineer = betweenCoders.some((d) => stripKnownSwarmPrefix(d.to) === "test_engineer");
47559
- const session = ensureAgentSession(sessionID);
47560
- const priorTaskStuckAtCoder = priorCoderTaskId !== null && getTaskState(session, priorCoderTaskId) === "coder_delegated";
47561
- if (!hasReviewer || !hasTestEngineer || priorTaskStuckAtCoder) {
47562
- if (session.qaSkipCount >= 1) {
47563
- const skippedTasks = session.qaSkipTaskIds.join(", ");
47564
- throw new Error(`\uD83D\uDED1 QA GATE ENFORCEMENT: ${session.qaSkipCount + 1} consecutive coder delegations without reviewer/test_engineer. ` + `Skipped tasks: [${skippedTasks}]. ` + `DELEGATE to reviewer and test_engineer NOW before any further coder work.`);
47565
- }
47566
- session.qaSkipCount++;
47567
- session.qaSkipTaskIds.push(currentTaskId ?? "unknown");
47568
- warnings.push(`\u26A0\uFE0F PROTOCOL VIOLATION: Previous coder task completed, but QA gate was skipped. ` + `You MUST delegate to reviewer (code review) and test_engineer (test execution) ` + `before starting a new coder task. Review RULES 7-8 in your system prompt.`);
47569
- }
47570
- }
47571
- }
47572
- }
47573
- if (warnings.length === 0)
47574
- return;
47575
- const warningLines = warnings.map((w) => `Detected signal: ${w}`);
47576
- const warningText = `\u26A0\uFE0F BATCH DETECTED: Your coder delegation appears to contain multiple tasks.
47577
- Rule 3: ONE task per coder call. Split this into separate delegations.
47578
- ${warningLines.join(`
47579
- `)}`;
47580
- const originalText = textPart.text ?? "";
47581
- textPart.text = `${warningText}
47582
-
47583
- ${originalText}`;
47584
- },
47585
- toolAfter
47586
- };
47587
- }
47588
- // src/hooks/delegation-sanitizer.ts
47589
- init_utils2();
47590
- import * as fs13 from "fs";
47591
- var SANITIZATION_PATTERNS = [
47592
- /\b\d+(st|nd|rd|th)\s+(attempt|try|time)\b/gi,
47593
- /\b(5th|fifth|final|last)\s+attempt\b/gi,
47594
- /attempt\s+\d+\s*\/\s*\d+/gi,
47595
- /\bthis\s+is\s+(the\s+)?(5th|fifth|final|last)\b/gi,
47596
- /\bwe('re|\s+are)\s+(behind|late)\b/gi,
47597
- /\buser\s+is\s+waiting\b/gi,
47598
- /\bship\s+(this|it)\s+now\b/gi,
47599
- /\bor\s+I('ll|\s+will)\s+(stop|halt|alert)\b/gi,
47600
- /\bor\s+all\s+work\s+stops\b/gi,
47601
- /\bthis\s+will\s+(delay|block)\s+everything\b/gi,
47602
- /\b(I'm|I\s+am)\s+(frustrated|disappointed)\b/gi
47603
- ];
47604
- function sanitizeMessage(text, patterns = SANITIZATION_PATTERNS) {
47605
- let sanitized = text;
47606
- const stripped = [];
47607
- for (const pattern of patterns) {
47608
- const matches = sanitized.match(pattern);
47609
- if (matches) {
47610
- stripped.push(...matches);
47611
- sanitized = sanitized.replace(pattern, "");
47612
- }
47613
- }
47614
- sanitized = sanitized.replace(/\s+/g, " ").trim();
47615
- return {
47616
- sanitized,
47617
- modified: stripped.length > 0,
47618
- stripped
47619
- };
47620
- }
47621
- function isGateAgentMessage(agentName) {
47622
- const gateAgents = ["reviewer", "test_engineer", "critic", "test-engineer"];
47623
- const normalized = agentName.toLowerCase().replace(/-/g, "_");
47624
- return gateAgents.includes(normalized);
47625
- }
47626
- function createDelegationSanitizerHook(directory) {
47627
- const hook = async (_input, output) => {
47628
- const messages = output?.messages;
47629
- if (!messages || !Array.isArray(messages)) {
47630
- return;
47631
- }
47632
- for (const message of messages) {
47633
- const info2 = message?.info;
47634
- if (!info2)
47635
- continue;
47636
- const agent = info2.agent;
47637
- if (!agent || !isGateAgentMessage(agent)) {
47638
- continue;
47639
- }
47640
- if (!message.parts || !Array.isArray(message.parts)) {
47641
- continue;
47642
- }
47643
- for (const part of message.parts) {
47644
- if (part?.type !== "text" || !part.text) {
47645
- continue;
47646
- }
47647
- const originalText = part.text;
47648
- const result = sanitizeMessage(originalText);
47649
- if (result.modified) {
47650
- part.text = result.sanitized;
47651
- try {
47652
- const eventsPath = validateSwarmPath(directory, "events.jsonl");
47653
- const event = {
47654
- event: "message_sanitized",
47655
- agent,
47656
- original_length: originalText.length,
47657
- stripped_count: result.stripped.length,
47658
- stripped_patterns: result.stripped,
47659
- timestamp: new Date().toISOString()
47660
- };
47661
- fs13.appendFileSync(eventsPath, `${JSON.stringify(event)}
47662
- `, "utf-8");
47663
- } catch {}
47664
- }
47665
- }
47666
- }
47667
- };
47668
- return safeHook(hook);
47669
- }
47670
- // src/hooks/delegation-tracker.ts
47671
- init_constants();
47672
- init_schema();
47673
- function createDelegationTrackerHook(config3, guardrailsEnabled = true) {
47674
- return async (input, _output) => {
47675
- const now = Date.now();
47676
- if (!input.agent || input.agent === "") {
47677
- const session2 = swarmState.agentSessions.get(input.sessionID);
47678
- if (session2?.delegationActive) {
47679
- session2.delegationActive = false;
47680
- swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
47681
- ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
47682
- updateAgentEventTime(input.sessionID);
47683
- } else if (!session2) {
47684
- ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
47685
- }
47686
- return;
47687
- }
47688
- const agentName = input.agent;
47689
- const previousAgent = swarmState.activeAgent.get(input.sessionID);
47690
- swarmState.activeAgent.set(input.sessionID, agentName);
47691
- const strippedAgent = stripKnownSwarmPrefix(agentName);
47692
- const isArchitect = strippedAgent === ORCHESTRATOR_NAME;
47693
- const session = ensureAgentSession(input.sessionID, agentName);
47694
- session.delegationActive = !isArchitect;
47695
- recordPhaseAgentDispatch(input.sessionID, agentName);
47696
- if (!isArchitect && guardrailsEnabled) {
47697
- beginInvocation(input.sessionID, agentName);
47698
- }
47699
- const delegationTrackerEnabled = config3.hooks?.delegation_tracker === true;
47700
- const delegationGateEnabled = config3.hooks?.delegation_gate !== false;
47701
- if ((delegationTrackerEnabled || delegationGateEnabled) && previousAgent && previousAgent !== agentName) {
47702
- const entry = {
47703
- from: previousAgent,
47704
- to: agentName,
47705
- timestamp: now
47706
- };
47707
- if (!swarmState.delegationChains.has(input.sessionID)) {
47708
- swarmState.delegationChains.set(input.sessionID, []);
47709
- }
47710
- const chain = swarmState.delegationChains.get(input.sessionID);
47711
- chain?.push(entry);
47712
- if (delegationTrackerEnabled) {
47713
- swarmState.pendingEvents++;
47714
- }
47715
- }
47716
- };
47717
- }
47324
+
47718
47325
  // src/hooks/guardrails.ts
47719
47326
  init_constants();
47720
47327
  init_schema();
47721
47328
  init_manager2();
47722
47329
  import * as path25 from "path";
47723
47330
  init_utils();
47331
+ var storedInputArgs = new Map;
47332
+ function debugStoredArgs(action, callID, extra) {
47333
+ const args2 = storedInputArgs.get(callID);
47334
+ const argsObj = args2;
47335
+ const subagentType = argsObj?.subagent_type;
47336
+ console.log(`[swarm-debug-task] stored-args.${action} | callID=${callID} subagent_type=${subagentType ?? "(none)"}`, extra ? JSON.stringify(extra) : "");
47337
+ }
47338
+ function getStoredInputArgs(callID) {
47339
+ debugStoredArgs("get", callID);
47340
+ return storedInputArgs.get(callID);
47341
+ }
47342
+ function setStoredInputArgs(callID, args2) {
47343
+ debugStoredArgs("set", callID);
47344
+ storedInputArgs.set(callID, args2);
47345
+ }
47346
+ function deleteStoredInputArgs(callID) {
47347
+ debugStoredArgs("delete", callID);
47348
+ storedInputArgs.delete(callID);
47349
+ }
47724
47350
  function extractPhaseNumber(phaseString) {
47725
47351
  if (!phaseString)
47726
47352
  return 1;
@@ -47854,7 +47480,6 @@ function createGuardrailsHooks(directoryOrConfig, config3) {
47854
47480
  };
47855
47481
  }
47856
47482
  const cfg = guardrailsConfig;
47857
- const inputArgsByCallID = new Map;
47858
47483
  return {
47859
47484
  toolBefore: async (input, output) => {
47860
47485
  const currentSession = swarmState.agentSessions.get(input.sessionID);
@@ -47868,495 +47493,1001 @@ function createGuardrailsHooks(directoryOrConfig, config3) {
47868
47493
  }
47869
47494
  }
47870
47495
  }
47871
- } else if (isArchitect(input.sessionID)) {
47872
- const coderDelegArgs = output.args;
47873
- const coderDeleg = isAgentDelegation(input.tool, coderDelegArgs);
47874
- if (coderDeleg.isDelegation && coderDeleg.targetAgent === "coder") {
47875
- const coderSession = swarmState.agentSessions.get(input.sessionID);
47876
- if (coderSession) {
47877
- coderSession.modifiedFilesThisCoderTask = [];
47878
- }
47496
+ } else if (isArchitect(input.sessionID)) {
47497
+ const coderDelegArgs = output.args;
47498
+ const coderDeleg = isAgentDelegation(input.tool, coderDelegArgs);
47499
+ if (coderDeleg.isDelegation && coderDeleg.targetAgent === "coder") {
47500
+ const coderSession = swarmState.agentSessions.get(input.sessionID);
47501
+ if (coderSession) {
47502
+ coderSession.modifiedFilesThisCoderTask = [];
47503
+ }
47504
+ }
47505
+ }
47506
+ if (isArchitect(input.sessionID) && isWriteTool(input.tool)) {
47507
+ const args2 = output.args;
47508
+ const targetPath = args2?.filePath ?? args2?.path ?? args2?.file ?? args2?.target;
47509
+ if (typeof targetPath === "string" && targetPath.length > 0) {
47510
+ const resolvedTarget = path25.resolve(directory, targetPath).toLowerCase();
47511
+ const planMdPath = path25.resolve(directory, ".swarm", "plan.md").toLowerCase();
47512
+ const planJsonPath = path25.resolve(directory, ".swarm", "plan.json").toLowerCase();
47513
+ if (resolvedTarget === planMdPath || resolvedTarget === planJsonPath) {
47514
+ throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47515
+ }
47516
+ }
47517
+ if (!targetPath && (input.tool === "apply_patch" || input.tool === "patch")) {
47518
+ const patchText = args2?.input ?? args2?.patch ?? (Array.isArray(args2?.cmd) ? args2.cmd[1] : undefined);
47519
+ if (typeof patchText === "string" && patchText.length <= 1e6) {
47520
+ const patchPathPattern = /\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)/gi;
47521
+ const diffPathPattern = /\+\+\+\s+b\/(.+)/gm;
47522
+ const paths = new Set;
47523
+ for (const match of patchText.matchAll(patchPathPattern)) {
47524
+ paths.add(match[1].trim());
47525
+ }
47526
+ for (const match of patchText.matchAll(diffPathPattern)) {
47527
+ const p = match[1].trim();
47528
+ if (p !== "/dev/null")
47529
+ paths.add(p);
47530
+ }
47531
+ const gitDiffPathPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
47532
+ for (const match of patchText.matchAll(gitDiffPathPattern)) {
47533
+ const aPath = match[1].trim();
47534
+ const bPath = match[2].trim();
47535
+ if (aPath !== "/dev/null")
47536
+ paths.add(aPath);
47537
+ if (bPath !== "/dev/null")
47538
+ paths.add(bPath);
47539
+ }
47540
+ const minusPathPattern = /^---\s+a\/(.+)$/gm;
47541
+ for (const match of patchText.matchAll(minusPathPattern)) {
47542
+ const p = match[1].trim();
47543
+ if (p !== "/dev/null")
47544
+ paths.add(p);
47545
+ }
47546
+ const traditionalMinusPattern = /^---\s+([^\s].+?)(?:\t.*)?$/gm;
47547
+ const traditionalPlusPattern = /^\+\+\+\s+([^\s].+?)(?:\t.*)?$/gm;
47548
+ for (const match of patchText.matchAll(traditionalMinusPattern)) {
47549
+ const p = match[1].trim();
47550
+ if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
47551
+ paths.add(p);
47552
+ }
47553
+ }
47554
+ for (const match of patchText.matchAll(traditionalPlusPattern)) {
47555
+ const p = match[1].trim();
47556
+ if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
47557
+ paths.add(p);
47558
+ }
47559
+ }
47560
+ for (const p of paths) {
47561
+ const resolvedP = path25.resolve(directory, p);
47562
+ const planMdPath = path25.resolve(directory, ".swarm", "plan.md").toLowerCase();
47563
+ const planJsonPath = path25.resolve(directory, ".swarm", "plan.json").toLowerCase();
47564
+ if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
47565
+ throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47566
+ }
47567
+ if (isOutsideSwarmDir(p, directory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
47568
+ const session2 = swarmState.agentSessions.get(input.sessionID);
47569
+ if (session2) {
47570
+ session2.architectWriteCount++;
47571
+ warn("Architect direct code edit detected via apply_patch", {
47572
+ tool: input.tool,
47573
+ sessionID: input.sessionID,
47574
+ targetPath: p,
47575
+ writeCount: session2.architectWriteCount
47576
+ });
47577
+ }
47578
+ break;
47579
+ }
47580
+ }
47581
+ }
47582
+ }
47583
+ if (typeof targetPath === "string" && targetPath.length > 0 && isOutsideSwarmDir(targetPath, directory) && isSourceCodePath(path25.relative(directory, path25.resolve(directory, targetPath)))) {
47584
+ const session2 = swarmState.agentSessions.get(input.sessionID);
47585
+ if (session2) {
47586
+ session2.architectWriteCount++;
47587
+ warn("Architect direct code edit detected", {
47588
+ tool: input.tool,
47589
+ sessionID: input.sessionID,
47590
+ targetPath,
47591
+ writeCount: session2.architectWriteCount
47592
+ });
47593
+ if (session2.lastGateFailure && Date.now() - session2.lastGateFailure.timestamp < 120000) {
47594
+ const failedGate = session2.lastGateFailure.tool;
47595
+ const failedTaskId = session2.lastGateFailure.taskId;
47596
+ warn("Self-fix after gate failure detected", {
47597
+ failedGate,
47598
+ failedTaskId,
47599
+ currentTool: input.tool,
47600
+ sessionID: input.sessionID
47601
+ });
47602
+ session2.selfFixAttempted = true;
47603
+ }
47604
+ }
47605
+ }
47606
+ }
47607
+ const rawActiveAgent = swarmState.activeAgent.get(input.sessionID);
47608
+ const strippedAgent = rawActiveAgent ? stripKnownSwarmPrefix(rawActiveAgent) : undefined;
47609
+ if (strippedAgent === ORCHESTRATOR_NAME) {
47610
+ return;
47611
+ }
47612
+ const existingSession = swarmState.agentSessions.get(input.sessionID);
47613
+ if (existingSession) {
47614
+ const sessionAgent = stripKnownSwarmPrefix(existingSession.agentName);
47615
+ if (sessionAgent === ORCHESTRATOR_NAME) {
47616
+ return;
47617
+ }
47618
+ }
47619
+ const agentName = swarmState.activeAgent.get(input.sessionID) ?? ORCHESTRATOR_NAME;
47620
+ const session = ensureAgentSession(input.sessionID, agentName);
47621
+ const resolvedName = stripKnownSwarmPrefix(session.agentName);
47622
+ if (resolvedName === ORCHESTRATOR_NAME) {
47623
+ return;
47624
+ }
47625
+ const agentConfig = resolveGuardrailsConfig(cfg, session.agentName);
47626
+ if (agentConfig.max_duration_minutes === 0 && agentConfig.max_tool_calls === 0) {
47627
+ return;
47628
+ }
47629
+ if (!getActiveWindow(input.sessionID)) {
47630
+ const fallbackAgent = swarmState.activeAgent.get(input.sessionID) ?? session.agentName;
47631
+ const stripped = stripKnownSwarmPrefix(fallbackAgent);
47632
+ if (stripped !== ORCHESTRATOR_NAME) {
47633
+ beginInvocation(input.sessionID, fallbackAgent);
47634
+ }
47635
+ }
47636
+ const window2 = getActiveWindow(input.sessionID);
47637
+ if (!window2) {
47638
+ return;
47639
+ }
47640
+ if (window2.hardLimitHit) {
47641
+ throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
47642
+ }
47643
+ window2.toolCalls++;
47644
+ const hash3 = hashArgs(output.args);
47645
+ window2.recentToolCalls.push({
47646
+ tool: input.tool,
47647
+ argsHash: hash3,
47648
+ timestamp: Date.now()
47649
+ });
47650
+ if (window2.recentToolCalls.length > 20) {
47651
+ window2.recentToolCalls.shift();
47652
+ }
47653
+ let repetitionCount = 0;
47654
+ if (window2.recentToolCalls.length > 0) {
47655
+ const lastEntry = window2.recentToolCalls[window2.recentToolCalls.length - 1];
47656
+ for (let i2 = window2.recentToolCalls.length - 1;i2 >= 0; i2--) {
47657
+ const entry = window2.recentToolCalls[i2];
47658
+ if (entry.tool === lastEntry.tool && entry.argsHash === lastEntry.argsHash) {
47659
+ repetitionCount++;
47660
+ } else {
47661
+ break;
47662
+ }
47663
+ }
47664
+ }
47665
+ const elapsedMinutes = (Date.now() - window2.startedAtMs) / 60000;
47666
+ if (agentConfig.max_tool_calls > 0 && window2.toolCalls >= agentConfig.max_tool_calls) {
47667
+ window2.hardLimitHit = true;
47668
+ warn("Circuit breaker: tool call limit hit", {
47669
+ sessionID: input.sessionID,
47670
+ agentName: window2.agentName,
47671
+ invocationId: window2.id,
47672
+ windowKey: `${window2.agentName}:${window2.id}`,
47673
+ resolvedMaxCalls: agentConfig.max_tool_calls,
47674
+ currentCalls: window2.toolCalls
47675
+ });
47676
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: Tool calls exhausted (${window2.toolCalls}/${agentConfig.max_tool_calls}). Finish the current operation and return your progress summary.`);
47677
+ }
47678
+ if (agentConfig.max_duration_minutes > 0 && elapsedMinutes >= agentConfig.max_duration_minutes) {
47679
+ window2.hardLimitHit = true;
47680
+ warn("Circuit breaker: duration limit hit", {
47681
+ sessionID: input.sessionID,
47682
+ agentName: window2.agentName,
47683
+ invocationId: window2.id,
47684
+ windowKey: `${window2.agentName}:${window2.id}`,
47685
+ resolvedMaxMinutes: agentConfig.max_duration_minutes,
47686
+ elapsedMinutes: Math.floor(elapsedMinutes)
47687
+ });
47688
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: Duration exhausted (${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min). Finish the current operation and return your progress summary.`);
47689
+ }
47690
+ if (repetitionCount >= agentConfig.max_repetitions) {
47691
+ window2.hardLimitHit = true;
47692
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: Repeated the same tool call ${repetitionCount} times. This suggests a loop. Return your progress summary.`);
47693
+ }
47694
+ if (window2.consecutiveErrors >= agentConfig.max_consecutive_errors) {
47695
+ window2.hardLimitHit = true;
47696
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${window2.consecutiveErrors} consecutive tool errors detected. Return your progress summary with details of what went wrong.`);
47697
+ }
47698
+ const idleMinutes = (Date.now() - window2.lastSuccessTimeMs) / 60000;
47699
+ if (idleMinutes >= agentConfig.idle_timeout_minutes) {
47700
+ window2.hardLimitHit = true;
47701
+ warn("Circuit breaker: idle timeout hit", {
47702
+ sessionID: input.sessionID,
47703
+ agentName: window2.agentName,
47704
+ invocationId: window2.id,
47705
+ windowKey: `${window2.agentName}:${window2.id}`,
47706
+ idleTimeoutMinutes: agentConfig.idle_timeout_minutes,
47707
+ idleMinutes: Math.floor(idleMinutes)
47708
+ });
47709
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: No successful tool call for ${Math.floor(idleMinutes)} minutes (idle timeout: ${agentConfig.idle_timeout_minutes} min). This suggests the agent may be stuck. Return your progress summary.`);
47710
+ }
47711
+ if (!window2.warningIssued) {
47712
+ const toolPct = agentConfig.max_tool_calls > 0 ? window2.toolCalls / agentConfig.max_tool_calls : 0;
47713
+ const durationPct = agentConfig.max_duration_minutes > 0 ? elapsedMinutes / agentConfig.max_duration_minutes : 0;
47714
+ const repPct = repetitionCount / agentConfig.max_repetitions;
47715
+ const errorPct = window2.consecutiveErrors / agentConfig.max_consecutive_errors;
47716
+ const reasons = [];
47717
+ if (agentConfig.max_tool_calls > 0 && toolPct >= agentConfig.warning_threshold) {
47718
+ reasons.push(`tool calls ${window2.toolCalls}/${agentConfig.max_tool_calls}`);
47719
+ }
47720
+ if (durationPct >= agentConfig.warning_threshold) {
47721
+ reasons.push(`duration ${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min`);
47722
+ }
47723
+ if (repPct >= agentConfig.warning_threshold) {
47724
+ reasons.push(`repetitions ${repetitionCount}/${agentConfig.max_repetitions}`);
47725
+ }
47726
+ if (errorPct >= agentConfig.warning_threshold) {
47727
+ reasons.push(`errors ${window2.consecutiveErrors}/${agentConfig.max_consecutive_errors}`);
47728
+ }
47729
+ if (reasons.length > 0) {
47730
+ window2.warningIssued = true;
47731
+ window2.warningReason = reasons.join(", ");
47879
47732
  }
47880
47733
  }
47881
- if (isArchitect(input.sessionID) && isWriteTool(input.tool)) {
47882
- const args2 = output.args;
47883
- const targetPath = args2?.filePath ?? args2?.path ?? args2?.file ?? args2?.target;
47884
- if (typeof targetPath === "string" && targetPath.length > 0) {
47885
- const resolvedTarget = path25.resolve(directory, targetPath).toLowerCase();
47886
- const planMdPath = path25.resolve(directory, ".swarm", "plan.md").toLowerCase();
47887
- const planJsonPath = path25.resolve(directory, ".swarm", "plan.json").toLowerCase();
47888
- if (resolvedTarget === planMdPath || resolvedTarget === planJsonPath) {
47889
- throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47734
+ setStoredInputArgs(input.callID, output.args);
47735
+ },
47736
+ toolAfter: async (input, output) => {
47737
+ const session = swarmState.agentSessions.get(input.sessionID);
47738
+ if (session) {
47739
+ if (isGateTool(input.tool)) {
47740
+ const taskId = getCurrentTaskId(input.sessionID);
47741
+ if (!session.gateLog.has(taskId)) {
47742
+ session.gateLog.set(taskId, new Set);
47890
47743
  }
47891
- }
47892
- if (!targetPath && (input.tool === "apply_patch" || input.tool === "patch")) {
47893
- const patchText = args2?.input ?? args2?.patch ?? (Array.isArray(args2?.cmd) ? args2.cmd[1] : undefined);
47894
- if (typeof patchText === "string" && patchText.length <= 1e6) {
47895
- const patchPathPattern = /\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)/gi;
47896
- const diffPathPattern = /\+\+\+\s+b\/(.+)/gm;
47897
- const paths = new Set;
47898
- for (const match of patchText.matchAll(patchPathPattern)) {
47899
- paths.add(match[1].trim());
47900
- }
47901
- for (const match of patchText.matchAll(diffPathPattern)) {
47902
- const p = match[1].trim();
47903
- if (p !== "/dev/null")
47904
- paths.add(p);
47905
- }
47906
- const gitDiffPathPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
47907
- for (const match of patchText.matchAll(gitDiffPathPattern)) {
47908
- const aPath = match[1].trim();
47909
- const bPath = match[2].trim();
47910
- if (aPath !== "/dev/null")
47911
- paths.add(aPath);
47912
- if (bPath !== "/dev/null")
47913
- paths.add(bPath);
47914
- }
47915
- const minusPathPattern = /^---\s+a\/(.+)$/gm;
47916
- for (const match of patchText.matchAll(minusPathPattern)) {
47917
- const p = match[1].trim();
47918
- if (p !== "/dev/null")
47919
- paths.add(p);
47920
- }
47921
- const traditionalMinusPattern = /^---\s+([^\s].+?)(?:\t.*)?$/gm;
47922
- const traditionalPlusPattern = /^\+\+\+\s+([^\s].+?)(?:\t.*)?$/gm;
47923
- for (const match of patchText.matchAll(traditionalMinusPattern)) {
47924
- const p = match[1].trim();
47925
- if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
47926
- paths.add(p);
47927
- }
47928
- }
47929
- for (const match of patchText.matchAll(traditionalPlusPattern)) {
47930
- const p = match[1].trim();
47931
- if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
47932
- paths.add(p);
47933
- }
47934
- }
47935
- for (const p of paths) {
47936
- const resolvedP = path25.resolve(directory, p);
47937
- const planMdPath = path25.resolve(directory, ".swarm", "plan.md").toLowerCase();
47938
- const planJsonPath = path25.resolve(directory, ".swarm", "plan.json").toLowerCase();
47939
- if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
47940
- throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47744
+ session.gateLog.get(taskId)?.add(input.tool);
47745
+ const outputStr = typeof output.output === "string" ? output.output : "";
47746
+ const hasFailure = output.output === null || output.output === undefined || outputStr.includes("FAIL") || outputStr.includes("error") || outputStr.toLowerCase().includes("gates_passed: false");
47747
+ if (hasFailure) {
47748
+ session.lastGateFailure = {
47749
+ tool: input.tool,
47750
+ taskId,
47751
+ timestamp: Date.now()
47752
+ };
47753
+ } else {
47754
+ session.lastGateFailure = null;
47755
+ if (input.tool === "pre_check_batch") {
47756
+ const successStr = typeof output.output === "string" ? output.output : "";
47757
+ let isPassed = false;
47758
+ try {
47759
+ const result = JSON.parse(successStr);
47760
+ isPassed = result.gates_passed === true;
47761
+ } catch {
47762
+ isPassed = false;
47941
47763
  }
47942
- if (isOutsideSwarmDir(p, directory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
47943
- const session2 = swarmState.agentSessions.get(input.sessionID);
47944
- if (session2) {
47945
- session2.architectWriteCount++;
47946
- warn("Architect direct code edit detected via apply_patch", {
47947
- tool: input.tool,
47948
- sessionID: input.sessionID,
47949
- targetPath: p,
47950
- writeCount: session2.architectWriteCount
47764
+ if (isPassed && session.currentTaskId) {
47765
+ try {
47766
+ advanceTaskState(session, session.currentTaskId, "pre_check_passed");
47767
+ } catch (err2) {
47768
+ warn("Failed to advance task state after pre_check_batch pass", {
47769
+ taskId: session.currentTaskId,
47770
+ error: String(err2)
47951
47771
  });
47952
47772
  }
47953
- break;
47954
47773
  }
47955
47774
  }
47956
47775
  }
47957
47776
  }
47958
- if (typeof targetPath === "string" && targetPath.length > 0 && isOutsideSwarmDir(targetPath, directory) && isSourceCodePath(path25.relative(directory, path25.resolve(directory, targetPath)))) {
47959
- const session2 = swarmState.agentSessions.get(input.sessionID);
47960
- if (session2) {
47961
- session2.architectWriteCount++;
47962
- warn("Architect direct code edit detected", {
47963
- tool: input.tool,
47964
- sessionID: input.sessionID,
47965
- targetPath,
47966
- writeCount: session2.architectWriteCount
47967
- });
47968
- if (session2.lastGateFailure && Date.now() - session2.lastGateFailure.timestamp < 120000) {
47969
- const failedGate = session2.lastGateFailure.tool;
47970
- const failedTaskId = session2.lastGateFailure.taskId;
47971
- warn("Self-fix after gate failure detected", {
47972
- failedGate,
47973
- failedTaskId,
47974
- currentTool: input.tool,
47975
- sessionID: input.sessionID
47976
- });
47977
- session2.selfFixAttempted = true;
47777
+ const inputArgs = getStoredInputArgs(input.callID);
47778
+ const delegation = isAgentDelegation(input.tool, inputArgs);
47779
+ if (delegation.isDelegation && (delegation.targetAgent === "reviewer" || delegation.targetAgent === "test_engineer")) {
47780
+ let currentPhase = 1;
47781
+ try {
47782
+ const plan = await loadPlan(directory);
47783
+ if (plan) {
47784
+ const phaseString = extractCurrentPhaseFromPlan2(plan);
47785
+ currentPhase = extractPhaseNumber(phaseString);
47786
+ }
47787
+ } catch {}
47788
+ const count = session.reviewerCallCount.get(currentPhase) ?? 0;
47789
+ session.reviewerCallCount.set(currentPhase, count + 1);
47790
+ }
47791
+ if (delegation.isDelegation && delegation.targetAgent === "coder" && session.lastCoderDelegationTaskId) {
47792
+ session.currentTaskId = session.lastCoderDelegationTaskId;
47793
+ session.partialGateWarningsIssuedForTask?.delete(session.currentTaskId);
47794
+ if (session.declaredCoderScope !== null) {
47795
+ const undeclaredFiles = session.modifiedFilesThisCoderTask.map((f) => f.replace(/[\r\n\t]/g, "_")).filter((f) => !isInDeclaredScope(f, session.declaredCoderScope));
47796
+ if (undeclaredFiles.length > 2) {
47797
+ const safeTaskId = String(session.currentTaskId ?? "").replace(/[\r\n\t]/g, "_");
47798
+ session.lastScopeViolation = `Scope violation for task ${safeTaskId}: ` + `${undeclaredFiles.length} undeclared files modified: ` + undeclaredFiles.join(", ");
47799
+ session.scopeViolationDetected = true;
47800
+ }
47801
+ }
47802
+ session.modifiedFilesThisCoderTask = [];
47803
+ }
47804
+ }
47805
+ const window2 = getActiveWindow(input.sessionID);
47806
+ if (!window2)
47807
+ return;
47808
+ const hasError = output.output === null || output.output === undefined;
47809
+ if (hasError) {
47810
+ window2.consecutiveErrors++;
47811
+ } else {
47812
+ window2.consecutiveErrors = 0;
47813
+ window2.lastSuccessTimeMs = Date.now();
47814
+ }
47815
+ },
47816
+ messagesTransform: async (_input, output) => {
47817
+ const messages = output.messages;
47818
+ if (!messages || messages.length === 0) {
47819
+ return;
47820
+ }
47821
+ const lastMessage = messages[messages.length - 1];
47822
+ const sessionId = lastMessage.info?.sessionID;
47823
+ if (!sessionId) {
47824
+ return;
47825
+ }
47826
+ {
47827
+ const { modelID } = extractModelInfo(messages);
47828
+ if (modelID && isLowCapabilityModel(modelID)) {
47829
+ for (const msg of messages) {
47830
+ if (msg.info?.role !== "system")
47831
+ continue;
47832
+ for (const part of msg.parts) {
47833
+ try {
47834
+ if (part == null)
47835
+ continue;
47836
+ if (part.type !== "text" || typeof part.text !== "string")
47837
+ continue;
47838
+ if (!part.text.includes("<!-- BEHAVIORAL_GUIDANCE_START -->"))
47839
+ continue;
47840
+ part.text = part.text.replace(/<!--\s*BEHAVIORAL_GUIDANCE_START\s*-->[\s\S]*?<!--\s*BEHAVIORAL_GUIDANCE_END\s*-->/g, "[Enforcement: programmatic gates active]");
47841
+ } catch {}
47842
+ }
47843
+ }
47844
+ }
47845
+ }
47846
+ const session = swarmState.agentSessions.get(sessionId);
47847
+ const activeAgent = swarmState.activeAgent.get(sessionId);
47848
+ const isArchitectSession = activeAgent ? stripKnownSwarmPrefix(activeAgent) === ORCHESTRATOR_NAME : session ? stripKnownSwarmPrefix(session.agentName) === ORCHESTRATOR_NAME : false;
47849
+ if (isArchitectSession && session && session.architectWriteCount > 0) {
47850
+ const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
47851
+ if (textPart2) {
47852
+ textPart2.text = `\u26A0\uFE0F SELF-CODING DETECTED: You have used ${session.architectWriteCount} write-class tool(s) directly on non-.swarm/ files.
47853
+ ` + `Rule 1 requires ALL coding to be delegated to @coder.
47854
+ ` + `If you have not exhausted QA_RETRY_LIMIT coder failures on this task, STOP and delegate.
47855
+
47856
+ ` + textPart2.text;
47857
+ }
47858
+ }
47859
+ if (isArchitectSession && session && session.selfFixAttempted && session.lastGateFailure && Date.now() - session.lastGateFailure.timestamp < 120000) {
47860
+ const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
47861
+ if (textPart2 && !textPart2.text.includes("SELF-FIX DETECTED")) {
47862
+ textPart2.text = `\u26A0\uFE0F SELF-FIX DETECTED: Gate '${session.lastGateFailure.tool}' failed on task ${session.lastGateFailure.taskId}.
47863
+ ` + `You are now using a write tool instead of delegating to @coder.
47864
+ ` + `GATE FAILURE RESPONSE RULES require: return to coder with structured rejection.
47865
+ ` + `Do NOT fix gate failures yourself.
47866
+
47867
+ ` + textPart2.text;
47868
+ session.selfFixAttempted = false;
47869
+ }
47870
+ }
47871
+ const isArchitectSessionForGates = activeAgent ? stripKnownSwarmPrefix(activeAgent) === ORCHESTRATOR_NAME : session ? stripKnownSwarmPrefix(session.agentName) === ORCHESTRATOR_NAME : false;
47872
+ if (isArchitectSessionForGates && session) {
47873
+ const taskId = getCurrentTaskId(sessionId);
47874
+ if (!session.partialGateWarningsIssuedForTask.has(taskId)) {
47875
+ const gates = session.gateLog.get(taskId);
47876
+ const REQUIRED_GATES = [
47877
+ "diff",
47878
+ "syntax_check",
47879
+ "placeholder_scan",
47880
+ "lint",
47881
+ "pre_check_batch"
47882
+ ];
47883
+ const missingGates = [];
47884
+ if (!gates) {
47885
+ missingGates.push(...REQUIRED_GATES);
47886
+ } else {
47887
+ for (const gate of REQUIRED_GATES) {
47888
+ if (!gates.has(gate)) {
47889
+ missingGates.push(gate);
47890
+ }
47891
+ }
47892
+ }
47893
+ let currentPhaseForCheck = 1;
47894
+ try {
47895
+ const plan = await loadPlan(directory);
47896
+ if (plan) {
47897
+ const phaseString = extractCurrentPhaseFromPlan2(plan);
47898
+ currentPhaseForCheck = extractPhaseNumber(phaseString);
47899
+ }
47900
+ } catch {}
47901
+ const hasReviewerDelegation = (session.reviewerCallCount.get(currentPhaseForCheck) ?? 0) > 0;
47902
+ if (missingGates.length > 0 || !hasReviewerDelegation) {
47903
+ const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
47904
+ if (textPart2 && !textPart2.text.includes("PARTIAL GATE VIOLATION")) {
47905
+ const missing = [...missingGates];
47906
+ if (!hasReviewerDelegation) {
47907
+ missing.push("reviewer/test_engineer (no delegations this phase)");
47908
+ }
47909
+ session.partialGateWarningsIssuedForTask.add(taskId);
47910
+ textPart2.text = `\u26A0\uFE0F PARTIAL GATE VIOLATION: Task may be marked complete but missing gates: [${missing.join(", ")}].
47911
+ ` + `The QA gate is ALL steps or NONE. Revert any \u2713 marks and run the missing gates.
47912
+
47913
+ ` + textPart2.text;
47978
47914
  }
47979
47915
  }
47980
47916
  }
47981
47917
  }
47982
- const rawActiveAgent = swarmState.activeAgent.get(input.sessionID);
47983
- const strippedAgent = rawActiveAgent ? stripKnownSwarmPrefix(rawActiveAgent) : undefined;
47984
- if (strippedAgent === ORCHESTRATOR_NAME) {
47985
- return;
47986
- }
47987
- const existingSession = swarmState.agentSessions.get(input.sessionID);
47988
- if (existingSession) {
47989
- const sessionAgent = stripKnownSwarmPrefix(existingSession.agentName);
47990
- if (sessionAgent === ORCHESTRATOR_NAME) {
47991
- return;
47918
+ if (isArchitectSessionForGates && session && session.scopeViolationDetected) {
47919
+ session.scopeViolationDetected = false;
47920
+ const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
47921
+ if (textPart2 && session.lastScopeViolation) {
47922
+ textPart2.text = `\u26A0\uFE0F SCOPE VIOLATION: ${session.lastScopeViolation}
47923
+ ` + `Only modify files within your declared scope. Request scope expansion from architect if needed.
47924
+
47925
+ ` + textPart2.text;
47992
47926
  }
47993
47927
  }
47994
- const agentName = swarmState.activeAgent.get(input.sessionID) ?? ORCHESTRATOR_NAME;
47995
- const session = ensureAgentSession(input.sessionID, agentName);
47996
- const resolvedName = stripKnownSwarmPrefix(session.agentName);
47997
- if (resolvedName === ORCHESTRATOR_NAME) {
47998
- return;
47928
+ if (isArchitectSessionForGates && session && session.catastrophicPhaseWarnings) {
47929
+ try {
47930
+ const plan = await loadPlan(directory);
47931
+ if (plan?.phases) {
47932
+ for (const phase of plan.phases) {
47933
+ if (phase.status === "complete") {
47934
+ const phaseNum = phase.id;
47935
+ if (!session.catastrophicPhaseWarnings.has(phaseNum)) {
47936
+ const reviewerCount = session.reviewerCallCount.get(phaseNum) ?? 0;
47937
+ if (reviewerCount === 0) {
47938
+ session.catastrophicPhaseWarnings.add(phaseNum);
47939
+ const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
47940
+ if (textPart2 && !textPart2.text.includes("CATASTROPHIC VIOLATION")) {
47941
+ textPart2.text = `[CATASTROPHIC VIOLATION: Phase ${phaseNum} completed with ZERO reviewer delegations.` + ` Every coder task requires reviewer approval. Recommend retrospective review of all Phase ${phaseNum} tasks.]
47942
+
47943
+ ` + textPart2.text;
47944
+ }
47945
+ break;
47946
+ }
47947
+ }
47948
+ }
47949
+ }
47950
+ }
47951
+ } catch {}
47999
47952
  }
48000
- const agentConfig = resolveGuardrailsConfig(cfg, session.agentName);
48001
- if (agentConfig.max_duration_minutes === 0 && agentConfig.max_tool_calls === 0) {
47953
+ const targetWindow = getActiveWindow(sessionId);
47954
+ if (!targetWindow || !targetWindow.warningIssued && !targetWindow.hardLimitHit) {
48002
47955
  return;
48003
47956
  }
48004
- if (!getActiveWindow(input.sessionID)) {
48005
- const fallbackAgent = swarmState.activeAgent.get(input.sessionID) ?? session.agentName;
48006
- const stripped = stripKnownSwarmPrefix(fallbackAgent);
48007
- if (stripped !== ORCHESTRATOR_NAME) {
48008
- beginInvocation(input.sessionID, fallbackAgent);
48009
- }
48010
- }
48011
- const window2 = getActiveWindow(input.sessionID);
48012
- if (!window2) {
47957
+ const textPart = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
47958
+ if (!textPart) {
48013
47959
  return;
48014
47960
  }
48015
- if (window2.hardLimitHit) {
48016
- throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
48017
- }
48018
- window2.toolCalls++;
48019
- const hash3 = hashArgs(output.args);
48020
- window2.recentToolCalls.push({
48021
- tool: input.tool,
48022
- argsHash: hash3,
48023
- timestamp: Date.now()
48024
- });
48025
- if (window2.recentToolCalls.length > 20) {
48026
- window2.recentToolCalls.shift();
48027
- }
48028
- let repetitionCount = 0;
48029
- if (window2.recentToolCalls.length > 0) {
48030
- const lastEntry = window2.recentToolCalls[window2.recentToolCalls.length - 1];
48031
- for (let i2 = window2.recentToolCalls.length - 1;i2 >= 0; i2--) {
48032
- const entry = window2.recentToolCalls[i2];
48033
- if (entry.tool === lastEntry.tool && entry.argsHash === lastEntry.argsHash) {
48034
- repetitionCount++;
48035
- } else {
48036
- break;
48037
- }
48038
- }
48039
- }
48040
- const elapsedMinutes = (Date.now() - window2.startedAtMs) / 60000;
48041
- if (agentConfig.max_tool_calls > 0 && window2.toolCalls >= agentConfig.max_tool_calls) {
48042
- window2.hardLimitHit = true;
48043
- warn("Circuit breaker: tool call limit hit", {
48044
- sessionID: input.sessionID,
48045
- agentName: window2.agentName,
48046
- invocationId: window2.id,
48047
- windowKey: `${window2.agentName}:${window2.id}`,
48048
- resolvedMaxCalls: agentConfig.max_tool_calls,
48049
- currentCalls: window2.toolCalls
48050
- });
48051
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: Tool calls exhausted (${window2.toolCalls}/${agentConfig.max_tool_calls}). Finish the current operation and return your progress summary.`);
48052
- }
48053
- if (agentConfig.max_duration_minutes > 0 && elapsedMinutes >= agentConfig.max_duration_minutes) {
48054
- window2.hardLimitHit = true;
48055
- warn("Circuit breaker: duration limit hit", {
48056
- sessionID: input.sessionID,
48057
- agentName: window2.agentName,
48058
- invocationId: window2.id,
48059
- windowKey: `${window2.agentName}:${window2.id}`,
48060
- resolvedMaxMinutes: agentConfig.max_duration_minutes,
48061
- elapsedMinutes: Math.floor(elapsedMinutes)
48062
- });
48063
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: Duration exhausted (${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min). Finish the current operation and return your progress summary.`);
48064
- }
48065
- if (repetitionCount >= agentConfig.max_repetitions) {
48066
- window2.hardLimitHit = true;
48067
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: Repeated the same tool call ${repetitionCount} times. This suggests a loop. Return your progress summary.`);
48068
- }
48069
- if (window2.consecutiveErrors >= agentConfig.max_consecutive_errors) {
48070
- window2.hardLimitHit = true;
48071
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${window2.consecutiveErrors} consecutive tool errors detected. Return your progress summary with details of what went wrong.`);
48072
- }
48073
- const idleMinutes = (Date.now() - window2.lastSuccessTimeMs) / 60000;
48074
- if (idleMinutes >= agentConfig.idle_timeout_minutes) {
48075
- window2.hardLimitHit = true;
48076
- warn("Circuit breaker: idle timeout hit", {
48077
- sessionID: input.sessionID,
48078
- agentName: window2.agentName,
48079
- invocationId: window2.id,
48080
- windowKey: `${window2.agentName}:${window2.id}`,
48081
- idleTimeoutMinutes: agentConfig.idle_timeout_minutes,
48082
- idleMinutes: Math.floor(idleMinutes)
48083
- });
48084
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: No successful tool call for ${Math.floor(idleMinutes)} minutes (idle timeout: ${agentConfig.idle_timeout_minutes} min). This suggests the agent may be stuck. Return your progress summary.`);
48085
- }
48086
- if (!window2.warningIssued) {
48087
- const toolPct = agentConfig.max_tool_calls > 0 ? window2.toolCalls / agentConfig.max_tool_calls : 0;
48088
- const durationPct = agentConfig.max_duration_minutes > 0 ? elapsedMinutes / agentConfig.max_duration_minutes : 0;
48089
- const repPct = repetitionCount / agentConfig.max_repetitions;
48090
- const errorPct = window2.consecutiveErrors / agentConfig.max_consecutive_errors;
48091
- const reasons = [];
48092
- if (agentConfig.max_tool_calls > 0 && toolPct >= agentConfig.warning_threshold) {
48093
- reasons.push(`tool calls ${window2.toolCalls}/${agentConfig.max_tool_calls}`);
48094
- }
48095
- if (durationPct >= agentConfig.warning_threshold) {
48096
- reasons.push(`duration ${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min`);
48097
- }
48098
- if (repPct >= agentConfig.warning_threshold) {
48099
- reasons.push(`repetitions ${repetitionCount}/${agentConfig.max_repetitions}`);
48100
- }
48101
- if (errorPct >= agentConfig.warning_threshold) {
48102
- reasons.push(`errors ${window2.consecutiveErrors}/${agentConfig.max_consecutive_errors}`);
48103
- }
48104
- if (reasons.length > 0) {
48105
- window2.warningIssued = true;
48106
- window2.warningReason = reasons.join(", ");
48107
- }
47961
+ if (targetWindow.hardLimitHit) {
47962
+ textPart.text = `[\uD83D\uDED1 LIMIT REACHED: Your resource budget is exhausted. Do not make additional tool calls. Return a summary of your progress and any remaining work.]
47963
+
47964
+ ` + textPart.text;
47965
+ } else if (targetWindow.warningIssued) {
47966
+ const reasonSuffix = targetWindow.warningReason ? ` (${targetWindow.warningReason})` : "";
47967
+ textPart.text = `[\u26A0\uFE0F APPROACHING LIMITS${reasonSuffix}: You still have capacity to finish your current step. Complete what you're working on, then return your results.]
47968
+
47969
+ ` + textPart.text;
48108
47970
  }
48109
- inputArgsByCallID.set(input.callID, output.args);
48110
- },
48111
- toolAfter: async (input, output) => {
48112
- const session = swarmState.agentSessions.get(input.sessionID);
48113
- if (session) {
48114
- if (isGateTool(input.tool)) {
48115
- const taskId = getCurrentTaskId(input.sessionID);
48116
- if (!session.gateLog.has(taskId)) {
48117
- session.gateLog.set(taskId, new Set);
48118
- }
48119
- session.gateLog.get(taskId)?.add(input.tool);
48120
- const outputStr = typeof output.output === "string" ? output.output : "";
48121
- const hasFailure = output.output === null || output.output === undefined || outputStr.includes("FAIL") || outputStr.includes("error") || outputStr.toLowerCase().includes("gates_passed: false");
48122
- if (hasFailure) {
48123
- session.lastGateFailure = {
48124
- tool: input.tool,
48125
- taskId,
48126
- timestamp: Date.now()
48127
- };
48128
- } else {
48129
- session.lastGateFailure = null;
48130
- if (input.tool === "pre_check_batch") {
48131
- const successStr = typeof output.output === "string" ? output.output : "";
48132
- let isPassed = false;
47971
+ }
47972
+ };
47973
+ }
47974
+ function hashArgs(args2) {
47975
+ try {
47976
+ if (typeof args2 !== "object" || args2 === null) {
47977
+ return 0;
47978
+ }
47979
+ const sortedKeys = Object.keys(args2).sort();
47980
+ return Number(Bun.hash(JSON.stringify(args2, sortedKeys)));
47981
+ } catch {
47982
+ return 0;
47983
+ }
47984
+ }
47985
+
47986
+ // src/hooks/delegation-gate.ts
47987
+ function extractTaskLine(text) {
47988
+ const match = text.match(/TASK:\s*(.+?)(?:\n|$)/i);
47989
+ return match ? match[1].trim() : null;
47990
+ }
47991
+ function extractPlanTaskId(text) {
47992
+ const taskListMatch = text.match(/^[ \t]*-[ \t]*(?:\[[ x]\][ \t]+)?(\d+\.\d+(?:\.\d+)*)[:. ]/m);
47993
+ if (taskListMatch) {
47994
+ return taskListMatch[1];
47995
+ }
47996
+ const taskLineMatch = text.match(/TASK:\s*(?:.+?\s)?(\d+\.\d+(?:\.\d+)*)(?:\s|$|:)/i);
47997
+ if (taskLineMatch) {
47998
+ return taskLineMatch[1];
47999
+ }
48000
+ return null;
48001
+ }
48002
+ function createDelegationGateHook(config3) {
48003
+ const enabled = config3.hooks?.delegation_gate !== false;
48004
+ const delegationMaxChars = config3.hooks?.delegation_max_chars ?? 4000;
48005
+ if (!enabled) {
48006
+ return {
48007
+ messagesTransform: async (_input, _output) => {},
48008
+ toolAfter: async () => {}
48009
+ };
48010
+ }
48011
+ const toolAfter = async (input, _output) => {
48012
+ if (!input.sessionID)
48013
+ return;
48014
+ const session = swarmState.agentSessions.get(input.sessionID);
48015
+ if (!session)
48016
+ return;
48017
+ const taskStates = session.taskWorkflowStates ? Object.entries(session.taskWorkflowStates) : [];
48018
+ const statesSummary = taskStates.length > 0 ? taskStates.map(([k, v]) => `${k}=${v}`).join(",") : "(none)";
48019
+ console.log(`[swarm-debug-task] delegation-gate.toolAfter | session=${input.sessionID} callID=${input.callID} tool=${input.tool}`);
48020
+ const normalized = input.tool.replace(/^[^:]+[:.]/, "");
48021
+ if (normalized === "Task" || normalized === "task") {
48022
+ const storedArgs = getStoredInputArgs(input.callID);
48023
+ const argsObj = storedArgs;
48024
+ const subagentType = argsObj?.subagent_type;
48025
+ console.log(`[swarm-debug-task] delegation-gate.taskDetected | session=${input.sessionID} subagent_type=${subagentType ?? "(none)"} currentStates=[${statesSummary}]`);
48026
+ let hasReviewer = false;
48027
+ let hasTestEngineer = false;
48028
+ if (typeof subagentType === "string") {
48029
+ const targetAgent = stripKnownSwarmPrefix(subagentType);
48030
+ if (targetAgent === "reviewer")
48031
+ hasReviewer = true;
48032
+ if (targetAgent === "test_engineer")
48033
+ hasTestEngineer = true;
48034
+ if (targetAgent === "reviewer" && session.taskWorkflowStates) {
48035
+ for (const [taskId, state] of session.taskWorkflowStates) {
48036
+ if (state === "coder_delegated" || state === "pre_check_passed") {
48037
+ try {
48038
+ advanceTaskState(session, taskId, "reviewer_run");
48039
+ } catch {}
48040
+ }
48041
+ }
48042
+ }
48043
+ if (targetAgent === "test_engineer" && session.taskWorkflowStates) {
48044
+ for (const [taskId, state] of session.taskWorkflowStates) {
48045
+ if (state === "reviewer_run") {
48133
48046
  try {
48134
- const result = JSON.parse(successStr);
48135
- isPassed = result.gates_passed === true;
48136
- } catch {
48137
- isPassed = false;
48047
+ advanceTaskState(session, taskId, "tests_run");
48048
+ } catch {}
48049
+ }
48050
+ }
48051
+ }
48052
+ if (targetAgent === "reviewer" || targetAgent === "test_engineer") {
48053
+ for (const [, otherSession] of swarmState.agentSessions) {
48054
+ if (otherSession === session)
48055
+ continue;
48056
+ if (!otherSession.taskWorkflowStates)
48057
+ continue;
48058
+ if (targetAgent === "reviewer") {
48059
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
48060
+ if (state === "coder_delegated" || state === "pre_check_passed") {
48061
+ try {
48062
+ advanceTaskState(otherSession, taskId, "reviewer_run");
48063
+ } catch {}
48064
+ }
48138
48065
  }
48139
- if (isPassed && session.currentTaskId) {
48140
- try {
48141
- advanceTaskState(session, session.currentTaskId, "pre_check_passed");
48142
- } catch (err2) {
48143
- warn("Failed to advance task state after pre_check_batch pass", {
48144
- taskId: session.currentTaskId,
48145
- error: String(err2)
48146
- });
48066
+ }
48067
+ if (targetAgent === "test_engineer") {
48068
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
48069
+ if (state === "reviewer_run") {
48070
+ try {
48071
+ advanceTaskState(otherSession, taskId, "tests_run");
48072
+ } catch {}
48147
48073
  }
48148
48074
  }
48149
48075
  }
48150
48076
  }
48151
48077
  }
48152
- const inputArgs = inputArgsByCallID.get(input.callID);
48153
- inputArgsByCallID.delete(input.callID);
48154
- const delegation = isAgentDelegation(input.tool, inputArgs);
48155
- if (delegation.isDelegation && (delegation.targetAgent === "reviewer" || delegation.targetAgent === "test_engineer")) {
48156
- let currentPhase = 1;
48157
- try {
48158
- const plan = await loadPlan(directory);
48159
- if (plan) {
48160
- const phaseString = extractCurrentPhaseFromPlan2(plan);
48161
- currentPhase = extractPhaseNumber(phaseString);
48078
+ }
48079
+ if (argsObj !== undefined) {
48080
+ deleteStoredInputArgs(input.callID);
48081
+ }
48082
+ if (!subagentType || !hasReviewer) {
48083
+ const delegationChain = swarmState.delegationChains.get(input.sessionID);
48084
+ if (delegationChain && delegationChain.length > 0) {
48085
+ let lastCoderIndex = -1;
48086
+ for (let i2 = delegationChain.length - 1;i2 >= 0; i2--) {
48087
+ const target = stripKnownSwarmPrefix(delegationChain[i2].to);
48088
+ if (target.includes("coder")) {
48089
+ lastCoderIndex = i2;
48090
+ break;
48162
48091
  }
48163
- } catch {}
48164
- const count = session.reviewerCallCount.get(currentPhase) ?? 0;
48165
- session.reviewerCallCount.set(currentPhase, count + 1);
48166
- }
48167
- if (delegation.isDelegation && delegation.targetAgent === "coder" && session.lastCoderDelegationTaskId) {
48168
- session.currentTaskId = session.lastCoderDelegationTaskId;
48169
- session.partialGateWarningsIssuedForTask?.delete(session.currentTaskId);
48170
- if (session.declaredCoderScope !== null) {
48171
- const undeclaredFiles = session.modifiedFilesThisCoderTask.map((f) => f.replace(/[\r\n\t]/g, "_")).filter((f) => !isInDeclaredScope(f, session.declaredCoderScope));
48172
- if (undeclaredFiles.length > 2) {
48173
- const safeTaskId = String(session.currentTaskId ?? "").replace(/[\r\n\t]/g, "_");
48174
- session.lastScopeViolation = `Scope violation for task ${safeTaskId}: ` + `${undeclaredFiles.length} undeclared files modified: ` + undeclaredFiles.join(", ");
48175
- session.scopeViolationDetected = true;
48092
+ }
48093
+ if (lastCoderIndex === -1) {
48094
+ return;
48095
+ }
48096
+ const afterCoder = delegationChain.slice(lastCoderIndex);
48097
+ for (const delegation of afterCoder) {
48098
+ const target = stripKnownSwarmPrefix(delegation.to);
48099
+ if (target === "reviewer")
48100
+ hasReviewer = true;
48101
+ if (target === "test_engineer")
48102
+ hasTestEngineer = true;
48103
+ }
48104
+ if (hasReviewer && hasTestEngineer) {
48105
+ session.qaSkipCount = 0;
48106
+ session.qaSkipTaskIds = [];
48107
+ }
48108
+ if (hasReviewer && session.taskWorkflowStates) {
48109
+ for (const [taskId, state] of session.taskWorkflowStates) {
48110
+ if (state === "coder_delegated" || state === "pre_check_passed") {
48111
+ try {
48112
+ advanceTaskState(session, taskId, "reviewer_run");
48113
+ } catch {}
48114
+ }
48115
+ }
48116
+ }
48117
+ if (hasReviewer && hasTestEngineer && session.taskWorkflowStates) {
48118
+ for (const [taskId, state] of session.taskWorkflowStates) {
48119
+ if (state === "reviewer_run") {
48120
+ try {
48121
+ advanceTaskState(session, taskId, "tests_run");
48122
+ } catch {}
48123
+ }
48124
+ }
48125
+ }
48126
+ if (hasReviewer) {
48127
+ for (const [, otherSession] of swarmState.agentSessions) {
48128
+ if (otherSession === session)
48129
+ continue;
48130
+ if (!otherSession.taskWorkflowStates)
48131
+ continue;
48132
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
48133
+ if (state === "coder_delegated" || state === "pre_check_passed") {
48134
+ try {
48135
+ advanceTaskState(otherSession, taskId, "reviewer_run");
48136
+ } catch {}
48137
+ }
48138
+ }
48139
+ }
48140
+ }
48141
+ if (hasReviewer && hasTestEngineer) {
48142
+ for (const [, otherSession] of swarmState.agentSessions) {
48143
+ if (otherSession === session)
48144
+ continue;
48145
+ if (!otherSession.taskWorkflowStates)
48146
+ continue;
48147
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
48148
+ if (state === "reviewer_run") {
48149
+ try {
48150
+ advanceTaskState(otherSession, taskId, "tests_run");
48151
+ } catch {}
48152
+ }
48153
+ }
48176
48154
  }
48177
48155
  }
48178
- session.modifiedFilesThisCoderTask = [];
48179
48156
  }
48180
48157
  }
48181
- const window2 = getActiveWindow(input.sessionID);
48182
- if (!window2)
48183
- return;
48184
- const hasError = output.output === null || output.output === undefined;
48185
- if (hasError) {
48186
- window2.consecutiveErrors++;
48187
- } else {
48188
- window2.consecutiveErrors = 0;
48189
- window2.lastSuccessTimeMs = Date.now();
48190
- }
48191
- },
48158
+ }
48159
+ };
48160
+ return {
48192
48161
  messagesTransform: async (_input, output) => {
48193
48162
  const messages = output.messages;
48194
- if (!messages || messages.length === 0) {
48163
+ if (!messages || messages.length === 0)
48195
48164
  return;
48165
+ let lastUserMessageIndex = -1;
48166
+ for (let i2 = messages.length - 1;i2 >= 0; i2--) {
48167
+ if (messages[i2]?.info?.role === "user") {
48168
+ lastUserMessageIndex = i2;
48169
+ break;
48170
+ }
48196
48171
  }
48197
- const lastMessage = messages[messages.length - 1];
48198
- const sessionId = lastMessage.info?.sessionID;
48199
- if (!sessionId) {
48172
+ if (lastUserMessageIndex === -1)
48200
48173
  return;
48201
- }
48202
- {
48203
- const { modelID } = extractModelInfo(messages);
48204
- if (modelID && isLowCapabilityModel(modelID)) {
48205
- for (const msg of messages) {
48206
- if (msg.info?.role !== "system")
48207
- continue;
48208
- for (const part of msg.parts) {
48209
- try {
48210
- if (part == null)
48211
- continue;
48212
- if (part.type !== "text" || typeof part.text !== "string")
48213
- continue;
48214
- if (!part.text.includes("<!-- BEHAVIORAL_GUIDANCE_START -->"))
48215
- continue;
48216
- part.text = part.text.replace(/<!--\s*BEHAVIORAL_GUIDANCE_START\s*-->[\s\S]*?<!--\s*BEHAVIORAL_GUIDANCE_END\s*-->/g, "[Enforcement: programmatic gates active]");
48217
- } catch {}
48218
- }
48174
+ const lastUserMessage = messages[lastUserMessageIndex];
48175
+ if (!lastUserMessage?.parts)
48176
+ return;
48177
+ const agent = lastUserMessage.info?.agent;
48178
+ const strippedAgent = agent ? stripKnownSwarmPrefix(agent) : undefined;
48179
+ if (strippedAgent && strippedAgent !== "architect")
48180
+ return;
48181
+ const textPartIndex = lastUserMessage.parts.findIndex((p) => p?.type === "text" && p.text !== undefined);
48182
+ if (textPartIndex === -1)
48183
+ return;
48184
+ const textPart = lastUserMessage.parts[textPartIndex];
48185
+ const text = textPart.text ?? "";
48186
+ const taskDisclosureSessionID = lastUserMessage.info?.sessionID;
48187
+ if (taskDisclosureSessionID) {
48188
+ const taskSession = ensureAgentSession(taskDisclosureSessionID);
48189
+ const currentTaskIdForWindow = taskSession.currentTaskId;
48190
+ if (currentTaskIdForWindow) {
48191
+ const taskLineRegex = /^[ \t]*-[ \t]*(?:\[[ x]\][ \t]+)?(\d+\.\d+(?:\.\d+)*)[:. ].*/gm;
48192
+ const taskLines = [];
48193
+ taskLineRegex.lastIndex = 0;
48194
+ let regexMatch = taskLineRegex.exec(text);
48195
+ while (regexMatch !== null) {
48196
+ taskLines.push({
48197
+ line: regexMatch[0],
48198
+ taskId: regexMatch[1],
48199
+ index: regexMatch.index
48200
+ });
48201
+ regexMatch = taskLineRegex.exec(text);
48202
+ }
48203
+ if (taskLines.length > 5) {
48204
+ const currentIdx = taskLines.findIndex((t) => t.taskId === currentTaskIdForWindow);
48205
+ const windowStart = Math.max(0, currentIdx - 2);
48206
+ const windowEnd = Math.min(taskLines.length - 1, currentIdx + 3);
48207
+ const visibleTasks = taskLines.slice(windowStart, windowEnd + 1);
48208
+ const hiddenBefore = windowStart;
48209
+ const hiddenAfter = taskLines.length - 1 - windowEnd;
48210
+ const totalTasks = taskLines.length;
48211
+ const visibleCount = visibleTasks.length;
48212
+ const firstTaskIndex = taskLines[0].index;
48213
+ const lastTask = taskLines[taskLines.length - 1];
48214
+ const lastTaskEnd = lastTask.index + lastTask.line.length;
48215
+ const before = text.slice(0, firstTaskIndex);
48216
+ const after = text.slice(lastTaskEnd);
48217
+ const visibleLines = visibleTasks.map((t) => t.line).join(`
48218
+ `);
48219
+ const trimComment = `[Task window: showing ${visibleCount} of ${totalTasks} tasks]`;
48220
+ const trimmedMiddle = (hiddenBefore > 0 ? `[...${hiddenBefore} tasks hidden...]
48221
+ ` : "") + visibleLines + (hiddenAfter > 0 ? `
48222
+ [...${hiddenAfter} tasks hidden...]` : "");
48223
+ textPart.text = `${before}${trimmedMiddle}
48224
+ ${trimComment}${after}`;
48219
48225
  }
48220
48226
  }
48221
48227
  }
48222
- const session = swarmState.agentSessions.get(sessionId);
48223
- const activeAgent = swarmState.activeAgent.get(sessionId);
48224
- const isArchitectSession = activeAgent ? stripKnownSwarmPrefix(activeAgent) === ORCHESTRATOR_NAME : session ? stripKnownSwarmPrefix(session.agentName) === ORCHESTRATOR_NAME : false;
48225
- if (isArchitectSession && session && session.architectWriteCount > 0) {
48226
- const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
48227
- if (textPart2) {
48228
- textPart2.text = `\u26A0\uFE0F SELF-CODING DETECTED: You have used ${session.architectWriteCount} write-class tool(s) directly on non-.swarm/ files.
48229
- ` + `Rule 1 requires ALL coding to be delegated to @coder.
48230
- ` + `If you have not exhausted QA_RETRY_LIMIT coder failures on this task, STOP and delegate.
48231
-
48232
- ` + textPart2.text;
48228
+ const sessionID = lastUserMessage.info?.sessionID;
48229
+ const planTaskId = extractPlanTaskId(text);
48230
+ const taskIdMatch = text.match(/TASK:\s*(.+?)(?:\n|$)/i);
48231
+ const taskIdFromLine = taskIdMatch ? taskIdMatch[1].trim() : null;
48232
+ const currentTaskId = planTaskId ?? taskIdFromLine;
48233
+ const coderDelegationPattern = /(?:^|\n)\s*(?:\w+_)?coder\s*\n\s*TASK:/i;
48234
+ const isCoderDelegation = coderDelegationPattern.test(text);
48235
+ const priorCoderTaskId = sessionID ? ensureAgentSession(sessionID).lastCoderDelegationTaskId ?? null : null;
48236
+ if (sessionID && isCoderDelegation && currentTaskId) {
48237
+ const session = ensureAgentSession(sessionID);
48238
+ session.lastCoderDelegationTaskId = currentTaskId;
48239
+ const fileDirPattern = /^FILE:\s*(.+)$/gm;
48240
+ const declaredFiles = [];
48241
+ for (const match of text.matchAll(fileDirPattern)) {
48242
+ const filePath = match[1].trim();
48243
+ if (filePath.length > 0 && !declaredFiles.includes(filePath)) {
48244
+ declaredFiles.push(filePath);
48245
+ }
48246
+ }
48247
+ session.declaredCoderScope = declaredFiles.length > 0 ? declaredFiles : null;
48248
+ try {
48249
+ advanceTaskState(session, currentTaskId, "coder_delegated");
48250
+ } catch (err2) {
48251
+ console.warn(`[delegation-gate] state machine warn: ${err2 instanceof Error ? err2.message : String(err2)}`);
48233
48252
  }
48234
48253
  }
48235
- if (isArchitectSession && session && session.selfFixAttempted && session.lastGateFailure && Date.now() - session.lastGateFailure.timestamp < 120000) {
48236
- const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
48237
- if (textPart2 && !textPart2.text.includes("SELF-FIX DETECTED")) {
48238
- textPart2.text = `\u26A0\uFE0F SELF-FIX DETECTED: Gate '${session.lastGateFailure.tool}' failed on task ${session.lastGateFailure.taskId}.
48239
- ` + `You are now using a write tool instead of delegating to @coder.
48240
- ` + `GATE FAILURE RESPONSE RULES require: return to coder with structured rejection.
48241
- ` + `Do NOT fix gate failures yourself.
48254
+ if (sessionID && !isCoderDelegation && currentTaskId) {
48255
+ const session = ensureAgentSession(sessionID);
48256
+ if (session.architectWriteCount > 0 && session.lastCoderDelegationTaskId !== currentTaskId) {
48257
+ const warningText2 = `\u26A0\uFE0F DELEGATION VIOLATION: Code modifications detected for task ${currentTaskId} with zero coder delegations.
48258
+ Rule 1: DELEGATE all coding to coder. You do NOT write code.`;
48259
+ textPart.text = `${warningText2}
48242
48260
 
48243
- ` + textPart2.text;
48244
- session.selfFixAttempted = false;
48261
+ ${text}`;
48245
48262
  }
48246
48263
  }
48247
- const isArchitectSessionForGates = activeAgent ? stripKnownSwarmPrefix(activeAgent) === ORCHESTRATOR_NAME : session ? stripKnownSwarmPrefix(session.agentName) === ORCHESTRATOR_NAME : false;
48248
- if (isArchitectSessionForGates && session) {
48249
- const taskId = getCurrentTaskId(sessionId);
48250
- if (!session.partialGateWarningsIssuedForTask.has(taskId)) {
48251
- const gates = session.gateLog.get(taskId);
48252
- const REQUIRED_GATES = [
48253
- "diff",
48254
- "syntax_check",
48255
- "placeholder_scan",
48256
- "lint",
48257
- "pre_check_batch"
48258
- ];
48259
- const missingGates = [];
48260
- if (!gates) {
48261
- missingGates.push(...REQUIRED_GATES);
48262
- } else {
48263
- for (const gate of REQUIRED_GATES) {
48264
- if (!gates.has(gate)) {
48265
- missingGates.push(gate);
48266
- }
48267
- }
48268
- }
48269
- let currentPhaseForCheck = 1;
48270
- try {
48271
- const plan = await loadPlan(directory);
48272
- if (plan) {
48273
- const phaseString = extractCurrentPhaseFromPlan2(plan);
48274
- currentPhaseForCheck = extractPhaseNumber(phaseString);
48264
+ {
48265
+ const deliberationSessionID = lastUserMessage.info?.sessionID;
48266
+ if (deliberationSessionID) {
48267
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(deliberationSessionID)) {} else {
48268
+ const deliberationSession = ensureAgentSession(deliberationSessionID);
48269
+ const lastGate = deliberationSession.lastGateOutcome;
48270
+ let preamble;
48271
+ if (lastGate) {
48272
+ const gateResult = lastGate.passed ? "PASSED" : "FAILED";
48273
+ const sanitizedGate = lastGate.gate.replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 64);
48274
+ const sanitizedTaskId = lastGate.taskId.replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 32);
48275
+ preamble = `[Last gate: ${sanitizedGate} ${gateResult} for task ${sanitizedTaskId}]
48276
+ [DELIBERATE: Before proceeding \u2014 what is the SINGLE next task? What gates must it pass?]`;
48277
+ } else {
48278
+ preamble = `[DELIBERATE: Identify the first task from the plan. What gates must it pass before marking complete?]`;
48275
48279
  }
48276
- } catch {}
48277
- const hasReviewerDelegation = (session.reviewerCallCount.get(currentPhaseForCheck) ?? 0) > 0;
48278
- if (missingGates.length > 0 || !hasReviewerDelegation) {
48279
- const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
48280
- if (textPart2 && !textPart2.text.includes("PARTIAL GATE VIOLATION")) {
48281
- const missing = [...missingGates];
48282
- if (!hasReviewerDelegation) {
48283
- missing.push("reviewer/test_engineer (no delegations this phase)");
48284
- }
48285
- session.partialGateWarningsIssuedForTask.add(taskId);
48286
- textPart2.text = `\u26A0\uFE0F PARTIAL GATE VIOLATION: Task may be marked complete but missing gates: [${missing.join(", ")}].
48287
- ` + `The QA gate is ALL steps or NONE. Revert any \u2713 marks and run the missing gates.
48280
+ const currentText = textPart.text ?? "";
48281
+ textPart.text = `${preamble}
48288
48282
 
48289
- ` + textPart2.text;
48290
- }
48283
+ ${currentText}`;
48291
48284
  }
48292
48285
  }
48293
48286
  }
48294
- if (isArchitectSessionForGates && session && session.scopeViolationDetected) {
48295
- session.scopeViolationDetected = false;
48296
- const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
48297
- if (textPart2 && session.lastScopeViolation) {
48298
- textPart2.text = `\u26A0\uFE0F SCOPE VIOLATION: ${session.lastScopeViolation}
48299
- ` + `Only modify files within your declared scope. Request scope expansion from architect if needed.
48300
-
48301
- ` + textPart2.text;
48287
+ if (!isCoderDelegation)
48288
+ return;
48289
+ const warnings = [];
48290
+ if (text.length > delegationMaxChars) {
48291
+ warnings.push(`Delegation exceeds recommended size (${text.length} chars, limit ${delegationMaxChars}). Consider splitting into smaller tasks.`);
48292
+ }
48293
+ const fileMatches = text.match(/^FILE:/gm);
48294
+ if (fileMatches && fileMatches.length > 1) {
48295
+ warnings.push(`Multiple FILE: directives detected (${fileMatches.length}). Each coder task should target ONE file.`);
48296
+ }
48297
+ const taskMatches = text.match(/^TASK:/gm);
48298
+ if (taskMatches && taskMatches.length > 1) {
48299
+ warnings.push(`Multiple TASK: sections detected (${taskMatches.length}). Send ONE task per coder call.`);
48300
+ }
48301
+ const batchingPattern = /\b(?:and also|then also|additionally|as well as|along with|while you'?re at it)[.,]?\b/gi;
48302
+ const batchingMatches = text.match(batchingPattern);
48303
+ if (batchingMatches && batchingMatches.length > 0) {
48304
+ warnings.push(`Batching language detected (${batchingMatches.join(", ")}). Break compound objectives into separate coder calls.`);
48305
+ }
48306
+ const taskLine = extractTaskLine(text);
48307
+ if (taskLine) {
48308
+ const andPattern = /\s+and\s+(update|add|remove|modify|refactor|implement|create|delete|fix|change|build|deploy|write|test|move|rename|extend|extract|convert|migrate|upgrade|replace)\b/i;
48309
+ if (andPattern.test(taskLine)) {
48310
+ warnings.push('TASK line contains "and" connecting separate actions');
48302
48311
  }
48303
48312
  }
48304
- if (isArchitectSessionForGates && session && session.catastrophicPhaseWarnings) {
48305
- try {
48306
- const plan = await loadPlan(directory);
48307
- if (plan?.phases) {
48308
- for (const phase of plan.phases) {
48309
- if (phase.status === "complete") {
48310
- const phaseNum = phase.id;
48311
- if (!session.catastrophicPhaseWarnings.has(phaseNum)) {
48312
- const reviewerCount = session.reviewerCallCount.get(phaseNum) ?? 0;
48313
- if (reviewerCount === 0) {
48314
- session.catastrophicPhaseWarnings.add(phaseNum);
48315
- const textPart2 = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
48316
- if (textPart2 && !textPart2.text.includes("CATASTROPHIC VIOLATION")) {
48317
- textPart2.text = `[CATASTROPHIC VIOLATION: Phase ${phaseNum} completed with ZERO reviewer delegations.` + ` Every coder task requires reviewer approval. Recommend retrospective review of all Phase ${phaseNum} tasks.]
48318
-
48319
- ` + textPart2.text;
48320
- }
48321
- break;
48322
- }
48323
- }
48313
+ if (sessionID) {
48314
+ const delegationChain = swarmState.delegationChains.get(sessionID);
48315
+ if (delegationChain && delegationChain.length >= 2) {
48316
+ const coderIndices = [];
48317
+ for (let i2 = delegationChain.length - 1;i2 >= 0; i2--) {
48318
+ if (stripKnownSwarmPrefix(delegationChain[i2].to).includes("coder")) {
48319
+ coderIndices.unshift(i2);
48320
+ if (coderIndices.length === 2)
48321
+ break;
48322
+ }
48323
+ }
48324
+ if (coderIndices.length === 2) {
48325
+ const prevCoderIndex = coderIndices[0];
48326
+ const betweenCoders = delegationChain.slice(prevCoderIndex + 1);
48327
+ const hasReviewer = betweenCoders.some((d) => stripKnownSwarmPrefix(d.to) === "reviewer");
48328
+ const hasTestEngineer = betweenCoders.some((d) => stripKnownSwarmPrefix(d.to) === "test_engineer");
48329
+ const session = ensureAgentSession(sessionID);
48330
+ const priorTaskStuckAtCoder = priorCoderTaskId !== null && getTaskState(session, priorCoderTaskId) === "coder_delegated";
48331
+ if (!hasReviewer || !hasTestEngineer || priorTaskStuckAtCoder) {
48332
+ if (session.qaSkipCount >= 1) {
48333
+ const skippedTasks = session.qaSkipTaskIds.join(", ");
48334
+ throw new Error(`\uD83D\uDED1 QA GATE ENFORCEMENT: ${session.qaSkipCount + 1} consecutive coder delegations without reviewer/test_engineer. ` + `Skipped tasks: [${skippedTasks}]. ` + `DELEGATE to reviewer and test_engineer NOW before any further coder work.`);
48324
48335
  }
48336
+ session.qaSkipCount++;
48337
+ session.qaSkipTaskIds.push(currentTaskId ?? "unknown");
48338
+ warnings.push(`\u26A0\uFE0F PROTOCOL VIOLATION: Previous coder task completed, but QA gate was skipped. ` + `You MUST delegate to reviewer (code review) and test_engineer (test execution) ` + `before starting a new coder task. Review RULES 7-8 in your system prompt.`);
48325
48339
  }
48326
48340
  }
48327
- } catch {}
48341
+ }
48328
48342
  }
48329
- const targetWindow = getActiveWindow(sessionId);
48330
- if (!targetWindow || !targetWindow.warningIssued && !targetWindow.hardLimitHit) {
48343
+ if (warnings.length === 0)
48331
48344
  return;
48345
+ const warningLines = warnings.map((w) => `Detected signal: ${w}`);
48346
+ const warningText = `\u26A0\uFE0F BATCH DETECTED: Your coder delegation appears to contain multiple tasks.
48347
+ Rule 3: ONE task per coder call. Split this into separate delegations.
48348
+ ${warningLines.join(`
48349
+ `)}`;
48350
+ const originalText = textPart.text ?? "";
48351
+ textPart.text = `${warningText}
48352
+
48353
+ ${originalText}`;
48354
+ },
48355
+ toolAfter
48356
+ };
48357
+ }
48358
+ // src/hooks/delegation-sanitizer.ts
48359
+ init_utils2();
48360
+ import * as fs13 from "fs";
48361
+ var SANITIZATION_PATTERNS = [
48362
+ /\b\d+(st|nd|rd|th)\s+(attempt|try|time)\b/gi,
48363
+ /\b(5th|fifth|final|last)\s+attempt\b/gi,
48364
+ /attempt\s+\d+\s*\/\s*\d+/gi,
48365
+ /\bthis\s+is\s+(the\s+)?(5th|fifth|final|last)\b/gi,
48366
+ /\bwe('re|\s+are)\s+(behind|late)\b/gi,
48367
+ /\buser\s+is\s+waiting\b/gi,
48368
+ /\bship\s+(this|it)\s+now\b/gi,
48369
+ /\bor\s+I('ll|\s+will)\s+(stop|halt|alert)\b/gi,
48370
+ /\bor\s+all\s+work\s+stops\b/gi,
48371
+ /\bthis\s+will\s+(delay|block)\s+everything\b/gi,
48372
+ /\b(I'm|I\s+am)\s+(frustrated|disappointed)\b/gi
48373
+ ];
48374
+ function sanitizeMessage(text, patterns = SANITIZATION_PATTERNS) {
48375
+ let sanitized = text;
48376
+ const stripped = [];
48377
+ for (const pattern of patterns) {
48378
+ const matches = sanitized.match(pattern);
48379
+ if (matches) {
48380
+ stripped.push(...matches);
48381
+ sanitized = sanitized.replace(pattern, "");
48382
+ }
48383
+ }
48384
+ sanitized = sanitized.replace(/\s+/g, " ").trim();
48385
+ return {
48386
+ sanitized,
48387
+ modified: stripped.length > 0,
48388
+ stripped
48389
+ };
48390
+ }
48391
+ function isGateAgentMessage(agentName) {
48392
+ const gateAgents = ["reviewer", "test_engineer", "critic", "test-engineer"];
48393
+ const normalized = agentName.toLowerCase().replace(/-/g, "_");
48394
+ return gateAgents.includes(normalized);
48395
+ }
48396
+ function createDelegationSanitizerHook(directory) {
48397
+ const hook = async (_input, output) => {
48398
+ const messages = output?.messages;
48399
+ if (!messages || !Array.isArray(messages)) {
48400
+ return;
48401
+ }
48402
+ for (const message of messages) {
48403
+ const info2 = message?.info;
48404
+ if (!info2)
48405
+ continue;
48406
+ const agent = info2.agent;
48407
+ if (!agent || !isGateAgentMessage(agent)) {
48408
+ continue;
48332
48409
  }
48333
- const textPart = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
48334
- if (!textPart) {
48335
- return;
48410
+ if (!message.parts || !Array.isArray(message.parts)) {
48411
+ continue;
48336
48412
  }
48337
- if (targetWindow.hardLimitHit) {
48338
- textPart.text = `[\uD83D\uDED1 LIMIT REACHED: Your resource budget is exhausted. Do not make additional tool calls. Return a summary of your progress and any remaining work.]
48339
-
48340
- ` + textPart.text;
48341
- } else if (targetWindow.warningIssued) {
48342
- const reasonSuffix = targetWindow.warningReason ? ` (${targetWindow.warningReason})` : "";
48343
- textPart.text = `[\u26A0\uFE0F APPROACHING LIMITS${reasonSuffix}: You still have capacity to finish your current step. Complete what you're working on, then return your results.]
48344
-
48345
- ` + textPart.text;
48413
+ for (const part of message.parts) {
48414
+ if (part?.type !== "text" || !part.text) {
48415
+ continue;
48416
+ }
48417
+ const originalText = part.text;
48418
+ const result = sanitizeMessage(originalText);
48419
+ if (result.modified) {
48420
+ part.text = result.sanitized;
48421
+ try {
48422
+ const eventsPath = validateSwarmPath(directory, "events.jsonl");
48423
+ const event = {
48424
+ event: "message_sanitized",
48425
+ agent,
48426
+ original_length: originalText.length,
48427
+ stripped_count: result.stripped.length,
48428
+ stripped_patterns: result.stripped,
48429
+ timestamp: new Date().toISOString()
48430
+ };
48431
+ fs13.appendFileSync(eventsPath, `${JSON.stringify(event)}
48432
+ `, "utf-8");
48433
+ } catch {}
48434
+ }
48346
48435
  }
48347
48436
  }
48348
48437
  };
48438
+ return safeHook(hook);
48349
48439
  }
48350
- function hashArgs(args2) {
48351
- try {
48352
- if (typeof args2 !== "object" || args2 === null) {
48353
- return 0;
48440
+ // src/hooks/delegation-tracker.ts
48441
+ init_constants();
48442
+ init_schema();
48443
+ function createDelegationTrackerHook(config3, guardrailsEnabled = true) {
48444
+ return async (input, _output) => {
48445
+ const now = Date.now();
48446
+ const debugSession = swarmState.agentSessions.get(input.sessionID);
48447
+ const taskStates = debugSession?.taskWorkflowStates ? Object.entries(debugSession.taskWorkflowStates) : [];
48448
+ const statesSummary = taskStates.length > 0 ? taskStates.map(([k, v]) => `${k}=${v}`).join(",") : "(none)";
48449
+ console.log(`[swarm-debug-task] chat.message | session=${input.sessionID} agent=${input.agent ?? "(none)"} prevAgent=${swarmState.activeAgent.get(input.sessionID) ?? "(none)"} taskStates=[${statesSummary}]`);
48450
+ if (!input.agent || input.agent === "") {
48451
+ const session2 = swarmState.agentSessions.get(input.sessionID);
48452
+ if (session2?.delegationActive) {
48453
+ session2.delegationActive = false;
48454
+ swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
48455
+ ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
48456
+ updateAgentEventTime(input.sessionID);
48457
+ } else if (!session2) {
48458
+ ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
48459
+ }
48460
+ return;
48354
48461
  }
48355
- const sortedKeys = Object.keys(args2).sort();
48356
- return Number(Bun.hash(JSON.stringify(args2, sortedKeys)));
48357
- } catch {
48358
- return 0;
48359
- }
48462
+ const agentName = input.agent;
48463
+ const previousAgent = swarmState.activeAgent.get(input.sessionID);
48464
+ swarmState.activeAgent.set(input.sessionID, agentName);
48465
+ const strippedAgent = stripKnownSwarmPrefix(agentName);
48466
+ const isArchitect2 = strippedAgent === ORCHESTRATOR_NAME;
48467
+ const session = ensureAgentSession(input.sessionID, agentName);
48468
+ session.delegationActive = !isArchitect2;
48469
+ recordPhaseAgentDispatch(input.sessionID, agentName);
48470
+ if (!isArchitect2 && guardrailsEnabled) {
48471
+ beginInvocation(input.sessionID, agentName);
48472
+ }
48473
+ const delegationTrackerEnabled = config3.hooks?.delegation_tracker === true;
48474
+ const delegationGateEnabled = config3.hooks?.delegation_gate !== false;
48475
+ if ((delegationTrackerEnabled || delegationGateEnabled) && previousAgent && previousAgent !== agentName) {
48476
+ const entry = {
48477
+ from: previousAgent,
48478
+ to: agentName,
48479
+ timestamp: now
48480
+ };
48481
+ if (!swarmState.delegationChains.has(input.sessionID)) {
48482
+ swarmState.delegationChains.set(input.sessionID, []);
48483
+ }
48484
+ const chain = swarmState.delegationChains.get(input.sessionID);
48485
+ chain?.push(entry);
48486
+ if (delegationTrackerEnabled) {
48487
+ swarmState.pendingEvents++;
48488
+ }
48489
+ }
48490
+ };
48360
48491
  }
48361
48492
  // src/hooks/messages-transform.ts
48362
48493
  function consolidateSystemMessages(messages) {
@@ -60436,6 +60567,13 @@ ${footerLines.join(`
60436
60567
  }
60437
60568
 
60438
60569
  // src/index.ts
60570
+ function debugTaskLog(hook, ctx, extra) {
60571
+ const activeAgent = swarmState.activeAgent.get(ctx.sessionID);
60572
+ const session = swarmState.agentSessions.get(ctx.sessionID);
60573
+ const taskStates = session?.taskWorkflowStates ? Object.entries(session.taskWorkflowStates) : [];
60574
+ const statesSummary = taskStates.length > 0 ? taskStates.map(([k, v]) => `${k}=${v}`).join(",") : "(none)";
60575
+ console.log(`[swarm-debug-task] ${hook} | session=${ctx.sessionID} callID=${ctx.callID ?? "(n/a)"} agent=${activeAgent ?? "(none)"} taskStates=[${statesSummary}]`, extra ? JSON.stringify(extra) : "");
60576
+ }
60439
60577
  var OpenCodeSwarm = async (ctx) => {
60440
60578
  const { config: config3, loadedFromFile } = loadPluginConfigWithMeta(ctx.directory);
60441
60579
  await loadSnapshot(ctx.directory);
@@ -60730,6 +60868,10 @@ var OpenCodeSwarm = async (ctx) => {
60730
60868
  "experimental.session.compacting": compactionHook["experimental.session.compacting"],
60731
60869
  "command.execute.before": safeHook(commandHandler),
60732
60870
  "tool.execute.before": async (input, output) => {
60871
+ debugTaskLog("tool.execute.before", {
60872
+ sessionID: input.sessionID,
60873
+ callID: input.callID
60874
+ }, { tool: input.tool });
60733
60875
  if (!swarmState.activeAgent.has(input.sessionID)) {
60734
60876
  swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
60735
60877
  }
@@ -60749,6 +60891,10 @@ var OpenCodeSwarm = async (ctx) => {
60749
60891
  await safeHook(activityHooks.toolBefore)(input, output);
60750
60892
  },
60751
60893
  "tool.execute.after": async (input, output) => {
60894
+ debugTaskLog("tool.execute.after", {
60895
+ sessionID: input.sessionID,
60896
+ callID: input.callID
60897
+ }, { tool: input.tool });
60752
60898
  await activityHooks.toolAfter(input, output);
60753
60899
  await guardrailsHooks.toolAfter(input, output);
60754
60900
  await safeHook(delegationGateHooks.toolAfter)(input, output);
@@ -60778,6 +60924,9 @@ var OpenCodeSwarm = async (ctx) => {
60778
60924
  }
60779
60925
  if (input.tool === "task") {
60780
60926
  const sessionId = input.sessionID;
60927
+ const beforeSession = swarmState.agentSessions.get(sessionId);
60928
+ const beforeStates = beforeSession?.taskWorkflowStates ? Object.entries(beforeSession.taskWorkflowStates).map(([k, v]) => `${k}=${v}`).join(",") : "(none)";
60929
+ console.log(`[swarm-debug-task] tool.execute.after.taskHandoff | session=${sessionId} BEFORE: taskStates=[${beforeStates}]`);
60781
60930
  swarmState.activeAgent.set(sessionId, ORCHESTRATOR_NAME);
60782
60931
  ensureAgentSession(sessionId, ORCHESTRATOR_NAME);
60783
60932
  const session = swarmState.agentSessions.get(sessionId);
@@ -60785,6 +60934,10 @@ var OpenCodeSwarm = async (ctx) => {
60785
60934
  session.delegationActive = false;
60786
60935
  session.lastAgentEventTime = Date.now();
60787
60936
  }
60937
+ const afterSession = swarmState.agentSessions.get(sessionId);
60938
+ const afterStates = afterSession?.taskWorkflowStates ? Object.entries(afterSession.taskWorkflowStates).map(([k, v]) => `${k}=${v}`).join(",") : "(none)";
60939
+ const afterActive = swarmState.activeAgent.get(sessionId);
60940
+ console.log(`[swarm-debug-task] tool.execute.after.taskHandoff | session=${sessionId} AFTER: activeAgent=${afterActive} delegationActive=${afterSession?.delegationActive} taskStates=[${afterStates}]`);
60788
60941
  }
60789
60942
  },
60790
60943
  "chat.message": safeHook(delegationHandler),