oh-my-opencode 4.11.1 → 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 (149) 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/types.d.ts +7 -6
  7. package/dist/cli/index.js +438 -306
  8. package/dist/cli-node/index.js +438 -306
  9. package/dist/features/background-agent/parent-wake-flush-runner.d.ts +1 -0
  10. package/dist/features/background-agent/parent-wake-history-state.d.ts +1 -0
  11. package/dist/features/background-agent/parent-wake-session-history.d.ts +1 -47
  12. package/dist/features/background-agent/parent-wake-session-message.d.ts +47 -0
  13. package/dist/index.js +245 -69
  14. package/dist/plugin/tool-execute-before.d.ts +2 -0
  15. package/dist/plugin-handlers/plugin-components-loader.d.ts +1 -0
  16. package/dist/skills/ulw-plan/SKILL.md +2 -1
  17. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  18. package/dist/skills/ulw-plan/references/intent-clear.md +2 -2
  19. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -2
  20. package/dist/tools/delegate-task/sync-session-turns.d.ts +3 -0
  21. package/dist/tui.js +16 -7
  22. package/package.json +13 -13
  23. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +22 -2
  24. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +25 -9
  25. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +10 -3
  28. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +9 -2
  29. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +7 -2
  31. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +2 -1
  32. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +9 -9
  33. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +94 -0
  34. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +33 -0
  35. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  36. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/comment-checker/test/package-smoke.test.ts +2 -71
  38. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  39. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  41. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  43. package/packages/omo-codex/plugin/components/lsp/package.json +2 -2
  44. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +26 -104
  45. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  46. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +12 -92
  48. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +157 -234
  49. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  50. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -2
  51. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +227 -15
  52. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +62 -2
  53. package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +34 -3
  54. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +139 -0
  55. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +111 -0
  56. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +250 -0
  57. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +220 -0
  58. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  59. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  60. package/packages/omo-codex/plugin/components/test-support/package-smoke-fixture.ts +158 -0
  61. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  62. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  63. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
  64. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/agents/openai.yaml +1 -1
  65. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  66. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -2
  67. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +76 -49
  68. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  69. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  70. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml +1 -1
  71. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +1 -1
  72. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +17 -0
  73. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +17 -0
  74. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +17 -0
  75. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +17 -0
  76. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +17 -0
  77. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +17 -0
  78. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +17 -0
  79. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +17 -0
  80. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +17 -0
  81. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +17 -0
  82. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +16 -0
  83. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +16 -0
  84. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +16 -0
  85. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +16 -0
  86. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +16 -0
  87. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +17 -0
  88. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +16 -0
  89. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +16 -0
  90. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +16 -0
  91. package/packages/omo-codex/plugin/package-lock.json +12 -22
  92. package/packages/omo-codex/plugin/package.json +1 -1
  93. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +14 -11
  94. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +15 -4
  95. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +37 -0
  96. package/packages/omo-codex/plugin/skills/ast-grep/agents/openai.yaml +2 -0
  97. package/packages/omo-codex/plugin/skills/comment-checker/agents/openai.yaml +2 -0
  98. package/packages/omo-codex/plugin/skills/debugging/agents/openai.yaml +2 -0
  99. package/packages/omo-codex/plugin/skills/frontend/agents/openai.yaml +2 -0
  100. package/packages/omo-codex/plugin/skills/git-master/agents/openai.yaml +1 -1
  101. package/packages/omo-codex/plugin/skills/init-deep/agents/openai.yaml +2 -0
  102. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  103. package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
  104. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  105. package/packages/omo-codex/plugin/skills/lsp/agents/openai.yaml +2 -0
  106. package/packages/omo-codex/plugin/skills/lsp-setup/agents/openai.yaml +2 -0
  107. package/packages/omo-codex/plugin/skills/programming/agents/openai.yaml +2 -0
  108. package/packages/omo-codex/plugin/skills/refactor/agents/openai.yaml +2 -0
  109. package/packages/omo-codex/plugin/skills/remove-ai-slops/agents/openai.yaml +2 -0
  110. package/packages/omo-codex/plugin/skills/review-work/agents/openai.yaml +2 -0
  111. package/packages/omo-codex/plugin/skills/rules/agents/openai.yaml +2 -0
  112. package/packages/omo-codex/plugin/skills/start-work/agents/openai.yaml +2 -0
  113. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +139 -0
  114. package/packages/omo-codex/plugin/skills/teammode/agents/openai.yaml +2 -0
  115. package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +111 -0
  116. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +250 -0
  117. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +220 -0
  118. package/packages/omo-codex/plugin/skills/ultraresearch/agents/openai.yaml +2 -0
  119. package/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml +1 -1
  120. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
  121. package/packages/omo-codex/plugin/skills/ulw-plan/agents/openai.yaml +1 -1
  122. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  123. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -2
  124. package/packages/omo-codex/plugin/skills/visual-qa/agents/openai.yaml +2 -0
  125. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +34 -33
  126. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +5 -2
  127. package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +13 -0
  128. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +1 -1
  129. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +14 -6
  130. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +7 -3
  131. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +3 -3
  132. package/packages/omo-codex/plugin/test/display-metadata.test.mjs +83 -0
  133. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +30 -19
  134. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +3 -3
  135. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
  136. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
  137. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +9 -4
  138. package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +88 -0
  139. package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +240 -0
  140. package/packages/omo-codex/scripts/install-dist/install-local.mjs +385 -206
  141. package/packages/omo-codex/scripts/install-hook-targets.test.mjs +93 -0
  142. package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
  143. package/packages/omo-codex/scripts/install-local-bun-global-update.test.mjs +63 -0
  144. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +109 -1
  145. package/packages/omo-codex/scripts/install-local.mjs +1 -0
  146. package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
  147. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  148. package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -2
  149. package/packages/omo-codex/plugin/hooks/hooks.json +0 -214
@@ -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
+ });