oh-my-codex 0.16.4 → 0.17.0
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/dist/catalog/__tests__/generator.test.js +2 -0
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +80 -7
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +17 -11
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-serve.test.js +4 -0
- package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +8 -3
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +27 -1
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +22 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +66 -10
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +17 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-serve.d.ts.map +1 -1
- package/dist/cli/mcp-serve.js +4 -0
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +20 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +115 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +29 -10
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +7 -1
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +136 -9
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +15 -0
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +13 -14
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +85 -7
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/generator.d.ts +4 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +15 -9
- package/dist/config/generator.js.map +1 -1
- package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
- package/dist/config/omx-first-party-mcp.js +7 -0
- package/dist/config/omx-first-party-mcp.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.d.ts +2 -0
- package/dist/hooks/__tests__/design-skill.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/design-skill.test.js +55 -0
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +265 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-guidance-contract.test.js +41 -0
- package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +5 -1
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +2 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +47 -2
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +3 -0
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.d.ts +2 -0
- package/dist/mcp/__tests__/hermes-bridge.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js +374 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -0
- package/dist/mcp/__tests__/state-paths.test.js +96 -13
- package/dist/mcp/__tests__/state-paths.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 +2 -0
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts +81 -0
- package/dist/mcp/hermes-bridge.d.ts.map +1 -0
- package/dist/mcp/hermes-bridge.js +400 -0
- package/dist/mcp/hermes-bridge.js.map +1 -0
- package/dist/mcp/hermes-server.d.ts +269 -0
- package/dist/mcp/hermes-server.d.ts.map +1 -0
- package/dist/mcp/hermes-server.js +121 -0
- package/dist/mcp/hermes-server.js.map +1 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +41 -9
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +31 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +187 -2
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +44 -17
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +91 -2
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +2 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +21 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +121 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +9 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +105 -3
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +31 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/paths.d.ts +6 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +18 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/wiki/lifecycle.js +3 -3
- package/dist/wiki/lifecycle.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/.mcp.json +8 -0
- package/plugins/oh-my-codex/skills/design/SKILL.md +180 -0
- package/plugins/oh-my-codex/skills/skill/SKILL.md +2 -1
- package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +161 -47
- package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +2 -2
- package/skills/design/SKILL.md +180 -0
- package/skills/frontend-ui-ux/SKILL.md +6 -2
- package/skills/skill/SKILL.md +2 -1
- package/skills/ultraqa/SKILL.md +161 -47
- package/skills/visual-ralph/SKILL.md +2 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +206 -1
- package/src/scripts/codex-native-hook.ts +45 -18
- package/src/scripts/notify-hook/tmux-injection.ts +110 -3
- package/templates/catalog-manifest.json +9 -2
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
dispatchCodexNativeHook,
|
|
19
19
|
isCodexNativeHookMainModule,
|
|
20
|
+
looksLikeGoalCompletionPrompt,
|
|
20
21
|
mapCodexHookEventToOmxEvent,
|
|
21
22
|
resolveSessionOwnerPidFromAncestry,
|
|
22
23
|
} from "../codex-native-hook.js";
|
|
@@ -25,7 +26,7 @@ import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
|
25
26
|
import { executeStateOperation } from "../../state/operations.js";
|
|
26
27
|
import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
|
|
27
28
|
import { readAllState } from "../../hud/state.js";
|
|
28
|
-
import { writePage } from "../../wiki/storage.js";
|
|
29
|
+
import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
|
|
29
30
|
import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
|
|
30
31
|
|
|
31
32
|
function nativeHookScriptPath(): string {
|
|
@@ -1094,6 +1095,68 @@ describe("codex native hook dispatch", () => {
|
|
|
1094
1095
|
}
|
|
1095
1096
|
});
|
|
1096
1097
|
|
|
1098
|
+
it("prefers repository project-memory.json during SessionStart while preserving legacy wiki guidance", async () => {
|
|
1099
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-root-memory-legacy-wiki-"));
|
|
1100
|
+
try {
|
|
1101
|
+
const now = new Date().toISOString();
|
|
1102
|
+
const legacyWikiDir = getLegacyWikiDir(cwd);
|
|
1103
|
+
await mkdir(legacyWikiDir, { recursive: true });
|
|
1104
|
+
await writeFile(join(legacyWikiDir, "legacy.md"), serializePage({
|
|
1105
|
+
filename: "legacy.md",
|
|
1106
|
+
frontmatter: {
|
|
1107
|
+
title: "Legacy",
|
|
1108
|
+
tags: ["legacy"],
|
|
1109
|
+
created: now,
|
|
1110
|
+
updated: now,
|
|
1111
|
+
sources: [],
|
|
1112
|
+
links: [],
|
|
1113
|
+
category: "reference",
|
|
1114
|
+
confidence: "medium",
|
|
1115
|
+
schemaVersion: WIKI_SCHEMA_VERSION,
|
|
1116
|
+
},
|
|
1117
|
+
content: "\n# Legacy\n\nLegacy wiki context must remain visible.\n",
|
|
1118
|
+
}));
|
|
1119
|
+
await writeJson(join(cwd, ".omx", "project-memory.json"), {
|
|
1120
|
+
techStack: "Legacy runtime memory should not win",
|
|
1121
|
+
notes: [{ category: "legacy", content: "stale legacy note", timestamp: now }],
|
|
1122
|
+
});
|
|
1123
|
+
await writeJson(join(cwd, "project-memory.json"), {
|
|
1124
|
+
techStack: "Canonical root memory",
|
|
1125
|
+
build: "npm run build && node --test dist/scripts/__tests__/codex-native-hook.test.js",
|
|
1126
|
+
conventions: "prefer repository-visible project memory at startup",
|
|
1127
|
+
directives: [
|
|
1128
|
+
{ directive: "Load root project-memory.json before legacy .omx memory.", priority: "high", timestamp: now },
|
|
1129
|
+
],
|
|
1130
|
+
notes: [
|
|
1131
|
+
{ category: "issue", content: "Regression fixture for issue #2273.", timestamp: now },
|
|
1132
|
+
],
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
const result = await dispatchCodexNativeHook(
|
|
1136
|
+
{
|
|
1137
|
+
hook_event_name: "SessionStart",
|
|
1138
|
+
cwd,
|
|
1139
|
+
session_id: "sess-root-memory-legacy-wiki",
|
|
1140
|
+
},
|
|
1141
|
+
{ cwd, sessionOwnerPid: 43210 },
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
const additionalContext = String(
|
|
1145
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
1146
|
+
);
|
|
1147
|
+
assert.match(additionalContext, /\[Project memory\]/);
|
|
1148
|
+
assert.match(additionalContext, /source: project-memory\.json/);
|
|
1149
|
+
assert.match(additionalContext, /Canonical root memory/);
|
|
1150
|
+
assert.match(additionalContext, /Load root project-memory\.json before legacy \.omx memory\./);
|
|
1151
|
+
assert.match(additionalContext, /Regression fixture for issue #2273\./);
|
|
1152
|
+
assert.doesNotMatch(additionalContext, /Legacy runtime memory should not win/);
|
|
1153
|
+
assert.match(additionalContext, /legacy pages at \.omx\/wiki\//);
|
|
1154
|
+
assert.match(additionalContext, /Legacy wiki fallback is read-only/);
|
|
1155
|
+
} finally {
|
|
1156
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1097
1160
|
it("starts a fresh native session without inheriting stale task-scoped context", async () => {
|
|
1098
1161
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-isolation-"));
|
|
1099
1162
|
try {
|
|
@@ -1347,6 +1410,36 @@ describe("codex native hook dispatch", () => {
|
|
|
1347
1410
|
}
|
|
1348
1411
|
});
|
|
1349
1412
|
|
|
1413
|
+
it("classifies only actionable goal completion wording", () => {
|
|
1414
|
+
const actionable = [
|
|
1415
|
+
"complete this goal now",
|
|
1416
|
+
"Performance goal complete; next call update_goal({status: \"complete\"}).",
|
|
1417
|
+
"get_goal returned a completed legacy goal, so ultragoal complete failed; marking complete now.",
|
|
1418
|
+
"omx ultragoal checkpoint --goal-id G001-demo --status complete --codex-goal-json goal.json",
|
|
1419
|
+
"Call update_goal({status: \"complete\"}) after verification.",
|
|
1420
|
+
"Goal complete.",
|
|
1421
|
+
"The goal is complete.",
|
|
1422
|
+
"Goal complete: verified with tests.",
|
|
1423
|
+
"Goal complete — verified with tests.",
|
|
1424
|
+
"The goal is complete: verified.",
|
|
1425
|
+
"The goal is complete — verified.",
|
|
1426
|
+
];
|
|
1427
|
+
|
|
1428
|
+
const ordinary = [
|
|
1429
|
+
"my goal is to complete the migration without regressions",
|
|
1430
|
+
"Our goal is to finish this carefully after tests pass.",
|
|
1431
|
+
"The goal of this patch is to close a review gap.",
|
|
1432
|
+
"A goal can be complete only after a human review.",
|
|
1433
|
+
];
|
|
1434
|
+
|
|
1435
|
+
for (const text of actionable) {
|
|
1436
|
+
assert.equal(looksLikeGoalCompletionPrompt(text), true, text);
|
|
1437
|
+
}
|
|
1438
|
+
for (const text of ordinary) {
|
|
1439
|
+
assert.equal(looksLikeGoalCompletionPrompt(text), false, text);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1350
1443
|
it("warns completion-like prompts when active goal workflows need Codex snapshot reconciliation", async () => {
|
|
1351
1444
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-warning-"));
|
|
1352
1445
|
try {
|
|
@@ -1400,6 +1493,118 @@ describe("codex native hook dispatch", () => {
|
|
|
1400
1493
|
}
|
|
1401
1494
|
});
|
|
1402
1495
|
|
|
1496
|
+
it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
|
|
1497
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
|
|
1498
|
+
try {
|
|
1499
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
1500
|
+
version: 1,
|
|
1501
|
+
activeGoalId: "G001-demo",
|
|
1502
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
const result = await dispatchCodexNativeHook({
|
|
1506
|
+
hook_event_name: "Stop",
|
|
1507
|
+
cwd,
|
|
1508
|
+
session_id: "sess-ultragoal-generic-complete-stop",
|
|
1509
|
+
thread_id: "thread-ultragoal-generic-complete-stop",
|
|
1510
|
+
last_assistant_message: "Goal complete.",
|
|
1511
|
+
}, { cwd });
|
|
1512
|
+
|
|
1513
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
1514
|
+
assert.match(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
1515
|
+
} finally {
|
|
1516
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it("does not block ultragoal Stop for ordinary prose about a goal to complete work", async () => {
|
|
1521
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-ordinary-stop-"));
|
|
1522
|
+
try {
|
|
1523
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
1524
|
+
version: 1,
|
|
1525
|
+
activeGoalId: "G001-demo",
|
|
1526
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
const result = await dispatchCodexNativeHook({
|
|
1530
|
+
hook_event_name: "Stop",
|
|
1531
|
+
cwd,
|
|
1532
|
+
session_id: "sess-ultragoal-ordinary-stop",
|
|
1533
|
+
thread_id: "thread-ultragoal-ordinary-stop",
|
|
1534
|
+
last_assistant_message: "My goal is to complete the migration without regressions, so I will keep testing.",
|
|
1535
|
+
}, { cwd });
|
|
1536
|
+
|
|
1537
|
+
assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
|
|
1538
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
1539
|
+
} finally {
|
|
1540
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it("blocks ultragoal Stop with blocked checkpoint and fresh-thread remediation for completed legacy snapshots", async () => {
|
|
1545
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
|
|
1546
|
+
try {
|
|
1547
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
1548
|
+
version: 1,
|
|
1549
|
+
activeGoalId: "G001-demo",
|
|
1550
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
const result = await dispatchCodexNativeHook({
|
|
1554
|
+
hook_event_name: "Stop",
|
|
1555
|
+
cwd,
|
|
1556
|
+
session_id: "sess-ultragoal-legacy-stop",
|
|
1557
|
+
thread_id: "thread-ultragoal-legacy-stop",
|
|
1558
|
+
last_assistant_message: "get_goal returned a completed legacy goal, so ultragoal complete failed; marking complete now.",
|
|
1559
|
+
}, { cwd });
|
|
1560
|
+
|
|
1561
|
+
const output = JSON.stringify(result.outputJson);
|
|
1562
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
1563
|
+
assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
1564
|
+
assert.match(output, /--status blocked/);
|
|
1565
|
+
assert.match(output, /fresh Codex thread/);
|
|
1566
|
+
assert.match(output, /Hooks must not mutate Codex goal state/);
|
|
1567
|
+
} finally {
|
|
1568
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
it("does not block ultragoal Stop after task-scoped reconciliation finishes exploded bookkeeping", async () => {
|
|
1574
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
|
|
1575
|
+
try {
|
|
1576
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
1577
|
+
version: 1,
|
|
1578
|
+
codexGoalMode: "aggregate",
|
|
1579
|
+
codexObjective: "Complete all ultragoal stories in .omx/ultragoal/goals.json: many micro goals",
|
|
1580
|
+
activeGoalId: "G001-micro",
|
|
1581
|
+
aggregateCompletion: {
|
|
1582
|
+
status: "complete",
|
|
1583
|
+
completedAt: "2026-05-04T10:04:00.000Z",
|
|
1584
|
+
evidence: "planned work done; validation complete; reviews clean",
|
|
1585
|
+
},
|
|
1586
|
+
goals: Array.from({ length: 136 }, (_, index) => ({
|
|
1587
|
+
id: `G${String(index + 1).padStart(3, "0")}-micro`,
|
|
1588
|
+
status: index === 0 ? "in_progress" : "pending",
|
|
1589
|
+
objective: `Synthetic slice ${index + 1}.`,
|
|
1590
|
+
})),
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
const result = await dispatchCodexNativeHook({
|
|
1594
|
+
hook_event_name: "Stop",
|
|
1595
|
+
cwd,
|
|
1596
|
+
session_id: "sess-ultragoal-reconciled-stop",
|
|
1597
|
+
thread_id: "thread-ultragoal-reconciled-stop",
|
|
1598
|
+
last_assistant_message: "Yes — planned implementation work is done; ultragoal bookkeeping reconciled complete.",
|
|
1599
|
+
}, { cwd });
|
|
1600
|
+
|
|
1601
|
+
assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
|
|
1602
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id/);
|
|
1603
|
+
} finally {
|
|
1604
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1403
1608
|
it("does not block Stop for non-passing autoresearch-goal professor-critic verdicts", async () => {
|
|
1404
1609
|
for (const verdict of ["blocked", "fail", "failed"]) {
|
|
1405
1610
|
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-autoresearch-${verdict}-stop-`));
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
writeTeamLeaderAttention,
|
|
34
34
|
writeTeamPhase,
|
|
35
35
|
} from "../team/state.js";
|
|
36
|
-
import { omxNotepadPath,
|
|
36
|
+
import { omxNotepadPath, resolveProjectMemoryPath } from "../utils/paths.js";
|
|
37
37
|
import { findGitLayout } from "../utils/git-layout.js";
|
|
38
38
|
import { getBaseStateDir, getStateFilePath, getStatePath } from "../mcp/state-paths.js";
|
|
39
39
|
import {
|
|
@@ -157,6 +157,12 @@ function safeObject(value: unknown): Record<string, unknown> {
|
|
|
157
157
|
return value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
function safeContextSnippet(value: unknown, maxLength = 300): string {
|
|
161
|
+
const text = safeString(value).replace(/\s+/g, " ").trim();
|
|
162
|
+
if (text.length <= maxLength) return text;
|
|
163
|
+
return `${text.slice(0, maxLength - 1).trimEnd()}…`;
|
|
164
|
+
}
|
|
165
|
+
|
|
160
166
|
interface NativeSubagentSessionStartMetadata {
|
|
161
167
|
parentThreadId: string;
|
|
162
168
|
agentNickname?: string;
|
|
@@ -1122,28 +1128,31 @@ async function buildSessionStartContext(
|
|
|
1122
1128
|
sections.push(["[Active OMX modes]", ...modeSummaries].join("\n"));
|
|
1123
1129
|
}
|
|
1124
1130
|
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1131
|
+
const projectMemoryPath = resolveProjectMemoryPath(cwd);
|
|
1132
|
+
const projectMemory = projectMemoryPath ? await readJsonIfExists(projectMemoryPath) : null;
|
|
1133
|
+
if (projectMemory && projectMemoryPath) {
|
|
1127
1134
|
const directives = Array.isArray(projectMemory.directives) ? projectMemory.directives : [];
|
|
1128
1135
|
const notes = Array.isArray(projectMemory.notes) ? projectMemory.notes : [];
|
|
1129
|
-
const techStack =
|
|
1130
|
-
const conventions =
|
|
1131
|
-
const build =
|
|
1136
|
+
const techStack = safeContextSnippet(projectMemory.techStack);
|
|
1137
|
+
const conventions = safeContextSnippet(projectMemory.conventions);
|
|
1138
|
+
const build = safeContextSnippet(projectMemory.build);
|
|
1132
1139
|
const summary: string[] = [];
|
|
1140
|
+
const relativeMemoryPath = relative(cwd, projectMemoryPath).replace(/\\/g, "/");
|
|
1141
|
+
summary.push(`- source: ${relativeMemoryPath === "project-memory.json" ? "project-memory.json" : ".omx/project-memory.json"}`);
|
|
1133
1142
|
if (techStack) summary.push(`- stack: ${techStack}`);
|
|
1134
1143
|
if (conventions) summary.push(`- conventions: ${conventions}`);
|
|
1135
1144
|
if (build) summary.push(`- build: ${build}`);
|
|
1136
1145
|
if (directives.length > 0) {
|
|
1137
1146
|
const firstDirective = directives[0] as Record<string, unknown>;
|
|
1138
|
-
const directive =
|
|
1147
|
+
const directive = safeContextSnippet(firstDirective.directive);
|
|
1139
1148
|
if (directive) summary.push(`- directive: ${directive}`);
|
|
1140
1149
|
}
|
|
1141
1150
|
if (notes.length > 0) {
|
|
1142
1151
|
const firstNote = notes[0] as Record<string, unknown>;
|
|
1143
|
-
const note =
|
|
1152
|
+
const note = safeContextSnippet(firstNote.content);
|
|
1144
1153
|
if (note) summary.push(`- note: ${note}`);
|
|
1145
1154
|
}
|
|
1146
|
-
if (summary.length >
|
|
1155
|
+
if (summary.length > 1) {
|
|
1147
1156
|
sections.push(["[Project memory]", ...summary].join("\n"));
|
|
1148
1157
|
}
|
|
1149
1158
|
}
|
|
@@ -1695,20 +1704,33 @@ async function buildModeBasedStopOutput(
|
|
|
1695
1704
|
};
|
|
1696
1705
|
}
|
|
1697
1706
|
|
|
1698
|
-
function looksLikeGoalCompletionPrompt(text: string): boolean {
|
|
1699
|
-
return /\
|
|
1700
|
-
|| /\
|
|
1701
|
-
|| /\
|
|
1707
|
+
export function looksLikeGoalCompletionPrompt(text: string): boolean {
|
|
1708
|
+
return /\bupdate_goal\s*\(/i.test(text)
|
|
1709
|
+
|| /\bomx\s+(?:ultragoal|performance-goal|autoresearch-goal)\s+(?:checkpoint|complete)\b/i.test(text)
|
|
1710
|
+
|| /\b(?:complete|checkpoint|finish|close|mark)\b.{0,80}\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b/i.test(text)
|
|
1711
|
+
|| /\b(?:ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b.{0,80}\b(?:complete|checkpoint|finish|close|mark)\b/i.test(text)
|
|
1712
|
+
|| /(?:^|[.!?]\s+)(?:the\s+)?goal\s+(?:is\s+|now\s+|has\s+been\s+)?(?:complete|completed|finished|closed)(?:\s*(?:[.!?]|$)|\s*[:;]\s*\S|\s*[—–-]\s*\S)/i.test(text);
|
|
1702
1713
|
}
|
|
1703
1714
|
|
|
1704
|
-
async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string } | null> {
|
|
1715
|
+
async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
|
|
1705
1716
|
const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
|
|
1717
|
+
const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
|
|
1718
|
+
const aggregateProductComplete = safeString(aggregateCompletion.status) === "complete";
|
|
1706
1719
|
const ultragoals = Array.isArray(ultragoal?.goals) ? ultragoal.goals.map(safeObject) : [];
|
|
1707
|
-
const activeUltragoal =
|
|
1720
|
+
const activeUltragoal = aggregateProductComplete
|
|
1721
|
+
? undefined
|
|
1722
|
+
: ultragoals.find((goal) => safeString(goal.status) === "in_progress" || safeString(goal.id) === safeString(ultragoal?.activeGoalId));
|
|
1708
1723
|
if (activeUltragoal) {
|
|
1724
|
+
const goalId = safeString(activeUltragoal.id) || "<goal-id>";
|
|
1709
1725
|
return {
|
|
1710
1726
|
workflow: "ultragoal",
|
|
1711
|
-
command: `omx ultragoal checkpoint --goal-id ${
|
|
1727
|
+
command: `omx ultragoal checkpoint --goal-id ${goalId} --status complete --codex-goal-json '<get_goal JSON or path>' --evidence '<evidence>'`,
|
|
1728
|
+
remediation: [
|
|
1729
|
+
`If get_goal returns a completed task-scoped objective for the same aggregate ultragoal plan, checkpoint ${goalId} with evidence naming ${goalId} plus .omx/ultragoal/goals.json or ledger.jsonl and pass final quality-gate JSON; OMX will reconcile the completed planned scope without mutating Codex goal state.`,
|
|
1730
|
+
`If get_goal instead returns a different completed legacy objective and complete checkpointing fails, do not repeat --status complete in this thread.`,
|
|
1731
|
+
`Record the non-terminal blocker with: omx ultragoal checkpoint --goal-id ${goalId} --status blocked --codex-goal-json '<different completed get_goal JSON or path>' --evidence '<completed legacy Codex goal blocks create_goal in this thread>'.`,
|
|
1732
|
+
"Then continue this ultragoal from a fresh Codex thread in the same repo/worktree and create the intended goal there.",
|
|
1733
|
+
].join(" "),
|
|
1712
1734
|
};
|
|
1713
1735
|
}
|
|
1714
1736
|
|
|
@@ -1752,7 +1774,8 @@ async function buildGoalWorkflowReconciliationPromptWarning(cwd: string, prompt:
|
|
|
1752
1774
|
`OMX ${requirement.workflow} goal workflow requires Codex goal snapshot reconciliation before completion.`,
|
|
1753
1775
|
"Call get_goal, pass the resulting JSON or a path with --codex-goal-json, and do not rely on hooks or shell commands to mutate Codex-owned goal state.",
|
|
1754
1776
|
`Required command shape: ${requirement.command}.`,
|
|
1755
|
-
|
|
1777
|
+
requirement.remediation,
|
|
1778
|
+
].filter(Boolean).join(" ");
|
|
1756
1779
|
}
|
|
1757
1780
|
|
|
1758
1781
|
async function buildGoalWorkflowReconciliationStopOutput(
|
|
@@ -1764,7 +1787,11 @@ async function buildGoalWorkflowReconciliationStopOutput(
|
|
|
1764
1787
|
const requirement = await findActiveGoalWorkflowReconciliationRequirement(cwd);
|
|
1765
1788
|
if (!requirement) return null;
|
|
1766
1789
|
const systemMessage =
|
|
1767
|
-
|
|
1790
|
+
[
|
|
1791
|
+
`OMX ${requirement.workflow} requires get_goal snapshot reconciliation before completion; call get_goal and pass --codex-goal-json to ${requirement.command}.`,
|
|
1792
|
+
requirement.remediation,
|
|
1793
|
+
"Hooks must not mutate Codex goal state.",
|
|
1794
|
+
].filter(Boolean).join(" ");
|
|
1768
1795
|
return {
|
|
1769
1796
|
decision: "block",
|
|
1770
1797
|
reason: systemMessage,
|
|
@@ -99,7 +99,7 @@ async function resolveCanonicalPaneFromPaneTarget(paneTarget: any, expectedCwd:
|
|
|
99
99
|
return finalizeResolvedPane(healedPaneId, 'healed_hud_pane_target', expectedCwd);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
async function resolvePreferredModePane(stateDir: string, allowedModes: string[]): Promise<{ mode: string; state: any; pane: string } | null> {
|
|
102
|
+
async function resolvePreferredModePane(stateDir: string, allowedModes: string[]): Promise<{ mode: string; state: any; pane: string; stateDir: string } | null> {
|
|
103
103
|
const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir).catch(() => [stateDir]);
|
|
104
104
|
const dirs = [...scopedDirs];
|
|
105
105
|
if (!dirs.map((dir) => resolvePath(dir)).includes(resolvePath(stateDir))) {
|
|
@@ -111,13 +111,84 @@ async function resolvePreferredModePane(stateDir: string, allowedModes: string[]
|
|
|
111
111
|
const parsed = await readJsonIfExists(path, null);
|
|
112
112
|
const pane = safeString(parsed?.tmux_pane_id || '').trim();
|
|
113
113
|
if (parsed?.active && pane) {
|
|
114
|
-
return { mode, state: parsed, pane };
|
|
114
|
+
return { mode, state: parsed, pane, stateDir: dir };
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
return null;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function modeStateMatchesInvocationOwner(modeState: any, payload: any, managedContext: any): { ok: true } | { ok: false; reason: string } {
|
|
122
|
+
const invocationSessionId = resolveInvocationSessionId(payload);
|
|
123
|
+
const canonicalSessionId = safeString(managedContext?.canonicalSessionId || managedContext?.sessionState?.session_id).trim();
|
|
124
|
+
const nativeSessionId = safeString(managedContext?.nativeSessionId || managedContext?.sessionState?.native_session_id || managedContext?.sessionState?.codex_session_id).trim();
|
|
125
|
+
const allowedSessionIds = new Set([
|
|
126
|
+
invocationSessionId,
|
|
127
|
+
canonicalSessionId,
|
|
128
|
+
nativeSessionId,
|
|
129
|
+
].filter(Boolean));
|
|
130
|
+
|
|
131
|
+
const ownerOmxSessionId = safeString(modeState?.owner_omx_session_id).trim();
|
|
132
|
+
if (ownerOmxSessionId && !allowedSessionIds.has(ownerOmxSessionId)) {
|
|
133
|
+
return { ok: false, reason: 'mode_owner_session_mismatch' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const stateSessionId = safeString(modeState?.session_id).trim();
|
|
137
|
+
if (!ownerOmxSessionId && stateSessionId && !allowedSessionIds.has(stateSessionId)) {
|
|
138
|
+
return { ok: false, reason: 'mode_session_mismatch' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ownerCodexSessionId = safeString(modeState?.owner_codex_session_id || modeState?.codex_session_id).trim();
|
|
142
|
+
if (ownerCodexSessionId && !allowedSessionIds.has(ownerCodexSessionId)) {
|
|
143
|
+
return { ok: false, reason: 'mode_codex_session_mismatch' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { ok: true };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function validateResolvedInjectionOwnership({
|
|
150
|
+
paneTarget,
|
|
151
|
+
cwd,
|
|
152
|
+
payload,
|
|
153
|
+
modeState,
|
|
154
|
+
modePane,
|
|
155
|
+
managedCurrentPane,
|
|
156
|
+
}: any): Promise<{ ok: true } | { ok: false; reason: string; managedContext?: any }> {
|
|
157
|
+
const ownership = await verifyManagedPaneTarget(paneTarget, cwd, payload, { allowTeamWorker: false });
|
|
158
|
+
if (!ownership.ok) {
|
|
159
|
+
return { ok: false, reason: ownership.reason || 'pane_not_managed_session', managedContext: ownership.managedContext };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const modeOwner = modeStateMatchesInvocationOwner(modeState, payload, ownership.managedContext);
|
|
163
|
+
if (!modeOwner.ok) return { ...modeOwner, managedContext: ownership.managedContext };
|
|
164
|
+
|
|
165
|
+
const statePane = safeString(modePane || modeState?.tmux_pane_id).trim();
|
|
166
|
+
const currentPane = safeString(managedCurrentPane).trim();
|
|
167
|
+
if (statePane && currentPane && statePane !== currentPane) {
|
|
168
|
+
return { ok: false, reason: 'mode_pane_current_pane_mismatch', managedContext: ownership.managedContext };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const expectedWindowId = safeString(modeState?.tmux_window_id || modeState?.tmuxWindowId).trim();
|
|
172
|
+
if (!expectedWindowId) {
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const windowResult = await runProcess('tmux', ['display-message', '-p', '-t', paneTarget, '#{window_id}'], 2000);
|
|
178
|
+
const paneWindowId = safeString(windowResult.stdout).trim();
|
|
179
|
+
if (!paneWindowId) {
|
|
180
|
+
return { ok: false, reason: 'pane_window_unverified', managedContext: ownership.managedContext };
|
|
181
|
+
}
|
|
182
|
+
if (paneWindowId !== expectedWindowId) {
|
|
183
|
+
return { ok: false, reason: 'pane_window_mismatch', managedContext: ownership.managedContext };
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
return { ok: false, reason: 'pane_window_unverified', managedContext: ownership.managedContext };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
121
192
|
async function readVisibleAllowedModes(
|
|
122
193
|
cwd: string,
|
|
123
194
|
stateDir: string,
|
|
@@ -460,7 +531,22 @@ export async function handleTmuxInjection({
|
|
|
460
531
|
turnId,
|
|
461
532
|
timestamp: nowIso,
|
|
462
533
|
}), sourceText);
|
|
463
|
-
const
|
|
534
|
+
const managedCurrentPane = await resolveManagedCurrentPane(cwd, payload, { allowTeamWorker: false });
|
|
535
|
+
if (modePane && managedCurrentPane && modePane !== managedCurrentPane) {
|
|
536
|
+
state.last_reason = 'mode_pane_current_pane_mismatch';
|
|
537
|
+
state.last_event_at = nowIso;
|
|
538
|
+
await writeFile(hookStatePath, JSON.stringify(state, null, 2)).catch(() => {});
|
|
539
|
+
await logTmuxHookEvent(logsDir, {
|
|
540
|
+
...baseLog,
|
|
541
|
+
event: 'injection_skipped',
|
|
542
|
+
reason: 'mode_pane_current_pane_mismatch',
|
|
543
|
+
mode_pane: modePane,
|
|
544
|
+
current_pane: managedCurrentPane,
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const preferredPaneTarget = modePane || managedCurrentPane;
|
|
464
550
|
let resolution = preferredModePane
|
|
465
551
|
? await resolvePaneTarget({ type: 'pane', value: preferredModePane.pane }, cwd, preferredModePane.pane, cwd, payload)
|
|
466
552
|
: preferredPaneTarget
|
|
@@ -484,6 +570,27 @@ export async function handleTmuxInjection({
|
|
|
484
570
|
}
|
|
485
571
|
const paneTarget = resolution.paneTarget;
|
|
486
572
|
|
|
573
|
+
const ownership = await validateResolvedInjectionOwnership({
|
|
574
|
+
paneTarget,
|
|
575
|
+
cwd,
|
|
576
|
+
payload,
|
|
577
|
+
modeState,
|
|
578
|
+
modePane,
|
|
579
|
+
managedCurrentPane,
|
|
580
|
+
});
|
|
581
|
+
if (!ownership.ok) {
|
|
582
|
+
state.last_reason = ownership.reason;
|
|
583
|
+
state.last_event_at = nowIso;
|
|
584
|
+
await writeFile(hookStatePath, JSON.stringify(state, null, 2)).catch(() => {});
|
|
585
|
+
await logTmuxHookEvent(logsDir, {
|
|
586
|
+
...baseLog,
|
|
587
|
+
event: 'injection_skipped',
|
|
588
|
+
reason: ownership.reason,
|
|
589
|
+
pane_target: paneTarget,
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
487
594
|
// Final guard phase: pane is canonical identity for quota/cooldown.
|
|
488
595
|
const guard = evaluateInjectionGuards({
|
|
489
596
|
config,
|
|
@@ -181,13 +181,20 @@
|
|
|
181
181
|
"internalRequired": false
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
|
-
"name": "
|
|
184
|
+
"name": "design",
|
|
185
185
|
"category": "shortcut",
|
|
186
|
-
"status": "
|
|
186
|
+
"status": "active",
|
|
187
187
|
"canonical": "designer",
|
|
188
188
|
"core": false,
|
|
189
189
|
"internalRequired": false
|
|
190
190
|
},
|
|
191
|
+
{
|
|
192
|
+
"name": "frontend-ui-ux",
|
|
193
|
+
"category": "shortcut",
|
|
194
|
+
"status": "deprecated",
|
|
195
|
+
"core": false,
|
|
196
|
+
"internalRequired": false
|
|
197
|
+
},
|
|
191
198
|
{
|
|
192
199
|
"name": "git-master",
|
|
193
200
|
"category": "shortcut",
|