@telora/daemon 0.17.36 → 0.17.42
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/build-info.json +5 -3
- package/dist/assembly-engine.d.ts +6 -0
- package/dist/assembly-engine.d.ts.map +1 -1
- package/dist/assembly-engine.js +19 -0
- package/dist/assembly-engine.js.map +1 -1
- package/dist/assembly-resolvers.d.ts +17 -8
- package/dist/assembly-resolvers.d.ts.map +1 -1
- package/dist/assembly-resolvers.js +19 -8
- package/dist/assembly-resolvers.js.map +1 -1
- package/dist/cli/session-state.d.ts +10 -0
- package/dist/cli/session-state.d.ts.map +1 -1
- package/dist/cli/session-state.js +31 -0
- package/dist/cli/session-state.js.map +1 -1
- package/dist/completion/completion-decision.d.ts +83 -0
- package/dist/completion/completion-decision.d.ts.map +1 -0
- package/dist/completion/completion-decision.js +48 -0
- package/dist/completion/completion-decision.js.map +1 -0
- package/dist/completion/event-escalations.d.ts +97 -0
- package/dist/completion/event-escalations.d.ts.map +1 -0
- package/dist/completion/event-escalations.js +213 -0
- package/dist/completion/event-escalations.js.map +1 -0
- package/dist/completion/event.d.ts +1 -72
- package/dist/completion/event.d.ts.map +1 -1
- package/dist/completion/event.js +148 -322
- package/dist/completion/event.js.map +1 -1
- package/dist/completion/exit-classification.d.ts +82 -0
- package/dist/completion/exit-classification.d.ts.map +1 -0
- package/dist/completion/exit-classification.js +61 -0
- package/dist/completion/exit-classification.js.map +1 -0
- package/dist/completion/index.d.ts +14 -5
- package/dist/completion/index.d.ts.map +1 -1
- package/dist/completion/index.js +14 -5
- package/dist/completion/index.js.map +1 -1
- package/dist/completion/merge-phase.d.ts +114 -0
- package/dist/completion/merge-phase.d.ts.map +1 -0
- package/dist/completion/merge-phase.js +198 -0
- package/dist/completion/merge-phase.js.map +1 -0
- package/dist/completion/review-exit-phase.d.ts +82 -0
- package/dist/completion/review-exit-phase.d.ts.map +1 -0
- package/dist/completion/review-exit-phase.js +228 -0
- package/dist/completion/review-exit-phase.js.map +1 -0
- package/dist/completion/status-advance-phase.d.ts +61 -0
- package/dist/completion/status-advance-phase.d.ts.map +1 -0
- package/dist/completion/status-advance-phase.js +182 -0
- package/dist/completion/status-advance-phase.js.map +1 -0
- package/dist/completion/team-completion.d.ts +28 -269
- package/dist/completion/team-completion.d.ts.map +1 -1
- package/dist/completion/team-completion.js +145 -676
- package/dist/completion/team-completion.js.map +1 -1
- package/dist/daemon-process.d.ts +18 -2
- package/dist/daemon-process.d.ts.map +1 -1
- package/dist/daemon-process.js +7 -2
- package/dist/daemon-process.js.map +1 -1
- package/dist/directive/close-loop-stage.d.ts +50 -0
- package/dist/directive/close-loop-stage.d.ts.map +1 -0
- package/dist/directive/close-loop-stage.js +196 -0
- package/dist/directive/close-loop-stage.js.map +1 -0
- package/dist/directive/directive-assembly.d.ts +33 -0
- package/dist/directive/directive-assembly.d.ts.map +1 -0
- package/dist/directive/directive-assembly.js +77 -0
- package/dist/directive/directive-assembly.js.map +1 -0
- package/dist/directive/directive-dispatch.d.ts +103 -0
- package/dist/directive/directive-dispatch.d.ts.map +1 -0
- package/dist/directive/directive-dispatch.js +279 -0
- package/dist/directive/directive-dispatch.js.map +1 -0
- package/dist/directive/phase-sync.d.ts +89 -0
- package/dist/directive/phase-sync.d.ts.map +1 -0
- package/dist/directive/phase-sync.js +173 -0
- package/dist/directive/phase-sync.js.map +1 -0
- package/dist/directive-executor.d.ts +21 -223
- package/dist/directive-executor.d.ts.map +1 -1
- package/dist/directive-executor.js +28 -687
- package/dist/directive-executor.js.map +1 -1
- package/dist/focus-engine.d.ts.map +1 -1
- package/dist/focus-engine.js +8 -0
- package/dist/focus-engine.js.map +1 -1
- package/dist/focus-executor.d.ts +29 -8
- package/dist/focus-executor.d.ts.map +1 -1
- package/dist/focus-executor.js +29 -10
- package/dist/focus-executor.js.map +1 -1
- package/dist/focus-stage-lifecycle.d.ts +7 -5
- package/dist/focus-stage-lifecycle.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle.js +11 -6
- package/dist/focus-stage-lifecycle.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline-config.d.ts +13 -0
- package/dist/pipeline-config.d.ts.map +1 -1
- package/dist/pipeline-config.js +15 -0
- package/dist/pipeline-config.js.map +1 -1
- package/dist/resolvers/agent-escalations.d.ts +8 -1
- package/dist/resolvers/agent-escalations.d.ts.map +1 -1
- package/dist/resolvers/agent-escalations.js +25 -22
- package/dist/resolvers/agent-escalations.js.map +1 -1
- package/dist/resolvers/agent-session-summaries.d.ts +8 -1
- package/dist/resolvers/agent-session-summaries.d.ts.map +1 -1
- package/dist/resolvers/agent-session-summaries.js +21 -18
- package/dist/resolvers/agent-session-summaries.js.map +1 -1
- package/dist/resolvers/delivery-acceptance-criteria.d.ts +7 -1
- package/dist/resolvers/delivery-acceptance-criteria.d.ts.map +1 -1
- package/dist/resolvers/delivery-acceptance-criteria.js +14 -11
- package/dist/resolvers/delivery-acceptance-criteria.js.map +1 -1
- package/dist/resolvers/delivery-description.d.ts +7 -1
- package/dist/resolvers/delivery-description.d.ts.map +1 -1
- package/dist/resolvers/delivery-description.js +14 -11
- package/dist/resolvers/delivery-description.js.map +1 -1
- package/dist/resolvers/delivery-issues.d.ts +8 -1
- package/dist/resolvers/delivery-issues.d.ts.map +1 -1
- package/dist/resolvers/delivery-issues.js +51 -48
- package/dist/resolvers/delivery-issues.js.map +1 -1
- package/dist/resolvers/delivery-listing.d.ts +16 -1
- package/dist/resolvers/delivery-listing.d.ts.map +1 -1
- package/dist/resolvers/delivery-listing.js +36 -33
- package/dist/resolvers/delivery-listing.js.map +1 -1
- package/dist/resolvers/delivery-tech-context.d.ts +7 -1
- package/dist/resolvers/delivery-tech-context.d.ts.map +1 -1
- package/dist/resolvers/delivery-tech-context.js +14 -11
- package/dist/resolvers/delivery-tech-context.js.map +1 -1
- package/dist/resolvers/deployment-profile.d.ts +8 -1
- package/dist/resolvers/deployment-profile.d.ts.map +1 -1
- package/dist/resolvers/deployment-profile.js +20 -17
- package/dist/resolvers/deployment-profile.js.map +1 -1
- package/dist/resolvers/focus-anchoring-injections.d.ts +26 -1
- package/dist/resolvers/focus-anchoring-injections.d.ts.map +1 -1
- package/dist/resolvers/focus-anchoring-injections.js +91 -88
- package/dist/resolvers/focus-anchoring-injections.js.map +1 -1
- package/dist/resolvers/focus-context.d.ts +9 -1
- package/dist/resolvers/focus-context.d.ts.map +1 -1
- package/dist/resolvers/focus-context.js +38 -35
- package/dist/resolvers/focus-context.js.map +1 -1
- package/dist/resolvers/focus-injections.d.ts +13 -1
- package/dist/resolvers/focus-injections.d.ts.map +1 -1
- package/dist/resolvers/focus-injections.js +60 -57
- package/dist/resolvers/focus-injections.js.map +1 -1
- package/dist/resolvers/focus-last-review-report.d.ts +2 -1
- package/dist/resolvers/focus-last-review-report.d.ts.map +1 -1
- package/dist/resolvers/focus-last-review-report.js +33 -30
- package/dist/resolvers/focus-last-review-report.js.map +1 -1
- package/dist/resolvers/focus-reality-tree.d.ts +15 -1
- package/dist/resolvers/focus-reality-tree.d.ts.map +1 -1
- package/dist/resolvers/focus-reality-tree.js +35 -32
- package/dist/resolvers/focus-reality-tree.js.map +1 -1
- package/dist/resolvers/git-diff-against-base.d.ts +8 -1
- package/dist/resolvers/git-diff-against-base.d.ts.map +1 -1
- package/dist/resolvers/git-diff-against-base.js +33 -30
- package/dist/resolvers/git-diff-against-base.js.map +1 -1
- package/dist/resolvers/guards-evaluation-results.d.ts +7 -1
- package/dist/resolvers/guards-evaluation-results.d.ts.map +1 -1
- package/dist/resolvers/guards-evaluation-results.js +25 -22
- package/dist/resolvers/guards-evaluation-results.js.map +1 -1
- package/dist/resolvers/index.d.ts +22 -40
- package/dist/resolvers/index.d.ts.map +1 -1
- package/dist/resolvers/index.js +112 -41
- package/dist/resolvers/index.js.map +1 -1
- package/dist/resolvers/loop-context.d.ts +18 -1
- package/dist/resolvers/loop-context.d.ts.map +1 -1
- package/dist/resolvers/loop-context.js +21 -18
- package/dist/resolvers/loop-context.js.map +1 -1
- package/dist/resolvers/loop-delivery-index.d.ts +17 -1
- package/dist/resolvers/loop-delivery-index.d.ts.map +1 -1
- package/dist/resolvers/loop-delivery-index.js +51 -48
- package/dist/resolvers/loop-delivery-index.js.map +1 -1
- package/dist/resolvers/loop-documents.d.ts +9 -1
- package/dist/resolvers/loop-documents.d.ts.map +1 -1
- package/dist/resolvers/loop-documents.js +22 -19
- package/dist/resolvers/loop-documents.js.map +1 -1
- package/dist/resolvers/loop-expected-effects.d.ts +11 -1
- package/dist/resolvers/loop-expected-effects.d.ts.map +1 -1
- package/dist/resolvers/loop-expected-effects.js +53 -50
- package/dist/resolvers/loop-expected-effects.js.map +1 -1
- package/dist/resolvers/loop-frt-statement.d.ts +11 -1
- package/dist/resolvers/loop-frt-statement.d.ts.map +1 -1
- package/dist/resolvers/loop-frt-statement.js +28 -25
- package/dist/resolvers/loop-frt-statement.js.map +1 -1
- package/dist/resolvers/loop-injection.d.ts +10 -1
- package/dist/resolvers/loop-injection.d.ts.map +1 -1
- package/dist/resolvers/loop-injection.js +38 -35
- package/dist/resolvers/loop-injection.js.map +1 -1
- package/dist/resolvers/loop-persona.d.ts +20 -1
- package/dist/resolvers/loop-persona.d.ts.map +1 -1
- package/dist/resolvers/loop-persona.js +20 -17
- package/dist/resolvers/loop-persona.js.map +1 -1
- package/dist/resolvers/loop-questions.d.ts +8 -1
- package/dist/resolvers/loop-questions.d.ts.map +1 -1
- package/dist/resolvers/loop-questions.js +39 -36
- package/dist/resolvers/loop-questions.js.map +1 -1
- package/dist/resolvers/loop-upstream-udes.d.ts +11 -1
- package/dist/resolvers/loop-upstream-udes.d.ts.map +1 -1
- package/dist/resolvers/loop-upstream-udes.js +74 -71
- package/dist/resolvers/loop-upstream-udes.js.map +1 -1
- package/dist/resolvers/prd-context.d.ts +2 -0
- package/dist/resolvers/prd-context.d.ts.map +1 -1
- package/dist/resolvers/prd-context.js +16 -13
- package/dist/resolvers/prd-context.js.map +1 -1
- package/dist/resolvers/prd-persona.d.ts +2 -0
- package/dist/resolvers/prd-persona.d.ts.map +1 -1
- package/dist/resolvers/prd-persona.js +6 -3
- package/dist/resolvers/prd-persona.js.map +1 -1
- package/dist/resolvers/reality-metrics.d.ts +8 -1
- package/dist/resolvers/reality-metrics.d.ts.map +1 -1
- package/dist/resolvers/reality-metrics.js +73 -70
- package/dist/resolvers/reality-metrics.js.map +1 -1
- package/dist/resolvers/reality-projections.d.ts +7 -1
- package/dist/resolvers/reality-projections.d.ts.map +1 -1
- package/dist/resolvers/reality-projections.js +35 -32
- package/dist/resolvers/reality-projections.js.map +1 -1
- package/dist/resolvers/reality-tree-snapshot.d.ts +10 -1
- package/dist/resolvers/reality-tree-snapshot.d.ts.map +1 -1
- package/dist/resolvers/reality-tree-snapshot.js +34 -31
- package/dist/resolvers/reality-tree-snapshot.js.map +1 -1
- package/dist/resolvers/retired-injections.d.ts +10 -1
- package/dist/resolvers/retired-injections.d.ts.map +1 -1
- package/dist/resolvers/retired-injections.js +68 -65
- package/dist/resolvers/retired-injections.js.map +1 -1
- package/dist/resolvers/review-outcomes.d.ts +11 -1
- package/dist/resolvers/review-outcomes.d.ts.map +1 -1
- package/dist/resolvers/review-outcomes.js +27 -24
- package/dist/resolvers/review-outcomes.js.map +1 -1
- package/dist/resolvers/security-advisory.d.ts +2 -1
- package/dist/resolvers/security-advisory.d.ts.map +1 -1
- package/dist/resolvers/security-advisory.js +47 -44
- package/dist/resolvers/security-advisory.js.map +1 -1
- package/dist/resolvers/wiki-search.d.ts +8 -1
- package/dist/resolvers/wiki-search.d.ts.map +1 -1
- package/dist/resolvers/wiki-search.js +17 -14
- package/dist/resolvers/wiki-search.js.map +1 -1
- package/dist/resolvers/wiki-topic.d.ts +9 -1
- package/dist/resolvers/wiki-topic.d.ts.map +1 -1
- package/dist/resolvers/wiki-topic.js +21 -18
- package/dist/resolvers/wiki-topic.js.map +1 -1
- package/dist/resolvers/workflow-stages.d.ts +7 -1
- package/dist/resolvers/workflow-stages.d.ts.map +1 -1
- package/dist/resolvers/workflow-stages.js +20 -17
- package/dist/resolvers/workflow-stages.js.map +1 -1
- package/dist/self-update.d.ts +6 -0
- package/dist/self-update.d.ts.map +1 -1
- package/dist/self-update.js +6 -1
- package/dist/self-update.js.map +1 -1
- package/dist/spawner/index.d.ts +2 -1
- package/dist/spawner/index.d.ts.map +1 -1
- package/dist/spawner/index.js +2 -1
- package/dist/spawner/index.js.map +1 -1
- package/dist/spawner/spawn-team.d.ts +14 -52
- package/dist/spawner/spawn-team.d.ts.map +1 -1
- package/dist/spawner/spawn-team.js +14 -469
- package/dist/spawner/spawn-team.js.map +1 -1
- package/dist/staleness.d.ts +124 -0
- package/dist/staleness.d.ts.map +1 -0
- package/dist/staleness.js +215 -0
- package/dist/staleness.js.map +1 -0
- package/dist/state-cascade.d.ts +3 -2
- package/dist/state-cascade.d.ts.map +1 -1
- package/dist/state-cascade.js +7 -3
- package/dist/state-cascade.js.map +1 -1
- package/dist/types/focus.d.ts +5 -4
- package/dist/types/focus.d.ts.map +1 -1
- package/dist/types/session.d.ts +2 -3
- package/dist/types/session.d.ts.map +1 -1
- package/dist/unified-shell.d.ts.map +1 -1
- package/dist/unified-shell.js +21 -12
- package/dist/unified-shell.js.map +1 -1
- package/package.json +2 -2
- package/dist/session-lifecycle.d.ts +0 -78
- package/dist/session-lifecycle.d.ts.map +0 -1
- package/dist/session-lifecycle.js +0 -382
- package/dist/session-lifecycle.js.map +0 -1
|
@@ -1,401 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Team completion -- explicit phase sequencer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* "What happens when a team exits" reads from handleTeamCompletion below:
|
|
5
|
+
* an ordered sequence of named phases. The phase implementations live in
|
|
6
|
+
* focused modules, re-exported here so existing importers are unaffected:
|
|
7
|
+
*
|
|
8
|
+
* - exit-classification.ts classify exit (success / controlled stop / crash)
|
|
9
|
+
* - merge-phase.ts merge gate + focus->integration merge +
|
|
10
|
+
* merge-failure escalation + CI integration->main
|
|
11
|
+
* - status-advance-phase.ts delivery status advancement from issue completion
|
|
12
|
+
* - review-exit-phase.ts verify-delivery routing on review exit
|
|
13
|
+
*
|
|
14
|
+
* Phase order on team exit:
|
|
15
|
+
* classify -> report git state -> read-only guard -> WIP preserve ->
|
|
16
|
+
* status advance -> merge -> CI merge-to-main -> session finalize ->
|
|
17
|
+
* review exit (escalate/handoff) -> terminate (cleanup + re-poll)
|
|
7
18
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { hasPendingSpawnDirective } from '../directive-executor.js';
|
|
11
|
-
import { advanceDeliveryStage, ensureWorktreeCommittedBeforeAdvance } from '../delivery-lifecycle.js';
|
|
12
|
-
import { withRetry, ESCALATION_REASONS, DELIVERY_STATUS } from '@telora/daemon-core';
|
|
13
|
-
import { mergeFocusBranch, escalateMergeConflict } from '../focus-merge.js';
|
|
14
|
-
import { mergeIntegrationToMain } from '../git-merge.js';
|
|
15
|
-
import { resolveCiDecision } from '../pipeline-config.js';
|
|
16
|
-
import { fileCdPushEscalation, shouldFileCdPushEscalation } from '../ci-escalation.js';
|
|
19
|
+
import { updateSession, reportGitState } from '../supabase.js';
|
|
20
|
+
import { withRetry, DELIVERY_STATUS } from '@telora/daemon-core';
|
|
17
21
|
import { recordSessionCompleted } from '../agent-state.js';
|
|
18
22
|
import { triggerCheck } from '../listener.js';
|
|
19
|
-
import { emitLoopTrigger } from '../loop-event-bus.js';
|
|
20
23
|
import { getActiveTeams } from '../focus-team-state.js';
|
|
21
24
|
import { commitWipChanges, worktreeHasUncommittedChanges, branchHasUnmergedCommits, runGitSync } from '../git.js';
|
|
22
25
|
import { computeGitDiffStats } from '../git-utils.js';
|
|
23
|
-
import { getFocusDeliveries
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
import {
|
|
26
|
+
import { getFocusDeliveries } from '../queries/focuses.js';
|
|
27
|
+
import { classifyTeamExit, deriveExitCategory } from './exit-classification.js';
|
|
28
|
+
import { selectWorkedDeliveryIds, attemptFocusMerge, escalateFailedMergeDeliveries, runCiMergeToMain, } from './merge-phase.js';
|
|
29
|
+
import { advanceDeliveryStatuses } from './status-advance-phase.js';
|
|
30
|
+
import { runReviewExitHandler } from './review-exit-phase.js';
|
|
31
|
+
// ── Re-exported public surface (callers unchanged) ───────────────────
|
|
28
32
|
export { isStatusTerminal, isStatusAgentActionable } from '../stage-classifier.js';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* Skip merge when:
|
|
39
|
-
* - The team is still in its planning phase (cancellation mid-planning --
|
|
40
|
-
* nothing scoped, nothing to merge), OR
|
|
41
|
-
* - The team is a read-only audit session (read-only violation; commits, if
|
|
42
|
-
* any, are surfaced separately by the existing readOnly guard), OR
|
|
43
|
-
* - The branch has no committed work ahead of integration (clean exit with
|
|
44
|
-
* no commits, e.g. audit team or planning-only team).
|
|
45
|
-
*
|
|
46
|
-
* Exported for unit testing the gate logic in isolation.
|
|
47
|
-
*/
|
|
48
|
-
export function shouldAttemptMerge(teamState, branchHasCommits) {
|
|
49
|
-
if (teamState.planningPhase)
|
|
50
|
-
return false;
|
|
51
|
-
if (teamState.readOnly)
|
|
52
|
-
return false;
|
|
53
|
-
return branchHasCommits;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Statuses for deliveries whose git state should be reported on team
|
|
57
|
-
* completion. Includes 'done' so a focus whose deliveries all reached done
|
|
58
|
-
* before the branch was merged (review SIGTERM, role-clear, deferred merge,
|
|
59
|
-
* etc.) still attempts the merge instead of being short-circuited as
|
|
60
|
-
* "no worked deliveries". Excludes 'queued'/'planning'/'paused'/'cancelled'
|
|
61
|
-
* — those represent work that never started or was abandoned.
|
|
62
|
-
*
|
|
63
|
-
* Exported for unit testing.
|
|
64
|
-
*/
|
|
65
|
-
export const REPORTABLE_DELIVERY_STATUSES = new Set([
|
|
66
|
-
'coding',
|
|
67
|
-
'verify',
|
|
68
|
-
'done',
|
|
69
|
-
]);
|
|
70
|
-
/**
|
|
71
|
-
* Filter delivery list to those whose git state should be reported on team
|
|
72
|
-
* completion. Excludes already-merged deliveries so we don't re-report them.
|
|
73
|
-
*
|
|
74
|
-
* Exported for unit testing the filter logic without invoking handleTeamCompletion.
|
|
75
|
-
*/
|
|
76
|
-
export function selectWorkedDeliveryIds(deliveries, alreadyMerged) {
|
|
77
|
-
return deliveries
|
|
78
|
-
.filter(d => REPORTABLE_DELIVERY_STATUSES.has(d.executionStatus ?? '') && !alreadyMerged.has(d.id))
|
|
79
|
-
.map(d => d.id);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Decide whether to merge the focus branch and, if so, run the merge.
|
|
83
|
-
*
|
|
84
|
-
* This is the integration of three separate concerns that the merge gate
|
|
85
|
-
* conflated before:
|
|
86
|
-
* 1. Branch state: does the focus branch actually have committed work?
|
|
87
|
-
* 2. Team gate: planningPhase + readOnly safety nets in shouldAttemptMerge.
|
|
88
|
-
* 3. Per-delivery telemetry: workedDeliveryIds is passed through for
|
|
89
|
-
* git-state reporting inside mergeFocusBranch.
|
|
90
|
-
*
|
|
91
|
-
* Returns attempted=false when the gate skips the merge (clean exit with
|
|
92
|
-
* nothing to merge, planning-phase, or read-only). Returns attempted=true
|
|
93
|
-
* with the merge result otherwise.
|
|
94
|
-
*
|
|
95
|
-
* Exported so tests can verify the threading from done-only deliveries +
|
|
96
|
-
* branch-with-commits to a real mergeFocusBranch invocation, without having
|
|
97
|
-
* to mock all of handleTeamCompletion's other side effects.
|
|
98
|
-
*/
|
|
99
|
-
export async function attemptFocusMerge(config, teamState, sessionId, workedDeliveryIds, deps = {
|
|
100
|
-
branchHasUnmergedCommits,
|
|
101
|
-
mergeFocusBranch,
|
|
102
|
-
}) {
|
|
103
|
-
const branchHasCommits = deps.branchHasUnmergedCommits(teamState.branchName, config.integrationBranch, config.repoPath);
|
|
104
|
-
if (!shouldAttemptMerge(teamState, branchHasCommits)) {
|
|
105
|
-
return { attempted: false, mergeSucceeded: false, exitReason: null };
|
|
106
|
-
}
|
|
107
|
-
const mergeResult = await deps.mergeFocusBranch(config, teamState, sessionId, teamState.branchName, workedDeliveryIds);
|
|
108
|
-
return {
|
|
109
|
-
attempted: true,
|
|
110
|
-
mergeSucceeded: mergeResult.mergeSucceeded,
|
|
111
|
-
exitReason: mergeResult.exitReason ?? null,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
const defaultReviewExitDeps = {
|
|
115
|
-
getActiveFocuses,
|
|
116
|
-
getFocusDeliveries,
|
|
117
|
-
getFocusIssues,
|
|
118
|
-
getDeliveryIssues,
|
|
119
|
-
fetchEffectiveWorkflow,
|
|
120
|
-
updateDeliveryStatus,
|
|
121
|
-
markDeliveryAutoApproved,
|
|
122
|
-
clearReviewRequestedAt,
|
|
123
|
-
hasPendingSpawnDirective,
|
|
124
|
-
emitLoopTrigger,
|
|
125
|
-
getFocusReviewReportForSession,
|
|
126
|
-
createEscalation,
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Handle a team exit when the focus has review_requested_at set.
|
|
130
|
-
*
|
|
131
|
-
* Routing is asymmetric in the downward direction (toward intent) -- open
|
|
132
|
-
* work on a verify delivery means the work isn't done, so it routes to
|
|
133
|
-
* verify_failed unconditionally. Closing a verify delivery to `done` is
|
|
134
|
-
* the other direction; it now happens via two paths: an explicit
|
|
135
|
-
* focus_reviews approval, OR a clean review session exit (auto-approve,
|
|
136
|
-
* reversible via the later unapprove affordance).
|
|
137
|
-
*
|
|
138
|
-
* Routing rules:
|
|
139
|
-
* 1. No review_requested_at -> nothing to do.
|
|
140
|
-
* 2. A spawn directive is pending -> defer to the incoming team.
|
|
141
|
-
* 3. For each verify delivery:
|
|
142
|
-
* - open work issues -> verify_failed (always, no evidence gate)
|
|
143
|
-
* - no open work + reportExists -> done (explicit approval)
|
|
144
|
-
* - no open work + no report + review session succeeded ->
|
|
145
|
-
* done (auto-approve; tagged with auto_approved_at /
|
|
146
|
-
* auto_approved_by_session for surfacing + unapprove)
|
|
147
|
-
* - no open work + non-review session (or failed review) ->
|
|
148
|
-
* leave in verify (next auto-review poll re-triggers)
|
|
149
|
-
* 4. If anything routed -> clear review_requested_at + emit
|
|
150
|
-
* review_completed event so phase re-derives on the next poll.
|
|
151
|
-
* 5. If routed via issue evidence (no report present), file a soft
|
|
152
|
-
* review_missing_report notice -- the loop continues regardless.
|
|
153
|
-
* 6. If nothing routed AND no evidence at all -> warn-and-leave. The
|
|
154
|
-
* strict gate no longer escalates: an idle focus with no work and no
|
|
155
|
-
* report is ambiguous, not failed; humans clear it via the MCP
|
|
156
|
-
* clear-review-request path.
|
|
157
|
-
*
|
|
158
|
-
* The independent "evidence" signals (focus_reviews row + review-filed
|
|
159
|
-
* issues) still feed into the issue-evidence notice and the warn-and-leave
|
|
160
|
-
* messaging, but they no longer gate routing -- open work on a delivery
|
|
161
|
-
* is its own evidence that the work isn't done.
|
|
162
|
-
*
|
|
163
|
-
* Exported with injectable deps for unit testing.
|
|
164
|
-
*/
|
|
165
|
-
export async function runReviewExitHandler(params, deps = defaultReviewExitDeps) {
|
|
166
|
-
const { focusId, focusName, organizationId, productId, sessionId, sessionType, succeeded } = params;
|
|
167
|
-
const focuses = await deps.getActiveFocuses(organizationId, productId);
|
|
168
|
-
const current = focuses.find(s => s.focus_id === focusId);
|
|
169
|
-
if (!current?.review_requested_at) {
|
|
170
|
-
// Nothing review-related to do.
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const reviewRequestedAt = current.review_requested_at;
|
|
174
|
-
// NOTE: a pending spawn directive no longer short-circuits the handler.
|
|
175
|
-
// Delivery finalization (auto-approve verify->done, done-via-report, and
|
|
176
|
-
// verify_failed on open work) plus clearing review_requested_at and emitting
|
|
177
|
-
// review_completed must run unconditionally on a session exit -- otherwise a
|
|
178
|
-
// transiently-pending spawn strands the verify delivery and feeds the
|
|
179
|
-
// per-poll re-fire spin. The pending-spawn signal is consulted ONLY in the
|
|
180
|
-
// non-routed branch below, where leaving the flag set for an incoming team is
|
|
181
|
-
// the legitimate behavior.
|
|
182
|
-
// ── Gather evidence ──────────────────────────────────────────────
|
|
183
|
-
let reportExists = false;
|
|
184
|
-
try {
|
|
185
|
-
const report = await deps.getFocusReviewReportForSession(focusId, sessionId);
|
|
186
|
-
reportExists = report.exists;
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
console.warn(`[focus-executor] getFocusReviewReportForSession failed for "${focusName}" session ${sessionId}: ${err.message}`);
|
|
190
|
-
// Treat read failure the same as missing.
|
|
191
|
-
}
|
|
192
|
-
let hasIssueEvidence = false;
|
|
193
|
-
try {
|
|
194
|
-
const focusIssues = await deps.getFocusIssues(focusId);
|
|
195
|
-
hasIssueEvidence = reviewFiledIssueExists(focusIssues, reviewRequestedAt);
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
console.warn(`[focus-executor] getFocusIssues failed for "${focusName}": ${err.message}`);
|
|
199
|
-
// Treat read failure as no evidence -- only affects the warn-and-leave
|
|
200
|
-
// message and the issue-evidence notice; routing runs regardless.
|
|
201
|
-
}
|
|
202
|
-
// ── Route verify deliveries ──────────────────────────────────────
|
|
203
|
-
// Runs unconditionally: open work routes to verify_failed even when no
|
|
204
|
-
// review evidence is present. This is the self-healing path -- a coding
|
|
205
|
-
// team that left pre-existing open work behind no longer needs the review
|
|
206
|
-
// agent to file fresh defects before the loop can correct itself.
|
|
207
|
-
const reviewDeliveries = await deps.getFocusDeliveries(focusId);
|
|
208
|
-
let routedAny = false;
|
|
209
|
-
for (const d of reviewDeliveries) {
|
|
210
|
-
if (d.executionStatus !== DELIVERY_STATUS.VERIFY)
|
|
211
|
-
continue;
|
|
212
|
-
try {
|
|
213
|
-
const [issues, dWorkflow] = await Promise.all([
|
|
214
|
-
deps.getDeliveryIssues(d.id),
|
|
215
|
-
deps.fetchEffectiveWorkflow(d.id),
|
|
216
|
-
]);
|
|
217
|
-
const workIssues = filterWorkIssues(issues);
|
|
218
|
-
const openWork = workIssues.filter(i => ACTIVE_WORK_STATUSES.has(i.status));
|
|
219
|
-
if (openWork.length > 0) {
|
|
220
|
-
const awaitingVerifyStage = dWorkflow.stages.find(s => s.name === 'verify_failed');
|
|
221
|
-
await deps.updateDeliveryStatus(d.id, 'verify_failed', awaitingVerifyStage?.id ?? null, undefined, undefined, { organizationId, fromStatus: 'verify' });
|
|
222
|
-
console.log(`[focus-executor] Delivery "${d.name}" moved from verify to verify_failed: ${openWork.length} open issue(s)`);
|
|
223
|
-
routedAny = true;
|
|
224
|
-
}
|
|
225
|
-
else if (reportExists) {
|
|
226
|
-
const doneStage = dWorkflow.stages.find(s => s.name === 'done');
|
|
227
|
-
await deps.updateDeliveryStatus(d.id, 'done', doneStage?.id ?? null, undefined, undefined, { organizationId, fromStatus: 'verify' });
|
|
228
|
-
console.log(`[focus-executor] Delivery "${d.name}" moved from verify to done (review passed)`);
|
|
229
|
-
routedAny = true;
|
|
230
|
-
}
|
|
231
|
-
else if (sessionType === 'review' && succeeded) {
|
|
232
|
-
// Clean review session: review agent exited without filing defects
|
|
233
|
-
// and without writing a focus_reviews row. Per the auto-approve-
|
|
234
|
-
// with-visibility direction (memory/feedback_auto_approve_with_visibility),
|
|
235
|
-
// promote verify -> done and tag the delivery with auto-approval
|
|
236
|
-
// metadata so the (later) dashboard/MCP unapprove path can surface
|
|
237
|
-
// it for human reversal. Reversible by construction.
|
|
238
|
-
const doneStage = dWorkflow.stages.find(s => s.name === 'done');
|
|
239
|
-
await deps.markDeliveryAutoApproved(d.id, sessionId, doneStage?.id ?? null);
|
|
240
|
-
console.log(`[focus-executor] Delivery "${d.name}" auto-approved verify -> done ` +
|
|
241
|
-
`(clean review session ${sessionId.slice(0, 8)}, no defects filed)`);
|
|
242
|
-
routedAny = true;
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
// Non-review session exit, or review session that didn't succeed --
|
|
246
|
-
// no signal to auto-approve. Leave in verify; the next state-cascade
|
|
247
|
-
// poll will re-trigger auto-review via checkAutoReview.
|
|
248
|
-
const reason = sessionType !== 'review'
|
|
249
|
-
? `${sessionType} session exited (not eligible for auto-approve)`
|
|
250
|
-
: 'review session did not succeed';
|
|
251
|
-
console.log(`[focus-executor] Delivery "${d.name}" left in verify: ${reason} ` +
|
|
252
|
-
`(verify-cycling; next auto-review poll will re-trigger).`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
console.warn(`[focus-executor] Failed to route delivery "${d.name}" after review: ${err.message}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (routedAny) {
|
|
260
|
-
await deps.clearReviewRequestedAt(focusId);
|
|
261
|
-
console.log(`[focus-executor] Review complete for "${focusName}" -- review_requested_at cleared ` +
|
|
262
|
-
`(reportExists=${reportExists}, issueEvidence=${hasIssueEvidence})`);
|
|
263
|
-
const routeVia = reportExists
|
|
264
|
-
? 'report'
|
|
265
|
-
: hasIssueEvidence
|
|
266
|
-
? 'issue-evidence'
|
|
267
|
-
: sessionType === 'review' && succeeded
|
|
268
|
-
? 'auto-approve'
|
|
269
|
-
: 'open-work';
|
|
270
|
-
deps.emitLoopTrigger({
|
|
271
|
-
type: 'review_completed',
|
|
272
|
-
focusId,
|
|
273
|
-
detail: `review complete -- "${focusName}" routed via ${routeVia}`,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
else if (deps.hasPendingSpawnDirective(focusId)) {
|
|
277
|
-
// Nothing routed AND a spawn directive is pending: the team was torn down
|
|
278
|
-
// to make room for an incoming team (e.g., the directive executor spawning
|
|
279
|
-
// a review team). Leave review_requested_at set so the incoming team's exit
|
|
280
|
-
// performs the routing. This is the ONLY case the pending-spawn signal
|
|
281
|
-
// gates -- it never blocks the finalization above.
|
|
282
|
-
console.log(`[focus-executor] "${focusName}" review exit routed nothing and a spawn directive ` +
|
|
283
|
-
`is pending -- leaving review_requested_at set for the incoming team.`);
|
|
284
|
-
}
|
|
285
|
-
else if (!reportExists && !hasIssueEvidence) {
|
|
286
|
-
// Strict gate: no routing happened AND no evidence. Warn-and-leave --
|
|
287
|
-
// the focus may simply be idle (no verify deliveries to route, no
|
|
288
|
-
// review report) or awaiting human handling. We deliberately no longer
|
|
289
|
-
// escalate here: an idle focus with no report is not a missing report,
|
|
290
|
-
// it's an idle focus. Recovery is via MCP clear-review-request.
|
|
291
|
-
const reason = sessionType !== 'review'
|
|
292
|
-
? `${sessionType} team exited (not a review team)`
|
|
293
|
-
: !succeeded
|
|
294
|
-
? 'review team did not succeed'
|
|
295
|
-
: 'no verify deliveries to route (clean review sessions auto-approve verify deliveries; this focus had none)';
|
|
296
|
-
console.warn(`[focus-executor] "${focusName}" has review_requested_at set but ${reason}; ` +
|
|
297
|
-
`phase remains 'reviewing' until a review team runs, work is queued, ` +
|
|
298
|
-
`or review_requested_at is cleared via MCP.`);
|
|
299
|
-
}
|
|
300
|
-
// Surface that the agent skipped review_complete even though the loop healed.
|
|
301
|
-
// Only fire when we actually routed something via issue evidence; otherwise
|
|
302
|
-
// the notice misleads (it claims the loop continued when nothing moved).
|
|
303
|
-
if (routedAny && !reportExists && hasIssueEvidence) {
|
|
304
|
-
try {
|
|
305
|
-
await deps.createEscalation({
|
|
306
|
-
organizationId,
|
|
307
|
-
sessionId,
|
|
308
|
-
issueId: null,
|
|
309
|
-
reasonType: ESCALATION_REASONS.BLOCKED_BY_EXTERNAL,
|
|
310
|
-
description: 'Review session filed issues but did not call focus_review_complete. ' +
|
|
311
|
-
'Deliveries routed via issue evidence; review_requested_at cleared. ' +
|
|
312
|
-
'Tighten the review directive so the agent always stamps an outcome.',
|
|
313
|
-
escalationKind: 'review_missing_report',
|
|
314
|
-
metadata: { focus_id: focusId, session_id: sessionId, routed_via: 'issue_evidence' },
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
catch (err) {
|
|
318
|
-
console.warn(`[focus-executor] createEscalation (issue-evidence notice) failed for "${focusName}": ${err.message}`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Classify how a team lead process exited.
|
|
324
|
-
*
|
|
325
|
-
* Pure and side-effect free so the "lifecycle SIGTERM is not a review failure"
|
|
326
|
-
* invariant is unit-testable in isolation. The crux: when the daemon proactively
|
|
327
|
-
* terminates a team because its work is complete, it sets phase='shutting_down'
|
|
328
|
-
* and shutdownReason='work_complete' BEFORE sending SIGTERM. We detect that
|
|
329
|
-
* teardown and classify it as succeeded -- otherwise a clean review team
|
|
330
|
-
* (which commits nothing of its own, then gets SIGTERMed once all deliveries
|
|
331
|
-
* are terminal) would look like a failed exit and its verify delivery would be
|
|
332
|
-
* stranded in `verify` instead of auto-approved to `done`.
|
|
333
|
-
*/
|
|
334
|
-
export function classifyTeamExit(params) {
|
|
335
|
-
const exitCode = params.code ?? 1;
|
|
336
|
-
const wasSigterm = params.signal === 'SIGTERM' || exitCode === 143;
|
|
337
|
-
const proactivelyTerminated = params.teamPhase === 'shutting_down' &&
|
|
338
|
-
wasSigterm &&
|
|
339
|
-
params.shutdownReason === 'work_complete';
|
|
340
|
-
const succeeded = exitCode === 0 || proactivelyTerminated;
|
|
341
|
-
// A controlled stop is any SIGTERM the daemon sent while the team was already
|
|
342
|
-
// shutting down -- regardless of reason. It is the signal that distinguishes
|
|
343
|
-
// a deliberate daemon teardown from a crash, without touching `succeeded`.
|
|
344
|
-
const controlledStop = params.teamPhase === 'shutting_down' && wasSigterm;
|
|
345
|
-
const disposition = succeeded ? 'completed' : controlledStop ? 'stopped' : 'failed';
|
|
346
|
-
return { exitCode, wasSigterm, proactivelyTerminated, succeeded, controlledStop, disposition };
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Derive the `exit_category` to record alongside the session status, given the
|
|
350
|
-
* classifier disposition and the team's shutdown reason.
|
|
351
|
-
*
|
|
352
|
-
* - 'completed' -> 'work_complete' when the daemon stopped the team because its
|
|
353
|
-
* work was done; otherwise null (a plain clean self-exit has no category).
|
|
354
|
-
* - 'stopped' -> the specific controlled-stop reason; any unknown/null reason
|
|
355
|
-
* falls back to 'handoff' (a benign daemon-initiated respawn).
|
|
356
|
-
* - 'failed' -> 'error' (genuine crash / unexpected exit).
|
|
357
|
-
*/
|
|
358
|
-
export function deriveExitCategory(disposition, shutdownReason) {
|
|
359
|
-
switch (disposition) {
|
|
360
|
-
case 'completed':
|
|
361
|
-
return shutdownReason === 'work_complete' ? 'work_complete' : null;
|
|
362
|
-
case 'stopped':
|
|
363
|
-
switch (shutdownReason) {
|
|
364
|
-
case 'deactivated': return 'deactivated';
|
|
365
|
-
case 'user_stopped': return 'user_stopped';
|
|
366
|
-
case 'handoff': return 'handoff';
|
|
367
|
-
case 'teardown': return 'teardown';
|
|
368
|
-
default: return 'handoff';
|
|
369
|
-
}
|
|
370
|
-
case 'failed':
|
|
371
|
-
return 'error';
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Handle the team lead process exiting.
|
|
376
|
-
*
|
|
377
|
-
* Responsibilities:
|
|
378
|
-
* - Report git state for all known deliveries
|
|
379
|
-
* - Advance delivery statuses based on issue completion
|
|
380
|
-
* - Merge focus branch to integration (if successful)
|
|
381
|
-
* - Clean up worktree
|
|
382
|
-
* - Update session record
|
|
383
|
-
* - Record heartbeat counters
|
|
384
|
-
* - Trigger re-poll for next work
|
|
385
|
-
*/
|
|
386
|
-
export async function handleTeamCompletion(params) {
|
|
387
|
-
const { config, teamState, sessionId, code, signal } = params;
|
|
388
|
-
const { focusId, focusName, organizationId, branchName } = teamState;
|
|
389
|
-
const worktreePath = teamState.worktreePath;
|
|
390
|
-
// Classify the exit. A lifecycle teardown (shutdownReason='work_complete')
|
|
391
|
-
// SIGTERM is treated as a success, NOT a review failure -- see classifyTeamExit.
|
|
392
|
-
const { exitCode, proactivelyTerminated, succeeded, controlledStop, disposition } = classifyTeamExit({
|
|
393
|
-
code,
|
|
394
|
-
signal,
|
|
395
|
-
teamPhase: teamState.phase,
|
|
396
|
-
shutdownReason: teamState.shutdownReason,
|
|
397
|
-
});
|
|
398
|
-
let exitReason = exitCode === 0
|
|
33
|
+
export { classifyTeamExit, deriveExitCategory, } from './exit-classification.js';
|
|
34
|
+
export { shouldAttemptMerge, REPORTABLE_DELIVERY_STATUSES, selectWorkedDeliveryIds, attemptFocusMerge, escalateFailedMergeDeliveries, runCiMergeToMain, } from './merge-phase.js';
|
|
35
|
+
export { advanceDeliveryStatuses, } from './status-advance-phase.js';
|
|
36
|
+
export { runReviewExitHandler, } from './review-exit-phase.js';
|
|
37
|
+
// ── Phase helpers ─────────────────────────────────────────────────────
|
|
38
|
+
/** Build the initial exit reason from the exit classification. */
|
|
39
|
+
function buildInitialExitReason(teamState, classification, signal) {
|
|
40
|
+
const { exitCode, proactivelyTerminated, succeeded, controlledStop } = classification;
|
|
41
|
+
return exitCode === 0
|
|
399
42
|
? 'Focus team completed.'
|
|
400
43
|
: proactivelyTerminated
|
|
401
44
|
? teamState.mergedDeliveryIds.size > 0
|
|
@@ -408,16 +51,20 @@ export async function handleTeamCompletion(params) {
|
|
|
408
51
|
: signal
|
|
409
52
|
? `Team lead terminated by signal: ${signal}`
|
|
410
53
|
: `Team lead exited with code: ${exitCode}`;
|
|
411
|
-
|
|
412
|
-
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fetch the fresh delivery list (to include mid-flight additions), falling
|
|
57
|
+
* back to a synthesized list from knownDeliveryIds on read failure.
|
|
58
|
+
*/
|
|
59
|
+
async function fetchDeliveriesForCompletion(teamState) {
|
|
413
60
|
try {
|
|
414
|
-
|
|
61
|
+
return await getFocusDeliveries(teamState.focusId);
|
|
415
62
|
}
|
|
416
63
|
catch (err) {
|
|
417
64
|
console.warn(`[focus-executor] Failed to fetch deliveries for gitState reporting, falling back to knownDeliveryIds:`, err.message);
|
|
418
65
|
// Empty knownDeliveryIds (planning teams that exited without scoping) yields
|
|
419
66
|
// an empty fallback array -- subsequent loops are intentional no-ops.
|
|
420
|
-
|
|
67
|
+
return [...teamState.knownDeliveryIds].map(id => ({
|
|
421
68
|
id, name: '', description: null, priorityRank: 0,
|
|
422
69
|
executionStatus: DELIVERY_STATUS.CODING, acceptanceCriteria: null,
|
|
423
70
|
techContext: null, currentWorkflowStageId: null,
|
|
@@ -427,6 +74,85 @@ export async function handleTeamCompletion(params) {
|
|
|
427
74
|
updatedAt: null,
|
|
428
75
|
}));
|
|
429
76
|
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read-only audit safety net: revert any uncommitted modifications, and if
|
|
80
|
+
* the agent somehow committed despite the pre-commit hook, mark the session
|
|
81
|
+
* failed and tear the team down WITHOUT merging.
|
|
82
|
+
*
|
|
83
|
+
* Returns true when the completion flow must abort (violation handled).
|
|
84
|
+
*/
|
|
85
|
+
async function runReadOnlyGuardPhase(config, teamState, sessionId) {
|
|
86
|
+
if (!teamState.readOnly)
|
|
87
|
+
return false;
|
|
88
|
+
const { focusId, focusName, branchName } = teamState;
|
|
89
|
+
const worktreePath = teamState.worktreePath;
|
|
90
|
+
// Revert any uncommitted changes
|
|
91
|
+
const hasChanges = worktreeHasUncommittedChanges(worktreePath);
|
|
92
|
+
if (hasChanges) {
|
|
93
|
+
console.warn(`[focus-executor] Read-only audit "${focusName}" left uncommitted changes. Reverting.`);
|
|
94
|
+
runGitSync(['checkout', '.'], worktreePath);
|
|
95
|
+
runGitSync(['clean', '-fd'], worktreePath);
|
|
96
|
+
}
|
|
97
|
+
// Check if the agent somehow committed (bypassed hook)
|
|
98
|
+
if (branchHasUnmergedCommits(branchName, config.integrationBranch, config.repoPath)) {
|
|
99
|
+
console.error(`[focus-executor] READ-ONLY VIOLATION: Audit "${focusName}" has commits on ${branchName}. ` +
|
|
100
|
+
`These will NOT be merged. Manual review required.`);
|
|
101
|
+
const exitReason = `Read-only audit violation: commits detected on ${branchName}. Merge skipped.`;
|
|
102
|
+
// Update session as failed due to read-only violation
|
|
103
|
+
try {
|
|
104
|
+
await withRetry(() => updateSession(sessionId, {
|
|
105
|
+
status: 'failed',
|
|
106
|
+
exit_reason: exitReason,
|
|
107
|
+
exit_category: 'error',
|
|
108
|
+
ended_at: new Date().toISOString(),
|
|
109
|
+
}), { maxAttempts: 3, baseDelayMs: 1000, label: 'session-update-readonly-violation' });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.warn(`[focus-executor] Failed to update session after retries:`, err.message);
|
|
113
|
+
}
|
|
114
|
+
// Record heartbeat and clean up
|
|
115
|
+
recordSessionCompleted(0, 0);
|
|
116
|
+
const activeTeams = getActiveTeams();
|
|
117
|
+
teamState.phase = 'terminated';
|
|
118
|
+
activeTeams.delete(focusId);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
// ── The phase sequencer ───────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Handle the team lead process exiting.
|
|
126
|
+
*
|
|
127
|
+
* Responsibilities (in phase order):
|
|
128
|
+
* - Classify the exit (lifecycle teardown vs crash -- see classifyTeamExit)
|
|
129
|
+
* - Report git state for all known deliveries
|
|
130
|
+
* - Read-only audit guard (revert / block merge on violation)
|
|
131
|
+
* - Preserve WIP commits on unclean exits
|
|
132
|
+
* - Advance delivery statuses based on issue completion
|
|
133
|
+
* - Merge focus branch to integration + escalate per-delivery merge failures
|
|
134
|
+
* - CI: merge integration -> main (CD optionally pushes)
|
|
135
|
+
* - Finalize the session record (status, exit reason, diff stats)
|
|
136
|
+
* - Review exit routing (escalate / handoff -- see runReviewExitHandler)
|
|
137
|
+
* - Terminate: heartbeat, team-state cleanup, re-poll trigger
|
|
138
|
+
*/
|
|
139
|
+
export async function handleTeamCompletion(params) {
|
|
140
|
+
const { config, teamState, sessionId, code, signal } = params;
|
|
141
|
+
const { focusId, focusName, branchName } = teamState;
|
|
142
|
+
const worktreePath = teamState.worktreePath;
|
|
143
|
+
// ── Phase: classify exit ────────────────────────────────────────────
|
|
144
|
+
// A lifecycle teardown (shutdownReason='work_complete') SIGTERM is treated
|
|
145
|
+
// as a success, NOT a review failure -- see classifyTeamExit.
|
|
146
|
+
const classification = classifyTeamExit({
|
|
147
|
+
code,
|
|
148
|
+
signal,
|
|
149
|
+
teamPhase: teamState.phase,
|
|
150
|
+
shutdownReason: teamState.shutdownReason,
|
|
151
|
+
});
|
|
152
|
+
const { succeeded, disposition } = classification;
|
|
153
|
+
let exitReason = buildInitialExitReason(teamState, classification, signal);
|
|
154
|
+
// ── Phase: report git state ─────────────────────────────────────────
|
|
155
|
+
const allDeliveries = await fetchDeliveriesForCompletion(teamState);
|
|
430
156
|
// Report git state for deliveries the team plausibly worked on or finished.
|
|
431
157
|
// See REPORTABLE_DELIVERY_STATUSES — including 'done' ensures focuses whose
|
|
432
158
|
// deliveries all reached done before the team exit still attempt the merge.
|
|
@@ -435,42 +161,11 @@ export async function handleTeamCompletion(params) {
|
|
|
435
161
|
for (const deliveryId of workedDeliveryIds) {
|
|
436
162
|
reportGitState(deliveryId, 'worktree_complete').catch(err => console.warn(`[focus-executor] reportGitState worktree_complete failed for ${deliveryId}:`, err.message));
|
|
437
163
|
}
|
|
438
|
-
// ──
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (teamState.readOnly) {
|
|
442
|
-
// Revert any uncommitted changes
|
|
443
|
-
const hasChanges = worktreeHasUncommittedChanges(worktreePath);
|
|
444
|
-
if (hasChanges) {
|
|
445
|
-
console.warn(`[focus-executor] Read-only audit "${focusName}" left uncommitted changes. Reverting.`);
|
|
446
|
-
runGitSync(['checkout', '.'], worktreePath);
|
|
447
|
-
runGitSync(['clean', '-fd'], worktreePath);
|
|
448
|
-
}
|
|
449
|
-
// Check if the agent somehow committed (bypassed hook)
|
|
450
|
-
if (branchHasUnmergedCommits(branchName, config.integrationBranch, config.repoPath)) {
|
|
451
|
-
console.error(`[focus-executor] READ-ONLY VIOLATION: Audit "${focusName}" has commits on ${branchName}. ` +
|
|
452
|
-
`These will NOT be merged. Manual review required.`);
|
|
453
|
-
exitReason = `Read-only audit violation: commits detected on ${branchName}. Merge skipped.`;
|
|
454
|
-
// Update session as failed due to read-only violation
|
|
455
|
-
try {
|
|
456
|
-
await withRetry(() => updateSession(sessionId, {
|
|
457
|
-
status: 'failed',
|
|
458
|
-
exit_reason: exitReason,
|
|
459
|
-
exit_category: 'error',
|
|
460
|
-
ended_at: new Date().toISOString(),
|
|
461
|
-
}), { maxAttempts: 3, baseDelayMs: 1000, label: 'session-update-readonly-violation' });
|
|
462
|
-
}
|
|
463
|
-
catch (err) {
|
|
464
|
-
console.warn(`[focus-executor] Failed to update session after retries:`, err.message);
|
|
465
|
-
}
|
|
466
|
-
// Record heartbeat and clean up
|
|
467
|
-
recordSessionCompleted(0, 0);
|
|
468
|
-
const activeTeams = getActiveTeams();
|
|
469
|
-
teamState.phase = 'terminated';
|
|
470
|
-
activeTeams.delete(focusId);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
164
|
+
// ── Phase: read-only guard ──────────────────────────────────────────
|
|
165
|
+
if (await runReadOnlyGuardPhase(config, teamState, sessionId)) {
|
|
166
|
+
return;
|
|
473
167
|
}
|
|
168
|
+
// ── Phase: WIP preserve ─────────────────────────────────────────────
|
|
474
169
|
// Commit any uncommitted WIP changes left by the agent (crash, kill, timeout).
|
|
475
170
|
// Only needed when the agent didn't exit cleanly — a clean exit means the
|
|
476
171
|
// agent committed its own work.
|
|
@@ -482,10 +177,12 @@ export async function handleTeamCompletion(params) {
|
|
|
482
177
|
console.warn(`[focus-executor] WIP commit failed for "${focusName}": ${err.message}`);
|
|
483
178
|
}
|
|
484
179
|
}
|
|
180
|
+
// ── Phase: status advance ───────────────────────────────────────────
|
|
485
181
|
// Advance delivery statuses based on issue completion
|
|
486
182
|
if (succeeded) {
|
|
487
183
|
await advanceDeliveryStatuses(config, teamState, sessionId);
|
|
488
184
|
}
|
|
185
|
+
// ── Phase: merge ────────────────────────────────────────────────────
|
|
489
186
|
// Merge focus branch to integration if the branch has committed work.
|
|
490
187
|
// Decoupled from team-exit success: a team that exits via review SIGTERM,
|
|
491
188
|
// role-clear, daemon crash, or manual termination still gets its committed
|
|
@@ -509,93 +206,26 @@ export async function handleTeamCompletion(params) {
|
|
|
509
206
|
}
|
|
510
207
|
// Escalate merge failure for worked deliveries only
|
|
511
208
|
if (mergeAttempt.attempted && !mergeSucceeded) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
branchName,
|
|
520
|
-
integrationBranch: config.integrationBranch,
|
|
521
|
-
mergeError: mergeAttempt.exitReason ?? 'unknown',
|
|
522
|
-
}).catch(err => console.warn(`[focus-executor] escalateMergeConflict failed for ${deliveryId}:`, err.message));
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
// ── CI: merge integration → main ──────────────────────────────
|
|
527
|
-
// When CI is enabled (default for legacy focuses without the field),
|
|
528
|
-
// always attempt the integration→main merge at team exit. This is the
|
|
529
|
-
// unconditional backstop: the eager path in focus-team-lifecycle fires
|
|
530
|
-
// when a focus drains mid-flight, but if it didn't (or fired and
|
|
531
|
-
// integration has since moved), the exit path closes the gap.
|
|
532
|
-
// Independent of mergeSucceeded — even when this focus had no new
|
|
533
|
-
// commits, past focuses may have left integration ahead of main.
|
|
534
|
-
// mergeIntegrationToMain no-ops cleanly when integration is already
|
|
535
|
-
// at main. CD additionally pushes.
|
|
536
|
-
const ciDecision = resolveCiDecision(teamState.pipelineConfig);
|
|
537
|
-
if (!ciDecision.shouldMerge) {
|
|
538
|
-
console.log(`[CI] Skipping integration→main merge for focus ${teamState.focusId} ("${focusName}"): ci.enabled=false`);
|
|
539
|
-
exitReason += ' CI skipped (ci.enabled=false).';
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
const { pushToRemote, mergeStrategy } = ciDecision;
|
|
543
|
-
const ciLabel = pushToRemote ? 'CI+CD' : 'CI';
|
|
544
|
-
console.log(`[${ciLabel}] Merging integration to main for "${focusName}" (strategy=${mergeStrategy})${pushToRemote ? ' (will push to remote)' : ''}`);
|
|
545
|
-
try {
|
|
546
|
-
const ciResult = await mergeIntegrationToMain({
|
|
547
|
-
config,
|
|
548
|
-
focusId: teamState.focusId,
|
|
549
|
-
focusName,
|
|
550
|
-
pushToRemote,
|
|
551
|
-
mergeStrategy,
|
|
552
|
-
});
|
|
553
|
-
if (ciResult.success) {
|
|
554
|
-
if (ciResult.pushedTo) {
|
|
555
|
-
console.log(`[${ciLabel}] Pushed main to ${ciResult.pushedTo.name} (${ciResult.pushedTo.url}) for focus ${teamState.focusId} ("${focusName}")`);
|
|
556
|
-
}
|
|
557
|
-
else {
|
|
558
|
-
console.log(`[${ciLabel}] Successfully merged integration to main for focus ${teamState.focusId} ("${focusName}")`);
|
|
559
|
-
}
|
|
560
|
-
// Report merged_to_main / pushed_to_remote for every delivery on
|
|
561
|
-
// integration. That's mid-flight-merged deliveries plus, if this
|
|
562
|
-
// exit's focus→integration merge succeeded, the just-merged ones.
|
|
563
|
-
// Failed focus merges leave workedDeliveryIds off integration —
|
|
564
|
-
// exclude them so we don't lie about where they landed.
|
|
565
|
-
const onIntegration = new Set(teamState.mergedDeliveryIds);
|
|
566
|
-
if (mergeSucceeded) {
|
|
567
|
-
for (const id of workedDeliveryIds)
|
|
568
|
-
onIntegration.add(id);
|
|
569
|
-
}
|
|
570
|
-
const newState = ciResult.pushedTo ? 'pushed_to_remote' : 'merged_to_main';
|
|
571
|
-
for (const deliveryId of onIntegration) {
|
|
572
|
-
reportGitState(deliveryId, newState).catch(err => console.warn(`[${ciLabel}] reportGitState ${newState} failed for ${deliveryId}:`, err.message));
|
|
573
|
-
}
|
|
574
|
-
exitReason += ` ${ciLabel}: merged to main${ciResult.pushedTo ? ' and pushed to remote' : ''}.`;
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
console.error(`[${ciLabel}] Failed to merge integration to main for "${focusName}": ${ciResult.error}`);
|
|
578
|
-
exitReason += ` ${ciLabel} failed: ${ciResult.error}`;
|
|
579
|
-
if (shouldFileCdPushEscalation(pushToRemote, ciResult)) {
|
|
580
|
-
await fileCdPushEscalation({
|
|
581
|
-
organizationId: teamState.organizationId,
|
|
582
|
-
sessionId,
|
|
583
|
-
focusId: teamState.focusId,
|
|
584
|
-
focusName,
|
|
585
|
-
integrationBranch: config.integrationBranch,
|
|
586
|
-
pushError: ciResult.error,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
catch (err) {
|
|
592
|
-
console.error(`[${ciLabel}] Error merging integration to main for "${focusName}":`, err.message);
|
|
593
|
-
exitReason += ` ${ciLabel} error: ${err.message}`;
|
|
594
|
-
}
|
|
209
|
+
escalateFailedMergeDeliveries({
|
|
210
|
+
config,
|
|
211
|
+
teamState,
|
|
212
|
+
sessionId,
|
|
213
|
+
workedDeliveryIds,
|
|
214
|
+
mergeExitReason: mergeAttempt.exitReason,
|
|
215
|
+
});
|
|
595
216
|
}
|
|
217
|
+
// ── Phase: CI merge-to-main ─────────────────────────────────────────
|
|
218
|
+
exitReason += await runCiMergeToMain({
|
|
219
|
+
config,
|
|
220
|
+
teamState,
|
|
221
|
+
sessionId,
|
|
222
|
+
mergeSucceeded,
|
|
223
|
+
workedDeliveryIds,
|
|
224
|
+
});
|
|
596
225
|
// Worktree is focus-owned — do NOT remove on team completion.
|
|
597
226
|
// It persists for the next team session or QA use.
|
|
598
227
|
// Removal only happens via coordinated teardown when focus is deactivated.
|
|
228
|
+
// ── Phase: session finalize ─────────────────────────────────────────
|
|
599
229
|
// Compute git diff stats from all commits on this branch since integration
|
|
600
230
|
let diffStats = { linesAdded: 0, linesRemoved: 0 };
|
|
601
231
|
try {
|
|
@@ -622,12 +252,13 @@ export async function handleTeamCompletion(params) {
|
|
|
622
252
|
catch (err) {
|
|
623
253
|
console.warn(`[focus-executor] Failed to update session after retries:`, err.message);
|
|
624
254
|
}
|
|
625
|
-
//
|
|
255
|
+
// ── Phase: review exit (escalate / handoff) ─────────────────────────
|
|
256
|
+
// See runReviewExitHandler for the routing rules.
|
|
626
257
|
try {
|
|
627
258
|
await runReviewExitHandler({
|
|
628
259
|
focusId,
|
|
629
260
|
focusName,
|
|
630
|
-
organizationId,
|
|
261
|
+
organizationId: teamState.organizationId,
|
|
631
262
|
productId: teamState.productId,
|
|
632
263
|
sessionId,
|
|
633
264
|
sessionType: teamState.sessionType,
|
|
@@ -637,6 +268,7 @@ export async function handleTeamCompletion(params) {
|
|
|
637
268
|
catch (err) {
|
|
638
269
|
console.warn(`[focus-executor] Review exit handler failed for "${focusName}":`, err.message);
|
|
639
270
|
}
|
|
271
|
+
// ── Phase: terminate ────────────────────────────────────────────────
|
|
640
272
|
// Record heartbeat
|
|
641
273
|
recordSessionCompleted(0, 0);
|
|
642
274
|
// Clean up team state
|
|
@@ -649,167 +281,4 @@ export async function handleTeamCompletion(params) {
|
|
|
649
281
|
}
|
|
650
282
|
console.log(`[focus-executor] Team for "${focusName}" cleaned up (merge: ${mergeSucceeded ? 'yes' : 'no'})`);
|
|
651
283
|
}
|
|
652
|
-
const defaultDeps = {
|
|
653
|
-
getFocusDeliveries,
|
|
654
|
-
getDeliveryIssues,
|
|
655
|
-
getDeliverySessionCount,
|
|
656
|
-
updateIssueStatus,
|
|
657
|
-
fetchEffectiveWorkflow,
|
|
658
|
-
updateDeliveryStatus,
|
|
659
|
-
advanceDeliveryStage,
|
|
660
|
-
};
|
|
661
|
-
/**
|
|
662
|
-
* Advance delivery statuses after team completion.
|
|
663
|
-
*
|
|
664
|
-
* Re-queries all deliveries for the focus from the database rather
|
|
665
|
-
* than relying on the spawn-time snapshot in teamState.knownDeliveryIds.
|
|
666
|
-
* This ensures deliveries added mid-flight (after the team was spawned)
|
|
667
|
-
* are discovered and advanced, and that stale deliveryStageIds don't
|
|
668
|
-
* cause deliveries to be skipped.
|
|
669
|
-
*
|
|
670
|
-
* For each delivery in 'coding' status:
|
|
671
|
-
* - All issues Done (or no issues): advance via workflow (coding -> verify -> done)
|
|
672
|
-
* - Open issues remain: re-queue to 'queued' so the next team session picks it up
|
|
673
|
-
*/
|
|
674
|
-
export async function advanceDeliveryStatuses(config, teamState, sessionId, deps = defaultDeps) {
|
|
675
|
-
// Re-query deliveries fresh from the DB to discover mid-flight additions
|
|
676
|
-
let deliveries;
|
|
677
|
-
try {
|
|
678
|
-
deliveries = await deps.getFocusDeliveries(teamState.focusId);
|
|
679
|
-
}
|
|
680
|
-
catch (err) {
|
|
681
|
-
console.warn(`[focus-executor] Failed to fetch deliveries for focus ${teamState.focusId}:`, err.message);
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
for (const delivery of deliveries) {
|
|
685
|
-
// Skip deliveries that were already merged mid-flight
|
|
686
|
-
if (teamState.mergedDeliveryIds.has(delivery.id)) {
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
// Only advance deliveries in agent-actionable status.
|
|
690
|
-
if (!isStatusAgentActionable(delivery.executionStatus ?? '')) {
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
try {
|
|
694
|
-
const [issues, sessionCount, workflow] = await Promise.all([
|
|
695
|
-
deps.getDeliveryIssues(delivery.id),
|
|
696
|
-
deps.getDeliverySessionCount(delivery.id),
|
|
697
|
-
deps.fetchEffectiveWorkflow(delivery.id),
|
|
698
|
-
]);
|
|
699
|
-
// Context Groups are reference material, not actionable work — exclude from open count
|
|
700
|
-
const workIssues = filterWorkIssues(issues);
|
|
701
|
-
const contextGroups = filterContextGroups(issues);
|
|
702
|
-
const openCount = workIssues.filter(i => !TERMINAL_ISSUE_STATUSES.has(i.status)).length;
|
|
703
|
-
const totalWork = workIssues.length;
|
|
704
|
-
// Use currentWorkflowStageId from the fresh query, falling back to
|
|
705
|
-
// the teamState cache for deliveries known at spawn time
|
|
706
|
-
const currentStageId = delivery.currentWorkflowStageId
|
|
707
|
-
?? teamState.deliveryStageIds.get(delivery.id)
|
|
708
|
-
?? null;
|
|
709
|
-
if (!currentStageId) {
|
|
710
|
-
console.warn(`[focus-executor] Delivery ${delivery.id}: no workflow stage ID, skipping advance`);
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
// Re-queue deliveries with open work issues so the next team picks them up
|
|
714
|
-
if (totalWork > 0 && openCount > 0) {
|
|
715
|
-
// Already queued — leave it there
|
|
716
|
-
if (delivery.executionStatus === DELIVERY_STATUS.QUEUED) {
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
const queuedStage = workflow.stages.find(s => s.name === 'queued');
|
|
720
|
-
if (queuedStage) {
|
|
721
|
-
console.log(`[focus-executor] Delivery ${delivery.id}: ${openCount}/${totalWork} work issues still open, re-queuing`);
|
|
722
|
-
await deps.updateDeliveryStatus(delivery.id, 'queued', queuedStage.id, undefined, undefined, { organizationId: teamState.organizationId, fromStatus: delivery.executionStatus });
|
|
723
|
-
teamState.deliveryStageIds.set(delivery.id, queuedStage.id);
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
console.warn(`[focus-executor] Delivery ${delivery.id}: ${openCount}/${totalWork} work issues still open, no queued stage in workflow — leaving in ${delivery.executionStatus}`);
|
|
727
|
-
}
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
// Auto-close Context Groups when all work issues are done
|
|
731
|
-
for (const cg of contextGroups) {
|
|
732
|
-
if (cg.status !== 'Done') {
|
|
733
|
-
try {
|
|
734
|
-
await deps.updateIssueStatus(cg.id, 'Done');
|
|
735
|
-
console.log(`[focus-executor] Auto-closed Context Group "${cg.title}" (all work issues done)`);
|
|
736
|
-
}
|
|
737
|
-
catch (err) {
|
|
738
|
-
console.warn(`[focus-executor] Failed to auto-close Context Group "${cg.title}":`, err.message);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
// Sweep Verified and In Review issues to Done before advancing.
|
|
743
|
-
// 'Verified': review agent confirmed the fix but used Verified instead of Done.
|
|
744
|
-
// 'In Review': dev signalled completion; no review agent ran before close.
|
|
745
|
-
// Both collapse to Done for a clean terminal record.
|
|
746
|
-
const sweepIds = planDeliveryCloseSweep(workIssues);
|
|
747
|
-
for (const issueId of sweepIds) {
|
|
748
|
-
try {
|
|
749
|
-
await deps.updateIssueStatus(issueId, 'Done');
|
|
750
|
-
console.log(`[focus-executor] Swept issue ${issueId} to Done before delivery advance`);
|
|
751
|
-
}
|
|
752
|
-
catch (err) {
|
|
753
|
-
console.warn(`[focus-executor] Failed to sweep issue ${issueId} to Done:`, err.message);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
// If delivery is queued or verify_failed, step through coding first
|
|
757
|
-
// (workflow doesn't allow queued/verify_failed -> verify directly).
|
|
758
|
-
// Update the stage ID for the subsequent advance call.
|
|
759
|
-
let effectiveStageId = currentStageId;
|
|
760
|
-
// from_status fed to the subsequent advanceDeliveryStage call: defaults to
|
|
761
|
-
// the delivery's current status, but becomes 'coding' once we step there.
|
|
762
|
-
let effectiveFromStatus = delivery.executionStatus ?? null;
|
|
763
|
-
if (delivery.executionStatus === DELIVERY_STATUS.QUEUED || delivery.executionStatus === DELIVERY_STATUS.VERIFY_FAILED) {
|
|
764
|
-
const codingStage = workflow.stages.find(s => s.name === 'coding');
|
|
765
|
-
if (codingStage) {
|
|
766
|
-
console.log(`[focus-executor] Delivery ${delivery.id}: stepping ${delivery.executionStatus} -> coding before verify advance`);
|
|
767
|
-
await deps.updateDeliveryStatus(delivery.id, 'coding', codingStage.id, undefined, undefined, { organizationId: teamState.organizationId, fromStatus: delivery.executionStatus });
|
|
768
|
-
effectiveStageId = codingStage.id;
|
|
769
|
-
effectiveFromStatus = 'coding';
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
console.log(`[focus-executor] Delivery ${delivery.id}: ${totalWork === 0 ? 'no issues' : `all ${totalWork} work issues done`}, advancing via workflow`);
|
|
773
|
-
const precheck = await ensureWorktreeCommittedBeforeAdvance({
|
|
774
|
-
deliveryId: delivery.id,
|
|
775
|
-
deliveryName: delivery.name,
|
|
776
|
-
focusId: teamState.focusId,
|
|
777
|
-
organizationId: teamState.organizationId,
|
|
778
|
-
sessionId,
|
|
779
|
-
});
|
|
780
|
-
if (!precheck.ok) {
|
|
781
|
-
console.log(`[focus-executor] Delivery ${delivery.id}: skip advance -- ${precheck.reason}`);
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
const result = await deps.advanceDeliveryStage({
|
|
785
|
-
deliveryId: delivery.id,
|
|
786
|
-
deliveryName: delivery.name,
|
|
787
|
-
organizationId: teamState.organizationId,
|
|
788
|
-
workflow,
|
|
789
|
-
currentStageId: effectiveStageId,
|
|
790
|
-
fromStatus: effectiveFromStatus,
|
|
791
|
-
exitCode: 0,
|
|
792
|
-
openIssueCount: openCount,
|
|
793
|
-
sessionCount,
|
|
794
|
-
policyFailureMode: config.policyFailureMode,
|
|
795
|
-
sessionId,
|
|
796
|
-
focusId: teamState.focusId,
|
|
797
|
-
config,
|
|
798
|
-
});
|
|
799
|
-
// Track the new stage ID for potential future advances
|
|
800
|
-
teamState.deliveryStageIds.set(delivery.id, result.deliveryStageId ?? null);
|
|
801
|
-
// Notify loop engine of delivery completion (verify or done)
|
|
802
|
-
if (result.advanced && (result.deliveryStatus === 'verify' || result.deliveryStatus === 'done')) {
|
|
803
|
-
emitLoopTrigger({
|
|
804
|
-
type: 'delivery_completed',
|
|
805
|
-
focusId: teamState.focusId,
|
|
806
|
-
detail: `${delivery.name} -> ${result.deliveryStatus}`,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
catch (err) {
|
|
811
|
-
console.warn(`[focus-executor] Failed to check/advance delivery ${delivery.id}:`, err.message);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
284
|
//# sourceMappingURL=team-completion.js.map
|