@leonardo-marziali/ad-lfl-kit 0.0.0-development → 0.0.1-development

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.
Files changed (2) hide show
  1. package/README.md +207 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # @leonardo-marziali/ad-lfl-kit
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40leonardo-marziali%2Fad-lfl-kit.svg)](https://www.npmjs.com/package/@leonardo-marziali/ad-lfl-kit)
4
+ [![ad-lfl-kit coverage](https://codecov.io/gh/leonardo-marziali/agentic-development-kit/branch/main/graph/badge.svg?flag=ad-lfl-kit)](https://codecov.io/gh/leonardo-marziali/agentic-development-kit)
5
+
6
+ **A**gentic **d**evelopment **l**int-**f**ix-**l**oop kit: the "track what
7
+ this session touched, then check it before the session ends" pattern used by
8
+ [Claude Code](https://claude.com/claude-code) plugin hooks, as a small
9
+ zero-dependency package instead of copy-pasted hook source.
10
+
11
+ It backs the session-scoped fix loops in this repository's
12
+ [`markdown`](https://github.com/leonardo-marziali/agentic-development-kit/tree/main/plugins/markdown)
13
+ and `sonarqube` plugins, and is published standalone so any Claude Code
14
+ plugin can build the same loop without re-implementing it.
15
+
16
+ ## The pattern
17
+
18
+ 1. A `PostToolUse` hook (matching `Edit`/`Write`) records every file the
19
+ current session touches that matches some predicate — only `.md` files,
20
+ say — into a session-scoped list.
21
+ 2. A `Stop`/`SubagentStop` hook reads that list, runs a check (optionally
22
+ preceded by an auto-fix pass), and:
23
+ - if it passes, cleans up the session state and lets the session end;
24
+ - if it fails, blocks the stop with `decision: "continue"` so Claude sees
25
+ the failures and keeps working, up to a capped number of attempts;
26
+ - at the cap, gives up — reporting what's left, optionally applying a
27
+ caller-supplied fallback — rather than blocking forever.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install @leonardo-marziali/ad-lfl-kit
33
+ ```
34
+
35
+ > **If you're shipping this inside a Claude Code plugin**, see
36
+ > [the parent repo's README](https://github.com/leonardo-marziali/agentic-development-kit#shared-packages-and-plugin-dependencies)
37
+ > for why the plugin needs its own committed `package-lock.json` for this to
38
+ > actually install where Claude Code runs the hook.
39
+
40
+ ## Quick start
41
+
42
+ Two hook scripts, wired up in the plugin's `hooks/hooks.json` the same way
43
+ as [`plugins/markdown/hooks/hooks.json`](https://github.com/leonardo-marziali/agentic-development-kit/blob/main/plugins/markdown/hooks/hooks.json):
44
+ a `PostToolUse` hook on `Edit|Write`, and `Stop`/`SubagentStop` hooks, both
45
+ pointed at `${CLAUDE_PLUGIN_ROOT}`.
46
+
47
+ **`scripts/hooks/track-touched.js`**
48
+
49
+ ```js
50
+ #!/usr/bin/env node
51
+ 'use strict';
52
+
53
+ const path = require('node:path');
54
+ const { readHookInput, stateDir, trackTouchedFile } = require('@leonardo-marziali/ad-lfl-kit');
55
+
56
+ const input = readHookInput();
57
+ if (input) {
58
+ const dir = stateDir({
59
+ sessionId: input.session_id,
60
+ namespace: 'my-plugin-session-scope',
61
+ });
62
+ trackTouchedFile({
63
+ input,
64
+ listFile: path.join(dir, 'touched-files.txt'),
65
+ matches: (absPath) => /\.md$/i.test(absPath),
66
+ });
67
+ }
68
+ ```
69
+
70
+ **`scripts/hooks/check-and-loop.js`**
71
+
72
+ ```js
73
+ #!/usr/bin/env node
74
+ 'use strict';
75
+
76
+ const path = require('node:path');
77
+ const {
78
+ readHookInput,
79
+ stateDir,
80
+ readTrackedFiles,
81
+ runFixLoop,
82
+ runBin,
83
+ } = require('@leonardo-marziali/ad-lfl-kit');
84
+
85
+ const input = readHookInput();
86
+ const dir = stateDir({
87
+ sessionId: input?.session_id,
88
+ namespace: 'my-plugin-session-scope',
89
+ });
90
+ const stateFiles = ['touched-files.txt', 'attempts.json'];
91
+ const files = readTrackedFiles(path.join(dir, 'touched-files.txt'));
92
+
93
+ runFixLoop({
94
+ input,
95
+ dir,
96
+ stateFiles,
97
+ files,
98
+ fix: (files) => runBin('npx', ['-y', 'markdownlint-cli', '--fix', ...files]),
99
+ check: (files) => {
100
+ const result = runBin('npx', ['-y', 'markdownlint-cli', ...files], {
101
+ encoding: 'utf8',
102
+ });
103
+ const report = ((result.stdout || '') + (result.stderr || '')).trim();
104
+ return { failed: result.status !== 0, report };
105
+ },
106
+ onCap: ({ outcome }) => {
107
+ process.stderr.write(`still failing after the attempt cap:\n${outcome.report}\n`);
108
+ },
109
+ buildMessage: ({ attempt, maxAttempts, outcome }) => ({
110
+ stopReason: 'Files touched this session still have lint violations.',
111
+ systemMessage: `Lint found issues (attempt ${attempt}/${maxAttempts}):\n\n${outcome.report}`,
112
+ }),
113
+ });
114
+ ```
115
+
116
+ That's a complete loop: nothing tracked → no-op; clean → allow the stop;
117
+ failing → block with a message Claude can act on, up to 3 attempts by
118
+ default; still failing at the cap → `onCap` runs once, then the stop is
119
+ allowed anyway.
120
+
121
+ ## API
122
+
123
+ ### Hook I/O (`hook-input.js`)
124
+
125
+ - **`readStdin()`** — reads file descriptor 0, returning `''` if nothing's
126
+ attached (a hook invoked without stdin, some test harnesses) instead of
127
+ throwing.
128
+ - **`readHookInput()`** — parses the hook payload Claude Code sends on
129
+ stdin. Returns `null` for empty or malformed input, never throws.
130
+
131
+ ### Session state (`state.js`)
132
+
133
+ - **`stateDir({ sessionId, namespace })`** — a directory scoped to one
134
+ session and one namespace, under `${CLAUDE_PLUGIN_DATA}` when set (so it
135
+ survives plugin updates) or the OS temp dir otherwise.
136
+ - **`readTrackedFiles(listFile)`** — the de-duplicated contents of a tracked
137
+ file list, filtered to files that still exist on disk.
138
+ - **`appendTrackedFile(listFile, absPath)`** — appends a path, skipping it
139
+ if already present.
140
+ - **`readAttempts(attemptsFile)`** / **`writeAttempts(attemptsFile, count)`**
141
+ — the attempt counter `runFixLoop` uses internally; exposed for callers
142
+ that need to inspect or reset it directly.
143
+ - **`cleanup(dir, ownStateFiles)`** — removes only the named files, then
144
+ removes `dir` if that leaves it empty. Deliberately not a recursive
145
+ delete: a session dir can hold another hook's still-pending state, and a
146
+ blind `rm -rf` would destroy it.
147
+
148
+ ### Tracking (`track.js`)
149
+
150
+ - **`trackTouchedFile({ input, listFile, matches })`** — the `PostToolUse`
151
+ half. No-ops unless `input.tool_name` is `Edit` or `Write`, the tool's
152
+ `file_path` still exists on disk (resolved against `input.cwd`), and
153
+ `matches(absPath)` returns true (omit `matches` to track everything).
154
+ Returns the tracked absolute path, or `null`.
155
+
156
+ ### Execution (`exec.js`)
157
+
158
+ - **`resolveBin(name, { extraDirs })`** — finds a CLI binary across
159
+ platforms. Checks beside `process.execPath` first (where `npx`/`npm`
160
+ live), then each of `extraDirs` (for binaries installed elsewhere — a
161
+ Homebrew or `mise` shim, say), trying the Windows-appropriate suffix
162
+ (`.cmd`/`.exe`) before falling back to the bare name for PATH lookup.
163
+ - **`quoteForCmd(arg)`** — quotes one argument for `cmd.exe`, which
164
+ `spawnSync` doesn't do for you when `shell: true`.
165
+ - **`runBin(name, args, options)`** — resolves and runs a binary, routing
166
+ through a shell only on Windows (where the shim is a `.cmd` batch file
167
+ Node won't exec directly), with arguments quoted by hand there so no
168
+ shell ever parses a file path on any other platform. `options` also
169
+ accepts `extraDirs`, forwarded to `resolveBin`.
170
+
171
+ ### The loop (`loop.js`)
172
+
173
+ - **`runFixLoop({ input, dir, stateFiles, attemptsFile, maxAttempts, files,
174
+ fix, check, onCap, buildMessage, write })`** — the `Stop`/`SubagentStop`
175
+ half described above. `check(files)` must
176
+ return `{ failed, report }`; `fix(files)` is optional and runs first when
177
+ given; `onCap({ files, outcome, maxAttempts })` is optional and runs
178
+ once instead of blocking when the attempt cap is reached;
179
+ `buildMessage({ attempt, maxAttempts, outcome, files })` returns
180
+ `{ stopReason?, systemMessage }` for the block payload. `maxAttempts`
181
+ defaults to `DEFAULT_MAX_ATTEMPTS` (3). Returns
182
+ `{ blocked, reason, ... }` so a caller or its tests can assert on which
183
+ path was taken (`'no-input'`, `'no-files'`, `'clean'`, `'cap'`,
184
+ `'blocked'`, or `'error'` if `fix`/`check` threw — in which case the
185
+ session's state is left untouched so the next `Stop` retries, rather than
186
+ either blocking on a broken toolchain or silently discarding progress).
187
+ - **`DEFAULT_MAX_ATTEMPTS`** — `3`.
188
+
189
+ ## Design notes
190
+
191
+ - **Every module is plain CommonJS with zero runtime dependencies** and no
192
+ build step. Claude Code installs a plugin's dependencies by running
193
+ `npm ci --ignore-scripts` against its own `package-lock.json`, capped at
194
+ 60 seconds, when it caches the plugin — a transitive dependency or a
195
+ compile step would risk that budget.
196
+ - **State cleanup is additive, never destructive.** `cleanup()` only removes
197
+ the file names it's given; two hooks sharing a session's state directory
198
+ (a tracking hook and, say, this repo's own test-verification hook) can't
199
+ clobber each other.
200
+ - **A failing `check`/`fix` is not the same as a failing check.** If either
201
+ throws, `runFixLoop` neither blocks the session nor discards its state —
202
+ a broken toolchain shouldn't trap a session in a loop, and it shouldn't
203
+ look like a clean pass either.
204
+
205
+ ## License
206
+
207
+ Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leonardo-marziali/ad-lfl-kit",
3
- "version": "0.0.0-development",
3
+ "version": "0.0.1-development",
4
4
  "description": "Agentic development lint-fix-loop kit: session-scoped file tracking and a capped Stop-hook fix loop for Claude Code plugins.",
5
5
  "keywords": [
6
6
  "claude-code",