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.
Files changed (177) hide show
  1. package/.agents/skills/codex-qa/scripts/lib/app-server-client.mjs +132 -82
  2. package/.agents/skills/codex-qa/scripts/lib/app-server-client.test.js +48 -0
  3. package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +361 -84
  4. package/.agents/skills/work-with-pr/SKILL.md +19 -5
  5. package/.opencode/skills/work-with-pr/SKILL.md +19 -5
  6. package/dist/agents/atlas/agent.d.ts +3 -2
  7. package/dist/agents/sisyphus/glm-5-2.d.ts +4 -0
  8. package/dist/agents/sisyphus/index.d.ts +1 -0
  9. package/dist/agents/sisyphus-agent-config.d.ts +1 -0
  10. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  11. package/dist/agents/sisyphus-junior/glm-5-2.d.ts +1 -0
  12. package/dist/agents/sisyphus-junior/index.d.ts +1 -0
  13. package/dist/agents/types.d.ts +7 -6
  14. package/dist/cli/index.js +449 -318
  15. package/dist/cli-node/index.js +449 -318
  16. package/dist/features/background-agent/parent-wake-flush-runner.d.ts +1 -0
  17. package/dist/features/background-agent/parent-wake-history-state.d.ts +1 -0
  18. package/dist/features/background-agent/parent-wake-session-history.d.ts +1 -47
  19. package/dist/features/background-agent/parent-wake-session-message.d.ts +47 -0
  20. package/dist/hooks/keyword-detector/ultrawork/glm.d.ts +2 -0
  21. package/dist/hooks/keyword-detector/ultrawork/index.d.ts +4 -2
  22. package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +5 -4
  23. package/dist/index.js +1185 -87
  24. package/dist/plugin/tool-execute-before.d.ts +2 -0
  25. package/dist/plugin-handlers/plugin-components-loader.d.ts +1 -0
  26. package/dist/skills/init-deep/SKILL.md +21 -26
  27. package/dist/skills/programming/SKILL.md +25 -121
  28. package/dist/skills/programming/references/code-smells.md +390 -0
  29. package/dist/skills/ulw-plan/SKILL.md +2 -1
  30. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  31. package/dist/skills/ulw-plan/references/intent-clear.md +2 -2
  32. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -2
  33. package/dist/tools/delegate-task/sync-session-turns.d.ts +3 -0
  34. package/dist/tui.js +16 -7
  35. package/package.json +13 -13
  36. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +22 -2
  37. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +51 -21
  38. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  39. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +18 -7
  41. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +9 -2
  42. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +8 -7
  44. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +7 -2
  45. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +2 -1
  46. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +20 -19
  47. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +94 -0
  48. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +33 -0
  49. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  50. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/comment-checker/test/package-smoke.test.ts +2 -71
  52. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  53. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  55. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  57. package/packages/omo-codex/plugin/components/lsp/package.json +2 -2
  58. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +26 -104
  59. package/packages/omo-codex/plugin/components/rules/dist/cli.js +2 -2
  60. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  61. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +2 -2
  63. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +12 -92
  64. package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +3 -0
  65. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +157 -234
  66. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  67. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -2
  68. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +227 -15
  69. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +62 -2
  70. package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +34 -3
  71. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +139 -0
  72. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +111 -0
  73. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +250 -0
  74. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +220 -0
  75. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  76. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/test-support/package-smoke-fixture.ts +158 -0
  78. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  79. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  80. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
  81. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/agents/openai.yaml +1 -1
  82. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  83. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -2
  84. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +76 -49
  85. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  86. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  87. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml +1 -1
  88. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +1 -1
  89. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +17 -0
  90. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +17 -0
  91. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +17 -0
  92. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +17 -0
  93. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +17 -0
  94. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +17 -0
  95. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +17 -0
  96. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +17 -0
  97. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +17 -0
  98. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +17 -0
  99. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +16 -0
  100. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +16 -0
  101. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +16 -0
  102. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +16 -0
  103. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +16 -0
  104. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +17 -0
  105. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +16 -0
  106. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +16 -0
  107. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +16 -0
  108. package/packages/omo-codex/plugin/package-lock.json +12 -22
  109. package/packages/omo-codex/plugin/package.json +1 -1
  110. package/packages/omo-codex/plugin/scripts/auto-update.mjs +1 -1
  111. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +14 -11
  112. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +15 -4
  113. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +37 -0
  114. package/packages/omo-codex/plugin/skills/ast-grep/agents/openai.yaml +2 -0
  115. package/packages/omo-codex/plugin/skills/comment-checker/agents/openai.yaml +2 -0
  116. package/packages/omo-codex/plugin/skills/debugging/agents/openai.yaml +2 -0
  117. package/packages/omo-codex/plugin/skills/frontend/agents/openai.yaml +2 -0
  118. package/packages/omo-codex/plugin/skills/git-master/agents/openai.yaml +1 -1
  119. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +21 -26
  120. package/packages/omo-codex/plugin/skills/init-deep/agents/openai.yaml +2 -0
  121. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  122. package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
  123. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  124. package/packages/omo-codex/plugin/skills/lsp/agents/openai.yaml +2 -0
  125. package/packages/omo-codex/plugin/skills/lsp-setup/agents/openai.yaml +2 -0
  126. package/packages/omo-codex/plugin/skills/programming/SKILL.md +25 -121
  127. package/packages/omo-codex/plugin/skills/programming/agents/openai.yaml +2 -0
  128. package/packages/omo-codex/plugin/skills/programming/references/code-smells.md +390 -0
  129. package/packages/omo-codex/plugin/skills/refactor/agents/openai.yaml +2 -0
  130. package/packages/omo-codex/plugin/skills/remove-ai-slops/agents/openai.yaml +2 -0
  131. package/packages/omo-codex/plugin/skills/review-work/agents/openai.yaml +2 -0
  132. package/packages/omo-codex/plugin/skills/rules/agents/openai.yaml +2 -0
  133. package/packages/omo-codex/plugin/skills/start-work/agents/openai.yaml +2 -0
  134. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +139 -0
  135. package/packages/omo-codex/plugin/skills/teammode/agents/openai.yaml +2 -0
  136. package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +111 -0
  137. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +250 -0
  138. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +220 -0
  139. package/packages/omo-codex/plugin/skills/ultraresearch/agents/openai.yaml +2 -0
  140. package/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml +1 -1
  141. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
  142. package/packages/omo-codex/plugin/skills/ulw-plan/agents/openai.yaml +1 -1
  143. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  144. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -2
  145. package/packages/omo-codex/plugin/skills/visual-qa/agents/openai.yaml +2 -0
  146. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +34 -33
  147. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +5 -2
  148. package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +13 -0
  149. package/packages/omo-codex/plugin/test/auto-update.test.mjs +1 -1
  150. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +1 -1
  151. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +14 -6
  152. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +7 -3
  153. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +31 -0
  154. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +3 -3
  155. package/packages/omo-codex/plugin/test/display-metadata.test.mjs +83 -0
  156. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +30 -19
  157. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +3 -3
  158. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
  159. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
  160. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +9 -4
  161. package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +88 -0
  162. package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +240 -0
  163. package/packages/omo-codex/scripts/install-dist/install-local.mjs +412 -216
  164. package/packages/omo-codex/scripts/install-git-bash-mcp-env.test.mjs +24 -0
  165. package/packages/omo-codex/scripts/install-hook-targets.test.mjs +93 -0
  166. package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
  167. package/packages/omo-codex/scripts/install-local-bun-global-update.test.mjs +63 -0
  168. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +109 -1
  169. package/packages/omo-codex/scripts/install-local.mjs +1 -0
  170. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +36 -0
  171. package/packages/shared-skills/skills/init-deep/SKILL.md +21 -26
  172. package/packages/shared-skills/skills/programming/SKILL.md +25 -121
  173. package/packages/shared-skills/skills/programming/references/code-smells.md +390 -0
  174. package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
  175. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  176. package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -2
  177. 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 hooks = await readJson("hooks/hooks.json");
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 LazyCodex with current version", async () => {
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, `LazyCodex(${version}): Checking Comments`);
139
+ assert.equal(message, "(OmO) Checking Comments");
127
140
  });
128
141
 
129
- test("#given hook status label with blank version #when formatting #then prefixes LazyCodex with local version", () => {
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, "LazyCodex(local): Checking Comments");
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, `LazyCodex(${version}): Checking Comments`);
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, `LazyCodex(${version}): Verifying LazyCodex Executor Evidence`);
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, "LazyCodex(4.10.0): Recommending Git Bash MCP");
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 LazyCodex comments label", async () => {
194
+ test("#given aggregate comment-checker hook #when status is inspected #then it uses OmO comments label", async () => {
182
195
  // given
183
- const aggregateVersion = await readPluginVersion();
184
- const aggregateHooks = await readJson("hooks/hooks.json");
196
+ const aggregateManifests = await readAggregateHookManifests();
185
197
 
186
198
  // when
187
- const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion);
188
- const commentCheckerHook = hooks.find((hook) => hook.id === "hooks/hooks.json:PostToolUse:0:0");
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(aggregateVersion, "Checking Comments"));
192
- assert.doesNotMatch(JSON.stringify(aggregateHooks), /checking\s+OMO\s+comments/i);
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 LazyCodex formatter", async () => {
207
+ test("#given aggregate and component hooks #when status messages are inspected #then all use the OmO formatter", async () => {
196
208
  // given
197
- const aggregateVersion = await readPluginVersion();
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(aggregateHooks, "hooks/hooks.json", aggregateVersion),
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.doesNotMatch(hook.statusMessage, /\bOMO\b/i);
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 \(omo\)"/);
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 \(omo\)"/);
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: "lcx-doctor \(omo\)"/);
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, "LazyCodex(0.1.0): Checking Comments");
92
- assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "LazyCodex(0.1.0): Checking LSP Diagnostics");
93
- assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
94
- assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.2.0): Checking LSP Diagnostics");
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, "LazyCodex(4.8.0): Checking Comments");
143
- assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(4.8.0): Checking Comments");
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: "ulw-loop \(omo\)"/);
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: "git-master \(omo\)"/);
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
+ });