@renseiai/agentfactory 0.8.3 → 0.8.5

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 (34) hide show
  1. package/dist/src/config/index.d.ts +1 -1
  2. package/dist/src/config/index.d.ts.map +1 -1
  3. package/dist/src/config/index.js +1 -1
  4. package/dist/src/config/repository-config.d.ts +53 -0
  5. package/dist/src/config/repository-config.d.ts.map +1 -1
  6. package/dist/src/config/repository-config.js +23 -0
  7. package/dist/src/config/repository-config.test.js +91 -1
  8. package/dist/src/orchestrator/detect-work-type.test.d.ts +2 -0
  9. package/dist/src/orchestrator/detect-work-type.test.d.ts.map +1 -0
  10. package/dist/src/orchestrator/detect-work-type.test.js +62 -0
  11. package/dist/src/orchestrator/heartbeat-writer.test.d.ts +2 -0
  12. package/dist/src/orchestrator/heartbeat-writer.test.d.ts.map +1 -0
  13. package/dist/src/orchestrator/heartbeat-writer.test.js +139 -0
  14. package/dist/src/orchestrator/orchestrator-utils.test.d.ts +2 -0
  15. package/dist/src/orchestrator/orchestrator-utils.test.d.ts.map +1 -0
  16. package/dist/src/orchestrator/orchestrator-utils.test.js +41 -0
  17. package/dist/src/orchestrator/orchestrator.d.ts +25 -0
  18. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  19. package/dist/src/orchestrator/orchestrator.js +92 -43
  20. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  21. package/dist/src/orchestrator/parse-work-result.js +6 -0
  22. package/dist/src/orchestrator/parse-work-result.test.js +13 -0
  23. package/dist/src/orchestrator/state-recovery.test.d.ts +2 -0
  24. package/dist/src/orchestrator/state-recovery.test.d.ts.map +1 -0
  25. package/dist/src/orchestrator/state-recovery.test.js +425 -0
  26. package/dist/src/orchestrator/types.d.ts +11 -1
  27. package/dist/src/orchestrator/types.d.ts.map +1 -1
  28. package/dist/src/providers/index.d.ts +71 -15
  29. package/dist/src/providers/index.d.ts.map +1 -1
  30. package/dist/src/providers/index.js +156 -28
  31. package/dist/src/providers/index.test.d.ts +2 -0
  32. package/dist/src/providers/index.test.d.ts.map +1 -0
  33. package/dist/src/providers/index.test.js +225 -0
  34. package/package.json +3 -3
@@ -8,7 +8,7 @@ import { execSync } from 'child_process';
8
8
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'fs';
9
9
  import { resolve, dirname } from 'path';
10
10
  import { parse as parseDotenv } from 'dotenv';
11
- import { createProvider, resolveProviderName, } from '../providers/index.js';
11
+ import { createProvider, resolveProviderName, resolveProviderWithSource, } from '../providers/index.js';
12
12
  import { initializeAgentDir, writeState, updateState, writeTodos, createInitialState, checkRecovery, buildRecoveryPrompt, getHeartbeatTimeoutFromEnv, getMaxRecoveryAttemptsFromEnv, } from './state-recovery.js';
13
13
  import { createHeartbeatWriter, getHeartbeatIntervalFromEnv } from './heartbeat-writer.js';
14
14
  import { createProgressLogger } from './progress-logger.js';
@@ -20,7 +20,7 @@ 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
- import { loadRepositoryConfig, getProjectConfig } from '../config/index.js';
23
+ import { loadRepositoryConfig, getProjectConfig, getProvidersConfig } from '../config/index.js';
24
24
  import { ToolRegistry, linearPlugin } from '../tools/index.js';
25
25
  // Default inactivity timeout: 5 minutes
26
26
  const DEFAULT_INACTIVITY_TIMEOUT_MS = 300000;
@@ -670,6 +670,30 @@ export function getWorktreeIdentifier(issueIdentifier, workType) {
670
670
  const suffix = WORK_TYPE_SUFFIX[workType];
671
671
  return `${issueIdentifier}-${suffix}`;
672
672
  }
673
+ /**
674
+ * Detect the appropriate work type for an issue based on its status,
675
+ * upgrading to coordination variants for parent issues with sub-issues.
676
+ *
677
+ * This prevents parent issues returning to Backlog after refinement from
678
+ * being dispatched as 'development' (which uses the wrong template and
679
+ * produces no sub-agent orchestration).
680
+ */
681
+ export function detectWorkType(statusName, isParent) {
682
+ let workType = STATUS_WORK_TYPE_MAP[statusName] ?? 'development';
683
+ console.log(`Auto-detected work type: ${workType} (from status: ${statusName})`);
684
+ if (isParent) {
685
+ if (workType === 'development')
686
+ workType = 'coordination';
687
+ else if (workType === 'qa')
688
+ workType = 'qa-coordination';
689
+ else if (workType === 'acceptance')
690
+ workType = 'acceptance-coordination';
691
+ else if (workType === 'refinement')
692
+ workType = 'refinement-coordination';
693
+ console.log(`Upgraded to coordination work type: ${workType} (parent issue)`);
694
+ }
695
+ return workType;
696
+ }
673
697
  export class AgentOrchestrator {
674
698
  config;
675
699
  client;
@@ -677,6 +701,8 @@ export class AgentOrchestrator {
677
701
  activeAgents = new Map();
678
702
  agentHandles = new Map();
679
703
  provider;
704
+ providerCache = new Map();
705
+ configProviders;
680
706
  agentSessions = new Map();
681
707
  activityEmitters = new Map();
682
708
  // Track session ID to issue ID mapping for stop signal handling
@@ -743,9 +769,10 @@ export class AgentOrchestrator {
743
769
  }
744
770
  this.client = createLinearAgentClient({ apiKey });
745
771
  this.events = events;
746
- // Initialize agent provider — defaults to Claude, configurable via env
772
+ // Initialize default agent provider — per-spawn resolution may override
747
773
  const providerName = resolveProviderName({ project: config.project });
748
774
  this.provider = config.provider ?? createProvider(providerName);
775
+ this.providerCache.set(this.provider.name, this.provider);
749
776
  // Initialize template registry for configurable workflow prompts
750
777
  try {
751
778
  const templateDirs = [];
@@ -810,6 +837,8 @@ export class AgentOrchestrator {
810
837
  if (repoConfig.validateCommand) {
811
838
  this.validateCommand = repoConfig.validateCommand;
812
839
  }
840
+ // Store providers config for per-spawn resolution
841
+ this.configProviders = getProvidersConfig(repoConfig);
813
842
  }
814
843
  }
815
844
  }
@@ -852,6 +881,18 @@ export class AgentOrchestrator {
852
881
  }
853
882
  return baseConfig;
854
883
  }
884
+ /**
885
+ * Detect the appropriate work type for an issue, upgrading to coordination
886
+ * variants for parent issues that have sub-issues.
887
+ *
888
+ * This prevents parent issues returning to Backlog after refinement from
889
+ * being dispatched as 'development' (which uses the wrong template and
890
+ * produces no sub-agent orchestration).
891
+ */
892
+ async detectWorkType(issueId, statusName) {
893
+ const isParent = await this.client.isParentIssue(issueId);
894
+ return detectWorkType(statusName, isParent);
895
+ }
855
896
  /**
856
897
  * Get backlog issues for the configured project
857
898
  */
@@ -1591,11 +1632,33 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1591
1632
  preInstallDependencies(worktreePath, identifier) {
1592
1633
  this.linkDependencies(worktreePath, identifier);
1593
1634
  }
1635
+ /**
1636
+ * Resolve the provider for a specific spawn, using the full priority cascade.
1637
+ * Returns a cached provider instance (creating one if needed) and the resolved name.
1638
+ */
1639
+ resolveProviderForSpawn(context) {
1640
+ const { name, source } = resolveProviderWithSource({
1641
+ project: context.projectName,
1642
+ workType: context.workType,
1643
+ labels: context.labels,
1644
+ mentionContext: context.mentionContext,
1645
+ configProviders: this.configProviders,
1646
+ });
1647
+ // Return cached instance or create a new one
1648
+ let provider = this.providerCache.get(name);
1649
+ if (!provider) {
1650
+ provider = createProvider(name);
1651
+ this.providerCache.set(name, provider);
1652
+ }
1653
+ return { provider, providerName: name, source };
1654
+ }
1594
1655
  /**
1595
1656
  * Spawn a Claude agent for a specific issue using the Agent SDK
1596
1657
  */
1597
1658
  spawnAgent(options) {
1598
- const { issueId, identifier, worktreeIdentifier, sessionId, worktreePath, streamActivities, workType = 'development', prompt: customPrompt, teamName, projectName, } = options;
1659
+ const { issueId, identifier, worktreeIdentifier, sessionId, worktreePath, streamActivities, workType = 'development', prompt: customPrompt, teamName, projectName, labels, mentionContext, } = options;
1660
+ // Resolve provider for this specific spawn (may differ from default)
1661
+ const { provider: spawnProvider, providerName: spawnProviderName, source: providerSource } = this.resolveProviderForSpawn({ workType, projectName, labels, mentionContext });
1599
1662
  // Generate prompt based on work type, or use custom prompt if provided
1600
1663
  // Try template registry first, fall back to hardcoded prompts
1601
1664
  let prompt;
@@ -1612,7 +1675,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1612
1675
  repository: this.config.repository,
1613
1676
  projectPath: perProject?.path ?? this.projectPaths?.[projectName ?? ''],
1614
1677
  sharedPaths: this.sharedPaths,
1615
- useToolPlugins: this.provider.name === 'claude',
1678
+ useToolPlugins: spawnProviderName === 'claude',
1616
1679
  linearCli: this.linearCli ?? 'pnpm af-linear',
1617
1680
  packageManager: perProject?.packageManager ?? this.packageManager ?? 'pnpm',
1618
1681
  buildCommand: perProject?.buildCommand ?? this.buildCommand,
@@ -1640,6 +1703,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1640
1703
  startedAt: now,
1641
1704
  lastActivityAt: now, // Initialize for inactivity tracking
1642
1705
  workType,
1706
+ providerName: spawnProviderName,
1643
1707
  };
1644
1708
  this.activeAgents.set(issueId, agent);
1645
1709
  // Track session to issue mapping for stop signal handling
@@ -1794,9 +1858,9 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1794
1858
  if (teamName) {
1795
1859
  env.LINEAR_TEAM_NAME = teamName;
1796
1860
  }
1797
- log.info('Starting agent via provider', { provider: this.provider.name, cwd: worktreePath ?? 'repo-root', workType, promptPreview: prompt.substring(0, 50) });
1861
+ log.info('Starting agent via provider', { provider: spawnProviderName, source: providerSource, cwd: worktreePath ?? 'repo-root', workType, promptPreview: prompt.substring(0, 50) });
1798
1862
  // Create in-process tool servers from registered plugins
1799
- const mcpServers = this.provider.name === 'claude'
1863
+ const mcpServers = spawnProviderName === 'claude'
1800
1864
  ? this.toolRegistry.createServers({ env, cwd: worktreePath ?? process.cwd() })
1801
1865
  : undefined;
1802
1866
  // Coordinators need significantly more turns than standard agents
@@ -1819,7 +1883,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1819
1883
  log.info('Agent process spawned', { pid });
1820
1884
  },
1821
1885
  };
1822
- const handle = this.provider.spawn(spawnConfig);
1886
+ const handle = spawnProvider.spawn(spawnConfig);
1823
1887
  this.agentHandles.set(issueId, handle);
1824
1888
  agent.status = 'running';
1825
1889
  // Process the event stream in the background
@@ -2521,8 +2585,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2521
2585
  this.events.onIssueSelected?.(issue);
2522
2586
  console.log(`Processing: ${issue.identifier} - ${issue.title}`);
2523
2587
  try {
2524
- // Backlog issues are always development work
2525
- const workType = 'development';
2588
+ // Detect work type — parent issues with sub-issues use coordination variants
2589
+ const workType = await this.detectWorkType(issue.id, 'Backlog');
2526
2590
  // Create worktree with work type suffix
2527
2591
  const { worktreePath, worktreeIdentifier } = this.createWorktree(issue.identifier, workType);
2528
2592
  // Link dependencies from main repo into worktree
@@ -2543,6 +2607,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2543
2607
  workType,
2544
2608
  teamName: issue.teamName,
2545
2609
  projectName: issue.projectName,
2610
+ labels: issue.labels,
2546
2611
  });
2547
2612
  result.agents.push(agent);
2548
2613
  }
@@ -2575,6 +2640,9 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2575
2640
  const issueId = issue.id; // Use the actual UUID
2576
2641
  const team = await issue.team;
2577
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);
2578
2646
  // Resolve project name for path scoping in monorepos
2579
2647
  let projectName;
2580
2648
  if (this.projectPaths) {
@@ -2599,21 +2667,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2599
2667
  if (!effectiveWorkType) {
2600
2668
  const state = await issue.state;
2601
2669
  const statusName = state?.name ?? 'Backlog';
2602
- effectiveWorkType = STATUS_WORK_TYPE_MAP[statusName] ?? 'development';
2603
- console.log(`Auto-detected work type: ${effectiveWorkType} (from status: ${statusName})`);
2604
- // Parent issues use coordination variants
2605
- const isParent = await this.client.isParentIssue(issueId);
2606
- if (isParent) {
2607
- if (effectiveWorkType === 'development')
2608
- effectiveWorkType = 'coordination';
2609
- else if (effectiveWorkType === 'qa')
2610
- effectiveWorkType = 'qa-coordination';
2611
- else if (effectiveWorkType === 'acceptance')
2612
- effectiveWorkType = 'acceptance-coordination';
2613
- else if (effectiveWorkType === 'refinement')
2614
- effectiveWorkType = 'refinement-coordination';
2615
- console.log(`Upgraded to coordination work type: ${effectiveWorkType} (parent issue)`);
2616
- }
2670
+ effectiveWorkType = await this.detectWorkType(issueId, statusName);
2617
2671
  }
2618
2672
  // Create isolated worktree for the agent
2619
2673
  let worktreePath;
@@ -2667,6 +2721,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2667
2721
  workType: recoveryWorkType,
2668
2722
  teamName,
2669
2723
  projectName,
2724
+ labels: labelNames,
2670
2725
  });
2671
2726
  }
2672
2727
  }
@@ -2691,6 +2746,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2691
2746
  prompt,
2692
2747
  teamName,
2693
2748
  projectName,
2749
+ labels: labelNames,
2694
2750
  });
2695
2751
  }
2696
2752
  /**
@@ -2862,19 +2918,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2862
2918
  // incorrect status transitions (e.g., Delivered → Started for acceptance work)
2863
2919
  if (!workType) {
2864
2920
  const statusName = currentStatus ?? 'Backlog';
2865
- workType = STATUS_WORK_TYPE_MAP[statusName] ?? 'development';
2866
- // Parent issues use coordination variants
2867
- const isParent = await this.client.isParentIssue(issue.id);
2868
- if (isParent) {
2869
- if (workType === 'development')
2870
- workType = 'coordination';
2871
- else if (workType === 'qa')
2872
- workType = 'qa-coordination';
2873
- else if (workType === 'acceptance')
2874
- workType = 'acceptance-coordination';
2875
- else if (workType === 'refinement')
2876
- workType = 'refinement-coordination';
2877
- }
2921
+ workType = await this.detectWorkType(issue.id, statusName);
2878
2922
  }
2879
2923
  // Create isolated worktree for the agent
2880
2924
  if (WORK_TYPES_REQUIRING_WORKTREE.has(workType)) {
@@ -2925,6 +2969,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2925
2969
  providerSessionId,
2926
2970
  workType,
2927
2971
  teamName,
2972
+ mentionContext: prompt,
2928
2973
  });
2929
2974
  return {
2930
2975
  forwarded: true,
@@ -3002,7 +3047,9 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3002
3047
  * If autoTransition is enabled, also transitions the issue status to the appropriate working state
3003
3048
  */
3004
3049
  async spawnAgentWithResume(options) {
3005
- const { issueId, identifier, worktreeIdentifier, sessionId, worktreePath, prompt, providerSessionId, workType, teamName } = options;
3050
+ const { issueId, identifier, worktreeIdentifier, sessionId, worktreePath, prompt, providerSessionId, workType, teamName, labels, mentionContext } = options;
3051
+ // Resolve provider for this specific spawn (may differ from default)
3052
+ const { provider: spawnProvider, providerName: spawnProviderName, source: providerSource } = this.resolveProviderForSpawn({ workType, projectName: options.projectName, labels, mentionContext });
3006
3053
  // Create logger for this agent
3007
3054
  const log = createLogger({ issueIdentifier: identifier });
3008
3055
  this.agentLoggers.set(issueId, log);
@@ -3035,6 +3082,7 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3035
3082
  startedAt: now,
3036
3083
  lastActivityAt: now, // Initialize for inactivity tracking
3037
3084
  workType,
3085
+ providerName: spawnProviderName,
3038
3086
  };
3039
3087
  this.activeAgents.set(issueId, agent);
3040
3088
  // Track session to issue mapping for stop signal handling
@@ -3172,13 +3220,14 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3172
3220
  ...(teamName && { LINEAR_TEAM_NAME: teamName }),
3173
3221
  };
3174
3222
  log.info('Starting agent via provider', {
3175
- provider: this.provider.name,
3223
+ provider: spawnProviderName,
3224
+ source: providerSource,
3176
3225
  cwd: worktreePath ?? 'repo-root',
3177
3226
  resuming: !!providerSessionId,
3178
3227
  workType: workType ?? 'development',
3179
3228
  });
3180
3229
  // Create in-process tool servers from registered plugins
3181
- const mcpServers = this.provider.name === 'claude'
3230
+ const mcpServers = spawnProviderName === 'claude'
3182
3231
  ? this.toolRegistry.createServers({ env, cwd: worktreePath ?? process.cwd() })
3183
3232
  : undefined;
3184
3233
  // Coordinators need significantly more turns than standard agents
@@ -3201,8 +3250,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3201
3250
  },
3202
3251
  };
3203
3252
  const handle = providerSessionId
3204
- ? this.provider.resume(providerSessionId, spawnConfig)
3205
- : this.provider.spawn(spawnConfig);
3253
+ ? spawnProvider.resume(providerSessionId, spawnConfig)
3254
+ : spawnProvider.spawn(spawnConfig);
3206
3255
  this.agentHandles.set(issueId, handle);
3207
3256
  agent.status = 'running';
3208
3257
  // Process the event stream in the background
@@ -1 +1 @@
1
- {"version":3,"file":"parse-work-result.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/parse-work-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA+FjD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,QAAQ,EAAE,aAAa,GACtB,eAAe,CAyCjB"}
1
+ {"version":3,"file":"parse-work-result.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/parse-work-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAqGjD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,QAAQ,EAAE,aAAa,GACtB,eAAe,CAyCjB"}
@@ -22,6 +22,12 @@ const QA_PASS_PATTERNS = [
22
22
  /Roll-?Up\s+Verdict:\s*PASS/i,
23
23
  // Bold standalone PASS (agents commonly output **PASS** or **PASS.**)
24
24
  /\*\*PASS\.?\*\*/,
25
+ // "Already done" patterns — agent recognized prior QA passed, treat as pass
26
+ /\b(?:QA\s+(?:coordination\s+)?)?(?:is\s+)?already\s+(?:done|complete|completed)\b/i,
27
+ // "APPROVED FOR MERGE" — explicit approval language
28
+ /\bAPPROVED\s+FOR\s+MERGE\b/i,
29
+ // "all checks passed" — agent confirmed all checks passed
30
+ /\ball\s+checks\s+passed\b/i,
25
31
  ];
26
32
  const QA_FAIL_PATTERNS = [
27
33
  // Heading patterns
@@ -105,6 +105,19 @@ describe('parseWorkResult', () => {
105
105
  it('does not false-positive on "tests pass" (no bold)', () => {
106
106
  expect(parseWorkResult('All tests pass and everything looks good.', 'qa')).toBe('unknown');
107
107
  });
108
+ // "Already done" patterns — agent recognized prior work was complete
109
+ it('detects "already done" as pass for qa', () => {
110
+ expect(parseWorkResult('QA coordination for SUP-1145 is already done — the report was posted.', 'qa')).toBe('passed');
111
+ });
112
+ it('detects "already complete" as pass for qa-coordination', () => {
113
+ expect(parseWorkResult('QA coordination is already complete. No further action needed.', 'qa-coordination')).toBe('passed');
114
+ });
115
+ it('detects "APPROVED FOR MERGE" as pass', () => {
116
+ expect(parseWorkResult('**Status: APPROVED FOR MERGE**\n\nAll checks passed.', 'qa-coordination')).toBe('passed');
117
+ });
118
+ it('detects "all checks passed" as pass', () => {
119
+ expect(parseWorkResult('The report was posted to Linear and all checks passed.', 'qa')).toBe('passed');
120
+ });
108
121
  });
109
122
  // Acceptance heuristic pattern tests
110
123
  describe('acceptance heuristic patterns', () => {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=state-recovery.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-recovery.test.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/state-recovery.test.ts"],"names":[],"mappings":""}