@kontextmind/kxm 0.7.86 → 0.7.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/docs/configuration.md +2 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +895 -498
- package/plugins/kxm/dist/extension.js +70 -61
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +44 -26
- package/plugins/kxm/dist/server.js +34 -23
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/context-skills.ts +51 -6
- package/plugins/kxm/src/cli/hub.ts +39 -14
- package/plugins/kxm/src/cli/project.ts +78 -23
- package/plugins/kxm/src/cli/roles.ts +48 -4
- package/plugins/kxm/src/cli/system.ts +18 -1
- package/plugins/kxm/src/cli/tasks.ts +34 -5
- package/plugins/kxm/src/cli/types.ts +30 -0
- package/plugins/kxm/src/cli/workflows.ts +20 -4
- package/plugins/kxm/src/cli.ts +57 -0
- package/plugins/kxm/src/config.ts +4 -3
- package/plugins/kxm/src/database.ts +72 -33
- package/plugins/kxm/src/local-snapshot.ts +4 -4
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/memory.ts +10 -7
- package/plugins/kxm/src/role.ts +18 -6
- package/plugins/kxm/src/session-work.ts +4 -1
- package/plugins/kxm/src/skills.ts +29 -15
- package/plugins/kxm/src/sqlite.ts +18 -0
- package/plugins/kxm/src/task-manager.ts +20 -13
- package/plugins/kxm/src/workflow-manager.ts +12 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "./sqlite.ts";
|
|
4
|
+
import { DatabaseSync, openReadOnlyDatabase } from "./sqlite.ts";
|
|
5
5
|
import type { AgentIdentity, AgentRecord, MessageRecord } from "./protocol.ts";
|
|
6
6
|
import type { WorkflowRun } from "./workflow.ts";
|
|
7
7
|
import { readRoutingRecords } from "./telemetry.ts";
|
|
@@ -250,7 +250,7 @@ export function loadLocalMeshSnapshot(
|
|
|
250
250
|
let plans: MeshTuiPlan[] = [];
|
|
251
251
|
if (existsSync(dataPath)) {
|
|
252
252
|
hasLegacy = true;
|
|
253
|
-
const database =
|
|
253
|
+
const database = openReadOnlyDatabase(dataPath);
|
|
254
254
|
try {
|
|
255
255
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
256
256
|
// Stored rows carry identity only. Presence is derived here against the
|
|
@@ -287,7 +287,7 @@ export function loadLocalMeshSnapshot(
|
|
|
287
287
|
if (existsSync(registryDbPath)) {
|
|
288
288
|
hasKxm = true;
|
|
289
289
|
try {
|
|
290
|
-
const regDb =
|
|
290
|
+
const regDb = openReadOnlyDatabase(registryDbPath);
|
|
291
291
|
try {
|
|
292
292
|
regDb.exec("PRAGMA busy_timeout = 5000");
|
|
293
293
|
const pRows = regDb.prepare("SELECT project_key FROM projects").all() as Array<{ project_key: string }>;
|
|
@@ -315,7 +315,7 @@ export function loadLocalMeshSnapshot(
|
|
|
315
315
|
if (existsSync(eventDbPath)) {
|
|
316
316
|
hasKxm = true;
|
|
317
317
|
try {
|
|
318
|
-
const eventDb =
|
|
318
|
+
const eventDb = openReadOnlyDatabase(eventDbPath);
|
|
319
319
|
try {
|
|
320
320
|
eventDb.exec("PRAGMA busy_timeout = 5000");
|
|
321
321
|
const runRows = eventDb.prepare(`
|
|
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
8
8
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
9
9
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.7.
|
|
11
|
+
const VERSION = "0.7.87";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -245,10 +245,10 @@ export function createMemoryNote(
|
|
|
245
245
|
body?: string | undefined;
|
|
246
246
|
sourceRef?: string | undefined;
|
|
247
247
|
runId?: string | undefined;
|
|
248
|
+
dryRun?: boolean | undefined;
|
|
248
249
|
} = {},
|
|
249
250
|
): { record: MemoryRecord; path: string } {
|
|
250
251
|
const { candidatesDir } = memoryDirectories(repoRoot);
|
|
251
|
-
mkdirSync(candidatesDir, { recursive: true });
|
|
252
252
|
|
|
253
253
|
const scope: MemoryScope = options.scope ?? "project";
|
|
254
254
|
if (!VALID_SCOPES.has(scope)) {
|
|
@@ -277,6 +277,8 @@ export function createMemoryNote(
|
|
|
277
277
|
};
|
|
278
278
|
|
|
279
279
|
const targetPath = join(candidatesDir, `${id}.md`);
|
|
280
|
+
if (options.dryRun) return { record, path: targetPath };
|
|
281
|
+
mkdirSync(candidatesDir, { recursive: true });
|
|
280
282
|
writeFileSync(targetPath, formatMemoryRecord(record), "utf8");
|
|
281
283
|
return { record, path: targetPath };
|
|
282
284
|
}
|
|
@@ -316,7 +318,8 @@ export function formatHarnessMemoryBlock(records: MemoryRecord[]): string {
|
|
|
316
318
|
return contentLines.join("\n");
|
|
317
319
|
}
|
|
318
320
|
|
|
319
|
-
|
|
321
|
+
/** Returns whether the projection block changed the file; `dryRun` answers without writing. */
|
|
322
|
+
export function updateHarnessDocument(filePath: string, block: string, defaultHeader: string, dryRun = false): boolean {
|
|
320
323
|
let original = "";
|
|
321
324
|
if (existsSync(filePath)) {
|
|
322
325
|
original = readFileSync(filePath, "utf8");
|
|
@@ -340,13 +343,13 @@ export function updateHarnessDocument(filePath: string, block: string, defaultHe
|
|
|
340
343
|
}
|
|
341
344
|
|
|
342
345
|
if (updated !== original) {
|
|
343
|
-
writeFileSync(filePath, updated, "utf8");
|
|
346
|
+
if (!dryRun) writeFileSync(filePath, updated, "utf8");
|
|
344
347
|
return true;
|
|
345
348
|
}
|
|
346
349
|
return false;
|
|
347
350
|
}
|
|
348
351
|
|
|
349
|
-
export function syncHarnessMemory(repoRoot: string): { updated: string[]; created: string[] } {
|
|
352
|
+
export function syncHarnessMemory(repoRoot: string, options: { dryRun?: boolean } = {}): { updated: string[]; created: string[] } {
|
|
350
353
|
const root = resolve(repoRoot);
|
|
351
354
|
const records = loadAuthoredMemory(root);
|
|
352
355
|
const block = formatHarnessMemoryBlock(records);
|
|
@@ -358,7 +361,7 @@ export function syncHarnessMemory(repoRoot: string): { updated: string[]; create
|
|
|
358
361
|
const agentsPath = join(root, "AGENTS.md");
|
|
359
362
|
const agentsHeader = "# AGENTS\n\nFollow project instructions.\n";
|
|
360
363
|
const agentsExisted = existsSync(agentsPath);
|
|
361
|
-
if (updateHarnessDocument(agentsPath, block, agentsHeader)) {
|
|
364
|
+
if (updateHarnessDocument(agentsPath, block, agentsHeader, options.dryRun)) {
|
|
362
365
|
if (agentsExisted) updated.push("AGENTS.md");
|
|
363
366
|
else created.push("AGENTS.md");
|
|
364
367
|
}
|
|
@@ -367,7 +370,7 @@ export function syncHarnessMemory(repoRoot: string): { updated: string[]; create
|
|
|
367
370
|
const claudePath = join(root, "CLAUDE.md");
|
|
368
371
|
const claudeHeader = `# KXM (Claude)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nYou are the **planner / architecture critic** unless the human explicitly asks\nyou to implement. Default writer is native Grok CLI (\`grok --model grok-4.6\`) — a starting\nrotation, not a sole writer. If \`grok\` is logged out, never bill Grok through another harness;\nuse a relief route Tracking **admits**, or stop and name the limits hit. Your reviews are\nartifacts, not hub \`peer-reply\` evidence.\n`;
|
|
369
372
|
const claudeExisted = existsSync(claudePath);
|
|
370
|
-
if (updateHarnessDocument(claudePath, block, claudeHeader)) {
|
|
373
|
+
if (updateHarnessDocument(claudePath, block, claudeHeader, options.dryRun)) {
|
|
371
374
|
if (claudeExisted) updated.push("CLAUDE.md");
|
|
372
375
|
else created.push("CLAUDE.md");
|
|
373
376
|
}
|
|
@@ -376,7 +379,7 @@ export function syncHarnessMemory(repoRoot: string): { updated: string[]; create
|
|
|
376
379
|
const geminiPath = join(root, "GEMINI.md");
|
|
377
380
|
const geminiHeader = `# KXM (Gemini / Antigravity)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nGoogle goes through the \`antigravity\` **Pi provider** (Tracking \u2192 Decided,\n2026-09-15); \`agy\` stays a harness catalog/helper entry, not the admission path.\nCurrent admissions come from Tracking and \`kxm harness list\`. Starting rotation\nremains Grok.\n`;
|
|
378
381
|
const geminiExisted = existsSync(geminiPath);
|
|
379
|
-
if (updateHarnessDocument(geminiPath, block, geminiHeader)) {
|
|
382
|
+
if (updateHarnessDocument(geminiPath, block, geminiHeader, options.dryRun)) {
|
|
380
383
|
if (geminiExisted) updated.push("GEMINI.md");
|
|
381
384
|
else created.push("GEMINI.md");
|
|
382
385
|
}
|
package/plugins/kxm/src/role.ts
CHANGED
|
@@ -308,18 +308,21 @@ export function addRole(
|
|
|
308
308
|
repoRoot?: string | undefined;
|
|
309
309
|
userConfigDir?: string | undefined;
|
|
310
310
|
overwrite?: boolean | undefined;
|
|
311
|
+
dryRun?: boolean | undefined;
|
|
311
312
|
} = {},
|
|
312
313
|
): { id: string; filePath: string; scope: "global" | "local" } {
|
|
313
314
|
const scope = options.scope ?? "local";
|
|
314
315
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
315
|
-
const dir =
|
|
316
|
+
const dir = options.dryRun
|
|
317
|
+
? rolesDirectory(scope, repoRoot, options.userConfigDir)
|
|
318
|
+
: ensureRolesDirectory(scope, repoRoot, options.userConfigDir);
|
|
316
319
|
const filePath = join(dir, `${role.id}.yaml`);
|
|
317
320
|
|
|
318
321
|
if (existsSync(filePath) && !options.overwrite) {
|
|
319
322
|
throw new Error(`role_already_exists: role '${role.id}' already exists at ${filePath}`);
|
|
320
323
|
}
|
|
321
324
|
|
|
322
|
-
writeFileSync(filePath, stringify(role), "utf8");
|
|
325
|
+
if (!options.dryRun) writeFileSync(filePath, stringify(role), "utf8");
|
|
323
326
|
return { id: role.id, filePath, scope };
|
|
324
327
|
}
|
|
325
328
|
|
|
@@ -329,6 +332,7 @@ export function removeRole(
|
|
|
329
332
|
scope?: "global" | "local" | undefined;
|
|
330
333
|
repoRoot?: string | undefined;
|
|
331
334
|
userConfigDir?: string | undefined;
|
|
335
|
+
dryRun?: boolean | undefined;
|
|
332
336
|
} = {},
|
|
333
337
|
): { id: string; removed: boolean; filePath: string; scope: "global" | "local" } {
|
|
334
338
|
const scope = options.scope ?? "local";
|
|
@@ -340,6 +344,7 @@ export function removeRole(
|
|
|
340
344
|
throw new Error(`role_not_found: role '${roleId}' not found in ${scope} directory (${filePath})`);
|
|
341
345
|
}
|
|
342
346
|
|
|
347
|
+
if (options.dryRun) return { id: roleId, removed: false, filePath, scope };
|
|
343
348
|
rmSync(filePath);
|
|
344
349
|
return { id: roleId, removed: true, filePath, scope };
|
|
345
350
|
}
|
|
@@ -351,6 +356,7 @@ export function modifyRole(
|
|
|
351
356
|
scope?: "global" | "local" | undefined;
|
|
352
357
|
repoRoot?: string | undefined;
|
|
353
358
|
userConfigDir?: string | undefined;
|
|
359
|
+
dryRun?: boolean | undefined;
|
|
354
360
|
} = {},
|
|
355
361
|
): { id: string; role: KxmRoleDefinition; filePath: string; scope: "global" | "local" } {
|
|
356
362
|
const target = getRole(roleId, { scope: options.scope, repoRoot: options.repoRoot, userConfigDir: options.userConfigDir });
|
|
@@ -367,10 +373,12 @@ export function modifyRole(
|
|
|
367
373
|
|
|
368
374
|
const scope = options.scope ?? target.scope;
|
|
369
375
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
370
|
-
const dir =
|
|
376
|
+
const dir = options.dryRun
|
|
377
|
+
? rolesDirectory(scope, repoRoot, options.userConfigDir)
|
|
378
|
+
: ensureRolesDirectory(scope, repoRoot, options.userConfigDir);
|
|
371
379
|
const filePath = join(dir, `${roleId}.yaml`);
|
|
372
380
|
|
|
373
|
-
writeFileSync(filePath, stringify(updated), "utf8");
|
|
381
|
+
if (!options.dryRun) writeFileSync(filePath, stringify(updated), "utf8");
|
|
374
382
|
return { id: roleId, role: updated, filePath, scope };
|
|
375
383
|
}
|
|
376
384
|
|
|
@@ -562,17 +570,19 @@ export function saveRoleHostsConfig(
|
|
|
562
570
|
repoRoot?: string | undefined;
|
|
563
571
|
userConfigDir?: string | undefined;
|
|
564
572
|
format?: "yaml" | "json" | undefined;
|
|
573
|
+
dryRun?: boolean | undefined;
|
|
565
574
|
} = {},
|
|
566
575
|
): { filePath: string; scope: "global" | "local" } {
|
|
567
576
|
const scope = options.scope ?? "local";
|
|
568
577
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
569
578
|
const dir = scope === "global" ? userConfigDirectory(options.userConfigDir) : repoConfigDirectory(repoRoot);
|
|
579
|
+
const format = options.format ?? "yaml";
|
|
580
|
+
const filePath = join(dir, format === "json" ? "role-hosts.json" : "role-hosts.yaml");
|
|
581
|
+
if (options.dryRun) return { filePath, scope };
|
|
570
582
|
if (!existsSync(dir)) {
|
|
571
583
|
mkdirSync(dir, { recursive: true });
|
|
572
584
|
}
|
|
573
585
|
|
|
574
|
-
const format = options.format ?? "yaml";
|
|
575
|
-
const filePath = join(dir, format === "json" ? "role-hosts.json" : "role-hosts.yaml");
|
|
576
586
|
const payload: RoleHostsConfig = {
|
|
577
587
|
schema: KXM_ROLE_HOSTS_SCHEMA,
|
|
578
588
|
seats: config.seats ?? {},
|
|
@@ -594,6 +604,7 @@ export function setRoleSeatHost(
|
|
|
594
604
|
repoRoot?: string | undefined;
|
|
595
605
|
userConfigDir?: string | undefined;
|
|
596
606
|
format?: "yaml" | "json" | undefined;
|
|
607
|
+
dryRun?: boolean | undefined;
|
|
597
608
|
} = {},
|
|
598
609
|
): { filePath: string; seatId: string; binding: RoleSeatBinding; scope: "global" | "local" } {
|
|
599
610
|
const scope = options.scope ?? "local";
|
|
@@ -624,6 +635,7 @@ export function setRoleSeatHost(
|
|
|
624
635
|
repoRoot: options.repoRoot,
|
|
625
636
|
userConfigDir: options.userConfigDir,
|
|
626
637
|
format: options.format,
|
|
638
|
+
dryRun: options.dryRun,
|
|
627
639
|
});
|
|
628
640
|
|
|
629
641
|
return { filePath: saved.filePath, seatId, binding, scope };
|
|
@@ -398,6 +398,8 @@ export function loadSessionBrief(
|
|
|
398
398
|
cost?: string;
|
|
399
399
|
sessionToken?: string;
|
|
400
400
|
ship?: SessionShipStatus;
|
|
401
|
+
/** false answers from disk and the hub without refreshing session-brief.json. */
|
|
402
|
+
writeCache?: boolean;
|
|
401
403
|
} = {},
|
|
402
404
|
): SessionBrief {
|
|
403
405
|
const paths = resolveKxmSnapshotPaths(cwd, env);
|
|
@@ -464,7 +466,7 @@ export function loadSessionBrief(
|
|
|
464
466
|
);
|
|
465
467
|
}
|
|
466
468
|
|
|
467
|
-
writeCachedSessionBrief(paths.stateDir, brief);
|
|
469
|
+
if (options.writeCache !== false) writeCachedSessionBrief(paths.stateDir, brief);
|
|
468
470
|
return brief;
|
|
469
471
|
}
|
|
470
472
|
|
|
@@ -479,6 +481,7 @@ export async function loadSessionBriefAsync(
|
|
|
479
481
|
cost?: string;
|
|
480
482
|
sessionToken?: string;
|
|
481
483
|
ship?: SessionShipStatus;
|
|
484
|
+
writeCache?: boolean;
|
|
482
485
|
} = {},
|
|
483
486
|
): Promise<SessionBrief> {
|
|
484
487
|
const paths = resolveKxmSnapshotPaths(cwd, env);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { redactSecrets } from "./redact.ts";
|
|
6
6
|
|
|
@@ -190,17 +190,32 @@ export interface SkillLifecycleOptions {
|
|
|
190
190
|
* evals. When unset, `optimization` evaluations are rejected. */
|
|
191
191
|
allowOptimizationEvals?: boolean;
|
|
192
192
|
now?: () => string;
|
|
193
|
+
/** Validate and record every change in `planned` without touching disk. */
|
|
194
|
+
dryRun?: boolean;
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
export class SkillLifecycle {
|
|
196
198
|
private readonly root: string;
|
|
197
199
|
private readonly now: () => string;
|
|
198
200
|
private readonly allowOptimizationEvals: boolean;
|
|
201
|
+
private readonly dryRun: boolean;
|
|
202
|
+
/** What a `dryRun` lifecycle would have written or moved, in order. */
|
|
203
|
+
readonly planned: Array<{ action: "write" | "move"; target: string }> = [];
|
|
199
204
|
|
|
200
205
|
constructor(root: string, options: SkillLifecycleOptions = {}) {
|
|
201
206
|
this.root = root;
|
|
202
207
|
this.now = options.now ?? (() => new Date().toISOString());
|
|
203
208
|
this.allowOptimizationEvals = options.allowOptimizationEvals === true;
|
|
209
|
+
this.dryRun = options.dryRun === true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private write(file: string, content: string): void {
|
|
213
|
+
if (this.dryRun) {
|
|
214
|
+
this.planned.push({ action: "write", target: file });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
218
|
+
writeFileSync(file, content);
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
private dir(state: SkillState): string {
|
|
@@ -217,15 +232,14 @@ export class SkillLifecycle {
|
|
|
217
232
|
}
|
|
218
233
|
|
|
219
234
|
private appendHistory(id: string, record: unknown): void {
|
|
220
|
-
mkdirSync(join(this.root, "history"), { recursive: true });
|
|
221
235
|
const line = `${JSON.stringify(record)}\n`;
|
|
222
236
|
if (existsSync(this.historyFile(id))) {
|
|
223
237
|
// Bound the history file: append within the audit limit.
|
|
224
238
|
const existing = readFileSync(this.historyFile(id), "utf8");
|
|
225
239
|
const lines = existing.split("\n").filter((entry) => entry.trim());
|
|
226
|
-
|
|
240
|
+
this.write(this.historyFile(id), [...lines.slice(-499), line.trim()].join("\n") + "\n");
|
|
227
241
|
} else {
|
|
228
|
-
|
|
242
|
+
this.write(this.historyFile(id), line);
|
|
229
243
|
}
|
|
230
244
|
}
|
|
231
245
|
|
|
@@ -252,6 +266,10 @@ export class SkillLifecycle {
|
|
|
252
266
|
if (!existsSync(fromDir)) {
|
|
253
267
|
throw new SkillLifecycleError("skill_not_found", `skill ${id} not found in ${from}`);
|
|
254
268
|
}
|
|
269
|
+
if (this.dryRun) {
|
|
270
|
+
this.planned.push({ action: "move", target: `${fromDir} -> ${toDir}` });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
255
273
|
mkdirSync(this.dir(to), { recursive: true });
|
|
256
274
|
if (existsSync(toDir)) rmSync(toDir, { recursive: true, force: true });
|
|
257
275
|
renameSync(fromDir, toDir);
|
|
@@ -299,7 +317,7 @@ export class SkillLifecycle {
|
|
|
299
317
|
|
|
300
318
|
const contentSha256 = skillContentSha256(content);
|
|
301
319
|
const id = skillIdFor(name, contentSha256);
|
|
302
|
-
const {
|
|
320
|
+
const { metadata, skill } = this.paths("candidate", id);
|
|
303
321
|
if (existsSync(metadata)) {
|
|
304
322
|
throw new SkillLifecycleError(
|
|
305
323
|
"skill_candidate_exists",
|
|
@@ -319,9 +337,8 @@ export class SkillLifecycle {
|
|
|
319
337
|
createdAt: this.now(),
|
|
320
338
|
...(input.supersedes ? { supersedes: input.supersedes } : {}),
|
|
321
339
|
};
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
writeFileSync(metadata, `${JSON.stringify(record, null, 2)}\n`);
|
|
340
|
+
this.write(skill, content);
|
|
341
|
+
this.write(metadata, `${JSON.stringify(record, null, 2)}\n`);
|
|
325
342
|
this.appendHistory(id, { schema: "kxm.skill-history-event.v1", event: "candidate_created", by: createdBy, supersedes: input.supersedes, at: record.createdAt });
|
|
326
343
|
return record;
|
|
327
344
|
}
|
|
@@ -429,20 +446,17 @@ export class SkillLifecycle {
|
|
|
429
446
|
// Instead of moving directory, write promoted directory and generate patch
|
|
430
447
|
const candidatePaths = this.paths("candidate", candidateId);
|
|
431
448
|
const promotedPaths = this.paths("promoted", candidateId);
|
|
432
|
-
mkdirSync(promotedPaths.dir, { recursive: true });
|
|
433
449
|
|
|
434
450
|
const skillContent = readFileSync(candidatePaths.skill, "utf8");
|
|
435
451
|
const metadataContent = readFileSync(candidatePaths.metadata, "utf8");
|
|
436
|
-
|
|
437
|
-
|
|
452
|
+
this.write(promotedPaths.skill, skillContent);
|
|
453
|
+
this.write(promotedPaths.metadata, metadataContent);
|
|
438
454
|
|
|
439
|
-
const
|
|
440
|
-
mkdirSync(patchesDir, { recursive: true });
|
|
441
|
-
const patchPath = join(patchesDir, `${candidateId}.patch`);
|
|
455
|
+
const patchPath = join(this.root, "patches", `${candidateId}.patch`);
|
|
442
456
|
const relSkillPath = `.kxm/skills/promoted/${candidateId}/SKILL.md`;
|
|
443
457
|
const relMetaPath = `.kxm/skills/promoted/${candidateId}/metadata.json`;
|
|
444
458
|
const patch = `${createUnifiedPatch(relSkillPath, skillContent)}${createUnifiedPatch(relMetaPath, metadataContent)}`;
|
|
445
|
-
|
|
459
|
+
this.write(patchPath, patch);
|
|
446
460
|
|
|
447
461
|
this.appendHistory(candidateId, record);
|
|
448
462
|
return { ...metadata, patch, patchPath };
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* flag `readOnly`, Bun spells it `readonly`. Options are normalized here so
|
|
9
9
|
* callers keep using the Node spelling everywhere.
|
|
10
10
|
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
11
12
|
import { createRequire } from "node:module";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
12
15
|
|
|
13
16
|
export interface DatabaseSyncOptions {
|
|
14
17
|
readOnly?: boolean;
|
|
@@ -74,3 +77,18 @@ export class DatabaseSync {
|
|
|
74
77
|
this.inner.close();
|
|
75
78
|
}
|
|
76
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Open a database for reading without touching its directory. A plain
|
|
83
|
+
* read-only open of a WAL database creates the `-wal` and `-shm` sidecars when
|
|
84
|
+
* they are missing and leaves them behind. With no `-wal` present, the main
|
|
85
|
+
* file already holds every committed page, so it is opened `immutable` (no
|
|
86
|
+
* locks, no sidecars); a present `-wal` means a writer or un-checkpointed
|
|
87
|
+
* frames, and the ordinary read-only open reads through them.
|
|
88
|
+
*/
|
|
89
|
+
export function openReadOnlyDatabase(path: string): DatabaseSync {
|
|
90
|
+
if (existsSync(`${path}-wal`)) return new DatabaseSync(path, { readOnly: true });
|
|
91
|
+
const uri = pathToFileURL(resolve(path));
|
|
92
|
+
uri.searchParams.set("immutable", "1");
|
|
93
|
+
return new DatabaseSync(uri.href, { readOnly: true });
|
|
94
|
+
}
|
|
@@ -58,6 +58,14 @@ function tasksDirectory(repoRoot: string): string {
|
|
|
58
58
|
return resolve(repoRoot, ".kxm", "tasks");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function goalFilePath(repoRoot: string, goalId: string): string {
|
|
62
|
+
return join(goalsDirectory(repoRoot), `${goalId}.yaml`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function taskFilePath(repoRoot: string, taskId: string): string {
|
|
66
|
+
return join(tasksDirectory(repoRoot), `${taskId}.yaml`);
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
export function createGoal(
|
|
62
70
|
repoRoot: string,
|
|
63
71
|
input: {
|
|
@@ -66,9 +74,8 @@ export function createGoal(
|
|
|
66
74
|
successMetrics?: string[] | undefined;
|
|
67
75
|
targetDate?: string | undefined;
|
|
68
76
|
},
|
|
77
|
+
options: { dryRun?: boolean | undefined } = {},
|
|
69
78
|
): GoalRecord {
|
|
70
|
-
const dir = goalsDirectory(repoRoot);
|
|
71
|
-
mkdirSync(dir, { recursive: true });
|
|
72
79
|
|
|
73
80
|
const id = `goal_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
74
81
|
const now = new Date().toISOString();
|
|
@@ -84,8 +91,9 @@ export function createGoal(
|
|
|
84
91
|
updatedAt: now,
|
|
85
92
|
};
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
if (options.dryRun) return record;
|
|
95
|
+
mkdirSync(goalsDirectory(repoRoot), { recursive: true });
|
|
96
|
+
writeFileSync(goalFilePath(repoRoot, id), stringify(record).trim() + "\n", "utf8");
|
|
89
97
|
return record;
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -123,9 +131,8 @@ export function createTask(
|
|
|
123
131
|
issueKey: string;
|
|
124
132
|
} | undefined;
|
|
125
133
|
},
|
|
134
|
+
options: { dryRun?: boolean | undefined } = {},
|
|
126
135
|
): TaskRecord {
|
|
127
|
-
const dir = tasksDirectory(repoRoot);
|
|
128
|
-
mkdirSync(dir, { recursive: true });
|
|
129
136
|
|
|
130
137
|
const id = `task_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
131
138
|
const now = new Date().toISOString();
|
|
@@ -151,8 +158,9 @@ export function createTask(
|
|
|
151
158
|
updatedAt: now,
|
|
152
159
|
};
|
|
153
160
|
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
if (options.dryRun) return record;
|
|
162
|
+
mkdirSync(tasksDirectory(repoRoot), { recursive: true });
|
|
163
|
+
writeFileSync(taskFilePath(repoRoot, id), stringify(record).trim() + "\n", "utf8");
|
|
156
164
|
return record;
|
|
157
165
|
}
|
|
158
166
|
|
|
@@ -201,7 +209,7 @@ export function updateTaskStatus(
|
|
|
201
209
|
repoRoot: string,
|
|
202
210
|
taskId: string,
|
|
203
211
|
status: TaskStatus,
|
|
204
|
-
options: { workflowRunId?: string } = {},
|
|
212
|
+
options: { workflowRunId?: string; dryRun?: boolean | undefined } = {},
|
|
205
213
|
): TaskRecord {
|
|
206
214
|
const task = getTask(repoRoot, taskId);
|
|
207
215
|
if (!task) {
|
|
@@ -212,8 +220,7 @@ export function updateTaskStatus(
|
|
|
212
220
|
if (options.workflowRunId) task.workflowRunId = options.workflowRunId;
|
|
213
221
|
task.updatedAt = new Date().toISOString();
|
|
214
222
|
|
|
215
|
-
|
|
216
|
-
writeFileSync(filePath, stringify(task).trim() + "\n", "utf8");
|
|
223
|
+
if (!options.dryRun) writeFileSync(taskFilePath(repoRoot, taskId), stringify(task).trim() + "\n", "utf8");
|
|
217
224
|
return task;
|
|
218
225
|
}
|
|
219
226
|
|
|
@@ -223,6 +230,7 @@ export function syncTaskWithTracker(
|
|
|
223
230
|
options: {
|
|
224
231
|
mockRemoteState?: string;
|
|
225
232
|
commentReceipt?: string;
|
|
233
|
+
dryRun?: boolean | undefined;
|
|
226
234
|
} = {},
|
|
227
235
|
): TaskRecord {
|
|
228
236
|
const task = getTask(repoRoot, taskId);
|
|
@@ -238,7 +246,6 @@ export function syncTaskWithTracker(
|
|
|
238
246
|
task.trackerSync.lastSyncedAt = now;
|
|
239
247
|
task.updatedAt = now;
|
|
240
248
|
|
|
241
|
-
|
|
242
|
-
writeFileSync(filePath, stringify(task).trim() + "\n", "utf8");
|
|
249
|
+
if (!options.dryRun) writeFileSync(taskFilePath(repoRoot, taskId), stringify(task).trim() + "\n", "utf8");
|
|
243
250
|
return task;
|
|
244
251
|
}
|
|
@@ -326,11 +326,14 @@ export function addWorkflowDefinition(
|
|
|
326
326
|
repoRoot?: string | undefined;
|
|
327
327
|
userConfigDir?: string | undefined;
|
|
328
328
|
overwrite?: boolean | undefined;
|
|
329
|
+
dryRun?: boolean | undefined;
|
|
329
330
|
} = {},
|
|
330
331
|
): { id: string; filePath: string; scope: "global" | "local" } {
|
|
331
332
|
const scope = options.scope ?? "local";
|
|
332
333
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
333
|
-
const dir =
|
|
334
|
+
const dir = options.dryRun
|
|
335
|
+
? workflowsDirectory(scope, repoRoot, options.userConfigDir)
|
|
336
|
+
: ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
334
337
|
const filePath = join(dir, `${workflowId}.yaml`);
|
|
335
338
|
|
|
336
339
|
if (existsSync(filePath) && !options.overwrite) {
|
|
@@ -338,7 +341,7 @@ export function addWorkflowDefinition(
|
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
const payload = typeof content === "string" ? content : stringify(content);
|
|
341
|
-
writeFileSync(filePath, payload, "utf8");
|
|
344
|
+
if (!options.dryRun) writeFileSync(filePath, payload, "utf8");
|
|
342
345
|
return { id: workflowId, filePath, scope };
|
|
343
346
|
}
|
|
344
347
|
|
|
@@ -348,6 +351,7 @@ export function removeWorkflowDefinition(
|
|
|
348
351
|
scope?: "global" | "local" | undefined;
|
|
349
352
|
repoRoot?: string | undefined;
|
|
350
353
|
userConfigDir?: string | undefined;
|
|
354
|
+
dryRun?: boolean | undefined;
|
|
351
355
|
} = {},
|
|
352
356
|
): { id: string; removed: boolean; filePath: string; scope: "global" | "local" } {
|
|
353
357
|
const scope = options.scope ?? "local";
|
|
@@ -359,6 +363,7 @@ export function removeWorkflowDefinition(
|
|
|
359
363
|
throw new Error(`workflow_not_found: workflow '${workflowId}' not found in ${scope} directory (${filePath})`);
|
|
360
364
|
}
|
|
361
365
|
|
|
366
|
+
if (options.dryRun) return { id: workflowId, removed: false, filePath, scope };
|
|
362
367
|
rmSync(filePath);
|
|
363
368
|
return { id: workflowId, removed: true, filePath, scope };
|
|
364
369
|
}
|
|
@@ -370,6 +375,7 @@ export function modifyWorkflowDefinition(
|
|
|
370
375
|
scope?: "global" | "local" | undefined;
|
|
371
376
|
repoRoot?: string | undefined;
|
|
372
377
|
userConfigDir?: string | undefined;
|
|
378
|
+
dryRun?: boolean | undefined;
|
|
373
379
|
} = {},
|
|
374
380
|
): { id: string; workflow: Record<string, unknown>; filePath: string; scope: "global" | "local" } {
|
|
375
381
|
const target = getWorkflowDefinition(workflowId, { scope: options.scope, repoRoot: options.repoRoot, userConfigDir: options.userConfigDir });
|
|
@@ -384,9 +390,11 @@ export function modifyWorkflowDefinition(
|
|
|
384
390
|
|
|
385
391
|
const scope = options.scope ?? target.scope;
|
|
386
392
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
387
|
-
const dir =
|
|
393
|
+
const dir = options.dryRun
|
|
394
|
+
? workflowsDirectory(scope, repoRoot, options.userConfigDir)
|
|
395
|
+
: ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
388
396
|
const filePath = join(dir, `${workflowId}.yaml`);
|
|
389
397
|
|
|
390
|
-
writeFileSync(filePath, stringify(updated), "utf8");
|
|
398
|
+
if (!options.dryRun) writeFileSync(filePath, stringify(updated), "utf8");
|
|
391
399
|
return { id: workflowId, workflow: updated, filePath, scope };
|
|
392
400
|
}
|