oh-my-codex 0.18.9 → 0.18.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +6 -6
  4. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts +2 -0
  5. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts.map +1 -0
  6. package/dist/autopilot/__tests__/deep-interview-gate.test.js +215 -0
  7. package/dist/autopilot/__tests__/deep-interview-gate.test.js.map +1 -0
  8. package/dist/autopilot/__tests__/ralplan-gate.test.js +148 -0
  9. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  10. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -1
  11. package/dist/autopilot/deep-interview-gate.js +140 -0
  12. package/dist/autopilot/deep-interview-gate.js.map +1 -1
  13. package/dist/catalog/__tests__/schema.test.js +6 -0
  14. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  15. package/dist/cli/__tests__/auth.test.js +36 -3
  16. package/dist/cli/__tests__/auth.test.js.map +1 -1
  17. package/dist/cli/__tests__/codex-feature-probe.test.d.ts +2 -0
  18. package/dist/cli/__tests__/codex-feature-probe.test.d.ts.map +1 -0
  19. package/dist/cli/__tests__/codex-feature-probe.test.js +46 -0
  20. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -0
  21. package/dist/cli/__tests__/doctor-spark-routing.test.d.ts +2 -0
  22. package/dist/cli/__tests__/doctor-spark-routing.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/doctor-spark-routing.test.js +79 -0
  24. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -0
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -0
  26. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  27. package/dist/cli/__tests__/explore.test.js +47 -1175
  28. package/dist/cli/__tests__/explore.test.js.map +1 -1
  29. package/dist/cli/__tests__/index.test.js +276 -6
  30. package/dist/cli/__tests__/index.test.js.map +1 -1
  31. package/dist/cli/__tests__/launch-fallback.test.js +19 -5
  32. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  33. package/dist/cli/__tests__/nested-help-routing.test.js +1 -1
  34. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  35. package/dist/cli/__tests__/package-bin-contract.test.js +19 -6
  36. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-refresh.test.js +6 -2
  38. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  39. package/dist/cli/__tests__/sparkshell-packaging.test.js +45 -2
  40. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  41. package/dist/cli/__tests__/team-decompose.test.js +10 -5
  42. package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
  43. package/dist/cli/__tests__/team.test.js +45 -1
  44. package/dist/cli/__tests__/team.test.js.map +1 -1
  45. package/dist/cli/__tests__/ultragoal.test.js +75 -0
  46. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  47. package/dist/cli/auth.d.ts.map +1 -1
  48. package/dist/cli/auth.js +25 -1
  49. package/dist/cli/auth.js.map +1 -1
  50. package/dist/cli/codex-feature-probe.d.ts +5 -2
  51. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  52. package/dist/cli/codex-feature-probe.js +25 -9
  53. package/dist/cli/codex-feature-probe.js.map +1 -1
  54. package/dist/cli/doctor.d.ts +17 -0
  55. package/dist/cli/doctor.d.ts.map +1 -1
  56. package/dist/cli/doctor.js +121 -0
  57. package/dist/cli/doctor.js.map +1 -1
  58. package/dist/cli/explore.d.ts +2 -21
  59. package/dist/cli/explore.d.ts.map +1 -1
  60. package/dist/cli/explore.js +25 -624
  61. package/dist/cli/explore.js.map +1 -1
  62. package/dist/cli/index.d.ts +34 -2
  63. package/dist/cli/index.d.ts.map +1 -1
  64. package/dist/cli/index.js +166 -89
  65. package/dist/cli/index.js.map +1 -1
  66. package/dist/cli/setup.d.ts.map +1 -1
  67. package/dist/cli/setup.js +9 -1
  68. package/dist/cli/setup.js.map +1 -1
  69. package/dist/cli/team.d.ts +4 -0
  70. package/dist/cli/team.d.ts.map +1 -1
  71. package/dist/cli/team.js +43 -4
  72. package/dist/cli/team.js.map +1 -1
  73. package/dist/cli/ultragoal.d.ts.map +1 -1
  74. package/dist/cli/ultragoal.js +29 -0
  75. package/dist/cli/ultragoal.js.map +1 -1
  76. package/dist/hooks/__tests__/agents-overlay.test.js +8 -10
  77. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  78. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +15 -0
  79. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  80. package/dist/hooks/__tests__/deep-interview-contract.test.js +16 -0
  81. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  82. package/dist/hooks/__tests__/explore-routing.test.js +5 -8
  83. package/dist/hooks/__tests__/explore-routing.test.js.map +1 -1
  84. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +6 -6
  85. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  86. package/dist/hooks/__tests__/skill-guidance-contract.test.js +14 -5
  87. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  88. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  89. package/dist/hooks/agents-overlay.js +2 -1
  90. package/dist/hooks/agents-overlay.js.map +1 -1
  91. package/dist/hooks/explore-routing.d.ts +1 -1
  92. package/dist/hooks/explore-routing.d.ts.map +1 -1
  93. package/dist/hooks/explore-routing.js +3 -8
  94. package/dist/hooks/explore-routing.js.map +1 -1
  95. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +112 -1
  96. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  97. package/dist/hooks/extensibility/plugin-runner-stdin.d.ts +2 -0
  98. package/dist/hooks/extensibility/plugin-runner-stdin.d.ts.map +1 -0
  99. package/dist/hooks/extensibility/plugin-runner-stdin.js +16 -0
  100. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -0
  101. package/dist/hooks/extensibility/plugin-runner.js +2 -4
  102. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  103. package/dist/hud/__tests__/index.test.js +23 -2
  104. package/dist/hud/__tests__/index.test.js.map +1 -1
  105. package/dist/hud/__tests__/reconcile.test.js +367 -1
  106. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  107. package/dist/hud/__tests__/tmux.test.js +118 -7
  108. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  109. package/dist/hud/constants.d.ts +10 -0
  110. package/dist/hud/constants.d.ts.map +1 -1
  111. package/dist/hud/constants.js +19 -0
  112. package/dist/hud/constants.js.map +1 -1
  113. package/dist/hud/index.d.ts +6 -1
  114. package/dist/hud/index.d.ts.map +1 -1
  115. package/dist/hud/index.js +12 -3
  116. package/dist/hud/index.js.map +1 -1
  117. package/dist/hud/reconcile.d.ts +10 -2
  118. package/dist/hud/reconcile.d.ts.map +1 -1
  119. package/dist/hud/reconcile.js +78 -30
  120. package/dist/hud/reconcile.js.map +1 -1
  121. package/dist/hud/tmux.d.ts +14 -1
  122. package/dist/hud/tmux.d.ts.map +1 -1
  123. package/dist/hud/tmux.js +129 -15
  124. package/dist/hud/tmux.js.map +1 -1
  125. package/dist/mcp/wiki-server.d.ts +3 -3
  126. package/dist/ralplan/consensus-gate.js +9 -1
  127. package/dist/ralplan/consensus-gate.js.map +1 -1
  128. package/dist/scripts/__tests__/codex-native-hook.test.js +170 -17
  129. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  130. package/dist/scripts/__tests__/run-test-files.test.js +115 -1
  131. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  132. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  133. package/dist/scripts/codex-native-hook.js +74 -11
  134. package/dist/scripts/codex-native-hook.js.map +1 -1
  135. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  136. package/dist/scripts/notify-hook/team-worker-stop.js +54 -21
  137. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  138. package/dist/scripts/run-test-files.js +218 -160
  139. package/dist/scripts/run-test-files.js.map +1 -1
  140. package/dist/state/__tests__/operations.test.js +463 -0
  141. package/dist/state/__tests__/operations.test.js.map +1 -1
  142. package/dist/team/__tests__/delivery-log.test.js +18 -0
  143. package/dist/team/__tests__/delivery-log.test.js.map +1 -1
  144. package/dist/team/__tests__/runtime.test.js +48 -0
  145. package/dist/team/__tests__/runtime.test.js.map +1 -1
  146. package/dist/team/__tests__/tmux-session.test.js +107 -0
  147. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  148. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  149. package/dist/team/__tests__/tmux-test-fixture.js +14 -2
  150. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  151. package/dist/team/__tests__/tmux-test-fixture.test.js +1 -0
  152. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  153. package/dist/team/__tests__/worker-bootstrap.test.js +54 -1
  154. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  155. package/dist/team/delivery-log.d.ts +1 -1
  156. package/dist/team/delivery-log.d.ts.map +1 -1
  157. package/dist/team/delivery-log.js.map +1 -1
  158. package/dist/team/repo-aware-decomposition.d.ts +4 -0
  159. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  160. package/dist/team/repo-aware-decomposition.js.map +1 -1
  161. package/dist/team/runtime.d.ts.map +1 -1
  162. package/dist/team/runtime.js +78 -9
  163. package/dist/team/runtime.js.map +1 -1
  164. package/dist/team/tmux-session.d.ts +1 -0
  165. package/dist/team/tmux-session.d.ts.map +1 -1
  166. package/dist/team/tmux-session.js +16 -5
  167. package/dist/team/tmux-session.js.map +1 -1
  168. package/dist/team/ultragoal-context.d.ts +12 -0
  169. package/dist/team/ultragoal-context.d.ts.map +1 -1
  170. package/dist/team/ultragoal-context.js +32 -8
  171. package/dist/team/ultragoal-context.js.map +1 -1
  172. package/dist/utils/__tests__/paths.test.js +23 -0
  173. package/dist/utils/__tests__/paths.test.js.map +1 -1
  174. package/dist/utils/paths.d.ts.map +1 -1
  175. package/dist/utils/paths.js +4 -2
  176. package/dist/utils/paths.js.map +1 -1
  177. package/dist/utils/toml.d.ts +4 -0
  178. package/dist/utils/toml.d.ts.map +1 -0
  179. package/dist/utils/toml.js +75 -0
  180. package/dist/utils/toml.js.map +1 -0
  181. package/package.json +1 -1
  182. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  183. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -0
  184. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +34 -0
  185. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +32 -17
  186. package/skills/autopilot/SKILL.md +3 -0
  187. package/skills/deep-interview/SKILL.md +34 -0
  188. package/skills/ultrawork/SKILL.md +32 -17
  189. package/src/scripts/__tests__/codex-native-hook.test.ts +218 -28
  190. package/src/scripts/__tests__/run-test-files.test.ts +138 -2
  191. package/src/scripts/codex-native-hook.ts +80 -10
  192. package/src/scripts/notify-hook/team-worker-stop.ts +58 -18
  193. package/src/scripts/run-test-files.ts +229 -150
  194. package/templates/AGENTS.md +39 -199
  195. package/templates/catalog-manifest.json +7 -0
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "child_process";
2
- import { closeSync, existsSync, openSync, readFileSync, readSync } from "fs";
2
+ import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from "fs";
3
3
  import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
4
4
  import { extname, join, relative, resolve } from "path";
5
5
  import { pathToFileURL } from "url";
@@ -216,6 +216,57 @@ function safeContextSnippet(value: unknown, maxLength = 300): string {
216
216
  return `${text.slice(0, maxLength - 1).trimEnd()}…`;
217
217
  }
218
218
 
219
+ const SIDE_CONVERSATION_BOUNDARY_PATTERNS = [
220
+ /side conversation boundary/i,
221
+ /inherited history from the parent thread/i,
222
+ /reference context only/i,
223
+ /only messages submitted after this boundary are active/i,
224
+ /side-conversation assistant/i,
225
+ ] as const;
226
+
227
+ function textHasSideConversationBoundary(text: string): boolean {
228
+ return SIDE_CONVERSATION_BOUNDARY_PATTERNS.some((pattern) => pattern.test(text));
229
+ }
230
+
231
+ function isLikelySideConversationStopPayload(payload: CodexHookPayload): boolean {
232
+ const payloadText = [
233
+ payload.prompt,
234
+ payload.user_prompt,
235
+ payload.userPrompt,
236
+ payload.input,
237
+ payload.last_user_message,
238
+ payload.lastUserMessage,
239
+ payload.last_assistant_message,
240
+ payload.lastAssistantMessage,
241
+ ].map(safeString).join("\n");
242
+ if (textHasSideConversationBoundary(payloadText)) return true;
243
+
244
+ const transcriptPath = safeString(payload.transcript_path ?? payload.transcriptPath).trim();
245
+ if (!transcriptPath || !existsSync(transcriptPath)) return false;
246
+
247
+ try {
248
+ const maxBytes = 256 * 1024;
249
+ const stats = statSync(transcriptPath);
250
+ const fd = openSync(transcriptPath, "r");
251
+ try {
252
+ const bytesToRead = Math.min(maxBytes, Math.max(0, stats.size));
253
+ const buffer = Buffer.alloc(bytesToRead);
254
+ const position = Math.max(0, stats.size - bytesToRead);
255
+ const bytesRead = readSync(fd, buffer, 0, bytesToRead, position);
256
+ return textHasSideConversationBoundary(buffer.toString("utf-8", 0, bytesRead));
257
+ } finally {
258
+ closeSync(fd);
259
+ }
260
+ } catch {
261
+ return false;
262
+ }
263
+ }
264
+
265
+ function shouldSuppressParentWorkflowStopForSideConversation(payload: CodexHookPayload): boolean {
266
+ if (safeString(payload.hook_event_name ?? payload.hookEventName).trim() !== "Stop") return false;
267
+ return isLikelySideConversationStopPayload(payload);
268
+ }
269
+
219
270
  interface NativeSubagentSessionStartMetadata {
220
271
  parentThreadId: string;
221
272
  agentNickname?: string;
@@ -2657,9 +2708,23 @@ function readPreToolUsePathCandidates(payload: CodexHookPayload): string[] {
2657
2708
  return candidates.map((candidate) => safeString(candidate).trim()).filter(Boolean);
2658
2709
  }
2659
2710
 
2711
+ function isNullDeviceRedirectTarget(target: string): boolean {
2712
+ const normalized = target.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
2713
+ return normalized === "/dev/null" || normalized === "nul";
2714
+ }
2715
+
2716
+ function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
2717
+ const targets: string[] = [];
2718
+ for (const match of command.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
2719
+ const candidate = safeString(match[2]).trim();
2720
+ if (candidate && !isNullDeviceRedirectTarget(candidate)) targets.push(candidate);
2721
+ }
2722
+ return targets;
2723
+ }
2724
+
2660
2725
  function commandHasDeepInterviewWriteIntent(command: string): boolean {
2661
2726
  return /\bapply_patch\b/.test(command)
2662
- || /(?:^|[;&|]\s*)(?:cat|printf|echo)\b[\s\S]{0,240}>\s*[^\s&|;]+/.test(command)
2727
+ || extractDeepInterviewCommandRedirectTargets(command).length > 0
2663
2728
  || /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
2664
2729
  || /\bsed\s+(?:[^\n;&|]*\s)?-i(?:\b|['"])/.test(command)
2665
2730
  || /\b(?:python3?|node|perl|ruby)\b[\s\S]{0,260}\b(?:writeFileSync|writeFile|write_text|open\([^)]*["']w|File\.write|Path\()/.test(command)
@@ -2667,11 +2732,7 @@ function commandHasDeepInterviewWriteIntent(command: string): boolean {
2667
2732
  }
2668
2733
 
2669
2734
  function extractDeepInterviewCommandWriteTargets(command: string): string[] {
2670
- const targets: string[] = [];
2671
- for (const match of command.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
2672
- const candidate = safeString(match[2]).trim();
2673
- if (candidate) targets.push(candidate);
2674
- }
2735
+ const targets = extractDeepInterviewCommandRedirectTargets(command);
2675
2736
  for (const match of command.matchAll(/\btee\s+(?:-a\s+)?(["']?)([^\s&|;<>]+)\1/g)) {
2676
2737
  const candidate = safeString(match[2]).trim();
2677
2738
  if (candidate) targets.push(candidate);
@@ -3735,9 +3796,13 @@ async function buildStopHookOutput(
3735
3796
  const sessionId = readPayloadSessionId(payload);
3736
3797
  const canonicalSessionId = await resolveInternalSessionIdForPayload(cwd, sessionId);
3737
3798
  const threadId = readPayloadThreadId(payload);
3799
+ const suppressParentWorkflowStop = shouldSuppressParentWorkflowStopForSideConversation(payload);
3738
3800
  if (canonicalSessionId) {
3739
3801
  await reconcileStaleRootSkillActiveStateForStop(cwd, stateDir, canonicalSessionId);
3740
3802
  }
3803
+ if (suppressParentWorkflowStop) {
3804
+ return null;
3805
+ }
3741
3806
  const execFollowupOutput = await buildExecFollowupStopOutput(cwd, canonicalSessionId);
3742
3807
  if (execFollowupOutput) return execFollowupOutput;
3743
3808
  const ralphOwnerContext = {
@@ -4452,12 +4517,17 @@ function inferHookEventNameFromMalformedInput(raw: string): CodexHookEventName |
4452
4517
  return readHookEventName({ hook_event_name: value });
4453
4518
  }
4454
4519
 
4455
- function buildMalformedStdinHookOutput(parseError: Error, rawInput: string): Record<string, unknown> {
4520
+ function buildMalformedStdinHookOutput(
4521
+ parseError: Error,
4522
+ rawInput: string,
4523
+ cwd = process.cwd(),
4524
+ ): Record<string, unknown> {
4456
4525
  const reason =
4457
4526
  "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.";
4458
4527
  const systemMessage =
4459
4528
  `${reason} stdin JSON parsing failed inside codex-native-hook: ${parseError.message}.`;
4460
- if (inferHookEventNameFromMalformedInput(rawInput) === "Stop") {
4529
+ const inferredHookEventName = inferHookEventNameFromMalformedInput(rawInput);
4530
+ if (inferredHookEventName === "Stop" || (!inferredHookEventName && hasNativeStopRuntimeSurface(cwd))) {
4461
4531
  return {
4462
4532
  decision: "block",
4463
4533
  reason,
@@ -4603,7 +4673,7 @@ export async function runCodexNativeHookCli(): Promise<void> {
4603
4673
  {},
4604
4674
  buildRawInputLogFields(rawInput),
4605
4675
  );
4606
- writeNativeHookJsonStdout(buildMalformedStdinHookOutput(parseError, rawInput));
4676
+ writeNativeHookJsonStdout(buildMalformedStdinHookOutput(parseError, rawInput, process.cwd()));
4607
4677
  return;
4608
4678
  }
4609
4679
 
@@ -14,7 +14,7 @@ import { appendTeamDeliveryLog } from '../../team/delivery-log.js';
14
14
  import { safeString, asNumber, isTerminalPhase } from './utils.js';
15
15
  import { readJsonIfExists } from './state-io.js';
16
16
  import { logTmuxHookEvent } from './log.js';
17
- import { evaluatePaneInjectionReadiness, queuePaneInput, sendPaneInput } from './team-tmux-guard.js';
17
+ import { evaluatePaneInjectionReadiness, sendPaneInput } from './team-tmux-guard.js';
18
18
  import { resolvePaneTarget } from './tmux-injection.js';
19
19
  import { readTeamWorkersForIdleCheck } from './team-worker.js';
20
20
 
@@ -147,6 +147,35 @@ async function resolveCanonicalLeaderPaneId(leaderPaneId) {
147
147
  return normalizedLeaderPaneId;
148
148
  }
149
149
 
150
+ async function recordSuppressedWorkerStopNudge({
151
+ logsDir,
152
+ teamName,
153
+ workerName,
154
+ reason,
155
+ }) {
156
+ const nowIso = new Date().toISOString();
157
+ await logTmuxHookEvent(logsDir, {
158
+ timestamp: nowIso,
159
+ type: 'worker_stop_leader_nudge_suppressed',
160
+ team: teamName,
161
+ worker: workerName,
162
+ to_worker: 'leader-fixed',
163
+ reason,
164
+ tmux_injection_attempted: false,
165
+ source_type: SOURCE_TYPE,
166
+ }).catch(() => {});
167
+ await appendTeamDeliveryLog(logsDir, {
168
+ event: 'nudge_triggered',
169
+ source: SOURCE_TYPE,
170
+ team: teamName,
171
+ from_worker: workerName,
172
+ to_worker: 'leader-fixed',
173
+ transport: 'none',
174
+ result: 'suppressed',
175
+ reason,
176
+ }).catch(() => {});
177
+ }
178
+
150
179
  async function recordDeferred({
151
180
  stateDir,
152
181
  logsDir,
@@ -229,18 +258,34 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
229
258
  };
230
259
  let tmuxSession = '';
231
260
  let leaderPaneId = '';
261
+ let recordedShutdownSuppression = false;
262
+ const recordShutdownSuppressionOnce = async () => {
263
+ if (recordedShutdownSuppression) return;
264
+ recordedShutdownSuppression = true;
265
+ await recordSuppressedWorkerStopNudge({
266
+ logsDir,
267
+ teamName,
268
+ workerName,
269
+ reason: TEAM_SHUTDOWN_NO_INJECTION_REASON,
270
+ });
271
+ };
232
272
 
233
273
  if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
274
+ await recordShutdownSuppressionOnce();
234
275
  return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
235
276
  }
236
277
 
237
278
  const lock = await acquireTeamStopNudgeLock(teamDir, nowMs, cooldownMs);
238
279
  if (!lock.acquired) {
280
+ if (lock.reason === TEAM_SHUTDOWN_NO_INJECTION_REASON) {
281
+ await recordShutdownSuppressionOnce();
282
+ }
239
283
  return { ok: true, result: lock.reason || TEAM_LOCK_HELD_REASON };
240
284
  }
241
285
 
242
286
  try {
243
287
  if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
288
+ await recordShutdownSuppressionOnce();
244
289
  return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
245
290
  }
246
291
 
@@ -263,6 +308,7 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
263
308
 
264
309
  if (!tmuxTarget) {
265
310
  if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
311
+ await recordShutdownSuppressionOnce();
266
312
  return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
267
313
  }
268
314
  await recordDeferred({
@@ -287,6 +333,7 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
287
333
  });
288
334
  if (!paneGuard.ok) {
289
335
  if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
336
+ await recordShutdownSuppressionOnce();
290
337
  return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
291
338
  }
292
339
  await recordDeferred({
@@ -305,6 +352,7 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
305
352
  }
306
353
 
307
354
  if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
355
+ await recordShutdownSuppressionOnce();
308
356
  return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
309
357
  }
310
358
 
@@ -314,23 +362,14 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
314
362
  + DEFAULT_MARKER;
315
363
 
316
364
  const leaderHasActiveTask = paneHasActiveTask(paneGuard.paneCapture);
317
- let deliveryMode = 'sent';
318
- if (leaderHasActiveTask) {
319
- const sendResult = await queuePaneInput({
320
- paneTarget: tmuxTarget,
321
- prompt,
322
- });
323
- if (!sendResult.ok) throw new Error(sendResult.error || sendResult.reason || 'send_failed');
324
- deliveryMode = 'queued';
325
- } else {
326
- const sendResult = await sendPaneInput({
327
- paneTarget: tmuxTarget,
328
- prompt,
329
- submitKeyPresses: 2,
330
- submitDelayMs: 100,
331
- });
332
- if (!sendResult.ok) throw new Error(sendResult.error || sendResult.reason || 'send_failed');
333
- }
365
+ const sendResult = await sendPaneInput({
366
+ paneTarget: tmuxTarget,
367
+ prompt,
368
+ submitKeyPresses: 2,
369
+ submitDelayMs: 100,
370
+ });
371
+ if (!sendResult.ok) throw new Error(sendResult.error || sendResult.reason || 'send_failed');
372
+ const deliveryMode = leaderHasActiveTask ? 'steered' : 'sent';
334
373
 
335
374
  const deliveryState = {
336
375
  ...nextState,
@@ -376,6 +415,7 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
376
415
  return { ok: true, result: deliveryMode };
377
416
  } catch (err) {
378
417
  if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
418
+ await recordShutdownSuppressionOnce();
379
419
  return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
380
420
  }
381
421
  // Worker Stop is already allowed before this helper runs; nudge failures are