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
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const requireFromHere = createRequire(import.meta.url);
6
+ const PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
7
+ const CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
8
+ const CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
9
+ export function ensureLspDaemonCliEnv(env = process.env) {
10
+ const configuredCli = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
11
+ const resolution = configuredCli ? resolveConfiguredLspDaemonCli(configuredCli) : resolveLspDaemonCli();
12
+ if (!configuredCli)
13
+ env[CODEX_LSP_DAEMON_CLI_ENV] = resolution.cliPath;
14
+ if (!env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim() && resolution.version !== null) {
15
+ env[CODEX_LSP_DAEMON_VERSION_ENV] = resolution.version;
16
+ }
17
+ }
18
+ export function resolveLspDaemonCliPath() {
19
+ return resolveLspDaemonCli().cliPath;
20
+ }
21
+ function resolveLspDaemonCli() {
22
+ const packageCli = resolvePackageLspDaemonCliPath();
23
+ if (packageCli !== null)
24
+ return resolveConfiguredLspDaemonCli(packageCli);
25
+ const bundledCli = fileURLToPath(new URL("../../lsp-daemon/dist/cli.js", import.meta.url));
26
+ if (existsSync(bundledCli))
27
+ return resolveConfiguredLspDaemonCli(bundledCli);
28
+ return resolveConfiguredLspDaemonCli(bundledCli);
29
+ }
30
+ function resolvePackageLspDaemonCliPath() {
31
+ try {
32
+ return requireFromHere.resolve(PACKAGE_LSP_DAEMON_CLI);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function resolveConfiguredLspDaemonCli(cliPath) {
39
+ return {
40
+ cliPath,
41
+ version: readDaemonPackageVersion(cliPath),
42
+ };
43
+ }
44
+ function readDaemonPackageVersion(cliPath) {
45
+ try {
46
+ const parsed = JSON.parse(readFileSync(join(dirname(cliPath), "package.json"), "utf8"));
47
+ if (isRecord(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
48
+ return parsed["version"];
49
+ }
50
+ }
51
+ catch { }
52
+ return null;
53
+ }
54
+ function isRecord(value) {
55
+ return typeof value === "object" && value !== null && !Array.isArray(value);
56
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-lsp",
3
- "version": "4.14.0",
3
+ "version": "4.14.1",
4
4
  "description": "Codex plugin that exposes Language Server Protocol tools and post-edit diagnostics.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -1,12 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { createRequire } from "node:module";
4
3
  import { argv, execPath, stderr } from "node:process";
5
4
 
6
5
  import { runPostCompactHookCli, runPostToolUseHookCli } from "./codex-hook-cli.js";
7
-
8
- const require = createRequire(import.meta.url);
9
- const PACKAGE_LSP_MCP_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
6
+ import { resolveLspDaemonCliPath } from "./daemon-cli-path.js";
10
7
 
11
8
  async function main(): Promise<void> {
12
9
  const [command = "mcp", subcommand = ""] = argv.slice(2);
@@ -35,7 +32,7 @@ main().catch((error: unknown) => {
35
32
  });
36
33
 
37
34
  async function runPackageLspMcpCli(): Promise<void> {
38
- const cliPath = require.resolve(PACKAGE_LSP_MCP_CLI);
35
+ const cliPath = resolveLspDaemonCliPath();
39
36
  const child = spawn(execPath, [cliPath, "mcp"], { stdio: "inherit" });
40
37
  await new Promise<void>((resolve, reject) => {
41
38
  child.once("error", reject);
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
 
3
3
  import { callDiagnosticsViaDaemon, currentRequestContext } from "@code-yeongyu/lsp-daemon";
4
4
 
5
+ import { ensureLspDaemonCliEnv } from "./daemon-cli-path.js";
5
6
  import {
6
7
  isLspDaemonUnreachableDiagnostics,
7
8
  isUnavailableLspDiagnostics,
@@ -60,6 +61,7 @@ const CONTEXT_PRESSURE_MARKERS = [
60
61
  ] as const;
61
62
 
62
63
  export async function runLspDiagnosticsText(filePath: string): Promise<string> {
64
+ ensureLspDaemonCliEnv();
63
65
  const result = await callDiagnosticsViaDaemon(filePath, { context: currentRequestContext() });
64
66
  return result.content.map((block) => block.text).join("\n");
65
67
  }
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const requireFromHere = createRequire(import.meta.url);
7
+
8
+ const PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
9
+ const CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
10
+ const CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
11
+
12
+ interface LspDaemonCliResolution {
13
+ cliPath: string;
14
+ version: string | null;
15
+ }
16
+
17
+ export function ensureLspDaemonCliEnv(env: NodeJS.ProcessEnv = process.env): void {
18
+ const configuredCli = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
19
+ const resolution = configuredCli ? resolveConfiguredLspDaemonCli(configuredCli) : resolveLspDaemonCli();
20
+ if (!configuredCli) env[CODEX_LSP_DAEMON_CLI_ENV] = resolution.cliPath;
21
+ if (!env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim() && resolution.version !== null) {
22
+ env[CODEX_LSP_DAEMON_VERSION_ENV] = resolution.version;
23
+ }
24
+ }
25
+
26
+ export function resolveLspDaemonCliPath(): string {
27
+ return resolveLspDaemonCli().cliPath;
28
+ }
29
+
30
+ function resolveLspDaemonCli(): LspDaemonCliResolution {
31
+ const packageCli = resolvePackageLspDaemonCliPath();
32
+ if (packageCli !== null) return resolveConfiguredLspDaemonCli(packageCli);
33
+ const bundledCli = fileURLToPath(new URL("../../lsp-daemon/dist/cli.js", import.meta.url));
34
+ if (existsSync(bundledCli)) return resolveConfiguredLspDaemonCli(bundledCli);
35
+ return resolveConfiguredLspDaemonCli(bundledCli);
36
+ }
37
+
38
+ function resolvePackageLspDaemonCliPath(): string | null {
39
+ try {
40
+ return requireFromHere.resolve(PACKAGE_LSP_DAEMON_CLI);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function resolveConfiguredLspDaemonCli(cliPath: string): LspDaemonCliResolution {
47
+ return {
48
+ cliPath,
49
+ version: readDaemonPackageVersion(cliPath),
50
+ };
51
+ }
52
+
53
+ function readDaemonPackageVersion(cliPath: string): string | null {
54
+ try {
55
+ const parsed: unknown = JSON.parse(readFileSync(join(dirname(cliPath), "package.json"), "utf8"));
56
+ if (isRecord(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
57
+ return parsed["version"];
58
+ }
59
+ } catch {}
60
+ return null;
61
+ }
62
+
63
+ function isRecord(value: unknown): value is Record<string, unknown> {
64
+ return typeof value === "object" && value !== null && !Array.isArray(value);
65
+ }
@@ -15,6 +15,7 @@ describe("plugin package metadata", () => {
15
15
  const hooksJson = readHooksJson("hooks/hooks.json");
16
16
  const mcpJson = readMcpJson(".mcp.json");
17
17
  const cliSource = readTextFile("src/cli.ts");
18
+ const daemonCliPathSource = readTextFile("src/daemon-cli-path.ts");
18
19
  const codexHookCliSource = readTextFile("src/codex-hook-cli.ts");
19
20
  const codexHookSource = readTextFile("src/codex-hook.ts");
20
21
  const sourceFiles = listDirectoryEntries("src");
@@ -43,8 +44,12 @@ describe("plugin package metadata", () => {
43
44
  expect(lspServer?.command).toBe("node");
44
45
  expect(lspServer?.args).toEqual(["../../../../lsp-daemon/dist/cli.js", "mcp"]);
45
46
  expect(cliSource).not.toContain("./lazy-lsp-mcp.js");
46
- expect(cliSource).toContain("@code-yeongyu/lsp-daemon/dist/cli.js");
47
+ expect(cliSource).toContain("resolveLspDaemonCliPath");
48
+ expect(daemonCliPathSource).toContain("@code-yeongyu/lsp-daemon/dist/cli.js");
49
+ expect(daemonCliPathSource).toContain("../../lsp-daemon/dist/cli.js");
50
+ expect(daemonCliPathSource).toContain("CODEX_LSP_DAEMON_VERSION");
47
51
  expect(cliSource).not.toContain("../../../../../lsp-daemon/dist/cli.js");
52
+ expect(codexHookSource).toContain("ensureLspDaemonCliEnv");
48
53
  expect(codexHookCliSource).toContain("@code-yeongyu/lsp-daemon");
49
54
  expect(codexHookSource).toContain("@code-yeongyu/lsp-daemon");
50
55
  expect(codexHookCliSource).not.toContain("../../../../../lsp-daemon");
@@ -3927,7 +3927,7 @@ function createRulesEngine(options, config = configFromEnvironment(options.env))
3927
3927
  }
3928
3928
 
3929
3929
  // components/rules/src/static-injection.ts
3930
- import { existsSync as existsSync5 } from "node:fs";
3930
+ import { existsSync as existsSync4 } from "node:fs";
3931
3931
 
3932
3932
  // components/rules/src/post-compact-directive.ts
3933
3933
  var DIRECTIVE_HEADER = [
@@ -3965,96 +3965,6 @@ function buildPostCompactReadDirective(rulePaths, maxChars) {
3965
3965
  `)}${DIRECTIVE_FOOTER}`;
3966
3966
  }
3967
3967
 
3968
- // components/rules/src/sparkshell-awareness.ts
3969
- import { existsSync as existsSync4 } from "node:fs";
3970
- import { join as join7 } from "node:path";
3971
- var SPARKSHELL_AWARENESS_MARKER = "## Sparkshell Runtime";
3972
- var SPARKSHELL_AWARENESS_DEDUP_KEY = "__omo_sparkshell_awareness__";
3973
- function isCodexAppServerActive(env = process.env) {
3974
- const originator = env["CODEX_INTERNAL_ORIGINATOR_OVERRIDE"]?.toLowerCase() ?? "";
3975
- const bundleIdentifier = env["__CFBundleIdentifier"]?.toLowerCase() ?? "";
3976
- const shellActive = isTruthy2(env["CODEX_SHELL"]);
3977
- return shellActive && (originator.includes("codex desktop") || originator.includes("codex app") || bundleIdentifier === "com.openai.codex");
3978
- }
3979
- function isSparkShellAppServerConfigured(env = process.env) {
3980
- const codexSocketPath = env["CODEX_APP_SERVER_SOCKET"]?.trim() ?? "";
3981
- const omoSocketPath = env["OMO_SPARKSHELL_APP_SERVER_SOCKET"]?.trim() ?? "";
3982
- return codexSocketPath.length > 0 || omoSocketPath.length > 0;
3983
- }
3984
- function resolveOmoInvocation(env = process.env, deps = {}) {
3985
- const fileExists = deps.fileExists ?? existsSync4;
3986
- const platform = deps.platform ?? process.platform;
3987
- const binNames = platform === "win32" ? ["omo.cmd", "omo.exe", "omo"] : ["omo"];
3988
- const pathDelimiter = platform === "win32" ? ";" : ":";
3989
- const pathEntries = (env["PATH"] ?? "").split(pathDelimiter).filter((entry) => entry.trim().length > 0);
3990
- for (const pathEntry of pathEntries) {
3991
- for (const binName of binNames) {
3992
- if (fileExists(join7(pathEntry, binName)))
3993
- return "omo";
3994
- }
3995
- }
3996
- for (const candidateDir of omoCandidateBinDirs(env)) {
3997
- for (const binName of binNames) {
3998
- const candidate = join7(candidateDir, binName);
3999
- if (fileExists(candidate))
4000
- return candidate;
4001
- }
4002
- }
4003
- return null;
4004
- }
4005
- function omoCandidateBinDirs(env) {
4006
- const dirs = [];
4007
- const localBinDir = env["CODEX_LOCAL_BIN_DIR"]?.trim() ?? "";
4008
- if (localBinDir.length > 0)
4009
- dirs.push(localBinDir);
4010
- const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || "";
4011
- const codexHome = env["CODEX_HOME"]?.trim() || (home.length > 0 ? join7(home, ".codex") : "");
4012
- if (codexHome.length > 0)
4013
- dirs.push(join7(codexHome, "bin"));
4014
- if (home.length > 0)
4015
- dirs.push(join7(home, ".local", "bin"));
4016
- return dirs;
4017
- }
4018
- function getSparkShellRuntimeAwareness(env = process.env, deps = {}) {
4019
- const override = env["OMO_SPARKSHELL_AWARENESS"] ?? env["LAZYCODEX_SPARKSHELL_AWARENESS"];
4020
- if (isFalsy(override)) {
4021
- return "";
4022
- }
4023
- if (!isTruthy2(override) && !isCodexAppServerActive(env) && !isSparkShellAppServerConfigured(env)) {
4024
- return "";
4025
- }
4026
- const resolved = resolveOmoInvocation(env, deps);
4027
- const invocation = resolved ?? (isTruthy2(override) ? "omo" : null);
4028
- if (invocation === null) {
4029
- return "";
4030
- }
4031
- const command = /\s/.test(invocation) ? `"${invocation}"` : invocation;
4032
- return [
4033
- SPARKSHELL_AWARENESS_MARKER,
4034
- "",
4035
- `- Use \`${command} sparkshell <command> [args...]\` first for repo inspection, CLI smoke tests, git/history checks, and bounded command output. Pass ordinary commands as executable and arguments in separate argv tokens, for example \`${command} sparkshell rg --files\`; not \`${command} sparkshell 'rg --files'\`, because the quoted string is treated as one executable name. Raw \`rg\`/\`grep\`/\`cat\`/\`git\` are fallbacks when Sparkshell is unavailable or too narrow for the task.`,
4036
- `- Use \`${command} sparkshell --shell '<command>'\` only for shell syntax such as metacharacters, pipelines, redirects, command substitution, or variable expansion.`,
4037
- `- Use \`${command} sparkshell --tmux-pane <pane-id> --tail-lines 400\` only to inspect an existing pane, never to launch ordinary commands. Tail lines must stay between 100 and 1000.`,
4038
- "- When no native sidecar or appserver is available, Sparkshell silently falls back to raw command execution. `OMO_SPARKSHELL_BIN` selects a native sidecar path.",
4039
- "- When `CODEX_THREAD_ID` identifies a Codex session, Sparkshell feeds recent session context (first/latest user request + last 5 conversation messages) into oversized-output condensation for relevance ranking, but never appends that context to command output. `OMO_SPARKSHELL_SESSION_CONTEXT=0` disables the lookup.",
4040
- `- Route potentially huge output (full log files, big diffs, \`cat\`/\`grep\` over large artifacts) through \`${command} sparkshell\` instead of reading it raw: oversized output is condensed to a budget while preserving error signatures, repeated patterns, session-goal-relevant lines, and head/tail. Tune with \`--budget <chars>\`; disable with \`OMO_SPARKSHELL_CONDENSE=0\`.`,
4041
- "- Oversized output is first summarized by the spark model (`codex exec`, default `gpt-5.3-codex-spark`) fed with the shell output plus session context: the summary keeps selected output as-is (no masking) and ends with a `[sparkshell caption]` line describing what ran, what the full output contained, and which lines were omitted. `OMO_SPARKSHELL_SPARK=0` skips the model and uses deterministic condensation directly."
4042
- ].join(`
4043
- `);
4044
- }
4045
- function isTruthy2(value) {
4046
- if (value === undefined) {
4047
- return false;
4048
- }
4049
- return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
4050
- }
4051
- function isFalsy(value) {
4052
- if (value === undefined) {
4053
- return false;
4054
- }
4055
- return ["0", "false", "no", "off"].includes(value.trim().toLowerCase());
4056
- }
4057
-
4058
3968
  // components/rules/src/transcript-rule-filter.ts
4059
3969
  function filterRulesAlreadyInTranscript(rules, transcriptPath, markInjected, options = {}) {
4060
3970
  if (rules.length === 0 || transcriptPath === null) {
@@ -4131,8 +4041,7 @@ function runStaticInjection(cwd, transcriptPath, eventName, cachePath, options,
4131
4041
  const rules = filterRulesAlreadyInTranscript(loaded.rules.filter((rule) => !engine.isStaticInjected(rule)), transcriptPath, (rule) => {
4132
4042
  engine.markStaticInjected(rule);
4133
4043
  }, transcriptSearchOptions);
4134
- const sparkshellAwareness = engine.state.staticDedup.has(SPARKSHELL_AWARENESS_DEDUP_KEY) ? "" : getSparkShellRuntimeAwareness(options.env);
4135
- if (rules.length === 0 && sparkshellAwareness.length === 0) {
4044
+ if (rules.length === 0) {
4136
4045
  persistEngineState(engine, cachePath);
4137
4046
  return "";
4138
4047
  }
@@ -4140,11 +4049,8 @@ function runStaticInjection(cwd, transcriptPath, eventName, cachePath, options,
4140
4049
  for (const rule of rules) {
4141
4050
  engine.markStaticInjected(rule);
4142
4051
  }
4143
- if (sparkshellAwareness.length > 0) {
4144
- engine.state.staticDedup.add(SPARKSHELL_AWARENESS_DEDUP_KEY);
4145
- }
4146
4052
  persistEngineState(engine, cachePath);
4147
- return formatAdditionalContextOutput(eventName, combineStaticContext(block, sparkshellAwareness));
4053
+ return formatAdditionalContextOutput(eventName, block);
4148
4054
  }
4149
4055
  function runPostCompactRecovery(input) {
4150
4056
  const effectiveConfig = withPostCompactBudget(input.config, {
@@ -4160,8 +4066,7 @@ function runPostCompactRecovery(input) {
4160
4066
  engine.markStaticInjected(rule);
4161
4067
  });
4162
4068
  const dynamicRulePaths = recoverDynamicRulePaths(engine, transcriptText, loaded.rules);
4163
- const sparkshellAwareness = engine.state.staticDedup.has(SPARKSHELL_AWARENESS_DEDUP_KEY) ? "" : getSparkShellRuntimeAwareness(input.options.env);
4164
- if (missingRules.length === 0 && dynamicRulePaths.length === 0 && sparkshellAwareness.length === 0) {
4069
+ if (missingRules.length === 0 && dynamicRulePaths.length === 0) {
4165
4070
  persistEngineState(engine, input.cachePath, input.channel);
4166
4071
  return "";
4167
4072
  }
@@ -4172,11 +4077,8 @@ function runPostCompactRecovery(input) {
4172
4077
  for (const rule of missingRules) {
4173
4078
  engine.markStaticInjected(rule);
4174
4079
  }
4175
- if (sparkshellAwareness.length > 0) {
4176
- engine.state.staticDedup.add(SPARKSHELL_AWARENESS_DEDUP_KEY);
4177
- }
4178
4080
  persistEngineState(engine, input.cachePath, input.channel);
4179
- return formatAdditionalContextOutput(input.eventName, combineStaticContext(bodyBlock, directive, sparkshellAwareness));
4081
+ return formatAdditionalContextOutput(input.eventName, combineStaticContext(bodyBlock, directive));
4180
4082
  }
4181
4083
  function readRecoveryTranscriptText(transcriptPath) {
4182
4084
  if (transcriptPath === null) {
@@ -4200,7 +4102,7 @@ function recoverDynamicRulePaths(engine, transcriptText, staticRules) {
4200
4102
  if (transcriptText !== null && transcriptText.includes(rulePath)) {
4201
4103
  continue;
4202
4104
  }
4203
- if (!existsSync5(rulePath)) {
4105
+ if (!existsSync4(rulePath)) {
4204
4106
  continue;
4205
4107
  }
4206
4108
  recoveredPaths.add(rulePath);
@@ -4218,7 +4120,7 @@ function combineStaticContext(...blocks) {
4218
4120
  }
4219
4121
 
4220
4122
  // components/rules/src/tool-paths.ts
4221
- import { existsSync as existsSync6, statSync as statSync6 } from "node:fs";
4123
+ import { existsSync as existsSync5, statSync as statSync6 } from "node:fs";
4222
4124
  import { isAbsolute as isAbsolute4, resolve as resolve10 } from "node:path";
4223
4125
  var COMMAND_TOOL_NAMES = new Set(["bash", "shell_command", "exec_command"]);
4224
4126
  var TRACKED_TOOL_NAMES = new Set([
@@ -4329,7 +4231,7 @@ function resolvePath(cwd, filePath) {
4329
4231
  }
4330
4232
  function isExistingFile(filePath) {
4331
4233
  try {
4332
- return existsSync6(filePath) && statSync6(filePath).isFile();
4234
+ return existsSync5(filePath) && statSync6(filePath).isFile();
4333
4235
  } catch {
4334
4236
  return false;
4335
4237
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-rules",
3
- "version": "4.14.0",
3
+ "version": "4.14.1",
4
4
  "description": "Codex plugin that injects project rule files into model context through lifecycle hooks.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -11,7 +11,6 @@ import type { Engine } from "@oh-my-opencode/rules-engine/engine";
11
11
  import { isNeverTruncatedRule } from "@oh-my-opencode/rules-engine/engine";
12
12
  import type { LoadedRule, PiRulesConfig } from "@oh-my-opencode/rules-engine/engine";
13
13
  import { createRulesEngine } from "./rules-engine-factory.js";
14
- import { getSparkShellRuntimeAwareness, SPARKSHELL_AWARENESS_DEDUP_KEY } from "./sparkshell-awareness.js";
15
14
  import { filterRulesAlreadyInTranscript, filterRulesNotInTranscriptText } from "./transcript-rule-filter.js";
16
15
  import type { TranscriptSearchOptions } from "./transcript-search.js";
17
16
  import { readTranscriptSearchText } from "./transcript-search.js";
@@ -61,10 +60,7 @@ export function runStaticInjection(
61
60
  },
62
61
  transcriptSearchOptions,
63
62
  );
64
- const sparkshellAwareness = engine.state.staticDedup.has(SPARKSHELL_AWARENESS_DEDUP_KEY)
65
- ? ""
66
- : getSparkShellRuntimeAwareness(options.env);
67
- if (rules.length === 0 && sparkshellAwareness.length === 0) {
63
+ if (rules.length === 0) {
68
64
  persistEngineState(engine, cachePath);
69
65
  return "";
70
66
  }
@@ -73,11 +69,8 @@ export function runStaticInjection(
73
69
  for (const rule of rules) {
74
70
  engine.markStaticInjected(rule);
75
71
  }
76
- if (sparkshellAwareness.length > 0) {
77
- engine.state.staticDedup.add(SPARKSHELL_AWARENESS_DEDUP_KEY);
78
- }
79
72
  persistEngineState(engine, cachePath);
80
- return formatAdditionalContextOutput(eventName, combineStaticContext(block, sparkshellAwareness));
73
+ return formatAdditionalContextOutput(eventName, block);
81
74
  }
82
75
 
83
76
  interface PostCompactRecoveryInput {
@@ -110,11 +103,8 @@ function runPostCompactRecovery(input: PostCompactRecoveryInput): string {
110
103
  },
111
104
  );
112
105
  const dynamicRulePaths = recoverDynamicRulePaths(engine, transcriptText, loaded.rules);
113
- const sparkshellAwareness = engine.state.staticDedup.has(SPARKSHELL_AWARENESS_DEDUP_KEY)
114
- ? ""
115
- : getSparkShellRuntimeAwareness(input.options.env);
116
106
 
117
- if (missingRules.length === 0 && dynamicRulePaths.length === 0 && sparkshellAwareness.length === 0) {
107
+ if (missingRules.length === 0 && dynamicRulePaths.length === 0) {
118
108
  persistEngineState(engine, input.cachePath, input.channel);
119
109
  return "";
120
110
  }
@@ -129,13 +119,10 @@ function runPostCompactRecovery(input: PostCompactRecoveryInput): string {
129
119
  for (const rule of missingRules) {
130
120
  engine.markStaticInjected(rule);
131
121
  }
132
- if (sparkshellAwareness.length > 0) {
133
- engine.state.staticDedup.add(SPARKSHELL_AWARENESS_DEDUP_KEY);
134
- }
135
122
  persistEngineState(engine, input.cachePath, input.channel);
136
123
  return formatAdditionalContextOutput(
137
124
  input.eventName,
138
- combineStaticContext(bodyBlock, directive, sparkshellAwareness),
125
+ combineStaticContext(bodyBlock, directive),
139
126
  );
140
127
  }
141
128
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-start-work-continuation",
3
- "version": "4.14.0",
3
+ "version": "4.14.1",
4
4
  "description": "Codex Stop hook continuation injector for omo-codex start-work plans.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-teammode",
3
- "version": "4.14.0",
3
+ "version": "4.14.1",
4
4
  "description": "Codex team-mode hook component that keeps background thread titles descriptive after create_thread.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -68,7 +68,7 @@ node "<skill-root>/scripts/team.mjs" set-status --team <session_id> --id A --s
68
68
  node "<skill-root>/scripts/team.mjs" worktree-add --team <session_id> --id A [--base-branch <branch>]
69
69
  node "<skill-root>/scripts/team.mjs" worktree-remove --team <session_id> --id A [--force]
70
70
  node "<skill-root>/scripts/team.mjs" integrate --team <session_id> [--id A]
71
- node "<skill-root>/scripts/team.mjs" archive --team <session_id> [--id A]
71
+ node "<skill-root>/scripts/team.mjs" archive --team <session_id> [--id A] [--note "<...>"]
72
72
  node "<skill-root>/scripts/team.mjs" delete --team <session_id> [--force]
73
73
  node "<skill-root>/scripts/team.mjs" status --team <session_id>
74
74
  ```
@@ -80,6 +80,13 @@ space). `{session_id}` is the leader's Codex session id when you can pass it via
80
80
  otherwise the script generates a stable handle. Re-running `init` is a safe no-op. Every mutating
81
81
  subcommand rewrites `guide.md`, so the manual always matches the current team.
82
82
 
83
+ Mutating subcommands take a per-team state lock before reading and rewriting `team.json`. It is
84
+ safe to run independent `add-member`, `bind-thread`, `set-status`, `archive`, `delete`, `guide`,
85
+ and worktree mutation commands concurrently against the same team: they serialize and each command
86
+ reads the latest committed state before writing. If a command reports that team state is locked,
87
+ do not treat the intended mutation as complete; retry after the named command finishes, or inspect
88
+ `.omo/teams/{session_id}/.team.lock/owner.json` if the previous command crashed.
89
+
83
90
  ## Create the team and its threads
84
91
 
85
92
  1. `init` the team, then `add-member` once per member.
@@ -183,12 +190,17 @@ result against that todo's acceptance criteria before you integrate.
183
190
 
184
191
  DISBAND the team the moment it is no longer needed. A team exists only to do its work; once that
185
192
  work is done, or the user no longer wants it, do not leave it lying around - archive every member,
186
- then delete the team state. A finished team that is never disbanded is a leak.
193
+ then delete the team state only after archival evidence is clean or preserved. A finished team that
194
+ is never disbanded is a leak.
187
195
 
188
196
  - `archive` closes the team: notify each active member, copy anything useful into `artifacts/`,
189
- archive each member thread with `codex_app.set_thread_archived`, then `archive` flips the team
190
- and all members to archived. If a thread-archive tool is unavailable, record that in the team log
191
- and tell the user - never pretend a member was archived.
197
+ then try to archive each member thread with `codex_app.set_thread_archived`. Treat Codex App
198
+ failures such as "Ambiguous Codex thread id" or a thread id that is ambiguous across hosts as an
199
+ app-thread archival blocker, not as a team-state blocker: record the failure in the team log,
200
+ tell the user which member thread was not proven archived, and continue the team-state archive
201
+ with `archive --note "<blocker>"`. Never pretend a member thread was archived. Do not delete the
202
+ team state after an app-thread archival blocker unless the evidence has been copied elsewhere or
203
+ the user explicitly accepts that evidence loss.
192
204
  - `delete` removes `.omo/teams/{session_id}` and refuses while the team is unarchived or any member
193
205
  is still active unless `--force`.
194
206
  - When the work wraps up, land it the way the user asked: `integrate --team <id>` for a direct merge
@@ -9,8 +9,9 @@
9
9
  // this file stays the state concern only.
10
10
 
11
11
  import { randomUUID } from "node:crypto";
12
- import { lstat, mkdir, readFile, rename, writeFile } from "node:fs/promises";
12
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
13
13
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
14
+ import { setTimeout as delay } from "node:timers/promises";
14
15
 
15
16
  export const LENSES = ["area", "ownership", "perspective"];
16
17
  export const MEMBER_STATUSES = ["pending", "active", "reported", "blocked", "archived"];
@@ -24,6 +25,8 @@ export function isUnderstaffed(team) {
24
25
  // A team dir is a single child of .omo/teams. This pattern alone blocks "/", "\", and a
25
26
  // leading "." so ".." and "a/b" can never name a team dir (the escape guard).
26
27
  const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
28
+ const LOCK_TIMEOUT_MS = Number.parseInt(process.env.OMO_TEAMMODE_LOCK_TIMEOUT_MS ?? "10000", 10);
29
+ const LOCK_RETRY_MS = Number.parseInt(process.env.OMO_TEAMMODE_LOCK_RETRY_MS ?? "25", 10);
27
30
 
28
31
  function isoNow(now) {
29
32
  return now ?? new Date().toISOString();
@@ -233,6 +236,65 @@ async function lstatOrNull(p) {
233
236
  });
234
237
  }
235
238
 
239
+ function positiveIntegerOrFallback(value, fallback) {
240
+ return Number.isInteger(value) && value > 0 ? value : fallback;
241
+ }
242
+
243
+ async function readLockOwner(lockDir) {
244
+ return readFile(join(lockDir, "owner.json"), "utf8")
245
+ .then((content) => JSON.parse(content))
246
+ .catch(() => null);
247
+ }
248
+
249
+ async function describeLockOwner(lockDir) {
250
+ const owner = await readLockOwner(lockDir);
251
+ if (!owner || typeof owner !== "object") return "another team.mjs command";
252
+ const parts = [];
253
+ if (owner.command) parts.push(String(owner.command));
254
+ if (owner.pid) parts.push(`pid ${owner.pid}`);
255
+ if (owner.createdAt) parts.push(`since ${owner.createdAt}`);
256
+ return parts.length > 0 ? parts.join(", ") : "another team.mjs command";
257
+ }
258
+
259
+ export async function withTeamLock(dir, command, fn, options = {}) {
260
+ const timeoutMs = positiveIntegerOrFallback(options.timeoutMs ?? LOCK_TIMEOUT_MS, 10000);
261
+ const retryMs = positiveIntegerOrFallback(options.retryMs ?? LOCK_RETRY_MS, 25);
262
+ const lockDir = join(dir, ".team.lock");
263
+ const deadline = Date.now() + timeoutMs;
264
+ let acquired = false;
265
+ for (;;) {
266
+ try {
267
+ await mkdir(lockDir, { mode: 0o700 });
268
+ acquired = true;
269
+ await writeFile(
270
+ join(lockDir, "owner.json"),
271
+ `${JSON.stringify({ pid: process.pid, command, createdAt: new Date().toISOString() }, null, 2)}\n`,
272
+ { encoding: "utf8", flag: "wx" },
273
+ );
274
+ break;
275
+ } catch (error) {
276
+ if (acquired) {
277
+ await rm(lockDir, { recursive: true, force: true });
278
+ throw error;
279
+ }
280
+ if (!error || error.code !== "EEXIST") throw error;
281
+ const st = await lstatOrNull(lockDir);
282
+ if (st?.isSymbolicLink()) throw new Error(`refused: team lock path is a symlink: ${lockDir}`);
283
+ if (st && !st.isDirectory()) throw new Error(`refused: team lock path is not a directory: ${lockDir}`);
284
+ if (Date.now() >= deadline) {
285
+ const owner = await describeLockOwner(lockDir);
286
+ throw new Error(`team state is locked by ${owner}; retry after that command completes`);
287
+ }
288
+ await delay(Math.min(retryMs, Math.max(1, deadline - Date.now())));
289
+ }
290
+ }
291
+ try {
292
+ return await fn();
293
+ } finally {
294
+ await rm(lockDir, { recursive: true, force: true });
295
+ }
296
+ }
297
+
236
298
  // Create a directory chain refusing any symlinked component, so team state can never be
237
299
  // written through a symlink that escapes the workspace.
238
300
  async function mkdirNoSymlink(dir, stopAt) {