@renseiai/agentfactory 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +2 -2
  2. package/dist/src/config/repository-config.d.ts +14 -0
  3. package/dist/src/config/repository-config.d.ts.map +1 -1
  4. package/dist/src/config/repository-config.js +20 -0
  5. package/dist/src/governor/decision-engine.d.ts +7 -0
  6. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  7. package/dist/src/governor/decision-engine.js +59 -1
  8. package/dist/src/governor/event-types.d.ts +18 -1
  9. package/dist/src/governor/event-types.d.ts.map +1 -1
  10. package/dist/src/governor/event-types.js +4 -0
  11. package/dist/src/governor/governor.d.ts +5 -1
  12. package/dist/src/governor/governor.d.ts.map +1 -1
  13. package/dist/src/governor/governor.js +6 -1
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +1 -0
  17. package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
  18. package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
  19. package/dist/src/merge-queue/adapters/github-native.js +243 -0
  20. package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
  21. package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
  22. package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
  23. package/dist/src/merge-queue/index.d.ts +18 -0
  24. package/dist/src/merge-queue/index.d.ts.map +1 -0
  25. package/dist/src/merge-queue/index.js +28 -0
  26. package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
  27. package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
  28. package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
  29. package/dist/src/merge-queue/types.d.ts +48 -0
  30. package/dist/src/merge-queue/types.d.ts.map +1 -0
  31. package/dist/src/merge-queue/types.js +8 -0
  32. package/dist/src/orchestrator/activity-emitter.d.ts +3 -3
  33. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -1
  34. package/dist/src/orchestrator/activity-emitter.js +1 -1
  35. package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
  36. package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
  37. package/dist/src/orchestrator/artifact-tracker.js +235 -0
  38. package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
  39. package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
  40. package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
  41. package/dist/src/orchestrator/context-manager.d.ts +72 -0
  42. package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
  43. package/dist/src/orchestrator/context-manager.js +120 -0
  44. package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
  45. package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
  46. package/dist/src/orchestrator/context-manager.test.js +137 -0
  47. package/dist/src/orchestrator/detect-work-type.test.js +25 -16
  48. package/dist/src/orchestrator/index.d.ts +12 -2
  49. package/dist/src/orchestrator/index.d.ts.map +1 -1
  50. package/dist/src/orchestrator/index.js +9 -1
  51. package/dist/src/orchestrator/issue-tracker-client.d.ts +103 -0
  52. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -0
  53. package/dist/src/orchestrator/issue-tracker-client.js +8 -0
  54. package/dist/src/orchestrator/log-analyzer.d.ts +19 -4
  55. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -1
  56. package/dist/src/orchestrator/log-analyzer.js +26 -50
  57. package/dist/src/orchestrator/orchestrator-utils.test.js +3 -0
  58. package/dist/src/orchestrator/orchestrator.d.ts +16 -2
  59. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  60. package/dist/src/orchestrator/orchestrator.js +449 -115
  61. package/dist/src/orchestrator/parse-work-result.d.ts +1 -1
  62. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  63. package/dist/src/orchestrator/parse-work-result.js +1 -1
  64. package/dist/src/orchestrator/session-logger.d.ts +1 -1
  65. package/dist/src/orchestrator/session-logger.d.ts.map +1 -1
  66. package/dist/src/orchestrator/state-recovery.d.ts +22 -3
  67. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
  68. package/dist/src/orchestrator/state-recovery.js +55 -2
  69. package/dist/src/orchestrator/state-recovery.test.js +106 -2
  70. package/dist/src/orchestrator/state-types.d.ts +63 -1
  71. package/dist/src/orchestrator/state-types.d.ts.map +1 -1
  72. package/dist/src/orchestrator/state-types.js +5 -1
  73. package/dist/src/orchestrator/summary-builder.d.ts +47 -0
  74. package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
  75. package/dist/src/orchestrator/summary-builder.js +240 -0
  76. package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
  77. package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
  78. package/dist/src/orchestrator/summary-builder.test.js +236 -0
  79. package/dist/src/orchestrator/types.d.ts +24 -2
  80. package/dist/src/orchestrator/types.d.ts.map +1 -1
  81. package/dist/src/orchestrator/work-types.d.ts +50 -0
  82. package/dist/src/orchestrator/work-types.d.ts.map +1 -0
  83. package/dist/src/orchestrator/work-types.js +20 -0
  84. package/dist/src/templates/registry.d.ts +1 -1
  85. package/dist/src/templates/registry.test.js +2 -2
  86. package/dist/src/templates/renderer.d.ts +1 -1
  87. package/dist/src/templates/types.d.ts +6 -2
  88. package/dist/src/templates/types.d.ts.map +1 -1
  89. package/dist/src/templates/types.js +2 -0
  90. package/dist/src/templates/types.test.js +4 -3
  91. package/dist/src/tools/index.d.ts +0 -3
  92. package/dist/src/tools/index.d.ts.map +1 -1
  93. package/dist/src/tools/index.js +0 -2
  94. package/dist/src/workflow/branching-router.d.ts +38 -0
  95. package/dist/src/workflow/branching-router.d.ts.map +1 -0
  96. package/dist/src/workflow/branching-router.js +52 -0
  97. package/dist/src/workflow/branching-router.test.d.ts +2 -0
  98. package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
  99. package/dist/src/workflow/branching-router.test.js +209 -0
  100. package/dist/src/workflow/duration.d.ts +28 -0
  101. package/dist/src/workflow/duration.d.ts.map +1 -0
  102. package/dist/src/workflow/duration.js +57 -0
  103. package/dist/src/workflow/duration.test.d.ts +2 -0
  104. package/dist/src/workflow/duration.test.d.ts.map +1 -0
  105. package/dist/src/workflow/duration.test.js +74 -0
  106. package/dist/src/workflow/expression/ast.d.ts +53 -0
  107. package/dist/src/workflow/expression/ast.d.ts.map +1 -0
  108. package/dist/src/workflow/expression/ast.js +8 -0
  109. package/dist/src/workflow/expression/context.d.ts +40 -0
  110. package/dist/src/workflow/expression/context.d.ts.map +1 -0
  111. package/dist/src/workflow/expression/context.js +37 -0
  112. package/dist/src/workflow/expression/evaluator.d.ts +28 -0
  113. package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
  114. package/dist/src/workflow/expression/evaluator.js +165 -0
  115. package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
  116. package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
  117. package/dist/src/workflow/expression/evaluator.test.js +792 -0
  118. package/dist/src/workflow/expression/expression.test.d.ts +2 -0
  119. package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
  120. package/dist/src/workflow/expression/expression.test.js +516 -0
  121. package/dist/src/workflow/expression/helpers.d.ts +21 -0
  122. package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
  123. package/dist/src/workflow/expression/helpers.js +56 -0
  124. package/dist/src/workflow/expression/index.d.ts +55 -0
  125. package/dist/src/workflow/expression/index.d.ts.map +1 -0
  126. package/dist/src/workflow/expression/index.js +71 -0
  127. package/dist/src/workflow/expression/lexer.d.ts +37 -0
  128. package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
  129. package/dist/src/workflow/expression/lexer.js +166 -0
  130. package/dist/src/workflow/expression/parser.d.ts +23 -0
  131. package/dist/src/workflow/expression/parser.d.ts.map +1 -0
  132. package/dist/src/workflow/expression/parser.js +181 -0
  133. package/dist/src/workflow/index.d.ts +21 -0
  134. package/dist/src/workflow/index.d.ts.map +1 -0
  135. package/dist/src/workflow/index.js +15 -0
  136. package/dist/src/workflow/retry-resolver.d.ts +51 -0
  137. package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
  138. package/dist/src/workflow/retry-resolver.js +70 -0
  139. package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
  140. package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
  141. package/dist/src/workflow/retry-resolver.test.js +149 -0
  142. package/dist/src/workflow/transition-engine.d.ts +46 -0
  143. package/dist/src/workflow/transition-engine.d.ts.map +1 -0
  144. package/dist/src/workflow/transition-engine.js +113 -0
  145. package/dist/src/workflow/transition-engine.test.d.ts +2 -0
  146. package/dist/src/workflow/transition-engine.test.d.ts.map +1 -0
  147. package/dist/src/workflow/transition-engine.test.js +425 -0
  148. package/dist/src/workflow/workflow-loader.d.ts +21 -0
  149. package/dist/src/workflow/workflow-loader.d.ts.map +1 -0
  150. package/dist/src/workflow/workflow-loader.js +40 -0
  151. package/dist/src/workflow/workflow-loader.test.d.ts +2 -0
  152. package/dist/src/workflow/workflow-loader.test.d.ts.map +1 -0
  153. package/dist/src/workflow/workflow-loader.test.js +134 -0
  154. package/dist/src/workflow/workflow-registry.d.ts +97 -0
  155. package/dist/src/workflow/workflow-registry.d.ts.map +1 -0
  156. package/dist/src/workflow/workflow-registry.js +173 -0
  157. package/dist/src/workflow/workflow-registry.test.d.ts +2 -0
  158. package/dist/src/workflow/workflow-registry.test.d.ts.map +1 -0
  159. package/dist/src/workflow/workflow-registry.test.js +201 -0
  160. package/dist/src/workflow/workflow-types.d.ts +442 -0
  161. package/dist/src/workflow/workflow-types.d.ts.map +1 -0
  162. package/dist/src/workflow/workflow-types.js +113 -0
  163. package/dist/src/workflow/workflow-types.test.d.ts +2 -0
  164. package/dist/src/workflow/workflow-types.test.d.ts.map +1 -0
  165. package/dist/src/workflow/workflow-types.test.js +440 -0
  166. package/package.json +3 -4
  167. package/dist/src/linear-cli.d.ts +0 -38
  168. package/dist/src/linear-cli.d.ts.map +0 -1
  169. package/dist/src/linear-cli.js +0 -674
  170. package/dist/src/tools/linear-runner.d.ts +0 -34
  171. package/dist/src/tools/linear-runner.d.ts.map +0 -1
  172. package/dist/src/tools/linear-runner.js +0 -700
  173. package/dist/src/tools/plugins/linear.d.ts +0 -9
  174. package/dist/src/tools/plugins/linear.d.ts.map +0 -1
  175. package/dist/src/tools/plugins/linear.js +0 -138
@@ -13,15 +13,16 @@ import { initializeAgentDir, writeState, updateState, writeTodos, createInitialS
13
13
  import { createHeartbeatWriter, getHeartbeatIntervalFromEnv } from './heartbeat-writer.js';
14
14
  import { createProgressLogger } from './progress-logger.js';
15
15
  import { createSessionLogger } from './session-logger.js';
16
+ import { ContextManager } from './context-manager.js';
16
17
  import { isSessionLoggingEnabled, getLogAnalysisConfig } from './log-config.js';
17
- import { createLinearAgentClient, createAgentSession, buildCompletionComments, STATUS_WORK_TYPE_MAP, WORK_TYPE_START_STATUS, WORK_TYPE_COMPLETE_STATUS, WORK_TYPE_FAIL_STATUS, TERMINAL_STATUSES, WORK_TYPES_REQUIRING_WORKTREE, } from '@renseiai/agentfactory-linear';
18
18
  import { parseWorkResult } from './parse-work-result.js';
19
19
  import { createActivityEmitter } from './activity-emitter.js';
20
20
  import { createApiActivityEmitter } from './api-activity-emitter.js';
21
21
  import { createLogger } from '../logger.js';
22
22
  import { TemplateRegistry, createToolPermissionAdapter } from '../templates/index.js';
23
23
  import { loadRepositoryConfig, getProjectConfig, getProvidersConfig } from '../config/index.js';
24
- import { ToolRegistry, linearPlugin } from '../tools/index.js';
24
+ import { ToolRegistry } from '../tools/index.js';
25
+ import { createMergeQueueAdapter } from '../merge-queue/index.js';
25
26
  // Default inactivity timeout: 5 minutes
26
27
  const DEFAULT_INACTIVITY_TIMEOUT_MS = 300000;
27
28
  // Default max session timeout: unlimited (undefined)
@@ -369,6 +370,53 @@ function checkForIncompleteWork(worktreePath) {
369
370
  };
370
371
  }
371
372
  }
373
+ function checkForPushedWorkWithoutPR(worktreePath) {
374
+ try {
375
+ const currentBranch = execSync('git branch --show-current', {
376
+ cwd: worktreePath,
377
+ encoding: 'utf-8',
378
+ timeout: 10000,
379
+ }).trim();
380
+ // If on main, no work to check
381
+ if (currentBranch === 'main' || currentBranch === 'master') {
382
+ return { hasPushedWork: false };
383
+ }
384
+ // Count commits ahead of main
385
+ const aheadOutput = execSync(`git rev-list --count main..HEAD`, {
386
+ cwd: worktreePath,
387
+ encoding: 'utf-8',
388
+ timeout: 10000,
389
+ }).trim();
390
+ const aheadCount = parseInt(aheadOutput, 10);
391
+ if (aheadCount === 0) {
392
+ return { hasPushedWork: false };
393
+ }
394
+ // Branch has commits ahead of main — check if they've been pushed
395
+ try {
396
+ const remoteRef = execSync(`git ls-remote --heads origin ${currentBranch}`, {
397
+ cwd: worktreePath,
398
+ encoding: 'utf-8',
399
+ timeout: 10000,
400
+ }).trim();
401
+ if (remoteRef.length > 0) {
402
+ // Branch exists on remote with commits ahead of main — likely missing a PR
403
+ return {
404
+ hasPushedWork: true,
405
+ branch: currentBranch,
406
+ details: `Branch \`${currentBranch}\` has ${aheadCount} commit(s) ahead of main and has been pushed to the remote, but no PR was detected.`,
407
+ };
408
+ }
409
+ }
410
+ catch {
411
+ // ls-remote failed — can't confirm remote state
412
+ }
413
+ return { hasPushedWork: false };
414
+ }
415
+ catch {
416
+ // Git commands failed — don't block on our check failing
417
+ return { hasPushedWork: false };
418
+ }
419
+ }
372
420
  /**
373
421
  * Generate a prompt for the agent based on work type
374
422
  *
@@ -442,6 +490,46 @@ Dependencies are symlinked from the main repo by the orchestrator. Do NOT run pn
442
490
  If you encounter a specific "Cannot find module" error, run it SYNCHRONOUSLY
443
491
  (never with run_in_background). Never use sleep or polling loops to wait for commands.
444
492
 
493
+ IMPORTANT: If you encounter "exceeds maximum allowed tokens" error when reading files:
494
+ - Use Grep to search for specific code patterns instead of reading entire files
495
+ - Use Read with offset/limit parameters to paginate through large files
496
+ - Avoid reading auto-generated files like payload-types.ts (use Grep instead)
497
+ See the "Working with Large Files" section in the project documentation (CLAUDE.md / AGENTS.md) for details.${LINEAR_CLI_INSTRUCTION}`;
498
+ break;
499
+ case 'inflight-coordination':
500
+ basePrompt = `Resume coordination of sub-issue execution for parent issue ${identifier}.
501
+ Check sub-issue statuses, continue work on incomplete sub-issues, and create a PR when all are done.
502
+
503
+ SUB-ISSUE STATUS MANAGEMENT:
504
+ You MUST update sub-issue statuses in Linear as work progresses:
505
+ - When starting work on a sub-issue: pnpm af-linear update-sub-issue <id> --state Started
506
+ - When a sub-agent completes a sub-issue: pnpm af-linear update-sub-issue <id> --state Finished --comment "Completed by coordinator agent"
507
+ - If a sub-agent fails on a sub-issue: pnpm af-linear create-comment <sub-issue-id> --body "Sub-agent failed: <reason>"
508
+
509
+ COMPLETION VERIFICATION:
510
+ Before marking the parent issue as complete, verify ALL sub-issues are in Finished status:
511
+ pnpm af-linear list-sub-issue-statuses ${identifier}
512
+ If any sub-issue is not Finished, report the failure and do not mark the parent as complete.
513
+
514
+ SUB-AGENT SAFETY RULES (CRITICAL):
515
+ This is a SHARED WORKTREE. Multiple sub-agents run concurrently in this directory.
516
+ Every sub-agent prompt you construct MUST include these rules:
517
+
518
+ 1. NEVER run: git worktree remove, git worktree prune
519
+ 2. NEVER run: git checkout, git switch (to a different branch)
520
+ 3. NEVER run: git reset --hard, git clean -fd, git restore .
521
+ 4. NEVER delete or modify the .git file in the worktree root
522
+ 5. Only the orchestrator manages worktree lifecycle
523
+ 6. Work only on files relevant to your sub-issue to minimize conflicts
524
+ 7. Commit changes with descriptive messages before reporting completion
525
+
526
+ Prefix every sub-agent prompt with: "SHARED WORKTREE — DO NOT MODIFY GIT STATE"
527
+
528
+ DEPENDENCY INSTALLATION:
529
+ Dependencies are symlinked from the main repo by the orchestrator. Do NOT run pnpm install.
530
+ If you encounter a specific "Cannot find module" error, run it SYNCHRONOUSLY
531
+ (never with run_in_background). Never use sleep or polling loops to wait for commands.
532
+
445
533
  IMPORTANT: If you encounter "exceeds maximum allowed tokens" error when reading files:
446
534
  - Use Grep to search for specific code patterns instead of reading entire files
447
535
  - Use Read with offset/limit parameters to paginate through large files
@@ -632,6 +720,13 @@ IMPORTANT: If you encounter "exceeds maximum allowed tokens" error when reading
632
720
  - Avoid reading auto-generated files like payload-types.ts (use Grep instead)
633
721
  See the "Working with Large Files" section in the project documentation (CLAUDE.md / AGENTS.md) for details.${LINEAR_CLI_INSTRUCTION}`;
634
722
  break;
723
+ case 'merge':
724
+ basePrompt = `Handle merge queue operations for ${identifier}.
725
+ Check PR merge readiness (CI status, approvals).
726
+ Attempt rebase onto latest main.
727
+ Resolve conflicts using mergiraf-enhanced git merge if available.
728
+ Push updated branch and trigger merge via configured merge queue provider.${LINEAR_CLI_INSTRUCTION}`;
729
+ break;
635
730
  }
636
731
  // Inject workflow failure context for retries
637
732
  if (options?.failureContext) {
@@ -651,6 +746,7 @@ const WORK_TYPE_SUFFIX = {
651
746
  'backlog-creation': 'BC',
652
747
  development: 'DEV',
653
748
  inflight: 'INF',
749
+ 'inflight-coordination': 'INF-COORD',
654
750
  coordination: 'COORD',
655
751
  qa: 'QA',
656
752
  acceptance: 'AC',
@@ -658,6 +754,7 @@ const WORK_TYPE_SUFFIX = {
658
754
  'refinement-coordination': 'REF-COORD',
659
755
  'qa-coordination': 'QA-COORD',
660
756
  'acceptance-coordination': 'AC-COORD',
757
+ merge: 'MRG',
661
758
  };
662
759
  /**
663
760
  * Generate a worktree identifier that includes the work type suffix
@@ -678,8 +775,9 @@ export function getWorktreeIdentifier(issueIdentifier, workType) {
678
775
  * being dispatched as 'development' (which uses the wrong template and
679
776
  * produces no sub-agent orchestration).
680
777
  */
681
- export function detectWorkType(statusName, isParent) {
682
- let workType = STATUS_WORK_TYPE_MAP[statusName] ?? 'development';
778
+ export function detectWorkType(statusName, isParent, statusToWorkType) {
779
+ const mapping = statusToWorkType ?? {};
780
+ let workType = mapping[statusName] ?? 'development';
683
781
  console.log(`Auto-detected work type: ${workType} (from status: ${statusName})`);
684
782
  if (isParent) {
685
783
  if (workType === 'development')
@@ -688,6 +786,8 @@ export function detectWorkType(statusName, isParent) {
688
786
  workType = 'qa-coordination';
689
787
  else if (workType === 'acceptance')
690
788
  workType = 'acceptance-coordination';
789
+ else if (workType === 'inflight')
790
+ workType = 'inflight-coordination';
691
791
  else if (workType === 'refinement')
692
792
  workType = 'refinement-coordination';
693
793
  console.log(`Upgraded to coordination work type: ${workType} (parent issue)`);
@@ -697,6 +797,7 @@ export function detectWorkType(statusName, isParent) {
697
797
  export class AgentOrchestrator {
698
798
  config;
699
799
  client;
800
+ statusMappings;
700
801
  events;
701
802
  activeAgents = new Map();
702
803
  agentHandles = new Map();
@@ -717,6 +818,7 @@ export class AgentOrchestrator {
717
818
  progressLoggers = new Map();
718
819
  // Session loggers per agent for verbose analysis logging
719
820
  sessionLoggers = new Map();
821
+ contextManagers = new Map();
720
822
  // Template registry for configurable workflow prompts
721
823
  templateRegistry;
722
824
  // Allowlisted project names from .agentfactory/config.yaml
@@ -737,10 +839,17 @@ export class AgentOrchestrator {
737
839
  validateCommand;
738
840
  // Tool plugin registry for in-process agent tools
739
841
  toolRegistry;
842
+ // Merge queue adapter for automated merge operations (initialized from config or repo config)
843
+ mergeQueueAdapter;
844
+ // Git repository root for running git commands (resolved from worktreePath or cwd)
845
+ gitRoot;
740
846
  constructor(config = {}, events = {}) {
741
- const apiKey = config.linearApiKey ?? process.env.LINEAR_API_KEY;
742
- if (!apiKey) {
743
- throw new Error('LINEAR_API_KEY is required');
847
+ // Validate that an issue tracker client is available
848
+ if (!config.issueTrackerClient) {
849
+ const apiKey = config.linearApiKey ?? process.env.LINEAR_API_KEY;
850
+ if (!apiKey) {
851
+ throw new Error('Either issueTrackerClient or LINEAR_API_KEY is required');
852
+ }
744
853
  }
745
854
  // Parse timeout config from environment variables (can be overridden by config)
746
855
  const envInactivityTimeout = process.env.AGENT_INACTIVITY_TIMEOUT_MS
@@ -752,7 +861,7 @@ export class AgentOrchestrator {
752
861
  this.config = {
753
862
  ...DEFAULT_CONFIG,
754
863
  ...config,
755
- linearApiKey: apiKey,
864
+ linearApiKey: config.linearApiKey ?? process.env.LINEAR_API_KEY ?? '',
756
865
  streamConfig: {
757
866
  ...DEFAULT_CONFIG.streamConfig,
758
867
  ...config.streamConfig,
@@ -763,11 +872,15 @@ export class AgentOrchestrator {
763
872
  inactivityTimeoutMs: config.inactivityTimeoutMs ?? envInactivityTimeout ?? DEFAULT_CONFIG.inactivityTimeoutMs,
764
873
  maxSessionTimeoutMs: config.maxSessionTimeoutMs ?? envMaxSessionTimeout ?? DEFAULT_CONFIG.maxSessionTimeoutMs,
765
874
  };
875
+ // Resolve git root from worktreePath (which may point to a different repo than cwd)
876
+ this.gitRoot = findRepoRoot(resolve(this.config.worktreePath)) ?? findRepoRoot(process.cwd()) ?? process.cwd();
766
877
  // Validate git remote matches configured repository (if set)
767
878
  if (this.config.repository) {
768
- validateGitRemote(this.config.repository);
879
+ validateGitRemote(this.config.repository, this.gitRoot);
769
880
  }
770
- this.client = createLinearAgentClient({ apiKey });
881
+ // Use injected client or fail (caller must provide one)
882
+ this.client = config.issueTrackerClient;
883
+ this.statusMappings = config.statusMappings;
771
884
  this.events = events;
772
885
  // Initialize default agent provider — per-spawn resolution may override
773
886
  const providerName = resolveProviderName({ project: config.project });
@@ -779,8 +892,8 @@ export class AgentOrchestrator {
779
892
  if (config.templateDir) {
780
893
  templateDirs.push(config.templateDir);
781
894
  }
782
- // Auto-detect .agentfactory/templates/ in working directory
783
- const projectTemplateDir = resolve(process.cwd(), '.agentfactory', 'templates');
895
+ // Auto-detect .agentfactory/templates/ in target repo
896
+ const projectTemplateDir = resolve(this.gitRoot, '.agentfactory', 'templates');
784
897
  if (existsSync(projectTemplateDir) && !templateDirs.includes(projectTemplateDir)) {
785
898
  templateDirs.push(projectTemplateDir);
786
899
  }
@@ -797,7 +910,7 @@ export class AgentOrchestrator {
797
910
  }
798
911
  // Auto-load .agentfactory/config.yaml from repository root
799
912
  try {
800
- const repoRoot = findRepoRoot(process.cwd());
913
+ const repoRoot = this.gitRoot;
801
914
  if (repoRoot) {
802
915
  const repoConfig = loadRepositoryConfig(repoRoot);
803
916
  if (repoConfig) {
@@ -805,7 +918,7 @@ export class AgentOrchestrator {
805
918
  // Use repository from config as fallback if not set in OrchestratorConfig
806
919
  if (!this.config.repository && repoConfig.repository) {
807
920
  this.config.repository = repoConfig.repository;
808
- validateGitRemote(this.config.repository);
921
+ validateGitRemote(this.config.repository, this.gitRoot);
809
922
  }
810
923
  // Store allowedProjects for backlog filtering
811
924
  if (repoConfig.projectPaths) {
@@ -839,15 +952,33 @@ export class AgentOrchestrator {
839
952
  }
840
953
  // Store providers config for per-spawn resolution
841
954
  this.configProviders = getProvidersConfig(repoConfig);
955
+ // Initialize merge queue adapter from repository config
956
+ if (repoConfig.mergeQueue?.enabled && !config.mergeQueueAdapter) {
957
+ try {
958
+ this.mergeQueueAdapter = createMergeQueueAdapter(repoConfig.mergeQueue.provider ?? 'github-native');
959
+ console.log(`[orchestrator] Merge queue adapter initialized: ${repoConfig.mergeQueue.provider ?? 'github-native'}`);
960
+ }
961
+ catch (error) {
962
+ console.warn(`[orchestrator] Failed to initialize merge queue adapter: ${error instanceof Error ? error.message : String(error)}`);
963
+ }
964
+ }
842
965
  }
843
966
  }
844
967
  }
845
968
  catch (err) {
846
969
  console.warn('[orchestrator] Failed to load .agentfactory/config.yaml:', err instanceof Error ? err.message : err);
847
970
  }
848
- // Initialize tool plugin registry with Linear plugin
971
+ // Accept merge queue adapter passed directly via config (takes precedence over repo config)
972
+ if (config.mergeQueueAdapter) {
973
+ this.mergeQueueAdapter = config.mergeQueueAdapter;
974
+ }
975
+ // Initialize tool plugin registry with injected plugins
849
976
  this.toolRegistry = new ToolRegistry();
850
- this.toolRegistry.register(linearPlugin);
977
+ if (config.toolPlugins) {
978
+ for (const plugin of config.toolPlugins) {
979
+ this.toolRegistry.register(plugin);
980
+ }
981
+ }
851
982
  }
852
983
  /**
853
984
  * Update the last activity timestamp for an agent (for inactivity timeout tracking)
@@ -891,86 +1022,63 @@ export class AgentOrchestrator {
891
1022
  */
892
1023
  async detectWorkType(issueId, statusName) {
893
1024
  const isParent = await this.client.isParentIssue(issueId);
894
- return detectWorkType(statusName, isParent);
1025
+ return detectWorkType(statusName, isParent, this.statusMappings.statusToWorkType);
895
1026
  }
896
1027
  /**
897
1028
  * Get backlog issues for the configured project
898
1029
  */
899
1030
  async getBacklogIssues(limit) {
900
1031
  const maxIssues = limit ?? this.config.maxConcurrent;
901
- // Build filter based on project
902
- const filter = {
903
- state: { name: { eqIgnoreCase: 'Backlog' } },
904
- };
905
- if (this.config.project) {
906
- const projects = await this.client.linearClient.projects({
907
- filter: { name: { eqIgnoreCase: this.config.project } },
908
- });
909
- if (projects.nodes.length > 0) {
910
- filter.project = { id: { eq: projects.nodes[0].id } };
911
- // Cross-reference project repo metadata with config (SUP-725)
912
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- runtime check for method added in SUP-725
913
- const clientAny = this.client;
914
- if (this.config.repository && typeof clientAny.getProjectRepositoryUrl === 'function') {
915
- try {
916
- const projectRepoUrl = await clientAny.getProjectRepositoryUrl(projects.nodes[0].id);
917
- if (projectRepoUrl) {
918
- const normalizedProjectRepo = projectRepoUrl
919
- .replace(/^https?:\/\//, '')
920
- .replace(/\.git$/, '');
921
- const normalizedConfigRepo = this.config.repository
922
- .replace(/^https?:\/\//, '')
923
- .replace(/\.git$/, '');
924
- if (!normalizedProjectRepo.includes(normalizedConfigRepo) && !normalizedConfigRepo.includes(normalizedProjectRepo)) {
925
- console.warn(`Warning: Project '${this.config.project}' repository metadata '${projectRepoUrl}' ` +
926
- `does not match configured repository '${this.config.repository}'. Skipping issues.`);
927
- return [];
928
- }
929
- }
930
- }
931
- catch (error) {
932
- // Non-fatal: log warning but continue if metadata check fails
933
- console.warn('Warning: Could not check project repository metadata:', error instanceof Error ? error.message : String(error));
1032
+ // Cross-reference project repo metadata with config
1033
+ if (this.config.project && this.config.repository) {
1034
+ try {
1035
+ const projectRepoUrl = await this.client.getProjectRepositoryUrl(this.config.project);
1036
+ if (projectRepoUrl) {
1037
+ const normalizedProjectRepo = projectRepoUrl
1038
+ .replace(/^https?:\/\//, '')
1039
+ .replace(/\.git$/, '');
1040
+ const normalizedConfigRepo = this.config.repository
1041
+ .replace(/^https?:\/\//, '')
1042
+ .replace(/\.git$/, '');
1043
+ if (!normalizedProjectRepo.includes(normalizedConfigRepo) && !normalizedConfigRepo.includes(normalizedProjectRepo)) {
1044
+ console.warn(`Warning: Project '${this.config.project}' repository metadata '${projectRepoUrl}' ` +
1045
+ `does not match configured repository '${this.config.repository}'. Skipping issues.`);
1046
+ return [];
934
1047
  }
935
1048
  }
936
1049
  }
1050
+ catch (error) {
1051
+ // Non-fatal: log warning but continue if metadata check fails
1052
+ console.warn('Warning: Could not check project repository metadata:', error instanceof Error ? error.message : String(error));
1053
+ }
937
1054
  }
938
- const issues = await this.client.linearClient.issues({
939
- filter,
940
- first: maxIssues * 2, // Fetch extra to account for filtering
1055
+ // Query issues using the abstract client
1056
+ const allIssues = await this.client.queryIssues({
1057
+ project: this.config.project,
1058
+ status: 'Backlog',
1059
+ maxResults: maxIssues * 2, // Fetch extra to account for filtering
941
1060
  });
942
1061
  const results = [];
943
- for (const issue of issues.nodes) {
1062
+ for (const issue of allIssues) {
944
1063
  if (results.length >= maxIssues)
945
1064
  break;
946
1065
  // Filter by allowedProjects from .agentfactory/config.yaml
947
- let resolvedProjectName;
948
1066
  if (this.allowedProjects && this.allowedProjects.length > 0) {
949
- const project = await issue.project;
950
- const projectName = project?.name;
951
- if (!projectName || !this.allowedProjects.includes(projectName)) {
952
- console.warn(`[orchestrator] Skipping issue ${issue.identifier} — project "${projectName ?? '(none)'}" is not in allowedProjects: [${this.allowedProjects.join(', ')}]`);
1067
+ if (!issue.projectName || !this.allowedProjects.includes(issue.projectName)) {
1068
+ console.warn(`[orchestrator] Skipping issue ${issue.identifier} — project "${issue.projectName ?? '(none)'}" is not in allowedProjects: [${this.allowedProjects.join(', ')}]`);
953
1069
  continue;
954
1070
  }
955
- resolvedProjectName = projectName;
956
- }
957
- // Resolve project name for path scoping even when not filtering by allowedProjects
958
- if (!resolvedProjectName && this.projectPaths) {
959
- const project = await issue.project;
960
- resolvedProjectName = project?.name;
961
1071
  }
962
- const labels = await issue.labels();
963
- const team = await issue.team;
964
1072
  results.push({
965
1073
  id: issue.id,
966
1074
  identifier: issue.identifier,
967
1075
  title: issue.title,
968
- description: issue.description ?? undefined,
1076
+ description: issue.description,
969
1077
  url: issue.url,
970
1078
  priority: issue.priority,
971
- labels: labels.nodes.map((l) => l.name),
972
- teamName: team?.key,
973
- projectName: resolvedProjectName,
1079
+ labels: issue.labels,
1080
+ teamName: issue.teamName,
1081
+ projectName: issue.projectName,
974
1082
  });
975
1083
  }
976
1084
  // Sort by priority (lower number = higher priority, 0 means no priority -> goes last)
@@ -1075,6 +1183,7 @@ export class AgentOrchestrator {
1075
1183
  const output = execSync('git worktree list --porcelain', {
1076
1184
  stdio: 'pipe',
1077
1185
  encoding: 'utf-8',
1186
+ cwd: this.gitRoot,
1078
1187
  });
1079
1188
  const mainTreeMatch = output.match(/^worktree (.+)$/m);
1080
1189
  if (mainTreeMatch) {
@@ -1134,7 +1243,7 @@ export class AgentOrchestrator {
1134
1243
  if (!existsSync(conflictPath)) {
1135
1244
  // Directory doesn't exist - just prune git's worktree list
1136
1245
  try {
1137
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1246
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1138
1247
  console.log(`Pruned stale worktree reference for branch ${branchName}`);
1139
1248
  return true;
1140
1249
  }
@@ -1203,6 +1312,7 @@ export class AgentOrchestrator {
1203
1312
  execSync(`git worktree remove "${conflictPath}" --force`, {
1204
1313
  stdio: 'pipe',
1205
1314
  encoding: 'utf-8',
1315
+ cwd: this.gitRoot,
1206
1316
  });
1207
1317
  console.log(`Removed stale worktree: ${conflictPath}`);
1208
1318
  return true;
@@ -1219,7 +1329,7 @@ export class AgentOrchestrator {
1219
1329
  // this path is inside .worktrees/ and is not the main tree)
1220
1330
  try {
1221
1331
  execSync(`rm -rf "${conflictPath}"`, { stdio: 'pipe', encoding: 'utf-8' });
1222
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1332
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1223
1333
  console.log(`Force-removed stale worktree: ${conflictPath}`);
1224
1334
  return true;
1225
1335
  }
@@ -1264,7 +1374,7 @@ export class AgentOrchestrator {
1264
1374
  }
1265
1375
  // Prune any stale worktrees first (handles deleted directories)
1266
1376
  try {
1267
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1377
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1268
1378
  }
1269
1379
  catch {
1270
1380
  // Ignore prune errors
@@ -1306,6 +1416,7 @@ export class AgentOrchestrator {
1306
1416
  execSync(`git worktree add "${worktreePath}" -b ${branchName} ${baseBranch}`, {
1307
1417
  stdio: 'pipe',
1308
1418
  encoding: 'utf-8',
1419
+ cwd: this.gitRoot,
1309
1420
  });
1310
1421
  }
1311
1422
  catch (error) {
@@ -1332,6 +1443,7 @@ export class AgentOrchestrator {
1332
1443
  execSync(`git worktree add "${worktreePath}" ${branchName}`, {
1333
1444
  stdio: 'pipe',
1334
1445
  encoding: 'utf-8',
1446
+ cwd: this.gitRoot,
1335
1447
  });
1336
1448
  }
1337
1449
  catch (innerError) {
@@ -1366,7 +1478,7 @@ export class AgentOrchestrator {
1366
1478
  if (existsSync(worktreePath)) {
1367
1479
  execSync(`rm -rf "${worktreePath}"`, { stdio: 'pipe', encoding: 'utf-8' });
1368
1480
  }
1369
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1481
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1370
1482
  }
1371
1483
  catch {
1372
1484
  // Ignore cleanup errors
@@ -1385,6 +1497,8 @@ export class AgentOrchestrator {
1385
1497
  }
1386
1498
  // Write helper scripts into .agent/ for agent use
1387
1499
  this.writeWorktreeHelpers(worktreePath);
1500
+ // Configure mergiraf merge driver if enabled
1501
+ this.configureMergiraf(worktreePath);
1388
1502
  return { worktreePath, worktreeIdentifier };
1389
1503
  }
1390
1504
  /**
@@ -1399,13 +1513,14 @@ export class AgentOrchestrator {
1399
1513
  execSync(`git worktree remove "${worktreePath}" --force`, {
1400
1514
  stdio: 'pipe',
1401
1515
  encoding: 'utf-8',
1516
+ cwd: this.gitRoot,
1402
1517
  });
1403
1518
  }
1404
1519
  catch (error) {
1405
1520
  console.warn(`Failed to remove worktree via git, trying fallback:`, error);
1406
1521
  try {
1407
1522
  execSync(`rm -rf "${worktreePath}"`, { stdio: 'pipe', encoding: 'utf-8' });
1408
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1523
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1409
1524
  }
1410
1525
  catch (fallbackError) {
1411
1526
  console.warn(`Fallback worktree removal also failed:`, fallbackError);
@@ -1415,7 +1530,7 @@ export class AgentOrchestrator {
1415
1530
  else {
1416
1531
  // Directory gone but git may still track it
1417
1532
  try {
1418
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1533
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1419
1534
  }
1420
1535
  catch {
1421
1536
  // Ignore
@@ -1464,6 +1579,39 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1464
1579
  console.warn(`Failed to write worktree helper scripts: ${error instanceof Error ? error.message : String(error)}`);
1465
1580
  }
1466
1581
  }
1582
+ /**
1583
+ * Configure mergiraf as the git merge driver in a worktree.
1584
+ * Falls back silently to default git merge if mergiraf is not installed.
1585
+ */
1586
+ configureMergiraf(worktreePath) {
1587
+ // Check if mergiraf is disabled via config
1588
+ if (this.repoConfig?.mergeDriver === 'default') {
1589
+ return;
1590
+ }
1591
+ try {
1592
+ // Check if mergiraf binary is available
1593
+ execSync('which mergiraf', { stdio: 'pipe', encoding: 'utf-8' });
1594
+ }
1595
+ catch {
1596
+ // mergiraf not installed — fall back to default merge silently
1597
+ console.log('mergiraf not found on PATH, using default git merge driver');
1598
+ return;
1599
+ }
1600
+ try {
1601
+ // Register mergiraf as the merge driver in this worktree
1602
+ // This sets up .git/config merge driver entries and .gitattributes
1603
+ execSync('mergiraf register', {
1604
+ stdio: 'pipe',
1605
+ encoding: 'utf-8',
1606
+ cwd: worktreePath,
1607
+ });
1608
+ console.log(`mergiraf registered as merge driver in ${worktreePath}`);
1609
+ }
1610
+ catch (error) {
1611
+ // Log warning but don't fail — merge driver is non-critical
1612
+ console.warn(`Failed to register mergiraf in worktree: ${error instanceof Error ? error.message : String(error)}`);
1613
+ }
1614
+ }
1467
1615
  /**
1468
1616
  * Link dependencies from the main repo into a worktree via symlinks.
1469
1617
  *
@@ -1754,6 +1902,9 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1754
1902
  this.sessionLoggers.set(issueId, sessionLogger);
1755
1903
  log.debug('Session logging initialized', { logsDir: logConfig.logsDir });
1756
1904
  }
1905
+ // Initialize context manager for context window management
1906
+ const contextManager = ContextManager.load(worktreePath);
1907
+ this.contextManagers.set(issueId, contextManager);
1757
1908
  log.debug('State persistence initialized', { agentDir: resolve(worktreePath, '.agent') });
1758
1909
  }
1759
1910
  catch (stateError) {
@@ -1792,8 +1943,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1792
1943
  else {
1793
1944
  // Direct Linear API - only works with OAuth tokens (not API keys)
1794
1945
  // This will fail for createAgentActivity calls but works for comments
1795
- const session = createAgentSession({
1796
- client: this.client.linearClient,
1946
+ const session = this.client.createSession({
1797
1947
  issueId,
1798
1948
  sessionId,
1799
1949
  autoTransition: false, // Orchestrator handles transitions
@@ -1847,7 +1997,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1847
1997
  // Set work type so agent knows what kind of work it's doing
1848
1998
  env.LINEAR_WORK_TYPE = workType;
1849
1999
  // Flag shared worktree for coordination mode so sub-agents know not to modify git state
1850
- if (workType === 'coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination' || workType === 'refinement-coordination') {
2000
+ if (workType === 'coordination' || workType === 'inflight-coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination' || workType === 'refinement-coordination') {
1851
2001
  env.SHARED_WORKTREE = 'true';
1852
2002
  }
1853
2003
  // Set Claude Code Task List ID for intra-issue task coordination
@@ -1866,7 +2016,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1866
2016
  // Coordinators need significantly more turns than standard agents
1867
2017
  // since they spawn sub-agents and poll their status repeatedly.
1868
2018
  // Inflight also gets the bump — it may be resuming coordination work.
1869
- const needsMoreTurns = workType === 'coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination' || workType === 'refinement-coordination' || workType === 'inflight';
2019
+ const needsMoreTurns = workType === 'coordination' || workType === 'inflight-coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination' || workType === 'refinement-coordination' || workType === 'inflight';
1870
2020
  const maxTurns = needsMoreTurns ? 200 : undefined;
1871
2021
  // Spawn agent via provider interface
1872
2022
  const spawnConfig = {
@@ -1958,6 +2108,41 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1958
2108
  if (emitter) {
1959
2109
  await emitter.flush();
1960
2110
  }
2111
+ // Post-exit PR detection: if the agent exited without a detected PR URL,
2112
+ // check GitHub directly in case the PR was created but the output wasn't captured
2113
+ if (agent.status === 'completed' && !agent.pullRequestUrl && agent.worktreePath) {
2114
+ const postExitWorkType = agent.workType ?? 'development';
2115
+ const isPostExitCodeProducing = postExitWorkType === 'development' || postExitWorkType === 'inflight';
2116
+ if (isPostExitCodeProducing) {
2117
+ try {
2118
+ const currentBranch = execSync('git branch --show-current', {
2119
+ cwd: agent.worktreePath,
2120
+ encoding: 'utf-8',
2121
+ timeout: 10000,
2122
+ }).trim();
2123
+ if (currentBranch && currentBranch !== 'main' && currentBranch !== 'master') {
2124
+ const prJson = execSync(`gh pr list --head "${currentBranch}" --json url --limit 1`, {
2125
+ cwd: agent.worktreePath,
2126
+ encoding: 'utf-8',
2127
+ timeout: 15000,
2128
+ }).trim();
2129
+ const prs = JSON.parse(prJson);
2130
+ if (prs.length > 0 && prs[0].url) {
2131
+ log?.info('Post-exit PR detection found existing PR', { prUrl: prs[0].url, branch: currentBranch });
2132
+ agent.pullRequestUrl = prs[0].url;
2133
+ if (sessionId) {
2134
+ await this.updateSessionPullRequest(sessionId, prs[0].url, agent);
2135
+ }
2136
+ }
2137
+ }
2138
+ }
2139
+ catch (error) {
2140
+ log?.debug('Post-exit PR detection failed (non-fatal)', {
2141
+ error: error instanceof Error ? error.message : String(error),
2142
+ });
2143
+ }
2144
+ }
2145
+ }
1961
2146
  // Update Linear status based on work type if auto-transition is enabled
1962
2147
  if (agent.status === 'completed' && this.config.autoTransition) {
1963
2148
  const workType = agent.workType ?? 'development';
@@ -1977,11 +2162,11 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1977
2162
  }
1978
2163
  agent.workResult = workResult;
1979
2164
  if (workResult === 'passed') {
1980
- targetStatus = WORK_TYPE_COMPLETE_STATUS[workType];
2165
+ targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
1981
2166
  log?.info('Work result: passed, promoting', { workType, targetStatus });
1982
2167
  }
1983
2168
  else if (workResult === 'failed') {
1984
- targetStatus = WORK_TYPE_FAIL_STATUS[workType];
2169
+ targetStatus = this.statusMappings.workTypeFailStatus[workType];
1985
2170
  log?.info('Work result: failed, transitioning to fail status', { workType, targetStatus });
1986
2171
  }
1987
2172
  else {
@@ -2009,8 +2194,63 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2009
2194
  }
2010
2195
  }
2011
2196
  else {
2012
- // Non-QA/acceptance: unchanged behavior always promote on completion
2013
- targetStatus = WORK_TYPE_COMPLETE_STATUS[workType];
2197
+ // Non-QA/acceptance: promote on completion, but validate code-producing work types first
2198
+ const isCodeProducing = workType === 'development' || workType === 'inflight';
2199
+ if (isCodeProducing && agent.worktreePath && !agent.pullRequestUrl) {
2200
+ // Code-producing agent completed without a detected PR — check for commits
2201
+ const incompleteCheck = checkForIncompleteWork(agent.worktreePath);
2202
+ if (incompleteCheck.hasIncompleteWork) {
2203
+ // Agent has uncommitted/unpushed changes — block promotion
2204
+ log?.error('Code-producing agent completed without PR and has incomplete work — blocking promotion', {
2205
+ workType,
2206
+ reason: incompleteCheck.reason,
2207
+ details: incompleteCheck.details,
2208
+ });
2209
+ // Post a diagnostic comment
2210
+ try {
2211
+ await this.client.createComment(issueId, `⚠️ **Agent completed but work was not persisted.**\n\n` +
2212
+ `The agent reported success but no PR was detected, and the worktree has ${incompleteCheck.details}.\n\n` +
2213
+ `**Issue status was NOT promoted** to prevent lost work from advancing through the pipeline.\n\n` +
2214
+ `The worktree has been preserved at \`${agent.worktreePath}\`. ` +
2215
+ `To recover: cd into the worktree, commit, push, and create a PR manually.`);
2216
+ }
2217
+ catch {
2218
+ // Best-effort comment
2219
+ }
2220
+ // Do NOT set targetStatus — leave issue in current state
2221
+ }
2222
+ else {
2223
+ // Worktree is clean (no uncommitted/unpushed changes) — but check if branch
2224
+ // has commits ahead of main that should have resulted in a PR
2225
+ const hasPushedWork = checkForPushedWorkWithoutPR(agent.worktreePath);
2226
+ if (hasPushedWork.hasPushedWork) {
2227
+ // Agent pushed commits to remote but never created a PR — block promotion
2228
+ log?.error('Code-producing agent pushed commits but no PR was created — blocking promotion', {
2229
+ workType,
2230
+ details: hasPushedWork.details,
2231
+ });
2232
+ try {
2233
+ await this.client.createComment(issueId, `⚠️ **Agent completed and pushed code, but no PR was created.**\n\n` +
2234
+ `${hasPushedWork.details}\n\n` +
2235
+ `**Issue status was NOT promoted** because work cannot be reviewed without a PR.\n\n` +
2236
+ `The branch has been pushed to the remote. To recover:\n` +
2237
+ `\`\`\`bash\ngh pr create --head ${hasPushedWork.branch} --title "feat: <title>" --body "..."\n\`\`\`\n` +
2238
+ `Or re-trigger the agent to complete the PR creation step.`);
2239
+ }
2240
+ catch {
2241
+ // Best-effort comment
2242
+ }
2243
+ // Do NOT set targetStatus — leave issue in current state
2244
+ }
2245
+ else {
2246
+ // No PR and no pushed commits ahead of main — genuinely clean completion
2247
+ targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
2248
+ }
2249
+ }
2250
+ }
2251
+ else {
2252
+ targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
2253
+ }
2014
2254
  }
2015
2255
  if (targetStatus) {
2016
2256
  try {
@@ -2027,6 +2267,28 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2027
2267
  else if (!isResultSensitive) {
2028
2268
  log?.info('No auto-transition configured for work type', { workType });
2029
2269
  }
2270
+ // Merge queue: enqueue PR after successful merge work
2271
+ if (workType === 'merge' && this.mergeQueueAdapter && agent.pullRequestUrl) {
2272
+ try {
2273
+ const prMatch = agent.pullRequestUrl.match(/\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
2274
+ if (prMatch) {
2275
+ const [, owner, repo, prNum] = prMatch;
2276
+ const canEnqueue = await this.mergeQueueAdapter.canEnqueue(owner, repo, parseInt(prNum, 10));
2277
+ if (canEnqueue) {
2278
+ const status = await this.mergeQueueAdapter.enqueue(owner, repo, parseInt(prNum, 10));
2279
+ log?.info('PR enqueued in merge queue', { owner, repo, prNumber: prNum, state: status.state });
2280
+ }
2281
+ else {
2282
+ log?.info('PR not eligible for merge queue', { owner, repo, prNumber: prNum });
2283
+ }
2284
+ }
2285
+ }
2286
+ catch (error) {
2287
+ log?.warn('Failed to enqueue PR in merge queue', {
2288
+ error: error instanceof Error ? error.message : String(error),
2289
+ });
2290
+ }
2291
+ }
2030
2292
  // Unassign agent from issue for clean handoff visibility
2031
2293
  // This enables automated QA pickup via webhook
2032
2294
  // Skip unassignment for research work (user should decide when to move to backlog)
@@ -2226,6 +2488,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2226
2488
  }
2227
2489
  else if (event.subtype === 'compact_boundary') {
2228
2490
  log?.debug('Context compacted');
2491
+ // Trigger incremental summarization on compaction boundary
2492
+ this.contextManagers.get(issueId)?.handleCompaction();
2229
2493
  }
2230
2494
  else if (event.subtype === 'hook_response') {
2231
2495
  // Provider-specific hook handling — access raw event for details
@@ -2246,6 +2510,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2246
2510
  case 'tool_result':
2247
2511
  // Tool results — track activity and detect PR URLs
2248
2512
  this.updateLastActivity(issueId, 'tool_result');
2513
+ // Feed to context manager for artifact tracking
2514
+ this.contextManagers.get(issueId)?.processEvent(event);
2249
2515
  sessionLogger?.logToolResult(event.toolUseId ?? 'unknown', event.content, event.isError);
2250
2516
  // Detect GitHub PR URLs in tool output (from gh pr create)
2251
2517
  if (sessionId) {
@@ -2260,8 +2526,19 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2260
2526
  case 'assistant_text':
2261
2527
  // Assistant text output
2262
2528
  this.updateLastActivity(issueId, 'assistant');
2529
+ // Feed to context manager for session intent extraction
2530
+ this.contextManagers.get(issueId)?.processEvent(event);
2263
2531
  heartbeatWriter?.recordThinking();
2264
2532
  sessionLogger?.logAssistant(event.text);
2533
+ // Detect GitHub PR URLs in assistant text (backup for tool_result detection)
2534
+ if (sessionId && !agent.pullRequestUrl) {
2535
+ const prUrl = this.extractPullRequestUrl(event.text);
2536
+ if (prUrl) {
2537
+ log?.info('Pull request detected in assistant text', { prUrl });
2538
+ agent.pullRequestUrl = prUrl;
2539
+ await this.updateSessionPullRequest(sessionId, prUrl, agent);
2540
+ }
2541
+ }
2265
2542
  if (emitter) {
2266
2543
  await emitter.emitThought(event.text.substring(0, 200));
2267
2544
  }
@@ -2269,6 +2546,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2269
2546
  case 'tool_use':
2270
2547
  // Tool invocation
2271
2548
  this.updateLastActivity(issueId, 'assistant');
2549
+ // Feed to context manager for artifact tracking
2550
+ this.contextManagers.get(issueId)?.processEvent(event);
2272
2551
  log?.toolCall(event.toolName, event.input);
2273
2552
  heartbeatWriter?.recordToolCall(event.toolName);
2274
2553
  progressLogger?.logTool(event.toolName, event.input);
@@ -2314,6 +2593,15 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2314
2593
  // Store full result for completion comment posting later
2315
2594
  if (event.message) {
2316
2595
  agent.resultMessage = event.message;
2596
+ // Detect GitHub PR URLs in final result message (backup for tool_result detection)
2597
+ if (sessionId && !agent.pullRequestUrl) {
2598
+ const prUrl = this.extractPullRequestUrl(event.message);
2599
+ if (prUrl) {
2600
+ log?.info('Pull request detected in result message', { prUrl });
2601
+ agent.pullRequestUrl = prUrl;
2602
+ await this.updateSessionPullRequest(sessionId, prUrl, agent);
2603
+ }
2604
+ }
2317
2605
  }
2318
2606
  // Update state to completing/completed (only for worktree-based agents)
2319
2607
  if (agent.worktreePath) {
@@ -2363,6 +2651,22 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2363
2651
  }
2364
2652
  progressLogger?.logError('Agent error result', new Error(errorMessage));
2365
2653
  sessionLogger?.logError('Agent error result', new Error(errorMessage), { subtype: event.errorSubtype });
2654
+ // Merge queue: dequeue PR on merge agent failure
2655
+ if (agent.workType === 'merge' && this.mergeQueueAdapter && agent.pullRequestUrl) {
2656
+ try {
2657
+ const prMatch = agent.pullRequestUrl.match(/\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
2658
+ if (prMatch) {
2659
+ const [, owner, repo, prNum] = prMatch;
2660
+ await this.mergeQueueAdapter.dequeue(owner, repo, parseInt(prNum, 10));
2661
+ log?.info('PR dequeued from merge queue after failure', { owner, repo, prNumber: prNum });
2662
+ }
2663
+ }
2664
+ catch (dequeueError) {
2665
+ log?.warn('Failed to dequeue PR from merge queue', {
2666
+ error: dequeueError instanceof Error ? dequeueError.message : String(dequeueError),
2667
+ });
2668
+ }
2669
+ }
2366
2670
  // Report tool errors as Linear issues for tracking
2367
2671
  // Only report for 'error_during_execution' subtype (tool/execution errors)
2368
2672
  if (event.errorSubtype === 'error_during_execution' &&
@@ -2455,7 +2759,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2455
2759
  }
2456
2760
  }
2457
2761
  else {
2458
- // Direct Linear API - use AgentSession if available
2762
+ // Direct issue tracker API - use session if available
2459
2763
  const session = this.agentSessions.get(agent.issueId);
2460
2764
  if (session) {
2461
2765
  try {
@@ -2476,7 +2780,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2476
2780
  */
2477
2781
  async postCompletionComment(issueId, sessionId, resultMessage, log) {
2478
2782
  // Build completion comments with multi-part splitting
2479
- const comments = buildCompletionComments(resultMessage, [], // No plan items to include (already shown via activities)
2783
+ const comments = this.client.buildCompletionComments(resultMessage, [], // No plan items to include (already shown via activities)
2480
2784
  sessionId ?? null);
2481
2785
  log?.info('Posting completion comment', {
2482
2786
  parts: comments.length,
@@ -2550,6 +2854,17 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2550
2854
  progressLogger.stop();
2551
2855
  this.progressLoggers.delete(issueId);
2552
2856
  }
2857
+ // Persist and cleanup context manager
2858
+ const contextManager = this.contextManagers.get(issueId);
2859
+ if (contextManager) {
2860
+ try {
2861
+ contextManager.persist();
2862
+ }
2863
+ catch {
2864
+ // Ignore persistence errors during cleanup
2865
+ }
2866
+ this.contextManagers.delete(issueId);
2867
+ }
2553
2868
  // Session logger is cleaned up separately (in finalizeSessionLogger)
2554
2869
  // to ensure the final status is captured before cleanup
2555
2870
  this.sessionLoggers.delete(issueId);
@@ -2591,7 +2906,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2591
2906
  const { worktreePath, worktreeIdentifier } = this.createWorktree(issue.identifier, workType);
2592
2907
  // Link dependencies from main repo into worktree
2593
2908
  this.linkDependencies(worktreePath, issue.identifier);
2594
- const startStatus = WORK_TYPE_START_STATUS[workType];
2909
+ const startStatus = this.statusMappings.workTypeStartStatus[workType];
2595
2910
  // Update issue status based on work type if auto-transition is enabled
2596
2911
  if (this.config.autoTransition && startStatus) {
2597
2912
  await this.client.updateIssueStatus(issue.id, startStatus);
@@ -2638,41 +2953,53 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2638
2953
  const issue = await this.client.getIssue(issueIdOrIdentifier);
2639
2954
  const identifier = issue.identifier;
2640
2955
  const issueId = issue.id; // Use the actual UUID
2641
- const team = await issue.team;
2642
- const teamName = team?.key;
2643
- // Fetch labels for provider resolution
2644
- const issueLabels = await issue.labels();
2645
- const labelNames = issueLabels.nodes.map((l) => l.name);
2956
+ const teamName = issue.teamName;
2957
+ // Labels for provider resolution (pre-resolved by IssueTrackerClient)
2958
+ const labelNames = issue.labels;
2646
2959
  // Resolve project name for path scoping in monorepos
2647
2960
  let projectName;
2648
2961
  if (this.projectPaths) {
2649
- const project = await issue.project;
2650
- projectName = project?.name;
2962
+ projectName = issue.projectName;
2651
2963
  }
2652
2964
  console.log(`Processing single issue: ${identifier} (${issueId}) - ${issue.title}`);
2653
2965
  // Guard: skip work if the issue has moved to a terminal status since being queued
2654
- const currentState = await issue.state;
2655
- const currentStatus = currentState?.name;
2656
- if (currentStatus && TERMINAL_STATUSES.includes(currentStatus)) {
2966
+ const currentStatus = issue.status;
2967
+ if (currentStatus && this.statusMappings.terminalStatuses.includes(currentStatus)) {
2657
2968
  throw new Error(`Issue ${identifier} is in terminal status '${currentStatus}' — skipping ${workType ?? 'auto'} work. ` +
2658
2969
  `The issue was likely accepted/canceled after being queued.`);
2659
2970
  }
2660
2971
  // Defense in depth: re-validate git remote before spawning (guards against long-running instances)
2661
2972
  if (this.config.repository) {
2662
- validateGitRemote(this.config.repository);
2973
+ validateGitRemote(this.config.repository, this.gitRoot);
2663
2974
  }
2664
2975
  // Auto-detect work type from issue status if not provided
2665
2976
  // This must happen BEFORE creating worktree since path includes work type suffix
2666
2977
  let effectiveWorkType = workType;
2667
2978
  if (!effectiveWorkType) {
2668
- const state = await issue.state;
2669
- const statusName = state?.name ?? 'Backlog';
2979
+ const statusName = issue.status ?? 'Backlog';
2670
2980
  effectiveWorkType = await this.detectWorkType(issueId, statusName);
2671
2981
  }
2982
+ else {
2983
+ // Re-validate: upgrade to coordination variant if this is a parent issue
2984
+ // The caller may have a stale work type from before the session was queued
2985
+ try {
2986
+ const isParent = await this.client.isParentIssue(issueId);
2987
+ if (isParent) {
2988
+ const upgraded = detectWorkType(issue.status ?? 'Backlog', isParent, this.statusMappings.statusToWorkType);
2989
+ if (upgraded !== effectiveWorkType) {
2990
+ console.log(`Upgrading work type from ${effectiveWorkType} to ${upgraded} (parent issue detected)`);
2991
+ effectiveWorkType = upgraded;
2992
+ }
2993
+ }
2994
+ }
2995
+ catch (err) {
2996
+ console.warn(`Failed to check parent status for coordination upgrade:`, err);
2997
+ }
2998
+ }
2672
2999
  // Create isolated worktree for the agent
2673
3000
  let worktreePath;
2674
3001
  let worktreeIdentifier;
2675
- if (WORK_TYPES_REQUIRING_WORKTREE.has(effectiveWorkType)) {
3002
+ if (this.statusMappings.workTypesRequiringWorktree.has(effectiveWorkType)) {
2676
3003
  const wt = this.createWorktree(identifier, effectiveWorkType);
2677
3004
  worktreePath = wt.worktreePath;
2678
3005
  worktreeIdentifier = wt.worktreeIdentifier;
@@ -2704,7 +3031,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2704
3031
  const effectiveSessionId = sessionId ?? recoveryCheck.state.linearSessionId ?? randomUUID();
2705
3032
  console.log(`Resuming work on ${identifier} (recovery attempt ${updatedState?.recoveryAttempts ?? 1})`);
2706
3033
  // Update status based on work type if auto-transition is enabled
2707
- const startStatus = WORK_TYPE_START_STATUS[recoveryWorkType];
3034
+ const startStatus = this.statusMappings.workTypeStartStatus[recoveryWorkType];
2708
3035
  if (this.config.autoTransition && startStatus) {
2709
3036
  await this.client.updateIssueStatus(issueId, startStatus);
2710
3037
  console.log(`Updated ${identifier} status to ${startStatus}`);
@@ -2727,7 +3054,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2727
3054
  }
2728
3055
  // No recovery needed - proceed with fresh spawn
2729
3056
  // Update status based on work type if auto-transition is enabled
2730
- const startStatus = WORK_TYPE_START_STATUS[effectiveWorkType];
3057
+ const startStatus = this.statusMappings.workTypeStartStatus[effectiveWorkType];
2731
3058
  if (this.config.autoTransition && startStatus) {
2732
3059
  await this.client.updateIssueStatus(issueId, startStatus);
2733
3060
  console.log(`Updated ${identifier} status to ${startStatus}`);
@@ -2749,6 +3076,13 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2749
3076
  labels: labelNames,
2750
3077
  });
2751
3078
  }
3079
+ /**
3080
+ * Get the merge queue adapter, if configured.
3081
+ * Returns undefined if no merge queue is enabled.
3082
+ */
3083
+ getMergeQueueAdapter() {
3084
+ return this.mergeQueueAdapter;
3085
+ }
2752
3086
  /**
2753
3087
  * Get all active agents
2754
3088
  */
@@ -2900,12 +3234,10 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2900
3234
  try {
2901
3235
  const issue = await this.client.getIssue(issueId);
2902
3236
  identifier = issue.identifier;
2903
- const issueTeam = await issue.team;
2904
- teamName = issueTeam?.key;
3237
+ teamName = issue.teamName;
2905
3238
  // Guard: skip work if the issue has moved to a terminal status since being queued
2906
- const currentState = await issue.state;
2907
- const currentStatus = currentState?.name;
2908
- if (currentStatus && TERMINAL_STATUSES.includes(currentStatus)) {
3239
+ const currentStatus = issue.status;
3240
+ if (currentStatus && this.statusMappings.terminalStatuses.includes(currentStatus)) {
2909
3241
  console.log(`Issue ${identifier} is in terminal status '${currentStatus}' — skipping work`);
2910
3242
  return {
2911
3243
  forwarded: false,
@@ -2921,7 +3253,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2921
3253
  workType = await this.detectWorkType(issue.id, statusName);
2922
3254
  }
2923
3255
  // Create isolated worktree for the agent
2924
- if (WORK_TYPES_REQUIRING_WORKTREE.has(workType)) {
3256
+ if (this.statusMappings.workTypesRequiringWorktree.has(workType)) {
2925
3257
  const result = this.createWorktree(identifier, workType);
2926
3258
  worktreePath = result.worktreePath;
2927
3259
  worktreeIdentifier = result.worktreeIdentifier;
@@ -2940,7 +3272,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2940
3272
  }
2941
3273
  // Check if worktree exists (only relevant for code work types)
2942
3274
  const effectiveWorkType = workType ?? 'development';
2943
- if (WORK_TYPES_REQUIRING_WORKTREE.has(effectiveWorkType) && worktreePath && !existsSync(worktreePath)) {
3275
+ if (this.statusMappings.workTypesRequiringWorktree.has(effectiveWorkType) && worktreePath && !existsSync(worktreePath)) {
2944
3276
  try {
2945
3277
  const result = this.createWorktree(identifier, effectiveWorkType);
2946
3278
  worktreePath = result.worktreePath;
@@ -3056,7 +3388,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3056
3388
  // Use the work type to determine if we need to transition on start
3057
3389
  // Only certain work types trigger a start transition
3058
3390
  const effectiveWorkType = workType ?? 'development';
3059
- const startStatus = WORK_TYPE_START_STATUS[effectiveWorkType];
3391
+ const startStatus = this.statusMappings.workTypeStartStatus[effectiveWorkType];
3060
3392
  if (this.config.autoTransition && startStatus) {
3061
3393
  try {
3062
3394
  await this.client.updateIssueStatus(issueId, startStatus);
@@ -3135,6 +3467,9 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3135
3467
  this.sessionLoggers.set(issueId, sessionLogger);
3136
3468
  log.debug('Session logging initialized', { logsDir: logConfig.logsDir });
3137
3469
  }
3470
+ // Initialize context manager for context window management
3471
+ const contextManager = ContextManager.load(worktreePath);
3472
+ this.contextManagers.set(issueId, contextManager);
3138
3473
  log.debug('State persistence initialized', { agentDir: resolve(worktreePath, '.agent') });
3139
3474
  }
3140
3475
  catch (stateError) {
@@ -3168,9 +3503,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3168
3503
  });
3169
3504
  }
3170
3505
  else {
3171
- // Direct Linear API
3172
- const session = createAgentSession({
3173
- client: this.client.linearClient,
3506
+ // Direct issue tracker API
3507
+ const session = this.client.createSession({
3174
3508
  issueId,
3175
3509
  sessionId,
3176
3510
  autoTransition: false,