continuous-improvement 3.21.0 → 3.22.1
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 +1 -1
- package/CHANGELOG.md +26 -0
- package/bin/check-reconcile-parity.mjs +168 -0
- package/bin/generate-plugin-manifests.mjs +2 -0
- package/bin/reconcile.mjs +283 -0
- package/commands/reconcile.md +33 -6
- package/lib/git-state.mjs +430 -0
- package/package.json +5 -3
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
- package/plugins/continuous-improvement/bin/reconcile.mjs +283 -0
- package/plugins/continuous-improvement/commands/reconcile.md +33 -6
- package/plugins/continuous-improvement/lib/git-state.mjs +430 -0
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +66 -12
- package/plugins/expert.json +1 -1
- package/skills/reconcile.md +66 -12
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-state: pure classifiers for the `reconcile` ground-truth contract.
|
|
3
|
+
*
|
|
4
|
+
* No I/O. `src/bin/reconcile.mts` does the actual `git` spawning and feeds raw
|
|
5
|
+
* stdout plus exit codes in here; keeping the logic pure lets the unit tests
|
|
6
|
+
* cover every boundary (no configured upstream, detached HEAD, linked worktree,
|
|
7
|
+
* failed remote probe, autocrlf phantom drift) without building a repo per case.
|
|
8
|
+
*
|
|
9
|
+
* This module is also the single source of truth for the ground-truth command
|
|
10
|
+
* set: `GROUND_TRUTH_PROBES` is what `bin/reconcile.mjs` runs and what
|
|
11
|
+
* `bin/check-reconcile-parity.mjs` asserts the reconcile skill and command docs
|
|
12
|
+
* still document. Prose that drifts from this array fails `verify:all`.
|
|
13
|
+
*
|
|
14
|
+
* Every classifier fails closed. "We could not tell" is reported as unknown or
|
|
15
|
+
* unverified, never as clean, even, absent, or landed.
|
|
16
|
+
*/
|
|
17
|
+
/** Branch names treated as protected unless the caller overrides the list. */
|
|
18
|
+
export const DEFAULT_PROTECTED_BRANCHES = [
|
|
19
|
+
"main",
|
|
20
|
+
"master",
|
|
21
|
+
"release",
|
|
22
|
+
"production",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* The ground-truth command set.
|
|
26
|
+
*
|
|
27
|
+
* Every entry is portable: plain `git` argv with no shell, no `ls`, no
|
|
28
|
+
* coreutils, and no literal `.git/` path — inside a linked worktree `.git` is a
|
|
29
|
+
* *file*, so a `.git/`-relative probe silently reports nothing. Ordering is the
|
|
30
|
+
* order the runner executes and the renderer prints.
|
|
31
|
+
*/
|
|
32
|
+
export const GROUND_TRUTH_PROBES = [
|
|
33
|
+
{
|
|
34
|
+
id: "repo-root",
|
|
35
|
+
args: ["rev-parse", "--show-toplevel"],
|
|
36
|
+
purpose: "Confirm we are inside a work tree and learn its root.",
|
|
37
|
+
tolerateFailure: false,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "head-sha",
|
|
41
|
+
args: ["rev-parse", "HEAD"],
|
|
42
|
+
purpose: "Pin the exact commit every later claim is relative to.",
|
|
43
|
+
tolerateFailure: false,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "head-branch",
|
|
47
|
+
args: ["symbolic-ref", "--quiet", "--short", "HEAD"],
|
|
48
|
+
purpose: "Branch name, or a non-zero exit that proves a detached HEAD.",
|
|
49
|
+
tolerateFailure: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "upstream-ref",
|
|
53
|
+
args: ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
|
54
|
+
purpose: "Configured upstream, or a non-zero exit that proves there is none.",
|
|
55
|
+
tolerateFailure: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "upstream-counts",
|
|
59
|
+
args: ["rev-list", "--left-right", "--count", "@{u}...HEAD"],
|
|
60
|
+
purpose: "Behind/ahead counts; only meaningful once an upstream exists.",
|
|
61
|
+
tolerateFailure: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "status-porcelain",
|
|
65
|
+
args: ["status", "--porcelain=v1"],
|
|
66
|
+
purpose: "Reported working-tree changes — inflated by autocrlf on Windows.",
|
|
67
|
+
tolerateFailure: false,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "content-drift",
|
|
71
|
+
args: ["diff", "--name-only", "--ignore-all-space"],
|
|
72
|
+
purpose: "Real content drift — the number to trust when autocrlf is on.",
|
|
73
|
+
tolerateFailure: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "stash-list",
|
|
77
|
+
args: ["stash", "list"],
|
|
78
|
+
purpose: "Stashes an earlier session may have left holding real work.",
|
|
79
|
+
tolerateFailure: false,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "worktree-list",
|
|
83
|
+
args: ["worktree", "list", "--porcelain"],
|
|
84
|
+
purpose: "Sibling worktrees another session may be writing to.",
|
|
85
|
+
tolerateFailure: false,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* In-progress operation markers, addressed via `git rev-parse --git-path` so
|
|
90
|
+
* they resolve correctly inside a linked worktree, where `.git` is a file and
|
|
91
|
+
* the real marker lives under `.git/worktrees/<name>/`.
|
|
92
|
+
*/
|
|
93
|
+
export const IN_PROGRESS_MARKERS = [
|
|
94
|
+
{ id: "merge", gitPath: "MERGE_HEAD", label: "merge in progress" },
|
|
95
|
+
{ id: "rebase-merge", gitPath: "rebase-merge", label: "interactive rebase in progress" },
|
|
96
|
+
{ id: "rebase-apply", gitPath: "rebase-apply", label: "rebase/am in progress" },
|
|
97
|
+
{ id: "cherry-pick", gitPath: "CHERRY_PICK_HEAD", label: "cherry-pick in progress" },
|
|
98
|
+
{ id: "revert", gitPath: "REVERT_HEAD", label: "revert in progress" },
|
|
99
|
+
{ id: "bisect", gitPath: "BISECT_LOG", label: "bisect in progress" },
|
|
100
|
+
];
|
|
101
|
+
const SHA = /^[0-9a-f]{7,64}$/i;
|
|
102
|
+
const SAFE_REF = /^[A-Za-z0-9._/-]+$/;
|
|
103
|
+
/** Split git output on either line ending — git on Windows emits CRLF. */
|
|
104
|
+
function toLines(raw) {
|
|
105
|
+
return raw.split(/\r?\n/);
|
|
106
|
+
}
|
|
107
|
+
function isSha(value) {
|
|
108
|
+
return SHA.test(value);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* True when `name` is safe to interpolate into a path, an argv entry, or a
|
|
112
|
+
* comparison that authorizes a mutation.
|
|
113
|
+
*
|
|
114
|
+
* Fails closed: null, empty, surrounding whitespace, shell metacharacters,
|
|
115
|
+
* `..`, refspec syntax (`@{`), a leading `-`, empty path components,
|
|
116
|
+
* dot-prefixed or dot-suffixed components, components ending `.lock`, and
|
|
117
|
+
* anything over 255 characters all return false rather than being sanitized
|
|
118
|
+
* into something that then looks valid.
|
|
119
|
+
*/
|
|
120
|
+
export function isSafeRefName(name) {
|
|
121
|
+
if (typeof name !== "string")
|
|
122
|
+
return false;
|
|
123
|
+
if (name.length === 0 || name.length > 255)
|
|
124
|
+
return false;
|
|
125
|
+
if (name.trim() !== name)
|
|
126
|
+
return false;
|
|
127
|
+
if (name.startsWith("-"))
|
|
128
|
+
return false;
|
|
129
|
+
if (name.endsWith("/") || name.endsWith(".lock"))
|
|
130
|
+
return false;
|
|
131
|
+
if (name.includes("..") || name.includes("@{"))
|
|
132
|
+
return false;
|
|
133
|
+
const components = name.split("/");
|
|
134
|
+
if (components.some((component) => component.length === 0 ||
|
|
135
|
+
component.startsWith(".") ||
|
|
136
|
+
component.endsWith(".") ||
|
|
137
|
+
component.endsWith(".lock"))) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return SAFE_REF.test(name);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Classify HEAD from `git symbolic-ref --quiet --short HEAD`.
|
|
144
|
+
*
|
|
145
|
+
* A detached HEAD makes that command exit non-zero with empty output. This
|
|
146
|
+
* module deliberately never uses `git branch --show-current`, which returns an
|
|
147
|
+
* empty string with exit 0 and so cannot be told apart from a successful read.
|
|
148
|
+
* Returns `{ kind: "unknown", branch: null }` when output is present but is not
|
|
149
|
+
* a usable ref name, so an unparseable branch never reads as a match against an
|
|
150
|
+
* expected one.
|
|
151
|
+
*/
|
|
152
|
+
export function classifyHead(rawBranch, exitCode = 0) {
|
|
153
|
+
const value = typeof rawBranch === "string" ? rawBranch.trim() : "";
|
|
154
|
+
if (exitCode !== 0 || value.length === 0) {
|
|
155
|
+
return { kind: "detached", branch: null };
|
|
156
|
+
}
|
|
157
|
+
if (!isSafeRefName(value))
|
|
158
|
+
return { kind: "unknown", branch: null };
|
|
159
|
+
return { kind: "branch", branch: value };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Parse `git rev-list --left-right --count @{u}...HEAD` output, which is
|
|
163
|
+
* `"<behind>\t<ahead>"`.
|
|
164
|
+
*
|
|
165
|
+
* Returns null for empty, malformed, negative, non-integer, or unsafe-integer
|
|
166
|
+
* output. Callers must treat null as "unknown" — never as zero.
|
|
167
|
+
*/
|
|
168
|
+
export function parseRevListCounts(raw) {
|
|
169
|
+
if (typeof raw !== "string")
|
|
170
|
+
return null;
|
|
171
|
+
const first = toLines(raw).find((line) => line.trim().length > 0);
|
|
172
|
+
if (first === undefined)
|
|
173
|
+
return null;
|
|
174
|
+
const parts = first.trim().split(/\s+/);
|
|
175
|
+
if (parts.length !== 2)
|
|
176
|
+
return null;
|
|
177
|
+
const behind = Number(parts[0]);
|
|
178
|
+
const ahead = Number(parts[1]);
|
|
179
|
+
if (!Number.isSafeInteger(behind) || behind < 0)
|
|
180
|
+
return null;
|
|
181
|
+
if (!Number.isSafeInteger(ahead) || ahead < 0)
|
|
182
|
+
return null;
|
|
183
|
+
return { behind, ahead };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Map counts to an upstream relation.
|
|
187
|
+
*
|
|
188
|
+
* Fails closed: no upstream returns `"no-upstream"`, and an upstream that does
|
|
189
|
+
* exist but whose counts would not parse returns `"unknown"` — never `"even"`.
|
|
190
|
+
*/
|
|
191
|
+
export function classifyUpstream(counts, hasUpstream) {
|
|
192
|
+
if (!hasUpstream)
|
|
193
|
+
return "no-upstream";
|
|
194
|
+
if (counts === null)
|
|
195
|
+
return "unknown";
|
|
196
|
+
const { behind, ahead } = counts;
|
|
197
|
+
if (behind === 0 && ahead === 0)
|
|
198
|
+
return "even";
|
|
199
|
+
if (behind > 0 && ahead > 0)
|
|
200
|
+
return "diverged";
|
|
201
|
+
return ahead > 0 ? "ahead" : "behind";
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Collect a label for every in-progress operation whose marker is present.
|
|
205
|
+
*
|
|
206
|
+
* `present` is keyed by `InProgressMarker.id`. A marker missing from the map is
|
|
207
|
+
* treated as *unprobed*, not absent, and reported as `"<id>: unprobed"` — a
|
|
208
|
+
* probe that never ran must not masquerade as a clean tree. Returns `[]` only
|
|
209
|
+
* when every known marker was probed and none was present.
|
|
210
|
+
*/
|
|
211
|
+
export function classifyInProgress(present) {
|
|
212
|
+
const found = [];
|
|
213
|
+
for (const marker of IN_PROGRESS_MARKERS) {
|
|
214
|
+
const state = present[marker.id];
|
|
215
|
+
if (state === undefined) {
|
|
216
|
+
found.push(`${marker.id}: unprobed`);
|
|
217
|
+
}
|
|
218
|
+
else if (state) {
|
|
219
|
+
found.push(marker.label);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return found;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* True when `branch` must not be pushed to or committed onto directly.
|
|
226
|
+
*
|
|
227
|
+
* Fails closed: null, an unparseable name, and a detached HEAD all return true,
|
|
228
|
+
* because an unknown branch is not proof of safety. Supports one trailing `/*`
|
|
229
|
+
* wildcard per pattern (`release/*`).
|
|
230
|
+
*/
|
|
231
|
+
export function isProtectedBranch(branch, patterns = DEFAULT_PROTECTED_BRANCHES) {
|
|
232
|
+
if (!isSafeRefName(branch))
|
|
233
|
+
return true;
|
|
234
|
+
const name = branch;
|
|
235
|
+
return patterns.some((pattern) => pattern.endsWith("/*")
|
|
236
|
+
? name.startsWith(pattern.slice(0, -1))
|
|
237
|
+
: name === pattern);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Decide whether a push actually landed, from `git ls-remote` output.
|
|
241
|
+
*
|
|
242
|
+
* Three outcomes, never collapsed into two: a non-zero exit is `"unverified"`
|
|
243
|
+
* (the network or the remote failed, so absence of the ref is unproven and the
|
|
244
|
+
* push may well have landed), an empty successful probe is `"not-landed"` (the
|
|
245
|
+
* ref is genuinely absent), and a present ref is compared by SHA. An
|
|
246
|
+
* unparseable local HEAD or unparseable remote output is `"unverified"`.
|
|
247
|
+
*/
|
|
248
|
+
export function verifyPushLanded(input) {
|
|
249
|
+
const local = (input.localHead ?? "").trim();
|
|
250
|
+
if (!isSha(local)) {
|
|
251
|
+
return { verdict: "unverified", reason: "local HEAD is not a usable sha" };
|
|
252
|
+
}
|
|
253
|
+
if (input.lsRemoteExitCode !== 0) {
|
|
254
|
+
return {
|
|
255
|
+
verdict: "unverified",
|
|
256
|
+
reason: `git ls-remote exited ${input.lsRemoteExitCode}; absence of the ref is unproven`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const raw = (input.lsRemoteStdout ?? "").trim();
|
|
260
|
+
if (raw.length === 0) {
|
|
261
|
+
return { verdict: "not-landed", reason: "remote ref does not exist" };
|
|
262
|
+
}
|
|
263
|
+
const remoteSha = ((toLines(raw)[0] ?? "").trim().split(/\s+/)[0] ?? "");
|
|
264
|
+
if (!isSha(remoteSha)) {
|
|
265
|
+
return { verdict: "unverified", reason: "could not parse a sha from ls-remote output" };
|
|
266
|
+
}
|
|
267
|
+
if (remoteSha.toLowerCase() === local.toLowerCase()) {
|
|
268
|
+
return {
|
|
269
|
+
verdict: "landed",
|
|
270
|
+
reason: `remote tip matches local HEAD ${local.slice(0, 12)}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
verdict: "not-landed",
|
|
275
|
+
reason: `remote tip ${remoteSha.slice(0, 12)} differs from local HEAD ${local.slice(0, 12)}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Reconcile `git status --porcelain` output against real content drift.
|
|
280
|
+
*
|
|
281
|
+
* On an `autocrlf=true` tree status reports line-ending-only modifications that
|
|
282
|
+
* carry no content change, which is why the reconcile skill says to trust
|
|
283
|
+
* `git diff` instead. A `contentDrift` of null means the drift probe never ran;
|
|
284
|
+
* it is reported as null rather than assumed to be zero.
|
|
285
|
+
*/
|
|
286
|
+
export function accountDirty(statusStdout, diffNamesStdout) {
|
|
287
|
+
const count = (raw) => typeof raw === "string"
|
|
288
|
+
? toLines(raw).filter((line) => line.trim().length > 0).length
|
|
289
|
+
: null;
|
|
290
|
+
const reported = count(statusStdout) ?? 0;
|
|
291
|
+
const contentDrift = count(diffNamesStdout);
|
|
292
|
+
return {
|
|
293
|
+
reported,
|
|
294
|
+
contentDrift,
|
|
295
|
+
phantomSuspected: contentDrift !== null && reported > contentDrift,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* True when the repo moved under us since `before` was captured — the check to
|
|
300
|
+
* run immediately before each mutation on a host with concurrent writers.
|
|
301
|
+
*
|
|
302
|
+
* Fails closed: a missing or unparseable sha on either side counts as shifted,
|
|
303
|
+
* because "we could not tell" must never read as "nothing moved".
|
|
304
|
+
*/
|
|
305
|
+
export function baselineShifted(before, after) {
|
|
306
|
+
if (before === null || after === null)
|
|
307
|
+
return true;
|
|
308
|
+
const beforeHead = (before.head ?? "").trim();
|
|
309
|
+
const afterHead = (after.head ?? "").trim();
|
|
310
|
+
if (!isSha(beforeHead) || !isSha(afterHead))
|
|
311
|
+
return true;
|
|
312
|
+
if (beforeHead.toLowerCase() !== afterHead.toLowerCase())
|
|
313
|
+
return true;
|
|
314
|
+
return (before.branch ?? null) !== (after.branch ?? null);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Turn a probed state into the ordered finding list the resolved-state block
|
|
318
|
+
* renders. Every finding carries a severity; `blocker` means do not mutate.
|
|
319
|
+
*/
|
|
320
|
+
export function assessGitState(input) {
|
|
321
|
+
const findings = [];
|
|
322
|
+
const protectedList = input.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
|
|
323
|
+
if (Object.hasOwn(input, "headCommit")) {
|
|
324
|
+
const commit = (input.headCommit ?? "").trim();
|
|
325
|
+
findings.push({
|
|
326
|
+
id: "head-commit",
|
|
327
|
+
label: "HEAD commit",
|
|
328
|
+
detail: isSha(commit)
|
|
329
|
+
? commit.slice(0, 12)
|
|
330
|
+
: "unborn or not a usable commit — create or verify the first commit before mutating",
|
|
331
|
+
severity: isSha(commit) ? "ok" : "blocker",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (input.head.kind === "branch") {
|
|
335
|
+
const onProtected = isProtectedBranch(input.head.branch, protectedList);
|
|
336
|
+
findings.push({
|
|
337
|
+
id: "head",
|
|
338
|
+
label: "HEAD",
|
|
339
|
+
detail: onProtected
|
|
340
|
+
? `on protected branch ${input.head.branch} — branch before mutating`
|
|
341
|
+
: `on ${input.head.branch}`,
|
|
342
|
+
severity: onProtected ? "warn" : "ok",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
else if (input.head.kind === "detached") {
|
|
346
|
+
findings.push({
|
|
347
|
+
id: "head",
|
|
348
|
+
label: "HEAD",
|
|
349
|
+
detail: "detached — no branch to commit onto, push, or name in a PR",
|
|
350
|
+
severity: "blocker",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
findings.push({
|
|
355
|
+
id: "head",
|
|
356
|
+
label: "HEAD",
|
|
357
|
+
detail: "branch name unparseable — treat repo identity as unknown",
|
|
358
|
+
severity: "blocker",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const relation = classifyUpstream(input.counts, input.upstreamRef !== null);
|
|
362
|
+
const upstreamRows = {
|
|
363
|
+
even: { detail: `even with ${input.upstreamRef}`, severity: "ok" },
|
|
364
|
+
ahead: {
|
|
365
|
+
detail: `${input.counts?.ahead ?? "?"} ahead of ${input.upstreamRef}`,
|
|
366
|
+
severity: "ok",
|
|
367
|
+
},
|
|
368
|
+
behind: {
|
|
369
|
+
detail: `${input.counts?.behind ?? "?"} behind ${input.upstreamRef} — pull --ff-only first`,
|
|
370
|
+
severity: "warn",
|
|
371
|
+
},
|
|
372
|
+
diverged: {
|
|
373
|
+
detail: `diverged from ${input.upstreamRef} ` +
|
|
374
|
+
`(${input.counts?.behind ?? "?"} behind / ${input.counts?.ahead ?? "?"} ahead) — ` +
|
|
375
|
+
"rebase or merge deliberately, never blind --force",
|
|
376
|
+
severity: "warn",
|
|
377
|
+
},
|
|
378
|
+
"no-upstream": {
|
|
379
|
+
detail: "no configured upstream — compare against origin/<default> explicitly before classifying",
|
|
380
|
+
severity: "warn",
|
|
381
|
+
},
|
|
382
|
+
unknown: {
|
|
383
|
+
detail: "upstream exists but counts did not parse — relationship unknown, do not assume even",
|
|
384
|
+
severity: "blocker",
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
findings.push({ id: "upstream", label: "upstream", ...upstreamRows[relation] });
|
|
388
|
+
const ops = classifyInProgress(Object.fromEntries(IN_PROGRESS_MARKERS.map((marker) => [marker.id, input.inProgress.includes(marker.id)])));
|
|
389
|
+
findings.push({
|
|
390
|
+
id: "in-progress",
|
|
391
|
+
label: "in-progress op",
|
|
392
|
+
detail: ops.length === 0 ? "none" : `${ops.join(", ")} — another actor may own this tree`,
|
|
393
|
+
severity: ops.length === 0 ? "ok" : "blocker",
|
|
394
|
+
});
|
|
395
|
+
const { reported, contentDrift, phantomSuspected } = input.dirty;
|
|
396
|
+
findings.push({
|
|
397
|
+
id: "dirty",
|
|
398
|
+
label: "working tree",
|
|
399
|
+
detail: contentDrift === null
|
|
400
|
+
? `${reported} path(s) reported by status; content drift unprobed`
|
|
401
|
+
: phantomSuspected
|
|
402
|
+
? `${reported} reported by status but only ${contentDrift} with content drift — ` +
|
|
403
|
+
"stage by explicit filename, never git add -A"
|
|
404
|
+
: `${contentDrift} path(s) with content drift`,
|
|
405
|
+
severity: contentDrift === null ? "warn" : "ok",
|
|
406
|
+
});
|
|
407
|
+
return findings;
|
|
408
|
+
}
|
|
409
|
+
/** True when any finding blocks mutation. */
|
|
410
|
+
export function hasBlockers(findings) {
|
|
411
|
+
return findings.some((finding) => finding.severity === "blocker");
|
|
412
|
+
}
|
|
413
|
+
const SEVERITY_TAG = {
|
|
414
|
+
ok: "ok",
|
|
415
|
+
warn: "WARN",
|
|
416
|
+
blocker: "BLOCKER",
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* Render the resolved-state block an operator reads before mutating. Returns a
|
|
420
|
+
* single "state unknown" line when no probe produced a finding, so an empty
|
|
421
|
+
* result never renders as a clean tree.
|
|
422
|
+
*/
|
|
423
|
+
export function renderResolvedState(findings) {
|
|
424
|
+
if (findings.length === 0) {
|
|
425
|
+
return "reconcile (resolved): no probes ran — state unknown";
|
|
426
|
+
}
|
|
427
|
+
const width = Math.max(...findings.map((finding) => finding.label.length));
|
|
428
|
+
const rows = findings.map((finding) => ` ${finding.label.padEnd(width)} ${SEVERITY_TAG[finding.severity].padEnd(7)} ${finding.detail}`);
|
|
429
|
+
return ["reconcile (resolved):", ...rows].join("\n");
|
|
430
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.1",
|
|
4
4
|
"description": "Claude Code that gets sharper every session: the persistent-memory and runtime-discipline layer built on the 7 Laws of AI Agent Discipline. It 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. 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. Beginner: one /plugin install command. Expert: adds MCP tools and session hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"ci": "bin/unified-cli.mjs",
|
|
36
36
|
"ci-plan-pack": "bin/plan-pack.mjs",
|
|
37
37
|
"ci-audit-actions": "bin/audit-actions.mjs",
|
|
38
|
-
"ci-portfolio-health": "bin/portfolio-health.mjs"
|
|
38
|
+
"ci-portfolio-health": "bin/portfolio-health.mjs",
|
|
39
|
+
"ci-reconcile": "bin/reconcile.mjs"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "tsc -p tsconfig.json && node bin/generate-plugin-manifests.mjs && node -e \"const fs=require('node:fs'); for (const f of fs.readdirSync('bin')) { if (f.endsWith('.mjs')) fs.chmodSync('bin/'+f, 0o755); } for (const f of fs.readdirSync('hooks')) { if (f.endsWith('.mjs')) fs.chmodSync('hooks/'+f, 0o755); } for (const f of fs.readdirSync('lib')) { if (f.endsWith('.mjs')) fs.chmodSync('lib/'+f, 0o755); } for (const f of fs.readdirSync('plugins/continuous-improvement/bin')) { if (f.endsWith('.mjs')) fs.chmodSync('plugins/continuous-improvement/bin/'+f, 0o755); } for (const f of fs.readdirSync('plugins/continuous-improvement/lib')) { if (f.endsWith('.mjs')) fs.chmodSync('plugins/continuous-improvement/lib/'+f, 0o755); } for (const f of fs.readdirSync('plugins/continuous-improvement/hooks')) { if (f.endsWith('.mjs')) fs.chmodSync('plugins/continuous-improvement/hooks/'+f, 0o755); } for (const f of fs.readdirSync('scripts')) { if (f.endsWith('.mjs')) fs.chmodSync('scripts/'+f, 0o755); } for (const f of fs.readdirSync('synthetic-checks')) { if (f.endsWith('.mjs')) fs.chmodSync('synthetic-checks/'+f, 0o755); } \"",
|
|
@@ -60,7 +61,8 @@
|
|
|
60
61
|
"verify:scripts-citation-drift": "node bin/check-scripts-citation-drift.mjs && node bin/check-scripts-citation-drift.mjs plugins/continuous-improvement",
|
|
61
62
|
"verify:third-party-shape": "node bin/check-third-party-shape.mjs",
|
|
62
63
|
"verify:tool-count": "node bin/check-tool-count.mjs",
|
|
63
|
-
"verify:
|
|
64
|
+
"verify:reconcile-parity": "node bin/check-reconcile-parity.mjs",
|
|
65
|
+
"verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:landing-version && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run verify:reconcile-parity && npm run typecheck"
|
|
64
66
|
},
|
|
65
67
|
"files": [
|
|
66
68
|
".claude-plugin/",
|
package/plugins/beginner.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.1",
|
|
4
4
|
"mode": "beginner",
|
|
5
5
|
"description": "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles three grounding skills (gateguard, tdd-workflow, verification-loop) so research, memory, tests, and verification happen by default — every edit starts from facts, not guesses.",
|
|
6
6
|
"tools": [
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "continuous-improvement",
|
|
10
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.
|
|
11
|
+
"version": "3.22.1",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.1",
|
|
4
4
|
"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.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "naimkatiman",
|