@telora/daemon 0.20.28 → 0.20.53
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 +2 -2
- package/dist/agent-state.d.ts +4 -6
- package/dist/agent-state.d.ts.map +1 -1
- package/dist/agent-state.js +4 -10
- package/dist/agent-state.js.map +1 -1
- package/dist/auto-update.d.ts +87 -0
- package/dist/auto-update.d.ts.map +1 -1
- package/dist/auto-update.js +142 -8
- package/dist/auto-update.js.map +1 -1
- package/dist/branch-status.d.ts +25 -3
- package/dist/branch-status.d.ts.map +1 -1
- package/dist/branch-status.js +130 -59
- package/dist/branch-status.js.map +1 -1
- package/dist/completion/completion-decision.d.ts +29 -3
- package/dist/completion/completion-decision.d.ts.map +1 -1
- package/dist/completion/completion-decision.js +15 -3
- package/dist/completion/completion-decision.js.map +1 -1
- package/dist/completion/event-entry-phases.d.ts +57 -0
- package/dist/completion/event-entry-phases.d.ts.map +1 -0
- package/dist/completion/event-entry-phases.js +205 -0
- package/dist/completion/event-entry-phases.js.map +1 -0
- package/dist/completion/event-phases.d.ts +14 -42
- package/dist/completion/event-phases.d.ts.map +1 -1
- package/dist/completion/event-phases.js +63 -190
- package/dist/completion/event-phases.js.map +1 -1
- package/dist/completion/event.d.ts +11 -1
- package/dist/completion/event.d.ts.map +1 -1
- package/dist/completion/event.js +5 -2
- package/dist/completion/event.js.map +1 -1
- package/dist/completion/index.d.ts +0 -3
- package/dist/completion/index.d.ts.map +1 -1
- package/dist/completion/index.js +0 -3
- package/dist/completion/index.js.map +1 -1
- package/dist/completion/review-exit-phase.d.ts +2 -1
- package/dist/completion/review-exit-phase.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.js +41 -1
- package/dist/completion/review-exit-phase.js.map +1 -1
- package/dist/completion/status-advance-phase.d.ts +2 -0
- package/dist/completion/status-advance-phase.d.ts.map +1 -1
- package/dist/completion/status-advance-phase.js +14 -28
- package/dist/completion/status-advance-phase.js.map +1 -1
- package/dist/completion/team-completion.js +1 -1
- package/dist/completion/team-completion.js.map +1 -1
- package/dist/completion/terminated-work-reconcile.d.ts.map +1 -1
- package/dist/completion/terminated-work-reconcile.js +10 -0
- package/dist/completion/terminated-work-reconcile.js.map +1 -1
- package/dist/constants.d.ts +0 -8
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -8
- package/dist/constants.js.map +1 -1
- package/dist/delivery-close-actions.d.ts +39 -0
- package/dist/delivery-close-actions.d.ts.map +1 -0
- package/dist/delivery-close-actions.js +53 -0
- package/dist/delivery-close-actions.js.map +1 -0
- package/dist/delivery-lifecycle.d.ts +11 -0
- package/dist/delivery-lifecycle.d.ts.map +1 -1
- package/dist/delivery-lifecycle.js +13 -14
- package/dist/delivery-lifecycle.js.map +1 -1
- package/dist/delivery-reconciler.d.ts +18 -4
- package/dist/delivery-reconciler.d.ts.map +1 -1
- package/dist/delivery-reconciler.js +18 -4
- package/dist/delivery-reconciler.js.map +1 -1
- package/dist/directive-executor.d.ts +1 -1
- package/dist/directive-executor.js +1 -1
- package/dist/discovery-fetch.d.ts +76 -0
- package/dist/discovery-fetch.d.ts.map +1 -0
- package/dist/discovery-fetch.js +206 -0
- package/dist/discovery-fetch.js.map +1 -0
- package/dist/discovery-poll.d.ts +1 -51
- package/dist/discovery-poll.d.ts.map +1 -1
- package/dist/discovery-poll.js +10 -192
- package/dist/discovery-poll.js.map +1 -1
- package/dist/drain-teardown.d.ts +11 -6
- package/dist/drain-teardown.d.ts.map +1 -1
- package/dist/drain-teardown.js +3 -2
- package/dist/drain-teardown.js.map +1 -1
- package/dist/focus-branch-ahead.d.ts +42 -0
- package/dist/focus-branch-ahead.d.ts.map +1 -0
- package/dist/focus-branch-ahead.js +45 -0
- package/dist/focus-branch-ahead.js.map +1 -0
- package/dist/focus-engine.d.ts.map +1 -1
- package/dist/focus-engine.js +21 -43
- package/dist/focus-engine.js.map +1 -1
- package/dist/focus-executor.js +2 -2
- package/dist/focus-executor.js.map +1 -1
- package/dist/focus-lifecycle.d.ts.map +1 -1
- package/dist/focus-lifecycle.js +32 -3
- package/dist/focus-lifecycle.js.map +1 -1
- package/dist/focus-loop-registry.d.ts.map +1 -1
- package/dist/focus-loop-registry.js +27 -0
- package/dist/focus-loop-registry.js.map +1 -1
- package/dist/focus-loop.d.ts +34 -12
- package/dist/focus-loop.d.ts.map +1 -1
- package/dist/focus-loop.js +78 -15
- package/dist/focus-loop.js.map +1 -1
- package/dist/focus-merge.d.ts.map +1 -1
- package/dist/focus-merge.js +6 -1
- package/dist/focus-merge.js.map +1 -1
- package/dist/focus-phase.d.ts +18 -3
- package/dist/focus-phase.d.ts.map +1 -1
- package/dist/focus-phase.js +85 -31
- package/dist/focus-phase.js.map +1 -1
- package/dist/focus-prompt-builder.d.ts.map +1 -1
- package/dist/focus-prompt-builder.js +13 -1
- package/dist/focus-prompt-builder.js.map +1 -1
- package/dist/focus-provisioning.d.ts.map +1 -1
- package/dist/focus-provisioning.js +4 -0
- package/dist/focus-provisioning.js.map +1 -1
- package/dist/focus-stage-lifecycle/planning-release.d.ts +35 -17
- package/dist/focus-stage-lifecycle/planning-release.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle/planning-release.js +53 -22
- package/dist/focus-stage-lifecycle/planning-release.js.map +1 -1
- package/dist/focus-worktree-state.d.ts +17 -6
- package/dist/focus-worktree-state.d.ts.map +1 -1
- package/dist/focus-worktree-state.js +21 -11
- package/dist/focus-worktree-state.js.map +1 -1
- package/dist/guard-engine.d.ts +1 -2
- package/dist/guard-engine.d.ts.map +1 -1
- package/dist/guard-engine.js +1 -2
- package/dist/guard-engine.js.map +1 -1
- package/dist/health-events.d.ts +10 -0
- package/dist/health-events.d.ts.map +1 -0
- package/dist/health-events.js +10 -0
- package/dist/health-events.js.map +1 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +98 -21
- package/dist/heartbeat.js.map +1 -1
- package/dist/host-control/catalog.d.ts.map +1 -1
- package/dist/host-control/catalog.js +4 -1
- package/dist/host-control/catalog.js.map +1 -1
- package/dist/host-control/client.d.ts +3 -4
- package/dist/host-control/client.d.ts.map +1 -1
- package/dist/host-control/client.js +4 -5
- package/dist/host-control/client.js.map +1 -1
- package/dist/listener-auto-advance.d.ts +2 -1
- package/dist/listener-auto-advance.d.ts.map +1 -1
- package/dist/listener-auto-advance.js +36 -40
- package/dist/listener-auto-advance.js.map +1 -1
- package/dist/listener.d.ts +2 -7
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +11 -9
- package/dist/listener.js.map +1 -1
- package/dist/operator-hold.d.ts +66 -0
- package/dist/operator-hold.d.ts.map +1 -0
- package/dist/operator-hold.js +99 -0
- package/dist/operator-hold.js.map +1 -0
- package/dist/poll-trigger.d.ts +28 -0
- package/dist/poll-trigger.d.ts.map +1 -0
- package/dist/poll-trigger.js +31 -0
- package/dist/poll-trigger.js.map +1 -0
- package/dist/prompt-listing.d.ts +25 -0
- package/dist/prompt-listing.d.ts.map +1 -1
- package/dist/prompt-listing.js +67 -0
- package/dist/prompt-listing.js.map +1 -1
- package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
- package/dist/prompt-sections/execution-sections.js +24 -0
- package/dist/prompt-sections/execution-sections.js.map +1 -1
- package/dist/queries/context-lock-queries.d.ts.map +1 -1
- package/dist/queries/context-lock-queries.js +4 -1
- package/dist/queries/context-lock-queries.js.map +1 -1
- package/dist/queries/deliveries.d.ts +17 -0
- package/dist/queries/deliveries.d.ts.map +1 -1
- package/dist/queries/deliveries.js.map +1 -1
- package/dist/queries/issues.d.ts +98 -0
- package/dist/queries/issues.d.ts.map +1 -1
- package/dist/queries/issues.js +135 -0
- package/dist/queries/issues.js.map +1 -1
- package/dist/queries/schemas.d.ts +3 -3
- package/dist/security-scan-engine.d.ts +9 -1
- package/dist/security-scan-engine.d.ts.map +1 -1
- package/dist/security-scan-engine.js +25 -2
- package/dist/security-scan-engine.js.map +1 -1
- package/dist/spawn/worktree-session.d.ts.map +1 -1
- package/dist/spawn/worktree-session.js +2 -0
- package/dist/spawn/worktree-session.js.map +1 -1
- package/dist/spawner/index.d.ts +7 -9
- package/dist/spawner/index.d.ts.map +1 -1
- package/dist/spawner/index.js +13 -11
- package/dist/spawner/index.js.map +1 -1
- package/dist/spawner/lifecycle.d.ts +1 -18
- package/dist/spawner/lifecycle.d.ts.map +1 -1
- package/dist/spawner/lifecycle.js +1 -54
- package/dist/spawner/lifecycle.js.map +1 -1
- package/dist/spawner/liveness.d.ts +5 -14
- package/dist/spawner/liveness.d.ts.map +1 -1
- package/dist/spawner/liveness.js +5 -67
- package/dist/spawner/liveness.js.map +1 -1
- package/dist/spawner/state.d.ts +8 -20
- package/dist/spawner/state.d.ts.map +1 -1
- package/dist/spawner/state.js +8 -34
- package/dist/spawner/state.js.map +1 -1
- package/dist/staleness-sensor.d.ts +153 -0
- package/dist/staleness-sensor.d.ts.map +1 -0
- package/dist/staleness-sensor.js +308 -0
- package/dist/staleness-sensor.js.map +1 -0
- package/dist/types/focus.d.ts +9 -0
- package/dist/types/focus.d.ts.map +1 -1
- package/dist/types/session.d.ts +0 -44
- package/dist/types/session.d.ts.map +1 -1
- package/dist/unified-shell.d.ts.map +1 -1
- package/dist/unified-shell.js +19 -0
- package/dist/unified-shell.js.map +1 -1
- package/dist/verification-engine.d.ts +6 -4
- package/dist/verification-engine.d.ts.map +1 -1
- package/dist/verification-engine.js +9 -43
- package/dist/verification-engine.js.map +1 -1
- package/dist/verification-feedback.d.ts +42 -0
- package/dist/verification-feedback.d.ts.map +1 -1
- package/dist/verification-feedback.js +93 -0
- package/dist/verification-feedback.js.map +1 -1
- package/dist/version-check.d.ts +6 -0
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +1 -1
- package/dist/version-check.js.map +1 -1
- package/package.json +2 -3
- package/dist/completion/agent-completion.d.ts +0 -48
- package/dist/completion/agent-completion.d.ts.map +0 -1
- package/dist/completion/agent-completion.js +0 -205
- package/dist/completion/agent-completion.js.map +0 -1
- package/dist/delivery-guards.d.ts +0 -41
- package/dist/delivery-guards.d.ts.map +0 -1
- package/dist/delivery-guards.js +0 -200
- package/dist/delivery-guards.js.map +0 -1
- package/dist/spawner/spawn-team.d.ts +0 -20
- package/dist/spawner/spawn-team.d.ts.map +0 -1
- package/dist/spawner/spawn-team.js +0 -19
- package/dist/spawner/spawn-team.js.map +0 -1
- package/dist/spawner/timeout.d.ts +0 -32
- package/dist/spawner/timeout.d.ts.map +0 -1
- package/dist/spawner/timeout.js +0 -126
- package/dist/spawner/timeout.js.map +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry phases of the completion-event pipeline -- the three that run BEFORE
|
|
3
|
+
* any factual picture is computed, each able to short-circuit the pipeline:
|
|
4
|
+
*
|
|
5
|
+
* 1. applyPlanningRefresh planning-team delivery discovery / failure
|
|
6
|
+
* 2. routeReviewWindowCompletion review routing (review pass vs coding pass)
|
|
7
|
+
* 3. runWakeCycleSelfHeal pre-guard advanceDeliveryStatuses self-heal
|
|
8
|
+
*
|
|
9
|
+
* Split out of event-phases.ts (which kept phases 4-6: the factual picture,
|
|
10
|
+
* the advancement watchdog, and action side effects) purely to hold both
|
|
11
|
+
* files under the 800-line daemon module limit. Behavior is unchanged: same
|
|
12
|
+
* reads, same order, same log lines, same early returns, and delivery
|
|
13
|
+
* execution_status transition semantics remain owned by the single-writer
|
|
14
|
+
* reconciler.
|
|
15
|
+
*/
|
|
16
|
+
import { DELIVERY_STATUS } from '@telora/daemon-core';
|
|
17
|
+
import { isStatusCascadable, isStatusAgentActionable } from '../stage-classifier.js';
|
|
18
|
+
import { OPEN_ISSUE_STATUSES } from '../constants.js';
|
|
19
|
+
import { isInReviewWindow } from './advanceability.js';
|
|
20
|
+
import { escalatePlanningFailure } from './event-escalations.js';
|
|
21
|
+
import { createLogger } from '../log.js';
|
|
22
|
+
const log = createLogger({ module: 'focus-executor' });
|
|
23
|
+
// ── Phase 1: planning refresh ───────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Wake-cycle refresh (Option A): planning teams spawn with knownDeliveryIds
|
|
26
|
+
* empty. The MCP-driven planning turn writes deliveries to the DB, so we
|
|
27
|
+
* re-snapshot them here before any downstream merge/completion logic looks
|
|
28
|
+
* at the set. WHY Option A: keeps the refresh on the existing wake path
|
|
29
|
+
* instead of polling, so it runs exactly when new deliveries become visible.
|
|
30
|
+
*
|
|
31
|
+
* Returns true when the team was terminated (planning produced nothing).
|
|
32
|
+
*/
|
|
33
|
+
export async function applyPlanningRefresh(ctx, deps, currentDeliveries) {
|
|
34
|
+
const { focusId, focusName, teamState } = ctx;
|
|
35
|
+
if (!teamState.planningPhase)
|
|
36
|
+
return false;
|
|
37
|
+
if (currentDeliveries.length === 0) {
|
|
38
|
+
log.warn(`Planning team for "${focusName}" produced no deliveries ` +
|
|
39
|
+
`-- escalating and terminating`);
|
|
40
|
+
await escalatePlanningFailure({
|
|
41
|
+
organizationId: teamState.organizationId,
|
|
42
|
+
sessionId: teamState.leadSessionId ?? '',
|
|
43
|
+
focusId,
|
|
44
|
+
focusName,
|
|
45
|
+
agentRoleId: teamState.roleId,
|
|
46
|
+
}, deps);
|
|
47
|
+
teamState.shutdownReason = 'work_complete';
|
|
48
|
+
deps.terminateTeam(focusId);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
for (const d of currentDeliveries) {
|
|
52
|
+
teamState.knownDeliveryIds.add(d.id);
|
|
53
|
+
teamState.deliveryStageIds.set(d.id, d.currentWorkflowStageId);
|
|
54
|
+
}
|
|
55
|
+
teamState.planningPhase = false;
|
|
56
|
+
log.info(`Planning refresh for "${focusName}": ` +
|
|
57
|
+
`${currentDeliveries.length} delivery(ies) discovered`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Review-window handling. Resolves review_requested_at FRESH because it can
|
|
62
|
+
* be set/cleared between spawn and this event.
|
|
63
|
+
*
|
|
64
|
+
* - A completing REVIEW pass routes verify deliveries deterministically
|
|
65
|
+
* (runReviewExitHandler) and terminates -> 'terminated'.
|
|
66
|
+
* - A completing CODING pass during a review window is left alive so the
|
|
67
|
+
* review's own completion closes the focus -> 'leave_running'.
|
|
68
|
+
* - Otherwise -> 'not_in_review' and the pipeline continues.
|
|
69
|
+
*/
|
|
70
|
+
export async function routeReviewWindowCompletion(ctx, deps, currentDeliveries) {
|
|
71
|
+
const { focusId, focusName, teamState } = ctx;
|
|
72
|
+
let reviewRequestedAt = null;
|
|
73
|
+
if (deps.getReviewRequestedAt) {
|
|
74
|
+
try {
|
|
75
|
+
reviewRequestedAt = await deps.getReviewRequestedAt(ctx.config, focusId);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
log.warn(`getReviewRequestedAt failed for "${focusName}": ${err.message}`);
|
|
79
|
+
// Fail open: a read failure must not block the existing teardown logic.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!isInReviewWindow(reviewRequestedAt, currentDeliveries))
|
|
83
|
+
return 'not_in_review';
|
|
84
|
+
const verifyCount = currentDeliveries.filter(d => d.executionStatus === DELIVERY_STATUS.VERIFY).length;
|
|
85
|
+
if (teamState.sessionType === 'review') {
|
|
86
|
+
// A REVIEW pass just completed -- it emitted its result, which IS the
|
|
87
|
+
// "a review cycle ran" signal. Close the focus DETERMINISTICALLY now,
|
|
88
|
+
// keyed on THIS completion event rather than on process exit. A review
|
|
89
|
+
// resumes its prior session to preserve context (INJ-D) and runs as an
|
|
90
|
+
// interactive stream-json process that emits a result but never exits;
|
|
91
|
+
// the old exit-keyed close therefore waited forever (the review-resume
|
|
92
|
+
// deadlock). runReviewExitHandler routes verify deliveries by the
|
|
93
|
+
// deterministic signal -- a pass that filed zero new issues and has no
|
|
94
|
+
// open work closes verify->done; open work routes verify->verify_failed
|
|
95
|
+
// to re-iterate -- and clears review_requested_at. The review team is then
|
|
96
|
+
// torn down: closure has ALREADY happened, so it no longer matters whether
|
|
97
|
+
// the process exits cleanly or is SIGTERM'd, and the next review cycle
|
|
98
|
+
// resumes the same session id to review only the new delta.
|
|
99
|
+
//
|
|
100
|
+
// succeeded is the ACTUAL result of the review turn (!isError), NOT a
|
|
101
|
+
// hardcoded true. A review that erred (e.g. the CLI failed to start, or a
|
|
102
|
+
// model/engine mismatch) emits a result event with isError=true and files
|
|
103
|
+
// no defects -- the old `succeeded: true` made that indistinguishable from
|
|
104
|
+
// a genuinely clean review, so an errored/no-op review auto-approved
|
|
105
|
+
// verify->done WITHOUT a real review. With the real flag, an errored
|
|
106
|
+
// review leaves the delivery in verify; the next auto-review poll
|
|
107
|
+
// re-triggers, and a persistently failing review is bounded by the
|
|
108
|
+
// review_window_stalled escalation.
|
|
109
|
+
const reviewSucceeded = !ctx.completionInfo.result.isError;
|
|
110
|
+
log.info(`Review pass completed for "${focusName}" (${verifyCount} ` +
|
|
111
|
+
`delivery(ies) in verify, succeeded=${reviewSucceeded}) -- routing deterministically ` +
|
|
112
|
+
`by filed issues, then terminating.`);
|
|
113
|
+
// Coverage evidence: spawn-tip (what the review saw, carried on the team
|
|
114
|
+
// state) is stamped as the watermark; exit-tip (HEAD now) is compared to
|
|
115
|
+
// it to detect mid-review drift. Exit-tip is best-effort via injected git
|
|
116
|
+
// deps; null makes coverage unconfirmable (auto-approve withheld).
|
|
117
|
+
const reviewSpawnTip = teamState.reviewSpawnTip;
|
|
118
|
+
let currentBranchTip = null;
|
|
119
|
+
try {
|
|
120
|
+
const wt = deps.getFocusWorktree?.(focusId);
|
|
121
|
+
if (wt?.worktreePath) {
|
|
122
|
+
currentBranchTip = deps.readWorktreeHeadSha?.(wt.worktreePath) ?? null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
log.warn(`Could not resolve exit-tip for "${focusName}" coverage gate: ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await deps.runReviewExitHandler({
|
|
130
|
+
focusId,
|
|
131
|
+
focusName,
|
|
132
|
+
organizationId: teamState.organizationId,
|
|
133
|
+
productId: teamState.productId,
|
|
134
|
+
sessionId: teamState.leadSessionId ?? '',
|
|
135
|
+
sessionType: 'review',
|
|
136
|
+
succeeded: reviewSucceeded,
|
|
137
|
+
reviewSpawnTip,
|
|
138
|
+
currentBranchTip,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
log.warn(`Deterministic review routing failed for "${focusName}": ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
teamState.shutdownReason = 'work_complete';
|
|
145
|
+
deps.terminateTeam(focusId);
|
|
146
|
+
return 'terminated';
|
|
147
|
+
}
|
|
148
|
+
// A non-review (coding) team completed a cycle while a review is in flight.
|
|
149
|
+
// Do NOT tear it down on the all-terminal / no-actionable-work branches
|
|
150
|
+
// below: `verify` is cascadable, so they would SIGTERM the team mid-review.
|
|
151
|
+
// Leave it running; the review team's own completion closes the focus.
|
|
152
|
+
log.info(`"${focusName}" is in a review window (review_requested_at set, ` +
|
|
153
|
+
`${verifyCount} delivery(ies) in verify) and the completing session is a ` +
|
|
154
|
+
`${teamState.sessionType} pass -- not tearing down; the review pass completion routes ` +
|
|
155
|
+
`the verify deliveries.`);
|
|
156
|
+
return 'leave_running';
|
|
157
|
+
}
|
|
158
|
+
// ── Phase 3: wake-cycle self-heal advance ───────────────────────────────
|
|
159
|
+
/**
|
|
160
|
+
* Self-heal BEFORE the no-progress guard. When actionable deliveries exist
|
|
161
|
+
* but none carry open issues, the team has nothing left to code. The common
|
|
162
|
+
* cause is a review that bounced a delivery to verify_failed while all its
|
|
163
|
+
* issues are already Done. The daemon's transparent
|
|
164
|
+
* verify_failed -> coding -> verify stepping (advanceDeliveryStatuses) clears
|
|
165
|
+
* that. Running it here -- before snapshotting -- means a benign "no work to
|
|
166
|
+
* do" cycle either terminates the team cleanly (all deliveries now terminal)
|
|
167
|
+
* or shows up as real progress (the delivery status moved), instead of being
|
|
168
|
+
* counted as coder no-progress. Either way it can no longer accrue toward a
|
|
169
|
+
* spurious team_failure escalation. (Previously this advance ran only AFTER
|
|
170
|
+
* the guard, so the counter could reach the limit and escalate first.)
|
|
171
|
+
*/
|
|
172
|
+
export async function runWakeCycleSelfHeal(ctx, deps, currentDeliveries, currentIssues) {
|
|
173
|
+
const { focusId, focusName, teamState } = ctx;
|
|
174
|
+
const actionableDeliveryIds = new Set(currentDeliveries
|
|
175
|
+
.filter(d => isStatusAgentActionable(d.executionStatus ?? ''))
|
|
176
|
+
.map(d => d.id));
|
|
177
|
+
const hasActionableWithOpenIssues = currentIssues.some(i => actionableDeliveryIds.has(i.deliveryId) && OPEN_ISSUE_STATUSES.has(i.status));
|
|
178
|
+
let effectiveDeliveries = currentDeliveries;
|
|
179
|
+
if (actionableDeliveryIds.size > 0 && !hasActionableWithOpenIssues) {
|
|
180
|
+
try {
|
|
181
|
+
await deps.advanceDeliveryStatuses(ctx.config, teamState, teamState.leadSessionId ?? '');
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
log.warn(`Wake-cycle advanceDeliveryStatuses failed for "${focusName}": ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
// Re-fetch and re-check termination after the advance -- if every delivery
|
|
187
|
+
// is now terminal-for-the-team, exit cleanly and hand off to review
|
|
188
|
+
// routing / verification instead of sending a stale re-engagement.
|
|
189
|
+
try {
|
|
190
|
+
effectiveDeliveries = await deps.getFocusDeliveries(focusId);
|
|
191
|
+
const stillNonTerminal = effectiveDeliveries.filter(d => !isStatusCascadable(d.executionStatus ?? ''));
|
|
192
|
+
if (stillNonTerminal.length === 0) {
|
|
193
|
+
log.info(`All deliveries terminal after wake-cycle advance for "${focusName}" -- terminating team`);
|
|
194
|
+
teamState.shutdownReason = 'work_complete';
|
|
195
|
+
deps.terminateTeam(focusId);
|
|
196
|
+
return { terminated: true, effectiveDeliveries };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
log.warn(`Failed to re-fetch deliveries after wake-cycle advance for "${focusName}": ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { terminated: false, effectiveDeliveries };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=event-entry-phases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-entry-phases.js","sourceRoot":"","sources":["../../src/completion/event-entry-phases.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* (completion/event.ts) so each sub-decision is independently testable
|
|
6
6
|
* without constructing the whole completion world:
|
|
7
7
|
*
|
|
8
|
-
* 1. applyPlanningRefresh planning-team delivery discovery / failure
|
|
9
|
-
* 2. routeReviewWindowCompletion review routing (review pass vs coding pass)
|
|
10
|
-
* 3. runWakeCycleSelfHeal pre-guard advanceDeliveryStatuses self-heal
|
|
11
8
|
* 4. computeCompletionFacts the post-advance factual picture
|
|
12
9
|
* 5. checkDeliveryAdvancementStall cross-respawn advancement watchdog
|
|
13
10
|
* 6. executeCompletionAction side effects of the chosen action
|
|
14
11
|
*
|
|
12
|
+
* Phases 1-3 -- the pre-decision entry phases that can short-circuit the
|
|
13
|
+
* pipeline (applyPlanningRefresh, routeReviewWindowCompletion,
|
|
14
|
+
* runWakeCycleSelfHeal) -- live in event-entry-phases.ts. The split is purely
|
|
15
|
+
* to hold both files under the 800-line daemon module limit; the pipeline
|
|
16
|
+
* order and semantics are unchanged.
|
|
17
|
+
*
|
|
15
18
|
* The decision itself stays pure in completion-decision.ts. Behavior is
|
|
16
19
|
* preserved exactly: same reads, same order, same log lines, same early
|
|
17
20
|
* returns; delivery execution_status transition semantics are untouched
|
|
@@ -20,45 +23,6 @@
|
|
|
20
23
|
import type { FocusDeliveryInfo, FocusIssueInfo, ProgressSnapshot } from '../types.js';
|
|
21
24
|
import { type CompletionAction } from './completion-decision.js';
|
|
22
25
|
import type { CompletionEventContext, CompletionEventDeps } from './event.js';
|
|
23
|
-
/**
|
|
24
|
-
* Wake-cycle refresh (Option A): planning teams spawn with knownDeliveryIds
|
|
25
|
-
* empty. The MCP-driven planning turn writes deliveries to the DB, so we
|
|
26
|
-
* re-snapshot them here before any downstream merge/completion logic looks
|
|
27
|
-
* at the set. WHY Option A: keeps the refresh on the existing wake path
|
|
28
|
-
* instead of polling, so it runs exactly when new deliveries become visible.
|
|
29
|
-
*
|
|
30
|
-
* Returns true when the team was terminated (planning produced nothing).
|
|
31
|
-
*/
|
|
32
|
-
export declare function applyPlanningRefresh(ctx: CompletionEventContext, deps: CompletionEventDeps, currentDeliveries: FocusDeliveryInfo[]): Promise<boolean>;
|
|
33
|
-
export type ReviewWindowOutcome = 'terminated' | 'leave_running' | 'not_in_review';
|
|
34
|
-
/**
|
|
35
|
-
* Review-window handling. Resolves review_requested_at FRESH because it can
|
|
36
|
-
* be set/cleared between spawn and this event.
|
|
37
|
-
*
|
|
38
|
-
* - A completing REVIEW pass routes verify deliveries deterministically
|
|
39
|
-
* (runReviewExitHandler) and terminates -> 'terminated'.
|
|
40
|
-
* - A completing CODING pass during a review window is left alive so the
|
|
41
|
-
* review's own completion closes the focus -> 'leave_running'.
|
|
42
|
-
* - Otherwise -> 'not_in_review' and the pipeline continues.
|
|
43
|
-
*/
|
|
44
|
-
export declare function routeReviewWindowCompletion(ctx: CompletionEventContext, deps: CompletionEventDeps, currentDeliveries: FocusDeliveryInfo[]): Promise<ReviewWindowOutcome>;
|
|
45
|
-
/**
|
|
46
|
-
* Self-heal BEFORE the no-progress guard. When actionable deliveries exist
|
|
47
|
-
* but none carry open issues, the team has nothing left to code. The common
|
|
48
|
-
* cause is a review that bounced a delivery to verify_failed while all its
|
|
49
|
-
* issues are already Done. The daemon's transparent
|
|
50
|
-
* verify_failed -> coding -> verify stepping (advanceDeliveryStatuses) clears
|
|
51
|
-
* that. Running it here -- before snapshotting -- means a benign "no work to
|
|
52
|
-
* do" cycle either terminates the team cleanly (all deliveries now terminal)
|
|
53
|
-
* or shows up as real progress (the delivery status moved), instead of being
|
|
54
|
-
* counted as coder no-progress. Either way it can no longer accrue toward a
|
|
55
|
-
* spurious team_failure escalation. (Previously this advance ran only AFTER
|
|
56
|
-
* the guard, so the counter could reach the limit and escalate first.)
|
|
57
|
-
*/
|
|
58
|
-
export declare function runWakeCycleSelfHeal(ctx: CompletionEventContext, deps: CompletionEventDeps, currentDeliveries: FocusDeliveryInfo[], currentIssues: FocusIssueInfo[]): Promise<{
|
|
59
|
-
terminated: boolean;
|
|
60
|
-
effectiveDeliveries: FocusDeliveryInfo[];
|
|
61
|
-
}>;
|
|
62
26
|
/** Everything the decision + action execution need, computed once. */
|
|
63
27
|
export interface CompletionFacts {
|
|
64
28
|
/** The ORIGINAL pre-advance delivery list (tracking-state update uses it). */
|
|
@@ -76,6 +40,14 @@ export interface CompletionFacts {
|
|
|
76
40
|
advancedThisCycle: boolean;
|
|
77
41
|
stuck: FocusDeliveryInfo | null;
|
|
78
42
|
updatedNoProgressCycles: number;
|
|
43
|
+
/**
|
|
44
|
+
* True when the team is parked on an open, unanswered directed escalation
|
|
45
|
+
* (needs_intent / needs_clarification) anchored to a delivery it is working.
|
|
46
|
+
* Fail-open (a lookup error resolves to false). Routes the decision to the
|
|
47
|
+
* `hold_blocked_on_directed_escalation` action and suppresses the cross-respawn
|
|
48
|
+
* advancement watchdog so a blocked team is neither escalated nor re-engaged.
|
|
49
|
+
*/
|
|
50
|
+
blockedOnDirectedEscalation: boolean;
|
|
79
51
|
}
|
|
80
52
|
/**
|
|
81
53
|
* Recompute the picture on the post-advance state. The team owns exactly two
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-phases.d.ts","sourceRoot":"","sources":["../../src/completion/event-phases.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"event-phases.d.ts","sourceRoot":"","sources":["../../src/completion/event-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAMvF,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAiB5F,OAAO,KAAK,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAO9E,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,iBAAiB,EAAE,iBAAiB,EAAE,CAAC;IACvC,mBAAmB,EAAE,iBAAiB,EAAE,CAAC;IACzC,aAAa,EAAE,cAAc,EAAE,CAAC;IAChC,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,aAAa,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACvC,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC;;;;;;OAMG;IACH,2BAA2B,EAAE,OAAO,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,sBAAsB,EAC3B,IAAI,EAAE,mBAAmB,EACzB,iBAAiB,EAAE,iBAAiB,EAAE,EACtC,mBAAmB,EAAE,iBAAiB,EAAE,EACxC,aAAa,EAAE,cAAc,EAAE,GAC9B,OAAO,CAAC,eAAe,CAAC,CA2K1B;AAID;;;;;;GAMG;AACH,wBAAsB,6BAA6B,CACjD,GAAG,EAAE,sBAAsB,EAC3B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,eAAe,EACtB,MAAM,EAAE,gBAAgB,EACxB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,OAAO,CAAC,CAwDlB;AAID;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,sBAAsB,EAC3B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,eAAe,EACtB,MAAM,EAAE,gBAAgB,EACxB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CA8Nf"}
|
|
@@ -5,217 +5,38 @@
|
|
|
5
5
|
* (completion/event.ts) so each sub-decision is independently testable
|
|
6
6
|
* without constructing the whole completion world:
|
|
7
7
|
*
|
|
8
|
-
* 1. applyPlanningRefresh planning-team delivery discovery / failure
|
|
9
|
-
* 2. routeReviewWindowCompletion review routing (review pass vs coding pass)
|
|
10
|
-
* 3. runWakeCycleSelfHeal pre-guard advanceDeliveryStatuses self-heal
|
|
11
8
|
* 4. computeCompletionFacts the post-advance factual picture
|
|
12
9
|
* 5. checkDeliveryAdvancementStall cross-respawn advancement watchdog
|
|
13
10
|
* 6. executeCompletionAction side effects of the chosen action
|
|
14
11
|
*
|
|
12
|
+
* Phases 1-3 -- the pre-decision entry phases that can short-circuit the
|
|
13
|
+
* pipeline (applyPlanningRefresh, routeReviewWindowCompletion,
|
|
14
|
+
* runWakeCycleSelfHeal) -- live in event-entry-phases.ts. The split is purely
|
|
15
|
+
* to hold both files under the 800-line daemon module limit; the pipeline
|
|
16
|
+
* order and semantics are unchanged.
|
|
17
|
+
*
|
|
15
18
|
* The decision itself stays pure in completion-decision.ts. Behavior is
|
|
16
19
|
* preserved exactly: same reads, same order, same log lines, same early
|
|
17
20
|
* returns; delivery execution_status transition semantics are untouched
|
|
18
21
|
* (single-writer reconciler).
|
|
19
22
|
*/
|
|
20
|
-
import { sendMessage
|
|
23
|
+
import { sendMessage } from '@telora/daemon-core';
|
|
21
24
|
import { isStatusCascadable, isStatusAgentActionable } from '../stage-classifier.js';
|
|
22
25
|
import { buildWakeMessage, buildReEngagementMessage } from '../focus-prompt-builder.js';
|
|
23
26
|
import { OPEN_ISSUE_STATUSES } from '../constants.js';
|
|
24
27
|
import { isAuditPhaseFocus } from '../audit-shape.js';
|
|
25
28
|
import { shouldCheckNonAdvanceable } from './completion-decision.js';
|
|
26
29
|
import { takeProgressSnapshot, snapshotsEqual } from './progress-snapshot.js';
|
|
27
|
-
import {
|
|
30
|
+
import { findNonAdvanceableDelivery } from './advanceability.js';
|
|
28
31
|
import { getFocusWorktree as defaultGetFocusWorktree } from '../focus-worktree-state.js';
|
|
29
32
|
import { worktreeHasUncommittedChanges as defaultWorktreeHasUncommittedChanges } from '../worktree/index.js';
|
|
30
33
|
import { readWorktreeHeadSha as defaultReadWorktreeHeadSha } from '../git/index.js';
|
|
31
34
|
import { resolveAutoRemediationBudget } from '../delivery-advance-block.js';
|
|
32
|
-
import {
|
|
35
|
+
import { escalateNoProgress, escalateNonAdvanceable, escalateDeliveryAdvancementStalled, } from './event-escalations.js';
|
|
33
36
|
import { recordDeliveryAdvancementCycle } from './delivery-advancement-guard.js';
|
|
34
37
|
import { reconcileTerminatedTeamWork as defaultReconcileTerminatedTeamWork, } from './terminated-work-reconcile.js';
|
|
35
38
|
import { createLogger } from '../log.js';
|
|
36
39
|
const log = createLogger({ module: 'focus-executor' });
|
|
37
|
-
// ── Phase 1: planning refresh ───────────────────────────────────────────
|
|
38
|
-
/**
|
|
39
|
-
* Wake-cycle refresh (Option A): planning teams spawn with knownDeliveryIds
|
|
40
|
-
* empty. The MCP-driven planning turn writes deliveries to the DB, so we
|
|
41
|
-
* re-snapshot them here before any downstream merge/completion logic looks
|
|
42
|
-
* at the set. WHY Option A: keeps the refresh on the existing wake path
|
|
43
|
-
* instead of polling, so it runs exactly when new deliveries become visible.
|
|
44
|
-
*
|
|
45
|
-
* Returns true when the team was terminated (planning produced nothing).
|
|
46
|
-
*/
|
|
47
|
-
export async function applyPlanningRefresh(ctx, deps, currentDeliveries) {
|
|
48
|
-
const { focusId, focusName, teamState } = ctx;
|
|
49
|
-
if (!teamState.planningPhase)
|
|
50
|
-
return false;
|
|
51
|
-
if (currentDeliveries.length === 0) {
|
|
52
|
-
log.warn(`Planning team for "${focusName}" produced no deliveries ` +
|
|
53
|
-
`-- escalating and terminating`);
|
|
54
|
-
await escalatePlanningFailure({
|
|
55
|
-
organizationId: teamState.organizationId,
|
|
56
|
-
sessionId: teamState.leadSessionId ?? '',
|
|
57
|
-
focusId,
|
|
58
|
-
focusName,
|
|
59
|
-
agentRoleId: teamState.roleId,
|
|
60
|
-
}, deps);
|
|
61
|
-
teamState.shutdownReason = 'work_complete';
|
|
62
|
-
deps.terminateTeam(focusId);
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
for (const d of currentDeliveries) {
|
|
66
|
-
teamState.knownDeliveryIds.add(d.id);
|
|
67
|
-
teamState.deliveryStageIds.set(d.id, d.currentWorkflowStageId);
|
|
68
|
-
}
|
|
69
|
-
teamState.planningPhase = false;
|
|
70
|
-
log.info(`Planning refresh for "${focusName}": ` +
|
|
71
|
-
`${currentDeliveries.length} delivery(ies) discovered`);
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Review-window handling. Resolves review_requested_at FRESH because it can
|
|
76
|
-
* be set/cleared between spawn and this event.
|
|
77
|
-
*
|
|
78
|
-
* - A completing REVIEW pass routes verify deliveries deterministically
|
|
79
|
-
* (runReviewExitHandler) and terminates -> 'terminated'.
|
|
80
|
-
* - A completing CODING pass during a review window is left alive so the
|
|
81
|
-
* review's own completion closes the focus -> 'leave_running'.
|
|
82
|
-
* - Otherwise -> 'not_in_review' and the pipeline continues.
|
|
83
|
-
*/
|
|
84
|
-
export async function routeReviewWindowCompletion(ctx, deps, currentDeliveries) {
|
|
85
|
-
const { focusId, focusName, teamState } = ctx;
|
|
86
|
-
let reviewRequestedAt = null;
|
|
87
|
-
if (deps.getReviewRequestedAt) {
|
|
88
|
-
try {
|
|
89
|
-
reviewRequestedAt = await deps.getReviewRequestedAt(ctx.config, focusId);
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
log.warn(`getReviewRequestedAt failed for "${focusName}": ${err.message}`);
|
|
93
|
-
// Fail open: a read failure must not block the existing teardown logic.
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (!isInReviewWindow(reviewRequestedAt, currentDeliveries))
|
|
97
|
-
return 'not_in_review';
|
|
98
|
-
const verifyCount = currentDeliveries.filter(d => d.executionStatus === DELIVERY_STATUS.VERIFY).length;
|
|
99
|
-
if (teamState.sessionType === 'review') {
|
|
100
|
-
// A REVIEW pass just completed -- it emitted its result, which IS the
|
|
101
|
-
// "a review cycle ran" signal. Close the focus DETERMINISTICALLY now,
|
|
102
|
-
// keyed on THIS completion event rather than on process exit. A review
|
|
103
|
-
// resumes its prior session to preserve context (INJ-D) and runs as an
|
|
104
|
-
// interactive stream-json process that emits a result but never exits;
|
|
105
|
-
// the old exit-keyed close therefore waited forever (the review-resume
|
|
106
|
-
// deadlock). runReviewExitHandler routes verify deliveries by the
|
|
107
|
-
// deterministic signal -- a pass that filed zero new issues and has no
|
|
108
|
-
// open work closes verify->done; open work routes verify->verify_failed
|
|
109
|
-
// to re-iterate -- and clears review_requested_at. The review team is then
|
|
110
|
-
// torn down: closure has ALREADY happened, so it no longer matters whether
|
|
111
|
-
// the process exits cleanly or is SIGTERM'd, and the next review cycle
|
|
112
|
-
// resumes the same session id to review only the new delta.
|
|
113
|
-
//
|
|
114
|
-
// succeeded is the ACTUAL result of the review turn (!isError), NOT a
|
|
115
|
-
// hardcoded true. A review that erred (e.g. the CLI failed to start, or a
|
|
116
|
-
// model/engine mismatch) emits a result event with isError=true and files
|
|
117
|
-
// no defects -- the old `succeeded: true` made that indistinguishable from
|
|
118
|
-
// a genuinely clean review, so an errored/no-op review auto-approved
|
|
119
|
-
// verify->done WITHOUT a real review. With the real flag, an errored
|
|
120
|
-
// review leaves the delivery in verify; the next auto-review poll
|
|
121
|
-
// re-triggers, and a persistently failing review is bounded by the
|
|
122
|
-
// review_window_stalled escalation.
|
|
123
|
-
const reviewSucceeded = !ctx.completionInfo.result.isError;
|
|
124
|
-
log.info(`Review pass completed for "${focusName}" (${verifyCount} ` +
|
|
125
|
-
`delivery(ies) in verify, succeeded=${reviewSucceeded}) -- routing deterministically ` +
|
|
126
|
-
`by filed issues, then terminating.`);
|
|
127
|
-
// Coverage evidence: spawn-tip (what the review saw, carried on the team
|
|
128
|
-
// state) is stamped as the watermark; exit-tip (HEAD now) is compared to
|
|
129
|
-
// it to detect mid-review drift. Exit-tip is best-effort via injected git
|
|
130
|
-
// deps; null makes coverage unconfirmable (auto-approve withheld).
|
|
131
|
-
const reviewSpawnTip = teamState.reviewSpawnTip;
|
|
132
|
-
let currentBranchTip = null;
|
|
133
|
-
try {
|
|
134
|
-
const wt = deps.getFocusWorktree?.(focusId);
|
|
135
|
-
if (wt?.worktreePath) {
|
|
136
|
-
currentBranchTip = deps.readWorktreeHeadSha?.(wt.worktreePath) ?? null;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
log.warn(`Could not resolve exit-tip for "${focusName}" coverage gate: ${err.message}`);
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
await deps.runReviewExitHandler({
|
|
144
|
-
focusId,
|
|
145
|
-
focusName,
|
|
146
|
-
organizationId: teamState.organizationId,
|
|
147
|
-
productId: teamState.productId,
|
|
148
|
-
sessionId: teamState.leadSessionId ?? '',
|
|
149
|
-
sessionType: 'review',
|
|
150
|
-
succeeded: reviewSucceeded,
|
|
151
|
-
reviewSpawnTip,
|
|
152
|
-
currentBranchTip,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
log.warn(`Deterministic review routing failed for "${focusName}": ${err.message}`);
|
|
157
|
-
}
|
|
158
|
-
teamState.shutdownReason = 'work_complete';
|
|
159
|
-
deps.terminateTeam(focusId);
|
|
160
|
-
return 'terminated';
|
|
161
|
-
}
|
|
162
|
-
// A non-review (coding) team completed a cycle while a review is in flight.
|
|
163
|
-
// Do NOT tear it down on the all-terminal / no-actionable-work branches
|
|
164
|
-
// below: `verify` is cascadable, so they would SIGTERM the team mid-review.
|
|
165
|
-
// Leave it running; the review team's own completion closes the focus.
|
|
166
|
-
log.info(`"${focusName}" is in a review window (review_requested_at set, ` +
|
|
167
|
-
`${verifyCount} delivery(ies) in verify) and the completing session is a ` +
|
|
168
|
-
`${teamState.sessionType} pass -- not tearing down; the review pass completion routes ` +
|
|
169
|
-
`the verify deliveries.`);
|
|
170
|
-
return 'leave_running';
|
|
171
|
-
}
|
|
172
|
-
// ── Phase 3: wake-cycle self-heal advance ───────────────────────────────
|
|
173
|
-
/**
|
|
174
|
-
* Self-heal BEFORE the no-progress guard. When actionable deliveries exist
|
|
175
|
-
* but none carry open issues, the team has nothing left to code. The common
|
|
176
|
-
* cause is a review that bounced a delivery to verify_failed while all its
|
|
177
|
-
* issues are already Done. The daemon's transparent
|
|
178
|
-
* verify_failed -> coding -> verify stepping (advanceDeliveryStatuses) clears
|
|
179
|
-
* that. Running it here -- before snapshotting -- means a benign "no work to
|
|
180
|
-
* do" cycle either terminates the team cleanly (all deliveries now terminal)
|
|
181
|
-
* or shows up as real progress (the delivery status moved), instead of being
|
|
182
|
-
* counted as coder no-progress. Either way it can no longer accrue toward a
|
|
183
|
-
* spurious team_failure escalation. (Previously this advance ran only AFTER
|
|
184
|
-
* the guard, so the counter could reach the limit and escalate first.)
|
|
185
|
-
*/
|
|
186
|
-
export async function runWakeCycleSelfHeal(ctx, deps, currentDeliveries, currentIssues) {
|
|
187
|
-
const { focusId, focusName, teamState } = ctx;
|
|
188
|
-
const actionableDeliveryIds = new Set(currentDeliveries
|
|
189
|
-
.filter(d => isStatusAgentActionable(d.executionStatus ?? ''))
|
|
190
|
-
.map(d => d.id));
|
|
191
|
-
const hasActionableWithOpenIssues = currentIssues.some(i => actionableDeliveryIds.has(i.deliveryId) && OPEN_ISSUE_STATUSES.has(i.status));
|
|
192
|
-
let effectiveDeliveries = currentDeliveries;
|
|
193
|
-
if (actionableDeliveryIds.size > 0 && !hasActionableWithOpenIssues) {
|
|
194
|
-
try {
|
|
195
|
-
await deps.advanceDeliveryStatuses(ctx.config, teamState, teamState.leadSessionId ?? '');
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
log.warn(`Wake-cycle advanceDeliveryStatuses failed for "${focusName}": ${err.message}`);
|
|
199
|
-
}
|
|
200
|
-
// Re-fetch and re-check termination after the advance -- if every delivery
|
|
201
|
-
// is now terminal-for-the-team, exit cleanly and hand off to review
|
|
202
|
-
// routing / verification instead of sending a stale re-engagement.
|
|
203
|
-
try {
|
|
204
|
-
effectiveDeliveries = await deps.getFocusDeliveries(focusId);
|
|
205
|
-
const stillNonTerminal = effectiveDeliveries.filter(d => !isStatusCascadable(d.executionStatus ?? ''));
|
|
206
|
-
if (stillNonTerminal.length === 0) {
|
|
207
|
-
log.info(`All deliveries terminal after wake-cycle advance for "${focusName}" -- terminating team`);
|
|
208
|
-
teamState.shutdownReason = 'work_complete';
|
|
209
|
-
deps.terminateTeam(focusId);
|
|
210
|
-
return { terminated: true, effectiveDeliveries };
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
catch (err) {
|
|
214
|
-
log.warn(`Failed to re-fetch deliveries after wake-cycle advance for "${focusName}": ${err.message}`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return { terminated: false, effectiveDeliveries };
|
|
218
|
-
}
|
|
219
40
|
/**
|
|
220
41
|
* Recompute the picture on the post-advance state. The team owns exactly two
|
|
221
42
|
* kinds of non-terminal work: planning deliveries (it scopes them) and
|
|
@@ -310,11 +131,40 @@ export async function computeCompletionFacts(ctx, deps, currentDeliveries, effec
|
|
|
310
131
|
const candidates = postNonTerminal.filter(d => isStatusAgentActionable(d.executionStatus ?? ''));
|
|
311
132
|
stuck = await findNonAdvanceableDelivery(candidates, deps.fetchEffectiveWorkflow);
|
|
312
133
|
}
|
|
134
|
+
// A team parked on its own unanswered DIRECTED escalation (needs_intent /
|
|
135
|
+
// needs_clarification) anchored to a delivery it is working is BLOCKED on a
|
|
136
|
+
// human answer, not stalling. Consult the lookup ONLY on the cycle where the
|
|
137
|
+
// team would otherwise accrue a no-progress increment (has actionable work AND
|
|
138
|
+
// did not move state), so a healthy cycle never pays the query. FAIL-OPEN: any
|
|
139
|
+
// lookup error resolves to not-blocked, so a genuine stall is never masked by
|
|
140
|
+
// a failed check -- the OPPOSITE of the operator-hold gate, which fails closed.
|
|
141
|
+
let blockedOnDirectedEscalation = false;
|
|
142
|
+
if (teamHasActionableWork && !madeProgress && deps.findOpenDirectedHold) {
|
|
143
|
+
const actionableDeliveries = effectiveDeliveries.filter(d => isStatusAgentActionable(d.executionStatus ?? ''));
|
|
144
|
+
for (const d of actionableDeliveries) {
|
|
145
|
+
try {
|
|
146
|
+
if (await deps.findOpenDirectedHold(d.id, focusId)) {
|
|
147
|
+
blockedOnDirectedEscalation = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
log.warn(`Directed-escalation hold lookup failed for "${focusName}" delivery ${d.id}, ` +
|
|
153
|
+
`treating as not-blocked: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
313
157
|
// The no-progress counter value AFTER this cycle's reset-or-increment.
|
|
314
158
|
// Only committed to teamState on the actionable-work paths in
|
|
315
159
|
// executeCompletionAction -- the no-actionable-work terminations deliberately
|
|
316
|
-
// do not touch the counter.
|
|
317
|
-
|
|
160
|
+
// do not touch the counter. A blocked team FREEZES the counter (neither reset
|
|
161
|
+
// nor increment): the wait is not progress, but it is not a stall either, so
|
|
162
|
+
// the count is preserved to resume exactly where it was once the block clears.
|
|
163
|
+
const updatedNoProgressCycles = madeProgress
|
|
164
|
+
? 0
|
|
165
|
+
: blockedOnDirectedEscalation
|
|
166
|
+
? (teamState.noProgressCycles ?? 0)
|
|
167
|
+
: (teamState.noProgressCycles ?? 0) + 1;
|
|
318
168
|
// Measurable progress this cycle means the prior dirty episode -- if any --
|
|
319
169
|
// is no longer stalled, so the commit-nudge budget resets alongside the
|
|
320
170
|
// no-progress counter.
|
|
@@ -336,6 +186,7 @@ export async function computeCompletionFacts(ctx, deps, currentDeliveries, effec
|
|
|
336
186
|
advancedThisCycle,
|
|
337
187
|
stuck,
|
|
338
188
|
updatedNoProgressCycles,
|
|
189
|
+
blockedOnDirectedEscalation,
|
|
339
190
|
};
|
|
340
191
|
}
|
|
341
192
|
// ── Phase 5: cross-respawn delivery-advancement watchdog ────────────────
|
|
@@ -348,7 +199,14 @@ export async function computeCompletionFacts(ctx, deps, currentDeliveries, effec
|
|
|
348
199
|
*/
|
|
349
200
|
export async function checkDeliveryAdvancementStall(ctx, deps, facts, action, noProgressLimit) {
|
|
350
201
|
const { focusId, focusName, teamState } = ctx;
|
|
202
|
+
// A team blocked on an unanswered directed escalation must not accrue on THIS
|
|
203
|
+
// watchdog either: it is the same false-positive the primary no-progress guard
|
|
204
|
+
// avoids one layer up, and recording it would let the cross-respawn guard file
|
|
205
|
+
// its own delivery-advancement-stalled team_failure on top of the open question
|
|
206
|
+
// (criterion 3). Skip recording entirely while blocked -- the counter simply
|
|
207
|
+
// holds until the block clears.
|
|
351
208
|
const shouldRecord = facts.teamHasActionableWork &&
|
|
209
|
+
!facts.blockedOnDirectedEscalation &&
|
|
352
210
|
action.kind !== 'escalate_no_progress_and_terminate' &&
|
|
353
211
|
(facts.priorSnapshot === null || !facts.madeProgress);
|
|
354
212
|
const deliveryAdvancement = shouldRecord
|
|
@@ -541,6 +399,21 @@ export async function executeCompletionAction(ctx, deps, facts, action, noProgre
|
|
|
541
399
|
deps.terminateTeam(focusId);
|
|
542
400
|
return;
|
|
543
401
|
}
|
|
402
|
+
case 'hold_blocked_on_directed_escalation': {
|
|
403
|
+
// The team is parked on its own unanswered directed escalation. Do NOT
|
|
404
|
+
// escalate (the open question is already the single signal a human sees),
|
|
405
|
+
// do NOT send a wake/handoff message (that re-engagement is the model spend
|
|
406
|
+
// this branch exists to stop), and do NOT terminate (the team resumes in
|
|
407
|
+
// place when the answer read-back re-engages it). Just commit the FROZEN
|
|
408
|
+
// no-progress counter + the fresh snapshot and fall silent -- with no
|
|
409
|
+
// message sent, the team emits no further 'complete', so the completion
|
|
410
|
+
// loop stops burning cycles until the escalation is answered.
|
|
411
|
+
teamState.noProgressCycles = updatedNoProgressCycles;
|
|
412
|
+
teamState.lastProgressSnapshot = snapshot;
|
|
413
|
+
log.info(`Team for "${focusName}" is blocked on an unanswered directed escalation ` +
|
|
414
|
+
`-- holding in place (no re-engagement, no team_failure) until it is answered.`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
544
417
|
case 'handoff_new_work':
|
|
545
418
|
case 'reengage': {
|
|
546
419
|
teamState.noProgressCycles = updatedNoProgressCycles;
|