oh-my-opencode 5.1.54 → 5.1.56
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/dist/cli/index.js +29 -18
- package/dist/cli-node/index.js +29 -18
- package/dist/config/schema/agent-overrides.d.ts +64 -0
- package/dist/config/schema/categories.d.ts +8 -0
- package/dist/config/schema/oh-my-opencode-config.d.ts +64 -0
- package/dist/index.js +120 -48
- package/dist/oh-my-opencode.schema.json +112 -0
- package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
- package/dist/skills/programming/SKILL.md +2 -0
- package/dist/skills/programming/references/testing/README.md +2 -2
- package/dist/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
- package/dist/skills/programming/scripts/typescript/check-test-junk.test.ts +125 -0
- package/dist/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
- package/dist/skills/programming/scripts/typescript/mutation-proof.test.ts +43 -0
- package/dist/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
- package/dist/skills/remove-ai-slops/SKILL.md +32 -20
- package/dist/skills/ulw-research/scripts/source-commands.mjs +3 -1
- package/dist/skills/ulw-research/scripts/source-integrity.test.ts +39 -2
- package/dist/tools/delegate-task/models-distribution.d.ts +1 -0
- package/dist/tui.js +28 -17
- package/package.json +16 -14
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
- 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/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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +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/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/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +2 -0
- package/packages/omo-codex/plugin/skills/programming/references/testing/README.md +2 -2
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +32 -20
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +3 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/programming/SKILL.md +2 -0
- package/packages/shared-skills/skills/programming/references/testing/README.md +2 -2
- package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.test.ts +125 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.test.ts +43 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
- package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +32 -20
- package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +3 -1
- package/packages/shared-skills/skills/ulw-research/scripts/source-integrity.test.ts +39 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { MutationError, runMutation } from "./mutation-proof"
|
|
7
|
+
|
|
8
|
+
let root = ""
|
|
9
|
+
const OWNER = "export const add = (a: number, b: number): number => a + b\n"
|
|
10
|
+
|
|
11
|
+
describe("#given a production owner and a test that exercises it", () => {
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
root = mkdtempSync(join(process.env.RUN_TMP ?? tmpdir(), "mutation-proof-"))
|
|
14
|
+
writeFileSync(join(root, "owner.ts"), OWNER)
|
|
15
|
+
writeFileSync(
|
|
16
|
+
join(root, "owner.test.ts"),
|
|
17
|
+
'import { expect, test } from "bun:test"\nimport { add } from "./owner"\ntest("adds", () => { expect(add(1, 2)).toBe(3) })\n',
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterAll(() => rmSync(root, { recursive: true, force: true }))
|
|
22
|
+
|
|
23
|
+
test("#when the owner is broken #then the test goes red and the owner is restored byte for byte", async () => {
|
|
24
|
+
const result = await runMutation({
|
|
25
|
+
cwd: root,
|
|
26
|
+
file: "owner.ts",
|
|
27
|
+
find: "a + b",
|
|
28
|
+
replace: "a - b",
|
|
29
|
+
tests: [{ label: "candidate", command: [process.execPath, "test", "owner.test.ts"] }],
|
|
30
|
+
})
|
|
31
|
+
expect(result.outcomes.map((outcome) => [outcome.label, outcome.verdict])).toEqual([["candidate", "red"]])
|
|
32
|
+
expect(result.restoredSha256).toBe(result.originalSha256)
|
|
33
|
+
expect(readFileSync(join(root, "owner.ts"), "utf8")).toBe(OWNER)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("#when the find text is not unique or empty #then it refuses before touching the file", async () => {
|
|
37
|
+
for (const [find, kind] of [["a", "FIND_NOT_UNIQUE"], ["", "FIND_EMPTY"]] as const) {
|
|
38
|
+
const attempt = runMutation({ cwd: root, file: "owner.ts", find, replace: "x", tests: [] })
|
|
39
|
+
await expect(attempt).rejects.toMatchObject({ kind })
|
|
40
|
+
}
|
|
41
|
+
expect(readFileSync(join(root, "owner.ts"), "utf8")).toBe(OWNER)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Prove that a test can fail: apply one exact-match edit to a production file, run each test
|
|
4
|
+
* command against the mutated owner, then restore the original bytes and verify their hash.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun run scripts/typescript/mutation-proof.ts --file <path> --find <text> --replace <text> \
|
|
8
|
+
* --test '<label>=<argv...>' [--test ...] [--cwd <dir>] [--json]
|
|
9
|
+
*
|
|
10
|
+
* --test takes "label=command words"; the command is split on spaces and quoting is not
|
|
11
|
+
* supported, so select a test by a pattern without spaces or run the whole file.
|
|
12
|
+
*
|
|
13
|
+
* Verdicts per test: red (exit != 0, the test caught the mutation) or green (exit 0, it did not).
|
|
14
|
+
* The owner is restored on success, on a thrown error, and on SIGINT/SIGTERM.
|
|
15
|
+
* Exit codes: 0 ran and restored, 1 the find text is empty or did not match exactly once or the
|
|
16
|
+
* restore hash differed (the file was still restored), 2 usage, 130/143 interrupted and restored.
|
|
17
|
+
* Read references/testing/junk-classes.md "Proof rule" for how to read a candidate/keeper pair.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHash } from "node:crypto"
|
|
21
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
22
|
+
import { resolve } from "node:path"
|
|
23
|
+
import process from "node:process"
|
|
24
|
+
import { parseArgs } from "node:util"
|
|
25
|
+
|
|
26
|
+
export type MutationTest = { readonly label: string; readonly command: readonly string[] }
|
|
27
|
+
|
|
28
|
+
export type MutationOutcome = {
|
|
29
|
+
readonly label: string
|
|
30
|
+
readonly command: readonly string[]
|
|
31
|
+
readonly exitCode: number
|
|
32
|
+
readonly verdict: "red" | "green"
|
|
33
|
+
readonly outputTail: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type MutationResult = {
|
|
37
|
+
readonly file: string
|
|
38
|
+
readonly find: string
|
|
39
|
+
readonly replace: string
|
|
40
|
+
readonly originalSha256: string
|
|
41
|
+
readonly restoredSha256: string
|
|
42
|
+
readonly outcomes: readonly MutationOutcome[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class MutationError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
readonly kind: "FIND_EMPTY" | "FIND_NOT_UNIQUE" | "RESTORE_MISMATCH",
|
|
48
|
+
message: string,
|
|
49
|
+
) {
|
|
50
|
+
super(message)
|
|
51
|
+
this.name = "MutationError"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sha256 = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex")
|
|
56
|
+
|
|
57
|
+
async function runCaptured(command: readonly string[], cwd: string): Promise<{ readonly exitCode: number; readonly outputTail: string }> {
|
|
58
|
+
const child = Bun.spawn([...command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" })
|
|
59
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
60
|
+
new Response(child.stdout).text(),
|
|
61
|
+
new Response(child.stderr).text(),
|
|
62
|
+
child.exited,
|
|
63
|
+
])
|
|
64
|
+
const lines = `${stdout}${stderr}`.trimEnd().split("\n")
|
|
65
|
+
return { exitCode, outputTail: lines.slice(-30).join("\n") }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runMutation(options: {
|
|
69
|
+
readonly cwd: string
|
|
70
|
+
readonly file: string
|
|
71
|
+
readonly find: string
|
|
72
|
+
readonly replace: string
|
|
73
|
+
readonly tests: readonly MutationTest[]
|
|
74
|
+
}): Promise<MutationResult> {
|
|
75
|
+
if (options.find.length === 0) throw new MutationError("FIND_EMPTY", `${options.file}: find text is empty`)
|
|
76
|
+
const full = resolve(options.cwd, options.file)
|
|
77
|
+
const original = readFileSync(full)
|
|
78
|
+
const text = original.toString("utf8")
|
|
79
|
+
const occurrences = text.split(options.find).length - 1
|
|
80
|
+
if (occurrences !== 1) {
|
|
81
|
+
throw new MutationError("FIND_NOT_UNIQUE", `${options.file}: find matched ${occurrences} times (need exactly 1)`)
|
|
82
|
+
}
|
|
83
|
+
const originalSha256 = sha256(original)
|
|
84
|
+
const outcomes: MutationOutcome[] = []
|
|
85
|
+
const restoreOnSignal = (signal: NodeJS.Signals): void => {
|
|
86
|
+
writeFileSync(full, original)
|
|
87
|
+
process.exit(signal === "SIGINT" ? 130 : 143)
|
|
88
|
+
}
|
|
89
|
+
process.on("SIGINT", restoreOnSignal)
|
|
90
|
+
process.on("SIGTERM", restoreOnSignal)
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(full, text.replace(options.find, () => options.replace))
|
|
93
|
+
for (const test of options.tests) {
|
|
94
|
+
const run = await runCaptured(test.command, options.cwd)
|
|
95
|
+
outcomes.push({
|
|
96
|
+
label: test.label,
|
|
97
|
+
command: test.command,
|
|
98
|
+
exitCode: run.exitCode,
|
|
99
|
+
verdict: run.exitCode === 0 ? "green" : "red",
|
|
100
|
+
outputTail: run.outputTail,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
writeFileSync(full, original)
|
|
105
|
+
process.off("SIGINT", restoreOnSignal)
|
|
106
|
+
process.off("SIGTERM", restoreOnSignal)
|
|
107
|
+
}
|
|
108
|
+
const restoredSha256 = sha256(readFileSync(full))
|
|
109
|
+
if (restoredSha256 !== originalSha256) {
|
|
110
|
+
throw new MutationError("RESTORE_MISMATCH", `${options.file} not restored byte for byte`)
|
|
111
|
+
}
|
|
112
|
+
return { file: options.file, find: options.find, replace: options.replace, originalSha256, restoredSha256, outcomes }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseTest(spec: string): MutationTest | undefined {
|
|
116
|
+
const separator = spec.indexOf("=")
|
|
117
|
+
if (separator <= 0) return undefined
|
|
118
|
+
const label = spec.slice(0, separator)
|
|
119
|
+
const command = spec
|
|
120
|
+
.slice(separator + 1)
|
|
121
|
+
.split(" ")
|
|
122
|
+
.filter((word) => word.length > 0)
|
|
123
|
+
return command.length === 0 ? undefined : { label, command }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function usage(): number {
|
|
127
|
+
console.error(
|
|
128
|
+
"usage: mutation-proof.ts --file <path> --find <text> --replace <text> --test '<label>=<argv...>' [--test ...] [--cwd <dir>] [--json]",
|
|
129
|
+
)
|
|
130
|
+
return 2
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main(argv: readonly string[]): Promise<number> {
|
|
134
|
+
const { values } = parseArgs({
|
|
135
|
+
args: [...argv],
|
|
136
|
+
options: {
|
|
137
|
+
file: { type: "string" },
|
|
138
|
+
find: { type: "string" },
|
|
139
|
+
replace: { type: "string" },
|
|
140
|
+
test: { type: "string", multiple: true },
|
|
141
|
+
cwd: { type: "string", default: process.cwd() },
|
|
142
|
+
json: { type: "boolean", default: false },
|
|
143
|
+
},
|
|
144
|
+
strict: true,
|
|
145
|
+
})
|
|
146
|
+
if (values.file === undefined || values.find === undefined || values.replace === undefined || values.test === undefined) {
|
|
147
|
+
return usage()
|
|
148
|
+
}
|
|
149
|
+
const tests: MutationTest[] = []
|
|
150
|
+
for (const spec of values.test) {
|
|
151
|
+
const parsed = parseTest(spec)
|
|
152
|
+
if (parsed === undefined) return usage()
|
|
153
|
+
tests.push(parsed)
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const result = await runMutation({
|
|
157
|
+
cwd: values.cwd ?? process.cwd(),
|
|
158
|
+
file: values.file,
|
|
159
|
+
find: values.find,
|
|
160
|
+
replace: values.replace,
|
|
161
|
+
tests,
|
|
162
|
+
})
|
|
163
|
+
if (values.json) {
|
|
164
|
+
console.log(JSON.stringify(result, null, 2))
|
|
165
|
+
} else {
|
|
166
|
+
for (const outcome of result.outcomes) console.log(`${outcome.label}: ${outcome.verdict} (exit ${outcome.exitCode})`)
|
|
167
|
+
console.log(`restored ${result.file} (sha256 ${result.restoredSha256.slice(0, 12)})`)
|
|
168
|
+
}
|
|
169
|
+
return 0
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof MutationError) {
|
|
172
|
+
console.error(`mutation-proof: ${error.kind}: ${error.message}`)
|
|
173
|
+
return 1
|
|
174
|
+
}
|
|
175
|
+
throw error
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (import.meta.main) process.exit(await main(process.argv.slice(2)))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: remove-ai-slops
|
|
3
|
-
description: "Removes AI-generated code smells from branch changes or an explicit file list behind regression tests. Use when the user asks to clean up, deslop,
|
|
3
|
+
description: "Removes AI-generated code smells from branch changes or an explicit file list behind regression tests, including junk tests (prose pins, source greps, copied inventories, mock echoes), each proven by its class's rule before a cut. Use when the user asks to clean up, deslop, remove AI-slop patterns, or prune tests from recent changes."
|
|
4
4
|
argument-hint: "[file ...]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ The core safety invariant: **behavior is locked by green tests before a single l
|
|
|
21
21
|
|
|
22
22
|
## Categories (what counts as slop)
|
|
23
23
|
|
|
24
|
-
The agent looks for these
|
|
24
|
+
The agent looks for these ten categories. The first three are stylistic, the next three are structural, the next two are about hidden cost, the ninth is tests, and the last is size.
|
|
25
25
|
|
|
26
26
|
### Stylistic
|
|
27
27
|
1. **Obvious comments** — comments restating code, trivial docstrings, section dividers, commented-out code, vague TODOs/Notes.
|
|
@@ -62,10 +62,13 @@ The agent looks for these nine categories. The first three are stylistic, the ne
|
|
|
62
62
|
|
|
63
63
|
**Hard rule**: only apply when behavior equivalence is obvious. Do NOT change algorithms with subtle correctness implications. Do NOT micro-optimize hot paths without a benchmark. If in doubt, SKIP.
|
|
64
64
|
|
|
65
|
-
###
|
|
66
|
-
9. **
|
|
65
|
+
### Tests
|
|
66
|
+
9. **Tests: missing or junk** — tests are code in scope like any other file, judged by the one table in the `programming` skill, `references/testing/junk-classes.md` (junk classes, retention bar, R/F/C/D marks, proof rule). Two directions:
|
|
67
|
+
- **Missing**: a behavior in the changed files that no test guards, where the repository keeps tests for that kind of behavior and a regression would otherwise pass unnoticed. ADD the narrowest test that passes the four-question authoring gate (`references/testing/README.md`); name its owner contract. A PROSE file (prompt, `SKILL.md`, rule, markdown) has no behavioral seam: cover only a machine-consumed value (parsed field, sentinel a runtime greps, a doc JSON sample through its real validator) or leave it to review.
|
|
68
|
+
- **Junk**: mark every in-scope test declaration `R` / `F` / `C` / `D`. A `D` or `C` needs the proof the table names for its class BEFORE the cut: for a behavior class (cannot fail, mock echo, source grep, duplicate contract, layer replay, implementation mirror, fabricated postcondition, input transcript) a mutation of the production owner run against the candidate and its named keeper on the unmodified test tree; for a prose pin the assertion line plus the repo's prose rule; for a change certificate the revert that turns it red; for a seam keeper the `knip`/grep showing no production caller; for a copied inventory the generator or consumer that owns it; for a capability restatement the delivery test that owns the promise (none: `F`, not `D`); for near-duplicate cases the one table that keeps every old assertion (typecheck after). A misnamed test is re-marked by what its assertions check, never cut for its name. A mutation proves `D` only when the keeper goes red and the candidate stays green (cannot fail) or both go red (redundant); a candidate that is the ONLY red is `R`, or `C` into the keeper and re-run. Mutations run in Phase 4b, serially, by the orchestrator, never by a batch worker. A test-only production seam (export, flag, getter, reset hook) whose last test was deleted goes with it.
|
|
69
|
+
- KEEP: everything under the retention bar (public API, protocol, config, migration, storage, security, platform, shipped-copy equality, Node-compat suites, issue regressions, machine-consumed text). A test that is red on the unchanged base is a product bug to report, never a deletion. Static or slow is never a reason to delete.
|
|
67
70
|
|
|
68
|
-
###
|
|
71
|
+
### Size
|
|
69
72
|
10. **Oversized modules** — any source file exceeding **250 pure LOC** (non-blank, non-comment lines). This is an architectural defect, not a style preference. Measure: `awk '!/^[[:space:]]*$/ && !/^[[:space:]]*(#|\/\/)/' <file> | wc -l`.
|
|
70
73
|
|
|
71
74
|
**When found, do NOT just flag it. Execute a full modular refactoring:**
|
|
@@ -123,16 +126,17 @@ For each in-scope source file:
|
|
|
123
126
|
|
|
124
127
|
1. Identify the public/observable behavior the file exposes (exported functions, HTTP handlers, CLI commands, classes used elsewhere).
|
|
125
128
|
2. Check whether existing tests cover that behavior. Use `git grep` / project test conventions to find related test files.
|
|
126
|
-
3. **If behavior
|
|
127
|
-
4. Run the test suite (or at minimum the relevant tests).
|
|
129
|
+
3. **If a behavior the cleanup could change is uncovered, write the narrowest test that passes the authoring gate (`programming/references/testing/README.md`) BEFORE editing the file**, and name the behavior it guards. It asserts observable outputs at the real boundary, never the implementation shape the cleanup is about to change, and never a value derived from the code under test. A PROSE file (prompt/`SKILL.md`/rule/markdown) is exempt: its wording is not behavior; skip the test and rely on review, or assert only a machine-consumed value.
|
|
130
|
+
4. Run the test suite (or at minimum the relevant tests). A test that is red on the unchanged base is recorded as a bug (baseline-red list), excluded from every proof, and neither fixed in this pass nor deleted; everything else must be **green** before any cleanup begins.
|
|
131
|
+
5. Run the `programming` skill's static detector over every in-scope test file, the ones written in step 3 included: `bun run <programming skill dir>/scripts/typescript/check-test-junk.ts <test files>` (exit 1 lists `file:line` per junk class it can see: source grep, prose pin, fixed sleep, `.only`/`.skip`, self-comparison, lower-bound cardinality). Its findings are category-9 candidates for the Tests Ledger (marked `R`/`F`/`C`/`D` by the table once its proof runs), never automatic cuts, and a test from step 3 that it flags is rewritten before the pass continues.
|
|
128
132
|
|
|
129
|
-
|
|
133
|
+
Only a broken runner stops the pass: if the suite cannot run at all, STOP and report. Do not proceed with cleanup on unverified ground.
|
|
130
134
|
|
|
131
135
|
### Phase 3: Cleanup plan — existence first, then smells
|
|
132
136
|
|
|
133
137
|
The largest, safest deletion is code that should not have existed. **Before categorizing smells, run the deletion ladder on each changed unit:**
|
|
134
138
|
|
|
135
|
-
- **Delete entirely** — the behavior is not needed (YAGNI, speculative, dead on arrival).
|
|
139
|
+
- **Delete entirely** — the behavior is not needed (YAGNI, speculative, dead on arrival); for a test, its contract is already owned by a keeper or it has none (category 9, with the proof).
|
|
136
140
|
- **Reuse** — an existing helper or pattern in this repo already does it; replace the reimplementation with a call to it.
|
|
137
141
|
- **Platform / stdlib / native / dependency** — the language stdlib, the runtime, or an already-installed dependency already does it (a hand-rolled date picker → `<input type="date">`, a custom query parser → `URLSearchParams`, a bespoke debounce → the util already imported).
|
|
138
142
|
- **Simplify in place** — it must exist; make it smaller.
|
|
@@ -159,11 +163,11 @@ File: src/bar.py
|
|
|
159
163
|
|
|
160
164
|
**Intentional shortcuts:** if the plan deliberately keeps a bounded simplification (a naive scan fine under N rows, a global lock, an O(n²) path), mark it in-code with a `debt:` comment naming the ceiling and the upgrade trigger (in omo, prefix with `// @allow` so the comment-checker treats it as intentional), and list it under "Remaining Risks / Deferred" in the report. That section is the debt ledger — a simplification with a known ceiling and no marker is indistinguishable from a bug.
|
|
161
165
|
|
|
162
|
-
Order rule (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → tests → oversized-modules. This minimizes blast radius of any one change.
|
|
166
|
+
Order rule (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → tests (`F` rows; `D`/`C` wait for Phase 4b) → oversized-modules. This minimizes blast radius of any one change.
|
|
163
167
|
|
|
164
168
|
### Phase 4: Parallel slop removal via `deep-low` agents in batches of 5
|
|
165
169
|
|
|
166
|
-
Files are processed by `deep-low` category agents with the `$omo:remove-ai-slops` skill loaded, **batched 5 at a time in parallel**. The executable skill name is `remove-ai-slops`. The `deep-low` category gives the agent enough thoroughness to correctly evaluate the
|
|
170
|
+
Files are processed by `deep-low` category agents with the `$omo:remove-ai-slops` skill loaded, **batched 5 at a time in parallel**. The executable skill name is `remove-ai-slops`. The `deep-low` category gives the agent enough thoroughness to correctly evaluate the 10 categories and respect the KEEP rules without slipping into surface fixes; the 5-wide batch is the sweet spot — more than 5 creates result-merging noise and context contention, fewer wastes parallelism.
|
|
167
171
|
|
|
168
172
|
**Batching protocol** (strict):
|
|
169
173
|
|
|
@@ -189,9 +193,9 @@ Remove AI slops from: {file_path}
|
|
|
189
193
|
|
|
190
194
|
First run the deletion ladder from Phase 3 on this file (delete entirely / reuse existing repo code / platform-stdlib-native / simplify in place); only code that must exist proceeds to smell removal.
|
|
191
195
|
|
|
192
|
-
Then evaluate EVERY category defined in this skill's "Categories (what counts as slop)" section, applying that section's KEEP and REFACTOR rules verbatim — the Categories section you have loaded is canonical, do not work from a restated subset.
|
|
196
|
+
Then evaluate EVERY category defined in this skill's "Categories (what counts as slop)" section, applying that section's KEEP and REFACTOR rules verbatim — the Categories section you have loaded is canonical, do not work from a restated subset. For a test file, do NOT cut yet: mark every declaration R/F/C/D against `programming/references/testing/junk-classes.md` and return one ledger row per D/C/F (`| test | mark | keeper or reason | proposed proof |`), where the proposed proof names the class's proof kind and, for a mutation, the owner file, the exact find/replace, and the keeper to run. This worker applies `F` rows in the test file it owns and reports them in the ledger as applied; `D`/`C` rows are returned uncut; it never mutates any file to prove a test - the orchestrator runs the proofs in Phase 4b.
|
|
193
197
|
|
|
194
|
-
Apply changes in this order (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → oversized-modules.
|
|
198
|
+
Apply changes in this order (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → tests (`F` rows; `D`/`C` wait for Phase 4b) → oversized-modules.
|
|
195
199
|
|
|
196
200
|
Hard constraints:
|
|
197
201
|
- Behavior MUST be preserved. When equivalence is not obvious, SKIP.
|
|
@@ -201,11 +205,15 @@ Hard constraints:
|
|
|
201
205
|
- Diff stays minimal and scoped to slop removal.
|
|
202
206
|
|
|
203
207
|
Report changes grouped by category. For each change, give before/after, why-slop, why-safe.
|
|
204
|
-
For each skipped issue, give reason.
|
|
208
|
+
For each skipped issue, give reason. For a test file, end with the ledger rows.
|
|
205
209
|
"""
|
|
206
210
|
)
|
|
207
211
|
```
|
|
208
212
|
|
|
213
|
+
### Phase 4b: Prove the test cuts, serially, then cut
|
|
214
|
+
|
|
215
|
+
After the last batch finishes and no worker is running, the orchestrator takes the collected ledger rows. For each row whose proof is a mutation, run the `programming` skill's `bun run <programming skill dir>/scripts/typescript/mutation-proof.ts --file <owner> --find <text> --replace <text> --test 'candidate=<argv>' --test 'keeper=<argv>'` (add `--json` for the ledger; each `--test` value is split on spaces with no quoting, so a filter that contains a space needs a wrapper script or a file-path target instead): it applies the find/replace to the production owner, runs the candidate and the keeper on the unmodified test tree, records both verdicts, and restores the owner byte for byte (sha256-checked) before the next row. One mutation at a time, never while another worker or test run is live, and never two on the same file; a mutation touches a file outside the diff only transiently and is never committed. Rows proven per the table become the cuts; a D or C whose proof fails or was not run is an R. Non-mutation proofs (prose pin, change certificate, seam keeper, copied inventory, capability restatement, near-duplicate cases) are checked here too, from the row's evidence.
|
|
216
|
+
|
|
209
217
|
**Batch failure handling**: a `multi_agent_v1.wait_agent` timeout only means no new mailbox update arrived, not that a `deep-low` agent failed. For long passes, require each child to send `WORKING: <file> - <current phase>` and `BLOCKED: <reason>` only when it cannot progress. Treat a running child as alive. Mark a file for retry only when the child is completed without the deliverable, ack-only after followup, explicitly `BLOCKED:`, or no longer running. Do NOT block the remaining 4 in that batch; collect successful results and retry the failed file once later. If retry also fails, escalate that file under "Issues Found & Fixed" in the final report.
|
|
210
218
|
|
|
211
219
|
### Phase 5: Verify with quality gates + critical review
|
|
@@ -260,8 +268,8 @@ Files: [N files]
|
|
|
260
268
|
|
|
261
269
|
Behavior Lock:
|
|
262
270
|
- Existing coverage: [N files already covered]
|
|
263
|
-
- Tests added: [M
|
|
264
|
-
- Baseline status: GREEN
|
|
271
|
+
- Tests added: [M gate-passing tests at path/to/test_X.py, each naming its contract]
|
|
272
|
+
- Baseline status: GREEN apart from the baseline-red list (recorded as bugs, excluded from proofs, not deleted): [none] OR [list]
|
|
265
273
|
|
|
266
274
|
Cleanup Plan:
|
|
267
275
|
- path/to/file1.ts: [ladder: 1 delete (native) + simplify-in-place] → [dead code → complexity → performance]
|
|
@@ -279,6 +287,12 @@ Per-File Results (each cut shows what replaces it):
|
|
|
279
287
|
- Obvious comments: 5 removed → nothing
|
|
280
288
|
- Over-defensive: 1 simplified (redundant isinstance on typed param)
|
|
281
289
|
|
|
290
|
+
Tests Ledger (category 9; one row per test declaration marked D/C/F, proofs run in Phase 4b):
|
|
291
|
+
| Test | Mark | Keeper / reason | Proof |
|
|
292
|
+
|---|---|---|---|
|
|
293
|
+
| `foo.test.ts > reads the source for "export"` | D | source grep; `foo.test.ts > exports parse()` owns the contract | mutation parse.ts `return tokens` -> `return []`: candidate green, keeper red; restored sha-equal |
|
|
294
|
+
| `skill.test.ts > mentions the gate` | D | prose pin on SKILL.md | assertion line + repo prose rule (no mutation applies) |
|
|
295
|
+
|
|
282
296
|
Quality Gates:
|
|
283
297
|
- Regression tests: PASS (12 tests, 0 failed)
|
|
284
298
|
- Lint: PASS
|
|
@@ -331,7 +345,5 @@ Final Status: CLEAN | ISSUES FIXED | REQUIRES ATTENTION
|
|
|
331
345
|
## Quality Assurance
|
|
332
346
|
|
|
333
347
|
- NEVER remove code that serves a functional purpose.
|
|
334
|
-
-
|
|
335
|
-
-
|
|
336
|
-
- If uncertain about a change, err on the side of keeping the original code.
|
|
337
|
-
- The default action when in doubt is SKIP, not GUESS.
|
|
348
|
+
- Preserve every proven contract: a test leaves only with its proof receipt (category 9), and a missing contract gets a gate-passing test, never a change certificate.
|
|
349
|
+
- When in doubt, SKIP and keep the original; never GUESS.
|
|
@@ -143,7 +143,9 @@ function declareOverKeptRaw(sourcesDir, id, input) {
|
|
|
143
143
|
const dir = join(sourcesDir, String(id))
|
|
144
144
|
if (input.kind !== "converted-only" || !existsSync(join(dir, "raw.bin")) || !existsSync(join(dir, "meta.json"))) return
|
|
145
145
|
const before = JSON.parse(readFileSync(join(dir, "meta.json"), "utf8"))
|
|
146
|
-
|
|
146
|
+
const raw = readFileSync(join(dir, "raw.bin"))
|
|
147
|
+
if (sha256Of(raw) !== before.sha256) throw new CliError(`snapshot_hash_mismatch: ${id} raw.bin no longer matches its pinned hash`)
|
|
148
|
+
Object.assign(input, { url: before.url, raw, kind: before.kind, observed_at: before.observed_at, declared: true })
|
|
147
149
|
delete input.path
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, expect, onTestFinished, test } from "bun:test"
|
|
2
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
|
|
6
|
-
import { sha256Of } from "./cli-support.mjs"
|
|
6
|
+
import { CliError, sha256Of } from "./cli-support.mjs"
|
|
7
7
|
import { addRow, readLedger, writeLedger } from "./fact-ledger.mjs"
|
|
8
8
|
import { run } from "./report-tools.mjs"
|
|
9
|
+
import { writeSnapshot } from "./source-snapshot.mjs"
|
|
9
10
|
|
|
10
11
|
const BRIEF = readFileSync(join(import.meta.dir, "tests/fixtures/fidelity-brief-locked.md"), "utf8")
|
|
11
12
|
const PAGE = '<html><body><p>The main deck spans <span title="9 m">320</span> m in total.</p><table id="sizes"><tr><th>Name</th><th>Length</th></tr><tr><td>Deck</td><td>320 m</td></tr></table></body></html>'
|
|
@@ -127,6 +128,42 @@ describe("converted text is never a raw snapshot", () => {
|
|
|
127
128
|
})
|
|
128
129
|
})
|
|
129
130
|
|
|
131
|
+
describe("replacement over retained raw bytes", () => {
|
|
132
|
+
for (const [name, tampered, recaptured] of [
|
|
133
|
+
["healthy retained bytes", false, false],
|
|
134
|
+
["tampered retained bytes", true, false],
|
|
135
|
+
["an explicit new capture after retained drift", true, true],
|
|
136
|
+
] as const) {
|
|
137
|
+
test(`#given ${name} #when text replaces the extraction #then only validated retained bytes keep their original observation`, async () => {
|
|
138
|
+
const dir = session()
|
|
139
|
+
const observedAt = "2026-01-01T00:00:00.000Z"
|
|
140
|
+
const { dir: source } = writeSnapshot(join(dir, "sources"), "S1", {
|
|
141
|
+
url: "https://example.invalid/bridge", html: PLAIN, extractor: "model", observed_at: observedAt,
|
|
142
|
+
})
|
|
143
|
+
if (tampered) writeFileSync(join(source, "raw.bin"), PLAIN.replace("320", "330"))
|
|
144
|
+
const before = Object.fromEntries(readdirSync(source).map((name) => [name, readFileSync(join(source, name))]))
|
|
145
|
+
const raw = recaptured ? "A new capture says 340 m." : PLAIN
|
|
146
|
+
const text = recaptured ? raw : "The corrected extraction says 320 m."
|
|
147
|
+
const replaced = snapshot(dir, "S1", "--text", file(dir, "corrected.txt", text), "--replace",
|
|
148
|
+
...(recaptured ? ["--raw", file(dir, "recaptured.txt", raw)] : []))
|
|
149
|
+
|
|
150
|
+
if (tampered && !recaptured) {
|
|
151
|
+
await expect(replaced).rejects.toBeInstanceOf(CliError)
|
|
152
|
+
expect(Object.fromEntries(readdirSync(source).map((name) => [name, readFileSync(join(source, name))]))).toEqual(before)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
expect((await replaced).exitCode).toBe(0)
|
|
157
|
+
expect(readFileSync(join(source, "raw.bin"), "utf8")).toBe(raw)
|
|
158
|
+
expect(readFileSync(join(source, "text.txt"), "utf8")).toBe(text)
|
|
159
|
+
const meta = JSON.parse(readFileSync(join(source, "meta.json"), "utf8"))
|
|
160
|
+
expect(meta).toMatchObject({ sha256: sha256Of(raw), text_source: "declared", text_sha256: sha256Of(text) })
|
|
161
|
+
if (recaptured) expect(meta.observed_at).not.toBe(observedAt)
|
|
162
|
+
else expect(meta.observed_at).toBe(observedAt)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
130
167
|
// chmod 000 does not stop the owner reading a file on Windows (no POSIX mode bits), so there the same
|
|
131
168
|
// fail-closed condition is produced the way an unreadable path occurs on win32: raw.bin is replaced by
|
|
132
169
|
// a directory, and reading it fails (EISDIR). Both paths must make verify exit 1 with the cause.
|