continuous-improvement 3.20.4 → 3.22.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.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +23 -0
  3. package/QUICKSTART.md +1 -1
  4. package/README.md +7 -6
  5. package/bin/check-reconcile-parity.mjs +168 -0
  6. package/bin/generate-plugin-manifests.mjs +2 -0
  7. package/bin/install.mjs +30 -69
  8. package/bin/reconcile.mjs +259 -0
  9. package/commands/production-readiness-review.md +5 -4
  10. package/commands/reconcile.md +30 -6
  11. package/commands/simplicity-review.md +35 -0
  12. package/commands/verify-install.md +2 -2
  13. package/hooks/session.mjs +85 -0
  14. package/lib/git-state.mjs +411 -0
  15. package/lib/plugin-metadata.mjs +18 -20
  16. package/llms.txt +1 -1
  17. package/package.json +6 -4
  18. package/plugins/beginner.json +1 -1
  19. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
  20. package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
  21. package/plugins/continuous-improvement/bin/reconcile.mjs +259 -0
  22. package/plugins/continuous-improvement/commands/production-readiness-review.md +5 -4
  23. package/plugins/continuous-improvement/commands/reconcile.md +30 -6
  24. package/plugins/continuous-improvement/commands/simplicity-review.md +35 -0
  25. package/plugins/continuous-improvement/commands/verify-install.md +2 -2
  26. package/plugins/continuous-improvement/hooks/hooks.json +15 -16
  27. package/plugins/continuous-improvement/hooks/session.mjs +85 -0
  28. package/plugins/continuous-improvement/lib/git-state.mjs +411 -0
  29. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +18 -20
  30. package/plugins/continuous-improvement/skills/README.md +1 -0
  31. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +1 -0
  32. package/plugins/continuous-improvement/skills/reconcile/SKILL.md +62 -12
  33. package/plugins/continuous-improvement/skills/simplicity-review/SKILL.md +80 -0
  34. package/plugins/expert.json +1 -1
  35. package/skills/proceed-with-the-recommendation.md +1 -0
  36. package/skills/reconcile.md +62 -12
  37. package/skills/simplicity-review.md +80 -0
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * reconcile (`ci-reconcile`).
4
+ *
5
+ * Run the `reconcile` skill's ground-truth pass and print the resolved-state
6
+ * block, so Law 1 is a command an agent runs rather than prose it claims to
7
+ * have followed.
8
+ *
9
+ * node bin/reconcile.mjs resolved-state block; exit 1 on a blocker
10
+ * node bin/reconcile.mjs --json the same state as JSON
11
+ * node bin/reconcile.mjs --snapshot one-line envelope, field-compatible
12
+ * with scripts/git-state-snapshot.sh
13
+ * node bin/reconcile.mjs --verify-push <br> prove a push landed on origin/<br>
14
+ * node bin/reconcile.mjs --explain print the probe set and why each runs
15
+ * node bin/reconcile.mjs --cwd <dir> run against another repo root
16
+ *
17
+ * Portability: every probe is `git` argv spawned with `shell: false`. No bash,
18
+ * no coreutils, no `.git/`-relative paths — so it behaves the same in PowerShell,
19
+ * cmd, Git Bash, WSL and a POSIX shell, and it stays correct inside a linked
20
+ * worktree where `.git` is a file rather than a directory.
21
+ *
22
+ * Exit codes:
23
+ * 0 — ground truth established, nothing blocks a mutation
24
+ * 1 — at least one blocker (detached HEAD, in-progress op, unknown upstream)
25
+ * 2 — not a git repository, or git is unavailable
26
+ */
27
+ import { existsSync } from "node:fs";
28
+ import { spawnSync } from "node:child_process";
29
+ import { argv, cwd, exit, stderr, stdout } from "node:process";
30
+ import { GROUND_TRUTH_PROBES, IN_PROGRESS_MARKERS, accountDirty, assessGitState, classifyHead, classifyUpstream, hasBlockers, isSafeRefName, parseRevListCounts, renderResolvedState, verifyPushLanded, } from "../lib/git-state.mjs";
31
+ const NOT_A_REPO = 2;
32
+ /** Spawn `git` with no shell. A missing git binary reads as a failed probe. */
33
+ function git(args, root) {
34
+ const result = spawnSync("git", [...args], {
35
+ cwd: root,
36
+ encoding: "utf8",
37
+ shell: false,
38
+ windowsHide: true,
39
+ });
40
+ if (result.error) {
41
+ return { stdout: "", stderr: result.error.message, code: 127 };
42
+ }
43
+ return {
44
+ stdout: result.stdout ?? "",
45
+ stderr: result.stderr ?? "",
46
+ code: typeof result.status === "number" ? result.status : 1,
47
+ };
48
+ }
49
+ function parseArgs(args) {
50
+ const options = {
51
+ json: false,
52
+ snapshot: false,
53
+ explain: false,
54
+ verifyPush: null,
55
+ root: cwd(),
56
+ };
57
+ for (let i = 0; i < args.length; i++) {
58
+ const arg = args[i] ?? "";
59
+ if (arg === "--json")
60
+ options.json = true;
61
+ else if (arg === "--snapshot")
62
+ options.snapshot = true;
63
+ else if (arg === "--explain")
64
+ options.explain = true;
65
+ else if (arg === "--verify-push") {
66
+ const value = args[i + 1];
67
+ if (value === undefined || value.startsWith("--")) {
68
+ throw new Error("--verify-push requires a branch name");
69
+ }
70
+ if (!isSafeRefName(value)) {
71
+ throw new Error(`--verify-push branch is not a usable ref name: ${value}`);
72
+ }
73
+ options.verifyPush = value;
74
+ i++;
75
+ }
76
+ else if (arg === "--cwd") {
77
+ const value = args[i + 1];
78
+ if (value === undefined || value.startsWith("--")) {
79
+ throw new Error("--cwd requires a directory");
80
+ }
81
+ options.root = value;
82
+ i++;
83
+ }
84
+ else if (arg === "--help" || arg === "-h") {
85
+ options.explain = true;
86
+ }
87
+ else {
88
+ throw new Error(`unknown argument: ${arg}`);
89
+ }
90
+ }
91
+ return options;
92
+ }
93
+ /** Probe each in-progress marker through `--git-path`, which is worktree-correct. */
94
+ function probeInProgress(root) {
95
+ const present = [];
96
+ for (const marker of IN_PROGRESS_MARKERS) {
97
+ const resolved = git(["rev-parse", "--git-path", marker.gitPath], root);
98
+ // A failed resolve leaves the marker unprobed rather than absent; the
99
+ // classifier reports that explicitly instead of implying a clean tree.
100
+ if (resolved.code !== 0)
101
+ continue;
102
+ const path = resolved.stdout.trim();
103
+ if (path.length > 0 && existsSync(path))
104
+ present.push(marker.id);
105
+ }
106
+ return present;
107
+ }
108
+ function explain() {
109
+ const rows = GROUND_TRUTH_PROBES.map((probe) => ` git ${probe.args.join(" ")}\n ${probe.purpose}` +
110
+ (probe.tolerateFailure ? "\n (a non-zero exit is an answer, not a failure)" : ""));
111
+ const markers = IN_PROGRESS_MARKERS.map((marker) => ` git rev-parse --git-path ${marker.gitPath} # ${marker.label}`);
112
+ return [
113
+ "reconcile ground-truth probes (source: src/lib/git-state.mts):",
114
+ ...rows,
115
+ "",
116
+ "in-progress operation markers:",
117
+ ...markers,
118
+ ].join("\n");
119
+ }
120
+ function main() {
121
+ let options;
122
+ try {
123
+ options = parseArgs(argv.slice(2));
124
+ }
125
+ catch (error) {
126
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
127
+ exit(NOT_A_REPO);
128
+ return;
129
+ }
130
+ if (options.explain) {
131
+ stdout.write(`${explain()}\n`);
132
+ exit(0);
133
+ return;
134
+ }
135
+ const root = git(["rev-parse", "--show-toplevel"], options.root);
136
+ if (root.code !== 0) {
137
+ if (options.snapshot) {
138
+ stdout.write('{"error":"not-a-git-repo"}\n');
139
+ }
140
+ else {
141
+ stderr.write("not a git repository (or git is unavailable)\n");
142
+ }
143
+ exit(NOT_A_REPO);
144
+ return;
145
+ }
146
+ const repoRoot = root.stdout.trim();
147
+ const headSha = git(["rev-parse", "HEAD"], options.root);
148
+ const branchProbe = git(["symbolic-ref", "--quiet", "--short", "HEAD"], options.root);
149
+ const head = classifyHead(branchProbe.stdout, branchProbe.code);
150
+ const upstreamProbe = git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], options.root);
151
+ const upstreamRef = upstreamProbe.code === 0 ? upstreamProbe.stdout.trim() || null : null;
152
+ // Only ask for counts once an upstream is known to exist — the bare command
153
+ // exits 128 with `fatal: no upstream configured` otherwise.
154
+ const countsProbe = upstreamRef === null
155
+ ? null
156
+ : git(["rev-list", "--left-right", "--count", "@{u}...HEAD"], options.root);
157
+ const counts = countsProbe !== null && countsProbe.code === 0
158
+ ? parseRevListCounts(countsProbe.stdout)
159
+ : null;
160
+ const status = git(["status", "--porcelain=v1"], options.root);
161
+ const drift = git(["diff", "--name-only", "--ignore-all-space"], options.root);
162
+ const dirty = accountDirty(status.code === 0 ? status.stdout : null, drift.code === 0 ? drift.stdout : null);
163
+ if (options.snapshot) {
164
+ // Field-compatible with scripts/git-state-snapshot.sh, plus `contentDrift`
165
+ // and `inProgress`, which the shell version cannot report.
166
+ const upstreamSha = upstreamRef === null ? "none" : git(["rev-parse", "--short", "@{u}"], options.root).stdout.trim() || "none";
167
+ stdout.write(`${JSON.stringify({
168
+ head: git(["rev-parse", "--short", "HEAD"], options.root).stdout.trim(),
169
+ upstream: upstreamSha,
170
+ dirty: dirty.reported,
171
+ root: repoRoot,
172
+ branch: head.branch ?? "detached",
173
+ contentDrift: dirty.contentDrift,
174
+ inProgress: probeInProgress(options.root),
175
+ })}\n`);
176
+ exit(0);
177
+ return;
178
+ }
179
+ if (options.verifyPush !== null) {
180
+ const lsRemote = git(["ls-remote", "origin", `refs/heads/${options.verifyPush}`], options.root);
181
+ const verdict = verifyPushLanded({
182
+ localHead: headSha.stdout.trim(),
183
+ lsRemoteStdout: lsRemote.stdout,
184
+ lsRemoteExitCode: lsRemote.code,
185
+ });
186
+ const line = `push ${options.verifyPush}: ${verdict.verdict} — ${verdict.reason}`;
187
+ if (options.json)
188
+ stdout.write(`${JSON.stringify({ branch: options.verifyPush, ...verdict })}\n`);
189
+ else
190
+ stdout.write(`${line}\n`);
191
+ exit(verdict.verdict === "landed" ? 0 : 1);
192
+ return;
193
+ }
194
+ const inProgress = probeInProgress(options.root);
195
+ const findings = assessGitState({
196
+ head,
197
+ upstreamRef,
198
+ counts,
199
+ inProgress,
200
+ dirty,
201
+ });
202
+ const stashes = git(["stash", "list"], options.root);
203
+ const stashCount = stashes.code === 0
204
+ ? stashes.stdout.split(/\r?\n/).filter((line) => line.trim().length > 0).length
205
+ : null;
206
+ findings.push({
207
+ id: "stashes",
208
+ label: "stashes",
209
+ detail: stashCount === null
210
+ ? "unprobed"
211
+ : stashCount === 0
212
+ ? "none"
213
+ : `${stashCount} — may hold uncommitted work from an earlier session`,
214
+ severity: stashCount === null ? "warn" : "ok",
215
+ });
216
+ const worktrees = git(["worktree", "list", "--porcelain"], options.root);
217
+ const worktreeCount = worktrees.code === 0
218
+ ? worktrees.stdout.split(/\r?\n/).filter((line) => line.startsWith("worktree ")).length
219
+ : null;
220
+ findings.push({
221
+ id: "worktrees",
222
+ label: "worktrees",
223
+ detail: worktreeCount === null
224
+ ? "unprobed"
225
+ : worktreeCount <= 1
226
+ ? "1 (this one)"
227
+ : `${worktreeCount} — a concurrent writer may hold another; re-read HEAD immediately before each mutation`,
228
+ severity: worktreeCount === null || (worktreeCount ?? 0) > 1 ? "warn" : "ok",
229
+ });
230
+ const blocked = hasBlockers(findings);
231
+ if (options.json) {
232
+ stdout.write(`${JSON.stringify({
233
+ root: repoRoot,
234
+ head: headSha.stdout.trim(),
235
+ branch: head.branch,
236
+ headKind: head.kind,
237
+ upstream: upstreamRef,
238
+ relation: classifyUpstream(counts, upstreamRef !== null),
239
+ counts,
240
+ inProgress,
241
+ dirty,
242
+ stashes: stashCount,
243
+ worktrees: worktreeCount,
244
+ findings,
245
+ blocked,
246
+ }, null, 2)}\n`);
247
+ }
248
+ else {
249
+ stdout.write(`${renderResolvedState(findings)}\n`);
250
+ if (blocked) {
251
+ stdout.write("\nBLOCKED — resolve every blocker above before mutating this tree.\n");
252
+ }
253
+ }
254
+ exit(blocked ? 1 : 0);
255
+ }
256
+ const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
257
+ if (invokedDirectly || argv[1]?.endsWith("reconcile.mjs")) {
258
+ main();
259
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: production-readiness-review
3
- description: "Parallel multi-agent readiness gate — fan blind reviewers across performance, security, UI/UX, and test coverage, each grounding findings in real code/logs/live data, then reconcile into one deduplicated, severity-ranked punch-list. Reports only; never fixes, merges, or deploys."
3
+ description: "Parallel multi-agent readiness gate — fan blind reviewers across performance, security, UI/UX, test coverage, and simplicity, each grounding findings in real code/logs/live data, then reconcile into one deduplicated, severity-ranked punch-list. Reports only; never fixes, merges, or deploys."
4
4
  ---
5
5
 
6
6
  # /production-readiness-review
@@ -20,11 +20,12 @@ Pure routing over existing skills and agents. Adds no new code.
20
20
  ## Behavior
21
21
 
22
22
  1. **Scope** — establish ground truth: the diff under review and which changes are recent (`git diff`; `reconcile` fallback for branch/base state). Recent changes get extra scrutiny because they are the likeliest source of self-inflicted defects.
23
- 2. **Fan out** — `superpowers:dispatching-parallel-agents` launches four reviewers, each blind to the others. Every reviewer is instructed to ground each finding in real code, logs, or live queries, and never to assume or fabricate state:
23
+ 2. **Fan out** — `superpowers:dispatching-parallel-agents` launches five reviewers, each blind to the others. Every reviewer is instructed to ground each finding in real code, logs, or live queries, and never to assume or fabricate state:
24
24
  - **Performance & bundle-size** — hot paths, N+1 queries, unbounded work, regressions.
25
25
  - **Security & data-access** (`security-auditor`) — authn/authz, input handling, injection, secret exposure, unsafe data access.
26
26
  - **UI/UX correctness** — verified live with Playwright when the MCP is available, else static review of the changed surface.
27
27
  - **Test coverage & flaky/stale mocks** (`test-engineer`) — uncovered branches, stale mocks, timing-flaky tests.
28
+ - **Simplicity & over-engineering** (`simplicity-review`) — code that could reuse an existing file, a stdlib or native feature, or fewer lines; reports trim opportunities via the reuse ladder and never flags input validation, data-loss handling, security, or accessibility.
28
29
  3. **Reconcile** — a final pass dedupes findings across reviewers, ranks each CRITICAL / HIGH / MEDIUM / LOW by severity and confidence, and explicitly flags any defect introduced by the changes under review.
29
30
  4. **Present** — emit the consolidated punch-list, severity-ranked, with file references. **Stop.**
30
31
 
@@ -42,7 +43,7 @@ Pure routing over existing skills and agents. Adds no new code.
42
43
 
43
44
  ## Composition
44
45
 
45
- Routes through: `reconcile` (scope/ground truth) → `superpowers:dispatching-parallel-agents` (fan-out) → the `security-auditor` and `test-engineer` agents (two of the four dimensions) → a reconciliation pass that ranks and dedupes. Each step falls back to its inline behavior when the preferred skill or agent is not installed.
46
+ Routes through: `reconcile` (scope/ground truth) → `superpowers:dispatching-parallel-agents` (fan-out) → the `security-auditor` and `test-engineer` agents and the `simplicity-review` skill (three of the five dimensions) → a reconciliation pass that ranks and dedupes. Each step falls back to its inline behavior when the preferred skill or agent is not installed.
46
47
 
47
48
  ## Example
48
49
 
@@ -50,4 +51,4 @@ Routes through: `reconcile` (scope/ground truth) → `superpowers:dispatching-pa
50
51
  /production-readiness-review #246
51
52
  ```
52
53
 
53
- Scopes PR #246's diff, fans four blind reviewers across performance, security, UI/UX, and test coverage, then returns one deduplicated severity-ranked punch-list — flagging anything the PR's own changes introduced — and stops for you to prioritize.
54
+ Scopes PR #246's diff, fans five blind reviewers across performance, security, UI/UX, test coverage, and simplicity, then returns one deduplicated severity-ranked punch-list — flagging anything the PR's own changes introduced — and stops for you to prioritize.
@@ -13,15 +13,36 @@ Snapshots the full git state in one pass, detects a concurrent writer, classifie
13
13
 
14
14
  ## Establish ground truth
15
15
 
16
+ One command, cross-platform, no shell required:
17
+
18
+ ```
19
+ npx ci-reconcile # resolved-state block; exit 0 clear / 1 blocked / 2 not a repo
20
+ npx ci-reconcile --json # machine-readable
21
+ npx ci-reconcile --explain # the probe set and why each probe runs
22
+ ```
23
+
24
+ The same pass by hand. `src/lib/git-state.mts` is the source of truth for this list, and `npm run verify:reconcile-parity` fails if this file drifts from it:
25
+
16
26
  ```
17
- git branch --show-current
18
- git status --porcelain=v1 # but trust git diff --stat for real drift (autocrlf)
19
- git rev-list --left-right --count '@{u}...HEAD' # behind / ahead (quote the ref — bare @{u} trips the Bash parser)
27
+ git rev-parse --show-toplevel # inside a work tree, and where
28
+ git rev-parse HEAD # the sha every later claim is relative to
29
+ git symbolic-ref --quiet --short HEAD # branch; NON-ZERO EXIT = detached HEAD
30
+ git rev-parse --abbrev-ref --symbolic-full-name @{u} # upstream, or non-zero = none configured
31
+ git rev-list --left-right --count @{u}...HEAD # behind/ahead — only after the line above succeeded
32
+ git status --porcelain=v1 # reported changes (inflated by autocrlf)
33
+ git diff --name-only --ignore-all-space # real content drift — the number to trust
20
34
  git stash list
21
- git worktree list
22
- ls .git/MERGE_HEAD .git/rebase-merge .git/rebase-apply 2>/dev/null # in-progress op = another actor; do not race
35
+ git worktree list --porcelain
36
+ git rev-parse --git-path MERGE_HEAD # in-progress op: test the RESOLVED path
23
37
  ```
24
38
 
39
+ Four boundaries where the obvious command lies:
40
+
41
+ - **No upstream** — asking `git rev-list` for counts against `@{u}` exits 128; it does not return zeros. Resolve the upstream first.
42
+ - **Detached HEAD** — the `--show-current` form of `git branch` prints "" and exits 0, indistinguishable from success. `symbolic-ref --quiet` exits non-zero instead. Detached blocks.
43
+ - **Linked worktree** — `.git` is a *file* there, so a `.git/`-relative marker probe exits 2 exactly as it does on a clean tree: a real conflicted merge reads as clean. Use `rev-parse --git-path`.
44
+ - **autocrlf** — `git status` overstates drift. Stage by explicit filename, never `git add -A`.
45
+
25
46
  ## Then act, with gates
26
47
 
27
48
  ```
@@ -51,9 +72,12 @@ gh pr create --fill --base main # open one PR, then STOP
51
72
  ## Verify the push landed
52
73
 
53
74
  ```
54
- git ls-remote origin refs/heads/<branch> # remote tip must equal local HEAD, else it did not land
75
+ npx ci-reconcile --verify-push <branch> # exit 0 only when the remote tip equals local HEAD
76
+ git ls-remote origin refs/heads/<branch> # by hand: remote tip must equal local HEAD
55
77
  ```
56
78
 
79
+ Three outcomes, never two: **landed** (tip matches), **not-landed** (probe succeeded, ref absent or different sha), **unverified** (`ls-remote` itself failed — you do not know; retry, and report neither success nor failure).
80
+
57
81
  ## Sync the default branch after the PR merges
58
82
 
59
83
  "Latest work on main" is true only once the PR merges, and on a protected branch that merge is a human action. After it lands:
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: simplicity-review
3
+ description: Review the current diff for over-engineering — flag code that could reuse an existing file, a stdlib or native feature, or fewer lines — and report GO/TRIM findings without touching code. Enforces Law 4 (Verify Before Reporting).
4
+ ---
5
+
6
+ # /simplicity-review — Judge the Diff Before You Ship It
7
+
8
+ Read the current change like the laziest senior dev in the room: could this have been smaller? Passing tests prove correctness, not minimality. Backed by the `simplicity-review` skill.
9
+
10
+ ## What it does
11
+
12
+ Takes the working-tree diff (vs HEAD by default; accepts an optional commit range or file list), reads each changed block, and walks a fixed reuse ladder:
13
+
14
+ ```
15
+ 1. Does this need to exist? -> skip it (YAGNI)
16
+ 2. Already in this codebase? -> reuse it
17
+ 3. Stdlib does it? -> use it
18
+ 4. Native platform feature? -> use it
19
+ 5. Installed dependency? -> use it
20
+ 6. One line? -> one line
21
+ 7. Only then: the minimum that works
22
+ ```
23
+
24
+ It reports `file:line`, what is over-built, the specific simpler path, and closes with `GO` (already minimal) or `TRIM` (findings to apply). It does not edit code.
25
+
26
+ ## Default skeptical
27
+
28
+ A finding is a hypothesis. Read the surrounding code and prove the simpler path exists and preserves behavior before asserting it; a wrong trim is worse than the over-build. Never flag input validation, data-loss-preventing error handling, security, or accessibility — lazy, not negligent.
29
+
30
+ ## Pairs with
31
+
32
+ - **`simplicity-review`** skill — the discipline this command runs.
33
+ - **`proceed-with-the-recommendation`** — apply the trims under the 7 Laws.
34
+ - **`verification-loop`** — the ladder to re-run on whatever you trim.
35
+ - **`production-readiness-review`** — the sibling diff review for performance, security, UI, and test coverage.
@@ -40,8 +40,8 @@ The observation hook appends one row per tool call to
40
40
  `<project-hash>` from the current repo, or check `~/.claude/instincts/global/`).
41
41
 
42
42
  - If it exists and has at least one row — capture is recording. Record `observe: ✓`.
43
- - If it is missing or empty record `observe: ✗ (observation hook not recording
44
- on Windows confirm Git Bash is installed before WSL Bash on PATH, then re-run the installer)`.
43
+ - If it is missing or empty, record `observe: ✗ (observation hook not recording; re-run
44
+ the installer to migrate legacy Bash hook rows to the Node observer)`.
45
45
 
46
46
  ## Report
47
47
 
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync, readdirSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { resolveHomeDir } from "../lib/resolve-home-dir.mjs";
7
+ function read(path) {
8
+ try {
9
+ return readFileSync(path, "utf8");
10
+ }
11
+ catch {
12
+ return "";
13
+ }
14
+ }
15
+ function eventFromStdin() {
16
+ const raw = read(0);
17
+ if (!raw)
18
+ return null;
19
+ try {
20
+ const payload = JSON.parse(raw);
21
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
22
+ return null;
23
+ const event = payload.hook_event_name ?? payload.hook_type ?? payload.event_type;
24
+ return event === "SessionStart" || event === "SessionEnd" ? event : "unknown";
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ function projectRoot() {
31
+ if (process.env.CLAUDE_PROJECT_DIR)
32
+ return process.env.CLAUDE_PROJECT_DIR;
33
+ try {
34
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
35
+ encoding: "utf8",
36
+ stdio: ["ignore", "pipe", "ignore"],
37
+ }).trim() || "global";
38
+ }
39
+ catch {
40
+ return "global";
41
+ }
42
+ }
43
+ function yamlFiles(dir) {
44
+ try {
45
+ return readdirSync(dir)
46
+ .filter((name) => name.endsWith(".yaml"))
47
+ .map((name) => join(dir, name));
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ function main() {
54
+ const event = eventFromStdin();
55
+ if (event === null)
56
+ return;
57
+ if (event === "SessionEnd") {
58
+ process.stderr.write("[continuous-improvement] Session ending. Run /continuous-improvement to reflect and capture learnings.\n");
59
+ return;
60
+ }
61
+ const home = resolveHomeDir();
62
+ if (!home)
63
+ return;
64
+ const instinctsRoot = join(home, ".claude", "instincts");
65
+ const hash = createHash("sha256").update(projectRoot()).digest("hex").slice(0, 12);
66
+ const projectDir = join(instinctsRoot, hash);
67
+ const files = [...yamlFiles(projectDir), ...yamlFiles(join(instinctsRoot, "global"))];
68
+ const observations = read(join(projectDir, "observations.jsonl")).split(/\r?\n/).filter(Boolean).length;
69
+ let level = observations >= 20 || files.length > 0 ? "ANALYZE" : "CAPTURE";
70
+ for (const file of files) {
71
+ const value = Number(read(file).match(/^confidence:\s*([0-9]*\.?[0-9]+)/m)?.[1]);
72
+ if (Number.isFinite(value) && value >= 0.7) {
73
+ level = "AUTO-APPLY";
74
+ break;
75
+ }
76
+ if (Number.isFinite(value) && value >= 0.5)
77
+ level = "SUGGEST";
78
+ }
79
+ process.stderr.write(`[continuous-improvement] Level: ${level} | Observations: ${observations} | Instincts: ${files.length}\n`);
80
+ }
81
+ try {
82
+ main();
83
+ }
84
+ catch {
85
+ }