@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.
- package/README.md +2 -2
- package/dist/src/config/repository-config.d.ts +14 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +20 -0
- package/dist/src/governor/decision-engine.d.ts +7 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +59 -1
- package/dist/src/governor/event-types.d.ts +18 -1
- package/dist/src/governor/event-types.d.ts.map +1 -1
- package/dist/src/governor/event-types.js +4 -0
- package/dist/src/governor/governor.d.ts +5 -1
- package/dist/src/governor/governor.d.ts.map +1 -1
- package/dist/src/governor/governor.js +6 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.js +243 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
- package/dist/src/merge-queue/index.d.ts +18 -0
- package/dist/src/merge-queue/index.d.ts.map +1 -0
- package/dist/src/merge-queue/index.js +28 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
- package/dist/src/merge-queue/types.d.ts +48 -0
- package/dist/src/merge-queue/types.d.ts.map +1 -0
- package/dist/src/merge-queue/types.js +8 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +3 -3
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -1
- package/dist/src/orchestrator/activity-emitter.js +1 -1
- package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.js +235 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
- package/dist/src/orchestrator/context-manager.d.ts +72 -0
- package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.js +120 -0
- package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
- package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.test.js +137 -0
- package/dist/src/orchestrator/detect-work-type.test.js +25 -16
- package/dist/src/orchestrator/index.d.ts +12 -2
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +9 -1
- package/dist/src/orchestrator/issue-tracker-client.d.ts +103 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -0
- package/dist/src/orchestrator/issue-tracker-client.js +8 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +19 -4
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -1
- package/dist/src/orchestrator/log-analyzer.js +26 -50
- package/dist/src/orchestrator/orchestrator-utils.test.js +3 -0
- package/dist/src/orchestrator/orchestrator.d.ts +16 -2
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +449 -115
- package/dist/src/orchestrator/parse-work-result.d.ts +1 -1
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +1 -1
- package/dist/src/orchestrator/session-logger.d.ts +1 -1
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.d.ts +22 -3
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.js +55 -2
- package/dist/src/orchestrator/state-recovery.test.js +106 -2
- package/dist/src/orchestrator/state-types.d.ts +63 -1
- package/dist/src/orchestrator/state-types.d.ts.map +1 -1
- package/dist/src/orchestrator/state-types.js +5 -1
- package/dist/src/orchestrator/summary-builder.d.ts +47 -0
- package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.js +240 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.test.js +236 -0
- package/dist/src/orchestrator/types.d.ts +24 -2
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/orchestrator/work-types.d.ts +50 -0
- package/dist/src/orchestrator/work-types.d.ts.map +1 -0
- package/dist/src/orchestrator/work-types.js +20 -0
- package/dist/src/templates/registry.d.ts +1 -1
- package/dist/src/templates/registry.test.js +2 -2
- package/dist/src/templates/renderer.d.ts +1 -1
- package/dist/src/templates/types.d.ts +6 -2
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +2 -0
- package/dist/src/templates/types.test.js +4 -3
- package/dist/src/tools/index.d.ts +0 -3
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +0 -2
- package/dist/src/workflow/branching-router.d.ts +38 -0
- package/dist/src/workflow/branching-router.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.js +52 -0
- package/dist/src/workflow/branching-router.test.d.ts +2 -0
- package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.test.js +209 -0
- package/dist/src/workflow/duration.d.ts +28 -0
- package/dist/src/workflow/duration.d.ts.map +1 -0
- package/dist/src/workflow/duration.js +57 -0
- package/dist/src/workflow/duration.test.d.ts +2 -0
- package/dist/src/workflow/duration.test.d.ts.map +1 -0
- package/dist/src/workflow/duration.test.js +74 -0
- package/dist/src/workflow/expression/ast.d.ts +53 -0
- package/dist/src/workflow/expression/ast.d.ts.map +1 -0
- package/dist/src/workflow/expression/ast.js +8 -0
- package/dist/src/workflow/expression/context.d.ts +40 -0
- package/dist/src/workflow/expression/context.d.ts.map +1 -0
- package/dist/src/workflow/expression/context.js +37 -0
- package/dist/src/workflow/expression/evaluator.d.ts +28 -0
- package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.js +165 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.test.js +792 -0
- package/dist/src/workflow/expression/expression.test.d.ts +2 -0
- package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/expression.test.js +516 -0
- package/dist/src/workflow/expression/helpers.d.ts +21 -0
- package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
- package/dist/src/workflow/expression/helpers.js +56 -0
- package/dist/src/workflow/expression/index.d.ts +55 -0
- package/dist/src/workflow/expression/index.d.ts.map +1 -0
- package/dist/src/workflow/expression/index.js +71 -0
- package/dist/src/workflow/expression/lexer.d.ts +37 -0
- package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
- package/dist/src/workflow/expression/lexer.js +166 -0
- package/dist/src/workflow/expression/parser.d.ts +23 -0
- package/dist/src/workflow/expression/parser.d.ts.map +1 -0
- package/dist/src/workflow/expression/parser.js +181 -0
- package/dist/src/workflow/index.d.ts +21 -0
- package/dist/src/workflow/index.d.ts.map +1 -0
- package/dist/src/workflow/index.js +15 -0
- package/dist/src/workflow/retry-resolver.d.ts +51 -0
- package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.js +70 -0
- package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
- package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.test.js +149 -0
- package/dist/src/workflow/transition-engine.d.ts +46 -0
- package/dist/src/workflow/transition-engine.d.ts.map +1 -0
- package/dist/src/workflow/transition-engine.js +113 -0
- package/dist/src/workflow/transition-engine.test.d.ts +2 -0
- package/dist/src/workflow/transition-engine.test.d.ts.map +1 -0
- package/dist/src/workflow/transition-engine.test.js +425 -0
- package/dist/src/workflow/workflow-loader.d.ts +21 -0
- package/dist/src/workflow/workflow-loader.d.ts.map +1 -0
- package/dist/src/workflow/workflow-loader.js +40 -0
- package/dist/src/workflow/workflow-loader.test.d.ts +2 -0
- package/dist/src/workflow/workflow-loader.test.d.ts.map +1 -0
- package/dist/src/workflow/workflow-loader.test.js +134 -0
- package/dist/src/workflow/workflow-registry.d.ts +97 -0
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -0
- package/dist/src/workflow/workflow-registry.js +173 -0
- package/dist/src/workflow/workflow-registry.test.d.ts +2 -0
- package/dist/src/workflow/workflow-registry.test.d.ts.map +1 -0
- package/dist/src/workflow/workflow-registry.test.js +201 -0
- package/dist/src/workflow/workflow-types.d.ts +442 -0
- package/dist/src/workflow/workflow-types.d.ts.map +1 -0
- package/dist/src/workflow/workflow-types.js +113 -0
- package/dist/src/workflow/workflow-types.test.d.ts +2 -0
- package/dist/src/workflow/workflow-types.test.d.ts.map +1 -0
- package/dist/src/workflow/workflow-types.test.js +440 -0
- package/package.json +3 -4
- package/dist/src/linear-cli.d.ts +0 -38
- package/dist/src/linear-cli.d.ts.map +0 -1
- package/dist/src/linear-cli.js +0 -674
- package/dist/src/tools/linear-runner.d.ts +0 -34
- package/dist/src/tools/linear-runner.d.ts.map +0 -1
- package/dist/src/tools/linear-runner.js +0 -700
- package/dist/src/tools/plugins/linear.d.ts +0 -9
- package/dist/src/tools/plugins/linear.d.ts.map +0 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
742
|
-
if (!
|
|
743
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
783
|
-
const projectTemplateDir = resolve(
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
|
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
|
-
|
|
950
|
-
|
|
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
|
|
1076
|
+
description: issue.description,
|
|
969
1077
|
url: issue.url,
|
|
970
1078
|
priority: issue.priority,
|
|
971
|
-
labels: labels
|
|
972
|
-
teamName:
|
|
973
|
-
projectName:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
2013
|
-
|
|
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
|
|
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 =
|
|
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
|
|
2642
|
-
|
|
2643
|
-
|
|
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
|
-
|
|
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
|
|
2655
|
-
|
|
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
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2907
|
-
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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
|
|
3172
|
-
const session =
|
|
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,
|