@kontextmind/kxm 0.6.0
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 +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,3707 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { Command, CommanderError } from "commander";
|
|
9
|
+
import {
|
|
10
|
+
createMemoryNote,
|
|
11
|
+
formatMemoryBriefText,
|
|
12
|
+
generateMemoryBrief,
|
|
13
|
+
syncHarnessMemory,
|
|
14
|
+
type MemoryScope,
|
|
15
|
+
} from "./memory.ts";
|
|
16
|
+
import { createBackup, restoreBackup } from "./database.ts";
|
|
17
|
+
import { verifyArtifactExists } from "./artifacts-exist.ts";
|
|
18
|
+
import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions } from "./workflow.ts";
|
|
19
|
+
import { postWorkflowSignal, watchGithubChecks } from "./github-watch.ts";
|
|
20
|
+
import { buildRetrospective, writeRetrospective } from "./retrospective.ts";
|
|
21
|
+
import { redactSecrets } from "./redact.ts";
|
|
22
|
+
import { SkillLifecycle, type SkillEvaluationKind, type SkillState } from "./skills.ts";
|
|
23
|
+
import { writeCompiledWiki } from "./wiki.ts";
|
|
24
|
+
import { agentWorker, gateWorker, workerResult, type Worker, type WorkerOutcome } from "./envelope.ts";
|
|
25
|
+
import { appendTelemetry, inferImprovementTarget, makeTelemetryEvent, readTelemetry, readRoutingRecords, telemetryPath } from "./telemetry.ts";
|
|
26
|
+
import { behavioralConfigHash, compareRoutingRecords, groupByBehavior, generateRoutingReport, formatRoutingReport } from "./routing.ts";
|
|
27
|
+
import { loadPriceCatalog, type PriceCatalog } from "./prices.ts";
|
|
28
|
+
import { createSession, loadNamedWorkers, rosterNames, sessionAssetDirs, workflowAssetDirs, writeSession } from "./session.ts";
|
|
29
|
+
import { buildImprovementReport, formatImprovementReport, writeImprovementReport } from "./improve.ts";
|
|
30
|
+
import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "./tui.ts";
|
|
31
|
+
import { formatSessionBriefText, loadSessionBrief, loadSessionBriefAsync, type SessionHubStatus } from "./session-work.ts";
|
|
32
|
+
import {
|
|
33
|
+
HUB_BINDING_SCHEMA,
|
|
34
|
+
HubBindingError,
|
|
35
|
+
hubBindingFile,
|
|
36
|
+
probeHubHealth,
|
|
37
|
+
readHubBinding,
|
|
38
|
+
removeHubBinding,
|
|
39
|
+
validateHubUrl,
|
|
40
|
+
writeHubBinding,
|
|
41
|
+
type HubHealth,
|
|
42
|
+
} from "./hub-binding.ts";
|
|
43
|
+
import {
|
|
44
|
+
fetchLatestKxmVersion,
|
|
45
|
+
kxmReleaseAssetName,
|
|
46
|
+
noticeFromVersions,
|
|
47
|
+
planKxmPackageUpdate,
|
|
48
|
+
readInstalledKxmVersion,
|
|
49
|
+
readUpdateCache,
|
|
50
|
+
verifyReleaseAssetDigest,
|
|
51
|
+
writeUpdateCache,
|
|
52
|
+
KxmUpdateConfigError,
|
|
53
|
+
type KxmPackageUpdateStep,
|
|
54
|
+
type KxmUpdateConfig,
|
|
55
|
+
type KxmUpdateNotice,
|
|
56
|
+
} from "./kxm-update.ts";
|
|
57
|
+
import { loadKxmUpdateConfig } from "./kxm-update-config.ts";
|
|
58
|
+
import {
|
|
59
|
+
classifyInstallRoot,
|
|
60
|
+
resolveInstallKind,
|
|
61
|
+
type InstallKindReport,
|
|
62
|
+
type InstallProbe,
|
|
63
|
+
} from "./kxm-install-kind.ts";
|
|
64
|
+
import { VnextConfigError, discoverVnextProjectRoot, type VnextInitializationPlan } from "./vnext-config.ts";
|
|
65
|
+
import { vnextUserStateRoot } from "./vnext-bindings.ts";
|
|
66
|
+
import { initializeVnextProject } from "./vnext-init.ts";
|
|
67
|
+
import { applyVnextMigration, planVnextMigration, verifyVnextMigration } from "./vnext-migrate.ts";
|
|
68
|
+
import { diffVnextProjectAgainstRevision, formatVnextPermissionDiff } from "./vnext-permission.ts";
|
|
69
|
+
import { readVnextLocalBindings } from "./vnext-bindings.ts";
|
|
70
|
+
import { loadVnextProject } from "./vnext-config.ts";
|
|
71
|
+
import {
|
|
72
|
+
ensureVnextSupervisor,
|
|
73
|
+
vnextRuntimeRequest,
|
|
74
|
+
vnextSupervisorStatus,
|
|
75
|
+
} from "./vnext-runtime-supervisor.ts";
|
|
76
|
+
import { vnextRuntimePaths } from "./vnext-runtime-store.ts";
|
|
77
|
+
import {
|
|
78
|
+
formatHarnessInventory,
|
|
79
|
+
formatHarnessUpdate,
|
|
80
|
+
planHarnessUpdate,
|
|
81
|
+
probeHarnesses,
|
|
82
|
+
runHarnessUpdate,
|
|
83
|
+
type HarnessUpdateScope,
|
|
84
|
+
} from "./vnext-harness.ts";
|
|
85
|
+
import type { WorkflowEvidenceInput, WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
|
|
86
|
+
import { HubClient } from "./client.ts";
|
|
87
|
+
import { AGENT_COMMANDS_MAP, enforceToolPolicy, mintSessionToken } from "./commands.ts";
|
|
88
|
+
import {
|
|
89
|
+
loadKxmConfig,
|
|
90
|
+
setKxmConfigValue,
|
|
91
|
+
getKxmConfigValue,
|
|
92
|
+
formatKxmConfig,
|
|
93
|
+
} from "./config.ts";
|
|
94
|
+
import { generateShellCompletion, type SupportedShell } from "./autocomplete.ts";
|
|
95
|
+
import { suggestWorkflowAndRoles } from "./suggest.ts";
|
|
96
|
+
import {
|
|
97
|
+
createGoal,
|
|
98
|
+
createTask,
|
|
99
|
+
listGoals,
|
|
100
|
+
listTasks,
|
|
101
|
+
getTask,
|
|
102
|
+
updateTaskStatus,
|
|
103
|
+
syncTaskWithTracker,
|
|
104
|
+
type TaskStatus,
|
|
105
|
+
type TrackerType,
|
|
106
|
+
} from "./task-manager.ts";
|
|
107
|
+
import { parse as parseYaml } from "yaml";
|
|
108
|
+
import { compileVnextWorkflow } from "./vnext-engine-compile.ts";
|
|
109
|
+
import { generateStudioLayout } from "./studio-layout.ts";
|
|
110
|
+
|
|
111
|
+
export interface CliSpawnResult {
|
|
112
|
+
status: number | null;
|
|
113
|
+
stdout: string;
|
|
114
|
+
stderr: string;
|
|
115
|
+
error?: Error;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface CliIo {
|
|
119
|
+
stdout: (text: string) => void;
|
|
120
|
+
stderr: (text: string) => void;
|
|
121
|
+
fetchImpl?: typeof fetch;
|
|
122
|
+
now?: () => number;
|
|
123
|
+
sleep?: (ms: number) => Promise<void>;
|
|
124
|
+
spawnHub?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
|
|
125
|
+
spawnWorker?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
|
|
126
|
+
installProbe?: Partial<InstallProbe>;
|
|
127
|
+
spawnSync?: (command: string, args: readonly string[]) => CliSpawnResult;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const CLI_NAME = "kxm";
|
|
131
|
+
const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
|
|
132
|
+
const USAGE_ERROR_CODES = new Set([
|
|
133
|
+
"commander.help",
|
|
134
|
+
"commander.helpDisplayed",
|
|
135
|
+
"commander.unknownCommand",
|
|
136
|
+
"commander.unknownOption",
|
|
137
|
+
"commander.missingArgument",
|
|
138
|
+
"commander.missingMandatoryOptionValue",
|
|
139
|
+
"commander.invalidArgument",
|
|
140
|
+
"commander.excessArguments",
|
|
141
|
+
"commander.variadicArgNotLast",
|
|
142
|
+
"commander.invalidOptionArgument",
|
|
143
|
+
"commander.optionMissingArgument",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
interface CliContext {
|
|
147
|
+
env: NodeJS.ProcessEnv;
|
|
148
|
+
io: CliIo;
|
|
149
|
+
cwd: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface GlobalOpts {
|
|
153
|
+
json?: boolean;
|
|
154
|
+
dryRun?: boolean;
|
|
155
|
+
workspace?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface Runtime extends CliContext, Required<Pick<GlobalOpts, "json" | "dryRun">> {
|
|
159
|
+
workspaceFlag?: string;
|
|
160
|
+
dirs: ReturnType<typeof workspaceDirs>;
|
|
161
|
+
serverUrl: string;
|
|
162
|
+
boundHubUrl?: string;
|
|
163
|
+
fetchImpl: typeof fetch;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function spawnScript(scriptName: string, extraEnv: NodeJS.ProcessEnv = {}): Promise<number> {
|
|
167
|
+
return new Promise((resolveExit) => {
|
|
168
|
+
const child = spawn(process.execPath, [join(repoRoot, "scripts", scriptName)], {
|
|
169
|
+
stdio: "inherit",
|
|
170
|
+
env: { ...process.env, ...extraEnv },
|
|
171
|
+
});
|
|
172
|
+
child.once("error", () => resolveExit(1));
|
|
173
|
+
child.once("exit", (code) => resolveExit(code ?? 1));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseEvidencePairs(values: string[]): WorkflowEvidenceInput {
|
|
178
|
+
const evidence = new Map<string, string>();
|
|
179
|
+
for (const value of values) {
|
|
180
|
+
const separator = value.indexOf("=");
|
|
181
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
182
|
+
throw new Error("evidence must use <required-key>=<evidence> syntax");
|
|
183
|
+
}
|
|
184
|
+
const requirement = canonicalWorkflowEvidenceKey(value.slice(0, separator));
|
|
185
|
+
const proof = value.slice(separator + 1).trim();
|
|
186
|
+
if (!requirement || !proof) throw new Error("evidence must use <required-key>=<evidence> syntax");
|
|
187
|
+
if (evidence.has(requirement)) throw new Error(`duplicate normalized evidence key: ${requirement}`);
|
|
188
|
+
evidence.set(requirement, proof);
|
|
189
|
+
}
|
|
190
|
+
return Object.fromEntries(evidence);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const CLI_RESULT_SCHEMA = "kxm.cli-result.v1";
|
|
194
|
+
|
|
195
|
+
/** Every CLI payload carries a schema tag; payloads that already declare one
|
|
196
|
+
* (worker envelopes) keep theirs. `ok:false` goes to stderr in both modes so
|
|
197
|
+
* agents can split results from errors without parsing. */
|
|
198
|
+
function print(io: CliIo, jsonMode: boolean, payload: object, text: string): void {
|
|
199
|
+
const tagged = { schema: CLI_RESULT_SCHEMA, ...payload } as Record<string, unknown>;
|
|
200
|
+
const safePayload = JSON.stringify(redactCliValue(tagged));
|
|
201
|
+
const line = jsonMode ? `${safePayload}\n` : `${redactSecrets(text)}\n`;
|
|
202
|
+
if (tagged.ok === false) io.stderr(line);
|
|
203
|
+
else io.stdout(line);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function printWorker(
|
|
207
|
+
runtime: Runtime,
|
|
208
|
+
worker: Worker,
|
|
209
|
+
payload: Record<string, unknown> & { command: string; ok: boolean },
|
|
210
|
+
text: string,
|
|
211
|
+
outcome?: WorkerOutcome,
|
|
212
|
+
): void {
|
|
213
|
+
const sessionId = runtime.env.KXM_SESSION_ID?.trim();
|
|
214
|
+
const envelope = workerResult(worker, {
|
|
215
|
+
...payload,
|
|
216
|
+
summary: text,
|
|
217
|
+
...(outcome ? { outcome } : {}),
|
|
218
|
+
...(sessionId ? { sessionId } : {}),
|
|
219
|
+
});
|
|
220
|
+
if (!runtime.dryRun) {
|
|
221
|
+
try {
|
|
222
|
+
const safeEnvelope = redactCliValue(envelope) as typeof envelope;
|
|
223
|
+
appendTelemetry(telemetryPath(runtime.dirs.logs), makeTelemetryEvent({
|
|
224
|
+
envelope: safeEnvelope,
|
|
225
|
+
...(sessionId ? { sessionId } : {}),
|
|
226
|
+
host: hostMode(runtime),
|
|
227
|
+
target: inferImprovementTarget({
|
|
228
|
+
...(worker.project ? { project: worker.project } : {}),
|
|
229
|
+
env: runtime.env,
|
|
230
|
+
}),
|
|
231
|
+
}));
|
|
232
|
+
} catch {
|
|
233
|
+
// Telemetry must never fail the operator command.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
print(runtime.io, runtime.json, envelope, text);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hostMode(runtime: Runtime): "local" | "hub" {
|
|
240
|
+
try {
|
|
241
|
+
const hostname = new URL(runtime.serverUrl).hostname;
|
|
242
|
+
if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") return "local";
|
|
243
|
+
} catch {
|
|
244
|
+
// Default local when the hub URL is not a URL.
|
|
245
|
+
}
|
|
246
|
+
return "hub";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function gateOf(runtime: Runtime, name: string): ReturnType<typeof gateWorker> {
|
|
250
|
+
const project = runtime.env.KXM_PROJECT?.trim();
|
|
251
|
+
return gateWorker({ name, ...(project ? { project } : {}) });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function redactCliValue(value: unknown, field = ""): unknown {
|
|
255
|
+
if (typeof value === "string") {
|
|
256
|
+
// These fields are public audit digests, not credentials. Preserve
|
|
257
|
+
// them only by exact field name and shape; every other 64-hex value keeps
|
|
258
|
+
// the conservative generic redaction behavior.
|
|
259
|
+
if (
|
|
260
|
+
(field === "requestSha256"
|
|
261
|
+
|| field === "replySha256"
|
|
262
|
+
|| field === "behavioralSha256"
|
|
263
|
+
|| field === "workflowDefinitionSha256"
|
|
264
|
+
|| field === "verifierConfigSha256"
|
|
265
|
+
|| field === "rolePromptSha256"
|
|
266
|
+
|| field === "contentSha256"
|
|
267
|
+
|| field === "configRevision"
|
|
268
|
+
|| field === "baseSha256"
|
|
269
|
+
|| field === "localSha256"
|
|
270
|
+
|| field === "targetSha256"
|
|
271
|
+
|| field === "sourceTemplateRevision"
|
|
272
|
+
|| field === "targetTemplateRevision"
|
|
273
|
+
|| field === "sourceDigest"
|
|
274
|
+
|| field === "decisionDigest"
|
|
275
|
+
|| field === "receiptSha256"
|
|
276
|
+
|| field === "sha256"
|
|
277
|
+
|| field === "valueSha256"
|
|
278
|
+
|| field === "baseRevision"
|
|
279
|
+
|| field === "candidateRevision"
|
|
280
|
+
|| field === "baseValueSha256"
|
|
281
|
+
|| field === "candidateValueSha256")
|
|
282
|
+
&& /^(?:sha256:)?[a-f0-9]{64}$/.test(value)
|
|
283
|
+
) return value;
|
|
284
|
+
return redactSecrets(value);
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(value)) return value.map((candidate) => redactCliValue(candidate));
|
|
287
|
+
if (value && typeof value === "object") {
|
|
288
|
+
return Object.fromEntries(
|
|
289
|
+
Object.entries(value as Record<string, unknown>)
|
|
290
|
+
.map(([key, candidate]) => [key, redactCliValue(candidate, key)]),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function workspaceDirs(cwd: string, workspaceFlag: string | undefined, env: NodeJS.ProcessEnv) {
|
|
297
|
+
const workdir = resolve(env.KXM_WORKDIR?.trim() || cwd);
|
|
298
|
+
const workspace = resolve(workdir, workspaceFlag || env.KXM_WORKSPACE_DIR?.trim() || ".kxm");
|
|
299
|
+
const derive = workspaceFlag !== undefined;
|
|
300
|
+
return {
|
|
301
|
+
workdir,
|
|
302
|
+
workspace,
|
|
303
|
+
config: derive ? join(workspace, "config") : resolve(workdir, env.KXM_CONFIG_DIR?.trim() || join(workspace, "config")),
|
|
304
|
+
logs: derive ? join(workspace, "logs") : resolve(workdir, env.KXM_LOGS_DIR?.trim() || join(workspace, "logs")),
|
|
305
|
+
assets: derive ? join(workspace, "assets") : resolve(workdir, env.KXM_ASSETS_DIR?.trim() || join(workspace, "assets")),
|
|
306
|
+
state: derive ? join(workspace, "state") : resolve(workdir, env.KXM_STATE_DIR?.trim() || join(workspace, "state")),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function maskEnvName(name: string): boolean {
|
|
311
|
+
return /TOKEN|SECRET|KEY|PASSWORD/i.test(name);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function redactConfiguredValues(text: string, env: NodeJS.ProcessEnv): string {
|
|
315
|
+
let safe = text;
|
|
316
|
+
for (const [name, value] of Object.entries(env)) {
|
|
317
|
+
if (!maskEnvName(name) || !value || value.length < 4) continue;
|
|
318
|
+
safe = safe.replaceAll(value, "[redacted]");
|
|
319
|
+
}
|
|
320
|
+
const trailingNewline = safe.endsWith("\n") ? "\n" : "";
|
|
321
|
+
try {
|
|
322
|
+
const parsed = JSON.parse(safe) as unknown;
|
|
323
|
+
return `${JSON.stringify(redactCliValue(parsed))}${trailingNewline}`;
|
|
324
|
+
} catch {
|
|
325
|
+
// Human-readable output and diagnostics keep conservative generic
|
|
326
|
+
// redaction, including opaque 64-hex values.
|
|
327
|
+
}
|
|
328
|
+
return redactSecrets(safe);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function processExists(pid: number): boolean {
|
|
332
|
+
try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Authenticated hub POST for context operations. The CLI operates as the
|
|
336
|
+
* control plane: the administrative token scopes one project per request. */
|
|
337
|
+
export async function hubContextPost(input: {
|
|
338
|
+
serverUrl: string;
|
|
339
|
+
path: string;
|
|
340
|
+
body: Record<string, unknown>;
|
|
341
|
+
authToken?: string;
|
|
342
|
+
fetchImpl: typeof fetch;
|
|
343
|
+
}): Promise<{ ok: boolean; status: number; body: unknown }> {
|
|
344
|
+
const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}${input.path}`, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: {
|
|
347
|
+
"content-type": "application/json",
|
|
348
|
+
...(input.authToken ? { authorization: `Bearer ${input.authToken}` } : {}),
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify(input.body),
|
|
351
|
+
});
|
|
352
|
+
const text = redactSecrets((await response.text()).slice(0, 64_000));
|
|
353
|
+
let body: unknown = text;
|
|
354
|
+
try {
|
|
355
|
+
body = JSON.parse(text);
|
|
356
|
+
} catch {
|
|
357
|
+
// Keep text for diagnostics without treating it as a secret.
|
|
358
|
+
}
|
|
359
|
+
return { ok: response.ok, status: response.status, body };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function hubGet(url: string, fetchImpl: typeof fetch): Promise<{ ok: boolean; status: number; body: unknown }> {
|
|
363
|
+
try {
|
|
364
|
+
const response = await fetchImpl(url);
|
|
365
|
+
const text = redactSecrets((await response.text()).slice(0, 8_000));
|
|
366
|
+
let body: unknown = text;
|
|
367
|
+
try {
|
|
368
|
+
body = JSON.parse(text);
|
|
369
|
+
} catch {
|
|
370
|
+
// Keep text for diagnostics without treating it as a secret.
|
|
371
|
+
}
|
|
372
|
+
return { ok: response.ok, status: response.status, body };
|
|
373
|
+
} catch {
|
|
374
|
+
return { ok: false, status: 0, body: { error: "hub_unreachable" } };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function localWorkflowSnapshot(dataPath: string, runId?: string): { runs: WorkflowRun[]; journal: WorkflowJournalEntry[] } {
|
|
379
|
+
if (!existsSync(dataPath)) throw new Error("state_database_not_found");
|
|
380
|
+
const database = new DatabaseSync(dataPath, { readOnly: true });
|
|
381
|
+
try {
|
|
382
|
+
const rows = runId
|
|
383
|
+
? database.prepare("SELECT record FROM workflow_runs WHERE id = ?").all(runId) as Array<{ record: string }>
|
|
384
|
+
: database.prepare("SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 200").all() as Array<{ record: string }>;
|
|
385
|
+
const runs = rows.map((row) => JSON.parse(row.record) as WorkflowRun);
|
|
386
|
+
const journal = runId
|
|
387
|
+
? (database.prepare("SELECT record FROM workflow_journal WHERE run_id = ? ORDER BY rowid").all(runId) as Array<{ record: string }>).map((row) => JSON.parse(row.record) as WorkflowJournalEntry)
|
|
388
|
+
: [];
|
|
389
|
+
return { runs, journal };
|
|
390
|
+
} finally { database.close(); }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function postWorkflowStart(input: { serverUrl: string; definitionId: string; secret: string; deliveryId: string; event?: string; payload: Record<string, unknown>; fetchImpl: typeof fetch }): Promise<{ status: number; runId?: string; duplicate?: boolean }> {
|
|
394
|
+
const payload = input.event && input.payload.event === undefined ? { ...input.payload, event: input.event } : input.payload;
|
|
395
|
+
const body = JSON.stringify(payload);
|
|
396
|
+
const signature = `sha256=${createHmac("sha256", input.secret).update(body).digest("hex")}`;
|
|
397
|
+
const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}/v1/webhooks/${encodeURIComponent(input.definitionId)}`, {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: { "content-type": "application/json", "x-hub-signature-256": signature, "x-kxm-delivery-id": input.deliveryId, ...(input.event ? { "x-github-event": input.event } : {}) },
|
|
400
|
+
body,
|
|
401
|
+
});
|
|
402
|
+
const responseText = (await response.text()).slice(0, 8_000);
|
|
403
|
+
let parsed: { run?: { id?: string }; duplicate?: boolean } = {};
|
|
404
|
+
try { parsed = JSON.parse(responseText) as typeof parsed; } catch { /* bounded adapter error */ }
|
|
405
|
+
if (!response.ok) throw new Error(`workflow_start_http_${response.status}`);
|
|
406
|
+
return { status: response.status, ...(parsed.run?.id ? { runId: parsed.run.id } : {}), duplicate: parsed.duplicate === true };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function postWorkflowDegradation(input: {
|
|
410
|
+
serverUrl: string;
|
|
411
|
+
authToken: string;
|
|
412
|
+
runId: string;
|
|
413
|
+
stageId: string;
|
|
414
|
+
requirementKey: string;
|
|
415
|
+
reason: string;
|
|
416
|
+
fetchImpl: typeof fetch;
|
|
417
|
+
}): Promise<{ status: number; duplicate: boolean; approvalId?: string }> {
|
|
418
|
+
const response = await input.fetchImpl(
|
|
419
|
+
`${input.serverUrl.replace(/\/$/, "")}/v1/workflows/${encodeURIComponent(input.runId)}/degradations`,
|
|
420
|
+
{
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: {
|
|
423
|
+
authorization: `Bearer ${input.authToken}`,
|
|
424
|
+
"content-type": "application/json",
|
|
425
|
+
},
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
stageId: input.stageId,
|
|
428
|
+
requirementKey: input.requirementKey,
|
|
429
|
+
reason: input.reason,
|
|
430
|
+
}),
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
const text = (await response.text()).slice(0, 8_000);
|
|
434
|
+
let parsed: { duplicate?: boolean; approval?: { id?: string } } = {};
|
|
435
|
+
try { parsed = JSON.parse(text) as typeof parsed; } catch { /* bounded adapter error */ }
|
|
436
|
+
if (!response.ok) throw new Error(`workflow_degradation_http_${response.status}`);
|
|
437
|
+
return {
|
|
438
|
+
status: response.status,
|
|
439
|
+
duplicate: parsed.duplicate === true,
|
|
440
|
+
...(parsed.approval?.id ? { approvalId: parsed.approval.id } : {}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function addGlobalOptions(command: Command): Command {
|
|
445
|
+
return command
|
|
446
|
+
.option("--json", "Print machine-readable JSON")
|
|
447
|
+
.option("--dry-run", "Plan without making changes")
|
|
448
|
+
.option("--workspace <dir>", "Workspace directory");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function runtimeFrom(ctx: CliContext, command: Command): Runtime {
|
|
452
|
+
const opts = command.optsWithGlobals() as GlobalOpts;
|
|
453
|
+
const envServerUrl = ctx.env.KXM_SERVER_URL?.trim();
|
|
454
|
+
let boundHubUrl: string | undefined;
|
|
455
|
+
try {
|
|
456
|
+
boundHubUrl = readHubBinding(ctx.env)?.url;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (error instanceof HubBindingError) {
|
|
459
|
+
ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; run kxm hub bind <url> again\n`);
|
|
460
|
+
} else {
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
...ctx,
|
|
466
|
+
json: Boolean(opts.json),
|
|
467
|
+
dryRun: Boolean(opts.dryRun),
|
|
468
|
+
...(opts.workspace === undefined ? {} : { workspaceFlag: opts.workspace }),
|
|
469
|
+
dirs: workspaceDirs(ctx.cwd, opts.workspace, ctx.env),
|
|
470
|
+
serverUrl: envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
|
|
471
|
+
...(boundHubUrl ? { boundHubUrl } : {}),
|
|
472
|
+
fetchImpl: ctx.io.fetchImpl ?? fetch,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function workspaceEnv(runtime: Runtime): NodeJS.ProcessEnv {
|
|
477
|
+
return {
|
|
478
|
+
KXM_WORKDIR: runtime.dirs.workdir,
|
|
479
|
+
KXM_WORKSPACE_DIR: runtime.dirs.workspace,
|
|
480
|
+
KXM_CONFIG_DIR: runtime.dirs.config,
|
|
481
|
+
KXM_LOGS_DIR: runtime.dirs.logs,
|
|
482
|
+
KXM_ASSETS_DIR: runtime.dirs.assets,
|
|
483
|
+
KXM_STATE_DIR: runtime.dirs.state,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function activeWorkflowDefinition(runtime: Runtime, definitionId: string) {
|
|
488
|
+
const inline = runtime.env.KXM_WEBHOOK_WORKFLOWS?.trim();
|
|
489
|
+
const file = runtime.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim();
|
|
490
|
+
if (inline && file) {
|
|
491
|
+
throw new Error("configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE");
|
|
492
|
+
}
|
|
493
|
+
if (!inline && !file) return undefined;
|
|
494
|
+
let raw: string;
|
|
495
|
+
if (file) {
|
|
496
|
+
try {
|
|
497
|
+
raw = readFileSync(resolve(runtime.cwd, file), "utf8");
|
|
498
|
+
} catch {
|
|
499
|
+
throw new Error("workflow definition file is unavailable");
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
raw = inline!;
|
|
503
|
+
}
|
|
504
|
+
const definition = parseWorkflowDefinitions(raw, runtime.env).find((candidate) => candidate.id === definitionId);
|
|
505
|
+
if (!definition) throw new Error(`workflow definition not found: ${definitionId}`);
|
|
506
|
+
return definition;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function workflowCredential(runtime: Runtime, definitionId: string, kind: "start" | "signal"): string | undefined {
|
|
510
|
+
const definition = activeWorkflowDefinition(runtime, definitionId);
|
|
511
|
+
if (definition) return kind === "start" ? definition.secret : definition.signalSecret ?? definition.secret;
|
|
512
|
+
return kind === "start"
|
|
513
|
+
? runtime.env.KXM_WORKFLOW_SECRET?.trim()
|
|
514
|
+
: runtime.env.KXM_WORKFLOW_SIGNAL_SECRET?.trim();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function reportWorkflowConfigError(runtime: Runtime, error: unknown): number {
|
|
518
|
+
const message = error instanceof Error ? redactSecrets(error.message) : "invalid workflow configuration";
|
|
519
|
+
runtime.io.stderr(`${message}\n`);
|
|
520
|
+
return 2;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function initPlanPayload(plan: VnextInitializationPlan): Record<string, unknown> {
|
|
524
|
+
return {
|
|
525
|
+
mode: plan.mode,
|
|
526
|
+
inspectedFrom: plan.inspectedFrom,
|
|
527
|
+
...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}),
|
|
528
|
+
...(plan.legacyRoot ? { legacyRoot: plan.legacyRoot } : {}),
|
|
529
|
+
changesRequired: plan.changesRequired,
|
|
530
|
+
legacyInputs: plan.legacyInputs,
|
|
531
|
+
issues: plan.issues,
|
|
532
|
+
...(plan.configRevision ? { configRevision: plan.configRevision } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function explicitRepositoryBindings(values: readonly string[]): Readonly<Record<string, string>> {
|
|
537
|
+
const result: Record<string, string> = Object.create(null) as Record<string, string>;
|
|
538
|
+
for (const value of values) {
|
|
539
|
+
const separator = value.indexOf("=");
|
|
540
|
+
const repositoryId = separator < 0 ? "" : value.slice(0, separator).trim();
|
|
541
|
+
const path = separator < 0 ? "" : value.slice(separator + 1).trim();
|
|
542
|
+
if (!repositoryId || !path) {
|
|
543
|
+
throw new VnextConfigError([{
|
|
544
|
+
phase: "discovery",
|
|
545
|
+
code: "repository_binding_argument_invalid",
|
|
546
|
+
file: "--repository",
|
|
547
|
+
message: "repository bindings must use <id=absolute-path>",
|
|
548
|
+
}]);
|
|
549
|
+
}
|
|
550
|
+
if (Object.hasOwn(result, repositoryId)) {
|
|
551
|
+
throw new VnextConfigError([{
|
|
552
|
+
phase: "discovery",
|
|
553
|
+
code: "repository_binding_argument_duplicate",
|
|
554
|
+
file: "--repository",
|
|
555
|
+
message: `repository binding ${repositoryId} was supplied more than once`,
|
|
556
|
+
}]);
|
|
557
|
+
}
|
|
558
|
+
result[repositoryId] = path;
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function cmdVnextInit(runtime: Runtime, options: { name?: string; projectId?: string; repository?: string[] }): Promise<number> {
|
|
564
|
+
if (runtime.workspaceFlag !== undefined) {
|
|
565
|
+
print(runtime.io, runtime.json, {
|
|
566
|
+
ok: false,
|
|
567
|
+
command: "init",
|
|
568
|
+
error: "workspace_option_unsupported",
|
|
569
|
+
}, "kxm init discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
570
|
+
return 2;
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const initialized = initializeVnextProject(runtime.cwd, {
|
|
574
|
+
...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
|
|
575
|
+
...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
|
|
576
|
+
repositoryBindings: explicitRepositoryBindings(options.repository ?? []),
|
|
577
|
+
localStateRoot: vnextUserStateRoot({ env: runtime.env }),
|
|
578
|
+
dryRun: runtime.dryRun,
|
|
579
|
+
});
|
|
580
|
+
const payload = {
|
|
581
|
+
ok: initialized.action !== "planned" || runtime.dryRun,
|
|
582
|
+
command: "init",
|
|
583
|
+
action: initialized.action,
|
|
584
|
+
...initPlanPayload(initialized.plan),
|
|
585
|
+
files: initialized.files,
|
|
586
|
+
...(initialized.configRevision ? { configRevision: initialized.configRevision } : {}),
|
|
587
|
+
...(initialized.localBindingFile ? { localBindingFile: initialized.localBindingFile } : {}),
|
|
588
|
+
...(initialized.bindingsChanged === undefined ? {} : { bindingsChanged: initialized.bindingsChanged }),
|
|
589
|
+
...(initialized.repairPlan === undefined ? {} : { repairPlan: initialized.repairPlan }),
|
|
590
|
+
...(initialized.resumePending === undefined ? {} : { resumePending: initialized.resumePending }),
|
|
591
|
+
...(initialized.transactionKind === undefined ? {} : { transactionKind: initialized.transactionKind }),
|
|
592
|
+
plannedOnly: initialized.action === "planned",
|
|
593
|
+
};
|
|
594
|
+
const finishInit = (code: number, text: string): number => {
|
|
595
|
+
print(runtime.io, runtime.json, payload, text);
|
|
596
|
+
return code;
|
|
597
|
+
};
|
|
598
|
+
if (initialized.action === "created") {
|
|
599
|
+
return finishInit(0, `initialized vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
600
|
+
}
|
|
601
|
+
if (initialized.action === "joined") {
|
|
602
|
+
return finishInit(0, `joined vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
603
|
+
}
|
|
604
|
+
if (initialized.action === "repaired") {
|
|
605
|
+
return finishInit(0, `repaired vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
606
|
+
}
|
|
607
|
+
if (initialized.action === "resumed") {
|
|
608
|
+
return finishInit(0, `resumed vNext ${initialized.transactionKind ?? "initialization"} at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
609
|
+
}
|
|
610
|
+
if (initialized.action === "validated") {
|
|
611
|
+
return finishInit(0, `validated vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
612
|
+
}
|
|
613
|
+
if (runtime.dryRun) {
|
|
614
|
+
return finishInit(0, `init plan: ${initialized.plan.mode}`);
|
|
615
|
+
}
|
|
616
|
+
const next = initialized.plan.mode === "migrate"
|
|
617
|
+
? "legacy state requires reviewed migration; conversion is not available in this implementation slice"
|
|
618
|
+
: initialized.repairPlan?.issues.length
|
|
619
|
+
? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
|
|
620
|
+
: "partial or provenance-free vNext state requires explicit repair; no files were overwritten";
|
|
621
|
+
return finishInit(1, next);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (error instanceof VnextConfigError) {
|
|
624
|
+
print(runtime.io, runtime.json, {
|
|
625
|
+
ok: false,
|
|
626
|
+
command: "init",
|
|
627
|
+
error: "vnext_initialization_failed",
|
|
628
|
+
issues: error.issues,
|
|
629
|
+
}, `vNext initialization failed: ${error.message}`);
|
|
630
|
+
return 1;
|
|
631
|
+
}
|
|
632
|
+
print(runtime.io, runtime.json, {
|
|
633
|
+
ok: false,
|
|
634
|
+
command: "init",
|
|
635
|
+
error: "vnext_initialization_io_failed",
|
|
636
|
+
}, "vNext initialization failed because a local filesystem operation did not complete");
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function cmdVnextMigratePlan(runtime: Runtime): Promise<number> {
|
|
642
|
+
if (runtime.workspaceFlag !== undefined) {
|
|
643
|
+
print(runtime.io, runtime.json, {
|
|
644
|
+
ok: false,
|
|
645
|
+
command: "migrate plan",
|
|
646
|
+
error: "workspace_option_unsupported",
|
|
647
|
+
}, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
648
|
+
return 2;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const result = planVnextMigration(runtime.cwd, {});
|
|
652
|
+
const ambiguities = (result.plan.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
|
|
653
|
+
const unmapped = (result.plan.unmapped as unknown[] | undefined) ?? [];
|
|
654
|
+
const payload = {
|
|
655
|
+
ok: result.plan.canApply === true,
|
|
656
|
+
command: "migrate plan",
|
|
657
|
+
plan: result.plan,
|
|
658
|
+
plannedOnly: result.plan.canApply !== true,
|
|
659
|
+
};
|
|
660
|
+
if (result.plan.canApply === true) {
|
|
661
|
+
print(runtime.io, runtime.json, payload, `migration plan: ${ambiguities.length} ambiguities, ${unmapped.length} preserved fields; ready to apply`);
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
print(
|
|
665
|
+
runtime.io,
|
|
666
|
+
runtime.json,
|
|
667
|
+
payload,
|
|
668
|
+
`migration plan requires ${ambiguities.length} reviewed decision(s):\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
|
|
669
|
+
);
|
|
670
|
+
return 1;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (error instanceof VnextConfigError) {
|
|
673
|
+
print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_failed", issues: error.issues }, `migration plan failed: ${error.message}`);
|
|
674
|
+
return 1;
|
|
675
|
+
}
|
|
676
|
+
print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_io_failed" }, "migration plan failed because a local filesystem operation did not complete");
|
|
677
|
+
return 1;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function cmdVnextMigrateApply(runtime: Runtime, options: { decisions?: string; projectId?: string; name?: string }): Promise<number> {
|
|
682
|
+
if (runtime.workspaceFlag !== undefined) {
|
|
683
|
+
print(runtime.io, runtime.json, {
|
|
684
|
+
ok: false,
|
|
685
|
+
command: "migrate apply",
|
|
686
|
+
error: "workspace_option_unsupported",
|
|
687
|
+
}, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
688
|
+
return 2;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const result = applyVnextMigration(runtime.cwd, {
|
|
692
|
+
...(options.decisions?.trim() ? { decisionsFile: options.decisions.trim() } : {}),
|
|
693
|
+
...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
|
|
694
|
+
...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
|
|
695
|
+
localStateRoot: vnextUserStateRoot({ env: runtime.env }),
|
|
696
|
+
dryRun: runtime.dryRun,
|
|
697
|
+
});
|
|
698
|
+
const payload = {
|
|
699
|
+
ok: result.action !== "planned" || (runtime.dryRun === true && result.plan?.canApply === true),
|
|
700
|
+
command: "migrate apply",
|
|
701
|
+
action: result.action,
|
|
702
|
+
files: result.files,
|
|
703
|
+
...(result.configRevision ? { configRevision: result.configRevision } : {}),
|
|
704
|
+
...(result.receiptPath ? { receiptPath: result.receiptPath } : {}),
|
|
705
|
+
plannedOnly: result.action === "planned",
|
|
706
|
+
};
|
|
707
|
+
if (result.action === "applied") {
|
|
708
|
+
print(runtime.io, runtime.json, payload, `migration applied: ${result.files.length} resources installed, receipt at ${result.receiptPath ?? ""}`);
|
|
709
|
+
return 0;
|
|
710
|
+
}
|
|
711
|
+
if (result.action === "already-migrated") {
|
|
712
|
+
print(runtime.io, runtime.json, payload, "migration receipt already exists; nothing to apply");
|
|
713
|
+
return 0;
|
|
714
|
+
}
|
|
715
|
+
if (runtime.dryRun && result.plan?.canApply === true) {
|
|
716
|
+
print(runtime.io, runtime.json, payload, `migration dry run: ${result.files.length} resources would be installed`);
|
|
717
|
+
return 0;
|
|
718
|
+
}
|
|
719
|
+
const ambiguities = (result.plan?.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
|
|
720
|
+
print(
|
|
721
|
+
runtime.io,
|
|
722
|
+
runtime.json,
|
|
723
|
+
{ ...payload, plan: result.plan },
|
|
724
|
+
`migration blocked by ${ambiguities.length} unresolved decision(s); review 'kxm migrate plan' and pass --decisions:\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
|
|
725
|
+
);
|
|
726
|
+
return 1;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (error instanceof VnextConfigError) {
|
|
729
|
+
print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_failed", issues: error.issues }, `migration apply failed: ${error.message}`);
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
732
|
+
print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_io_failed" }, "migration apply failed because a local filesystem operation did not complete");
|
|
733
|
+
return 1;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function cmdVnextMigrateVerify(runtime: Runtime): Promise<number> {
|
|
738
|
+
if (runtime.workspaceFlag !== undefined) {
|
|
739
|
+
print(runtime.io, runtime.json, {
|
|
740
|
+
ok: false,
|
|
741
|
+
command: "migrate verify",
|
|
742
|
+
error: "workspace_option_unsupported",
|
|
743
|
+
}, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
744
|
+
return 2;
|
|
745
|
+
}
|
|
746
|
+
let result: ReturnType<typeof verifyVnextMigration>;
|
|
747
|
+
try {
|
|
748
|
+
result = verifyVnextMigration(runtime.cwd, {});
|
|
749
|
+
} catch (error) {
|
|
750
|
+
if (error instanceof VnextConfigError) {
|
|
751
|
+
print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_failed", issues: error.issues }, `migration verification failed: ${error.message}`);
|
|
752
|
+
return 1;
|
|
753
|
+
}
|
|
754
|
+
print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_io_failed" }, "migration verification failed because a local filesystem operation did not complete");
|
|
755
|
+
return 1;
|
|
756
|
+
}
|
|
757
|
+
const payload = {
|
|
758
|
+
ok: result.ok,
|
|
759
|
+
command: "migrate verify",
|
|
760
|
+
...(result.configRevision ? { configRevision: result.configRevision } : {}),
|
|
761
|
+
issues: result.issues,
|
|
762
|
+
};
|
|
763
|
+
if (result.ok) {
|
|
764
|
+
print(runtime.io, runtime.json, payload, `migration receipt verified: legacy sources unchanged, target bundle matches ${result.configRevision ?? ""}`);
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
767
|
+
print(runtime.io, runtime.json, payload, `migration verification failed:\n${result.issues.map((issue) => ` - ${issue.file}: ${issue.code}: ${issue.message}`).join("\n")}`);
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function cmdBackup(runtime: Runtime, options: { out?: string }): Promise<number> {
|
|
772
|
+
try {
|
|
773
|
+
const { manifest, outDir } = createBackup({
|
|
774
|
+
projectRoot: runtime.cwd,
|
|
775
|
+
...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
|
|
776
|
+
});
|
|
777
|
+
const payload = {
|
|
778
|
+
ok: true,
|
|
779
|
+
command: "backup",
|
|
780
|
+
backupId: manifest.backupId,
|
|
781
|
+
outDir,
|
|
782
|
+
manifest,
|
|
783
|
+
};
|
|
784
|
+
const summary = [
|
|
785
|
+
`Created SQLite backup with ${manifest.stores.length} store(s):`,
|
|
786
|
+
...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
|
|
787
|
+
`Manifest: ${join(outDir, "manifest.json")}`,
|
|
788
|
+
].join("\n");
|
|
789
|
+
print(runtime.io, runtime.json, payload, summary);
|
|
790
|
+
return 0;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
if (error instanceof VnextConfigError) {
|
|
793
|
+
print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", issues: error.issues }, `backup failed: ${error.message}`);
|
|
794
|
+
return 1;
|
|
795
|
+
}
|
|
796
|
+
print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", message: error instanceof Error ? error.message : String(error) }, `backup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
797
|
+
return 1;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async function cmdRestore(runtime: Runtime, manifestArg: string): Promise<number> {
|
|
802
|
+
try {
|
|
803
|
+
const result = restoreBackup(resolve(runtime.cwd, manifestArg), {
|
|
804
|
+
projectRoot: runtime.cwd,
|
|
805
|
+
});
|
|
806
|
+
const payload = {
|
|
807
|
+
ok: true,
|
|
808
|
+
command: "restore",
|
|
809
|
+
backupId: result.backupId,
|
|
810
|
+
manifestPath: result.manifestPath,
|
|
811
|
+
restoredStores: result.restoredStores,
|
|
812
|
+
};
|
|
813
|
+
const summary = [
|
|
814
|
+
`Restored ${result.restoredStores.length} SQLite store(s) from ${result.manifestPath}:`,
|
|
815
|
+
...result.restoredStores.map((s) => ` - ${s.storeId}: -> ${s.sourcePath} (schema v${s.schemaVersion}, integrity ${s.integrity})`),
|
|
816
|
+
].join("\n");
|
|
817
|
+
print(runtime.io, runtime.json, payload, summary);
|
|
818
|
+
return 0;
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (error instanceof VnextConfigError) {
|
|
821
|
+
print(runtime.io, runtime.json, { ok: false, command: "restore", error: "restore_failed", issues: error.issues }, `restore failed: ${error.message}`);
|
|
822
|
+
return 1;
|
|
823
|
+
}
|
|
824
|
+
print(runtime.io, runtime.json, { ok: false, command: "restore", error: "restore_failed", message: error instanceof Error ? error.message : String(error) }, `restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
825
|
+
return 1;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function cmdVnextTrust(runtime: Runtime, check: boolean, options: { base?: string }): Promise<number> {
|
|
830
|
+
if (runtime.workspaceFlag !== undefined) {
|
|
831
|
+
print(runtime.io, runtime.json, {
|
|
832
|
+
ok: false,
|
|
833
|
+
command: check ? "trust check" : "trust diff",
|
|
834
|
+
error: "workspace_option_unsupported",
|
|
835
|
+
}, "kxm trust discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
836
|
+
return 2;
|
|
837
|
+
}
|
|
838
|
+
const command = check ? "trust check" : "trust diff";
|
|
839
|
+
try {
|
|
840
|
+
// Host-local member bindings travel with the user state, not Git; pass
|
|
841
|
+
// them so member repositories diff with the same identities on both sides.
|
|
842
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
843
|
+
const bindings = readVnextLocalBindings(projectRoot, { stateRoot: vnextUserStateRoot({ env: runtime.env }) });
|
|
844
|
+
const diff = diffVnextProjectAgainstRevision(projectRoot, options.base?.trim() || "HEAD", {
|
|
845
|
+
repositoryBindings: bindings?.repositories ?? {},
|
|
846
|
+
});
|
|
847
|
+
const payload = {
|
|
848
|
+
ok: !check || !diff.requiresReview,
|
|
849
|
+
command,
|
|
850
|
+
baseRevision: diff.baseRevision,
|
|
851
|
+
candidateRevision: diff.candidateRevision,
|
|
852
|
+
requiresReview: diff.requiresReview,
|
|
853
|
+
expansions: diff.expansions.length,
|
|
854
|
+
narrowings: diff.narrowings.length,
|
|
855
|
+
neutralChanges: diff.neutralChanges.length,
|
|
856
|
+
changes: diff.changes,
|
|
857
|
+
};
|
|
858
|
+
const text = formatVnextPermissionDiff(diff);
|
|
859
|
+
if (check && diff.requiresReview) {
|
|
860
|
+
print(runtime.io, runtime.json, payload, `${text}\ntrust check failed: review every expansion above before merging`);
|
|
861
|
+
return 1;
|
|
862
|
+
}
|
|
863
|
+
print(runtime.io, runtime.json, payload, text);
|
|
864
|
+
return 0;
|
|
865
|
+
} catch (error) {
|
|
866
|
+
if (error instanceof VnextConfigError) {
|
|
867
|
+
print(runtime.io, runtime.json, { ok: false, command, error: "trust_diff_failed", issues: error.issues }, `permission diff failed: ${error.message}`);
|
|
868
|
+
return 1;
|
|
869
|
+
}
|
|
870
|
+
print(runtime.io, runtime.json, { ok: false, command, error: "trust_diff_io_failed" }, "permission diff failed because a local filesystem or Git operation did not complete");
|
|
871
|
+
return 1;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** No step engine exists yet: `kxm run` durably accepts a run and stops.
|
|
876
|
+
* The run engine replaces this notice with real progress. */
|
|
877
|
+
const RUN_ENGINE_PHASE = "pre-3a";
|
|
878
|
+
const RUN_ENGINE_NOTICE = "runs remain created until the run engine lands; no steps execute yet";
|
|
879
|
+
|
|
880
|
+
async function cmdVnextRun(runtime: Runtime, workflow: string | undefined, promptParts: string[]): Promise<number> {
|
|
881
|
+
if (runtime.workspaceFlag !== undefined) {
|
|
882
|
+
print(runtime.io, runtime.json, {
|
|
883
|
+
ok: false,
|
|
884
|
+
command: "run",
|
|
885
|
+
error: "workspace_option_unsupported",
|
|
886
|
+
}, "kxm run discovers the authoritative project from the current directory; --workspace is not supported");
|
|
887
|
+
return 2;
|
|
888
|
+
}
|
|
889
|
+
if (!workflow) {
|
|
890
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "workflow_required" }, "usage: kxm run <workflow> [prompt]");
|
|
891
|
+
return 2;
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
895
|
+
if (!projectRoot) {
|
|
896
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "project_required" }, "kxm run requires a vNext project (run kxm init first)");
|
|
897
|
+
return 1;
|
|
898
|
+
}
|
|
899
|
+
const bundle = loadVnextProject(projectRoot, {});
|
|
900
|
+
if (!bundle.workflows.has(workflow)) {
|
|
901
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow }, `workflow ${workflow} does not exist in this project`);
|
|
902
|
+
return 1;
|
|
903
|
+
}
|
|
904
|
+
if (runtime.dryRun) {
|
|
905
|
+
print(runtime.io, runtime.json, {
|
|
906
|
+
ok: true,
|
|
907
|
+
command: "run",
|
|
908
|
+
dryRun: true,
|
|
909
|
+
projectRoot,
|
|
910
|
+
workflowId: workflow,
|
|
911
|
+
configRevision: bundle.configRevision,
|
|
912
|
+
}, `run plan: workflow ${workflow} at ${bundle.configRevision.slice(0, 19)}… (no run created)`);
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
916
|
+
const prompt = promptParts.join(" ").trim();
|
|
917
|
+
const acceptance = await vnextRuntimeRequest(supervisor, "POST", "/v1/runs", {
|
|
918
|
+
projectRoot,
|
|
919
|
+
workflowId: workflow,
|
|
920
|
+
prompt,
|
|
921
|
+
});
|
|
922
|
+
const run = acceptance.run as { runId: string; homeRuntimeId: string; status: string; configRevision: string };
|
|
923
|
+
print(runtime.io, runtime.json, {
|
|
924
|
+
ok: true,
|
|
925
|
+
command: "run",
|
|
926
|
+
phase: RUN_ENGINE_PHASE,
|
|
927
|
+
idempotent: acceptance.idempotent === true,
|
|
928
|
+
run,
|
|
929
|
+
supervisor: { runtimeId: supervisor.runtimeId, port: supervisor.port, started: supervisor.started },
|
|
930
|
+
}, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${RUN_ENGINE_NOTICE}`);
|
|
931
|
+
return 0;
|
|
932
|
+
} catch (error) {
|
|
933
|
+
if (error instanceof VnextConfigError) {
|
|
934
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_failed", issues: error.issues }, `run failed: ${error.message}`);
|
|
935
|
+
return 1;
|
|
936
|
+
}
|
|
937
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_io_failed" }, "run failed because a local operation did not complete");
|
|
938
|
+
return 1;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<number> {
|
|
943
|
+
try {
|
|
944
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
945
|
+
if (!projectRoot) {
|
|
946
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "project_required" }, "kxm runs status requires a vNext project (run kxm init first)");
|
|
947
|
+
return 1;
|
|
948
|
+
}
|
|
949
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
950
|
+
const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
951
|
+
const run = result.run as { runId: string; status: string; workflowId: string; configRevision: string; updatedAt: string };
|
|
952
|
+
print(runtime.io, runtime.json, { ok: true, command: "runs status", run }, `run ${run.runId}: ${run.status} (workflow ${run.workflowId}, updated ${run.updatedAt})`);
|
|
953
|
+
return 0;
|
|
954
|
+
} catch (error) {
|
|
955
|
+
if (error instanceof VnextConfigError) {
|
|
956
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "run_status_failed", issues: error.issues }, `run status failed: ${error.message}`);
|
|
957
|
+
return 1;
|
|
958
|
+
}
|
|
959
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "run_status_io_failed" }, "run status failed because a local operation did not complete");
|
|
960
|
+
return 1;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function cmdVnextRunCancel(runtime: Runtime, runId: string): Promise<number> {
|
|
965
|
+
try {
|
|
966
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
967
|
+
if (!projectRoot) {
|
|
968
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "project_required" }, "kxm runs cancel requires a vNext project (run kxm init first)");
|
|
969
|
+
return 1;
|
|
970
|
+
}
|
|
971
|
+
if (runtime.dryRun) {
|
|
972
|
+
print(runtime.io, runtime.json, { ok: true, command: "runs cancel", dryRun: true, runId }, `cancel plan: run ${runId} (no events written)`);
|
|
973
|
+
return 0;
|
|
974
|
+
}
|
|
975
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
976
|
+
const result = await vnextRuntimeRequest(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/cancel?projectRoot=${encodeURIComponent(projectRoot)}`, {});
|
|
977
|
+
const run = result.run as { runId: string; status: string };
|
|
978
|
+
print(runtime.io, runtime.json, {
|
|
979
|
+
ok: true,
|
|
980
|
+
command: "runs cancel",
|
|
981
|
+
idempotent: result.idempotent === true,
|
|
982
|
+
run,
|
|
983
|
+
}, `run ${run.runId}: ${run.status}`);
|
|
984
|
+
return 0;
|
|
985
|
+
} catch (error) {
|
|
986
|
+
if (error instanceof VnextConfigError) {
|
|
987
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "run_cancel_failed", issues: error.issues }, `run cancel failed: ${error.message}`);
|
|
988
|
+
return 1;
|
|
989
|
+
}
|
|
990
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "run_cancel_io_failed" }, "run cancel failed because a local operation did not complete");
|
|
991
|
+
return 1;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function cmdVnextRunList(runtime: Runtime): Promise<number> {
|
|
996
|
+
try {
|
|
997
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
998
|
+
if (!projectRoot) {
|
|
999
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "project_required" }, "kxm runs list requires a vNext project (run kxm init first)");
|
|
1000
|
+
return 1;
|
|
1001
|
+
}
|
|
1002
|
+
const bundle = loadVnextProject(projectRoot, {});
|
|
1003
|
+
const projectId = String(bundle.project.value.id);
|
|
1004
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
1005
|
+
const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/projects/${encodeURIComponent(projectId)}/runs?projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
1006
|
+
const runs = (result.runs ?? []) as Array<{ runId: string; status: string; workflowId: string; createdAt: string }>;
|
|
1007
|
+
print(
|
|
1008
|
+
runtime.io,
|
|
1009
|
+
runtime.json,
|
|
1010
|
+
{ ok: true, command: "runs list", runs },
|
|
1011
|
+
runs.length === 0 ? "no runs" : runs.map((run) => `${run.runId} ${run.status} ${run.workflowId} ${run.createdAt}`).join("\n"),
|
|
1012
|
+
);
|
|
1013
|
+
return 0;
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
if (error instanceof VnextConfigError) {
|
|
1016
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "run_list_failed", issues: error.issues }, `run list failed: ${error.message}`);
|
|
1017
|
+
return 1;
|
|
1018
|
+
}
|
|
1019
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "run_list_io_failed" }, "run list failed because a local operation did not complete");
|
|
1020
|
+
return 1;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function cmdHarnessList(runtime: Runtime): Promise<number> {
|
|
1025
|
+
const inventory = probeHarnesses({ env: runtime.env });
|
|
1026
|
+
print(runtime.io, runtime.json, { ok: true, command: "harness list", ...inventory }, formatHarnessInventory(inventory));
|
|
1027
|
+
return 0;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function installProbeFrom(runtime: Runtime): InstallProbe {
|
|
1031
|
+
const partial = runtime.io.installProbe ?? {};
|
|
1032
|
+
return {
|
|
1033
|
+
moduleDir: partial.moduleDir ?? dirname(fileURLToPath(import.meta.url)),
|
|
1034
|
+
repoRoot: partial.repoRoot ?? repoRoot,
|
|
1035
|
+
homeDir: partial.homeDir ?? homedir(),
|
|
1036
|
+
platform: partial.platform ?? process.platform,
|
|
1037
|
+
env: partial.env ?? runtime.env,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function cliSpawn(runtime: Runtime, command: string, args: readonly string[], extra?: { timeout?: number }): CliSpawnResult {
|
|
1042
|
+
if (runtime.io.spawnSync) return runtime.io.spawnSync(command, args);
|
|
1043
|
+
const result = spawnSync(command, [...args], {
|
|
1044
|
+
encoding: "utf8",
|
|
1045
|
+
windowsHide: true,
|
|
1046
|
+
shell: process.platform === "win32",
|
|
1047
|
+
...extra,
|
|
1048
|
+
});
|
|
1049
|
+
return {
|
|
1050
|
+
status: result.status,
|
|
1051
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
1052
|
+
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
1053
|
+
...(result.error ? { error: result.error } : {}),
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function npmGlobalRootFn(runtime: Runtime): () => string | undefined {
|
|
1058
|
+
return () => {
|
|
1059
|
+
const result = cliSpawn(runtime, "npm", ["root", "-g"], { timeout: 5_000 });
|
|
1060
|
+
if (result.error || result.status !== 0) return undefined;
|
|
1061
|
+
const out = result.stdout.trim();
|
|
1062
|
+
return out || undefined;
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function warnIgnoredProjectUpdateYaml(runtime: Runtime): void {
|
|
1067
|
+
const projectFile = join(runtime.dirs.workspace, "update.yaml");
|
|
1068
|
+
if (!existsSync(projectFile)) return;
|
|
1069
|
+
const userFile = join(vnextUserStateRoot({ env: runtime.env }), "update.yaml");
|
|
1070
|
+
runtime.io.stderr(`kxm: ignoring .kxm/update.yaml in ${runtime.dirs.workdir}; update settings are read only from ${userFile}\n`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function installKindPayload(report: InstallKindReport): { installKind: string; root: string } {
|
|
1074
|
+
return { installKind: report.kind, root: report.root };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function formatPackageUpdateStep(step: KxmPackageUpdateStep): string {
|
|
1078
|
+
if (step.kind === "verify") return `verify sha256 ${step.path}`;
|
|
1079
|
+
return `${step.command} ${step.args.join(" ")}`;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async function refreshKxmUpdateNotice(runtime: Runtime, config?: KxmUpdateConfig): Promise<KxmUpdateNotice> {
|
|
1083
|
+
const resolved = config ?? loadKxmUpdateConfig(runtime.env);
|
|
1084
|
+
const current = readInstalledKxmVersion(repoRoot);
|
|
1085
|
+
const fetched = await fetchLatestKxmVersion(resolved.source, runtime.env, runtime.fetchImpl);
|
|
1086
|
+
const notice = noticeFromVersions(current, fetched.latest, resolved, fetched.error, fetched.asset);
|
|
1087
|
+
writeUpdateCache(runtime.dirs.state, notice);
|
|
1088
|
+
return notice;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice): { ok: boolean; detail: string; error?: string } {
|
|
1092
|
+
if (!notice.latest) return { ok: false, detail: "no_latest_version" };
|
|
1093
|
+
if (notice.source === "github" && !notice.asset?.sha256) {
|
|
1094
|
+
const name = kxmReleaseAssetName(notice.latest);
|
|
1095
|
+
return {
|
|
1096
|
+
ok: false,
|
|
1097
|
+
error: "release_digest_missing",
|
|
1098
|
+
detail: `release v${notice.latest} has no sha256 digest for ${name}; refusing to install`,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const releaseDir = mkdtempSync(join(tmpdir(), "kxm-pkg-update-"));
|
|
1102
|
+
try {
|
|
1103
|
+
const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset);
|
|
1104
|
+
if (runtime.dryRun) {
|
|
1105
|
+
return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
|
|
1106
|
+
}
|
|
1107
|
+
for (const step of planned) {
|
|
1108
|
+
if (step.kind === "verify") {
|
|
1109
|
+
if (!verifyReleaseAssetDigest(step.path, step.sha256)) {
|
|
1110
|
+
const actual = existsSync(step.path)
|
|
1111
|
+
? createHash("sha256").update(readFileSync(step.path)).digest("hex")
|
|
1112
|
+
: "missing";
|
|
1113
|
+
return {
|
|
1114
|
+
ok: false,
|
|
1115
|
+
error: "release_digest_mismatch",
|
|
1116
|
+
detail: `${kxmReleaseAssetName(notice.latest)} sha256 ${actual} does not match release digest ${step.sha256}; refusing to install`,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const result = cliSpawn(runtime, step.command, step.args);
|
|
1122
|
+
if (result.status !== 0) {
|
|
1123
|
+
const detail = (result.stderr || result.stdout || result.error?.message || "update_failed").trim().slice(0, 500);
|
|
1124
|
+
return { ok: false, detail };
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return { ok: true, detail: `installed ${notice.latest} from ${notice.source}` };
|
|
1128
|
+
} finally {
|
|
1129
|
+
rmSync(releaseDir, { recursive: true, force: true });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function cmdUpdate(runtime: Runtime, harness: string | undefined, options: {
|
|
1134
|
+
self?: boolean;
|
|
1135
|
+
extensions?: boolean;
|
|
1136
|
+
models?: boolean;
|
|
1137
|
+
check?: boolean;
|
|
1138
|
+
kxm?: boolean;
|
|
1139
|
+
}): Promise<number> {
|
|
1140
|
+
if (options.check && (options.kxm || options.self || options.extensions || options.models || harness)) {
|
|
1141
|
+
print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "--check cannot be combined with other update flags");
|
|
1142
|
+
return 2;
|
|
1143
|
+
}
|
|
1144
|
+
const selected = [options.self, options.extensions, options.models].filter(Boolean).length;
|
|
1145
|
+
if (selected > 1) {
|
|
1146
|
+
print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "specify at most one of --self, --extensions, or --models");
|
|
1147
|
+
return 2;
|
|
1148
|
+
}
|
|
1149
|
+
warnIgnoredProjectUpdateYaml(runtime);
|
|
1150
|
+
const probe = installProbeFrom(runtime);
|
|
1151
|
+
const classified = classifyInstallRoot(probe);
|
|
1152
|
+
const current = readInstalledKxmVersion(repoRoot);
|
|
1153
|
+
let notice: KxmUpdateNotice;
|
|
1154
|
+
let kindReport = classified;
|
|
1155
|
+
if (classified.kind === "source") {
|
|
1156
|
+
if (options.check) {
|
|
1157
|
+
const message = `kxm ${current} (running from source at ${classified.root})`;
|
|
1158
|
+
print(runtime.io, runtime.json, {
|
|
1159
|
+
ok: true,
|
|
1160
|
+
command: "update check",
|
|
1161
|
+
current,
|
|
1162
|
+
available: false,
|
|
1163
|
+
auto: false,
|
|
1164
|
+
source: "github",
|
|
1165
|
+
installKind: "source",
|
|
1166
|
+
root: classified.root,
|
|
1167
|
+
message,
|
|
1168
|
+
}, message);
|
|
1169
|
+
return 0;
|
|
1170
|
+
}
|
|
1171
|
+
if (options.kxm) {
|
|
1172
|
+
print(runtime.io, runtime.json, {
|
|
1173
|
+
ok: false,
|
|
1174
|
+
command: "update",
|
|
1175
|
+
error: "install_kind_source",
|
|
1176
|
+
installKind: "source",
|
|
1177
|
+
root: classified.root,
|
|
1178
|
+
instruction: classified.instruction,
|
|
1179
|
+
}, classified.instruction);
|
|
1180
|
+
return 2;
|
|
1181
|
+
}
|
|
1182
|
+
notice = {
|
|
1183
|
+
current,
|
|
1184
|
+
available: false,
|
|
1185
|
+
auto: false,
|
|
1186
|
+
source: "github",
|
|
1187
|
+
message: `kxm ${current} (running from source)`,
|
|
1188
|
+
};
|
|
1189
|
+
} else {
|
|
1190
|
+
try {
|
|
1191
|
+
notice = await refreshKxmUpdateNotice(runtime);
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
if (error instanceof KxmUpdateConfigError) {
|
|
1194
|
+
print(runtime.io, runtime.json, { ok: false, command: "update", error: error.code, ...installKindPayload(classified) }, error.message);
|
|
1195
|
+
return 2;
|
|
1196
|
+
}
|
|
1197
|
+
throw error;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (options.check) {
|
|
1201
|
+
print(runtime.io, runtime.json, {
|
|
1202
|
+
ok: true,
|
|
1203
|
+
command: "update check",
|
|
1204
|
+
...notice,
|
|
1205
|
+
...installKindPayload(classified),
|
|
1206
|
+
}, notice.message);
|
|
1207
|
+
return 0;
|
|
1208
|
+
}
|
|
1209
|
+
const applyKxm = Boolean(options.kxm || notice.auto);
|
|
1210
|
+
let kxmApply: { ok: boolean; detail: string; error?: string } | undefined;
|
|
1211
|
+
if (applyKxm) {
|
|
1212
|
+
const resolved = resolveInstallKind(probe, npmGlobalRootFn(runtime));
|
|
1213
|
+
kindReport = resolved;
|
|
1214
|
+
if (resolved.kind !== "npm-global") {
|
|
1215
|
+
if (options.kxm) {
|
|
1216
|
+
print(runtime.io, runtime.json, {
|
|
1217
|
+
ok: false,
|
|
1218
|
+
command: "update",
|
|
1219
|
+
error: `install_kind_${resolved.kind}`,
|
|
1220
|
+
installKind: resolved.kind,
|
|
1221
|
+
root: resolved.root,
|
|
1222
|
+
instruction: resolved.instruction,
|
|
1223
|
+
notice,
|
|
1224
|
+
}, resolved.instruction);
|
|
1225
|
+
return 2;
|
|
1226
|
+
}
|
|
1227
|
+
if (notice.available) runtime.io.stderr(`kxm: ${resolved.instruction}\n`);
|
|
1228
|
+
} else if (notice.available) {
|
|
1229
|
+
kxmApply = applyKxmPackageUpdate(runtime, notice);
|
|
1230
|
+
if (!kxmApply.ok && options.kxm) {
|
|
1231
|
+
print(runtime.io, runtime.json, {
|
|
1232
|
+
ok: false,
|
|
1233
|
+
command: "update",
|
|
1234
|
+
error: kxmApply.error,
|
|
1235
|
+
kxm: kxmApply,
|
|
1236
|
+
notice,
|
|
1237
|
+
...installKindPayload(resolved),
|
|
1238
|
+
}, kxmApply.detail);
|
|
1239
|
+
return 1;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
const skipHarness = Boolean(options.kxm && selected === 0 && !harness && !notice.auto);
|
|
1244
|
+
if (skipHarness) {
|
|
1245
|
+
print(
|
|
1246
|
+
runtime.io,
|
|
1247
|
+
runtime.json,
|
|
1248
|
+
{ ok: kxmApply?.ok !== false, command: "update", dryRun: runtime.dryRun, notice, kxm: kxmApply, ...installKindPayload(kindReport) },
|
|
1249
|
+
kxmApply?.detail ?? notice.message,
|
|
1250
|
+
);
|
|
1251
|
+
return kxmApply?.ok === false ? 1 : 0;
|
|
1252
|
+
}
|
|
1253
|
+
const scope: HarnessUpdateScope = options.self ? "self" : options.extensions ? "extensions" : options.models ? "models" : "all";
|
|
1254
|
+
const inventory = probeHarnesses({ env: runtime.env });
|
|
1255
|
+
const planned = planHarnessUpdate(inventory, { ...(harness ? { harness } : {}), scope });
|
|
1256
|
+
const steps = runHarnessUpdate(planned, { env: runtime.env, dryRun: runtime.dryRun });
|
|
1257
|
+
const failed = steps.some((step) => step.outcome === "failed") || kxmApply?.ok === false;
|
|
1258
|
+
const skippedUnknown = steps.some((step) => step.detail === "unknown_harness");
|
|
1259
|
+
const text = [notice.available ? notice.message : undefined, kxmApply?.detail, formatHarnessUpdate(steps)].filter(Boolean).join("\n");
|
|
1260
|
+
print(runtime.io, runtime.json, {
|
|
1261
|
+
ok: !failed && !skippedUnknown,
|
|
1262
|
+
command: "update",
|
|
1263
|
+
dryRun: runtime.dryRun,
|
|
1264
|
+
scope,
|
|
1265
|
+
notice,
|
|
1266
|
+
...(kxmApply ? { kxm: kxmApply } : {}),
|
|
1267
|
+
steps,
|
|
1268
|
+
...installKindPayload(kindReport),
|
|
1269
|
+
}, text);
|
|
1270
|
+
if (skippedUnknown) return 2;
|
|
1271
|
+
return failed ? 1 : 0;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async function cmdVnextRuntime(runtime: Runtime, action: string): Promise<number> {
|
|
1275
|
+
const paths = vnextRuntimePaths({ env: runtime.env });
|
|
1276
|
+
try {
|
|
1277
|
+
if (action === "start") {
|
|
1278
|
+
if (runtime.dryRun) {
|
|
1279
|
+
print(runtime.io, runtime.json, { ok: true, command: "runtime start", dryRun: true }, "runtime supervisor would auto-start");
|
|
1280
|
+
return 0;
|
|
1281
|
+
}
|
|
1282
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
1283
|
+
print(runtime.io, runtime.json, {
|
|
1284
|
+
ok: true,
|
|
1285
|
+
command: "runtime start",
|
|
1286
|
+
runtimeId: supervisor.runtimeId,
|
|
1287
|
+
port: supervisor.port,
|
|
1288
|
+
started: supervisor.started,
|
|
1289
|
+
}, `runtime supervisor ${supervisor.started ? "started" : "already running"}: ${supervisor.runtimeId} on 127.0.0.1:${supervisor.port}`);
|
|
1290
|
+
return 0;
|
|
1291
|
+
}
|
|
1292
|
+
if (action === "status") {
|
|
1293
|
+
const status = vnextSupervisorStatus(paths);
|
|
1294
|
+
print(runtime.io, runtime.json, { ok: true, command: "runtime status", ...status }, status.running
|
|
1295
|
+
? `runtime supervisor running: ${status.runtimeId} pid ${status.pid} on 127.0.0.1:${status.port}`
|
|
1296
|
+
: "runtime supervisor is not running");
|
|
1297
|
+
return status.running ? 0 : 1;
|
|
1298
|
+
}
|
|
1299
|
+
if (action === "stop") {
|
|
1300
|
+
const status = vnextSupervisorStatus(paths);
|
|
1301
|
+
if (!status.running || !status.port) {
|
|
1302
|
+
print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: false }, "runtime supervisor is not running");
|
|
1303
|
+
return 0;
|
|
1304
|
+
}
|
|
1305
|
+
if (runtime.dryRun) {
|
|
1306
|
+
print(runtime.io, runtime.json, { ok: true, command: "runtime stop", dryRun: true }, `would stop runtime supervisor pid ${status.pid}`);
|
|
1307
|
+
return 0;
|
|
1308
|
+
}
|
|
1309
|
+
// The stop request posts a bearer token, so prove the listener is the
|
|
1310
|
+
// real supervisor (keyed healthz challenge) before sending it.
|
|
1311
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
1312
|
+
await vnextRuntimeRequest(supervisor, "POST", "/v1/shutdown", {});
|
|
1313
|
+
print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: true }, `runtime supervisor ${status.runtimeId} stopping`);
|
|
1314
|
+
return 0;
|
|
1315
|
+
}
|
|
1316
|
+
print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "unknown_action" }, `unknown runtime action: ${action}`);
|
|
1317
|
+
return 2;
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
if (error instanceof VnextConfigError) {
|
|
1320
|
+
print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_failed", issues: error.issues }, `runtime failed: ${error.message}`);
|
|
1321
|
+
return 1;
|
|
1322
|
+
}
|
|
1323
|
+
print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_io_failed" }, "runtime failed because a local operation did not complete");
|
|
1324
|
+
return 1;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
async function cmdValidate(runtime: Runtime, fileFlag?: string): Promise<number> {
|
|
1329
|
+
const worker = gateOf(runtime, "validate");
|
|
1330
|
+
const explicitFile = fileFlag?.trim();
|
|
1331
|
+
const configuredFile = runtime.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim();
|
|
1332
|
+
const inline = runtime.env.KXM_WEBHOOK_WORKFLOWS?.trim();
|
|
1333
|
+
if (!explicitFile && configuredFile && inline) {
|
|
1334
|
+
printWorker(
|
|
1335
|
+
runtime,
|
|
1336
|
+
worker,
|
|
1337
|
+
{ ok: false, command: "validate", error: "ambiguous_workflow_source" },
|
|
1338
|
+
"configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE",
|
|
1339
|
+
);
|
|
1340
|
+
return 2;
|
|
1341
|
+
}
|
|
1342
|
+
if (!explicitFile && !configuredFile && !inline) {
|
|
1343
|
+
printWorker(
|
|
1344
|
+
runtime,
|
|
1345
|
+
worker,
|
|
1346
|
+
{ ok: false, command: "validate", error: "workflow_source_required" },
|
|
1347
|
+
"provide --file or configure exactly one workflow source environment variable",
|
|
1348
|
+
);
|
|
1349
|
+
return 2;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const selectedFile = explicitFile || configuredFile;
|
|
1353
|
+
const file = selectedFile ? resolve(runtime.cwd, selectedFile) : undefined;
|
|
1354
|
+
if (file && !existsSync(file)) {
|
|
1355
|
+
printWorker(runtime, worker, { ok: false, command: "validate", error: "file_not_found", file }, `workflow file not found: ${file}`);
|
|
1356
|
+
return 1;
|
|
1357
|
+
}
|
|
1358
|
+
try {
|
|
1359
|
+
const raw = file ? readFileSync(file, "utf8") : inline!;
|
|
1360
|
+
const warnings: string[] = [];
|
|
1361
|
+
const definitions = parseWorkflowDefinitions(raw, runtime.env, (message) => warnings.push(message));
|
|
1362
|
+
const secretEnvs = definitions.map((definition) => ({
|
|
1363
|
+
id: definition.id,
|
|
1364
|
+
secretConfigured: Boolean(definition.secret),
|
|
1365
|
+
signalSecretConfigured: Boolean(definition.signalSecret),
|
|
1366
|
+
}));
|
|
1367
|
+
const source = file ? "file" : "inline";
|
|
1368
|
+
printWorker(
|
|
1369
|
+
runtime,
|
|
1370
|
+
worker,
|
|
1371
|
+
{ ok: true, command: "validate", source, ...(file ? { file } : {}), workflows: secretEnvs, warnings },
|
|
1372
|
+
`validated ${definitions.length} workflow(s) from ${source}${warnings.length ? ` with ${warnings.length} warning(s)` : ""}`,
|
|
1373
|
+
warnings.length ? "warning" : undefined,
|
|
1374
|
+
);
|
|
1375
|
+
return 0;
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
const message = error instanceof Error ? redactSecrets(error.message) : "invalid_workflow";
|
|
1378
|
+
printWorker(runtime, worker, { ok: false, command: "validate", error: message }, message);
|
|
1379
|
+
return 1;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
async function cmdArtifactsExist(runtime: Runtime, pathFlag: string): Promise<number> {
|
|
1384
|
+
const worker = gateOf(runtime, "artifacts-exist");
|
|
1385
|
+
const checked = verifyArtifactExists(runtime.dirs.assets, resolve(runtime.cwd, pathFlag));
|
|
1386
|
+
if (!checked.ok) {
|
|
1387
|
+
printWorker(
|
|
1388
|
+
runtime,
|
|
1389
|
+
worker,
|
|
1390
|
+
{ ok: false, command: "artifacts-exist", error: checked.error, path: checked.path },
|
|
1391
|
+
`artifact verification failed: ${checked.error}`,
|
|
1392
|
+
);
|
|
1393
|
+
return 1;
|
|
1394
|
+
}
|
|
1395
|
+
printWorker(
|
|
1396
|
+
runtime,
|
|
1397
|
+
worker,
|
|
1398
|
+
{ ok: true, command: "artifacts-exist", path: checked.path, bytes: checked.bytes },
|
|
1399
|
+
"artifact exists and is non-empty under workspace assets",
|
|
1400
|
+
);
|
|
1401
|
+
return 0;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
async function cmdStatus(runtime: Runtime): Promise<number> {
|
|
1405
|
+
const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl);
|
|
1406
|
+
const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl);
|
|
1407
|
+
const payload = { ok: health.ok && ready.ok, command: "hub view", health: health.body, ready: ready.body };
|
|
1408
|
+
print(runtime.io, runtime.json, payload, `hub health=${health.ok} ready=${ready.ok}`);
|
|
1409
|
+
return payload.ok ? 0 : 1;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
async function cmdDash(runtime: Runtime, options: { screen?: string } = {}): Promise<number> {
|
|
1413
|
+
const requested = options.screen?.trim();
|
|
1414
|
+
if (requested && !(MESH_TUI_PANELS as readonly string[]).includes(requested)) {
|
|
1415
|
+
print(runtime.io, runtime.json, { ok: false, command: "dash", error: "unknown_screen" }, `unknown screen ${requested}; use agents, tasks, workflows, plans, inbox, procs, or spend`);
|
|
1416
|
+
return 2;
|
|
1417
|
+
}
|
|
1418
|
+
const screen = requested as MeshTuiPanel | undefined;
|
|
1419
|
+
const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
|
|
1420
|
+
if (runtime.dryRun) {
|
|
1421
|
+
print(runtime.io, runtime.json, {
|
|
1422
|
+
ok: true,
|
|
1423
|
+
command: "dash",
|
|
1424
|
+
dryRun: true,
|
|
1425
|
+
serverUrl: runtime.serverUrl,
|
|
1426
|
+
transport: "sse",
|
|
1427
|
+
...(screen ? { screen } : {}),
|
|
1428
|
+
}, "would start kxm dash");
|
|
1429
|
+
return 0;
|
|
1430
|
+
}
|
|
1431
|
+
if (runtime.json) {
|
|
1432
|
+
runtime.io.stderr("kxm dash does not support --json; use kxm hub view\n");
|
|
1433
|
+
return 2;
|
|
1434
|
+
}
|
|
1435
|
+
const project = runtime.env.KXM_PROJECT?.trim() || basename(runtime.dirs.workdir) || "project";
|
|
1436
|
+
const authToken = runtime.env.KXM_AUTH_TOKEN?.trim();
|
|
1437
|
+
return await runMeshTui({
|
|
1438
|
+
serverUrl: runtime.serverUrl,
|
|
1439
|
+
dataPath,
|
|
1440
|
+
stateDir: runtime.dirs.state,
|
|
1441
|
+
project,
|
|
1442
|
+
env: runtime.env,
|
|
1443
|
+
...(authToken ? { authToken } : {}),
|
|
1444
|
+
...(screen ? { screen } : {}),
|
|
1445
|
+
fetchImpl: runtime.fetchImpl,
|
|
1446
|
+
stdout: runtime.io.stdout,
|
|
1447
|
+
stdin: process.stdin,
|
|
1448
|
+
isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
async function cmdHub(runtime: Runtime): Promise<number> {
|
|
1453
|
+
let refresh: Promise<unknown> | undefined;
|
|
1454
|
+
if (!runtime.dryRun) {
|
|
1455
|
+
const probe = installProbeFrom(runtime);
|
|
1456
|
+
if (classifyInstallRoot(probe).kind !== "source") {
|
|
1457
|
+
warnIgnoredProjectUpdateYaml(runtime);
|
|
1458
|
+
const cached = readUpdateCache(runtime.dirs.state);
|
|
1459
|
+
if (cached?.available) runtime.io.stderr(`${cached.message}\n`);
|
|
1460
|
+
let config: KxmUpdateConfig | undefined;
|
|
1461
|
+
try {
|
|
1462
|
+
config = loadKxmUpdateConfig(runtime.env);
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
if (error instanceof KxmUpdateConfigError) {
|
|
1465
|
+
const yamlPath = join(vnextUserStateRoot({ env: runtime.env }), "update.yaml");
|
|
1466
|
+
runtime.io.stderr(`kxm: ${error.message}; update check skipped; fix or remove ${yamlPath}\n`);
|
|
1467
|
+
} else {
|
|
1468
|
+
throw error;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (config) {
|
|
1472
|
+
refresh = refreshKxmUpdateNotice(runtime, config).then((notice) => {
|
|
1473
|
+
if (notice.available && !cached?.available) runtime.io.stderr(`${notice.message}\n`);
|
|
1474
|
+
}).catch(() => undefined);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
const extraEnv = workspaceEnv(runtime);
|
|
1479
|
+
if (runtime.dryRun) {
|
|
1480
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub start", dryRun: true, workspace: runtime.dirs.workspace }, "would start hub");
|
|
1481
|
+
return 0;
|
|
1482
|
+
}
|
|
1483
|
+
const code = await (runtime.io.spawnHub ?? ((launchEnv) => spawnScript("kxm-hub.mjs", launchEnv)))(extraEnv);
|
|
1484
|
+
if (refresh) await refresh;
|
|
1485
|
+
return code;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function formatHubBindHealth(health: HubHealth): string {
|
|
1489
|
+
if (health === "on") return "health=on";
|
|
1490
|
+
if (health === "off") return "health=off (nothing answered; run kxm hub start)";
|
|
1491
|
+
return "health=unknown (no reply within 300 ms)";
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<number> {
|
|
1495
|
+
let url: string;
|
|
1496
|
+
try {
|
|
1497
|
+
url = validateHubUrl(rawUrl);
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
if (error instanceof HubBindingError) {
|
|
1500
|
+
print(
|
|
1501
|
+
runtime.io,
|
|
1502
|
+
runtime.json,
|
|
1503
|
+
{ ok: false, command: "hub bind", error: "hub_url_invalid" },
|
|
1504
|
+
"hub bind needs an http or https URL without credentials, query, or fragment",
|
|
1505
|
+
);
|
|
1506
|
+
return 2;
|
|
1507
|
+
}
|
|
1508
|
+
throw error;
|
|
1509
|
+
}
|
|
1510
|
+
const file = hubBindingFile(runtime.env);
|
|
1511
|
+
if (runtime.dryRun) {
|
|
1512
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub bind", dryRun: true, url, file }, `would bind hub ${url}`);
|
|
1513
|
+
return 0;
|
|
1514
|
+
}
|
|
1515
|
+
writeHubBinding({ schema: HUB_BINDING_SCHEMA, url, boundAt: new Date().toISOString() }, runtime.env);
|
|
1516
|
+
const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
|
|
1517
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, file, health, probeMs }, `bound hub ${url} · ${formatHubBindHealth(health)}`);
|
|
1518
|
+
return 0;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
async function cmdHubUnbind(runtime: Runtime): Promise<number> {
|
|
1522
|
+
const file = hubBindingFile(runtime.env);
|
|
1523
|
+
if (runtime.dryRun) {
|
|
1524
|
+
print(
|
|
1525
|
+
runtime.io,
|
|
1526
|
+
runtime.json,
|
|
1527
|
+
{ ok: true, command: "hub unbind", dryRun: true, ...(runtime.boundHubUrl ? { url: runtime.boundHubUrl } : {}), file },
|
|
1528
|
+
`would unbind hub${runtime.boundHubUrl ? ` ${runtime.boundHubUrl}` : ""}`,
|
|
1529
|
+
);
|
|
1530
|
+
return 0;
|
|
1531
|
+
}
|
|
1532
|
+
let url: string | undefined;
|
|
1533
|
+
let malformed = false;
|
|
1534
|
+
try {
|
|
1535
|
+
url = readHubBinding(runtime.env)?.url;
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
if (error instanceof HubBindingError) malformed = true;
|
|
1538
|
+
else throw error;
|
|
1539
|
+
}
|
|
1540
|
+
if (!malformed && !url) {
|
|
1541
|
+
print(runtime.io, runtime.json, { ok: false, command: "hub unbind", error: "hub_not_bound" }, `no hub binding at ${file}`);
|
|
1542
|
+
return 1;
|
|
1543
|
+
}
|
|
1544
|
+
removeHubBinding(runtime.env);
|
|
1545
|
+
if (malformed) {
|
|
1546
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub unbind", file }, "unbound hub (record was malformed)");
|
|
1547
|
+
return 0;
|
|
1548
|
+
}
|
|
1549
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub unbind", url, file }, `unbound hub ${url}`);
|
|
1550
|
+
return 0;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
async function cmdWorker(runtime: Runtime, options: {
|
|
1554
|
+
name?: string;
|
|
1555
|
+
project?: string;
|
|
1556
|
+
model?: string;
|
|
1557
|
+
fallbackModels?: string;
|
|
1558
|
+
tools?: string;
|
|
1559
|
+
sessionIsolation?: string;
|
|
1560
|
+
continue?: boolean;
|
|
1561
|
+
freshStart?: boolean;
|
|
1562
|
+
}): Promise<number> {
|
|
1563
|
+
const name = options.name?.trim() || runtime.env.KXM_AGENT_NAME?.trim();
|
|
1564
|
+
const project = options.project?.trim() || runtime.env.KXM_PROJECT?.trim();
|
|
1565
|
+
const model = options.model?.trim() || runtime.env.KXM_WORKER_MODEL?.trim();
|
|
1566
|
+
const fallbackModels = options.fallbackModels?.trim() || runtime.env.KXM_WORKER_FALLBACK_MODELS?.trim();
|
|
1567
|
+
const tools = options.tools?.trim() || runtime.env.KXM_WORKER_TOOLS?.trim();
|
|
1568
|
+
const sessionIsolation = options.sessionIsolation?.trim() || runtime.env.KXM_WORKER_SESSION_ISOLATION?.trim() || "off";
|
|
1569
|
+
if (sessionIsolation !== "workflow" && sessionIsolation !== "off") {
|
|
1570
|
+
runtime.io.stderr("worker --session-isolation must be workflow or off\n");
|
|
1571
|
+
return 2;
|
|
1572
|
+
}
|
|
1573
|
+
const extraEnv = {
|
|
1574
|
+
...workspaceEnv(runtime),
|
|
1575
|
+
...(name ? { KXM_AGENT_NAME: name } : {}),
|
|
1576
|
+
...(project ? { KXM_PROJECT: project } : {}),
|
|
1577
|
+
...(model ? { KXM_WORKER_MODEL: model } : {}),
|
|
1578
|
+
...(fallbackModels ? { KXM_WORKER_FALLBACK_MODELS: fallbackModels } : {}),
|
|
1579
|
+
...(tools ? { KXM_WORKER_TOOLS: tools } : {}),
|
|
1580
|
+
KXM_WORKER_SESSION_ISOLATION: sessionIsolation,
|
|
1581
|
+
...(options.continue === false ? { KXM_WORKER_CONTINUE: "false" } : {}),
|
|
1582
|
+
...(options.freshStart ? { KXM_WORKER_INITIAL_CONTINUE: "false" } : {}),
|
|
1583
|
+
};
|
|
1584
|
+
if (runtime.dryRun) {
|
|
1585
|
+
printWorker(runtime, agentWorker({
|
|
1586
|
+
name: name || "required",
|
|
1587
|
+
project: project || "required",
|
|
1588
|
+
...(model ? { model } : {}),
|
|
1589
|
+
}), {
|
|
1590
|
+
ok: true,
|
|
1591
|
+
command: "worker",
|
|
1592
|
+
dryRun: true,
|
|
1593
|
+
workspace: runtime.dirs.workspace,
|
|
1594
|
+
name: name || "required",
|
|
1595
|
+
project: project || "required",
|
|
1596
|
+
model: model || "provider default",
|
|
1597
|
+
fallbackModels: fallbackModels || "none",
|
|
1598
|
+
tools: tools || "Pi defaults",
|
|
1599
|
+
sessionIsolation,
|
|
1600
|
+
continue: options.continue !== false,
|
|
1601
|
+
freshStart: Boolean(options.freshStart),
|
|
1602
|
+
}, "would start worker");
|
|
1603
|
+
return 0;
|
|
1604
|
+
}
|
|
1605
|
+
if (!name || !project) {
|
|
1606
|
+
runtime.io.stderr("worker requires --name and --project (or KXM_AGENT_NAME and KXM_PROJECT)\n");
|
|
1607
|
+
return 2;
|
|
1608
|
+
}
|
|
1609
|
+
return await (runtime.io.spawnWorker ?? ((launchEnv) => spawnScript("kxm-worker.mjs", launchEnv)))(extraEnv);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
async function cmdStop(runtime: Runtime, waitMsFlag?: string): Promise<number> {
|
|
1613
|
+
const pids = existsSync(runtime.dirs.state)
|
|
1614
|
+
? readdirSync(runtime.dirs.state).filter((name) => name.endsWith(".pid"))
|
|
1615
|
+
: [];
|
|
1616
|
+
if (runtime.dryRun) {
|
|
1617
|
+
print(runtime.io, runtime.json, { ok: true, command: "stop", dryRun: true, pidFiles: pids }, "would signal pid files");
|
|
1618
|
+
return 0;
|
|
1619
|
+
}
|
|
1620
|
+
if (pids.length === 0) {
|
|
1621
|
+
print(runtime.io, runtime.json, { ok: false, command: "stop", error: "no_pid_files" }, "no hub/worker pid files found");
|
|
1622
|
+
return 1;
|
|
1623
|
+
}
|
|
1624
|
+
const requested: string[] = [];
|
|
1625
|
+
const ignored: string[] = [];
|
|
1626
|
+
const records = new Map<string, { pid: number; startedAt: string; generation?: string }>();
|
|
1627
|
+
for (const file of pids) {
|
|
1628
|
+
try {
|
|
1629
|
+
const record = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { version?: number; pid?: number; role?: string; startedAt?: string; generation?: string; controlFile?: string };
|
|
1630
|
+
const expectedControl = file === "hub.pid" ? "hub.stop" : file.startsWith("worker-") ? `${file.slice(0, -4)}.stop` : undefined;
|
|
1631
|
+
const expectedRole = file === "hub.pid" ? "hub" : file.startsWith("worker-") ? "worker" : undefined;
|
|
1632
|
+
if (record.version !== 1 || !Number.isInteger(record.pid) || record.pid! <= 0 || !record.startedAt || !expectedControl || record.controlFile !== expectedControl || record.role !== expectedRole || !processExists(record.pid!)) { ignored.push(file); continue; }
|
|
1633
|
+
writeFileSync(join(runtime.dirs.state, record.controlFile), `${JSON.stringify({ startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}), requestedAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600 });
|
|
1634
|
+
requested.push(file); records.set(file, { pid: record.pid!, startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}) });
|
|
1635
|
+
} catch { ignored.push(file); }
|
|
1636
|
+
}
|
|
1637
|
+
if (requested.length === 0) { print(runtime.io, runtime.json, { ok: false, command: "stop", requested, ignored }, "no current managed processes found"); return 1; }
|
|
1638
|
+
const waitMs = Math.min(30_000, Math.max(100, Number(waitMsFlag || 5_000)));
|
|
1639
|
+
const deadline = Date.now() + waitMs;
|
|
1640
|
+
const stopped = new Set<string>();
|
|
1641
|
+
while (Date.now() <= deadline && stopped.size < requested.length) {
|
|
1642
|
+
for (const [file, record] of records) {
|
|
1643
|
+
try {
|
|
1644
|
+
const current = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { pid?: number; startedAt?: string; generation?: string };
|
|
1645
|
+
if (current.pid !== record.pid || current.startedAt !== record.startedAt || current.generation !== record.generation || !processExists(record.pid)) stopped.add(file);
|
|
1646
|
+
} catch { stopped.add(file); }
|
|
1647
|
+
}
|
|
1648
|
+
if (stopped.size < requested.length) await (runtime.io.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))))(100);
|
|
1649
|
+
}
|
|
1650
|
+
const timedOut = requested.filter((file) => !stopped.has(file));
|
|
1651
|
+
const ok = timedOut.length === 0;
|
|
1652
|
+
print(runtime.io, runtime.json, { ok, command: "stop", requested, stopped: [...stopped], timedOut, ignored }, ok ? "managed processes stopped" : "stop request timed out");
|
|
1653
|
+
return ok ? 0 : 1;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
async function cmdSessionStatus(runtime: Runtime): Promise<number> {
|
|
1657
|
+
const stateDir = runtime.dirs.state;
|
|
1658
|
+
const names = existsSync(stateDir) ? readdirSync(stateDir) : [];
|
|
1659
|
+
const claims = [];
|
|
1660
|
+
for (const file of names.filter((name) => name.endsWith(".pid"))) {
|
|
1661
|
+
try {
|
|
1662
|
+
const record = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as { pid?: number; role?: string; startedAt?: string; generation?: string };
|
|
1663
|
+
claims.push({
|
|
1664
|
+
file,
|
|
1665
|
+
role: record.role,
|
|
1666
|
+
pid: record.pid,
|
|
1667
|
+
startedAt: record.startedAt,
|
|
1668
|
+
live: Number.isInteger(record.pid) && record.pid! > 0 && processExists(record.pid!),
|
|
1669
|
+
});
|
|
1670
|
+
} catch {
|
|
1671
|
+
claims.push({ file, live: false, error: "invalid_pid_record" });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const recoveries = [];
|
|
1675
|
+
for (const file of names.filter((name) => name.startsWith("worker-recovery-") && name.endsWith(".json"))) {
|
|
1676
|
+
try {
|
|
1677
|
+
const envelope = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as {
|
|
1678
|
+
reason?: string;
|
|
1679
|
+
agentName?: string;
|
|
1680
|
+
project?: string;
|
|
1681
|
+
createdAt?: string;
|
|
1682
|
+
runId?: string | null;
|
|
1683
|
+
stageId?: string | null;
|
|
1684
|
+
freshSession?: boolean;
|
|
1685
|
+
};
|
|
1686
|
+
recoveries.push({
|
|
1687
|
+
file,
|
|
1688
|
+
reason: envelope.reason,
|
|
1689
|
+
agentName: envelope.agentName,
|
|
1690
|
+
project: envelope.project,
|
|
1691
|
+
createdAt: envelope.createdAt,
|
|
1692
|
+
runId: envelope.runId ?? undefined,
|
|
1693
|
+
stageId: envelope.stageId ?? undefined,
|
|
1694
|
+
freshSession: envelope.freshSession === true,
|
|
1695
|
+
});
|
|
1696
|
+
} catch {
|
|
1697
|
+
recoveries.push({ file, error: "invalid_recovery_envelope" });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
print(
|
|
1701
|
+
runtime.io,
|
|
1702
|
+
runtime.json,
|
|
1703
|
+
{ ok: true, command: "session status", claims, recoveries },
|
|
1704
|
+
`${claims.length} session claim(s), ${recoveries.length} recovery envelope(s)`,
|
|
1705
|
+
);
|
|
1706
|
+
return 0;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async function cmdSessionBrief(runtime: Runtime, options: { status?: boolean; token?: boolean } = {}): Promise<number> {
|
|
1710
|
+
const sessionToken = mintSessionToken({ preset: "operator" });
|
|
1711
|
+
if (options.token) {
|
|
1712
|
+
print(runtime.io, runtime.json, { ok: true, command: "session brief", sessionToken }, sessionToken);
|
|
1713
|
+
return 0;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
|
|
1717
|
+
const env = {
|
|
1718
|
+
...runtime.env,
|
|
1719
|
+
KXM_STATE_DIR: runtime.dirs.state,
|
|
1720
|
+
KXM_DATA_PATH: runtime.env.KXM_DATA_PATH?.trim() || dataPath,
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1723
|
+
let hub: SessionHubStatus | undefined;
|
|
1724
|
+
const targetUrl = runtime.env.KXM_SERVER_URL?.trim() || runtime.boundHubUrl || readHubBinding(runtime.env)?.url;
|
|
1725
|
+
if (targetUrl) {
|
|
1726
|
+
const { health } = await probeHubHealth(targetUrl, runtime.fetchImpl, 300);
|
|
1727
|
+
hub = {
|
|
1728
|
+
state: health,
|
|
1729
|
+
evidence: health === "unknown" ? "timeout" : "probed",
|
|
1730
|
+
online: health === "on",
|
|
1731
|
+
url: targetUrl,
|
|
1732
|
+
};
|
|
1733
|
+
} else {
|
|
1734
|
+
hub = { state: "off", evidence: "unconfigured", online: false };
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const brief = await loadSessionBriefAsync(runtime.dirs.workdir, env, undefined, hub, {
|
|
1738
|
+
fetchImpl: runtime.fetchImpl,
|
|
1739
|
+
sessionToken,
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
if (options.status) {
|
|
1743
|
+
print(runtime.io, runtime.json, brief, brief.statusLine);
|
|
1744
|
+
return 0;
|
|
1745
|
+
}
|
|
1746
|
+
print(
|
|
1747
|
+
runtime.io,
|
|
1748
|
+
runtime.json,
|
|
1749
|
+
brief,
|
|
1750
|
+
`${formatSessionBriefText(brief)}\n\nSession token: ${sessionToken}\n`,
|
|
1751
|
+
);
|
|
1752
|
+
return 0;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
async function cmdSessionStart(runtime: Runtime, options: { id?: string; workflow?: string; mix?: string }): Promise<number> {
|
|
1756
|
+
const id = options.id?.trim() || `session_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
1757
|
+
const workflowId = options.workflow?.trim();
|
|
1758
|
+
const mix = options.mix?.trim();
|
|
1759
|
+
if (workflowId && mix) {
|
|
1760
|
+
runtime.io.stderr("session start takes --workflow or --mix, not both\n");
|
|
1761
|
+
return 2;
|
|
1762
|
+
}
|
|
1763
|
+
if (!workflowId && !mix) {
|
|
1764
|
+
runtime.io.stderr("session start requires --workflow <id> or --mix <agent,gate,...>\n");
|
|
1765
|
+
return 2;
|
|
1766
|
+
}
|
|
1767
|
+
const project = runtime.env.KXM_PROJECT?.trim();
|
|
1768
|
+
let workers: Worker[];
|
|
1769
|
+
try {
|
|
1770
|
+
const names = mix
|
|
1771
|
+
? mix.split(",").map((name) => name.trim()).filter(Boolean)
|
|
1772
|
+
: rosterNames(runtime.dirs.config);
|
|
1773
|
+
workers = loadNamedWorkers(runtime.dirs.config, names, project);
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
const errorName = error && typeof error === "object" && "name" in error ? String(error.name) : "";
|
|
1776
|
+
if (errorName !== "SessionConfigError") throw error;
|
|
1777
|
+
const message = error instanceof Error ? redactSecrets(error.message) : "invalid session configuration";
|
|
1778
|
+
runtime.io.stderr(`${message}\n`);
|
|
1779
|
+
return 2;
|
|
1780
|
+
}
|
|
1781
|
+
const session = createSession({
|
|
1782
|
+
id,
|
|
1783
|
+
host: hostMode(runtime),
|
|
1784
|
+
mode: workflowId ? "workflow" : "mix",
|
|
1785
|
+
workers,
|
|
1786
|
+
assetsDir: runtime.dirs.assets,
|
|
1787
|
+
...(workflowId ? { workflowId } : {}),
|
|
1788
|
+
});
|
|
1789
|
+
const created = [
|
|
1790
|
+
...sessionAssetDirs(runtime.dirs.assets, session.id),
|
|
1791
|
+
...(workflowId ? workflowAssetDirs(runtime.dirs.assets, workflowId) : []),
|
|
1792
|
+
];
|
|
1793
|
+
if (!runtime.dryRun) {
|
|
1794
|
+
for (const directory of created) mkdirSync(directory, { recursive: true });
|
|
1795
|
+
writeSession(runtime.dirs.assets, session);
|
|
1796
|
+
}
|
|
1797
|
+
print(runtime.io, runtime.json, {
|
|
1798
|
+
ok: true,
|
|
1799
|
+
command: "session start",
|
|
1800
|
+
dryRun: runtime.dryRun || undefined,
|
|
1801
|
+
session,
|
|
1802
|
+
created,
|
|
1803
|
+
}, `session ${session.id} (${session.mode})`);
|
|
1804
|
+
return 0;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
async function cmdImprove(runtime: Runtime, options: { file?: string; target?: string; outDir?: string } = {}): Promise<number> {
|
|
1808
|
+
const file = options.file ? resolve(runtime.cwd, options.file) : telemetryPath(runtime.dirs.logs);
|
|
1809
|
+
const routingRecords = existsSync(file)
|
|
1810
|
+
? readRoutingRecords(file).map((entry) => entry.routing)
|
|
1811
|
+
: [];
|
|
1812
|
+
|
|
1813
|
+
const candidatesDir = options.outDir
|
|
1814
|
+
? resolve(runtime.cwd, options.outDir)
|
|
1815
|
+
: join(runtime.cwd, ".kxm", "candidates");
|
|
1816
|
+
|
|
1817
|
+
const report = buildImprovementReport(routingRecords, {
|
|
1818
|
+
candidatesDir,
|
|
1819
|
+
projectRoot: runtime.cwd,
|
|
1820
|
+
dryRun: runtime.dryRun,
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
const reportDir = join(runtime.dirs.assets, "improvements");
|
|
1824
|
+
const reportPath = writeImprovementReport(reportDir, report, runtime.dryRun);
|
|
1825
|
+
|
|
1826
|
+
const text = formatImprovementReport(report);
|
|
1827
|
+
print(runtime.io, runtime.json, {
|
|
1828
|
+
ok: true,
|
|
1829
|
+
command: "improve",
|
|
1830
|
+
dryRun: runtime.dryRun || undefined,
|
|
1831
|
+
path: reportPath,
|
|
1832
|
+
events: report.recordsCount,
|
|
1833
|
+
recordsCount: report.recordsCount,
|
|
1834
|
+
groupsCount: report.groups.length,
|
|
1835
|
+
candidatesCount: report.candidates.length,
|
|
1836
|
+
candidates: report.candidates,
|
|
1837
|
+
report,
|
|
1838
|
+
}, text);
|
|
1839
|
+
return 0;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function parseContextKinds(value: string | undefined): string[] | undefined {
|
|
1843
|
+
if (!value) return undefined;
|
|
1844
|
+
return value.split(",").map((kind) => kind.trim()).filter((kind) => kind.length > 0);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
async function cmdContextGet(runtime: Runtime, project: string, options: { role: string; task: string; run?: string; stage?: string; budget?: string; kinds?: string }): Promise<number> {
|
|
1848
|
+
const budget = options.budget === undefined ? undefined : Number(options.budget);
|
|
1849
|
+
if (options.budget !== undefined && (!Number.isInteger(budget) || (budget as number) < 512 || (budget as number) > 200_000)) {
|
|
1850
|
+
runtime.io.stderr("context get --budget must be an integer between 512 and 200000\n");
|
|
1851
|
+
return 2;
|
|
1852
|
+
}
|
|
1853
|
+
const body: Record<string, unknown> = {
|
|
1854
|
+
project,
|
|
1855
|
+
role: options.role,
|
|
1856
|
+
task: options.task,
|
|
1857
|
+
};
|
|
1858
|
+
if (options.run) body.workflowRunId = options.run;
|
|
1859
|
+
if (options.stage) body.stageId = options.stage;
|
|
1860
|
+
if (budget !== undefined) body.budgetTokens = budget;
|
|
1861
|
+
const kinds = parseContextKinds(options.kinds);
|
|
1862
|
+
if (kinds) body.includeKinds = kinds;
|
|
1863
|
+
const response = await hubContextPost({
|
|
1864
|
+
serverUrl: runtime.serverUrl,
|
|
1865
|
+
path: "/v1/context/get",
|
|
1866
|
+
body,
|
|
1867
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1868
|
+
fetchImpl: runtime.fetchImpl,
|
|
1869
|
+
});
|
|
1870
|
+
print(runtime.io, runtime.json, { ok: response.ok, command: "context get", status: response.status, ...(response.body as object) }, `context get ${response.ok ? "assembled" : `failed (${response.status})`}`);
|
|
1871
|
+
return response.ok ? 0 : 1;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
async function cmdContextRecall(runtime: Runtime, project: string, options: { query?: string; kinds?: string; limit?: string }): Promise<number> {
|
|
1875
|
+
const body: Record<string, unknown> = { project };
|
|
1876
|
+
if (options.query) body.query = options.query;
|
|
1877
|
+
const kinds = parseContextKinds(options.kinds);
|
|
1878
|
+
if (kinds) body.kinds = kinds;
|
|
1879
|
+
if (options.limit) body.limit = Number(options.limit);
|
|
1880
|
+
const response = await hubContextPost({
|
|
1881
|
+
serverUrl: runtime.serverUrl,
|
|
1882
|
+
path: "/v1/context/recall",
|
|
1883
|
+
body,
|
|
1884
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1885
|
+
fetchImpl: runtime.fetchImpl,
|
|
1886
|
+
});
|
|
1887
|
+
print(runtime.io, runtime.json, { ok: response.ok, command: "context recall", status: response.status, ...(response.body as object) }, `context recall ${response.ok ? "complete" : `failed (${response.status})`}`);
|
|
1888
|
+
return response.ok ? 0 : 1;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
async function cmdContextState(runtime: Runtime, project: string, key: string, options: { asOf?: string }): Promise<number> {
|
|
1892
|
+
const body: Record<string, unknown> = { project, key };
|
|
1893
|
+
if (options.asOf) body.asOf = options.asOf;
|
|
1894
|
+
const response = await hubContextPost({
|
|
1895
|
+
serverUrl: runtime.serverUrl,
|
|
1896
|
+
path: "/v1/context/state",
|
|
1897
|
+
body,
|
|
1898
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1899
|
+
fetchImpl: runtime.fetchImpl,
|
|
1900
|
+
});
|
|
1901
|
+
print(runtime.io, runtime.json, { ok: response.ok, command: "context state", status: response.status, ...(response.body as object) }, `context state ${response.ok ? "resolved" : `failed (${response.status})`}`);
|
|
1902
|
+
return response.ok ? 0 : 1;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
async function cmdContextEpisode(runtime: Runtime, project: string, options: { run?: string }): Promise<number> {
|
|
1906
|
+
const body: Record<string, unknown> = { project };
|
|
1907
|
+
if (options.run) body.workflowRunId = options.run;
|
|
1908
|
+
const response = await hubContextPost({
|
|
1909
|
+
serverUrl: runtime.serverUrl,
|
|
1910
|
+
path: "/v1/context/episode",
|
|
1911
|
+
body,
|
|
1912
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1913
|
+
fetchImpl: runtime.fetchImpl,
|
|
1914
|
+
});
|
|
1915
|
+
print(runtime.io, runtime.json, { ok: response.ok, command: "context episode", status: response.status, ...(response.body as object) }, `context episode ${response.ok ? "complete" : `failed (${response.status})`}`);
|
|
1916
|
+
return response.ok ? 0 : 1;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
async function cmdContextPromote(runtime: Runtime, project: string, proposalId: string, options: { evidence: string }): Promise<number> {
|
|
1920
|
+
const evidence = options.evidence.split(",").map((ref) => ref.trim()).filter((ref) => ref.length > 0);
|
|
1921
|
+
if (evidence.length === 0) {
|
|
1922
|
+
runtime.io.stderr("context promote --evidence must contain at least one durable evidence reference\n");
|
|
1923
|
+
return 2;
|
|
1924
|
+
}
|
|
1925
|
+
const response = await hubContextPost({
|
|
1926
|
+
serverUrl: runtime.serverUrl,
|
|
1927
|
+
path: "/v1/context/state/promote",
|
|
1928
|
+
body: { project, proposalId, evidence },
|
|
1929
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1930
|
+
fetchImpl: runtime.fetchImpl,
|
|
1931
|
+
});
|
|
1932
|
+
print(runtime.io, runtime.json, { ok: response.ok, command: "context promote", status: response.status, ...(response.body as object) }, `context promote ${response.ok ? "recorded" : `failed (${response.status})`}`);
|
|
1933
|
+
return response.ok ? 0 : 1;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
async function cmdContextExplain(runtime: Runtime, project: string, itemId: string): Promise<number> {
|
|
1937
|
+
const response = await hubContextPost({
|
|
1938
|
+
serverUrl: runtime.serverUrl,
|
|
1939
|
+
path: "/v1/context/explain",
|
|
1940
|
+
body: { project, id: itemId },
|
|
1941
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1942
|
+
fetchImpl: runtime.fetchImpl,
|
|
1943
|
+
});
|
|
1944
|
+
print(runtime.io, runtime.json, { ok: response.ok, command: "context explain", status: response.status, ...(response.body as object) }, `context explain ${response.ok ? "complete" : `failed (${response.status})`}`);
|
|
1945
|
+
return response.ok ? 0 : 1;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
async function cmdContextWikiCompile(runtime: Runtime, project: string, options: { out?: string }): Promise<number> {
|
|
1949
|
+
const response = await hubContextPost({
|
|
1950
|
+
serverUrl: runtime.serverUrl,
|
|
1951
|
+
path: "/v1/context/wiki/compile",
|
|
1952
|
+
body: { project },
|
|
1953
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1954
|
+
fetchImpl: runtime.fetchImpl,
|
|
1955
|
+
});
|
|
1956
|
+
if (!response.ok) {
|
|
1957
|
+
print(runtime.io, runtime.json, { ok: false, command: "context wiki-compile", status: response.status, body: response.body }, `wiki compile failed (${response.status})`);
|
|
1958
|
+
return 1;
|
|
1959
|
+
}
|
|
1960
|
+
const compiled = response.body as { audit: { pages: string[]; contradictions: number }; pages: { path: string; content: string }[] };
|
|
1961
|
+
let written: string[] = [];
|
|
1962
|
+
if (options.out) {
|
|
1963
|
+
const pages = new Map(compiled.pages.map((page) => [page.path, page.content]));
|
|
1964
|
+
written = writeCompiledWiki(options.out, { pages, index: pages.get(".kxm/knowledge/wiki/index.md") ?? "", audit: { project, pages: compiled.audit.pages, stateItems: 0, contextItems: 0, contradictions: compiled.audit.contradictions, compiledAt: "" } });
|
|
1965
|
+
}
|
|
1966
|
+
print(runtime.io, runtime.json, { ok: true, command: "context wiki-compile", project, pages: compiled.audit.pages, openContradictions: compiled.audit.contradictions, ...(written.length > 0 ? { written: written.length, outDir: options.out } : { dryRun: true }) }, `compiled ${compiled.audit.pages.length} wiki page(s)${written.length > 0 ? ` to ${options.out}` : " (dry-run)"}`);
|
|
1967
|
+
return 0;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
async function cmdContextWikiLint(runtime: Runtime, project: string): Promise<number> {
|
|
1971
|
+
const response = await hubContextPost({
|
|
1972
|
+
serverUrl: runtime.serverUrl,
|
|
1973
|
+
path: "/v1/context/wiki/compile",
|
|
1974
|
+
body: { project },
|
|
1975
|
+
...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
|
|
1976
|
+
fetchImpl: runtime.fetchImpl,
|
|
1977
|
+
});
|
|
1978
|
+
if (!response.ok) {
|
|
1979
|
+
print(runtime.io, runtime.json, { ok: false, command: "context wiki-lint", status: response.status, body: response.body }, `wiki lint failed (${response.status})`);
|
|
1980
|
+
return 1;
|
|
1981
|
+
}
|
|
1982
|
+
const compiled = response.body as {
|
|
1983
|
+
audit: { stateItems: number; contextItems: number; contradictions: number; compiledAt: string };
|
|
1984
|
+
lint: { severity: string; rule: string; path: string; message: string }[];
|
|
1985
|
+
};
|
|
1986
|
+
const issues = compiled.lint;
|
|
1987
|
+
print(runtime.io, runtime.json, { ok: issues.length === 0, command: "context wiki-lint", project, issues, audit: compiled.audit }, issues.length === 0 ? "wiki lint clean" : `wiki lint found ${issues.length} issue(s)`);
|
|
1988
|
+
return issues.every((issue) => issue.severity !== "error") ? 0 : 1;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function skillStateFromFlag(value: string): SkillState {
|
|
1992
|
+
if (value === "candidate" || value === "promoted" || value === "quarantined" || value === "rejected") return value;
|
|
1993
|
+
throw new Error(`invalid skill state ${value}`);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function skillsRoot(runtime: Runtime): string {
|
|
1997
|
+
return join(runtime.dirs.workdir, ".kxm", "skills");
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function csv(value: string | undefined): string[] | undefined {
|
|
2001
|
+
if (!value) return undefined;
|
|
2002
|
+
const items = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
2003
|
+
return items.length > 0 ? items : undefined;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
async function cmdSkillsCreate(runtime: Runtime, options: { file: string; name: string; description?: string; createdBy: string; run?: string; journal?: string; receipt?: string; harness: string; models: string; supersedes?: string }): Promise<number> {
|
|
2007
|
+
if (runtime.dryRun) {
|
|
2008
|
+
print(runtime.io, runtime.json, { ok: true, command: "skills create", dryRun: true, name: options.name }, "would create skill candidate");
|
|
2009
|
+
return 0;
|
|
2010
|
+
}
|
|
2011
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
2012
|
+
try {
|
|
2013
|
+
const metadata = lifecycle.create({
|
|
2014
|
+
name: options.name,
|
|
2015
|
+
description: options.description ?? "",
|
|
2016
|
+
content: readFileSync(options.file, "utf8"),
|
|
2017
|
+
createdBy: options.createdBy,
|
|
2018
|
+
sources: {
|
|
2019
|
+
runIds: csv(options.run) ?? [],
|
|
2020
|
+
journalEntryIds: csv(options.journal) ?? [],
|
|
2021
|
+
evidenceReceipts: csv(options.receipt) ?? [],
|
|
2022
|
+
},
|
|
2023
|
+
compatibility: { harness: options.harness, models: csv(options.models) ?? [] },
|
|
2024
|
+
...(options.supersedes ? { supersedes: options.supersedes } : {}),
|
|
2025
|
+
});
|
|
2026
|
+
print(runtime.io, runtime.json, { ok: true, command: "skills create", metadata }, `created skill candidate ${metadata.id}`);
|
|
2027
|
+
return 0;
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2030
|
+
runtime.io.stderr(`skills create failed: ${message}\n`);
|
|
2031
|
+
return 1;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, options: { kind: string; evaluator: string; fail?: boolean; score?: string; details?: string }): Promise<number> {
|
|
2036
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
2037
|
+
try {
|
|
2038
|
+
const outcome = lifecycle.evaluate(skillId, {
|
|
2039
|
+
kind: options.kind as SkillEvaluationKind,
|
|
2040
|
+
evaluatorVersion: options.evaluator,
|
|
2041
|
+
passed: options.fail !== true,
|
|
2042
|
+
...(options.score !== undefined ? { score: Number(options.score) } : {}),
|
|
2043
|
+
...(options.details ? { details: options.details } : {}),
|
|
2044
|
+
});
|
|
2045
|
+
print(runtime.io, runtime.json, { ok: true, command: "skills evaluate", skillId, quarantined: outcome.quarantined, evaluation: outcome.evaluation }, `recorded ${options.kind} evaluation${outcome.quarantined ? " (candidate quarantined)" : ""}`);
|
|
2046
|
+
return 0;
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2049
|
+
runtime.io.stderr(`skills evaluate failed: ${message}\n`);
|
|
2050
|
+
return 1;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
async function cmdSkillsPromote(runtime: Runtime, skillId: string, options: { decidedBy: string; evidence: string; reason?: string }): Promise<number> {
|
|
2055
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
2056
|
+
try {
|
|
2057
|
+
const promoted = lifecycle.promote(skillId, {
|
|
2058
|
+
decidedBy: options.decidedBy,
|
|
2059
|
+
reason: options.reason ?? "passed protected evaluation",
|
|
2060
|
+
evidenceRefs: csv(options.evidence) ?? [],
|
|
2061
|
+
});
|
|
2062
|
+
print(runtime.io, runtime.json, { ok: true, command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, `promoted skill ${skillId} (patch: ${promoted.patchPath})`);
|
|
2063
|
+
return 0;
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2066
|
+
runtime.io.stderr(`skills promote failed: ${message}\n`);
|
|
2067
|
+
return 1;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
async function cmdSkillsReject(runtime: Runtime, skillId: string, options: { decidedBy: string; reason?: string }): Promise<number> {
|
|
2072
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
2073
|
+
try {
|
|
2074
|
+
const metadata = lifecycle.reject(skillId, { decidedBy: options.decidedBy, reason: options.reason ?? "rejected" });
|
|
2075
|
+
print(runtime.io, runtime.json, { ok: true, command: "skills reject", skillId, metadata }, `rejected skill ${skillId} (history retained)`);
|
|
2076
|
+
return 0;
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2079
|
+
runtime.io.stderr(`skills reject failed: ${message}\n`);
|
|
2080
|
+
return 1;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
async function cmdSkillsList(runtime: Runtime, options: { state: string }): Promise<number> {
|
|
2085
|
+
try {
|
|
2086
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
2087
|
+
const state = skillStateFromFlag(options.state);
|
|
2088
|
+
const items = lifecycle.list(state).map((metadata) => ({
|
|
2089
|
+
id: metadata.id,
|
|
2090
|
+
name: metadata.name,
|
|
2091
|
+
version: metadata.version,
|
|
2092
|
+
createdBy: metadata.createdBy,
|
|
2093
|
+
createdAt: metadata.createdAt,
|
|
2094
|
+
models: metadata.compatibility.models,
|
|
2095
|
+
}));
|
|
2096
|
+
print(runtime.io, runtime.json, { ok: true, command: "skills list", state: options.state, skills: items }, `${items.length} ${state} skill(s)`);
|
|
2097
|
+
return 0;
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2100
|
+
runtime.io.stderr(`skills list failed: ${message}\n`);
|
|
2101
|
+
return 1;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
async function cmdSkillsVerify(runtime: Runtime, skillId: string, options: { state: string }): Promise<number> {
|
|
2106
|
+
try {
|
|
2107
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
2108
|
+
const state = skillStateFromFlag(options.state);
|
|
2109
|
+
const metadata = lifecycle.verify(state, skillId);
|
|
2110
|
+
print(runtime.io, runtime.json, { ok: true, command: "skills verify", skillId, state: options.state, contentSha256: metadata.contentSha256 }, `skill ${skillId} integrity verified`);
|
|
2111
|
+
return 0;
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2114
|
+
runtime.io.stderr(`skills verify failed: ${message}\n`);
|
|
2115
|
+
return 1;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async function cmdMemoryBrief(runtime: Runtime): Promise<number> {
|
|
2120
|
+
try {
|
|
2121
|
+
const brief = generateMemoryBrief(runtime.cwd);
|
|
2122
|
+
const text = formatMemoryBriefText(brief);
|
|
2123
|
+
print(runtime.io, runtime.json, { ok: true, command: "memory brief", brief }, text);
|
|
2124
|
+
return 0;
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2127
|
+
runtime.io.stderr(`memory brief failed: ${message}\n`);
|
|
2128
|
+
return 1;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
async function cmdMemoryNote(
|
|
2133
|
+
runtime: Runtime,
|
|
2134
|
+
fact: string,
|
|
2135
|
+
options: { scope?: string; kind?: string; body?: string },
|
|
2136
|
+
): Promise<number> {
|
|
2137
|
+
try {
|
|
2138
|
+
const { record, path } = createMemoryNote(runtime.cwd, fact, {
|
|
2139
|
+
scope: (options.scope ?? "project") as MemoryScope,
|
|
2140
|
+
...(options.kind !== undefined ? { kind: options.kind } : {}),
|
|
2141
|
+
...(options.body !== undefined ? { body: options.body } : {}),
|
|
2142
|
+
});
|
|
2143
|
+
const relPath = relative(runtime.cwd, path);
|
|
2144
|
+
print(
|
|
2145
|
+
runtime.io,
|
|
2146
|
+
runtime.json,
|
|
2147
|
+
{ ok: true, command: "memory note", candidate: record, path: relPath },
|
|
2148
|
+
`Recorded memory candidate ${record.id} in ${relPath} (promoted via PR)`,
|
|
2149
|
+
);
|
|
2150
|
+
return 0;
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2153
|
+
runtime.io.stderr(`memory note failed: ${message}\n`);
|
|
2154
|
+
return 1;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
async function cmdMemorySync(runtime: Runtime): Promise<number> {
|
|
2159
|
+
try {
|
|
2160
|
+
const result = syncHarnessMemory(runtime.cwd);
|
|
2161
|
+
print(
|
|
2162
|
+
runtime.io,
|
|
2163
|
+
runtime.json,
|
|
2164
|
+
{ ok: true, command: "memory sync", ...result },
|
|
2165
|
+
`Synced project memory across AGENTS.md, CLAUDE.md, and GEMINI.md`,
|
|
2166
|
+
);
|
|
2167
|
+
return 0;
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2170
|
+
runtime.io.stderr(`memory sync failed: ${message}\n`);
|
|
2171
|
+
return 1;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
async function cmdConfigGet(runtime: Runtime, key: string): Promise<number> {
|
|
2176
|
+
try {
|
|
2177
|
+
const config = loadKxmConfig(runtime.cwd);
|
|
2178
|
+
const value = getKxmConfigValue(config, key);
|
|
2179
|
+
print(
|
|
2180
|
+
runtime.io,
|
|
2181
|
+
runtime.json,
|
|
2182
|
+
{ ok: true, command: "config get", key, value },
|
|
2183
|
+
value !== undefined ? String(value) : "(undefined)",
|
|
2184
|
+
);
|
|
2185
|
+
return 0;
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2188
|
+
runtime.io.stderr(`config get failed: ${message}\n`);
|
|
2189
|
+
return 1;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
async function cmdConfigSet(
|
|
2194
|
+
runtime: Runtime,
|
|
2195
|
+
key: string,
|
|
2196
|
+
value: string,
|
|
2197
|
+
options: { scope: string },
|
|
2198
|
+
): Promise<number> {
|
|
2199
|
+
try {
|
|
2200
|
+
const scope = options.scope === "user" ? "user" : "project";
|
|
2201
|
+
let parsedVal: unknown = value;
|
|
2202
|
+
try {
|
|
2203
|
+
parsedVal = JSON.parse(value);
|
|
2204
|
+
} catch {
|
|
2205
|
+
// keep string
|
|
2206
|
+
}
|
|
2207
|
+
setKxmConfigValue(runtime.cwd, key, parsedVal, { scope });
|
|
2208
|
+
print(
|
|
2209
|
+
runtime.io,
|
|
2210
|
+
runtime.json,
|
|
2211
|
+
{ ok: true, command: "config set", key, value: parsedVal, scope },
|
|
2212
|
+
`Set ${key} = ${value} in ${scope} config`,
|
|
2213
|
+
);
|
|
2214
|
+
return 0;
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2217
|
+
runtime.io.stderr(`config set failed: ${message}\n`);
|
|
2218
|
+
return 1;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
async function cmdConfigList(runtime: Runtime): Promise<number> {
|
|
2223
|
+
try {
|
|
2224
|
+
const config = loadKxmConfig(runtime.cwd);
|
|
2225
|
+
const text = formatKxmConfig(config);
|
|
2226
|
+
print(runtime.io, runtime.json, { ok: true, command: "config list", config }, text);
|
|
2227
|
+
return 0;
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2230
|
+
runtime.io.stderr(`config list failed: ${message}\n`);
|
|
2231
|
+
return 1;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
async function cmdCompletion(runtime: Runtime, shell: string): Promise<number> {
|
|
2236
|
+
try {
|
|
2237
|
+
if (shell !== "bash" && shell !== "zsh" && shell !== "fish") {
|
|
2238
|
+
runtime.io.stderr(`unsupported shell: ${shell}; must be bash, zsh, or fish\n`);
|
|
2239
|
+
return 1;
|
|
2240
|
+
}
|
|
2241
|
+
const script = generateShellCompletion(shell as SupportedShell);
|
|
2242
|
+
runtime.io.stdout(script);
|
|
2243
|
+
return 0;
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2246
|
+
runtime.io.stderr(`completion generation failed: ${message}\n`);
|
|
2247
|
+
return 1;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
async function cmdSuggest(runtime: Runtime, promptParts: string[]): Promise<number> {
|
|
2252
|
+
try {
|
|
2253
|
+
const prompt = promptParts.join(" ").trim();
|
|
2254
|
+
if (!prompt) {
|
|
2255
|
+
runtime.io.stderr("prompt must be non-empty\n");
|
|
2256
|
+
return 2;
|
|
2257
|
+
}
|
|
2258
|
+
const inventory = probeHarnesses({ env: runtime.env });
|
|
2259
|
+
const availableHarnesses = inventory.harnesses.map((h) => ({
|
|
2260
|
+
harness: h.id,
|
|
2261
|
+
auth: h.authenticated === true ? "authenticated" : "unauthenticated",
|
|
2262
|
+
}));
|
|
2263
|
+
const suggestion = suggestWorkflowAndRoles(prompt, { availableHarnesses });
|
|
2264
|
+
|
|
2265
|
+
const text = [
|
|
2266
|
+
`Suggested Workflow: ${suggestion.workflowId} (${suggestion.area})`,
|
|
2267
|
+
`Confidence: ${(suggestion.confidence * 100).toFixed(0)}%`,
|
|
2268
|
+
`Reasons: ${suggestion.reasons.join("; ")}`,
|
|
2269
|
+
`Suggested Skills: ${suggestion.suggestedSkills.join(", ") || "none"}`,
|
|
2270
|
+
`Roles:`,
|
|
2271
|
+
` Planner: ${suggestion.roles.planner.harness} (${suggestion.roles.planner.model})`,
|
|
2272
|
+
` Writer: ${suggestion.roles.writer.harness} (${suggestion.roles.writer.model})`,
|
|
2273
|
+
` Critics: ${suggestion.roles.critics.map((c) => `${c.harness}:${c.model}`).join(", ")}`,
|
|
2274
|
+
` Verifier: ${suggestion.roles.verifier.command}`,
|
|
2275
|
+
``,
|
|
2276
|
+
`Execute with:`,
|
|
2277
|
+
` ${suggestion.suggestedCommand}`,
|
|
2278
|
+
].join("\n");
|
|
2279
|
+
|
|
2280
|
+
print(runtime.io, runtime.json, { ok: true, command: "suggest", prompt, ...suggestion }, text);
|
|
2281
|
+
return 0;
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2284
|
+
runtime.io.stderr(`suggest failed: ${message}\n`);
|
|
2285
|
+
return 1;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
async function cmdGoalCreate(
|
|
2290
|
+
runtime: Runtime,
|
|
2291
|
+
title: string,
|
|
2292
|
+
options: { area?: string; metric?: string[]; targetDate?: string },
|
|
2293
|
+
): Promise<number> {
|
|
2294
|
+
try {
|
|
2295
|
+
const goal = createGoal(runtime.cwd, {
|
|
2296
|
+
title,
|
|
2297
|
+
area: options.area,
|
|
2298
|
+
successMetrics: options.metric,
|
|
2299
|
+
targetDate: options.targetDate,
|
|
2300
|
+
});
|
|
2301
|
+
print(
|
|
2302
|
+
runtime.io,
|
|
2303
|
+
runtime.json,
|
|
2304
|
+
{ ok: true, command: "goal create", goal },
|
|
2305
|
+
`Created goal ${goal.id}: ${goal.title}`,
|
|
2306
|
+
);
|
|
2307
|
+
return 0;
|
|
2308
|
+
} catch (error) {
|
|
2309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2310
|
+
runtime.io.stderr(`goal create failed: ${message}\n`);
|
|
2311
|
+
return 1;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
async function cmdGoalList(runtime: Runtime): Promise<number> {
|
|
2316
|
+
try {
|
|
2317
|
+
const goals = listGoals(runtime.cwd);
|
|
2318
|
+
const text = goals.length === 0
|
|
2319
|
+
? "No goals recorded in .kxm/goals/"
|
|
2320
|
+
: goals.map((g) => `[${g.status}] ${g.id}: ${g.title} (${g.area})`).join("\n");
|
|
2321
|
+
print(runtime.io, runtime.json, { ok: true, command: "goal list", count: goals.length, goals }, text);
|
|
2322
|
+
return 0;
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2325
|
+
runtime.io.stderr(`goal list failed: ${message}\n`);
|
|
2326
|
+
return 1;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
async function cmdTaskCreate(
|
|
2331
|
+
runtime: Runtime,
|
|
2332
|
+
title: string,
|
|
2333
|
+
options: { goal?: string; objective?: string; workflow?: string; tracker?: string; issue?: string },
|
|
2334
|
+
): Promise<number> {
|
|
2335
|
+
try {
|
|
2336
|
+
const task = createTask(runtime.cwd, {
|
|
2337
|
+
title,
|
|
2338
|
+
goalId: options.goal,
|
|
2339
|
+
objective: options.objective ?? title,
|
|
2340
|
+
assignedWorkflow: options.workflow,
|
|
2341
|
+
trackerSync: options.tracker && options.issue
|
|
2342
|
+
? { tracker: options.tracker as TrackerType, issueKey: options.issue }
|
|
2343
|
+
: undefined,
|
|
2344
|
+
});
|
|
2345
|
+
print(
|
|
2346
|
+
runtime.io,
|
|
2347
|
+
runtime.json,
|
|
2348
|
+
{ ok: true, command: "task create", task },
|
|
2349
|
+
`Created task ${task.id}: ${task.title} [${task.status}]`,
|
|
2350
|
+
);
|
|
2351
|
+
return 0;
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2354
|
+
runtime.io.stderr(`task create failed: ${message}\n`);
|
|
2355
|
+
return 1;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
async function cmdTaskList(
|
|
2360
|
+
runtime: Runtime,
|
|
2361
|
+
options: { goal?: string; status?: string },
|
|
2362
|
+
): Promise<number> {
|
|
2363
|
+
try {
|
|
2364
|
+
const tasks = listTasks(runtime.cwd, {
|
|
2365
|
+
goalId: options.goal,
|
|
2366
|
+
status: options.status as TaskStatus | undefined,
|
|
2367
|
+
});
|
|
2368
|
+
const text = tasks.length === 0
|
|
2369
|
+
? "No tasks recorded in .kxm/tasks/"
|
|
2370
|
+
: tasks.map((t) => `[${t.status}] ${t.id}: ${t.title}${t.assignedWorkflow ? ` -> ${t.assignedWorkflow}` : ""}`).join("\n");
|
|
2371
|
+
print(runtime.io, runtime.json, { ok: true, command: "task list", count: tasks.length, tasks }, text);
|
|
2372
|
+
return 0;
|
|
2373
|
+
} catch (error) {
|
|
2374
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2375
|
+
runtime.io.stderr(`task list failed: ${message}\n`);
|
|
2376
|
+
return 1;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
async function cmdTaskGet(runtime: Runtime, taskId: string): Promise<number> {
|
|
2381
|
+
try {
|
|
2382
|
+
const task = getTask(runtime.cwd, taskId);
|
|
2383
|
+
if (!task) {
|
|
2384
|
+
runtime.io.stderr(`Task ${taskId} not found\n`);
|
|
2385
|
+
return 1;
|
|
2386
|
+
}
|
|
2387
|
+
const text = [
|
|
2388
|
+
`Task: ${task.id}`,
|
|
2389
|
+
`Title: ${task.title}`,
|
|
2390
|
+
`Status: ${task.status}`,
|
|
2391
|
+
`Objective: ${task.objective}`,
|
|
2392
|
+
task.assignedWorkflow ? `Workflow: ${task.assignedWorkflow}` : "",
|
|
2393
|
+
task.workflowRunId ? `Active Run: ${task.workflowRunId}` : "",
|
|
2394
|
+
task.trackerSync ? `Tracker: ${task.trackerSync.tracker} (#${task.trackerSync.issueKey}) [${task.trackerSync.syncStatus}]` : "",
|
|
2395
|
+
].filter(Boolean).join("\n");
|
|
2396
|
+
print(runtime.io, runtime.json, { ok: true, command: "task get", task }, text);
|
|
2397
|
+
return 0;
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2400
|
+
runtime.io.stderr(`task get failed: ${message}\n`);
|
|
2401
|
+
return 1;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
async function cmdTaskRun(runtime: Runtime, taskId: string): Promise<number> {
|
|
2406
|
+
try {
|
|
2407
|
+
const task = getTask(runtime.cwd, taskId);
|
|
2408
|
+
if (!task) {
|
|
2409
|
+
runtime.io.stderr(`Task ${taskId} not found\n`);
|
|
2410
|
+
return 1;
|
|
2411
|
+
}
|
|
2412
|
+
const workflow = task.assignedWorkflow ?? "software-engineering/feature-implementation";
|
|
2413
|
+
const exitCode = await cmdVnextRun(runtime, workflow, [task.objective]);
|
|
2414
|
+
if (exitCode === 0) {
|
|
2415
|
+
updateTaskStatus(runtime.cwd, taskId, "in_progress");
|
|
2416
|
+
}
|
|
2417
|
+
return exitCode;
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2420
|
+
runtime.io.stderr(`task run failed: ${message}\n`);
|
|
2421
|
+
return 1;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
async function cmdTaskSync(runtime: Runtime, taskId: string): Promise<number> {
|
|
2426
|
+
try {
|
|
2427
|
+
const synced = syncTaskWithTracker(runtime.cwd, taskId);
|
|
2428
|
+
print(
|
|
2429
|
+
runtime.io,
|
|
2430
|
+
runtime.json,
|
|
2431
|
+
{ ok: true, command: "task sync", task: synced },
|
|
2432
|
+
`Synced task ${taskId} with ${synced.trackerSync?.tracker} #${synced.trackerSync?.issueKey}`,
|
|
2433
|
+
);
|
|
2434
|
+
return 0;
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2437
|
+
runtime.io.stderr(`task sync failed: ${message}\n`);
|
|
2438
|
+
return 1;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
async function cmdStudioLayout(runtime: Runtime, workflowPath?: string): Promise<number> {
|
|
2443
|
+
try {
|
|
2444
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
2445
|
+
let filePath = workflowPath;
|
|
2446
|
+
if (!filePath) {
|
|
2447
|
+
filePath = resolve(projectRoot, ".kxm", "workflows", "feature-implementation.yaml");
|
|
2448
|
+
}
|
|
2449
|
+
let yamlContent: string;
|
|
2450
|
+
let workflowId = "software-engineering/feature-implementation";
|
|
2451
|
+
if (existsSync(filePath)) {
|
|
2452
|
+
yamlContent = readFileSync(filePath, "utf8");
|
|
2453
|
+
} else {
|
|
2454
|
+
yamlContent = `schema: kxm.workflow.v1
|
|
2455
|
+
description: Feature implementation workflow
|
|
2456
|
+
coordinator: coordinator
|
|
2457
|
+
limits:
|
|
2458
|
+
maxTransitions: 12
|
|
2459
|
+
steps:
|
|
2460
|
+
- id: plan
|
|
2461
|
+
kind: agent
|
|
2462
|
+
agent: planner
|
|
2463
|
+
maxAttempts: 2
|
|
2464
|
+
on:
|
|
2465
|
+
passed: implement
|
|
2466
|
+
failed:
|
|
2467
|
+
target: $terminal
|
|
2468
|
+
terminalStatus: failed
|
|
2469
|
+
- id: implement
|
|
2470
|
+
kind: agent
|
|
2471
|
+
agent: writer
|
|
2472
|
+
maxAttempts: 3
|
|
2473
|
+
on:
|
|
2474
|
+
passed: verify
|
|
2475
|
+
failed:
|
|
2476
|
+
target: $terminal
|
|
2477
|
+
terminalStatus: failed
|
|
2478
|
+
- id: verify
|
|
2479
|
+
kind: gate
|
|
2480
|
+
gate: verify-gate
|
|
2481
|
+
expect: pass
|
|
2482
|
+
maxAttempts: 2
|
|
2483
|
+
on:
|
|
2484
|
+
passed:
|
|
2485
|
+
target: $terminal
|
|
2486
|
+
terminalStatus: completed
|
|
2487
|
+
failed:
|
|
2488
|
+
target: implement
|
|
2489
|
+
maxTransitions: 2
|
|
2490
|
+
`;
|
|
2491
|
+
}
|
|
2492
|
+
const parsedYaml = parseYaml(yamlContent) as any;
|
|
2493
|
+
const plan = compileVnextWorkflow({ id: workflowId, value: parsedYaml });
|
|
2494
|
+
const layout = generateStudioLayout(plan);
|
|
2495
|
+
print(runtime.io, runtime.json, { ok: true, command: "studio layout", layout }, JSON.stringify(layout, null, 2));
|
|
2496
|
+
return 0;
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2499
|
+
runtime.io.stderr(`studio layout failed: ${message}\n`);
|
|
2500
|
+
return 1;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
async function cmdRoutingReport(
|
|
2505
|
+
runtime: Runtime,
|
|
2506
|
+
options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string },
|
|
2507
|
+
): Promise<number> {
|
|
2508
|
+
const file = options.file ?? telemetryPath(runtime.dirs.logs);
|
|
2509
|
+
const records = readRoutingRecords(file).map((entry) => entry.routing);
|
|
2510
|
+
const includeEquivalentListCost = Boolean(options.equivalentListCost || options.listPrices);
|
|
2511
|
+
|
|
2512
|
+
let catalog: PriceCatalog | undefined;
|
|
2513
|
+
if (includeEquivalentListCost) {
|
|
2514
|
+
try {
|
|
2515
|
+
const pricesPath = options.prices ? resolve(runtime.cwd, options.prices) : join(runtime.dirs.workspace, "prices.yaml");
|
|
2516
|
+
catalog = loadPriceCatalog(pricesPath);
|
|
2517
|
+
} catch {
|
|
2518
|
+
// price catalog optional / best effort
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
const report = generateRoutingReport(records, { catalog, includeEquivalentListCost });
|
|
2523
|
+
|
|
2524
|
+
if (records.length === 0) {
|
|
2525
|
+
print(runtime.io, runtime.json, { ok: true, command: "routing report", file, configurations: [], report }, "no routing records in telemetry");
|
|
2526
|
+
return 0;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const v1Records = records.filter((r) => r.schema === "kxm.routing-record.v1") as any[];
|
|
2530
|
+
const configurations = v1Records.length > 0
|
|
2531
|
+
? [...groupByBehavior(v1Records).entries()]
|
|
2532
|
+
.map(([hash, group]) => ({ ...compareRoutingRecords(group), behavioralSha256: hash }))
|
|
2533
|
+
.sort((left, right) => right.runs - left.runs || left.behavioralSha256.localeCompare(right.behavioralSha256))
|
|
2534
|
+
: [];
|
|
2535
|
+
|
|
2536
|
+
const text = formatRoutingReport(report, { equivalentListCost: includeEquivalentListCost });
|
|
2537
|
+
print(
|
|
2538
|
+
runtime.io,
|
|
2539
|
+
runtime.json,
|
|
2540
|
+
{ ok: true, command: "routing report", file, configurations, report },
|
|
2541
|
+
text,
|
|
2542
|
+
);
|
|
2543
|
+
return 0;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string | undefined, options: { payload?: string; deliveryId?: string; event?: string }): Promise<number> {
|
|
2547
|
+
const definitionId = definitionIdArg || runtime.env.KXM_WORKFLOW_ID?.trim();
|
|
2548
|
+
const deliveryId = String(options.deliveryId || `cli-${randomUUID()}`);
|
|
2549
|
+
const event = options.event;
|
|
2550
|
+
const payloadFlag = options.payload ?? "{}";
|
|
2551
|
+
if (!definitionId) {
|
|
2552
|
+
runtime.io.stderr("workflow start requires <definitionId> (or KXM_WORKFLOW_ID)\n");
|
|
2553
|
+
return 2;
|
|
2554
|
+
}
|
|
2555
|
+
let secret: string | undefined;
|
|
2556
|
+
try {
|
|
2557
|
+
secret = workflowCredential(runtime, definitionId, "start");
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
return reportWorkflowConfigError(runtime, error);
|
|
2560
|
+
}
|
|
2561
|
+
if (!secret) {
|
|
2562
|
+
runtime.io.stderr("workflow start requires KXM_WORKFLOW_SECRET when no active definition source is configured\n");
|
|
2563
|
+
return 2;
|
|
2564
|
+
}
|
|
2565
|
+
let payload: Record<string, unknown>;
|
|
2566
|
+
try {
|
|
2567
|
+
const raw = payloadFlag.startsWith("@") ? readFileSync(resolve(runtime.cwd, payloadFlag.slice(1)), "utf8") : payloadFlag;
|
|
2568
|
+
const value = JSON.parse(raw) as unknown;
|
|
2569
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("object required");
|
|
2570
|
+
payload = value as Record<string, unknown>;
|
|
2571
|
+
} catch {
|
|
2572
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "invalid_payload" }, "workflow payload must be a JSON object or @file");
|
|
2573
|
+
return 2;
|
|
2574
|
+
}
|
|
2575
|
+
if (runtime.dryRun) {
|
|
2576
|
+
print(runtime.io, runtime.json, { ok: true, command: "workflow start", dryRun: true, definitionId, deliveryId, event }, "would POST a signed workflow webhook");
|
|
2577
|
+
return 0;
|
|
2578
|
+
}
|
|
2579
|
+
try {
|
|
2580
|
+
const response = await postWorkflowStart({ serverUrl: runtime.serverUrl, definitionId, secret, deliveryId, ...(event ? { event } : {}), payload, fetchImpl: runtime.fetchImpl });
|
|
2581
|
+
print(runtime.io, runtime.json, { ok: true, command: "workflow start", definitionId, deliveryId, ...response }, `started workflow ${response.runId ?? "accepted"}`);
|
|
2582
|
+
return 0;
|
|
2583
|
+
} catch {
|
|
2584
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "workflow_start_failed" }, "signed workflow start failed");
|
|
2585
|
+
return 1;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
async function cmdWorkflowDegrade(runtime: Runtime, runId: string, stageId: string, options: { requirement?: string; reason?: string }): Promise<number> {
|
|
2590
|
+
const requirementKey = options.requirement?.trim() ?? "";
|
|
2591
|
+
const reason = options.reason?.trim() ?? "";
|
|
2592
|
+
const adminToken = runtime.env.KXM_AUTH_TOKEN?.trim();
|
|
2593
|
+
if (!runId || !stageId || !requirementKey || !reason || !adminToken) {
|
|
2594
|
+
runtime.io.stderr("workflow degrade requires <runId> <stageId>, --requirement, --reason, and KXM_AUTH_TOKEN\n");
|
|
2595
|
+
return 2;
|
|
2596
|
+
}
|
|
2597
|
+
const worker = gateOf(runtime, "degrade");
|
|
2598
|
+
if (runtime.dryRun) {
|
|
2599
|
+
printWorker(
|
|
2600
|
+
runtime,
|
|
2601
|
+
worker,
|
|
2602
|
+
{ ok: true, command: "workflow degrade", dryRun: true, runId, stageId, requirementKey },
|
|
2603
|
+
`would approve configured degraded quorum for ${runId}/${stageId}/${requirementKey}`,
|
|
2604
|
+
);
|
|
2605
|
+
return 0;
|
|
2606
|
+
}
|
|
2607
|
+
try {
|
|
2608
|
+
const result = await postWorkflowDegradation({
|
|
2609
|
+
serverUrl: runtime.serverUrl,
|
|
2610
|
+
authToken: adminToken,
|
|
2611
|
+
runId,
|
|
2612
|
+
stageId,
|
|
2613
|
+
requirementKey,
|
|
2614
|
+
reason,
|
|
2615
|
+
fetchImpl: runtime.fetchImpl,
|
|
2616
|
+
});
|
|
2617
|
+
printWorker(
|
|
2618
|
+
runtime,
|
|
2619
|
+
worker,
|
|
2620
|
+
{ ok: true, command: "workflow degrade", runId, stageId, requirementKey, ...result },
|
|
2621
|
+
result.duplicate ? "degraded quorum was already approved" : "approved configured degraded quorum",
|
|
2622
|
+
);
|
|
2623
|
+
return 0;
|
|
2624
|
+
} catch {
|
|
2625
|
+
printWorker(
|
|
2626
|
+
runtime,
|
|
2627
|
+
worker,
|
|
2628
|
+
{ ok: false, command: "workflow degrade", error: "workflow_degradation_failed", runId, stageId, requirementKey },
|
|
2629
|
+
"workflow degradation approval failed",
|
|
2630
|
+
);
|
|
2631
|
+
return 1;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
async function cmdWorkflowInspect(runtime: Runtime, action: "list" | "get", runId?: string): Promise<number> {
|
|
2636
|
+
const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
|
|
2637
|
+
if (action === "get" && !runId) {
|
|
2638
|
+
runtime.io.stderr(`Usage: ${CLI_NAME} workflow get <runId>\n`);
|
|
2639
|
+
return 2;
|
|
2640
|
+
}
|
|
2641
|
+
try {
|
|
2642
|
+
const snapshot = localWorkflowSnapshot(dataPath, runId);
|
|
2643
|
+
if (runId && snapshot.runs.length === 0) {
|
|
2644
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow get", error: "workflow_not_found" }, "workflow not found");
|
|
2645
|
+
return 1;
|
|
2646
|
+
}
|
|
2647
|
+
const value = action === "get"
|
|
2648
|
+
? { ok: true, command: "workflow get", run: snapshot.runs[0], journal: snapshot.journal }
|
|
2649
|
+
: { ok: true, command: "workflow list", runs: snapshot.runs };
|
|
2650
|
+
print(runtime.io, runtime.json, value, action === "get" ? `workflow ${runId}` : `${snapshot.runs.length} workflow(s)`);
|
|
2651
|
+
return 0;
|
|
2652
|
+
} catch {
|
|
2653
|
+
print(runtime.io, runtime.json, { ok: false, command: `workflow ${action}`, error: "state_unavailable" }, "local workflow state is unavailable");
|
|
2654
|
+
return 1;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string): Promise<number> {
|
|
2659
|
+
if (!runId || !signalKey || !status || !summary) {
|
|
2660
|
+
runtime.io.stderr(`Usage: ${CLI_NAME} gate signal <runId> <signalKey> <passed|warning|failed> <summary> [<required-key>=<evidence> ...]\n`);
|
|
2661
|
+
return 2;
|
|
2662
|
+
}
|
|
2663
|
+
if (status !== "passed" && status !== "warning" && status !== "failed") {
|
|
2664
|
+
runtime.io.stderr("status must be passed, warning, or failed\n");
|
|
2665
|
+
return 2;
|
|
2666
|
+
}
|
|
2667
|
+
let evidence: WorkflowEvidenceInput;
|
|
2668
|
+
try {
|
|
2669
|
+
evidence = parseEvidencePairs(evidenceArgs);
|
|
2670
|
+
} catch (error) {
|
|
2671
|
+
runtime.io.stderr(`${error instanceof Error ? error.message : "invalid evidence"}\n`);
|
|
2672
|
+
return 2;
|
|
2673
|
+
}
|
|
2674
|
+
const worker = gateOf(runtime, "signal");
|
|
2675
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
2676
|
+
if (projectRoot && /^run_[a-f0-9]{32}$/i.test(runId)) {
|
|
2677
|
+
if (runtime.dryRun) {
|
|
2678
|
+
printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, summary, evidence }, "would post signal to vNext run");
|
|
2679
|
+
return 0;
|
|
2680
|
+
}
|
|
2681
|
+
const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`);
|
|
2682
|
+
try {
|
|
2683
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
2684
|
+
const posted = await vnextRuntimeRequest(
|
|
2685
|
+
supervisor,
|
|
2686
|
+
"POST",
|
|
2687
|
+
`/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`,
|
|
2688
|
+
{ signalKey, status, summary, evidence, deliveryId },
|
|
2689
|
+
);
|
|
2690
|
+
printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, unblocked: posted.unblocked === true, deliveryId }, "posted signal to vNext run");
|
|
2691
|
+
return 0;
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
const msg = error instanceof Error ? error.message : "signal_failed";
|
|
2694
|
+
printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", detail: msg, deliveryId }, "signal to vNext run failed");
|
|
2695
|
+
return 1;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim();
|
|
2700
|
+
if (!definitionId) {
|
|
2701
|
+
runtime.io.stderr("signal requires KXM_WORKFLOW_ID\n");
|
|
2702
|
+
return 2;
|
|
2703
|
+
}
|
|
2704
|
+
let signalSecret: string | undefined;
|
|
2705
|
+
try {
|
|
2706
|
+
signalSecret = workflowCredential(runtime, definitionId, "signal");
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
return reportWorkflowConfigError(runtime, error);
|
|
2709
|
+
}
|
|
2710
|
+
if (!signalSecret) {
|
|
2711
|
+
runtime.io.stderr("signal requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n");
|
|
2712
|
+
return 2;
|
|
2713
|
+
}
|
|
2714
|
+
if (runtime.dryRun) {
|
|
2715
|
+
printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, summary, evidence }, "would post signed signal");
|
|
2716
|
+
return 0;
|
|
2717
|
+
}
|
|
2718
|
+
const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`);
|
|
2719
|
+
try {
|
|
2720
|
+
const posted = await postWorkflowSignal({
|
|
2721
|
+
serverUrl: runtime.serverUrl,
|
|
2722
|
+
definitionId,
|
|
2723
|
+
signalSecret,
|
|
2724
|
+
runId,
|
|
2725
|
+
signalKey,
|
|
2726
|
+
status,
|
|
2727
|
+
summary,
|
|
2728
|
+
evidence,
|
|
2729
|
+
deliveryId,
|
|
2730
|
+
fetchImpl: runtime.fetchImpl,
|
|
2731
|
+
});
|
|
2732
|
+
printWorker(runtime, worker, { ok: true, command: "signal", duplicate: posted.duplicate, deliveryId }, "posted signed signal");
|
|
2733
|
+
return 0;
|
|
2734
|
+
} catch {
|
|
2735
|
+
printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", deliveryId }, "signed signal failed");
|
|
2736
|
+
return 1;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
async function cmdGithubWatch(runtime: Runtime, options: {
|
|
2741
|
+
runId?: string;
|
|
2742
|
+
stageId?: string;
|
|
2743
|
+
signalKey?: string;
|
|
2744
|
+
repo?: string;
|
|
2745
|
+
pr?: string;
|
|
2746
|
+
required?: string;
|
|
2747
|
+
timeoutMs?: string;
|
|
2748
|
+
intervalMs?: string;
|
|
2749
|
+
deliveryId?: string;
|
|
2750
|
+
}): Promise<number> {
|
|
2751
|
+
const token = runtime.env.GITHUB_TOKEN?.trim() || runtime.env.GH_TOKEN?.trim();
|
|
2752
|
+
const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim();
|
|
2753
|
+
const runId = String(options.runId || "");
|
|
2754
|
+
const stageId = String(options.stageId || "");
|
|
2755
|
+
const signalKey = String(options.signalKey || "");
|
|
2756
|
+
const repo = String(options.repo || "");
|
|
2757
|
+
const pr = Number(options.pr);
|
|
2758
|
+
if (!definitionId || !runId || !stageId || !signalKey || !repo || !Number.isInteger(pr)) {
|
|
2759
|
+
runtime.io.stderr("github watch requires KXM_WORKFLOW_ID, --run-id, --stage-id, --signal-key, --repo, --pr\n");
|
|
2760
|
+
return 2;
|
|
2761
|
+
}
|
|
2762
|
+
let signalSecret: string | undefined;
|
|
2763
|
+
try {
|
|
2764
|
+
signalSecret = workflowCredential(runtime, definitionId, "signal");
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
return reportWorkflowConfigError(runtime, error);
|
|
2767
|
+
}
|
|
2768
|
+
if (!signalSecret) {
|
|
2769
|
+
runtime.io.stderr("github watch requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n");
|
|
2770
|
+
return 2;
|
|
2771
|
+
}
|
|
2772
|
+
const result = await watchGithubChecks({
|
|
2773
|
+
serverUrl: runtime.serverUrl,
|
|
2774
|
+
definitionId,
|
|
2775
|
+
signalSecret,
|
|
2776
|
+
runId,
|
|
2777
|
+
stageId,
|
|
2778
|
+
signalKey,
|
|
2779
|
+
repo,
|
|
2780
|
+
pr,
|
|
2781
|
+
required: options.required
|
|
2782
|
+
? [...new Set(String(options.required).split(",").map((name) => name.trim()).filter(Boolean))]
|
|
2783
|
+
: [],
|
|
2784
|
+
timeoutMs: Number(options.timeoutMs || 1_800_000),
|
|
2785
|
+
intervalMs: Number(options.intervalMs || 15_000),
|
|
2786
|
+
...(options.deliveryId ? { deliveryId: options.deliveryId } : {}),
|
|
2787
|
+
...(token ? { token } : {}),
|
|
2788
|
+
dryRun: runtime.dryRun,
|
|
2789
|
+
fetchImpl: runtime.fetchImpl,
|
|
2790
|
+
...(runtime.io.now ? { now: runtime.io.now } : {}),
|
|
2791
|
+
...(runtime.io.sleep ? { sleep: runtime.io.sleep } : {}),
|
|
2792
|
+
});
|
|
2793
|
+
const worker = gateOf(runtime, "github-watch");
|
|
2794
|
+
const payload = {
|
|
2795
|
+
ok: result.exitCode === 0,
|
|
2796
|
+
command: "github watch",
|
|
2797
|
+
posted: result.posted,
|
|
2798
|
+
status: result.status,
|
|
2799
|
+
summary: result.summary,
|
|
2800
|
+
evidence: result.evidence,
|
|
2801
|
+
deliveryId: result.deliveryId,
|
|
2802
|
+
skipped: result.skipped,
|
|
2803
|
+
};
|
|
2804
|
+
const outcome: WorkerOutcome = result.exitCode === 0
|
|
2805
|
+
? (result.status === "warning" ? "warning" : "passed")
|
|
2806
|
+
: "failed";
|
|
2807
|
+
if (JSON.stringify(payload).includes(token ?? "___never___") || Object.keys(runtime.env).some((key) => maskEnvName(key) && JSON.stringify(payload).includes(String(runtime.env[key])))) {
|
|
2808
|
+
printWorker(runtime, worker, { ok: false, command: "github watch", error: "redaction_failure" }, "refusing to print a payload that contains a secret");
|
|
2809
|
+
return 1;
|
|
2810
|
+
}
|
|
2811
|
+
printWorker(runtime, worker, payload, result.summary, outcome);
|
|
2812
|
+
return result.exitCode;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
async function cmdRetrospectiveExport(runtime: Runtime, runId: string, options: { input?: string; outDir?: string }): Promise<number> {
|
|
2816
|
+
if (!runId) {
|
|
2817
|
+
runtime.io.stderr(`Usage: ${CLI_NAME} workflow export <runId>\n`);
|
|
2818
|
+
return 2;
|
|
2819
|
+
}
|
|
2820
|
+
const snapshotFlag = String(options.input || "");
|
|
2821
|
+
const snapshotPath = snapshotFlag ? resolve(runtime.cwd, snapshotFlag) : "";
|
|
2822
|
+
let snapshot: { run: WorkflowRun; journal: WorkflowJournalEntry[] };
|
|
2823
|
+
try {
|
|
2824
|
+
if (snapshotPath) {
|
|
2825
|
+
if (!existsSync(snapshotPath)) throw new Error("snapshot_missing");
|
|
2826
|
+
snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as typeof snapshot;
|
|
2827
|
+
} else {
|
|
2828
|
+
const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
|
|
2829
|
+
const local = localWorkflowSnapshot(dataPath, runId);
|
|
2830
|
+
if (!local.runs[0]) throw new Error("workflow_not_found");
|
|
2831
|
+
snapshot = { run: local.runs[0], journal: local.journal };
|
|
2832
|
+
}
|
|
2833
|
+
if (snapshot.run.id !== runId) throw new Error("run_id_mismatch");
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
const reason = error instanceof Error ? error.message : "snapshot_invalid";
|
|
2836
|
+
print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: reason }, "retrospective source is invalid or unavailable");
|
|
2837
|
+
return 1;
|
|
2838
|
+
}
|
|
2839
|
+
const doc = buildRetrospective(snapshot.run, snapshot.journal);
|
|
2840
|
+
const outDir = resolve(runtime.cwd, String(options.outDir || join(runtime.dirs.assets, "retrospectives")));
|
|
2841
|
+
const assetsRoot = resolve(runtime.dirs.assets);
|
|
2842
|
+
const assetsPrefix = `${assetsRoot}${process.platform === "win32" ? "\\" : "/"}`;
|
|
2843
|
+
if (outDir !== assetsRoot && !outDir.startsWith(assetsPrefix)) {
|
|
2844
|
+
print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: "output_outside_workspace_assets" }, "retrospectives must stay under the workspace assets directory");
|
|
2845
|
+
return 2;
|
|
2846
|
+
}
|
|
2847
|
+
if (runtime.dryRun) {
|
|
2848
|
+
print(runtime.io, runtime.json, { ok: true, command: "retrospective export", dryRun: true, runId: doc.runId }, `would export ${doc.runId}`);
|
|
2849
|
+
return 0;
|
|
2850
|
+
}
|
|
2851
|
+
const written = writeRetrospective(outDir, doc);
|
|
2852
|
+
print(runtime.io, runtime.json, { ok: true, command: "retrospective export", ...written, reviewDecision: doc.reviewDecision }, `exported ${written.jsonPath}`);
|
|
2853
|
+
return 0;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
async function ensureCliClient(runtime: Runtime): Promise<HubClient> {
|
|
2857
|
+
const serverUrl = runtime.serverUrl;
|
|
2858
|
+
const project = runtime.env.KXM_PROJECT?.trim() || basename(runtime.dirs.workspace || runtime.cwd);
|
|
2859
|
+
const name = runtime.env.KXM_AGENT_NAME?.trim() || `cli-${process.pid}`;
|
|
2860
|
+
const purpose = runtime.env.KXM_AGENT_PURPOSE?.trim() || "CLI agent client";
|
|
2861
|
+
const authToken = runtime.env.KXM_AUTH_TOKEN?.trim();
|
|
2862
|
+
const client = new HubClient({
|
|
2863
|
+
serverUrl,
|
|
2864
|
+
name,
|
|
2865
|
+
project,
|
|
2866
|
+
purpose,
|
|
2867
|
+
...(authToken ? { authToken } : {}),
|
|
2868
|
+
});
|
|
2869
|
+
await client.start(() => {});
|
|
2870
|
+
return client;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
function parseJsonOption(val: unknown): unknown {
|
|
2874
|
+
if (typeof val === "string") {
|
|
2875
|
+
const trimmed = val.trim();
|
|
2876
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
2877
|
+
try {
|
|
2878
|
+
return JSON.parse(trimmed);
|
|
2879
|
+
} catch {
|
|
2880
|
+
return val;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
return val;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
async function dispatchAgentCliCommand(
|
|
2888
|
+
runtime: Runtime,
|
|
2889
|
+
toolName: string,
|
|
2890
|
+
rawArgs: Record<string, unknown>,
|
|
2891
|
+
): Promise<number> {
|
|
2892
|
+
const policy = enforceToolPolicy(toolName, runtime.env);
|
|
2893
|
+
if (!policy.allowed) {
|
|
2894
|
+
print(
|
|
2895
|
+
runtime.io,
|
|
2896
|
+
runtime.json,
|
|
2897
|
+
{ ok: false, error: policy.error ?? "tool_policy_denied", detail: policy.detail },
|
|
2898
|
+
`tool_policy_denied: ${policy.detail ?? policy.error}`,
|
|
2899
|
+
);
|
|
2900
|
+
return 1;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
const cmd = AGENT_COMMANDS_MAP.get(toolName);
|
|
2904
|
+
if (!cmd) {
|
|
2905
|
+
print(runtime.io, runtime.json, { ok: false, error: "unknown_command", detail: toolName }, `unknown command: ${toolName}`);
|
|
2906
|
+
return 2;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
let args: Record<string, unknown> = {};
|
|
2910
|
+
if (typeof rawArgs.payload === "string" && rawArgs.payload.trim()) {
|
|
2911
|
+
try {
|
|
2912
|
+
const parsed = JSON.parse(rawArgs.payload.trim());
|
|
2913
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2914
|
+
args = { ...parsed };
|
|
2915
|
+
}
|
|
2916
|
+
} catch {
|
|
2917
|
+
print(runtime.io, runtime.json, { ok: false, error: "invalid_payload", detail: "failed to parse --payload JSON" }, "invalid payload JSON");
|
|
2918
|
+
return 2;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
for (const [key, val] of Object.entries(rawArgs)) {
|
|
2923
|
+
if (val !== undefined && key !== "payload") {
|
|
2924
|
+
if (key === "timeoutMs" || key === "ttlMs" || key === "attempt") {
|
|
2925
|
+
args[key] = Number(val);
|
|
2926
|
+
} else if (key === "targets" && typeof val === "string") {
|
|
2927
|
+
args[key] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2928
|
+
} else {
|
|
2929
|
+
args[key] = parseJsonOption(val);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// Handle vNext run binding for workflow wait
|
|
2935
|
+
if (toolName === "kxm_workflow_wait") {
|
|
2936
|
+
const runId = typeof args.runId === "string" ? args.runId : undefined;
|
|
2937
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
2938
|
+
if (runId && projectRoot && /^run_[a-f0-9]{32}$/i.test(runId)) {
|
|
2939
|
+
if (runtime.dryRun) {
|
|
2940
|
+
print(runtime.io, runtime.json, { ok: true, command: "workflow wait", runId, dryRun: true }, `would wait for signal on vNext run ${runId}`);
|
|
2941
|
+
return 0;
|
|
2942
|
+
}
|
|
2943
|
+
try {
|
|
2944
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
2945
|
+
const result = await vnextRuntimeRequest(
|
|
2946
|
+
supervisor,
|
|
2947
|
+
"POST",
|
|
2948
|
+
`/v1/runs/${encodeURIComponent(runId)}/wait?projectRoot=${encodeURIComponent(projectRoot)}`,
|
|
2949
|
+
args,
|
|
2950
|
+
);
|
|
2951
|
+
print(runtime.io, runtime.json, result, `waiting for signal on vNext run ${runId}`);
|
|
2952
|
+
return 0;
|
|
2953
|
+
} catch (error) {
|
|
2954
|
+
const msg = error instanceof Error ? error.message : "vnext_wait_failed";
|
|
2955
|
+
print(runtime.io, runtime.json, { ok: false, error: "vnext_wait_failed", detail: msg }, `vNext wait failed: ${msg}`);
|
|
2956
|
+
return 1;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
if (runtime.dryRun) {
|
|
2962
|
+
print(runtime.io, runtime.json, { ok: true, command: `${cmd.group} ${cmd.verb}`, dryRun: true, args }, `would execute ${cmd.group} ${cmd.verb}`);
|
|
2963
|
+
return 0;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
let client: HubClient | undefined;
|
|
2967
|
+
try {
|
|
2968
|
+
client = await ensureCliClient(runtime);
|
|
2969
|
+
const output = await cmd.execute(client, args);
|
|
2970
|
+
const payload = (output && typeof output === "object" ? output : { result: output }) as object;
|
|
2971
|
+
print(runtime.io, runtime.json, payload, JSON.stringify(output, null, 2));
|
|
2972
|
+
return 0;
|
|
2973
|
+
} catch (error) {
|
|
2974
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2975
|
+
print(runtime.io, runtime.json, { ok: false, error: "command_failed", detail: message }, `command failed: ${message}`);
|
|
2976
|
+
return 1;
|
|
2977
|
+
} finally {
|
|
2978
|
+
if (client) {
|
|
2979
|
+
try {
|
|
2980
|
+
await client.stop();
|
|
2981
|
+
} catch {
|
|
2982
|
+
// best effort
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
2989
|
+
const bind = (action: (runtime: Runtime, ...args: never[]) => Promise<number>) => {
|
|
2990
|
+
return async function commandAction(this: Command, ...args: unknown[]) {
|
|
2991
|
+
const command = args.at(-1) instanceof Command ? args.at(-1) as Command : this;
|
|
2992
|
+
result.code = await (action as (runtime: Runtime, ...rest: unknown[]) => Promise<number>)(runtimeFrom(ctx, command), ...args.slice(0, -1));
|
|
2993
|
+
};
|
|
2994
|
+
};
|
|
2995
|
+
|
|
2996
|
+
const program = new Command(CLI_NAME);
|
|
2997
|
+
program
|
|
2998
|
+
.description("KontextMind local-first orchestration CLI")
|
|
2999
|
+
.version(readInstalledKxmVersion(repoRoot), "-V, --version", "Print the installed kxm version")
|
|
3000
|
+
.exitOverride()
|
|
3001
|
+
.configureOutput({
|
|
3002
|
+
writeOut: (text) => ctx.io.stdout(text),
|
|
3003
|
+
writeErr: (text) => ctx.io.stderr(text),
|
|
3004
|
+
})
|
|
3005
|
+
.helpCommand("help", "Show help");
|
|
3006
|
+
addGlobalOptions(program);
|
|
3007
|
+
|
|
3008
|
+
program.command("init").description("Create, validate, or plan migration of a vNext project")
|
|
3009
|
+
.option("--json", "Print machine-readable JSON")
|
|
3010
|
+
.option("--dry-run", "Plan without making changes")
|
|
3011
|
+
.option("--name <name>", "Project display name for a new project")
|
|
3012
|
+
.option("--project-id <id>", "Stable project ID for controlled provisioning")
|
|
3013
|
+
.option("--repository <id=absolute-path>", "Bind a member repository outside Git configuration", (value, previous: string[]) => [...previous, value], [])
|
|
3014
|
+
.action(async function initAction(this: Command, options: { name?: string; projectId?: string; repository?: string[] }) {
|
|
3015
|
+
result.code = await cmdVnextInit(runtimeFrom(ctx, this), options);
|
|
3016
|
+
});
|
|
3017
|
+
|
|
3018
|
+
const migrate = addGlobalOptions(program.command("migrate").description("Plan, apply, and verify legacy JSON configuration migration")); migrate.helpCommand("help", "Show migrate help");
|
|
3019
|
+
addGlobalOptions(migrate.command("plan").description("Compute the deterministic legacy-to-vNext migration plan without writes"))
|
|
3020
|
+
.action(async function migratePlanAction(this: Command) {
|
|
3021
|
+
result.code = await cmdVnextMigratePlan(runtimeFrom(ctx, this));
|
|
3022
|
+
});
|
|
3023
|
+
addGlobalOptions(migrate.command("apply").description("Install a reviewed migration with a hash-linked receipt"))
|
|
3024
|
+
.option("--decisions <file>", "Reviewed kxm.migration-decision.v1 YAML file")
|
|
3025
|
+
.option("--project-id <id>", "Stable project ID for controlled provisioning")
|
|
3026
|
+
.option("--name <name>", "Project display name")
|
|
3027
|
+
.action(async function migrateApplyAction(this: Command, options: { decisions?: string; projectId?: string; name?: string }) {
|
|
3028
|
+
result.code = await cmdVnextMigrateApply(runtimeFrom(ctx, this), options);
|
|
3029
|
+
});
|
|
3030
|
+
addGlobalOptions(migrate.command("verify").description("Verify a migration receipt against current sources and target bundle"))
|
|
3031
|
+
.action(async function migrateVerifyAction(this: Command) {
|
|
3032
|
+
result.code = await cmdVnextMigrateVerify(runtimeFrom(ctx, this));
|
|
3033
|
+
});
|
|
3034
|
+
|
|
3035
|
+
addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of all stores with a hashed manifest"))
|
|
3036
|
+
.option("--out <dir>", "Directory to write backup and manifest")
|
|
3037
|
+
.action(async function backupAction(this: Command, options: { out?: string }) {
|
|
3038
|
+
result.code = await cmdBackup(runtimeFrom(ctx, this), options);
|
|
3039
|
+
});
|
|
3040
|
+
|
|
3041
|
+
addGlobalOptions(program.command("restore <manifest>").description("Restore SQLite stores from a verified backup manifest"))
|
|
3042
|
+
.action(async function restoreAction(this: Command, manifest: string) {
|
|
3043
|
+
result.code = await cmdRestore(runtimeFrom(ctx, this), manifest);
|
|
3044
|
+
});
|
|
3045
|
+
|
|
3046
|
+
addGlobalOptions(program.command("run").description("Create a vNext run (offline-first; no steps execute until the run engine lands)")
|
|
3047
|
+
.argument("[workflow]", "Workflow id to run")
|
|
3048
|
+
.argument("[prompt...]", "Run prompt (hashed, never stored raw)")
|
|
3049
|
+
.action(async function runAction(this: Command, workflow: string | undefined, promptParts: string[]) {
|
|
3050
|
+
result.code = await cmdVnextRun(runtimeFrom(ctx, this), workflow, promptParts);
|
|
3051
|
+
}));
|
|
3052
|
+
const runCmd = addGlobalOptions(program.command("runs").description("Inspect vNext runs"));
|
|
3053
|
+
runCmd.helpCommand("help", "Show runs help");
|
|
3054
|
+
addGlobalOptions(runCmd.command("status").description("Show the projected status of a run"))
|
|
3055
|
+
.argument("<runId>", "Run id")
|
|
3056
|
+
.action(async function runStatusAction(this: Command, runId: string) {
|
|
3057
|
+
result.code = await cmdVnextRunStatus(runtimeFrom(ctx, this), runId);
|
|
3058
|
+
});
|
|
3059
|
+
addGlobalOptions(runCmd.command("cancel").description("Durably request cancellation of a run"))
|
|
3060
|
+
.argument("<runId>", "Run id")
|
|
3061
|
+
.action(async function runCancelAction(this: Command, runId: string) {
|
|
3062
|
+
result.code = await cmdVnextRunCancel(runtimeFrom(ctx, this), runId);
|
|
3063
|
+
});
|
|
3064
|
+
addGlobalOptions(runCmd.command("list").description("List recent runs for the current project"))
|
|
3065
|
+
.action(async function runListAction(this: Command) {
|
|
3066
|
+
result.code = await cmdVnextRunList(runtimeFrom(ctx, this));
|
|
3067
|
+
});
|
|
3068
|
+
|
|
3069
|
+
const harnessCmd = addGlobalOptions(program.command("harness").description("Detect coding-agent harnesses and authentication"));
|
|
3070
|
+
harnessCmd.helpCommand("help", "Show harness help");
|
|
3071
|
+
addGlobalOptions(harnessCmd.command("list").description("Show installed harnesses, auth, and native updaters")).action(bind(cmdHarnessList));
|
|
3072
|
+
addGlobalOptions(program.command("update").description("Update kxm, harness CLIs, extensions, plugins, and model catalogs")
|
|
3073
|
+
.argument("[harness]", "Harness id (default: every detected harness)")
|
|
3074
|
+
.option("--check", "Check for a kxm package update without applying")
|
|
3075
|
+
.option("--kxm", "Apply the kxm operator package update (GitHub release tarball or npm)")
|
|
3076
|
+
.option("--self", "Update only the harness CLI")
|
|
3077
|
+
.option("--extensions", "Update only extensions/plugins (Pi packages, Claude kxm)")
|
|
3078
|
+
.option("--models", "Refresh model catalogs where the harness supports it"))
|
|
3079
|
+
.action(async function updateAction(this: Command, harness: string | undefined, options: {
|
|
3080
|
+
self?: boolean;
|
|
3081
|
+
extensions?: boolean;
|
|
3082
|
+
models?: boolean;
|
|
3083
|
+
check?: boolean;
|
|
3084
|
+
kxm?: boolean;
|
|
3085
|
+
}) {
|
|
3086
|
+
result.code = await cmdUpdate(runtimeFrom(ctx, this), harness, options);
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
const runtimeCmd = addGlobalOptions(program.command("runtime").description("Manage the vNext Runtime supervisor"));
|
|
3090
|
+
runtimeCmd.helpCommand("help", "Show runtime help");
|
|
3091
|
+
addGlobalOptions(runtimeCmd.command("start").description("Start the Runtime supervisor if not running"))
|
|
3092
|
+
.action(async function runtimeStartAction(this: Command) {
|
|
3093
|
+
result.code = await cmdVnextRuntime(runtimeFrom(ctx, this), "start");
|
|
3094
|
+
});
|
|
3095
|
+
addGlobalOptions(runtimeCmd.command("status").description("Show Runtime supervisor liveness"))
|
|
3096
|
+
.action(async function runtimeStatusAction(this: Command) {
|
|
3097
|
+
result.code = await cmdVnextRuntime(runtimeFrom(ctx, this), "status");
|
|
3098
|
+
});
|
|
3099
|
+
addGlobalOptions(runtimeCmd.command("stop").description("Gracefully stop the Runtime supervisor"))
|
|
3100
|
+
.action(async function runtimeStopAction(this: Command) {
|
|
3101
|
+
result.code = await cmdVnextRuntime(runtimeFrom(ctx, this), "stop");
|
|
3102
|
+
});
|
|
3103
|
+
|
|
3104
|
+
const trust = addGlobalOptions(program.command("trust").description("Permission-diff trust review for vNext configuration"));
|
|
3105
|
+
trust.helpCommand("help", "Show trust help");
|
|
3106
|
+
addGlobalOptions(trust.command("diff").description("Show the structured permission diff against a base Git revision"))
|
|
3107
|
+
.option("--base <revision>", "Base Git revision (default: HEAD)")
|
|
3108
|
+
.action(async function trustDiffAction(this: Command, options: { base?: string }) {
|
|
3109
|
+
result.code = await cmdVnextTrust(runtimeFrom(ctx, this), false, options);
|
|
3110
|
+
});
|
|
3111
|
+
addGlobalOptions(trust.command("check").description("Exit non-zero when the working tree expands permissions against the base revision"))
|
|
3112
|
+
.option("--base <revision>", "Base Git revision (default: HEAD)")
|
|
3113
|
+
.action(async function trustCheckAction(this: Command, options: { base?: string }) {
|
|
3114
|
+
result.code = await cmdVnextTrust(runtimeFrom(ctx, this), true, options);
|
|
3115
|
+
});
|
|
3116
|
+
|
|
3117
|
+
const agent = addGlobalOptions(program.command("agent").description("Run and supervise agents"));
|
|
3118
|
+
agent.helpCommand("help", "Show agent help");
|
|
3119
|
+
addGlobalOptions(agent.command("worker").description("Start a long-lived Pi worker"))
|
|
3120
|
+
.option("--name <name>", "Agent name")
|
|
3121
|
+
.option("--project <project>", "Hub project")
|
|
3122
|
+
.option("--model <id>", "Primary model")
|
|
3123
|
+
.option("--fallback-models <ids>", "Comma-separated fallback models")
|
|
3124
|
+
.option("--tools <names>", "Comma-separated Pi tool allowlist")
|
|
3125
|
+
.option("--session-isolation <mode>", "Pi session isolation: workflow or off (default: off for upgrade compatibility)")
|
|
3126
|
+
.option("--no-continue", "Disable every session resume")
|
|
3127
|
+
.option("--fresh-start", "Skip only the initial session resume")
|
|
3128
|
+
.action(async function workerAction(this: Command, options: {
|
|
3129
|
+
name?: string;
|
|
3130
|
+
project?: string;
|
|
3131
|
+
model?: string;
|
|
3132
|
+
fallbackModels?: string;
|
|
3133
|
+
tools?: string;
|
|
3134
|
+
sessionIsolation?: string;
|
|
3135
|
+
continue?: boolean;
|
|
3136
|
+
freshStart?: boolean;
|
|
3137
|
+
}) {
|
|
3138
|
+
result.code = await cmdWorker(runtimeFrom(ctx, this), options);
|
|
3139
|
+
});
|
|
3140
|
+
|
|
3141
|
+
const session = addGlobalOptions(program.command("session").description("Create manifests, inspect sessions, and brief recent hub work"));
|
|
3142
|
+
session.helpCommand("help", "Show session help");
|
|
3143
|
+
addGlobalOptions(session.command("status").description("Show session claims and recovery envelopes")).action(bind(cmdSessionStatus));
|
|
3144
|
+
addGlobalOptions(session.command("brief").description("Show recent hub tasks and plans for a new session (read-only)"))
|
|
3145
|
+
.option("--status", "Print only the status line")
|
|
3146
|
+
.option("--token", "Issue interactive session token with operator policy")
|
|
3147
|
+
.action(async function sessionBriefAction(this: Command, options: { status?: boolean; token?: boolean }) {
|
|
3148
|
+
result.code = await cmdSessionBrief(runtimeFrom(ctx, this), options);
|
|
3149
|
+
});
|
|
3150
|
+
addGlobalOptions(session.command("start").description("Create an agent/gate or workflow session manifest (does not launch processes)"))
|
|
3151
|
+
.option("--id <id>", "Session id")
|
|
3152
|
+
.option("--workflow <id>", "Workflow definition id")
|
|
3153
|
+
.option("--mix <names>", "Comma-separated agent and gate names")
|
|
3154
|
+
.action(async function sessionStartAction(this: Command, options: { id?: string; workflow?: string; mix?: string }) {
|
|
3155
|
+
result.code = await cmdSessionStart(runtimeFrom(ctx, this), options);
|
|
3156
|
+
});
|
|
3157
|
+
addGlobalOptions(session.command("stop").description("Request managed hub and worker session shutdown"))
|
|
3158
|
+
.option("--wait-ms <ms>", "How long to wait for PID files to clear")
|
|
3159
|
+
.action(async function sessionStopAction(this: Command, options: { waitMs?: string }) {
|
|
3160
|
+
result.code = await cmdStop(runtimeFrom(ctx, this), options.waitMs);
|
|
3161
|
+
});
|
|
3162
|
+
|
|
3163
|
+
const peer = addGlobalOptions(program.command("peer").description("Peer agent messaging and coordination"));
|
|
3164
|
+
peer.helpCommand("help", "Show peer help");
|
|
3165
|
+
|
|
3166
|
+
addGlobalOptions(peer.command("list").description("List online peer agents in this project's hub pool"))
|
|
3167
|
+
.option("--payload <json>", "JSON payload")
|
|
3168
|
+
.action(async function peerListAction(this: Command, opts?: Record<string, unknown>) {
|
|
3169
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_list", opts ?? {});
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
addGlobalOptions(peer.command("send [target] [content]").description("Send a focused request to a peer agent"))
|
|
3173
|
+
.option("--target <name>", "Peer name or agent ID")
|
|
3174
|
+
.option("--content <text>", "Focused request content")
|
|
3175
|
+
.option("--delivery <mode>", "steer, followUp, or nextTurn")
|
|
3176
|
+
.option("--correlation-id <id>", "Task grouping ID")
|
|
3177
|
+
.option("--idempotency-key <key>", "Deduplication key")
|
|
3178
|
+
.option("--workflow-context <json>", "Workflow context JSON")
|
|
3179
|
+
.option("--ttl-ms <ms>", "Message TTL in milliseconds")
|
|
3180
|
+
.option("--payload <json>", "JSON payload")
|
|
3181
|
+
.action(async function peerSendAction(this: Command, target?: string, content?: string, opts?: Record<string, unknown>) {
|
|
3182
|
+
const options = { ...opts, ...(target ? { target } : {}), ...(content ? { content } : {}) };
|
|
3183
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_send", options);
|
|
3184
|
+
});
|
|
3185
|
+
|
|
3186
|
+
addGlobalOptions(peer.command("get [messageId]").description("Check a peer request status and reply"))
|
|
3187
|
+
.option("--message-id <id>", "Message ID")
|
|
3188
|
+
.option("--payload <json>", "JSON payload")
|
|
3189
|
+
.action(async function peerGetAction(this: Command, messageId?: string, opts?: Record<string, unknown>) {
|
|
3190
|
+
const options = { ...opts, ...(messageId ? { messageId } : {}) };
|
|
3191
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_get", options);
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
addGlobalOptions(peer.command("await [messageId]").description("Wait for a peer request reply (capped at 60 seconds)"))
|
|
3195
|
+
.option("--message-id <id>", "Message ID")
|
|
3196
|
+
.option("--timeout-ms <ms>", "Timeout in milliseconds (max 60000)")
|
|
3197
|
+
.option("--payload <json>", "JSON payload")
|
|
3198
|
+
.action(async function peerAwaitAction(this: Command, messageId?: string, opts?: Record<string, unknown>) {
|
|
3199
|
+
const options = { ...opts, ...(messageId ? { messageId } : {}) };
|
|
3200
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_await", options);
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
addGlobalOptions(peer.command("cancel [messageId]").description("Cancel a sent peer request"))
|
|
3204
|
+
.option("--message-id <id>", "Message ID")
|
|
3205
|
+
.option("--payload <json>", "JSON payload")
|
|
3206
|
+
.action(async function peerCancelAction(this: Command, messageId?: string, opts?: Record<string, unknown>) {
|
|
3207
|
+
const options = { ...opts, ...(messageId ? { messageId } : {}) };
|
|
3208
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_cancel", options);
|
|
3209
|
+
});
|
|
3210
|
+
|
|
3211
|
+
addGlobalOptions(peer.command("fanout").description("Send the same request to one through three peers"))
|
|
3212
|
+
.option("--targets <items...>", "Target peer names (1-3)")
|
|
3213
|
+
.option("--content <text>", "Request content")
|
|
3214
|
+
.option("--correlation-id <id>", "Task grouping ID")
|
|
3215
|
+
.option("--idempotency-key-prefix <prefix>", "Idempotency prefix")
|
|
3216
|
+
.option("--workflow-context <json>", "Workflow context JSON")
|
|
3217
|
+
.option("--ttl-ms <ms>", "Message TTL in milliseconds")
|
|
3218
|
+
.option("--timeout-ms <ms>", "Timeout in milliseconds")
|
|
3219
|
+
.option("--payload <json>", "JSON payload")
|
|
3220
|
+
.action(async function peerFanoutAction(this: Command, opts?: Record<string, unknown>) {
|
|
3221
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_fanout", opts ?? {});
|
|
3222
|
+
});
|
|
3223
|
+
|
|
3224
|
+
addGlobalOptions(peer.command("inbox").description("List inbound peer requests"))
|
|
3225
|
+
.option("--payload <json>", "JSON payload")
|
|
3226
|
+
.action(async function peerInboxAction(this: Command, opts?: Record<string, unknown>) {
|
|
3227
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_inbox", opts ?? {});
|
|
3228
|
+
});
|
|
3229
|
+
|
|
3230
|
+
addGlobalOptions(peer.command("reply [messageId] [content]").description("Reply to an inbound request"))
|
|
3231
|
+
.option("--message-id <id>", "Message ID")
|
|
3232
|
+
.option("--content <text>", "Reply content")
|
|
3233
|
+
.option("--payload <json>", "JSON payload")
|
|
3234
|
+
.action(async function peerReplyAction(this: Command, messageId?: string, content?: string, opts?: Record<string, unknown>) {
|
|
3235
|
+
const options = { ...opts, ...(messageId ? { messageId } : {}), ...(content ? { content } : {}) };
|
|
3236
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_reply", options);
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3239
|
+
const workflow = addGlobalOptions(program.command("workflow").description("Start and inspect workflow runs"));
|
|
3240
|
+
workflow.helpCommand("help", "Show workflow help");
|
|
3241
|
+
addGlobalOptions(workflow.command("list").description("List local workflow runs")).action(async function listAction(this: Command) {
|
|
3242
|
+
result.code = await cmdWorkflowInspect(runtimeFrom(ctx, this), "list");
|
|
3243
|
+
});
|
|
3244
|
+
addGlobalOptions(workflow.command("get").description("Show one local workflow run"))
|
|
3245
|
+
.argument("<runId>", "Workflow run ID")
|
|
3246
|
+
.action(async function getAction(this: Command, runId: string) {
|
|
3247
|
+
result.code = await cmdWorkflowInspect(runtimeFrom(ctx, this), "get", runId);
|
|
3248
|
+
});
|
|
3249
|
+
addGlobalOptions(workflow.command("checkpoint [runId] [stageId] [status] [summary]").description("Record a workflow stage checkpoint with evidence"))
|
|
3250
|
+
.option("--run-id <id>", "Workflow run ID")
|
|
3251
|
+
.option("--stage-id <id>", "Active stage ID")
|
|
3252
|
+
.option("--status <status>", "passed, warning, or failed")
|
|
3253
|
+
.option("--summary <text>", "Stage summary")
|
|
3254
|
+
.option("--evidence <json>", "Key-value evidence JSON")
|
|
3255
|
+
.option("--evidence-refs <json>", "Peer evidence references JSON")
|
|
3256
|
+
.option("--payload <json>", "JSON payload")
|
|
3257
|
+
.action(async function checkpointAction(this: Command, runId?: string, stageId?: string, status?: string, summary?: string, opts?: Record<string, unknown>) {
|
|
3258
|
+
const options = {
|
|
3259
|
+
...opts,
|
|
3260
|
+
...(runId ? { runId } : {}),
|
|
3261
|
+
...(stageId ? { stageId } : {}),
|
|
3262
|
+
...(status ? { status } : {}),
|
|
3263
|
+
...(summary ? { summary } : {}),
|
|
3264
|
+
};
|
|
3265
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_workflow_checkpoint", options);
|
|
3266
|
+
});
|
|
3267
|
+
addGlobalOptions(workflow.command("record [runId] [category] [area] [summary]").description("Record workflow journal knowledge"))
|
|
3268
|
+
.option("--run-id <id>", "Workflow run ID")
|
|
3269
|
+
.option("--category <category>", "plan, decision, contradiction, error, lesson")
|
|
3270
|
+
.option("--area <area>", "harness, gates, implementation, workflow, documentation, security, other")
|
|
3271
|
+
.option("--severity <level>", "info, warning, error")
|
|
3272
|
+
.option("--summary <text>", "Entry summary")
|
|
3273
|
+
.option("--details <text>", "Detailed text")
|
|
3274
|
+
.option("--evidence <items...>", "Evidence strings")
|
|
3275
|
+
.option("--related-entry-ids <ids...>", "Related entry IDs")
|
|
3276
|
+
.option("--payload <json>", "JSON payload")
|
|
3277
|
+
.action(async function recordAction(this: Command, runId?: string, category?: string, area?: string, summary?: string, opts?: Record<string, unknown>) {
|
|
3278
|
+
const options = {
|
|
3279
|
+
...opts,
|
|
3280
|
+
...(runId ? { runId } : {}),
|
|
3281
|
+
...(category ? { category } : {}),
|
|
3282
|
+
...(area ? { area } : {}),
|
|
3283
|
+
...(summary ? { summary } : {}),
|
|
3284
|
+
};
|
|
3285
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_workflow_record", options);
|
|
3286
|
+
});
|
|
3287
|
+
addGlobalOptions(workflow.command("wait [runId] [stageId] [signalKey] [summary]").description("Wait for a workflow signal callback"))
|
|
3288
|
+
.option("--run-id <id>", "Workflow run ID")
|
|
3289
|
+
.option("--stage-id <id>", "Active stage ID")
|
|
3290
|
+
.option("--signal-key <key>", "Wait signal key")
|
|
3291
|
+
.option("--summary <text>", "Expected result summary")
|
|
3292
|
+
.option("--evidence <json>", "Evidence JSON")
|
|
3293
|
+
.option("--evidence-refs <json>", "Peer evidence refs JSON")
|
|
3294
|
+
.option("--timeout-ms <ms>", "Wait timeout in milliseconds")
|
|
3295
|
+
.option("--payload <json>", "JSON payload")
|
|
3296
|
+
.action(async function waitAction(this: Command, runId?: string, stageId?: string, signalKey?: string, summary?: string, opts?: Record<string, unknown>) {
|
|
3297
|
+
const options = {
|
|
3298
|
+
...opts,
|
|
3299
|
+
...(runId ? { runId } : {}),
|
|
3300
|
+
...(stageId ? { stageId } : {}),
|
|
3301
|
+
...(signalKey ? { signalKey } : {}),
|
|
3302
|
+
...(summary ? { summary } : {}),
|
|
3303
|
+
};
|
|
3304
|
+
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_workflow_wait", options);
|
|
3305
|
+
});
|
|
3306
|
+
addGlobalOptions(workflow.command("signal").description("Post a signed workflow callback or unblock a vNext run"))
|
|
3307
|
+
.argument("<runId>", "Workflow run ID")
|
|
3308
|
+
.argument("<signalKey>", "Wait signal key")
|
|
3309
|
+
.argument("<status>", "passed, warning, or failed")
|
|
3310
|
+
.argument("<summary>", "Callback summary")
|
|
3311
|
+
.argument("[evidence...]", "required-key=evidence pairs")
|
|
3312
|
+
.option("--delivery-id <id>", "Stable callback delivery ID")
|
|
3313
|
+
.action(async function workflowSignalAction(this: Command, runId: string, signalKey: string, status: string, summary: string, evidence: string[], options: { deliveryId?: string }) {
|
|
3314
|
+
result.code = await cmdSignal(runtimeFrom(ctx, this), runId, signalKey, status, summary, evidence ?? [], options.deliveryId);
|
|
3315
|
+
});
|
|
3316
|
+
addGlobalOptions(workflow.command("start").description("POST a signed workflow-start webhook"))
|
|
3317
|
+
.argument("[definitionId]", "Workflow definition ID")
|
|
3318
|
+
.option("--payload <json>", "JSON object or @file", "{}")
|
|
3319
|
+
.option("--delivery-id <id>", "Stable provider delivery ID")
|
|
3320
|
+
.option("--event <name>", "Optional provider event name")
|
|
3321
|
+
.action(async function startAction(this: Command, definitionId: string | undefined, options: { payload?: string; deliveryId?: string; event?: string }) {
|
|
3322
|
+
result.code = await cmdWorkflowStart(runtimeFrom(ctx, this), definitionId, options);
|
|
3323
|
+
});
|
|
3324
|
+
addGlobalOptions(workflow.command("export").description("Export a proposed retrospective"))
|
|
3325
|
+
.argument("<runId>", "Workflow run ID")
|
|
3326
|
+
.option("--input <file>", "Offline snapshot JSON")
|
|
3327
|
+
.option("--out-dir <dir>", "Directory under workspace assets")
|
|
3328
|
+
.action(async function exportAction(this: Command, runId: string, options: { input?: string; outDir?: string }) {
|
|
3329
|
+
result.code = await cmdRetrospectiveExport(runtimeFrom(ctx, this), runId, options);
|
|
3330
|
+
});
|
|
3331
|
+
|
|
3332
|
+
const gate = addGlobalOptions(program.command("gate").description("Validate definitions and operate evidence gates"));
|
|
3333
|
+
gate.helpCommand("help", "Show gate help");
|
|
3334
|
+
addGlobalOptions(gate.command("validate").description("Parse workflow definitions without printing secrets"))
|
|
3335
|
+
.option("--file <path>", "Workflow definition file")
|
|
3336
|
+
.action(async function validateAction(this: Command, options: { file?: string }) {
|
|
3337
|
+
result.code = await cmdValidate(runtimeFrom(ctx, this), options.file);
|
|
3338
|
+
});
|
|
3339
|
+
addGlobalOptions(gate.command("artifacts-exist").description("Verify a non-empty file under workspace assets"))
|
|
3340
|
+
.requiredOption("--path <file>", "Artifact file under workspace assets")
|
|
3341
|
+
.action(async function artifactsExistAction(this: Command, options: { path: string }) {
|
|
3342
|
+
result.code = await cmdArtifactsExist(runtimeFrom(ctx, this), options.path);
|
|
3343
|
+
});
|
|
3344
|
+
addGlobalOptions(gate.command("degrade").description("Approve a configured lower peer quorum"))
|
|
3345
|
+
.argument("<runId>", "Workflow run ID")
|
|
3346
|
+
.argument("<stageId>", "Active stage ID")
|
|
3347
|
+
.option("--requirement <key>", "Canonical requirement key")
|
|
3348
|
+
.option("--reason <text>", "Non-secret operator reason")
|
|
3349
|
+
.action(async function degradeAction(this: Command, runId: string, stageId: string, options: { requirement?: string; reason?: string }) {
|
|
3350
|
+
result.code = await cmdWorkflowDegrade(runtimeFrom(ctx, this), runId, stageId, options);
|
|
3351
|
+
});
|
|
3352
|
+
addGlobalOptions(gate.command("signal").description("Post a signed workflow callback"))
|
|
3353
|
+
.argument("<runId>", "Workflow run ID")
|
|
3354
|
+
.argument("<signalKey>", "Wait signal key")
|
|
3355
|
+
.argument("<status>", "passed, warning, or failed")
|
|
3356
|
+
.argument("<summary>", "Callback summary")
|
|
3357
|
+
.argument("[evidence...]", "required-key=evidence pairs")
|
|
3358
|
+
.option("--delivery-id <id>", "Stable callback delivery ID")
|
|
3359
|
+
.action(async function signalAction(this: Command, runId: string, signalKey: string, status: string, summary: string, evidence: string[], options: { deliveryId?: string }) {
|
|
3360
|
+
result.code = await cmdSignal(runtimeFrom(ctx, this), runId, signalKey, status, summary, evidence ?? [], options.deliveryId);
|
|
3361
|
+
});
|
|
3362
|
+
const github = addGlobalOptions(gate.command("github").description("GitHub adapters"));
|
|
3363
|
+
github.helpCommand("help", "Show GitHub help");
|
|
3364
|
+
addGlobalOptions(github.command("watch").description("Poll required checks and post the signed signal"))
|
|
3365
|
+
.option("--run-id <id>", "Workflow run ID")
|
|
3366
|
+
.option("--stage-id <id>", "Waiting stage ID")
|
|
3367
|
+
.option("--signal-key <key>", "Wait signal key")
|
|
3368
|
+
.option("--repo <owner/name>", "GitHub repository")
|
|
3369
|
+
.option("--pr <number>", "Pull request number")
|
|
3370
|
+
.option("--required <names>", "Comma-separated required check names")
|
|
3371
|
+
.option("--timeout-ms <ms>", "Watch timeout")
|
|
3372
|
+
.option("--interval-ms <ms>", "Poll interval")
|
|
3373
|
+
.option("--delivery-id <id>", "Stable callback delivery ID")
|
|
3374
|
+
.action(async function watchAction(this: Command, options: {
|
|
3375
|
+
runId?: string;
|
|
3376
|
+
stageId?: string;
|
|
3377
|
+
signalKey?: string;
|
|
3378
|
+
repo?: string;
|
|
3379
|
+
pr?: string;
|
|
3380
|
+
required?: string;
|
|
3381
|
+
timeoutMs?: string;
|
|
3382
|
+
intervalMs?: string;
|
|
3383
|
+
deliveryId?: string;
|
|
3384
|
+
}) {
|
|
3385
|
+
result.code = await cmdGithubWatch(runtimeFrom(ctx, this), options);
|
|
3386
|
+
});
|
|
3387
|
+
|
|
3388
|
+
const improve = addGlobalOptions(program.command("improve").description("Propose CLI or project improvements from routing records and telemetry"));
|
|
3389
|
+
improve.helpCommand("help", "Show improve help");
|
|
3390
|
+
addGlobalOptions(improve.command("report", { isDefault: true }).description("Generate improvement report and candidates from routing records"))
|
|
3391
|
+
.option("--file <path>", "Telemetry JSONL file to read routing records from")
|
|
3392
|
+
.option("--target <cli|project>", "Limit proposals to cli or project")
|
|
3393
|
+
.option("--out-dir <path>", "Directory for candidates (default .kxm/candidates)")
|
|
3394
|
+
.action(async function improveReportAction(this: Command, options: { file?: string; target?: string; outDir?: string }) {
|
|
3395
|
+
result.code = await cmdImprove(runtimeFrom(ctx, this), options);
|
|
3396
|
+
});
|
|
3397
|
+
|
|
3398
|
+
const context = addGlobalOptions(program.command("context").description("KXM context operating-system queries"));
|
|
3399
|
+
context.helpCommand("help", "Show context help");
|
|
3400
|
+
addGlobalOptions(context.command("get").description("Assemble a role-aware context packet"))
|
|
3401
|
+
.argument("<project>", "Project scope")
|
|
3402
|
+
.requiredOption("--role <role>", "Requesting role (repro, planner, critic, implementer, verifier, or custom)")
|
|
3403
|
+
.requiredOption("--task <task>", "What the role is trying to do")
|
|
3404
|
+
.option("--run <runId>", "Workflow run scope")
|
|
3405
|
+
.option("--stage <stageId>", "Workflow stage scope")
|
|
3406
|
+
.option("--budget <tokens>", "Token budget for the packet")
|
|
3407
|
+
.option("--kinds <kinds>", "Comma-separated item kinds to include")
|
|
3408
|
+
.action(async function contextGetAction(this: Command, project: string, options: { role: string; task: string; run?: string; stage?: string; budget?: string; kinds?: string }) {
|
|
3409
|
+
result.code = await cmdContextGet(runtimeFrom(ctx, this), project, options);
|
|
3410
|
+
});
|
|
3411
|
+
addGlobalOptions(context.command("recall").description("Search durable context records (metadata only)"))
|
|
3412
|
+
.argument("<project>", "Project scope")
|
|
3413
|
+
.option("--query <text>", "Substring query against summaries and state keys")
|
|
3414
|
+
.option("--kinds <kinds>", "Comma-separated item kinds to include")
|
|
3415
|
+
.option("--limit <n>", "Maximum results (1-100)")
|
|
3416
|
+
.action(async function contextRecallAction(this: Command, project: string, options: { query?: string; kinds?: string; limit?: string }) {
|
|
3417
|
+
result.code = await cmdContextRecall(runtimeFrom(ctx, this), project, options);
|
|
3418
|
+
});
|
|
3419
|
+
addGlobalOptions(context.command("state").description("Current or historical value for one state key"))
|
|
3420
|
+
.argument("<project>", "Project scope")
|
|
3421
|
+
.argument("<key>", "State key")
|
|
3422
|
+
.option("--as-of <iso>", "Historical timestamp query")
|
|
3423
|
+
.action(async function contextStateAction(this: Command, project: string, key: string, options: { asOf?: string }) {
|
|
3424
|
+
result.code = await cmdContextState(runtimeFrom(ctx, this), project, key, options);
|
|
3425
|
+
});
|
|
3426
|
+
addGlobalOptions(context.command("episode").description("Episodic learning records from workflow journals"))
|
|
3427
|
+
.argument("<project>", "Project scope")
|
|
3428
|
+
.option("--run <runId>", "Limit to one workflow run")
|
|
3429
|
+
.action(async function contextEpisodeAction(this: Command, project: string, options: { run?: string }) {
|
|
3430
|
+
result.code = await cmdContextEpisode(runtimeFrom(ctx, this), project, options);
|
|
3431
|
+
});
|
|
3432
|
+
addGlobalOptions(context.command("promote").description("Promote an approved state proposal (control plane)"))
|
|
3433
|
+
.argument("<project>", "Project scope")
|
|
3434
|
+
.argument("<proposalId>", "State proposal ID")
|
|
3435
|
+
.requiredOption("--evidence <refs>", "Comma-separated durable evidence references")
|
|
3436
|
+
.action(async function contextPromoteAction(this: Command, project: string, proposalId: string, options: { evidence: string }) {
|
|
3437
|
+
result.code = await cmdContextPromote(runtimeFrom(ctx, this), project, proposalId, options);
|
|
3438
|
+
});
|
|
3439
|
+
addGlobalOptions(context.command("explain").description("Explain which evidence and lineage back a context item"))
|
|
3440
|
+
.argument("<project>", "Project scope")
|
|
3441
|
+
.argument("<itemId>", "Context item ID")
|
|
3442
|
+
.action(async function contextExplainAction(this: Command, project: string, itemId: string) {
|
|
3443
|
+
result.code = await cmdContextExplain(runtimeFrom(ctx, this), project, itemId);
|
|
3444
|
+
});
|
|
3445
|
+
|
|
3446
|
+
addGlobalOptions(context.command("wiki-compile").description("Compile the Karpathy-style knowledge wiki for review"))
|
|
3447
|
+
.argument("<project>", "Project scope")
|
|
3448
|
+
.option("--out <dir>", "Workspace root to write .kxm/knowledge/wiki into (default: dry-run output only)")
|
|
3449
|
+
.action(async function contextWikiCompileAction(this: Command, project: string, options: { out?: string }) {
|
|
3450
|
+
result.code = await cmdContextWikiCompile(runtimeFrom(ctx, this), project, options);
|
|
3451
|
+
});
|
|
3452
|
+
addGlobalOptions(context.command("wiki-lint").description("Lint a compiled wiki for broken refs, orphans, and stale state"))
|
|
3453
|
+
.argument("<project>", "Project scope")
|
|
3454
|
+
.action(async function contextWikiLintAction(this: Command, project: string) {
|
|
3455
|
+
result.code = await cmdContextWikiLint(runtimeFrom(ctx, this), project);
|
|
3456
|
+
});
|
|
3457
|
+
|
|
3458
|
+
const skills = addGlobalOptions(program.command("skills").description("Governed skill candidate lifecycle"));
|
|
3459
|
+
skills.helpCommand("help", "Show skills help");
|
|
3460
|
+
addGlobalOptions(skills.command("create").description("Submit a skill candidate from verified episodes"))
|
|
3461
|
+
.requiredOption("--file <path>", "SKILL.md content file")
|
|
3462
|
+
.requiredOption("--name <name>", "Skill name")
|
|
3463
|
+
.option("--description <text>", "Short description")
|
|
3464
|
+
.requiredOption("--created-by <id>", "Author identity")
|
|
3465
|
+
.option("--run <ids>", "Comma-separated source run IDs")
|
|
3466
|
+
.option("--journal <ids>", "Comma-separated source journal entry IDs")
|
|
3467
|
+
.option("--receipt <refs>", "Comma-separated evidence receipts")
|
|
3468
|
+
.requiredOption("--harness <name>", "Harness compatibility (pi, claude-code, ...)" )
|
|
3469
|
+
.requiredOption("--models <models>", "Comma-separated compatible models")
|
|
3470
|
+
.option("--supersedes <id>", "Prior skill this candidate supersedes")
|
|
3471
|
+
.action(async function skillsCreateAction(this: Command, options: { file: string; name: string; description?: string; createdBy: string; run?: string; journal?: string; receipt?: string; harness: string; models: string; supersedes?: string }) {
|
|
3472
|
+
result.code = await cmdSkillsCreate(runtimeFrom(ctx, this), options);
|
|
3473
|
+
});
|
|
3474
|
+
addGlobalOptions(skills.command("evaluate").description("Record a protected evaluation for a candidate"))
|
|
3475
|
+
.argument("<skillId>", "Skill candidate ID")
|
|
3476
|
+
.requiredOption("--kind <kind>", "static-review, sandbox, functional, safety, or optimization")
|
|
3477
|
+
.requiredOption("--evaluator <version>", "Evaluator version")
|
|
3478
|
+
.option("--fail", "Record a failed evaluation")
|
|
3479
|
+
.option("--score <n>", "Numeric score")
|
|
3480
|
+
.option("--details <text>", "Bounded evaluation details")
|
|
3481
|
+
.action(async function skillsEvaluateAction(this: Command, skillId: string, options: { kind: string; evaluator: string; fail?: boolean; score?: string; details?: string }) {
|
|
3482
|
+
result.code = await cmdSkillsEvaluate(runtimeFrom(ctx, this), skillId, options);
|
|
3483
|
+
});
|
|
3484
|
+
addGlobalOptions(skills.command("promote").description("Promote a candidate that passed all protected evaluations"))
|
|
3485
|
+
.argument("<skillId>", "Skill candidate ID")
|
|
3486
|
+
.requiredOption("--decided-by <id>", "Promoter identity (must differ from the author)")
|
|
3487
|
+
.requiredOption("--evidence <refs>", "Comma-separated durable evidence references")
|
|
3488
|
+
.option("--reason <text>", "Decision reason")
|
|
3489
|
+
.action(async function skillsPromoteAction(this: Command, skillId: string, options: { decidedBy: string; evidence: string; reason?: string }) {
|
|
3490
|
+
result.code = await cmdSkillsPromote(runtimeFrom(ctx, this), skillId, options);
|
|
3491
|
+
});
|
|
3492
|
+
addGlobalOptions(skills.command("reject").description("Reject a candidate; history is retained for learning"))
|
|
3493
|
+
.argument("<skillId>", "Skill candidate ID")
|
|
3494
|
+
.requiredOption("--decided-by <id>", "Decider identity")
|
|
3495
|
+
.option("--reason <text>", "Decision reason")
|
|
3496
|
+
.action(async function skillsRejectAction(this: Command, skillId: string, options: { decidedBy: string; reason?: string }) {
|
|
3497
|
+
result.code = await cmdSkillsReject(runtimeFrom(ctx, this), skillId, options);
|
|
3498
|
+
});
|
|
3499
|
+
addGlobalOptions(skills.command("list").description("List skills by state"))
|
|
3500
|
+
.option("--state <state>", "candidate, promoted, quarantined, or rejected", "promoted")
|
|
3501
|
+
.action(async function skillsListAction(this: Command, options: { state: string }) {
|
|
3502
|
+
result.code = await cmdSkillsList(runtimeFrom(ctx, this), options);
|
|
3503
|
+
});
|
|
3504
|
+
addGlobalOptions(skills.command("verify").description("Verify a stored skill against its pinned content hash"))
|
|
3505
|
+
.argument("<skillId>", "Skill ID")
|
|
3506
|
+
.option("--state <state>", "candidate, promoted, quarantined, or rejected", "promoted")
|
|
3507
|
+
.action(async function skillsVerifyAction(this: Command, skillId: string, options: { state: string }) {
|
|
3508
|
+
result.code = await cmdSkillsVerify(runtimeFrom(ctx, this), skillId, options);
|
|
3509
|
+
});
|
|
3510
|
+
|
|
3511
|
+
const memory = addGlobalOptions(program.command("memory").description("Harness-agnostic Git memory operations"));
|
|
3512
|
+
memory.helpCommand("help", "Show memory help");
|
|
3513
|
+
addGlobalOptions(memory.command("brief").description("Show active project memory facts for harness context"))
|
|
3514
|
+
.action(async function memoryBriefAction(this: Command) {
|
|
3515
|
+
result.code = await cmdMemoryBrief(runtimeFrom(ctx, this));
|
|
3516
|
+
});
|
|
3517
|
+
addGlobalOptions(memory.command("note").description("Record an evidence-based memory candidate (promoted via PR)"))
|
|
3518
|
+
.argument("<fact>", "Summary of the observed fact or learning")
|
|
3519
|
+
.option("--scope <scope>", "Scope: agent, project, run, or operator (default: project)")
|
|
3520
|
+
.option("--kind <kind>", "Kind: decision, architecture, convention, policy, learning (default: learning)")
|
|
3521
|
+
.option("--body <text>", "Detailed markdown context for the fact")
|
|
3522
|
+
.action(async function memoryNoteAction(this: Command, fact: string, options: { scope?: string; kind?: string; body?: string }) {
|
|
3523
|
+
result.code = await cmdMemoryNote(runtimeFrom(ctx, this), fact, options);
|
|
3524
|
+
});
|
|
3525
|
+
addGlobalOptions(memory.command("sync").description("Regenerate memory projection blocks across AGENTS.md, CLAUDE.md, and GEMINI.md"))
|
|
3526
|
+
.action(async function memorySyncAction(this: Command) {
|
|
3527
|
+
result.code = await cmdMemorySync(runtimeFrom(ctx, this));
|
|
3528
|
+
});
|
|
3529
|
+
|
|
3530
|
+
const routing = addGlobalOptions(program.command("routing").description("Model/harness routing telemetry and behavioral comparisons"));
|
|
3531
|
+
routing.helpCommand("help", "Show routing help");
|
|
3532
|
+
addGlobalOptions(routing.command("report").description("Compare verified completion, cost, and rework per behavioral configuration"))
|
|
3533
|
+
.option("-f, --file <path>", "Telemetry or event log JSONL file (default: workspace telemetry)")
|
|
3534
|
+
.option("-l, --equivalent-list-cost", "Include equivalent list price column using price catalog")
|
|
3535
|
+
.option("--list-prices", "Alias for --equivalent-list-cost")
|
|
3536
|
+
.option("--prices <path>", "Path to price catalog (default: .kxm/prices.yaml)")
|
|
3537
|
+
.action(async function routingReportAction(this: Command, options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string }) {
|
|
3538
|
+
result.code = await cmdRoutingReport(runtimeFrom(ctx, this), options);
|
|
3539
|
+
});
|
|
3540
|
+
|
|
3541
|
+
const hub = addGlobalOptions(program.command("hub").description("Start, inspect, and stop the local KXM hub"));
|
|
3542
|
+
hub.helpCommand("help", "Show hub help");
|
|
3543
|
+
addGlobalOptions(hub.command("view").description("Check hub /health and /ready")).action(bind(cmdStatus));
|
|
3544
|
+
addGlobalOptions(hub.command("start").description("Start the hub")).action(bind(cmdHub));
|
|
3545
|
+
addGlobalOptions(hub.command("stop").description("Request managed hub and worker shutdown"))
|
|
3546
|
+
.option("--wait-ms <ms>", "How long to wait for PID files to clear")
|
|
3547
|
+
.action(async function hubStopAction(this: Command, options: { waitMs?: string }) {
|
|
3548
|
+
result.code = await cmdStop(runtimeFrom(ctx, this), options.waitMs);
|
|
3549
|
+
});
|
|
3550
|
+
addGlobalOptions(hub.command("bind").description("Bind this machine to a running hub").argument("<url>", "Hub base URL (http or https)"))
|
|
3551
|
+
.action(async function hubBindAction(this: Command, url: string) {
|
|
3552
|
+
result.code = await cmdHubBind(runtimeFrom(ctx, this), url);
|
|
3553
|
+
});
|
|
3554
|
+
addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));
|
|
3555
|
+
addGlobalOptions(program.command("dash").description("Live screens for headless agents, tasks, workflows, and plans")
|
|
3556
|
+
.option("--screen <name>", "agents, tasks, workflows, plans, inbox, procs, or spend"))
|
|
3557
|
+
.action(async function dashAction(this: Command, options: { screen?: string }) {
|
|
3558
|
+
result.code = await cmdDash(runtimeFrom(ctx, this), options);
|
|
3559
|
+
});
|
|
3560
|
+
|
|
3561
|
+
const configCmd = addGlobalOptions(program.command("config").description("Inspect and update personalization and workflow configuration"));
|
|
3562
|
+
configCmd.helpCommand("help", "Show config help");
|
|
3563
|
+
addGlobalOptions(configCmd.command("get <key>").description("Get a configuration value by key"))
|
|
3564
|
+
.action(async function configGetAction(this: Command, key: string) {
|
|
3565
|
+
result.code = await cmdConfigGet(runtimeFrom(ctx, this), key);
|
|
3566
|
+
});
|
|
3567
|
+
addGlobalOptions(configCmd.command("set <key> <value>").description("Set a configuration value"))
|
|
3568
|
+
.option("--scope <scope>", "Configuration scope: user or project (default: project)", "project")
|
|
3569
|
+
.action(async function configSetAction(this: Command, key: string, value: string, options: { scope: string }) {
|
|
3570
|
+
result.code = await cmdConfigSet(runtimeFrom(ctx, this), key, value, options);
|
|
3571
|
+
});
|
|
3572
|
+
addGlobalOptions(configCmd.command("list", { isDefault: true }).description("List resolved configuration values"))
|
|
3573
|
+
.action(async function configListAction(this: Command) {
|
|
3574
|
+
result.code = await cmdConfigList(runtimeFrom(ctx, this));
|
|
3575
|
+
});
|
|
3576
|
+
|
|
3577
|
+
addGlobalOptions(program.command("completion <shell>").description("Generate shell completion script (bash, zsh, fish)"))
|
|
3578
|
+
.action(async function completionAction(this: Command, shell: string) {
|
|
3579
|
+
result.code = await cmdCompletion(runtimeFrom(ctx, this), shell);
|
|
3580
|
+
});
|
|
3581
|
+
|
|
3582
|
+
addGlobalOptions(program.command("suggest <prompt...>").description("Recommend workflow, area, roles, and skills from a prompt or issue description"))
|
|
3583
|
+
.action(async function suggestAction(this: Command, promptParts: string[]) {
|
|
3584
|
+
result.code = await cmdSuggest(runtimeFrom(ctx, this), promptParts);
|
|
3585
|
+
});
|
|
3586
|
+
|
|
3587
|
+
const goalCmd = addGlobalOptions(program.command("goal").description("Internal project goal management"));
|
|
3588
|
+
goalCmd.helpCommand("help", "Show goal help");
|
|
3589
|
+
addGlobalOptions(goalCmd.command("create <title>").description("Create a project goal"))
|
|
3590
|
+
.option("--area <area>", "Workflow area (e.g. software-engineering, security-reliability)")
|
|
3591
|
+
.option("--metric <metric...>", "Success metrics for this goal")
|
|
3592
|
+
.option("--target-date <date>", "Target achievement date (ISO-8601 or YYYY-MM-DD)")
|
|
3593
|
+
.action(async function goalCreateAction(this: Command, title: string, options: { area?: string; metric?: string[]; targetDate?: string }) {
|
|
3594
|
+
result.code = await cmdGoalCreate(runtimeFrom(ctx, this), title, options);
|
|
3595
|
+
});
|
|
3596
|
+
addGlobalOptions(goalCmd.command("list", { isDefault: true }).description("List project goals"))
|
|
3597
|
+
.action(async function goalListAction(this: Command) {
|
|
3598
|
+
result.code = await cmdGoalList(runtimeFrom(ctx, this));
|
|
3599
|
+
});
|
|
3600
|
+
|
|
3601
|
+
const taskCmd = addGlobalOptions(program.command("task").description("Project task management driving workflows and issue board synchronization"));
|
|
3602
|
+
taskCmd.helpCommand("help", "Show task help");
|
|
3603
|
+
addGlobalOptions(taskCmd.command("create <title>").description("Create a task"))
|
|
3604
|
+
.option("--goal <goalId>", "Parent goal ID")
|
|
3605
|
+
.option("--objective <text>", "Task objective")
|
|
3606
|
+
.option("--workflow <id>", "Assigned workflow ID")
|
|
3607
|
+
.option("--tracker <tracker>", "Issue tracker (github or jira)")
|
|
3608
|
+
.option("--issue <key>", "Issue number or Jira key")
|
|
3609
|
+
.action(async function taskCreateAction(this: Command, title: string, options: { goal?: string; objective?: string; workflow?: string; tracker?: string; issue?: string }) {
|
|
3610
|
+
result.code = await cmdTaskCreate(runtimeFrom(ctx, this), title, options);
|
|
3611
|
+
});
|
|
3612
|
+
addGlobalOptions(taskCmd.command("list", { isDefault: true }).description("List project tasks"))
|
|
3613
|
+
.option("--goal <goalId>", "Filter by goal ID")
|
|
3614
|
+
.option("--status <status>", "Filter by status: todo, in_progress, blocked, in_review, done")
|
|
3615
|
+
.action(async function taskListAction(this: Command, options: { goal?: string; status?: string }) {
|
|
3616
|
+
result.code = await cmdTaskList(runtimeFrom(ctx, this), options);
|
|
3617
|
+
});
|
|
3618
|
+
addGlobalOptions(taskCmd.command("get <taskId>").description("Get task details and linked workflow status"))
|
|
3619
|
+
.action(async function taskGetAction(this: Command, taskId: string) {
|
|
3620
|
+
result.code = await cmdTaskGet(runtimeFrom(ctx, this), taskId);
|
|
3621
|
+
});
|
|
3622
|
+
addGlobalOptions(taskCmd.command("run <taskId>").description("Launch a workflow run driven by this task"))
|
|
3623
|
+
.action(async function taskRunAction(this: Command, taskId: string) {
|
|
3624
|
+
result.code = await cmdTaskRun(runtimeFrom(ctx, this), taskId);
|
|
3625
|
+
});
|
|
3626
|
+
addGlobalOptions(taskCmd.command("sync <taskId>").description("Sync task status and evidence with its linked issue board"))
|
|
3627
|
+
.action(async function taskSyncAction(this: Command, taskId: string) {
|
|
3628
|
+
result.code = await cmdTaskSync(runtimeFrom(ctx, this), taskId);
|
|
3629
|
+
});
|
|
3630
|
+
|
|
3631
|
+
const studioCmd = addGlobalOptions(program.command("studio").description("KXM Web Studio layout and inspection utilities"));
|
|
3632
|
+
studioCmd.helpCommand("help", "Show studio help");
|
|
3633
|
+
addGlobalOptions(studioCmd.command("layout [workflowPath]").description("Generate Decision D14 DAG, stepper, and Temporal swimlanes layout JSON"))
|
|
3634
|
+
.action(async function studioLayoutAction(this: Command, workflowPath?: string) {
|
|
3635
|
+
result.code = await cmdStudioLayout(runtimeFrom(ctx, this), workflowPath);
|
|
3636
|
+
});
|
|
3637
|
+
|
|
3638
|
+
return program;
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
function mapCommanderError(error: CommanderError): number {
|
|
3642
|
+
if (error.exitCode === 0) return 0;
|
|
3643
|
+
if (USAGE_ERROR_CODES.has(error.code)) return 2;
|
|
3644
|
+
return error.exitCode || 1;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
const MESH_REMOVED_TEXT = "kxm mesh was removed. Use kxm init, kxm hub start|view|stop, and node scripts/smoke-multi-pi.mjs (KXM_SMOKE=1).";
|
|
3648
|
+
|
|
3649
|
+
/** Detect `mesh` as the first command token after recognized nonterminal global
|
|
3650
|
+
* options so `kxm --json mesh` fails closed. `--version`, `--help`, and unknown
|
|
3651
|
+
* options are left for Commander. */
|
|
3652
|
+
function removedMeshInvocation(argv: string[]): { invoked: boolean; json: boolean } {
|
|
3653
|
+
let json = false;
|
|
3654
|
+
let invoked = false;
|
|
3655
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3656
|
+
const arg = argv[i]!;
|
|
3657
|
+
if (arg === "--") {
|
|
3658
|
+
if (argv[i + 1] === "mesh") invoked = true;
|
|
3659
|
+
break;
|
|
3660
|
+
}
|
|
3661
|
+
if (arg === "--json") {
|
|
3662
|
+
json = true;
|
|
3663
|
+
continue;
|
|
3664
|
+
}
|
|
3665
|
+
if (arg === "--dry-run") continue;
|
|
3666
|
+
if (arg === "--workspace") {
|
|
3667
|
+
i += 1;
|
|
3668
|
+
continue;
|
|
3669
|
+
}
|
|
3670
|
+
if (arg.startsWith("--workspace=")) continue;
|
|
3671
|
+
if (arg.startsWith("-")) break;
|
|
3672
|
+
invoked = arg === "mesh";
|
|
3673
|
+
break;
|
|
3674
|
+
}
|
|
3675
|
+
if (invoked && argv.includes("--json")) json = true;
|
|
3676
|
+
return { invoked, json };
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
export async function runCli(
|
|
3680
|
+
argv: string[],
|
|
3681
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
3682
|
+
io: CliIo = { stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text) },
|
|
3683
|
+
cwd = process.cwd(),
|
|
3684
|
+
): Promise<number> {
|
|
3685
|
+
const originalStdout = io.stdout;
|
|
3686
|
+
const originalStderr = io.stderr;
|
|
3687
|
+
io = { ...io, stdout: (text) => originalStdout(redactConfiguredValues(text, env)), stderr: (text) => originalStderr(redactConfiguredValues(text, env)) };
|
|
3688
|
+
const mesh = removedMeshInvocation(argv);
|
|
3689
|
+
if (mesh.invoked) {
|
|
3690
|
+
print(io, mesh.json, { ok: false, command: "mesh", error: "removed_command" }, MESH_REMOVED_TEXT);
|
|
3691
|
+
return 2;
|
|
3692
|
+
}
|
|
3693
|
+
const result = { code: 0 };
|
|
3694
|
+
const program = createProgram({ env, io, cwd }, result);
|
|
3695
|
+
try {
|
|
3696
|
+
await program.parseAsync(argv, { from: "user" });
|
|
3697
|
+
return result.code;
|
|
3698
|
+
} catch (error) {
|
|
3699
|
+
if (error instanceof CommanderError) return mapCommanderError(error);
|
|
3700
|
+
throw error;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
3705
|
+
const code = await runCli(process.argv.slice(2));
|
|
3706
|
+
process.exitCode = code;
|
|
3707
|
+
}
|