@kontextmind/kxm 0.6.0 → 0.7.4
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/.kxm/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +4 -0
- package/docs/agent-skills.md +121 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +8955 -3934
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5637 -1125
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5808 -565
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +73 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/cli.ts +1637 -73
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +267 -83
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +166 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +179 -224
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
|
@@ -5,7 +5,6 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
|
|
|
5
5
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { loadVnextProject, VnextConfigError, type VnextConfigOptions } from "./vnext-config.ts";
|
|
8
|
-
|
|
9
8
|
const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
|
|
10
9
|
import {
|
|
11
10
|
VnextRuntimeRegistry,
|
|
@@ -23,7 +22,9 @@ import {
|
|
|
23
22
|
rebuildVnextRunProjection,
|
|
24
23
|
type VnextRuntimeContext,
|
|
25
24
|
} from "./vnext-runtime.ts";
|
|
26
|
-
import {
|
|
25
|
+
import { createVnextOneShotProducer } from "./vnext-oneshot-producer.ts";
|
|
26
|
+
import { isProducerAdmitted } from "./producers.ts";
|
|
27
|
+
import { VnextRunScheduler, createVnextSimulatedProducer, driveVnextRun, pinVnextCompiledPlan, recoverVnextRun, startVnextRun } from "./vnext-engine.ts";
|
|
27
28
|
|
|
28
29
|
/* ------------------------------------------------------------------ *
|
|
29
30
|
* Token management
|
|
@@ -321,6 +322,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
321
322
|
const runtimeId = `rtm_${createHash("sha256").update(`${paths.stateRoot}\0${process.pid}\0${now()}\0${randomBytes(16).toString("hex")}`, "utf8").digest("hex").slice(0, 24)}`;
|
|
322
323
|
const requestedPort = requestedPortOption ?? 0;
|
|
323
324
|
let activeRuntimeId = runtimeId;
|
|
325
|
+
const activeDrives = new Map<string, Promise<unknown>>();
|
|
324
326
|
|
|
325
327
|
const contexts = new Map<string, VnextRuntimeContext>();
|
|
326
328
|
const contextFor = (projectRoot: string): VnextRuntimeContext => {
|
|
@@ -394,7 +396,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
394
396
|
return;
|
|
395
397
|
}
|
|
396
398
|
|
|
397
|
-
const runMatch = /^\/v1\/runs\/([A-Za-z0-9_-]+)(?:\/(events|cancel|signal|wait))?$/.exec(url.pathname);
|
|
399
|
+
const runMatch = /^\/v1\/runs\/([A-Za-z0-9_-]+)(?:\/(events|cancel|signal|wait|drive))?$/.exec(url.pathname);
|
|
398
400
|
if (runMatch) {
|
|
399
401
|
const runId = runMatch[1] as string;
|
|
400
402
|
const sub = runMatch[2];
|
|
@@ -404,11 +406,120 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
404
406
|
return;
|
|
405
407
|
}
|
|
406
408
|
const context = contextFor(projectRoot);
|
|
409
|
+
const bundle = loadVnextProject(projectRoot, {});
|
|
407
410
|
if (request.method === "GET" && !sub) {
|
|
408
411
|
const projected = rebuildVnextRunProjection(context, runId);
|
|
409
412
|
sendJson(response, 200, { ok: true, run: projected });
|
|
410
413
|
return;
|
|
411
414
|
}
|
|
415
|
+
if (request.method === "POST" && sub === "drive") {
|
|
416
|
+
const body = await readJsonBody(request);
|
|
417
|
+
if (body.mode !== "simulated" && body.mode !== "live") {
|
|
418
|
+
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "drive mode must be simulated or live" });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const run = context.eventStore.run(runId);
|
|
422
|
+
if (!run) throw runtimeError("run_unknown", runId, "run not found");
|
|
423
|
+
if (activeDrives.has(runId)) {
|
|
424
|
+
sendJson(response, 409, { ok: false, error: "run_busy", message: `run ${runId} is already executing` });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "cancelling") {
|
|
428
|
+
sendJson(response, 409, { ok: false, error: "run_busy", message: `run ${runId} is already ${run.status}` });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
let releaseActive = () => {};
|
|
432
|
+
const activePromise = new Promise<void>((resolve) => { releaseActive = resolve; });
|
|
433
|
+
activeDrives.set(runId, activePromise);
|
|
434
|
+
const cleanupActive = () => {
|
|
435
|
+
activeDrives.delete(runId);
|
|
436
|
+
releaseActive();
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
if (run.status === "created") {
|
|
440
|
+
pinVnextCompiledPlan(context, bundle, runId);
|
|
441
|
+
const plan = startVnextRun(context, runId, { allowLimits: true });
|
|
442
|
+
if (plan.handoff) {
|
|
443
|
+
cleanupActive();
|
|
444
|
+
sendJson(response, 409, { ok: false, error: "run_handoff_required", handoff: plan.handoff });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const producer = body.mode === "live"
|
|
449
|
+
? createVnextOneShotProducer({
|
|
450
|
+
projectRoot,
|
|
451
|
+
defaultHarness: String(bundle.project.value.defaultHarness ?? "pi"),
|
|
452
|
+
resolveHarness: (agentId) => {
|
|
453
|
+
const agent = bundle.agents.get(agentId);
|
|
454
|
+
return typeof agent?.value.harness === "string" ? agent.value.harness : undefined;
|
|
455
|
+
},
|
|
456
|
+
resolveModel: (agentId) => {
|
|
457
|
+
const agent = bundle.agents.get(agentId);
|
|
458
|
+
const model = agent?.value.model;
|
|
459
|
+
if (!model || typeof model !== "object" || Array.isArray(model)) return undefined;
|
|
460
|
+
const value = model as Record<string, unknown>;
|
|
461
|
+
const provider = typeof value.provider === "string" ? value.provider : undefined;
|
|
462
|
+
const modelName = typeof value.model === "string" ? value.model : undefined;
|
|
463
|
+
if (!provider || !modelName || !isProducerAdmitted(projectRoot, `${provider}/${modelName}`)) {
|
|
464
|
+
throw new Error("producer_route_not_admitted");
|
|
465
|
+
}
|
|
466
|
+
return { provider, model: modelName };
|
|
467
|
+
},
|
|
468
|
+
})
|
|
469
|
+
: (() => {
|
|
470
|
+
const delayMs = typeof body.delayMs === "number" && body.delayMs > 0 ? body.delayMs : 0;
|
|
471
|
+
return createVnextSimulatedProducer(async () => {
|
|
472
|
+
if (delayMs > 0) {
|
|
473
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
474
|
+
}
|
|
475
|
+
return { outcome: "passed" };
|
|
476
|
+
});
|
|
477
|
+
})();
|
|
478
|
+
|
|
479
|
+
let drivePromise: Promise<unknown>;
|
|
480
|
+
let earlyError: unknown;
|
|
481
|
+
try {
|
|
482
|
+
const scheduler = VnextRunScheduler.for(context, bundle);
|
|
483
|
+
drivePromise = scheduler.enqueue(runId, producer, { allowLimits: true, liveMode: body.mode === "live" });
|
|
484
|
+
drivePromise.catch((err) => { earlyError = err; });
|
|
485
|
+
} catch (err) {
|
|
486
|
+
earlyError = err;
|
|
487
|
+
drivePromise = Promise.reject(err);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Yield microtask to catch synchronous duplicate queue check (e.g. run_busy)
|
|
491
|
+
await Promise.resolve();
|
|
492
|
+
|
|
493
|
+
if (earlyError) {
|
|
494
|
+
cleanupActive();
|
|
495
|
+
const errStr = String(earlyError);
|
|
496
|
+
if (errStr.includes("run_busy") || errStr.includes("scheduler_policy_conflict")) {
|
|
497
|
+
if ("close" in producer && typeof producer.close === "function") {
|
|
498
|
+
try { await producer.close(); } catch { /* ignore */ }
|
|
499
|
+
}
|
|
500
|
+
sendJson(response, 409, { ok: false, error: "run_busy", message: `run ${runId} is already admitted or queued` });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
throw earlyError;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
void drivePromise.finally(async () => {
|
|
507
|
+
cleanupActive();
|
|
508
|
+
if ("close" in producer && typeof producer.close === "function") {
|
|
509
|
+
try { await producer.close(); } catch { /* ignore */ }
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
sendJson(response, 202, {
|
|
514
|
+
ok: true,
|
|
515
|
+
status: "accepted",
|
|
516
|
+
runId,
|
|
517
|
+
poll: `/v1/runs/${runId}`,
|
|
518
|
+
mode: body.mode,
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
412
523
|
if (request.method === "GET" && sub === "events") {
|
|
413
524
|
const after = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
|
|
414
525
|
const events = context.eventStore.events(runId, Number.isFinite(after) && after >= 0 ? after : 0, 500);
|
|
@@ -429,9 +540,10 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
429
540
|
if (!run) throw runtimeError("run_unknown", runId, "run not found");
|
|
430
541
|
const state = foldStoredVnextRun(context, run);
|
|
431
542
|
let unblocked = false;
|
|
432
|
-
if (state.status === "blocked_uncertain" || state.currentStep?.effectState === "blocked_uncertain") {
|
|
543
|
+
if (state.status === "blocked_uncertain" || state.status === "cancelling" || state.currentStep?.effectState === "blocked_uncertain") {
|
|
544
|
+
const action = body.action === "cancel" || body.action === "fail" || body.action === "retry" || body.action === "unblock" ? body.action : "unblock";
|
|
433
545
|
const rec = recoverVnextRun(context, runId, {
|
|
434
|
-
action
|
|
546
|
+
action,
|
|
435
547
|
reason: typeof body.summary === "string" ? body.summary : `signal_${body.signalKey ?? "callback"}`,
|
|
436
548
|
});
|
|
437
549
|
unblocked = rec.unblocked;
|
|
@@ -543,6 +655,9 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
543
655
|
stopping = true;
|
|
544
656
|
clearInterval(heartbeat);
|
|
545
657
|
try { registry.markStopping(process.pid, now()); } catch { /* best effort */ }
|
|
658
|
+
if (activeDrives.size > 0) {
|
|
659
|
+
await Promise.allSettled([...activeDrives.values()]);
|
|
660
|
+
}
|
|
546
661
|
for (const context of contexts.values()) closeVnextRuntimeContext(context);
|
|
547
662
|
contexts.clear();
|
|
548
663
|
const closed = new Promise<void>((resolveStop) => server.close(() => resolveStop()));
|
|
@@ -159,6 +159,19 @@ export function computeVnextMemoryRevision(
|
|
|
159
159
|
return `ctxrev_${hash.digest("hex")}`;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
export function checkMemoryRevisionDrift(
|
|
163
|
+
pinnedRevision: string,
|
|
164
|
+
bundle: VnextProjectBundle,
|
|
165
|
+
options?: VnextMemoryRevisionOptions,
|
|
166
|
+
): { drifted: boolean; currentRevision: string; pinnedRevision: string } {
|
|
167
|
+
const currentRevision = computeVnextMemoryRevision(bundle, options);
|
|
168
|
+
return {
|
|
169
|
+
drifted: currentRevision !== pinnedRevision,
|
|
170
|
+
currentRevision,
|
|
171
|
+
pinnedRevision,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
162
175
|
export function vnextPolicyRevisions(
|
|
163
176
|
bundle: VnextProjectBundle,
|
|
164
177
|
options?: VnextMemoryRevisionOptions,
|
|
@@ -423,6 +436,7 @@ export function acceptVnextRun(
|
|
|
423
436
|
result: { runId, homeRuntimeId: context.homeRuntimeId, status: "created" },
|
|
424
437
|
recordedAt: now,
|
|
425
438
|
});
|
|
439
|
+
context.eventStore.putRunPrompt(runId, request.prompt);
|
|
426
440
|
return { accepted: true, idempotent: false, run, event };
|
|
427
441
|
});
|
|
428
442
|
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KXM Workflow Management Subsystem.
|
|
3
|
+
* Supports add, remove, modify, list, and inspection of workflow YAML definitions.
|
|
4
|
+
* Handles global (~/.config/kxm/workflows/) and local (.kxm/workflows/) scoping.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { parse, stringify } from "yaml";
|
|
10
|
+
import { repoConfigDirectory, userConfigDirectory } from "./config.ts";
|
|
11
|
+
|
|
12
|
+
export interface WorkflowDefSummary {
|
|
13
|
+
id: string;
|
|
14
|
+
description: string;
|
|
15
|
+
scope: "global" | "local" | "overridden";
|
|
16
|
+
filePath: string;
|
|
17
|
+
stepCount: number;
|
|
18
|
+
roles: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const WORKFLOW_TEMPLATES: Record<string, Record<string, unknown>> = {
|
|
22
|
+
"implement-and-verify": {
|
|
23
|
+
schema: "kxm.workflow.v1",
|
|
24
|
+
description: "Standard implement and verify workflow",
|
|
25
|
+
coordinator: "coordinator",
|
|
26
|
+
limits: {
|
|
27
|
+
maxTransitions: 8,
|
|
28
|
+
},
|
|
29
|
+
steps: [
|
|
30
|
+
{
|
|
31
|
+
id: "implement",
|
|
32
|
+
kind: "agent",
|
|
33
|
+
role: "writer",
|
|
34
|
+
maxAttempts: 2,
|
|
35
|
+
on: {
|
|
36
|
+
passed: "verify",
|
|
37
|
+
failed: {
|
|
38
|
+
target: "$terminal",
|
|
39
|
+
terminalStatus: "failed",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "verify",
|
|
45
|
+
kind: "gate",
|
|
46
|
+
gate: "verify-gate",
|
|
47
|
+
expect: "pass",
|
|
48
|
+
maxAttempts: 1,
|
|
49
|
+
on: {
|
|
50
|
+
passed: {
|
|
51
|
+
target: "$terminal",
|
|
52
|
+
terminalStatus: "completed",
|
|
53
|
+
},
|
|
54
|
+
failed: {
|
|
55
|
+
target: "implement",
|
|
56
|
+
maxTransitions: 2,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
"dual-critic-review": {
|
|
63
|
+
schema: "kxm.workflow.v1",
|
|
64
|
+
description: "Dual-critic review workflow with independent Fable architecture and Sol CLI critics",
|
|
65
|
+
coordinator: "coordinator",
|
|
66
|
+
limits: {
|
|
67
|
+
maxTransitions: 12,
|
|
68
|
+
},
|
|
69
|
+
steps: [
|
|
70
|
+
{
|
|
71
|
+
id: "implement",
|
|
72
|
+
kind: "agent",
|
|
73
|
+
role: "writer",
|
|
74
|
+
maxAttempts: 2,
|
|
75
|
+
on: {
|
|
76
|
+
passed: "review-arch",
|
|
77
|
+
failed: {
|
|
78
|
+
target: "$terminal",
|
|
79
|
+
terminalStatus: "failed",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "review-arch",
|
|
85
|
+
kind: "agent",
|
|
86
|
+
role: "critic-arch",
|
|
87
|
+
maxAttempts: 2,
|
|
88
|
+
on: {
|
|
89
|
+
passed: "review-cli",
|
|
90
|
+
failed: {
|
|
91
|
+
target: "implement",
|
|
92
|
+
maxTransitions: 2,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "review-cli",
|
|
98
|
+
kind: "agent",
|
|
99
|
+
role: "critic-cli",
|
|
100
|
+
maxAttempts: 2,
|
|
101
|
+
on: {
|
|
102
|
+
passed: "verify",
|
|
103
|
+
failed: {
|
|
104
|
+
target: "implement",
|
|
105
|
+
maxTransitions: 2,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "verify",
|
|
111
|
+
kind: "gate",
|
|
112
|
+
gate: "verify-gate",
|
|
113
|
+
expect: "pass",
|
|
114
|
+
maxAttempts: 1,
|
|
115
|
+
on: {
|
|
116
|
+
passed: {
|
|
117
|
+
target: "$terminal",
|
|
118
|
+
terminalStatus: "completed",
|
|
119
|
+
},
|
|
120
|
+
failed: {
|
|
121
|
+
target: "implement",
|
|
122
|
+
maxTransitions: 2,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
"spec-and-plan": {
|
|
129
|
+
schema: "kxm.workflow.v1",
|
|
130
|
+
description: "Specification and architecture breakdown planning workflow",
|
|
131
|
+
coordinator: "coordinator",
|
|
132
|
+
limits: {
|
|
133
|
+
maxTransitions: 6,
|
|
134
|
+
},
|
|
135
|
+
steps: [
|
|
136
|
+
{
|
|
137
|
+
id: "plan",
|
|
138
|
+
kind: "agent",
|
|
139
|
+
role: "planner",
|
|
140
|
+
maxAttempts: 2,
|
|
141
|
+
on: {
|
|
142
|
+
passed: "review-arch",
|
|
143
|
+
failed: {
|
|
144
|
+
target: "$terminal",
|
|
145
|
+
terminalStatus: "failed",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "review-arch",
|
|
151
|
+
kind: "agent",
|
|
152
|
+
role: "critic-arch",
|
|
153
|
+
maxAttempts: 2,
|
|
154
|
+
on: {
|
|
155
|
+
passed: {
|
|
156
|
+
target: "$terminal",
|
|
157
|
+
terminalStatus: "completed",
|
|
158
|
+
},
|
|
159
|
+
failed: {
|
|
160
|
+
target: "plan",
|
|
161
|
+
maxTransitions: 2,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const DEFAULT_WORKFLOW_TEMPLATE = WORKFLOW_TEMPLATES["implement-and-verify"]!;
|
|
170
|
+
|
|
171
|
+
export function workflowsDirectory(
|
|
172
|
+
scope: "global" | "local",
|
|
173
|
+
repoRoot = process.cwd(),
|
|
174
|
+
userConfigDir?: string,
|
|
175
|
+
): string {
|
|
176
|
+
if (scope === "global") {
|
|
177
|
+
return join(userConfigDirectory(userConfigDir), "workflows");
|
|
178
|
+
}
|
|
179
|
+
return join(repoConfigDirectory(repoRoot), "workflows");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function ensureWorkflowsDirectory(
|
|
183
|
+
scope: "global" | "local",
|
|
184
|
+
repoRoot = process.cwd(),
|
|
185
|
+
userConfigDir?: string,
|
|
186
|
+
): string {
|
|
187
|
+
const dir = workflowsDirectory(scope, repoRoot, userConfigDir);
|
|
188
|
+
if (!existsSync(dir)) {
|
|
189
|
+
mkdirSync(dir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
return dir;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function parseWorkflowFile(filePath: string): Record<string, unknown> | undefined {
|
|
195
|
+
if (!existsSync(filePath)) return undefined;
|
|
196
|
+
try {
|
|
197
|
+
const raw = readFileSync(filePath, "utf8");
|
|
198
|
+
const parsed = parse(raw) as Record<string, unknown>;
|
|
199
|
+
if (!parsed || typeof parsed !== "object") return undefined;
|
|
200
|
+
return parsed;
|
|
201
|
+
} catch {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function listWorkflowDefinitions(options: {
|
|
207
|
+
scope?: "all" | "global" | "local" | undefined;
|
|
208
|
+
repoRoot?: string | undefined;
|
|
209
|
+
userConfigDir?: string | undefined;
|
|
210
|
+
} = {}): WorkflowDefSummary[] {
|
|
211
|
+
const scopeFilter = options.scope ?? "all";
|
|
212
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
213
|
+
const globalDir = workflowsDirectory("global", repoRoot, options.userConfigDir);
|
|
214
|
+
const localDir = workflowsDirectory("local", repoRoot, options.userConfigDir);
|
|
215
|
+
|
|
216
|
+
const localDefs = new Map<string, { def: Record<string, unknown>; filePath: string }>();
|
|
217
|
+
if (scopeFilter !== "global" && existsSync(localDir)) {
|
|
218
|
+
for (const entry of readdirSync(localDir)) {
|
|
219
|
+
if (entry.endsWith(".yaml") || entry.endsWith(".yml")) {
|
|
220
|
+
const filePath = join(localDir, entry);
|
|
221
|
+
const def = parseWorkflowFile(filePath);
|
|
222
|
+
if (def) {
|
|
223
|
+
const id = (def.id as string) || entry.replace(/\.ya?ml$/i, "");
|
|
224
|
+
localDefs.set(id, { def, filePath });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const globalDefs = new Map<string, { def: Record<string, unknown>; filePath: string }>();
|
|
231
|
+
if (scopeFilter !== "local" && existsSync(globalDir)) {
|
|
232
|
+
for (const entry of readdirSync(globalDir)) {
|
|
233
|
+
if (entry.endsWith(".yaml") || entry.endsWith(".yml")) {
|
|
234
|
+
const filePath = join(globalDir, entry);
|
|
235
|
+
const def = parseWorkflowFile(filePath);
|
|
236
|
+
if (def) {
|
|
237
|
+
const id = (def.id as string) || entry.replace(/\.ya?ml$/i, "");
|
|
238
|
+
globalDefs.set(id, { def, filePath });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const extractRoles = (def: Record<string, unknown>): string[] => {
|
|
245
|
+
const roles: string[] = [];
|
|
246
|
+
const steps = Array.isArray(def.steps) ? def.steps : [];
|
|
247
|
+
for (const step of steps) {
|
|
248
|
+
if (step && typeof step === "object") {
|
|
249
|
+
const role = (step as Record<string, unknown>).role || (step as Record<string, unknown>).agent;
|
|
250
|
+
if (typeof role === "string" && !roles.includes(role)) {
|
|
251
|
+
roles.push(role);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return roles;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const result: WorkflowDefSummary[] = [];
|
|
259
|
+
|
|
260
|
+
for (const [id, { def, filePath }] of localDefs) {
|
|
261
|
+
const isOverridden = globalDefs.has(id);
|
|
262
|
+
const steps = Array.isArray(def.steps) ? def.steps : [];
|
|
263
|
+
result.push({
|
|
264
|
+
id,
|
|
265
|
+
description: typeof def.description === "string" ? def.description : "",
|
|
266
|
+
scope: isOverridden ? "overridden" : "local",
|
|
267
|
+
filePath,
|
|
268
|
+
stepCount: steps.length,
|
|
269
|
+
roles: extractRoles(def),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const [id, { def, filePath }] of globalDefs) {
|
|
274
|
+
if (scopeFilter === "global" || !localDefs.has(id)) {
|
|
275
|
+
const steps = Array.isArray(def.steps) ? def.steps : [];
|
|
276
|
+
result.push({
|
|
277
|
+
id,
|
|
278
|
+
description: typeof def.description === "string" ? def.description : "",
|
|
279
|
+
scope: "global",
|
|
280
|
+
filePath,
|
|
281
|
+
stepCount: steps.length,
|
|
282
|
+
roles: extractRoles(def),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
result.sort((a, b) => a.id.localeCompare(b.id));
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function getWorkflowDefinition(
|
|
292
|
+
workflowId: string,
|
|
293
|
+
options: {
|
|
294
|
+
scope?: "all" | "global" | "local" | undefined;
|
|
295
|
+
repoRoot?: string | undefined;
|
|
296
|
+
userConfigDir?: string | undefined;
|
|
297
|
+
} = {},
|
|
298
|
+
): { workflow: Record<string, unknown>; scope: "global" | "local"; filePath: string } | undefined {
|
|
299
|
+
const scope = options.scope ?? "all";
|
|
300
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
301
|
+
|
|
302
|
+
// 1. Check local first
|
|
303
|
+
if (scope !== "global") {
|
|
304
|
+
const localDir = workflowsDirectory("local", repoRoot, options.userConfigDir);
|
|
305
|
+
const localFile = join(localDir, `${workflowId}.yaml`);
|
|
306
|
+
const parsed = parseWorkflowFile(localFile);
|
|
307
|
+
if (parsed) return { workflow: parsed, scope: "local", filePath: localFile };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 2. Check global
|
|
311
|
+
if (scope !== "local") {
|
|
312
|
+
const globalDir = workflowsDirectory("global", repoRoot, options.userConfigDir);
|
|
313
|
+
const globalFile = join(globalDir, `${workflowId}.yaml`);
|
|
314
|
+
const parsed = parseWorkflowFile(globalFile);
|
|
315
|
+
if (parsed) return { workflow: parsed, scope: "global", filePath: globalFile };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function addWorkflowDefinition(
|
|
322
|
+
workflowId: string,
|
|
323
|
+
content: Record<string, unknown> | string = DEFAULT_WORKFLOW_TEMPLATE,
|
|
324
|
+
options: {
|
|
325
|
+
scope?: "global" | "local" | undefined;
|
|
326
|
+
repoRoot?: string | undefined;
|
|
327
|
+
userConfigDir?: string | undefined;
|
|
328
|
+
overwrite?: boolean | undefined;
|
|
329
|
+
} = {},
|
|
330
|
+
): { id: string; filePath: string; scope: "global" | "local" } {
|
|
331
|
+
const scope = options.scope ?? "local";
|
|
332
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
333
|
+
const dir = ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
334
|
+
const filePath = join(dir, `${workflowId}.yaml`);
|
|
335
|
+
|
|
336
|
+
if (existsSync(filePath) && !options.overwrite) {
|
|
337
|
+
throw new Error(`workflow_already_exists: workflow '${workflowId}' already exists at ${filePath}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const payload = typeof content === "string" ? content : stringify(content);
|
|
341
|
+
writeFileSync(filePath, payload, "utf8");
|
|
342
|
+
return { id: workflowId, filePath, scope };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function removeWorkflowDefinition(
|
|
346
|
+
workflowId: string,
|
|
347
|
+
options: {
|
|
348
|
+
scope?: "global" | "local" | undefined;
|
|
349
|
+
repoRoot?: string | undefined;
|
|
350
|
+
userConfigDir?: string | undefined;
|
|
351
|
+
} = {},
|
|
352
|
+
): { id: string; removed: boolean; filePath: string; scope: "global" | "local" } {
|
|
353
|
+
const scope = options.scope ?? "local";
|
|
354
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
355
|
+
const dir = workflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
356
|
+
const filePath = join(dir, `${workflowId}.yaml`);
|
|
357
|
+
|
|
358
|
+
if (!existsSync(filePath)) {
|
|
359
|
+
throw new Error(`workflow_not_found: workflow '${workflowId}' not found in ${scope} directory (${filePath})`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
rmSync(filePath);
|
|
363
|
+
return { id: workflowId, removed: true, filePath, scope };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function modifyWorkflowDefinition(
|
|
367
|
+
workflowId: string,
|
|
368
|
+
updates: Record<string, unknown>,
|
|
369
|
+
options: {
|
|
370
|
+
scope?: "global" | "local" | undefined;
|
|
371
|
+
repoRoot?: string | undefined;
|
|
372
|
+
userConfigDir?: string | undefined;
|
|
373
|
+
} = {},
|
|
374
|
+
): { id: string; workflow: Record<string, unknown>; filePath: string; scope: "global" | "local" } {
|
|
375
|
+
const target = getWorkflowDefinition(workflowId, { scope: options.scope, repoRoot: options.repoRoot, userConfigDir: options.userConfigDir });
|
|
376
|
+
if (!target) {
|
|
377
|
+
throw new Error(`workflow_not_found: workflow '${workflowId}' does not exist`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const updated: Record<string, unknown> = {
|
|
381
|
+
...target.workflow,
|
|
382
|
+
...updates,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const scope = options.scope ?? target.scope;
|
|
386
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
387
|
+
const dir = ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
388
|
+
const filePath = join(dir, `${workflowId}.yaml`);
|
|
389
|
+
|
|
390
|
+
writeFileSync(filePath, stringify(updated), "utf8");
|
|
391
|
+
return { id: workflowId, workflow: updated, filePath, scope };
|
|
392
|
+
}
|