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
@@ -1,14 +1,30 @@
1
- import { join } from "node:path";
2
- import type { BoulderWorkStatus, PlanChecklist } from "@oh-my-opencode/boulder-state";
3
- import {
4
- getBoulderFilePath,
5
- getPlanChecklist,
6
- getWorkForSession,
7
- normalizeSessionId,
8
- resolveBoulderPlanPathForWork,
9
- } from "@oh-my-opencode/boulder-state";
10
-
11
- export type { PlanChecklist };
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+
4
+ export type PlanChecklist = {
5
+ readonly completed: number;
6
+ readonly remaining: number;
7
+ readonly total: number;
8
+ readonly nextTaskLabel: string | null;
9
+ };
10
+
11
+ type BoulderWorkStatus = "active" | "paused" | "completed" | "abandoned";
12
+
13
+ type BoulderWork = {
14
+ readonly activePlan: string;
15
+ readonly planName: string;
16
+ readonly status?: BoulderWorkStatus;
17
+ readonly startedAt?: string;
18
+ readonly updatedAt?: string;
19
+ readonly sessionIds: readonly string[];
20
+ readonly worktreePath?: string;
21
+ };
22
+
23
+ type BoulderState = {
24
+ readonly works: readonly BoulderWork[];
25
+ readonly mirrorWork: BoulderWork | null;
26
+ readonly hasWorksMap: boolean;
27
+ };
12
28
 
13
29
  export type ContinuationState = {
14
30
  readonly planName: string;
@@ -19,22 +35,218 @@ export type ContinuationState = {
19
35
  readonly checklist: PlanChecklist;
20
36
  };
21
37
 
38
+ const TODO_HEADING = "TODOs";
39
+ const FINAL_VERIFICATION_HEADING = "Final Verification Wave";
40
+ const CHECKBOX_PREFIX_LENGTH = "- [ ] ".length;
41
+ const SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/;
42
+
22
43
  export function readContinuationState(cwd: string, sessionId: string): ContinuationState | null {
23
- const work = getWorkForSession(cwd, normalizeSessionId(sessionId, "codex"));
44
+ const boulderPath = getBoulderFilePath(cwd);
45
+ const boulderState = readBoulderState(boulderPath);
46
+ if (boulderState === null) return null;
47
+
48
+ const work = getWorkForSession(boulderState, normalizeSessionId(sessionId, "codex"));
24
49
  if (work === null || !isContinuableStatus(work.status)) return null;
50
+
25
51
  const planPath = resolveBoulderPlanPathForWork(cwd, work);
26
52
  const checklist = getPlanChecklist(planPath);
27
53
  if (checklist.remaining === 0) return null;
54
+
28
55
  return {
29
- planName: work.plan_name,
56
+ planName: work.planName,
30
57
  planPath,
31
- boulderPath: getBoulderFilePath(cwd),
58
+ boulderPath,
32
59
  ledgerPath: join(cwd, ".omo", "start-work", "ledger.jsonl"),
33
- worktreePath: work.worktree_path ?? null,
60
+ worktreePath: work.worktreePath ?? null,
34
61
  checklist,
35
62
  };
36
63
  }
37
64
 
65
+ export function getPlanChecklist(planPath: string): PlanChecklist {
66
+ if (!existsSync(planPath)) return emptyChecklist();
67
+
68
+ try {
69
+ return parsePlanChecklist(readFileSync(planPath, "utf8"));
70
+ } catch (error) {
71
+ if (error instanceof Error) return emptyChecklist();
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ function parsePlanChecklist(markdown: string): PlanChecklist {
77
+ const lines = markdown.split(/\r?\n/);
78
+ const hasCountedSections = lines.some((line) => isCountedHeading(parseLevelTwoHeading(line)));
79
+ let completed = 0;
80
+ let remaining = 0;
81
+ let nextTaskLabel: string | null = null;
82
+ let isCountedSection = !hasCountedSections;
83
+
84
+ for (const line of lines) {
85
+ const heading = parseLevelTwoHeading(line);
86
+ if (heading !== null) {
87
+ isCountedSection = isCountedHeading(heading);
88
+ continue;
89
+ }
90
+ if (!isCountedSection) continue;
91
+
92
+ const checkbox = parseTopLevelCheckbox(line);
93
+ if (checkbox === null) continue;
94
+
95
+ if (checkbox.checked) {
96
+ completed += 1;
97
+ } else {
98
+ remaining += 1;
99
+ nextTaskLabel = nextTaskLabel ?? checkbox.label;
100
+ }
101
+ }
102
+
103
+ return { completed, remaining, total: completed + remaining, nextTaskLabel };
104
+ }
105
+
106
+ function readBoulderState(path: string): BoulderState | null {
107
+ try {
108
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
109
+ return parseBoulderState(parsed);
110
+ } catch (error) {
111
+ if (error instanceof Error) return null;
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ function parseBoulderState(value: unknown): BoulderState | null {
117
+ if (!isRecord(value)) return null;
118
+
119
+ const works: BoulderWork[] = [];
120
+ const worksValue = value["works"];
121
+ const hasWorksMap = isRecord(worksValue);
122
+ if (hasWorksMap) {
123
+ for (const workValue of Object.values(worksValue)) {
124
+ const work = parseBoulderWork(workValue);
125
+ if (work !== null) works.push(work);
126
+ }
127
+ }
128
+
129
+ const mirrorWork = parseBoulderWork(value);
130
+ if (works.length === 0 && mirrorWork === null) return null;
131
+ return { works, mirrorWork, hasWorksMap };
132
+ }
133
+
134
+ function parseBoulderWork(value: unknown): BoulderWork | null {
135
+ if (!isRecord(value)) return null;
136
+
137
+ const activePlan = value["active_plan"];
138
+ const planName = value["plan_name"];
139
+ if (typeof activePlan !== "string") return null;
140
+
141
+ const status = parseBoulderWorkStatus(value["status"]);
142
+ const sessionIds = parseSessionIds(value["session_ids"]);
143
+ const worktreePath = value["worktree_path"];
144
+ const startedAt = value["started_at"];
145
+ const updatedAt = value["updated_at"];
146
+
147
+ return {
148
+ activePlan,
149
+ planName: typeof planName === "string" ? planName : activePlan,
150
+ sessionIds,
151
+ ...(status === undefined ? {} : { status }),
152
+ ...(typeof startedAt === "string" ? { startedAt } : {}),
153
+ ...(typeof updatedAt === "string" ? { updatedAt } : {}),
154
+ ...(typeof worktreePath === "string" ? { worktreePath } : {}),
155
+ };
156
+ }
157
+
158
+ function getWorkForSession(state: BoulderState, normalizedSessionId: string): BoulderWork | null {
159
+ let newestWork: BoulderWork | null = null;
160
+ let newestWorkMs = 0;
161
+
162
+ for (const work of state.works) {
163
+ if (!work.sessionIds.includes(normalizedSessionId)) continue;
164
+
165
+ const workMs = parseIsoToMs(work.updatedAt ?? work.startedAt) ?? 0;
166
+ if (newestWork === null || workMs > newestWorkMs) {
167
+ newestWork = work;
168
+ newestWorkMs = workMs;
169
+ }
170
+ }
171
+
172
+ if (newestWork !== null) return newestWork;
173
+ if (state.hasWorksMap) return null;
174
+ if (state.mirrorWork?.sessionIds.includes(normalizedSessionId) === true) return state.mirrorWork;
175
+ return null;
176
+ }
177
+
178
+ function resolveBoulderPlanPathForWork(cwd: string, work: BoulderWork): string {
179
+ const absolutePlanPath = resolveTrackedPath(cwd, work.activePlan);
180
+ const worktreePath = work.worktreePath?.trim();
181
+ if (worktreePath === undefined || worktreePath.length === 0) return absolutePlanPath;
182
+
183
+ const relativePlanPath = relative(resolve(cwd), absolutePlanPath);
184
+ if (relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) {
185
+ return absolutePlanPath;
186
+ }
187
+
188
+ const worktreePlanPath = resolve(resolveTrackedPath(cwd, worktreePath), relativePlanPath);
189
+ return existsSync(worktreePlanPath) ? worktreePlanPath : absolutePlanPath;
190
+ }
191
+
192
+ function resolveTrackedPath(baseDirectory: string, trackedPath: string): string {
193
+ return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath);
194
+ }
195
+
196
+ function parseTopLevelCheckbox(line: string): { readonly checked: boolean; readonly label: string } | null {
197
+ if (line.startsWith("- [ ] ")) return { checked: false, label: line.slice(CHECKBOX_PREFIX_LENGTH) };
198
+ if (line.startsWith("- [x] ") || line.startsWith("- [X] ")) {
199
+ return { checked: true, label: line.slice(CHECKBOX_PREFIX_LENGTH) };
200
+ }
201
+ return null;
202
+ }
203
+
204
+ function parseLevelTwoHeading(line: string): string | null {
205
+ if (!line.startsWith("## ")) return null;
206
+ return line.slice("## ".length).trim();
207
+ }
208
+
209
+ function isCountedHeading(heading: string | null): boolean {
210
+ return heading === TODO_HEADING || heading === FINAL_VERIFICATION_HEADING;
211
+ }
212
+
213
+ function parseBoulderWorkStatus(value: unknown): BoulderWorkStatus | undefined {
214
+ if (value === "active" || value === "paused" || value === "completed" || value === "abandoned") return value;
215
+ return undefined;
216
+ }
217
+
218
+ function parseSessionIds(value: unknown): readonly string[] {
219
+ if (!Array.isArray(value)) return [];
220
+ const sessionIds: string[] = [];
221
+ for (const item of value) {
222
+ if (typeof item === "string") sessionIds.push(normalizeSessionId(item));
223
+ }
224
+ return sessionIds;
225
+ }
226
+
227
+ function normalizeSessionId(sessionId: string, platform: "codex" | "opencode" = "opencode"): string {
228
+ if (SESSION_ID_PREFIX_PATTERN.test(sessionId)) return sessionId;
229
+ return `${platform}:${sessionId}`;
230
+ }
231
+
232
+ function parseIsoToMs(value: string | undefined): number | null {
233
+ if (value === undefined) return null;
234
+ const parsed = Date.parse(value);
235
+ return Number.isNaN(parsed) ? null : parsed;
236
+ }
237
+
38
238
  function isContinuableStatus(status: BoulderWorkStatus | undefined): boolean {
39
239
  return status === "active" || status === "paused";
40
240
  }
241
+
242
+ function getBoulderFilePath(cwd: string): string {
243
+ return join(cwd, ".omo", "boulder.json");
244
+ }
245
+
246
+ function emptyChecklist(): PlanChecklist {
247
+ return { completed: 0, remaining: 0, total: 0, nextTaskLabel: null };
248
+ }
249
+
250
+ function isRecord(value: unknown): value is Record<string, unknown> {
251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
252
+ }
@@ -1,9 +1,8 @@
1
1
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { getPlanChecklist } from "@oh-my-opencode/boulder-state";
5
4
  import { afterEach, describe, expect, it } from "vitest";
6
- import { readContinuationState } from "../src/boulder-reader.js";
5
+ import { getPlanChecklist, readContinuationState } from "../src/boulder-reader.js";
7
6
 
8
7
  const cleanupRoots: string[] = [];
9
8
 
@@ -109,6 +108,21 @@ describe("start-work boulder state reader", () => {
109
108
  expect(state).toBeNull();
110
109
  });
111
110
 
111
+ it("#given paused codex work with remaining checklist #when state is read #then continuation is present", () => {
112
+ // given
113
+ const workspace = createWorkspace({
114
+ boulderJson: createBoulderJson({ status: "paused", sessionIds: ["codex:sess_abc"] }),
115
+ planMarkdown: "# Plan\n\n## TODOs\n- [ ] First\n",
116
+ });
117
+
118
+ // when
119
+ const state = readContinuationState(workspace, "sess_abc");
120
+
121
+ // then
122
+ expect(state?.planName).toBe("launch-plan");
123
+ expect(state?.checklist).toEqual({ completed: 0, remaining: 1, total: 1, nextTaskLabel: "First" });
124
+ });
125
+
112
126
  it("#given no remaining top-level checklist items #when state is read #then continuation is absent", () => {
113
127
  // given
114
128
  const workspace = createWorkspace({
@@ -136,6 +150,52 @@ describe("start-work boulder state reader", () => {
136
150
  // then
137
151
  expect(state).toBeNull();
138
152
  });
153
+
154
+ it("#given bare boulder session id #when codex state is read #then continuation is absent", () => {
155
+ // given
156
+ const workspace = createWorkspace({
157
+ boulderJson: createBoulderJson({ status: "active", sessionIds: ["sess_abc"] }),
158
+ planMarkdown: "# Plan\n\n## TODOs\n- [ ] First\n",
159
+ });
160
+
161
+ // when
162
+ const state = readContinuationState(workspace, "sess_abc");
163
+
164
+ // then
165
+ expect(state).toBeNull();
166
+ });
167
+
168
+ it("#given works omit the codex session but stale mirror matches #when state is read #then continuation is absent", () => {
169
+ // given
170
+ const workspace = createWorkspace({
171
+ boulderJson: JSON.stringify({
172
+ schema_version: 2,
173
+ active_work_id: "work_1",
174
+ works: {
175
+ work_1: {
176
+ work_id: "work_1",
177
+ active_plan: ".omo/plans/plan.md",
178
+ plan_name: "current-work",
179
+ status: "active",
180
+ started_at: "2026-06-13T00:00:00.000Z",
181
+ session_ids: ["opencode:sess_other"],
182
+ },
183
+ },
184
+ active_plan: ".omo/plans/plan.md",
185
+ plan_name: "stale-mirror",
186
+ status: "active",
187
+ started_at: "2026-06-12T00:00:00.000Z",
188
+ session_ids: ["codex:sess_abc"],
189
+ }),
190
+ planMarkdown: "# Plan\n\n## TODOs\n- [ ] First\n",
191
+ });
192
+
193
+ // when
194
+ const state = readContinuationState(workspace, "sess_abc");
195
+
196
+ // then
197
+ expect(state).toBeNull();
198
+ });
139
199
  });
140
200
 
141
201
  type WorkspaceInput = {
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { execPath } from "node:process";
@@ -23,7 +23,11 @@ describe("start-work continuation CLI", () => {
23
23
  // then
24
24
  if (result.error !== undefined) throw result.error;
25
25
  expect(result.status).toBe(0);
26
- expect(result.stdout).toContain('"decision":"block"');
26
+ const output = parseStopHookOutput(result.stdout);
27
+ expect(output.decision).toBe("block");
28
+ expect(output.reason).toContain("Remaining top-level checkboxes: `2` of `3`");
29
+ expect(output.reason).toContain("Next incomplete task: `Task one`");
30
+ expect(output.reason).toContain("Your session id in boulder.json: `codex:s1`");
27
31
  });
28
32
 
29
33
  it("#given valid SubagentStop stdin #when CLI runs #then stdout contains block JSON", () => {
@@ -80,6 +84,18 @@ describe("start-work continuation CLI", () => {
80
84
  expect(result.status).toBe(0);
81
85
  expect(result.stdout).toBe("");
82
86
  });
87
+
88
+ it("#given built CLI bundle #when inspected #then it does not depend on the monorepo boulder package", () => {
89
+ // given
90
+ const cliPath = join(process.cwd(), "dist", "cli.js");
91
+
92
+ // when
93
+ const source = readFileSync(cliPath, "utf8");
94
+
95
+ // then
96
+ expect(source).not.toContain("@oh-my-opencode/boulder-state");
97
+ expect(source).not.toContain("../../boulder-state");
98
+ });
83
99
  });
84
100
 
85
101
  function runCli(subcommand: "stop" | "subagent-stop", input: string) {
@@ -90,7 +106,7 @@ function createWorkspace(sessionIds: readonly string[]): string {
90
106
  const root = mkdtempSync(join(tmpdir(), "codex-continuation-cli-"));
91
107
  cleanupRoots.push(root);
92
108
  mkdirSync(join(root, ".omo", "plans"), { recursive: true });
93
- writeFileSync(join(root, ".omo", "plans", "plan.md"), "## TODOs\n\n- [ ] Task one\n");
109
+ writeFileSync(join(root, ".omo", "plans", "plan.md"), "## TODOs\n\n- [ ] Task one\n- [x] Done\n- [ ] Task two\n");
94
110
  const work = {
95
111
  work_id: "w1",
96
112
  active_plan: ".omo/plans/plan.md",
@@ -122,3 +138,18 @@ function makePayload(
122
138
  last_assistant_message: "done",
123
139
  };
124
140
  }
141
+
142
+ type ParsedStopHookOutput = {
143
+ readonly decision: unknown;
144
+ readonly reason: string;
145
+ };
146
+
147
+ function parseStopHookOutput(stdout: string): ParsedStopHookOutput {
148
+ const parsed: unknown = JSON.parse(stdout);
149
+ if (!isRecord(parsed) || typeof parsed["reason"] !== "string") throw new Error("CLI did not emit Stop hook JSON");
150
+ return { decision: parsed["decision"], reason: parsed["reason"] };
151
+ }
152
+
153
+ function isRecord(value: unknown): value is Record<string, unknown> {
154
+ return typeof value === "object" && value !== null && !Array.isArray(value);
155
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ // components/teammode/src/codex-hook.ts
4
+ var CREATE_THREAD_TOOL_NAMES = new Set(["create_thread", "codex_app.create_thread"]);
5
+ function parsePostToolUsePayload(raw) {
6
+ if (raw.trim().length === 0)
7
+ return null;
8
+ try {
9
+ const parsed = JSON.parse(raw);
10
+ return isPostToolUsePayload(parsed) ? parsed : null;
11
+ } catch (error) {
12
+ if (error instanceof SyntaxError)
13
+ return null;
14
+ return null;
15
+ }
16
+ }
17
+ function runPostToolUseHook(payload) {
18
+ if (payload.hook_event_name !== "PostToolUse")
19
+ return "";
20
+ if (!CREATE_THREAD_TOOL_NAMES.has(payload.tool_name))
21
+ return "";
22
+ const threadReference = extractThreadCreationReference(payload.tool_response);
23
+ if (threadReference === null)
24
+ return "";
25
+ const output = {
26
+ hookSpecificOutput: {
27
+ hookEventName: "PostToolUse",
28
+ additionalContext: threadTitleReminder(threadReference)
29
+ }
30
+ };
31
+ return `${JSON.stringify(output)}
32
+ `;
33
+ }
34
+ async function runTeammodeHookCli(stdin, stdout) {
35
+ const payload = parsePostToolUsePayload(await readAll(stdin));
36
+ if (payload === null)
37
+ return;
38
+ const output = runPostToolUseHook(payload);
39
+ if (output.length > 0)
40
+ stdout.write(output);
41
+ }
42
+ function threadTitleReminder(threadReference) {
43
+ const id = formatIdentifier(threadReference.id);
44
+ return threadReference.kind === "thread" ? `THREAD ID ${id}: CALL codex_app.set_thread_title NOW. USE THE REAL TASK/ROLE.` : `PENDING WORKTREE ID ${id}: CALL codex_app.set_thread_title AS SOON AS THREAD ID EXISTS. USE THE REAL TASK/ROLE.`;
45
+ }
46
+ function formatIdentifier(value) {
47
+ const normalized = value.replace(/\s+/g, " ").trim();
48
+ return normalized.length <= 200 ? normalized : `${normalized.slice(0, 197)}...`;
49
+ }
50
+ function extractThreadCreationReference(toolResponse) {
51
+ if (!isRecord(toolResponse))
52
+ return null;
53
+ const threadId = toolResponse["threadId"];
54
+ if (typeof threadId === "string" && threadId.trim().length > 0) {
55
+ return { kind: "thread", id: threadId };
56
+ }
57
+ const pendingWorktreeId = toolResponse["pendingWorktreeId"];
58
+ if (typeof pendingWorktreeId === "string" && pendingWorktreeId.trim().length > 0) {
59
+ return { kind: "pendingWorktree", id: pendingWorktreeId };
60
+ }
61
+ return null;
62
+ }
63
+ function isPostToolUsePayload(value) {
64
+ if (!isRecord(value))
65
+ return false;
66
+ return value["hook_event_name"] === "PostToolUse" && typeof value["session_id"] === "string" && typeof value["tool_name"] === "string" && Object.hasOwn(value, "tool_input") && Object.hasOwn(value, "tool_response") && optionalString(value["turn_id"]) && optionalString(value["cwd"]) && optionalString(value["model"]) && optionalString(value["permission_mode"]) && optionalString(value["tool_use_id"]) && (value["transcript_path"] === undefined || value["transcript_path"] === null || typeof value["transcript_path"] === "string");
67
+ }
68
+ function optionalString(value) {
69
+ return value === undefined || typeof value === "string";
70
+ }
71
+ function isRecord(value) {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value);
73
+ }
74
+ function readAll(stdin) {
75
+ return new Promise((resolve, reject) => {
76
+ let data = "";
77
+ stdin.setEncoding("utf8");
78
+ stdin.on("data", (chunk) => {
79
+ data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
80
+ });
81
+ stdin.once("error", reject);
82
+ stdin.once("end", () => resolve(data));
83
+ });
84
+ }
85
+
86
+ // components/teammode/src/cli.ts
87
+ var [command, subcommand] = process.argv.slice(2);
88
+ if (command === "hook" && subcommand === "post-tool-use") {
89
+ await runTeammodeHookCli(process.stdin, process.stdout);
90
+ } else {
91
+ process.stderr.write(`Usage: omo-teammode hook post-tool-use
92
+ `);
93
+ process.exitCode = 2;
94
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "^(create_thread|codex_app\\.create_thread)$",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
+ "timeout": 10,
11
+ "statusMessage": "(OmO) Checking Thread Title Hygiene"
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ }
17
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@sisyphuslabs/codex-teammode",
3
+ "version": "4.12.1",
4
+ "description": "Codex team-mode hook component that keeps background thread titles descriptive after create_thread.",
5
+ "type": "module",
6
+ "private": true,
7
+ "files": [
8
+ "dist",
9
+ "hooks",
10
+ "skills"
11
+ ],
12
+ "scripts": {
13
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && bun build src/cli.ts --target node --format esm --outfile dist/cli.js",
14
+ "test": "bun test test/*.test.ts",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^25.9.3",
19
+ "bun-types": "^1.3.1",
20
+ "typescript": "^6.0.3",
21
+ "vitest": "^4.1.8"
22
+ },
23
+ "engines": {
24
+ "node": ">=20.0.0"
25
+ }
26
+ }