@jjchill/probity-rules 0.4.11 → 0.4.12
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/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/scripts/probity-claude.mjs +96 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.12
|
|
4
|
+
|
|
5
|
+
`probity-claude` now runs Probity in the worktree a session is working
|
|
6
|
+
in, so rules judge that tree's specs and files (#66).
|
|
7
|
+
|
|
8
|
+
- **What went wrong.** The documented hook is
|
|
9
|
+
`cd "$CLAUDE_PROJECT_DIR" && ./node_modules/.bin/probity-claude`.
|
|
10
|
+
After a session moves into a worktree (`EnterWorktree`, or an
|
|
11
|
+
`isolation: "worktree"` sub-agent), `CLAUDE_PROJECT_DIR` still names
|
|
12
|
+
the main checkout. Probity finds `probity.config.ts` by searching
|
|
13
|
+
upward from its working directory, so it loaded the main checkout's
|
|
14
|
+
config, and `ROOT`, `specsDir` and `glossaryPath` all pointed there.
|
|
15
|
+
In #66 the spec-first rule blocked an acceptance test whose
|
|
16
|
+
`Covers:` scenario existed only in the worktree's `specs/`
|
|
17
|
+
submodule, and named the main checkout's `specs/features`.
|
|
18
|
+
- **Now:** the wrapper finds the git worktree the action targets: the
|
|
19
|
+
edited file's location, or for a command its `git -C <dir>` or
|
|
20
|
+
leading `cd <dir> &&`, else the session's `cwd`. When that worktree
|
|
21
|
+
is nested inside the project and has its own `probity.config.*`, the
|
|
22
|
+
wrapper starts Probity there. Its config's imports resolve from the
|
|
23
|
+
project's `node_modules` (one level up the folder tree), so the
|
|
24
|
+
worktree needs no install. Work in the main checkout, worktrees
|
|
25
|
+
without a Probity config, and paths outside the project run as before.
|
|
26
|
+
- A relative `--debug <path>` is anchored to the project directory, so
|
|
27
|
+
the debug log stays in one place.
|
|
28
|
+
- Only hooks that run `probity-claude` get this. A worktree outside the
|
|
29
|
+
project folder still runs against the project.
|
|
30
|
+
- Consumers: no config change. The worktree's own `probity.config.ts`
|
|
31
|
+
(from its branch) is the one that runs.
|
|
32
|
+
|
|
3
33
|
## 0.4.11
|
|
4
34
|
|
|
5
35
|
The commit-on-green gate now catches every form of `git commit`, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjchill/probity-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "Versioned Probity rule presets and CLI tooling for the agent-skills catalog (test-driven-development, ports-and-adapters, acceptance-testing, spec-test parity, ubiquitous-language) — see hooks/PROBITY.md.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
* deletes the copy when Probity exits. The gates only need recent
|
|
25
25
|
* events: the last test run, the red before a green.
|
|
26
26
|
*
|
|
27
|
+
* And it runs Probity in the right tree (issue #66). The hook `cd`s to
|
|
28
|
+
* $CLAUDE_PROJECT_DIR, which still names the main checkout after a
|
|
29
|
+
* session moves into a worktree (EnterWorktree, or an `isolation:
|
|
30
|
+
* "worktree"` sub-agent). Probity finds its config by searching upward
|
|
31
|
+
* from its working directory, so every rule judged the main checkout's
|
|
32
|
+
* specs and files. When the edited file (or, for a command, the
|
|
33
|
+
* session's cwd) is inside a git worktree nested in the project that
|
|
34
|
+
* has its own probity.config.*, the wrapper starts Probity there. That
|
|
35
|
+
* config's imports still resolve from the project's node_modules, one
|
|
36
|
+
* directory level up the tree.
|
|
37
|
+
*
|
|
27
38
|
* Use it in place of the probity bin in `.claude/settings.json`:
|
|
28
39
|
*
|
|
29
40
|
* "command": "cd \"$CLAUDE_PROJECT_DIR\" && ./node_modules/.bin/probity-claude"
|
|
@@ -47,7 +58,7 @@ import {
|
|
|
47
58
|
} from 'node:fs'
|
|
48
59
|
import { createRequire } from 'node:module'
|
|
49
60
|
import { tmpdir } from 'node:os'
|
|
50
|
-
import { basename, dirname, join } from 'node:path'
|
|
61
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
51
62
|
import { fileURLToPath } from 'node:url'
|
|
52
63
|
|
|
53
64
|
/** The sub-agent transcript for a payload, or null to keep the original. */
|
|
@@ -134,6 +145,75 @@ export function preparePayload(raw, limits = {}) {
|
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
const CONFIG_NAMES = ['ts', 'mts', 'js', 'mjs'].map((ext) => `probity.config.${ext}`)
|
|
149
|
+
|
|
150
|
+
function canonical(path) {
|
|
151
|
+
// Resolve symlinks on the longest existing prefix (a file being
|
|
152
|
+
// created does not exist yet), so paths compare like the project dir.
|
|
153
|
+
let head = resolve(path)
|
|
154
|
+
const tail = []
|
|
155
|
+
while (!existsSync(head) && dirname(head) !== head) {
|
|
156
|
+
tail.unshift(basename(head))
|
|
157
|
+
head = dirname(head)
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
head = realpathSync(head)
|
|
161
|
+
} catch {
|
|
162
|
+
// keep the resolved path
|
|
163
|
+
}
|
|
164
|
+
return join(head, ...tail)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isBelow(parent, child) {
|
|
168
|
+
const path = relative(parent, child)
|
|
169
|
+
return path !== '' && path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The directory a command explicitly acts in: `git -C <dir>`, else a
|
|
173
|
+
// leading `cd <dir> &&`; null when it names none. Mirrors the commit
|
|
174
|
+
// gates' commitDirectory (rules/commit-target.ts), so a worktree session
|
|
175
|
+
// that commits in the main checkout is judged there.
|
|
176
|
+
function commandDirectory(command) {
|
|
177
|
+
const unquote = (value) => value.replace(/^(['"])(.*)\1$/, '$2')
|
|
178
|
+
const gitDashC = command.match(/\bgit\s+-C\s+("[^"]+"|'[^']+'|\S+)/)
|
|
179
|
+
const cd = command.match(/^\s*cd\s+("[^"]+"|'[^']+'|[^\s;&|]+)\s*(?:&&|;)/)
|
|
180
|
+
const target = gitDashC?.[1] ?? cd?.[1]
|
|
181
|
+
return target ? unquote(target) : null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The git worktree to run Probity in for this payload, or null to stay
|
|
186
|
+
* in `projectDir`: the nearest directory holding `.git` above the edited
|
|
187
|
+
* file (for a command, the session's cwd), when it lies below
|
|
188
|
+
* `projectDir` and has its own Probity config. A command's directory is
|
|
189
|
+
* its `git -C <dir>` or leading `cd <dir> &&`, else the session's cwd.
|
|
190
|
+
*/
|
|
191
|
+
export function probityDirectory(payload, projectDir) {
|
|
192
|
+
const input = payload?.tool_input ?? {}
|
|
193
|
+
const cwd = typeof payload?.cwd === 'string' ? payload.cwd : null
|
|
194
|
+
const file = [input.file_path, input.notebook_path].find((value) => typeof value === 'string')
|
|
195
|
+
const base = cwd ?? projectDir
|
|
196
|
+
let start
|
|
197
|
+
if (file) start = dirname(isAbsolute(file) ? file : resolve(base, file))
|
|
198
|
+
else if (typeof input.command === 'string') start = resolve(base, commandDirectory(input.command) ?? '.')
|
|
199
|
+
else if (cwd) start = cwd
|
|
200
|
+
else return null
|
|
201
|
+
const project = canonical(projectDir)
|
|
202
|
+
for (let dir = canonical(start); isBelow(project, dir); dir = dirname(dir)) {
|
|
203
|
+
if (existsSync(join(dir, '.git'))) {
|
|
204
|
+
return CONFIG_NAMES.some((name) => existsSync(join(dir, name))) ? dir : null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `args` with a relative `--debug <path>` anchored to `projectDir`, so the log stays put when Probity runs elsewhere. */
|
|
211
|
+
export function debugArgs(args, projectDir) {
|
|
212
|
+
return args.map((arg, i) =>
|
|
213
|
+
args[i - 1] === '--debug' && !isAbsolute(arg) ? join(projectDir, arg) : arg,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
137
217
|
function probityBin() {
|
|
138
218
|
// @nizos/probity doesn't export its package.json, so resolve the main
|
|
139
219
|
// entry and walk up to the package root for the bin path.
|
|
@@ -156,9 +236,18 @@ function probityBin() {
|
|
|
156
236
|
async function main() {
|
|
157
237
|
const chunks = []
|
|
158
238
|
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
159
|
-
const
|
|
239
|
+
const raw = Buffer.concat(chunks).toString('utf8')
|
|
240
|
+
const { input, cleanup } = preparePayload(raw)
|
|
241
|
+
let payload
|
|
242
|
+
try {
|
|
243
|
+
payload = JSON.parse(raw)
|
|
244
|
+
} catch {
|
|
245
|
+
payload = null
|
|
246
|
+
}
|
|
247
|
+
const projectDir = process.cwd()
|
|
248
|
+
const workDir = probityDirectory(payload, projectDir) ?? projectDir
|
|
160
249
|
|
|
161
|
-
const args = process.argv.slice(2)
|
|
250
|
+
const args = debugArgs(process.argv.slice(2), projectDir)
|
|
162
251
|
if (!args.includes('--agent')) args.unshift('--agent', 'claude-code')
|
|
163
252
|
|
|
164
253
|
let bin
|
|
@@ -169,7 +258,10 @@ async function main() {
|
|
|
169
258
|
cleanup()
|
|
170
259
|
process.exit(2)
|
|
171
260
|
}
|
|
172
|
-
const child = spawn(process.execPath, [bin, ...args], {
|
|
261
|
+
const child = spawn(process.execPath, [bin, ...args], {
|
|
262
|
+
cwd: workDir,
|
|
263
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
264
|
+
})
|
|
173
265
|
child.stdin.end(input)
|
|
174
266
|
child.on('exit', (code, signal) => {
|
|
175
267
|
cleanup()
|