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
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"plugins": [
|
|
8
8
|
{
|
|
9
9
|
"name": "continuous-improvement",
|
|
10
|
-
"description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as
|
|
11
|
-
"version": "3.
|
|
10
|
+
"description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 28 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
|
|
11
|
+
"version": "3.22.0",
|
|
12
12
|
"source": "./plugins/continuous-improvement",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to this skill are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [3.22.0] — 2026-08-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`ci-reconcile`**: the `reconcile` ground-truth pass is now a command, not just prose. It spawns `git` argv with no shell — no bash, no coreutils, no `.git/`-relative path — so it behaves the same in PowerShell, cmd, Git Bash and WSL, and stays correct inside a linked worktree. `--json`, `--explain`, `--verify-push <branch>`, and `--snapshot` (field-compatible with `scripts/git-state-snapshot.sh`, plus `contentDrift` and `inProgress`). Exits `0` clear / `1` blocked / `2` not a git repository. (#289)
|
|
12
|
+
- **`verify:reconcile-parity`**, the 16th `verify:all` invariant: a fenced code block in each of `skills/reconcile.md` and `commands/reconcile.md` must prescribe every probe in `GROUND_TRUTH_PROBES` verbatim, and no fenced block may reintroduce a retired form. Fenced-only, so prose stays free to explain *why* a command was retired. (#289)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`reconcile` no longer reports a false clean state at four boundaries.** Reproduced against real git: asking `git rev-list` for counts against `@{u}` exits **128** with `fatal: no upstream configured` rather than returning zeros; `git branch --show-current` prints an empty string and exits **0** on a detached HEAD, indistinguishable from a successful read; inside a linked worktree `.git` is a **file**, so listing a `.git/`-relative `MERGE_HEAD` exits **2** exactly as it does on a clean tree, meaning a real conflicted merge read as clean; and `git status` overstates drift on an `autocrlf` tree. The skill and command now document a portable probe for each, plus a compatibility matrix. (#289)
|
|
17
|
+
- **Every `git-state` classifier fails closed.** Unparseable ahead/behind counts read as `unknown` (a blocker), never `even`. A failed `git ls-remote` reads as `unverified` — never `not-landed` and never `landed` — so a network failure can no longer be reported as either outcome. An unprobed in-progress marker reads as `unprobed`, never `absent`. An unparseable branch name never matches an expected one. (#289)
|
|
18
|
+
- Verify-lint count prose corrected across `CLAUDE.md` (claimed 14 and omitted `landing-version`), `CONTRIBUTING.md` (13) and `docs/RELEASING.md` (12); the actual count was 15, now 16. (#289)
|
|
19
|
+
|
|
20
|
+
## [3.21.0] — 2026-07-11
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`/simplicity-review`**: a diff-scoped over-engineering reviewer. It reads the current diff and walks a reuse ladder (does it need to exist? already in the codebase? stdlib? native feature? one line?), reporting `GO` or `TRIM` trim findings without editing, with a safety carve-out that never flags input validation, data-loss handling, security, or accessibility. Review-only (Law 4), tier 2, routed into `proceed-with-the-recommendation`. Brings the bundle to 28 skills. (#285)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **`/production-readiness-review` gains a fifth blind reviewer**: a simplicity and over-engineering dimension that delegates to the `simplicity-review` skill, so the readiness gate now spans performance, security, UI/UX, test coverage, and simplicity. (#286)
|
|
29
|
+
|
|
7
30
|
## [3.17.0] — 2026-06-28
|
|
8
31
|
|
|
9
32
|
### Added
|
package/QUICKSTART.md
CHANGED
|
@@ -139,7 +139,7 @@ npx continuous-improvement install --mode expert
|
|
|
139
139
|
npx continuous-improvement install --pack react # optional: react | python | go | meta
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
Precondition: Node 18 / 20 / 22. Runtime hooks execute Node directly, so Git Bash and `jq` are not required. Re-run the installer once after upgrading to migrate legacy Bash hook rows. See [README.md § Expert](README.md#expert--adds-mcp-server-observation-hooks-and-instinct-packs) for the troubleshooting matrix.
|
|
143
143
|
|
|
144
144
|
Verify with `/dashboard` — you should see instinct health and observation count.
|
|
145
145
|
|
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ V1 honest limitations: the runtime gate is honor-system once the agent flips `_g
|
|
|
137
137
|
|
|
138
138
|
Pick this if you want the MCP tools (19 of them, including `ci_plan_init` / `ci_plan_status` for `task_plan.md`-style planning), the session hooks that feed Mulahazah, and starter packs.
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
Precondition: Node 18 / 20 / 22. Observation and session hooks execute Node directly, so Git Bash and `jq` are not required. Re-run the installer once after upgrading to migrate installer-owned `observe.sh` and `session.sh` rows to the Node entrypoints.
|
|
141
141
|
|
|
142
142
|
```bash
|
|
143
143
|
npx continuous-improvement install --mode expert
|
|
@@ -158,7 +158,7 @@ Three failures account for nearly every install support thread. Try them in orde
|
|
|
158
158
|
| Symptom | Real cause | Fix |
|
|
159
159
|
|---|---|---|
|
|
160
160
|
| `/discipline` says "command not recognized" right after `/plugin install` | Slash commands load on session start; the marketplace did pick the plugin up | Quit and reopen Claude Code, then run `/discipline` again |
|
|
161
|
-
|
|
|
161
|
+
| Hooks report Bash path errors after upgrading | Stale installer-owned `observe.sh` or `session.sh` rows remain in `settings.json` | Re-run `npx continuous-improvement install --mode expert`; the installer migrates those rows to Node and preserves foreign hooks |
|
|
162
162
|
| `/plugin marketplace add ...` returned nothing visible | Marketplace add was silent; the plugin is not yet selected | Run `/plugin install continuous-improvement@continuous-improvement` to select and activate it |
|
|
163
163
|
|
|
164
164
|
If none of those apply, paste the output of `npx continuous-improvement install` into a GitHub issue — that surface logs every step.
|
|
@@ -270,7 +270,7 @@ Hooks capture every tool call. After ~20 observations Claude analyzes patterns a
|
|
|
270
270
|
## Slash commands
|
|
271
271
|
|
|
272
272
|
<details>
|
|
273
|
-
<summary><b>All
|
|
273
|
+
<summary><b>All 29 commands (Beginner gets every one)</b></summary>
|
|
274
274
|
|
|
275
275
|
`/seven-laws` is the canonical reflect-and-learn command. `/continuous-improvement` is kept as an alias for backward compatibility — both run the same workflow.
|
|
276
276
|
|
|
@@ -290,6 +290,7 @@ Hooks capture every tool call. After ~20 observations Claude analyzes patterns a
|
|
|
290
290
|
/audit Audit recent commits for real defects, confirm each before fixing
|
|
291
291
|
/ship Single-defect fast path — reconcile, TDD fix, verify, one PR
|
|
292
292
|
/production-readiness-review Parallel readiness gate — severity-ranked punch-list (reports only)
|
|
293
|
+
/simplicity-review Judge the current diff for over-engineering (GO / TRIM, reports only)
|
|
293
294
|
/handoff End-of-session compaction into mktemp brief for the next agent
|
|
294
295
|
/recall BM25 search over past observations — "have I hit this before?"
|
|
295
296
|
/discipline Quick reference card of the 7 Laws
|
|
@@ -305,7 +306,7 @@ Hooks capture every tool call. After ~20 observations Claude analyzes patterns a
|
|
|
305
306
|
/swarm Fan-out coordination across parallel sub-agents
|
|
306
307
|
```
|
|
307
308
|
|
|
308
|
-
All
|
|
309
|
+
All 29 ship in the marketplace bundle. The Beginner install gets all of them — with one caveat: `/learn-eval`, `/harvest`, and `/distill` only produce useful output once Mulahazah has accumulated observation history (~20 observations), so running them on day 1 returns an empty result, not a broken command. `/swarm` and `/release-train` are orchestration commands aimed at larger multi-agent or multi-PR work. In Expert (`npx`) mode, the installer mirrors the full set into `~/.claude/commands/` and additionally exposes the planning workflow through the MCP tools `ci_plan_init` (initialize `task_plan.md`, `findings.md`, `progress.md` in the project root) and `ci_plan_status` (summarize their current contents).
|
|
309
310
|
|
|
310
311
|
</details>
|
|
311
312
|
|
|
@@ -313,7 +314,7 @@ All 28 ship in the marketplace bundle. The Beginner install gets all of them —
|
|
|
313
314
|
|
|
314
315
|
## Skills
|
|
315
316
|
|
|
316
|
-
The plugin ships **
|
|
317
|
+
The plugin ships **28 skills** — 1 core + 1 featured + 6 tier-1 + 17 tier-2 + 3 always-bundled. Beginner install gets tier-1, featured, and the always-bundled companion; Expert adds tier-2, the MCP server, and observation hooks. Full catalog with per-skill descriptions, Law tagging, and drop-in single-file install: [docs/skills.md](docs/skills.md). Adding a 29th skill: [CONTRIBUTING.md § Evolution — adding a new skill](CONTRIBUTING.md#evolution--adding-a-new-skill).
|
|
317
318
|
|
|
318
319
|
---
|
|
319
320
|
|
|
@@ -356,7 +357,7 @@ Proof-format templates ship in [templates/](templates/): `release_receipt_templa
|
|
|
356
357
|
|
|
357
358
|
- [QUICKSTART.md](QUICKSTART.md) — 2-minute setup
|
|
358
359
|
- [SKILL.md](SKILL.md) — full 7 Laws spec
|
|
359
|
-
- [docs/skills.md](docs/skills.md) — full
|
|
360
|
+
- [docs/skills.md](docs/skills.md) — full 28-skill catalog
|
|
360
361
|
- [examples/](examples/) — bug fix, feature build, refactor walkthroughs
|
|
361
362
|
- [templates/insights-claude-md.md](templates/insights-claude-md.md) — paste-in CLAUDE.md blocks for verification discipline, environment notes, think-before-acting, and git/deploy workflow (sourced from the 28-day usage report)
|
|
362
363
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — architecture, repo internals, adding a new skill
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reconcile-Parity Invariant Check
|
|
4
|
+
*
|
|
5
|
+
* The ground-truth git command set exists in three places at once: the runner
|
|
6
|
+
* (`bin/reconcile.mjs`), the skill (`skills/reconcile.md`) and the slash command
|
|
7
|
+
* (`commands/reconcile.md`). Prose copies drift — the `'@{u}'` Bash-quoting fix
|
|
8
|
+
* had to be applied by hand to both docs, and a portability fix applied to one
|
|
9
|
+
* copy leaves the other telling agents to run the broken form.
|
|
10
|
+
*
|
|
11
|
+
* `GROUND_TRUTH_PROBES` in src/lib/git-state.mts is the single source of truth.
|
|
12
|
+
* This lint asserts the docs still document exactly that set, and that neither
|
|
13
|
+
* doc has regressed to a command proven non-portable on 2026-08-02:
|
|
14
|
+
*
|
|
15
|
+
* Side A — a fenced code block in each doc prescribes every probe in
|
|
16
|
+
* GROUND_TRUTH_PROBES verbatim. Fenced blocks only: a probe named in
|
|
17
|
+
* prose but dropped from the copyable block would otherwise pass
|
|
18
|
+
* while the command an agent actually runs had lost it.
|
|
19
|
+
* Side B — no fenced code block in either doc *prescribes* a retired form:
|
|
20
|
+
* `ls .git/...` (`.git` is a FILE in a linked worktree,
|
|
21
|
+
* so the probe silently reports nothing)
|
|
22
|
+
* `branch --show-current` (empty stdout + exit 0 on a detached
|
|
23
|
+
* HEAD is indistinguishable from success)
|
|
24
|
+
* Only fenced blocks are scanned, so the prose stays free to explain
|
|
25
|
+
* why each form was retired without tripping its own lint.
|
|
26
|
+
* Side C — both docs document `rev-parse --git-path`, the worktree-correct way
|
|
27
|
+
* to locate an in-progress-operation marker.
|
|
28
|
+
*
|
|
29
|
+
* Fail-closed: a missing file or an empty doc is a violation, not a silent pass.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* node bin/check-reconcile-parity.mjs # Check the current repo
|
|
33
|
+
* node bin/check-reconcile-parity.mjs <repo-root> # Check a specific repo root
|
|
34
|
+
*
|
|
35
|
+
* Exit codes:
|
|
36
|
+
* 0 — both docs match the shipped probe set and carry no retired form
|
|
37
|
+
* 1 — at least one drift
|
|
38
|
+
*/
|
|
39
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
import { argv, cwd, exit } from "node:process";
|
|
42
|
+
import { GROUND_TRUTH_PROBES } from "../lib/git-state.mjs";
|
|
43
|
+
const DOCS = ["skills/reconcile.md", "commands/reconcile.md"];
|
|
44
|
+
const RETIRED_FORMS = [
|
|
45
|
+
{
|
|
46
|
+
pattern: /\bls\s+[^\n`]*\.git\//,
|
|
47
|
+
label: "ls .git/<marker>",
|
|
48
|
+
why: "`.git` is a FILE inside a linked worktree, so this probe exits 2 exactly as it does on a clean tree — a real merge reads as no operation in progress",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /\bbranch\s+--show-current\b/,
|
|
52
|
+
label: "git branch --show-current",
|
|
53
|
+
why: "returns an empty string with exit 0 on a detached HEAD, which cannot be told apart from a successful read; use `symbolic-ref --quiet --short HEAD`",
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const REQUIRED_SUBSTRINGS = [
|
|
57
|
+
{
|
|
58
|
+
needle: "rev-parse --git-path",
|
|
59
|
+
why: "the only worktree-correct way to locate MERGE_HEAD / rebase-merge / rebase-apply",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Normalize a doc or a command for comparison: collapse whitespace and drop the
|
|
64
|
+
* shell quoting docs add around `@{u}` refspecs. Returns "" for absent input.
|
|
65
|
+
*/
|
|
66
|
+
export function normalizeForMatch(text) {
|
|
67
|
+
if (typeof text !== "string")
|
|
68
|
+
return "";
|
|
69
|
+
return text.replace(/[`'"]/g, "").replace(/\s+/g, " ");
|
|
70
|
+
}
|
|
71
|
+
/** The literal command line a doc must contain for a probe, before normalization. */
|
|
72
|
+
export function probeCommand(args) {
|
|
73
|
+
return `git ${args.join(" ")}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Concatenate the bodies of every fenced code block in `body`.
|
|
77
|
+
*
|
|
78
|
+
* Retired forms are only a defect when a doc *prescribes* them, so Side B scans
|
|
79
|
+
* fenced blocks alone — prose may name a retired command to explain it. Returns
|
|
80
|
+
* "" when the doc has no fenced block.
|
|
81
|
+
*/
|
|
82
|
+
export function fencedBlocks(body) {
|
|
83
|
+
const lines = body.split(/\r?\n/);
|
|
84
|
+
const collected = [];
|
|
85
|
+
let inside = false;
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (/^\s*```/.test(line)) {
|
|
88
|
+
inside = !inside;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (inside)
|
|
92
|
+
collected.push(line);
|
|
93
|
+
}
|
|
94
|
+
return collected.join("\n");
|
|
95
|
+
}
|
|
96
|
+
/** Check one doc body against the shipped probe set. Returns [] when reconciled. */
|
|
97
|
+
export function checkDoc(doc, body) {
|
|
98
|
+
if (body === null)
|
|
99
|
+
return [{ doc, kind: "missing-file", detail: "file does not exist" }];
|
|
100
|
+
if (body.trim().length === 0)
|
|
101
|
+
return [{ doc, kind: "empty", detail: "file is empty" }];
|
|
102
|
+
const violations = [];
|
|
103
|
+
const prescribed = fencedBlocks(body);
|
|
104
|
+
const prescribedHaystack = normalizeForMatch(prescribed);
|
|
105
|
+
// Side A scans fenced blocks only. A probe named in prose but absent from the
|
|
106
|
+
// copyable block would otherwise satisfy the check while the command an agent
|
|
107
|
+
// actually runs had silently dropped it.
|
|
108
|
+
for (const probe of GROUND_TRUTH_PROBES) {
|
|
109
|
+
const command = probeCommand(probe.args);
|
|
110
|
+
if (!prescribedHaystack.includes(normalizeForMatch(command))) {
|
|
111
|
+
violations.push({
|
|
112
|
+
doc,
|
|
113
|
+
kind: "missing-probe",
|
|
114
|
+
detail: `no fenced block prescribes probe "${probe.id}": ${command}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const retired of RETIRED_FORMS) {
|
|
119
|
+
if (retired.pattern.test(prescribed)) {
|
|
120
|
+
violations.push({
|
|
121
|
+
doc,
|
|
122
|
+
kind: "retired-form",
|
|
123
|
+
detail: `a fenced block still prescribes \`${retired.label}\` — ${retired.why}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const haystack = normalizeForMatch(body);
|
|
128
|
+
for (const required of REQUIRED_SUBSTRINGS) {
|
|
129
|
+
if (!haystack.includes(normalizeForMatch(required.needle))) {
|
|
130
|
+
violations.push({
|
|
131
|
+
doc,
|
|
132
|
+
kind: "missing-substring",
|
|
133
|
+
detail: `does not mention \`${required.needle}\` — ${required.why}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return violations;
|
|
138
|
+
}
|
|
139
|
+
/** Read every doc under `repoRoot` and collect violations across all of them. */
|
|
140
|
+
export function checkRepo(repoRoot) {
|
|
141
|
+
return DOCS.flatMap((doc) => {
|
|
142
|
+
const path = join(repoRoot, doc);
|
|
143
|
+
const body = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
144
|
+
return checkDoc(doc, body);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function main() {
|
|
148
|
+
const repoRoot = argv[2] ?? cwd();
|
|
149
|
+
const violations = checkRepo(repoRoot);
|
|
150
|
+
if (violations.length === 0) {
|
|
151
|
+
console.log(`OK reconcile-parity: ${DOCS.length} doc(s) document all ${GROUND_TRUTH_PROBES.length} ground-truth probe(s), no retired forms.`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.error(`FAIL reconcile-parity: ${violations.length} drift(s).`);
|
|
155
|
+
console.error("");
|
|
156
|
+
for (const violation of violations) {
|
|
157
|
+
console.error(` ${violation.doc} — ${violation.detail}`);
|
|
158
|
+
}
|
|
159
|
+
console.error("");
|
|
160
|
+
console.error("Fix: GROUND_TRUTH_PROBES in src/lib/git-state.mts is the source of truth. Update the");
|
|
161
|
+
console.error("doc prose to match it (or change the probe set there first), then `npm run build` so the");
|
|
162
|
+
console.error("plugin mirrors regenerate.");
|
|
163
|
+
exit(1);
|
|
164
|
+
}
|
|
165
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
166
|
+
if (invokedDirectly || argv[1]?.endsWith("check-reconcile-parity.mjs")) {
|
|
167
|
+
main();
|
|
168
|
+
}
|
|
@@ -147,6 +147,8 @@ async function writePluginBundle() {
|
|
|
147
147
|
copyFileTo(join(REPO_ROOT, "bin", "mcp-server.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "mcp-server.mjs")),
|
|
148
148
|
copyFileTo(join(REPO_ROOT, "bin", "observe.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "observe.mjs")),
|
|
149
149
|
copyFileTo(join(REPO_ROOT, "bin", "backfill.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "backfill.mjs")),
|
|
150
|
+
copyFileTo(join(REPO_ROOT, "bin", "reconcile.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "reconcile.mjs")),
|
|
151
|
+
copyFileTo(join(REPO_ROOT, "lib", "git-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "git-state.mjs")),
|
|
150
152
|
copyFileTo(join(REPO_ROOT, "bin", "gateguard-clear.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "gateguard-clear.mjs")),
|
|
151
153
|
copyFileTo(join(REPO_ROOT, "lib", "gateguard-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "gateguard-state.mjs")),
|
|
152
154
|
copyFileTo(join(REPO_ROOT, "lib", "plugin-metadata.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "plugin-metadata.mjs")),
|
package/bin/install.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* npx continuous-improvement install --uninstall # remove everything
|
|
11
11
|
*/
|
|
12
12
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
13
|
-
import {
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -45,29 +45,25 @@ const SESSION_HOOK_TYPES = ["SessionStart", "SessionEnd"];
|
|
|
45
45
|
// accidental overlap between HOOK_TYPES and SESSION_HOOK_TYPES never causes
|
|
46
46
|
// a hook bucket to be processed twice.
|
|
47
47
|
const ALL_HOOK_TYPES = Array.from(new Set([...HOOK_TYPES, ...SESSION_HOOK_TYPES]));
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const INSTALLER_OBSERVE_SESSION_COMMAND_RE = /^bash ".*[\\/]\.claude[\\/]instincts[\\/](?:observe|session)\.sh"$/;
|
|
54
|
-
function isBrokenObserveOrSessionCommand(command) {
|
|
48
|
+
// Match only installer-owned lifecycle commands. Bash rows are legacy and get
|
|
49
|
+
// migrated on install; both legacy and current Node rows are removed on uninstall.
|
|
50
|
+
const LEGACY_OBSERVE_SESSION_COMMAND_RE = /^bash ".*[\\/]\.claude[\\/]instincts[\\/](?:observe|session)\.sh"$/;
|
|
51
|
+
const NODE_OBSERVE_SESSION_COMMAND_RE = /^node ".*[\\/]\.claude[\\/]instincts[\\/](?:bin[\\/]observe|session)\.mjs"$/;
|
|
52
|
+
function isLegacyObserveOrSessionCommand(command) {
|
|
55
53
|
if (typeof command !== "string")
|
|
56
54
|
return false;
|
|
57
|
-
return
|
|
55
|
+
return LEGACY_OBSERVE_SESSION_COMMAND_RE.test(command);
|
|
58
56
|
}
|
|
59
|
-
// Any installer-owned observe.sh / session.sh hook command, broken or clean.
|
|
60
|
-
// Used by uninstall to drop both freshly-installed forward-slash hooks and any
|
|
61
|
-
// stale legacy entries. Pairs with isBrokenObserveOrSessionCommand above.
|
|
62
57
|
function isOurObserveOrSessionCommand(command) {
|
|
63
58
|
if (typeof command !== "string")
|
|
64
59
|
return false;
|
|
65
|
-
return
|
|
60
|
+
return LEGACY_OBSERVE_SESSION_COMMAND_RE.test(command) ||
|
|
61
|
+
NODE_OBSERVE_SESSION_COMMAND_RE.test(command);
|
|
66
62
|
}
|
|
67
63
|
function getHomeDir() {
|
|
68
64
|
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
69
65
|
}
|
|
70
|
-
function
|
|
66
|
+
function toCommandPath(filePath) {
|
|
71
67
|
return filePath.replace(/\\/g, "/");
|
|
72
68
|
}
|
|
73
69
|
function isInstallMode(value) {
|
|
@@ -84,28 +80,6 @@ function readJsonFile(filePath) {
|
|
|
84
80
|
return null;
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
|
-
// The observation hooks are Bash scripts, and the native Windows settings use
|
|
88
|
-
// forward-slash drive paths such as C:/Users/... . Git Bash resolves that form;
|
|
89
|
-
// WSL's C:\Windows\System32\bash.exe does not. Probe the real packaged hook path
|
|
90
|
-
// instead of accepting any executable that happens to answer `bash --version`.
|
|
91
|
-
function assertBashCanReadHookOnWindows() {
|
|
92
|
-
if (process.platform !== "win32")
|
|
93
|
-
return;
|
|
94
|
-
const hookSource = toBashPath(join(REPO_ROOT, "hooks", "observe.sh"));
|
|
95
|
-
try {
|
|
96
|
-
runFileSync("bash", ["-c", 'test -r "$CONTINUOUS_IMPROVEMENT_HOOK_SOURCE"'], {
|
|
97
|
-
env: { ...process.env, CONTINUOUS_IMPROVEMENT_HOOK_SOURCE: hookSource },
|
|
98
|
-
stdio: "ignore",
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
console.error(` ✗ Install refused: Bash on PATH cannot read the Windows hook path ${hookSource}. ` +
|
|
103
|
-
"Install Git Bash and ensure its bin directory comes before " +
|
|
104
|
-
"C:\\Windows\\System32 on PATH, then reopen your shell and re-run. " +
|
|
105
|
-
"See README > Troubleshooting install.");
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
83
|
// The marketplace `/plugin install` path and this npx installer both write into
|
|
110
84
|
// ~/.claude/. Running both duplicates hooks, commands, and skills. We cannot
|
|
111
85
|
// fully resolve Claude Code's marketplace layout from here, so this is a loud
|
|
@@ -169,39 +143,26 @@ function setupMulahazah() {
|
|
|
169
143
|
const globalDir = join(instinctsDir, "global");
|
|
170
144
|
mkdirSync(globalDir, { recursive: true });
|
|
171
145
|
console.log(` ✓ Instincts dir → ${instinctsDir}/`);
|
|
172
|
-
|
|
173
|
-
const observeDest = join(instinctsDir, "observe.sh");
|
|
174
|
-
if (existsSync(observeSrc)) {
|
|
175
|
-
copyFileSync(observeSrc, observeDest);
|
|
176
|
-
chmodSync(observeDest, 0o755);
|
|
177
|
-
console.log(` ✓ observe.sh → ${observeDest}`);
|
|
178
|
-
}
|
|
179
|
-
// Node observer (Phase 1 of the two-phase hook). The bash shim above
|
|
180
|
-
// exec's this when `node` and the file are both present; otherwise it
|
|
181
|
-
// falls back to the in-bash thin-schema path. Layout under instinctsDir
|
|
182
|
-
// mirrors the repo's bin/ + lib/ structure so the relative import in
|
|
183
|
-
// observe.mjs (`../lib/observe-event.mjs`) resolves correctly.
|
|
146
|
+
// Layout mirrors bin/ + lib/ so observe.mjs's relative import resolves.
|
|
184
147
|
const observerJsSrc = join(REPO_ROOT, "bin", "observe.mjs");
|
|
185
148
|
const observeEventSrc = join(REPO_ROOT, "lib", "observe-event.mjs");
|
|
149
|
+
const observerJsDest = join(instinctsDir, "bin", "observe.mjs");
|
|
186
150
|
if (existsSync(observerJsSrc) && existsSync(observeEventSrc)) {
|
|
187
151
|
const binDir = join(instinctsDir, "bin");
|
|
188
152
|
const libDir = join(instinctsDir, "lib");
|
|
189
153
|
mkdirSync(binDir, { recursive: true });
|
|
190
154
|
mkdirSync(libDir, { recursive: true });
|
|
191
|
-
const observerJsDest = join(binDir, "observe.mjs");
|
|
192
155
|
const observeEventDest = join(libDir, "observe-event.mjs");
|
|
193
156
|
copyFileSync(observerJsSrc, observerJsDest);
|
|
194
157
|
copyFileSync(observeEventSrc, observeEventDest);
|
|
195
158
|
console.log(` ✓ Node observer → ${observerJsDest}`);
|
|
196
159
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
console.log(` ✓ session.sh → ${sessionDest}`);
|
|
204
|
-
}
|
|
160
|
+
const sessionSrc = join(REPO_ROOT, "hooks", "session.mjs");
|
|
161
|
+
const sessionDest = join(instinctsDir, "session.mjs");
|
|
162
|
+
if (existsSync(sessionSrc)) {
|
|
163
|
+
copyFileSync(sessionSrc, sessionDest);
|
|
164
|
+
chmodSync(sessionDest, 0o755);
|
|
165
|
+
console.log(` ✓ Node session hook → ${sessionDest}`);
|
|
205
166
|
}
|
|
206
167
|
const commandsDir = join(home, ".claude", "commands");
|
|
207
168
|
mkdirSync(commandsDir, { recursive: true });
|
|
@@ -213,7 +174,7 @@ function setupMulahazah() {
|
|
|
213
174
|
console.log(` ✓ /${commandFile.replace(".md", "")} command → ${commandDest}`);
|
|
214
175
|
}
|
|
215
176
|
}
|
|
216
|
-
patchClaudeSettings(
|
|
177
|
+
patchClaudeSettings(observerJsDest);
|
|
217
178
|
if (INSTALL_MODE === "expert") {
|
|
218
179
|
setupMcpServer();
|
|
219
180
|
}
|
|
@@ -276,6 +237,10 @@ function patchClaudeSettings(observePath) {
|
|
|
276
237
|
if (!settings.hooks) {
|
|
277
238
|
settings.hooks = {};
|
|
278
239
|
}
|
|
240
|
+
const preserveSessionHooks = INSTALL_MODE === "expert" || SESSION_HOOK_TYPES.some((hookType) => {
|
|
241
|
+
const entries = settings.hooks?.[hookType];
|
|
242
|
+
return Array.isArray(entries) && entries.some((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => isOurObserveOrSessionCommand(hook?.command)));
|
|
243
|
+
});
|
|
279
244
|
// Strip broken legacy observe/session hooks at the hook level, not the entry
|
|
280
245
|
// level. A single entry may carry a foreign command alongside a broken hook;
|
|
281
246
|
// dropping the whole entry to remove the broken hook would also wipe the
|
|
@@ -294,7 +259,7 @@ function patchClaudeSettings(observePath) {
|
|
|
294
259
|
cleanedEntries.push(entry);
|
|
295
260
|
continue;
|
|
296
261
|
}
|
|
297
|
-
const filteredHooks = entryHooks.filter((hook) => !
|
|
262
|
+
const filteredHooks = entryHooks.filter((hook) => !isLegacyObserveOrSessionCommand(hook?.command));
|
|
298
263
|
if (filteredHooks.length === entryHooks.length) {
|
|
299
264
|
cleanedEntries.push(entry);
|
|
300
265
|
continue;
|
|
@@ -313,7 +278,7 @@ function patchClaudeSettings(observePath) {
|
|
|
313
278
|
changed = true;
|
|
314
279
|
}
|
|
315
280
|
}
|
|
316
|
-
const observeCommand = `
|
|
281
|
+
const observeCommand = `node "${toCommandPath(observePath)}"`;
|
|
317
282
|
for (const hookType of HOOK_TYPES) {
|
|
318
283
|
if (!Array.isArray(settings.hooks[hookType])) {
|
|
319
284
|
settings.hooks[hookType] = [];
|
|
@@ -332,9 +297,9 @@ function patchClaudeSettings(observePath) {
|
|
|
332
297
|
changed = true;
|
|
333
298
|
}
|
|
334
299
|
}
|
|
335
|
-
if (
|
|
336
|
-
const sessionPath = join(getHomeDir(), ".claude", "instincts", "session.
|
|
337
|
-
const sessionCommand = `
|
|
300
|
+
if (preserveSessionHooks) {
|
|
301
|
+
const sessionPath = join(getHomeDir(), ".claude", "instincts", "session.mjs");
|
|
302
|
+
const sessionCommand = `node "${toCommandPath(sessionPath)}"`;
|
|
338
303
|
for (const hookType of SESSION_HOOK_TYPES) {
|
|
339
304
|
if (!Array.isArray(settings.hooks[hookType])) {
|
|
340
305
|
settings.hooks[hookType] = [];
|
|
@@ -351,7 +316,7 @@ function patchClaudeSettings(observePath) {
|
|
|
351
316
|
}
|
|
352
317
|
if (changed) {
|
|
353
318
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
354
|
-
const hookTypes =
|
|
319
|
+
const hookTypes = preserveSessionHooks
|
|
355
320
|
? "PreToolUse/PostToolUse/SessionStart/SessionEnd"
|
|
356
321
|
: "PreToolUse/PostToolUse";
|
|
357
322
|
console.log(` ✓ Patched ~/.claude/settings.json with ${hookTypes} hooks`);
|
|
@@ -387,7 +352,7 @@ function uninstallAll() {
|
|
|
387
352
|
console.error(` ✗ ${commandName}: ${getErrorMessage(error)}`);
|
|
388
353
|
}
|
|
389
354
|
}
|
|
390
|
-
for (const hookFile of ["observe.sh", "session.sh"]) {
|
|
355
|
+
for (const hookFile of ["observe.sh", "session.sh", "session.mjs"]) {
|
|
391
356
|
const filePath = join(home, ".claude", "instincts", hookFile);
|
|
392
357
|
if (!existsSync(filePath)) {
|
|
393
358
|
continue;
|
|
@@ -617,10 +582,6 @@ function installNonClaudeTargets(targetIds) {
|
|
|
617
582
|
for (const note of notes)
|
|
618
583
|
console.log(` ℹ ${note}`);
|
|
619
584
|
}
|
|
620
|
-
// A mixed target install can write non-Claude rule files below. Validate the
|
|
621
|
-
// Claude hook runtime first so an incompatible Bash cannot leave a partial install.
|
|
622
|
-
if (requestedTargets.includes("claude"))
|
|
623
|
-
assertBashCanReadHookOnWindows();
|
|
624
585
|
const nonClaudeTargets = requestedTargets.filter((targetId) => targetId !== "claude");
|
|
625
586
|
if (nonClaudeTargets.length > 0) {
|
|
626
587
|
console.log("\ncontinuous-improvement multi-platform install\n");
|