opencode-extended-sidebar 0.2.90

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 (105) hide show
  1. package/.oesignore +17 -0
  2. package/CHANGELOG.md +95 -0
  3. package/LICENSE +21 -0
  4. package/README.md +235 -0
  5. package/oes.json +11 -0
  6. package/package.json +100 -0
  7. package/scripts/install-git-hooks.mjs +29 -0
  8. package/src/pware.oc.core/constants/index.ts +18 -0
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
  19. package/src/pware.oc.core/git/index.ts +7 -0
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
  22. package/src/pware.oc.core/index.ts +18 -0
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
  34. package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
  35. package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
  36. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
  37. package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
  38. package/src/pware.oc.omo/constants/index.ts +15 -0
  39. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
  40. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
  47. package/src/pware.oc.omo/index.ts +7 -0
  48. package/src/pware.oc.omo/resolver/index.ts +19 -0
  49. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
  50. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
  62. package/src/pware.oc.opencode/constants/index.ts +10 -0
  63. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
  64. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
  67. package/src/pware.oc.opencode/index.ts +8 -0
  68. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
  69. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
  70. package/src/pware.oc.opencode/resolver/index.ts +170 -0
  71. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
  72. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
  76. package/src/pware.oc.perf/index.ts +14 -0
  77. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
  78. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
  79. package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
  80. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
  81. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
  82. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
  85. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
  86. package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
  87. package/src/pware.oc.runtime/index.ts +13 -0
  88. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
  89. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
  90. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
  91. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
  92. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
  93. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
  95. package/src/pware.oc.runtime/resolver/index.ts +224 -0
  96. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
  97. package/src/pware.oc.ui/index.ts +12 -0
  98. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
  99. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
  100. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
  101. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
  102. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
  103. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
  104. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
  105. package/src/pware.oc.ui.tsx +98 -0
@@ -0,0 +1,503 @@
1
+ /**
2
+ * pware.oc.core.omo.resolver.boulder
3
+ *
4
+ * Read-only boulder.json resolution: works, tasks, delegates, the active
5
+ * boulder mirror, and the `.omo`/`.sisyphus` layout probes (boulder path,
6
+ * watch dirs, presence marker, stamps).
7
+ */
8
+ import fs from "node:fs"
9
+ import path from "node:path"
10
+ import { canonicalizePath, fileStamp, readJson, resolveProjectFile } from "../../pware.oc.core/pware.oc.core.paths.js"
11
+ import { profile } from "../../pware.oc.core/pware.oc.core.debug.js"
12
+ import { composeMark, formatAge, pulseAgeMs, stripSessionPrefix, toEpochMs, type AgentMark } from "../../pware.oc.core/pware.oc.core.pulse.js"
13
+ import {
14
+ normalizeStatus,
15
+ taskRank,
16
+ toWorkLabel,
17
+ workIsTerminal,
18
+ workStatusGlyph,
19
+ } from "../../pware.oc.core/pware.oc.core.status.js"
20
+ import {
21
+ MARK_READY,
22
+ } from "../../pware.oc.core/constants/pware.oc.core.constants.pulse.js"
23
+ import {
24
+ STATUS_COMPLETED,
25
+ STATUS_ERROR,
26
+ STATUS_UNKNOWN,
27
+ } from "../../pware.oc.core/constants/pware.oc.core.constants.status.js"
28
+ import { BOULDER_STATUS_PENDING, BOULDER_STATUS_RUNNING } from "../constants/pware.oc.omo.constants.boulderStatus.js"
29
+ import {
30
+ WORK_STATE_COMPLETED,
31
+ WORK_STATE_NOT_COMPLETED,
32
+ type WorkState,
33
+ } from "../constants/pware.oc.omo.constants.planStatus.js"
34
+
35
+ export { workIsTerminal }
36
+ export { toWorkLabel as workStatusLabel }
37
+
38
+ export type PlanStep = { checked: boolean; text: string }
39
+
40
+ export type Delegate = {
41
+ taskKey: string
42
+ title: string
43
+ sessionId: string | null
44
+ agent: string | null
45
+ status: string
46
+ /** Boulder updated_at / started_at — fallback pulse when no session row. */
47
+ updatedAt: number | null
48
+ }
49
+
50
+ export type BoulderSession = {
51
+ id: string
52
+ /** `direct` opened the work, `appended` was pulled in later. */
53
+ origin: "direct" | "appended" | null
54
+ }
55
+
56
+ /** One `task_sessions` entry: a plan unit handed to a subagent. */
57
+ export type TaskView = {
58
+ taskKey: string
59
+ /** Short label from the plan (`todo:3`), when boulder recorded one. */
60
+ label: string | null
61
+ title: string
62
+ sessionId: string | null
63
+ agent: string | null
64
+ category: string | null
65
+ status: string
66
+ startedAt: number | null
67
+ endedAt: number | null
68
+ elapsedMs: number | null
69
+ updatedAt: number | null
70
+ }
71
+
72
+ /** One boulder run. Keyed by `work_id` — the same plan run twice is two works. */
73
+ export type WorkView = {
74
+ workId: string
75
+ name: string
76
+ status: string
77
+ sessionId: string | null
78
+ sessions: BoulderSession[]
79
+ agent: string | null
80
+ /** Project-relative plan markdown path, when boulder lists active_plan. */
81
+ planPath: string | null
82
+ startedAt: number | null
83
+ updatedAt: number | null
84
+ endedAt: number | null
85
+ elapsedMs: number | null
86
+ current: boolean
87
+ }
88
+
89
+ /** The active work, as mirrored on the boulder root. Blank when nothing is live. */
90
+ export type BoulderView = {
91
+ workId: string | null
92
+ name: string | null
93
+ status: string | null
94
+ agent: string | null
95
+ startedAt: number | null
96
+ updatedAt: number | null
97
+ endedAt: number | null
98
+ elapsedMs: number | null
99
+ sessions: BoulderSession[]
100
+ tasks: TaskView[]
101
+ counts: { running: number; done: number; other: number; total: number }
102
+ }
103
+
104
+ export type OmoSnapshot = {
105
+ present: boolean
106
+ boulderPath: string | null
107
+ planPath: string | null
108
+ status: string | null
109
+ agent: string | null
110
+ planName: string | null
111
+ plan: { total: number; completed: number; percent: number; steps: PlanStep[] }
112
+ works: WorkView[]
113
+ boulder: BoulderView
114
+ delegates: Delegate[]
115
+ stamp: string
116
+ }
117
+
118
+ type RawTask = {
119
+ task_key?: string
120
+ task_label?: string
121
+ task_title?: string
122
+ session_id?: string
123
+ agent?: string
124
+ category?: string
125
+ status?: string
126
+ updated_at?: string | number
127
+ started_at?: string | number
128
+ ended_at?: string | number
129
+ elapsed_ms?: number
130
+ }
131
+
132
+ function parseStamp(v: string | number | undefined): number | null {
133
+ return toEpochMs(v)
134
+ }
135
+
136
+ type RawWork = {
137
+ plan_name?: string
138
+ active_plan?: string
139
+ agent?: string
140
+ status?: string
141
+ updated_at?: string | number
142
+ started_at?: string | number
143
+ ended_at?: string | number
144
+ elapsed_ms?: number
145
+ session_ids?: string[]
146
+ session_origins?: Record<string, string>
147
+ task_sessions?: Record<string, RawTask>
148
+ }
149
+
150
+ type RawBoulder = RawWork & {
151
+ active_work_id?: string
152
+ works?: Record<string, RawWork>
153
+ }
154
+
155
+ /** The two OMO layout roots, newest-first: `.omo` and the legacy `.sisyphus`. */
156
+ export const OMO_DIRS = [".omo", ".sisyphus"] as const
157
+
158
+ /** An OMO layout root directory name. */
159
+ export type OmoDir = (typeof OMO_DIRS)[number]
160
+
161
+ /** First existing `<root>/<omoDir>/<...rel>` across the OMO dirs, or null. */
162
+ export function firstOmoPath(root: string, ...rel: string[]): string | null {
163
+ for (const dir of OMO_DIRS) {
164
+ const p = path.join(root, dir, ...rel)
165
+ if (fs.existsSync(p)) return p
166
+ }
167
+ return null
168
+ }
169
+
170
+ export function findBoulder(projectRoot: string): string | null {
171
+ return firstOmoPath(projectRoot, "boulder.json")
172
+ }
173
+
174
+ export function findOmoWatchDirs(projectRoot: string): string[] {
175
+ const out: string[] = []
176
+ for (const dir of OMO_DIRS) {
177
+ const p = path.join(projectRoot, dir)
178
+ if (fs.existsSync(p)) out.push(p)
179
+ }
180
+ return out
181
+ }
182
+
183
+ /** Installed omo means its config marker — `.omo/omo.jsonc` or `.sisyphus/omo.jsonc`. */
184
+ export function isOmoPresent(projectRoot: string): boolean {
185
+ return firstOmoPath(projectRoot, "omo.jsonc") !== null
186
+ }
187
+
188
+ /**
189
+ * Boulder + omo config marker. Drafts/notepads/evidence must not invalidate the
190
+ * hot path, but the marker (`.omo/omo.jsonc`) must — its arrival is what turns
191
+ * the OMO group from hidden to visible.
192
+ */
193
+ export function omoStamp(projectRoot?: string | null): string {
194
+ if (!projectRoot) return "0"
195
+ return profile("omo.stamp", () => {
196
+ const root = canonicalizePath(projectRoot)
197
+ const parts: string[] = []
198
+ const boulderPath = findBoulder(root)
199
+ if (boulderPath) parts.push(fileStamp(boulderPath))
200
+ for (const dir of OMO_DIRS) {
201
+ const p = path.join(root, dir, "omo.jsonc")
202
+ if (fs.existsSync(p)) parts.push(fileStamp(p))
203
+ }
204
+ return parts.join("|") || "0"
205
+ })
206
+ }
207
+
208
+ function lastSessionId(ids: string[] | undefined): string | null {
209
+ if (!ids?.length) return null
210
+ for (let i = ids.length - 1; i >= 0; i--) {
211
+ const id = stripSessionPrefix(ids[i])
212
+ if (id) return id
213
+ }
214
+ return null
215
+ }
216
+
217
+ function planLabel(name: string | null | undefined, activePlan: string | null | undefined): string {
218
+ const n = typeof name === "string" ? name.trim() : ""
219
+ if (n) return n
220
+ if (activePlan && typeof activePlan === "string") {
221
+ const base = path.basename(activePlan.replace(/\\/g, "/")).replace(/\.md$/i, "")
222
+ if (base) return base
223
+ }
224
+ return "plan"
225
+ }
226
+
227
+ /** One row's mark / glyph / age suffix for the OMO works list. */
228
+ export function workRowView(
229
+ work: { status: string; updatedAt: number | null },
230
+ now: number,
231
+ seen?: number | null,
232
+ ): { mark: AgentMark; glyph: string | null; suffix: string } {
233
+ const ageMs = pulseAgeMs(now, work.updatedAt, seen)
234
+ const mark = workIsTerminal(work.status)
235
+ ? toWorkLabel(work.status) === STATUS_ERROR
236
+ ? STATUS_ERROR
237
+ : MARK_READY
238
+ : composeMark({ lifecycle: work.status, ageMs })
239
+ return {
240
+ mark,
241
+ glyph: workStatusGlyph(work.status),
242
+ suffix: formatAge(ageMs),
243
+ }
244
+ }
245
+
246
+ function relativePlanPath(projectRoot: string, activePlan: string | null | undefined): string | null {
247
+ return resolveProjectFile(projectRoot, activePlan)?.rel ?? null
248
+ }
249
+
250
+ function collectSessions(
251
+ ids: string[] | undefined,
252
+ origins: Record<string, string> | undefined,
253
+ ): BoulderSession[] {
254
+ if (!Array.isArray(ids)) return []
255
+ const out: BoulderSession[] = []
256
+ const seen = new Set<string>()
257
+ for (const raw of ids) {
258
+ const id = stripSessionPrefix(raw)
259
+ if (!id || seen.has(id)) continue
260
+ seen.add(id)
261
+ // Boulder keys origins by the stored id, prefixed or not.
262
+ const hint = origins?.[String(raw)] ?? origins?.[id] ?? null
263
+ const origin = hint === "direct" || hint === "appended" ? hint : null
264
+ out.push({ id, origin })
265
+ }
266
+ return out.slice(0, 24)
267
+ }
268
+
269
+ function asWork(
270
+ workId: string,
271
+ w: RawWork,
272
+ current: boolean,
273
+ projectRoot: string,
274
+ fallbackPlan?: string | null,
275
+ ): WorkView {
276
+ const activePlan = w.active_plan || fallbackPlan || null
277
+ const sessions = collectSessions(w.session_ids, w.session_origins)
278
+ return {
279
+ workId,
280
+ name: planLabel(w.plan_name, activePlan),
281
+ status: (w.status || STATUS_UNKNOWN).toLowerCase(),
282
+ sessionId: lastSessionId(w.session_ids),
283
+ sessions,
284
+ agent: w.agent ?? null,
285
+ planPath: relativePlanPath(projectRoot, activePlan),
286
+ startedAt: parseStamp(w.started_at),
287
+ updatedAt: parseStamp(w.updated_at) ?? parseStamp(w.started_at),
288
+ endedAt: parseStamp(w.ended_at),
289
+ elapsedMs: typeof w.elapsed_ms === "number" && Number.isFinite(w.elapsed_ms) ? w.elapsed_ms : null,
290
+ current,
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Boulder runs, newest first, active pinned to the top. Keyed by `work_id`, so
296
+ * two runs of the same plan stay two rows. The root fields are a mirror of the
297
+ * active work — they only become a work of their own on a legacy state with no
298
+ * `works` map.
299
+ */
300
+ function collectWorks(raw: RawBoulder, projectRoot: string): WorkView[] {
301
+ const out: WorkView[] = []
302
+ const workId = raw.active_work_id ?? null
303
+ const entries =
304
+ raw.works && typeof raw.works === "object" && !Array.isArray(raw.works)
305
+ ? Object.entries(raw.works)
306
+ : []
307
+
308
+ for (const [id, w] of entries) {
309
+ if (!w || typeof w !== "object") continue
310
+ if (!(w.plan_name || w.active_plan || w.status)) continue
311
+ out.push(asWork(id, w, Boolean(workId && id === workId), projectRoot, raw.active_plan))
312
+ }
313
+
314
+ if (out.length === 0 && (raw.plan_name || raw.active_plan || raw.status)) {
315
+ out.push(asWork(workId || "active", raw, true, projectRoot))
316
+ }
317
+
318
+ out.sort((a, b) => {
319
+ if (a.current !== b.current) return a.current ? -1 : 1
320
+ return (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
321
+ })
322
+
323
+ // A dangling active_work_id leaves nobody current; boulder itself then mirrors
324
+ // the most recently updated work.
325
+ if (out.length > 0 && !out.some((w) => w.current)) out[0]!.current = true
326
+ return out.slice(0, 20)
327
+ }
328
+
329
+ function resolvePlanPath(projectRoot: string, activePlan: string | null): string | null {
330
+ if (!activePlan) return null
331
+ const planPath = path.isAbsolute(activePlan)
332
+ ? activePlan
333
+ : path.join(projectRoot, activePlan)
334
+ try {
335
+ const root = path.resolve(projectRoot)
336
+ const real = path.resolve(planPath)
337
+ if (!real.startsWith(root) || !fs.existsSync(real)) return null
338
+ return real
339
+ } catch {
340
+ return null
341
+ }
342
+ }
343
+
344
+ function collectTasks(raw: Record<string, RawTask> | undefined): TaskView[] {
345
+ if (!raw || typeof raw !== "object") return []
346
+ const list: TaskView[] = []
347
+ for (const [key, t] of Object.entries(raw)) {
348
+ if (!t || typeof t !== "object") continue
349
+ list.push({
350
+ taskKey: t.task_key || key,
351
+ label: t.task_label ?? null,
352
+ title: t.task_title || key,
353
+ sessionId: stripSessionPrefix(t.session_id),
354
+ agent: t.agent ?? null,
355
+ category: t.category ?? null,
356
+ status: (t.status || "").toLowerCase() || (t.started_at ? BOULDER_STATUS_RUNNING : BOULDER_STATUS_PENDING),
357
+ startedAt: parseStamp(t.started_at),
358
+ endedAt: parseStamp(t.ended_at),
359
+ elapsedMs: typeof t.elapsed_ms === "number" && Number.isFinite(t.elapsed_ms) ? t.elapsed_ms : null,
360
+ updatedAt: parseStamp(t.updated_at) ?? parseStamp(t.started_at),
361
+ })
362
+ }
363
+ list.sort((a, b) => taskRank(a.status) - taskRank(b.status) || a.title.localeCompare(b.title))
364
+ return list.slice(0, 40)
365
+ }
366
+
367
+ function taskToDelegate(t: TaskView): Delegate {
368
+ return {
369
+ taskKey: t.taskKey,
370
+ title: t.title,
371
+ sessionId: t.sessionId,
372
+ agent: t.agent,
373
+ status: t.status,
374
+ updatedAt: t.updatedAt,
375
+ }
376
+ }
377
+
378
+ function countTasks(tasks: readonly TaskView[]): BoulderView["counts"] {
379
+ let running = 0
380
+ let done = 0
381
+ for (const t of tasks) {
382
+ const r = taskRank(t.status)
383
+ if (r === 0) running += 1
384
+ else if (r === 3) done += 1
385
+ }
386
+ return { running, done, other: tasks.length - running - done, total: tasks.length }
387
+ }
388
+
389
+ export function emptyBoulder(): BoulderView {
390
+ return {
391
+ workId: null,
392
+ name: null,
393
+ status: null,
394
+ agent: null,
395
+ startedAt: null,
396
+ updatedAt: null,
397
+ endedAt: null,
398
+ elapsedMs: null,
399
+ sessions: [],
400
+ tasks: [],
401
+ counts: { running: 0, done: 0, other: 0, total: 0 },
402
+ }
403
+ }
404
+
405
+ /** The task boulder is on right now, if any. */
406
+ export function currentTask(boulder: BoulderView): TaskView | null {
407
+ return boulder.tasks.find((t) => taskRank(t.status) === 0) ?? null
408
+ }
409
+
410
+ export function emptyOmo(): OmoSnapshot {
411
+ return {
412
+ present: false,
413
+ boulderPath: null,
414
+ planPath: null,
415
+ status: null,
416
+ agent: null,
417
+ planName: null,
418
+ plan: { total: 0, completed: 0, percent: 0, steps: [] },
419
+ works: [],
420
+ boulder: emptyBoulder(),
421
+ delegates: [],
422
+ stamp: "0",
423
+ }
424
+ }
425
+
426
+ /** Read boulder + plan for a project directory. Never throws. */
427
+ export function readOmo(projectRoot: string | null | undefined): OmoSnapshot {
428
+ if (!projectRoot) return emptyOmo()
429
+ return profile("omo.read", () => readOmoInner(projectRoot))
430
+ }
431
+
432
+ /**
433
+ * Plan name → the latest boulder work's completion state, for the My-work
434
+ * reconciliation. Works are sorted newest-first, so the first entry per name
435
+ * wins — a completed plan re-run and still running stays `not-completed`.
436
+ */
437
+ export function planWorkStateByPlanName(
438
+ projectRoot: string | null | undefined,
439
+ ): Map<string, WorkState> {
440
+ const out = new Map<string, WorkState>()
441
+ if (!projectRoot) return out
442
+ for (const w of readOmo(projectRoot).works) {
443
+ if (!w.name || out.has(w.name)) continue
444
+ out.set(w.name, normalizeStatus(w.status) === STATUS_COMPLETED ? WORK_STATE_COMPLETED : WORK_STATE_NOT_COMPLETED)
445
+ }
446
+ return out
447
+ }
448
+
449
+ function readOmoInner(projectRoot: string): OmoSnapshot {
450
+ const root = canonicalizePath(projectRoot)
451
+ const boulderPath = findBoulder(root)
452
+ // Installed omo (config marker) with no active run: present, but nothing to show.
453
+ if (!boulderPath) {
454
+ if (isOmoPresent(root)) return { ...emptyOmo(), present: true }
455
+ return emptyOmo()
456
+ }
457
+
458
+ const raw = readJson(boulderPath) as RawBoulder | null
459
+ if (!raw) return { ...emptyOmo(), present: true, boulderPath, stamp: fileStamp(boulderPath) }
460
+
461
+ const works = collectWorks(raw, root)
462
+ const active = works.find((w) => w.current) ?? null
463
+ // Root fields mirror the active work; fall back to the work entry itself.
464
+ const mirror: RawWork = (active && raw.works?.[active.workId]) || raw
465
+
466
+ const planName = raw.plan_name || mirror.plan_name || null
467
+ const activePlan = raw.active_plan || mirror.active_plan || null
468
+ const agent = raw.agent || mirror.agent || null
469
+ const status = raw.status || mirror.status || null
470
+ const rootTasks = raw.task_sessions
471
+ const tasks = collectTasks(
472
+ rootTasks && Object.keys(rootTasks).length ? rootTasks : mirror.task_sessions,
473
+ )
474
+
475
+ const planPath = resolvePlanPath(root, activePlan)
476
+ // Plan checklist is not shown in the sidebar; Docs reads the file on demand.
477
+
478
+ return {
479
+ present: true,
480
+ boulderPath,
481
+ planPath,
482
+ status,
483
+ agent,
484
+ planName,
485
+ plan: { total: 0, completed: 0, percent: 0, steps: [] },
486
+ works,
487
+ boulder: {
488
+ workId: active?.workId ?? null,
489
+ name: active?.name ?? planName,
490
+ status: active?.status ?? status,
491
+ agent: active?.agent ?? agent,
492
+ startedAt: active?.startedAt ?? parseStamp(raw.started_at),
493
+ updatedAt: active?.updatedAt ?? parseStamp(raw.updated_at),
494
+ endedAt: active?.endedAt ?? parseStamp(raw.ended_at),
495
+ elapsedMs: active?.elapsedMs ?? null,
496
+ sessions: active?.sessions ?? collectSessions(raw.session_ids, raw.session_origins),
497
+ tasks,
498
+ counts: countTasks(tasks),
499
+ },
500
+ delegates: tasks.map(taskToDelegate),
501
+ stamp: fileStamp(boulderPath),
502
+ }
503
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * pware.oc.core.omo.resolver.config
3
+ *
4
+ * Oh-my-openagent config resolution — team mode + agent names from the user's
5
+ * `oh-my-openagent.json`. Best-effort, never throws.
6
+ */
7
+ import os from "node:os"
8
+ import path from "node:path"
9
+ import { profile } from "../../pware.oc.core/pware.oc.core.debug.js"
10
+ import { readJson } from "../../pware.oc.core/pware.oc.core.paths.js"
11
+
12
+ export type OmoConfigView = {
13
+ present: boolean
14
+ path: string | null
15
+ teamMode: boolean | null
16
+ agents: string[]
17
+ }
18
+
19
+ export function readOmoConfig(): OmoConfigView {
20
+ return profile("omo.config", () => {
21
+ const home = os.homedir()
22
+ const candidates = [
23
+ path.join(process.env.XDG_CONFIG_HOME || path.join(home, ".config"), "opencode", "oh-my-openagent.json"),
24
+ path.join(home, ".config", "opencode", "oh-my-openagent.json"),
25
+ ]
26
+ for (const p of candidates) {
27
+ const raw = readJson(p)
28
+ if (!raw) continue
29
+ const team = raw.team_mode as { enabled?: boolean } | undefined
30
+ const agents = raw.agents as Record<string, unknown> | undefined
31
+ return {
32
+ present: true,
33
+ path: p,
34
+ teamMode: team?.enabled ?? null,
35
+ agents: Object.keys(agents || {}),
36
+ }
37
+ }
38
+ return { present: false, path: null, teamMode: null, agents: [] }
39
+ })
40
+ }