oh-my-opencode 4.11.0 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/scripts/lib/app-server-client.mjs +132 -82
- package/.agents/skills/codex-qa/scripts/lib/app-server-client.test.js +48 -0
- package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +361 -84
- package/.agents/skills/work-with-pr/SKILL.md +19 -5
- package/.opencode/skills/work-with-pr/SKILL.md +19 -5
- package/dist/agents/atlas/agent.d.ts +3 -2
- package/dist/agents/sisyphus/glm-5-2.d.ts +4 -0
- package/dist/agents/sisyphus/index.d.ts +1 -0
- package/dist/agents/sisyphus-agent-config.d.ts +1 -0
- package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
- package/dist/agents/sisyphus-junior/glm-5-2.d.ts +1 -0
- package/dist/agents/sisyphus-junior/index.d.ts +1 -0
- package/dist/agents/types.d.ts +7 -6
- package/dist/cli/index.js +449 -318
- package/dist/cli-node/index.js +449 -318
- package/dist/features/background-agent/parent-wake-flush-runner.d.ts +1 -0
- package/dist/features/background-agent/parent-wake-history-state.d.ts +1 -0
- package/dist/features/background-agent/parent-wake-session-history.d.ts +1 -47
- package/dist/features/background-agent/parent-wake-session-message.d.ts +47 -0
- package/dist/hooks/keyword-detector/ultrawork/glm.d.ts +2 -0
- package/dist/hooks/keyword-detector/ultrawork/index.d.ts +4 -2
- package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +5 -4
- package/dist/index.js +1185 -87
- package/dist/plugin/tool-execute-before.d.ts +2 -0
- package/dist/plugin-handlers/plugin-components-loader.d.ts +1 -0
- package/dist/skills/init-deep/SKILL.md +21 -26
- package/dist/skills/programming/SKILL.md +25 -121
- package/dist/skills/programming/references/code-smells.md +390 -0
- package/dist/skills/ulw-plan/SKILL.md +2 -1
- package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
- package/dist/skills/ulw-plan/references/intent-clear.md +2 -2
- package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -2
- package/dist/tools/delegate-task/sync-session-turns.d.ts +3 -0
- package/dist/tui.js +16 -7
- package/package.json +13 -13
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +22 -2
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +51 -21
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +18 -7
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +9 -2
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +8 -7
- package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +7 -2
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +2 -1
- package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +20 -19
- package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +94 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +33 -0
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/test/package-smoke.test.ts +2 -71
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +26 -104
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +2 -2
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +2 -2
- package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +12 -92
- package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +3 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +157 -234
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +227 -15
- package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +62 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +34 -3
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +139 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +111 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +250 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +220 -0
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/test-support/package-smoke-fixture.ts +158 -0
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +76 -49
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +17 -0
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +17 -0
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +17 -0
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +17 -0
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +17 -0
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +16 -0
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +16 -0
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +16 -0
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +16 -0
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +16 -0
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +17 -0
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +16 -0
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +16 -0
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +16 -0
- package/packages/omo-codex/plugin/package-lock.json +12 -22
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +1 -1
- package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +14 -11
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +15 -4
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +37 -0
- package/packages/omo-codex/plugin/skills/ast-grep/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/comment-checker/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/debugging/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/frontend/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/git-master/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +21 -26
- package/packages/omo-codex/plugin/skills/init-deep/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lsp/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/lsp-setup/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +25 -121
- package/packages/omo-codex/plugin/skills/programming/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/programming/references/code-smells.md +390 -0
- package/packages/omo-codex/plugin/skills/refactor/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/remove-ai-slops/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/review-work/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/rules/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/start-work/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +139 -0
- package/packages/omo-codex/plugin/skills/teammode/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +111 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +250 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +220 -0
- package/packages/omo-codex/plugin/skills/ultraresearch/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -2
- package/packages/omo-codex/plugin/skills/visual-qa/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +34 -33
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +5 -2
- package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +13 -0
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +14 -6
- package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +7 -3
- package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +31 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +3 -3
- package/packages/omo-codex/plugin/test/display-metadata.test.mjs +83 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +30 -19
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +3 -3
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +9 -4
- package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +88 -0
- package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +240 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +412 -216
- package/packages/omo-codex/scripts/install-git-bash-mcp-env.test.mjs +24 -0
- package/packages/omo-codex/scripts/install-hook-targets.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
- package/packages/omo-codex/scripts/install-local-bun-global-update.test.mjs +63 -0
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +109 -1
- package/packages/omo-codex/scripts/install-local.mjs +1 -0
- package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +36 -0
- package/packages/shared-skills/skills/init-deep/SKILL.md +21 -26
- package/packages/shared-skills/skills/programming/SKILL.md +25 -121
- package/packages/shared-skills/skills/programming/references/code-smells.md +390 -0
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -2
- package/packages/omo-codex/plugin/hooks/hooks.json +0 -214
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
|
|
9
|
-
import { readJson, root } from "./aggregate-plugin-fixture.mjs";
|
|
9
|
+
import { readAggregateHookManifests, readJson, root } from "./aggregate-plugin-fixture.mjs";
|
|
10
10
|
import { componentHookContractCases } from "./component-hook-contract-cases.mjs";
|
|
11
11
|
|
|
12
12
|
const HOOK_EVENTS_BY_COMPONENT = {
|
|
@@ -142,8 +142,8 @@ test("#given malformed comment-checker stdin #when executed through dist CLI con
|
|
|
142
142
|
|
|
143
143
|
test("#given aggregate hook manifest #when command hooks are inspected #then component CLI invocation contract is unchanged", async () => {
|
|
144
144
|
// given
|
|
145
|
-
const
|
|
146
|
-
const commands = collectHookCommands(hooks.hooks);
|
|
145
|
+
const manifests = await readAggregateHookManifests();
|
|
146
|
+
const commands = manifests.flatMap(({ hooks }) => collectHookCommands(hooks.hooks));
|
|
147
147
|
const components = await workspaceComponents();
|
|
148
148
|
|
|
149
149
|
// when
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
import { readJson, root } from "./aggregate-plugin-fixture.mjs";
|
|
7
|
+
|
|
8
|
+
const hookSlugPattern = /^\.\/hooks\/[a-z0-9]+(?:-[a-z0-9]+)*\.json$/;
|
|
9
|
+
const displayNamePrefix = "(OmO) ";
|
|
10
|
+
|
|
11
|
+
async function exists(path) {
|
|
12
|
+
try {
|
|
13
|
+
await stat(path);
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readSkillName(skillPath) {
|
|
22
|
+
const content = await readFile(skillPath, "utf8");
|
|
23
|
+
const frontmatter = content.match(/^---\n(?<body>[\s\S]*?)\n---\n/);
|
|
24
|
+
assert(frontmatter?.groups?.body, `${skillPath} must have YAML frontmatter`);
|
|
25
|
+
const name = frontmatter.groups.body.match(/^name:\s*"?([^"\n]+)"?\s*$/m)?.[1]?.trim();
|
|
26
|
+
assert(name, `${skillPath} must have a frontmatter name`);
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readOpenAiDisplayName(skillDir) {
|
|
31
|
+
const metadataPath = join(skillDir, "agents", "openai.yaml");
|
|
32
|
+
assert(await exists(metadataPath), `${basename(skillDir)} must include agents/openai.yaml`);
|
|
33
|
+
const content = await readFile(metadataPath, "utf8");
|
|
34
|
+
const displayName = content.match(/^\s*display_name:\s*"?([^"\n]+)"?\s*$/m)?.[1]?.trim();
|
|
35
|
+
assert(displayName, `${metadataPath} must define interface.display_name`);
|
|
36
|
+
return displayName;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("#given Codex plugin hooks #when plugin metadata is inspected #then hook keys contain human-readable hook slugs", async () => {
|
|
40
|
+
// given
|
|
41
|
+
const manifest = await readJson(".codex-plugin/plugin.json");
|
|
42
|
+
|
|
43
|
+
// when
|
|
44
|
+
const hookPaths = manifest.hooks;
|
|
45
|
+
|
|
46
|
+
// then
|
|
47
|
+
assert(Array.isArray(hookPaths), "plugin manifest hooks must list per-hook metadata files");
|
|
48
|
+
assert(hookPaths.length > 0, "plugin manifest must expose at least one hook file");
|
|
49
|
+
for (const hookPath of hookPaths) {
|
|
50
|
+
assert.equal(typeof hookPath, "string");
|
|
51
|
+
assert.match(hookPath, hookSlugPattern);
|
|
52
|
+
assert.notEqual(hookPath, "./hooks/hooks.json");
|
|
53
|
+
const hooksJson = await readJson(hookPath.replace(/^\.\//, ""));
|
|
54
|
+
const commandHookCount = Object.values(hooksJson.hooks)
|
|
55
|
+
.flat()
|
|
56
|
+
.flatMap((group) => group.hooks)
|
|
57
|
+
.filter((hook) => hook.type === "command").length;
|
|
58
|
+
assert.equal(commandHookCount, 1, `${hookPath} should describe exactly one displayed hook`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("#given Codex plugin skills #when skill metadata is inspected #then every skill has an OmO display name", async () => {
|
|
63
|
+
// given
|
|
64
|
+
const skillsDir = join(root, "skills");
|
|
65
|
+
const skillEntries = await readdir(skillsDir, { withFileTypes: true });
|
|
66
|
+
const skillDirs = skillEntries.filter((entry) => entry.isDirectory()).map((entry) => join(skillsDir, entry.name));
|
|
67
|
+
|
|
68
|
+
// when
|
|
69
|
+
const invalidDisplayNames = [];
|
|
70
|
+
for (const skillDir of skillDirs) {
|
|
71
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
72
|
+
if (!(await exists(skillPath))) continue;
|
|
73
|
+
const skillName = await readSkillName(skillPath);
|
|
74
|
+
const displayName = await readOpenAiDisplayName(dirname(skillPath));
|
|
75
|
+
const expected = `${displayNamePrefix}${skillName}`;
|
|
76
|
+
if (displayName !== expected) {
|
|
77
|
+
invalidDisplayNames.push(`${basename(skillDir)}: expected ${expected}, got ${displayName}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// then
|
|
82
|
+
assert.deepEqual(invalidDisplayNames, []);
|
|
83
|
+
});
|
|
@@ -68,6 +68,19 @@ async function readPluginVersion() {
|
|
|
68
68
|
return (await readJson(".codex-plugin/plugin.json")).version;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
async function readAggregateHookManifests() {
|
|
72
|
+
const manifest = await readJson(".codex-plugin/plugin.json");
|
|
73
|
+
const hookPaths = Array.isArray(manifest.hooks) ? manifest.hooks : [manifest.hooks];
|
|
74
|
+
return Promise.all(
|
|
75
|
+
hookPaths
|
|
76
|
+
.filter((hookPath) => typeof hookPath === "string")
|
|
77
|
+
.map(async (hookPath) => {
|
|
78
|
+
const source = hookPath.replace(/^\.\//, "");
|
|
79
|
+
return { source, version: await readPluginVersion(), hooks: await readJson(source) };
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
async function readComponentVersion(componentName) {
|
|
72
85
|
return (await readJson(join("components", componentName, "package.json"))).version;
|
|
73
86
|
}
|
|
@@ -114,7 +127,7 @@ function collectCommandHooks(hooks, source, version) {
|
|
|
114
127
|
return commandHooks;
|
|
115
128
|
}
|
|
116
129
|
|
|
117
|
-
test("#given hook status label #when formatting #then prefixes
|
|
130
|
+
test("#given hook status label #when formatting #then prefixes OmO display namespace", async () => {
|
|
118
131
|
// given
|
|
119
132
|
const version = (await readRepoJson("package.json")).version;
|
|
120
133
|
const label = "Checking Comments";
|
|
@@ -123,10 +136,10 @@ test("#given hook status label #when formatting #then prefixes LazyCodex with cu
|
|
|
123
136
|
const message = formatLazyCodexHookStatusMessage(version, label);
|
|
124
137
|
|
|
125
138
|
// then
|
|
126
|
-
assert.equal(message,
|
|
139
|
+
assert.equal(message, "(OmO) Checking Comments");
|
|
127
140
|
});
|
|
128
141
|
|
|
129
|
-
test("#given hook status label with blank version #when formatting #then prefixes
|
|
142
|
+
test("#given hook status label with blank version #when formatting #then still prefixes OmO display namespace", () => {
|
|
130
143
|
// given
|
|
131
144
|
const version = " ";
|
|
132
145
|
const label = "Checking Comments";
|
|
@@ -135,7 +148,7 @@ test("#given hook status label with blank version #when formatting #then prefixe
|
|
|
135
148
|
const message = formatLazyCodexHookStatusMessage(version, label);
|
|
136
149
|
|
|
137
150
|
// then
|
|
138
|
-
assert.equal(message, "
|
|
151
|
+
assert.equal(message, "(OmO) Checking Comments");
|
|
139
152
|
});
|
|
140
153
|
|
|
141
154
|
test("#given loose legacy status label #when normalizing #then removes OMO wording and title-cases label", async () => {
|
|
@@ -149,7 +162,7 @@ test("#given loose legacy status label #when normalizing #then removes OMO wordi
|
|
|
149
162
|
|
|
150
163
|
// then
|
|
151
164
|
assert.equal(normalized, "Checking Comments");
|
|
152
|
-
assert.equal(message,
|
|
165
|
+
assert.equal(message, "(OmO) Checking Comments");
|
|
153
166
|
});
|
|
154
167
|
|
|
155
168
|
test("#given LazyCodex appears inside hook label #when normalizing #then product casing is preserved", async () => {
|
|
@@ -163,7 +176,7 @@ test("#given LazyCodex appears inside hook label #when normalizing #then product
|
|
|
163
176
|
|
|
164
177
|
// then
|
|
165
178
|
assert.equal(normalized, "Verifying LazyCodex Executor Evidence");
|
|
166
|
-
assert.equal(message,
|
|
179
|
+
assert.equal(message, "(OmO) Verifying LazyCodex Executor Evidence");
|
|
167
180
|
});
|
|
168
181
|
|
|
169
182
|
test("#given MCP appears inside hook label #when normalizing #then protocol casing is preserved", () => {
|
|
@@ -176,31 +189,29 @@ test("#given MCP appears inside hook label #when normalizing #then protocol casi
|
|
|
176
189
|
|
|
177
190
|
// then
|
|
178
191
|
assert.equal(normalized, "Recommending Git Bash MCP");
|
|
179
|
-
assert.equal(message, "
|
|
192
|
+
assert.equal(message, "(OmO) Recommending Git Bash MCP");
|
|
180
193
|
});
|
|
181
|
-
test("#given aggregate comment-checker hook #when status is inspected #then it uses
|
|
194
|
+
test("#given aggregate comment-checker hook #when status is inspected #then it uses OmO comments label", async () => {
|
|
182
195
|
// given
|
|
183
|
-
const
|
|
184
|
-
const aggregateHooks = await readJson("hooks/hooks.json");
|
|
196
|
+
const aggregateManifests = await readAggregateHookManifests();
|
|
185
197
|
|
|
186
198
|
// when
|
|
187
|
-
const hooks = collectCommandHooks(
|
|
188
|
-
const commentCheckerHook = hooks.find((hook) => hook.
|
|
199
|
+
const hooks = aggregateManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version));
|
|
200
|
+
const commentCheckerHook = hooks.find((hook) => hook.command.includes("components/comment-checker/dist/cli.js"));
|
|
189
201
|
|
|
190
202
|
// then
|
|
191
|
-
assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage(
|
|
192
|
-
assert.doesNotMatch(JSON.stringify(
|
|
203
|
+
assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage("", "Checking Comments"));
|
|
204
|
+
assert.doesNotMatch(JSON.stringify(aggregateManifests), /checking\s+OMO\s+comments/i);
|
|
193
205
|
});
|
|
194
206
|
|
|
195
|
-
test("#given aggregate and component hooks #when status messages are inspected #then all use the
|
|
207
|
+
test("#given aggregate and component hooks #when status messages are inspected #then all use the OmO formatter", async () => {
|
|
196
208
|
// given
|
|
197
|
-
const
|
|
198
|
-
const aggregateHooks = await readJson("hooks/hooks.json");
|
|
209
|
+
const aggregateManifests = await readAggregateHookManifests();
|
|
199
210
|
const componentManifests = await readComponentHookManifests();
|
|
200
211
|
|
|
201
212
|
// when
|
|
202
213
|
const commandHooks = [
|
|
203
|
-
...collectCommandHooks(
|
|
214
|
+
...aggregateManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)),
|
|
204
215
|
...componentManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)),
|
|
205
216
|
];
|
|
206
217
|
const expectedLabels = new Map([...AGGREGATE_EXPECTED_LABELS, ...COMPONENT_EXPECTED_LABELS]);
|
|
@@ -219,6 +230,6 @@ test("#given aggregate and component hooks #when status messages are inspected #
|
|
|
219
230
|
const actualLabels = new Set(commandHooks.map((hook) => parseLazyCodexHookStatusMessage(hook.statusMessage)?.label));
|
|
220
231
|
assert.deepEqual([...expectedLabels.values()].filter((label) => !actualLabels.has(label)), []);
|
|
221
232
|
for (const hook of commandHooks) {
|
|
222
|
-
assert.
|
|
233
|
+
assert.match(hook.statusMessage, /^\(OmO\) /);
|
|
223
234
|
}
|
|
224
235
|
});
|
|
@@ -20,7 +20,7 @@ test("#given synced lcx-report-bug skill #when inspected #then it files LazyCode
|
|
|
20
20
|
assert.match(skill, /gh pr create --repo openai\/codex/);
|
|
21
21
|
assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
|
|
22
22
|
assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
|
|
23
|
-
assert.match(interfaceMetadata, /display_name: "lcx-report-bug
|
|
23
|
+
assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-report-bug"/);
|
|
24
24
|
assert.match(interfaceMetadata, /- "lazycodex bug"/);
|
|
25
25
|
assert.match(interfaceMetadata, /- "openai codex bug"/);
|
|
26
26
|
});
|
|
@@ -40,7 +40,7 @@ test("#given synced lcx-contribute-bug-fix skill #when inspected #then it delive
|
|
|
40
40
|
assert.match(skill, /gh pr create --repo openai\/codex/);
|
|
41
41
|
assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
|
|
42
42
|
assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
|
|
43
|
-
assert.match(interfaceMetadata, /display_name: "lcx-contribute-bug-fix
|
|
43
|
+
assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-contribute-bug-fix"/);
|
|
44
44
|
assert.match(interfaceMetadata, /- "contribute a bug fix"/);
|
|
45
45
|
assert.match(interfaceMetadata, /- "fix bug pr"/);
|
|
46
46
|
});
|
|
@@ -55,7 +55,7 @@ test("#given synced lcx-doctor skill #when inspected #then it diagnoses installs
|
|
|
55
55
|
|
|
56
56
|
// then
|
|
57
57
|
assert.match(skill, /^---\r?\nname: lcx-doctor\r?\n/m);
|
|
58
|
-
assert.match(interfaceMetadata, /display_name: "
|
|
58
|
+
assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-doctor"/);
|
|
59
59
|
assert.match(interfaceMetadata, /- "lazycodex doctor"/);
|
|
60
60
|
assert.match(interfaceMetadata, /- "lazycodex health check"/);
|
|
61
61
|
});
|
|
@@ -88,10 +88,10 @@ test("#given source package versions and component without hooks #when hook stat
|
|
|
88
88
|
const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
|
|
89
89
|
const componentHooks = await readJson(join(root, "components", "comment-checker", "hooks", "hooks.json"));
|
|
90
90
|
const lspHooks = await readJson(join(root, "components", "lsp", "hooks", "hooks.json"));
|
|
91
|
-
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "
|
|
92
|
-
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "
|
|
93
|
-
assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "
|
|
94
|
-
assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "
|
|
91
|
+
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
|
|
92
|
+
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "(OmO) Checking LSP Diagnostics");
|
|
93
|
+
assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
|
|
94
|
+
assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking LSP Diagnostics");
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
test("#given release version override #when hook status messages sync #then aggregate hooks use release version", async () => {
|
|
@@ -139,6 +139,6 @@ test("#given release version override #when hook status messages sync #then aggr
|
|
|
139
139
|
// then
|
|
140
140
|
const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
|
|
141
141
|
const componentHooks = await readJson(join(root, "components", "comment-checker", "hooks", "hooks.json"));
|
|
142
|
-
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "
|
|
143
|
-
assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "
|
|
142
|
+
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
|
|
143
|
+
assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
|
|
144
144
|
});
|
|
@@ -22,6 +22,7 @@ export const expectedSkills = [
|
|
|
22
22
|
"review-work",
|
|
23
23
|
"rules",
|
|
24
24
|
"start-work",
|
|
25
|
+
"teammode",
|
|
25
26
|
"ultraresearch",
|
|
26
27
|
"ulw-loop",
|
|
27
28
|
"ulw-plan",
|
|
@@ -32,6 +33,7 @@ export const componentSkillSources = [
|
|
|
32
33
|
["comment-checker", "components/comment-checker/skills/comment-checker"],
|
|
33
34
|
["lsp", "components/lsp/skills/lsp"],
|
|
34
35
|
["rules", "components/rules/skills/rules"],
|
|
36
|
+
["teammode", "components/teammode/skills/teammode"],
|
|
35
37
|
["ulw-loop", "components/ulw-loop/skills/ulw-loop"],
|
|
36
38
|
["ulw-plan", "components/ultrawork/skills/ulw-plan"],
|
|
37
39
|
];
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
18
|
const repoRoot = join(root, "..", "..", "..");
|
|
19
19
|
const opencodeOnlyToolPattern = /\b(?:call_omo_agent|background_output|team_[a-z_]+|task)\s*\(/;
|
|
20
|
+
const generatedSkillMetadataFiles = new Set(["agents/openai.yaml"]);
|
|
20
21
|
|
|
21
22
|
async function readPackagedSkillFile(...segments) {
|
|
22
23
|
const path = join(root, "skills", ...segments);
|
|
@@ -24,6 +25,10 @@ async function readPackagedSkillFile(...segments) {
|
|
|
24
25
|
return { path, content };
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function excludeGeneratedSkillMetadata(files) {
|
|
29
|
+
return files.filter((file) => !generatedSkillMetadataFiles.has(file.replaceAll("\\", "/")));
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
test("#given synced aggregate Codex skills #when inspected #then component and shared skills are present", async () => {
|
|
28
33
|
// given
|
|
29
34
|
const skillsRoot = join(root, "skills");
|
|
@@ -121,8 +126,8 @@ test("#given component skill sources #when aggregate Codex component skills are
|
|
|
121
126
|
for (const [skillName, sourcePath] of componentSkillSources) {
|
|
122
127
|
const sourceDir = join(root, sourcePath);
|
|
123
128
|
const aggregateDir = join(aggregateSkillsRoot, skillName);
|
|
124
|
-
const sourceFiles = await listSkillFiles(sourceDir);
|
|
125
|
-
const aggregateFiles = await listSkillFiles(aggregateDir);
|
|
129
|
+
const sourceFiles = excludeGeneratedSkillMetadata(await listSkillFiles(sourceDir));
|
|
130
|
+
const aggregateFiles = excludeGeneratedSkillMetadata(await listSkillFiles(aggregateDir));
|
|
126
131
|
assert.deepEqual(aggregateFiles, sourceFiles, `${skillName} resource set drifted from its component skill source`);
|
|
127
132
|
for (const relativePath of sourceFiles) {
|
|
128
133
|
const sourceContent = await readFile(join(sourceDir, relativePath), "utf8");
|
|
@@ -146,7 +151,7 @@ test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then
|
|
|
146
151
|
|
|
147
152
|
// then
|
|
148
153
|
assert.match(skill, /^---\r?\nname: ulw-loop\r?\n/m);
|
|
149
|
-
assert.match(interfaceMetadata, /display_name: "
|
|
154
|
+
assert.match(interfaceMetadata, /display_name: "\(OmO\) ulw-loop"/);
|
|
150
155
|
assert.doesNotMatch(interfaceMetadata, /ulw-loop \/ ulw-loop/);
|
|
151
156
|
assert.match(interfaceMetadata, /short_description: "Goal-like ultrawork loop for systematic decomposition"/);
|
|
152
157
|
assert.match(interfaceMetadata, /default_prompt: "Use \$ulw-loop/);
|
|
@@ -179,7 +184,7 @@ test("#given synced git-master skill #when inspected #then commits and git histo
|
|
|
179
184
|
assert.match(skill, /Choose the Git tool by the question/);
|
|
180
185
|
assert.match(skill, /git log -S "text"/);
|
|
181
186
|
assert.match(skill, /git blame -L start,end -- file/);
|
|
182
|
-
assert.match(interfaceMetadata, /display_name: "
|
|
187
|
+
assert.match(interfaceMetadata, /display_name: "\(OmO\) git-master"/);
|
|
183
188
|
assert.match(interfaceMetadata, /- "git commit"/);
|
|
184
189
|
assert.match(interfaceMetadata, /- "history search"/);
|
|
185
190
|
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { root } from "./aggregate-plugin-fixture.mjs";
|
|
8
|
+
|
|
9
|
+
const teamScript = join(root, "components", "teammode", "skills", "teammode", "scripts", "team.mjs");
|
|
10
|
+
|
|
11
|
+
export function createTeamRoot(prefix) {
|
|
12
|
+
return mkdtempSync(join(tmpdir(), prefix));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function cleanupTeamRoot(tempRoot) {
|
|
16
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function teamDir(tempRoot, sessionId) {
|
|
20
|
+
return join(tempRoot, ".omo", "teams", sessionId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function teamJsonPath(tempRoot, sessionId) {
|
|
24
|
+
return join(teamDir(tempRoot, sessionId), "team.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readTeamJson(tempRoot, sessionId) {
|
|
28
|
+
return JSON.parse(readFileSync(teamJsonPath(tempRoot, sessionId), "utf8"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runTeam(cwd, ...args) {
|
|
32
|
+
const result = runTeamRaw(cwd, ...args);
|
|
33
|
+
assert.equal(result.status, 0, `team.mjs ${args.join(" ")} failed: ${result.stderr}`);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function runTeamRaw(cwd, ...args) {
|
|
38
|
+
return spawnSync(process.execPath, [teamScript, ...args], {
|
|
39
|
+
cwd,
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
timeout: 10_000,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function symlinkOrSkip(t, target, path, type) {
|
|
46
|
+
try {
|
|
47
|
+
symlinkSync(target, path, type);
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (isUnavailableSymlinkError(error)) {
|
|
51
|
+
t.skip(`symlink unavailable on this filesystem: ${error.code}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isUnavailableSymlinkError(error) {
|
|
59
|
+
return error?.code === "EPERM" || error?.code === "EACCES" || error?.code === "EINVAL";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createArchivedOutsideTeam(tempRoot, sessionId) {
|
|
63
|
+
const outsideTeams = join(tempRoot, "outside-teams");
|
|
64
|
+
const outsideTeamDir = join(outsideTeams, sessionId);
|
|
65
|
+
mkdirSync(outsideTeamDir, { recursive: true });
|
|
66
|
+
writeFileSync(
|
|
67
|
+
join(outsideTeamDir, "team.json"),
|
|
68
|
+
`${JSON.stringify(
|
|
69
|
+
{
|
|
70
|
+
schemaVersion: 2,
|
|
71
|
+
teamId: "outside-team",
|
|
72
|
+
teamName: "Outside",
|
|
73
|
+
sessionName: "Escape",
|
|
74
|
+
leader: { kind: "main-session", sessionId },
|
|
75
|
+
status: "archived",
|
|
76
|
+
members: [],
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2,
|
|
80
|
+
)}\n`,
|
|
81
|
+
);
|
|
82
|
+
return { outsideTeams, outsideTeamDir };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function assertOutsideTeamIntact(outsideTeamDir) {
|
|
86
|
+
assert.equal(existsSync(outsideTeamDir), true);
|
|
87
|
+
assert.equal(JSON.parse(readFileSync(join(outsideTeamDir, "team.json"), "utf8")).teamId, "outside-team");
|
|
88
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
assertOutsideTeamIntact,
|
|
8
|
+
cleanupTeamRoot,
|
|
9
|
+
createArchivedOutsideTeam,
|
|
10
|
+
createTeamRoot,
|
|
11
|
+
readTeamJson,
|
|
12
|
+
runTeam,
|
|
13
|
+
runTeamRaw,
|
|
14
|
+
symlinkOrSkip,
|
|
15
|
+
teamDir,
|
|
16
|
+
teamJsonPath,
|
|
17
|
+
} from "./teammode-safety-fixture.mjs";
|
|
18
|
+
|
|
19
|
+
test("#given guide.md is a symlink #when guide is regenerated #then the outside target stays untouched", (t) => {
|
|
20
|
+
const tempRoot = createTeamRoot("omo-codex-teammode-guide-");
|
|
21
|
+
try {
|
|
22
|
+
runTeam(tempRoot, "init", "--name", "Symlink", "--session-name", "Escape", "--session", "safe-guide");
|
|
23
|
+
const guidePath = join(teamDir(tempRoot, "safe-guide"), "guide.md");
|
|
24
|
+
const outsidePath = join(tempRoot, "outside-guide-target.md");
|
|
25
|
+
writeFileSync(outsidePath, "ORIGINAL_OUTSIDE\n");
|
|
26
|
+
unlinkSync(guidePath);
|
|
27
|
+
if (!symlinkOrSkip(t, outsidePath, guidePath)) return;
|
|
28
|
+
|
|
29
|
+
const result = runTeamRaw(tempRoot, "guide", "--team", "safe-guide");
|
|
30
|
+
|
|
31
|
+
assert.notEqual(result.status, 0);
|
|
32
|
+
assert.match(result.stderr, /guide\.md is a symlink|persist target escapes/);
|
|
33
|
+
assert.equal(readFileSync(outsidePath, "utf8"), "ORIGINAL_OUTSIDE\n");
|
|
34
|
+
} finally {
|
|
35
|
+
cleanupTeamRoot(tempRoot);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("#given member A exists #when add-member receives A with trailing space #then state is not partially mutated", () => {
|
|
40
|
+
const tempRoot = createTeamRoot("omo-codex-teammode-duplicate-");
|
|
41
|
+
try {
|
|
42
|
+
runTeam(tempRoot, "init", "--name", "Duplicate", "--session-name", "Members", "--session", "safe-duplicate");
|
|
43
|
+
runTeam(
|
|
44
|
+
tempRoot,
|
|
45
|
+
"add-member",
|
|
46
|
+
"--team",
|
|
47
|
+
"safe-duplicate",
|
|
48
|
+
"--id",
|
|
49
|
+
"A",
|
|
50
|
+
"--focus",
|
|
51
|
+
"alpha",
|
|
52
|
+
"--lens",
|
|
53
|
+
"area",
|
|
54
|
+
"--deliverable",
|
|
55
|
+
"first",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const result = runTeamRaw(
|
|
59
|
+
tempRoot,
|
|
60
|
+
"add-member",
|
|
61
|
+
"--team",
|
|
62
|
+
"safe-duplicate",
|
|
63
|
+
"--id",
|
|
64
|
+
"A ",
|
|
65
|
+
"--focus",
|
|
66
|
+
"beta",
|
|
67
|
+
"--lens",
|
|
68
|
+
"ownership",
|
|
69
|
+
"--deliverable",
|
|
70
|
+
"second",
|
|
71
|
+
);
|
|
72
|
+
const team = readTeamJson(tempRoot, "safe-duplicate");
|
|
73
|
+
|
|
74
|
+
assert.notEqual(result.status, 0);
|
|
75
|
+
assert.match(result.stderr, /member id "A" already exists/);
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
team.members.map((member) => member.id),
|
|
78
|
+
["A"],
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
cleanupTeamRoot(tempRoot);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("#given member focus already exists #when add-member receives same focus with different spacing and case #then state is not partially mutated", () => {
|
|
86
|
+
const tempRoot = createTeamRoot("omo-codex-teammode-duplicate-focus-");
|
|
87
|
+
try {
|
|
88
|
+
runTeam(tempRoot, "init", "--name", "DuplicateFocus", "--session-name", "Members", "--session", "safe-duplicate-focus");
|
|
89
|
+
runTeam(
|
|
90
|
+
tempRoot,
|
|
91
|
+
"add-member",
|
|
92
|
+
"--team",
|
|
93
|
+
"safe-duplicate-focus",
|
|
94
|
+
"--id",
|
|
95
|
+
"A",
|
|
96
|
+
"--focus",
|
|
97
|
+
"Plugin Hook Packaging",
|
|
98
|
+
"--lens",
|
|
99
|
+
"ownership",
|
|
100
|
+
"--deliverable",
|
|
101
|
+
"first",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const result = runTeamRaw(
|
|
105
|
+
tempRoot,
|
|
106
|
+
"add-member",
|
|
107
|
+
"--team",
|
|
108
|
+
"safe-duplicate-focus",
|
|
109
|
+
"--id",
|
|
110
|
+
"B",
|
|
111
|
+
"--focus",
|
|
112
|
+
" plugin hook packaging ",
|
|
113
|
+
"--lens",
|
|
114
|
+
"area",
|
|
115
|
+
"--deliverable",
|
|
116
|
+
"second",
|
|
117
|
+
);
|
|
118
|
+
const team = readTeamJson(tempRoot, "safe-duplicate-focus");
|
|
119
|
+
|
|
120
|
+
assert.notEqual(result.status, 0);
|
|
121
|
+
assert.match(result.stderr, /member focus "plugin hook packaging" duplicates "Plugin Hook Packaging"/);
|
|
122
|
+
assert.deepEqual(
|
|
123
|
+
team.members.map((member) => member.id),
|
|
124
|
+
["A"],
|
|
125
|
+
);
|
|
126
|
+
} finally {
|
|
127
|
+
cleanupTeamRoot(tempRoot);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("#given only one member exists #when bind-thread runs #then member is not activated", () => {
|
|
132
|
+
const tempRoot = createTeamRoot("omo-codex-teammode-understaffed-bind-");
|
|
133
|
+
try {
|
|
134
|
+
runTeam(tempRoot, "init", "--name", "Understaffed", "--session-name", "Members", "--session", "safe-understaffed");
|
|
135
|
+
runTeam(
|
|
136
|
+
tempRoot,
|
|
137
|
+
"add-member",
|
|
138
|
+
"--team",
|
|
139
|
+
"safe-understaffed",
|
|
140
|
+
"--id",
|
|
141
|
+
"A",
|
|
142
|
+
"--focus",
|
|
143
|
+
"installer",
|
|
144
|
+
"--lens",
|
|
145
|
+
"area",
|
|
146
|
+
"--deliverable",
|
|
147
|
+
"first",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const result = runTeamRaw(tempRoot, "bind-thread", "--team", "safe-understaffed", "--id", "A", "--thread", "thread-a");
|
|
151
|
+
let team = readTeamJson(tempRoot, "safe-understaffed");
|
|
152
|
+
|
|
153
|
+
assert.notEqual(result.status, 0);
|
|
154
|
+
assert.match(result.stderr, /at least 2 distinct members/);
|
|
155
|
+
assert.equal(team.members[0].threadId, null);
|
|
156
|
+
assert.equal(team.members[0].status, "pending");
|
|
157
|
+
|
|
158
|
+
runTeam(
|
|
159
|
+
tempRoot,
|
|
160
|
+
"add-member",
|
|
161
|
+
"--team",
|
|
162
|
+
"safe-understaffed",
|
|
163
|
+
"--id",
|
|
164
|
+
"B",
|
|
165
|
+
"--focus",
|
|
166
|
+
"runtime qa",
|
|
167
|
+
"--lens",
|
|
168
|
+
"perspective",
|
|
169
|
+
"--deliverable",
|
|
170
|
+
"second",
|
|
171
|
+
);
|
|
172
|
+
runTeam(tempRoot, "bind-thread", "--team", "safe-understaffed", "--id", "A", "--thread", "thread-a");
|
|
173
|
+
team = readTeamJson(tempRoot, "safe-understaffed");
|
|
174
|
+
|
|
175
|
+
assert.equal(team.members[0].threadId, "thread-a");
|
|
176
|
+
assert.equal(team.members[0].status, "active");
|
|
177
|
+
} finally {
|
|
178
|
+
cleanupTeamRoot(tempRoot);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("#given persisted paths are mutated outside trusted team dir #when guide is regenerated #then outside file stays untouched", () => {
|
|
183
|
+
const tempRoot = createTeamRoot("omo-codex-teammode-mutated-paths-");
|
|
184
|
+
try {
|
|
185
|
+
runTeam(tempRoot, "init", "--name", "Mutated", "--session-name", "Paths", "--session", "safe-mutated");
|
|
186
|
+
const teamPath = teamJsonPath(tempRoot, "safe-mutated");
|
|
187
|
+
const outsideDir = join(tempRoot, "outside");
|
|
188
|
+
const outsideGuide = join(outsideDir, "guide.md");
|
|
189
|
+
mkdirSync(outsideDir);
|
|
190
|
+
writeFileSync(outsideGuide, "ORIGINAL_OUTSIDE\n");
|
|
191
|
+
const team = JSON.parse(readFileSync(teamPath, "utf8"));
|
|
192
|
+
team.paths.dir = outsideDir;
|
|
193
|
+
team.paths.guide = outsideGuide;
|
|
194
|
+
writeFileSync(teamPath, `${JSON.stringify(team, null, 2)}\n`);
|
|
195
|
+
|
|
196
|
+
const result = runTeamRaw(tempRoot, "guide", "--team", "safe-mutated");
|
|
197
|
+
|
|
198
|
+
assert.notEqual(result.status, 0);
|
|
199
|
+
assert.match(result.stderr, /persisted team dir does not match trusted team dir/);
|
|
200
|
+
assert.equal(readFileSync(outsideGuide, "utf8"), "ORIGINAL_OUTSIDE\n");
|
|
201
|
+
} finally {
|
|
202
|
+
cleanupTeamRoot(tempRoot);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("#given team dir is swapped to a symlink #when status reads team state #then command refuses the symlink", (t) => {
|
|
207
|
+
const tempRoot = createTeamRoot("omo-codex-teammode-dir-symlink-");
|
|
208
|
+
try {
|
|
209
|
+
runTeam(tempRoot, "init", "--name", "SymlinkDir", "--session-name", "Paths", "--session", "safe-dir");
|
|
210
|
+
const targetTeamDir = teamDir(tempRoot, "safe-dir");
|
|
211
|
+
const outsideDir = join(tempRoot, "outside-team-dir");
|
|
212
|
+
mkdirSync(outsideDir);
|
|
213
|
+
rmSync(targetTeamDir, { recursive: true, force: true });
|
|
214
|
+
if (!symlinkOrSkip(t, outsideDir, targetTeamDir, "dir")) return;
|
|
215
|
+
|
|
216
|
+
const result = runTeamRaw(tempRoot, "status", "--team", "safe-dir");
|
|
217
|
+
|
|
218
|
+
assert.notEqual(result.status, 0);
|
|
219
|
+
assert.match(result.stderr, /path component is a symlink|team dir is a symlink/);
|
|
220
|
+
} finally {
|
|
221
|
+
cleanupTeamRoot(tempRoot);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("#given teams root is a symlink #when delete runs #then outside team state stays untouched", (t) => {
|
|
226
|
+
const tempRoot = createTeamRoot("omo-codex-teammode-delete-root-symlink-");
|
|
227
|
+
try {
|
|
228
|
+
const { outsideTeams, outsideTeamDir } = createArchivedOutsideTeam(tempRoot, "escape");
|
|
229
|
+
mkdirSync(join(tempRoot, ".omo"), { recursive: true });
|
|
230
|
+
if (!symlinkOrSkip(t, outsideTeams, join(tempRoot, ".omo", "teams"), "dir")) return;
|
|
231
|
+
|
|
232
|
+
const result = runTeamRaw(tempRoot, "delete", "--team", "escape", "--force");
|
|
233
|
+
|
|
234
|
+
assert.notEqual(result.status, 0);
|
|
235
|
+
assert.match(result.stderr, /path component is a symlink/);
|
|
236
|
+
assertOutsideTeamIntact(outsideTeamDir);
|
|
237
|
+
} finally {
|
|
238
|
+
cleanupTeamRoot(tempRoot);
|
|
239
|
+
}
|
|
240
|
+
});
|