oh-my-codex 0.18.5 → 0.18.7
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/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +56 -7
- package/dist/agents/__tests__/definitions.test.js +11 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +14 -5
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +2 -0
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +4 -1
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.js +2 -2
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
- package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/fsm.test.js +75 -0
- package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
- package/dist/autopilot/deep-interview-gate.d.ts +18 -0
- package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
- package/dist/autopilot/deep-interview-gate.js +256 -0
- package/dist/autopilot/deep-interview-gate.js.map +1 -0
- package/dist/autopilot/fsm.d.ts +13 -0
- package/dist/autopilot/fsm.d.ts.map +1 -0
- package/dist/autopilot/fsm.js +70 -0
- package/dist/autopilot/fsm.js.map +1 -0
- package/dist/autopilot/ralplan-gate.d.ts +17 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
- package/dist/autopilot/ralplan-gate.js +61 -0
- package/dist/autopilot/ralplan-gate.js.map +1 -0
- package/dist/cli/__tests__/index.test.js +24 -4
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +175 -6
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +100 -0
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +18 -0
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +2 -2
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +191 -36
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/question.d.ts.map +1 -1
- package/dist/cli/question.js +36 -5
- package/dist/cli/question.js.map +1 -1
- package/dist/config/__tests__/deep-interview.test.js +7 -6
- package/dist/config/__tests__/deep-interview.test.js.map +1 -1
- package/dist/config/deep-interview.d.ts.map +1 -1
- package/dist/config/deep-interview.js +14 -4
- package/dist/config/deep-interview.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +649 -11
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +63 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +25 -0
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/deep-interview-config-instruction.js +1 -1
- package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +1 -0
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +171 -21
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +1 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/session.d.ts +2 -0
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +13 -5
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/authority.test.js +35 -0
- package/dist/hud/__tests__/authority.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +320 -3
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +117 -14
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +117 -8
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +80 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +134 -1
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/authority.d.ts.map +1 -1
- package/dist/hud/authority.js +13 -2
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/colors.d.ts +1 -0
- package/dist/hud/colors.d.ts.map +1 -1
- package/dist/hud/colors.js +4 -0
- package/dist/hud/colors.js.map +1 -1
- package/dist/hud/constants.d.ts +3 -2
- package/dist/hud/constants.d.ts.map +1 -1
- package/dist/hud/constants.js +3 -2
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.d.ts +20 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +99 -18
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts +2 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +6 -3
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts +1 -0
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +69 -17
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +16 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +2 -0
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +39 -2
- package/dist/hud/tmux.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +13 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts +12 -2
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +83 -9
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +130 -0
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.js +1 -1
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts +1 -0
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +14 -5
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/question/__tests__/deep-interview.test.js +160 -2
- package/dist/question/__tests__/deep-interview.test.js.map +1 -1
- package/dist/question/__tests__/policy.test.js +63 -3
- package/dist/question/__tests__/policy.test.js.map +1 -1
- package/dist/question/__tests__/renderer.test.js +191 -2
- package/dist/question/__tests__/renderer.test.js.map +1 -1
- package/dist/question/__tests__/state.test.js +94 -3
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js +4 -0
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.d.ts +12 -2
- package/dist/question/autopilot-wait.d.ts.map +1 -1
- package/dist/question/autopilot-wait.js +158 -47
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/deep-interview.d.ts.map +1 -1
- package/dist/question/deep-interview.js +22 -6
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/policy.d.ts.map +1 -1
- package/dist/question/policy.js +2 -5
- package/dist/question/policy.js.map +1 -1
- package/dist/question/renderer.d.ts +12 -0
- package/dist/question/renderer.d.ts.map +1 -1
- package/dist/question/renderer.js +87 -3
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts +8 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js +54 -14
- package/dist/question/state.js.map +1 -1
- package/dist/question/types.d.ts +1 -1
- package/dist/question/types.d.ts.map +1 -1
- package/dist/question/ui.d.ts +1 -0
- package/dist/question/ui.d.ts.map +1 -1
- package/dist/question/ui.js +1 -0
- package/dist/question/ui.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +191 -0
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +84 -2
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +9 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +32 -11
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1487 -34
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/check-version-sync.js +8 -4
- package/dist/scripts/check-version-sync.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +356 -38
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +79 -1
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts +9 -0
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
- package/dist/scripts/hook-payload-guard.js +111 -0
- package/dist/scripts/hook-payload-guard.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +8 -1
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook.js +11 -2
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +1012 -1
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +59 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +73 -7
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +102 -0
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +33 -3
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +6 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +28 -1
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +10 -3
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +139 -0
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +3 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +41 -4
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
- package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
- package/dist/team/__tests__/coordination-protocol.test.js +173 -0
- package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
- package/dist/team/__tests__/runtime.test.js +51 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +83 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +45 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/coordination-protocol.d.ts +14 -0
- package/dist/team/coordination-protocol.d.ts.map +1 -0
- package/dist/team/coordination-protocol.js +244 -0
- package/dist/team/coordination-protocol.js.map +1 -0
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +19 -3
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +24 -0
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +21 -1
- 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 +17 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +12 -5
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +1 -1
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +19 -1
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +63 -0
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +4 -2
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +3 -0
- package/dist/utils/agents-model-table.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
- package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
- package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
- package/skills/autopilot/SKILL.md +10 -5
- package/skills/deep-interview/SKILL.md +9 -4
- package/skills/ralplan/SKILL.md +12 -0
- package/skills/team/SKILL.md +16 -0
- package/skills/worker/SKILL.md +14 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +2202 -523
- package/src/scripts/check-version-sync.ts +8 -4
- package/src/scripts/codex-native-hook.ts +444 -36
- package/src/scripts/codex-native-pre-post.ts +80 -0
- package/src/scripts/hook-payload-guard.ts +113 -0
- package/src/scripts/notify-fallback-watcher.ts +8 -1
- package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
- package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
- package/src/scripts/notify-hook.ts +14 -2
|
@@ -1223,6 +1223,85 @@ function hasActionableBashHardFailure(normalized: NormalizedPostToolUsePayload):
|
|
|
1223
1223
|
return containsHardFailure(`${normalized.stderrText}\n${normalized.stdoutText}`);
|
|
1224
1224
|
}
|
|
1225
1225
|
|
|
1226
|
+
function isShellEnvAssignment(token: string): boolean {
|
|
1227
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function shellCommandBasename(token: string): string {
|
|
1231
|
+
const normalized = token.replace(/\\/g, "/");
|
|
1232
|
+
return (normalized.split("/").pop() || normalized).toLowerCase();
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function skipEnvWrapper(tokens: string[]): number {
|
|
1236
|
+
let index = 0;
|
|
1237
|
+
while (isShellEnvAssignment(tokens[index] || "")) index += 1;
|
|
1238
|
+
if (!isEnvExecutableToken(tokens[index] || "")) return index;
|
|
1239
|
+
|
|
1240
|
+
index += 1;
|
|
1241
|
+
while (index < tokens.length) {
|
|
1242
|
+
const token = tokens[index] || "";
|
|
1243
|
+
if (token === "--") {
|
|
1244
|
+
index += 1;
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
if (isShellEnvAssignment(token)) {
|
|
1248
|
+
index += 1;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (envOptionConsumesNextValue(token)) {
|
|
1252
|
+
index += 2;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
if (token === "-i" || token === "--ignore-environment" || token.startsWith("-u") && token.length > 2) {
|
|
1256
|
+
index += 1;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
return index;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function skipGhGlobalOptions(tokens: string[], startIndex: number): number {
|
|
1265
|
+
let index = startIndex;
|
|
1266
|
+
const optionsWithValue = new Set(["--repo", "-R", "--hostname", "--config-dir"]);
|
|
1267
|
+
while (index < tokens.length) {
|
|
1268
|
+
const token = tokens[index] || "";
|
|
1269
|
+
if (optionsWithValue.has(token)) {
|
|
1270
|
+
index += 2;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if (/^(?:--repo|--hostname|--config-dir)=/.test(token)) {
|
|
1274
|
+
index += 1;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (token.startsWith("-") && token !== "-") {
|
|
1278
|
+
index += 1;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
return index;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function isReviewableGhPrChecksTokens(tokens: string[]): boolean {
|
|
1287
|
+
const commandIndex = skipEnvWrapper(tokens);
|
|
1288
|
+
const basename = shellCommandBasename(tokens[commandIndex] || "");
|
|
1289
|
+
if (basename !== "gh" && basename !== "gh.exe") return false;
|
|
1290
|
+
const subcommandIndex = skipGhGlobalOptions(tokens, commandIndex + 1);
|
|
1291
|
+
return tokens[subcommandIndex] === "pr" && tokens[subcommandIndex + 1] === "checks";
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function isReviewableNonZeroBashCommand(command: string): boolean {
|
|
1295
|
+
const boundaryTokens = tokenizeShellCommandWithBoundaries(removeHereDocBodies(command));
|
|
1296
|
+
if (!boundaryTokens) return false;
|
|
1297
|
+
for (let commandStart = 0; commandStart < boundaryTokens.length; commandStart = nextCommandStart(boundaryTokens, commandStart)) {
|
|
1298
|
+
const commandEnd = nextCommandStart(boundaryTokens, commandStart);
|
|
1299
|
+
const commandTokens = boundaryTokens.slice(commandStart, commandEnd).map((candidate) => candidate.value);
|
|
1300
|
+
if (isReviewableGhPrChecksTokens(commandTokens)) return true;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1226
1305
|
export function buildNativePostToolUseOutput(
|
|
1227
1306
|
payload: CodexHookPayload,
|
|
1228
1307
|
): Record<string, unknown> | null {
|
|
@@ -1264,6 +1343,7 @@ export function buildNativePostToolUseOutput(
|
|
|
1264
1343
|
&& normalized.exitCode !== 0
|
|
1265
1344
|
&& combined.length > 0
|
|
1266
1345
|
&& !containsHardFailure(combined)
|
|
1346
|
+
&& isReviewableNonZeroBashCommand(normalized.normalizedCommand)
|
|
1267
1347
|
) {
|
|
1268
1348
|
return {
|
|
1269
1349
|
decision: "block",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export const MAX_NOTIFY_ARGV_JSON_BYTES = 64 * 1024;
|
|
2
|
+
export const MAX_NATIVE_STDIN_JSON_BYTES = 1024 * 1024;
|
|
3
|
+
export const RAW_JSON_FIELD_SCAN_BYTES = 64 * 1024;
|
|
4
|
+
|
|
5
|
+
export const CODEX_HOOK_EVENT_NAMES = [
|
|
6
|
+
"SessionStart",
|
|
7
|
+
"PreToolUse",
|
|
8
|
+
"PostToolUse",
|
|
9
|
+
"UserPromptSubmit",
|
|
10
|
+
"PreCompact",
|
|
11
|
+
"PostCompact",
|
|
12
|
+
"Stop",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export type RawCodexHookEventName = typeof CODEX_HOOK_EVENT_NAMES[number];
|
|
16
|
+
|
|
17
|
+
export function utf8ByteLength(value: string): number {
|
|
18
|
+
return Buffer.byteLength(value, "utf-8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function skipJsonWhitespace(raw: string, index: number): number {
|
|
22
|
+
while (index < raw.length && /\s/.test(raw[index] ?? "")) index += 1;
|
|
23
|
+
return index;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readJsonStringLiteral(raw: string, quoteIndex: number): { value: string; endIndex: number } | null {
|
|
27
|
+
if (raw[quoteIndex] !== '"') return null;
|
|
28
|
+
let value = "";
|
|
29
|
+
for (let index = quoteIndex + 1; index < raw.length; index += 1) {
|
|
30
|
+
const char = raw[index];
|
|
31
|
+
if (char === '"') return { value, endIndex: index + 1 };
|
|
32
|
+
if (char !== "\\") {
|
|
33
|
+
value += char;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
index += 1;
|
|
38
|
+
if (index >= raw.length) return null;
|
|
39
|
+
const escaped = raw[index];
|
|
40
|
+
switch (escaped) {
|
|
41
|
+
case '"':
|
|
42
|
+
case "\\":
|
|
43
|
+
case "/":
|
|
44
|
+
value += escaped;
|
|
45
|
+
break;
|
|
46
|
+
case "b":
|
|
47
|
+
value += "\b";
|
|
48
|
+
break;
|
|
49
|
+
case "f":
|
|
50
|
+
value += "\f";
|
|
51
|
+
break;
|
|
52
|
+
case "n":
|
|
53
|
+
value += "\n";
|
|
54
|
+
break;
|
|
55
|
+
case "r":
|
|
56
|
+
value += "\r";
|
|
57
|
+
break;
|
|
58
|
+
case "t":
|
|
59
|
+
value += "\t";
|
|
60
|
+
break;
|
|
61
|
+
case "u": {
|
|
62
|
+
const hex = raw.slice(index + 1, index + 5);
|
|
63
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null;
|
|
64
|
+
value += String.fromCharCode(Number.parseInt(hex, 16));
|
|
65
|
+
index += 4;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function extractRawJsonStringField(rawInput: string, fieldNames: readonly string[]): string | null {
|
|
76
|
+
const raw = rawInput.slice(0, RAW_JSON_FIELD_SCAN_BYTES);
|
|
77
|
+
const wanted = new Set(fieldNames);
|
|
78
|
+
let depth = 0;
|
|
79
|
+
let index = 0;
|
|
80
|
+
|
|
81
|
+
while (index < raw.length) {
|
|
82
|
+
const char = raw[index];
|
|
83
|
+
if (char === '"') {
|
|
84
|
+
const key = readJsonStringLiteral(raw, index);
|
|
85
|
+
if (!key) return null;
|
|
86
|
+
index = key.endIndex;
|
|
87
|
+
const afterKey = skipJsonWhitespace(raw, index);
|
|
88
|
+
if (depth === 1 && raw[afterKey] === ":" && wanted.has(key.value)) {
|
|
89
|
+
const valueStart = skipJsonWhitespace(raw, afterKey + 1);
|
|
90
|
+
const value = readJsonStringLiteral(raw, valueStart);
|
|
91
|
+
return value?.value ?? null;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === "{") depth += 1;
|
|
96
|
+
else if (char === "}") depth = Math.max(0, depth - 1);
|
|
97
|
+
index += 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function extractRawCodexHookEventName(rawInput: string): RawCodexHookEventName | null {
|
|
104
|
+
const raw = extractRawJsonStringField(rawInput, [
|
|
105
|
+
"hook_event_name",
|
|
106
|
+
"hookEventName",
|
|
107
|
+
"event",
|
|
108
|
+
"name",
|
|
109
|
+
]);
|
|
110
|
+
return CODEX_HOOK_EVENT_NAMES.includes(raw as RawCodexHookEventName)
|
|
111
|
+
? raw as RawCodexHookEventName
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
@@ -1963,10 +1963,15 @@ function shutdown(signal: string): void {
|
|
|
1963
1963
|
}
|
|
1964
1964
|
|
|
1965
1965
|
async function main(): Promise<void> {
|
|
1966
|
+
if (process.env.NODE_ENV === 'test' && process.env.OMX_NOTIFY_FALLBACK_TEST_FATAL === '1') {
|
|
1967
|
+
throw new Error('test fatal notify fallback failure');
|
|
1968
|
+
}
|
|
1966
1969
|
await mkdir(logsDir, { recursive: true }).catch(() => {});
|
|
1967
1970
|
await mkdir(stateDir, { recursive: true }).catch(() => {});
|
|
1968
1971
|
if (!existsSync(notifyScript)) {
|
|
1972
|
+
const reason = `notify script missing: ${notifyScript}`;
|
|
1969
1973
|
await eventLog({ type: 'watcher_error', reason: 'notify_script_missing', notify_script: notifyScript });
|
|
1974
|
+
process.stderr.write(`notify-fallback-watcher: ${reason}\n`);
|
|
1970
1975
|
process.exit(1);
|
|
1971
1976
|
}
|
|
1972
1977
|
|
|
@@ -2006,10 +2011,12 @@ async function main(): Promise<void> {
|
|
|
2006
2011
|
|
|
2007
2012
|
main().catch(async (err) => {
|
|
2008
2013
|
await mkdir(dirname(logPath), { recursive: true }).catch(() => {});
|
|
2014
|
+
const message = err instanceof Error ? err.message : safeString(err);
|
|
2009
2015
|
await eventLog({
|
|
2010
2016
|
type: 'watcher_error',
|
|
2011
2017
|
reason: 'fatal',
|
|
2012
|
-
error:
|
|
2018
|
+
error: message,
|
|
2013
2019
|
});
|
|
2020
|
+
process.stderr.write(`notify-fallback-watcher: fatal: ${message || 'unknown error'}\n`);
|
|
2014
2021
|
process.exit(1);
|
|
2015
2022
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { describe, it } from 'node:test';
|
|
8
|
+
import { MAX_NOTIFY_ARGV_JSON_BYTES } from '../../hook-payload-guard.js';
|
|
9
|
+
|
|
10
|
+
function notifyHookScriptPath(): string {
|
|
11
|
+
return join(process.cwd(), 'dist', 'scripts', 'notify-hook.js');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('notify-hook raw payload guard', () => {
|
|
15
|
+
it('ignores oversized argv JSON before parsing or writing hook state', async () => {
|
|
16
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-notify-hook-oversized-'));
|
|
17
|
+
try {
|
|
18
|
+
await mkdir(join(cwd, '.omx'), { recursive: true });
|
|
19
|
+
await writeFile(join(cwd, '.omx', 'setup-scope.json'), '{}', 'utf-8');
|
|
20
|
+
const payload = JSON.stringify({
|
|
21
|
+
cwd,
|
|
22
|
+
type: 'agent-turn-complete',
|
|
23
|
+
session_id: 'sess-notify-oversized',
|
|
24
|
+
turn_id: 'turn-notify-oversized',
|
|
25
|
+
input_messages: ['hello'],
|
|
26
|
+
last_assistant_message: 'x'.repeat(MAX_NOTIFY_ARGV_JSON_BYTES + 1),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
execFileSync(process.execPath, [notifyHookScriptPath(), payload], {
|
|
30
|
+
cwd,
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
env: process.env,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(existsSync(join(cwd, '.omx', 'logs')), false);
|
|
36
|
+
assert.equal(existsSync(join(cwd, '.omx', 'state')), false);
|
|
37
|
+
} finally {
|
|
38
|
+
await rm(cwd, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* It must not depend on idle/heartbeat freshness or inferred progress stalls.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { appendFile, mkdir, rename, rm, stat, writeFile } from 'fs/promises';
|
|
11
|
+
import { dirname, join, relative } from 'path';
|
|
11
12
|
import { DEFAULT_MARKER, paneHasActiveTask } from '../tmux-hook-engine.js';
|
|
12
13
|
import { appendTeamDeliveryLog } from '../../team/delivery-log.js';
|
|
13
|
-
import { safeString, asNumber } from './utils.js';
|
|
14
|
+
import { safeString, asNumber, isTerminalPhase } from './utils.js';
|
|
14
15
|
import { readJsonIfExists } from './state-io.js';
|
|
15
16
|
import { logTmuxHookEvent } from './log.js';
|
|
16
17
|
import { evaluatePaneInjectionReadiness, queuePaneInput, sendPaneInput } from './team-tmux-guard.js';
|
|
@@ -21,6 +22,110 @@ const STOP_NUDGE_COOLDOWN_MS = 30_000;
|
|
|
21
22
|
const SOURCE_TYPE = 'worker_stop';
|
|
22
23
|
const LEADER_PANE_MISSING_NO_INJECTION_REASON = 'leader_pane_missing_no_injection';
|
|
23
24
|
const LEADER_PANE_SHELL_NO_INJECTION_REASON = 'leader_pane_shell_no_injection';
|
|
25
|
+
const TEAM_SHUTDOWN_NO_INJECTION_REASON = 'team_state_gone_or_shutdown';
|
|
26
|
+
const TEAM_LOCK_HELD_REASON = 'suppressed_team_lock_held';
|
|
27
|
+
|
|
28
|
+
async function teamStateAllowsWorkerStopNudge(stateDir, teamName) {
|
|
29
|
+
const teamDir = join(stateDir, 'team', teamName);
|
|
30
|
+
if (!existsSync(teamDir)) return false;
|
|
31
|
+
if (existsSync(join(teamDir, 'shutdown.json'))) return false;
|
|
32
|
+
|
|
33
|
+
const phase = await readJsonIfExists(join(teamDir, 'phase.json'), null);
|
|
34
|
+
const currentPhase = safeString(phase?.current_phase || phase?.phase || '').trim();
|
|
35
|
+
if (currentPhase && isTerminalPhase(currentPhase)) return false;
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function acquireTeamStopNudgeLock(teamDir, nowMs, cooldownMs) {
|
|
41
|
+
const lockDir = join(teamDir, 'worker-stop-nudge.lock');
|
|
42
|
+
const staleAfterMs = Math.max(cooldownMs, 30_000);
|
|
43
|
+
|
|
44
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
45
|
+
try {
|
|
46
|
+
await mkdir(lockDir);
|
|
47
|
+
await writeFile(join(lockDir, 'owner.json'), JSON.stringify({
|
|
48
|
+
pid: process.pid,
|
|
49
|
+
acquired_at_ms: nowMs,
|
|
50
|
+
acquired_at: new Date(nowMs).toISOString(),
|
|
51
|
+
}, null, 2)).catch(() => {});
|
|
52
|
+
return { acquired: true, lockDir };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === 'ENOENT') return { acquired: false, reason: TEAM_SHUTDOWN_NO_INJECTION_REASON };
|
|
55
|
+
if (error?.code !== 'EEXIST') return { acquired: false, reason: TEAM_LOCK_HELD_REASON };
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const lockStat = await stat(lockDir);
|
|
59
|
+
if ((nowMs - lockStat.mtimeMs) >= staleAfterMs) {
|
|
60
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return { acquired: false, reason: TEAM_LOCK_HELD_REASON };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { acquired: false, reason: TEAM_LOCK_HELD_REASON };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function releaseTeamStopNudgeLock(lockDir) {
|
|
74
|
+
if (!lockDir) return;
|
|
75
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function ensureDirectoryUnderExistingTeam(teamDir, targetDir) {
|
|
79
|
+
if (!existsSync(teamDir)) return false;
|
|
80
|
+
const rel = relative(teamDir, targetDir);
|
|
81
|
+
if (!rel) return true;
|
|
82
|
+
if (rel.startsWith('..')) throw new Error('state_path_outside_team_dir');
|
|
83
|
+
|
|
84
|
+
let current = teamDir;
|
|
85
|
+
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
86
|
+
if (!existsSync(teamDir)) return false;
|
|
87
|
+
current = join(current, segment);
|
|
88
|
+
try {
|
|
89
|
+
await mkdir(current);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error?.code === 'ENOENT' && !existsSync(teamDir)) return false;
|
|
92
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
93
|
+
const currentStat = await stat(current);
|
|
94
|
+
if (!currentStat.isDirectory()) throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return existsSync(teamDir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function writeStopNudgeStateIfTeamExists(teamDir, statePath, state) {
|
|
101
|
+
const stateDir = dirname(statePath);
|
|
102
|
+
const ready = await ensureDirectoryUnderExistingTeam(teamDir, stateDir);
|
|
103
|
+
if (!ready) return false;
|
|
104
|
+
try {
|
|
105
|
+
const tmpPath = `${statePath}.tmp.${process.pid}`;
|
|
106
|
+
await writeFile(tmpPath, JSON.stringify(state, null, 2));
|
|
107
|
+
await rename(tmpPath, statePath);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error?.code === 'ENOENT' && !existsSync(teamDir)) return false;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
return existsSync(teamDir);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function appendWorkerStopEventIfTeamExists(stateDir, teamName, event) {
|
|
116
|
+
const teamDir = join(stateDir, 'team', teamName);
|
|
117
|
+
const eventsDir = join(teamDir, 'events');
|
|
118
|
+
const ready = await ensureDirectoryUnderExistingTeam(teamDir, eventsDir);
|
|
119
|
+
if (!ready) return false;
|
|
120
|
+
const eventsPath = join(eventsDir, 'events.ndjson');
|
|
121
|
+
try {
|
|
122
|
+
await appendFile(eventsPath, JSON.stringify(event) + '\n');
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code === 'ENOENT' && !existsSync(teamDir)) return false;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return existsSync(teamDir);
|
|
128
|
+
}
|
|
24
129
|
|
|
25
130
|
function resolveWorkerStopCooldownMs() {
|
|
26
131
|
const raw = safeString(process.env.OMX_TEAM_WORKER_STOP_COOLDOWN_MS || '');
|
|
@@ -42,20 +147,6 @@ async function resolveCanonicalLeaderPaneId(leaderPaneId) {
|
|
|
42
147
|
return normalizedLeaderPaneId;
|
|
43
148
|
}
|
|
44
149
|
|
|
45
|
-
async function appendWorkerStopEvent(stateDir, teamName, event) {
|
|
46
|
-
const eventsDir = join(stateDir, 'team', teamName, 'events');
|
|
47
|
-
const eventsPath = join(eventsDir, 'events.ndjson');
|
|
48
|
-
await mkdir(eventsDir, { recursive: true }).catch(() => {});
|
|
49
|
-
await appendFile(eventsPath, JSON.stringify(event) + '\n').catch(() => {});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function writeStopNudgeState(statePath, state) {
|
|
53
|
-
await mkdir(dirname(statePath), { recursive: true }).catch(() => {});
|
|
54
|
-
const tmpPath = `${statePath}.tmp.${process.pid}`;
|
|
55
|
-
await writeFile(tmpPath, JSON.stringify(state, null, 2));
|
|
56
|
-
await rename(tmpPath, statePath);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
150
|
async function recordDeferred({
|
|
60
151
|
stateDir,
|
|
61
152
|
logsDir,
|
|
@@ -69,23 +160,26 @@ async function recordDeferred({
|
|
|
69
160
|
paneCurrentCommand = '',
|
|
70
161
|
}) {
|
|
71
162
|
const nowIso = nextState.last_notified_at;
|
|
72
|
-
|
|
163
|
+
const teamDir = join(stateDir, 'team', teamName);
|
|
164
|
+
const wroteState = await writeStopNudgeStateIfTeamExists(teamDir, statePath, {
|
|
73
165
|
...nextState,
|
|
74
166
|
delivery: 'deferred',
|
|
75
167
|
reason,
|
|
76
168
|
pane_current_command: paneCurrentCommand || null,
|
|
77
169
|
}).catch(() => {});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
170
|
+
if (wroteState) {
|
|
171
|
+
await appendWorkerStopEventIfTeamExists(stateDir, teamName, {
|
|
172
|
+
event_id: `worker-stop-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
173
|
+
team: teamName,
|
|
174
|
+
type: 'worker_stop_leader_nudge',
|
|
175
|
+
worker: workerName,
|
|
176
|
+
to_worker: 'leader-fixed',
|
|
177
|
+
delivery: 'deferred',
|
|
178
|
+
reason,
|
|
179
|
+
created_at: nowIso,
|
|
180
|
+
source_type: SOURCE_TYPE,
|
|
181
|
+
}).catch(() => {});
|
|
182
|
+
}
|
|
89
183
|
await logTmuxHookEvent(logsDir, {
|
|
90
184
|
timestamp: nowIso,
|
|
91
185
|
type: 'leader_notification_deferred',
|
|
@@ -119,30 +213,58 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
|
|
|
119
213
|
const { teamName, workerName } = workerContext || {};
|
|
120
214
|
if (!teamName || !workerName || !stateDir) return { ok: false, result: 'unresolved' };
|
|
121
215
|
|
|
122
|
-
const
|
|
216
|
+
const teamDir = join(stateDir, 'team', teamName);
|
|
217
|
+
const workerDir = join(teamDir, 'workers', workerName);
|
|
123
218
|
const statePath = join(workerDir, 'worker-stop-nudge.json');
|
|
219
|
+
const teamStatePath = join(teamDir, 'worker-stop-nudge.json');
|
|
124
220
|
const nowMs = Date.now();
|
|
125
221
|
const nowIso = new Date(nowMs).toISOString();
|
|
222
|
+
const cooldownMs = resolveWorkerStopCooldownMs();
|
|
223
|
+
const nextState = {
|
|
224
|
+
last_notified_at_ms: nowMs,
|
|
225
|
+
last_notified_at: nowIso,
|
|
226
|
+
team: teamName,
|
|
227
|
+
worker: workerName,
|
|
228
|
+
source_type: SOURCE_TYPE,
|
|
229
|
+
};
|
|
230
|
+
let tmuxSession = '';
|
|
231
|
+
let leaderPaneId = '';
|
|
232
|
+
|
|
233
|
+
if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
|
|
234
|
+
return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const lock = await acquireTeamStopNudgeLock(teamDir, nowMs, cooldownMs);
|
|
238
|
+
if (!lock.acquired) {
|
|
239
|
+
return { ok: true, result: lock.reason || TEAM_LOCK_HELD_REASON };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
|
|
244
|
+
return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const teamExisting = (await readJsonIfExists(teamStatePath, null)) || {};
|
|
248
|
+
const teamLastNotifiedMs = asNumber(teamExisting.last_notified_at_ms) ?? 0;
|
|
249
|
+
if ((nowMs - teamLastNotifiedMs) < cooldownMs) {
|
|
250
|
+
return { ok: true, result: 'suppressed_team_cooldown' };
|
|
251
|
+
}
|
|
252
|
+
|
|
126
253
|
const existing = (await readJsonIfExists(statePath, null)) || {};
|
|
127
254
|
const lastNotifiedMs = asNumber(existing.last_notified_at_ms) ?? 0;
|
|
128
|
-
const cooldownMs = resolveWorkerStopCooldownMs();
|
|
129
255
|
if ((nowMs - lastNotifiedMs) < cooldownMs) {
|
|
130
256
|
return { ok: true, result: 'suppressed_cooldown' };
|
|
131
257
|
}
|
|
132
258
|
|
|
133
259
|
const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
|
|
134
260
|
if (!teamInfo) return { ok: false, result: 'unresolved' };
|
|
135
|
-
|
|
261
|
+
({ tmuxSession, leaderPaneId } = teamInfo);
|
|
136
262
|
const tmuxTarget = await resolveCanonicalLeaderPaneId(leaderPaneId);
|
|
137
|
-
const nextState = {
|
|
138
|
-
last_notified_at_ms: nowMs,
|
|
139
|
-
last_notified_at: nowIso,
|
|
140
|
-
team: teamName,
|
|
141
|
-
worker: workerName,
|
|
142
|
-
source_type: SOURCE_TYPE,
|
|
143
|
-
};
|
|
144
263
|
|
|
145
264
|
if (!tmuxTarget) {
|
|
265
|
+
if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
|
|
266
|
+
return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
|
|
267
|
+
}
|
|
146
268
|
await recordDeferred({
|
|
147
269
|
stateDir,
|
|
148
270
|
logsDir,
|
|
@@ -164,6 +286,9 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
|
|
|
164
286
|
requireIdle: false,
|
|
165
287
|
});
|
|
166
288
|
if (!paneGuard.ok) {
|
|
289
|
+
if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
|
|
290
|
+
return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
|
|
291
|
+
}
|
|
167
292
|
await recordDeferred({
|
|
168
293
|
stateDir,
|
|
169
294
|
logsDir,
|
|
@@ -179,12 +304,15 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
|
|
|
179
304
|
return { ok: true, result: 'deferred' };
|
|
180
305
|
}
|
|
181
306
|
|
|
307
|
+
if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
|
|
308
|
+
return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
|
|
309
|
+
}
|
|
310
|
+
|
|
182
311
|
const prompt =
|
|
183
312
|
`[OMX] ${workerName} native Stop allowed. `
|
|
184
313
|
+ `Run \`omx team status ${teamName}\`, read worker messages/results, then assign next task, reconcile completion, or shut down. `
|
|
185
314
|
+ DEFAULT_MARKER;
|
|
186
315
|
|
|
187
|
-
try {
|
|
188
316
|
const leaderHasActiveTask = paneHasActiveTask(paneGuard.paneCapture);
|
|
189
317
|
let deliveryMode = 'sent';
|
|
190
318
|
if (leaderHasActiveTask) {
|
|
@@ -204,22 +332,28 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
|
|
|
204
332
|
if (!sendResult.ok) throw new Error(sendResult.error || sendResult.reason || 'send_failed');
|
|
205
333
|
}
|
|
206
334
|
|
|
207
|
-
|
|
335
|
+
const deliveryState = {
|
|
208
336
|
...nextState,
|
|
209
337
|
delivery: deliveryMode,
|
|
210
338
|
leader_pane_id: leaderPaneId || null,
|
|
211
339
|
tmux_target: tmuxTarget,
|
|
212
|
-
}
|
|
213
|
-
await
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
340
|
+
};
|
|
341
|
+
const wroteWorkerState = await writeStopNudgeStateIfTeamExists(teamDir, statePath, deliveryState);
|
|
342
|
+
const wroteTeamState = wroteWorkerState
|
|
343
|
+
? await writeStopNudgeStateIfTeamExists(teamDir, teamStatePath, deliveryState)
|
|
344
|
+
: false;
|
|
345
|
+
if (wroteTeamState) {
|
|
346
|
+
await appendWorkerStopEventIfTeamExists(stateDir, teamName, {
|
|
347
|
+
event_id: `worker-stop-nudge-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
348
|
+
team: teamName,
|
|
349
|
+
type: 'worker_stop_leader_nudge',
|
|
350
|
+
worker: workerName,
|
|
351
|
+
to_worker: 'leader-fixed',
|
|
352
|
+
delivery: deliveryMode,
|
|
353
|
+
created_at: nowIso,
|
|
354
|
+
source_type: SOURCE_TYPE,
|
|
355
|
+
}).catch(() => {});
|
|
356
|
+
}
|
|
223
357
|
await logTmuxHookEvent(logsDir, {
|
|
224
358
|
timestamp: nowIso,
|
|
225
359
|
type: 'worker_stop_leader_nudge',
|
|
@@ -241,6 +375,11 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
|
|
|
241
375
|
}).catch(() => {});
|
|
242
376
|
return { ok: true, result: deliveryMode };
|
|
243
377
|
} catch (err) {
|
|
378
|
+
if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
|
|
379
|
+
return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
|
|
380
|
+
}
|
|
381
|
+
// Worker Stop is already allowed before this helper runs; nudge failures are
|
|
382
|
+
// surfaced as deferred operational evidence instead of re-blocking Stop.
|
|
244
383
|
await recordDeferred({
|
|
245
384
|
stateDir,
|
|
246
385
|
logsDir,
|
|
@@ -253,5 +392,7 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
|
|
|
253
392
|
leaderPaneId,
|
|
254
393
|
});
|
|
255
394
|
return { ok: true, result: 'deferred' };
|
|
395
|
+
} finally {
|
|
396
|
+
await releaseTeamStopNudgeLock(lock.lockDir);
|
|
256
397
|
}
|
|
257
398
|
}
|
|
@@ -68,6 +68,11 @@ import {
|
|
|
68
68
|
} from './notify-hook/team-worker.js';
|
|
69
69
|
import { DEFAULT_MARKER } from './tmux-hook-engine.js';
|
|
70
70
|
import { sameFilePath } from '../utils/paths.js';
|
|
71
|
+
import {
|
|
72
|
+
MAX_NOTIFY_ARGV_JSON_BYTES,
|
|
73
|
+
extractRawJsonStringField,
|
|
74
|
+
utf8ByteLength,
|
|
75
|
+
} from './hook-payload-guard.js';
|
|
71
76
|
|
|
72
77
|
const RALPH_ACTIVE_PROGRESS_PHASES = new Set([
|
|
73
78
|
'start',
|
|
@@ -285,6 +290,9 @@ async function main() {
|
|
|
285
290
|
if (!rawPayload || rawPayload.startsWith('-')) {
|
|
286
291
|
process.exit(0);
|
|
287
292
|
}
|
|
293
|
+
if (utf8ByteLength(rawPayload) > MAX_NOTIFY_ARGV_JSON_BYTES) {
|
|
294
|
+
process.exit(0);
|
|
295
|
+
}
|
|
288
296
|
|
|
289
297
|
let payload;
|
|
290
298
|
try {
|
|
@@ -909,8 +917,12 @@ async function logFatalNotifyHookError(err: unknown): Promise<void> {
|
|
|
909
917
|
try {
|
|
910
918
|
const rawPayload = process.argv[process.argv.length - 1];
|
|
911
919
|
if (rawPayload && !rawPayload.startsWith('-')) {
|
|
912
|
-
|
|
913
|
-
|
|
920
|
+
if (utf8ByteLength(rawPayload) <= MAX_NOTIFY_ARGV_JSON_BYTES) {
|
|
921
|
+
const payload = JSON.parse(rawPayload) as Record<string, unknown>;
|
|
922
|
+
cwd = safeString(payload.cwd || payload['cwd'] || cwd) || cwd;
|
|
923
|
+
} else {
|
|
924
|
+
cwd = extractRawJsonStringField(rawPayload, ['cwd']) || cwd;
|
|
925
|
+
}
|
|
914
926
|
}
|
|
915
927
|
} catch {
|
|
916
928
|
// Keep notification hook failures silent in Codex TUI surfaces.
|