oh-my-codex 0.17.3 → 0.18.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 +13 -5
- package/Cargo.toml +2 -1
- package/README.md +1 -0
- package/crates/omx-api/Cargo.toml +19 -0
- package/crates/omx-api/src/lib.rs +2940 -0
- package/crates/omx-api/src/main.rs +10 -0
- package/crates/omx-api/tests/cli.rs +558 -0
- package/crates/omx-explore/src/main.rs +4 -0
- package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
- package/crates/omx-sparkshell/src/exec.rs +4 -0
- package/crates/omx-sparkshell/src/main.rs +738 -29
- package/crates/omx-sparkshell/src/prompt.rs +25 -3
- package/crates/omx-sparkshell/src/redaction.rs +241 -0
- package/crates/omx-sparkshell/tests/execution.rs +479 -238
- package/dist/cli/__tests__/api.test.d.ts +2 -0
- package/dist/cli/__tests__/api.test.d.ts.map +1 -0
- package/dist/cli/__tests__/api.test.js +175 -0
- package/dist/cli/__tests__/api.test.js.map +1 -0
- package/dist/cli/__tests__/ask.test.js +72 -5
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +23 -0
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +123 -5
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +76 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
- package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts +26 -0
- package/dist/cli/api.d.ts.map +1 -0
- package/dist/cli/api.js +153 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/explore.d.ts +2 -0
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +43 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +10 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +128 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +2 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +1 -0
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +20 -3
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +90 -0
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +1 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +2 -2
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +23 -18
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +7 -6
- package/dist/hud/tmux.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +75 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +3 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +71 -2
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/build-api.d.ts +2 -0
- package/dist/scripts/build-api.d.ts.map +1 -0
- package/dist/scripts/build-api.js +44 -0
- package/dist/scripts/build-api.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +208 -8
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +89 -24
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-dispatcher.js +88 -0
- package/dist/scripts/notify-dispatcher.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +27 -9
- package/dist/scripts/notify-hook/team-dispatch.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 -11
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/run-provider-advisor.js +9 -3
- package/dist/scripts/run-provider-advisor.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +1 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +2 -0
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +2 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +96 -19
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +34 -10
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/package.json +4 -3
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/prompts/researcher.md +15 -10
- package/skills/best-practice-research/SKILL.md +83 -0
- package/skills/deep-interview/SKILL.md +1 -0
- package/skills/ralplan/SKILL.md +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
- package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
- package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
- package/src/scripts/build-api.ts +48 -0
- package/src/scripts/codex-native-hook.ts +262 -10
- package/src/scripts/codex-native-pre-post.ts +103 -24
- package/src/scripts/notify-dispatcher.ts +97 -0
- package/src/scripts/notify-hook/team-dispatch.ts +27 -8
- package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
- package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
- package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
- package/src/scripts/run-provider-advisor.ts +11 -3
- package/src/scripts/smoke-packed-install.ts +2 -0
- package/templates/catalog-manifest.json +7 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync } from "node:fs";
|
|
4
4
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -91,6 +91,228 @@ describe("notify dispatcher previousNotify guard", () => {
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it("skips stale turn-ended wrappers whose previousNotify is an OMX dispatcher", () => {
|
|
95
|
+
const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-wrapper-"));
|
|
96
|
+
try {
|
|
97
|
+
const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
|
|
98
|
+
mkdirSync(oldPkgScripts, { recursive: true });
|
|
99
|
+
const stalePreviousMarker = join(wd, "stale-wrapper-ran");
|
|
100
|
+
const omxMarker = join(wd, "omx-ran");
|
|
101
|
+
const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
|
|
102
|
+
const turnEndedWrapper = join(wd, "SkyComputerUseClient");
|
|
103
|
+
const omxHook = join(wd, "current-notify-hook.js");
|
|
104
|
+
writeFileSync(
|
|
105
|
+
turnEndedWrapper,
|
|
106
|
+
`import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
|
|
107
|
+
);
|
|
108
|
+
writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
|
|
109
|
+
const metadataPath = join(wd, "notify-dispatch.json");
|
|
110
|
+
writeFileSync(
|
|
111
|
+
metadataPath,
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
managedBy: "oh-my-codex",
|
|
114
|
+
version: 1,
|
|
115
|
+
previousNotify: [
|
|
116
|
+
process.execPath,
|
|
117
|
+
turnEndedWrapper,
|
|
118
|
+
"turn-ended",
|
|
119
|
+
"--previous-notify",
|
|
120
|
+
JSON.stringify([
|
|
121
|
+
process.execPath,
|
|
122
|
+
staleDispatcher,
|
|
123
|
+
"--metadata",
|
|
124
|
+
metadataPath,
|
|
125
|
+
]),
|
|
126
|
+
],
|
|
127
|
+
omxNotify: [process.execPath, omxHook],
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
runDispatcher(metadataPath);
|
|
132
|
+
|
|
133
|
+
assert.equal(existsSync(stalePreviousMarker), false);
|
|
134
|
+
assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
|
|
135
|
+
} finally {
|
|
136
|
+
rmSync(wd, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("skips stale turn-ended wrappers whose previousNotify text is an OMX hook", () => {
|
|
141
|
+
const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-wrapper-text-"));
|
|
142
|
+
try {
|
|
143
|
+
const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
|
|
144
|
+
mkdirSync(oldPkgScripts, { recursive: true });
|
|
145
|
+
const stalePreviousMarker = join(wd, "stale-wrapper-ran");
|
|
146
|
+
const omxMarker = join(wd, "omx-ran");
|
|
147
|
+
const staleHook = join(oldPkgScripts, "notify-hook.js");
|
|
148
|
+
const turnEndedWrapper = join(wd, "SkyComputerUseClient");
|
|
149
|
+
const omxHook = join(wd, "current-notify-hook.js");
|
|
150
|
+
writeFileSync(
|
|
151
|
+
turnEndedWrapper,
|
|
152
|
+
`import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
|
|
153
|
+
);
|
|
154
|
+
writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
|
|
155
|
+
const metadataPath = join(wd, "notify-dispatch.json");
|
|
156
|
+
writeFileSync(
|
|
157
|
+
metadataPath,
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
managedBy: "oh-my-codex",
|
|
160
|
+
version: 1,
|
|
161
|
+
previousNotify: [
|
|
162
|
+
process.execPath,
|
|
163
|
+
turnEndedWrapper,
|
|
164
|
+
"turn-ended",
|
|
165
|
+
`--previous-notify=node ${staleHook}`,
|
|
166
|
+
],
|
|
167
|
+
omxNotify: [process.execPath, omxHook],
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
runDispatcher(metadataPath);
|
|
172
|
+
|
|
173
|
+
assert.equal(existsSync(stalePreviousMarker), false);
|
|
174
|
+
assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
|
|
175
|
+
} finally {
|
|
176
|
+
rmSync(wd, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("skips SkyComputerUseClient wrappers with quoted nested previousNotify self-references", () => {
|
|
181
|
+
const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-nested-wrapper-"));
|
|
182
|
+
try {
|
|
183
|
+
const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
|
|
184
|
+
mkdirSync(oldPkgScripts, { recursive: true });
|
|
185
|
+
const stalePreviousMarker = join(wd, "nested-wrapper-ran");
|
|
186
|
+
const omxMarker = join(wd, "omx-ran");
|
|
187
|
+
const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
|
|
188
|
+
const turnEndedWrapper = join(wd, "SkyComputerUseClient");
|
|
189
|
+
const omxHook = join(wd, "current-notify-hook.js");
|
|
190
|
+
writeFileSync(
|
|
191
|
+
turnEndedWrapper,
|
|
192
|
+
`import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
|
|
193
|
+
);
|
|
194
|
+
writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
|
|
195
|
+
const metadataPath = join(wd, "notify-dispatch.json");
|
|
196
|
+
const nestedSelfReference = JSON.stringify([
|
|
197
|
+
process.execPath,
|
|
198
|
+
turnEndedWrapper,
|
|
199
|
+
"turn-ended",
|
|
200
|
+
"--previous-notify",
|
|
201
|
+
JSON.stringify([process.execPath, staleDispatcher, "--metadata", metadataPath]),
|
|
202
|
+
]);
|
|
203
|
+
writeFileSync(
|
|
204
|
+
metadataPath,
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
managedBy: "oh-my-codex",
|
|
207
|
+
version: 1,
|
|
208
|
+
previousNotify: [
|
|
209
|
+
process.execPath,
|
|
210
|
+
turnEndedWrapper,
|
|
211
|
+
"turn-ended",
|
|
212
|
+
"--previous-notify",
|
|
213
|
+
JSON.stringify(nestedSelfReference),
|
|
214
|
+
],
|
|
215
|
+
omxNotify: [process.execPath, omxHook],
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
runDispatcher(metadataPath);
|
|
220
|
+
|
|
221
|
+
assert.equal(existsSync(stalePreviousMarker), false);
|
|
222
|
+
assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
|
|
223
|
+
} finally {
|
|
224
|
+
rmSync(wd, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("skips wrapper metadata objects with encoded OMX dispatcher payloads", () => {
|
|
229
|
+
const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-object-wrapper-"));
|
|
230
|
+
try {
|
|
231
|
+
const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
|
|
232
|
+
mkdirSync(oldPkgScripts, { recursive: true });
|
|
233
|
+
const stalePreviousMarker = join(wd, "object-wrapper-ran");
|
|
234
|
+
const omxMarker = join(wd, "omx-ran");
|
|
235
|
+
const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
|
|
236
|
+
const turnEndedWrapper = join(wd, "SkyComputerUseClient");
|
|
237
|
+
const omxHook = join(wd, "current-notify-hook.js");
|
|
238
|
+
writeFileSync(
|
|
239
|
+
turnEndedWrapper,
|
|
240
|
+
`import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
|
|
241
|
+
);
|
|
242
|
+
writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
|
|
243
|
+
const metadataPath = join(wd, "notify-dispatch.json");
|
|
244
|
+
writeFileSync(
|
|
245
|
+
metadataPath,
|
|
246
|
+
JSON.stringify({
|
|
247
|
+
managedBy: "oh-my-codex",
|
|
248
|
+
version: 1,
|
|
249
|
+
previousNotify: [
|
|
250
|
+
process.execPath,
|
|
251
|
+
turnEndedWrapper,
|
|
252
|
+
"turn-ended",
|
|
253
|
+
`--previous-notify=${JSON.stringify({ previousNotify: JSON.stringify([process.execPath, staleDispatcher]) })}`,
|
|
254
|
+
],
|
|
255
|
+
omxNotify: [process.execPath, omxHook],
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
runDispatcher(metadataPath);
|
|
260
|
+
|
|
261
|
+
assert.equal(existsSync(stalePreviousMarker), false);
|
|
262
|
+
assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
|
|
263
|
+
} finally {
|
|
264
|
+
rmSync(wd, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("skips reporter-shaped SkyComputerUseClient previousNotify dispatcher recursion", () => {
|
|
269
|
+
const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-reporter-wrapper-"));
|
|
270
|
+
try {
|
|
271
|
+
const pkgScripts = join(wd, "pkg-without-managed-name", "dist", "scripts");
|
|
272
|
+
mkdirSync(pkgScripts, { recursive: true });
|
|
273
|
+
const stalePreviousMarker = join(wd, "skycomputer-ran");
|
|
274
|
+
const omxMarker = join(wd, "omx-ran");
|
|
275
|
+
const dispatcher = join(pkgScripts, "notify-dispatcher.js");
|
|
276
|
+
const turnEndedWrapper = join(wd, "SkyComputerUseClient");
|
|
277
|
+
const omxHook = join(wd, "current-notify-hook.js");
|
|
278
|
+
writeFileSync(dispatcher, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "dispatcher");\n`);
|
|
279
|
+
writeFileSync(
|
|
280
|
+
turnEndedWrapper,
|
|
281
|
+
`#!/usr/bin/env node\nimport { appendFileSync } from "node:fs"; appendFileSync(${JSON.stringify(stalePreviousMarker)}, "wrapper\\n");\n`,
|
|
282
|
+
);
|
|
283
|
+
chmodSync(turnEndedWrapper, 0o755);
|
|
284
|
+
writeFileSync(omxHook, `import { appendFileSync } from "node:fs"; appendFileSync(${JSON.stringify(omxMarker)}, "omx\\n");\n`);
|
|
285
|
+
const metadataPath = join(wd, "notify-dispatch.json");
|
|
286
|
+
writeFileSync(
|
|
287
|
+
metadataPath,
|
|
288
|
+
JSON.stringify({
|
|
289
|
+
managedBy: "oh-my-codex",
|
|
290
|
+
version: 1,
|
|
291
|
+
previousNotify: [
|
|
292
|
+
turnEndedWrapper,
|
|
293
|
+
"turn-ended",
|
|
294
|
+
"--previous-notify",
|
|
295
|
+
JSON.stringify([
|
|
296
|
+
"node",
|
|
297
|
+
dispatcher,
|
|
298
|
+
"--metadata",
|
|
299
|
+
metadataPath,
|
|
300
|
+
]),
|
|
301
|
+
],
|
|
302
|
+
omxNotify: [process.execPath, omxHook],
|
|
303
|
+
dispatcherNotify: ["node", dispatcher, "--metadata", metadataPath],
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
runDispatcher(metadataPath);
|
|
308
|
+
|
|
309
|
+
assert.equal(existsSync(stalePreviousMarker), false);
|
|
310
|
+
assert.equal(readFileSync(omxMarker, "utf-8"), "omx\n");
|
|
311
|
+
} finally {
|
|
312
|
+
rmSync(wd, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
94
316
|
it("preserves and runs real user previousNotify entries", () => {
|
|
95
317
|
const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-user-"));
|
|
96
318
|
try {
|
|
@@ -17,10 +17,16 @@ test('packed install smoke stays limited to boot + core commands', () => {
|
|
|
17
17
|
assert.deepEqual(PACKED_INSTALL_SMOKE_CORE_COMMANDS, [
|
|
18
18
|
['--help'],
|
|
19
19
|
['version'],
|
|
20
|
+
['api', '--help'],
|
|
21
|
+
['sparkshell', '--help'],
|
|
20
22
|
]);
|
|
21
23
|
assert.equal(
|
|
22
|
-
PACKED_INSTALL_SMOKE_CORE_COMMANDS.some((argv) => argv.includes('
|
|
23
|
-
|
|
24
|
+
PACKED_INSTALL_SMOKE_CORE_COMMANDS.some((argv) => argv.includes('api')),
|
|
25
|
+
true,
|
|
26
|
+
);
|
|
27
|
+
assert.equal(
|
|
28
|
+
PACKED_INSTALL_SMOKE_CORE_COMMANDS.some((argv) => argv.includes('sparkshell')),
|
|
29
|
+
true,
|
|
24
30
|
);
|
|
25
31
|
});
|
|
26
32
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { arch, platform } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const projectRoot = join(__dirname, '..', '..');
|
|
10
|
+
const nativeRoot = join(projectRoot, 'crates', 'omx-api');
|
|
11
|
+
const manifestPath = process.env.OMX_API_MANIFEST ?? join(nativeRoot, 'Cargo.toml');
|
|
12
|
+
const binaryName = platform() === 'win32' ? 'omx-api.exe' : 'omx-api';
|
|
13
|
+
const releaseBinaryPath = join(projectRoot, 'target', 'release', binaryName);
|
|
14
|
+
const stagedBinaryRoot = process.env.OMX_API_STAGE_DIR
|
|
15
|
+
? join(process.env.OMX_API_STAGE_DIR, `${platform()}-${arch()}`)
|
|
16
|
+
: join(projectRoot, 'bin', 'native', `${platform()}-${arch()}`);
|
|
17
|
+
const packagedBinaryDir = stagedBinaryRoot;
|
|
18
|
+
const packagedBinaryPath = join(packagedBinaryDir, binaryName);
|
|
19
|
+
const extraArgs = process.argv.slice(2);
|
|
20
|
+
const args = ['build', '--manifest-path', manifestPath, '--release', ...extraArgs];
|
|
21
|
+
|
|
22
|
+
if (!existsSync(manifestPath)) {
|
|
23
|
+
console.error(`omx api build: missing Rust manifest at ${manifestPath}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = spawnSync('cargo', args, {
|
|
28
|
+
cwd: projectRoot,
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
env: process.env,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (result.error) {
|
|
34
|
+
console.error(`omx api build: failed to launch cargo: ${result.error.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1);
|
|
39
|
+
|
|
40
|
+
if (!existsSync(releaseBinaryPath)) {
|
|
41
|
+
console.error(`omx api build: expected release binary at ${releaseBinaryPath}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
mkdirSync(packagedBinaryDir, { recursive: true });
|
|
46
|
+
copyFileSync(releaseBinaryPath, packagedBinaryPath);
|
|
47
|
+
if (platform() !== 'win32') chmodSync(packagedBinaryPath, 0o755);
|
|
48
|
+
console.log(`omx api build: staged native binary at ${packagedBinaryPath}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
2
|
import { closeSync, existsSync, openSync, readFileSync, readSync } from "fs";
|
|
3
|
-
import { appendFile, mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
|
|
4
4
|
import { extname, join, relative, resolve } from "path";
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
6
|
import { readModeState, readModeStateForActiveDecision, readModeStateForSession, updateModeState } from "../modes/base.js";
|
|
@@ -128,6 +128,7 @@ const TEAM_STOP_BLOCKING_TASK_STATUSES = new Set(["pending", "in_progress", "blo
|
|
|
128
128
|
const TEAM_WORKER_TERMINAL_RUN_STATES = new Set(["done", "complete", "completed", "failed", "stopped", "cancelled"]);
|
|
129
129
|
const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
|
|
130
130
|
const ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS = 8;
|
|
131
|
+
const RALPH_ORPHANED_STARTING_STALE_MS = 15 * 60_000;
|
|
131
132
|
const ORDINARY_STOP_NO_PROGRESS_DEFAULT_IDLE_MS = 10 * 60_000;
|
|
132
133
|
const ORDINARY_STOP_NO_PROGRESS_MAX_MESSAGE_LENGTH = 240;
|
|
133
134
|
const STABLE_FINAL_RECOMMENDATION_PATTERNS = [
|
|
@@ -513,6 +514,52 @@ function isRalphStartingPhase(state: Record<string, unknown>): boolean {
|
|
|
513
514
|
return safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase() === "starting";
|
|
514
515
|
}
|
|
515
516
|
|
|
517
|
+
|
|
518
|
+
function parseTimestampMs(value: unknown): number | null {
|
|
519
|
+
const text = safeString(value).trim();
|
|
520
|
+
if (!text) return null;
|
|
521
|
+
const ms = Date.parse(text);
|
|
522
|
+
return Number.isFinite(ms) ? ms : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function numericValue(value: unknown): number | null {
|
|
526
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
527
|
+
if (typeof value === "string" && value.trim()) {
|
|
528
|
+
const parsed = Number(value);
|
|
529
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function hasRalphOwnerHint(state: Record<string, unknown>): boolean {
|
|
535
|
+
return [
|
|
536
|
+
state.owner_omx_session_id,
|
|
537
|
+
state.owner_codex_session_id,
|
|
538
|
+
state.owner_codex_thread_id,
|
|
539
|
+
state.thread_id,
|
|
540
|
+
state.tmux_pane_id,
|
|
541
|
+
state.task_slug,
|
|
542
|
+
].some((value) => safeString(value).trim() !== "");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function isStaleOrphanedRalphStartingState(
|
|
546
|
+
state: Record<string, unknown>,
|
|
547
|
+
path: string,
|
|
548
|
+
nowMs = Date.now(),
|
|
549
|
+
): Promise<boolean> {
|
|
550
|
+
if (!isRalphStartingPhase(state)) return false;
|
|
551
|
+
if (numericValue(state.iteration) !== 0) return false;
|
|
552
|
+
if (hasRalphOwnerHint(state)) return false;
|
|
553
|
+
|
|
554
|
+
const timestampMs = parseTimestampMs(state.updated_at)
|
|
555
|
+
?? parseTimestampMs(state.started_at)
|
|
556
|
+
?? parseTimestampMs(state.created_at)
|
|
557
|
+
?? await stat(path).then((info) => info.mtimeMs, () => null);
|
|
558
|
+
if (timestampMs === null) return false;
|
|
559
|
+
|
|
560
|
+
return nowMs - timestampMs > RALPH_ORPHANED_STARTING_STALE_MS;
|
|
561
|
+
}
|
|
562
|
+
|
|
516
563
|
function hasValue(values: string[], value: string): boolean {
|
|
517
564
|
return value !== "" && values.some((candidate) => candidate === value);
|
|
518
565
|
}
|
|
@@ -597,6 +644,99 @@ async function hasConsistentRalphSkillActivation(stateDir: string, sessionId: st
|
|
|
597
644
|
return true;
|
|
598
645
|
}
|
|
599
646
|
|
|
647
|
+
function isShadowableRalphStartingSeed(state: Record<string, unknown>): boolean {
|
|
648
|
+
if (state.active !== true) return false;
|
|
649
|
+
if (!isRalphStartingPhase(state)) return false;
|
|
650
|
+
if (state.completion_audit || state.completionAudit) return false;
|
|
651
|
+
const iteration = numericValue(state.iteration);
|
|
652
|
+
return iteration === null || iteration <= 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function hasPassingCompletedRalphAudit(state: Record<string, unknown> | null, cwd: string): boolean {
|
|
656
|
+
if (!state) return false;
|
|
657
|
+
if (state.mode && safeString(state.mode) !== "ralph") return false;
|
|
658
|
+
if (!isRalphCompletePhase(state.current_phase ?? state.currentPhase)) return false;
|
|
659
|
+
if (state.active === true) return false;
|
|
660
|
+
return evaluateRalphCompletionAuditEvidence(state, cwd).complete === true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function shouldRetireShadowedRalphStartingSeed(
|
|
664
|
+
seedState: Record<string, unknown>,
|
|
665
|
+
completedState: Record<string, unknown> | null,
|
|
666
|
+
cwd: string,
|
|
667
|
+
ownerContext?: {
|
|
668
|
+
completedSessionId?: string;
|
|
669
|
+
payloadSessionId?: string;
|
|
670
|
+
threadId?: string;
|
|
671
|
+
currentNativeSessionId?: string;
|
|
672
|
+
tmuxPaneId?: string;
|
|
673
|
+
},
|
|
674
|
+
): boolean {
|
|
675
|
+
if (!isShadowableRalphStartingSeed(seedState)) return false;
|
|
676
|
+
if (!hasPassingCompletedRalphAudit(completedState, cwd)) return false;
|
|
677
|
+
if (!completedState) return false;
|
|
678
|
+
|
|
679
|
+
const completedSessionId = safeString(ownerContext?.completedSessionId ?? completedState.session_id).trim();
|
|
680
|
+
if (
|
|
681
|
+
completedSessionId
|
|
682
|
+
&& !activeRalphStateMatchesStopOwner(completedState, {
|
|
683
|
+
sessionId: completedSessionId,
|
|
684
|
+
payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
|
|
685
|
+
threadId: safeString(ownerContext?.threadId).trim(),
|
|
686
|
+
currentNativeSessionId: safeString(ownerContext?.currentNativeSessionId).trim(),
|
|
687
|
+
tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
|
|
688
|
+
})
|
|
689
|
+
) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const seedThreadId = safeString(seedState.owner_codex_thread_id ?? seedState.thread_id).trim();
|
|
694
|
+
const completedThreadId = safeString(completedState?.owner_codex_thread_id ?? completedState?.thread_id).trim();
|
|
695
|
+
const stopThreadId = safeString(ownerContext?.threadId).trim();
|
|
696
|
+
if (seedThreadId && completedThreadId && seedThreadId !== completedThreadId) return false;
|
|
697
|
+
if (seedThreadId && stopThreadId && seedThreadId !== stopThreadId) return false;
|
|
698
|
+
if (completedThreadId && stopThreadId && completedThreadId !== stopThreadId) return false;
|
|
699
|
+
|
|
700
|
+
const seedPaneId = safeString(seedState.tmux_pane_id).trim();
|
|
701
|
+
const completedPaneId = safeString(completedState?.tmux_pane_id).trim();
|
|
702
|
+
const stopPaneId = safeString(ownerContext?.tmuxPaneId).trim();
|
|
703
|
+
if (seedPaneId && completedPaneId && seedPaneId !== completedPaneId) return false;
|
|
704
|
+
if (seedPaneId && stopPaneId && seedPaneId !== stopPaneId) return false;
|
|
705
|
+
if (completedPaneId && stopPaneId && completedPaneId !== stopPaneId) return false;
|
|
706
|
+
|
|
707
|
+
const seedStartedAt = parseTimestampMs(seedState.started_at ?? seedState.startedAt);
|
|
708
|
+
const completedAt = parseTimestampMs(completedState?.completed_at ?? completedState?.completedAt);
|
|
709
|
+
if (completedAt === null) return false;
|
|
710
|
+
if (seedStartedAt !== null && seedStartedAt > completedAt) return false;
|
|
711
|
+
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function retireShadowedRalphStartingSeed(
|
|
716
|
+
path: string,
|
|
717
|
+
seedState: Record<string, unknown>,
|
|
718
|
+
completedSessionId: string,
|
|
719
|
+
completedPath: string,
|
|
720
|
+
completedState: Record<string, unknown>,
|
|
721
|
+
): Promise<void> {
|
|
722
|
+
const nowIso = new Date().toISOString();
|
|
723
|
+
const completedAt = safeString(completedState.completed_at ?? completedState.completedAt).trim() || nowIso;
|
|
724
|
+
const next: Record<string, unknown> = {
|
|
725
|
+
...seedState,
|
|
726
|
+
active: false,
|
|
727
|
+
current_phase: "complete",
|
|
728
|
+
completed_at: completedAt,
|
|
729
|
+
stop_reason: "shadowed_by_completed_canonical_ralph",
|
|
730
|
+
shadowed_by_completed_canonical_ralph: {
|
|
731
|
+
session_id: completedSessionId,
|
|
732
|
+
state_path: completedPath,
|
|
733
|
+
completed_at: completedAt,
|
|
734
|
+
reconciled_at: nowIso,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
await writeFile(path, JSON.stringify(next, null, 2));
|
|
738
|
+
}
|
|
739
|
+
|
|
600
740
|
|
|
601
741
|
async function readRalphCompletionAuditBlockState(
|
|
602
742
|
cwd: string,
|
|
@@ -686,6 +826,12 @@ async function readActiveRalphState(
|
|
|
686
826
|
safeString(preferredSessionId).trim(),
|
|
687
827
|
currentOmxSessionId,
|
|
688
828
|
].filter(Boolean))];
|
|
829
|
+
const completedCanonicalPath = currentOmxSessionId
|
|
830
|
+
? getStateFilePath("ralph-state.json", cwd, currentOmxSessionId)
|
|
831
|
+
: "";
|
|
832
|
+
const completedCanonicalState = completedCanonicalPath
|
|
833
|
+
? await readJsonIfExists(completedCanonicalPath)
|
|
834
|
+
: null;
|
|
689
835
|
|
|
690
836
|
// Ralph Stop stays authoritative-scope-only once the Stop payload is session-bound.
|
|
691
837
|
// That is intentionally stricter than generic state MCP reads: do not scan sibling
|
|
@@ -699,12 +845,37 @@ async function readActiveRalphState(
|
|
|
699
845
|
}
|
|
700
846
|
const sessionScopedPath = getStateFilePath("ralph-state.json", cwd, sessionId);
|
|
701
847
|
const sessionScoped = await readJsonIfExists(sessionScopedPath);
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
848
|
+
if (sessionScoped?.active === true) {
|
|
849
|
+
if (
|
|
850
|
+
currentOmxSessionId
|
|
851
|
+
&& sessionId !== currentOmxSessionId
|
|
852
|
+
&& completedCanonicalState
|
|
853
|
+
&& shouldRetireShadowedRalphStartingSeed(sessionScoped, completedCanonicalState, cwd, {
|
|
854
|
+
completedSessionId: currentOmxSessionId,
|
|
855
|
+
payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
|
|
856
|
+
threadId: safeString(ownerContext?.threadId).trim(),
|
|
857
|
+
currentNativeSessionId,
|
|
858
|
+
tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
|
|
859
|
+
})
|
|
860
|
+
) {
|
|
861
|
+
await retireShadowedRalphStartingSeed(
|
|
862
|
+
sessionScopedPath,
|
|
863
|
+
sessionScoped,
|
|
864
|
+
currentOmxSessionId,
|
|
865
|
+
completedCanonicalPath,
|
|
866
|
+
completedCanonicalState,
|
|
867
|
+
);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (await isStaleOrphanedRalphStartingState(sessionScoped, sessionScopedPath)) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (
|
|
874
|
+
isRalphStartingPhase(sessionScoped)
|
|
875
|
+
&& !(await isVisibleRalphActiveForSession(stateDir, sessionId))
|
|
876
|
+
) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
708
879
|
}
|
|
709
880
|
if (
|
|
710
881
|
sessionScoped?.active === true
|
|
@@ -1699,11 +1870,14 @@ async function buildModeBasedStopOutput(
|
|
|
1699
1870
|
const state = await readModeStateForActiveDecision(mode, sessionId?.trim() || undefined, cwd);
|
|
1700
1871
|
if (!state || !shouldContinueRun(state)) return null;
|
|
1701
1872
|
const phase = formatPhase(state.current_phase);
|
|
1873
|
+
const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
|
|
1874
|
+
? "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state."
|
|
1875
|
+
: `OMX ${mode} is still active (phase: ${phase}).`;
|
|
1702
1876
|
return {
|
|
1703
1877
|
decision: "block",
|
|
1704
1878
|
reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
|
|
1705
1879
|
stopReason: `${mode}_${phase}`,
|
|
1706
|
-
systemMessage
|
|
1880
|
+
systemMessage,
|
|
1707
1881
|
};
|
|
1708
1882
|
}
|
|
1709
1883
|
|
|
@@ -1715,6 +1889,12 @@ export function looksLikeGoalCompletionPrompt(text: string): boolean {
|
|
|
1715
1889
|
|| /(?:^|[.!?]\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);
|
|
1716
1890
|
}
|
|
1717
1891
|
|
|
1892
|
+
function reportsAutoresearchGoalObjectiveMismatch(text: string): boolean {
|
|
1893
|
+
return /\bautoresearch[-\s]goal\b/i.test(text)
|
|
1894
|
+
&& /\b(?:complete|completion|reconciliation)\b/i.test(text)
|
|
1895
|
+
&& /objective mismatch/i.test(text);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1718
1898
|
async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
|
|
1719
1899
|
const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
|
|
1720
1900
|
const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
|
|
@@ -1758,10 +1938,19 @@ async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Pro
|
|
|
1758
1938
|
const completion = await readJsonIfExists(join(autoresearchRoot, entry.name, "completion.json"));
|
|
1759
1939
|
const completionVerdict = safeString(completion?.verdict);
|
|
1760
1940
|
const completionPassed = completion?.passed === true || completionVerdict === "pass";
|
|
1761
|
-
if (
|
|
1941
|
+
if (
|
|
1942
|
+
mission?.workflow === "autoresearch-goal"
|
|
1943
|
+
&& status
|
|
1944
|
+
&& status !== "complete"
|
|
1945
|
+
&& completionPassed
|
|
1946
|
+
) {
|
|
1762
1947
|
return {
|
|
1763
1948
|
workflow: "autoresearch-goal",
|
|
1764
1949
|
command: `omx autoresearch-goal complete --slug ${safeString(mission.slug) || entry.name} --codex-goal-json '<get_goal JSON or path>'`,
|
|
1950
|
+
remediation: [
|
|
1951
|
+
"If that command fails with a Codex goal objective mismatch after a fresh get_goal snapshot, do not repeat the same complete command blindly in this thread.",
|
|
1952
|
+
"Either retry with a correct fresh snapshot or record an explicit blocked verdict for this autoresearch-goal and continue it from a fresh Codex thread.",
|
|
1953
|
+
].join(" "),
|
|
1765
1954
|
};
|
|
1766
1955
|
}
|
|
1767
1956
|
}
|
|
@@ -1789,6 +1978,9 @@ async function buildGoalWorkflowReconciliationStopOutput(
|
|
|
1789
1978
|
if (!looksLikeGoalCompletionPrompt(lastAssistantMessage)) return null;
|
|
1790
1979
|
const requirement = await findActiveGoalWorkflowReconciliationRequirement(cwd);
|
|
1791
1980
|
if (!requirement) return null;
|
|
1981
|
+
if (requirement.workflow === "autoresearch-goal" && reportsAutoresearchGoalObjectiveMismatch(lastAssistantMessage)) {
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1792
1984
|
const systemMessage =
|
|
1793
1985
|
[
|
|
1794
1986
|
`OMX ${requirement.workflow} requires get_goal snapshot reconciliation before completion; call get_goal and pass --codex-goal-json to ${requirement.command}.`,
|
|
@@ -1914,7 +2106,7 @@ function readPayloadSessionId(payload: CodexHookPayload): string {
|
|
|
1914
2106
|
}
|
|
1915
2107
|
|
|
1916
2108
|
function readPayloadThreadId(payload: CodexHookPayload): string {
|
|
1917
|
-
return safeString(payload.thread_id ?? payload.threadId).trim();
|
|
2109
|
+
return safeString(payload.owner_codex_thread_id ?? payload.thread_id ?? payload.threadId).trim();
|
|
1918
2110
|
}
|
|
1919
2111
|
|
|
1920
2112
|
function readPayloadTurnId(payload: CodexHookPayload): string {
|
|
@@ -2017,6 +2209,14 @@ async function readBlockingSkillForStop(
|
|
|
2017
2209
|
const modeSnapshot = getRunContinuationSnapshot(modeState);
|
|
2018
2210
|
if (modeSnapshot?.terminal === true) continue;
|
|
2019
2211
|
|
|
2212
|
+
if (await shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
|
|
2213
|
+
cwd,
|
|
2214
|
+
stateDir,
|
|
2215
|
+
skill,
|
|
2216
|
+
sessionId,
|
|
2217
|
+
threadId,
|
|
2218
|
+
)) continue;
|
|
2219
|
+
|
|
2020
2220
|
const phase = formatPhase(
|
|
2021
2221
|
modeState.current_phase,
|
|
2022
2222
|
formatPhase(
|
|
@@ -2068,6 +2268,58 @@ function isTerminalOrInactiveModeState(state: Record<string, unknown> | null): b
|
|
|
2068
2268
|
return phase !== "" && TERMINAL_MODE_PHASES.has(phase);
|
|
2069
2269
|
}
|
|
2070
2270
|
|
|
2271
|
+
function rootSkillStateHasNoActiveSkillForStopContext(
|
|
2272
|
+
rootState: SkillActiveStateLike | null,
|
|
2273
|
+
skill: string,
|
|
2274
|
+
sessionId: string,
|
|
2275
|
+
threadId: string,
|
|
2276
|
+
): boolean {
|
|
2277
|
+
if (!rootState) return false;
|
|
2278
|
+
return !listActiveSkills(rootState).some((entry) => (
|
|
2279
|
+
entry.skill === skill
|
|
2280
|
+
&& matchesSkillStopContext(entry, rootState, sessionId, threadId)
|
|
2281
|
+
));
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function rootModeStateIsCanonicalForStopContext(
|
|
2285
|
+
state: Record<string, unknown>,
|
|
2286
|
+
cwd: string,
|
|
2287
|
+
sessionId: string,
|
|
2288
|
+
threadId: string,
|
|
2289
|
+
): boolean {
|
|
2290
|
+
if (!modeStateMatchesSkillStopContext(state, cwd, sessionId)) return false;
|
|
2291
|
+
|
|
2292
|
+
const stateSessionId = safeString(
|
|
2293
|
+
state.owner_omx_session_id
|
|
2294
|
+
?? state.session_id
|
|
2295
|
+
?? state.codex_session_id
|
|
2296
|
+
?? state.owner_codex_session_id,
|
|
2297
|
+
).trim();
|
|
2298
|
+
if (sessionId && stateSessionId !== sessionId) return false;
|
|
2299
|
+
|
|
2300
|
+
const stateThreadId = safeString(state.owner_codex_thread_id ?? state.thread_id).trim();
|
|
2301
|
+
if (threadId && stateThreadId && stateThreadId !== threadId) return false;
|
|
2302
|
+
|
|
2303
|
+
return true;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
async function shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
|
|
2307
|
+
cwd: string,
|
|
2308
|
+
stateDir: string,
|
|
2309
|
+
skill: string,
|
|
2310
|
+
sessionId: string,
|
|
2311
|
+
threadId: string,
|
|
2312
|
+
): Promise<boolean> {
|
|
2313
|
+
const rootModeState = await readJsonIfExists(join(stateDir, `${skill}-state.json`));
|
|
2314
|
+
if (!rootModeState) return false;
|
|
2315
|
+
if (!rootModeStateIsCanonicalForStopContext(rootModeState, cwd, sessionId, threadId)) return false;
|
|
2316
|
+
if (!isTerminalOrInactiveModeState(rootModeState)) return false;
|
|
2317
|
+
|
|
2318
|
+
const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
|
|
2319
|
+
const rootSkillState = await readSkillActiveState(rootPath);
|
|
2320
|
+
return rootSkillStateHasNoActiveSkillForStopContext(rootSkillState, skill, sessionId, threadId);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2071
2323
|
async function readSessionScopedModeStateForRootSkill(
|
|
2072
2324
|
cwd: string,
|
|
2073
2325
|
stateDir: string,
|