oh-my-codex 0.13.1 → 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 +2 -0
- 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 +95 -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 +70 -8
- 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__/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/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/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 +178 -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 +23 -44
- 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 +214 -17
- package/src/scripts/__tests__/generate-release-body.test.ts +166 -0
- package/src/scripts/codex-native-hook.ts +81 -61
- 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
|
@@ -464,6 +464,33 @@ describe("codex native hook dispatch", () => {
|
|
|
464
464
|
}
|
|
465
465
|
});
|
|
466
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
|
+
|
|
467
494
|
it("clarifies that prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
|
|
468
495
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-routing-"));
|
|
469
496
|
try {
|
|
@@ -2111,7 +2138,13 @@ esac
|
|
|
2111
2138
|
{ cwd },
|
|
2112
2139
|
);
|
|
2113
2140
|
|
|
2114
|
-
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
|
+
});
|
|
2115
2148
|
} finally {
|
|
2116
2149
|
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
2117
2150
|
else delete process.env.OMX_TEAM_WORKER;
|
|
@@ -2942,8 +2975,9 @@ esac
|
|
|
2942
2975
|
}
|
|
2943
2976
|
});
|
|
2944
2977
|
|
|
2945
|
-
it("
|
|
2946
|
-
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;
|
|
2947
2981
|
try {
|
|
2948
2982
|
const stateDir = join(cwd, ".omx", "state");
|
|
2949
2983
|
await mkdir(stateDir, { recursive: true });
|
|
@@ -2955,23 +2989,42 @@ esac
|
|
|
2955
2989
|
}),
|
|
2956
2990
|
);
|
|
2957
2991
|
|
|
2958
|
-
|
|
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(
|
|
2959
3009
|
{
|
|
2960
|
-
|
|
2961
|
-
cwd,
|
|
2962
|
-
session_id: "sess-stop-ralph-once",
|
|
3010
|
+
...payload,
|
|
2963
3011
|
stop_hook_active: true,
|
|
2964
3012
|
},
|
|
2965
3013
|
{ cwd },
|
|
2966
3014
|
);
|
|
2967
3015
|
|
|
2968
|
-
assert.equal(
|
|
2969
|
-
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);
|
|
2970
3020
|
} finally {
|
|
3021
|
+
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3022
|
+
else delete process.env.OMX_SESSION_ID;
|
|
2971
3023
|
await rm(cwd, { recursive: true, force: true });
|
|
2972
3024
|
}
|
|
2973
3025
|
});
|
|
2974
3026
|
|
|
3027
|
+
|
|
2975
3028
|
it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
|
|
2976
3029
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
2977
3030
|
try {
|
|
@@ -3002,7 +3055,7 @@ esac
|
|
|
3002
3055
|
}
|
|
3003
3056
|
});
|
|
3004
3057
|
|
|
3005
|
-
it("
|
|
3058
|
+
it("re-blocks duplicate native auto-nudge replays for the same Stop reply", async () => {
|
|
3006
3059
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-once-"));
|
|
3007
3060
|
try {
|
|
3008
3061
|
const stateDir = join(cwd, ".omx", "state");
|
|
@@ -3035,13 +3088,19 @@ esac
|
|
|
3035
3088
|
);
|
|
3036
3089
|
|
|
3037
3090
|
assert.equal(result.omxEventName, "stop");
|
|
3038
|
-
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
|
+
});
|
|
3039
3098
|
} finally {
|
|
3040
3099
|
await rm(cwd, { recursive: true, force: true });
|
|
3041
3100
|
}
|
|
3042
3101
|
});
|
|
3043
3102
|
|
|
3044
|
-
it("
|
|
3103
|
+
it("re-blocks duplicate native auto-nudge replays across native/canonical session-id drift", async () => {
|
|
3045
3104
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-session-drift-"));
|
|
3046
3105
|
try {
|
|
3047
3106
|
const stateDir = join(cwd, ".omx", "state");
|
|
@@ -3078,7 +3137,13 @@ esac
|
|
|
3078
3137
|
);
|
|
3079
3138
|
|
|
3080
3139
|
assert.equal(result.omxEventName, "stop");
|
|
3081
|
-
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
|
+
});
|
|
3082
3147
|
|
|
3083
3148
|
const persisted = JSON.parse(
|
|
3084
3149
|
await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
|
|
@@ -3465,7 +3530,7 @@ esac
|
|
|
3465
3530
|
}
|
|
3466
3531
|
});
|
|
3467
3532
|
|
|
3468
|
-
it("
|
|
3533
|
+
it("auto-continues native Stop for permission-seeking prompts even outside OMX runtime", async () => {
|
|
3469
3534
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-plain-session-"));
|
|
3470
3535
|
try {
|
|
3471
3536
|
await dispatchCodexNativeHook(
|
|
@@ -3487,13 +3552,19 @@ esac
|
|
|
3487
3552
|
session_id: "plain-stop-session",
|
|
3488
3553
|
thread_id: "plain-thread",
|
|
3489
3554
|
turn_id: "plain-turn-1",
|
|
3490
|
-
last_assistant_message: "
|
|
3555
|
+
last_assistant_message: "If you want, I can continue with the cleanup from here.",
|
|
3491
3556
|
},
|
|
3492
3557
|
{ cwd },
|
|
3493
3558
|
);
|
|
3494
3559
|
|
|
3495
3560
|
assert.equal(result.omxEventName, "stop");
|
|
3496
|
-
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
|
+
});
|
|
3497
3568
|
} finally {
|
|
3498
3569
|
await rm(cwd, { recursive: true, force: true });
|
|
3499
3570
|
}
|
|
@@ -3601,7 +3672,13 @@ esac
|
|
|
3601
3672
|
);
|
|
3602
3673
|
|
|
3603
3674
|
assert.equal(duplicate.omxEventName, "stop");
|
|
3604
|
-
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
|
+
});
|
|
3605
3682
|
|
|
3606
3683
|
const fresh = await dispatchCodexNativeHook(
|
|
3607
3684
|
{
|
|
@@ -3633,6 +3710,126 @@ esac
|
|
|
3633
3710
|
}
|
|
3634
3711
|
});
|
|
3635
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
|
+
|
|
3636
3833
|
it("does not block Stop from another session's stale root team state when no scoped team state exists", async () => {
|
|
3637
3834
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-team-"));
|
|
3638
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
|
+
});
|