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
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
writeTeamPhase,
|
|
20
20
|
} from "../team/state.js";
|
|
21
21
|
import { omxNotepadPath, omxProjectMemoryPath } from "../utils/paths.js";
|
|
22
|
-
import { getStateFilePath } from "../mcp/state-paths.js";
|
|
22
|
+
import { getStateFilePath, getStatePath } from "../mcp/state-paths.js";
|
|
23
23
|
import {
|
|
24
24
|
detectKeywords,
|
|
25
25
|
detectPrimaryKeyword,
|
|
@@ -44,7 +44,6 @@ import type { HookEventEnvelope } from "../hooks/extensibility/types.js";
|
|
|
44
44
|
import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
|
|
45
45
|
import { reconcileHudForPromptSubmit } from "../hud/reconcile.js";
|
|
46
46
|
import { onSessionStart as buildWikiSessionStartContext } from "../wiki/lifecycle.js";
|
|
47
|
-
import { sessionModelInstructionsPath } from "../hooks/agents-overlay.js";
|
|
48
47
|
|
|
49
48
|
type CodexHookEventName =
|
|
50
49
|
| "SessionStart"
|
|
@@ -387,7 +386,7 @@ async function buildSessionStartContext(
|
|
|
387
386
|
|
|
388
387
|
const modeSummaries: string[] = [];
|
|
389
388
|
for (const mode of ["ralph", "autopilot", "ultrawork", "ultraqa", "ralplan", "deep-interview", "team"] as const) {
|
|
390
|
-
const state = await
|
|
389
|
+
const state = await readJsonIfExists(getStatePath(mode, cwd, sessionId));
|
|
391
390
|
if (state?.active !== true || !isNonTerminalPhase(state.current_phase)) continue;
|
|
392
391
|
if (mode === "team") {
|
|
393
392
|
const teamName = safeString(state.team_name).trim();
|
|
@@ -435,9 +434,22 @@ async function buildSessionStartContext(
|
|
|
435
434
|
if (existsSync(omxNotepadPath(cwd))) {
|
|
436
435
|
try {
|
|
437
436
|
const notepad = await readFile(omxNotepadPath(cwd), "utf-8");
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
437
|
+
const header = "## PRIORITY";
|
|
438
|
+
const idx = notepad.indexOf(header);
|
|
439
|
+
if (idx >= 0) {
|
|
440
|
+
const nextHeader = notepad.indexOf("\n## ", idx + header.length);
|
|
441
|
+
const section = (
|
|
442
|
+
nextHeader < 0
|
|
443
|
+
? notepad.slice(idx + header.length)
|
|
444
|
+
: notepad.slice(idx + header.length, nextHeader)
|
|
445
|
+
)
|
|
446
|
+
.split(/\r?\n/)
|
|
447
|
+
.map((line) => line.trim())
|
|
448
|
+
.filter(Boolean)
|
|
449
|
+
.join(" ");
|
|
450
|
+
if (section) {
|
|
451
|
+
sections.push(`[Priority notes]\n- ${section.slice(0, 220)}`);
|
|
452
|
+
}
|
|
441
453
|
}
|
|
442
454
|
} catch {
|
|
443
455
|
// best effort only
|
|
@@ -964,29 +976,6 @@ function readNativeStopSessionKey(
|
|
|
964
976
|
return resolveRepeatableStopSessionId(payload, canonicalSessionId) || readPayloadThreadId(payload) || "global";
|
|
965
977
|
}
|
|
966
978
|
|
|
967
|
-
function hasManagedStopSessionEnv(sessionIds: string[]): boolean {
|
|
968
|
-
const envSessionId = safeString(process.env.OMX_SESSION_ID).trim();
|
|
969
|
-
if (!envSessionId) return false;
|
|
970
|
-
return sessionIds.length === 0 || sessionIds.includes(envSessionId);
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
async function hasManagedStopContext(
|
|
974
|
-
cwd: string,
|
|
975
|
-
payload: CodexHookPayload,
|
|
976
|
-
canonicalSessionId: string,
|
|
977
|
-
): Promise<boolean> {
|
|
978
|
-
if (hasTeamWorkerContext()) return true;
|
|
979
|
-
|
|
980
|
-
const sessionIds = [...new Set([
|
|
981
|
-
canonicalSessionId,
|
|
982
|
-
readPayloadSessionId(payload),
|
|
983
|
-
].map((value) => safeString(value).trim()).filter(Boolean))];
|
|
984
|
-
|
|
985
|
-
if (hasManagedStopSessionEnv(sessionIds)) return true;
|
|
986
|
-
|
|
987
|
-
return sessionIds.some((sessionId) => existsSync(sessionModelInstructionsPath(cwd, sessionId)));
|
|
988
|
-
}
|
|
989
|
-
|
|
990
979
|
function readPreviousNativeStopSignature(
|
|
991
980
|
state: Record<string, unknown>,
|
|
992
981
|
sessionKey: string,
|
|
@@ -1025,10 +1014,11 @@ async function maybeReturnRepeatableStopOutput(
|
|
|
1025
1014
|
signature: string,
|
|
1026
1015
|
output: Record<string, unknown> | null,
|
|
1027
1016
|
canonicalSessionId?: string,
|
|
1017
|
+
options: { allowRepeatDuringStopHook?: boolean } = {},
|
|
1028
1018
|
): Promise<Record<string, unknown> | null> {
|
|
1029
1019
|
if (!output) return null;
|
|
1030
1020
|
const stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
1031
|
-
if (stopHookActive) {
|
|
1021
|
+
if (stopHookActive && options.allowRepeatDuringStopHook !== true) {
|
|
1032
1022
|
const state = await readJsonIfExists(join(stateDir, NATIVE_STOP_STATE_FILE)) ?? {};
|
|
1033
1023
|
const previousSignature = readPreviousNativeStopSignature(
|
|
1034
1024
|
state,
|
|
@@ -1042,6 +1032,24 @@ async function maybeReturnRepeatableStopOutput(
|
|
|
1042
1032
|
return output;
|
|
1043
1033
|
}
|
|
1044
1034
|
|
|
1035
|
+
async function returnPersistentStopBlock(
|
|
1036
|
+
payload: CodexHookPayload,
|
|
1037
|
+
stateDir: string,
|
|
1038
|
+
signatureKind: string,
|
|
1039
|
+
signatureValue: string,
|
|
1040
|
+
output: Record<string, unknown> | null,
|
|
1041
|
+
canonicalSessionId?: string,
|
|
1042
|
+
): Promise<Record<string, unknown> | null> {
|
|
1043
|
+
return await maybeReturnRepeatableStopOutput(
|
|
1044
|
+
payload,
|
|
1045
|
+
stateDir,
|
|
1046
|
+
buildRepeatableStopSignature(payload, signatureKind, signatureValue, canonicalSessionId),
|
|
1047
|
+
output,
|
|
1048
|
+
canonicalSessionId,
|
|
1049
|
+
{ allowRepeatDuringStopHook: true },
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1045
1053
|
async function findCanonicalActiveTeamForSession(
|
|
1046
1054
|
cwd: string,
|
|
1047
1055
|
sessionId: string,
|
|
@@ -1274,20 +1282,45 @@ async function buildStopHookOutput(
|
|
|
1274
1282
|
const canonicalSessionId = await resolveInternalSessionIdForPayload(cwd, sessionId);
|
|
1275
1283
|
const threadId = readPayloadThreadId(payload);
|
|
1276
1284
|
const ralphState = await readActiveRalphState(stateDir, canonicalSessionId);
|
|
1277
|
-
const stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
1278
|
-
const managedStopContext = await hasManagedStopContext(cwd, payload, canonicalSessionId);
|
|
1279
1285
|
if (!ralphState) {
|
|
1280
1286
|
const teamWorkerOutput = await buildTeamWorkerStopOutput(cwd);
|
|
1281
|
-
if (
|
|
1287
|
+
if (hasTeamWorkerContext() && teamWorkerOutput) return teamWorkerOutput;
|
|
1282
1288
|
|
|
1283
1289
|
const autopilotOutput = await buildModeBasedStopOutput("autopilot", cwd, canonicalSessionId);
|
|
1284
|
-
if (
|
|
1290
|
+
if (autopilotOutput) {
|
|
1291
|
+
return await returnPersistentStopBlock(
|
|
1292
|
+
payload,
|
|
1293
|
+
stateDir,
|
|
1294
|
+
"autopilot-stop",
|
|
1295
|
+
safeString(autopilotOutput.stopReason),
|
|
1296
|
+
autopilotOutput,
|
|
1297
|
+
canonicalSessionId,
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1285
1300
|
|
|
1286
1301
|
const ultraworkOutput = await buildModeBasedStopOutput("ultrawork", cwd, canonicalSessionId);
|
|
1287
|
-
if (
|
|
1302
|
+
if (ultraworkOutput) {
|
|
1303
|
+
return await returnPersistentStopBlock(
|
|
1304
|
+
payload,
|
|
1305
|
+
stateDir,
|
|
1306
|
+
"ultrawork-stop",
|
|
1307
|
+
safeString(ultraworkOutput.stopReason),
|
|
1308
|
+
ultraworkOutput,
|
|
1309
|
+
canonicalSessionId,
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1288
1312
|
|
|
1289
1313
|
const ultraqaOutput = await buildModeBasedStopOutput("ultraqa", cwd, canonicalSessionId);
|
|
1290
|
-
if (
|
|
1314
|
+
if (ultraqaOutput) {
|
|
1315
|
+
return await returnPersistentStopBlock(
|
|
1316
|
+
payload,
|
|
1317
|
+
stateDir,
|
|
1318
|
+
"ultraqa-stop",
|
|
1319
|
+
safeString(ultraqaOutput.stopReason),
|
|
1320
|
+
ultraqaOutput,
|
|
1321
|
+
canonicalSessionId,
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1291
1324
|
|
|
1292
1325
|
const releaseReadinessFinalizeResult = await maybeBuildReleaseReadinessFinalizeStopOutput(
|
|
1293
1326
|
payload,
|
|
@@ -1299,16 +1332,11 @@ async function buildStopHookOutput(
|
|
|
1299
1332
|
|
|
1300
1333
|
const teamOutput = await buildTeamStopOutput(cwd, canonicalSessionId);
|
|
1301
1334
|
if (teamOutput) {
|
|
1302
|
-
|
|
1335
|
+
return await returnPersistentStopBlock(
|
|
1303
1336
|
payload,
|
|
1337
|
+
stateDir,
|
|
1304
1338
|
"team-stop",
|
|
1305
1339
|
safeString(teamOutput.stopReason),
|
|
1306
|
-
canonicalSessionId,
|
|
1307
|
-
);
|
|
1308
|
-
return await maybeReturnRepeatableStopOutput(
|
|
1309
|
-
payload,
|
|
1310
|
-
stateDir,
|
|
1311
|
-
teamSignature,
|
|
1312
1340
|
teamOutput,
|
|
1313
1341
|
canonicalSessionId,
|
|
1314
1342
|
);
|
|
@@ -1321,16 +1349,11 @@ async function buildStopHookOutput(
|
|
|
1321
1349
|
canonicalTeam.teamName,
|
|
1322
1350
|
canonicalTeam.phase,
|
|
1323
1351
|
);
|
|
1324
|
-
const
|
|
1352
|
+
const repeatedCanonicalTeamOutput = await returnPersistentStopBlock(
|
|
1325
1353
|
payload,
|
|
1354
|
+
stateDir,
|
|
1326
1355
|
"team-stop",
|
|
1327
1356
|
`${canonicalTeam.teamName}|${canonicalTeam.phase}`,
|
|
1328
|
-
canonicalSessionId,
|
|
1329
|
-
);
|
|
1330
|
-
const repeatedCanonicalTeamOutput = await maybeReturnRepeatableStopOutput(
|
|
1331
|
-
payload,
|
|
1332
|
-
stateDir,
|
|
1333
|
-
canonicalTeamSignature,
|
|
1334
1357
|
canonicalTeamOutput,
|
|
1335
1358
|
canonicalSessionId,
|
|
1336
1359
|
);
|
|
@@ -1338,12 +1361,18 @@ async function buildStopHookOutput(
|
|
|
1338
1361
|
}
|
|
1339
1362
|
|
|
1340
1363
|
const skillOutput = await buildSkillStopOutput(cwd, canonicalSessionId, threadId);
|
|
1341
|
-
if (
|
|
1364
|
+
if (skillOutput) {
|
|
1365
|
+
return await returnPersistentStopBlock(
|
|
1366
|
+
payload,
|
|
1367
|
+
stateDir,
|
|
1368
|
+
"skill-stop",
|
|
1369
|
+
safeString(skillOutput.stopReason),
|
|
1370
|
+
skillOutput,
|
|
1371
|
+
canonicalSessionId,
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1342
1374
|
}
|
|
1343
1375
|
|
|
1344
|
-
if (!managedStopContext) {
|
|
1345
|
-
return null;
|
|
1346
|
-
}
|
|
1347
1376
|
|
|
1348
1377
|
const lastAssistantMessage = safeString(
|
|
1349
1378
|
payload.last_assistant_message ?? payload.lastAssistantMessage,
|
|
@@ -1356,10 +1385,11 @@ async function buildStopHookOutput(
|
|
|
1356
1385
|
&& detectNativeStopStallPattern(lastAssistantMessage, autoNudgeConfig.patterns, autoNudgePhase)
|
|
1357
1386
|
) {
|
|
1358
1387
|
const effectiveResponse = resolveEffectiveAutoNudgeResponse(autoNudgeConfig.response);
|
|
1359
|
-
return await
|
|
1388
|
+
return await returnPersistentStopBlock(
|
|
1360
1389
|
payload,
|
|
1361
1390
|
stateDir,
|
|
1362
|
-
|
|
1391
|
+
"auto-nudge",
|
|
1392
|
+
lastAssistantMessage,
|
|
1363
1393
|
{
|
|
1364
1394
|
decision: "block",
|
|
1365
1395
|
reason: effectiveResponse,
|
|
@@ -1374,21 +1404,24 @@ async function buildStopHookOutput(
|
|
|
1374
1404
|
return null;
|
|
1375
1405
|
}
|
|
1376
1406
|
|
|
1377
|
-
if (stopHookActive) {
|
|
1378
|
-
return null;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
1407
|
const currentPhase = safeString(ralphState?.current_phase).trim() || "executing";
|
|
1382
1408
|
const stopReason = `ralph_${currentPhase}`;
|
|
1383
1409
|
const systemMessage =
|
|
1384
1410
|
`OMX Ralph is still active (phase: ${currentPhase}); continue the task and gather fresh verification evidence before stopping.`;
|
|
1385
1411
|
|
|
1386
|
-
return
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1412
|
+
return await returnPersistentStopBlock(
|
|
1413
|
+
payload,
|
|
1414
|
+
stateDir,
|
|
1415
|
+
"ralph-stop",
|
|
1416
|
+
currentPhase,
|
|
1417
|
+
{
|
|
1418
|
+
decision: "block",
|
|
1419
|
+
reason: systemMessage,
|
|
1420
|
+
stopReason,
|
|
1421
|
+
systemMessage,
|
|
1422
|
+
},
|
|
1423
|
+
canonicalSessionId,
|
|
1424
|
+
);
|
|
1392
1425
|
}
|
|
1393
1426
|
|
|
1394
1427
|
export async function dispatchCodexNativeHook(
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export interface Contributor {
|
|
8
|
+
displayName: string;
|
|
9
|
+
login?: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CompareCommit {
|
|
14
|
+
author?: {
|
|
15
|
+
login?: string;
|
|
16
|
+
html_url?: string;
|
|
17
|
+
} | null;
|
|
18
|
+
commit?: {
|
|
19
|
+
author?: {
|
|
20
|
+
name?: string;
|
|
21
|
+
} | null;
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CompareResponse {
|
|
26
|
+
commits?: CompareCommit[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface GenerateReleaseBodyOptions {
|
|
30
|
+
templatePath: string;
|
|
31
|
+
outPath: string;
|
|
32
|
+
currentTag?: string;
|
|
33
|
+
previousTag?: string;
|
|
34
|
+
repo?: string;
|
|
35
|
+
githubToken?: string;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function usage(): never {
|
|
40
|
+
console.error('Usage: node scripts/generate-release-body.mjs --template <path> --out <path> [--current-tag <tag>] [--previous-tag <tag>] [--repo <owner/name>]');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function arg(name: string): string | undefined {
|
|
45
|
+
const index = process.argv.indexOf(name);
|
|
46
|
+
if (index === -1) return undefined;
|
|
47
|
+
return process.argv[index + 1];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runGit(args: string[], cwd: string, allowFailure = false): string {
|
|
51
|
+
const result = spawnSync('git', args, {
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
stdio: 'pipe',
|
|
55
|
+
});
|
|
56
|
+
if (result.status !== 0) {
|
|
57
|
+
if (allowFailure) return '';
|
|
58
|
+
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`.trim());
|
|
59
|
+
}
|
|
60
|
+
return String(result.stdout || '').trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveRepositoryFromRemote(cwd: string): string | undefined {
|
|
64
|
+
const remote = runGit(['config', '--get', 'remote.origin.url'], cwd, true);
|
|
65
|
+
if (!remote) return undefined;
|
|
66
|
+
const httpsMatch = remote.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
67
|
+
return httpsMatch?.[1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveCurrentTag(cwd: string, explicit?: string): string {
|
|
71
|
+
if (explicit) return explicit;
|
|
72
|
+
if (process.env.GITHUB_REF_NAME) return process.env.GITHUB_REF_NAME;
|
|
73
|
+
const described = runGit(['describe', '--tags', '--exact-match'], cwd, true);
|
|
74
|
+
if (described) return described;
|
|
75
|
+
throw new Error('unable to determine current release tag; pass --current-tag or set GITHUB_REF_NAME');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolvePreviousTag(cwd: string, currentTag: string, explicit?: string): string | undefined {
|
|
79
|
+
if (explicit) return explicit;
|
|
80
|
+
const tags = runGit(['tag', '--list', 'v*', '--sort=-v:refname'], cwd, true)
|
|
81
|
+
.split(/\r?\n/)
|
|
82
|
+
.map((line) => line.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
if (tags.length === 0) return undefined;
|
|
85
|
+
const currentIndex = tags.indexOf(currentTag);
|
|
86
|
+
if (currentIndex >= 0) return tags.slice(currentIndex + 1).find(Boolean);
|
|
87
|
+
return tags.find((tag) => tag !== currentTag);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeContributors(contributors: Contributor[]): Contributor[] {
|
|
91
|
+
const deduped = new Map<string, Contributor>();
|
|
92
|
+
for (const contributor of contributors) {
|
|
93
|
+
const login = contributor.login?.trim();
|
|
94
|
+
const displayName = contributor.displayName.trim();
|
|
95
|
+
if (!displayName && !login) continue;
|
|
96
|
+
const key = (login || displayName).toLowerCase();
|
|
97
|
+
if (!deduped.has(key)) {
|
|
98
|
+
deduped.set(key, {
|
|
99
|
+
displayName: displayName || `@${login}`,
|
|
100
|
+
...(login ? { login } : {}),
|
|
101
|
+
...(contributor.url ? { url: contributor.url } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...deduped.values()].sort((left, right) => {
|
|
106
|
+
const leftKey = (left.login || left.displayName).toLowerCase();
|
|
107
|
+
const rightKey = (right.login || right.displayName).toLowerCase();
|
|
108
|
+
return leftKey.localeCompare(rightKey);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatContributor(contributor: Contributor): string {
|
|
113
|
+
if (contributor.login && contributor.url) {
|
|
114
|
+
return `[@${contributor.login}](${contributor.url})`;
|
|
115
|
+
}
|
|
116
|
+
if (contributor.login) {
|
|
117
|
+
return `@${contributor.login}`;
|
|
118
|
+
}
|
|
119
|
+
if (contributor.url) {
|
|
120
|
+
return `[${contributor.displayName}](${contributor.url})`;
|
|
121
|
+
}
|
|
122
|
+
return contributor.displayName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function joinHumanList(values: string[]): string {
|
|
126
|
+
if (values.length === 0) return 'the contributors';
|
|
127
|
+
if (values.length === 1) return values[0]!;
|
|
128
|
+
if (values.length === 2) return `${values[0]} and ${values[1]}`;
|
|
129
|
+
return `${values.slice(0, -1).join(', ')}, and ${values.at(-1)}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function renderContributorsSection(contributors: Contributor[]): string {
|
|
133
|
+
const normalized = normalizeContributors(contributors);
|
|
134
|
+
if (normalized.length === 0) {
|
|
135
|
+
return 'Thanks to the contributors who made this release possible.';
|
|
136
|
+
}
|
|
137
|
+
return `Thanks to ${joinHumanList(normalized.map((contributor) => formatContributor(contributor)))} for contributing to this release.`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildFullChangelogLine(repo: string, currentTag: string, previousTag?: string): string {
|
|
141
|
+
if (!repo) {
|
|
142
|
+
throw new Error('unable to determine GitHub repository; pass --repo or set GITHUB_REPOSITORY');
|
|
143
|
+
}
|
|
144
|
+
if (previousTag) {
|
|
145
|
+
return `**Full Changelog**: [\`${previousTag}...${currentTag}\`](https://github.com/${repo}/compare/${previousTag}...${currentTag})`;
|
|
146
|
+
}
|
|
147
|
+
return `**Full Changelog**: [\`${currentTag}\`](https://github.com/${repo}/releases/tag/${currentTag})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function replaceTitle(markdown: string, currentTag: string): string {
|
|
151
|
+
if (!/^#\s+/m.test(markdown)) {
|
|
152
|
+
throw new Error('release body template is missing a top-level title');
|
|
153
|
+
}
|
|
154
|
+
return markdown.replace(/^#\s+.*$/m, `# oh-my-codex ${currentTag}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findSectionEnd(lines: string[], startIndex: number): number {
|
|
158
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
159
|
+
if (/^##\s+/.test(lines[index] ?? '')) return index;
|
|
160
|
+
if (/^\*\*Full Changelog\*\*:/.test(lines[index] ?? '')) return index;
|
|
161
|
+
}
|
|
162
|
+
return lines.length;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function replaceSectionBody(markdown: string, heading: string, body: string): string {
|
|
166
|
+
const lines = markdown.split(/\r?\n/);
|
|
167
|
+
const headingLine = `## ${heading}`;
|
|
168
|
+
const startIndex = lines.findIndex((line) => line.trim() === headingLine);
|
|
169
|
+
if (startIndex === -1) {
|
|
170
|
+
throw new Error(`release body template is missing section: ${headingLine}`);
|
|
171
|
+
}
|
|
172
|
+
const endIndex = findSectionEnd(lines, startIndex);
|
|
173
|
+
lines.splice(startIndex + 1, endIndex - startIndex - 1, '', ...body.split('\n'), '');
|
|
174
|
+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function replaceFullChangelogLine(markdown: string, fullChangelogLine: string): string {
|
|
178
|
+
const lines = markdown.split(/\r?\n/);
|
|
179
|
+
const lineIndex = lines.findIndex((line) => /^\*\*Full Changelog\*\*:/.test(line));
|
|
180
|
+
if (lineIndex === -1) {
|
|
181
|
+
throw new Error('release body template is missing the Full Changelog line');
|
|
182
|
+
}
|
|
183
|
+
lines[lineIndex] = fullChangelogLine;
|
|
184
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseShortlogLine(line: string): Contributor | undefined {
|
|
188
|
+
const match = line.match(/^\s*\d+\s+(.+?)(?:\s+<[^>]+>)?$/);
|
|
189
|
+
if (!match) return undefined;
|
|
190
|
+
const displayName = match[1]?.trim();
|
|
191
|
+
if (!displayName) return undefined;
|
|
192
|
+
return { displayName };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getGitContributors(cwd: string, currentTag: string, previousTag?: string): Contributor[] {
|
|
196
|
+
const range = previousTag ? `${previousTag}..${currentTag}` : currentTag;
|
|
197
|
+
const shortlog = runGit(['shortlog', '-sne', range], cwd, true);
|
|
198
|
+
if (!shortlog) return [];
|
|
199
|
+
return normalizeContributors(shortlog.split(/\r?\n/).map((line) => parseShortlogLine(line)).filter((value): value is Contributor => Boolean(value)));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function getGitHubCompareContributors(
|
|
203
|
+
repo: string,
|
|
204
|
+
currentTag: string,
|
|
205
|
+
previousTag: string,
|
|
206
|
+
githubToken: string,
|
|
207
|
+
fetchImpl: typeof fetch = fetch,
|
|
208
|
+
): Promise<Contributor[]> {
|
|
209
|
+
const response = await fetchImpl(`https://api.github.com/repos/${repo}/compare/${previousTag}...${currentTag}`, {
|
|
210
|
+
headers: {
|
|
211
|
+
Accept: 'application/vnd.github+json',
|
|
212
|
+
Authorization: `Bearer ${githubToken}`,
|
|
213
|
+
'User-Agent': 'oh-my-codex-release-body-generator',
|
|
214
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
throw new Error(`GitHub compare API failed (${response.status})`);
|
|
219
|
+
}
|
|
220
|
+
const payload = await response.json() as CompareResponse;
|
|
221
|
+
const contributors = (payload.commits ?? []).map((commit) => {
|
|
222
|
+
if (commit.author?.login) {
|
|
223
|
+
return {
|
|
224
|
+
displayName: `@${commit.author.login}`,
|
|
225
|
+
login: commit.author.login,
|
|
226
|
+
...(commit.author.html_url ? { url: commit.author.html_url } : {}),
|
|
227
|
+
} satisfies Contributor;
|
|
228
|
+
}
|
|
229
|
+
const name = commit.commit?.author?.name?.trim();
|
|
230
|
+
return name ? { displayName: name } satisfies Contributor : undefined;
|
|
231
|
+
}).filter((value): value is Contributor => Boolean(value));
|
|
232
|
+
return normalizeContributors(contributors);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function resolveContributors(options: {
|
|
236
|
+
cwd: string;
|
|
237
|
+
repo?: string;
|
|
238
|
+
currentTag: string;
|
|
239
|
+
previousTag?: string;
|
|
240
|
+
githubToken?: string;
|
|
241
|
+
}): Promise<Contributor[]> {
|
|
242
|
+
const { cwd, repo, currentTag, previousTag, githubToken } = options;
|
|
243
|
+
if (repo && previousTag && githubToken) {
|
|
244
|
+
try {
|
|
245
|
+
return await getGitHubCompareContributors(repo, currentTag, previousTag, githubToken);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error(`[generate-release-body] falling back to git shortlog: ${error instanceof Error ? error.message : String(error)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return getGitContributors(cwd, currentTag, previousTag);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function generateReleaseBody(options: GenerateReleaseBodyOptions): Promise<string> {
|
|
254
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
255
|
+
const templatePath = resolve(cwd, options.templatePath);
|
|
256
|
+
const outPath = resolve(cwd, options.outPath);
|
|
257
|
+
const currentTag = resolveCurrentTag(cwd, options.currentTag);
|
|
258
|
+
const previousTag = resolvePreviousTag(cwd, currentTag, options.previousTag);
|
|
259
|
+
const repo = options.repo || process.env.GITHUB_REPOSITORY || resolveRepositoryFromRemote(cwd);
|
|
260
|
+
const contributors = await resolveContributors({
|
|
261
|
+
cwd,
|
|
262
|
+
repo,
|
|
263
|
+
currentTag,
|
|
264
|
+
previousTag,
|
|
265
|
+
githubToken: options.githubToken || process.env.GITHUB_TOKEN,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
let markdown = readFileSync(templatePath, 'utf-8');
|
|
269
|
+
markdown = replaceTitle(markdown, currentTag);
|
|
270
|
+
markdown = replaceSectionBody(markdown, 'Contributors', renderContributorsSection(contributors));
|
|
271
|
+
markdown = replaceFullChangelogLine(markdown, buildFullChangelogLine(repo || '', currentTag, previousTag));
|
|
272
|
+
writeFileSync(outPath, markdown);
|
|
273
|
+
return markdown;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function main(): Promise<void> {
|
|
277
|
+
const templatePath = arg('--template');
|
|
278
|
+
const outPath = arg('--out');
|
|
279
|
+
if (!templatePath || !outPath) usage();
|
|
280
|
+
await generateReleaseBody({
|
|
281
|
+
templatePath,
|
|
282
|
+
outPath,
|
|
283
|
+
currentTag: arg('--current-tag'),
|
|
284
|
+
previousTag: arg('--previous-tag'),
|
|
285
|
+
repo: arg('--repo'),
|
|
286
|
+
});
|
|
287
|
+
console.log(resolve(process.cwd(), outPath));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
291
|
+
main().catch((error) => {
|
|
292
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
293
|
+
process.exit(1);
|
|
294
|
+
});
|
|
295
|
+
}
|