continuous-improvement 3.1.0 → 3.8.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 +78 -0
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -21
- package/QUICKSTART.md +101 -81
- package/README.md +207 -359
- package/SKILL.md +87 -9
- package/action.yml +33 -33
- package/bin/analyze.sh +161 -153
- package/bin/backfill.mjs +172 -0
- package/bin/check-docs-substrings.mjs +333 -0
- package/bin/check-everything-mirror.mjs +145 -0
- package/bin/check-routing-targets.mjs +151 -0
- package/bin/check-skill-law-tag.mjs +128 -0
- package/bin/check-skill-mirror.mjs +119 -0
- package/bin/check-skill-tiers.mjs +116 -0
- package/bin/check-third-party-shape.mjs +202 -0
- package/bin/generate-plugin-manifests.mjs +169 -0
- package/bin/harvest-friction.mjs +279 -0
- package/bin/hook-stats.mjs +258 -0
- package/bin/install.mjs +417 -516
- package/bin/lint-transcript.mjs +182 -210
- package/bin/mcp-server.mjs +840 -617
- package/bin/observe.mjs +148 -0
- package/bin/pre-commit-block-strays.sh +49 -0
- package/bin/refresh-third-party.mjs +416 -0
- package/bin/unified-cli.mjs +533 -0
- package/commands/continuous-improvement.md +115 -74
- package/commands/dashboard.md +56 -56
- package/commands/discipline.md +51 -37
- package/commands/harvest.md +76 -0
- package/commands/learn-eval.md +117 -0
- package/commands/planning-with-files.md +66 -0
- package/commands/proceed-with-the-recommendation.md +62 -0
- package/commands/ralph.md +103 -0
- package/commands/release-train.md +81 -0
- package/commands/seven-laws.md +16 -0
- package/commands/superpowers.md +153 -0
- package/commands/swarm.md +101 -0
- package/commands/workspace-surface-audit.md +77 -0
- package/hooks/observe.sh +172 -134
- package/hooks/session.sh +106 -106
- package/hooks/three-section-close.mjs +181 -0
- package/instinct-packs/go.json +58 -58
- package/instinct-packs/meta.json +16 -0
- package/instinct-packs/python.json +58 -58
- package/instinct-packs/react.json +58 -58
- package/lib/cli-anything.mjs +401 -0
- package/lib/compound-engineering.mjs +831 -0
- package/lib/observe-event.mjs +128 -0
- package/lib/plugin-metadata.mjs +432 -0
- package/lib/pm-marketplace.mjs +61 -0
- package/lib/pm-skills.mjs +1274 -0
- package/lib/resolve-home-dir.mjs +43 -0
- package/lib/skill-tiers.mjs +137 -0
- package/lib/unified-plugin.mjs +924 -0
- package/llms.txt +68 -43
- package/package.json +28 -19
- package/plugins/beginner.json +17 -6
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +20 -0
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +26 -0
- package/plugins/continuous-improvement/LICENSE +21 -0
- package/plugins/continuous-improvement/README.md +56 -0
- package/plugins/continuous-improvement/bin/backfill.mjs +172 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +886 -0
- package/plugins/continuous-improvement/bin/observe.mjs +148 -0
- package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -0
- package/plugins/continuous-improvement/commands/dashboard.md +56 -0
- package/plugins/continuous-improvement/commands/discipline.md +51 -0
- package/plugins/continuous-improvement/commands/harvest.md +76 -0
- package/plugins/continuous-improvement/commands/learn-eval.md +117 -0
- package/plugins/continuous-improvement/commands/planning-with-files.md +66 -0
- package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +62 -0
- package/plugins/continuous-improvement/commands/ralph.md +103 -0
- package/plugins/continuous-improvement/commands/release-train.md +81 -0
- package/plugins/continuous-improvement/commands/seven-laws.md +16 -0
- package/plugins/continuous-improvement/commands/superpowers.md +153 -0
- package/plugins/continuous-improvement/commands/swarm.md +101 -0
- package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -0
- package/plugins/continuous-improvement/hooks/hooks.json +60 -0
- package/plugins/continuous-improvement/hooks/observe.sh +172 -0
- package/plugins/continuous-improvement/hooks/session.sh +106 -0
- package/plugins/continuous-improvement/hooks/three-section-close.mjs +181 -0
- package/plugins/continuous-improvement/instinct-packs/go.json +58 -0
- package/plugins/continuous-improvement/instinct-packs/meta.json +16 -0
- package/plugins/continuous-improvement/instinct-packs/python.json +58 -0
- package/plugins/continuous-improvement/instinct-packs/react.json +58 -0
- package/plugins/continuous-improvement/lib/observe-event.mjs +128 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +432 -0
- package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +43 -0
- package/plugins/continuous-improvement/skills/README.md +34 -0
- package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +249 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +131 -0
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +155 -0
- package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +108 -0
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +454 -0
- package/plugins/continuous-improvement/skills/ralph/SKILL.md +221 -0
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +76 -0
- package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +104 -0
- package/plugins/continuous-improvement/skills/superpowers/SKILL.md +212 -0
- package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -0
- package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +136 -0
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +192 -0
- package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +191 -0
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +147 -0
- package/plugins/continuous-improvement/templates/planning-with-files/findings.md +8 -0
- package/plugins/continuous-improvement/templates/planning-with-files/progress.md +7 -0
- package/plugins/continuous-improvement/templates/planning-with-files/task_plan.md +23 -0
- package/plugins/expert.json +26 -5
- package/skills/README.md +79 -0
- package/skills/deploy-receipt.md +131 -0
- package/skills/gateguard.md +155 -0
- package/skills/para-memory-files.md +108 -0
- package/skills/proceed-with-the-recommendation.md +454 -0
- package/skills/ralph.md +221 -0
- package/skills/safety-guard.md +76 -0
- package/skills/strategic-compact.md +104 -0
- package/skills/superpowers.md +212 -0
- package/skills/tdd-workflow.md +411 -0
- package/skills/token-budget-advisor.md +136 -0
- package/skills/verification-loop.md +192 -0
- package/skills/wild-risa-balance.md +191 -0
- package/skills/workspace-surface-audit.md +147 -0
- package/templates/planning-with-files/findings.md +8 -0
- package/templates/planning-with-files/progress.md +7 -0
- package/templates/planning-with-files/task_plan.md +23 -0
- package/templates/verify-ladder.example.json +47 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Third-Party Snapshot Shape Invariant
|
|
4
|
+
*
|
|
5
|
+
* Verifies that every `third-party/<name>/` snapshot directory conforms to the
|
|
6
|
+
* vendor playbook documented in `third-party/README.md`. For each snapshot
|
|
7
|
+
* directory the check asserts:
|
|
8
|
+
*
|
|
9
|
+
* 1. `OUR_NOTES.md` exists and is non-empty
|
|
10
|
+
* 2. `LICENSE` exists
|
|
11
|
+
* 3. `third-party/MANIFEST.md` has at least one `### <heading>` block whose
|
|
12
|
+
* `Local path` row points at this snapshot directory
|
|
13
|
+
* 4. The matching MANIFEST entry has a `Pinned SHA` row containing a
|
|
14
|
+
* 40-char lowercase hex string wrapped in backticks
|
|
15
|
+
*
|
|
16
|
+
* The mapping between snapshot directory and MANIFEST heading is NOT identity
|
|
17
|
+
* (e.g. dir `superpowers` <-> heading `obra/superpowers`). The check looks up
|
|
18
|
+
* by `Local path`, not by name.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node bin/check-third-party-shape.mjs # check the current repo
|
|
22
|
+
* node bin/check-third-party-shape.mjs <repo-root> # check a specific repo root
|
|
23
|
+
*
|
|
24
|
+
* Exit codes:
|
|
25
|
+
* 0 - every snapshot is shape-compliant
|
|
26
|
+
* 1 - at least one drift found
|
|
27
|
+
*/
|
|
28
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { argv, cwd, exit } from "node:process";
|
|
31
|
+
const THIRD_PARTY_DIR = "third-party";
|
|
32
|
+
const MANIFEST_FILENAME = "MANIFEST.md";
|
|
33
|
+
const OUR_NOTES_FILENAME = "OUR_NOTES.md";
|
|
34
|
+
const LICENSE_FILENAME = "LICENSE";
|
|
35
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
36
|
+
export function discoverSnapshots(repoRoot) {
|
|
37
|
+
const dir = join(repoRoot, THIRD_PARTY_DIR);
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(dir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const snapshots = [];
|
|
46
|
+
for (const name of entries) {
|
|
47
|
+
const full = join(dir, name);
|
|
48
|
+
let entryStat;
|
|
49
|
+
try {
|
|
50
|
+
entryStat = statSync(full);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!entryStat.isDirectory())
|
|
56
|
+
continue;
|
|
57
|
+
snapshots.push({
|
|
58
|
+
name,
|
|
59
|
+
hasOurNotes: hasNonEmptyFile(join(full, OUR_NOTES_FILENAME)),
|
|
60
|
+
hasLicense: fileExists(join(full, LICENSE_FILENAME)),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return snapshots;
|
|
64
|
+
}
|
|
65
|
+
function fileExists(path) {
|
|
66
|
+
try {
|
|
67
|
+
return statSync(path).isFile();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function hasNonEmptyFile(path) {
|
|
74
|
+
try {
|
|
75
|
+
const s = statSync(path);
|
|
76
|
+
return s.isFile() && s.size > 0;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function parseManifestEntries(manifestText) {
|
|
83
|
+
const lines = manifestText.split(/\r?\n/);
|
|
84
|
+
const entries = [];
|
|
85
|
+
let current = null;
|
|
86
|
+
const flush = () => {
|
|
87
|
+
if (current && current.localPath !== null)
|
|
88
|
+
entries.push(current);
|
|
89
|
+
current = null;
|
|
90
|
+
};
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const headingMatch = line.match(/^###\s+(.+?)\s*$/);
|
|
93
|
+
if (headingMatch) {
|
|
94
|
+
flush();
|
|
95
|
+
current = { heading: headingMatch[1], pinnedSha: null, localPath: null };
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (line.match(/^##\s+/) || line.match(/^#\s+/)) {
|
|
99
|
+
flush();
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!current)
|
|
103
|
+
continue;
|
|
104
|
+
const shaRow = line.match(/^\|\s*Pinned SHA\s*\|\s*`?([0-9a-fA-F]+)`?\s*\|/);
|
|
105
|
+
if (shaRow) {
|
|
106
|
+
current.pinnedSha = shaRow[1].toLowerCase();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const pathRow = line.match(/^\|\s*Local path\s*\|\s*`?([^`|]+?)`?\s*\|/);
|
|
110
|
+
if (pathRow) {
|
|
111
|
+
current.localPath = pathRow[1].trim();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
flush();
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
function normalizeLocalPath(p) {
|
|
119
|
+
let out = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
120
|
+
if (out.startsWith("./"))
|
|
121
|
+
out = out.slice(2);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
export function checkThirdPartyShape(repoRoot) {
|
|
125
|
+
const snapshots = discoverSnapshots(repoRoot);
|
|
126
|
+
const manifestPath = join(repoRoot, THIRD_PARTY_DIR, MANIFEST_FILENAME);
|
|
127
|
+
let manifestText = "";
|
|
128
|
+
try {
|
|
129
|
+
manifestText = readFileSync(manifestPath, "utf8");
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* no manifest at all - every snapshot will fail no-manifest-entry */
|
|
133
|
+
}
|
|
134
|
+
const entries = parseManifestEntries(manifestText);
|
|
135
|
+
const entryByLocalPath = new Map();
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
if (e.localPath)
|
|
138
|
+
entryByLocalPath.set(normalizeLocalPath(e.localPath), e);
|
|
139
|
+
}
|
|
140
|
+
const drifts = [];
|
|
141
|
+
for (const snap of snapshots) {
|
|
142
|
+
if (!snap.hasOurNotes) {
|
|
143
|
+
drifts.push({
|
|
144
|
+
kind: "missing-our-notes",
|
|
145
|
+
snapshot: snap.name,
|
|
146
|
+
detail: `expected non-empty ${OUR_NOTES_FILENAME} at ${THIRD_PARTY_DIR}/${snap.name}/`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!snap.hasLicense) {
|
|
150
|
+
drifts.push({
|
|
151
|
+
kind: "missing-license",
|
|
152
|
+
snapshot: snap.name,
|
|
153
|
+
detail: `expected ${LICENSE_FILENAME} at ${THIRD_PARTY_DIR}/${snap.name}/`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const expectedPath = `${THIRD_PARTY_DIR}/${snap.name}`;
|
|
157
|
+
const entry = entryByLocalPath.get(expectedPath);
|
|
158
|
+
if (!entry) {
|
|
159
|
+
drifts.push({
|
|
160
|
+
kind: "no-manifest-entry",
|
|
161
|
+
snapshot: snap.name,
|
|
162
|
+
detail: `no MANIFEST.md ### block has Local path \`${expectedPath}/\``,
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!entry.pinnedSha || !SHA_RE.test(entry.pinnedSha)) {
|
|
167
|
+
drifts.push({
|
|
168
|
+
kind: "invalid-sha",
|
|
169
|
+
snapshot: snap.name,
|
|
170
|
+
detail: `MANIFEST entry "${entry.heading}" has Pinned SHA "${entry.pinnedSha ?? "(none)"}" - expected 40-char lowercase hex`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
drifts,
|
|
176
|
+
snapshotCount: snapshots.length,
|
|
177
|
+
manifestEntryCount: entries.length,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function main() {
|
|
181
|
+
const repoRoot = argv[2] ?? cwd();
|
|
182
|
+
const { drifts, snapshotCount, manifestEntryCount } = checkThirdPartyShape(repoRoot);
|
|
183
|
+
if (drifts.length === 0) {
|
|
184
|
+
console.log(`OK third-party-shape: all ${snapshotCount} snapshot(s) compliant ` +
|
|
185
|
+
`(${manifestEntryCount} MANIFEST entr${manifestEntryCount === 1 ? "y" : "ies"} with Local path).`);
|
|
186
|
+
exit(0);
|
|
187
|
+
}
|
|
188
|
+
console.error(`FAIL third-party-shape: ${drifts.length} drift(s) across ${snapshotCount} snapshot(s).\n`);
|
|
189
|
+
for (const d of drifts) {
|
|
190
|
+
console.error(` - [${d.kind}] ${d.snapshot}`);
|
|
191
|
+
if (d.detail)
|
|
192
|
+
console.error(` ${d.detail}`);
|
|
193
|
+
}
|
|
194
|
+
console.error(`\nFix: see third-party/README.md "Layout" + third-party/MANIFEST.md ` +
|
|
195
|
+
`existing entries for the playbook shape (OUR_NOTES.md, LICENSE, ` +
|
|
196
|
+
`### <heading> block with Pinned SHA + Local path rows).`);
|
|
197
|
+
exit(1);
|
|
198
|
+
}
|
|
199
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
200
|
+
if (invokedDirectly || argv[1]?.endsWith("check-third-party-shape.mjs")) {
|
|
201
|
+
main();
|
|
202
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFile, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { PACKAGE_NAME, PLUGIN_MODES, getPluginHooksConfig, getClaudePluginManifest, getClaudePluginMarketplaceManifest, getClaudeRepoMarketplaceManifest, getPluginManifest, } from "../lib/plugin-metadata.mjs";
|
|
6
|
+
import { normalizeTier, parseSkillFrontmatter, renderBundledSkillsReadme, } from "../lib/skill-tiers.mjs";
|
|
7
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const PLUGINS_DIR = join(REPO_ROOT, "plugins");
|
|
9
|
+
const PLUGIN_BUNDLE_DIR = join(PLUGINS_DIR, PACKAGE_NAME);
|
|
10
|
+
const CLAUDE_REPO_MARKETPLACE_PATH = join(REPO_ROOT, ".claude-plugin", "marketplace.json");
|
|
11
|
+
async function writeJson(filePath, data) {
|
|
12
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
13
|
+
await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
async function copyFileTo(sourcePath, targetPath) {
|
|
16
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
17
|
+
await copyFile(sourcePath, targetPath);
|
|
18
|
+
}
|
|
19
|
+
async function copyDirectory(sourceDir, targetDir) {
|
|
20
|
+
await mkdir(targetDir, { recursive: true });
|
|
21
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const sourcePath = join(sourceDir, entry.name);
|
|
24
|
+
const targetPath = join(targetDir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
await copyDirectory(sourcePath, targetPath);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (entry.isFile()) {
|
|
30
|
+
await copyFile(sourcePath, targetPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function writeBundledSkills() {
|
|
35
|
+
const sourceSkillsDir = join(REPO_ROOT, "skills");
|
|
36
|
+
const targetSkillsDir = join(PLUGIN_BUNDLE_DIR, "skills");
|
|
37
|
+
const companionSkills = (await readdir(sourceSkillsDir))
|
|
38
|
+
.filter((file) => /^[a-z][a-z0-9-]*\.md$/.test(file))
|
|
39
|
+
.sort();
|
|
40
|
+
await mkdir(targetSkillsDir, { recursive: true });
|
|
41
|
+
await copyFileTo(join(REPO_ROOT, "SKILL.md"), join(targetSkillsDir, PACKAGE_NAME, "SKILL.md"));
|
|
42
|
+
// Build the SkillMeta list as we copy each source skill into the bundle.
|
|
43
|
+
// The core continuous-improvement skill is treated as tier "core" — it's
|
|
44
|
+
// the 7 Laws spec itself, not a companion — so it leads the bundled README
|
|
45
|
+
// in its own section, separate from the featured companion.
|
|
46
|
+
const coreSkillContent = await readFile(join(REPO_ROOT, "SKILL.md"), "utf8");
|
|
47
|
+
const coreFrontmatter = parseSkillFrontmatter(coreSkillContent);
|
|
48
|
+
const skillsForReadme = [
|
|
49
|
+
{
|
|
50
|
+
name: PACKAGE_NAME,
|
|
51
|
+
tier: "core",
|
|
52
|
+
description: coreFrontmatter.description,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
for (const file of companionSkills) {
|
|
56
|
+
const skillName = basename(file, ".md");
|
|
57
|
+
const skillDir = join(targetSkillsDir, skillName);
|
|
58
|
+
await mkdir(skillDir, { recursive: true });
|
|
59
|
+
await copyFile(join(sourceSkillsDir, file), join(skillDir, "SKILL.md"));
|
|
60
|
+
const sourcePath = join(sourceSkillsDir, file);
|
|
61
|
+
const content = await readFile(sourcePath, "utf8");
|
|
62
|
+
const front = parseSkillFrontmatter(content);
|
|
63
|
+
skillsForReadme.push({
|
|
64
|
+
name: front.name ?? skillName,
|
|
65
|
+
tier: normalizeTier(front.tier),
|
|
66
|
+
description: front.description,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
await writeFile(join(targetSkillsDir, "README.md"), renderBundledSkillsReadme(skillsForReadme));
|
|
70
|
+
}
|
|
71
|
+
async function writePluginBundleReadme() {
|
|
72
|
+
const readme = [
|
|
73
|
+
"# Continuous Improvement Plugin Bundle (Claude Code)",
|
|
74
|
+
"",
|
|
75
|
+
"This directory is generated by `npm run build`.",
|
|
76
|
+
"Do not edit files here manually; edit the source files in the repo root and rebuild.",
|
|
77
|
+
"",
|
|
78
|
+
"Included surfaces:",
|
|
79
|
+
"- `.claude-plugin/plugin.json`",
|
|
80
|
+
"- `.claude-plugin/marketplace.json`",
|
|
81
|
+
"- `skills/`",
|
|
82
|
+
"- `commands/`",
|
|
83
|
+
"- `hooks/`",
|
|
84
|
+
"- `bin/mcp-server.mjs`",
|
|
85
|
+
"- `bin/observe.mjs`",
|
|
86
|
+
"- `bin/backfill.mjs`",
|
|
87
|
+
"- `lib/plugin-metadata.mjs`",
|
|
88
|
+
"- `lib/observe-event.mjs`",
|
|
89
|
+
"- `instinct-packs/`",
|
|
90
|
+
"- `templates/planning-with-files/`",
|
|
91
|
+
"",
|
|
92
|
+
"## Required vs Optional companions",
|
|
93
|
+
"",
|
|
94
|
+
"The `proceed-with-the-recommendation` skill (bundled here) routes each",
|
|
95
|
+
"recommendation it walks to a specialist skill. Some specialists ship in",
|
|
96
|
+
"this bundle; the rest live in external plugins. Every external routing",
|
|
97
|
+
"target has a concretely-worded inline fallback in the orchestrator skill,",
|
|
98
|
+
"so the plugin works on a clean install with no other plugins present —",
|
|
99
|
+
"the trade-off is fallback quality vs dedicated-skill quality.",
|
|
100
|
+
"",
|
|
101
|
+
"**Bundled with this plugin (no extra install needed):**",
|
|
102
|
+
"",
|
|
103
|
+
"- `continuous-improvement` — core 7 Laws skill",
|
|
104
|
+
"- `proceed-with-the-recommendation` — the orchestrator itself",
|
|
105
|
+
"- `ralph` — autonomous PRD-scale loop",
|
|
106
|
+
"- `tdd-workflow` — RED/GREEN/REFACTOR + 80% coverage gate",
|
|
107
|
+
"- `workspace-surface-audit` — environment + capability audit",
|
|
108
|
+
"- Tier-1/Tier-2 enforcement skills (`gateguard`, `verification-loop`,",
|
|
109
|
+
" `para-memory-files`, `safety-guard`, `token-budget-advisor`,",
|
|
110
|
+
" `strategic-compact`, `wild-risa-balance`)",
|
|
111
|
+
"",
|
|
112
|
+
"**Optional companions the orchestrator routes to (install separately if",
|
|
113
|
+
"you want the dedicated skill instead of the inline fallback):**",
|
|
114
|
+
"",
|
|
115
|
+
"| Source plugin | Routing targets | Notes |",
|
|
116
|
+
"|---|---|---|",
|
|
117
|
+
"| `obra/superpowers` | `superpowers:brainstorming`, `:writing-plans`, `:systematic-debugging`, `:test-driven-development`, `:requesting-code-review`, `:verification-before-completion`, `:dispatching-parallel-agents`, `:finishing-a-development-branch`, `simplify`, `schedule`, `loop`, `update-config` | Largest external surface; covers planning, debugging, parallel dispatch, branch-finish |",
|
|
118
|
+
"| Claude Code host (Anthropic built-in) | `security-review` | Ships with the host CLI itself, not a separate plugin install |",
|
|
119
|
+
"| `code-review` plugin | `code-review` | Diff walk + severity tagging |",
|
|
120
|
+
"| `documentation-lookup` agent / `context7` MCP | `documentation-lookup` | Library docs lookup with citations |",
|
|
121
|
+
"| `frontend-design` plugin | `frontend-design:frontend-design` | Distinctive UI generation |",
|
|
122
|
+
"| `commit-commands` plugin | `commit-commands:commit`, `:commit-push-pr` | Conventional-commit + push-PR helpers |",
|
|
123
|
+
"",
|
|
124
|
+
"Full per-target audit (license status, bundled flag, inline fallback,",
|
|
125
|
+
"risk-if-absent) lives in `docs/audits/2026-05-06-routing-target-gap.md`.",
|
|
126
|
+
"Per-target inline fallbacks are spelled out in the orchestrator skill at",
|
|
127
|
+
"`skills/proceed-with-the-recommendation/SKILL.md` § \"Routing Table",
|
|
128
|
+
"(with Inline Fallbacks)\".",
|
|
129
|
+
"",
|
|
130
|
+
].join("\n");
|
|
131
|
+
await writeFile(join(PLUGIN_BUNDLE_DIR, "README.md"), readme);
|
|
132
|
+
}
|
|
133
|
+
async function writePluginBundle() {
|
|
134
|
+
await rm(PLUGIN_BUNDLE_DIR, { recursive: true, force: true });
|
|
135
|
+
await mkdir(PLUGIN_BUNDLE_DIR, { recursive: true });
|
|
136
|
+
await Promise.all([
|
|
137
|
+
writeJson(join(PLUGIN_BUNDLE_DIR, ".claude-plugin", "plugin.json"), getClaudePluginManifest()),
|
|
138
|
+
writeJson(join(PLUGIN_BUNDLE_DIR, ".claude-plugin", "marketplace.json"), getClaudePluginMarketplaceManifest()),
|
|
139
|
+
copyDirectory(join(REPO_ROOT, "commands"), join(PLUGIN_BUNDLE_DIR, "commands")),
|
|
140
|
+
copyDirectory(join(REPO_ROOT, "hooks"), join(PLUGIN_BUNDLE_DIR, "hooks")),
|
|
141
|
+
copyDirectory(join(REPO_ROOT, "instinct-packs"), join(PLUGIN_BUNDLE_DIR, "instinct-packs")),
|
|
142
|
+
copyDirectory(join(REPO_ROOT, "templates", "planning-with-files"), join(PLUGIN_BUNDLE_DIR, "templates", "planning-with-files")),
|
|
143
|
+
copyFileTo(join(REPO_ROOT, "bin", "mcp-server.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "mcp-server.mjs")),
|
|
144
|
+
copyFileTo(join(REPO_ROOT, "bin", "observe.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "observe.mjs")),
|
|
145
|
+
copyFileTo(join(REPO_ROOT, "bin", "backfill.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "backfill.mjs")),
|
|
146
|
+
copyFileTo(join(REPO_ROOT, "lib", "plugin-metadata.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "plugin-metadata.mjs")),
|
|
147
|
+
copyFileTo(join(REPO_ROOT, "lib", "resolve-home-dir.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "resolve-home-dir.mjs")),
|
|
148
|
+
copyFileTo(join(REPO_ROOT, "lib", "observe-event.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "observe-event.mjs")),
|
|
149
|
+
copyFileTo(join(REPO_ROOT, "LICENSE"), join(PLUGIN_BUNDLE_DIR, "LICENSE")),
|
|
150
|
+
writePluginBundleReadme(),
|
|
151
|
+
]);
|
|
152
|
+
await writeJson(join(PLUGIN_BUNDLE_DIR, "hooks", "hooks.json"), getPluginHooksConfig());
|
|
153
|
+
await writeBundledSkills();
|
|
154
|
+
// PM plugins are no longer surfaced in the repo-level marketplace as the project
|
|
155
|
+
// refocuses on the 7 Laws of AI Agent Discipline. Source plugin directories under
|
|
156
|
+
// plugins/pm-* remain on disk pending follow-up removal. The discoverPm... helper
|
|
157
|
+
// is preserved so downstream consumers (and the function-level test) still work.
|
|
158
|
+
const pmEntries = [];
|
|
159
|
+
await writeJson(CLAUDE_REPO_MARKETPLACE_PATH, getClaudeRepoMarketplaceManifest(pmEntries));
|
|
160
|
+
}
|
|
161
|
+
await mkdir(PLUGINS_DIR, { recursive: true });
|
|
162
|
+
for (const mode of PLUGIN_MODES) {
|
|
163
|
+
const outputPath = join(PLUGINS_DIR, `${mode}.json`);
|
|
164
|
+
await writeFile(outputPath, `${JSON.stringify(getPluginManifest(mode), null, 2)}\n`);
|
|
165
|
+
console.log(`Generated ${outputPath}`);
|
|
166
|
+
}
|
|
167
|
+
await writePluginBundle();
|
|
168
|
+
console.log(`Generated ${join(PLUGIN_BUNDLE_DIR, ".claude-plugin", "plugin.json")}`);
|
|
169
|
+
console.log(`Generated ${CLAUDE_REPO_MARKETPLACE_PATH}`);
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Friction Harvest — turn observation rows into typed instincts.
|
|
4
|
+
*
|
|
5
|
+
* PR D of the second-release train (item 8 from the 28-day usage report).
|
|
6
|
+
*
|
|
7
|
+
* Reads `~/.claude/instincts/<project-hash>/observations.jsonl` produced by
|
|
8
|
+
* the Mulahazah hook (see `hooks/observe.sh` and `src/lib/observe-event.mts`),
|
|
9
|
+
* classifies failure rows into typed instincts, aggregates by deduplication
|
|
10
|
+
* key, scores confidence with a recency-weighted decay, and appends new
|
|
11
|
+
* instincts to `<project-hash>/instincts.jsonl`. Idempotent: re-running on
|
|
12
|
+
* the same observations does not duplicate previously-written instincts.
|
|
13
|
+
*
|
|
14
|
+
* Schema awareness:
|
|
15
|
+
* - Rich-schema rows (input_summary + output_summary present) are the
|
|
16
|
+
* contract surface. The classifier reads output_summary to detect
|
|
17
|
+
* command-not-found, permission-denied, file-not-read, etc.
|
|
18
|
+
* - Thin-schema rows (no input/output fields — emitted by the bash
|
|
19
|
+
* fallback when jq is missing AND the Node observer is not on PATH)
|
|
20
|
+
* return null from classifyObservation. The CLI reports the gap
|
|
21
|
+
* explicitly rather than misclassifying.
|
|
22
|
+
*
|
|
23
|
+
* Slash-command wrapper (commands/harvest.md), Law-7 SKILL.md prose, and
|
|
24
|
+
* a scheduled trigger are explicitly out of scope for this PR. The trim
|
|
25
|
+
* is documented in docs/plans/2026-05-07-second-release-train.md.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node bin/harvest-friction.mjs # current project
|
|
29
|
+
* node bin/harvest-friction.mjs <project-hash> # specific project
|
|
30
|
+
* node bin/harvest-friction.mjs --list # show classifications without writing
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
34
|
+
import { homedir } from "node:os";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { argv, cwd, exit } from "node:process";
|
|
37
|
+
const SUMMARY_TRUNCATE = 120;
|
|
38
|
+
const DEDUP_KEY_INPUT_TRUNCATE = 120;
|
|
39
|
+
/**
|
|
40
|
+
* Stable deduplication key. Truncates summary at 120 chars before hashing so
|
|
41
|
+
* minor output variance (timestamps, retry counts, paths) past the boundary
|
|
42
|
+
* does not split the same friction event into multiple instincts.
|
|
43
|
+
*/
|
|
44
|
+
export function computeDedupKey(c) {
|
|
45
|
+
const truncated = c.summary.length > DEDUP_KEY_INPUT_TRUNCATE
|
|
46
|
+
? c.summary.slice(0, DEDUP_KEY_INPUT_TRUNCATE)
|
|
47
|
+
: c.summary;
|
|
48
|
+
return createHash("sha1").update(`${c.type}|${c.tool}|${truncated}`).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Classify a single observation row. Returns null when:
|
|
52
|
+
* - The row is not a tool_complete event (no failure signal yet)
|
|
53
|
+
* - The row lacks output_summary (thin-schema row — gap, not classifiable)
|
|
54
|
+
* - The output does not match any known friction pattern (success row)
|
|
55
|
+
*
|
|
56
|
+
* Heuristics are deliberately specific to keep false-positive rate low.
|
|
57
|
+
* Each branch matches a documented failure mode the operator hit on this
|
|
58
|
+
* host in the last 28 days.
|
|
59
|
+
*/
|
|
60
|
+
export function classifyObservation(row) {
|
|
61
|
+
if (row.event !== "tool_complete")
|
|
62
|
+
return null;
|
|
63
|
+
const out = row.output_summary;
|
|
64
|
+
if (out === undefined || out === "")
|
|
65
|
+
return null;
|
|
66
|
+
const tool = row.tool;
|
|
67
|
+
const summary = out.length > SUMMARY_TRUNCATE ? out.slice(0, SUMMARY_TRUNCATE) : out;
|
|
68
|
+
// env_issue — missing tooling, wrong shell flavor, missing env var.
|
|
69
|
+
// Match before permission_block because "command not found" is a stronger signal
|
|
70
|
+
// than the generic "Permission denied" tail of the output.
|
|
71
|
+
if (/command not found/i.test(out) ||
|
|
72
|
+
/not recognized as (?:an internal|the name)/i.test(out) ||
|
|
73
|
+
/is not recognized as a cmdlet/i.test(out)) {
|
|
74
|
+
return build("env_issue", tool, summary, row.ts);
|
|
75
|
+
}
|
|
76
|
+
// permission_block — harness, sandbox, or OS denied the action.
|
|
77
|
+
if (/permission denied/i.test(out) ||
|
|
78
|
+
/harness[- ]blocked/i.test(out) ||
|
|
79
|
+
/sandbox/i.test(out) ||
|
|
80
|
+
/operation not permitted/i.test(out)) {
|
|
81
|
+
return build("permission_block", tool, summary, row.ts);
|
|
82
|
+
}
|
|
83
|
+
// wrong_approach — agent acted on stale state. The "file changed since last
|
|
84
|
+
// read" signal is the canonical case (parallel actor or skipped Read).
|
|
85
|
+
if (/file changed since last read/i.test(out) ||
|
|
86
|
+
/modified by (?:another session|the user)/i.test(out)) {
|
|
87
|
+
return build("wrong_approach", tool, summary, row.ts);
|
|
88
|
+
}
|
|
89
|
+
// buggy_code — agent's own input was malformed for the tool contract.
|
|
90
|
+
// Includes Edit-without-Read, ambiguous old_string, file-too-large, etc.
|
|
91
|
+
if (/file has not been read/i.test(out) ||
|
|
92
|
+
/old_string is not unique/i.test(out) ||
|
|
93
|
+
/string_to_replace not found/i.test(out) ||
|
|
94
|
+
/file too large/i.test(out)) {
|
|
95
|
+
return build("buggy_code", tool, summary, row.ts);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function build(type, tool, summary, evidence_ts) {
|
|
100
|
+
return {
|
|
101
|
+
type,
|
|
102
|
+
tool,
|
|
103
|
+
summary,
|
|
104
|
+
evidence_ts,
|
|
105
|
+
dedup_key: computeDedupKey({ type, tool, summary }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Aggregate classifications by dedup_key into Instincts with frequency and
|
|
110
|
+
* recency-weighted confidence.
|
|
111
|
+
*
|
|
112
|
+
* Confidence model (deliberately simple for the first cut; tune in a follow-up
|
|
113
|
+
* once the operator has confirmed the classifier output is plausible):
|
|
114
|
+
* confidence = min(1.0, log10(occurrence_count + 1) * recency_factor)
|
|
115
|
+
* recency_factor = 0.5 + 0.5 * exp(-days_since_last_seen / 14)
|
|
116
|
+
*
|
|
117
|
+
* A single occurrence today: confidence ≈ log10(2) * 1.0 ≈ 0.30.
|
|
118
|
+
* Ten occurrences today: confidence ≈ log10(11) * 1.0 ≈ 1.04 → clamp 1.0.
|
|
119
|
+
* One occurrence 30 days ago: confidence ≈ log10(2) * (0.5 + 0.5 * 0.118) ≈ 0.17.
|
|
120
|
+
*/
|
|
121
|
+
export function aggregateInstincts(classified) {
|
|
122
|
+
if (classified.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const groups = new Map();
|
|
125
|
+
for (const c of classified) {
|
|
126
|
+
const list = groups.get(c.dedup_key) ?? [];
|
|
127
|
+
list.push(c);
|
|
128
|
+
groups.set(c.dedup_key, list);
|
|
129
|
+
}
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const instincts = [];
|
|
132
|
+
for (const [, group] of groups) {
|
|
133
|
+
const sorted = [...group].sort((a, b) => a.evidence_ts.localeCompare(b.evidence_ts));
|
|
134
|
+
const first = sorted[0];
|
|
135
|
+
const last = sorted[sorted.length - 1];
|
|
136
|
+
const occurrence_count = group.length;
|
|
137
|
+
const lastSeenMs = Date.parse(last.evidence_ts);
|
|
138
|
+
const daysSince = isFinite(lastSeenMs) ? (now - lastSeenMs) / 86400000 : 0;
|
|
139
|
+
const recency_factor = 0.5 + 0.5 * Math.exp(-Math.max(0, daysSince) / 14);
|
|
140
|
+
const raw = Math.log10(occurrence_count + 1) * recency_factor;
|
|
141
|
+
const confidence = Math.max(0, Math.min(1, raw));
|
|
142
|
+
instincts.push({
|
|
143
|
+
type: first.type,
|
|
144
|
+
tool: first.tool,
|
|
145
|
+
summary: first.summary,
|
|
146
|
+
evidence_ts: last.evidence_ts,
|
|
147
|
+
dedup_key: first.dedup_key,
|
|
148
|
+
confidence,
|
|
149
|
+
occurrence_count,
|
|
150
|
+
first_seen: first.evidence_ts,
|
|
151
|
+
last_seen: last.evidence_ts,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return instincts.sort((a, b) => b.confidence - a.confidence);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read existing instincts.jsonl and return the set of dedup keys already
|
|
158
|
+
* recorded. Missing file is treated as empty (idempotent first run).
|
|
159
|
+
* Malformed lines are skipped silently — a partially-written instincts.jsonl
|
|
160
|
+
* should not block the next harvest.
|
|
161
|
+
*/
|
|
162
|
+
export function loadExistingDedupKeys(instinctsJsonlPath) {
|
|
163
|
+
if (!existsSync(instinctsJsonlPath))
|
|
164
|
+
return new Set();
|
|
165
|
+
const keys = new Set();
|
|
166
|
+
const content = readFileSync(instinctsJsonlPath, "utf8");
|
|
167
|
+
for (const line of content.split("\n")) {
|
|
168
|
+
if (line.trim() === "")
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const obj = JSON.parse(line);
|
|
172
|
+
if (typeof obj.dedup_key === "string")
|
|
173
|
+
keys.add(obj.dedup_key);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Skip malformed line.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return keys;
|
|
180
|
+
}
|
|
181
|
+
export function harvest(observationsPath, instinctsPath) {
|
|
182
|
+
if (!existsSync(observationsPath)) {
|
|
183
|
+
return { totalRows: 0, toolCompleteRows: 0, classifiedRows: 0, thinSchemaRows: 0, newInstincts: [], skippedExisting: 0 };
|
|
184
|
+
}
|
|
185
|
+
const content = readFileSync(observationsPath, "utf8");
|
|
186
|
+
const lines = content.split("\n").filter((l) => l.trim() !== "");
|
|
187
|
+
const classified = [];
|
|
188
|
+
let toolCompleteRows = 0;
|
|
189
|
+
let thinSchemaRows = 0;
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
let row;
|
|
192
|
+
try {
|
|
193
|
+
row = JSON.parse(line);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (row.event === "tool_complete") {
|
|
199
|
+
toolCompleteRows++;
|
|
200
|
+
if (row.output_summary === undefined || row.output_summary === "") {
|
|
201
|
+
thinSchemaRows++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const c = classifyObservation(row);
|
|
206
|
+
if (c)
|
|
207
|
+
classified.push(c);
|
|
208
|
+
}
|
|
209
|
+
const allInstincts = aggregateInstincts(classified);
|
|
210
|
+
const existing = loadExistingDedupKeys(instinctsPath);
|
|
211
|
+
const newInstincts = allInstincts.filter((i) => !existing.has(i.dedup_key));
|
|
212
|
+
return {
|
|
213
|
+
totalRows: lines.length,
|
|
214
|
+
toolCompleteRows,
|
|
215
|
+
classifiedRows: classified.length,
|
|
216
|
+
thinSchemaRows,
|
|
217
|
+
newInstincts,
|
|
218
|
+
skippedExisting: allInstincts.length - newInstincts.length,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function defaultProjectHash() {
|
|
222
|
+
// Mirror the bash hook's hashing rule: sha256(project_root) → first 12 hex chars.
|
|
223
|
+
// Keeps the CLI compatible with whatever directory the bash hook was already
|
|
224
|
+
// populating, without re-implementing project-root detection here.
|
|
225
|
+
const root = cwd();
|
|
226
|
+
return createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
227
|
+
}
|
|
228
|
+
function main() {
|
|
229
|
+
const args = argv.slice(2);
|
|
230
|
+
const listOnly = args.includes("--list");
|
|
231
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
232
|
+
const projectHash = positional[0] ?? defaultProjectHash();
|
|
233
|
+
const projectDir = join(homedir(), ".claude", "instincts", projectHash);
|
|
234
|
+
const observationsPath = join(projectDir, "observations.jsonl");
|
|
235
|
+
const instinctsPath = join(projectDir, "instincts.jsonl");
|
|
236
|
+
const result = harvest(observationsPath, instinctsPath);
|
|
237
|
+
console.log(`harvest-friction project=${projectHash}`);
|
|
238
|
+
console.log(` observations rows: ${result.totalRows}`);
|
|
239
|
+
console.log(` tool_complete rows: ${result.toolCompleteRows}`);
|
|
240
|
+
console.log(` classified failures: ${result.classifiedRows}`);
|
|
241
|
+
console.log(` thin-schema rows: ${result.thinSchemaRows}`);
|
|
242
|
+
console.log(` new instincts: ${result.newInstincts.length}`);
|
|
243
|
+
console.log(` skipped (existing): ${result.skippedExisting}`);
|
|
244
|
+
if (result.totalRows > 0 && result.toolCompleteRows === 0) {
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log("WARNING: observations contain only tool_start events.");
|
|
247
|
+
console.log(" The bash fallback in hooks/observe.sh emits tool_start when jq is missing AND");
|
|
248
|
+
console.log(" the Node observer is not on PATH — never tool_complete with output_summary,");
|
|
249
|
+
console.log(" which is what the classifier needs.");
|
|
250
|
+
console.log(" Install jq (winget install jqlang.jq | brew install jq | apt install jq)");
|
|
251
|
+
console.log(" OR ensure hooks/bin/observe.mjs is reachable from the hook script.");
|
|
252
|
+
}
|
|
253
|
+
else if (result.thinSchemaRows > 0 && result.classifiedRows === 0) {
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log("WARNING: tool_complete rows found but lack output_summary fields.");
|
|
256
|
+
console.log(" The classifier needs the rich-schema output_summary to detect failures.");
|
|
257
|
+
console.log(" Confirm the Node observer (src/bin/observe.mts) is the active hook.");
|
|
258
|
+
}
|
|
259
|
+
if (result.newInstincts.length === 0) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
console.log("");
|
|
263
|
+
console.log("New instincts:");
|
|
264
|
+
for (const i of result.newInstincts) {
|
|
265
|
+
const conf = i.confidence.toFixed(2);
|
|
266
|
+
console.log(` [${conf}] ${i.type} on ${i.tool} (×${i.occurrence_count}): ${i.summary}`);
|
|
267
|
+
}
|
|
268
|
+
if (listOnly)
|
|
269
|
+
return;
|
|
270
|
+
for (const i of result.newInstincts) {
|
|
271
|
+
appendFileSync(instinctsPath, JSON.stringify(i) + "\n");
|
|
272
|
+
}
|
|
273
|
+
console.log(`\nAppended ${result.newInstincts.length} instinct(s) to ${instinctsPath}`);
|
|
274
|
+
}
|
|
275
|
+
const invokedDirectly = argv[1]?.endsWith("harvest-friction.mjs");
|
|
276
|
+
if (invokedDirectly) {
|
|
277
|
+
main();
|
|
278
|
+
exit(0);
|
|
279
|
+
}
|