@renseiai/agentfactory 0.8.5 → 0.8.7

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 (90) hide show
  1. package/README.md +2 -2
  2. package/dist/src/governor/decision-engine.d.ts +7 -0
  3. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  4. package/dist/src/governor/decision-engine.js +59 -1
  5. package/dist/src/governor/governor.d.ts +5 -1
  6. package/dist/src/governor/governor.d.ts.map +1 -1
  7. package/dist/src/governor/governor.js +6 -1
  8. package/dist/src/index.d.ts +1 -0
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +1 -0
  11. package/dist/src/orchestrator/activity-emitter.d.ts +3 -3
  12. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -1
  13. package/dist/src/orchestrator/activity-emitter.js +1 -1
  14. package/dist/src/orchestrator/detect-work-type.test.js +25 -16
  15. package/dist/src/orchestrator/index.d.ts +4 -0
  16. package/dist/src/orchestrator/index.d.ts.map +1 -1
  17. package/dist/src/orchestrator/index.js +1 -0
  18. package/dist/src/orchestrator/issue-tracker-client.d.ts +103 -0
  19. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -0
  20. package/dist/src/orchestrator/issue-tracker-client.js +8 -0
  21. package/dist/src/orchestrator/log-analyzer.d.ts +19 -4
  22. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -1
  23. package/dist/src/orchestrator/log-analyzer.js +26 -50
  24. package/dist/src/orchestrator/orchestrator-utils.test.js +3 -0
  25. package/dist/src/orchestrator/orchestrator.d.ts +4 -2
  26. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  27. package/dist/src/orchestrator/orchestrator.js +193 -115
  28. package/dist/src/orchestrator/parse-work-result.d.ts +1 -1
  29. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  30. package/dist/src/orchestrator/parse-work-result.js +3 -1
  31. package/dist/src/orchestrator/parse-work-result.test.js +9 -0
  32. package/dist/src/orchestrator/session-logger.d.ts +1 -1
  33. package/dist/src/orchestrator/session-logger.d.ts.map +1 -1
  34. package/dist/src/orchestrator/state-recovery.d.ts +1 -1
  35. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
  36. package/dist/src/orchestrator/state-recovery.js +1 -0
  37. package/dist/src/orchestrator/state-types.d.ts +1 -1
  38. package/dist/src/orchestrator/state-types.d.ts.map +1 -1
  39. package/dist/src/orchestrator/types.d.ts +22 -2
  40. package/dist/src/orchestrator/types.d.ts.map +1 -1
  41. package/dist/src/orchestrator/work-types.d.ts +50 -0
  42. package/dist/src/orchestrator/work-types.d.ts.map +1 -0
  43. package/dist/src/orchestrator/work-types.js +20 -0
  44. package/dist/src/templates/registry.d.ts +1 -1
  45. package/dist/src/templates/registry.test.js +2 -2
  46. package/dist/src/templates/renderer.d.ts +1 -1
  47. package/dist/src/templates/types.d.ts +4 -2
  48. package/dist/src/templates/types.d.ts.map +1 -1
  49. package/dist/src/templates/types.js +1 -0
  50. package/dist/src/templates/types.test.js +4 -3
  51. package/dist/src/tools/index.d.ts +0 -3
  52. package/dist/src/tools/index.d.ts.map +1 -1
  53. package/dist/src/tools/index.js +0 -2
  54. package/dist/src/workflow/index.d.ts +14 -0
  55. package/dist/src/workflow/index.d.ts.map +1 -0
  56. package/dist/src/workflow/index.js +10 -0
  57. package/dist/src/workflow/transition-engine.d.ts +44 -0
  58. package/dist/src/workflow/transition-engine.d.ts.map +1 -0
  59. package/dist/src/workflow/transition-engine.js +106 -0
  60. package/dist/src/workflow/transition-engine.test.d.ts +2 -0
  61. package/dist/src/workflow/transition-engine.test.d.ts.map +1 -0
  62. package/dist/src/workflow/transition-engine.test.js +313 -0
  63. package/dist/src/workflow/workflow-loader.d.ts +21 -0
  64. package/dist/src/workflow/workflow-loader.d.ts.map +1 -0
  65. package/dist/src/workflow/workflow-loader.js +40 -0
  66. package/dist/src/workflow/workflow-loader.test.d.ts +2 -0
  67. package/dist/src/workflow/workflow-loader.test.d.ts.map +1 -0
  68. package/dist/src/workflow/workflow-loader.test.js +134 -0
  69. package/dist/src/workflow/workflow-registry.d.ts +56 -0
  70. package/dist/src/workflow/workflow-registry.d.ts.map +1 -0
  71. package/dist/src/workflow/workflow-registry.js +107 -0
  72. package/dist/src/workflow/workflow-registry.test.d.ts +2 -0
  73. package/dist/src/workflow/workflow-registry.test.d.ts.map +1 -0
  74. package/dist/src/workflow/workflow-registry.test.js +201 -0
  75. package/dist/src/workflow/workflow-types.d.ts +269 -0
  76. package/dist/src/workflow/workflow-types.d.ts.map +1 -0
  77. package/dist/src/workflow/workflow-types.js +88 -0
  78. package/dist/src/workflow/workflow-types.test.d.ts +2 -0
  79. package/dist/src/workflow/workflow-types.test.d.ts.map +1 -0
  80. package/dist/src/workflow/workflow-types.test.js +440 -0
  81. package/package.json +3 -4
  82. package/dist/src/linear-cli.d.ts +0 -38
  83. package/dist/src/linear-cli.d.ts.map +0 -1
  84. package/dist/src/linear-cli.js +0 -674
  85. package/dist/src/tools/linear-runner.d.ts +0 -34
  86. package/dist/src/tools/linear-runner.d.ts.map +0 -1
  87. package/dist/src/tools/linear-runner.js +0 -700
  88. package/dist/src/tools/plugins/linear.d.ts +0 -9
  89. package/dist/src/tools/plugins/linear.d.ts.map +0 -1
  90. package/dist/src/tools/plugins/linear.js +0 -138
@@ -14,14 +14,13 @@ import { createHeartbeatWriter, getHeartbeatIntervalFromEnv } from './heartbeat-
14
14
  import { createProgressLogger } from './progress-logger.js';
15
15
  import { createSessionLogger } from './session-logger.js';
16
16
  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
17
  import { parseWorkResult } from './parse-work-result.js';
19
18
  import { createActivityEmitter } from './activity-emitter.js';
20
19
  import { createApiActivityEmitter } from './api-activity-emitter.js';
21
20
  import { createLogger } from '../logger.js';
22
21
  import { TemplateRegistry, createToolPermissionAdapter } from '../templates/index.js';
23
22
  import { loadRepositoryConfig, getProjectConfig, getProvidersConfig } from '../config/index.js';
24
- import { ToolRegistry, linearPlugin } from '../tools/index.js';
23
+ import { ToolRegistry } from '../tools/index.js';
25
24
  // Default inactivity timeout: 5 minutes
26
25
  const DEFAULT_INACTIVITY_TIMEOUT_MS = 300000;
27
26
  // Default max session timeout: unlimited (undefined)
@@ -442,6 +441,46 @@ Dependencies are symlinked from the main repo by the orchestrator. Do NOT run pn
442
441
  If you encounter a specific "Cannot find module" error, run it SYNCHRONOUSLY
443
442
  (never with run_in_background). Never use sleep or polling loops to wait for commands.
444
443
 
444
+ IMPORTANT: If you encounter "exceeds maximum allowed tokens" error when reading files:
445
+ - Use Grep to search for specific code patterns instead of reading entire files
446
+ - Use Read with offset/limit parameters to paginate through large files
447
+ - Avoid reading auto-generated files like payload-types.ts (use Grep instead)
448
+ See the "Working with Large Files" section in the project documentation (CLAUDE.md / AGENTS.md) for details.${LINEAR_CLI_INSTRUCTION}`;
449
+ break;
450
+ case 'inflight-coordination':
451
+ basePrompt = `Resume coordination of sub-issue execution for parent issue ${identifier}.
452
+ Check sub-issue statuses, continue work on incomplete sub-issues, and create a PR when all are done.
453
+
454
+ SUB-ISSUE STATUS MANAGEMENT:
455
+ You MUST update sub-issue statuses in Linear as work progresses:
456
+ - When starting work on a sub-issue: pnpm af-linear update-sub-issue <id> --state Started
457
+ - When a sub-agent completes a sub-issue: pnpm af-linear update-sub-issue <id> --state Finished --comment "Completed by coordinator agent"
458
+ - If a sub-agent fails on a sub-issue: pnpm af-linear create-comment <sub-issue-id> --body "Sub-agent failed: <reason>"
459
+
460
+ COMPLETION VERIFICATION:
461
+ Before marking the parent issue as complete, verify ALL sub-issues are in Finished status:
462
+ pnpm af-linear list-sub-issue-statuses ${identifier}
463
+ If any sub-issue is not Finished, report the failure and do not mark the parent as complete.
464
+
465
+ SUB-AGENT SAFETY RULES (CRITICAL):
466
+ This is a SHARED WORKTREE. Multiple sub-agents run concurrently in this directory.
467
+ Every sub-agent prompt you construct MUST include these rules:
468
+
469
+ 1. NEVER run: git worktree remove, git worktree prune
470
+ 2. NEVER run: git checkout, git switch (to a different branch)
471
+ 3. NEVER run: git reset --hard, git clean -fd, git restore .
472
+ 4. NEVER delete or modify the .git file in the worktree root
473
+ 5. Only the orchestrator manages worktree lifecycle
474
+ 6. Work only on files relevant to your sub-issue to minimize conflicts
475
+ 7. Commit changes with descriptive messages before reporting completion
476
+
477
+ Prefix every sub-agent prompt with: "SHARED WORKTREE — DO NOT MODIFY GIT STATE"
478
+
479
+ DEPENDENCY INSTALLATION:
480
+ Dependencies are symlinked from the main repo by the orchestrator. Do NOT run pnpm install.
481
+ If you encounter a specific "Cannot find module" error, run it SYNCHRONOUSLY
482
+ (never with run_in_background). Never use sleep or polling loops to wait for commands.
483
+
445
484
  IMPORTANT: If you encounter "exceeds maximum allowed tokens" error when reading files:
446
485
  - Use Grep to search for specific code patterns instead of reading entire files
447
486
  - Use Read with offset/limit parameters to paginate through large files
@@ -651,6 +690,7 @@ const WORK_TYPE_SUFFIX = {
651
690
  'backlog-creation': 'BC',
652
691
  development: 'DEV',
653
692
  inflight: 'INF',
693
+ 'inflight-coordination': 'INF-COORD',
654
694
  coordination: 'COORD',
655
695
  qa: 'QA',
656
696
  acceptance: 'AC',
@@ -678,8 +718,9 @@ export function getWorktreeIdentifier(issueIdentifier, workType) {
678
718
  * being dispatched as 'development' (which uses the wrong template and
679
719
  * produces no sub-agent orchestration).
680
720
  */
681
- export function detectWorkType(statusName, isParent) {
682
- let workType = STATUS_WORK_TYPE_MAP[statusName] ?? 'development';
721
+ export function detectWorkType(statusName, isParent, statusToWorkType) {
722
+ const mapping = statusToWorkType ?? {};
723
+ let workType = mapping[statusName] ?? 'development';
683
724
  console.log(`Auto-detected work type: ${workType} (from status: ${statusName})`);
684
725
  if (isParent) {
685
726
  if (workType === 'development')
@@ -688,6 +729,8 @@ export function detectWorkType(statusName, isParent) {
688
729
  workType = 'qa-coordination';
689
730
  else if (workType === 'acceptance')
690
731
  workType = 'acceptance-coordination';
732
+ else if (workType === 'inflight')
733
+ workType = 'inflight-coordination';
691
734
  else if (workType === 'refinement')
692
735
  workType = 'refinement-coordination';
693
736
  console.log(`Upgraded to coordination work type: ${workType} (parent issue)`);
@@ -697,6 +740,7 @@ export function detectWorkType(statusName, isParent) {
697
740
  export class AgentOrchestrator {
698
741
  config;
699
742
  client;
743
+ statusMappings;
700
744
  events;
701
745
  activeAgents = new Map();
702
746
  agentHandles = new Map();
@@ -737,10 +781,15 @@ export class AgentOrchestrator {
737
781
  validateCommand;
738
782
  // Tool plugin registry for in-process agent tools
739
783
  toolRegistry;
784
+ // Git repository root for running git commands (resolved from worktreePath or cwd)
785
+ gitRoot;
740
786
  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');
787
+ // Validate that an issue tracker client is available
788
+ if (!config.issueTrackerClient) {
789
+ const apiKey = config.linearApiKey ?? process.env.LINEAR_API_KEY;
790
+ if (!apiKey) {
791
+ throw new Error('Either issueTrackerClient or LINEAR_API_KEY is required');
792
+ }
744
793
  }
745
794
  // Parse timeout config from environment variables (can be overridden by config)
746
795
  const envInactivityTimeout = process.env.AGENT_INACTIVITY_TIMEOUT_MS
@@ -752,7 +801,7 @@ export class AgentOrchestrator {
752
801
  this.config = {
753
802
  ...DEFAULT_CONFIG,
754
803
  ...config,
755
- linearApiKey: apiKey,
804
+ linearApiKey: config.linearApiKey ?? process.env.LINEAR_API_KEY ?? '',
756
805
  streamConfig: {
757
806
  ...DEFAULT_CONFIG.streamConfig,
758
807
  ...config.streamConfig,
@@ -763,11 +812,15 @@ export class AgentOrchestrator {
763
812
  inactivityTimeoutMs: config.inactivityTimeoutMs ?? envInactivityTimeout ?? DEFAULT_CONFIG.inactivityTimeoutMs,
764
813
  maxSessionTimeoutMs: config.maxSessionTimeoutMs ?? envMaxSessionTimeout ?? DEFAULT_CONFIG.maxSessionTimeoutMs,
765
814
  };
815
+ // Resolve git root from worktreePath (which may point to a different repo than cwd)
816
+ this.gitRoot = findRepoRoot(resolve(this.config.worktreePath)) ?? findRepoRoot(process.cwd()) ?? process.cwd();
766
817
  // Validate git remote matches configured repository (if set)
767
818
  if (this.config.repository) {
768
- validateGitRemote(this.config.repository);
819
+ validateGitRemote(this.config.repository, this.gitRoot);
769
820
  }
770
- this.client = createLinearAgentClient({ apiKey });
821
+ // Use injected client or fail (caller must provide one)
822
+ this.client = config.issueTrackerClient;
823
+ this.statusMappings = config.statusMappings;
771
824
  this.events = events;
772
825
  // Initialize default agent provider — per-spawn resolution may override
773
826
  const providerName = resolveProviderName({ project: config.project });
@@ -779,8 +832,8 @@ export class AgentOrchestrator {
779
832
  if (config.templateDir) {
780
833
  templateDirs.push(config.templateDir);
781
834
  }
782
- // Auto-detect .agentfactory/templates/ in working directory
783
- const projectTemplateDir = resolve(process.cwd(), '.agentfactory', 'templates');
835
+ // Auto-detect .agentfactory/templates/ in target repo
836
+ const projectTemplateDir = resolve(this.gitRoot, '.agentfactory', 'templates');
784
837
  if (existsSync(projectTemplateDir) && !templateDirs.includes(projectTemplateDir)) {
785
838
  templateDirs.push(projectTemplateDir);
786
839
  }
@@ -797,7 +850,7 @@ export class AgentOrchestrator {
797
850
  }
798
851
  // Auto-load .agentfactory/config.yaml from repository root
799
852
  try {
800
- const repoRoot = findRepoRoot(process.cwd());
853
+ const repoRoot = this.gitRoot;
801
854
  if (repoRoot) {
802
855
  const repoConfig = loadRepositoryConfig(repoRoot);
803
856
  if (repoConfig) {
@@ -805,7 +858,7 @@ export class AgentOrchestrator {
805
858
  // Use repository from config as fallback if not set in OrchestratorConfig
806
859
  if (!this.config.repository && repoConfig.repository) {
807
860
  this.config.repository = repoConfig.repository;
808
- validateGitRemote(this.config.repository);
861
+ validateGitRemote(this.config.repository, this.gitRoot);
809
862
  }
810
863
  // Store allowedProjects for backlog filtering
811
864
  if (repoConfig.projectPaths) {
@@ -845,9 +898,13 @@ export class AgentOrchestrator {
845
898
  catch (err) {
846
899
  console.warn('[orchestrator] Failed to load .agentfactory/config.yaml:', err instanceof Error ? err.message : err);
847
900
  }
848
- // Initialize tool plugin registry with Linear plugin
901
+ // Initialize tool plugin registry with injected plugins
849
902
  this.toolRegistry = new ToolRegistry();
850
- this.toolRegistry.register(linearPlugin);
903
+ if (config.toolPlugins) {
904
+ for (const plugin of config.toolPlugins) {
905
+ this.toolRegistry.register(plugin);
906
+ }
907
+ }
851
908
  }
852
909
  /**
853
910
  * Update the last activity timestamp for an agent (for inactivity timeout tracking)
@@ -891,86 +948,63 @@ export class AgentOrchestrator {
891
948
  */
892
949
  async detectWorkType(issueId, statusName) {
893
950
  const isParent = await this.client.isParentIssue(issueId);
894
- return detectWorkType(statusName, isParent);
951
+ return detectWorkType(statusName, isParent, this.statusMappings.statusToWorkType);
895
952
  }
896
953
  /**
897
954
  * Get backlog issues for the configured project
898
955
  */
899
956
  async getBacklogIssues(limit) {
900
957
  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));
958
+ // Cross-reference project repo metadata with config
959
+ if (this.config.project && this.config.repository) {
960
+ try {
961
+ const projectRepoUrl = await this.client.getProjectRepositoryUrl(this.config.project);
962
+ if (projectRepoUrl) {
963
+ const normalizedProjectRepo = projectRepoUrl
964
+ .replace(/^https?:\/\//, '')
965
+ .replace(/\.git$/, '');
966
+ const normalizedConfigRepo = this.config.repository
967
+ .replace(/^https?:\/\//, '')
968
+ .replace(/\.git$/, '');
969
+ if (!normalizedProjectRepo.includes(normalizedConfigRepo) && !normalizedConfigRepo.includes(normalizedProjectRepo)) {
970
+ console.warn(`Warning: Project '${this.config.project}' repository metadata '${projectRepoUrl}' ` +
971
+ `does not match configured repository '${this.config.repository}'. Skipping issues.`);
972
+ return [];
934
973
  }
935
974
  }
936
975
  }
976
+ catch (error) {
977
+ // Non-fatal: log warning but continue if metadata check fails
978
+ console.warn('Warning: Could not check project repository metadata:', error instanceof Error ? error.message : String(error));
979
+ }
937
980
  }
938
- const issues = await this.client.linearClient.issues({
939
- filter,
940
- first: maxIssues * 2, // Fetch extra to account for filtering
981
+ // Query issues using the abstract client
982
+ const allIssues = await this.client.queryIssues({
983
+ project: this.config.project,
984
+ status: 'Backlog',
985
+ maxResults: maxIssues * 2, // Fetch extra to account for filtering
941
986
  });
942
987
  const results = [];
943
- for (const issue of issues.nodes) {
988
+ for (const issue of allIssues) {
944
989
  if (results.length >= maxIssues)
945
990
  break;
946
991
  // Filter by allowedProjects from .agentfactory/config.yaml
947
- let resolvedProjectName;
948
992
  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(', ')}]`);
993
+ if (!issue.projectName || !this.allowedProjects.includes(issue.projectName)) {
994
+ console.warn(`[orchestrator] Skipping issue ${issue.identifier} — project "${issue.projectName ?? '(none)'}" is not in allowedProjects: [${this.allowedProjects.join(', ')}]`);
953
995
  continue;
954
996
  }
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
997
  }
962
- const labels = await issue.labels();
963
- const team = await issue.team;
964
998
  results.push({
965
999
  id: issue.id,
966
1000
  identifier: issue.identifier,
967
1001
  title: issue.title,
968
- description: issue.description ?? undefined,
1002
+ description: issue.description,
969
1003
  url: issue.url,
970
1004
  priority: issue.priority,
971
- labels: labels.nodes.map((l) => l.name),
972
- teamName: team?.key,
973
- projectName: resolvedProjectName,
1005
+ labels: issue.labels,
1006
+ teamName: issue.teamName,
1007
+ projectName: issue.projectName,
974
1008
  });
975
1009
  }
976
1010
  // Sort by priority (lower number = higher priority, 0 means no priority -> goes last)
@@ -1075,6 +1109,7 @@ export class AgentOrchestrator {
1075
1109
  const output = execSync('git worktree list --porcelain', {
1076
1110
  stdio: 'pipe',
1077
1111
  encoding: 'utf-8',
1112
+ cwd: this.gitRoot,
1078
1113
  });
1079
1114
  const mainTreeMatch = output.match(/^worktree (.+)$/m);
1080
1115
  if (mainTreeMatch) {
@@ -1134,7 +1169,7 @@ export class AgentOrchestrator {
1134
1169
  if (!existsSync(conflictPath)) {
1135
1170
  // Directory doesn't exist - just prune git's worktree list
1136
1171
  try {
1137
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1172
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1138
1173
  console.log(`Pruned stale worktree reference for branch ${branchName}`);
1139
1174
  return true;
1140
1175
  }
@@ -1203,6 +1238,7 @@ export class AgentOrchestrator {
1203
1238
  execSync(`git worktree remove "${conflictPath}" --force`, {
1204
1239
  stdio: 'pipe',
1205
1240
  encoding: 'utf-8',
1241
+ cwd: this.gitRoot,
1206
1242
  });
1207
1243
  console.log(`Removed stale worktree: ${conflictPath}`);
1208
1244
  return true;
@@ -1219,7 +1255,7 @@ export class AgentOrchestrator {
1219
1255
  // this path is inside .worktrees/ and is not the main tree)
1220
1256
  try {
1221
1257
  execSync(`rm -rf "${conflictPath}"`, { stdio: 'pipe', encoding: 'utf-8' });
1222
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1258
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1223
1259
  console.log(`Force-removed stale worktree: ${conflictPath}`);
1224
1260
  return true;
1225
1261
  }
@@ -1264,7 +1300,7 @@ export class AgentOrchestrator {
1264
1300
  }
1265
1301
  // Prune any stale worktrees first (handles deleted directories)
1266
1302
  try {
1267
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1303
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1268
1304
  }
1269
1305
  catch {
1270
1306
  // Ignore prune errors
@@ -1306,6 +1342,7 @@ export class AgentOrchestrator {
1306
1342
  execSync(`git worktree add "${worktreePath}" -b ${branchName} ${baseBranch}`, {
1307
1343
  stdio: 'pipe',
1308
1344
  encoding: 'utf-8',
1345
+ cwd: this.gitRoot,
1309
1346
  });
1310
1347
  }
1311
1348
  catch (error) {
@@ -1332,6 +1369,7 @@ export class AgentOrchestrator {
1332
1369
  execSync(`git worktree add "${worktreePath}" ${branchName}`, {
1333
1370
  stdio: 'pipe',
1334
1371
  encoding: 'utf-8',
1372
+ cwd: this.gitRoot,
1335
1373
  });
1336
1374
  }
1337
1375
  catch (innerError) {
@@ -1366,7 +1404,7 @@ export class AgentOrchestrator {
1366
1404
  if (existsSync(worktreePath)) {
1367
1405
  execSync(`rm -rf "${worktreePath}"`, { stdio: 'pipe', encoding: 'utf-8' });
1368
1406
  }
1369
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1407
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1370
1408
  }
1371
1409
  catch {
1372
1410
  // Ignore cleanup errors
@@ -1399,13 +1437,14 @@ export class AgentOrchestrator {
1399
1437
  execSync(`git worktree remove "${worktreePath}" --force`, {
1400
1438
  stdio: 'pipe',
1401
1439
  encoding: 'utf-8',
1440
+ cwd: this.gitRoot,
1402
1441
  });
1403
1442
  }
1404
1443
  catch (error) {
1405
1444
  console.warn(`Failed to remove worktree via git, trying fallback:`, error);
1406
1445
  try {
1407
1446
  execSync(`rm -rf "${worktreePath}"`, { stdio: 'pipe', encoding: 'utf-8' });
1408
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1447
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1409
1448
  }
1410
1449
  catch (fallbackError) {
1411
1450
  console.warn(`Fallback worktree removal also failed:`, fallbackError);
@@ -1415,7 +1454,7 @@ export class AgentOrchestrator {
1415
1454
  else {
1416
1455
  // Directory gone but git may still track it
1417
1456
  try {
1418
- execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8' });
1457
+ execSync('git worktree prune', { stdio: 'pipe', encoding: 'utf-8', cwd: this.gitRoot });
1419
1458
  }
1420
1459
  catch {
1421
1460
  // Ignore
@@ -1792,8 +1831,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1792
1831
  else {
1793
1832
  // Direct Linear API - only works with OAuth tokens (not API keys)
1794
1833
  // This will fail for createAgentActivity calls but works for comments
1795
- const session = createAgentSession({
1796
- client: this.client.linearClient,
1834
+ const session = this.client.createSession({
1797
1835
  issueId,
1798
1836
  sessionId,
1799
1837
  autoTransition: false, // Orchestrator handles transitions
@@ -1847,7 +1885,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1847
1885
  // Set work type so agent knows what kind of work it's doing
1848
1886
  env.LINEAR_WORK_TYPE = workType;
1849
1887
  // 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') {
1888
+ if (workType === 'coordination' || workType === 'inflight-coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination' || workType === 'refinement-coordination') {
1851
1889
  env.SHARED_WORKTREE = 'true';
1852
1890
  }
1853
1891
  // Set Claude Code Task List ID for intra-issue task coordination
@@ -1866,7 +1904,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1866
1904
  // Coordinators need significantly more turns than standard agents
1867
1905
  // since they spawn sub-agents and poll their status repeatedly.
1868
1906
  // 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';
1907
+ const needsMoreTurns = workType === 'coordination' || workType === 'inflight-coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination' || workType === 'refinement-coordination' || workType === 'inflight';
1870
1908
  const maxTurns = needsMoreTurns ? 200 : undefined;
1871
1909
  // Spawn agent via provider interface
1872
1910
  const spawnConfig = {
@@ -1977,11 +2015,11 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1977
2015
  }
1978
2016
  agent.workResult = workResult;
1979
2017
  if (workResult === 'passed') {
1980
- targetStatus = WORK_TYPE_COMPLETE_STATUS[workType];
2018
+ targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
1981
2019
  log?.info('Work result: passed, promoting', { workType, targetStatus });
1982
2020
  }
1983
2021
  else if (workResult === 'failed') {
1984
- targetStatus = WORK_TYPE_FAIL_STATUS[workType];
2022
+ targetStatus = this.statusMappings.workTypeFailStatus[workType];
1985
2023
  log?.info('Work result: failed, transitioning to fail status', { workType, targetStatus });
1986
2024
  }
1987
2025
  else {
@@ -2009,8 +2047,39 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2009
2047
  }
2010
2048
  }
2011
2049
  else {
2012
- // Non-QA/acceptance: unchanged behavior always promote on completion
2013
- targetStatus = WORK_TYPE_COMPLETE_STATUS[workType];
2050
+ // Non-QA/acceptance: promote on completion, but validate code-producing work types first
2051
+ const isCodeProducing = workType === 'development' || workType === 'inflight';
2052
+ if (isCodeProducing && agent.worktreePath && !agent.pullRequestUrl) {
2053
+ // Code-producing agent completed without a detected PR — check for commits
2054
+ const incompleteCheck = checkForIncompleteWork(agent.worktreePath);
2055
+ if (incompleteCheck.hasIncompleteWork) {
2056
+ // Agent has uncommitted/unpushed changes — block promotion
2057
+ log?.error('Code-producing agent completed without PR and has incomplete work — blocking promotion', {
2058
+ workType,
2059
+ reason: incompleteCheck.reason,
2060
+ details: incompleteCheck.details,
2061
+ });
2062
+ // Post a diagnostic comment
2063
+ try {
2064
+ await this.client.createComment(issueId, `⚠️ **Agent completed but work was not persisted.**\n\n` +
2065
+ `The agent reported success but no PR was detected, and the worktree has ${incompleteCheck.details}.\n\n` +
2066
+ `**Issue status was NOT promoted** to prevent lost work from advancing through the pipeline.\n\n` +
2067
+ `The worktree has been preserved at \`${agent.worktreePath}\`. ` +
2068
+ `To recover: cd into the worktree, commit, push, and create a PR manually.`);
2069
+ }
2070
+ catch {
2071
+ // Best-effort comment
2072
+ }
2073
+ // Do NOT set targetStatus — leave issue in current state
2074
+ }
2075
+ else {
2076
+ // No PR but worktree is clean — either no changes needed or agent cleaned up
2077
+ targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
2078
+ }
2079
+ }
2080
+ else {
2081
+ targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
2082
+ }
2014
2083
  }
2015
2084
  if (targetStatus) {
2016
2085
  try {
@@ -2455,7 +2524,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2455
2524
  }
2456
2525
  }
2457
2526
  else {
2458
- // Direct Linear API - use AgentSession if available
2527
+ // Direct issue tracker API - use session if available
2459
2528
  const session = this.agentSessions.get(agent.issueId);
2460
2529
  if (session) {
2461
2530
  try {
@@ -2476,7 +2545,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2476
2545
  */
2477
2546
  async postCompletionComment(issueId, sessionId, resultMessage, log) {
2478
2547
  // Build completion comments with multi-part splitting
2479
- const comments = buildCompletionComments(resultMessage, [], // No plan items to include (already shown via activities)
2548
+ const comments = this.client.buildCompletionComments(resultMessage, [], // No plan items to include (already shown via activities)
2480
2549
  sessionId ?? null);
2481
2550
  log?.info('Posting completion comment', {
2482
2551
  parts: comments.length,
@@ -2591,7 +2660,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2591
2660
  const { worktreePath, worktreeIdentifier } = this.createWorktree(issue.identifier, workType);
2592
2661
  // Link dependencies from main repo into worktree
2593
2662
  this.linkDependencies(worktreePath, issue.identifier);
2594
- const startStatus = WORK_TYPE_START_STATUS[workType];
2663
+ const startStatus = this.statusMappings.workTypeStartStatus[workType];
2595
2664
  // Update issue status based on work type if auto-transition is enabled
2596
2665
  if (this.config.autoTransition && startStatus) {
2597
2666
  await this.client.updateIssueStatus(issue.id, startStatus);
@@ -2638,41 +2707,53 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2638
2707
  const issue = await this.client.getIssue(issueIdOrIdentifier);
2639
2708
  const identifier = issue.identifier;
2640
2709
  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);
2710
+ const teamName = issue.teamName;
2711
+ // Labels for provider resolution (pre-resolved by IssueTrackerClient)
2712
+ const labelNames = issue.labels;
2646
2713
  // Resolve project name for path scoping in monorepos
2647
2714
  let projectName;
2648
2715
  if (this.projectPaths) {
2649
- const project = await issue.project;
2650
- projectName = project?.name;
2716
+ projectName = issue.projectName;
2651
2717
  }
2652
2718
  console.log(`Processing single issue: ${identifier} (${issueId}) - ${issue.title}`);
2653
2719
  // 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)) {
2720
+ const currentStatus = issue.status;
2721
+ if (currentStatus && this.statusMappings.terminalStatuses.includes(currentStatus)) {
2657
2722
  throw new Error(`Issue ${identifier} is in terminal status '${currentStatus}' — skipping ${workType ?? 'auto'} work. ` +
2658
2723
  `The issue was likely accepted/canceled after being queued.`);
2659
2724
  }
2660
2725
  // Defense in depth: re-validate git remote before spawning (guards against long-running instances)
2661
2726
  if (this.config.repository) {
2662
- validateGitRemote(this.config.repository);
2727
+ validateGitRemote(this.config.repository, this.gitRoot);
2663
2728
  }
2664
2729
  // Auto-detect work type from issue status if not provided
2665
2730
  // This must happen BEFORE creating worktree since path includes work type suffix
2666
2731
  let effectiveWorkType = workType;
2667
2732
  if (!effectiveWorkType) {
2668
- const state = await issue.state;
2669
- const statusName = state?.name ?? 'Backlog';
2733
+ const statusName = issue.status ?? 'Backlog';
2670
2734
  effectiveWorkType = await this.detectWorkType(issueId, statusName);
2671
2735
  }
2736
+ else {
2737
+ // Re-validate: upgrade to coordination variant if this is a parent issue
2738
+ // The caller may have a stale work type from before the session was queued
2739
+ try {
2740
+ const isParent = await this.client.isParentIssue(issueId);
2741
+ if (isParent) {
2742
+ const upgraded = detectWorkType(issue.status ?? 'Backlog', isParent, this.statusMappings.statusToWorkType);
2743
+ if (upgraded !== effectiveWorkType) {
2744
+ console.log(`Upgrading work type from ${effectiveWorkType} to ${upgraded} (parent issue detected)`);
2745
+ effectiveWorkType = upgraded;
2746
+ }
2747
+ }
2748
+ }
2749
+ catch (err) {
2750
+ console.warn(`Failed to check parent status for coordination upgrade:`, err);
2751
+ }
2752
+ }
2672
2753
  // Create isolated worktree for the agent
2673
2754
  let worktreePath;
2674
2755
  let worktreeIdentifier;
2675
- if (WORK_TYPES_REQUIRING_WORKTREE.has(effectiveWorkType)) {
2756
+ if (this.statusMappings.workTypesRequiringWorktree.has(effectiveWorkType)) {
2676
2757
  const wt = this.createWorktree(identifier, effectiveWorkType);
2677
2758
  worktreePath = wt.worktreePath;
2678
2759
  worktreeIdentifier = wt.worktreeIdentifier;
@@ -2704,7 +2785,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2704
2785
  const effectiveSessionId = sessionId ?? recoveryCheck.state.linearSessionId ?? randomUUID();
2705
2786
  console.log(`Resuming work on ${identifier} (recovery attempt ${updatedState?.recoveryAttempts ?? 1})`);
2706
2787
  // Update status based on work type if auto-transition is enabled
2707
- const startStatus = WORK_TYPE_START_STATUS[recoveryWorkType];
2788
+ const startStatus = this.statusMappings.workTypeStartStatus[recoveryWorkType];
2708
2789
  if (this.config.autoTransition && startStatus) {
2709
2790
  await this.client.updateIssueStatus(issueId, startStatus);
2710
2791
  console.log(`Updated ${identifier} status to ${startStatus}`);
@@ -2727,7 +2808,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2727
2808
  }
2728
2809
  // No recovery needed - proceed with fresh spawn
2729
2810
  // Update status based on work type if auto-transition is enabled
2730
- const startStatus = WORK_TYPE_START_STATUS[effectiveWorkType];
2811
+ const startStatus = this.statusMappings.workTypeStartStatus[effectiveWorkType];
2731
2812
  if (this.config.autoTransition && startStatus) {
2732
2813
  await this.client.updateIssueStatus(issueId, startStatus);
2733
2814
  console.log(`Updated ${identifier} status to ${startStatus}`);
@@ -2900,12 +2981,10 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2900
2981
  try {
2901
2982
  const issue = await this.client.getIssue(issueId);
2902
2983
  identifier = issue.identifier;
2903
- const issueTeam = await issue.team;
2904
- teamName = issueTeam?.key;
2984
+ teamName = issue.teamName;
2905
2985
  // 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)) {
2986
+ const currentStatus = issue.status;
2987
+ if (currentStatus && this.statusMappings.terminalStatuses.includes(currentStatus)) {
2909
2988
  console.log(`Issue ${identifier} is in terminal status '${currentStatus}' — skipping work`);
2910
2989
  return {
2911
2990
  forwarded: false,
@@ -2921,7 +3000,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2921
3000
  workType = await this.detectWorkType(issue.id, statusName);
2922
3001
  }
2923
3002
  // Create isolated worktree for the agent
2924
- if (WORK_TYPES_REQUIRING_WORKTREE.has(workType)) {
3003
+ if (this.statusMappings.workTypesRequiringWorktree.has(workType)) {
2925
3004
  const result = this.createWorktree(identifier, workType);
2926
3005
  worktreePath = result.worktreePath;
2927
3006
  worktreeIdentifier = result.worktreeIdentifier;
@@ -2940,7 +3019,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2940
3019
  }
2941
3020
  // Check if worktree exists (only relevant for code work types)
2942
3021
  const effectiveWorkType = workType ?? 'development';
2943
- if (WORK_TYPES_REQUIRING_WORKTREE.has(effectiveWorkType) && worktreePath && !existsSync(worktreePath)) {
3022
+ if (this.statusMappings.workTypesRequiringWorktree.has(effectiveWorkType) && worktreePath && !existsSync(worktreePath)) {
2944
3023
  try {
2945
3024
  const result = this.createWorktree(identifier, effectiveWorkType);
2946
3025
  worktreePath = result.worktreePath;
@@ -3056,7 +3135,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3056
3135
  // Use the work type to determine if we need to transition on start
3057
3136
  // Only certain work types trigger a start transition
3058
3137
  const effectiveWorkType = workType ?? 'development';
3059
- const startStatus = WORK_TYPE_START_STATUS[effectiveWorkType];
3138
+ const startStatus = this.statusMappings.workTypeStartStatus[effectiveWorkType];
3060
3139
  if (this.config.autoTransition && startStatus) {
3061
3140
  try {
3062
3141
  await this.client.updateIssueStatus(issueId, startStatus);
@@ -3168,9 +3247,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3168
3247
  });
3169
3248
  }
3170
3249
  else {
3171
- // Direct Linear API
3172
- const session = createAgentSession({
3173
- client: this.client.linearClient,
3250
+ // Direct issue tracker API
3251
+ const session = this.client.createSession({
3174
3252
  issueId,
3175
3253
  sessionId,
3176
3254
  autoTransition: false,