lazycodex-ai 5.0.0-beta.89 → 5.0.0-beta.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 (159) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +1 -1
  5. package/README.zh-cn.md +1 -1
  6. package/dist/cli/config-manager/refresh-opencode-plugin-sandbox.d.ts +31 -0
  7. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +11 -0
  8. package/dist/cli/index.js +1661 -859
  9. package/dist/cli/install-native/index.d.ts +9 -1
  10. package/dist/cli/install-native/legacy-omo-bin.d.ts +27 -0
  11. package/dist/cli/install-native/offer-native-setup.d.ts +31 -0
  12. package/dist/cli/install-native/omo-bin-test-fixtures.d.ts +35 -0
  13. package/dist/cli/install-native/plan.d.ts +8 -0
  14. package/dist/cli/install-native/repair-legacy-omo-bin.d.ts +26 -0
  15. package/dist/cli/install-native/run-native-install.d.ts +11 -1
  16. package/dist/cli/install-native/verify-omo-command.d.ts +22 -0
  17. package/dist/cli/native-edition-hint.d.ts +2 -2
  18. package/dist/cli-node/index.js +1661 -859
  19. package/package.json +1 -1
  20. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  21. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  22. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  24. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  26. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  28. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  30. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  31. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -1
  33. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +5 -1
  34. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  35. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  36. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  37. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  39. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +7 -3
  41. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  44. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  45. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  46. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +7 -3
  48. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  49. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  71. package/packages/omo-codex/plugin/package-lock.json +12 -12
  72. package/packages/omo-codex/plugin/package.json +1 -1
  73. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
  74. package/packages/omo-codex/plugin/skills/browser/SKILL.md +8 -1
  75. package/packages/omo-codex/plugin/skills/browser/references/install.md +34 -5
  76. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +2 -2
  77. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
  78. package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +14 -3
  79. package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +13 -1
  80. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +7 -3
  81. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +63 -0
  82. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -25
  83. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
  84. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +66 -0
  85. package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
  86. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
  87. package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
  88. package/packages/omo-codex/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
  89. package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
  90. package/packages/omo-codex/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
  91. package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
  92. package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
  93. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
  94. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
  95. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
  96. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
  97. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
  98. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
  99. package/packages/omo-codex/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
  100. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
  101. package/packages/omo-codex/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
  102. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
  103. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
  104. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +3 -1
  105. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +11 -0
  106. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
  107. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +20 -0
  108. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  109. package/packages/prompts-core/prompts/ultrawork/codex.md +7 -3
  110. package/packages/shared-skills/skills/browser/SKILL.md +8 -1
  111. package/packages/shared-skills/skills/browser/references/install.md +34 -5
  112. package/packages/shared-skills/skills/browser/runtime/omowright/index.js +2 -2
  113. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
  114. package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +14 -3
  115. package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +13 -1
  116. package/packages/shared-skills/skills/ulw-research/AGENTS.md +63 -0
  117. package/packages/shared-skills/skills/ulw-research/SKILL.md +38 -26
  118. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +187 -0
  119. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +66 -0
  120. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +102 -0
  121. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +88 -0
  122. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +162 -0
  123. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +177 -0
  124. package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +204 -0
  125. package/packages/shared-skills/skills/ulw-research/scripts/css-lite.test.ts +159 -0
  126. package/packages/shared-skills/skills/ulw-research/scripts/design-spec.mjs +161 -0
  127. package/packages/shared-skills/skills/ulw-research/scripts/design-spec.test.ts +162 -0
  128. package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +39 -0
  129. package/packages/shared-skills/skills/ulw-research/scripts/entry-guard.mjs +21 -0
  130. package/packages/shared-skills/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
  131. package/packages/shared-skills/skills/ulw-research/scripts/format-extract.mjs +229 -0
  132. package/packages/shared-skills/skills/ulw-research/scripts/format-extract.test.ts +243 -0
  133. package/packages/shared-skills/skills/ulw-research/scripts/gates-figures.mjs +173 -0
  134. package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.mjs +146 -0
  135. package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.test.ts +236 -0
  136. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +167 -0
  137. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.test.ts +265 -0
  138. package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +118 -0
  139. package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +190 -0
  140. package/packages/shared-skills/skills/ulw-research/scripts/glossary-vocabulary.test.ts +43 -0
  141. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +239 -0
  142. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.test.ts +191 -0
  143. package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.mjs +83 -0
  144. package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.test.ts +89 -0
  145. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +250 -0
  146. package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +281 -0
  147. package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
  148. package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.test.ts +246 -0
  149. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
  150. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +61 -0
  151. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +213 -0
  152. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/assets/figure-1.svg +1 -0
  153. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/bad-report.html +44 -0
  154. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/boxes.json +90 -0
  155. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/design-spec.md +29 -0
  156. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/good-report.html +57 -0
  157. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference-report.html +86 -0
  158. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference.md +23 -0
  159. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +3 -1
@@ -0,0 +1,171 @@
1
+ // repair-tracker.mjs - the bounded repair loop as a pure state machine.
2
+ //
3
+ // Every decide() call is one check of the current artifact: the first call is attempt 0 (the
4
+ // initial check), each later call follows one repair already taken. The loop stops on max
5
+ // attempts, a measure plateau, a repeated defect signature (oscillation), or the wall clock.
6
+ // On a stop the artifact is delivered with its residual defects unless a content-integrity
7
+ // code remains, which blocks delivery. Time is always injected; this module never reads a
8
+ // clock. Severities come from contracts.mjs only, so no caller can demote a defect.
9
+
10
+ import { DEFECT_CODES, INTEGRITY_CODES, REPAIR_DEFAULTS, SCHEMA_VERSION, validateRepairState } from "./contracts.mjs"
11
+
12
+ /**
13
+ * @typedef {{ code: string, message?: string }} Defect
14
+ * @typedef {{ at: string, measure: number[], signature: string, action: string }} Attempt
15
+ * @typedef {{ schemaVersion: number, startedAt: string, budget: { maxAttempts: number, plateauLimit: number, wallClockMs: number }, integrityCodes: string[], attempts: Attempt[], consecutiveNonProgress: number, seenSignatures: string[] }} RepairState
16
+ * @typedef {{ action: "repair"|"deliver"|"block", reason: string|null, residual: Defect[], blocking: Defect[] }} Decision
17
+ */
18
+
19
+ const SEVERITY_INDEX = Object.freeze({ blocker: 0, major: 1, minor: 2 })
20
+ const MIN_ARTIFACT_BYTES = 512
21
+ const MIN_RENDERS = 1
22
+
23
+ export class RepairTrackerError extends Error {
24
+ /** @param {string} code @param {string} message */
25
+ constructor(code, message) {
26
+ super(message)
27
+ this.name = "RepairTrackerError"
28
+ this.code = code
29
+ }
30
+ }
31
+
32
+ function toIso(value, label) {
33
+ const date = value instanceof Date ? value : typeof value === "string" || typeof value === "number" ? new Date(value) : null
34
+ if (date === null || !Number.isFinite(date.getTime())) {
35
+ throw new RepairTrackerError("invalid_time", `${label} must be a Date, an ISO timestamp, or epoch milliseconds`)
36
+ }
37
+ return date.toISOString()
38
+ }
39
+
40
+ function assertKnownCode(code, label) {
41
+ if (typeof code !== "string" || !Object.hasOwn(DEFECT_CODES, code)) {
42
+ throw new RepairTrackerError("unknown_defect_code", `${label} ${JSON.stringify(code)} is not a known defect code`)
43
+ }
44
+ }
45
+
46
+ /** Callers may add integrity codes; the contract set is always included, so none can be removed. */
47
+ function integritySet(extra) {
48
+ return new Set([...INTEGRITY_CODES, ...(extra ?? [])])
49
+ }
50
+
51
+ /**
52
+ * @param {{ maxAttempts?: number, plateauLimit?: number, wallClockMs?: number, integrityCodes?: Iterable<string>, startedAt: Date|string|number }} options
53
+ * @returns {RepairState}
54
+ */
55
+ export function createTracker({ maxAttempts, plateauLimit, wallClockMs, integrityCodes, startedAt }) {
56
+ if (startedAt === undefined) throw new RepairTrackerError("invalid_time", "startedAt is required (the tracker never reads the clock)")
57
+ const codes = [...integritySet(integrityCodes)].sort()
58
+ for (const code of codes) assertKnownCode(code, "integrity code")
59
+ const state = {
60
+ schemaVersion: SCHEMA_VERSION,
61
+ startedAt: toIso(startedAt, "startedAt"),
62
+ budget: {
63
+ maxAttempts: maxAttempts ?? REPAIR_DEFAULTS.maxAttempts,
64
+ plateauLimit: plateauLimit ?? REPAIR_DEFAULTS.plateauLimit,
65
+ wallClockMs: wallClockMs ?? REPAIR_DEFAULTS.wallClockMs,
66
+ },
67
+ integrityCodes: codes,
68
+ attempts: [],
69
+ consecutiveNonProgress: 0,
70
+ seenSignatures: [],
71
+ }
72
+ assertValid(state)
73
+ return state
74
+ }
75
+
76
+ /** The artifact is worth delivering with residual defects only when it exists and was rendered. */
77
+ export function isArtifactOk({ bytes, renders }) {
78
+ return Number.isFinite(bytes) && bytes >= MIN_ARTIFACT_BYTES && Number.isFinite(renders) && renders >= MIN_RENDERS
79
+ }
80
+
81
+ export function measureOf(defects) {
82
+ const measure = [0, 0, 0]
83
+ for (const defect of defects) measure[SEVERITY_INDEX[DEFECT_CODES[defect.code].severity]] += 1
84
+ return measure
85
+ }
86
+
87
+ export function signatureOf(defects) {
88
+ return [...new Set(defects.map((defect) => defect.code))].sort().join("|")
89
+ }
90
+
91
+ function lessThan(a, b) {
92
+ for (let i = 0; i < a.length; i += 1) {
93
+ if (a[i] !== b[i]) return a[i] < b[i]
94
+ }
95
+ return false
96
+ }
97
+
98
+ function stopReason(state, { signature, consecutiveNonProgress, repairsTaken, elapsedMs }) {
99
+ const { maxAttempts, plateauLimit, wallClockMs } = state.budget
100
+ if (state.seenSignatures.includes(signature)) return "oscillation"
101
+ if (consecutiveNonProgress >= plateauLimit) return "plateau"
102
+ if (repairsTaken >= maxAttempts) return "max_attempts"
103
+ if (elapsedMs >= wallClockMs) return "wall_clock"
104
+ return null
105
+ }
106
+
107
+ /**
108
+ * @param {RepairState} state a tracker state (createTracker or deserializeState)
109
+ * @param {Defect[]} defects the current check's defects
110
+ * @param {Date|string|number} now injected time of this check
111
+ * @param {{ artifactOk?: boolean }} [artifact] caller-supplied; see isArtifactOk
112
+ * @returns {{ state: RepairState, decision: Decision }}
113
+ */
114
+ export function decide(state, defects, now, { artifactOk = false } = {}) {
115
+ if (!Array.isArray(defects)) throw new RepairTrackerError("invalid_defects", "defects must be an array")
116
+ defects.forEach((defect, index) => assertKnownCode(defect?.code, `defects[${index}].code`))
117
+ const at = toIso(now, "now")
118
+ const measure = measureOf(defects)
119
+ const signature = signatureOf(defects)
120
+ const previous = state.attempts.at(-1)
121
+ const consecutiveNonProgress = previous && !lessThan(measure, previous.measure) ? state.consecutiveNonProgress + 1 : 0
122
+ const repairsTaken = state.attempts.filter((attempt) => attempt.action === "repair").length
123
+ const integrity = integritySet(state.integrityCodes)
124
+ const blocking = defects.filter((defect) => integrity.has(defect.code))
125
+
126
+ /** @type {Decision} */
127
+ let decision
128
+ if (defects.length === 0) decision = { action: "deliver", reason: "clean", residual: [], blocking: [] }
129
+ else {
130
+ const elapsedMs = Date.parse(at) - Date.parse(state.startedAt)
131
+ const reason = stopReason(state, { signature, consecutiveNonProgress, repairsTaken, elapsedMs })
132
+ if (reason === null) decision = { action: "repair", reason: null, residual: [...defects], blocking }
133
+ else if (blocking.length > 0) decision = { action: "block", reason: "blocking", residual: [...defects], blocking }
134
+ else decision = { action: artifactOk ? "deliver" : "block", reason, residual: [...defects], blocking }
135
+ }
136
+
137
+ const next = {
138
+ ...state,
139
+ budget: { ...state.budget },
140
+ integrityCodes: [...state.integrityCodes],
141
+ attempts: [...state.attempts, { at, measure, signature, action: decision.action }],
142
+ consecutiveNonProgress,
143
+ seenSignatures: state.seenSignatures.includes(signature) ? [...state.seenSignatures] : [...state.seenSignatures, signature],
144
+ }
145
+ return { state: next, decision }
146
+ }
147
+
148
+ function assertValid(state) {
149
+ const { ok, errors } = validateRepairState(state)
150
+ if (!ok) throw new RepairTrackerError("invalid_repair_state", `invalid repair state: ${errors.join("; ")}`)
151
+ if (!Array.isArray(state.integrityCodes)) throw new RepairTrackerError("invalid_repair_state", "integrityCodes must be an array")
152
+ for (const code of state.integrityCodes) assertKnownCode(code, "integrity code")
153
+ }
154
+
155
+ /** @returns {string} the repair-state.json text */
156
+ export function serializeState(state) {
157
+ assertValid(state)
158
+ return `${JSON.stringify(state, null, 2)}\n`
159
+ }
160
+
161
+ /** @param {string} text repair-state.json contents @returns {RepairState} */
162
+ export function deserializeState(text) {
163
+ let state
164
+ try {
165
+ state = JSON.parse(text)
166
+ } catch (error) {
167
+ throw new RepairTrackerError("invalid_repair_state", `repair state is not JSON: ${error.message}`)
168
+ }
169
+ assertValid(state)
170
+ return state
171
+ }
@@ -0,0 +1,214 @@
1
+ // report-tools-commands.mjs - one function per report-tools subcommand. Each returns
2
+ // { json | text, summary?, exitCode } and never exits the process; report-tools.mjs dispatches.
3
+
4
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
5
+ import { dirname, join, resolve } from "node:path"
6
+
7
+ import { CliError, parseArgs, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
8
+ import { REPAIR_DEFAULTS } from "./contracts.mjs"
9
+ import { parseLineageMode, parsePalette, renderDesignSpec } from "./design-spec.mjs"
10
+ import { extractFromHtml, extractFromPath, fetchHtml } from "./format-extract.mjs"
11
+ import { checkLayout } from "./gates-layout.mjs"
12
+ import { checkStatic } from "./gates-static.mjs"
13
+ import { parseHtml } from "./html-lite.mjs"
14
+ import { buildProbeSource } from "./layout-probe.mjs"
15
+ import * as outcome from "./outcome.mjs"
16
+ import { createTracker, decide, deserializeState, isArtifactOk, serializeState } from "./repair-tracker.mjs"
17
+
18
+ const SEVERITY_ORDER = { blocker: 0, major: 1, minor: 2 }
19
+
20
+ function readText(path) {
21
+ try {
22
+ return readFileSync(path, "utf8")
23
+ } catch (error) {
24
+ throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
25
+ }
26
+ }
27
+
28
+ function nowFrom(flags) {
29
+ if (flags.now === undefined) return new Date()
30
+ const date = new Date(flags.now)
31
+ if (Number.isNaN(date.getTime())) throw new CliError("--now requires an ISO timestamp")
32
+ return date
33
+ }
34
+
35
+ function sessionDirFrom(flags) {
36
+ const dir = flags["session-dir"] ?? process.env.SESSION_DIR
37
+ if (!dir) throw new CliError("--session-dir is required (or set SESSION_DIR)")
38
+ return resolve(dir)
39
+ }
40
+
41
+ export function runCheck(argv) {
42
+ const args = parseArgs(argv, { repeatable: ["require-section"] })
43
+ const reportPath = args.positionals[0]
44
+ if (!reportPath) throw new CliError("usage: check <report.html> --design-spec <design-spec.md> [--layout <boxes.json>] [--require-section <regex>]... [--max-defects N]")
45
+ const specText = readText(args.required("design-spec"))
46
+ const doc = parseHtml(readText(reportPath))
47
+ const requireSections = (args.flags["require-section"] ?? []).map((source) => new RegExp(source, "i"))
48
+ const result = checkStatic(doc, {
49
+ palette: parsePalette(specText),
50
+ lineageMode: parseLineageMode(specText),
51
+ requireSections,
52
+ baseDir: dirname(resolve(reportPath)),
53
+ })
54
+ const defects = [...result.defects]
55
+ const gatesRun = [...result.gatesRun]
56
+ let layout = "not_run"
57
+ if (args.flags.layout !== undefined) {
58
+ const layoutResult = checkLayout(readJsonFile(args.flags.layout))
59
+ defects.push(...layoutResult.defects)
60
+ gatesRun.push("layout")
61
+ layout = layoutResult.defects.some((d) => d.severity === "blocker") ? "fail" : "pass"
62
+ }
63
+ defects.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || (a.line ?? 0) - (b.line ?? 0) || a.code.localeCompare(b.code))
64
+ const counts = { blocker: 0, major: 0, minor: 0, total: defects.length }
65
+ for (const defect of defects) counts[defect.severity] += 1
66
+ const max = args.int("max-defects", 200)
67
+ const status = counts.blocker > 0 ? "fail" : "pass"
68
+ const exitCode = status === "fail" ? 1 : 0
69
+ return {
70
+ json: { command: "check", status, exitCode, layout, defects: defects.slice(0, max), counts, gatesRun, gatesSkipped: result.gatesSkipped },
71
+ summary: `check: ${status.toUpperCase()} - ${counts.blocker} blocker, ${counts.major} major, ${counts.minor} minor; layout ${layout}`,
72
+ exitCode,
73
+ }
74
+ }
75
+
76
+ export function runLayoutProbe(argv) {
77
+ const args = parseArgs(argv, { booleans: ["json"] })
78
+ const source = buildProbeSource({ cap: args.int("cap", 400), root: args.flags.root ?? "main, article" })
79
+ return args.flags.json ? { json: { command: "layout-probe", source }, exitCode: 0 } : { text: source, exitCode: 0 }
80
+ }
81
+
82
+ export function runRepairDecide(argv) {
83
+ const args = parseArgs(argv)
84
+ const statePath = args.required("state")
85
+ const now = nowFrom(args.flags)
86
+ const defectsInput = readJsonFile(args.required("defects"))
87
+ const defects = Array.isArray(defectsInput) ? defectsInput : defectsInput.defects
88
+ if (!Array.isArray(defects)) throw new CliError("--defects must be a JSON array or a check result with a defects array")
89
+ const state = existsSync(statePath)
90
+ ? deserializeState(readText(statePath))
91
+ : createTracker({
92
+ maxAttempts: args.int("max-attempts", REPAIR_DEFAULTS.maxAttempts),
93
+ plateauLimit: args.int("plateau-limit", REPAIR_DEFAULTS.plateauLimit),
94
+ wallClockMs: args.int("wall-clock-ms", REPAIR_DEFAULTS.wallClockMs),
95
+ startedAt: now,
96
+ })
97
+ // Fail closed: deliver-on-exhaustion needs proof of a usable artifact (bytes and renders).
98
+ const artifactOk =
99
+ args.flags["artifact-bytes"] !== undefined && args.flags.renders !== undefined
100
+ ? isArtifactOk({ bytes: Number(args.flags["artifact-bytes"]), renders: Number(args.flags.renders) })
101
+ : false
102
+ const { state: next, decision } = decide(state, defects, now, { artifactOk })
103
+ writeFileSync(statePath, serializeState(next))
104
+ recordRepair(args.flags, next, decision)
105
+ const exitCode = decision.action === "block" ? 1 : 0
106
+ return { json: { command: "repair decide", decision }, summary: `repair: ${decision.action}${decision.reason ? ` (${decision.reason})` : ""}`, exitCode }
107
+ }
108
+
109
+ /** With a session dir that holds outcome.json, the repair summary and residual list land in the manifest. */
110
+ function recordRepair(flags, state, decision) {
111
+ const dir = flags["session-dir"] ?? process.env.SESSION_DIR
112
+ if (!dir) return
113
+ const file = join(resolve(dir), outcome.MANIFEST_FILENAME)
114
+ if (!existsSync(file)) return
115
+ const manifest = outcome.readManifest(file)
116
+ manifest.repair = { attempts: state.attempts.length, action: decision.action, reason: decision.reason }
117
+ if (decision.action !== "repair") manifest.residualDefects = decision.residual.map((defect) => ({ ...defect, message: defect.message ?? defect.code }))
118
+ outcome.writeManifest(file, manifest)
119
+ }
120
+
121
+ function manifestPath(flags) {
122
+ return join(sessionDirFrom(flags), outcome.MANIFEST_FILENAME)
123
+ }
124
+
125
+ export function runOutcome(argv) {
126
+ const [sub, ...rest] = argv
127
+ const args = parseArgs(rest, { booleans: ["json"] })
128
+ const file = sub === "state" ? null : manifestPath(args.flags)
129
+ switch (sub) {
130
+ case "init": {
131
+ const promised = args.required("promised").split(",").map((s) => s.trim()).filter(Boolean)
132
+ const manifest = outcome.initManifest({ sessionDir: sessionDirFrom(args.flags), promised, lane: args.flags.lane ?? null, now: nowFrom(args.flags) })
133
+ outcome.writeManifest(file, manifest)
134
+ return { json: { command: "outcome init", path: file, deliverables: manifest.deliverables }, exitCode: 0 }
135
+ }
136
+ case "set": {
137
+ const [format, status] = args.positionals
138
+ if (!format || !status) throw new CliError("usage: outcome set <format> <status> [--path p] [--reason r] [--pages n]")
139
+ const manifest = outcome.readManifest(file)
140
+ const detail = {}
141
+ if (args.flags.path !== undefined) {
142
+ detail.path = resolve(args.flags.path)
143
+ if (existsSync(detail.path)) detail.bytes = statSync(detail.path).size
144
+ }
145
+ if (args.flags.reason !== undefined) detail.reason = args.flags.reason
146
+ if (args.flags.pages !== undefined) detail.pages = args.int("pages")
147
+ outcome.writeManifest(file, outcome.setDeliverable(manifest, format, status, { ...detail, now: nowFrom(args.flags) }))
148
+ return { json: { command: "outcome set", format, status }, exitCode: 0 }
149
+ }
150
+ case "gate": {
151
+ const [gate, status] = args.positionals
152
+ if (!gate || !status) throw new CliError("usage: outcome gate <static|layout|visual|proofread> <pass|fail|not_run>")
153
+ outcome.writeManifest(file, outcome.setGate(outcome.readManifest(file), gate, status))
154
+ return { json: { command: "outcome gate", gate, status }, exitCode: 0 }
155
+ }
156
+ case "render": {
157
+ const [path] = args.positionals
158
+ if (!path) throw new CliError("usage: outcome render <path> --page N")
159
+ outcome.writeManifest(file, outcome.addRender(outcome.readManifest(file), resolve(path), args.int("page")))
160
+ return { json: { command: "outcome render", path: resolve(path) }, exitCode: 0 }
161
+ }
162
+ case "state": {
163
+ const deliverable = args.required("deliverable")
164
+ const sibling = join(dirname(resolve(deliverable)), outcome.MANIFEST_FILENAME)
165
+ const dir = args.flags["session-dir"] ?? process.env.SESSION_DIR
166
+ const candidate = dir ? join(resolve(dir), outcome.MANIFEST_FILENAME) : sibling
167
+ const manifest = existsSync(candidate) ? outcome.readManifest(candidate) : null
168
+ const state = outcome.deliverableState(resolve(deliverable), manifest)
169
+ return { json: { command: "outcome state", state }, summary: `state: ${state}`, exitCode: 0 }
170
+ }
171
+ case "verify": {
172
+ const result = outcome.verifyManifest(outcome.readManifest(file))
173
+ return { json: { command: "outcome verify", ...result }, summary: `verify: ${result.ok ? "OK" : `${result.problems.length} problem(s)`}`, exitCode: result.ok ? 0 : 1 }
174
+ }
175
+ case "finish": {
176
+ const manifest = outcome.readManifest(file)
177
+ const ledgerPath = args.flags.ledger ?? join(sessionDirFrom(args.flags), "sources-ledger.md")
178
+ const now = nowFrom(args.flags)
179
+ const facts = outcome.briefingFacts(manifest, existsSync(ledgerPath) ? readText(ledgerPath) : "", now)
180
+ manifest.finishedAt = now.toISOString()
181
+ manifest.elapsedMinutes = facts.minutes
182
+ manifest.sources = { total: facts.sources, domains: facts.domains }
183
+ outcome.writeManifest(file, manifest)
184
+ return { json: { command: "outcome finish", finishedAt: manifest.finishedAt, elapsedMinutes: facts.minutes, sources: manifest.sources }, exitCode: 0 }
185
+ }
186
+ case "briefing": {
187
+ const manifest = outcome.readManifest(file)
188
+ const ledgerPath = args.flags.ledger ?? join(sessionDirFrom(args.flags), "sources-ledger.md")
189
+ const ledger = existsSync(ledgerPath) ? readText(ledgerPath) : ""
190
+ const now = nowFrom(args.flags)
191
+ if (args.flags.json) return { json: { command: "outcome briefing", ...outcome.briefingFacts(manifest, ledger, now) }, exitCode: 0 }
192
+ return { text: outcome.buildBriefing(manifest, ledger, now), exitCode: 0 }
193
+ }
194
+ default:
195
+ throw new CliError("usage: outcome <init|set|gate|render|state|verify|finish|briefing> ...")
196
+ }
197
+ }
198
+
199
+ export async function runFormatExtract(argv) {
200
+ const args = parseArgs(argv, { booleans: ["from-url"] })
201
+ const [reference] = args.positionals
202
+ if (!reference) throw new CliError("usage: format-extract <reference> --out <design-spec.md> [--from-url]")
203
+ const out = args.required("out")
204
+ let extract
205
+ try {
206
+ extract = args.flags["from-url"] ? extractFromHtml(await fetchHtml(reference), { origin: reference }) : extractFromPath(reference)
207
+ } catch (error) {
208
+ // An unsupported, unreadable, or unreachable reference is an input problem (exit 2), not a gate result.
209
+ throw new CliError(`format-extract ${reference}: ${error.message}`)
210
+ }
211
+ writeJsonAtomic(`${out}.extract.json`, { ...extract, tokens: { light: Object.fromEntries(extract.tokens?.light ?? []), dark: Object.fromEntries(extract.tokens?.dark ?? []) } })
212
+ writeFileSync(out, renderDesignSpec(extract))
213
+ return { json: { command: "format-extract", out: resolve(out), origin: extract.origin, lineageMode: extract.lineageMode, notes: extract.notes ?? [] }, summary: `format-extract: wrote ${out}`, exitCode: 0 }
214
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // report-tools.mjs - the ulw-research deliverable CLI. Invoked as
3
+ // node "$SKILL_DIR/scripts/report-tools.mjs" <command> ...
4
+ // Exit codes: 0 pass, 1 semantic failure (blockers, a failed verify, a blocked repair), 2 usage or IO.
5
+
6
+ import { CliError, emit, exitCodeFor } from "./cli-support.mjs"
7
+ import { isCliEntry } from "./entry-guard.mjs"
8
+ import { runCheck, runFormatExtract, runLayoutProbe, runOutcome, runRepairDecide } from "./report-tools-commands.mjs"
9
+
10
+ export const COMMANDS = Object.freeze([
11
+ "check",
12
+ "layout-probe",
13
+ "repair decide",
14
+ "outcome init",
15
+ "outcome set",
16
+ "outcome gate",
17
+ "outcome render",
18
+ "outcome state",
19
+ "outcome verify",
20
+ "outcome finish",
21
+ "outcome briefing",
22
+ "format-extract",
23
+ ])
24
+
25
+ export async function run(argv) {
26
+ const [command, ...rest] = argv
27
+ switch (command) {
28
+ case "check":
29
+ return runCheck(rest)
30
+ case "layout-probe":
31
+ return runLayoutProbe(rest)
32
+ case "repair":
33
+ if (rest[0] !== "decide") throw new CliError("usage: repair decide --state <file> --defects <json>")
34
+ return runRepairDecide(rest.slice(1))
35
+ case "outcome":
36
+ return runOutcome(rest)
37
+ case "format-extract":
38
+ return runFormatExtract(rest)
39
+ case "--help":
40
+ case "help":
41
+ if (rest.includes("--json")) return { json: { commands: COMMANDS }, exitCode: 0 }
42
+ return { text: `report-tools <command>\n${COMMANDS.map((name) => ` ${name}`).join("\n")}`, exitCode: 0 }
43
+ default:
44
+ throw new CliError(`usage: report-tools <${COMMANDS.join(" | ")}> (run --help)`)
45
+ }
46
+ }
47
+
48
+ export async function main(argv = process.argv.slice(2)) {
49
+ try {
50
+ const result = await run(argv)
51
+ emit(result)
52
+ return exitCodeFor(result)
53
+ } catch (error) {
54
+ process.stderr.write(`report-tools: ${error.message}\n`)
55
+ return exitCodeFor(error)
56
+ }
57
+ }
58
+
59
+ if (isCliEntry(import.meta.url)) {
60
+ process.exitCode = await main()
61
+ }
@@ -58,7 +58,9 @@ try {
58
58
 
59
59
  NEVER launch anything against, or clear cookies/cache/site data from, the user's live profile;
60
60
  the attached engine is the only sanctioned way to a signed-in page. If no extension is connected,
61
- run the `browser` skill's `scripts/browser-install.mjs`, relay its one human step, and wait — do
61
+ run the `browser` skill's `scripts/browser-install.mjs` for the browser the user actually uses
62
+ (from memory, or its detection; on `needsChoice` ask them and pass `--browser=<id>`), relay its
63
+ one human step, and wait — do
62
64
  not fall back to the owned engine for an authenticated criterion.
63
65
 
64
66
  ## Capture a screenshot at a fixed viewport
@@ -166,3 +166,14 @@ test("#given the flattened plugin cache the Codex installer produces #when sync-
166
166
  await rm(codexHome, { recursive: true, force: true });
167
167
  }
168
168
  });
169
+
170
+ test("#given the shared ulw-research skill #when the Codex overlay runs #then the overlay applies", async () => {
171
+ // given
172
+ const shared = await readFile(join(sharedSkillsRoot, "ulw-research", "SKILL.md"), "utf8");
173
+
174
+ // when
175
+ const actual = applyCodexSkillOverlays("ulw-research", shared);
176
+
177
+ // then
178
+ assert.notEqual(actual, shared, "the ulw-research overlay anchor no longer matches the shared SKILL.md");
179
+ });
@@ -158,8 +158,8 @@ prefixes when identity is needed.
158
158
  `;
159
159
  const reviewWorkCodexGatePattern = new RegExp(reviewWorkCodexGate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
160
160
 
161
- const ulwResearchOriginalDeliveryGates = "### The delivery gates \u2014 every gate must PASS, in order\n\nNothing reaches the user until the gates pass:\n\n1. **Visual QA (always).** Render the produced artifact back to images \u2014 PDF pages to PNG, the HTML in a real browser \u2014 and look at them: missing or broken figures, images stretched or spilling their containers, diagram or chart text rendered off the spec's font or palette, clipped tables, overflowing CJK text, blank pages, unlabeled chart values, wrong page breaks. Fix and re-render until the pages are clean. Reading the source markup is not visual QA; inspect the pixels.\n2. **Proofread gate \u2014 `task(category=\"writing\", ...)`.** Hand the final text to a dedicated `writing` worker whose only job is language: grammar, spelling, punctuation, terminology consistency, and whether the prose reads NATIVELY in the report's own language. It returns a defect list; fix every item and re-run the gate on the delta. Deliver only on a clean pass \u2014 this gate runs BEFORE the first delivery, not after the user finds the typo.";
162
- const ulwResearchCodexDeliveryGate = "### The delivery gate \u2014 visual QA must PASS\n\nNothing reaches the user until the gate passes:\n\n**Visual QA (always).** Render the produced artifact back to images \u2014 PDF pages to PNG, the HTML in a real browser \u2014 and look at them: missing or broken figures, images stretched or spilling their containers, diagram or chart text rendered off the spec's font or palette, clipped tables, overflowing CJK text, blank pages, unlabeled chart values, wrong page breaks. Fix and re-render until the pages are clean. Reading the source markup is not visual QA; inspect the pixels.";
161
+ const ulwResearchOriginalDeliveryGates = "Reading the source markup is not visual QA; inspect the pixels.\n4. **Proofread gate \u2014 `task(category=\"writing\", ...)`.** Hand the final text to a dedicated `writing` worker whose only job is language: grammar, spelling, punctuation, terminology consistency, and whether the prose reads NATIVELY in the report's own language. It proofreads only and never composes. It returns a defect list; fix every item and re-run the gate on the delta. Deliver only on a clean pass \u2014 this gate runs BEFORE the first delivery, not after the user finds the typo.";
162
+ const ulwResearchCodexDeliveryGate = "Reading the source markup is not visual QA; inspect the pixels.";
163
163
 
164
164
  export function removeCodexSkillOverlays(skillName, content) {
165
165
  if (skillName === "ulw-research") {
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import test from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { sharedSkillsRootPath } from "@oh-my-opencode/shared-skills";
7
+ import { createSkillSourceCopyFilter } from "@oh-my-opencode/shared-skills/skill-source-filter";
7
8
  import {
8
9
  canonicalUltraworkDirectiveRelativePath,
9
10
  componentSkillSources,
@@ -260,3 +261,22 @@ test("#given packaged Codex ulw-plan surfaces #when inspected #then dangerous sa
260
261
  assert.doesNotMatch(packagedWorkflow.content, dangerousBypassPattern, `${packagedWorkflow.path} ships unsafe Codex bypass guidance`);
261
262
  assert.doesNotMatch(componentWorkflow.content, dangerousBypassPattern, `${componentWorkflow.path} ships unsafe Codex bypass guidance`);
262
263
  });
264
+
265
+ test("#given the shared ulw-research runtime and references #when aggregate Codex skills are inspected #then every packaged file is byte-equal to its shared source", async () => {
266
+ // given
267
+ const sharedSkillRoot = join(sharedSkillsRootPath(), "ulw-research");
268
+ const packagedSkillRoot = join(root, "skills", "ulw-research");
269
+ const keep = createSkillSourceCopyFilter(sharedSkillRoot);
270
+ const inAssetDirs = (file) => /^(scripts|references)\//.test(file.replaceAll("\\", "/"));
271
+ const sharedFiles = (await listSkillFiles(sharedSkillRoot)).filter(inAssetDirs).filter((file) => keep(join(sharedSkillRoot, file))).sort();
272
+
273
+ // when
274
+ const packagedFiles = (await listSkillFiles(packagedSkillRoot)).filter(inAssetDirs).sort();
275
+
276
+ // then
277
+ assert.deepEqual(packagedFiles, sharedFiles);
278
+ for (const file of sharedFiles) {
279
+ const [packaged, shared] = await Promise.all([readFile(join(packagedSkillRoot, file)), readFile(join(sharedSkillRoot, file))]);
280
+ assert.ok(packaged.equals(shared), `ulw-research/${file} must ship the shared bytes`);
281
+ }
282
+ });
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // omo-codex-install:ed5913a0451b102580823bb453ab16a6b0fe5490e3c3cc96f21285ce1b271763:fef32404e9543827207e1db51df847e2c997e70ee31a7440d6a8cfb13c3450d0
2
+ // omo-codex-install:cf5a89b3eed10326f90d8a72b6f8cbce5e75da7812a5d6483320ae4783e8b221:9ca19658ee60a48edee0d04d97631bfaacced4ecde342545a420a1f2639bc3de
3
3
  var __esm = (fn, res, err) => () => {
4
4
  if (fn)
5
5
  try {
@@ -9984,7 +9984,7 @@ var package_default;
9984
9984
  var init_package = __esm(() => {
9985
9985
  package_default = {
9986
9986
  name: "@oh-my-opencode/omo-codex",
9987
- version: "5.0.0-beta.89",
9987
+ version: "5.0.0-beta.90",
9988
9988
  type: "module",
9989
9989
  private: true,
9990
9990
  description: "Codex harness adapter for oh-my-openagent. Vendored Codex plugin namespace (omo) + TypeScript installer + telemetry.",
@@ -6,7 +6,7 @@
6
6
  [CODE RED] Maximum precision. Outcome-first. Evidence-driven.
7
7
 
8
8
  # Role
9
- Expert coding agent. Ship verified work. No process narration.
9
+ Expert coding agent. Ship verified work; report at handoffs, not between them.
10
10
 
11
11
  # Goal
12
12
  Deliver EXACTLY what the user asked, end-to-end working, proven by
@@ -438,8 +438,12 @@ commits this session — then stage + draft the message instead.
438
438
  # Output discipline
439
439
  - First line literally: `ULTRAWORK MODE ENABLED!`
440
440
  - After bootstrap: 1-2 paragraph plan summary + notepad path.
441
- - During execution: surface only state changes (existing tests read,
442
- scenario PASS/FAIL with evidence paths, reviewer verdict).
441
+ - During execution: at every handoff - todo phase change, blocker,
442
+ plan change, before a long pass - one handoff block composed after
443
+ weighing what the user asked and needs to know now:
444
+ `[Outcome so far] toward [ask + wanted]. You need: [ledger,
445
+ evidence paths, PASS/FAIL, reviewer verdict]. Now: [todo in
446
+ progress]. Next: [next open todo].`; nothing between handoffs.
443
447
  - Final message: outcome + success-criteria checklist with evidence
444
448
  refs + notepad path + reviewer approval (if gate triggered) + commit
445
449
  list (`<sha> <subject>`). No file-by-file changelog unless asked.
@@ -32,9 +32,16 @@ node "<skill-root>/scripts/browser-doctor.mjs" --json
32
32
  | State | Meaning | Next |
33
33
  |---|---|---|
34
34
  | `ready` | CLI, daemon and a connected browser | start a session |
35
- | `no-cli` / `no-daemon` / `no-extension` | something is missing | `node "<skill-root>/scripts/browser-install.mjs"` prepares everything it can, then prints the **single** step only the user can do (relaunch the browser and click **Enable**); relay it verbatim, wait, re-run the doctor |
35
+ | `no-cli` / `no-daemon` / `no-extension` | something is missing | `node "<skill-root>/scripts/browser-install.mjs" [--browser=<id>]` prepares everything it can for **the browser the user uses**, then prints the **single** step only the user can do (relaunch that browser and click **Enable**); relay it verbatim, wait, re-run the doctor |
36
+ | `choose-browser` | the signals do not single out one browser (Safari/Firefox default, an idle default while another browser runs, several in use) | nothing was installed; take the browser from memory or ask the user, then `browser-install.mjs --browser=<id>` |
36
37
  | `no-browser-support` | no Chromium-family profile on this machine | say so and stop |
37
38
 
39
+ **Install into the browser the user actually uses, never into whatever happens to be on disk.** Before
40
+ installing, check your memory for the user's browser; otherwise read the doctor's `browser` (picked
41
+ from the OS default browser, running apps and recent use — `candidates` shows the evidence). If memory
42
+ and the doctor disagree, or the doctor says `choose-browser`, ask the user. Pass the answer as
43
+ `--browser=<id>` and record it in memory. A Chrome that is merely installed is not their browser.
44
+
38
45
  **Never launch a headless browser because the attached one is missing.** It has none of the
39
46
  user's sessions, so every login turns into a ladder you should not be climbing. Say which state
40
47
  you hit and ask.
@@ -13,18 +13,46 @@ Three pieces. The onboarding script prepares all three; a human finishes exactly
13
13
  | | |
14
14
  |---|---|
15
15
  | Operating systems | macOS (Apple Silicon and Intel), Linux (x64, ARM64), Windows x64 |
16
- | Browsers | Chrome, Microsoft Edge, Brave, Chromium — any profile the doctor lists under `browsersDetected` |
16
+ | Browsers | Chrome, Microsoft Edge, Brave, Chromium, Arc, Dia, Vivaldi, Opera, Comet, Naver Whale — whichever the doctor lists under `candidates` |
17
+
18
+ ## Which browser
19
+
20
+ The extension goes into exactly one browser: the one the user works in. Installed is not the same as
21
+ used — a Chrome launched once keeps its profile directory forever — so the scripts rank candidates by:
22
+
23
+ | Signal | Where it comes from |
24
+ |---|---|
25
+ | Default browser | macOS LaunchServices `https` handler, Linux `xdg-settings`, Windows `UserChoice` ProgId |
26
+ | Running now | the app in `/Applications` (macOS), the process table elsewhere |
27
+ | Used in the last 7 days | the profile's `Local State` / `History` modification time |
28
+
29
+ Order of authority:
30
+
31
+ 1. **Your memory.** If you recorded the user's browser before, pass it: `--browser=<id>`.
32
+ 2. **`--browser=<id>` / `OMOWRIGHT_BROWSER`** set by the user.
33
+ 3. **The default browser, when it is also in use** (or nothing else is).
34
+ 4. **The only browser in use** when no default can be read.
35
+
36
+ Anything else is `choose-browser` (doctor) / `needsChoice: true` (install): nothing is registered and
37
+ `candidates` lists every browser with its signals. Ask the user which one they use, run the install
38
+ with `--browser=<id>`, and save the answer to memory so the next session does not ask again. When
39
+ the default browser is Safari or Firefox, BrowserSkill cannot drive it — say so and ask which
40
+ Chromium-family browser to use.
41
+
42
+ The doctor also lists `registeredElsewhere`: browsers that still carry an entry from an older
43
+ onboarding. They are not removed automatically (removing the entry uninstalls an extension the user
44
+ may have enabled); remove one only when the user asks, with omowright's `unregisterExternalExtension`.
17
45
 
18
46
  ## One command
19
47
 
20
48
  ```bash
21
- node "<skill-root>/scripts/browser-install.mjs" [--json] [--wait-ms=180000]
49
+ node "<skill-root>/scripts/browser-install.mjs" [--browser=<id>] [--json] [--wait-ms=180000]
22
50
  ```
23
51
 
24
52
  In order it: runs upstream's official `install.sh` / `install.ps1` into `~/.local/bin` (any PATH
25
53
  line the installer appends to a shell rc file is reverted — the library calls the binary by
26
- absolute path); starts the daemon with `bsk status`; and, for every Chromium-family profile it
27
- finds, registers the Web Store listing as an external extension:
54
+ absolute path); starts the daemon with `bsk status`; and, for **the one browser the user actually
55
+ uses**, registers the Web Store listing as an external extension:
28
56
 
29
57
  | Platform | Where the entry goes | The user's one step |
30
58
  |---|---|---|
@@ -32,6 +60,7 @@ finds, registers the Web Store listing as an external extension:
32
60
  | Windows | `HKCU\Software\<vendor>\<browser>\Extensions\<id>` (`reg add`, no admin) | click **Enable** on the toolbar badge; no restart |
33
61
  | Linux (Chromium) | `<user data dir>/External Extensions/<id>.json` | relaunch; installs without a prompt |
34
62
  | Linux (Chrome / Edge / Brave) | `/opt/google/chrome/extensions/<id>.json` and siblings — root only | otherwise open the store link it prints and click **Add** |
63
+ | Opera, and Vivaldi / Whale off macOS | no external-extension path | open the store link it prints in that browser and click **Add** |
35
64
 
36
65
  The script prints that step as `humanStep`. **Relay it verbatim and wait**; with `--wait-ms` it
37
66
  keeps polling the daemon until the extension connects. An empty `browsersConnected` afterwards is
@@ -57,7 +86,7 @@ engine, one of them shadowing the one that knows about omowright and this packag
57
86
  ## Diagnosing
58
87
 
59
88
  ```bash
60
- node "<skill-root>/scripts/browser-doctor.mjs" --json # this package's view: cli, daemon, profiles, registrations, connections
89
+ node "<skill-root>/scripts/browser-doctor.mjs" --json # this package's view: cli, daemon, the user's browser + candidates, registrations, connections
61
90
  bsk doctor # upstream's own checks
62
91
  bsk logs # daemon log
63
92
  ```