tackbox 0.1.97 → 0.1.99
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.md +84 -31
- package/bin/tackbox-codex-hook.js +12 -0
- package/bin/tackbox-eslint.js +7 -0
- package/js/README.md +7 -0
- package/js/codex/hook.js +131 -0
- package/js/omp/hook.js +17 -0
- package/js/omp/index.js +4 -19
- package/js/omp/payload.js +2 -1
- package/js/rules/_shared.js +47 -19
- package/js/rules/no-broad-notify.js +4 -3
- package/js/rules/no-parse-fallback.js +2 -3
- package/js/rules/no-skipped-test.js +3 -5
- package/js/rules/no-swallow-allsettled.js +2 -3
- package/js/rules/no-swallow-catch.js +2 -3
- package/js/rules/no-swallow-promise-catch.js +2 -3
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -163,12 +163,12 @@ past (argparse misuse, and the per-command cases below).
|
|
|
163
163
|
`--draft` is a generator, not a gate: `0` when every uncovered
|
|
164
164
|
marker was drafted (an orphan-only tree included), `2` only when
|
|
165
165
|
unresolvable files leave the draft incomplete.
|
|
166
|
-
- **hook** - `0` a no-op, a clean event, or a JSON decision (
|
|
167
|
-
|
|
166
|
+
- **hook** - `0` a no-op, a clean event, or a JSON pre decision (an
|
|
167
|
+
approval prompt or a refusal before an unrelated edit); `1` a
|
|
168
168
|
non-blocking infra error (unreadable stdin, a git failure); `2` a
|
|
169
|
-
PostToolUse finding on
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
PostToolUse finding on edited lines, a non-compiling Go package, or
|
|
170
|
+
session approvals debt in a touched file. A post error reports a
|
|
171
|
+
mutation that already landed; it does not block or undo the write.
|
|
172
172
|
- **hook-protocol** - `0` whenever a decision was reached, whatever the
|
|
173
173
|
decision says (it rides the JSON on stdout, never the exit code); `1`
|
|
174
174
|
plus one stderr line when none was: unreadable stdin, or a request
|
|
@@ -473,8 +473,8 @@ scopes (lambdas, arrows, IIFEs) appear as 8-hex content hashes; Java
|
|
|
473
473
|
overloads carry a parameter-type signature; same-name siblings take
|
|
474
474
|
an `@k` ordinal. Repeat the line for each identical occurrence.
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
marker without a covering entry and an entry without a live marker
|
|
476
|
+
For `tackbox lint` and `tackbox approvals`, the bidirectional check covers the whole tree:
|
|
477
|
+
a marker without a covering entry and an entry without a live marker
|
|
478
478
|
(an orphan) are both findings, reported by `tackbox lint` under the
|
|
479
479
|
`approvals (whole tree):` header whatever the lint scope.
|
|
480
480
|
`tackbox approvals` runs the same check standalone;
|
|
@@ -488,11 +488,19 @@ Approving is adding the line. In an agent session the edit that adds
|
|
|
488
488
|
a manifest line draws the PreToolUse ask quoting the entry (several
|
|
489
489
|
lines in one edit draw one all-or-nothing ask), so the only route to
|
|
490
490
|
a green check passes through a visible diff and a human decision.
|
|
491
|
-
Writing a marker itself never asks - by any channel, Edit or shell
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
491
|
+
Writing a marker itself never asks - by any channel, Edit or shell.
|
|
492
|
+
New session debt blocks the next edit outside the affected marker files
|
|
493
|
+
and `.tackbox/approvals`; post-edit feedback reports debt only in touched
|
|
494
|
+
files. Bash and eval run no approvals checks. `dev.py check` and CI still
|
|
495
|
+
report all tree inconsistencies. Removing a manifest line is free; a
|
|
496
|
+
marker whose text, scope, or count changes needs its entry updated.
|
|
497
|
+
|
|
498
|
+
Approvals messages carry the suppressed rule's text, or say the marker
|
|
499
|
+
suppresses nothing when no finding is available, such as in the standalone
|
|
500
|
+
`approvals` command. Each inconsistency takes one line; no message supplies
|
|
501
|
+
an approval entry to copy. Suppressed findings stay out of ordinary lint
|
|
502
|
+
output, its exit status, and codequality. See D020 in
|
|
503
|
+
[rules/DECISIONS.md](rules/DECISIONS.md) for the session boundary.
|
|
496
504
|
|
|
497
505
|
## Generated and vendored code
|
|
498
506
|
|
|
@@ -575,15 +583,18 @@ specified in [docs/report-contracts.md](docs/report-contracts.md).
|
|
|
575
583
|
|
|
576
584
|
## Agent hook
|
|
577
585
|
|
|
578
|
-
The rules wire into a coding agent's edit loop through one shared core:
|
|
579
|
-
approval gates,
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
586
|
+
The rules wire into a coding agent's edit loop through one shared core:
|
|
587
|
+
approval gates, diff-scoped lint, and session approvals debt have the same
|
|
588
|
+
semantics whichever host drives them. Session means the worktree difference
|
|
589
|
+
from HEAD, including staged changes and untracked files; without a commit,
|
|
590
|
+
the whole tree is added. Existing debt is left to `dev.py check` and CI.
|
|
591
|
+
|
|
592
|
+
- **Post-edit** re-lints touched files (Go: their package). A finding on
|
|
593
|
+
added lines or session approvals debt in a touched file becomes a tool
|
|
594
|
+
error carrying the findings. The mutation has already landed; this is
|
|
595
|
+
not a block or rollback. Debt includes a new uncovered marker, a new
|
|
596
|
+
orphaned approval, and an approval whose marker was deleted.
|
|
597
|
+
A verified violation is always a tool error. A post event
|
|
587
598
|
that cannot be verified reports three facts: the mutation may already have
|
|
588
599
|
landed, why verification did not complete, and that the mutation must not be
|
|
589
600
|
repeated before `dev.py check`. OMP appends that warning to the model-facing
|
|
@@ -596,6 +607,10 @@ are the same whichever host drives them. In host-neutral terms:
|
|
|
596
607
|
A known target whose content is ambiguous asks when it reaches a bypass
|
|
597
608
|
surface. An unclassifiable file mutation or a failed policy dependency blocks
|
|
598
609
|
before it can run; it is never weakened into an approval prompt.
|
|
610
|
+
Session approvals debt also blocks an edit if any target is outside the
|
|
611
|
+
repair set: files of the debt's markers and `.tackbox/approvals`.
|
|
612
|
+
A multi-file edit with one unrelated target is refused as a whole.
|
|
613
|
+
The existing approval gates still apply to every permitted repair.
|
|
599
614
|
|
|
600
615
|
Only markers in files an engine would lint participate in the check
|
|
601
616
|
(D012): a marker in a Go `testdata/` path or a non-lintable fixture
|
|
@@ -631,6 +646,44 @@ failure is unverified, not a no-op.
|
|
|
631
646
|
`uvx tackbox hook` runs the cached tackbox (no `@latest`): the hook is
|
|
632
647
|
fast in-loop feedback, not the authoritative gate.
|
|
633
648
|
|
|
649
|
+
### Codex
|
|
650
|
+
|
|
651
|
+
Codex exposes its canonical file mutation as `apply_patch`. Wire the npm
|
|
652
|
+
adapter once, globally, in `~/.codex/hooks.json`:
|
|
653
|
+
|
|
654
|
+
Install the adapter once so hook execution does not depend on registry access:
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
npm install --global tackbox@latest
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
```json
|
|
661
|
+
{
|
|
662
|
+
"hooks": {
|
|
663
|
+
"PreToolUse": [
|
|
664
|
+
{"matcher": "apply_patch",
|
|
665
|
+
"hooks": [{"type": "command",
|
|
666
|
+
"command": "tackbox-codex-hook"}]}
|
|
667
|
+
],
|
|
668
|
+
"PostToolUse": [
|
|
669
|
+
{"matcher": "apply_patch",
|
|
670
|
+
"hooks": [{"type": "command",
|
|
671
|
+
"command": "tackbox-codex-hook",
|
|
672
|
+
"timeout": 120}]}
|
|
673
|
+
]
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
The adapter converts Codex's patch payload to `tackbox hook-protocol` and pins
|
|
679
|
+
the Python wheel to the resolved npm package version. Codex does not support an
|
|
680
|
+
`ask` decision from `PreToolUse`; an approval-gated edit therefore blocks with
|
|
681
|
+
the reason instead of approving itself. Make that change in an interactive host
|
|
682
|
+
that supports the prompt.
|
|
683
|
+
Before a patch, any non-allow decision blocks. After a patch, verified findings
|
|
684
|
+
replace the tool result; an unverified decision adds model-visible context that
|
|
685
|
+
the mutation may already have landed and must not be repeated.
|
|
686
|
+
|
|
634
687
|
### Oh My Pi
|
|
635
688
|
|
|
636
689
|
```bash
|
|
@@ -658,17 +711,17 @@ the current file.
|
|
|
658
711
|
three-fact warning, omits an `isError` override so OMP preserves the host
|
|
659
712
|
state, and tells the model not to repeat the mutation before `dev.py check`.
|
|
660
713
|
- an opaque write channel (`xd://` tool devices, archive members, SQLite rows),
|
|
661
|
-
every `bash` call, and every `eval` call name no file, so they run
|
|
662
|
-
|
|
714
|
+
every `bash` call, and every `eval` call name no file, so they run
|
|
715
|
+
neither approvals checks nor targeted lint.
|
|
663
716
|
- MCP tool names are not enumerated by this extension. Their file mutations are
|
|
664
|
-
an explicit residual outside its pre gate and post
|
|
665
|
-
run `dev.py check`.
|
|
717
|
+
an explicit residual outside its pre gate and targeted post checks;
|
|
718
|
+
review their diff and run `dev.py check`.
|
|
666
719
|
- the post adapter consumes each result-detail record independently. It falls
|
|
667
720
|
back to a snapshot only for that record when the record is pruned; failed
|
|
668
721
|
records do not widen the scope of successful landed records. OMP 18.x does
|
|
669
722
|
not identify a landed subset for a single aggregate error without per-file
|
|
670
|
-
details, so Tackbox
|
|
671
|
-
|
|
723
|
+
details, so Tackbox preserves the host error and cannot safely perform
|
|
724
|
+
targeted lint for that residual. It does not run a whole-tree approvals check.
|
|
672
725
|
|
|
673
726
|
The extension runs `uvx tackbox@<npm package version> hook-protocol`. A tagged
|
|
674
727
|
wheel is built and protocol-canary tested, published to PyPI, then a successful
|
|
@@ -711,16 +764,16 @@ one JSON decision on stdout:
|
|
|
711
764
|
landed). Pre requests omit `succeeded`; post requests require the boolean
|
|
712
765
|
`succeeded`, so a failed tool is not misreported as a missing landed file.
|
|
713
766
|
- `tool` is one of `edit`, `apply_patch`, `write`, `bash`, or `eval`. `bash` and
|
|
714
|
-
`eval` are target-free
|
|
767
|
+
`eval` are target-free channels and run no approvals or targeted lint.
|
|
715
768
|
- a **target** is one file mutation with an absolute `path`, `op`, and
|
|
716
769
|
`expectedPresent`. `edit` and `write` expect the path to exist, `delete`
|
|
717
770
|
expects it absent, and a move reports an absent source plus a present
|
|
718
771
|
destination. `content` is a full replacement; otherwise `added` and
|
|
719
772
|
`removed` are text fragments. `content` and fragments are mutually exclusive.
|
|
720
773
|
`ambiguous: true` means a known target needs whole-file treatment.
|
|
721
|
-
- **zero targets** is the opaque channel:
|
|
722
|
-
|
|
723
|
-
|
|
774
|
+
- **zero targets** is the opaque channel: no approvals or targeted lint runs.
|
|
775
|
+
`unknown` is a non-empty reason only when no concrete target can be named;
|
|
776
|
+
it blocks pre and warns post.
|
|
724
777
|
- the wire decisions are `allow`, `ask`, `block`, and `warn`. The semantic
|
|
725
778
|
outcomes are inactive, allow, approval-required, violation, and unverified:
|
|
726
779
|
unverified maps to `block` pre and `warn` post. Hosts must make a post warning
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
function fatal(error) {
|
|
6
|
+
process.exitCode = 2
|
|
7
|
+
process.stderr.write(`tackbox Codex hook: ${String((error && error.stack) || error)}\n`)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
process.once('uncaughtException', fatal)
|
|
11
|
+
process.once('unhandledRejection', fatal)
|
|
12
|
+
require('../js/codex/hook').main()
|
package/bin/tackbox-eslint.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const { ESLint } = require('eslint')
|
|
5
|
+
const { setSuppressedReporter } = require('../js/rules/_shared')
|
|
5
6
|
|
|
6
7
|
const REPORTERS_FLAG = '--reporters='
|
|
7
8
|
|
|
@@ -129,6 +130,12 @@ async function main() {
|
|
|
129
130
|
process.stderr.write('tackbox-eslint: no files supplied\n')
|
|
130
131
|
process.exit(2)
|
|
131
132
|
}
|
|
133
|
+
if (machine) {
|
|
134
|
+
setSuppressedReporter(finding => {
|
|
135
|
+
const file = path.relative(process.cwd(), finding.file).split(path.sep).join('/')
|
|
136
|
+
process.stdout.write(JSON.stringify({ ...finding, file }) + '\n')
|
|
137
|
+
})
|
|
138
|
+
}
|
|
132
139
|
validateDeclarations(decls)
|
|
133
140
|
const eslint = new ESLint({
|
|
134
141
|
// Inline directives (eslint-disable ... tackbox/<rule>) would be an
|
package/js/README.md
CHANGED
|
@@ -146,6 +146,13 @@ siblings still report. Recognition reads the enclosing element's
|
|
|
146
146
|
preceding `SvelteHTMLComment` sibling; `/* ... */` block comments
|
|
147
147
|
are never markers.
|
|
148
148
|
|
|
149
|
+
The wrapper's `--machine` output retains suppressed findings with the
|
|
150
|
+
same rule text plus `suppressed`, `marker_kind`, and `marker_line`.
|
|
151
|
+
Human ESLint output and its exit status ignore them. Only the shared
|
|
152
|
+
approvals renderer presents them (D020 in
|
|
153
|
+
[../rules/DECISIONS.md](../rules/DECISIONS.md)); a marker that excuses
|
|
154
|
+
no violation produces no suppressed finding.
|
|
155
|
+
|
|
149
156
|
## Markdown
|
|
150
157
|
|
|
151
158
|
The CLI also lints `.md` files through `tackbox-mdlint`, a thin markdownlint
|
package/js/codex/hook.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const manifest = require('../../package.json')
|
|
6
|
+
const protocol = require('../omp/hook')
|
|
7
|
+
const payload = require('../omp/payload')
|
|
8
|
+
|
|
9
|
+
const PRE = 'PreToolUse'
|
|
10
|
+
const POST = 'PostToolUse'
|
|
11
|
+
const APPLY_PATCH = 'apply_patch'
|
|
12
|
+
const APPROVAL_UNAVAILABLE =
|
|
13
|
+
'Codex hooks cannot present a Tackbox approval prompt, so this edit is blocked. ' +
|
|
14
|
+
'Make the approval-gated change in an interactive host that supports the prompt.'
|
|
15
|
+
|
|
16
|
+
function requestFromEvent(event) {
|
|
17
|
+
if (event === null || typeof event !== 'object' || Array.isArray(event)) {
|
|
18
|
+
return { failure: 'Codex supplied a non-object hook event' }
|
|
19
|
+
}
|
|
20
|
+
const phase = event.hook_event_name === PRE
|
|
21
|
+
? 'pre'
|
|
22
|
+
: event.hook_event_name === POST
|
|
23
|
+
? 'post'
|
|
24
|
+
: null
|
|
25
|
+
if (phase === null) return { ignored: true }
|
|
26
|
+
if (event.tool_name !== APPLY_PATCH) return { ignored: true }
|
|
27
|
+
if (typeof event.cwd !== 'string' || !path.isAbsolute(event.cwd)) {
|
|
28
|
+
return { phase, failure: 'Codex supplied no absolute cwd' }
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
event.tool_input === null ||
|
|
32
|
+
typeof event.tool_input !== 'object' ||
|
|
33
|
+
Array.isArray(event.tool_input) ||
|
|
34
|
+
typeof event.tool_input.command !== 'string'
|
|
35
|
+
) {
|
|
36
|
+
return { phase, failure: 'Codex supplied apply_patch without a string tool_input.command' }
|
|
37
|
+
}
|
|
38
|
+
if (event.tool_input.command.trim() === '') {
|
|
39
|
+
return { phase, failure: 'Codex supplied an empty apply_patch command' }
|
|
40
|
+
}
|
|
41
|
+
const normalized = payload.normalize(
|
|
42
|
+
APPLY_PATCH,
|
|
43
|
+
{ input: event.tool_input.command },
|
|
44
|
+
event.cwd,
|
|
45
|
+
)
|
|
46
|
+
if (normalized === null) return { ignored: true }
|
|
47
|
+
if (normalized.failure) return { phase, failure: normalized.failure }
|
|
48
|
+
if (phase === 'post') normalized.succeeded = true
|
|
49
|
+
return {
|
|
50
|
+
phase,
|
|
51
|
+
request: protocol.request(phase, event.cwd, normalized),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handle(event, options = {}) {
|
|
56
|
+
const built = requestFromEvent(event)
|
|
57
|
+
if (built.ignored) return { code: 0, stdout: '', stderr: '' }
|
|
58
|
+
const phase = built.phase || 'pre'
|
|
59
|
+
const decision = built.failure
|
|
60
|
+
? protocol.unverified(built.failure)
|
|
61
|
+
: await (options.decide || protocol.decide)(built.request, {
|
|
62
|
+
env: options.env || process.env,
|
|
63
|
+
version: options.version || manifest.version,
|
|
64
|
+
timers: options.timers,
|
|
65
|
+
})
|
|
66
|
+
return render(phase, decision)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function render(phase, decision) {
|
|
70
|
+
const kind = decision && decision.kind
|
|
71
|
+
if (kind === protocol.ALLOW) return { code: 0, stdout: '', stderr: '' }
|
|
72
|
+
if (phase === 'pre') {
|
|
73
|
+
let reason
|
|
74
|
+
if (kind === protocol.ASK) {
|
|
75
|
+
reason = `${decision.reason}\n\n${APPROVAL_UNAVAILABLE}`
|
|
76
|
+
} else if (kind === protocol.BLOCK || kind === protocol.UNVERIFIED) {
|
|
77
|
+
reason = decision.reason
|
|
78
|
+
} else {
|
|
79
|
+
reason = `tackbox returned an unrecognized pre decision ${String(kind)}`
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
code: 0,
|
|
83
|
+
stdout: `${JSON.stringify({ decision: 'block', reason })}\n`,
|
|
84
|
+
stderr: '',
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (kind === protocol.BLOCK) {
|
|
88
|
+
return {
|
|
89
|
+
code: 0,
|
|
90
|
+
stdout: `${JSON.stringify({ decision: 'block', reason: decision.reason })}\n`,
|
|
91
|
+
stderr: '',
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const reason = kind === protocol.UNVERIFIED
|
|
95
|
+
? decision.reason
|
|
96
|
+
: `tackbox returned an unrecognized post decision ${String(kind)}`
|
|
97
|
+
return {
|
|
98
|
+
code: 0,
|
|
99
|
+
stdout: `${JSON.stringify({
|
|
100
|
+
hookSpecificOutput: {
|
|
101
|
+
hookEventName: POST,
|
|
102
|
+
additionalContext: protocol.unverifiedPostMessage(reason),
|
|
103
|
+
},
|
|
104
|
+
})}\n`,
|
|
105
|
+
stderr: '',
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
const event = protocol.parseJson(fs.readFileSync(0, 'utf8'))
|
|
111
|
+
if (event.error !== null) {
|
|
112
|
+
process.stderr.write(`tackbox Codex hook: unreadable stdin: ${event.error}\n`)
|
|
113
|
+
process.exitCode = 2
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
const result = await handle(event.value)
|
|
117
|
+
if (result.stdout) process.stdout.write(result.stdout)
|
|
118
|
+
if (result.stderr) process.stderr.write(result.stderr)
|
|
119
|
+
process.exitCode = result.code
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
APPLY_PATCH,
|
|
124
|
+
APPROVAL_UNAVAILABLE,
|
|
125
|
+
POST,
|
|
126
|
+
PRE,
|
|
127
|
+
handle,
|
|
128
|
+
main,
|
|
129
|
+
render,
|
|
130
|
+
requestFromEvent,
|
|
131
|
+
}
|
package/js/omp/hook.js
CHANGED
|
@@ -182,6 +182,21 @@ function unverified(reason) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
function unverifiedPostMessage(reason) {
|
|
186
|
+
const text = typeof reason === 'string' ? reason : String(reason)
|
|
187
|
+
if (
|
|
188
|
+
text.includes('The mutation may already have landed.') &&
|
|
189
|
+
text.includes('Do not repeat the mutation; dev.py check remains required.')
|
|
190
|
+
) {
|
|
191
|
+
return text
|
|
192
|
+
}
|
|
193
|
+
return [
|
|
194
|
+
'The mutation may already have landed.',
|
|
195
|
+
`Tackbox verification did not complete: ${text}`,
|
|
196
|
+
'Do not repeat the mutation; dev.py check remains required.',
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
function firstLine(text) {
|
|
186
201
|
for (const line of String(text || '').split('\n')) {
|
|
187
202
|
if (line.trim() !== '') return line.trim()
|
|
@@ -209,5 +224,7 @@ module.exports = {
|
|
|
209
224
|
request,
|
|
210
225
|
resolveArgv,
|
|
211
226
|
asDecision,
|
|
227
|
+
parseJson,
|
|
212
228
|
unverified,
|
|
229
|
+
unverifiedPostMessage,
|
|
213
230
|
}
|
package/js/omp/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const HEADLESS_NOTE =
|
|
|
9
9
|
'tackbox cannot ask here (no interactive session), so the call is blocked' +
|
|
10
10
|
' instead of approved. Re-issue it interactively, or drop the gated line.'
|
|
11
11
|
const DENIED = 'tackbox: approval denied.'
|
|
12
|
-
const
|
|
12
|
+
const FINDINGS = 'tackbox found issues after this change:'
|
|
13
13
|
|
|
14
14
|
module.exports = function tackbox(pi) {
|
|
15
15
|
pi.setLabel('tackbox')
|
|
@@ -61,39 +61,24 @@ function applyPost(decision, event) {
|
|
|
61
61
|
const kind = decision && decision.kind
|
|
62
62
|
if (kind === hook.BLOCK) {
|
|
63
63
|
return {
|
|
64
|
-
content: appended(event, `${
|
|
64
|
+
content: appended(event, `${FINDINGS}\n${decision.reason}`),
|
|
65
65
|
isError: true,
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
if (kind === hook.UNVERIFIED) {
|
|
69
69
|
return {
|
|
70
|
-
content: appended(event, unverifiedPostMessage(decision.reason)),
|
|
70
|
+
content: appended(event, hook.unverifiedPostMessage(decision.reason)),
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
if (kind === hook.ALLOW) return undefined
|
|
74
74
|
return {
|
|
75
75
|
content: appended(
|
|
76
76
|
event,
|
|
77
|
-
unverifiedPostMessage(`tackbox returned an unrecognized post decision ${String(kind)}`),
|
|
77
|
+
hook.unverifiedPostMessage(`tackbox returned an unrecognized post decision ${String(kind)}`),
|
|
78
78
|
),
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function unverifiedPostMessage(reason) {
|
|
83
|
-
const text = typeof reason === 'string' ? reason : String(reason)
|
|
84
|
-
if (
|
|
85
|
-
text.includes('The mutation may already have landed.') &&
|
|
86
|
-
text.includes('Do not repeat the mutation; dev.py check remains required.')
|
|
87
|
-
) {
|
|
88
|
-
return text
|
|
89
|
-
}
|
|
90
|
-
return [
|
|
91
|
-
'The mutation may already have landed.',
|
|
92
|
-
`Tackbox verification did not complete: ${text}`,
|
|
93
|
-
'Do not repeat the mutation; dev.py check remains required.',
|
|
94
|
-
].join('\n')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
82
|
function appended(event, text) {
|
|
98
83
|
const content = Array.isArray(event && event.content) ? event.content : []
|
|
99
84
|
return [...content, { type: 'text', text }]
|
package/js/omp/payload.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('node:path')
|
|
|
4
4
|
|
|
5
5
|
const TAGGED = /^\[(.+)#[0-9A-Fa-f]{4}\]\s*$/
|
|
6
6
|
const ENVELOPE = /^\*\*\* (?:Begin|End) Patch\s*$/
|
|
7
|
+
const EOF_SENTINEL = /^\*\*\* End of File\s*$/
|
|
7
8
|
const FILE_SENTINEL = /^\*\*\* (Update|Add|Delete) File:\s*(.+?)\s*$/
|
|
8
9
|
const MOVE_SENTINEL = /^\*\*\* Move to:\s*(.+?)\s*$/
|
|
9
10
|
const MV_OP = /^MV\s+(.+?)\s*$/
|
|
@@ -256,7 +257,7 @@ function parsePatchText(tool, text, cwd) {
|
|
|
256
257
|
continue
|
|
257
258
|
}
|
|
258
259
|
flush()
|
|
259
|
-
if (line.trim() === '' || ENVELOPE.test(line) || HUNK.test(line)) continue
|
|
260
|
+
if (line.trim() === '' || ENVELOPE.test(line) || EOF_SENTINEL.test(line) || HUNK.test(line)) continue
|
|
260
261
|
const tagged = TAGGED.exec(line)
|
|
261
262
|
if (tagged !== null) {
|
|
262
263
|
open(tagged[1])
|
package/js/rules/_shared.js
CHANGED
|
@@ -392,12 +392,18 @@ function blockHasReport(context, block, errName) {
|
|
|
392
392
|
return found
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
395
|
+
// Multiline HTML comments locate approval identity at the keyword, not the delimiter.
|
|
396
|
+
function markerLine(comment, prefix) {
|
|
397
|
+
const raw = comment.value
|
|
398
398
|
const text = raw.trim()
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
const marker = prefix + ':'
|
|
400
|
+
if (!text.startsWith(marker) || text.slice(marker.length).trim().length < MIN_REASON) return null
|
|
401
|
+
let line = comment.loc.start.line
|
|
402
|
+
const offset = raw.indexOf(marker)
|
|
403
|
+
for (let i = 0; i < offset; i++) {
|
|
404
|
+
if (raw[i] === '\n') line++
|
|
405
|
+
}
|
|
406
|
+
return line
|
|
401
407
|
}
|
|
402
408
|
|
|
403
409
|
// precedingSvelteSibling returns the template node immediately before `el` among
|
|
@@ -416,30 +422,51 @@ function precedingSvelteSibling(el) {
|
|
|
416
422
|
return null
|
|
417
423
|
}
|
|
418
424
|
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// reason can be followed by human context, a blank line breaking the block - and,
|
|
423
|
-
// in a Svelte template, an HTML comment `<!-- ... -->` immediately above an
|
|
424
|
-
// enclosing element, which covers the whole element (residual A8: an inline
|
|
425
|
-
// handler can span lines). getAllComments omits SvelteHTMLComment nodes, so the
|
|
426
|
-
// template form is read off the element's preceding sibling.
|
|
427
|
-
function hasMarkerAbove(context, node, prefix) {
|
|
428
|
-
if (!node || !node.loc) return false
|
|
425
|
+
// Svelte HTML comments cover the following element, not just its first line.
|
|
426
|
+
function markerAbove(context, node, prefix) {
|
|
427
|
+
if (!node || !node.loc) return null
|
|
429
428
|
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
430
429
|
const byEndLine = new Map()
|
|
431
430
|
for (const c of sourceCode.getAllComments()) {
|
|
432
431
|
if (c.type === 'Line') byEndLine.set(c.loc.end.line, c)
|
|
433
432
|
}
|
|
434
433
|
for (let line = node.loc.start.line - 1; byEndLine.has(line); line--) {
|
|
435
|
-
|
|
434
|
+
const comment = byEndLine.get(line)
|
|
435
|
+
const marker = markerLine(comment, prefix)
|
|
436
|
+
if (marker !== null) return marker
|
|
436
437
|
}
|
|
437
438
|
for (let cur = node.parent; cur; cur = cur.parent) {
|
|
438
439
|
if (cur.type !== 'SvelteElement') continue
|
|
439
440
|
const sib = precedingSvelteSibling(cur)
|
|
440
|
-
if (sib && sib.type === 'SvelteHTMLComment'
|
|
441
|
+
if (sib && sib.type === 'SvelteHTMLComment') {
|
|
442
|
+
const marker = markerLine(sib, prefix)
|
|
443
|
+
if (marker !== null) return marker
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return null
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let suppressedReporter = null
|
|
450
|
+
|
|
451
|
+
function setSuppressedReporter(reporter) {
|
|
452
|
+
suppressedReporter = reporter
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function reportWithMarker(context, descriptor, anchor, kind, messages) {
|
|
456
|
+
const line = markerAbove(context, anchor, kind)
|
|
457
|
+
if (line === null) {
|
|
458
|
+
context.report(descriptor)
|
|
459
|
+
} else if (suppressedReporter) {
|
|
460
|
+
suppressedReporter({
|
|
461
|
+
file: context.filename || context.getFilename(),
|
|
462
|
+
line: descriptor.node.loc.start.line,
|
|
463
|
+
rule: context.id,
|
|
464
|
+
message: messages[descriptor.messageId],
|
|
465
|
+
suppressed: true,
|
|
466
|
+
marker_kind: kind,
|
|
467
|
+
marker_line: line,
|
|
468
|
+
})
|
|
441
469
|
}
|
|
442
|
-
return false
|
|
443
470
|
}
|
|
444
471
|
|
|
445
472
|
// --- F2b: path-sensitive no-swallow analysis -----------------------------
|
|
@@ -835,7 +862,8 @@ module.exports = {
|
|
|
835
862
|
walk,
|
|
836
863
|
blockHasThrow,
|
|
837
864
|
blockHasReport,
|
|
838
|
-
|
|
865
|
+
reportWithMarker,
|
|
866
|
+
setSuppressedReporter,
|
|
839
867
|
enclosingFn,
|
|
840
868
|
fnReturnsResultLike,
|
|
841
869
|
someNode,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { reportWithMarker, walk, isTier1Notify, argFlows, isTestFile } = require('./_shared')
|
|
2
2
|
|
|
3
3
|
// guarded: `call` sits under an additional condition strictly inside the catch
|
|
4
4
|
// body - an if-branch (consequent/alternate) or a switch case. A notify
|
|
@@ -35,11 +35,12 @@ module.exports = {
|
|
|
35
35
|
if (errName == null) return
|
|
36
36
|
const body = node.body
|
|
37
37
|
if (!body || body.type !== 'BlockStatement') return
|
|
38
|
-
if (hasMarkerAbove(context, node.parent, 'no-report')) return
|
|
39
38
|
walk(body, call => {
|
|
40
39
|
if (call.type !== 'CallExpression') return
|
|
41
40
|
if (!isTier1Notify(context, call) || !argFlows(call, errName)) return
|
|
42
|
-
if (!guarded(call, body))
|
|
41
|
+
if (!guarded(call, body)) {
|
|
42
|
+
reportWithMarker(context, { node: call, messageId: 'broad' }, node.parent, 'no-report', module.exports.meta.messages)
|
|
43
|
+
}
|
|
43
44
|
})
|
|
44
45
|
},
|
|
45
46
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const {
|
|
2
|
-
|
|
2
|
+
reportWithMarker,
|
|
3
3
|
enclosingFn,
|
|
4
4
|
fnReturnsResultLike,
|
|
5
5
|
errObjectFlows,
|
|
@@ -139,11 +139,10 @@ module.exports = {
|
|
|
139
139
|
if (!tryBlockParses(node.block)) return
|
|
140
140
|
const handler = node.handler
|
|
141
141
|
if (!handler || !handler.body || handler.body.type !== 'BlockStatement') return
|
|
142
|
-
if (hasMarkerAbove(context, node, 'parse-skip')) return
|
|
143
142
|
const errName = handler.param && handler.param.type === 'Identifier' ? handler.param.name : null
|
|
144
143
|
const allowBoundary = fnReturnsResultLike(enclosingFn(node))
|
|
145
144
|
if (catchPropagates(handler.body, errName, allowBoundary)) return
|
|
146
|
-
context
|
|
145
|
+
reportWithMarker(context, { node: handler, messageId: 'fallback' }, node, 'parse-skip', module.exports.meta.messages)
|
|
147
146
|
},
|
|
148
147
|
}
|
|
149
148
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { reportWithMarker, matchesTestModifier, isStaticString, staticStringValue, TEST_ROOTS } = require('./_shared')
|
|
2
2
|
|
|
3
3
|
const SKIP_PROPS = new Set(['skip', 'todo', 'skipIf', 'fixme'])
|
|
4
4
|
const BARE = new Set(['xit', 'xdescribe', 'xtest'])
|
|
@@ -80,14 +80,12 @@ module.exports = {
|
|
|
80
80
|
CallExpression(node) {
|
|
81
81
|
if (matchesTestModifier(node.callee, BARE, n => SKIP_PROPS.has(n))) {
|
|
82
82
|
if (hasInCallReason(node)) return
|
|
83
|
-
|
|
84
|
-
context.report({ node, messageId: 'skipped' })
|
|
83
|
+
reportWithMarker(context, { node, messageId: 'skipped' }, outermostCall(node), 'test-skip', module.exports.meta.messages)
|
|
85
84
|
return
|
|
86
85
|
}
|
|
87
86
|
if (node.callee.type === 'Identifier' && TEST_ROOTS.has(node.callee.name)) {
|
|
88
87
|
if (optionsSkipVerdict(node) !== 'flag') return
|
|
89
|
-
|
|
90
|
-
context.report({ node, messageId: 'skipped' })
|
|
88
|
+
reportWithMarker(context, { node, messageId: 'skipped' }, outermostCall(node), 'test-skip', module.exports.meta.messages)
|
|
91
89
|
}
|
|
92
90
|
},
|
|
93
91
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { reportWithMarker, enclosingFn, someNode } = require('./_shared')
|
|
2
2
|
|
|
3
3
|
// isAllSettledCall: syntactic `Promise.allSettled(...)`. Matched by shape, not
|
|
4
4
|
// resolution - Promise is a global and allSettled is unambiguous.
|
|
@@ -43,9 +43,8 @@ module.exports = {
|
|
|
43
43
|
return {
|
|
44
44
|
CallExpression(node) {
|
|
45
45
|
if (!isAllSettledCall(node)) return
|
|
46
|
-
if (hasMarkerAbove(context, node, 'no-report')) return
|
|
47
46
|
if (refsReason(enclosingFn(node) || sc.ast)) return
|
|
48
|
-
context
|
|
47
|
+
reportWithMarker(context, { node, messageId: 'swallow' }, node, 'no-report', module.exports.meta.messages)
|
|
49
48
|
},
|
|
50
49
|
}
|
|
51
50
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const {
|
|
2
|
-
|
|
2
|
+
reportWithMarker,
|
|
3
3
|
enclosingFn,
|
|
4
4
|
fnReturnsResultLike,
|
|
5
5
|
makeHandledAnalysis,
|
|
@@ -20,11 +20,10 @@ module.exports = {
|
|
|
20
20
|
const body = node.body
|
|
21
21
|
if (!body || body.type !== 'BlockStatement') return
|
|
22
22
|
const tryStmt = node.parent
|
|
23
|
-
if (tryStmt && hasMarkerAbove(context, tryStmt, 'no-report')) return
|
|
24
23
|
const errName = node.param && node.param.type === 'Identifier' ? node.param.name : null
|
|
25
24
|
const allowBoundary = fnReturnsResultLike(enclosingFn(node))
|
|
26
25
|
if (makeHandledAnalysis({ context, errName, allowBoundary }).handled(body)) return
|
|
27
|
-
context
|
|
26
|
+
reportWithMarker(context, { node, messageId: 'swallow' }, tryStmt, 'no-report', module.exports.meta.messages)
|
|
28
27
|
},
|
|
29
28
|
}
|
|
30
29
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { reportWithMarker, makeHandledAnalysis } = require('./_shared')
|
|
2
2
|
|
|
3
3
|
// rejectionHandler returns the rejection-handler argument of a promise
|
|
4
4
|
// method: `.catch(onErr)` -> arg 0, `.then(onOk, onErr)` -> arg 1. A single-arg
|
|
@@ -28,10 +28,9 @@ module.exports = {
|
|
|
28
28
|
const handler = rejectionHandler(node)
|
|
29
29
|
if (!handler) return
|
|
30
30
|
if (handler.type !== 'ArrowFunctionExpression' && handler.type !== 'FunctionExpression') return
|
|
31
|
-
if (hasMarkerAbove(context, node, 'no-report')) return
|
|
32
31
|
const errName = handler.params[0] && handler.params[0].type === 'Identifier' ? handler.params[0].name : null
|
|
33
32
|
if (makeHandledAnalysis({ context, errName, allowBoundary: false, returnIdentity: true }).handled(handler.body)) return
|
|
34
|
-
context
|
|
33
|
+
reportWithMarker(context, { node, messageId: 'swallow' }, node, 'no-report', module.exports.meta.messages)
|
|
35
34
|
},
|
|
36
35
|
}
|
|
37
36
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tackbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.99",
|
|
4
4
|
"description": "ESLint and Markdown lint plugins, an Oh My Pi extension, plus direct error-reporting helpers for JavaScript and TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./js/eslint-plugin.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./report": "./js/report.js"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
|
+
"tackbox-codex-hook": "./bin/tackbox-codex-hook.js",
|
|
12
13
|
"tackbox-eslint": "./bin/tackbox-eslint.js",
|
|
13
14
|
"tackbox-mdlint": "./bin/tackbox-mdlint.js"
|
|
14
15
|
},
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"js/report.js",
|
|
23
24
|
"js/rules/",
|
|
24
25
|
"js/markdownlint-rules/",
|
|
26
|
+
"js/codex/",
|
|
25
27
|
"js/omp/index.mjs",
|
|
26
28
|
"js/omp/index.js",
|
|
27
29
|
"js/omp/hook.js",
|