@olegkoval/agent-skills 1.58.1 → 1.59.0
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/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +105 -9
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/prover.md +18 -6
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/fix-mode.md +2 -1
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/github-post.md +6 -1
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/traces.md +39 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/deps.mjs +305 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/revmux-engine.sh +7 -2
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/run-log.mjs +344 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +107 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +16 -1
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/test-runner.mjs +75 -0
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +105 -9
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/prover.md +18 -6
- package/plugins/olko-github-pr/skills/lekker-review/references/fix-mode.md +2 -1
- package/plugins/olko-github-pr/skills/lekker-review/references/github-post.md +6 -1
- package/plugins/olko-github-pr/skills/lekker-review/references/traces.md +39 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/deps.mjs +305 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/revmux-engine.sh +7 -2
- package/plugins/olko-github-pr/skills/lekker-review/scripts/run-log.mjs +344 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +107 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +16 -1
- package/plugins/olko-github-pr/skills/lekker-review/scripts/test-runner.mjs +75 -0
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +14 -2
- package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
|
@@ -141,6 +141,12 @@ returns the number checked as `hardRuleCount`.
|
|
|
141
141
|
Resolve every `references/...` and script path relative to `SKILL_ROOT`; never
|
|
142
142
|
assume a `~/.claude/skills/` installation path.
|
|
143
143
|
|
|
144
|
+
Run one simple command per shell call. Do not prefix commands with
|
|
145
|
+
`cd <dir> &&`; use absolute paths, `git -C <dir>`, and `grep -rn <pattern> <dir>`
|
|
146
|
+
instead. Permission checkers cannot vet a compound or `cd`-prefixed command,
|
|
147
|
+
so in an unattended or auto-approved run each one becomes a denial and a
|
|
148
|
+
wasted turn.
|
|
149
|
+
|
|
144
150
|
## Host selection
|
|
145
151
|
|
|
146
152
|
Set `HOST=codex` when native Codex collaboration tools such as `spawn_agent`
|
|
@@ -159,6 +165,39 @@ single-agent review is not equivalent to lekker-review.
|
|
|
159
165
|
|
|
160
166
|
## Step 0 - Parse input
|
|
161
167
|
|
|
168
|
+
### Dependencies (first thing, every run)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
node ${SKILL_ROOT}/scripts/deps.mjs check # add --unattended when no human is present
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Milliseconds: it only checks presence. The upstream version check needs the
|
|
175
|
+
network, so it runs at most once per `cadenceDays` (default 7) and the ledger
|
|
176
|
+
`~/.local/state/lekker-review/deps.json` records when it last ran. Act on `state`:
|
|
177
|
+
|
|
178
|
+
- `first-run`: ask ONCE (AskUserQuestion in Claude Code): a multiSelect of the
|
|
179
|
+
`offer[]` tools (label = tool, description = `usedBy`), plus "keep them
|
|
180
|
+
updated automatically?" yes/no. Then
|
|
181
|
+
`deps.mjs init --install <picked> --skip <the rest> --auto-update on|off`.
|
|
182
|
+
A tool not picked is recorded as skipped and never offered again; the user can
|
|
183
|
+
still run `deps.mjs install <tool>` later. Unattended: do not ask, do not
|
|
184
|
+
install; carry on and say `deps not initialised` in the header.
|
|
185
|
+
- `missing-required`: stop and print the `missing` list. The review needs them.
|
|
186
|
+
- `undecided` non-empty (a tool added to the registry since init): ask about
|
|
187
|
+
those tools only, then `deps.mjs install <tool>` or `deps.mjs skip <tool>`.
|
|
188
|
+
- `outdated` non-empty and `updated` null: one line, e.g.
|
|
189
|
+
`deps outdated: revmux 0.2.6 -> 0.2.7 (deps.mjs update)`. With auto-update on,
|
|
190
|
+
`check` already upgraded them when the check was due; report `updated`.
|
|
191
|
+
- `revmuxPromptsInSync: false`: run
|
|
192
|
+
`bash ${SKILL_ROOT}/scripts/install-revmux-prompts.sh`, which resyncs the
|
|
193
|
+
lekker profiles into `~/.config/revmux` (`deps.mjs update revmux` does the
|
|
194
|
+
same after upgrading).
|
|
195
|
+
- An install that returns `manual` (no Homebrew on PATH) prints the command to
|
|
196
|
+
run by hand; pass it on, do not improvise another installer.
|
|
197
|
+
|
|
198
|
+
`ENGINE=revmux` additionally needs revmux `present`; when it is missing or
|
|
199
|
+
skipped, fall back to the host's native engine and say why in the header.
|
|
200
|
+
|
|
162
201
|
### Pick the mode first
|
|
163
202
|
|
|
164
203
|
`MODE=branch` when ANY of these hold:
|
|
@@ -313,6 +352,25 @@ treat as a full review and leave PREV_SHA unset. Also grep the same file for
|
|
|
313
352
|
- Step 3.5 reuses that exact URL or local path so the artifact stays a living
|
|
314
353
|
page for this PR.
|
|
315
354
|
|
|
355
|
+
**Open the run log** once mode, depth and engine are settled (see
|
|
356
|
+
`references/traces.md` for where every trace of a run lives):
|
|
357
|
+
|
|
358
|
+
Resolve the run-log script path in every shell call. The examples below use
|
|
359
|
+
Claude Code's `CLAUDE_PLUGIN_ROOT`. In Codex, replace the quoted script path in
|
|
360
|
+
each invocation with the absolute path to `scripts/run-log.mjs` beside the
|
|
361
|
+
selected `SKILL.md`. Do not rely on a shell variable set by an earlier call.
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/run-log.mjs" start --repo <REPO_SLUG> --target <TARGET_SLUG> --mode <MODE> \
|
|
365
|
+
--depth <depth> --engine <ENGINE> [--fix]
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Keep the returned `runId` as `RUN_ID`. Then run
|
|
369
|
+
`node "${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/run-log.mjs" mark <RUN_ID> <phase>`
|
|
370
|
+
at each step boundary: `context` (Step 1), `review` (Step 2), `synthesis`
|
|
371
|
+
(Step 3), `fix` (Step 4), `post` (Step 5). A marker is one line of JSON; never
|
|
372
|
+
skip the review because the log failed, just say `run log unavailable`.
|
|
373
|
+
|
|
316
374
|
---
|
|
317
375
|
|
|
318
376
|
## Depth gate
|
|
@@ -389,7 +447,14 @@ On completion, read `worktree.json`. Keys emitted:
|
|
|
389
447
|
`worktreePath`, `repoRoot`, `headSha`, `headShaShort`, `tscTail`,
|
|
390
448
|
`tscChangedTail`, `tscErrorCount`, `eslintTail`, `eslintScope`, `eslintJson`,
|
|
391
449
|
`changedFiles`, `baseRef`, `mergeBase`, `projectRules`, `deltaFile`, `notes`,
|
|
392
|
-
`depsInstall`.
|
|
450
|
+
`depsInstall`, `testRunner`.
|
|
451
|
+
|
|
452
|
+
`testRunner` is `{runner, dirs, reason}` from `scripts/test-runner.mjs`:
|
|
453
|
+
`runner` is `vitest|jest|mocha|node-test` only when its binary is installed
|
|
454
|
+
in the worktree, `dirs` lists `{dir, runner}` pairs for each usable package
|
|
455
|
+
(including binaries installed in a parent dir within the worktree), and `reason`
|
|
456
|
+
says why `runner` is null. Pass it to the workflow unchanged; Prove skips
|
|
457
|
+
without one instead of paying provers to find the missing `node_modules`.
|
|
393
458
|
|
|
394
459
|
`notes` containing `changed-files-mismatch:<got>-vs-<expected>` means the
|
|
395
460
|
merge-base still disagrees with the PR. Use the `files` list from `gh pr view`
|
|
@@ -602,7 +667,10 @@ First copy the script into the scratchpad. The Workflow tool rejects a
|
|
|
602
667
|
`scriptPath` outside the working directory or added dirs ("scriptPath must be a
|
|
603
668
|
script path this tool returned, or a file you can already read"), and the skill
|
|
604
669
|
dir is usually neither: `cp ${SKILL_ROOT}/workflow.js <scratchpad>/workflow.js`.
|
|
605
|
-
If
|
|
670
|
+
If `workflow.js` is already in the working directory (an unattended runner may
|
|
671
|
+
stage it there), use that copy and skip the `cp`: some permission checks deny
|
|
672
|
+
reading files out of the skill dir. If the Workflow tool still rejects the
|
|
673
|
+
path, pass the script inline via `script` instead.
|
|
606
674
|
|
|
607
675
|
Invoke the Workflow tool:
|
|
608
676
|
|
|
@@ -618,7 +686,8 @@ args: {
|
|
|
618
686
|
contextFile: "<scratchpad>/context.json",
|
|
619
687
|
worktreePath: <null for scan, else from worktree.json>,
|
|
620
688
|
promptDir: "${SKILL_ROOT}/references/agents",
|
|
621
|
-
prevSha: <null unless re-review
|
|
689
|
+
prevSha: <null unless re-review>,
|
|
690
|
+
testRunner: <worktree.json testRunner, null for scan>
|
|
622
691
|
}
|
|
623
692
|
```
|
|
624
693
|
|
|
@@ -663,14 +732,16 @@ The workflow runs three phases:
|
|
|
663
732
|
thirty-finding PR launches thirty concurrent agents.
|
|
664
733
|
- **Critic (deep only):** completeness critic gets the full deduped finding
|
|
665
734
|
list + DIFF_FILE; its findings go through verifier agents before promotion.
|
|
666
|
-
- **Prove (medium/deep, worktree
|
|
667
|
-
one prover agent (`references/agents/prover.md`, sonnet, max 5, run
|
|
735
|
+
- **Prove (medium/deep, worktree and an installed test runner required):**
|
|
736
|
+
each Critical without a `rule` tag gets one prover agent (`references/agents/prover.md`, sonnet, max 5, run
|
|
668
737
|
`maxConcurrent` at a time) that writes a test asserting the CORRECT behaviour,
|
|
669
738
|
runs it in the worktree, and captures it failing because of the bug. The proof rides on the finding as
|
|
670
739
|
`proof: {attempted, proven, outcome, reason, testCode?, testCommand?, redOutput?}`.
|
|
671
740
|
A proof that comes back GREEN (code behaved correctly) is counter-evidence -
|
|
672
741
|
the workflow automatically downgrades the finding from Critical to Important.
|
|
673
|
-
|
|
742
|
+
Rule-tagged findings are never proved (policy violations have no failing
|
|
743
|
+
test). With `testRunner.runner` null, no prover runs and each candidate
|
|
744
|
+
carries `proof: {attempted: false, reason: "no runnable test runner: ..."}`.
|
|
674
745
|
|
|
675
746
|
Findings have schema:
|
|
676
747
|
`{file, line, severity, title, description, badCode, fix, rule?, precedent?, agreedBy?, verificationStatus?, verifierReasoning?, proof?}`
|
|
@@ -732,7 +803,8 @@ round; Prove runs through the current host's native engine. Steps:
|
|
|
732
803
|
adapter's `agents` array has `degraded: true`. State the fallback in the
|
|
733
804
|
review header (`**Note:** revmux unavailable - fell back to workflow
|
|
734
805
|
engine` or `...fell back to codex engine`) and re-run the native path from
|
|
735
|
-
the top of Step 2.
|
|
806
|
+
the top of Step 2. Quote the last lines of `<round dir>/revmux.stderr.log`
|
|
807
|
+
in that note: it is the only record of why the agents failed.
|
|
736
808
|
|
|
737
809
|
**Trust boundary:** always pass `--config-dir ~/.config/revmux` explicitly.
|
|
738
810
|
Without it revmux also reads the reviewed repo's checked-in `.revmux/`, which
|
|
@@ -972,6 +1044,24 @@ Write it against the sha that was actually reviewed. When fix mode later adds
|
|
|
972
1044
|
commits, write a marker for the NEW head sha too - a gate keys on the tip
|
|
973
1045
|
that is about to be pushed, and a marker for a superseded sha would be a lie.
|
|
974
1046
|
|
|
1047
|
+
**Close the run log** right after the receipt, then print the review:
|
|
1048
|
+
|
|
1049
|
+
```bash
|
|
1050
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/run-log.mjs" attach <RUN_ID> <scratchpad>/findings.json <scratchpad>/worktree.json \
|
|
1051
|
+
<scratchpad>/preflight.json <scratchpad>/context.json
|
|
1052
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/run-log.mjs" finish <RUN_ID> --status ok --verdict lgtm|lgtm-with-changes|needs-work \
|
|
1053
|
+
--review-file "$REVIEW_FILE" [--wf <wf_id of the lekker-review-core run>]
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
`--wf` is the `runId` the Claude Code Workflow tool returned (omit it on other
|
|
1057
|
+
hosts). `finish` copies duration, tokens, agent count, verifier drops and
|
|
1058
|
+
prove results out of the harness's own record, which Claude Code deletes with
|
|
1059
|
+
old sessions. When fix mode, the artifact or a post follows, call `finish`
|
|
1060
|
+
again at the very end using the resolved script path, with every `--wf` id of
|
|
1061
|
+
the run (review and fix) and
|
|
1062
|
+
`--artifact-url`: the newest row wins. A run that dies part-way still gets
|
|
1063
|
+
`node "${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/run-log.mjs" finish <RUN_ID> --status failed --note <why>`.
|
|
1064
|
+
|
|
975
1065
|
Then print the full review as the response.
|
|
976
1066
|
|
|
977
1067
|
**Closer goes last.** The run's FINAL message (this one, or the later summary
|
|
@@ -1038,8 +1128,9 @@ Read `references/fix-mode.md` and follow it. Shape of the run:
|
|
|
1038
1128
|
its severity. Never auto-fix any other Observation or Idiomatic finding, or a
|
|
1039
1129
|
title/process rule.
|
|
1040
1130
|
3. Invoke the host's fix workflow. In Claude, copy `fix-workflow.js` to the
|
|
1041
|
-
scratchpad first (same reason as the review workflow;
|
|
1042
|
-
path is still
|
|
1131
|
+
scratchpad first (same reason as the review workflow; skip the copy when it
|
|
1132
|
+
is already in the working directory; inline `script` if the path is still
|
|
1133
|
+
rejected), then run:
|
|
1043
1134
|
```
|
|
1044
1135
|
scriptPath: <scratchpad>/fix-workflow.js
|
|
1045
1136
|
args: { repoSlug, prNumber, targetLabel, worktreePath, diffFile, contextFile,
|
|
@@ -1233,6 +1324,11 @@ If `repoRoot` starts with `/tmp/lekker-clone-`, also remove that clone dir.
|
|
|
1233
1324
|
Remove the delta file if `deltaFile` was set in worktree.json. Verify with
|
|
1234
1325
|
`git -C <repoRoot> worktree list` that no `lekker-review` entries remain.
|
|
1235
1326
|
|
|
1327
|
+
**Measuring changes.** `node "${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/run-log.mjs" report --days 30`
|
|
1328
|
+
prints duration, tokens, findings, verifier drop rate and prove yield per
|
|
1329
|
+
engine and depth. Run it before and after a change to `workflow.js` or the
|
|
1330
|
+
prompts and quote both. `backfill` imports past runs (idempotent).
|
|
1331
|
+
|
|
1236
1332
|
---
|
|
1237
1333
|
|
|
1238
1334
|
## Failure rules
|
|
@@ -44,9 +44,10 @@ hold:
|
|
|
44
44
|
network) and the repo has no test infra to fake it cheaply.
|
|
45
45
|
- The buggy logic is not importable/reachable from a test without large
|
|
46
46
|
scaffolding (deep framework wiring, a webhook server bootstrap).
|
|
47
|
-
-
|
|
48
|
-
`
|
|
49
|
-
|
|
47
|
+
- The code under test lives outside every package dir the prompt's
|
|
48
|
+
`TEST_RUNNER` line lists. The workflow already checked those dirs for an
|
|
49
|
+
installed runner binary, so do not re-check; a finding in any other package
|
|
50
|
+
has nothing to run its test.
|
|
50
51
|
|
|
51
52
|
Deciding "not testable" quickly is a GOOD outcome, not a failure of yours --
|
|
52
53
|
say why in one sentence and stop. Do not burn effort scaffolding around a fake
|
|
@@ -56,7 +57,8 @@ IO layer just to force a test into existence.
|
|
|
56
57
|
|
|
57
58
|
## Step 2 -- Write the test
|
|
58
59
|
|
|
59
|
-
One file at the
|
|
60
|
+
One file at the root of the listed package dir that contains the finding
|
|
61
|
+
(`.` means the worktree root) named `lekker-proof-<file-slug>-<line>.test.ts`,
|
|
60
62
|
where `<file-slug>` is the finding's file path with every `/` and `.` replaced
|
|
61
63
|
by `-` (e.g. finding at `src/sync/orders.ts:142` ->
|
|
62
64
|
`lekker-proof-src-sync-orders-ts-142.test.ts`). The slug matters: another
|
|
@@ -77,10 +79,20 @@ repo's real test directories and trivially findable for cleanup.
|
|
|
77
79
|
|
|
78
80
|
## Step 3 -- Run it
|
|
79
81
|
|
|
80
|
-
Exactly one run command, scoped to your file only
|
|
82
|
+
Exactly one run command from that package dir, scoped to your file only, for
|
|
83
|
+
the runner paired with that package dir in the `TEST_RUNNER` mapping
|
|
84
|
+
(use the most specific package dir containing the finding):
|
|
81
85
|
|
|
82
86
|
- vitest: `npx vitest run lekker-proof-<file-slug>-<line>.test.ts --no-coverage --reporter=verbose`
|
|
83
87
|
- jest: `npx jest lekker-proof-<file-slug>-<line>.test.ts --ci`
|
|
88
|
+
- mocha: `npx mocha lekker-proof-<file-slug>-<line>.test.ts`
|
|
89
|
+
- node-test: `node --test lekker-proof-<file-slug>-<line>.test.ts` (needs a
|
|
90
|
+
runtime that strips types; if it rejects TypeScript, that is an environment
|
|
91
|
+
failure, below)
|
|
92
|
+
|
|
93
|
+
When the package dir is not `.`, prefix the command with
|
|
94
|
+
`cd <package dir> && ` and keep that prefix in `testCommand`: fix mode
|
|
95
|
+
re-runs the proof from the worktree root.
|
|
84
96
|
|
|
85
97
|
Set the Bash tool's `timeout` parameter to 120000 for this call.
|
|
86
98
|
|
|
@@ -114,7 +126,7 @@ Delete your test file and verify the worktree is exactly as clean as you
|
|
|
114
126
|
found it:
|
|
115
127
|
|
|
116
128
|
```bash
|
|
117
|
-
rm lekker-proof-<file-slug>-<line>.test.ts
|
|
129
|
+
rm <package dir>/lekker-proof-<file-slug>-<line>.test.ts
|
|
118
130
|
git -C <WORKTREE_PATH> status --porcelain
|
|
119
131
|
```
|
|
120
132
|
|
|
@@ -189,7 +189,8 @@ For each proven finding in a committable group:
|
|
|
189
189
|
|
|
190
190
|
1. **Re-materialize the test**: write `proof.testCode` back to its original
|
|
191
191
|
filename (the same `lekker-proof-<file-slug>-<line>.test.ts` name it was captured
|
|
192
|
-
under)
|
|
192
|
+
under) in the package dir `proof.testCommand` starts with `cd` into, or at
|
|
193
|
+
the worktree root when it has no `cd` prefix.
|
|
193
194
|
2. **Run** exactly `proof.testCommand` (Bash `timeout` 120000).
|
|
194
195
|
3. **Judge**:
|
|
195
196
|
- **PASSES now** -> the fix demonstrably resolves the finding. Record
|
|
@@ -133,9 +133,14 @@ comments.
|
|
|
133
133
|
```bash
|
|
134
134
|
gh api "repos/<REPO_SLUG>/pulls/<PR_NUMBER>/reviews" \
|
|
135
135
|
--method POST \
|
|
136
|
-
--input /tmp/lekker-post-$$.json
|
|
136
|
+
--input /tmp/lekker-post-$$.json # pending-ok
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
The payload has no `event` field on purpose: the review lands as a PENDING
|
|
140
|
+
draft for the user to submit. Keep the `# pending-ok` comment. Guard hooks
|
|
141
|
+
that block event-less review POSTs (they would otherwise create an invisible
|
|
142
|
+
draft by accident) let a command through when it carries that marker.
|
|
143
|
+
|
|
139
144
|
---
|
|
140
145
|
|
|
141
146
|
## Step 4 -- Handle 422 errors
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Where a run leaves traces
|
|
2
|
+
|
|
3
|
+
One table, so nobody has to rediscover this. The run log is the index: it
|
|
4
|
+
points at, and copies the numbers out of, everything else.
|
|
5
|
+
|
|
6
|
+
| Trace | Path | Written by | Kept | What it answers |
|
|
7
|
+
|---|---|---|---|---|
|
|
8
|
+
| Run ledger | `~/.local/state/lekker-review/runs.jsonl` | `scripts/run-log.mjs finish`, `backfill` | forever (one line per run) | how long, how many tokens, findings, drops, proofs, verdict, per engine and depth |
|
|
9
|
+
| Run archive | `~/.local/state/lekker-review/runs/<runId>/` | `run-log.mjs start/mark/attach/finish` | 90 days (`prune`) | `run.json` with timeline, plus the run's `findings.json`, `worktree.json`, `preflight.json`, `context.json` |
|
|
10
|
+
| Dependency ledger | `~/.local/state/lekker-review/deps.json` | `scripts/deps.mjs` | forever | which optional tools were installed or skipped, versions, last upstream check |
|
|
11
|
+
| Review report | `~/code-reviews/<date>-<target>-<repo>.md` | Step 3 | forever | the review as printed, including the `Review Cost` block |
|
|
12
|
+
| revmux archive | `~/code-reviews/revmux-tasks/<task>/<NN-run>/` | revmux, plus `revmux.stderr.log` from `scripts/revmux-engine.sh` | forever, never edit | `events.jsonl` (agent start, degraded, retried), `agents/*.jsonl` (per-agent cost and usage), `revmux.stderr.log` (why a round degraded; rounds before 2026-10-20 have none) |
|
|
13
|
+
| Harness workflow record | `~/.claude/projects/*/<session>/workflows/wf_*.json` | Claude Code Workflow tool | until Claude Code cleans the session | the source `finish` copies from: `durationMs`, `totalTokens`, `agentCount`, `result` |
|
|
14
|
+
| Cache | `~/.cache/lekker-review/` | Steps 1, 3, 5 | disposable | `reviewed/<sha>` branch-mode markers, `bot-precision.jsonl`, `repo-map.tsv` |
|
|
15
|
+
| Pattern memory | ICM (`icm recall`) | Step 5 housekeeping | ICM decay | recurring review and false-positive patterns per repo |
|
|
16
|
+
|
|
17
|
+
`LEKKER_STATE_DIR` moves the state root (tests use it). `LEKKER_CLAUDE_PROJECTS`
|
|
18
|
+
and `LEKKER_REVMUX_TASKS_DIR` point `run-log.mjs` at other harness and revmux
|
|
19
|
+
roots.
|
|
20
|
+
|
|
21
|
+
## Reading it
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node ${SKILL_ROOT}/scripts/run-log.mjs report --days 30 # table
|
|
25
|
+
node ${SKILL_ROOT}/scripts/run-log.mjs report --days 90 --json # for scripts
|
|
26
|
+
jq -c 'select(.verdict=="needs-work")' ~/.local/state/lekker-review/runs.jsonl # ad hoc
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`report` keeps the newest row per `runId`, so a run finished twice (review,
|
|
30
|
+
then fix) counts once. Rows with `source: harness-backfill` or
|
|
31
|
+
`revmux-backfill` were imported from history and have no timeline or verdict.
|
|
32
|
+
|
|
33
|
+
## Known gaps
|
|
34
|
+
|
|
35
|
+
- Backfilled harness rows cannot tell which review fell back from revmux to the
|
|
36
|
+
workflow engine; new runs record the engine at `start`.
|
|
37
|
+
- revmux tokens include cache reads, workflow `totalTokens` does not break them
|
|
38
|
+
out. Compare cost in dollars or wall time, not raw tokens, across engines.
|
|
39
|
+
- Verdict and severity counts exist only for runs logged with `finish`.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// First-run setup and dependency upkeep for lekker-review.
|
|
3
|
+
//
|
|
4
|
+
// Required tools are only checked, never installed: a review cannot run
|
|
5
|
+
// without them, and installing git or node behind someone's back is not this
|
|
6
|
+
// skill's call. Optional tools are offered once, on the first run, and the
|
|
7
|
+
// answer is remembered. A tool the user skipped is never offered again until
|
|
8
|
+
// `reset`.
|
|
9
|
+
//
|
|
10
|
+
// The upstream version check needs the network, so it runs on a cadence
|
|
11
|
+
// (default 7 days) and not on every review. The ledger records when it last
|
|
12
|
+
// ran and what it found.
|
|
13
|
+
//
|
|
14
|
+
// Usage:
|
|
15
|
+
// node deps.mjs check [--force] [--unattended] fast presence check; full version check when due
|
|
16
|
+
// node deps.mjs init [--install a,b] [--skip c] [--auto-update on|off] [--cadence-days N]
|
|
17
|
+
// node deps.mjs install <tool>
|
|
18
|
+
// node deps.mjs update [tool] upgrade outdated opted-in tools, resync revmux prompts
|
|
19
|
+
// node deps.mjs skip <tool>
|
|
20
|
+
// node deps.mjs reset forget every choice; the next check is a first run
|
|
21
|
+
//
|
|
22
|
+
// Ledger: ${LEKKER_STATE_DIR:-~/.local/state/lekker-review}/deps.json
|
|
23
|
+
// Every command prints one JSON object on stdout. Exit 0 unless the command
|
|
24
|
+
// itself failed; a missing required tool is reported as `state`, not an exit.
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { dirname, join } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const SKILL_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const STATE_DIR = process.env.LEKKER_STATE_DIR || join(homedir(), '.local', 'state', 'lekker-review');
|
|
34
|
+
const LEDGER = join(STATE_DIR, 'deps.json');
|
|
35
|
+
const DAY_MS = 86400000;
|
|
36
|
+
// A failed upstream lookup is retried, not recorded as "up to date". This keeps
|
|
37
|
+
// a flaky network from re-running the lookup on every single review.
|
|
38
|
+
const RETRY_AFTER_MS = 6 * 3600000;
|
|
39
|
+
|
|
40
|
+
const REQUIRED = ['git', 'gh', 'node', 'npx', 'jq', 'python3'];
|
|
41
|
+
|
|
42
|
+
// `usedBy` is shown in the first-run question, so the user knows what each
|
|
43
|
+
// tool buys before agreeing to install it.
|
|
44
|
+
const OPTIONAL = {
|
|
45
|
+
revmux: {
|
|
46
|
+
usedBy: '--engine revmux (alternative review engine)',
|
|
47
|
+
versionCmd: ['revmux', ['--version']],
|
|
48
|
+
repo: 'umputun/revmux',
|
|
49
|
+
brew: ['install', '--cask', 'umputun/apps/revmux'],
|
|
50
|
+
brewUpgrade: ['upgrade', '--cask', 'umputun/apps/revmux'],
|
|
51
|
+
postInstall: ['bash', [join(SKILL_DIR, 'scripts', 'install-revmux-prompts.sh')]],
|
|
52
|
+
},
|
|
53
|
+
icm: {
|
|
54
|
+
usedBy: 'pattern memory: Track B recall and Step 5 housekeeping',
|
|
55
|
+
versionCmd: ['icm', ['--version']],
|
|
56
|
+
repo: 'rtk-ai/icm',
|
|
57
|
+
brew: ['install', 'rtk-ai/tap/icm'],
|
|
58
|
+
brewUpgrade: ['upgrade', 'rtk-ai/tap/icm'],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const argv = process.argv.slice(2);
|
|
63
|
+
const cmd = argv[0] || 'check';
|
|
64
|
+
|
|
65
|
+
function flag(name, fallback = null) {
|
|
66
|
+
const i = argv.indexOf('--' + name);
|
|
67
|
+
return i === -1 ? fallback : argv[i + 1];
|
|
68
|
+
}
|
|
69
|
+
const has = (name) => argv.includes('--' + name);
|
|
70
|
+
|
|
71
|
+
function run(bin, args, timeout = 15000) {
|
|
72
|
+
try {
|
|
73
|
+
return { ok: true, out: execFileSync(bin, args, { encoding: 'utf8', timeout, stdio: ['ignore', 'pipe', 'pipe'] }).trim() };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return { ok: false, out: String(err.stderr || err.message).trim().split('\n').slice(-3).join(' ') };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onPath = (bin) => run('/bin/sh', ['-c', `command -v ${bin}`]).ok;
|
|
80
|
+
const semver = (text) => (String(text).match(/(\d+)\.(\d+)\.(\d+)/) || [])[0] || null;
|
|
81
|
+
|
|
82
|
+
function newer(latest, installed) {
|
|
83
|
+
if (!latest || !installed) return false;
|
|
84
|
+
const a = latest.split('.').map(Number);
|
|
85
|
+
const b = installed.split('.').map(Number);
|
|
86
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// brew is looked up explicitly: the agent's shell PATH often lacks it, and MDM
|
|
91
|
+
// has deleted the /opt/homebrew/bin/brew symlink on this fleet before.
|
|
92
|
+
function findBrew() {
|
|
93
|
+
for (const p of ['/opt/homebrew/bin/brew', '/usr/local/bin/brew', '/home/linuxbrew/.linuxbrew/bin/brew']) {
|
|
94
|
+
if (existsSync(p)) return p;
|
|
95
|
+
}
|
|
96
|
+
return onPath('brew') ? 'brew' : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function loadLedger() {
|
|
100
|
+
if (!existsSync(LEDGER)) return null;
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(readFileSync(LEDGER, 'utf8'));
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function saveLedger(ledger) {
|
|
109
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
110
|
+
const tmp = LEDGER + '.tmp';
|
|
111
|
+
writeFileSync(tmp, JSON.stringify(ledger, null, 2) + '\n');
|
|
112
|
+
renameSync(tmp, LEDGER);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function blankLedger() {
|
|
116
|
+
return { schema: 1, initialisedAt: null, cadenceDays: 7, autoUpdate: false, checkedAt: null, lastAttemptAt: null, tools: {} };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function installedVersion(name) {
|
|
120
|
+
const [bin, args] = OPTIONAL[name].versionCmd;
|
|
121
|
+
if (!onPath(bin)) return null;
|
|
122
|
+
const r = run(bin, args);
|
|
123
|
+
return r.ok ? semver(r.out) : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function latestVersion(name) {
|
|
127
|
+
const r = run('gh', ['release', 'view', '-R', OPTIONAL[name].repo, '--json', 'tagName', '-q', '.tagName']);
|
|
128
|
+
return r.ok ? semver(r.out) : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function requiredMissing() {
|
|
132
|
+
return REQUIRED.filter((bin) => !onPath(bin));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function presence(ledger) {
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const [name, entry] of Object.entries(ledger.tools)) {
|
|
138
|
+
if (entry.choice !== 'installed' || !Object.hasOwn(OPTIONAL, name)) continue;
|
|
139
|
+
out[name] = onPath(OPTIONAL[name].versionCmd[0]) ? 'present' : 'missing';
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The network half of `check`. Updates the ledger in place.
|
|
145
|
+
function fullCheck(ledger, now) {
|
|
146
|
+
ledger.lastAttemptAt = new Date(now).toISOString();
|
|
147
|
+
let allResolved = true;
|
|
148
|
+
for (const [name, entry] of Object.entries(ledger.tools)) {
|
|
149
|
+
if (entry.choice !== 'installed' || !Object.hasOwn(OPTIONAL, name)) continue;
|
|
150
|
+
entry.version = installedVersion(name);
|
|
151
|
+
const latest = latestVersion(name);
|
|
152
|
+
if (latest) entry.latest = latest;
|
|
153
|
+
else allResolved = false;
|
|
154
|
+
entry.outdated = newer(entry.latest, entry.version);
|
|
155
|
+
}
|
|
156
|
+
if (allResolved) ledger.checkedAt = ledger.lastAttemptAt;
|
|
157
|
+
if (existsSync(join(SKILL_DIR, 'scripts', 'install-revmux-prompts.sh')) && ledger.tools.revmux?.choice === 'installed') {
|
|
158
|
+
const r = run('bash', [join(SKILL_DIR, 'scripts', 'install-revmux-prompts.sh'), '--check']);
|
|
159
|
+
ledger.tools.revmux.promptsInSync = r.ok;
|
|
160
|
+
}
|
|
161
|
+
return allResolved;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isDue(ledger, now) {
|
|
165
|
+
const age = (iso) => (iso ? now - Date.parse(iso) : Infinity);
|
|
166
|
+
return age(ledger.checkedAt) > ledger.cadenceDays * DAY_MS && age(ledger.lastAttemptAt) > RETRY_AFTER_MS;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function brewRun(args) {
|
|
170
|
+
const brew = findBrew();
|
|
171
|
+
if (!brew) return { status: 'manual', reason: 'brew not found', command: `brew ${args.join(' ')}` };
|
|
172
|
+
const r = run(brew, args, 300000);
|
|
173
|
+
return r.ok ? { status: 'ok' } : { status: 'failed', command: `brew ${args.join(' ')}`, tail: r.out };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function postInstall(name) {
|
|
177
|
+
const p = OPTIONAL[name].postInstall;
|
|
178
|
+
if (!p) return null;
|
|
179
|
+
const r = run(p[0], p[1], 60000);
|
|
180
|
+
return r.ok ? 'ok' : `failed: ${r.out}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function install(ledger, name) {
|
|
184
|
+
const res = brewRun(OPTIONAL[name].brew);
|
|
185
|
+
const version = installedVersion(name);
|
|
186
|
+
// brew can fail with "already installed": the binary on PATH is the truth.
|
|
187
|
+
if (version) {
|
|
188
|
+
ledger.tools[name] = { ...ledger.tools[name], choice: 'installed', version, decidedAt: new Date().toISOString() };
|
|
189
|
+
return { tool: name, status: 'ok', version, postInstall: postInstall(name) };
|
|
190
|
+
}
|
|
191
|
+
ledger.tools[name] = { ...ledger.tools[name], choice: 'installed', version: null, decidedAt: new Date().toISOString() };
|
|
192
|
+
return { tool: name, ...res };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function update(ledger, names) {
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const name of names) {
|
|
198
|
+
const entry = ledger.tools[name];
|
|
199
|
+
if (!entry || entry.choice !== 'installed') continue;
|
|
200
|
+
const before = installedVersion(name);
|
|
201
|
+
const res = before ? brewRun(OPTIONAL[name].brewUpgrade) : brewRun(OPTIONAL[name].brew);
|
|
202
|
+
const after = installedVersion(name);
|
|
203
|
+
entry.version = after;
|
|
204
|
+
entry.outdated = newer(entry.latest, after);
|
|
205
|
+
// Resync even when brew could not run: the prompts follow the skill, not the binary.
|
|
206
|
+
const post = after ? postInstall(name) : null;
|
|
207
|
+
if (name === 'revmux' && post) entry.promptsInSync = post === 'ok';
|
|
208
|
+
results.push({ tool: name, before, after, ...res, postInstall: post });
|
|
209
|
+
}
|
|
210
|
+
return results;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function unknownTool(name) {
|
|
214
|
+
if (OPTIONAL[name]) return false;
|
|
215
|
+
console.log(JSON.stringify({ error: `unknown tool "${name}"`, known: Object.keys(OPTIONAL) }));
|
|
216
|
+
process.exitCode = 2;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
let ledger = loadLedger();
|
|
222
|
+
|
|
223
|
+
if (cmd === 'check') {
|
|
224
|
+
const missing = requiredMissing();
|
|
225
|
+
if (missing.length) {
|
|
226
|
+
console.log(JSON.stringify({ state: 'missing-required', missing }));
|
|
227
|
+
} else if (!ledger?.initialisedAt) {
|
|
228
|
+
const offer = Object.entries(OPTIONAL).map(([name, t]) => ({ tool: name, usedBy: t.usedBy, present: onPath(t.versionCmd[0]) }));
|
|
229
|
+
console.log(JSON.stringify({ state: 'first-run', offer }));
|
|
230
|
+
} else {
|
|
231
|
+
const due = has('force') || isDue(ledger, now);
|
|
232
|
+
if (due) fullCheck(ledger, now);
|
|
233
|
+
const outdated = Object.entries(ledger.tools).filter(([, e]) => e.choice === 'installed' && e.outdated).map(([n, e]) => ({ tool: n, version: e.version, latest: e.latest }));
|
|
234
|
+
let updated = null;
|
|
235
|
+
// Unattended runs only report: upgrading a binary is a machine change.
|
|
236
|
+
if (due && outdated.length && ledger.autoUpdate && !has('unattended')) {
|
|
237
|
+
updated = update(ledger, outdated.map((o) => o.tool));
|
|
238
|
+
}
|
|
239
|
+
if (due) saveLedger(ledger);
|
|
240
|
+
console.log(JSON.stringify({
|
|
241
|
+
state: due ? 'checked' : 'fresh',
|
|
242
|
+
checkedAt: ledger.checkedAt,
|
|
243
|
+
nextCheckAfter: ledger.checkedAt ? new Date(Date.parse(ledger.checkedAt) + ledger.cadenceDays * DAY_MS).toISOString() : null,
|
|
244
|
+
presence: presence(ledger),
|
|
245
|
+
outdated,
|
|
246
|
+
updated,
|
|
247
|
+
revmuxPromptsInSync: ledger.tools.revmux?.promptsInSync ?? null,
|
|
248
|
+
skipped: Object.entries(ledger.tools).filter(([, e]) => e.choice === 'skipped').map(([n]) => n),
|
|
249
|
+
// Tools added to the registry after the first run: offer these once.
|
|
250
|
+
undecided: Object.keys(OPTIONAL).filter((n) => !ledger.tools[n]).map((n) => ({ tool: n, usedBy: OPTIONAL[n].usedBy })),
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
} else if (cmd === 'init') {
|
|
254
|
+
ledger = ledger || blankLedger();
|
|
255
|
+
const list = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : []);
|
|
256
|
+
const auto = flag('auto-update');
|
|
257
|
+
if (auto) ledger.autoUpdate = auto === 'on';
|
|
258
|
+
const cadence = Number(flag('cadence-days'));
|
|
259
|
+
if (cadence > 0) ledger.cadenceDays = cadence;
|
|
260
|
+
const results = [];
|
|
261
|
+
for (const name of list(flag('skip'))) {
|
|
262
|
+
if (unknownTool(name)) break;
|
|
263
|
+
ledger.tools[name] = { choice: 'skipped', decidedAt: new Date().toISOString() };
|
|
264
|
+
results.push({ tool: name, status: 'skipped' });
|
|
265
|
+
}
|
|
266
|
+
for (const name of list(flag('install'))) {
|
|
267
|
+
if (unknownTool(name)) break;
|
|
268
|
+
results.push(install(ledger, name));
|
|
269
|
+
}
|
|
270
|
+
// Answering the offer covers every tool in it: one not picked counts as
|
|
271
|
+
// skipped, so the question is never asked twice.
|
|
272
|
+
for (const name of Object.keys(OPTIONAL)) {
|
|
273
|
+
if (!ledger.tools[name]) ledger.tools[name] = { choice: 'skipped', decidedAt: new Date().toISOString() };
|
|
274
|
+
}
|
|
275
|
+
ledger.initialisedAt = ledger.initialisedAt || new Date(now).toISOString();
|
|
276
|
+
fullCheck(ledger, now);
|
|
277
|
+
saveLedger(ledger);
|
|
278
|
+
console.log(JSON.stringify({ state: 'initialised', ledger: LEDGER, autoUpdate: ledger.autoUpdate, cadenceDays: ledger.cadenceDays, results }));
|
|
279
|
+
} else if (cmd === 'install' || cmd === 'skip') {
|
|
280
|
+
const name = argv[1];
|
|
281
|
+
if (!unknownTool(name)) {
|
|
282
|
+
ledger = ledger || blankLedger();
|
|
283
|
+
const result = cmd === 'install'
|
|
284
|
+
? install(ledger, name)
|
|
285
|
+
: ((ledger.tools[name] = { choice: 'skipped', decidedAt: new Date().toISOString() }), { tool: name, status: 'skipped' });
|
|
286
|
+
saveLedger(ledger);
|
|
287
|
+
console.log(JSON.stringify(result));
|
|
288
|
+
}
|
|
289
|
+
} else if (cmd === 'update') {
|
|
290
|
+
if (!ledger) {
|
|
291
|
+
console.log(JSON.stringify({ state: 'first-run', hint: 'run init first' }));
|
|
292
|
+
} else {
|
|
293
|
+
fullCheck(ledger, now);
|
|
294
|
+
const names = argv[1] ? [argv[1]] : Object.entries(ledger.tools).filter(([, e]) => e.choice === 'installed' && e.outdated).map(([n]) => n);
|
|
295
|
+
const results = update(ledger, names);
|
|
296
|
+
saveLedger(ledger);
|
|
297
|
+
console.log(JSON.stringify({ state: 'updated', results }));
|
|
298
|
+
}
|
|
299
|
+
} else if (cmd === 'reset') {
|
|
300
|
+
saveLedger(blankLedger());
|
|
301
|
+
console.log(JSON.stringify({ state: 'reset', ledger: LEDGER }));
|
|
302
|
+
} else {
|
|
303
|
+
console.log(JSON.stringify({ error: `unknown command "${cmd}"` }));
|
|
304
|
+
process.exitCode = 2;
|
|
305
|
+
}
|
|
@@ -134,18 +134,23 @@ cp "$WORKTREE_JSON" "$CONTEXT_DIR/worktree.json"
|
|
|
134
134
|
cp "$PROFILE_FILE" "$PROFILE_INPUT_FILE"
|
|
135
135
|
|
|
136
136
|
REVMUX_OUT="$OUT"
|
|
137
|
+
# revmux keeps no stderr of its own. Seven rounds between 2026-10-08 and
|
|
138
|
+
# 2026-10-14 had every agent exit -1 within seconds with nothing in the
|
|
139
|
+
# archive to say why, so keep it beside the round (input/scope.md -> round dir).
|
|
140
|
+
STDERR_LOG="$(dirname "$(dirname "$SCOPE_FILE")")/revmux.stderr.log"
|
|
137
141
|
set +e
|
|
138
142
|
revmux --task "$TASK" --run "$RUN" --profile "$PROFILE_NAME" \
|
|
139
143
|
--workdir "$WORKDIR" --tasks-dir "$TASKS_DIR" --config-dir "$CONFIG_DIR" \
|
|
140
144
|
--tools=Read,Grep,Glob,WebFetch,WebSearch \
|
|
141
|
-
--no-tui > "$REVMUX_OUT"
|
|
145
|
+
--no-tui > "$REVMUX_OUT" 2> "$STDERR_LOG"
|
|
142
146
|
CODE=$?
|
|
143
147
|
set -e
|
|
148
|
+
cat "$STDERR_LOG" >&2
|
|
144
149
|
|
|
145
150
|
case "$CODE" in
|
|
146
151
|
0) printf 'revmux exit 0 (no findings)\n' >&2 ;;
|
|
147
152
|
1) printf 'revmux exit 1 (findings reported)\n' >&2 ;;
|
|
148
|
-
*) printf 'revmux exit %s (tool error)\n' "$CODE" >&2 ;;
|
|
153
|
+
*) printf 'revmux exit %s (tool error), stderr kept in %s\n' "$CODE" "$STDERR_LOG" >&2 ;;
|
|
149
154
|
esac
|
|
150
155
|
|
|
151
156
|
exit "$CODE"
|