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.
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh-cn.md +1 -1
- package/dist/cli/config-manager/refresh-opencode-plugin-sandbox.d.ts +31 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +11 -0
- package/dist/cli/index.js +1661 -859
- package/dist/cli/install-native/index.d.ts +9 -1
- package/dist/cli/install-native/legacy-omo-bin.d.ts +27 -0
- package/dist/cli/install-native/offer-native-setup.d.ts +31 -0
- package/dist/cli/install-native/omo-bin-test-fixtures.d.ts +35 -0
- package/dist/cli/install-native/plan.d.ts +8 -0
- package/dist/cli/install-native/repair-legacy-omo-bin.d.ts +26 -0
- package/dist/cli/install-native/run-native-install.d.ts +11 -1
- package/dist/cli/install-native/verify-omo-command.d.ts +22 -0
- package/dist/cli/native-edition-hint.d.ts +2 -2
- package/dist/cli-node/index.js +1661 -859
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +7 -3
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +8 -1
- package/packages/omo-codex/plugin/skills/browser/references/install.md +34 -5
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +7 -3
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -25
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +3 -1
- package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +11 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +20 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +7 -3
- package/packages/shared-skills/skills/browser/SKILL.md +8 -1
- package/packages/shared-skills/skills/browser/references/install.md +34 -5
- package/packages/shared-skills/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +63 -0
- package/packages/shared-skills/skills/ulw-research/SKILL.md +38 -26
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +88 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +177 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.test.ts +159 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.test.ts +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.test.ts +243 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.test.ts +236 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.test.ts +265 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/glossary-vocabulary.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.test.ts +191 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.test.ts +89 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +281 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.test.ts +246 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +213 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/assets/figure-1.svg +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/bad-report.html +44 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/boxes.json +90 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/design-spec.md +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/good-report.html +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference-report.html +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference.md +23 -0
- 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
|
|
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 = "
|
|
162
|
-
const ulwResearchCodexDeliveryGate = "
|
|
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:
|
|
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.
|
|
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
|
|
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:
|
|
442
|
-
|
|
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
|
|
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 —
|
|
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
|
|
27
|
-
|
|
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,
|
|
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
|
```
|