tackbox 0.1.98 → 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 +38 -0
- package/bin/tackbox-codex-hook.js +12 -0
- package/js/codex/hook.js +131 -0
- package/js/omp/hook.js +17 -0
- package/js/omp/index.js +2 -17
- package/js/omp/payload.js +2 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -646,6 +646,44 @@ failure is unverified, not a no-op.
|
|
|
646
646
|
`uvx tackbox hook` runs the cached tackbox (no `@latest`): the hook is
|
|
647
647
|
fast in-loop feedback, not the authoritative gate.
|
|
648
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
|
+
|
|
649
687
|
### Oh My Pi
|
|
650
688
|
|
|
651
689
|
```bash
|
|
@@ -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/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
|
@@ -67,33 +67,18 @@ function applyPost(decision, event) {
|
|
|
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/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",
|