oh-my-codex 0.13.0 → 0.13.2
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 +5 -5
- package/Cargo.toml +1 -1
- package/README.md +40 -6
- package/crates/omx-explore/src/main.rs +221 -10
- package/dist/catalog/__tests__/generator.test.js +2 -0
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +150 -1
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +41 -3
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +25 -1
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +73 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/update.js +1 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +20 -2
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/analyze-routing-contract.test.js +36 -0
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.js +48 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +32 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +185 -8
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +26 -0
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +44 -0
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +126 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +21 -0
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +9 -0
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +8 -1
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +9 -0
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +55 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +23 -4
- package/dist/hud/state.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +38 -0
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +1 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +11 -3
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js +34 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts +1 -0
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js +14 -2
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +248 -15
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/generate-release-body.test.d.ts +2 -0
- package/dist/scripts/__tests__/generate-release-body.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js +144 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +39 -49
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-release-body.d.ts +34 -0
- package/dist/scripts/generate-release-body.d.ts.map +1 -0
- package/dist/scripts/generate-release-body.js +249 -0
- package/dist/scripts/generate-release-body.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +43 -20
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/active-team.d.ts.map +1 -1
- package/dist/scripts/notify-hook/active-team.js +2 -1
- package/dist/scripts/notify-hook/active-team.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +17 -2
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +16 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +26 -5
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook.js +1 -7
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +6 -0
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +1 -1
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.d.ts +2 -0
- package/dist/team/__tests__/worker-runtime-identity.test.d.ts.map +1 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js +250 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -0
- package/dist/team/leader-activity.d.ts.map +1 -1
- package/dist/team/leader-activity.js +26 -15
- package/dist/team/leader-activity.js.map +1 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +9 -8
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +10 -9
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +3 -2
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +3 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/wiki/__tests__/slug-nonascii.test.js +11 -5
- package/dist/wiki/__tests__/slug-nonascii.test.js.map +1 -1
- package/dist/wiki/storage.d.ts.map +1 -1
- package/dist/wiki/storage.js +2 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +3 -1
- package/skills/analyze/SKILL.md +101 -134
- package/src/scripts/__tests__/codex-native-hook.test.ts +297 -17
- package/src/scripts/__tests__/generate-release-body.test.ts +166 -0
- package/src/scripts/codex-native-hook.ts +99 -66
- package/src/scripts/generate-release-body.ts +295 -0
- package/src/scripts/notify-fallback-watcher.ts +44 -21
- package/src/scripts/notify-hook/active-team.ts +2 -1
- package/src/scripts/notify-hook/ralph-session-resume.ts +17 -2
- package/src/scripts/notify-hook/state-io.ts +16 -0
- package/src/scripts/notify-hook/team-leader-nudge.ts +24 -4
- package/src/scripts/notify-hook.ts +1 -6
- package/templates/AGENTS.md +1 -1
- package/templates/catalog-manifest.json +2 -4
|
@@ -324,6 +324,89 @@ describe("codex native hook dispatch", () => {
|
|
|
324
324
|
}
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
+
it("starts a fresh native session without inheriting stale task-scoped context", async () => {
|
|
328
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-isolation-"));
|
|
329
|
+
try {
|
|
330
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
331
|
+
const priorSessionId = "omx-old-session";
|
|
332
|
+
await mkdir(join(stateDir, "sessions", priorSessionId), { recursive: true });
|
|
333
|
+
await writeSessionStart(cwd, priorSessionId, {
|
|
334
|
+
nativeSessionId: "codex-native-old",
|
|
335
|
+
});
|
|
336
|
+
await writeJson(join(stateDir, "sessions", priorSessionId, "ralph-state.json"), {
|
|
337
|
+
active: true,
|
|
338
|
+
current_phase: "executing",
|
|
339
|
+
});
|
|
340
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
341
|
+
schemaVersion: 1,
|
|
342
|
+
sessions: {
|
|
343
|
+
[priorSessionId]: {
|
|
344
|
+
session_id: priorSessionId,
|
|
345
|
+
leader_thread_id: "leader-1",
|
|
346
|
+
updated_at: new Date().toISOString(),
|
|
347
|
+
threads: {
|
|
348
|
+
"leader-1": {
|
|
349
|
+
thread_id: "leader-1",
|
|
350
|
+
kind: "leader",
|
|
351
|
+
first_seen_at: new Date().toISOString(),
|
|
352
|
+
last_seen_at: new Date().toISOString(),
|
|
353
|
+
turn_count: 1,
|
|
354
|
+
},
|
|
355
|
+
"sub-1": {
|
|
356
|
+
thread_id: "sub-1",
|
|
357
|
+
kind: "subagent",
|
|
358
|
+
first_seen_at: new Date().toISOString(),
|
|
359
|
+
last_seen_at: new Date().toISOString(),
|
|
360
|
+
turn_count: 1,
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
await writeFile(
|
|
367
|
+
join(cwd, ".omx", "notepad.md"),
|
|
368
|
+
[
|
|
369
|
+
"# OMX Notepad",
|
|
370
|
+
"",
|
|
371
|
+
"## PRIORITY",
|
|
372
|
+
"Preserve durable project guidance.",
|
|
373
|
+
"",
|
|
374
|
+
"## WORKING MEMORY",
|
|
375
|
+
"[2026-04-06T00:33:44Z] stale UI rework context snapshot .omx/context/ui-rework-plan-01-20260406T003344Z.md",
|
|
376
|
+
].join("\n"),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const result = await dispatchCodexNativeHook(
|
|
380
|
+
{
|
|
381
|
+
hook_event_name: "SessionStart",
|
|
382
|
+
cwd,
|
|
383
|
+
session_id: "codex-native-new",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
cwd,
|
|
387
|
+
sessionOwnerPid: process.pid,
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const sessionState = JSON.parse(
|
|
392
|
+
await readFile(join(stateDir, "session.json"), "utf-8"),
|
|
393
|
+
) as { session_id?: string; native_session_id?: string };
|
|
394
|
+
assert.equal(sessionState.session_id, "codex-native-new");
|
|
395
|
+
assert.equal(sessionState.native_session_id, "codex-native-new");
|
|
396
|
+
|
|
397
|
+
const additionalContext = String(
|
|
398
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
399
|
+
);
|
|
400
|
+
assert.match(additionalContext, /\[Priority notes\]/);
|
|
401
|
+
assert.match(additionalContext, /Preserve durable project guidance/);
|
|
402
|
+
assert.doesNotMatch(additionalContext, /stale UI rework context snapshot/);
|
|
403
|
+
assert.doesNotMatch(additionalContext, /\[Subagents\]/);
|
|
404
|
+
assert.doesNotMatch(additionalContext, /ralph phase: executing/);
|
|
405
|
+
} finally {
|
|
406
|
+
await rm(cwd, { recursive: true, force: true });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
327
410
|
it("resolves the Codex owner from ancestry without mistaking codex-native-hook wrappers for Codex", () => {
|
|
328
411
|
const commands = new Map<number, string>([
|
|
329
412
|
[2100, 'sh -c node "/repo/dist/scripts/codex-native-hook.js"'],
|
|
@@ -381,6 +464,33 @@ describe("codex native hook dispatch", () => {
|
|
|
381
464
|
}
|
|
382
465
|
});
|
|
383
466
|
|
|
467
|
+
it("does not activate Ralph workflow state from a plain conversational mention", async () => {
|
|
468
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-plain-text-"));
|
|
469
|
+
try {
|
|
470
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
471
|
+
const result = await dispatchCodexNativeHook(
|
|
472
|
+
{
|
|
473
|
+
hook_event_name: "UserPromptSubmit",
|
|
474
|
+
cwd,
|
|
475
|
+
session_id: "sess-ralph-plain-text",
|
|
476
|
+
thread_id: "thread-ralph-plain-text",
|
|
477
|
+
turn_id: "turn-ralph-plain-text",
|
|
478
|
+
prompt: "why does ralph keep blocking stop?",
|
|
479
|
+
},
|
|
480
|
+
{ cwd },
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
484
|
+
assert.equal(result.skillState, null);
|
|
485
|
+
assert.equal(result.outputJson, null);
|
|
486
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
487
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "skill-active-state.json")), false);
|
|
488
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "ralph-state.json")), false);
|
|
489
|
+
} finally {
|
|
490
|
+
await rm(cwd, { recursive: true, force: true });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
384
494
|
it("clarifies that prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
|
|
385
495
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-routing-"));
|
|
386
496
|
try {
|
|
@@ -2028,7 +2138,13 @@ esac
|
|
|
2028
2138
|
{ cwd },
|
|
2029
2139
|
);
|
|
2030
2140
|
|
|
2031
|
-
assert.
|
|
2141
|
+
assert.deepEqual(result.outputJson, {
|
|
2142
|
+
decision: "block",
|
|
2143
|
+
reason:
|
|
2144
|
+
`OMX team pipeline is still active (worker-stop-team-terminal) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
2145
|
+
stopReason: "team_team-exec",
|
|
2146
|
+
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
2147
|
+
});
|
|
2032
2148
|
} finally {
|
|
2033
2149
|
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
2034
2150
|
else delete process.env.OMX_TEAM_WORKER;
|
|
@@ -2859,8 +2975,9 @@ esac
|
|
|
2859
2975
|
}
|
|
2860
2976
|
});
|
|
2861
2977
|
|
|
2862
|
-
it("
|
|
2863
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-
|
|
2978
|
+
it("keeps blocking Ralph Stop replays until the active task advances", async () => {
|
|
2979
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-replay-"));
|
|
2980
|
+
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
2864
2981
|
try {
|
|
2865
2982
|
const stateDir = join(cwd, ".omx", "state");
|
|
2866
2983
|
await mkdir(stateDir, { recursive: true });
|
|
@@ -2872,23 +2989,42 @@ esac
|
|
|
2872
2989
|
}),
|
|
2873
2990
|
);
|
|
2874
2991
|
|
|
2875
|
-
|
|
2992
|
+
process.env.OMX_SESSION_ID = "sess-stop-ralph-replay";
|
|
2993
|
+
const payload = {
|
|
2994
|
+
hook_event_name: "Stop",
|
|
2995
|
+
cwd,
|
|
2996
|
+
last_assistant_message: "Next active targets:\n\n1. scheduler integration\n\nI am continuing.",
|
|
2997
|
+
};
|
|
2998
|
+
const expected = {
|
|
2999
|
+
decision: "block",
|
|
3000
|
+
reason:
|
|
3001
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3002
|
+
stopReason: "ralph_executing",
|
|
3003
|
+
systemMessage:
|
|
3004
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3005
|
+
};
|
|
3006
|
+
|
|
3007
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
3008
|
+
const replay = await dispatchCodexNativeHook(
|
|
2876
3009
|
{
|
|
2877
|
-
|
|
2878
|
-
cwd,
|
|
2879
|
-
session_id: "sess-stop-ralph-once",
|
|
3010
|
+
...payload,
|
|
2880
3011
|
stop_hook_active: true,
|
|
2881
3012
|
},
|
|
2882
3013
|
{ cwd },
|
|
2883
3014
|
);
|
|
2884
3015
|
|
|
2885
|
-
assert.equal(
|
|
2886
|
-
assert.
|
|
3016
|
+
assert.equal(first.omxEventName, "stop");
|
|
3017
|
+
assert.deepEqual(first.outputJson, expected);
|
|
3018
|
+
assert.equal(replay.omxEventName, "stop");
|
|
3019
|
+
assert.deepEqual(replay.outputJson, expected);
|
|
2887
3020
|
} finally {
|
|
3021
|
+
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3022
|
+
else delete process.env.OMX_SESSION_ID;
|
|
2888
3023
|
await rm(cwd, { recursive: true, force: true });
|
|
2889
3024
|
}
|
|
2890
3025
|
});
|
|
2891
3026
|
|
|
3027
|
+
|
|
2892
3028
|
it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
|
|
2893
3029
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
2894
3030
|
try {
|
|
@@ -2919,7 +3055,7 @@ esac
|
|
|
2919
3055
|
}
|
|
2920
3056
|
});
|
|
2921
3057
|
|
|
2922
|
-
it("
|
|
3058
|
+
it("re-blocks duplicate native auto-nudge replays for the same Stop reply", async () => {
|
|
2923
3059
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-once-"));
|
|
2924
3060
|
try {
|
|
2925
3061
|
const stateDir = join(cwd, ".omx", "state");
|
|
@@ -2952,13 +3088,19 @@ esac
|
|
|
2952
3088
|
);
|
|
2953
3089
|
|
|
2954
3090
|
assert.equal(result.omxEventName, "stop");
|
|
2955
|
-
assert.
|
|
3091
|
+
assert.deepEqual(result.outputJson, {
|
|
3092
|
+
decision: "block",
|
|
3093
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3094
|
+
stopReason: "auto_nudge",
|
|
3095
|
+
systemMessage:
|
|
3096
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3097
|
+
});
|
|
2956
3098
|
} finally {
|
|
2957
3099
|
await rm(cwd, { recursive: true, force: true });
|
|
2958
3100
|
}
|
|
2959
3101
|
});
|
|
2960
3102
|
|
|
2961
|
-
it("
|
|
3103
|
+
it("re-blocks duplicate native auto-nudge replays across native/canonical session-id drift", async () => {
|
|
2962
3104
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-session-drift-"));
|
|
2963
3105
|
try {
|
|
2964
3106
|
const stateDir = join(cwd, ".omx", "state");
|
|
@@ -2995,7 +3137,13 @@ esac
|
|
|
2995
3137
|
);
|
|
2996
3138
|
|
|
2997
3139
|
assert.equal(result.omxEventName, "stop");
|
|
2998
|
-
assert.
|
|
3140
|
+
assert.deepEqual(result.outputJson, {
|
|
3141
|
+
decision: "block",
|
|
3142
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3143
|
+
stopReason: "auto_nudge",
|
|
3144
|
+
systemMessage:
|
|
3145
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3146
|
+
});
|
|
2999
3147
|
|
|
3000
3148
|
const persisted = JSON.parse(
|
|
3001
3149
|
await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
|
|
@@ -3382,7 +3530,7 @@ esac
|
|
|
3382
3530
|
}
|
|
3383
3531
|
});
|
|
3384
3532
|
|
|
3385
|
-
it("
|
|
3533
|
+
it("auto-continues native Stop for permission-seeking prompts even outside OMX runtime", async () => {
|
|
3386
3534
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-plain-session-"));
|
|
3387
3535
|
try {
|
|
3388
3536
|
await dispatchCodexNativeHook(
|
|
@@ -3404,13 +3552,19 @@ esac
|
|
|
3404
3552
|
session_id: "plain-stop-session",
|
|
3405
3553
|
thread_id: "plain-thread",
|
|
3406
3554
|
turn_id: "plain-turn-1",
|
|
3407
|
-
last_assistant_message: "
|
|
3555
|
+
last_assistant_message: "If you want, I can continue with the cleanup from here.",
|
|
3408
3556
|
},
|
|
3409
3557
|
{ cwd },
|
|
3410
3558
|
);
|
|
3411
3559
|
|
|
3412
3560
|
assert.equal(result.omxEventName, "stop");
|
|
3413
|
-
assert.
|
|
3561
|
+
assert.deepEqual(result.outputJson, {
|
|
3562
|
+
decision: "block",
|
|
3563
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3564
|
+
stopReason: "auto_nudge",
|
|
3565
|
+
systemMessage:
|
|
3566
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3567
|
+
});
|
|
3414
3568
|
} finally {
|
|
3415
3569
|
await rm(cwd, { recursive: true, force: true });
|
|
3416
3570
|
}
|
|
@@ -3518,7 +3672,13 @@ esac
|
|
|
3518
3672
|
);
|
|
3519
3673
|
|
|
3520
3674
|
assert.equal(duplicate.omxEventName, "stop");
|
|
3521
|
-
assert.
|
|
3675
|
+
assert.deepEqual(duplicate.outputJson, {
|
|
3676
|
+
decision: "block",
|
|
3677
|
+
reason:
|
|
3678
|
+
`OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
3679
|
+
stopReason: "team_team-verify",
|
|
3680
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
3681
|
+
});
|
|
3522
3682
|
|
|
3523
3683
|
const fresh = await dispatchCodexNativeHook(
|
|
3524
3684
|
{
|
|
@@ -3550,6 +3710,126 @@ esac
|
|
|
3550
3710
|
}
|
|
3551
3711
|
});
|
|
3552
3712
|
|
|
3713
|
+
it("re-blocks active execution modes on repeated Stop hooks", async () => {
|
|
3714
|
+
const cases = [
|
|
3715
|
+
{
|
|
3716
|
+
mode: "autopilot",
|
|
3717
|
+
phase: "execution",
|
|
3718
|
+
reason:
|
|
3719
|
+
"OMX autopilot is still active (phase: execution); continue the task and gather fresh verification evidence before stopping.",
|
|
3720
|
+
},
|
|
3721
|
+
{
|
|
3722
|
+
mode: "ultrawork",
|
|
3723
|
+
phase: "executing",
|
|
3724
|
+
reason:
|
|
3725
|
+
"OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3726
|
+
},
|
|
3727
|
+
{
|
|
3728
|
+
mode: "ultraqa",
|
|
3729
|
+
phase: "diagnose",
|
|
3730
|
+
reason:
|
|
3731
|
+
"OMX ultraqa is still active (phase: diagnose); continue the task and gather fresh verification evidence before stopping.",
|
|
3732
|
+
},
|
|
3733
|
+
] as const;
|
|
3734
|
+
|
|
3735
|
+
for (const testCase of cases) {
|
|
3736
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-${testCase.mode}-repeat-`));
|
|
3737
|
+
try {
|
|
3738
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3739
|
+
await mkdir(stateDir, { recursive: true });
|
|
3740
|
+
await writeJson(join(stateDir, `${testCase.mode}-state.json`), {
|
|
3741
|
+
active: true,
|
|
3742
|
+
current_phase: testCase.phase,
|
|
3743
|
+
});
|
|
3744
|
+
|
|
3745
|
+
await dispatchCodexNativeHook(
|
|
3746
|
+
{
|
|
3747
|
+
hook_event_name: "Stop",
|
|
3748
|
+
cwd,
|
|
3749
|
+
session_id: `sess-stop-${testCase.mode}-repeat`,
|
|
3750
|
+
thread_id: `thread-stop-${testCase.mode}-repeat`,
|
|
3751
|
+
turn_id: `turn-stop-${testCase.mode}-repeat-1`,
|
|
3752
|
+
},
|
|
3753
|
+
{ cwd },
|
|
3754
|
+
);
|
|
3755
|
+
|
|
3756
|
+
const repeated = await dispatchCodexNativeHook(
|
|
3757
|
+
{
|
|
3758
|
+
hook_event_name: "Stop",
|
|
3759
|
+
cwd,
|
|
3760
|
+
session_id: `sess-stop-${testCase.mode}-repeat`,
|
|
3761
|
+
thread_id: `thread-stop-${testCase.mode}-repeat`,
|
|
3762
|
+
turn_id: `turn-stop-${testCase.mode}-repeat-1`,
|
|
3763
|
+
stop_hook_active: true,
|
|
3764
|
+
},
|
|
3765
|
+
{ cwd },
|
|
3766
|
+
);
|
|
3767
|
+
|
|
3768
|
+
assert.equal(repeated.omxEventName, "stop");
|
|
3769
|
+
assert.deepEqual(repeated.outputJson, {
|
|
3770
|
+
decision: "block",
|
|
3771
|
+
reason: testCase.reason,
|
|
3772
|
+
stopReason: `${testCase.mode}_${testCase.phase}`,
|
|
3773
|
+
systemMessage: `OMX ${testCase.mode} is still active (phase: ${testCase.phase}).`,
|
|
3774
|
+
});
|
|
3775
|
+
} finally {
|
|
3776
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
|
|
3781
|
+
it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
|
|
3782
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
|
|
3783
|
+
try {
|
|
3784
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3785
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
|
|
3786
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
|
|
3787
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
|
|
3788
|
+
active: true,
|
|
3789
|
+
skill: "ralplan",
|
|
3790
|
+
phase: "planning",
|
|
3791
|
+
});
|
|
3792
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
|
|
3793
|
+
active: true,
|
|
3794
|
+
current_phase: "planning",
|
|
3795
|
+
});
|
|
3796
|
+
|
|
3797
|
+
await dispatchCodexNativeHook(
|
|
3798
|
+
{
|
|
3799
|
+
hook_event_name: "Stop",
|
|
3800
|
+
cwd,
|
|
3801
|
+
session_id: "sess-stop-skill-repeat",
|
|
3802
|
+
thread_id: "thread-stop-skill-repeat",
|
|
3803
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
3804
|
+
},
|
|
3805
|
+
{ cwd },
|
|
3806
|
+
);
|
|
3807
|
+
|
|
3808
|
+
const repeated = await dispatchCodexNativeHook(
|
|
3809
|
+
{
|
|
3810
|
+
hook_event_name: "Stop",
|
|
3811
|
+
cwd,
|
|
3812
|
+
session_id: "sess-stop-skill-repeat",
|
|
3813
|
+
thread_id: "thread-stop-skill-repeat",
|
|
3814
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
3815
|
+
stop_hook_active: true,
|
|
3816
|
+
},
|
|
3817
|
+
{ cwd },
|
|
3818
|
+
);
|
|
3819
|
+
|
|
3820
|
+
assert.equal(repeated.omxEventName, "stop");
|
|
3821
|
+
assert.deepEqual(repeated.outputJson, {
|
|
3822
|
+
decision: "block",
|
|
3823
|
+
reason:
|
|
3824
|
+
"OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
|
|
3825
|
+
stopReason: "skill_ralplan_planning",
|
|
3826
|
+
systemMessage: "OMX skill ralplan is still active (phase: planning).",
|
|
3827
|
+
});
|
|
3828
|
+
} finally {
|
|
3829
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3553
3833
|
it("does not block Stop from another session's stale root team state when no scoped team state exists", async () => {
|
|
3554
3834
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-team-"));
|
|
3555
3835
|
try {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import {
|
|
8
|
+
buildFullChangelogLine,
|
|
9
|
+
generateReleaseBody,
|
|
10
|
+
renderContributorsSection,
|
|
11
|
+
type Contributor,
|
|
12
|
+
} from '../generate-release-body.js';
|
|
13
|
+
|
|
14
|
+
function git(cwd: string, args: string[], env: NodeJS.ProcessEnv = {}): string {
|
|
15
|
+
const result = spawnSync('git', args, {
|
|
16
|
+
cwd,
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
env: { ...process.env, ...env },
|
|
19
|
+
});
|
|
20
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
21
|
+
return String(result.stdout || '').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TEMPLATE = `# oh-my-codex v0.0.0
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
Custom summary that must stay intact.
|
|
29
|
+
|
|
30
|
+
## Fixed
|
|
31
|
+
|
|
32
|
+
- Keep this handwritten section.
|
|
33
|
+
|
|
34
|
+
## Verification
|
|
35
|
+
|
|
36
|
+
- npm test
|
|
37
|
+
|
|
38
|
+
## Contributors
|
|
39
|
+
|
|
40
|
+
Outdated contributor text.
|
|
41
|
+
|
|
42
|
+
**Full Changelog**: [\`v0.0.0...v0.0.1\`](https://github.com/example/compare/v0.0.0...v0.0.1)
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
describe('generate-release-body', () => {
|
|
46
|
+
it('preserves custom sections while refreshing contributors and compare metadata from git', async () => {
|
|
47
|
+
const root = await mkdtemp(join(tmpdir(), 'omx-generate-release-body-'));
|
|
48
|
+
const originalGitHubRepository = process.env.GITHUB_REPOSITORY;
|
|
49
|
+
try {
|
|
50
|
+
git(root, ['init']);
|
|
51
|
+
git(root, ['config', 'user.name', 'Release Bot']);
|
|
52
|
+
git(root, ['config', 'user.email', 'release@example.com']);
|
|
53
|
+
git(root, ['remote', 'add', 'origin', 'https://github.com/example/oh-my-codex.git']);
|
|
54
|
+
|
|
55
|
+
await writeFile(join(root, 'RELEASE_BODY.md'), TEMPLATE);
|
|
56
|
+
await writeFile(join(root, 'notes.txt'), 'base\n');
|
|
57
|
+
git(root, ['add', '.']);
|
|
58
|
+
git(root, ['commit', '-m', 'base']);
|
|
59
|
+
git(root, ['tag', 'v0.12.0']);
|
|
60
|
+
|
|
61
|
+
await writeFile(join(root, 'notes.txt'), 'alice\n');
|
|
62
|
+
git(root, ['add', 'notes.txt']);
|
|
63
|
+
git(root, ['commit', '-m', 'alice change'], { GIT_AUTHOR_NAME: 'Alice Example', GIT_AUTHOR_EMAIL: 'alice@example.com' });
|
|
64
|
+
|
|
65
|
+
await writeFile(join(root, 'notes.txt'), 'bob\n');
|
|
66
|
+
git(root, ['add', 'notes.txt']);
|
|
67
|
+
git(root, ['commit', '-m', 'bob change'], { GIT_AUTHOR_NAME: 'Bob Example', GIT_AUTHOR_EMAIL: 'bob@example.com' });
|
|
68
|
+
git(root, ['tag', 'v0.13.0']);
|
|
69
|
+
|
|
70
|
+
delete process.env.GITHUB_REPOSITORY;
|
|
71
|
+
await generateReleaseBody({
|
|
72
|
+
cwd: root,
|
|
73
|
+
templatePath: 'RELEASE_BODY.md',
|
|
74
|
+
outPath: 'RELEASE_BODY.generated.md',
|
|
75
|
+
currentTag: 'v0.13.0',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const generated = await readFile(join(root, 'RELEASE_BODY.generated.md'), 'utf-8');
|
|
79
|
+
assert.match(generated, /^# oh-my-codex v0.13.0/m);
|
|
80
|
+
assert.match(generated, /Custom summary that must stay intact\./);
|
|
81
|
+
assert.match(generated, /Keep this handwritten section\./);
|
|
82
|
+
assert.match(generated, /## Contributors\n\nThanks to Alice Example and Bob Example for contributing to this release\./);
|
|
83
|
+
assert.match(generated, /\*\*Full Changelog\*\*: \[`v0\.12\.0\.\.\.v0\.13\.0`\]\(https:\/\/github\.com\/example\/oh-my-codex\/compare\/v0\.12\.0\.\.\.v0\.13\.0\)/);
|
|
84
|
+
} finally {
|
|
85
|
+
if (originalGitHubRepository === undefined) {
|
|
86
|
+
delete process.env.GITHUB_REPOSITORY;
|
|
87
|
+
} else {
|
|
88
|
+
process.env.GITHUB_REPOSITORY = originalGitHubRepository;
|
|
89
|
+
}
|
|
90
|
+
await rm(root, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('prefers GitHub contributor handles when compare metadata is available', async () => {
|
|
95
|
+
const root = await mkdtemp(join(tmpdir(), 'omx-generate-release-body-gh-'));
|
|
96
|
+
try {
|
|
97
|
+
await writeFile(join(root, 'RELEASE_BODY.md'), TEMPLATE);
|
|
98
|
+
const originalFetch = global.fetch;
|
|
99
|
+
global.fetch = (async () => new Response(JSON.stringify({
|
|
100
|
+
commits: [
|
|
101
|
+
{ author: { login: 'alice', html_url: 'https://github.com/alice' } },
|
|
102
|
+
{ author: { login: 'bob', html_url: 'https://github.com/bob' } },
|
|
103
|
+
{ author: { login: 'alice', html_url: 'https://github.com/alice' } },
|
|
104
|
+
],
|
|
105
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } })) as typeof fetch;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await generateReleaseBody({
|
|
109
|
+
cwd: root,
|
|
110
|
+
templatePath: 'RELEASE_BODY.md',
|
|
111
|
+
outPath: 'RELEASE_BODY.generated.md',
|
|
112
|
+
currentTag: 'v0.13.1',
|
|
113
|
+
previousTag: 'v0.13.0',
|
|
114
|
+
repo: 'example/oh-my-codex',
|
|
115
|
+
githubToken: 'test-token',
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
global.fetch = originalFetch;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const generated = await readFile(join(root, 'RELEASE_BODY.generated.md'), 'utf-8');
|
|
122
|
+
assert.match(generated, /Thanks to \[@alice\]\(https:\/\/github\.com\/alice\) and \[@bob\]\(https:\/\/github\.com\/bob\) for contributing to this release\./);
|
|
123
|
+
} finally {
|
|
124
|
+
await rm(root, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
it('fails validation when the template is missing required metadata anchors', async () => {
|
|
130
|
+
const root = await mkdtemp(join(tmpdir(), 'omx-generate-release-body-invalid-'));
|
|
131
|
+
try {
|
|
132
|
+
await writeFile(join(root, 'RELEASE_BODY.md'), `# oh-my-codex v0.0.0
|
|
133
|
+
|
|
134
|
+
## Summary
|
|
135
|
+
|
|
136
|
+
Missing required sections.
|
|
137
|
+
`);
|
|
138
|
+
await assert.rejects(
|
|
139
|
+
generateReleaseBody({
|
|
140
|
+
cwd: root,
|
|
141
|
+
templatePath: 'RELEASE_BODY.md',
|
|
142
|
+
outPath: 'RELEASE_BODY.generated.md',
|
|
143
|
+
currentTag: 'v0.13.1',
|
|
144
|
+
previousTag: 'v0.13.0',
|
|
145
|
+
repo: 'example/oh-my-codex',
|
|
146
|
+
}),
|
|
147
|
+
/missing section: ## Contributors|missing the Full Changelog line/,
|
|
148
|
+
);
|
|
149
|
+
} finally {
|
|
150
|
+
await rm(root, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('renders contributor and changelog helpers for edge cases', () => {
|
|
155
|
+
const contributors: Contributor[] = [];
|
|
156
|
+
assert.equal(renderContributorsSection(contributors), 'Thanks to the contributors who made this release possible.');
|
|
157
|
+
assert.equal(
|
|
158
|
+
buildFullChangelogLine('example/oh-my-codex', 'v0.13.1', 'v0.13.0'),
|
|
159
|
+
'**Full Changelog**: [`v0.13.0...v0.13.1`](https://github.com/example/oh-my-codex/compare/v0.13.0...v0.13.1)',
|
|
160
|
+
);
|
|
161
|
+
assert.equal(
|
|
162
|
+
buildFullChangelogLine('example/oh-my-codex', 'v0.1.0'),
|
|
163
|
+
'**Full Changelog**: [`v0.1.0`](https://github.com/example/oh-my-codex/releases/tag/v0.1.0)',
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|