@ngockhoale/ukit 2.6.6 → 2.6.8
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 +37 -0
- package/README.md +40 -177
- package/manifests/documentation.yaml +143 -15
- package/manifests/hostCapabilities.yaml +49 -0
- package/manifests/instructionRules.yaml +383 -0
- package/manifests/platform.full.yaml +15 -0
- package/package.json +3 -1
- package/scripts/bench/goldTasks.json +38 -0
- package/scripts/bench/runGold.mjs +220 -0
- package/scripts/docs/render-instructions.mjs +42 -0
- package/scripts/release/verify-release.mjs +6 -0
- package/src/cli/commands/code.js +182 -0
- package/src/cli/commands/doctor.js +35 -3
- package/src/cli/commands/indexTools.js +102 -1
- package/src/cli/commands/memory.js +137 -0
- package/src/cli/index.js +7 -0
- package/src/core/codeintel/compiler.js +316 -0
- package/src/core/codeintel/diagnostics.js +114 -0
- package/src/core/codeintel/freshness.js +295 -0
- package/src/core/codeintel/impact.js +251 -0
- package/src/core/codeintel/invalidation.js +150 -0
- package/src/core/codeintel/manifest.js +176 -0
- package/src/core/codeintel/packet.js +146 -0
- package/src/core/codeintel/providers.js +201 -0
- package/src/core/codeintel/retriever.js +372 -0
- package/src/core/codeintel/router.js +149 -0
- package/src/core/codeintel/semanticProvider.js +235 -0
- package/src/core/docContracts.js +723 -0
- package/src/core/memory/migrate.js +324 -0
- package/src/core/memory/records.js +172 -0
- package/src/core/memory/retrieval.js +161 -11
- package/src/core/memory/store.js +398 -0
- package/src/core/memory/storeV2.js +171 -0
- package/src/core/memory/storeV2Loader.js +22 -0
- package/src/core/projectImportant.js +1 -1
- package/src/core/runtimeConfig.js +125 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/uninstall.js +1 -1
- package/src/index/taskRouting.js +39 -0
- package/src/render/instructionRenderer.js +226 -0
- package/templates/.claude/ukit/index/route-task.mjs +40 -0
- package/templates/.gitignore +2 -2
- package/templates/.omp/RULES.md +1 -0
- package/templates/AGENTS.md +89 -218
- package/templates/CLAUDE.md +85 -212
- package/templates/docs/AI_HANDOFF/tasks/_TEMPLATE.md +5 -0
- package/templates/docs/BUGFIX.md +2 -19
- package/templates/docs/BUG_INDEX.md +43 -0
- package/templates/docs/BUG_METRICS.md +1 -5
- package/templates/docs/BUG_TEMPLATE.md +1 -11
- package/templates/docs/UKIT_INTERNALS.md +223 -0
- package/templates/instructions/core.md +157 -0
- package/templates/instructions/layout.yaml +149 -0
- package/templates/instructions/overlays/agents.md +15 -0
- package/templates/instructions/overlays/claude.md +3 -0
- package/templates/instructions/overlays/omp-rules.md +74 -0
- package/templates/instructions/overlays/repo.md +9 -0
- package/templates/instructions/repo-vars.yaml +23 -0
- package/templates/ukit/storage/config.json +30 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Memory v2 store — SPEC §3.
|
|
2
|
+
// Single atomic JSON document at <memoryRoot>/v2/records.json
|
|
3
|
+
// ({ schemaVersion: 2, records: [...] }); writes via fileOps.writeJson (tmp+rename).
|
|
4
|
+
// Read ops call ensureMigrated() first so every consumer triggers the lazy
|
|
5
|
+
// v1→v2 migration (SPEC §4); migrate.js is lazy-imported to break the
|
|
6
|
+
// storeV2↔migrate import cycle (runMigration writes via saveRecords).
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
10
|
+
import { readJsonIfExists, writeJson } from '../fileOps.js';
|
|
11
|
+
import { loadRuntimeConfig } from '../runtimeConfig.js';
|
|
12
|
+
import { createRecord, normalizeRecord } from './records.js';
|
|
13
|
+
|
|
14
|
+
const SCHEMA_VERSION = 2;
|
|
15
|
+
|
|
16
|
+
// Per-process memoization for ensureMigrated — one-shot check per projectRoot.
|
|
17
|
+
const migratedRoots = new Set();
|
|
18
|
+
|
|
19
|
+
async function ensureMigrated(projectRoot) {
|
|
20
|
+
const key = String(projectRoot);
|
|
21
|
+
if (migratedRoots.has(key)) return;
|
|
22
|
+
migratedRoots.add(key); // mark first: a failed/again migration must not loop
|
|
23
|
+
try {
|
|
24
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
25
|
+
let recordsExists = false;
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(paths.memoryV2RecordsPath);
|
|
28
|
+
recordsExists = true;
|
|
29
|
+
} catch {
|
|
30
|
+
recordsExists = false;
|
|
31
|
+
}
|
|
32
|
+
if (recordsExists) return;
|
|
33
|
+
|
|
34
|
+
const config = await loadRuntimeConfig(projectRoot);
|
|
35
|
+
if (config?.memoryV2?.autoMigrate === false) return;
|
|
36
|
+
|
|
37
|
+
const migrate = await import('./migrate.js');
|
|
38
|
+
if (await migrate.needsMigration(projectRoot)) {
|
|
39
|
+
await migrate.runMigration(projectRoot);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Lazy migration is best-effort: a failed auto-run must not break reads.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRawRecord(input) {
|
|
47
|
+
return input && typeof input === 'object' && typeof input.id === 'string'
|
|
48
|
+
&& typeof input.status === 'string';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* loadRecords(projectRoot) → record[] — tolerant: invalid entries skipped.
|
|
53
|
+
*/
|
|
54
|
+
export async function loadRecords(projectRoot) {
|
|
55
|
+
await ensureMigrated(projectRoot);
|
|
56
|
+
const { records } = await readStore(projectRoot);
|
|
57
|
+
return records;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readStore(projectRoot) {
|
|
61
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
62
|
+
let doc;
|
|
63
|
+
try {
|
|
64
|
+
doc = await readJsonIfExists(paths.memoryV2RecordsPath);
|
|
65
|
+
} catch {
|
|
66
|
+
return { records: [], invalidSkipped: 1 };
|
|
67
|
+
}
|
|
68
|
+
if (!doc) return { records: [], invalidSkipped: 0 };
|
|
69
|
+
|
|
70
|
+
const rawRecords = Array.isArray(doc.records) ? doc.records : [];
|
|
71
|
+
const records = [];
|
|
72
|
+
let invalidSkipped = Array.isArray(doc.records) ? 0 : 1;
|
|
73
|
+
for (const raw of rawRecords) {
|
|
74
|
+
const normalized = normalizeRecord(raw);
|
|
75
|
+
if (normalized) records.push(normalized);
|
|
76
|
+
else invalidSkipped += 1;
|
|
77
|
+
}
|
|
78
|
+
return { records, invalidSkipped };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* saveRecords(projectRoot, records) → void — atomic write; drops unrecoverable entries.
|
|
83
|
+
*/
|
|
84
|
+
export async function saveRecords(projectRoot, records) {
|
|
85
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
86
|
+
const normalized = (Array.isArray(records) ? records : [])
|
|
87
|
+
.map((raw) => normalizeRecord(raw))
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
await writeJson(paths.memoryV2RecordsPath, {
|
|
90
|
+
schemaVersion: SCHEMA_VERSION,
|
|
91
|
+
records: normalized,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* addRecord(projectRoot, recordInput) → record
|
|
97
|
+
* Accepts either createRecord-style input or a full raw record (normalized).
|
|
98
|
+
*/
|
|
99
|
+
export async function addRecord(projectRoot, recordInput) {
|
|
100
|
+
const record = isRawRecord(recordInput)
|
|
101
|
+
? normalizeRecord(recordInput)
|
|
102
|
+
: createRecord(recordInput);
|
|
103
|
+
if (!record) {
|
|
104
|
+
throw new Error('addRecord: input could not be normalized into a valid record');
|
|
105
|
+
}
|
|
106
|
+
const { records } = await readStore(projectRoot);
|
|
107
|
+
await saveRecords(projectRoot, [...records, record]);
|
|
108
|
+
return record;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PATCHABLE_FIELDS = ['type', 'scope', 'text', 'provenance', 'confidence',
|
|
112
|
+
'valid_until', 'status', 'project_id', 'meta', 'source_fingerprint'];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* updateRecord(projectRoot, id, patch) → record | null
|
|
116
|
+
*/
|
|
117
|
+
export async function updateRecord(projectRoot, id, patch = {}) {
|
|
118
|
+
await ensureMigrated(projectRoot);
|
|
119
|
+
const { records } = await readStore(projectRoot);
|
|
120
|
+
const index = records.findIndex((r) => r.id === id);
|
|
121
|
+
if (index === -1) return null;
|
|
122
|
+
|
|
123
|
+
const next = { ...records[index] };
|
|
124
|
+
for (const field of PATCHABLE_FIELDS) {
|
|
125
|
+
if (Object.prototype.hasOwnProperty.call(patch, field)) {
|
|
126
|
+
next[field] = patch[field];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const normalized = normalizeRecord(next);
|
|
130
|
+
if (!normalized) {
|
|
131
|
+
throw new Error(`updateRecord: patch produces an invalid record (id ${id})`);
|
|
132
|
+
}
|
|
133
|
+
records[index] = normalized;
|
|
134
|
+
await saveRecords(projectRoot, records);
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* getRecord(projectRoot, id) → record | null
|
|
140
|
+
*/
|
|
141
|
+
export async function getRecord(projectRoot, id) {
|
|
142
|
+
await ensureMigrated(projectRoot);
|
|
143
|
+
const { records } = await readStore(projectRoot);
|
|
144
|
+
return records.find((r) => r.id === id) ?? null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* queryRecords(projectRoot, { type?, scope?, status?, projectId? }) → record[]
|
|
149
|
+
*/
|
|
150
|
+
export async function queryRecords(projectRoot, { type, scope, status, projectId } = {}) {
|
|
151
|
+
const records = await loadRecords(projectRoot);
|
|
152
|
+
return records.filter((r) => (type == null || r.type === type)
|
|
153
|
+
&& (scope == null || r.scope === scope)
|
|
154
|
+
&& (status == null || r.status === status)
|
|
155
|
+
&& (projectId == null || r.project_id === projectId));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* stats(projectRoot) → { total, byType, byStatus, invalidSkipped }
|
|
160
|
+
*/
|
|
161
|
+
export async function stats(projectRoot) {
|
|
162
|
+
await ensureMigrated(projectRoot);
|
|
163
|
+
const { records, invalidSkipped } = await readStore(projectRoot);
|
|
164
|
+
const byType = {};
|
|
165
|
+
const byStatus = {};
|
|
166
|
+
for (const r of records) {
|
|
167
|
+
byType[r.type] = (byType[r.type] ?? 0) + 1;
|
|
168
|
+
byStatus[r.status] = (byStatus[r.status] ?? 0) + 1;
|
|
169
|
+
}
|
|
170
|
+
return { total: records.length, byType, byStatus, invalidSkipped };
|
|
171
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Lazy/defensive bridge to storeV2.js (owned by TASK-002).
|
|
2
|
+
// Real storeV2.queryRecords performs ensureMigrated auto-migration internally —
|
|
3
|
+
// consumers must never call migration themselves; just call this and treat an
|
|
4
|
+
// empty result as "fall back to the legacy memory path".
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* loadV2Records(projectRoot) → record[] | []
|
|
8
|
+
* Returns [] when storeV2 is unavailable, disabled upstream, or the v2 store
|
|
9
|
+
* holds no records yet. Callers decide fallback.
|
|
10
|
+
*/
|
|
11
|
+
export async function loadV2Records(projectRoot, filter = {}) {
|
|
12
|
+
try {
|
|
13
|
+
const storeV2 = await import('./storeV2.js');
|
|
14
|
+
if (typeof storeV2?.queryRecords !== 'function') {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const records = await storeV2.queryRecords(projectRoot, filter);
|
|
18
|
+
return Array.isArray(records) ? records : [];
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* projectImportant.js (TASK-037)
|
|
3
3
|
*
|
|
4
4
|
* Source-side inspector + deterministic envelope renderer for
|
|
5
|
-
* PROJECT_IMPORTANT.md per docs/PROJECT_IMPORTANT_SPEC.md §3, §4, §5.2, §6,
|
|
5
|
+
* PROJECT_IMPORTANT.md per docs/archive/specs/PROJECT_IMPORTANT_SPEC.md §3, §4, §5.2, §6,
|
|
6
6
|
* §12.1.
|
|
7
7
|
*
|
|
8
8
|
* Guarantees:
|
|
@@ -157,6 +157,36 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
157
157
|
cap: 'smart',
|
|
158
158
|
},
|
|
159
159
|
},
|
|
160
|
+
codeIntel: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
router: { enabled: true, defaultMode: 'auto' },
|
|
163
|
+
budgets: {
|
|
164
|
+
peek: 500,
|
|
165
|
+
targeted: 2000,
|
|
166
|
+
impact: 3000,
|
|
167
|
+
explore: 4000,
|
|
168
|
+
deep_flow: 6000,
|
|
169
|
+
analogy: 4000,
|
|
170
|
+
},
|
|
171
|
+
freshness: { guardLevel: 'L0', autoRefresh: false, incremental: true },
|
|
172
|
+
retriever: {
|
|
173
|
+
limit: 20,
|
|
174
|
+
bm25: { k1: 1.2, b: 0.75 },
|
|
175
|
+
merge: 'rrf',
|
|
176
|
+
rrfK: 60,
|
|
177
|
+
weights: { exact: 1.0, symbol: 1.2, bm25: 0.8, semantic: 1.0 },
|
|
178
|
+
},
|
|
179
|
+
impact: { defaultDepth: 2, maxDepth: 4, maxNodes: 200 },
|
|
180
|
+
diagnostics: { enabled: true, timeoutMs: 8000 },
|
|
181
|
+
providers: { semantic: 'null' },
|
|
182
|
+
},
|
|
183
|
+
memoryV2: {
|
|
184
|
+
enabled: true,
|
|
185
|
+
autoMigrate: true,
|
|
186
|
+
episodeTtlDays: 90,
|
|
187
|
+
promotion: { episodeToRuleRequiresApproval: true },
|
|
188
|
+
recall: { maxRecords: 8 },
|
|
189
|
+
},
|
|
160
190
|
memory: {
|
|
161
191
|
enabled: true,
|
|
162
192
|
autoCapture: true,
|
|
@@ -354,6 +384,101 @@ export function validateRuntimeConfig(config) {
|
|
|
354
384
|
}
|
|
355
385
|
}
|
|
356
386
|
|
|
387
|
+
if (!isPlainObject(config.codeIntel)) {
|
|
388
|
+
errors.push('codeIntel must be an object.');
|
|
389
|
+
} else {
|
|
390
|
+
const codeIntel = config.codeIntel;
|
|
391
|
+
pushBooleanError(errors, codeIntel.enabled, 'codeIntel.enabled');
|
|
392
|
+
if (!isPlainObject(codeIntel.router)) {
|
|
393
|
+
errors.push('codeIntel.router must be an object.');
|
|
394
|
+
} else {
|
|
395
|
+
pushBooleanError(errors, codeIntel.router.enabled, 'codeIntel.router.enabled');
|
|
396
|
+
const VALID_ROUTER_MODES = new Set(['auto', 'none', 'peek', 'targeted', 'explore', 'impact', 'deep_flow', 'analogy']);
|
|
397
|
+
if (!VALID_ROUTER_MODES.has(codeIntel.router.defaultMode)) {
|
|
398
|
+
errors.push(`codeIntel.router.defaultMode must be one of: ${[...VALID_ROUTER_MODES].join(', ')}.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!isPlainObject(codeIntel.budgets)) {
|
|
402
|
+
errors.push('codeIntel.budgets must be an object.');
|
|
403
|
+
} else {
|
|
404
|
+
for (const key of ['peek', 'targeted', 'impact', 'explore', 'deep_flow', 'analogy']) {
|
|
405
|
+
pushPositiveNumberError(errors, codeIntel.budgets[key], `codeIntel.budgets.${key}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (!isPlainObject(codeIntel.freshness)) {
|
|
409
|
+
errors.push('codeIntel.freshness must be an object.');
|
|
410
|
+
} else {
|
|
411
|
+
const VALID_GUARD_LEVELS = new Set(['L0', 'L1', 'L2', 'L3', 'L4']);
|
|
412
|
+
if (!VALID_GUARD_LEVELS.has(codeIntel.freshness.guardLevel)) {
|
|
413
|
+
errors.push(`codeIntel.freshness.guardLevel must be one of: ${[...VALID_GUARD_LEVELS].join(', ')}.`);
|
|
414
|
+
}
|
|
415
|
+
pushBooleanError(errors, codeIntel.freshness.autoRefresh, 'codeIntel.freshness.autoRefresh');
|
|
416
|
+
pushBooleanError(errors, codeIntel.freshness.incremental, 'codeIntel.freshness.incremental');
|
|
417
|
+
}
|
|
418
|
+
if (!isPlainObject(codeIntel.retriever)) {
|
|
419
|
+
errors.push('codeIntel.retriever must be an object.');
|
|
420
|
+
} else {
|
|
421
|
+
pushPositiveNumberError(errors, codeIntel.retriever.limit, 'codeIntel.retriever.limit');
|
|
422
|
+
if (!isPlainObject(codeIntel.retriever.bm25)) {
|
|
423
|
+
errors.push('codeIntel.retriever.bm25 must be an object.');
|
|
424
|
+
} else {
|
|
425
|
+
pushPositiveNumberError(errors, codeIntel.retriever.bm25.k1, 'codeIntel.retriever.bm25.k1');
|
|
426
|
+
pushPositiveNumberError(errors, codeIntel.retriever.bm25.b, 'codeIntel.retriever.bm25.b');
|
|
427
|
+
}
|
|
428
|
+
const VALID_RETRIEVER_MERGES = new Set(['rrf', 'concat']);
|
|
429
|
+
if (!VALID_RETRIEVER_MERGES.has(codeIntel.retriever.merge)) {
|
|
430
|
+
errors.push(`codeIntel.retriever.merge must be one of: ${[...VALID_RETRIEVER_MERGES].join(', ')}.`);
|
|
431
|
+
}
|
|
432
|
+
pushPositiveNumberError(errors, codeIntel.retriever.rrfK, 'codeIntel.retriever.rrfK');
|
|
433
|
+
if (!isPlainObject(codeIntel.retriever.weights)) {
|
|
434
|
+
errors.push('codeIntel.retriever.weights must be an object.');
|
|
435
|
+
} else {
|
|
436
|
+
for (const lane of ['exact', 'symbol', 'bm25', 'semantic']) {
|
|
437
|
+
if (typeof codeIntel.retriever.weights[lane] !== 'number' || Number.isNaN(codeIntel.retriever.weights[lane])) {
|
|
438
|
+
errors.push(`codeIntel.retriever.weights.${lane} must be a number.`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!isPlainObject(codeIntel.impact)) {
|
|
444
|
+
errors.push('codeIntel.impact must be an object.');
|
|
445
|
+
} else {
|
|
446
|
+
pushPositiveNumberError(errors, codeIntel.impact.defaultDepth, 'codeIntel.impact.defaultDepth');
|
|
447
|
+
pushPositiveNumberError(errors, codeIntel.impact.maxDepth, 'codeIntel.impact.maxDepth');
|
|
448
|
+
pushPositiveNumberError(errors, codeIntel.impact.maxNodes, 'codeIntel.impact.maxNodes');
|
|
449
|
+
}
|
|
450
|
+
if (!isPlainObject(codeIntel.diagnostics)) {
|
|
451
|
+
errors.push('codeIntel.diagnostics must be an object.');
|
|
452
|
+
} else {
|
|
453
|
+
pushBooleanError(errors, codeIntel.diagnostics.enabled, 'codeIntel.diagnostics.enabled');
|
|
454
|
+
pushPositiveNumberError(errors, codeIntel.diagnostics.timeoutMs, 'codeIntel.diagnostics.timeoutMs');
|
|
455
|
+
}
|
|
456
|
+
if (!isPlainObject(codeIntel.providers)) {
|
|
457
|
+
errors.push('codeIntel.providers must be an object.');
|
|
458
|
+
} else {
|
|
459
|
+
pushNonEmptyStringError(errors, codeIntel.providers.semantic, 'codeIntel.providers.semantic');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!isPlainObject(config.memoryV2)) {
|
|
464
|
+
errors.push('memoryV2 must be an object.');
|
|
465
|
+
} else {
|
|
466
|
+
const memoryV2 = config.memoryV2;
|
|
467
|
+
pushBooleanError(errors, memoryV2.enabled, 'memoryV2.enabled');
|
|
468
|
+
pushBooleanError(errors, memoryV2.autoMigrate, 'memoryV2.autoMigrate');
|
|
469
|
+
pushPositiveNumberError(errors, memoryV2.episodeTtlDays, 'memoryV2.episodeTtlDays');
|
|
470
|
+
if (!isPlainObject(memoryV2.promotion)) {
|
|
471
|
+
errors.push('memoryV2.promotion must be an object.');
|
|
472
|
+
} else {
|
|
473
|
+
pushBooleanError(errors, memoryV2.promotion.episodeToRuleRequiresApproval, 'memoryV2.promotion.episodeToRuleRequiresApproval');
|
|
474
|
+
}
|
|
475
|
+
if (!isPlainObject(memoryV2.recall)) {
|
|
476
|
+
errors.push('memoryV2.recall must be an object.');
|
|
477
|
+
} else {
|
|
478
|
+
pushPositiveNumberError(errors, memoryV2.recall.maxRecords, 'memoryV2.recall.maxRecords');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
357
482
|
if (!isPlainObject(config.memory)) {
|
|
358
483
|
errors.push('memory must be an object.');
|
|
359
484
|
} else {
|
package/src/core/runtimePaths.js
CHANGED
|
@@ -4,6 +4,7 @@ export function buildRuntimePaths(projectRoot) {
|
|
|
4
4
|
const runtimeRoot = path.join(projectRoot, '.ukit');
|
|
5
5
|
const storageRoot = path.join(runtimeRoot, 'storage');
|
|
6
6
|
const memoryRoot = path.join(storageRoot, 'memory');
|
|
7
|
+
const memoryV2Dir = path.join(memoryRoot, 'v2');
|
|
7
8
|
const cacheRoot = path.join(storageRoot, 'cache');
|
|
8
9
|
|
|
9
10
|
return {
|
|
@@ -11,6 +12,8 @@ export function buildRuntimePaths(projectRoot) {
|
|
|
11
12
|
storageRoot,
|
|
12
13
|
cacheRoot,
|
|
13
14
|
memoryRoot,
|
|
15
|
+
memoryV2Dir,
|
|
16
|
+
memoryV2RecordsPath: path.join(memoryV2Dir, 'records.json'),
|
|
14
17
|
teeCacheDir: path.join(cacheRoot, 'tee'),
|
|
15
18
|
configPath: path.join(storageRoot, 'config.json'),
|
|
16
19
|
promptCachePath: path.join(cacheRoot, 'prompt-cache.json'),
|
package/src/core/uninstall.js
CHANGED
|
@@ -16,7 +16,7 @@ import { PROJECT_IMPORTANT_FILENAME } from './projectImportant.js';
|
|
|
16
16
|
// tracked/followed. Before any destructive uninstall step a regular source gets a
|
|
17
17
|
// raw-byte backup beside it at the project root (never under .claude/.ukit/.codex/
|
|
18
18
|
// .omp — those may be deleted): `<name>.ukit-backup`, then `.ukit-backup.1`, `.2`, …
|
|
19
|
-
// via bounded exclusive-create collision walk. See docs/PROJECT_IMPORTANT_SPEC.md §15.
|
|
19
|
+
// via bounded exclusive-create collision walk. See docs/archive/specs/PROJECT_IMPORTANT_SPEC.md §15.
|
|
20
20
|
const IMPORTANT_BACKUP_SUFFIX = '.ukit-backup';
|
|
21
21
|
const IMPORTANT_BACKUP_COLLISION_LIMIT = 100;
|
|
22
22
|
|
package/src/index/taskRouting.js
CHANGED
|
@@ -14,6 +14,34 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const MAX_ACTIVE_ROUTE_SKILLS = 2;
|
|
16
16
|
|
|
17
|
+
// Declared complexity→docs mapping (DOC-201 FR-001). Canonical declaration lives in
|
|
18
|
+
// manifests/documentation.yaml `context_layers`; this constant is the routing-side copy
|
|
19
|
+
// (the router must not read the registry at runtime — zero new deps). Keep in sync.
|
|
20
|
+
// `queued-task` is omitted v1: docs/TASKS.md does not exist in every repo (SPEC §14).
|
|
21
|
+
// `shared-simple` mirrors the non-trivial layer to match deriveContextMode's FULL lane.
|
|
22
|
+
const CONTEXT_LAYER_DOCS = {
|
|
23
|
+
taskTypes: {
|
|
24
|
+
trivial: [],
|
|
25
|
+
simple: ['docs/MEMORY.md'],
|
|
26
|
+
'non-trivial': ['docs/MEMORY.md', 'docs/PROJECT.md', 'docs/CODE_MAP.md'],
|
|
27
|
+
'shared-simple': ['docs/MEMORY.md', 'docs/PROJECT.md', 'docs/CODE_MAP.md'],
|
|
28
|
+
},
|
|
29
|
+
intents: {
|
|
30
|
+
'open-ended': ['docs/STATUS.md'],
|
|
31
|
+
'open-ended-status': ['docs/STATUS.md'],
|
|
32
|
+
handoff: ['docs/AI_HANDOFF/INDEX.md'],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const CONTEXT_DOCS_MAX = 4;
|
|
36
|
+
|
|
37
|
+
function deriveContextDocs({ taskType = null, intentMode = null } = {}) {
|
|
38
|
+
const docs = [
|
|
39
|
+
...(CONTEXT_LAYER_DOCS.taskTypes[taskType] ?? []),
|
|
40
|
+
...(CONTEXT_LAYER_DOCS.intents[intentMode] ?? []),
|
|
41
|
+
];
|
|
42
|
+
return unique(docs).slice(0, CONTEXT_DOCS_MAX);
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
export async function deriveTaskRoute({
|
|
18
46
|
rootDir = process.cwd(),
|
|
19
47
|
promptText = '',
|
|
@@ -206,6 +234,7 @@ export function buildRouteSummary({
|
|
|
206
234
|
nextAction = null,
|
|
207
235
|
handoffBudget = null,
|
|
208
236
|
worklogBudget = null,
|
|
237
|
+
contextDocs = null,
|
|
209
238
|
} = {}) {
|
|
210
239
|
const autonomyLevel = routingContext.autonomyLevel ?? 'balanced';
|
|
211
240
|
const delegationRecommendation = deriveDelegationRecommendation({
|
|
@@ -264,10 +293,19 @@ export function buildRouteSummary({
|
|
|
264
293
|
);
|
|
265
294
|
const nextActionCommand = compactHelperLane ? null : nextAction?.command ?? null;
|
|
266
295
|
const handoffFile = routingContext.intentMode === 'handoff' ? 'docs/AI_HANDOFF/ACTIVE.md' : null;
|
|
296
|
+
// DOC-201 FR-002: resolved context-layer docs (declared in manifests/documentation.yaml).
|
|
297
|
+
// Explicit override wins; otherwise derive from taskType + intentMode.
|
|
298
|
+
const resolvedContextDocs = ((Array.isArray(contextDocs) ? contextDocs : null)
|
|
299
|
+
?? deriveContextDocs({ taskType, intentMode: routingContext.intentMode ?? null }))
|
|
300
|
+
.slice(0, CONTEXT_DOCS_MAX);
|
|
301
|
+
const docsSegment = resolvedContextDocs.length > 0
|
|
302
|
+
? `docs=[${resolvedContextDocs.slice(0, CONTEXT_DOCS_MAX).map((p) => path.posix.basename(String(p).replaceAll('\\', '/'))).join(',')}]`
|
|
303
|
+
: null;
|
|
267
304
|
const summaryLine = [
|
|
268
305
|
routingContext.taskType ? `task=${routingContext.taskType}` : null,
|
|
269
306
|
handoffFile ? `handoff=${handoffFile}` : null,
|
|
270
307
|
formatCompactSegment('targets', primaryTargets),
|
|
308
|
+
docsSegment,
|
|
271
309
|
formatCompactSegment('tests', relatedTests),
|
|
272
310
|
formatCompactSegment('styles', styleFiles),
|
|
273
311
|
editGuardHint ? `editGuard=${editGuardHint}` : null,
|
|
@@ -303,6 +341,7 @@ export function buildRouteSummary({
|
|
|
303
341
|
nextActionCommand,
|
|
304
342
|
helperHint,
|
|
305
343
|
contextMode,
|
|
344
|
+
contextDocs: resolvedContextDocs,
|
|
306
345
|
line: summaryLine || 'task=unknown',
|
|
307
346
|
};
|
|
308
347
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
|
|
5
|
+
import { renderTemplateString } from './renderTemplate.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deterministic core/overlay renderer for the five instruction-contract
|
|
9
|
+
* outputs (SPEC §8.1, FR-004..FR-007). Pure functions; disk access only in
|
|
10
|
+
* renderInstructionsFromDisk / checkRenderedInstructions.
|
|
11
|
+
*
|
|
12
|
+
* Emission rules (byte-exact):
|
|
13
|
+
* h1 + '\n' + banner + '\n\n' + blocks.join('') [+ trailer] [+ '\n']
|
|
14
|
+
* where each `## ` block is `## <heading>` + the raw body slice — the body
|
|
15
|
+
* keeps its leading AND trailing whitespace, so concatenation reproduces
|
|
16
|
+
* the source bytes exactly; the reserved
|
|
17
|
+
* `__preamble__` block is the source's leading non-## text verbatim.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const PREAMBLE_KEY = '__preamble__';
|
|
21
|
+
|
|
22
|
+
const SECTION_HEADING_RE = /^## (.+)$/gm;
|
|
23
|
+
const LEFTOVER_TOKEN_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Split markdown into a Map<heading, body> at `## ` boundaries.
|
|
27
|
+
* `### ` and deeper stay inside the enclosing body. Leading non-## text lands
|
|
28
|
+
* under the reserved `__preamble__` key (absent when the file starts with `## `).
|
|
29
|
+
* Throws on a duplicate `## ` heading, naming it.
|
|
30
|
+
*/
|
|
31
|
+
export function parseSections(markdown) {
|
|
32
|
+
const sections = new Map();
|
|
33
|
+
const matches = [...markdown.matchAll(SECTION_HEADING_RE)];
|
|
34
|
+
const first = matches[0];
|
|
35
|
+
|
|
36
|
+
const preamble = first ? markdown.slice(0, first.index) : markdown;
|
|
37
|
+
if (preamble.trim().length > 0) {
|
|
38
|
+
// Raw slice kept verbatim: its trailing newline(s) are the separator
|
|
39
|
+
// before the first ## section (e.g. the RULES.md preamble block).
|
|
40
|
+
sections.set(PREAMBLE_KEY, preamble);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < matches.length; i++) {
|
|
44
|
+
const m = matches[i];
|
|
45
|
+
const heading = m[1].trimEnd();
|
|
46
|
+
if (sections.has(heading)) {
|
|
47
|
+
throw new Error(`duplicate ## heading in source: ${heading}`);
|
|
48
|
+
}
|
|
49
|
+
const bodyStart = m.index + m[0].length;
|
|
50
|
+
const bodyEnd = i + 1 < matches.length ? matches[i + 1].index : markdown.length;
|
|
51
|
+
// Raw slice up to (not including) the next `## ` heading — leading AND
|
|
52
|
+
// trailing whitespace are byte-meaningful (marker line vs blank line
|
|
53
|
+
// under the heading; multi-blank separators between sections).
|
|
54
|
+
sections.set(heading, markdown.slice(bodyStart, bodyEnd));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sections;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Render one layout output. `outputSpec` = layout.outputs[i];
|
|
62
|
+
* `sources` = { <sourceName>: Map<heading, body> }; `vars` = flat/nested
|
|
63
|
+
* variables applied when `outputSpec.resolve_vars` is true.
|
|
64
|
+
* Reserved specifiers: heading '__preamble__' emits the source's leading
|
|
65
|
+
* non-## block verbatim (no `## ` prefix); heading '*' expands to every
|
|
66
|
+
* `## ` section of that source in file order.
|
|
67
|
+
*/
|
|
68
|
+
export function renderOutput(outputSpec, sources, vars = {}) {
|
|
69
|
+
if (!Array.isArray(outputSpec.sections) || outputSpec.sections.length === 0) {
|
|
70
|
+
throw new Error(`output ${outputSpec.target}: sections must be a non-empty list`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const blocks = [];
|
|
74
|
+
for (const ref of outputSpec.sections) {
|
|
75
|
+
const source = sources.get(ref.from);
|
|
76
|
+
if (!source) {
|
|
77
|
+
throw new Error(`output ${outputSpec.target}: unknown source '${ref.from}'`);
|
|
78
|
+
}
|
|
79
|
+
if (ref.heading === '*') {
|
|
80
|
+
for (const [heading, body] of source) {
|
|
81
|
+
if (heading === PREAMBLE_KEY) continue;
|
|
82
|
+
blocks.push(`## ${heading}${body}`);
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ref.heading === PREAMBLE_KEY) {
|
|
87
|
+
if (!source.has(PREAMBLE_KEY)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`output ${outputSpec.target}: source '${ref.from}' has no __preamble__ block`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
blocks.push(source.get(PREAMBLE_KEY));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!source.has(ref.heading)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`output ${outputSpec.target}: heading '${ref.heading}' missing from source '${ref.from}'`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
blocks.push(`## ${ref.heading}${source.get(ref.heading)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let out = `${outputSpec.h1}\n${outputSpec.banner}\n\n${blocks.join('')}`;
|
|
104
|
+
if (outputSpec.trailer !== undefined && outputSpec.trailer !== null && outputSpec.trailer !== '') {
|
|
105
|
+
if (!out.endsWith('\n')) out += '\n';
|
|
106
|
+
out += `${outputSpec.trailer}`;
|
|
107
|
+
}
|
|
108
|
+
if (!out.endsWith('\n')) out += '\n';
|
|
109
|
+
|
|
110
|
+
if (outputSpec.resolve_vars) {
|
|
111
|
+
out = renderTemplateString(out, vars);
|
|
112
|
+
const leftover = out.match(LEFTOVER_TOKEN_RE);
|
|
113
|
+
if (leftover) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`output ${outputSpec.target}: unresolved template variable '{{${leftover[1]}}}'`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render every output in `layout` from `sourceTexts` ({name: markdown}).
|
|
124
|
+
* Source names are looked up as `sources.get(ref.from)`; overlay sources are
|
|
125
|
+
* named `overlay:<file-stem>` by convention in renderInstructionsFromDisk.
|
|
126
|
+
* `varsByResolve` supplies variables to outputs with resolve_vars: true.
|
|
127
|
+
* FR-006 strictness: a `## ` section in any source referenced by zero outputs
|
|
128
|
+
* throws, naming the heading.
|
|
129
|
+
*/
|
|
130
|
+
export function renderAll(layout, sourceTexts, varsByResolve = {}) {
|
|
131
|
+
if (!layout || !Array.isArray(layout.outputs) || layout.outputs.length === 0) {
|
|
132
|
+
throw new Error('layout.outputs must be a non-empty list');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sources = new Map(
|
|
136
|
+
Object.entries(sourceTexts).map(([name, text]) => [name, parseSections(text)]),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Referenced-heading accounting for the unreferenced-section check.
|
|
140
|
+
const referenced = new Map(); // sourceName -> Set<heading>
|
|
141
|
+
const mark = (from, heading) => {
|
|
142
|
+
if (!referenced.has(from)) referenced.set(from, new Set());
|
|
143
|
+
referenced.get(from).add(heading);
|
|
144
|
+
};
|
|
145
|
+
for (const spec of layout.outputs) {
|
|
146
|
+
for (const ref of spec.sections || []) {
|
|
147
|
+
if (ref.heading === '*') {
|
|
148
|
+
const source = sources.get(ref.from);
|
|
149
|
+
if (!source) throw new Error(`unknown source '${ref.from}' referenced by '*'`);
|
|
150
|
+
for (const heading of source.keys()) {
|
|
151
|
+
if (heading !== PREAMBLE_KEY) mark(ref.from, heading);
|
|
152
|
+
}
|
|
153
|
+
} else if (ref.heading !== PREAMBLE_KEY) {
|
|
154
|
+
mark(ref.from, ref.heading);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const [name, source] of sources) {
|
|
159
|
+
for (const heading of source.keys()) {
|
|
160
|
+
if (heading === PREAMBLE_KEY) continue;
|
|
161
|
+
if (!referenced.get(name) || !referenced.get(name).has(heading)) {
|
|
162
|
+
throw new Error(`source '${name}': ## section '${heading}' referenced by zero outputs`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const rendered = new Map();
|
|
168
|
+
for (const spec of layout.outputs) {
|
|
169
|
+
const vars = spec.resolve_vars ? varsByResolve : {};
|
|
170
|
+
rendered.set(spec.target, renderOutput(spec, sources, vars));
|
|
171
|
+
}
|
|
172
|
+
return rendered;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const SOURCE_FILES = {
|
|
176
|
+
core: 'templates/instructions/core.md',
|
|
177
|
+
'overlay:claude': 'templates/instructions/overlays/claude.md',
|
|
178
|
+
'overlay:agents': 'templates/instructions/overlays/agents.md',
|
|
179
|
+
'overlay:omp-rules': 'templates/instructions/overlays/omp-rules.md',
|
|
180
|
+
'overlay:repo': 'templates/instructions/overlays/repo.md',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Read layout + sources + repo-vars + package.json version from disk, render
|
|
185
|
+
* all outputs, and (write=true) write each target. Returns Map<target, content>.
|
|
186
|
+
*/
|
|
187
|
+
export async function renderInstructionsFromDisk({ repoRoot, write = true } = {}) {
|
|
188
|
+
const abs = (p) => path.join(repoRoot, p);
|
|
189
|
+
const layout = YAML.parse(
|
|
190
|
+
fs.readFileSync(abs('templates/instructions/layout.yaml'), 'utf8'),
|
|
191
|
+
);
|
|
192
|
+
const repoVars =
|
|
193
|
+
YAML.parse(fs.readFileSync(abs('templates/instructions/repo-vars.yaml'), 'utf8')) || {};
|
|
194
|
+
const pkg = JSON.parse(fs.readFileSync(abs('package.json'), 'utf8'));
|
|
195
|
+
|
|
196
|
+
const sourceTexts = {};
|
|
197
|
+
for (const [name, rel] of Object.entries(SOURCE_FILES)) {
|
|
198
|
+
sourceTexts[name] = fs.readFileSync(abs(rel), 'utf8');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const vars = { ...repoVars, ukit: { ...(repoVars.ukit || {}), version: pkg.version } };
|
|
202
|
+
const rendered = renderAll(layout, sourceTexts, vars);
|
|
203
|
+
|
|
204
|
+
if (write) {
|
|
205
|
+
for (const [target, content] of rendered) {
|
|
206
|
+
fs.writeFileSync(abs(target), content);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return rendered;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* → string[] of drifted target paths (empty = clean). Drift means rendered
|
|
214
|
+
* bytes differ from disk bytes, or the target is missing.
|
|
215
|
+
*/
|
|
216
|
+
export async function checkRenderedInstructions({ repoRoot } = {}) {
|
|
217
|
+
const rendered = await renderInstructionsFromDisk({ repoRoot, write: false });
|
|
218
|
+
const drift = [];
|
|
219
|
+
for (const [target, content] of rendered) {
|
|
220
|
+
const file = path.join(repoRoot, target);
|
|
221
|
+
if (!fs.existsSync(file) || fs.readFileSync(file, 'utf8') !== content) {
|
|
222
|
+
drift.push(target);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return drift;
|
|
226
|
+
}
|