oh-my-opencode 4.14.0 → 4.14.2
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/.agents/skills/codex-qa/references/isolation.md +1 -1
- package/README.md +1 -1
- package/dist/cli/doctor/framework/types.d.ts +2 -0
- package/dist/cli/index.js +223 -1447
- package/dist/cli-node/index.js +223 -1447
- package/dist/index.js +4255 -714
- package/dist/plugin/native-skills.d.ts +9 -0
- package/dist/plugin/system-transform.d.ts +1 -1
- package/dist/skills/frontend/ATTRIBUTION.md +7 -1
- package/dist/skills/frontend/SKILL.md +4 -3
- package/dist/skills/frontend/references/design/README.md +4 -3
- package/dist/skills/frontend/references/design/_INDEX.md +5 -3
- package/dist/skills/frontend/references/design/aside.md +209 -0
- package/dist/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/dist/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/dist/skills/lcx-doctor/SKILL.md +48 -13
- package/dist/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/dist/skills/lcx-report-bug/SKILL.md +44 -13
- package/dist/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/dist/skills/visual-qa/SKILL.md +5 -5
- package/dist/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/dist/skills/visual-qa/scripts/cli.test.ts +7 -2
- package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
- package/dist/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/dist/tools/delegate-task/skill-resolver.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +2 -0
- package/dist/tools/skill/types.d.ts +1 -0
- package/package.json +26 -13
- package/packages/git-bash-mcp/dist/cli.js +2 -0
- package/packages/lsp-daemon/dist/cli.js +15 -4
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +1 -0
- package/packages/lsp-daemon/dist/ensure-daemon.js +9 -3
- package/packages/lsp-daemon/dist/index.js +15 -4
- package/packages/lsp-daemon/dist/paths.d.ts +1 -0
- package/packages/lsp-daemon/dist/paths.js +6 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -7
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +123 -10
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +123 -10
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +126 -9
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +141 -0
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +90 -25
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +2 -0
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +2 -0
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +56 -0
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +2 -5
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +2 -0
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +65 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +6 -1
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +8 -106
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +4 -17
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +17 -5
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +63 -1
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +80 -50
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +4 -12
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +7 -33
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +4 -12
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-quality-gate.json +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-doc.test.ts +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +7 -6
- package/packages/omo-codex/plugin/package-lock.json +16 -56
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +7 -1
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +4 -3
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +4 -3
- package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +5 -3
- package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +209 -0
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +48 -13
- package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +44 -13
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +17 -5
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +63 -1
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +80 -50
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +5 -5
- package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/packages/omo-codex/plugin/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +23 -0
- package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +38 -2
- package/packages/omo-codex/plugin/test/component-bin-names.test.mjs +12 -11
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +99 -1
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +62 -8
- package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/teammode-archive-ambiguity.test.mjs +102 -0
- package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +41 -1
- package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-bin-links.test.mjs +6 -0
- package/packages/omo-codex/scripts/install-cache-copy.test.mjs +28 -0
- package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +43 -3
- package/packages/omo-codex/scripts/install-delegated-command.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +169 -41
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +21 -2
- package/packages/omo-codex/scripts/install-local.test.mjs +0 -1
- package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +7 -1
- package/packages/shared-skills/skills/frontend/SKILL.md +4 -3
- package/packages/shared-skills/skills/frontend/references/design/README.md +4 -3
- package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +5 -3
- package/packages/shared-skills/skills/frontend/references/design/aside.md +209 -0
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/lcx-doctor/SKILL.md +48 -13
- package/packages/shared-skills/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +44 -13
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/visual-qa/SKILL.md +5 -5
- package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/packages/shared-skills/skills/visual-qa/scripts/cli.test.ts +7 -2
- package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
- package/packages/shared-skills/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/dist/cli/sparkshell-appserver-websocket.d.ts +0 -4
- package/dist/cli/sparkshell-appserver.d.ts +0 -16
- package/dist/cli/sparkshell-condense.d.ts +0 -10
- package/dist/cli/sparkshell-parse.d.ts +0 -24
- package/dist/cli/sparkshell-session-context.d.ts +0 -20
- package/dist/cli/sparkshell-spark.d.ts +0 -23
- package/dist/cli/sparkshell.d.ts +0 -33
- package/dist/shared/sparkshell-awareness.d.ts +0 -5
- package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +0 -106
- 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,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 =
|
|
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("
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
4234
|
+
return existsSync5(filePath) && statSync6(filePath).isFile();
|
|
4333
4235
|
} catch {
|
|
4334
4236
|
return false;
|
|
4335
4237
|
}
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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
|
|
125
|
+
combineStaticContext(bodyBlock, directive),
|
|
139
126
|
);
|
|
140
127
|
}
|
|
141
128
|
|
|
@@ -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
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
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
|
package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs
CHANGED
|
@@ -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) {
|