@ngockhoale/ukit 1.6.8 → 2.0.2
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 +43 -0
- package/manifests/platform.full.yaml +47 -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/hooks/context-hardcap-gate.sh +102 -0
- package/templates/.claude/hooks/reset-compact-pressure.sh +25 -0
- package/templates/.claude/settings.json +15 -0
- 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/runtime/compact-threshold.mjs +28 -0
- package/templates/.claude/ukit/runtime/reinject-context.mjs +14 -1
- package/templates/CLAUDE.md +4 -0
- package/templates/ukit/storage/config.json +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,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.0.2 - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Absolute context hard cap** — new `compact.hardCapTokens` (default `220000`) and `compact.hardCapBlock` (default `true`). Crossing the cap makes `context-hardcap-gate.sh` refuse `Edit`/`Write`/`Bash` outright until real compaction happens, instead of only printing a suggestion. `Read`/`Grep`/`Glob` stay open so the agent can still explain the block — only a human can invoke compaction. This is a new, separate ceiling: the existing soft/hard advisory phases (`compact.tokenThreshold` 50k, derived hard 80k) are unchanged.
|
|
10
|
+
- **`reset-compact-pressure.sh`** (SessionStart) — zeroes the compact pressure tracker at session start, so the cap measures the current session rather than accumulating over a project's lifetime.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **The hard cap would have shipped dead.** Both new hooks were wired into `settings.json` but had no entry in `manifests/platform.full.yaml`, so `ukit install` never copied them: real installs got `settings.json` pointing at scripts that do not exist. Found by a scratch-project install, not by the test suite. Added a `tests/manifest/settingsHookCoverage.test.js` regression guard that fails whenever a hook is wired without a manifest entry, or a manifest hook's `sourceTemplate` is missing, or the dev mirror and template `settings.json` disagree.
|
|
15
|
+
- **Permanent-block deadlock.** `compact-pressure.json` accumulated forever, so the cap would eventually block `Edit`/`Write`/`Bash` in every future session even with tiny context — and because the gate covers `Bash`, the documented remedies could not run. Fixed by the SessionStart reset plus a PreCompact reset.
|
|
16
|
+
- **Reset skipped on cache hit.** The PreCompact reset sat after `reinject-context.mjs`'s prompt-cache early return, so a cache hit left the gate tripped permanently. Moved ahead of the return.
|
|
17
|
+
- **Gate bypass via `NotebookEdit`/`MultiEdit`.** The gate compared `tool_name` exactly while the `settings.json` matcher is a regex (`Edit|Write` also matches both), so those tools fired the hook and passed through it. Now matched against an explicit tool set.
|
|
18
|
+
- Prompt cache in `reinject-context.mjs` no longer keys on the live pressure value when the threshold plan is inactive (where it cannot affect output), which had turned every run into a cache miss.
|
|
19
|
+
- `compact.hardCapTokens` reads straight from config instead of deriving from the advisory hard threshold, so raising `tokenThreshold` can never silently raise the ceiling.
|
|
20
|
+
|
|
21
|
+
## 2.0.1 - 2026-08-10
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `ukit memory approve` / `reject` accept an optional `--project <id>` flag, so a pattern candidate belonging to a non-current project (surfaced by `list --pending`'s cross-project scan) can actually be resolved instead of failing "not found".
|
|
26
|
+
- `ukit memory list` reports a conventions count alongside decisions, so an approval's effect is visible.
|
|
27
|
+
|
|
28
|
+
## 2.0.0 - 2026-08-10
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`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`.
|
|
33
|
+
- **`executing-plans` Preflight checklist + Blocker Classifier** — structured pre-execution checks and a formal blocker taxonomy, replacing an unstructured "When to Stop" bullet list.
|
|
34
|
+
- **`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.
|
|
35
|
+
- **`RULES.md` / task template Interfaces block** — `Consumes`/`Produces` fields for explicit task-to-task data contracts in handoff cycles.
|
|
36
|
+
- **`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.
|
|
37
|
+
- **`skillProfile.js` + `ukit doctor --skills`** — word-count budget report for every `templates/.claude/skills/*/SKILL.md`, flagging files over a 500-word default threshold.
|
|
38
|
+
- **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).
|
|
39
|
+
- **`project-learning` skill** — guides proposing/confirming project conventions through the new pattern-learning CLI.
|
|
40
|
+
- Wired the previously dead `runHygiene()` (decision-superseding + session consolidation) into the live memory flow instead of leaving it uncalled.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- 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.
|
|
45
|
+
- 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.
|
|
46
|
+
- 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.
|
|
47
|
+
|
|
5
48
|
## 1.6.8 - 2026-08-09
|
|
6
49
|
|
|
7
50
|
### 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
|
|
@@ -1074,6 +1098,29 @@ items:
|
|
|
1074
1098
|
packs:
|
|
1075
1099
|
- core
|
|
1076
1100
|
|
|
1101
|
+
- id: hook-context-hardcap-gate
|
|
1102
|
+
type: hook
|
|
1103
|
+
sourceTemplate: .claude/hooks/context-hardcap-gate.sh
|
|
1104
|
+
targetPath: .claude/hooks/context-hardcap-gate.sh
|
|
1105
|
+
requires:
|
|
1106
|
+
- ukit-runtime-compact-threshold-script
|
|
1107
|
+
mergeStrategy: overwrite_with_backup
|
|
1108
|
+
variables: []
|
|
1109
|
+
enabledByDefault: true
|
|
1110
|
+
packs:
|
|
1111
|
+
- core
|
|
1112
|
+
|
|
1113
|
+
- id: hook-reset-compact-pressure
|
|
1114
|
+
type: hook
|
|
1115
|
+
sourceTemplate: .claude/hooks/reset-compact-pressure.sh
|
|
1116
|
+
targetPath: .claude/hooks/reset-compact-pressure.sh
|
|
1117
|
+
requires: []
|
|
1118
|
+
mergeStrategy: overwrite_with_backup
|
|
1119
|
+
variables: []
|
|
1120
|
+
enabledByDefault: true
|
|
1121
|
+
packs:
|
|
1122
|
+
- core
|
|
1123
|
+
|
|
1077
1124
|
- id: hook-compress-output
|
|
1078
1125
|
type: hook
|
|
1079
1126
|
sourceTemplate: .claude/hooks/compress-output.sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngockhoale/ukit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.2",
|
|
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
|
+
}
|