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.
Files changed (138) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/dist/catalog/__tests__/generator.test.js +2 -0
  4. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  5. package/dist/cli/__tests__/doctor-warning-copy.test.js +80 -7
  6. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  7. package/dist/cli/__tests__/index.test.js +17 -11
  8. package/dist/cli/__tests__/index.test.js.map +1 -1
  9. package/dist/cli/__tests__/mcp-serve.test.js +4 -0
  10. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
  11. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +8 -3
  12. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  13. package/dist/cli/__tests__/setup-install-mode.test.js +27 -1
  14. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  15. package/dist/cli/__tests__/ultragoal.test.js +22 -0
  16. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  17. package/dist/cli/doctor.d.ts.map +1 -1
  18. package/dist/cli/doctor.js +66 -10
  19. package/dist/cli/doctor.js.map +1 -1
  20. package/dist/cli/index.d.ts +8 -2
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +17 -7
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/cli/mcp-serve.d.ts.map +1 -1
  25. package/dist/cli/mcp-serve.js +4 -0
  26. package/dist/cli/mcp-serve.js.map +1 -1
  27. package/dist/cli/plugin-marketplace.d.ts +20 -0
  28. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  29. package/dist/cli/plugin-marketplace.js +115 -1
  30. package/dist/cli/plugin-marketplace.js.map +1 -1
  31. package/dist/cli/setup.d.ts.map +1 -1
  32. package/dist/cli/setup.js +29 -10
  33. package/dist/cli/setup.js.map +1 -1
  34. package/dist/cli/ultragoal.d.ts.map +1 -1
  35. package/dist/cli/ultragoal.js +7 -1
  36. package/dist/cli/ultragoal.js.map +1 -1
  37. package/dist/config/__tests__/codex-hooks.test.js +136 -9
  38. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  39. package/dist/config/__tests__/generator-idempotent.test.js +15 -0
  40. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  41. package/dist/config/codex-hooks.d.ts +13 -14
  42. package/dist/config/codex-hooks.d.ts.map +1 -1
  43. package/dist/config/codex-hooks.js +85 -7
  44. package/dist/config/codex-hooks.js.map +1 -1
  45. package/dist/config/generator.d.ts +4 -1
  46. package/dist/config/generator.d.ts.map +1 -1
  47. package/dist/config/generator.js +15 -9
  48. package/dist/config/generator.js.map +1 -1
  49. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  50. package/dist/config/omx-first-party-mcp.js +7 -0
  51. package/dist/config/omx-first-party-mcp.js.map +1 -1
  52. package/dist/hooks/__tests__/design-skill.test.d.ts +2 -0
  53. package/dist/hooks/__tests__/design-skill.test.d.ts.map +1 -0
  54. package/dist/hooks/__tests__/design-skill.test.js +55 -0
  55. package/dist/hooks/__tests__/design-skill.test.js.map +1 -0
  56. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +265 -0
  57. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  58. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +1 -1
  59. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  60. package/dist/hooks/__tests__/skill-guidance-contract.test.js +41 -0
  61. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  62. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  63. package/dist/hooks/keyword-detector.js +5 -1
  64. package/dist/hooks/keyword-detector.js.map +1 -1
  65. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  66. package/dist/hooks/keyword-registry.js +2 -0
  67. package/dist/hooks/keyword-registry.js.map +1 -1
  68. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  69. package/dist/hooks/prompt-guidance-contract.js +47 -2
  70. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  71. package/dist/mcp/__tests__/bootstrap.test.js +3 -0
  72. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  73. package/dist/mcp/__tests__/hermes-bridge.test.d.ts +2 -0
  74. package/dist/mcp/__tests__/hermes-bridge.test.d.ts.map +1 -0
  75. package/dist/mcp/__tests__/hermes-bridge.test.js +374 -0
  76. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -0
  77. package/dist/mcp/__tests__/state-paths.test.js +96 -13
  78. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  79. package/dist/mcp/bootstrap.d.ts +1 -1
  80. package/dist/mcp/bootstrap.d.ts.map +1 -1
  81. package/dist/mcp/bootstrap.js +2 -0
  82. package/dist/mcp/bootstrap.js.map +1 -1
  83. package/dist/mcp/hermes-bridge.d.ts +81 -0
  84. package/dist/mcp/hermes-bridge.d.ts.map +1 -0
  85. package/dist/mcp/hermes-bridge.js +400 -0
  86. package/dist/mcp/hermes-bridge.js.map +1 -0
  87. package/dist/mcp/hermes-server.d.ts +269 -0
  88. package/dist/mcp/hermes-server.d.ts.map +1 -0
  89. package/dist/mcp/hermes-server.js +121 -0
  90. package/dist/mcp/hermes-server.js.map +1 -0
  91. package/dist/mcp/state-paths.d.ts.map +1 -1
  92. package/dist/mcp/state-paths.js +41 -9
  93. package/dist/mcp/state-paths.js.map +1 -1
  94. package/dist/modes/__tests__/base-tmux-pane.test.js +31 -1
  95. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  96. package/dist/scripts/__tests__/codex-native-hook.test.js +187 -2
  97. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  98. package/dist/scripts/codex-native-hook.d.ts +1 -0
  99. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  100. package/dist/scripts/codex-native-hook.js +44 -17
  101. package/dist/scripts/codex-native-hook.js.map +1 -1
  102. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  103. package/dist/scripts/notify-hook/tmux-injection.js +91 -2
  104. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  105. package/dist/state/mode-state-context.d.ts +2 -0
  106. package/dist/state/mode-state-context.d.ts.map +1 -1
  107. package/dist/state/mode-state-context.js +21 -0
  108. package/dist/state/mode-state-context.js.map +1 -1
  109. package/dist/ultragoal/__tests__/artifacts.test.js +121 -0
  110. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  111. package/dist/ultragoal/artifacts.d.ts +9 -1
  112. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  113. package/dist/ultragoal/artifacts.js +105 -3
  114. package/dist/ultragoal/artifacts.js.map +1 -1
  115. package/dist/utils/__tests__/paths.test.js +31 -1
  116. package/dist/utils/__tests__/paths.test.js.map +1 -1
  117. package/dist/utils/paths.d.ts +6 -0
  118. package/dist/utils/paths.d.ts.map +1 -1
  119. package/dist/utils/paths.js +18 -0
  120. package/dist/utils/paths.js.map +1 -1
  121. package/dist/wiki/lifecycle.js +3 -3
  122. package/dist/wiki/lifecycle.js.map +1 -1
  123. package/package.json +1 -1
  124. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  125. package/plugins/oh-my-codex/.mcp.json +8 -0
  126. package/plugins/oh-my-codex/skills/design/SKILL.md +180 -0
  127. package/plugins/oh-my-codex/skills/skill/SKILL.md +2 -1
  128. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +161 -47
  129. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +2 -2
  130. package/skills/design/SKILL.md +180 -0
  131. package/skills/frontend-ui-ux/SKILL.md +6 -2
  132. package/skills/skill/SKILL.md +2 -1
  133. package/skills/ultraqa/SKILL.md +161 -47
  134. package/skills/visual-ralph/SKILL.md +2 -2
  135. package/src/scripts/__tests__/codex-native-hook.test.ts +206 -1
  136. package/src/scripts/codex-native-hook.ts +45 -18
  137. package/src/scripts/notify-hook/tmux-injection.ts +110 -3
  138. 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, omxProjectMemoryPath } from "../utils/paths.js";
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 projectMemory = await readJsonIfExists(omxProjectMemoryPath(cwd));
1126
- if (projectMemory) {
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 = safeString(projectMemory.techStack).trim();
1130
- const conventions = safeString(projectMemory.conventions).trim();
1131
- const build = safeString(projectMemory.build).trim();
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 = safeString(firstDirective.directive).trim();
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 = safeString(firstNote.content).trim();
1152
+ const note = safeContextSnippet(firstNote.content);
1144
1153
  if (note) summary.push(`- note: ${note}`);
1145
1154
  }
1146
- if (summary.length > 0) {
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 /\b(?:complete|checkpoint|finish|close|mark)\b.{0,80}\b(?:goal|ultragoal|performance-goal|autoresearch-goal)\b/i.test(text)
1700
- || /\bupdate_goal\s*\(/i.test(text)
1701
- || /\bomx\s+(?:ultragoal|performance-goal|autoresearch-goal)\s+(?:checkpoint|complete)\b/i.test(text);
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 = ultragoals.find((goal) => safeString(goal.status) === "in_progress" || safeString(goal.id) === safeString(ultragoal?.activeGoalId));
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 ${safeString(activeUltragoal.id) || "<goal-id>"} --status complete --codex-goal-json '<get_goal JSON or path>' --evidence '<evidence>'`,
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
- ].join(" ");
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
- `OMX ${requirement.workflow} requires get_goal snapshot reconciliation before completion; call get_goal and pass --codex-goal-json to ${requirement.command}. Hooks must not mutate Codex goal state.`;
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 preferredPaneTarget = modePane || await resolveManagedCurrentPane(cwd, payload, { allowTeamWorker: false });
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": "frontend-ui-ux",
184
+ "name": "design",
185
185
  "category": "shortcut",
186
- "status": "alias",
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",