@ngockhoale/ukit 1.6.8 → 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.
Files changed (58) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/manifests/platform.full.yaml +24 -0
  3. package/package.json +2 -1
  4. package/scripts/skill/audit-skill.mjs +39 -0
  5. package/src/cli/commands/doctor.js +22 -2
  6. package/src/cli/commands/memory.js +76 -1
  7. package/src/core/memory/store.js +125 -1
  8. package/src/core/skillProfile.js +45 -0
  9. package/src/skill/auditSkill.js +99 -0
  10. package/templates/.claude/agents/code-reviewer.md +51 -7
  11. package/templates/.claude/agents/handoff-planner.md +18 -2
  12. package/templates/.claude/skills/canvas-design/SKILL.md +2 -20
  13. package/templates/.claude/skills/canvas-design/philosophy-examples.md +23 -0
  14. package/templates/.claude/skills/debugging-toolkit/SKILL.md +2 -30
  15. package/templates/.claude/skills/debugging-toolkit/reference-tables.md +33 -0
  16. package/templates/.claude/skills/docs-manager/SKILL.md +7 -249
  17. package/templates/.claude/skills/docs-manager/conventions-and-examples.md +221 -0
  18. package/templates/.claude/skills/docx/SKILL.md +3 -34
  19. package/templates/.claude/skills/docx/redlining-reference.md +34 -0
  20. package/templates/.claude/skills/duraone/SKILL.md +12 -16
  21. package/templates/.claude/skills/executing-plans/SKILL.md +31 -19
  22. package/templates/.claude/skills/file-organizer/SKILL.md +2 -170
  23. package/templates/.claude/skills/file-organizer/examples-and-practices.md +173 -0
  24. package/templates/.claude/skills/pdf/SKILL.md +1 -62
  25. package/templates/.claude/skills/pdf/reference.md +65 -0
  26. package/templates/.claude/skills/pdf-processing-pro/SKILL.md +2 -73
  27. package/templates/.claude/skills/pdf-processing-pro/workflows-and-troubleshooting.md +80 -0
  28. package/templates/.claude/skills/pptx/SKILL.md +14 -286
  29. package/templates/.claude/skills/pptx/design-references.md +81 -0
  30. package/templates/.claude/skills/pptx/template-replacement-reference.md +150 -0
  31. package/templates/.claude/skills/pptx/utilities.md +62 -0
  32. package/templates/.claude/skills/project-learning/SKILL.md +32 -0
  33. package/templates/.claude/skills/root-cause-tracing/SKILL.md +2 -35
  34. package/templates/.claude/skills/root-cause-tracing/diagrams.md +44 -0
  35. package/templates/.claude/skills/sharing-skills/SKILL.md +1 -41
  36. package/templates/.claude/skills/sharing-skills/complete-example.md +41 -0
  37. package/templates/.claude/skills/skill-quality/SKILL.md +37 -0
  38. package/templates/.claude/skills/skill-quality/pressure-scenario-template.md +20 -0
  39. package/templates/.claude/skills/skill-quality/rationalization-table-template.md +15 -0
  40. package/templates/.claude/skills/skill-quality/trigger-accuracy-template.md +32 -0
  41. package/templates/.claude/skills/sql-optimization-patterns/SKILL.md +13 -440
  42. package/templates/.claude/skills/sql-optimization-patterns/references/advanced-techniques.md +128 -0
  43. package/templates/.claude/skills/sql-optimization-patterns/references/core-concepts.md +112 -0
  44. package/templates/.claude/skills/sql-optimization-patterns/references/query-patterns.md +204 -0
  45. package/templates/.claude/skills/subagent-driven-development/SKILL.md +4 -51
  46. package/templates/.claude/skills/subagent-driven-development/example-workflow.md +40 -0
  47. package/templates/.claude/skills/systematic-debugging/SKILL.md +2 -28
  48. package/templates/.claude/skills/systematic-debugging/reference-tables.md +33 -0
  49. package/templates/.claude/skills/test-driven-development/SKILL.md +2 -51
  50. package/templates/.claude/skills/test-driven-development/reference-tables.md +56 -0
  51. package/templates/.claude/skills/testing-anti-patterns/SKILL.md +1 -10
  52. package/templates/.claude/skills/testing-anti-patterns/reference-tables.md +14 -0
  53. package/templates/.claude/skills/verification-before-completion/SKILL.md +1 -31
  54. package/templates/.claude/skills/verification-before-completion/key-patterns.md +33 -0
  55. package/templates/CLAUDE.md +4 -0
  56. package/src/core/memory/index.js +0 -2
  57. package/src/core/router/index.js +0 -2
  58. package/src/core/validation/index.js +0 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
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
+
5
25
  ## 1.6.8 - 2026-08-09
6
26
 
7
27
  ### 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": "1.6.8",
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
  }
@@ -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
+ }