oh-my-opencode 4.11.1 → 4.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) 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 +460 -309
  8. package/dist/cli-node/index.js +460 -309
  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 +593 -310
  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/ultraresearch/SKILL.md +11 -2
  17. package/dist/skills/ulw-plan/SKILL.md +2 -1
  18. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  19. package/dist/skills/ulw-plan/references/intent-clear.md +2 -2
  20. package/dist/tools/background-task/constants.d.ts +1 -1
  21. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -2
  22. package/dist/tools/delegate-task/sync-session-turns.d.ts +3 -0
  23. package/dist/tui.js +25 -10
  24. package/package.json +13 -13
  25. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +24 -2
  26. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +25 -9
  27. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  28. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +336 -173
  30. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +284 -33
  31. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +11 -0
  33. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +12 -0
  34. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +34 -0
  35. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +47 -5
  36. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +7 -5
  37. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +58 -96
  38. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +94 -0
  39. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +33 -0
  40. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +48 -0
  41. package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +1 -0
  42. package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +142 -0
  43. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/comment-checker/test/package-smoke.test.ts +2 -71
  46. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  47. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  49. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  50. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/lsp/package.json +2 -2
  52. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +26 -104
  53. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  54. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +12 -92
  56. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +157 -234
  57. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  58. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -2
  59. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +227 -15
  60. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +62 -2
  61. package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +34 -3
  62. package/packages/omo-codex/plugin/components/teammode/dist/cli.js +94 -0
  63. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +17 -0
  64. package/packages/omo-codex/plugin/components/teammode/package.json +26 -0
  65. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +139 -0
  66. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +111 -0
  67. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +250 -0
  68. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +220 -0
  69. package/packages/omo-codex/plugin/components/teammode/src/cli.ts +12 -0
  70. package/packages/omo-codex/plugin/components/teammode/src/codex-hook.ts +125 -0
  71. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +116 -0
  72. package/packages/omo-codex/plugin/components/teammode/tsconfig.json +25 -0
  73. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  74. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  75. package/packages/omo-codex/plugin/components/test-support/package-smoke-fixture.ts +158 -0
  76. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  77. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  78. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
  79. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/agents/openai.yaml +1 -1
  80. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  81. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -2
  82. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +76 -49
  83. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  84. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  85. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml +1 -1
  86. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +1 -1
  87. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +17 -0
  88. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +17 -0
  89. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +17 -0
  90. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +17 -0
  91. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +17 -0
  92. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +17 -0
  93. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.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 +30 -22
  109. package/packages/omo-codex/plugin/package.json +2 -1
  110. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +15 -11
  111. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +15 -4
  112. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +37 -0
  113. package/packages/omo-codex/plugin/skills/ast-grep/agents/openai.yaml +2 -0
  114. package/packages/omo-codex/plugin/skills/comment-checker/agents/openai.yaml +2 -0
  115. package/packages/omo-codex/plugin/skills/debugging/agents/openai.yaml +2 -0
  116. package/packages/omo-codex/plugin/skills/frontend/agents/openai.yaml +2 -0
  117. package/packages/omo-codex/plugin/skills/git-master/agents/openai.yaml +1 -1
  118. package/packages/omo-codex/plugin/skills/init-deep/agents/openai.yaml +2 -0
  119. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  120. package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
  121. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  122. package/packages/omo-codex/plugin/skills/lsp/agents/openai.yaml +2 -0
  123. package/packages/omo-codex/plugin/skills/lsp-setup/agents/openai.yaml +2 -0
  124. package/packages/omo-codex/plugin/skills/programming/agents/openai.yaml +2 -0
  125. package/packages/omo-codex/plugin/skills/refactor/agents/openai.yaml +2 -0
  126. package/packages/omo-codex/plugin/skills/remove-ai-slops/agents/openai.yaml +2 -0
  127. package/packages/omo-codex/plugin/skills/review-work/agents/openai.yaml +2 -0
  128. package/packages/omo-codex/plugin/skills/rules/agents/openai.yaml +2 -0
  129. package/packages/omo-codex/plugin/skills/start-work/agents/openai.yaml +2 -0
  130. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +139 -0
  131. package/packages/omo-codex/plugin/skills/teammode/agents/openai.yaml +2 -0
  132. package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +111 -0
  133. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +250 -0
  134. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +220 -0
  135. package/packages/omo-codex/plugin/skills/ultraresearch/SKILL.md +11 -2
  136. package/packages/omo-codex/plugin/skills/ultraresearch/agents/openai.yaml +2 -0
  137. package/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml +1 -1
  138. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
  139. package/packages/omo-codex/plugin/skills/ulw-plan/agents/openai.yaml +1 -1
  140. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  141. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -2
  142. package/packages/omo-codex/plugin/skills/visual-qa/agents/openai.yaml +2 -0
  143. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +70 -33
  144. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +6 -2
  145. package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +14 -1
  146. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +1 -1
  147. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +14 -6
  148. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +7 -3
  149. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -3
  150. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +24 -0
  151. package/packages/omo-codex/plugin/test/display-metadata.test.mjs +83 -0
  152. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +31 -19
  153. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +3 -3
  154. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
  155. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
  156. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +9 -4
  157. package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +88 -0
  158. package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +240 -0
  159. package/packages/omo-codex/plugin/test/ultraresearch-skill-contract.test.mjs +48 -0
  160. package/packages/omo-codex/scripts/install-dist/install-local.mjs +398 -206
  161. package/packages/omo-codex/scripts/install-hook-targets.test.mjs +93 -0
  162. package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
  163. package/packages/omo-codex/scripts/install-local-bun-global-update.test.mjs +63 -0
  164. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +109 -1
  165. package/packages/omo-codex/scripts/install-local.mjs +1 -0
  166. package/packages/shared-skills/skills/ultraresearch/SKILL.md +11 -2
  167. package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
  168. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  169. package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -2
  170. package/packages/omo-codex/plugin/hooks/hooks.json +0 -214
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ // team.mjs - the teammode controller CLI. Thin argv dispatch over team-state.mjs (the
3
+ // state model) and team-guide.mjs (the member field manual). It replaces hand-written
4
+ // team.json / guide.md so the model never has to author those files by hand.
5
+ //
6
+ // Zero external dependencies (node builtins only): runs identically under node and bun on
7
+ // macOS, Linux, and Windows.
8
+ //
9
+ // node "<skill-root>/scripts/team.mjs" init --name "<team>" --session-name "<sess>" [--session <id>] [--worktree] [--base-branch dev]
10
+ // node "<skill-root>/scripts/team.mjs" add-member --team <id> --id A --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>" [--branch <b>]
11
+ // node "<skill-root>/scripts/team.mjs" bind-thread --team <id> --id A --thread <thread-id> [--cwd <path>]
12
+ // node "<skill-root>/scripts/team.mjs" member-prompt --team <id> --id A
13
+ // node "<skill-root>/scripts/team.mjs" set-status --team <id> --id A --status reported|blocked|active|archived [--note "<...>"]
14
+ // node "<skill-root>/scripts/team.mjs" archive --team <id> [--id A] [--note "<...>"]
15
+ // node "<skill-root>/scripts/team.mjs" delete --team <id> [--force]
16
+ // node "<skill-root>/scripts/team.mjs" status --team <id>
17
+ // node "<skill-root>/scripts/team.mjs" guide --team <id>
18
+
19
+ import { randomUUID } from "node:crypto";
20
+ import { realpathSync } from "node:fs";
21
+ import { rm } from "node:fs/promises";
22
+ import { fileURLToPath, pathToFileURL } from "node:url";
23
+ import { buildGuide, buildMemberPrompt } from "./team-guide.mjs";
24
+ import {
25
+ addMember,
26
+ archive,
27
+ assertSafeTeamDir,
28
+ bindThread,
29
+ buildTeam,
30
+ ensureTeamDir,
31
+ isUnderstaffed,
32
+ MIN_MEMBERS,
33
+ readTeam,
34
+ resolveTeamDir,
35
+ setMemberStatus,
36
+ teamExists,
37
+ writeGuideAtomic,
38
+ writeTeamAtomic,
39
+ } from "./team-state.mjs";
40
+
41
+ function parseFlags(args) {
42
+ const flags = { _: [] };
43
+ for (let i = 0; i < args.length; i++) {
44
+ const arg = args[i];
45
+ if (!arg.startsWith("--")) {
46
+ flags._.push(arg);
47
+ continue;
48
+ }
49
+ const key = arg.slice(2);
50
+ const next = args[i + 1];
51
+ if (next === undefined || next.startsWith("--")) {
52
+ flags[key] = true;
53
+ } else {
54
+ flags[key] = next;
55
+ i++;
56
+ }
57
+ }
58
+ return flags;
59
+ }
60
+
61
+ function requireFlag(flags, name) {
62
+ const value = flags[name];
63
+ if (value === undefined || value === true) throw new Error(`missing required flag --${name}`);
64
+ return value;
65
+ }
66
+
67
+ async function loadTeam(cwd, sessionId) {
68
+ const dir = await assertSafeTeamDir(cwd, sessionId);
69
+ return { dir, team: await readTeam(dir) };
70
+ }
71
+
72
+ async function persist(team, dir) {
73
+ await writeTeamAtomic(team, dir);
74
+ await writeGuideAtomic(team, buildGuide(team), dir);
75
+ }
76
+
77
+ const handlers = {
78
+ async init(cwd, flags) {
79
+ const teamName = requireFlag(flags, "name");
80
+ const sessionName = requireFlag(flags, "session-name");
81
+ const sessionId = typeof flags.session === "string" ? flags.session : `team-${randomUUID().slice(0, 8)}`;
82
+ const dir = await ensureTeamDir(cwd, sessionId);
83
+ if (await teamExists(dir)) {
84
+ process.stdout.write(`exists: ${dir} (left untouched; re-init is a safe no-op)\n`);
85
+ return;
86
+ }
87
+ const team = buildTeam({
88
+ teamName,
89
+ sessionName,
90
+ sessionId,
91
+ dir,
92
+ worktreeEnabled: flags.worktree === true,
93
+ baseBranch: typeof flags["base-branch"] === "string" ? flags["base-branch"] : "dev",
94
+ });
95
+ await persist(team, dir);
96
+ process.stdout.write(`created: ${dir}\n`);
97
+ process.stdout.write(`team.json + guide.md written; artifacts/ ready. session id: ${sessionId}\n`);
98
+ process.stdout.write(`next: add-member --team ${sessionId} --id A --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>"\n`);
99
+ },
100
+
101
+ async "add-member"(cwd, flags) {
102
+ const sessionId = requireFlag(flags, "team");
103
+ const { dir, team } = await loadTeam(cwd, sessionId);
104
+ const memberId = requireFlag(flags, "id").trim();
105
+ addMember(team, {
106
+ id: memberId,
107
+ focus: requireFlag(flags, "focus"),
108
+ lens: requireFlag(flags, "lens"),
109
+ deliverable: typeof flags.deliverable === "string" ? flags.deliverable : "",
110
+ branch: typeof flags.branch === "string" ? flags.branch : null,
111
+ });
112
+ await persist(team, dir);
113
+ process.stdout.write(`added member ${memberId} to team ${sessionId}.\n\nSend this as the new thread's first message (title it "${team.threadTitleConvention}"):\n---\n${buildMemberPrompt(team, memberId)}\n---\n`);
114
+ },
115
+
116
+ async "bind-thread"(cwd, flags) {
117
+ const sessionId = requireFlag(flags, "team");
118
+ const { dir, team } = await loadTeam(cwd, sessionId);
119
+ bindThread(team, {
120
+ id: requireFlag(flags, "id"),
121
+ threadId: requireFlag(flags, "thread"),
122
+ cwd: typeof flags.cwd === "string" ? flags.cwd : null,
123
+ worktreePath: typeof flags["worktree-path"] === "string" ? flags["worktree-path"] : null,
124
+ });
125
+ await persist(team, dir);
126
+ process.stdout.write(`bound member ${flags.id} to thread ${flags.thread}.\n`);
127
+ },
128
+
129
+ async "member-prompt"(cwd, flags) {
130
+ const sessionId = requireFlag(flags, "team");
131
+ const { team } = await loadTeam(cwd, sessionId);
132
+ process.stdout.write(`${buildMemberPrompt(team, requireFlag(flags, "id"))}\n`);
133
+ },
134
+
135
+ async "set-status"(cwd, flags) {
136
+ const sessionId = requireFlag(flags, "team");
137
+ const { dir, team } = await loadTeam(cwd, sessionId);
138
+ setMemberStatus(team, {
139
+ id: requireFlag(flags, "id"),
140
+ status: requireFlag(flags, "status"),
141
+ note: typeof flags.note === "string" ? flags.note : "",
142
+ });
143
+ await persist(team, dir);
144
+ process.stdout.write(`member ${flags.id} -> ${flags.status}\n`);
145
+ },
146
+
147
+ async archive(cwd, flags) {
148
+ const sessionId = requireFlag(flags, "team");
149
+ const { dir, team } = await loadTeam(cwd, sessionId);
150
+ archive(team, {
151
+ id: typeof flags.id === "string" ? flags.id : null,
152
+ note: typeof flags.note === "string" ? flags.note : "",
153
+ });
154
+ await persist(team, dir);
155
+ process.stdout.write(flags.id ? `archived member ${flags.id}\n` : `archived team ${sessionId} and closed all members\n`);
156
+ },
157
+
158
+ async delete(cwd, flags) {
159
+ const sessionId = requireFlag(flags, "team");
160
+ const { dir, team } = await loadTeam(cwd, sessionId);
161
+ const active = team.members.filter((m) => m.status !== "archived");
162
+ if (flags.force !== true && (team.status !== "archived" || active.length > 0)) {
163
+ throw new Error(
164
+ `refused: team "${sessionId}" is not archived or still has ${active.length} active member(s). Archive first, or pass --force to delete anyway.`,
165
+ );
166
+ }
167
+ await rm(dir, { recursive: true, force: true });
168
+ process.stdout.write(`deleted team state: ${dir}\n`);
169
+ },
170
+
171
+ async status(cwd, flags) {
172
+ const sessionId = requireFlag(flags, "team");
173
+ const { team } = await loadTeam(cwd, sessionId);
174
+ process.stdout.write(`Team ${team.teamName} [${team.status}] - leader: main session - ${team.members.length} member(s)\n`);
175
+ for (const m of team.members) {
176
+ process.stdout.write(` ${m.id} (${m.lens}) ${m.focus} -> ${m.deliverable || "(no deliverable)"} [${m.status}]${m.threadId ? ` thread=${m.threadId}` : ""}${m.cwd ? ` cwd=${m.cwd}` : ""}\n`);
177
+ }
178
+ if (isUnderstaffed(team)) {
179
+ process.stdout.write(
180
+ `WARNING: a team needs at least ${MIN_MEMBERS} members; this team has ${team.members.length}. A single-member team is not a team - add another distinct slice or use a subagent.\n`,
181
+ );
182
+ }
183
+ },
184
+
185
+ async guide(cwd, flags) {
186
+ const sessionId = requireFlag(flags, "team");
187
+ const { dir, team } = await loadTeam(cwd, sessionId);
188
+ await writeGuideAtomic(team, buildGuide(team), dir);
189
+ process.stdout.write(`${team.paths.guide}\n`);
190
+ },
191
+ };
192
+
193
+ async function main() {
194
+ const [subcommand, ...rest] = process.argv.slice(2);
195
+ const handler = handlers[subcommand];
196
+ if (!handler) {
197
+ throw new Error(`unknown subcommand "${subcommand ?? ""}" - expected one of: ${Object.keys(handlers).join(", ")}`);
198
+ }
199
+ await handler(process.cwd(), parseFlags(rest));
200
+ }
201
+
202
+ // Symlink-robust main-module check: a symlinked install invokes this file through a symlink
203
+ // path while node resolves import.meta.url to the realpath, so compare the resolved real paths
204
+ // (falling back to the literal url comparison if a path cannot be resolved).
205
+ function isInvokedAsScript() {
206
+ const entry = process.argv[1];
207
+ if (!entry) return false;
208
+ try {
209
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
210
+ } catch {
211
+ return import.meta.url === pathToFileURL(entry).href;
212
+ }
213
+ }
214
+
215
+ if (isInvokedAsScript()) {
216
+ await main().catch((error) => {
217
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
218
+ process.exit(1);
219
+ });
220
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runTeammodeHookCli } from "./codex-hook.js";
4
+
5
+ const [command, subcommand] = process.argv.slice(2);
6
+
7
+ if (command === "hook" && subcommand === "post-tool-use") {
8
+ await runTeammodeHookCli(process.stdin, process.stdout);
9
+ } else {
10
+ process.stderr.write("Usage: omo-teammode hook post-tool-use\n");
11
+ process.exitCode = 2;
12
+ }
@@ -0,0 +1,125 @@
1
+ export interface PostToolUsePayload {
2
+ readonly hook_event_name: "PostToolUse";
3
+ readonly session_id: string;
4
+ readonly turn_id?: string;
5
+ readonly transcript_path?: string | null;
6
+ readonly cwd?: string;
7
+ readonly model?: string;
8
+ readonly permission_mode?: string;
9
+ readonly tool_name: string;
10
+ readonly tool_use_id?: string;
11
+ readonly tool_input: unknown;
12
+ readonly tool_response: unknown;
13
+ }
14
+
15
+ interface PostToolUseHookOutput {
16
+ readonly hookSpecificOutput: {
17
+ readonly hookEventName: "PostToolUse";
18
+ readonly additionalContext: string;
19
+ };
20
+ }
21
+
22
+ type ThreadCreationReference =
23
+ | { readonly kind: "thread"; readonly id: string }
24
+ | { readonly kind: "pendingWorktree"; readonly id: string };
25
+
26
+ const CREATE_THREAD_TOOL_NAMES = new Set(["create_thread", "codex_app.create_thread"]);
27
+
28
+ export function parsePostToolUsePayload(raw: string): PostToolUsePayload | null {
29
+ if (raw.trim().length === 0) return null;
30
+ try {
31
+ const parsed: unknown = JSON.parse(raw);
32
+ return isPostToolUsePayload(parsed) ? parsed : null;
33
+ } catch (error) {
34
+ if (error instanceof SyntaxError) return null;
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function runPostToolUseHook(payload: PostToolUsePayload): string {
40
+ if (payload.hook_event_name !== "PostToolUse") return "";
41
+ if (!CREATE_THREAD_TOOL_NAMES.has(payload.tool_name)) return "";
42
+ const threadReference = extractThreadCreationReference(payload.tool_response);
43
+ if (threadReference === null) return "";
44
+ const output: PostToolUseHookOutput = {
45
+ hookSpecificOutput: {
46
+ hookEventName: "PostToolUse",
47
+ additionalContext: threadTitleReminder(threadReference),
48
+ },
49
+ };
50
+ return `${JSON.stringify(output)}\n`;
51
+ }
52
+
53
+ export async function runTeammodeHookCli(
54
+ stdin: NodeJS.ReadableStream,
55
+ stdout: NodeJS.WritableStream,
56
+ ): Promise<void> {
57
+ const payload = parsePostToolUsePayload(await readAll(stdin));
58
+ if (payload === null) return;
59
+ const output = runPostToolUseHook(payload);
60
+ if (output.length > 0) stdout.write(output);
61
+ }
62
+
63
+ function threadTitleReminder(threadReference: ThreadCreationReference): string {
64
+ const id = formatIdentifier(threadReference.id);
65
+ return threadReference.kind === "thread"
66
+ ? `THREAD ID ${id}: CALL codex_app.set_thread_title NOW. USE THE REAL TASK/ROLE.`
67
+ : `PENDING WORKTREE ID ${id}: CALL codex_app.set_thread_title AS SOON AS THREAD ID EXISTS. USE THE REAL TASK/ROLE.`;
68
+ }
69
+
70
+ function formatIdentifier(value: string): string {
71
+ const normalized = value.replace(/\s+/g, " ").trim();
72
+ return normalized.length <= 200 ? normalized : `${normalized.slice(0, 197)}...`;
73
+ }
74
+
75
+ function extractThreadCreationReference(toolResponse: unknown): ThreadCreationReference | null {
76
+ if (!isRecord(toolResponse)) return null;
77
+ const threadId = toolResponse["threadId"];
78
+ if (typeof threadId === "string" && threadId.trim().length > 0) {
79
+ return { kind: "thread", id: threadId };
80
+ }
81
+ const pendingWorktreeId = toolResponse["pendingWorktreeId"];
82
+ if (typeof pendingWorktreeId === "string" && pendingWorktreeId.trim().length > 0) {
83
+ return { kind: "pendingWorktree", id: pendingWorktreeId };
84
+ }
85
+ return null;
86
+ }
87
+
88
+ function isPostToolUsePayload(value: unknown): value is PostToolUsePayload {
89
+ if (!isRecord(value)) return false;
90
+ return (
91
+ value["hook_event_name"] === "PostToolUse" &&
92
+ typeof value["session_id"] === "string" &&
93
+ typeof value["tool_name"] === "string" &&
94
+ Object.hasOwn(value, "tool_input") &&
95
+ Object.hasOwn(value, "tool_response") &&
96
+ optionalString(value["turn_id"]) &&
97
+ optionalString(value["cwd"]) &&
98
+ optionalString(value["model"]) &&
99
+ optionalString(value["permission_mode"]) &&
100
+ optionalString(value["tool_use_id"]) &&
101
+ (value["transcript_path"] === undefined ||
102
+ value["transcript_path"] === null ||
103
+ typeof value["transcript_path"] === "string")
104
+ );
105
+ }
106
+
107
+ function optionalString(value: unknown): boolean {
108
+ return value === undefined || typeof value === "string";
109
+ }
110
+
111
+ function isRecord(value: unknown): value is Record<string, unknown> {
112
+ return typeof value === "object" && value !== null && !Array.isArray(value);
113
+ }
114
+
115
+ function readAll(stdin: NodeJS.ReadableStream): Promise<string> {
116
+ return new Promise((resolve, reject) => {
117
+ let data = "";
118
+ stdin.setEncoding("utf8");
119
+ stdin.on("data", (chunk: unknown) => {
120
+ data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
121
+ });
122
+ stdin.once("error", reject);
123
+ stdin.once("end", () => resolve(data));
124
+ });
125
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { runPostToolUseHook } from "../src/codex-hook.js";
4
+
5
+ describe("thread title PostToolUse guidance", () => {
6
+ it("#given codex_app.create_thread completed #when the hook runs #then it asks Codex to immediately set a descriptive title", () => {
7
+ // given
8
+ const output = runPostToolUseHook({
9
+ hook_event_name: "PostToolUse",
10
+ session_id: "s-team",
11
+ turn_id: "t-team",
12
+ transcript_path: null,
13
+ cwd: "/repo",
14
+ model: "gpt-5.5",
15
+ permission_mode: "default",
16
+ tool_name: "create_thread",
17
+ tool_use_id: "tool-create-thread",
18
+ tool_input: {
19
+ prompt: "Investigate package install failures",
20
+ target: { type: "project", projectId: "/repo", environment: { type: "local" } },
21
+ },
22
+ tool_response: { threadId: "thread-123" },
23
+ });
24
+
25
+ // when
26
+ const parsed: unknown = JSON.parse(output);
27
+
28
+ // then
29
+ expect(isHookOutput(parsed)).toBe(true);
30
+ if (!isHookOutput(parsed)) return;
31
+ expect(parsed.hookSpecificOutput.hookEventName).toBe("PostToolUse");
32
+ expect(parsed.hookSpecificOutput.additionalContext).toBe(
33
+ "THREAD ID thread-123: CALL codex_app.set_thread_title NOW. USE THE REAL TASK/ROLE.",
34
+ );
35
+ });
36
+
37
+ it("#given an unrelated tool completed #when the hook runs #then it stays silent", () => {
38
+ // given
39
+ const output = runPostToolUseHook({
40
+ hook_event_name: "PostToolUse",
41
+ session_id: "s-team",
42
+ turn_id: "t-team",
43
+ transcript_path: null,
44
+ cwd: "/repo",
45
+ model: "gpt-5.5",
46
+ permission_mode: "default",
47
+ tool_name: "read_thread",
48
+ tool_use_id: "tool-read-thread",
49
+ tool_input: { threadId: "thread-123" },
50
+ tool_response: { status: "ok" },
51
+ });
52
+
53
+ // when
54
+ const actual = output;
55
+
56
+ // then
57
+ expect(actual).toBe("");
58
+ });
59
+
60
+ it("#given worktree-backed thread creation is pending #when the hook runs #then it tells Codex to title the thread once the thread id exists", () => {
61
+ // given
62
+ const output = runPostToolUseHook({
63
+ hook_event_name: "PostToolUse",
64
+ session_id: "s-team",
65
+ turn_id: "t-team",
66
+ transcript_path: null,
67
+ cwd: "/repo",
68
+ model: "gpt-5.5",
69
+ permission_mode: "default",
70
+ tool_name: "codex_app.create_thread",
71
+ tool_use_id: "tool-create-thread",
72
+ tool_input: {
73
+ prompt: "Fix CodeGraph provisioned launcher skip on Node 25",
74
+ target: {
75
+ type: "project",
76
+ projectId: "/repo",
77
+ environment: { type: "worktree", startingState: { type: "working-tree" } },
78
+ },
79
+ },
80
+ tool_response: {
81
+ pendingWorktreeId: "remote-control:env:test-worktree",
82
+ },
83
+ });
84
+
85
+ // when
86
+ const parsed: unknown = JSON.parse(output);
87
+
88
+ // then
89
+ expect(isHookOutput(parsed)).toBe(true);
90
+ if (!isHookOutput(parsed)) return;
91
+ expect(parsed.hookSpecificOutput.additionalContext).toBe(
92
+ "PENDING WORKTREE ID remote-control:env:test-worktree: CALL codex_app.set_thread_title AS SOON AS THREAD ID EXISTS. USE THE REAL TASK/ROLE.",
93
+ );
94
+ });
95
+ });
96
+
97
+ interface HookOutput {
98
+ readonly hookSpecificOutput: {
99
+ readonly hookEventName: "PostToolUse";
100
+ readonly additionalContext: string;
101
+ };
102
+ }
103
+
104
+ function isHookOutput(value: unknown): value is HookOutput {
105
+ if (!isRecord(value)) return false;
106
+ const hookSpecificOutput = value["hookSpecificOutput"];
107
+ return (
108
+ isRecord(hookSpecificOutput) &&
109
+ hookSpecificOutput["hookEventName"] === "PostToolUse" &&
110
+ typeof hookSpecificOutput["additionalContext"] === "string"
111
+ );
112
+ }
113
+
114
+ function isRecord(value: unknown): value is Record<string, unknown> {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
116
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "noPropertyAccessFromIndexSignature": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noImplicitOverride": true,
13
+ "noImplicitReturns": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "esModuleInterop": true,
18
+ "allowImportingTsExtensions": true,
19
+ "skipLibCheck": true,
20
+ "forceConsistentCasingInFileNames": true,
21
+ "types": ["node", "bun-types"],
22
+ "noEmit": true
23
+ },
24
+ "include": ["src/**/*", "test/**/*"]
25
+ }
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook session-start",
9
9
  "timeout": 5,
10
- "statusMessage": "LazyCodex(4.11.1): Recording Session Telemetry"
10
+ "statusMessage": "(OmO) Recording Session Telemetry"
11
11
  }
12
12
  ]
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-telemetry",
3
- "version": "4.11.1",
3
+ "version": "4.12.1",
4
4
  "description": "Codex plugin component that emits omo-codex anonymous daily-active telemetry on SessionStart.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -0,0 +1,158 @@
1
+ /// <reference path="../../node_modules/@types/node/index.d.ts" />
2
+
3
+ import { readdirSync, readFileSync } from "node:fs";
4
+
5
+ export type ComponentPackageJson = {
6
+ readonly version?: string;
7
+ readonly type: string;
8
+ readonly packageManager: string;
9
+ readonly bin: Readonly<Record<string, string>>;
10
+ readonly dependencies?: Readonly<Record<string, string>>;
11
+ readonly optionalDependencies?: Readonly<Record<string, string>>;
12
+ readonly scripts?: Readonly<Record<string, string>>;
13
+ readonly files?: readonly string[];
14
+ };
15
+
16
+ export type HookCommand = {
17
+ readonly command: string;
18
+ };
19
+
20
+ export type HookEntry = {
21
+ readonly matcher?: string;
22
+ readonly hooks: readonly HookCommand[];
23
+ };
24
+
25
+ export type HooksJson = {
26
+ readonly hooks: Record<string, readonly HookEntry[]>;
27
+ };
28
+
29
+ export type McpServer = {
30
+ readonly command: string;
31
+ readonly args: readonly string[];
32
+ };
33
+
34
+ export type McpJson = {
35
+ readonly mcpServers: Record<string, McpServer>;
36
+ };
37
+
38
+ export type PluginJson = {
39
+ readonly hooks: string;
40
+ };
41
+
42
+ export function readPackageJson(path: string): ComponentPackageJson {
43
+ const parsed = readJsonFile(path);
44
+ if (!isComponentPackageJson(parsed)) throw new TypeError(`Invalid package metadata: ${path}`);
45
+ return parsed;
46
+ }
47
+
48
+ export function readHooksJson(path: string): HooksJson {
49
+ const parsed = readJsonFile(path);
50
+ if (!isHooksJson(parsed)) throw new TypeError(`Invalid hooks metadata: ${path}`);
51
+ return parsed;
52
+ }
53
+
54
+ export function readMcpJson(path: string): McpJson {
55
+ const parsed = readJsonFile(path);
56
+ if (!isMcpJson(parsed)) throw new TypeError(`Invalid MCP metadata: ${path}`);
57
+ return parsed;
58
+ }
59
+
60
+ export function readPluginJson(path: string): PluginJson {
61
+ const parsed = readJsonFile(path);
62
+ if (!isPluginJson(parsed)) throw new TypeError(`Invalid plugin metadata: ${path}`);
63
+ return parsed;
64
+ }
65
+
66
+ export function readJsonFile(path: string): unknown {
67
+ return JSON.parse(readFileSync(path, "utf8"));
68
+ }
69
+
70
+ export function readTextFile(path: string): string {
71
+ return readFileSync(path, "utf8");
72
+ }
73
+
74
+ export function listDirectoryEntries(path: string): readonly string[] {
75
+ return readdirSync(path);
76
+ }
77
+
78
+ export function requireFiles(packageJson: ComponentPackageJson, path: string): readonly string[] {
79
+ if (packageJson.files === undefined) throw new TypeError(`Package metadata missing files: ${path}`);
80
+ return packageJson.files;
81
+ }
82
+
83
+ export function requireScripts(packageJson: ComponentPackageJson, path: string): Readonly<Record<string, string>> {
84
+ if (packageJson.scripts === undefined) throw new TypeError(`Package metadata missing scripts: ${path}`);
85
+ return packageJson.scripts;
86
+ }
87
+
88
+ export function collectHookCommandsFromValue(value: unknown): readonly string[] {
89
+ if (typeof value === "string") return [];
90
+ if (Array.isArray(value)) return value.flatMap(collectHookCommandsFromValue);
91
+ if (!isRecord(value)) return [];
92
+ const ownCommand = typeof value["command"] === "string" ? [value["command"]] : [];
93
+ return [...ownCommand, ...Object.values(value).flatMap(collectHookCommandsFromValue)];
94
+ }
95
+
96
+ function isComponentPackageJson(value: unknown): value is ComponentPackageJson {
97
+ if (!isRecord(value)) return false;
98
+ const dependencies = value["dependencies"];
99
+ const optionalDependencies = value["optionalDependencies"];
100
+ const scripts = value["scripts"];
101
+ const files = value["files"];
102
+ return (
103
+ value["type"] === "module" &&
104
+ value["packageManager"] === "npm@11.12.1" &&
105
+ isStringRecord(value["bin"]) &&
106
+ (dependencies === undefined || isStringRecord(dependencies)) &&
107
+ (optionalDependencies === undefined || isStringRecord(optionalDependencies)) &&
108
+ (scripts === undefined || isStringRecord(scripts)) &&
109
+ (files === undefined || isStringArray(files))
110
+ );
111
+ }
112
+
113
+ function isHooksJson(value: unknown): value is HooksJson {
114
+ if (!isRecord(value) || !isRecord(value["hooks"])) return false;
115
+ return Object.values(value["hooks"]).every(isHookEntries);
116
+ }
117
+
118
+ function isHookEntries(value: unknown): value is readonly HookEntry[] {
119
+ return Array.isArray(value) && value.every(isHookEntry);
120
+ }
121
+
122
+ function isHookEntry(value: unknown): value is HookEntry {
123
+ return isRecord(value) && Array.isArray(value["hooks"]) && value["hooks"].every(isHookCommand);
124
+ }
125
+
126
+ function isHookCommand(value: unknown): value is HookCommand {
127
+ return isRecord(value) && typeof value["command"] === "string";
128
+ }
129
+
130
+ function isMcpJson(value: unknown): value is McpJson {
131
+ if (!isRecord(value) || !isRecord(value["mcpServers"])) return false;
132
+ return Object.values(value["mcpServers"]).every(isMcpServer);
133
+ }
134
+
135
+ function isMcpServer(value: unknown): value is McpServer {
136
+ return (
137
+ isRecord(value) &&
138
+ typeof value["command"] === "string" &&
139
+ Array.isArray(value["args"]) &&
140
+ value["args"].every((item) => typeof item === "string")
141
+ );
142
+ }
143
+
144
+ function isPluginJson(value: unknown): value is PluginJson {
145
+ return isRecord(value) && typeof value["hooks"] === "string";
146
+ }
147
+
148
+ function isStringArray(value: unknown): value is readonly string[] {
149
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
150
+ }
151
+
152
+ function isStringRecord(value: unknown): value is Record<string, string> {
153
+ return isRecord(value) && Object.values(value).every((item) => typeof item === "string");
154
+ }
155
+
156
+ function isRecord(value: unknown): value is Record<string, unknown> {
157
+ return typeof value === "object" && value !== null && !Array.isArray(value);
158
+ }
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit",
9
9
  "timeout": 5,
10
- "statusMessage": "LazyCodex(4.11.1): Checking Ultrawork Trigger"
10
+ "statusMessage": "(OmO) Checking Ultrawork Trigger"
11
11
  }
12
12
  ]
13
13
  }