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.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +23 -0
- package/QUICKSTART.md +1 -1
- package/README.md +7 -6
- package/bin/check-reconcile-parity.mjs +168 -0
- package/bin/generate-plugin-manifests.mjs +2 -0
- package/bin/install.mjs +30 -69
- package/bin/reconcile.mjs +259 -0
- package/commands/production-readiness-review.md +5 -4
- package/commands/reconcile.md +30 -6
- package/commands/simplicity-review.md +35 -0
- package/commands/verify-install.md +2 -2
- package/hooks/session.mjs +85 -0
- package/lib/git-state.mjs +411 -0
- package/lib/plugin-metadata.mjs +18 -20
- package/llms.txt +1 -1
- package/package.json +6 -4
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/bin/reconcile.mjs +259 -0
- package/plugins/continuous-improvement/commands/production-readiness-review.md +5 -4
- package/plugins/continuous-improvement/commands/reconcile.md +30 -6
- package/plugins/continuous-improvement/commands/simplicity-review.md +35 -0
- package/plugins/continuous-improvement/commands/verify-install.md +2 -2
- package/plugins/continuous-improvement/hooks/hooks.json +15 -16
- package/plugins/continuous-improvement/hooks/session.mjs +85 -0
- package/plugins/continuous-improvement/lib/git-state.mjs +411 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +18 -20
- package/plugins/continuous-improvement/skills/README.md +1 -0
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +62 -12
- package/plugins/continuous-improvement/skills/simplicity-review/SKILL.md +80 -0
- package/plugins/expert.json +1 -1
- package/skills/proceed-with-the-recommendation.md +1 -0
- package/skills/reconcile.md +62 -12
- 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,
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
package/commands/reconcile.md
CHANGED
|
@@ -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
|
|
18
|
-
git
|
|
19
|
-
git
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
|
|
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
|
+
}
|