@ngockhoale/ukit 1.6.7 → 2.0.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/CHANGELOG.md +26 -0
- package/manifests/platform.full.yaml +24 -0
- package/package.json +2 -1
- package/scripts/skill/audit-skill.mjs +39 -0
- package/src/cli/commands/doctor.js +22 -2
- package/src/cli/commands/memory.js +76 -1
- package/src/core/memory/store.js +125 -1
- package/src/core/skillProfile.js +45 -0
- package/src/skill/auditSkill.js +99 -0
- package/templates/.claude/agents/code-reviewer.md +51 -7
- package/templates/.claude/agents/handoff-planner.md +18 -2
- package/templates/.claude/skills/canvas-design/SKILL.md +2 -20
- package/templates/.claude/skills/canvas-design/philosophy-examples.md +23 -0
- package/templates/.claude/skills/debugging-toolkit/SKILL.md +2 -30
- package/templates/.claude/skills/debugging-toolkit/reference-tables.md +33 -0
- package/templates/.claude/skills/docs-manager/SKILL.md +7 -249
- package/templates/.claude/skills/docs-manager/conventions-and-examples.md +221 -0
- package/templates/.claude/skills/docx/SKILL.md +3 -34
- package/templates/.claude/skills/docx/redlining-reference.md +34 -0
- package/templates/.claude/skills/duraone/SKILL.md +12 -16
- package/templates/.claude/skills/executing-plans/SKILL.md +31 -19
- package/templates/.claude/skills/file-organizer/SKILL.md +2 -170
- package/templates/.claude/skills/file-organizer/examples-and-practices.md +173 -0
- package/templates/.claude/skills/pdf/SKILL.md +1 -62
- package/templates/.claude/skills/pdf/reference.md +65 -0
- package/templates/.claude/skills/pdf-processing-pro/SKILL.md +2 -73
- package/templates/.claude/skills/pdf-processing-pro/workflows-and-troubleshooting.md +80 -0
- package/templates/.claude/skills/pptx/SKILL.md +14 -286
- package/templates/.claude/skills/pptx/design-references.md +81 -0
- package/templates/.claude/skills/pptx/template-replacement-reference.md +150 -0
- package/templates/.claude/skills/pptx/utilities.md +62 -0
- package/templates/.claude/skills/project-learning/SKILL.md +32 -0
- package/templates/.claude/skills/root-cause-tracing/SKILL.md +2 -35
- package/templates/.claude/skills/root-cause-tracing/diagrams.md +44 -0
- package/templates/.claude/skills/sharing-skills/SKILL.md +1 -41
- package/templates/.claude/skills/sharing-skills/complete-example.md +41 -0
- package/templates/.claude/skills/skill-quality/SKILL.md +37 -0
- package/templates/.claude/skills/skill-quality/pressure-scenario-template.md +20 -0
- package/templates/.claude/skills/skill-quality/rationalization-table-template.md +15 -0
- package/templates/.claude/skills/skill-quality/trigger-accuracy-template.md +32 -0
- package/templates/.claude/skills/sql-optimization-patterns/SKILL.md +13 -440
- package/templates/.claude/skills/sql-optimization-patterns/references/advanced-techniques.md +128 -0
- package/templates/.claude/skills/sql-optimization-patterns/references/core-concepts.md +112 -0
- package/templates/.claude/skills/sql-optimization-patterns/references/query-patterns.md +204 -0
- package/templates/.claude/skills/subagent-driven-development/SKILL.md +4 -51
- package/templates/.claude/skills/subagent-driven-development/example-workflow.md +40 -0
- package/templates/.claude/skills/systematic-debugging/SKILL.md +2 -28
- package/templates/.claude/skills/systematic-debugging/reference-tables.md +33 -0
- package/templates/.claude/skills/test-driven-development/SKILL.md +2 -51
- package/templates/.claude/skills/test-driven-development/reference-tables.md +56 -0
- package/templates/.claude/skills/testing-anti-patterns/SKILL.md +1 -10
- package/templates/.claude/skills/testing-anti-patterns/reference-tables.md +14 -0
- package/templates/.claude/skills/verification-before-completion/SKILL.md +1 -31
- package/templates/.claude/skills/verification-before-completion/key-patterns.md +33 -0
- package/templates/.claude/ukit/index/extract-image.mjs +10 -3
- package/templates/CLAUDE.md +4 -0
- package/src/core/memory/index.js +0 -2
- package/src/core/router/index.js +0 -2
- package/src/core/validation/index.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.0.0 - 2026-08-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`skill-quality` skill** (maintainer-only) — adversarial pressure-testing process for template skills/agents before shipping changes, with `pressure-scenario-template.md`, `rationalization-table-template.md`, and `trigger-accuracy-template.md` companion files, plus `scripts/skill/audit-skill.mjs` (`yarn skill:audit`) for scaffolding scenario files and recording results. Not a `ukit` CLI command — a maintainer dev-tool, same tier as `yarn bug:triage`.
|
|
10
|
+
- **`executing-plans` Preflight checklist + Blocker Classifier** — structured pre-execution checks and a formal blocker taxonomy, replacing an unstructured "When to Stop" bullet list.
|
|
11
|
+
- **`handoff-planner` Scope Explosion Detector + Right-Sizing rule** — flags oversized multi-subsystem handoff requests and enforces task-granularity limits, on top of the existing wave-dependency logic.
|
|
12
|
+
- **`RULES.md` / task template Interfaces block** — `Consumes`/`Produces` fields for explicit task-to-task data contracts in handoff cycles.
|
|
13
|
+
- **`code-reviewer` agent generalized** with a `REVIEW_TARGET_TYPE` branch (`code` / `spec`), so it can review non-diff artifacts (e.g. specs) in addition to code diffs.
|
|
14
|
+
- **`skillProfile.js` + `ukit doctor --skills`** — word-count budget report for every `templates/.claude/skills/*/SKILL.md`, flagging files over a 500-word default threshold.
|
|
15
|
+
- **Project Pattern Learning** — schema + `ukit memory list --pending` / `approve` / `reject` CLI surface for proposing and confirming project-local conventions, gated on human approval (never auto-applied, project-local scope only).
|
|
16
|
+
- **`project-learning` skill** — guides proposing/confirming project conventions through the new pattern-learning CLI.
|
|
17
|
+
- Wired the previously dead `runHygiene()` (decision-superseding + session consolidation) into the live memory flow instead of leaving it uncalled.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Trimmed `templates/.claude/skills/**/SKILL.md` word count 31,227 → 25,900 words (17%) via companion-file extraction and in-place deduplication, with zero behavior loss (content relocated to companion `.md` files, never deleted, except one verbatim-duplicate line removed from `duraone`). See `docs/WORKLOG.md` Task 13 entry for the full per-skill breakdown.
|
|
22
|
+
- Deleted 3 orphaned barrel `index.js` re-export files (`src/core/router/index.js`, `src/core/validation/index.js`, `src/core/memory/index.js`) with zero importers anywhere in the repo.
|
|
23
|
+
- Raised the packed-artifact size budget in `tests/integration/packageArtifact.test.js` (`1,680,000` → `1,684,000` bytes packed, `5,283,000` → `5,291,000` bytes unpacked) — the artifact is dominated by binary assets (canvas fonts, OOXML XSD schemas) that skill-text trimming can't reduce.
|
|
24
|
+
|
|
25
|
+
## 1.6.8 - 2026-08-09
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`extract-image.mjs`** no longer re-arms `pending-<sha>.json` for an image that already has an `analyzed-<sha>.json` receipt. `selectImages()` always returns the most recent N images from the whole transcript, so a fully-analyzed image never fell out of that window on its own; since `vision-router.sh` calls `--mark-pending` on every prompt, this re-triggered the vision hint/gate on the same closed-out image forever, even on unrelated prompts. Both `--detect` and `--mark-pending` now skip any sha with an existing analyzed receipt.
|
|
30
|
+
|
|
5
31
|
## 1.6.7 - 2026-08-09
|
|
6
32
|
|
|
7
33
|
### Fixed
|
|
@@ -686,6 +686,30 @@ items:
|
|
|
686
686
|
packs:
|
|
687
687
|
- core
|
|
688
688
|
|
|
689
|
+
- id: skill-quality-skill
|
|
690
|
+
type: skill
|
|
691
|
+
sourceTemplate: .claude/skills/skill-quality
|
|
692
|
+
targetPath: .claude/skills/skill-quality
|
|
693
|
+
requires:
|
|
694
|
+
- core-skill-delivery
|
|
695
|
+
mergeStrategy: overwrite_with_backup
|
|
696
|
+
variables: []
|
|
697
|
+
enabledByDefault: true
|
|
698
|
+
packs:
|
|
699
|
+
- core
|
|
700
|
+
|
|
701
|
+
- id: project-learning-skill
|
|
702
|
+
type: skill
|
|
703
|
+
sourceTemplate: .claude/skills/project-learning/SKILL.md
|
|
704
|
+
targetPath: .claude/skills/project-learning/SKILL.md
|
|
705
|
+
requires:
|
|
706
|
+
- core-skill-delivery
|
|
707
|
+
mergeStrategy: overwrite_with_backup
|
|
708
|
+
variables: []
|
|
709
|
+
enabledByDefault: true
|
|
710
|
+
packs:
|
|
711
|
+
- core
|
|
712
|
+
|
|
689
713
|
- id: sql-optimization-patterns-skill
|
|
690
714
|
type: skill
|
|
691
715
|
sourceTemplate: .claude/skills/sql-optimization-patterns/SKILL.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngockhoale/ukit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Install/update an index-first AI workspace for Claude Code, Antigravity, OpenAI Codex, and OpenCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"index:refresh": "node ./scripts/index/refresh-index.mjs",
|
|
54
54
|
"index:query": "node ./scripts/index/query-index.mjs",
|
|
55
55
|
"bug:triage": "node ./scripts/bug/triage.mjs",
|
|
56
|
+
"skill:audit": "node ./scripts/skill/audit-skill.mjs",
|
|
56
57
|
"test:artifact": "vitest run tests/integration/packageArtifact.test.js",
|
|
57
58
|
"test:release-core": "vitest run --exclude tests/integration/packageArtifact.test.js",
|
|
58
59
|
"release:verify": "node ./scripts/release/verify-release.mjs",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { initSkillAudit, recordAuditEntry, statusSkillAudit } from '../../src/skill/auditSkill.js';
|
|
3
|
+
|
|
4
|
+
const [command, skillName] = process.argv.slice(2);
|
|
5
|
+
const rootDir = process.cwd();
|
|
6
|
+
|
|
7
|
+
if (!command || !skillName) {
|
|
8
|
+
console.error('Usage: yarn skill:audit <init|status|record> <skill-name>');
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
} else if (command === 'init') {
|
|
11
|
+
const result = await initSkillAudit({ rootDir, skillName });
|
|
12
|
+
console.log(`[skill:audit init] ${skillName}`);
|
|
13
|
+
console.log(`dir: ${result.auditDir}`);
|
|
14
|
+
console.log(`copied: ${result.copied.join(', ') || 'none'}`);
|
|
15
|
+
console.log(`skipped (already present): ${result.skipped.join(', ') || 'none'}`);
|
|
16
|
+
} else if (command === 'status') {
|
|
17
|
+
const result = await statusSkillAudit({ rootDir, skillName });
|
|
18
|
+
console.log(`[skill:audit status] ${skillName}`);
|
|
19
|
+
console.log(`entries: ${result.entryCount}`);
|
|
20
|
+
console.log(`latest: ${result.latest ? JSON.stringify(result.latest) : 'none'}`);
|
|
21
|
+
console.log(`open rationalizations: ${result.openRationalizations.length}`);
|
|
22
|
+
} else if (command === 'record') {
|
|
23
|
+
const stdinRaw = await readStdin();
|
|
24
|
+
const entry = JSON.parse(stdinRaw);
|
|
25
|
+
const record = await recordAuditEntry({ rootDir, skillName, entry });
|
|
26
|
+
console.log(`[skill:audit record] ${skillName}`);
|
|
27
|
+
console.log(JSON.stringify(record, null, 2));
|
|
28
|
+
} else {
|
|
29
|
+
console.error(`Unknown command: ${command}. Usage: yarn skill:audit <init|status|record> <skill-name>`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readStdin() {
|
|
34
|
+
const chunks = [];
|
|
35
|
+
for await (const chunk of process.stdin) {
|
|
36
|
+
chunks.push(chunk);
|
|
37
|
+
}
|
|
38
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
39
|
+
}
|
|
@@ -7,9 +7,10 @@ import { inspectRuntimeConfig } from '../../core/runtimeConfig.js';
|
|
|
7
7
|
import { loadManifest } from '../../manifest/loadManifest.js';
|
|
8
8
|
import { detectStack } from '../../stack/detectStack.js';
|
|
9
9
|
import { detectProviders } from '../../context/detectProviders.js';
|
|
10
|
+
import { profileSkills } from '../../core/skillProfile.js';
|
|
10
11
|
|
|
11
12
|
export const DOCTOR_HELP_FLAGS = new Set(['--help', '-h']);
|
|
12
|
-
const KNOWN_FLAGS = new Set([...DOCTOR_HELP_FLAGS]);
|
|
13
|
+
const KNOWN_FLAGS = new Set([...DOCTOR_HELP_FLAGS, '--skills']);
|
|
13
14
|
|
|
14
15
|
export function printDoctorHelp() {
|
|
15
16
|
console.log('Usage: ukit doctor [options]');
|
|
@@ -18,12 +19,13 @@ export function printDoctorHelp() {
|
|
|
18
19
|
console.log('');
|
|
19
20
|
console.log('Options:');
|
|
20
21
|
console.log(' --help, -h Show this help message');
|
|
22
|
+
console.log(' --skills Also print a skill word-count/budget report');
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
|
|
24
26
|
const unknownFlags = argv.filter((flag) => !KNOWN_FLAGS.has(flag));
|
|
25
27
|
if (unknownFlags.length > 0) {
|
|
26
|
-
throw new Error(`Unknown option: ${unknownFlags[0]}. Supported: --help, -h`);
|
|
28
|
+
throw new Error(`Unknown option: ${unknownFlags[0]}. Supported: --help, -h, --skills`);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
if (argv.some((flag) => DOCTOR_HELP_FLAGS.has(flag))) {
|
|
@@ -129,6 +131,24 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
|
|
|
129
131
|
console.log(`[UKit] Runtime config issues: ${runtimeConfigInspection.errors.join(' | ')}`);
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
if (argv.includes('--skills')) {
|
|
135
|
+
const skillsDir = path.join(pathConfig.templatesRoot, '.claude', 'skills');
|
|
136
|
+
const skillReport = await profileSkills(skillsDir);
|
|
137
|
+
const overBudgetCount = skillReport.filter((s) => s.status === 'over-budget').length;
|
|
138
|
+
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log('[UKit] Skill word-count report:');
|
|
141
|
+
console.log('[UKit] Skill | Words | Status');
|
|
142
|
+
for (const skill of skillReport) {
|
|
143
|
+
console.log(`[UKit] ${skill.name} | ${skill.wordCount} | ${skill.status}`);
|
|
144
|
+
}
|
|
145
|
+
console.log(
|
|
146
|
+
overBudgetCount > 0
|
|
147
|
+
? `[UKit] ${overBudgetCount} skill(s) over budget — consider moving detail to a companion reference file.`
|
|
148
|
+
: '[UKit] All skills within budget.',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
const allPassed = Object.values(checks).every(Boolean);
|
|
133
153
|
if (!allPassed) {
|
|
134
154
|
console.log('[UKit] Some checks failed. Run `ukit install` to fix missing files.');
|
|
@@ -2,15 +2,39 @@ import {
|
|
|
2
2
|
exportMemory,
|
|
3
3
|
forgetMemoryItem,
|
|
4
4
|
listMemoryItems,
|
|
5
|
+
listPendingPatternCandidates,
|
|
6
|
+
resolvePatternCandidate,
|
|
7
|
+
runProjectHygiene,
|
|
5
8
|
} from '../../core/memory/store.js';
|
|
6
9
|
import { getContextInjection, search } from '../../core/memory/retrieval.js';
|
|
7
10
|
import { inspectRuntimeConfig } from '../../core/runtimeConfig.js';
|
|
8
11
|
import { buildRuntimePaths } from '../../core/runtimePaths.js';
|
|
9
12
|
import { pathExists } from '../../core/fileOps.js';
|
|
10
13
|
import { detectProjectContext } from '../../context/detectProjectContext.js';
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
11
15
|
|
|
12
16
|
const HELP_FLAGS = new Set(['--help', '-h', 'help']);
|
|
13
17
|
|
|
18
|
+
async function listAllPendingPatternCandidates(projectRoot, runtimePaths) {
|
|
19
|
+
let entries = [];
|
|
20
|
+
try {
|
|
21
|
+
entries = await fs.readdir(runtimePaths.projectsDir, { withFileTypes: true });
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
29
|
+
const projectId = entry.name.replace(/\.json$/, '');
|
|
30
|
+
const pending = await listPendingPatternCandidates(projectRoot, projectId);
|
|
31
|
+
for (const candidate of pending) {
|
|
32
|
+
results.push({ ...candidate, projectId });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
14
38
|
export async function runMemory({ projectRoot, argv = [] }) {
|
|
15
39
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
16
40
|
if (!(await pathExists(runtimePaths.runtimeRoot))) {
|
|
@@ -26,6 +50,19 @@ export async function runMemory({ projectRoot, argv = [] }) {
|
|
|
26
50
|
}
|
|
27
51
|
|
|
28
52
|
if (subcommand === 'list') {
|
|
53
|
+
if (rest.includes('--pending')) {
|
|
54
|
+
const pending = await listAllPendingPatternCandidates(projectRoot, runtimePaths);
|
|
55
|
+
if (pending.length === 0) {
|
|
56
|
+
console.log('[UKit] No pending pattern candidates.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const candidate of pending) {
|
|
61
|
+
console.log(`${candidate.id} [${candidate.projectId}] — ${candidate.text}`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
29
66
|
const items = await listMemoryItems(projectRoot);
|
|
30
67
|
if (items.length === 0) {
|
|
31
68
|
console.log('[UKit] No memory items found.');
|
|
@@ -38,6 +75,40 @@ export async function runMemory({ projectRoot, argv = [] }) {
|
|
|
38
75
|
return;
|
|
39
76
|
}
|
|
40
77
|
|
|
78
|
+
if (subcommand === 'approve' || subcommand === 'reject') {
|
|
79
|
+
const projectFlagIndex = rest.indexOf('--project');
|
|
80
|
+
const explicitProjectId = projectFlagIndex >= 0 ? rest[projectFlagIndex + 1] : null;
|
|
81
|
+
const candidateArgs = projectFlagIndex >= 0
|
|
82
|
+
? [...rest.slice(0, projectFlagIndex), ...rest.slice(projectFlagIndex + 2)]
|
|
83
|
+
: rest;
|
|
84
|
+
const candidateId = candidateArgs.join(' ').trim();
|
|
85
|
+
if (!candidateId) {
|
|
86
|
+
throw new Error(`Missing candidate id. Usage: ukit memory ${subcommand} <candidateId> [--project <id>]`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const projectId = explicitProjectId ?? (await detectProjectContext(projectRoot)).project.name;
|
|
90
|
+
const resolved = await resolvePatternCandidate(
|
|
91
|
+
projectRoot,
|
|
92
|
+
projectId,
|
|
93
|
+
candidateId,
|
|
94
|
+
subcommand === 'approve' ? 'approve' : 'reject',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
console.log(`[UKit] ${resolved.status === 'approved' ? 'Approved' : 'Rejected'} ${candidateId}.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (subcommand === 'hygiene') {
|
|
102
|
+
const projectFlagIndex = rest.indexOf('--project');
|
|
103
|
+
const explicitProjectId = projectFlagIndex >= 0 ? rest[projectFlagIndex + 1] : null;
|
|
104
|
+
const projectId = explicitProjectId ?? (await detectProjectContext(projectRoot)).project.name;
|
|
105
|
+
|
|
106
|
+
const result = await runProjectHygiene(projectRoot, projectId);
|
|
107
|
+
console.log(`[UKit] Ran hygiene for project ${projectId}.`);
|
|
108
|
+
console.log(`[UKit] conventions=${result.conventions.length}, decisions=${result.decisions.length}, sessions=${result.sessions.length}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
41
112
|
if (subcommand === 'search') {
|
|
42
113
|
const query = rest.join(' ').trim();
|
|
43
114
|
if (!query) {
|
|
@@ -115,12 +186,16 @@ export async function runMemory({ projectRoot, argv = [] }) {
|
|
|
115
186
|
|
|
116
187
|
export function printMemoryHelp() {
|
|
117
188
|
console.log('UKit Memory Commands');
|
|
118
|
-
console.log('Usage: ukit memory <list|search|recall|forget|export> [args]');
|
|
189
|
+
console.log('Usage: ukit memory <list|search|recall|forget|export|approve|reject|hygiene> [args]');
|
|
119
190
|
console.log('');
|
|
120
191
|
console.log('Subcommands:');
|
|
121
192
|
console.log(' list List memory items in shared .ukit/storage/memory');
|
|
193
|
+
console.log(' list --pending List pending pattern candidates awaiting approval');
|
|
122
194
|
console.log(' search <query> Search memory summaries and content');
|
|
123
195
|
console.log(' recall <task> Print a compact previous-context block for the current task');
|
|
124
196
|
console.log(' forget <id> Remove one memory item by id');
|
|
125
197
|
console.log(' export Print all memory as JSON');
|
|
198
|
+
console.log(' approve <id> [--project <id>] Approve a pending pattern candidate into project conventions');
|
|
199
|
+
console.log(' reject <id> [--project <id>] Reject a pending pattern candidate');
|
|
200
|
+
console.log(' hygiene [--project <id>] Run decision-conflict resolution + session archiving now');
|
|
126
201
|
}
|
package/src/core/memory/store.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
4
5
|
import { readJsonIfExists, writeJson } from '../fileOps.js';
|
|
6
|
+
import { loadRuntimeConfig } from '../runtimeConfig.js';
|
|
7
|
+
import { runHygiene } from './hygiene.js';
|
|
5
8
|
|
|
6
9
|
function defaultUserMemory() {
|
|
7
10
|
return {
|
|
@@ -47,8 +50,9 @@ function summarizeUserMemory(userMemory) {
|
|
|
47
50
|
|
|
48
51
|
function summarizeProjectMemory(projectMemory) {
|
|
49
52
|
const decisionCount = Array.isArray(projectMemory.decisions) ? projectMemory.decisions.length : 0;
|
|
53
|
+
const conventionCount = Array.isArray(projectMemory.conventions) ? projectMemory.conventions.length : 0;
|
|
50
54
|
const techStack = Array.isArray(projectMemory.techStack) ? projectMemory.techStack.join(', ') : '';
|
|
51
|
-
return `${projectMemory.name ?? projectMemory.id ?? 'project'} — ${decisionCount} decisions${techStack ? ` — ${techStack}` : ''}`;
|
|
55
|
+
return `${projectMemory.name ?? projectMemory.id ?? 'project'} — ${decisionCount} decisions, ${conventionCount} conventions${techStack ? ` — ${techStack}` : ''}`;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
function summarizeSessionMemory(sessionMemory) {
|
|
@@ -170,6 +174,126 @@ export async function forgetMemoryItem(projectRoot, memoryId) {
|
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
function normalizePatternText(text) {
|
|
178
|
+
return String(text ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sanitizeProjectId(projectId) {
|
|
182
|
+
return String(projectId).replace(/[\\/]/g, '-');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function projectMemoryPath(runtimePaths, projectId) {
|
|
186
|
+
return path.join(runtimePaths.projectsDir, `${sanitizeProjectId(projectId)}.json`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function readProjectMemoryForId(runtimePaths, projectId) {
|
|
190
|
+
const filePath = projectMemoryPath(runtimePaths, projectId);
|
|
191
|
+
const existing = await readJsonIfExists(filePath);
|
|
192
|
+
const memory = {
|
|
193
|
+
id: projectId,
|
|
194
|
+
conventions: [],
|
|
195
|
+
patternCandidates: [],
|
|
196
|
+
...existing,
|
|
197
|
+
};
|
|
198
|
+
return { filePath, memory };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function appendSessionArchive(runtimePaths, projectId, archivedSessions) {
|
|
202
|
+
if (!archivedSessions || archivedSessions.length === 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const archivePath = path.join(runtimePaths.projectsDir, `${sanitizeProjectId(projectId)}.archive.json`);
|
|
207
|
+
const existing = (await readJsonIfExists(archivePath)) ?? { sessions: [] };
|
|
208
|
+
const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
|
|
209
|
+
await writeJson(archivePath, { sessions: [...sessions, ...archivedSessions] });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function persistProjectMemoryWithHygiene(projectRoot, runtimePaths, projectId, filePath, memory) {
|
|
213
|
+
const runtimeConfig = await loadRuntimeConfig(projectRoot);
|
|
214
|
+
const memoryConfig = runtimeConfig.memory ?? {};
|
|
215
|
+
const hygieneConfig = {
|
|
216
|
+
archiveAfterDays: memoryConfig.archiveAfterDays,
|
|
217
|
+
maxSessionsKept: memoryConfig.maxSessions,
|
|
218
|
+
...(memoryConfig.redactSecrets === false ? { redactPatterns: [] } : {}),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const { projectMemory: hygienicMemory, archivedSessions } = runHygiene(memory, hygieneConfig);
|
|
222
|
+
await appendSessionArchive(runtimePaths, projectId, archivedSessions);
|
|
223
|
+
await writeJson(filePath, hygienicMemory);
|
|
224
|
+
return hygienicMemory;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function runProjectHygiene(projectRoot, projectId) {
|
|
228
|
+
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
229
|
+
const { filePath, memory } = await readProjectMemoryForId(runtimePaths, projectId);
|
|
230
|
+
return persistProjectMemoryWithHygiene(projectRoot, runtimePaths, projectId, filePath, memory);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function proposePatternCandidate(projectRoot, projectId, candidate) {
|
|
234
|
+
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
235
|
+
const { filePath, memory } = await readProjectMemoryForId(runtimePaths, projectId);
|
|
236
|
+
|
|
237
|
+
const normalizedText = normalizePatternText(candidate?.text);
|
|
238
|
+
if (!normalizedText) {
|
|
239
|
+
throw new Error('proposePatternCandidate requires candidate.text');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const patternCandidates = Array.isArray(memory.patternCandidates) ? memory.patternCandidates : [];
|
|
243
|
+
const existingPending = patternCandidates.find(
|
|
244
|
+
(entry) => entry.status === 'pending' && normalizePatternText(entry.text) === normalizedText,
|
|
245
|
+
);
|
|
246
|
+
if (existingPending) {
|
|
247
|
+
return existingPending;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const entry = {
|
|
251
|
+
id: `pc_${crypto.randomBytes(6).toString('hex')}`,
|
|
252
|
+
text: candidate.text,
|
|
253
|
+
category: candidate?.category ?? null,
|
|
254
|
+
detectedFrom: candidate?.detectedFrom ?? null,
|
|
255
|
+
detectedAt: Date.now(),
|
|
256
|
+
status: 'pending',
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
memory.patternCandidates = [...patternCandidates, entry];
|
|
260
|
+
await persistProjectMemoryWithHygiene(projectRoot, runtimePaths, projectId, filePath, memory);
|
|
261
|
+
return entry;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function listPendingPatternCandidates(projectRoot, projectId) {
|
|
265
|
+
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
266
|
+
const { memory } = await readProjectMemoryForId(runtimePaths, projectId);
|
|
267
|
+
const patternCandidates = Array.isArray(memory.patternCandidates) ? memory.patternCandidates : [];
|
|
268
|
+
return patternCandidates.filter((entry) => entry.status === 'pending');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function resolvePatternCandidate(projectRoot, projectId, candidateId, decision) {
|
|
272
|
+
if (decision !== 'approve' && decision !== 'reject') {
|
|
273
|
+
throw new Error(`Unknown decision: ${decision}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
277
|
+
const { filePath, memory } = await readProjectMemoryForId(runtimePaths, projectId);
|
|
278
|
+
|
|
279
|
+
const patternCandidates = Array.isArray(memory.patternCandidates) ? memory.patternCandidates : [];
|
|
280
|
+
const target = patternCandidates.find((entry) => entry.id === candidateId);
|
|
281
|
+
if (!target) {
|
|
282
|
+
throw new Error(`Pattern candidate not found: ${candidateId}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
target.status = decision === 'approve' ? 'approved' : 'rejected';
|
|
286
|
+
if (decision === 'approve') {
|
|
287
|
+
const conventions = Array.isArray(memory.conventions) ? memory.conventions : [];
|
|
288
|
+
if (!conventions.includes(target.text)) {
|
|
289
|
+
memory.conventions = [...conventions, target.text];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await persistProjectMemoryWithHygiene(projectRoot, runtimePaths, projectId, filePath, memory);
|
|
294
|
+
return target;
|
|
295
|
+
}
|
|
296
|
+
|
|
173
297
|
export async function countMemoryItems(projectRoot) {
|
|
174
298
|
const exported = await exportMemory(projectRoot);
|
|
175
299
|
return {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_WORD_THRESHOLD = 500;
|
|
5
|
+
|
|
6
|
+
function stripFrontmatter(content) {
|
|
7
|
+
if (!content.startsWith('---')) return content;
|
|
8
|
+
const end = content.indexOf('\n---', 3);
|
|
9
|
+
if (end === -1) return content;
|
|
10
|
+
return content.slice(end + 4);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function countWords(text) {
|
|
14
|
+
const trimmed = text.trim();
|
|
15
|
+
return trimmed ? trimmed.split(/\s+/).length : 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function profileSkills(skillsDir, { threshold = DEFAULT_WORD_THRESHOLD } = {}) {
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory()) continue;
|
|
29
|
+
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
30
|
+
let content;
|
|
31
|
+
try {
|
|
32
|
+
content = await fs.readFile(skillFile, 'utf8');
|
|
33
|
+
} catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const wordCount = countWords(stripFrontmatter(content));
|
|
37
|
+
results.push({
|
|
38
|
+
name: entry.name,
|
|
39
|
+
wordCount,
|
|
40
|
+
status: wordCount > threshold ? 'over-budget' : 'ok',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return results.sort((a, b) => b.wordCount - a.wordCount);
|
|
45
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const TEMPLATE_FILES = [
|
|
5
|
+
'pressure-scenario-template.md',
|
|
6
|
+
'rationalization-table-template.md',
|
|
7
|
+
'trigger-accuracy-template.md',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export function getAuditDir(rootDir, skillName) {
|
|
11
|
+
return path.join(rootDir, 'docs', 'skill-audits', skillName);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getHistoryPath(rootDir, skillName) {
|
|
15
|
+
return path.join(getAuditDir(rootDir, skillName), 'history.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function initSkillAudit({ rootDir = process.cwd(), skillName } = {}) {
|
|
19
|
+
if (!skillName) {
|
|
20
|
+
throw new Error('skillName is required. Usage: yarn skill:audit init <skill-name>');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const auditDir = getAuditDir(rootDir, skillName);
|
|
24
|
+
await fs.mkdir(auditDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
const templateSourceDir = path.join(rootDir, 'templates/.claude/skills/skill-quality');
|
|
27
|
+
const copied = [];
|
|
28
|
+
const skipped = [];
|
|
29
|
+
|
|
30
|
+
for (const fileName of TEMPLATE_FILES) {
|
|
31
|
+
const destPath = path.join(auditDir, fileName);
|
|
32
|
+
const alreadyExists = await fs.access(destPath).then(() => true).catch(() => false);
|
|
33
|
+
if (alreadyExists) {
|
|
34
|
+
skipped.push(fileName);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const sourcePath = path.join(templateSourceDir, fileName);
|
|
38
|
+
await fs.copyFile(sourcePath, destPath);
|
|
39
|
+
copied.push(fileName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { auditDir, copied, skipped };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function recordAuditEntry({ rootDir = process.cwd(), skillName, entry } = {}) {
|
|
46
|
+
if (!skillName) {
|
|
47
|
+
throw new Error('skillName is required. Usage: yarn skill:audit record <skill-name>');
|
|
48
|
+
}
|
|
49
|
+
if (!entry || !entry.phase) {
|
|
50
|
+
throw new Error('entry with a "phase" field (RED|GREEN|REFACTOR) is required');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const auditDir = getAuditDir(rootDir, skillName);
|
|
54
|
+
await fs.mkdir(auditDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const historyPath = getHistoryPath(rootDir, skillName);
|
|
57
|
+
const history = await readHistory(historyPath);
|
|
58
|
+
|
|
59
|
+
const record = {
|
|
60
|
+
date: entry.date ?? new Date().toISOString().slice(0, 10),
|
|
61
|
+
phase: entry.phase,
|
|
62
|
+
complianceRate: entry.complianceRate ?? null,
|
|
63
|
+
rationalizationsFound: entry.rationalizationsFound ?? [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
history.push(record);
|
|
67
|
+
await fs.writeFile(historyPath, `${JSON.stringify(history, null, 2)}\n`, 'utf8');
|
|
68
|
+
|
|
69
|
+
return record;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function statusSkillAudit({ rootDir = process.cwd(), skillName } = {}) {
|
|
73
|
+
if (!skillName) {
|
|
74
|
+
throw new Error('skillName is required. Usage: yarn skill:audit status <skill-name>');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const historyPath = getHistoryPath(rootDir, skillName);
|
|
78
|
+
const history = await readHistory(historyPath);
|
|
79
|
+
const latest = history.length > 0 ? history[history.length - 1] : null;
|
|
80
|
+
|
|
81
|
+
const openRationalizations = history
|
|
82
|
+
.flatMap((record) => record.rationalizationsFound ?? [])
|
|
83
|
+
.filter((item) => !(typeof item === 'object' && item.closed === true));
|
|
84
|
+
|
|
85
|
+
return { latest, openRationalizations, entryCount: history.length };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readHistory(historyPath) {
|
|
89
|
+
const raw = await fs.readFile(historyPath, 'utf8').catch(() => null);
|
|
90
|
+
if (!raw) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|