oh-my-codex 0.8.11 → 0.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -35
- package/dist/agents/__tests__/definitions.test.js +1 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +11 -0
- package/dist/agents/definitions.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-invalid-config.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-invalid-config.test.js +52 -0
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -0
- package/dist/cli/__tests__/index.test.js +35 -3
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.d.ts +2 -0
- package/dist/cli/__tests__/launch-fallback.test.d.ts.map +1 -0
- package/dist/cli/__tests__/launch-fallback.test.js +60 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.d.ts +2 -0
- package/dist/cli/__tests__/resume.test.d.ts.map +1 -0
- package/dist/cli/__tests__/resume.test.js +78 -0
- package/dist/cli/__tests__/resume.test.js.map +1 -0
- package/dist/cli/__tests__/session-search-help.test.d.ts +2 -0
- package/dist/cli/__tests__/session-search-help.test.d.ts.map +1 -0
- package/dist/cli/__tests__/session-search-help.test.js +36 -0
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -0
- package/dist/cli/__tests__/session-search.test.d.ts +2 -0
- package/dist/cli/__tests__/session-search.test.d.ts.map +1 -0
- package/dist/cli/__tests__/session-search.test.js +77 -0
- package/dist/cli/__tests__/session-search.test.js.map +1 -0
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +2 -0
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/team-decompose.test.js +41 -15
- package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +208 -3
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +26 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +4 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +73 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/session-search.d.ts +8 -0
- package/dist/cli/session-search.d.ts.map +1 -0
- package/dist/cli/session-search.js +133 -0
- package/dist/cli/session-search.js.map +1 -0
- package/dist/cli/team.d.ts +13 -12
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +123 -39
- package/dist/cli/team.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +33 -1
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +219 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +64 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +420 -5
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +95 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +3 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +39 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts +6 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +45 -4
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/mcp/team-server.js +1 -1
- package/dist/mcp/team-server.js.map +1 -1
- package/dist/session-history/__tests__/search.test.d.ts +2 -0
- package/dist/session-history/__tests__/search.test.d.ts.map +1 -0
- package/dist/session-history/__tests__/search.test.js +150 -0
- package/dist/session-history/__tests__/search.test.js.map +1 -0
- package/dist/session-history/search.d.ts +31 -0
- package/dist/session-history/search.d.ts.map +1 -0
- package/dist/session-history/search.js +326 -0
- package/dist/session-history/search.js.map +1 -0
- package/dist/team/__tests__/allocation-policy.test.d.ts +2 -0
- package/dist/team/__tests__/allocation-policy.test.d.ts.map +1 -0
- package/dist/team/__tests__/allocation-policy.test.js +39 -0
- package/dist/team/__tests__/allocation-policy.test.js.map +1 -0
- package/dist/team/__tests__/api-interop.test.js +140 -4
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/followup-planner.test.js +12 -0
- package/dist/team/__tests__/followup-planner.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +6 -1
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/rebalance-policy.test.d.ts +2 -0
- package/dist/team/__tests__/rebalance-policy.test.d.ts.map +1 -0
- package/dist/team/__tests__/rebalance-policy.test.js +125 -0
- package/dist/team/__tests__/rebalance-policy.test.js.map +1 -0
- package/dist/team/__tests__/runtime.test.js +315 -12
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +20 -1
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +1 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +20 -3
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/allocation-policy.d.ts +23 -0
- package/dist/team/allocation-policy.d.ts.map +1 -0
- package/dist/team/allocation-policy.js +71 -0
- package/dist/team/allocation-policy.js.map +1 -0
- package/dist/team/api-interop.d.ts +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +159 -0
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/idle-nudge.js +1 -1
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/rebalance-policy.d.ts +19 -0
- package/dist/team/rebalance-policy.d.ts.map +1 -0
- package/dist/team/rebalance-policy.js +48 -0
- package/dist/team/rebalance-policy.js.map +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +132 -17
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/types.d.ts +3 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state.d.ts +8 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +28 -12
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +2 -1
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +1 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +5 -4
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +5 -67
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +1 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +9 -2
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/package.json +2 -1
- package/prompts/team-executor.md +57 -0
- package/prompts/team-orchestrator.md +8 -0
- package/scripts/notify-fallback-watcher.js +295 -1
- package/scripts/notify-hook/auto-nudge.js +20 -4
- package/scripts/notify-hook/team-dispatch.js +11 -58
- package/scripts/notify-hook/team-leader-nudge.js +59 -12
- package/scripts/notify-hook/team-tmux-guard.js +28 -11
- package/scripts/notify-hook/team-worker.js +3 -1
- package/scripts/notify-hook/tmux-injection.js +12 -13
- package/scripts/tmux-hook-engine.js +56 -0
- package/skills/team/SKILL.md +14 -0
- package/templates/catalog-manifest.json +5 -0
- package/dist/rtk/__tests__/index.test.d.ts +0 -2
- package/dist/rtk/__tests__/index.test.d.ts.map +0 -1
- package/dist/rtk/__tests__/index.test.js +0 -104
- package/dist/rtk/__tests__/index.test.js.map +0 -1
- package/dist/rtk/index.d.ts +0 -130
- package/dist/rtk/index.d.ts.map +0 -1
- package/dist/rtk/index.js +0 -257
- package/dist/rtk/index.js.map +0 -1
|
@@ -6,6 +6,14 @@ import { spawnSync } from 'child_process';
|
|
|
6
6
|
import { dirname, join, resolve } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { drainPendingTeamDispatch } from './notify-hook/team-dispatch.js';
|
|
9
|
+
import { resolveNudgePaneTarget } from './notify-hook/auto-nudge.js';
|
|
10
|
+
import { checkPaneReadyForTeamSendKeys } from './notify-hook/team-tmux-guard.js';
|
|
11
|
+
import {
|
|
12
|
+
isLeaderStale,
|
|
13
|
+
maybeNudgeTeamLeader,
|
|
14
|
+
resolveLeaderStalenessThresholdMs,
|
|
15
|
+
} from './notify-hook/team-leader-nudge.js';
|
|
16
|
+
import { DEFAULT_MARKER } from './tmux-hook-engine.js';
|
|
9
17
|
|
|
10
18
|
function argValue(name, fallback = '') {
|
|
11
19
|
const idx = process.argv.indexOf(name);
|
|
@@ -35,7 +43,10 @@ function isPidAlive(pid) {
|
|
|
35
43
|
const cwd = resolve(argValue('--cwd', process.cwd()));
|
|
36
44
|
const notifyScript = resolve(argValue('--notify-script', join(cwd, 'scripts', 'notify-hook.js')));
|
|
37
45
|
const runOnce = process.argv.includes('--once');
|
|
38
|
-
|
|
46
|
+
// Keep fallback control-plane ticks comfortably below the default dispatch
|
|
47
|
+
// ack budget so leaderless team dispatch + stale-alert recovery do not feel
|
|
48
|
+
// laggy between native notify-hook turns.
|
|
49
|
+
const pollMs = Math.max(50, asNumber(argValue('--poll-ms', '250'), 250));
|
|
39
50
|
const parentPid = Math.trunc(asNumber(argValue('--parent-pid', String(process.ppid || 0)), process.ppid || 0));
|
|
40
51
|
const startedAt = Date.now();
|
|
41
52
|
const fileWindowMs = runOnce ? 15000 : 30000;
|
|
@@ -56,6 +67,9 @@ const stateDir = join(omxDir, 'state');
|
|
|
56
67
|
const statePath = join(stateDir, 'notify-fallback-state.json');
|
|
57
68
|
const pidFilePath = resolve(argValue('--pid-file', join(stateDir, 'notify-fallback.pid')));
|
|
58
69
|
const logPath = join(logsDir, `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
70
|
+
const RALPH_CONTINUE_TEXT = 'Ralph loop active continue';
|
|
71
|
+
const RALPH_CONTINUE_CADENCE_MS = 60_000;
|
|
72
|
+
const RALPH_TERMINAL_PHASES = new Set(['complete', 'failed', 'cancelled']);
|
|
59
73
|
|
|
60
74
|
const fileState = new Map();
|
|
61
75
|
const seenTurnKeys = new Set();
|
|
@@ -69,11 +83,185 @@ let lastDispatchDrain = {
|
|
|
69
83
|
last_result: null,
|
|
70
84
|
last_error: null,
|
|
71
85
|
};
|
|
86
|
+
let leaderNudgeRuns = 0;
|
|
87
|
+
let lastLeaderNudge = {
|
|
88
|
+
enabled: true,
|
|
89
|
+
leader_only: safeString(process.env.OMX_TEAM_WORKER || '').trim() === '',
|
|
90
|
+
stale_threshold_ms: null,
|
|
91
|
+
precomputed_leader_stale: null,
|
|
92
|
+
last_tick_at: null,
|
|
93
|
+
last_error: null,
|
|
94
|
+
};
|
|
95
|
+
let lastRalphContinueSteer = {
|
|
96
|
+
enabled: true,
|
|
97
|
+
cadence_ms: RALPH_CONTINUE_CADENCE_MS,
|
|
98
|
+
message: RALPH_CONTINUE_TEXT,
|
|
99
|
+
active: false,
|
|
100
|
+
last_state_check_at: null,
|
|
101
|
+
last_sent_at: '',
|
|
102
|
+
last_reason: 'init',
|
|
103
|
+
state_path: '',
|
|
104
|
+
pane_id: '',
|
|
105
|
+
pane_current_command: '',
|
|
106
|
+
current_phase: '',
|
|
107
|
+
};
|
|
108
|
+
let lastParentGuard = {
|
|
109
|
+
reason: '',
|
|
110
|
+
state_path: '',
|
|
111
|
+
current_phase: '',
|
|
112
|
+
};
|
|
72
113
|
|
|
73
114
|
function eventLog(event) {
|
|
74
115
|
return appendFile(logPath, `${JSON.stringify({ timestamp: new Date().toISOString(), ...event })}\n`).catch(() => {});
|
|
75
116
|
}
|
|
76
117
|
|
|
118
|
+
function normalizeRalphContinueSteerState(raw) {
|
|
119
|
+
if (!raw || typeof raw !== 'object') return { ...lastRalphContinueSteer };
|
|
120
|
+
return {
|
|
121
|
+
enabled: raw.enabled !== false,
|
|
122
|
+
cadence_ms: Number.isFinite(raw.cadence_ms) && raw.cadence_ms > 0 ? raw.cadence_ms : RALPH_CONTINUE_CADENCE_MS,
|
|
123
|
+
message: safeString(raw.message) || RALPH_CONTINUE_TEXT,
|
|
124
|
+
active: raw.active === true,
|
|
125
|
+
last_state_check_at: safeString(raw.last_state_check_at) || null,
|
|
126
|
+
last_sent_at: safeString(raw.last_sent_at),
|
|
127
|
+
last_reason: safeString(raw.last_reason) || 'init',
|
|
128
|
+
state_path: safeString(raw.state_path),
|
|
129
|
+
pane_id: safeString(raw.pane_id),
|
|
130
|
+
pane_current_command: safeString(raw.pane_current_command),
|
|
131
|
+
current_phase: safeString(raw.current_phase),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function hasRalphTerminalState(raw) {
|
|
136
|
+
if (!raw || typeof raw !== 'object') return true;
|
|
137
|
+
if (raw.active !== true) return true;
|
|
138
|
+
const phase = safeString(raw.current_phase).trim().toLowerCase();
|
|
139
|
+
if (phase && RALPH_TERMINAL_PHASES.has(phase)) return true;
|
|
140
|
+
if (safeString(raw.completed_at).trim()) return true;
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function loadPersistedWatcherState() {
|
|
145
|
+
const persisted = await readFile(statePath, 'utf-8')
|
|
146
|
+
.then((content) => JSON.parse(content))
|
|
147
|
+
.catch(() => null);
|
|
148
|
+
lastRalphContinueSteer = normalizeRalphContinueSteerState(persisted?.ralph_continue_steer);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function resolveActiveRalphState() {
|
|
152
|
+
const candidateDirs = [];
|
|
153
|
+
const sessionPath = join(stateDir, 'session.json');
|
|
154
|
+
try {
|
|
155
|
+
const session = JSON.parse(await readFile(sessionPath, 'utf-8'));
|
|
156
|
+
const sessionId = safeString(session?.session_id).trim();
|
|
157
|
+
if (sessionId) {
|
|
158
|
+
candidateDirs.push(join(stateDir, 'sessions', sessionId));
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// No active session file; fall back to root state only.
|
|
162
|
+
}
|
|
163
|
+
if (!candidateDirs.includes(stateDir)) candidateDirs.push(stateDir);
|
|
164
|
+
|
|
165
|
+
for (const dir of candidateDirs) {
|
|
166
|
+
const path = join(dir, 'ralph-state.json');
|
|
167
|
+
if (!existsSync(path)) continue;
|
|
168
|
+
const parsed = await readFile(path, 'utf-8')
|
|
169
|
+
.then((content) => JSON.parse(content))
|
|
170
|
+
.catch(() => null);
|
|
171
|
+
if (!parsed || typeof parsed !== 'object') continue;
|
|
172
|
+
if (hasRalphTerminalState(parsed)) {
|
|
173
|
+
return {
|
|
174
|
+
active: false,
|
|
175
|
+
reason: 'terminal',
|
|
176
|
+
path,
|
|
177
|
+
state: parsed,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
active: true,
|
|
182
|
+
reason: 'active',
|
|
183
|
+
path,
|
|
184
|
+
state: parsed,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
active: false,
|
|
190
|
+
reason: 'cleared',
|
|
191
|
+
path: '',
|
|
192
|
+
state: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function emitRalphContinueSteer(paneId, message) {
|
|
197
|
+
const markedText = `${message} ${DEFAULT_MARKER}`;
|
|
198
|
+
await new Promise((resolve) => {
|
|
199
|
+
const typed = spawnSync('tmux', ['send-keys', '-t', paneId, '-l', markedText], { encoding: 'utf-8' });
|
|
200
|
+
if (typed.status !== 0) throw new Error((typed.stderr || typed.stdout || '').trim() || 'tmux send-keys failed');
|
|
201
|
+
setTimeout(resolve, 100);
|
|
202
|
+
});
|
|
203
|
+
await new Promise((resolve) => {
|
|
204
|
+
const submitA = spawnSync('tmux', ['send-keys', '-t', paneId, 'C-m'], { encoding: 'utf-8' });
|
|
205
|
+
if (submitA.status !== 0) throw new Error((submitA.stderr || submitA.stdout || '').trim() || 'tmux send-keys C-m failed');
|
|
206
|
+
setTimeout(resolve, 100);
|
|
207
|
+
});
|
|
208
|
+
const submitB = spawnSync('tmux', ['send-keys', '-t', paneId, 'C-m'], { encoding: 'utf-8' });
|
|
209
|
+
if (submitB.status !== 0) {
|
|
210
|
+
throw new Error((submitB.stderr || submitB.stdout || '').trim() || 'tmux send-keys C-m failed');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function runRalphContinueSteerTick() {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const nowIso = new Date(now).toISOString();
|
|
217
|
+
const activeRalph = await resolveActiveRalphState();
|
|
218
|
+
lastRalphContinueSteer = {
|
|
219
|
+
...lastRalphContinueSteer,
|
|
220
|
+
active: activeRalph.active,
|
|
221
|
+
current_phase: safeString(activeRalph.state?.current_phase),
|
|
222
|
+
last_state_check_at: nowIso,
|
|
223
|
+
last_reason: activeRalph.reason,
|
|
224
|
+
state_path: activeRalph.path,
|
|
225
|
+
pane_current_command: '',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (!activeRalph.active) return;
|
|
229
|
+
|
|
230
|
+
const lastSentMs = Date.parse(lastRalphContinueSteer.last_sent_at);
|
|
231
|
+
if (Number.isFinite(lastSentMs) && now - lastSentMs < RALPH_CONTINUE_CADENCE_MS) {
|
|
232
|
+
lastRalphContinueSteer.last_reason = 'cooldown';
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const paneId = safeString(activeRalph.state?.tmux_pane_id).trim() || await resolveNudgePaneTarget(stateDir);
|
|
237
|
+
if (!paneId) {
|
|
238
|
+
lastRalphContinueSteer.last_reason = 'pane_missing';
|
|
239
|
+
lastRalphContinueSteer.pane_id = '';
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const paneGuard = await checkPaneReadyForTeamSendKeys(paneId);
|
|
244
|
+
lastRalphContinueSteer.pane_id = paneId;
|
|
245
|
+
lastRalphContinueSteer.pane_current_command = paneGuard.paneCurrentCommand || '';
|
|
246
|
+
if (!paneGuard.ok) {
|
|
247
|
+
lastRalphContinueSteer.last_reason = paneGuard.reason || 'pane_guard_blocked';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await emitRalphContinueSteer(paneId, RALPH_CONTINUE_TEXT);
|
|
252
|
+
lastRalphContinueSteer.last_sent_at = nowIso;
|
|
253
|
+
lastRalphContinueSteer.last_reason = 'sent';
|
|
254
|
+
await eventLog({
|
|
255
|
+
type: 'ralph_continue_steer',
|
|
256
|
+
reason: 'sent',
|
|
257
|
+
pane_id: paneId,
|
|
258
|
+
state_path: activeRalph.path,
|
|
259
|
+
current_phase: safeString(activeRalph.state?.current_phase) || null,
|
|
260
|
+
cadence_ms: RALPH_CONTINUE_CADENCE_MS,
|
|
261
|
+
message: RALPH_CONTINUE_TEXT,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
77
265
|
async function readPidFilePid(path) {
|
|
78
266
|
const raw = await readFile(path, 'utf-8');
|
|
79
267
|
const trimmed = raw.trim();
|
|
@@ -151,6 +339,17 @@ async function writeState(extra = {}) {
|
|
|
151
339
|
run_count: dispatchDrainRuns,
|
|
152
340
|
...lastDispatchDrain,
|
|
153
341
|
},
|
|
342
|
+
leader_nudge: {
|
|
343
|
+
enabled: true,
|
|
344
|
+
run_count: leaderNudgeRuns,
|
|
345
|
+
...lastLeaderNudge,
|
|
346
|
+
},
|
|
347
|
+
ralph_continue_steer: {
|
|
348
|
+
...lastRalphContinueSteer,
|
|
349
|
+
enabled: true,
|
|
350
|
+
cadence_ms: RALPH_CONTINUE_CADENCE_MS,
|
|
351
|
+
message: RALPH_CONTINUE_TEXT,
|
|
352
|
+
},
|
|
154
353
|
...extra,
|
|
155
354
|
};
|
|
156
355
|
await writeFile(statePath, JSON.stringify(state, null, 2)).catch(() => {});
|
|
@@ -177,6 +376,30 @@ async function requestShutdown(reason, signal = null) {
|
|
|
177
376
|
async function enforceLifecycleGuards() {
|
|
178
377
|
if (runOnce) return false;
|
|
179
378
|
if (parentIsGone()) {
|
|
379
|
+
const activeRalph = await resolveActiveRalphState();
|
|
380
|
+
if (activeRalph.active) {
|
|
381
|
+
const currentPhase = safeString(activeRalph.state?.current_phase);
|
|
382
|
+
const nextParentGuard = {
|
|
383
|
+
reason: 'parent_gone_deferred_for_active_ralph',
|
|
384
|
+
state_path: activeRalph.path,
|
|
385
|
+
current_phase: currentPhase,
|
|
386
|
+
};
|
|
387
|
+
if (
|
|
388
|
+
lastParentGuard.reason !== nextParentGuard.reason
|
|
389
|
+
|| lastParentGuard.state_path !== nextParentGuard.state_path
|
|
390
|
+
|| lastParentGuard.current_phase !== nextParentGuard.current_phase
|
|
391
|
+
) {
|
|
392
|
+
await eventLog({
|
|
393
|
+
type: 'watcher_parent_guard',
|
|
394
|
+
reason: nextParentGuard.reason,
|
|
395
|
+
state_path: nextParentGuard.state_path,
|
|
396
|
+
current_phase: currentPhase || null,
|
|
397
|
+
});
|
|
398
|
+
lastParentGuard = nextParentGuard;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
lastParentGuard = { reason: '', state_path: '', current_phase: '' };
|
|
180
403
|
await requestShutdown('parent_gone');
|
|
181
404
|
return true;
|
|
182
405
|
}
|
|
@@ -340,6 +563,72 @@ async function pollFiles() {
|
|
|
340
563
|
}
|
|
341
564
|
}
|
|
342
565
|
|
|
566
|
+
async function runLeaderNudgeTick() {
|
|
567
|
+
const startedIso = new Date().toISOString();
|
|
568
|
+
const leaderOnly = safeString(process.env.OMX_TEAM_WORKER || '').trim() === '';
|
|
569
|
+
const staleThresholdMs = resolveLeaderStalenessThresholdMs();
|
|
570
|
+
|
|
571
|
+
if (!leaderOnly) {
|
|
572
|
+
leaderNudgeRuns += 1;
|
|
573
|
+
lastLeaderNudge = {
|
|
574
|
+
enabled: true,
|
|
575
|
+
leader_only: false,
|
|
576
|
+
stale_threshold_ms: staleThresholdMs,
|
|
577
|
+
precomputed_leader_stale: null,
|
|
578
|
+
last_tick_at: startedIso,
|
|
579
|
+
last_error: 'worker_context',
|
|
580
|
+
};
|
|
581
|
+
await eventLog({
|
|
582
|
+
type: 'leader_nudge_tick',
|
|
583
|
+
leader_only: false,
|
|
584
|
+
run_count: leaderNudgeRuns,
|
|
585
|
+
reason: 'worker_context',
|
|
586
|
+
stale_threshold_ms: staleThresholdMs,
|
|
587
|
+
});
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
const preComputedLeaderStale = await isLeaderStale(stateDir, staleThresholdMs, Date.now());
|
|
593
|
+
await maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputedLeaderStale });
|
|
594
|
+
leaderNudgeRuns += 1;
|
|
595
|
+
lastLeaderNudge = {
|
|
596
|
+
enabled: true,
|
|
597
|
+
leader_only: true,
|
|
598
|
+
stale_threshold_ms: staleThresholdMs,
|
|
599
|
+
precomputed_leader_stale: preComputedLeaderStale,
|
|
600
|
+
last_tick_at: startedIso,
|
|
601
|
+
last_error: null,
|
|
602
|
+
};
|
|
603
|
+
await eventLog({
|
|
604
|
+
type: 'leader_nudge_tick',
|
|
605
|
+
leader_only: true,
|
|
606
|
+
run_count: leaderNudgeRuns,
|
|
607
|
+
stale_threshold_ms: staleThresholdMs,
|
|
608
|
+
precomputed_leader_stale: preComputedLeaderStale,
|
|
609
|
+
reason: 'leader_nudge_checked',
|
|
610
|
+
});
|
|
611
|
+
} catch (err) {
|
|
612
|
+
leaderNudgeRuns += 1;
|
|
613
|
+
lastLeaderNudge = {
|
|
614
|
+
enabled: true,
|
|
615
|
+
leader_only: true,
|
|
616
|
+
stale_threshold_ms: staleThresholdMs,
|
|
617
|
+
precomputed_leader_stale: null,
|
|
618
|
+
last_tick_at: startedIso,
|
|
619
|
+
last_error: err instanceof Error ? err.message : safeString(err),
|
|
620
|
+
};
|
|
621
|
+
await eventLog({
|
|
622
|
+
type: 'leader_nudge_tick',
|
|
623
|
+
leader_only: true,
|
|
624
|
+
run_count: leaderNudgeRuns,
|
|
625
|
+
stale_threshold_ms: staleThresholdMs,
|
|
626
|
+
reason: 'leader_nudge_failed',
|
|
627
|
+
error: lastLeaderNudge.last_error,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
343
632
|
async function runDispatchDrainTick() {
|
|
344
633
|
const startedIso = new Date().toISOString();
|
|
345
634
|
try {
|
|
@@ -383,6 +672,8 @@ async function tick() {
|
|
|
383
672
|
await ensureTrackedFiles();
|
|
384
673
|
await pollFiles();
|
|
385
674
|
await runDispatchDrainTick();
|
|
675
|
+
await runLeaderNudgeTick();
|
|
676
|
+
await runRalphContinueSteerTick();
|
|
386
677
|
await writeState();
|
|
387
678
|
if (await enforceLifecycleGuards()) return;
|
|
388
679
|
setTimeout(() => {
|
|
@@ -403,6 +694,7 @@ async function main() {
|
|
|
403
694
|
}
|
|
404
695
|
|
|
405
696
|
await registerPidFile();
|
|
697
|
+
await loadPersistedWatcherState();
|
|
406
698
|
await eventLog({
|
|
407
699
|
type: 'watcher_start',
|
|
408
700
|
cwd,
|
|
@@ -423,6 +715,8 @@ async function main() {
|
|
|
423
715
|
await ensureTrackedFiles();
|
|
424
716
|
await pollFiles();
|
|
425
717
|
await runDispatchDrainTick();
|
|
718
|
+
await runLeaderNudgeTick();
|
|
719
|
+
await runRalphContinueSteerTick();
|
|
426
720
|
await writeState();
|
|
427
721
|
await eventLog({ type: 'watcher_once_complete', seen_turns: seenTurnKeys.size });
|
|
428
722
|
process.exit(0);
|
|
@@ -10,7 +10,8 @@ import { asNumber, safeString } from './utils.js';
|
|
|
10
10
|
import { readJsonIfExists, getScopedStateDirsForCurrentSession, readdir } from './state-io.js';
|
|
11
11
|
import { runProcess } from './process-runner.js';
|
|
12
12
|
import { logTmuxHookEvent } from './log.js';
|
|
13
|
-
import {
|
|
13
|
+
import { checkPaneReadyForTeamSendKeys } from './team-tmux-guard.js';
|
|
14
|
+
import { buildCapturePaneArgv, DEFAULT_MARKER } from '../tmux-hook-engine.js';
|
|
14
15
|
|
|
15
16
|
export const SKILL_ACTIVE_STATE_FILE = 'skill-active-state.json';
|
|
16
17
|
export const DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS = ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'];
|
|
@@ -178,6 +179,8 @@ export const DEFAULT_STALL_PATTERNS = [
|
|
|
178
179
|
'would you like',
|
|
179
180
|
'shall i',
|
|
180
181
|
'next i can',
|
|
182
|
+
'continue with',
|
|
183
|
+
'continue on',
|
|
181
184
|
'do you want me to',
|
|
182
185
|
'let me know if',
|
|
183
186
|
'do you want',
|
|
@@ -186,6 +189,9 @@ export const DEFAULT_STALL_PATTERNS = [
|
|
|
186
189
|
'just let me know',
|
|
187
190
|
'i can also',
|
|
188
191
|
'i could also',
|
|
192
|
+
'pick up with',
|
|
193
|
+
'next step',
|
|
194
|
+
'next steps',
|
|
189
195
|
'ready to proceed',
|
|
190
196
|
'should i',
|
|
191
197
|
'whenever you',
|
|
@@ -249,9 +255,7 @@ export function detectStallPattern(text, patterns) {
|
|
|
249
255
|
|
|
250
256
|
export async function capturePane(paneId, lines = 10) {
|
|
251
257
|
try {
|
|
252
|
-
const result = await runProcess('tmux',
|
|
253
|
-
'capture-pane', '-t', paneId, '-p', '-l', String(lines),
|
|
254
|
-
], 3000);
|
|
258
|
+
const result = await runProcess('tmux', buildCapturePaneArgv(paneId, lines), 3000);
|
|
255
259
|
return result.stdout || '';
|
|
256
260
|
} catch {
|
|
257
261
|
return '';
|
|
@@ -336,6 +340,18 @@ export async function maybeAutoNudge({ cwd, stateDir, logsDir, payload }) {
|
|
|
336
340
|
if (skillState?.phase === 'completing' && !detected) return;
|
|
337
341
|
if (!detected || !paneId) return;
|
|
338
342
|
|
|
343
|
+
const paneGuard = await checkPaneReadyForTeamSendKeys(paneId);
|
|
344
|
+
if (!paneGuard.ok) {
|
|
345
|
+
await logTmuxHookEvent(logsDir, {
|
|
346
|
+
timestamp: new Date().toISOString(),
|
|
347
|
+
type: 'auto_nudge_skipped',
|
|
348
|
+
pane_id: paneId,
|
|
349
|
+
reason: paneGuard.reason,
|
|
350
|
+
source,
|
|
351
|
+
}).catch(() => {});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
339
355
|
const deepInterviewLockActive = isDeepInterviewAutoApprovalLocked(skillState) && !releaseReason;
|
|
340
356
|
if (deepInterviewLockActive && isBlockedAutoApprovalInput(config.response, skillState.input_lock?.blocked_inputs)) {
|
|
341
357
|
const blockedMessage = skillState.input_lock?.message || DEEP_INTERVIEW_INPUT_LOCK_MESSAGE;
|
|
@@ -4,7 +4,14 @@ import { dirname, join, resolve } from 'path';
|
|
|
4
4
|
import { safeString } from './utils.js';
|
|
5
5
|
import { runProcess } from './process-runner.js';
|
|
6
6
|
import { resolvePaneTarget } from './tmux-injection.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildCapturePaneArgv,
|
|
9
|
+
buildPaneInModeArgv,
|
|
10
|
+
buildSendKeysArgv,
|
|
11
|
+
normalizeTmuxCapture,
|
|
12
|
+
paneHasActiveTask,
|
|
13
|
+
paneLooksReady,
|
|
14
|
+
} from '../tmux-hook-engine.js';
|
|
8
15
|
|
|
9
16
|
function readJson(path, fallback) {
|
|
10
17
|
return readFile(path, 'utf8')
|
|
@@ -260,18 +267,14 @@ function resolveWorkerCliForRequest(request, config) {
|
|
|
260
267
|
return 'codex';
|
|
261
268
|
}
|
|
262
269
|
|
|
263
|
-
function normalizeCaptureText(value) {
|
|
264
|
-
return safeString(value).replace(/\r/g, '').replace(/\s+/g, ' ').trim();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
270
|
function capturedPaneContainsTrigger(captured, trigger) {
|
|
268
271
|
if (!captured || !trigger) return false;
|
|
269
|
-
return
|
|
272
|
+
return normalizeTmuxCapture(captured).includes(normalizeTmuxCapture(trigger));
|
|
270
273
|
}
|
|
271
274
|
|
|
272
275
|
function capturedPaneContainsTriggerNearTail(captured, trigger, nonEmptyTailLines = 24) {
|
|
273
276
|
if (!captured || !trigger) return false;
|
|
274
|
-
const normalizedTrigger =
|
|
277
|
+
const normalizedTrigger = normalizeTmuxCapture(trigger);
|
|
275
278
|
if (!normalizedTrigger) return false;
|
|
276
279
|
const lines = safeString(captured)
|
|
277
280
|
.split('\n')
|
|
@@ -279,57 +282,7 @@ function capturedPaneContainsTriggerNearTail(captured, trigger, nonEmptyTailLine
|
|
|
279
282
|
.filter((line) => line.length > 0);
|
|
280
283
|
if (lines.length === 0) return false;
|
|
281
284
|
const tail = lines.slice(-Math.max(1, nonEmptyTailLines)).join(' ');
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Ported from src/team/tmux-session.ts:949-963 — detects active CLI task indicators.
|
|
286
|
-
function paneHasActiveTask(captured) {
|
|
287
|
-
const lines = safeString(captured)
|
|
288
|
-
.split('\n')
|
|
289
|
-
.map((line) => line.replace(/\r/g, '').trim())
|
|
290
|
-
.filter((line) => line.length > 0);
|
|
291
|
-
const tail = lines.slice(-40);
|
|
292
|
-
if (tail.some((line) => /\b\d+\s+background terminal running\b/i.test(line))) return true;
|
|
293
|
-
if (tail.some((line) => /esc to interrupt/i.test(line))) return true;
|
|
294
|
-
if (tail.some((line) => /\bbackground terminal running\b/i.test(line))) return true;
|
|
295
|
-
if (tail.some((line) => /^•\s.+\(.+•\s*esc to interrupt\)$/i.test(line))) return true;
|
|
296
|
-
// Claude active generation lines
|
|
297
|
-
if (tail.some((line) => /^[·✻]\s+[A-Za-z][A-Za-z0-9''-]*(?:\s+[A-Za-z][A-Za-z0-9''-]*){0,3}(?:…|\.{3})$/u.test(line))) return true;
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function paneIsBootstrapping(captured) {
|
|
302
|
-
const lines = safeString(captured)
|
|
303
|
-
.split('\n')
|
|
304
|
-
.map((line) => line.replace(/\r/g, '').trim())
|
|
305
|
-
.filter((line) => line.length > 0);
|
|
306
|
-
return lines.some((line) =>
|
|
307
|
-
/\b(loading|initializing|starting up)\b/i.test(line)
|
|
308
|
-
|| /\bmodel:\s*loading\b/i.test(line)
|
|
309
|
-
|| /\bconnecting\s+to\b/i.test(line),
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function paneLooksReady(captured) {
|
|
314
|
-
const content = safeString(captured).trimEnd();
|
|
315
|
-
if (content === '') return false;
|
|
316
|
-
|
|
317
|
-
const lines = content
|
|
318
|
-
.split('\n')
|
|
319
|
-
.map((line) => line.replace(/\r/g, ''))
|
|
320
|
-
.map((line) => line.trimEnd())
|
|
321
|
-
.filter((line) => line.trim() !== '');
|
|
322
|
-
|
|
323
|
-
if (paneIsBootstrapping(content)) return false;
|
|
324
|
-
|
|
325
|
-
const lastLine = lines.length > 0 ? lines[lines.length - 1] : '';
|
|
326
|
-
if (/^\s*[›>❯]\s*/u.test(lastLine)) return true;
|
|
327
|
-
|
|
328
|
-
const hasCodexPromptLine = lines.some((line) => /^\s*›\s*/u.test(line));
|
|
329
|
-
const hasClaudePromptLine = lines.some((line) => /^\s*❯\s*/u.test(line));
|
|
330
|
-
if (hasCodexPromptLine || hasClaudePromptLine) return true;
|
|
331
|
-
|
|
332
|
-
return false;
|
|
285
|
+
return normalizeTmuxCapture(tail).includes(normalizedTrigger);
|
|
333
286
|
}
|
|
334
287
|
|
|
335
288
|
const INJECT_VERIFY_DELAY_MS = 250;
|
|
@@ -53,6 +53,40 @@ export function resolveLeaderProgressStallThresholdMs() {
|
|
|
53
53
|
return 120_000;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function buildStatusCheckReminder(teamName) {
|
|
57
|
+
return `Next: check messages; keep orchestrating; if done, gracefully shut down: omx team shutdown ${teamName}.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildMailboxCheckReminder(teamName) {
|
|
61
|
+
return `Next: read messages; keep orchestrating; if done, gracefully shut down: omx team shutdown ${teamName}.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildWorkerStartEvidenceReminder(teamName, workerName) {
|
|
65
|
+
return `Next: check ${workerName} msg/output, confirm task in omx team status ${teamName}, then reassign/nudge.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildLeaderActionGuidance(teamName, {
|
|
69
|
+
allWorkersIdle = false,
|
|
70
|
+
workerPanesAlive = false,
|
|
71
|
+
taskCounts = {},
|
|
72
|
+
} = {}) {
|
|
73
|
+
const pending = Number.isFinite(taskCounts.pending) ? taskCounts.pending : 0;
|
|
74
|
+
const blocked = Number.isFinite(taskCounts.blocked) ? taskCounts.blocked : 0;
|
|
75
|
+
const inProgress = Number.isFinite(taskCounts.in_progress) ? taskCounts.in_progress : 0;
|
|
76
|
+
const tasksComplete = pending === 0 && blocked === 0 && inProgress === 0;
|
|
77
|
+
const pendingFollowUpTasks = allWorkersIdle && pending > 0 && blocked === 0 && inProgress === 0;
|
|
78
|
+
|
|
79
|
+
if (pendingFollowUpTasks) {
|
|
80
|
+
return workerPanesAlive
|
|
81
|
+
? 'Next: assign the next follow-up task to this idle team.'
|
|
82
|
+
: 'Next: launch a new team for the next task set.';
|
|
83
|
+
}
|
|
84
|
+
if (allWorkersIdle && tasksComplete) {
|
|
85
|
+
return `Next: omx team shutdown ${teamName}.`;
|
|
86
|
+
}
|
|
87
|
+
return buildStatusCheckReminder(teamName);
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
export async function checkWorkerPanesAlive(tmuxTarget, workerPaneIds = []) {
|
|
57
91
|
const sessionName = tmuxTarget.split(':')[0];
|
|
58
92
|
try {
|
|
@@ -492,6 +526,11 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
|
|
|
492
526
|
: [];
|
|
493
527
|
const allWorkersIdle = workerStates.length > 0 && workerStates.every((state) => state === 'idle' || state === 'done');
|
|
494
528
|
const progressSnapshot = await readTeamProgressSnapshot(stateDir, teamName, workerNames);
|
|
529
|
+
const leaderActionGuidance = buildLeaderActionGuidance(teamName, {
|
|
530
|
+
allWorkersIdle,
|
|
531
|
+
workerPanesAlive: paneStatus.alive,
|
|
532
|
+
taskCounts: progressSnapshot.taskCounts,
|
|
533
|
+
});
|
|
495
534
|
const prevProgress = nudgeState.progress_by_team[teamName] && typeof nudgeState.progress_by_team[teamName] === 'object'
|
|
496
535
|
? nudgeState.progress_by_team[teamName]
|
|
497
536
|
: {};
|
|
@@ -543,7 +582,10 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
|
|
|
543
582
|
// Stale-leader follow-up is the only periodic visible nudge path.
|
|
544
583
|
// This keeps the leader pane quieter when the leader is not actually stale.
|
|
545
584
|
const stalePanesNudge = paneStatus.alive && leaderStale;
|
|
546
|
-
const
|
|
585
|
+
const stalledTeamReason = leaderStale ? 'leader_stale_with_stalled_team' : 'stalled_team_progress';
|
|
586
|
+
const previousStalledTeamNudge =
|
|
587
|
+
prevReason === 'leader_stale_with_stalled_team' || prevReason === 'stalled_team_progress';
|
|
588
|
+
const stalledTeamNudge = teamProgressStalled && (dueByTime || !previousStalledTeamNudge);
|
|
547
589
|
const staleFollowupDue = stalePanesNudge && dueByTime;
|
|
548
590
|
|
|
549
591
|
if (!shouldSendAllIdleNudge && !hasNewMessage && !stalledTeamNudge && !staleFollowupDue) continue;
|
|
@@ -553,32 +595,37 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
|
|
|
553
595
|
if (shouldSendAllIdleNudge) {
|
|
554
596
|
nudgeReason = 'all_workers_idle';
|
|
555
597
|
const N = workerNames.length;
|
|
556
|
-
text = `[OMX] All ${N} worker${N === 1 ? '' : 's'} idle.
|
|
598
|
+
text = `[OMX] All ${N} worker${N === 1 ? '' : 's'} idle. ${leaderActionGuidance}`;
|
|
557
599
|
} else if (ackWithoutStartEvidence) {
|
|
558
600
|
nudgeReason = ACK_WITHOUT_START_EVIDENCE_REASON;
|
|
559
601
|
text =
|
|
560
602
|
`Team ${teamName}: ${ackWithoutStartEvidence.worker} said "${ackWithoutStartEvidence.body}" `
|
|
561
|
-
+ `but has no
|
|
562
|
-
+
|
|
603
|
+
+ `but has no start evidence (status: ${ackWithoutStartEvidence.statusState}). `
|
|
604
|
+
+ buildWorkerStartEvidenceReminder(teamName, ackWithoutStartEvidence.worker);
|
|
563
605
|
} else if (stalledTeamNudge) {
|
|
564
|
-
nudgeReason =
|
|
606
|
+
nudgeReason = stalledTeamReason;
|
|
565
607
|
const { pending, in_progress, blocked } = progressSnapshot.taskCounts;
|
|
566
608
|
const missingSignals = progressSnapshot.missingSignalWorkers > 0
|
|
567
|
-
? `; ${progressSnapshot.missingSignalWorkers}
|
|
609
|
+
? `; ${progressSnapshot.missingSignalWorkers} signal${progressSnapshot.missingSignalWorkers === 1 ? '' : 's'} missing`
|
|
568
610
|
: '';
|
|
611
|
+
const stallPrefix = leaderStale ? 'leader stale, ' : 'worker panes stalled, ';
|
|
569
612
|
text =
|
|
570
|
-
`Team ${teamName}:
|
|
571
|
-
+
|
|
572
|
-
+ `
|
|
613
|
+
`Team ${teamName}: ${stallPrefix}no progress ${formatDurationMs(stalledForMs)}. `
|
|
614
|
+
+ `${leaderActionGuidance} `
|
|
615
|
+
+ `(p:${pending} ip:${in_progress} b:${blocked}${missingSignals})`;
|
|
573
616
|
} else if (stalePanesNudge && hasNewMessage) {
|
|
574
617
|
nudgeReason = 'stale_leader_with_messages';
|
|
575
|
-
text =
|
|
618
|
+
text =
|
|
619
|
+
`Team ${teamName}: leader stale, ${paneStatus.paneCount} pane(s) active, ${messages.length} msg(s) pending. `
|
|
620
|
+
+ buildMailboxCheckReminder(teamName);
|
|
576
621
|
} else if (staleFollowupDue) {
|
|
577
622
|
nudgeReason = 'stale_leader_panes_alive';
|
|
578
|
-
text =
|
|
623
|
+
text =
|
|
624
|
+
`Team ${teamName}: leader stale, ${paneStatus.paneCount} worker pane(s) still active. `
|
|
625
|
+
+ leaderActionGuidance;
|
|
579
626
|
} else if (hasNewMessage) {
|
|
580
627
|
nudgeReason = 'new_mailbox_message';
|
|
581
|
-
text = `Team ${teamName}: ${messages.length} msg(s) for leader.
|
|
628
|
+
text = `Team ${teamName}: ${messages.length} msg(s) for leader. ${buildMailboxCheckReminder(teamName)}`;
|
|
582
629
|
} else {
|
|
583
630
|
continue;
|
|
584
631
|
}
|