continuous-improvement 3.12.3 → 3.16.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 +45 -1
- package/LICENSE +21 -21
- package/QUICKSTART.md +1 -1
- package/README.md +29 -8
- package/bin/analyze.sh +161 -161
- package/bin/backfill.mjs +19 -19
- package/bin/check-doc-runtime-claims.mjs +0 -0
- package/bin/check-docs-substrings.mjs +0 -0
- package/bin/check-everything-mirror.mjs +0 -0
- package/bin/check-routing-targets.mjs +0 -0
- package/bin/check-scripts-citation-drift.mjs +0 -0
- package/bin/check-skill-count-prose.mjs +168 -0
- package/bin/check-skill-count.mjs +32 -1
- package/bin/check-skill-law-tag.mjs +0 -0
- package/bin/check-skill-mirror.mjs +0 -0
- package/bin/check-skill-tiers.mjs +0 -0
- package/bin/check-test-imports-only.mjs +1 -1
- package/bin/check-third-party-shape.mjs +0 -0
- package/bin/check-tool-count.mjs +0 -0
- package/bin/companion-preference-status.mjs +0 -0
- package/bin/gateguard-clear.mjs +0 -0
- package/bin/generate-plugin-manifests.mjs +2 -1
- package/bin/harvest-friction.mjs +0 -0
- package/bin/hook-stats.mjs +21 -21
- package/bin/install.mjs +120 -2
- package/bin/lint-transcript.mjs +15 -3
- package/bin/mcp-server.mjs +66 -8
- package/bin/observe.mjs +0 -0
- package/bin/plan-pack.mjs +77 -0
- package/bin/pre-commit-block-strays.sh +0 -0
- package/bin/refresh-third-party.mjs +0 -0
- package/bin/unified-cli.mjs +55 -410
- package/commands/continuous-improvement.md +115 -115
- package/commands/dashboard.md +56 -56
- package/commands/model-forward.md +13 -0
- package/commands/production-readiness-review.md +53 -0
- package/commands/roast.md +34 -0
- package/commands/seven-laws.md +16 -16
- package/commands/ship.md +57 -0
- package/commands/workspace-surface-audit.md +77 -77
- package/hooks/companion-preference.mjs +31 -19
- package/hooks/gateguard.mjs +38 -16
- package/hooks/goal-drift-stop.mjs +0 -0
- package/hooks/hook-pack.mjs +110 -0
- package/hooks/observe.sh +0 -0
- package/hooks/recall-briefing.mjs +0 -0
- package/hooks/route-prompt.mjs +0 -0
- package/hooks/session.sh +106 -106
- package/hooks/three-section-close.mjs +0 -0
- package/hooks/workflow-distill.mjs +145 -0
- package/instinct-packs/go.json +58 -58
- package/instinct-packs/meta.json +16 -16
- package/instinct-packs/python.json +58 -58
- package/instinct-packs/react.json +58 -58
- package/lib/cli-anything.mjs +0 -0
- package/lib/gateguard-state.mjs +62 -13
- package/lib/goal-drift-gate.mjs +0 -0
- package/lib/goal-state.mjs +8 -4
- package/lib/hook-pack-gate.mjs +65 -0
- package/lib/install-targets.mjs +121 -0
- package/lib/observe-event.mjs +0 -0
- package/lib/plan-review-packet.mjs +96 -0
- package/lib/plugin-metadata.mjs +30 -4
- package/lib/pm-marketplace.mjs +0 -0
- package/lib/recall-briefing.mjs +0 -0
- package/lib/recall-index.mjs +0 -0
- package/lib/resolve-home-dir.mjs +0 -0
- package/lib/skill-distill.mjs +141 -0
- package/lib/skill-tiers.mjs +0 -0
- package/lib/version-check.mjs +115 -0
- package/llms.txt +2 -2
- package/package.json +6 -4
- package/plugins/beginner.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/LICENSE +21 -21
- package/plugins/continuous-improvement/README.md +1 -1
- package/plugins/continuous-improvement/bin/backfill.mjs +19 -19
- package/plugins/continuous-improvement/bin/gateguard-clear.mjs +0 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +66 -8
- package/plugins/continuous-improvement/bin/observe.mjs +0 -0
- package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -115
- package/plugins/continuous-improvement/commands/dashboard.md +56 -56
- package/plugins/continuous-improvement/commands/model-forward.md +13 -0
- package/plugins/continuous-improvement/commands/production-readiness-review.md +53 -0
- package/plugins/continuous-improvement/commands/roast.md +34 -0
- package/plugins/continuous-improvement/commands/seven-laws.md +16 -16
- package/plugins/continuous-improvement/commands/ship.md +57 -0
- package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -77
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +31 -19
- package/plugins/continuous-improvement/hooks/gateguard.mjs +38 -16
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +0 -0
- package/plugins/continuous-improvement/hooks/hook-pack.mjs +110 -0
- package/plugins/continuous-improvement/hooks/hooks.json +16 -1
- package/plugins/continuous-improvement/hooks/observe.sh +0 -0
- package/plugins/continuous-improvement/hooks/recall-briefing.mjs +0 -0
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +0 -0
- package/plugins/continuous-improvement/hooks/session.sh +106 -106
- package/plugins/continuous-improvement/hooks/three-section-close.mjs +0 -0
- package/plugins/continuous-improvement/hooks/workflow-distill.mjs +145 -0
- package/plugins/continuous-improvement/instinct-packs/go.json +58 -58
- package/plugins/continuous-improvement/instinct-packs/meta.json +16 -16
- package/plugins/continuous-improvement/instinct-packs/python.json +58 -58
- package/plugins/continuous-improvement/instinct-packs/react.json +58 -58
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +62 -13
- package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +0 -0
- package/plugins/continuous-improvement/lib/goal-state.mjs +8 -4
- package/plugins/continuous-improvement/lib/hook-pack-gate.mjs +65 -0
- package/plugins/continuous-improvement/lib/observe-event.mjs +0 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +30 -4
- package/plugins/continuous-improvement/lib/recall-briefing.mjs +0 -0
- package/plugins/continuous-improvement/lib/recall-index.mjs +0 -0
- package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -0
- package/plugins/continuous-improvement/lib/skill-distill.mjs +141 -0
- package/plugins/continuous-improvement/skills/README.md +2 -1
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +4 -4
- package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/handoff/SKILL.md +0 -1
- package/plugins/continuous-improvement/skills/model-forward/SKILL.md +44 -0
- package/plugins/continuous-improvement/skills/roast/SKILL.md +108 -0
- package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +12 -32
- package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -411
- package/plugins/expert.json +5 -1
- package/skills/README.md +5 -3
- package/skills/gateguard.md +4 -4
- package/skills/goal-monitor.md +1 -1
- package/skills/handoff.md +0 -1
- package/skills/model-forward.md +44 -0
- package/skills/roast.md +108 -0
- package/skills/strategic-compact.md +12 -32
- package/skills/tdd-workflow.md +411 -411
- package/lib/compound-engineering.mjs +0 -831
- package/lib/pm-skills.mjs +0 -1274
- package/lib/unified-plugin.mjs +0 -924
- package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +0 -108
- package/skills/para-memory-files.md +0 -108
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Count Prose Check
|
|
4
|
+
*
|
|
5
|
+
* Sibling to `verify:skill-count`. That lint gates the literal phrase
|
|
6
|
+
* "N bundled skills" in the 5 generator-owned manifest surfaces. This lint
|
|
7
|
+
* gates the *human-facing* prose — the phrasings the marketing/docs copy
|
|
8
|
+
* actually uses, which no other invariant checks and which therefore rot
|
|
9
|
+
* silently every time a skill is added without a full prose cascade.
|
|
10
|
+
*
|
|
11
|
+
* Origin: PR #255 (`/roast`, the 26th skill) bumped the gated "N bundled
|
|
12
|
+
* skills" manifests but left README.md / docs/skills.md / docs/skill-use-cases.md
|
|
13
|
+
* / CONTRIBUTING.md / docs/using-this-plugin.md at "25 skills / 14 tier-2".
|
|
14
|
+
* verify:all stayed green because nothing checked those surfaces.
|
|
15
|
+
* `reports/daily-improvement.md` logs three prior rounds of the same drift.
|
|
16
|
+
*
|
|
17
|
+
* Single source of truth: the skill set on disk. The total and the
|
|
18
|
+
* core/featured/tier-1/tier-2/companion breakdown are *derived* from
|
|
19
|
+
* `skills/*.md` + the root `SKILL.md` frontmatter via the same primitives the
|
|
20
|
+
* tier lint uses (`discoverSkillSources`, `parseSkillFrontmatter`,
|
|
21
|
+
* `normalizeTier`). Nothing is hardcoded — add or retire a skill and this lint
|
|
22
|
+
* recomputes the expected numbers, then names every doc that disagrees.
|
|
23
|
+
*
|
|
24
|
+
* Checked surfaces (file → which derived claims it must contain):
|
|
25
|
+
* - README.md breakdown, total-skills, next-ordinal
|
|
26
|
+
* - docs/skills.md breakdown, total-skills, total-catalog, next-ordinal
|
|
27
|
+
* - docs/skill-use-cases.md breakdown, total-skills
|
|
28
|
+
* - CONTRIBUTING.md total-skills
|
|
29
|
+
* - docs/using-this-plugin.md total-catalog
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* node bin/check-skill-count-prose.mjs # Check the current repo
|
|
33
|
+
* node bin/check-skill-count-prose.mjs <repo-root> # Check a specific repo root
|
|
34
|
+
*
|
|
35
|
+
* Exit codes:
|
|
36
|
+
* 0 — every checked surface states the bundle-derived count
|
|
37
|
+
* 1 — at least one surface states a stale count, or a surface file is missing
|
|
38
|
+
*/
|
|
39
|
+
import { readFileSync } from "node:fs";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
import { argv, cwd, exit } from "node:process";
|
|
42
|
+
import { normalizeTier, parseSkillFrontmatter } from "../lib/skill-tiers.mjs";
|
|
43
|
+
import { discoverSkillSources } from "./check-skill-tiers.mjs";
|
|
44
|
+
/** Derive the total + per-tier breakdown from `skills/` + root SKILL.md frontmatter. */
|
|
45
|
+
export function deriveCounts(repoRoot) {
|
|
46
|
+
const sources = discoverSkillSources(repoRoot);
|
|
47
|
+
const tally = { core: 0, featured: 0, tier1: 0, tier2: 0, companion: 0, unknown: 0 };
|
|
48
|
+
for (const src of sources) {
|
|
49
|
+
const front = parseSkillFrontmatter(readFileSync(src.path, "utf8"));
|
|
50
|
+
switch (normalizeTier(front.tier)) {
|
|
51
|
+
case "core":
|
|
52
|
+
tally.core++;
|
|
53
|
+
break;
|
|
54
|
+
case "featured":
|
|
55
|
+
tally.featured++;
|
|
56
|
+
break;
|
|
57
|
+
case "1":
|
|
58
|
+
tally.tier1++;
|
|
59
|
+
break;
|
|
60
|
+
case "2":
|
|
61
|
+
tally.tier2++;
|
|
62
|
+
break;
|
|
63
|
+
case "companion":
|
|
64
|
+
tally.companion++;
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
tally.unknown++;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const total = sources.length;
|
|
72
|
+
return { ...tally, total, next: total + 1 };
|
|
73
|
+
}
|
|
74
|
+
/** English ordinal: 1→1st, 2→2nd, 3→3rd, 11→11th, 21→21st, 27→27th. */
|
|
75
|
+
export function ordinal(n) {
|
|
76
|
+
const mod100 = n % 100;
|
|
77
|
+
if (mod100 >= 11 && mod100 <= 13)
|
|
78
|
+
return `${n}th`;
|
|
79
|
+
switch (n % 10) {
|
|
80
|
+
case 1:
|
|
81
|
+
return `${n}st`;
|
|
82
|
+
case 2:
|
|
83
|
+
return `${n}nd`;
|
|
84
|
+
case 3:
|
|
85
|
+
return `${n}rd`;
|
|
86
|
+
default:
|
|
87
|
+
return `${n}th`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The exact substring a surface must contain for the given check kind. */
|
|
91
|
+
export function expectedFor(check, c) {
|
|
92
|
+
switch (check) {
|
|
93
|
+
case "breakdown":
|
|
94
|
+
return `${c.core} core + ${c.featured} featured + ${c.tier1} tier-1 + ${c.tier2} tier-2 + ${c.companion} always-bundled`;
|
|
95
|
+
case "total-skills":
|
|
96
|
+
return `${c.total} skills`;
|
|
97
|
+
case "total-catalog":
|
|
98
|
+
return `${c.total}-skill catalog`;
|
|
99
|
+
case "next-ordinal":
|
|
100
|
+
return `${ordinal(c.next)} skill`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export const PROSE_SURFACES = [
|
|
104
|
+
{ file: "README.md", checks: ["breakdown", "total-skills", "next-ordinal"] },
|
|
105
|
+
{
|
|
106
|
+
file: "docs/skills.md",
|
|
107
|
+
checks: ["breakdown", "total-skills", "total-catalog", "next-ordinal"],
|
|
108
|
+
},
|
|
109
|
+
{ file: "docs/skill-use-cases.md", checks: ["breakdown", "total-skills"] },
|
|
110
|
+
{ file: "CONTRIBUTING.md", checks: ["total-skills"] },
|
|
111
|
+
{ file: "docs/using-this-plugin.md", checks: ["total-catalog"] },
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Assert every surface contains every derived claim it owns. `counts` defaults
|
|
115
|
+
* to the value derived from `repoRoot`; pass it explicitly to test the
|
|
116
|
+
* surface-matching logic in isolation.
|
|
117
|
+
*/
|
|
118
|
+
export function checkProseSurfaces(repoRoot, counts, surfaces = PROSE_SURFACES) {
|
|
119
|
+
const c = counts ?? deriveCounts(repoRoot);
|
|
120
|
+
const failures = [];
|
|
121
|
+
for (const surface of surfaces) {
|
|
122
|
+
let content;
|
|
123
|
+
try {
|
|
124
|
+
content = readFileSync(join(repoRoot, surface.file), "utf8");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
content = null;
|
|
128
|
+
}
|
|
129
|
+
for (const check of surface.checks) {
|
|
130
|
+
const expected = expectedFor(check, c);
|
|
131
|
+
if (content === null) {
|
|
132
|
+
failures.push({ file: surface.file, check, expected, reason: "target-missing" });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!content.includes(expected)) {
|
|
136
|
+
failures.push({ file: surface.file, check, expected, reason: "stale" });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return failures;
|
|
141
|
+
}
|
|
142
|
+
function main() {
|
|
143
|
+
const repoRoot = argv[2] ?? cwd();
|
|
144
|
+
const counts = deriveCounts(repoRoot);
|
|
145
|
+
const failures = checkProseSurfaces(repoRoot, counts);
|
|
146
|
+
if (failures.length === 0) {
|
|
147
|
+
console.log(`OK skill-count-prose: all ${PROSE_SURFACES.length} doc surface(s) state the derived count (total=${counts.total}: ${expectedFor("breakdown", counts)}).`);
|
|
148
|
+
exit(0);
|
|
149
|
+
}
|
|
150
|
+
console.error(`FAIL skill-count-prose: ${failures.length} doc prose claim(s) do not match the bundle-derived count of ${counts.total}.`);
|
|
151
|
+
console.error(`Source of truth: skills/ frontmatter → ${expectedFor("breakdown", counts)} = ${counts.total} skills; next skill is the ${ordinal(counts.next)}.`);
|
|
152
|
+
console.error("");
|
|
153
|
+
for (const f of failures) {
|
|
154
|
+
if (f.reason === "target-missing") {
|
|
155
|
+
console.error(` ${f.file} — file not found (expected to contain "${f.expected}")`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error(` ${f.file} — missing "${f.expected}" [${f.check}]`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.error("");
|
|
162
|
+
console.error("Fix: bump the prose in the named file(s) to the derived count. When a skill is genuinely added or retired, this lint enumerates every human-facing surface that needs the same change.");
|
|
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-skill-count-prose.mjs")) {
|
|
167
|
+
main();
|
|
168
|
+
}
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* - plugins/continuous-improvement/.claude-plugin/plugin.json
|
|
25
25
|
* - package.json
|
|
26
26
|
* - llms.txt
|
|
27
|
+
* - .cloudplugin/marketplace.json (hand-maintained, not generator-owned)
|
|
28
|
+
*
|
|
29
|
+
* Stat-block files (count lives in a split HTML stat, not the phrase):
|
|
30
|
+
* - docs/landing/index.html — `<span class="v">N</span><span class="k">Bundled skills</span>`
|
|
31
|
+
* (PR #229 found this surface stale at 25 while every phrase file said 26)
|
|
27
32
|
*
|
|
28
33
|
* Usage:
|
|
29
34
|
* node bin/check-skill-count.mjs # Check the current repo
|
|
@@ -42,7 +47,9 @@ const CHECKED_FILES = [
|
|
|
42
47
|
"plugins/continuous-improvement/.claude-plugin/plugin.json",
|
|
43
48
|
"package.json",
|
|
44
49
|
"llms.txt",
|
|
50
|
+
".cloudplugin/marketplace.json",
|
|
45
51
|
];
|
|
52
|
+
const STAT_CHECKED_FILES = ["docs/landing/index.html"];
|
|
46
53
|
export function countSkills(repoRoot) {
|
|
47
54
|
const skillsDir = join(repoRoot, BUNDLE_SKILLS_DIR);
|
|
48
55
|
const entries = readdirSync(skillsDir);
|
|
@@ -75,6 +82,24 @@ function checkFile(repoRoot, relPath, expected) {
|
|
|
75
82
|
found: staleMatch ? staleMatch[0] : undefined,
|
|
76
83
|
};
|
|
77
84
|
}
|
|
85
|
+
function checkStatFile(repoRoot, relPath, expected) {
|
|
86
|
+
let content;
|
|
87
|
+
try {
|
|
88
|
+
content = readFileSync(join(repoRoot, relPath), "utf8");
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { file: relPath, reason: "missing" };
|
|
92
|
+
}
|
|
93
|
+
const expectedPattern = new RegExp(`class="v">${expected}</span><span class="k">Bundled skills`);
|
|
94
|
+
if (expectedPattern.test(content))
|
|
95
|
+
return null;
|
|
96
|
+
const staleMatch = content.match(/class="v">(\d+)<\/span><span class="k">Bundled skills/);
|
|
97
|
+
return {
|
|
98
|
+
file: relPath,
|
|
99
|
+
reason: staleMatch ? "stale" : "missing",
|
|
100
|
+
found: staleMatch ? `${staleMatch[1]} Bundled skills (stat block)` : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
78
103
|
function main() {
|
|
79
104
|
const repoRoot = argv[2] ?? cwd();
|
|
80
105
|
const expected = countSkills(repoRoot);
|
|
@@ -84,8 +109,14 @@ function main() {
|
|
|
84
109
|
if (v)
|
|
85
110
|
violations.push(v);
|
|
86
111
|
}
|
|
112
|
+
for (const file of STAT_CHECKED_FILES) {
|
|
113
|
+
const v = checkStatFile(repoRoot, file, expected);
|
|
114
|
+
if (v)
|
|
115
|
+
violations.push(v);
|
|
116
|
+
}
|
|
117
|
+
const totalChecked = CHECKED_FILES.length + STAT_CHECKED_FILES.length;
|
|
87
118
|
if (violations.length === 0) {
|
|
88
|
-
console.log(`OK skill-count: all ${
|
|
119
|
+
console.log(`OK skill-count: all ${totalChecked} description string(s) state "${expected} bundled skills" (matches skills/ source-of-truth).`);
|
|
89
120
|
exit(0);
|
|
90
121
|
}
|
|
91
122
|
console.error(`FAIL skill-count: ${violations.length} description string(s) do not state the actual bundled-skill count of ${expected}.`);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - reach-through into `hooks/`, `scripts/`, `src/`, or deeper paths under
|
|
13
13
|
* `bin/` or `lib/`
|
|
14
14
|
*
|
|
15
|
-
* Baseline: all
|
|
15
|
+
* Baseline: all 56 `src/test/*.test.mts` files pass today.
|
|
16
16
|
*
|
|
17
17
|
* Usage:
|
|
18
18
|
* node bin/check-test-imports-only.mjs # Check the current repo
|
|
File without changes
|
package/bin/check-tool-count.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/bin/gateguard-clear.mjs
CHANGED
|
File without changes
|
|
@@ -107,7 +107,7 @@ async function writePluginBundleReadme() {
|
|
|
107
107
|
"- `tdd-workflow` — RED/GREEN/REFACTOR + 80% coverage gate",
|
|
108
108
|
"- `workspace-surface-audit` — environment + capability audit",
|
|
109
109
|
"- Tier-1/Tier-2 enforcement skills (`gateguard`, `verification-loop`,",
|
|
110
|
-
" `
|
|
110
|
+
" `safety-guard`, `token-budget-advisor`,",
|
|
111
111
|
" `strategic-compact`, `wild-risa-balance`)",
|
|
112
112
|
"",
|
|
113
113
|
"**Optional companions the orchestrator routes to (install separately if",
|
|
@@ -152,6 +152,7 @@ async function writePluginBundle() {
|
|
|
152
152
|
copyFileTo(join(REPO_ROOT, "lib", "observe-event.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "observe-event.mjs")),
|
|
153
153
|
copyFileTo(join(REPO_ROOT, "lib", "goal-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "goal-state.mjs")),
|
|
154
154
|
copyFileTo(join(REPO_ROOT, "lib", "goal-drift-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "goal-drift-gate.mjs")),
|
|
155
|
+
copyFileTo(join(REPO_ROOT, "lib", "hook-pack-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "hook-pack-gate.mjs")),
|
|
155
156
|
copyFileTo(join(REPO_ROOT, "lib", "recall-index.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-index.mjs")),
|
|
156
157
|
copyFileTo(join(REPO_ROOT, "lib", "recall-briefing.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-briefing.mjs")),
|
|
157
158
|
copyFileTo(join(REPO_ROOT, "lib", "skill-distill.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "skill-distill.mjs")),
|
package/bin/harvest-friction.mjs
CHANGED
|
File without changes
|
package/bin/hook-stats.mjs
CHANGED
|
@@ -46,27 +46,27 @@ import { platform } from "node:os";
|
|
|
46
46
|
import { join } from "node:path";
|
|
47
47
|
import { argv, cwd, exit, stderr, stdout } from "node:process";
|
|
48
48
|
import { resolveHomeDir } from "../lib/resolve-home-dir.mjs";
|
|
49
|
-
const USAGE = `Usage: node bin/hook-stats.mjs [options]
|
|
50
|
-
|
|
51
|
-
Reads JSONL telemetry from <HOME>/.claude/hook-telemetry/*.jsonl and
|
|
52
|
-
prints per-hook aggregate counts over a recent time window.
|
|
53
|
-
|
|
54
|
-
Options:
|
|
55
|
-
--hours=<n> Time window in hours (default: 24)
|
|
56
|
-
--hash=<hash> Only read this single <hash>.jsonl file
|
|
57
|
-
(default: sha256 of the current working directory,
|
|
58
|
-
normalized to forward slashes and lowercased on
|
|
59
|
-
Windows, then first 12 hex chars)
|
|
60
|
-
--telemetry-dir=<path> Override telemetry dir
|
|
61
|
-
(default: <HOME>/.claude/hook-telemetry)
|
|
62
|
-
--help Show this help and exit 0
|
|
63
|
-
|
|
64
|
-
Output:
|
|
65
|
-
One line per hook: "<hook> last <hours>h: pass=<n> block=<n>
|
|
66
|
-
skip-short=<n> total=<n> avg-duration=<X>ms".
|
|
67
|
-
If there are no records in the window: "no records in last <hours>h".
|
|
68
|
-
|
|
69
|
-
The reader is parse-tolerant: malformed JSONL lines are skipped silently.
|
|
49
|
+
const USAGE = `Usage: node bin/hook-stats.mjs [options]
|
|
50
|
+
|
|
51
|
+
Reads JSONL telemetry from <HOME>/.claude/hook-telemetry/*.jsonl and
|
|
52
|
+
prints per-hook aggregate counts over a recent time window.
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--hours=<n> Time window in hours (default: 24)
|
|
56
|
+
--hash=<hash> Only read this single <hash>.jsonl file
|
|
57
|
+
(default: sha256 of the current working directory,
|
|
58
|
+
normalized to forward slashes and lowercased on
|
|
59
|
+
Windows, then first 12 hex chars)
|
|
60
|
+
--telemetry-dir=<path> Override telemetry dir
|
|
61
|
+
(default: <HOME>/.claude/hook-telemetry)
|
|
62
|
+
--help Show this help and exit 0
|
|
63
|
+
|
|
64
|
+
Output:
|
|
65
|
+
One line per hook: "<hook> last <hours>h: pass=<n> block=<n>
|
|
66
|
+
skip-short=<n> total=<n> avg-duration=<X>ms".
|
|
67
|
+
If there are no records in the window: "no records in last <hours>h".
|
|
68
|
+
|
|
69
|
+
The reader is parse-tolerant: malformed JSONL lines are skipped silently.
|
|
70
70
|
`;
|
|
71
71
|
function parseArgs(rawArgs) {
|
|
72
72
|
const opts = {
|
package/bin/install.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* npx continuous-improvement install # beginner mode (default)
|
|
7
7
|
* npx continuous-improvement install --mode expert # + MCP server + session hooks
|
|
8
8
|
* npx continuous-improvement install --pack react # load starter instinct pack
|
|
9
|
+
* npx continuous-improvement install --target gemini,codex # skill into other agents' rules files
|
|
9
10
|
* npx continuous-improvement install --uninstall # remove everything
|
|
10
11
|
*/
|
|
11
12
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
@@ -14,7 +15,9 @@ import { homedir } from "node:os";
|
|
|
14
15
|
import { dirname, join } from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { createHash } from "node:crypto";
|
|
17
|
-
import { getToolNames } from "../lib/plugin-metadata.mjs";
|
|
18
|
+
import { PACKAGE_NAME, VERSION, getToolNames } from "../lib/plugin-metadata.mjs";
|
|
19
|
+
import { TARGET_IDS, planTargetWrites, resolveTargets } from "../lib/install-targets.mjs";
|
|
20
|
+
import { evaluateUpdateCheck, fetchLatestNpmVersion, isThrottled, pendingNotice, } from "../lib/version-check.mjs";
|
|
18
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
22
|
const __dirname = dirname(__filename);
|
|
20
23
|
const SKILL_SOURCE = join(__dirname, "..", "SKILL.md");
|
|
@@ -493,6 +496,17 @@ Options for 'install':
|
|
|
493
496
|
beginner — hooks + skill + commands (default)
|
|
494
497
|
expert — beginner + MCP server + session hooks
|
|
495
498
|
--pack <name> Load a starter instinct pack (react, python, go, meta)
|
|
499
|
+
--target <names> Comma-separated platform list (default: claude):
|
|
500
|
+
claude — full install: hooks + skill + commands
|
|
501
|
+
gemini — GEMINI.md (Gemini CLI)
|
|
502
|
+
codex — AGENTS.md (Codex CLI / agents.md standard)
|
|
503
|
+
cursor — .cursor/rules/continuous-improvement.mdc
|
|
504
|
+
windsurf — .windsurf/rules/continuous-improvement.md
|
|
505
|
+
zed — .rules
|
|
506
|
+
aider — CONVENTIONS.md + .aider.conf.yml
|
|
507
|
+
copilot — .github/copilot-instructions.md
|
|
508
|
+
Non-claude targets write the 7-Laws skill text into the
|
|
509
|
+
current project; hooks/MCP/instincts stay Claude Code-only.
|
|
496
510
|
--help Show this help
|
|
497
511
|
|
|
498
512
|
Options for 'backfill':
|
|
@@ -503,6 +517,7 @@ Examples:
|
|
|
503
517
|
npx continuous-improvement install # beginner (default)
|
|
504
518
|
npx continuous-improvement install --mode expert # full power
|
|
505
519
|
npx continuous-improvement install --pack react # load React instincts
|
|
520
|
+
npx continuous-improvement install --target gemini,codex # other agents' rules files
|
|
506
521
|
npx continuous-improvement backfill --dry-run # report thin/rich row counts
|
|
507
522
|
npx continuous-improvement backfill # tag rows in place
|
|
508
523
|
npx continuous-improvement install --uninstall # remove everything
|
|
@@ -510,7 +525,12 @@ Examples:
|
|
|
510
525
|
}
|
|
511
526
|
function getProjectHashSync() {
|
|
512
527
|
try {
|
|
513
|
-
|
|
528
|
+
// No shell redirect — `2>/dev/null` breaks under cmd.exe (tries to open a
|
|
529
|
+
// file literally named /dev/null); ignore stderr via stdio instead.
|
|
530
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
531
|
+
encoding: "utf8",
|
|
532
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
533
|
+
}).trim();
|
|
514
534
|
const hash = createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
515
535
|
return { root, hash };
|
|
516
536
|
}
|
|
@@ -547,6 +567,60 @@ if (args.includes("--uninstall")) {
|
|
|
547
567
|
uninstallAll();
|
|
548
568
|
process.exit(0);
|
|
549
569
|
}
|
|
570
|
+
// --target <names>: comma-separated platform list, default claude-only.
|
|
571
|
+
// Non-claude targets receive the skill text in their platform's rules file
|
|
572
|
+
// (project-level); the full hook/MCP/instinct install stays Claude Code-only.
|
|
573
|
+
const targetFlagIndex = rawArgs.indexOf("--target");
|
|
574
|
+
let requestedTargets = ["claude"];
|
|
575
|
+
if (targetFlagIndex !== -1) {
|
|
576
|
+
const targetCsv = rawArgs[targetFlagIndex + 1];
|
|
577
|
+
if (!targetCsv || targetCsv.startsWith("--")) {
|
|
578
|
+
console.error(`--target requires a value. Valid targets: ${TARGET_IDS.join(", ")}`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
const resolved = resolveTargets(targetCsv);
|
|
582
|
+
if (resolved.unknown.length > 0) {
|
|
583
|
+
console.error(`Unknown --target name(s): ${resolved.unknown.join(", ")}. Valid targets: ${TARGET_IDS.join(", ")}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
if (resolved.targets.length === 0) {
|
|
587
|
+
console.error(`--target requires at least one name. Valid targets: ${TARGET_IDS.join(", ")}`);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
requestedTargets = resolved.targets;
|
|
591
|
+
}
|
|
592
|
+
function installNonClaudeTargets(targetIds) {
|
|
593
|
+
const skillMd = readFileSync(SKILL_SOURCE, "utf8");
|
|
594
|
+
const project = getProjectHashSync();
|
|
595
|
+
const projectRoot = project.root === "global" ? process.cwd() : project.root;
|
|
596
|
+
const readExisting = (relPath) => {
|
|
597
|
+
const absPath = join(projectRoot, relPath);
|
|
598
|
+
return existsSync(absPath) ? readFileSync(absPath, "utf8") : null;
|
|
599
|
+
};
|
|
600
|
+
const notes = new Set();
|
|
601
|
+
for (const targetId of targetIds) {
|
|
602
|
+
const plan = planTargetWrites(targetId, skillMd, readExisting);
|
|
603
|
+
for (const write of plan.writes) {
|
|
604
|
+
const absPath = join(projectRoot, write.relPath);
|
|
605
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
606
|
+
writeFileSync(absPath, write.content);
|
|
607
|
+
console.log(` ✓ ${targetId} → ${write.relPath}`);
|
|
608
|
+
}
|
|
609
|
+
for (const note of plan.notes)
|
|
610
|
+
notes.add(note);
|
|
611
|
+
}
|
|
612
|
+
for (const note of notes)
|
|
613
|
+
console.log(` ℹ ${note}`);
|
|
614
|
+
}
|
|
615
|
+
const nonClaudeTargets = requestedTargets.filter((targetId) => targetId !== "claude");
|
|
616
|
+
if (nonClaudeTargets.length > 0) {
|
|
617
|
+
console.log("\ncontinuous-improvement multi-platform install\n");
|
|
618
|
+
installNonClaudeTargets(nonClaudeTargets);
|
|
619
|
+
if (!requestedTargets.includes("claude")) {
|
|
620
|
+
console.log("\nDone. Claude Code install skipped (not in --target list).");
|
|
621
|
+
process.exit(0);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
550
624
|
console.log(`
|
|
551
625
|
continuous-improvement (mode: ${INSTALL_MODE})
|
|
552
626
|
Research → Plan → Execute → Verify → Reflect → Learn → Iterate
|
|
@@ -621,3 +695,47 @@ Next steps:
|
|
|
621
695
|
${INSTALL_MODE === "expert" ? `\nMCP tools available (${getToolNames("expert").length}): ${getToolNames("expert").join(", ")}` : ""}
|
|
622
696
|
Available instinct packs: npx continuous-improvement install --pack react|python|go|meta
|
|
623
697
|
`);
|
|
698
|
+
// Update-available nudge for the npm/CLI install path (the marketplace path is
|
|
699
|
+
// covered by Claude Code's native plugin auto-update). Reads the public npm
|
|
700
|
+
// registry only — no telemetry. Throttled, default-on, off via
|
|
701
|
+
// CLAUDE_CI_UPDATE_CHECK=off. Fully fail-open: it can never change the
|
|
702
|
+
// installer's outcome. See docs/plans/2026-06-25-version-check-nudge.md.
|
|
703
|
+
async function maybeNotifyUpdate() {
|
|
704
|
+
if ((process.env.CLAUDE_CI_UPDATE_CHECK ?? "on").trim().toLowerCase() === "off")
|
|
705
|
+
return;
|
|
706
|
+
const cacheFile = join(getHomeDir(), ".claude", "instincts", "global", "update-check.json");
|
|
707
|
+
const now = Date.now();
|
|
708
|
+
let cache = null;
|
|
709
|
+
try {
|
|
710
|
+
if (existsSync(cacheFile))
|
|
711
|
+
cache = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
cache = null;
|
|
715
|
+
}
|
|
716
|
+
if (isThrottled(cache, now)) {
|
|
717
|
+
const notice = pendingNotice(cache, VERSION);
|
|
718
|
+
if (notice)
|
|
719
|
+
console.log(`\n${notice}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const remote = await fetchLatestNpmVersion(PACKAGE_NAME);
|
|
723
|
+
const decision = evaluateUpdateCheck({ local: VERSION, remote, now });
|
|
724
|
+
if (decision.notice)
|
|
725
|
+
console.log(`\n${decision.notice}`);
|
|
726
|
+
if (decision.nextCache) {
|
|
727
|
+
try {
|
|
728
|
+
mkdirSync(dirname(cacheFile), { recursive: true });
|
|
729
|
+
writeFileSync(cacheFile, JSON.stringify(decision.nextCache) + "\n", "utf8");
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
// cache persistence is best-effort; never throw
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
await maybeNotifyUpdate();
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// a version check must never change the installer's outcome
|
|
741
|
+
}
|
package/bin/lint-transcript.mjs
CHANGED
|
@@ -160,6 +160,12 @@ function formatReport(result) {
|
|
|
160
160
|
return lines.join("\n");
|
|
161
161
|
}
|
|
162
162
|
const args = process.argv.slice(2);
|
|
163
|
+
function actionInput(name) {
|
|
164
|
+
return (process.env[`INPUT_${name.toUpperCase()}`] ?? "").trim();
|
|
165
|
+
}
|
|
166
|
+
function actionInputBoolean(name) {
|
|
167
|
+
return /^(1|true|yes)$/i.test(actionInput(name));
|
|
168
|
+
}
|
|
163
169
|
if (args.includes("--help") || args.includes("-h")) {
|
|
164
170
|
console.log(`
|
|
165
171
|
Agent Transcript Linter — The 7 Laws of AI Agent Discipline
|
|
@@ -176,7 +182,7 @@ Options:
|
|
|
176
182
|
`);
|
|
177
183
|
process.exit(0);
|
|
178
184
|
}
|
|
179
|
-
const isStrict = args.includes("--strict");
|
|
185
|
+
const isStrict = args.includes("--strict") || actionInputBoolean("strict");
|
|
180
186
|
const isJson = args.includes("--json");
|
|
181
187
|
const isStdin = args.includes("--stdin");
|
|
182
188
|
async function main() {
|
|
@@ -190,7 +196,9 @@ async function main() {
|
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
else {
|
|
193
|
-
const filePath = args.find((arg) => !arg.startsWith("--"))
|
|
199
|
+
const filePath = args.find((arg) => !arg.startsWith("--"))
|
|
200
|
+
|| actionInput("transcript-path")
|
|
201
|
+
|| actionInput("observations-path");
|
|
194
202
|
if (!filePath) {
|
|
195
203
|
console.error("Error: provide a file path or use --stdin");
|
|
196
204
|
process.exit(1);
|
|
@@ -213,16 +221,20 @@ async function main() {
|
|
|
213
221
|
process.exit(0);
|
|
214
222
|
}
|
|
215
223
|
const result = analyzeTranscript(events, parseFailures);
|
|
224
|
+
const report = formatReport(result);
|
|
216
225
|
if (isJson) {
|
|
217
226
|
console.log(JSON.stringify(result, null, 2));
|
|
218
227
|
}
|
|
219
228
|
else {
|
|
220
|
-
console.log(
|
|
229
|
+
console.log(report);
|
|
221
230
|
}
|
|
222
231
|
if (process.env.GITHUB_OUTPUT) {
|
|
223
232
|
const outputLines = [
|
|
224
233
|
`violations=${result.violations.length}`,
|
|
225
234
|
`score=${result.score}`,
|
|
235
|
+
`report<<CI_REPORT_EOF`,
|
|
236
|
+
report,
|
|
237
|
+
"CI_REPORT_EOF",
|
|
226
238
|
];
|
|
227
239
|
const { appendFileSync } = await import("node:fs");
|
|
228
240
|
for (const line of outputLines) {
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
import { execSync } from "node:child_process";
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
import { basename, dirname, join } from "node:path";
|
|
16
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
17
17
|
import { createInterface } from "node:readline";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import { PACKAGE_NAME, VERSION, getToolDefinitions, isPluginMode, } from "../lib/plugin-metadata.mjs";
|
|
21
21
|
import { formatDriftReport, parseGoalFromPlan, scoreObservations, } from "../lib/goal-state.mjs";
|
|
22
22
|
import { buildIndex, formatRecallHits, parseSince, query as queryRecall, } from "../lib/recall-index.mjs";
|
|
23
|
-
import { draftFromCandidate, extractTrajectories, findCandidates, formatCandidates, serializeDraft, } from "../lib/skill-distill.mjs";
|
|
24
|
-
import { MAX_CLEARED_FILES, clearFiles, resolveSessionDir, } from "../lib/gateguard-state.mjs";
|
|
23
|
+
import { draftFromCandidate, draftFromWorkflowRun, extractTrajectories, findCandidates, formatCandidates, serializeDraft, workflowRunFromObservations, } from "../lib/skill-distill.mjs";
|
|
24
|
+
import { MAX_CLEARED_FILES, canonicalizeFileKey, clearFiles, resolveInstinctsRoot, resolveSessionDir, } from "../lib/gateguard-state.mjs";
|
|
25
25
|
function getHomeDir() {
|
|
26
26
|
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
27
27
|
}
|
|
@@ -219,6 +219,7 @@ function readDistillObservations(projectHash) {
|
|
|
219
219
|
tool: getString(observation.tool),
|
|
220
220
|
input_summary: getString(observation.input_summary),
|
|
221
221
|
output_summary: getString(observation.output_summary),
|
|
222
|
+
event: getString(observation.event),
|
|
222
223
|
}));
|
|
223
224
|
}
|
|
224
225
|
function detectLevel(projectHash) {
|
|
@@ -508,10 +509,11 @@ function handleTool(name, params) {
|
|
|
508
509
|
}
|
|
509
510
|
case "ci_gateguard_clear": {
|
|
510
511
|
// Beginner-available on purpose: the GateGuard hook fires for every
|
|
511
|
-
// install, so the clearance action must too.
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
512
|
+
// install, so the clearance action must too. The hook's block reason now
|
|
513
|
+
// prints state_path (the session-scoped state file); honoring it writes
|
|
514
|
+
// the marker exactly where the hook looks. Without it the MCP server can't
|
|
515
|
+
// know the caller's session, so it falls back to the canonical session dir
|
|
516
|
+
// — correct only on the legacy unscoped path.
|
|
515
517
|
const rawList = Array.isArray(params.file_paths) ? params.file_paths : [];
|
|
516
518
|
const listPaths = rawList.filter((value) => typeof value === "string" && value.length > 0);
|
|
517
519
|
const single = getString(params.file_path).trim();
|
|
@@ -519,7 +521,24 @@ function handleTool(name, params) {
|
|
|
519
521
|
if (paths.length === 0) {
|
|
520
522
|
return error("file_paths is required — pass the file path(s) named in the GateGuard block reason, e.g. { file_paths: [\"src/x.ts\"] }.");
|
|
521
523
|
}
|
|
522
|
-
|
|
524
|
+
// state_path is a model-controlled argument, so it could be a traversal
|
|
525
|
+
// string (`../../etc/...`). dirname leaves `..` segments for the OS to
|
|
526
|
+
// resolve at write time, so without a bound this is an arbitrary-write
|
|
527
|
+
// primitive. Resolve + canonicalize, then require containment within the
|
|
528
|
+
// instincts root (the only tree the hook ever prints a state_path inside).
|
|
529
|
+
const rawStatePath = getString(params.state_path).trim();
|
|
530
|
+
let sessionDir;
|
|
531
|
+
if (rawStatePath) {
|
|
532
|
+
const resolvedKey = canonicalizeFileKey(resolve(rawStatePath));
|
|
533
|
+
const rootKey = canonicalizeFileKey(resolveInstinctsRoot());
|
|
534
|
+
if (resolvedKey !== rootKey && !resolvedKey.startsWith(`${rootKey}/`)) {
|
|
535
|
+
return error("state_path must resolve inside ~/.claude/instincts/. Pass it verbatim from the GateGuard block reason.");
|
|
536
|
+
}
|
|
537
|
+
sessionDir = dirname(resolve(rawStatePath));
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
sessionDir = resolveSessionDir();
|
|
541
|
+
}
|
|
523
542
|
const { cleared, skippedForCap } = clearFiles(sessionDir, paths);
|
|
524
543
|
const lines = [
|
|
525
544
|
"## GateGuard clearance",
|
|
@@ -837,6 +856,45 @@ function handleTool(name, params) {
|
|
|
837
856
|
"```",
|
|
838
857
|
].join("\n"));
|
|
839
858
|
}
|
|
859
|
+
case "ci_distill_from_workflow": {
|
|
860
|
+
if (MODE !== "expert") {
|
|
861
|
+
return error("ci_distill_from_workflow requires expert mode");
|
|
862
|
+
}
|
|
863
|
+
const run = workflowRunFromObservations(readDistillObservations(project.hash));
|
|
864
|
+
if (!run) {
|
|
865
|
+
return text("No completed-and-verified Workflow run found in this project's observations. " +
|
|
866
|
+
"A run qualifies when a `Workflow` tool call is followed by a passing verify/test/build in the same feed. " +
|
|
867
|
+
"Run a workflow, verify its output, then try `ci_distill_from_workflow` again.");
|
|
868
|
+
}
|
|
869
|
+
const draftInstinct = draftFromWorkflowRun(run);
|
|
870
|
+
if (!isSafeDraftId(draftInstinct.id)) {
|
|
871
|
+
return error(`Internal error: generated draft id "${draftInstinct.id}" failed the safety check.`);
|
|
872
|
+
}
|
|
873
|
+
const draft = serializeDraft(draftInstinct);
|
|
874
|
+
const draftsDir = join(INSTINCTS_DIR, project.hash, "drafts");
|
|
875
|
+
const draftPath = join(draftsDir, `${draftInstinct.id}.yaml`);
|
|
876
|
+
try {
|
|
877
|
+
mkdirSync(draftsDir, { recursive: true });
|
|
878
|
+
writeFileSync(draftPath, draft);
|
|
879
|
+
}
|
|
880
|
+
catch (err) {
|
|
881
|
+
return error(`Failed to write draft to ${draftPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
882
|
+
}
|
|
883
|
+
return text([
|
|
884
|
+
"## Draft written from a verified workflow run",
|
|
885
|
+
"",
|
|
886
|
+
`**Workflow:** ${run.name}`,
|
|
887
|
+
`**Path:** ${draftPath}`,
|
|
888
|
+
"",
|
|
889
|
+
"Edit the body to capture the real recipe (preconditions, concrete steps, gotchas), then promote with:",
|
|
890
|
+
"",
|
|
891
|
+
` ci_distill_promote id=${draftInstinct.id}`,
|
|
892
|
+
"",
|
|
893
|
+
"```yaml",
|
|
894
|
+
draft.trimEnd(),
|
|
895
|
+
"```",
|
|
896
|
+
].join("\n"));
|
|
897
|
+
}
|
|
840
898
|
case "ci_distill_promote": {
|
|
841
899
|
if (MODE !== "expert") {
|
|
842
900
|
return error("ci_distill_promote requires expert mode");
|