oh-my-opencode 4.14.0 → 4.14.1

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 (124) hide show
  1. package/.agents/skills/codex-qa/references/isolation.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/cli/doctor/framework/types.d.ts +2 -0
  4. package/dist/cli/index.js +149 -1426
  5. package/dist/cli-node/index.js +149 -1426
  6. package/dist/index.js +4255 -714
  7. package/dist/plugin/native-skills.d.ts +9 -0
  8. package/dist/plugin/system-transform.d.ts +1 -1
  9. package/dist/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
  10. package/dist/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  11. package/dist/skills/lcx-doctor/SKILL.md +48 -13
  12. package/dist/skills/lcx-doctor/agents/openai.yaml +1 -1
  13. package/dist/skills/lcx-report-bug/SKILL.md +44 -13
  14. package/dist/skills/lcx-report-bug/agents/openai.yaml +1 -1
  15. package/dist/skills/visual-qa/SKILL.md +5 -5
  16. package/dist/skills/visual-qa/references/agent-browser-setup.md +5 -4
  17. package/dist/skills/visual-qa/scripts/cli.test.ts +7 -2
  18. package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
  19. package/dist/skills/visual-qa/scripts/visual-qa.mjs +530 -0
  20. package/dist/tools/delegate-task/skill-resolver.d.ts +1 -0
  21. package/dist/tools/delegate-task/types.d.ts +2 -0
  22. package/dist/tools/skill/types.d.ts +1 -0
  23. package/package.json +26 -13
  24. package/packages/git-bash-mcp/dist/cli.js +2 -0
  25. package/packages/lsp-daemon/dist/cli.js +15 -4
  26. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +1 -0
  27. package/packages/lsp-daemon/dist/ensure-daemon.js +9 -3
  28. package/packages/lsp-daemon/dist/index.js +15 -4
  29. package/packages/lsp-daemon/dist/paths.d.ts +1 -0
  30. package/packages/lsp-daemon/dist/paths.js +6 -1
  31. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  32. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -7
  33. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +1 -1
  35. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +123 -10
  36. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +123 -10
  37. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +126 -9
  39. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +141 -0
  40. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +90 -25
  44. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +2 -0
  45. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +2 -0
  46. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +56 -0
  47. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +2 -5
  49. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +2 -0
  50. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +65 -0
  51. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +6 -1
  52. package/packages/omo-codex/plugin/components/rules/dist/cli.js +8 -106
  53. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +4 -17
  55. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +17 -5
  58. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +63 -1
  59. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +80 -50
  60. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +1 -1
  62. package/packages/omo-codex/plugin/components/ultrawork/directive.md +4 -12
  63. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  64. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +7 -33
  65. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +4 -12
  66. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -2
  67. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  68. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -4
  69. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-quality-gate.json +7 -7
  70. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +3 -1
  71. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-doc.test.ts +2 -2
  72. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +7 -6
  73. package/packages/omo-codex/plugin/package-lock.json +16 -56
  74. package/packages/omo-codex/plugin/package.json +1 -1
  75. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
  76. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  77. package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +48 -13
  78. package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
  79. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +44 -13
  80. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  81. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +17 -5
  82. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +63 -1
  83. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +80 -50
  84. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  85. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -4
  86. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +5 -5
  87. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +5 -4
  88. package/packages/omo-codex/plugin/skills/visual-qa/scripts/visual-qa.mjs +530 -0
  89. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +38 -2
  90. package/packages/omo-codex/plugin/test/component-bin-names.test.mjs +12 -11
  91. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +99 -1
  92. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +62 -8
  93. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +1 -0
  94. package/packages/omo-codex/plugin/test/teammode-archive-ambiguity.test.mjs +102 -0
  95. package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +41 -1
  96. package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +93 -0
  97. package/packages/omo-codex/scripts/install-bin-links.test.mjs +6 -0
  98. package/packages/omo-codex/scripts/install-cache-copy.test.mjs +28 -0
  99. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +43 -3
  100. package/packages/omo-codex/scripts/install-delegated-command.test.mjs +93 -0
  101. package/packages/omo-codex/scripts/install-dist/install-local.mjs +103 -28
  102. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +21 -2
  103. package/packages/omo-codex/scripts/install-local.test.mjs +0 -1
  104. package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
  105. package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  106. package/packages/shared-skills/skills/lcx-doctor/SKILL.md +48 -13
  107. package/packages/shared-skills/skills/lcx-doctor/agents/openai.yaml +1 -1
  108. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +44 -13
  109. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
  110. package/packages/shared-skills/skills/visual-qa/SKILL.md +5 -5
  111. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +5 -4
  112. package/packages/shared-skills/skills/visual-qa/scripts/cli.test.ts +7 -2
  113. package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
  114. package/packages/shared-skills/skills/visual-qa/scripts/visual-qa.mjs +530 -0
  115. package/dist/cli/sparkshell-appserver-websocket.d.ts +0 -4
  116. package/dist/cli/sparkshell-appserver.d.ts +0 -16
  117. package/dist/cli/sparkshell-condense.d.ts +0 -10
  118. package/dist/cli/sparkshell-parse.d.ts +0 -24
  119. package/dist/cli/sparkshell-session-context.d.ts +0 -20
  120. package/dist/cli/sparkshell-spark.d.ts +0 -23
  121. package/dist/cli/sparkshell.d.ts +0 -33
  122. package/dist/shared/sparkshell-awareness.d.ts +0 -5
  123. package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +0 -106
  124. package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +0 -355
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
@@ -125,6 +125,54 @@ test("#given representative component hook payloads #when executed through dist
125
125
  }
126
126
  });
127
127
 
128
+ test("#given bundled LSP hook CLI in installed layout #when diagnostics run #then it spawns sibling daemon target", () => {
129
+ const tempRoot = mkdtempSync(join(tmpdir(), "omo-codex-lsp-installed-"));
130
+ try {
131
+ const lspDist = join(tempRoot, "components", "lsp", "dist");
132
+ const daemonDist = join(tempRoot, "components", "lsp-daemon", "dist");
133
+ const daemonDir = join(tempRoot, "daemon");
134
+ const invocationLog = join(tempRoot, "fake-daemon-invocations.jsonl");
135
+ mkdirSync(lspDist, { recursive: true });
136
+ mkdirSync(daemonDist, { recursive: true });
137
+ mkdirSync(join(tempRoot, "src"), { recursive: true });
138
+ writeFileSync(join(tempRoot, "package.json"), JSON.stringify({ type: "module" }));
139
+ writeFileSync(join(lspDist, "cli.js"), readFileSync(componentCliPath("lsp"), "utf8"));
140
+ writeFileSync(join(daemonDist, "package.json"), JSON.stringify({ type: "module", version: "0.1.0" }));
141
+ writeFakeLspDaemonCli(join(daemonDist, "cli.js"));
142
+ const editedFile = join(tempRoot, "src", "broken.c");
143
+ writeFileSync(editedFile, "int main(void) { return missing_symbol; }\n");
144
+
145
+ const result = spawnSync(process.execPath, [join(lspDist, "cli.js"), "hook", "post-tool-use"], {
146
+ cwd: tempRoot,
147
+ encoding: "utf8",
148
+ env: hookEnv(tempRoot, {
149
+ CODEX_LSP_DAEMON_DIR: daemonDir,
150
+ FAKE_LSP_DAEMON_LOG: invocationLog,
151
+ }),
152
+ input: JSON.stringify({
153
+ session_id: "bundled-lsp-hook",
154
+ tool_name: "Edit",
155
+ tool_input: { file_path: editedFile },
156
+ tool_response: { ok: true },
157
+ }),
158
+ timeout: HOOK_CLI_TEST_TIMEOUT_MS,
159
+ });
160
+
161
+ const daemonInvocations = existsSync(invocationLog) ? readFileSync(invocationLog, "utf8") : "";
162
+ const failureContext = `stdout: ${result.stdout}\nstderr: ${result.stderr}\ndaemon log: ${daemonInvocations}`;
163
+ assert.equal(result.status, 0, failureContext);
164
+ assert.equal(result.stderr, "", failureContext);
165
+ assert.notEqual(result.stdout, "", failureContext);
166
+ const parsed = JSON.parse(result.stdout);
167
+ assert.equal(parsed.decision, "block");
168
+ assert.match(parsed.reason, /error\[fake\] \(1\) at 1:1: Missing fake symbol\./);
169
+ assert.deepEqual(daemonInvocations.trim().split("\n").map(JSON.parse), [["daemon"]]);
170
+ assert.equal(existsSync(join(daemonDir, "v0.1.0", "daemon.log")), true);
171
+ } finally {
172
+ rmSync(tempRoot, { recursive: true, force: true });
173
+ }
174
+ });
175
+
128
176
  test("#given malformed comment-checker stdin #when executed through dist CLI contract #then it exits successfully without output", () => {
129
177
  // given
130
178
  const tempRoot = mkdtempSync(join(tmpdir(), "omo-codex-cli-malformed-"));
@@ -276,6 +324,56 @@ function smokeImportComponent(component, event) {
276
324
  }
277
325
  }
278
326
 
327
+ function writeFakeLspDaemonCli(path) {
328
+ writeFileSync(
329
+ path,
330
+ [
331
+ "#!/usr/bin/env node",
332
+ 'import { createHash } from "node:crypto";',
333
+ 'import { appendFileSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";',
334
+ 'import { tmpdir } from "node:os";',
335
+ 'import { dirname, join } from "node:path";',
336
+ 'import { createServer } from "node:net";',
337
+ "",
338
+ "appendFileSync(process.env.FAKE_LSP_DAEMON_LOG, `${JSON.stringify(process.argv.slice(2))}\\n`);",
339
+ "const baseDir = process.env.CODEX_LSP_DAEMON_DIR;",
340
+ 'const versionDirName = readdirSync(baseDir).find((entry) => entry.startsWith("v")) ?? "v0";',
341
+ "const version = versionDirName.slice(1);",
342
+ "const dir = join(baseDir, versionDirName);",
343
+ 'const naturalSocket = join(dir, "daemon.sock");',
344
+ 'const digest = createHash("sha256").update(dir).digest("hex").slice(0, 16);',
345
+ "const socketPath = process.platform === \"win32\"",
346
+ "\t? `\\\\\\\\.\\\\pipe\\\\omo-lsp-${version}-${digest}`",
347
+ "\t: naturalSocket.length < 100 ? naturalSocket : join(tmpdir(), `omo-lsp-${version}-${digest}.sock`);",
348
+ "if (process.platform !== \"win32\") {",
349
+ "\tmkdirSync(dirname(socketPath), { recursive: true });",
350
+ "\ttry { unlinkSync(socketPath); } catch {}",
351
+ "}",
352
+ "const server = createServer((socket) => {",
353
+ '\tlet raw = "";',
354
+ '\tsocket.setEncoding("utf8");',
355
+ '\tsocket.on("data", (chunk) => {',
356
+ "\t\traw += chunk;",
357
+ '\t\tif (!raw.includes("\\n")) return;',
358
+ "\t\tconst request = JSON.parse(raw.trim());",
359
+ "\t\tconst response = {",
360
+ '\t\t\tjsonrpc: "2.0",',
361
+ "\t\t\tid: request.id,",
362
+ "\t\t\tresult: {",
363
+ '\t\t\t\tcontent: [{ type: "text", text: "error[fake] (1) at 1:1: Missing fake symbol." }],',
364
+ "\t\t\t},",
365
+ "\t\t};",
366
+ '\t\tsocket.end(`${JSON.stringify(response)}\\n`);',
367
+ "\t\tserver.close(() => process.exit(0));",
368
+ "\t});",
369
+ "});",
370
+ "server.listen(socketPath);",
371
+ "setTimeout(() => process.exit(1), 10000).unref();",
372
+ "",
373
+ ].join("\n"),
374
+ );
375
+ }
376
+
279
377
  function hookEnv(tempRoot, extraEnv = {}) {
280
378
  return {
281
379
  ...process.env,
@@ -4,11 +4,16 @@ import { dirname, join } from "node:path";
4
4
  import test from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
- const root = dirname(dirname(fileURLToPath(import.meta.url)));
7
+ const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ const sharedSkillsRoot = join(pluginRoot, "..", "..", "shared-skills");
9
+
10
+ async function readSkill(name) {
11
+ return readFile(join(sharedSkillsRoot, "skills", name, "SKILL.md"), "utf8");
12
+ }
8
13
 
9
14
  test("#given synced lcx-report-bug skill #when inspected #then it files LazyCodex bug issues with generated labels", async () => {
10
15
  // given
11
- const skillRoot = join(root, "skills", "lcx-report-bug");
16
+ const skillRoot = join(sharedSkillsRoot, "skills", "lcx-report-bug");
12
17
 
13
18
  // when
14
19
  const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8");
@@ -20,14 +25,14 @@ test("#given synced lcx-report-bug skill #when inspected #then it files LazyCode
20
25
  assert.match(skill, /gh pr create --repo openai\/codex/);
21
26
  assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
22
27
  assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
23
- assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-report-bug"/);
28
+ assert.match(interfaceMetadata, /display_name: "lcx-report-bug \(omo\)"/);
24
29
  assert.match(interfaceMetadata, /- "lazycodex bug"/);
25
30
  assert.match(interfaceMetadata, /- "openai codex bug"/);
26
31
  });
27
32
 
28
33
  test("#given synced lcx-contribute-bug-fix skill #when inspected #then it delivers LazyCodex fixes as issues and upstream fixes as fork PRs", async () => {
29
34
  // given
30
- const skillRoot = join(root, "skills", "lcx-contribute-bug-fix");
35
+ const skillRoot = join(sharedSkillsRoot, "skills", "lcx-contribute-bug-fix");
31
36
 
32
37
  // when
33
38
  const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8");
@@ -40,14 +45,14 @@ test("#given synced lcx-contribute-bug-fix skill #when inspected #then it delive
40
45
  assert.match(skill, /gh pr create --repo openai\/codex/);
41
46
  assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
42
47
  assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
43
- assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-contribute-bug-fix"/);
48
+ assert.match(interfaceMetadata, /display_name: "lcx-contribute-bug-fix \(omo\)"/);
44
49
  assert.match(interfaceMetadata, /- "contribute a bug fix"/);
45
50
  assert.match(interfaceMetadata, /- "fix bug pr"/);
46
51
  });
47
52
 
48
- test("#given synced lcx-doctor skill #when inspected #then it diagnoses installs against latest /tmp sources without mutating them", async () => {
53
+ test("#given synced lcx-doctor skill #when inspected #then it diagnoses installs against latest temp-root sources without mutating them", async () => {
49
54
  // given
50
- const skillRoot = join(root, "skills", "lcx-doctor");
55
+ const skillRoot = join(sharedSkillsRoot, "skills", "lcx-doctor");
51
56
 
52
57
  // when
53
58
  const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8");
@@ -55,7 +60,56 @@ test("#given synced lcx-doctor skill #when inspected #then it diagnoses installs
55
60
 
56
61
  // then
57
62
  assert.match(skill, /^---\r?\nname: lcx-doctor\r?\n/m);
58
- assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-doctor"/);
63
+ assert.match(interfaceMetadata, /display_name: "lcx-doctor \(omo\)"/);
59
64
  assert.match(interfaceMetadata, /- "lazycodex doctor"/);
60
65
  assert.match(interfaceMetadata, /- "lazycodex health check"/);
61
66
  });
67
+
68
+ test("#given lcx source-sync skills #when inspected #then source caches are validated before reuse", async () => {
69
+ // given
70
+ const skillNames = ["lcx-doctor", "lcx-report-bug", "lcx-contribute-bug-fix"];
71
+
72
+ for (const skillName of skillNames) {
73
+ // when
74
+ const skill = await readSkill(skillName);
75
+
76
+ // then
77
+ assert.match(skill, /LAZYCODEX_SOURCE_ROOT="\$\{LAZYCODEX_SOURCE_ROOT:-\$\{TMPDIR:-\/tmp\}\/lazycodex-sources\}"/);
78
+ assert.match(skill, /git -C "\$DEST" rev-parse --is-inside-work-tree/);
79
+ assert.match(skill, /git -C "\$DEST" config --get remote\.origin\.url/);
80
+ assert.match(skill, /DEST\.corrupt\.\$\(date \+%Y%m%d%H%M%S\)/);
81
+ assert.doesNotMatch(skill, /if \[ ! -d "\$DEST\/\.git" \]; then/);
82
+ assert.doesNotMatch(skill, /sync_latest_source code-yeongyu\/lazycodex \/tmp\/lazycodex-source/);
83
+ assert.doesNotMatch(skill, /sync_latest_source openai\/codex \/tmp\/openai-codex-source/);
84
+ }
85
+ });
86
+
87
+ test("#given lcx-doctor skill #when inspected #then aggregate hook validation follows the current manifest", async () => {
88
+ // given
89
+ const skill = await readSkill("lcx-doctor");
90
+
91
+ // then
92
+ assert.match(skill, /\.codex-plugin\/plugin\.json/);
93
+ assert.match(skill, /manifest declares a `hooks` array/);
94
+ assert.match(skill, /validate every direct hook path declared by the manifest/);
95
+ assert.match(skill, /`hooks\/hooks\.json` only when the manifest declares it/);
96
+ assert.match(skill, /do not require retired paths such as `components\/workflow-selector`/);
97
+ assert.match(skill, /`hooks\/user-prompt-submit-selecting-lazycodex-workflow\.json` unless the current manifest declares them/);
98
+ assert.doesNotMatch(skill, /Plugin payload present and non-empty: `hooks\/hooks\.json`/);
99
+ });
100
+
101
+ test("#given lcx-doctor skill #when inspected #then runtime probes and materialized payloads distinguish expected rewrites from missing files", async () => {
102
+ // given
103
+ const skill = await readSkill("lcx-doctor");
104
+
105
+ // then
106
+ assert.match(skill, /manifest-declared runtime payload/);
107
+ assert.match(skill, /`dist\/cli\/index\.js` and `dist\/cli-node\/index\.js`/);
108
+ assert.match(skill, /install-time materialization rewrites as expected/);
109
+ assert.match(skill, /absolute `\.mcp\.json` runtime paths/);
110
+ assert.match(skill, /Missing or zero-byte rewritten targets are FAIL/);
111
+ assert.match(skill, /Use the configured Codex default model/);
112
+ assert.match(skill, /unless the user explicitly passed a model override/);
113
+ assert.match(skill, /never force a guessed\/rejected model such as `gpt-5\.5-codex-mini`/);
114
+ assert.doesNotMatch(skill, /--model gpt-5\.5-codex-mini/);
115
+ });
@@ -25,5 +25,6 @@ test("#given Codex Light install docs #when inspected #then lazycodex is npm-fir
25
25
  for (const [path, text] of docs) {
26
26
  assert.match(text, /\bnpx lazycodex-ai install\b/, `${path} should document the Node/npm install command`);
27
27
  assert.doesNotMatch(text, /\bbunx lazycodex-ai\b/, `${path} should not require Bun for lazycodex`);
28
+ assert.doesNotMatch(text, /npx\s+--package\s+oh-my-openagent\s+omo\s+install/, `${path} should not recommend the Windows-broken --package install command`);
28
29
  }
29
30
  });
@@ -0,0 +1,102 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ import { root } from "./aggregate-plugin-fixture.mjs";
7
+ import { cleanupTeamRoot, createTeamRoot, readTeamJson, runTeam } from "./teammode-safety-fixture.mjs";
8
+
9
+ const teammodeSkillPath = join("components", "teammode", "skills", "teammode", "SKILL.md");
10
+
11
+ test("#given Codex App thread ids can be ambiguous across hosts #when archive guidance is read #then it separates app-thread archival from team-state archival", () => {
12
+ const body = readFileSync(join(root, teammodeSkillPath), "utf8");
13
+
14
+ assert.match(
15
+ body,
16
+ /Ambiguous Codex thread id|ambiguous across hosts/i,
17
+ "archive guidance must name the Codex App multi-host ambiguity failure instead of only saying the archive tool is unavailable",
18
+ );
19
+ assert.match(
20
+ body,
21
+ /app-thread archival blocker/i,
22
+ "archive guidance must classify ambiguous app-thread ids as app-thread archival blockers",
23
+ );
24
+ assert.match(
25
+ body,
26
+ /record .*team log/i,
27
+ "archive guidance must require a durable team-log record when app-thread archival cannot be proven",
28
+ );
29
+ assert.match(
30
+ body,
31
+ /continue .*team-state archive/i,
32
+ "archive guidance must let the team-state archive proceed after recording the app-thread archival blocker",
33
+ );
34
+ assert.match(
35
+ body,
36
+ /archive --note/i,
37
+ "archive guidance must route ambiguous app-thread archive blockers into archive --note evidence",
38
+ );
39
+ assert.match(
40
+ body,
41
+ /Do not delete/i,
42
+ "archive guidance must keep evidence available until ambiguous app-thread archival is resolved",
43
+ );
44
+ });
45
+
46
+ test("#given app-thread archival is blocked by an ambiguous id #when archive runs with a note #then team state is archived and keeps the blocker in the log", () => {
47
+ const tempRoot = createTeamRoot("omo-codex-teammode-archive-ambiguity-");
48
+ try {
49
+ runTeam(tempRoot, "init", "--name", "ArchiveQA", "--session-name", "cleanup", "--session", "archive-ambiguity");
50
+ runTeam(
51
+ tempRoot,
52
+ "add-member",
53
+ "--team",
54
+ "archive-ambiguity",
55
+ "--id",
56
+ "A",
57
+ "--name",
58
+ "local",
59
+ "--focus",
60
+ "local host member",
61
+ "--lens",
62
+ "area",
63
+ "--deliverable",
64
+ "local notes",
65
+ );
66
+ runTeam(
67
+ tempRoot,
68
+ "add-member",
69
+ "--team",
70
+ "archive-ambiguity",
71
+ "--id",
72
+ "B",
73
+ "--name",
74
+ "remote",
75
+ "--focus",
76
+ "remote host member",
77
+ "--lens",
78
+ "perspective",
79
+ "--deliverable",
80
+ "remote notes",
81
+ );
82
+ runTeam(tempRoot, "bind-thread", "--team", "archive-ambiguity", "--id", "A", "--thread", "duplicate-thread-id");
83
+ runTeam(tempRoot, "bind-thread", "--team", "archive-ambiguity", "--id", "B", "--thread", "remote-thread-id");
84
+
85
+ const blocker = "app-thread archive blocker: Ambiguous Codex thread id duplicate-thread-id; matching hosts: local, remote-ssh-discovered:m5";
86
+ runTeam(tempRoot, "archive", "--team", "archive-ambiguity", "--note", blocker);
87
+ const team = readTeamJson(tempRoot, "archive-ambiguity");
88
+
89
+ assert.equal(team.status, "archived");
90
+ assert.deepEqual(
91
+ team.members.map((member) => member.status),
92
+ ["archived", "archived"],
93
+ );
94
+ assert.equal(
95
+ team.log.some((entry) => entry.event === "archive" && entry.detail === blocker),
96
+ true,
97
+ "team log must preserve the app-thread archival blocker after durable team-state archival",
98
+ );
99
+ } finally {
100
+ cleanupTeamRoot(tempRoot);
101
+ }
102
+ });
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { spawnSync } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
@@ -42,6 +42,46 @@ export function runTeamRaw(cwd, ...args) {
42
42
  });
43
43
  }
44
44
 
45
+ export function runTeamRawWithEnv(cwd, env, ...args) {
46
+ return spawnSync(process.execPath, [teamScript, ...args], {
47
+ cwd,
48
+ encoding: "utf8",
49
+ env: { ...process.env, ...env },
50
+ timeout: 10_000,
51
+ });
52
+ }
53
+
54
+ export function runTeamAsync(cwd, ...args) {
55
+ return new Promise((resolve, reject) => {
56
+ const child = spawn(process.execPath, [teamScript, ...args], {
57
+ cwd,
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ });
60
+ let stdout = "";
61
+ let stderr = "";
62
+ const timer = setTimeout(() => {
63
+ child.kill("SIGKILL");
64
+ reject(new Error(`team.mjs ${args.join(" ")} timed out`));
65
+ }, 10_000);
66
+ child.stdout.setEncoding("utf8");
67
+ child.stderr.setEncoding("utf8");
68
+ child.stdout.on("data", (chunk) => {
69
+ stdout += chunk;
70
+ });
71
+ child.stderr.on("data", (chunk) => {
72
+ stderr += chunk;
73
+ });
74
+ child.on("error", (error) => {
75
+ clearTimeout(timer);
76
+ reject(error);
77
+ });
78
+ child.on("close", (status, signal) => {
79
+ clearTimeout(timer);
80
+ resolve({ status, signal, stdout, stderr });
81
+ });
82
+ });
83
+ }
84
+
45
85
  export function symlinkOrSkip(t, target, path, type) {
46
86
  try {
47
87
  symlinkSync(target, path, type);
@@ -10,7 +10,9 @@ import {
10
10
  createTeamRoot,
11
11
  readTeamJson,
12
12
  runTeam,
13
+ runTeamAsync,
13
14
  runTeamRaw,
15
+ runTeamRawWithEnv,
14
16
  symlinkOrSkip,
15
17
  teamDir,
16
18
  teamJsonPath,
@@ -82,6 +84,97 @@ test("#given member A exists #when add-member receives A with trailing space #th
82
84
  }
83
85
  });
84
86
 
87
+ test("#given two add-member commands run concurrently #then both successful mutations are preserved", async () => {
88
+ const tempRoot = createTeamRoot("omo-codex-teammode-concurrent-add-");
89
+ try {
90
+ runTeam(tempRoot, "init", "--name", "Concurrent", "--session-name", "Members", "--session", "safe-concurrent-add");
91
+
92
+ const [alpha, beta] = await Promise.all([
93
+ runTeamAsync(
94
+ tempRoot,
95
+ "add-member",
96
+ "--team",
97
+ "safe-concurrent-add",
98
+ "--id",
99
+ "A",
100
+ "--name",
101
+ "alpha",
102
+ "--focus",
103
+ "alpha",
104
+ "--lens",
105
+ "area",
106
+ "--deliverable",
107
+ "first",
108
+ ),
109
+ runTeamAsync(
110
+ tempRoot,
111
+ "add-member",
112
+ "--team",
113
+ "safe-concurrent-add",
114
+ "--id",
115
+ "B",
116
+ "--name",
117
+ "beta",
118
+ "--focus",
119
+ "beta",
120
+ "--lens",
121
+ "perspective",
122
+ "--deliverable",
123
+ "second",
124
+ ),
125
+ ]);
126
+ const team = readTeamJson(tempRoot, "safe-concurrent-add");
127
+
128
+ assert.equal(alpha.status, 0, alpha.stderr);
129
+ assert.equal(beta.status, 0, beta.stderr);
130
+ assert.deepEqual(
131
+ team.members.map((member) => member.id).sort(),
132
+ ["A", "B"],
133
+ );
134
+ assert.equal(team.log.filter((entry) => entry.event === "add-member").length, 2);
135
+ } finally {
136
+ cleanupTeamRoot(tempRoot);
137
+ }
138
+ });
139
+
140
+ test("#given another command holds the team lock #when add-member times out #then it fails without mutating team state", () => {
141
+ const tempRoot = createTeamRoot("omo-codex-teammode-lock-timeout-");
142
+ try {
143
+ runTeam(tempRoot, "init", "--name", "Locked", "--session-name", "Members", "--session", "safe-lock-timeout");
144
+ const lockDir = join(teamDir(tempRoot, "safe-lock-timeout"), ".team.lock");
145
+ mkdirSync(lockDir);
146
+ writeFileSync(
147
+ join(lockDir, "owner.json"),
148
+ `${JSON.stringify({ command: "held-test", pid: 12345, createdAt: "2026-06-30T00:00:00.000Z" }, null, 2)}\n`,
149
+ );
150
+
151
+ const result = runTeamRawWithEnv(
152
+ tempRoot,
153
+ { OMO_TEAMMODE_LOCK_TIMEOUT_MS: "25", OMO_TEAMMODE_LOCK_RETRY_MS: "5" },
154
+ "add-member",
155
+ "--team",
156
+ "safe-lock-timeout",
157
+ "--id",
158
+ "A",
159
+ "--name",
160
+ "alpha",
161
+ "--focus",
162
+ "alpha",
163
+ "--lens",
164
+ "area",
165
+ "--deliverable",
166
+ "first",
167
+ );
168
+ const team = readTeamJson(tempRoot, "safe-lock-timeout");
169
+
170
+ assert.notEqual(result.status, 0);
171
+ assert.match(result.stderr, /team state is locked by held-test, pid 12345/);
172
+ assert.deepEqual(team.members, []);
173
+ } finally {
174
+ cleanupTeamRoot(tempRoot);
175
+ }
176
+ });
177
+
85
178
  test("#given member focus already exists #when add-member receives same focus with different spacing and case #then state is not partially mutated", () => {
86
179
  const tempRoot = createTeamRoot("omo-codex-teammode-duplicate-focus-");
87
180
  try {
@@ -247,6 +247,8 @@ test("#given nested component declares reserved omo bin #when linking bins #then
247
247
  bin: {
248
248
  omo: "./dist/cli.js",
249
249
  "omo-ulw-loop": "./dist/cli.js",
250
+ ulw: "./dist/cli.js",
251
+ "ulw-loop": "./dist/cli.js",
250
252
  },
251
253
  });
252
254
  await writeFile(join(componentRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
@@ -255,9 +257,13 @@ test("#given nested component declares reserved omo bin #when linking bins #then
255
257
 
256
258
  assert.deepEqual(linked, [
257
259
  { name: "omo-ulw-loop", path: join(binDir, "omo-ulw-loop"), target: join(componentRoot, "dist", "cli.js") },
260
+ { name: "ulw", path: join(binDir, "ulw"), target: join(componentRoot, "dist", "cli.js") },
261
+ { name: "ulw-loop", path: join(binDir, "ulw-loop"), target: join(componentRoot, "dist", "cli.js") },
258
262
  ]);
259
263
  await assert.rejects(readlink(join(binDir, "omo")));
260
264
  assert.equal(await readlink(join(binDir, "omo-ulw-loop")), join(componentRoot, "dist", "cli.js"));
265
+ assert.equal(await readlink(join(binDir, "ulw")), join(componentRoot, "dist", "cli.js"));
266
+ assert.equal(await readlink(join(binDir, "ulw-loop")), join(componentRoot, "dist", "cli.js"));
261
267
  });
262
268
 
263
269
  test("#given stale managed ulw-loop omo symlink #when linking bins #then removes it without touching user-owned omo", async () => {
@@ -138,6 +138,34 @@ test("#given npm creates workspace bin shims in the cache #when caching plugin #
138
138
  assert.equal(await readFile(join(installed.path, "node_modules", ".bin", "other-tool.cmd"), "utf8"), "@echo off\r\necho preserved\r\n");
139
139
  });
140
140
 
141
+ test("#given packaged root CLI runtimes #when caching omo plugin #then root dist is materialized into the plugin cache", async () => {
142
+ // given
143
+ const root = await makeTempDir();
144
+ const repoRoot = join(root, "repo");
145
+ const codexHome = join(root, "codex-home");
146
+ const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
147
+ await mkdir(sourceRoot, { recursive: true });
148
+ await mkdir(join(repoRoot, "dist", "cli"), { recursive: true });
149
+ await mkdir(join(repoRoot, "dist", "cli-node"), { recursive: true });
150
+ await writeFile(join(sourceRoot, "package.json"), JSON.stringify({ name: "@scope/omo", version: "0.1.0" }));
151
+ await writeFile(join(repoRoot, "dist", "cli", "index.js"), "console.log('omo')\n");
152
+ await writeFile(join(repoRoot, "dist", "cli-node", "index.js"), "console.log('omo node')\n");
153
+
154
+ // when
155
+ const installed = await installCachedPlugin({
156
+ codexHome,
157
+ marketplaceName: "sisyphuslabs",
158
+ name: "omo",
159
+ sourcePath: sourceRoot,
160
+ version: "0.1.0",
161
+ runCommand: async () => {},
162
+ });
163
+
164
+ // then
165
+ assert.equal(await readFile(join(installed.path, "dist", "cli", "index.js"), "utf8"), "console.log('omo')\n");
166
+ assert.equal(await readFile(join(installed.path, "dist", "cli-node", "index.js"), "utf8"), "console.log('omo node')\n");
167
+ });
168
+
141
169
  test("#given existing cache #when npm install fails #then previous active cache is preserved", async () => {
142
170
  // given
143
171
  const root = await makeTempDir();
@@ -6,7 +6,7 @@ import test from "node:test";
6
6
 
7
7
  import { updateCodexConfig } from "./install-dist/install-local.mjs";
8
8
 
9
- const ALWAYS_ON_FEATURES = ["plugins", "plugin_hooks", "multi_agent", "child_agents_md"];
9
+ const ALWAYS_ON_FEATURES = ["plugins", "plugin_hooks", "multi_agent"];
10
10
  const AUTONOMOUS_PERMISSION_FEATURES = ["unified_exec", "goals"];
11
11
 
12
12
  test("#given autonomous permissions requested #when script installer updates config #then enables Codex autonomy feature flags", async () => {
@@ -20,7 +20,6 @@ test("#given autonomous permissions requested #when script installer updates con
20
20
  "",
21
21
  "[features]",
22
22
  "multi_agent = false",
23
- "child_agents_md = false",
24
23
  "unified_exec = false",
25
24
  "goals = false",
26
25
  "",
@@ -59,7 +58,6 @@ test("#given autonomous permissions disabled #when script installer updates conf
59
58
  "",
60
59
  "[features]",
61
60
  "multi_agent = false",
62
- "child_agents_md = false",
63
61
  "unified_exec = false",
64
62
  "goals = false",
65
63
  "",
@@ -86,3 +84,45 @@ test("#given autonomous permissions disabled #when script installer updates conf
86
84
  assert.match(content, new RegExp(`${featureName} = false`));
87
85
  }
88
86
  });
87
+
88
+ test("#given existing child_agents_md setting #when script installer updates config #then preserves it without stamping unsupported values", async () => {
89
+ // given
90
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-child-agents-preserve-"));
91
+ const configPath = join(root, "config.toml");
92
+ await writeFile(configPath, ["[features]", "child_agents_md = false", ""].join("\n"));
93
+
94
+ // when
95
+ await updateCodexConfig({
96
+ configPath,
97
+ repoRoot: "/repo/packages/omo-codex",
98
+ marketplaceName: "debug",
99
+ marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
100
+ pluginNames: ["omo"],
101
+ autonomousPermissions: true,
102
+ });
103
+
104
+ // then
105
+ const content = await readFile(configPath, "utf8");
106
+ assert.match(content, /child_agents_md = false/);
107
+ assert.doesNotMatch(content, /child_agents_md = true/);
108
+ });
109
+
110
+ test("#given config without child_agents_md #when script installer updates config #then does not add unsupported feature key", async () => {
111
+ // given
112
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-child-agents-absent-"));
113
+ const configPath = join(root, "config.toml");
114
+
115
+ // when
116
+ await updateCodexConfig({
117
+ configPath,
118
+ repoRoot: "/repo/packages/omo-codex",
119
+ marketplaceName: "debug",
120
+ marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
121
+ pluginNames: ["omo"],
122
+ autonomousPermissions: true,
123
+ });
124
+
125
+ // then
126
+ const content = await readFile(configPath, "utf8");
127
+ assert.doesNotMatch(content, /child_agents_md/);
128
+ });