opencode-extended-sidebar 0.2.90 → 0.2.91

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 (108) hide show
  1. package/.oesignore +17 -17
  2. package/CHANGELOG.md +96 -95
  3. package/LICENSE +21 -21
  4. package/README.md +150 -235
  5. package/oes.json +13 -11
  6. package/package.json +100 -100
  7. package/scripts/install-git-hooks.mjs +29 -29
  8. package/src/pware.oc.core/constants/index.ts +18 -18
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
  19. package/src/pware.oc.core/git/index.ts +7 -7
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
  22. package/src/pware.oc.core/index.ts +19 -18
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
  34. package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
  35. package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
  36. package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
  37. package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
  38. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
  39. package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
  40. package/src/pware.oc.omo/constants/index.ts +15 -15
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
  47. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
  48. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
  49. package/src/pware.oc.omo/index.ts +7 -7
  50. package/src/pware.oc.omo/resolver/index.ts +19 -19
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
  62. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
  63. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
  64. package/src/pware.oc.opencode/constants/index.ts +10 -10
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
  67. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
  68. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
  69. package/src/pware.oc.opencode/index.ts +8 -8
  70. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
  71. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
  72. package/src/pware.oc.opencode/resolver/index.ts +223 -170
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
  76. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
  77. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
  78. package/src/pware.oc.perf/index.ts +14 -14
  79. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
  80. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
  81. package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
  82. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
  85. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
  86. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
  87. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
  88. package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
  89. package/src/pware.oc.runtime/index.ts +13 -13
  90. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
  91. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
  92. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
  93. package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
  95. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
  96. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
  97. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
  98. package/src/pware.oc.runtime/resolver/index.ts +268 -224
  99. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
  100. package/src/pware.oc.ui/index.ts +12 -12
  101. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
  102. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
  103. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
  104. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
  105. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
  106. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
  107. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
  108. package/src/pware.oc.ui.tsx +98 -98
@@ -1,18 +1,18 @@
1
- /**
2
- * pware.oc.omo.resolver.notepadsFile
3
- *
4
- * Notepad-file → writer-session index. Thin re-export of the shared omo file
5
- * engine (`planFile.ts`), pinned to the `.omo/notepads/` document kind.
6
- */
7
- import { DOC_KIND_NOTEPAD } from "../constants/pware.oc.omo.constants.docKind.js"
8
- import {
9
- makeOmoFileList,
10
- makeOmoFileResolver,
11
- } from "./pware.oc.omo.resolver.planFile.js"
12
-
13
- /** Bidirectional notepad-file index for one project (see `omoFileIndex`). */
14
- export const { sessionIndex: notepadsSessionIndex, sessionForFile: sessionForNotepadFile } =
15
- makeOmoFileResolver("notepad")
16
-
17
- /** Notepad files under `.omo/notepads/`, optionally filtered by writer session. */
18
- export const NotepadFile = makeOmoFileList(DOC_KIND_NOTEPAD)
1
+ /**
2
+ * pware.oc.omo.resolver.notepadsFile
3
+ *
4
+ * Notepad-file → writer-session index. Thin re-export of the shared omo file
5
+ * engine (`planFile.ts`), pinned to the `.omo/notepads/` document kind.
6
+ */
7
+ import { DOC_KIND_NOTEPAD } from "../constants/pware.oc.omo.constants.docKind.js"
8
+ import {
9
+ makeOmoFileList,
10
+ makeOmoFileResolver,
11
+ } from "./pware.oc.omo.resolver.planFile.js"
12
+
13
+ /** Bidirectional notepad-file index for one project (see `omoFileIndex`). */
14
+ export const { sessionIndex: notepadsSessionIndex, sessionForFile: sessionForNotepadFile } =
15
+ makeOmoFileResolver("notepad")
16
+
17
+ /** Notepad files under `.omo/notepads/`, optionally filtered by writer session. */
18
+ export const NotepadFile = makeOmoFileList(DOC_KIND_NOTEPAD)
@@ -1,155 +1,155 @@
1
- /**
2
- * pware.oc.core.omo.resolver.plan
3
- *
4
- * Plan.md parsing: status / slug / pending-action from YAML frontmatter or the
5
- * `## State` section, plus the approval row label. Pure text — no fs, no scan.
6
- */
7
- import path from "node:path"
8
- import type { WorkState } from "../constants/pware.oc.omo.constants.planStatus.js"
9
-
10
- export type ApprovalItem = {
11
- /** Project-relative path. The panel must never show a root. */
12
- rel: string
13
- name: string
14
- /** Frontmatter `status:`; null when the draft has none (a bare working note). */
15
- status: string | null
16
- /** What happens after approval — e.g. `write .omo/plans/<slug>.md`. */
17
- pendingAction: string | null
18
- updatedAt: number | null
19
- /** ulw-plan review lifecycle from the draft frontmatter; null = no review requested. */
20
- review: ReviewState | null
21
- /** Boulder work completion state, for the My-work reconciliation. */
22
- workState: WorkState
23
- }
24
-
25
- /** One ulw-plan review lane (momus / independent). */
26
- export type ReviewLane = {
27
- /** Lane lifecycle: pending | launching | in_flight | approved | changes_requested | inconclusive. */
28
- status: string | null
29
- /** Lane verdict, when the lane reported one (pass / fail / approved / blocked / …). */
30
- result: string | null
31
- }
32
-
33
- /**
34
- * The ulw-plan review state persisted in the draft/plan frontmatter: one live
35
- * round keyed by `review_round_id` + `plan_sha256` (any plan change bumps the
36
- * hash and starts a fresh round), with a `momus` and an `independent` lane.
37
- */
38
- export type ReviewState = {
39
- required: boolean
40
- /** `review_round_id` — the live round; a changed `plan_sha256` forces a new one. */
41
- roundId: string | null
42
- /** `round_status` — `active` while a round is live. */
43
- roundStatus: string | null
44
- /** `plan_sha256` — the reviewed plan's hash, null until a round is initialized. */
45
- planSha256: string | null
46
- lanes: { momus: ReviewLane; independent: ReviewLane }
47
- }
48
-
49
- /** Row label for an approval item — basename without the `.md` extension. */
50
- export function approvalName(rel: string): string {
51
- return path.basename(rel).replace(/\.md$/i, "")
52
- }
53
-
54
- function frontmatter(text: string): string | null {
55
- const m = text.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/)
56
- return m ? (m[1] ?? "") : null
57
- }
58
-
59
- function stateSection(text: string): string | null {
60
- const m = text.match(/##\s+State\s*\r?\n([\s\S]*?)(?=\r?\n##\s|$)/)
61
- return m ? (m[1] ?? "") : null
62
- }
63
-
64
- export function parsePlanStatus(text: string): string | null {
65
- const fm = frontmatter(text)
66
- if (fm) {
67
- const m = fm.match(/^status:\s*["']?([^"'\r\n]+)["']?/m)
68
- if (m?.[1]) return m[1].trim()
69
- }
70
- const state = stateSection(text)
71
- if (state) {
72
- const m = state.match(/status:\s*[`'"]?\s*([^\s`'"\r\n]+)/m)
73
- if (m?.[1]) return m[1].trim()
74
- }
75
- return null
76
- }
77
-
78
- function parsePlanSlug(text: string): string | null {
79
- const fm = frontmatter(text)
80
- if (fm) {
81
- const m = fm.match(/^slug:\s*["']?([^"'\r\n]+)["']?/m)
82
- if (m?.[1]) return m[1].trim()
83
- }
84
- const state = stateSection(text)
85
- if (state) {
86
- const m = state.match(/slug:\s*[`'"]?\s*([^\s`'"\r\n]+)/m)
87
- if (m?.[1]) return m[1].trim()
88
- }
89
- return null
90
- }
91
-
92
- export function parsePlanPendingAction(text: string): string | null {
93
- const fm = frontmatter(text)
94
- if (fm) {
95
- const m = fm.match(/^pending-action:\s*["']?([^"'\r\n]+)["']?/m)
96
- if (m?.[1]) return m[1].trim()
97
- }
98
- const slug = parsePlanSlug(text)
99
- return slug ? `.omo/plans/${slug}.md` : null
100
- }
101
-
102
- function scalar(fm: string, key: string): string | null {
103
- const v = fm.match(new RegExp(`^${key}:\\s*(\\S+)`, "m"))?.[1]
104
- if (!v || v === "null") return null
105
- return v.trim()
106
- }
107
-
108
- function stripQuotes(v: string): string {
109
- return v.replace(/^["']|["']$/g, "")
110
- }
111
-
112
- /**
113
- * One review lane (`momus:` / `independent:`) from the frontmatter lines:
114
- * the lane's own `status` and `result`, stopping at the next top-level key.
115
- * Line-based so the last frontmatter line needs no trailing newline.
116
- */
117
- function laneState(fm: string, lane: "momus" | "independent"): ReviewLane | null {
118
- const lines = fm.split(/\r?\n/)
119
- const idx = lines.findIndex((l) => l.trim() === `${lane}:`)
120
- if (idx < 0) return null
121
- const laneIndent = lines[idx]!.match(/^\s*/)?.[0].length ?? 0
122
- const out: ReviewLane = { status: null, result: null }
123
- for (const line of lines.slice(idx + 1)) {
124
- if ((line.match(/^\s*/)?.[0].length ?? 0) <= laneIndent) break
125
- const m = line.match(/^\s*(\w[\w-]*):\s*(\S+)?/)
126
- if (!m) continue
127
- const val = m[2] && m[2] !== "null" ? stripQuotes(m[2]) : null
128
- if (m[1] === "status") out.status = val
129
- else if (m[1] === "result") out.result = val
130
- }
131
- return out
132
- }
133
-
134
- /**
135
- * ulw-plan review lifecycle from the frontmatter `review:` block and its
136
- * siblings (`review_required`, `review_round_id`, `round_status`,
137
- * `plan_sha256`). Null when the draft carries no review fields at all.
138
- */
139
- export function parseReviewBlock(text: string): ReviewState | null {
140
- const fm = frontmatter(text)
141
- if (!fm) return null
142
- const required = /^review_required:\s*true/m.test(fm)
143
- const hasBlock = /^review:\s*$/m.test(fm)
144
- if (!required && !hasBlock) return null
145
- return {
146
- required,
147
- roundId: scalar(fm, "review_round_id"),
148
- roundStatus: scalar(fm, "round_status"),
149
- planSha256: scalar(fm, "plan_sha256"),
150
- lanes: {
151
- momus: laneState(fm, "momus") ?? { status: null, result: null },
152
- independent: laneState(fm, "independent") ?? { status: null, result: null },
153
- },
154
- }
155
- }
1
+ /**
2
+ * pware.oc.core.omo.resolver.plan
3
+ *
4
+ * Plan.md parsing: status / slug / pending-action from YAML frontmatter or the
5
+ * `## State` section, plus the approval row label. Pure text — no fs, no scan.
6
+ */
7
+ import path from "node:path"
8
+ import type { WorkState } from "../constants/pware.oc.omo.constants.planStatus.js"
9
+
10
+ export type ApprovalItem = {
11
+ /** Project-relative path. The panel must never show a root. */
12
+ rel: string
13
+ name: string
14
+ /** Frontmatter `status:`; null when the draft has none (a bare working note). */
15
+ status: string | null
16
+ /** What happens after approval — e.g. `write .omo/plans/<slug>.md`. */
17
+ pendingAction: string | null
18
+ updatedAt: number | null
19
+ /** ulw-plan review lifecycle from the draft frontmatter; null = no review requested. */
20
+ review: ReviewState | null
21
+ /** Boulder work completion state, for the My-work reconciliation. */
22
+ workState: WorkState
23
+ }
24
+
25
+ /** One ulw-plan review lane (momus / independent). */
26
+ export type ReviewLane = {
27
+ /** Lane lifecycle: pending | launching | in_flight | approved | changes_requested | inconclusive. */
28
+ status: string | null
29
+ /** Lane verdict, when the lane reported one (pass / fail / approved / blocked / …). */
30
+ result: string | null
31
+ }
32
+
33
+ /**
34
+ * The ulw-plan review state persisted in the draft/plan frontmatter: one live
35
+ * round keyed by `review_round_id` + `plan_sha256` (any plan change bumps the
36
+ * hash and starts a fresh round), with a `momus` and an `independent` lane.
37
+ */
38
+ export type ReviewState = {
39
+ required: boolean
40
+ /** `review_round_id` — the live round; a changed `plan_sha256` forces a new one. */
41
+ roundId: string | null
42
+ /** `round_status` — `active` while a round is live. */
43
+ roundStatus: string | null
44
+ /** `plan_sha256` — the reviewed plan's hash, null until a round is initialized. */
45
+ planSha256: string | null
46
+ lanes: { momus: ReviewLane; independent: ReviewLane }
47
+ }
48
+
49
+ /** Row label for an approval item — basename without the `.md` extension. */
50
+ export function approvalName(rel: string): string {
51
+ return path.basename(rel).replace(/\.md$/i, "")
52
+ }
53
+
54
+ function frontmatter(text: string): string | null {
55
+ const m = text.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/)
56
+ return m ? (m[1] ?? "") : null
57
+ }
58
+
59
+ function stateSection(text: string): string | null {
60
+ const m = text.match(/##\s+State\s*\r?\n([\s\S]*?)(?=\r?\n##\s|$)/)
61
+ return m ? (m[1] ?? "") : null
62
+ }
63
+
64
+ export function parsePlanStatus(text: string): string | null {
65
+ const fm = frontmatter(text)
66
+ if (fm) {
67
+ const m = fm.match(/^status:\s*["']?([^"'\r\n]+)["']?/m)
68
+ if (m?.[1]) return m[1].trim()
69
+ }
70
+ const state = stateSection(text)
71
+ if (state) {
72
+ const m = state.match(/status:\s*[`'"]?\s*([^\s`'"\r\n]+)/m)
73
+ if (m?.[1]) return m[1].trim()
74
+ }
75
+ return null
76
+ }
77
+
78
+ function parsePlanSlug(text: string): string | null {
79
+ const fm = frontmatter(text)
80
+ if (fm) {
81
+ const m = fm.match(/^slug:\s*["']?([^"'\r\n]+)["']?/m)
82
+ if (m?.[1]) return m[1].trim()
83
+ }
84
+ const state = stateSection(text)
85
+ if (state) {
86
+ const m = state.match(/slug:\s*[`'"]?\s*([^\s`'"\r\n]+)/m)
87
+ if (m?.[1]) return m[1].trim()
88
+ }
89
+ return null
90
+ }
91
+
92
+ export function parsePlanPendingAction(text: string): string | null {
93
+ const fm = frontmatter(text)
94
+ if (fm) {
95
+ const m = fm.match(/^pending-action:\s*["']?([^"'\r\n]+)["']?/m)
96
+ if (m?.[1]) return m[1].trim()
97
+ }
98
+ const slug = parsePlanSlug(text)
99
+ return slug ? `.omo/plans/${slug}.md` : null
100
+ }
101
+
102
+ function scalar(fm: string, key: string): string | null {
103
+ const v = fm.match(new RegExp(`^${key}:\\s*(\\S+)`, "m"))?.[1]
104
+ if (!v || v === "null") return null
105
+ return v.trim()
106
+ }
107
+
108
+ function stripQuotes(v: string): string {
109
+ return v.replace(/^["']|["']$/g, "")
110
+ }
111
+
112
+ /**
113
+ * One review lane (`momus:` / `independent:`) from the frontmatter lines:
114
+ * the lane's own `status` and `result`, stopping at the next top-level key.
115
+ * Line-based so the last frontmatter line needs no trailing newline.
116
+ */
117
+ function laneState(fm: string, lane: "momus" | "independent"): ReviewLane | null {
118
+ const lines = fm.split(/\r?\n/)
119
+ const idx = lines.findIndex((l) => l.trim() === `${lane}:`)
120
+ if (idx < 0) return null
121
+ const laneIndent = lines[idx]!.match(/^\s*/)?.[0].length ?? 0
122
+ const out: ReviewLane = { status: null, result: null }
123
+ for (const line of lines.slice(idx + 1)) {
124
+ if ((line.match(/^\s*/)?.[0].length ?? 0) <= laneIndent) break
125
+ const m = line.match(/^\s*(\w[\w-]*):\s*(\S+)?/)
126
+ if (!m) continue
127
+ const val = m[2] && m[2] !== "null" ? stripQuotes(m[2]) : null
128
+ if (m[1] === "status") out.status = val
129
+ else if (m[1] === "result") out.result = val
130
+ }
131
+ return out
132
+ }
133
+
134
+ /**
135
+ * ulw-plan review lifecycle from the frontmatter `review:` block and its
136
+ * siblings (`review_required`, `review_round_id`, `round_status`,
137
+ * `plan_sha256`). Null when the draft carries no review fields at all.
138
+ */
139
+ export function parseReviewBlock(text: string): ReviewState | null {
140
+ const fm = frontmatter(text)
141
+ if (!fm) return null
142
+ const required = /^review_required:\s*true/m.test(fm)
143
+ const hasBlock = /^review:\s*$/m.test(fm)
144
+ if (!required && !hasBlock) return null
145
+ return {
146
+ required,
147
+ roundId: scalar(fm, "review_round_id"),
148
+ roundStatus: scalar(fm, "round_status"),
149
+ planSha256: scalar(fm, "plan_sha256"),
150
+ lanes: {
151
+ momus: laneState(fm, "momus") ?? { status: null, result: null },
152
+ independent: laneState(fm, "independent") ?? { status: null, result: null },
153
+ },
154
+ }
155
+ }