kortext 2.2.0 → 3.0.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.
Files changed (209) hide show
  1. package/AGENTS.md +7 -11
  2. package/CHANGELOG.md +81 -0
  3. package/LICENSE +21 -0
  4. package/README.md +144 -47
  5. package/USER-GUIDE.md +505 -0
  6. package/bin/kortext.js +23 -0
  7. package/bin/kortext.ts +377 -0
  8. package/dist/bin/kortext.js +346 -0
  9. package/dist/bin/kortext.js.map +1 -0
  10. package/dist/mcp/index.js +4 -0
  11. package/dist/mcp/index.js.map +1 -0
  12. package/dist/mcp/server.js +553 -0
  13. package/dist/mcp/server.js.map +1 -0
  14. package/dist/mcp/sse.js +38 -0
  15. package/dist/mcp/sse.js.map +1 -0
  16. package/dist/mcp/stdio.js +44 -0
  17. package/dist/mcp/stdio.js.map +1 -0
  18. package/dist/server/cli/cleanup.js +73 -0
  19. package/dist/server/cli/cleanup.js.map +1 -0
  20. package/dist/server/cli/commands.js +73 -0
  21. package/dist/server/cli/commands.js.map +1 -0
  22. package/dist/server/cli/doctor.js +118 -0
  23. package/dist/server/cli/doctor.js.map +1 -0
  24. package/dist/server/cli/executor-factory.js +39 -0
  25. package/dist/server/cli/executor-factory.js.map +1 -0
  26. package/dist/server/cli/init.js +121 -0
  27. package/dist/server/cli/init.js.map +1 -0
  28. package/dist/server/cli/logs.js +28 -0
  29. package/dist/server/cli/logs.js.map +1 -0
  30. package/dist/server/cli/serve.js +72 -0
  31. package/dist/server/cli/serve.js.map +1 -0
  32. package/dist/server/config/env.js +12 -0
  33. package/dist/server/config/env.js.map +1 -0
  34. package/dist/server/db/client.js +32 -0
  35. package/dist/server/db/client.js.map +1 -0
  36. package/dist/server/db/json.js +21 -0
  37. package/dist/server/db/json.js.map +1 -0
  38. package/dist/server/db/migrate.js +70 -0
  39. package/dist/server/db/migrate.js.map +1 -0
  40. package/dist/server/db/migrations/001_init.sql +243 -0
  41. package/dist/server/db/repositories/audit-log.js +58 -0
  42. package/dist/server/db/repositories/audit-log.js.map +1 -0
  43. package/dist/server/db/repositories/backlog.js +96 -0
  44. package/dist/server/db/repositories/backlog.js.map +1 -0
  45. package/dist/server/db/repositories/contexts.js +54 -0
  46. package/dist/server/db/repositories/contexts.js.map +1 -0
  47. package/dist/server/db/repositories/decisions.js +71 -0
  48. package/dist/server/db/repositories/decisions.js.map +1 -0
  49. package/dist/server/db/repositories/handovers.js +53 -0
  50. package/dist/server/db/repositories/handovers.js.map +1 -0
  51. package/dist/server/db/repositories/index.js +30 -0
  52. package/dist/server/db/repositories/index.js.map +1 -0
  53. package/dist/server/db/repositories/locks.js +59 -0
  54. package/dist/server/db/repositories/locks.js.map +1 -0
  55. package/dist/server/db/repositories/notifications.js +63 -0
  56. package/dist/server/db/repositories/notifications.js.map +1 -0
  57. package/dist/server/db/repositories/pending-questions.js +63 -0
  58. package/dist/server/db/repositories/pending-questions.js.map +1 -0
  59. package/dist/server/db/repositories/runs.js +138 -0
  60. package/dist/server/db/repositories/runs.js.map +1 -0
  61. package/dist/server/db/repositories/runtime-artifacts.js +39 -0
  62. package/dist/server/db/repositories/runtime-artifacts.js.map +1 -0
  63. package/dist/server/db/repositories/secrets.js +65 -0
  64. package/dist/server/db/repositories/secrets.js.map +1 -0
  65. package/dist/server/db/repositories/sessions.js +48 -0
  66. package/dist/server/db/repositories/sessions.js.map +1 -0
  67. package/dist/server/db/schemas.js +308 -0
  68. package/dist/server/db/schemas.js.map +1 -0
  69. package/dist/server/engine/consistency.js +25 -0
  70. package/dist/server/engine/consistency.js.map +1 -0
  71. package/dist/server/engine/dag.js +86 -0
  72. package/dist/server/engine/dag.js.map +1 -0
  73. package/dist/server/engine/executor.js +2 -0
  74. package/dist/server/engine/executor.js.map +1 -0
  75. package/dist/server/engine/executors/claude-cli-executor.js +83 -0
  76. package/dist/server/engine/executors/claude-cli-executor.js.map +1 -0
  77. package/dist/server/engine/executors/cli-spawn.js +127 -0
  78. package/dist/server/engine/executors/cli-spawn.js.map +1 -0
  79. package/dist/server/engine/executors/codex-cli-executor.js +69 -0
  80. package/dist/server/engine/executors/codex-cli-executor.js.map +1 -0
  81. package/dist/server/engine/executors/gemini-cli-executor.js +69 -0
  82. package/dist/server/engine/executors/gemini-cli-executor.js.map +1 -0
  83. package/dist/server/engine/executors/mock-executor.js +52 -0
  84. package/dist/server/engine/executors/mock-executor.js.map +1 -0
  85. package/dist/server/engine/executors/persona-routed-executor.js +17 -0
  86. package/dist/server/engine/executors/persona-routed-executor.js.map +1 -0
  87. package/dist/server/engine/gate-enforcer.js +75 -0
  88. package/dist/server/engine/gate-enforcer.js.map +1 -0
  89. package/dist/server/engine/git-commit.js +42 -0
  90. package/dist/server/engine/git-commit.js.map +1 -0
  91. package/dist/server/engine/handover.js +120 -0
  92. package/dist/server/engine/handover.js.map +1 -0
  93. package/dist/server/engine/item-lifecycle.js +74 -0
  94. package/dist/server/engine/item-lifecycle.js.map +1 -0
  95. package/dist/server/engine/persona-registry.js +108 -0
  96. package/dist/server/engine/persona-registry.js.map +1 -0
  97. package/dist/server/engine/worker-pool.js +324 -0
  98. package/dist/server/engine/worker-pool.js.map +1 -0
  99. package/dist/server/engine/workflow-loader.js +55 -0
  100. package/dist/server/engine/workflow-loader.js.map +1 -0
  101. package/dist/server/engine/workflow-parser.js +158 -0
  102. package/dist/server/engine/workflow-parser.js.map +1 -0
  103. package/dist/server/engine/worktree.js +176 -0
  104. package/dist/server/engine/worktree.js.map +1 -0
  105. package/dist/server/index.js +99 -0
  106. package/dist/server/index.js.map +1 -0
  107. package/dist/server/notifications/dispatcher.js +60 -0
  108. package/dist/server/notifications/dispatcher.js.map +1 -0
  109. package/dist/server/notifications/slack.js +37 -0
  110. package/dist/server/notifications/slack.js.map +1 -0
  111. package/dist/server/notifications/telegram.js +33 -0
  112. package/dist/server/notifications/telegram.js.map +1 -0
  113. package/dist/server/orchestrator/approval-queue.js +83 -0
  114. package/dist/server/orchestrator/approval-queue.js.map +1 -0
  115. package/dist/server/orchestrator/blueprint-watcher.js +94 -0
  116. package/dist/server/orchestrator/blueprint-watcher.js.map +1 -0
  117. package/dist/server/orchestrator/orchestrator.js +283 -0
  118. package/dist/server/orchestrator/orchestrator.js.map +1 -0
  119. package/dist/server/orchestrator/pipeline-chainer.js +96 -0
  120. package/dist/server/orchestrator/pipeline-chainer.js.map +1 -0
  121. package/dist/server/orchestrator/resume.js +36 -0
  122. package/dist/server/orchestrator/resume.js.map +1 -0
  123. package/dist/server/routes/approvals.js +52 -0
  124. package/dist/server/routes/approvals.js.map +1 -0
  125. package/dist/server/routes/backlog.js +44 -0
  126. package/dist/server/routes/backlog.js.map +1 -0
  127. package/dist/server/routes/db-info.js +21 -0
  128. package/dist/server/routes/db-info.js.map +1 -0
  129. package/dist/server/routes/docs.js +70 -0
  130. package/dist/server/routes/docs.js.map +1 -0
  131. package/dist/server/routes/doctor.js +19 -0
  132. package/dist/server/routes/doctor.js.map +1 -0
  133. package/dist/server/routes/handovers.js +25 -0
  134. package/dist/server/routes/handovers.js.map +1 -0
  135. package/dist/server/routes/health.js +11 -0
  136. package/dist/server/routes/health.js.map +1 -0
  137. package/dist/server/routes/personas.js +96 -0
  138. package/dist/server/routes/personas.js.map +1 -0
  139. package/dist/server/routes/runs.js +41 -0
  140. package/dist/server/routes/runs.js.map +1 -0
  141. package/dist/server/routes/workflows.js +38 -0
  142. package/dist/server/routes/workflows.js.map +1 -0
  143. package/dist/server/safety/harmful-output-filter.js +40 -0
  144. package/dist/server/safety/harmful-output-filter.js.map +1 -0
  145. package/dist/server/safety/secret-scanner.js +169 -0
  146. package/dist/server/safety/secret-scanner.js.map +1 -0
  147. package/dist/server/services/markdown-sync.js +101 -0
  148. package/dist/server/services/markdown-sync.js.map +1 -0
  149. package/dist/web/assets/index-B80d5ZPZ.js +75 -0
  150. package/dist/web/assets/index-B80d5ZPZ.js.map +1 -0
  151. package/dist/web/assets/index-Bge5Gzqv.css +1 -0
  152. package/dist/web/index.html +13 -0
  153. package/docs/architecture.md +413 -0
  154. package/package.json +83 -11
  155. package/rules/behavior.md +2 -2
  156. package/scripts/copy-migrations.mjs +29 -0
  157. package/workspace/.locks/workspace_handover.md.lock +3 -0
  158. package/hooks/audit-logger.sh +0 -25
  159. package/hooks/auto-locker.sh +0 -74
  160. package/hooks/auto-unlocker.sh +0 -17
  161. package/hooks/backlog-sync-guard.sh +0 -12
  162. package/hooks/branch-guard.sh +0 -32
  163. package/hooks/commit-msg-guard.sh +0 -43
  164. package/hooks/git-pre-commit.sh +0 -76
  165. package/hooks/git-pre-push.sh +0 -17
  166. package/hooks/handover-guard.sh +0 -33
  167. package/hooks/kortext-init.sh +0 -194
  168. package/hooks/kortext-lib.sh +0 -141
  169. package/hooks/lint-guard.sh +0 -50
  170. package/hooks/secret-scanner.sh +0 -89
  171. package/hooks/size-guard.sh +0 -48
  172. package/hooks/snapshot-guard.sh +0 -39
  173. package/hooks/write-guard.sh +0 -77
  174. package/scripts/kortext-backlog-add.py +0 -123
  175. package/scripts/kortext-backlog-health.py +0 -112
  176. package/scripts/kortext-backlog-sync.py +0 -117
  177. package/scripts/kortext-bulk-plan.py +0 -63
  178. package/scripts/kortext-cli.py +0 -295
  179. package/scripts/kortext-consistency-check.py +0 -74
  180. package/scripts/kortext-context-check.py +0 -146
  181. package/scripts/kortext-handover.py +0 -107
  182. package/scripts/kortext-item-check.py +0 -61
  183. package/scripts/kortext-item-start.py +0 -70
  184. package/scripts/kortext-item-transition.py +0 -78
  185. package/scripts/kortext-lock.py +0 -174
  186. package/scripts/kortext-session-start.py +0 -127
  187. package/scripts/lock_kortext.sh +0 -34
  188. package/settings/.claude-settings.template.json +0 -48
  189. package/settings/CHANGELOG.md +0 -176
  190. package/settings/INTEGRATION-MAP.md +0 -276
  191. package/settings/README.md +0 -255
  192. package/settings/USER-GUIDE.md +0 -502
  193. package/settings/VERSION +0 -1
  194. package/settings/config.md +0 -8
  195. package/settings/runtime-adapters.md +0 -104
  196. package/skills/backend-developer/.gitkeep +0 -0
  197. package/skills/compliance-expert/.gitkeep +0 -0
  198. package/skills/copywriter/.gitkeep +0 -0
  199. package/skills/db-admin/.gitkeep +0 -0
  200. package/skills/delivery-manager/.gitkeep +0 -0
  201. package/skills/designer/.gitkeep +0 -0
  202. package/skills/devops-engineer/.gitkeep +0 -0
  203. package/skills/engineering-manager/.gitkeep +0 -0
  204. package/skills/frontend-developer/.gitkeep +0 -0
  205. package/skills/growth-expert/.gitkeep +0 -0
  206. package/skills/operation-manager/.gitkeep +0 -0
  207. package/skills/product-manager/.gitkeep +0 -0
  208. package/skills/qa-engineer/.gitkeep +0 -0
  209. package/skills/security-engineer/.gitkeep +0 -0
package/bin/kortext.ts ADDED
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Kortext CLI entry — user-facing surface.
4
+ *
5
+ * kortext init scaffold .kortext/ + workflows + DB
6
+ * kortext serve start backend + dashboard together
7
+ * kortext start <workflow-id> run a workflow with the mock executor
8
+ * kortext approve <run-id> [answer] answer the oldest open question
9
+ * kortext status recent runs + open questions
10
+ * kortext logs tail of the audit log
11
+ * kortext cleanup remove old quarantine + branches
12
+ * kortext doctor consistency scan
13
+ * kortext mcp run the MCP server over stdio
14
+ *
15
+ * Compiled to JS by `npm run build:server`; the `bin/kortext.js` shim
16
+ * prefers the compiled entry and falls back to tsx in dev.
17
+ */
18
+
19
+ import { spawn, type ChildProcess } from 'node:child_process';
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+ import { dirname, join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { getDb } from '../server/db/client.ts';
24
+ import { ApprovalQueue } from '../server/orchestrator/approval-queue.ts';
25
+ import {
26
+ startCommand,
27
+ approveCommand,
28
+ statusCommand,
29
+ } from '../server/cli/commands.ts';
30
+ import type { ExecutorKind } from '../server/cli/executor-factory.ts';
31
+ import { cleanupQuarantine, cleanupBranches } from '../server/cli/cleanup.ts';
32
+ import { runDoctor, formatDoctorReport } from '../server/cli/doctor.ts';
33
+ import { initCommand } from '../server/cli/init.ts';
34
+ import { logsCommand, formatLogsForCli } from '../server/cli/logs.ts';
35
+ import { buildServeCommands, type ServeMode } from '../server/cli/serve.ts';
36
+ import { loadWorkflowsFromDir } from '../server/engine/workflow-loader.ts';
37
+ import { loadPersonasFromDir } from '../server/engine/persona-registry.ts';
38
+ import { runStdioServer } from '../mcp/stdio.ts';
39
+
40
+ const args = process.argv.slice(2);
41
+ const cmd = args[0];
42
+
43
+ const VALID_EXECUTORS: ReadonlySet<ExecutorKind> = new Set([
44
+ 'mock',
45
+ 'claude',
46
+ 'codex',
47
+ 'gemini',
48
+ ]);
49
+
50
+ function parseFlag(name: string, fallback: string | undefined = undefined): string | undefined {
51
+ const prefix = `--${name}=`;
52
+ for (const arg of args) {
53
+ if (arg.startsWith(prefix)) return arg.slice(prefix.length);
54
+ }
55
+ return fallback;
56
+ }
57
+
58
+ function parseExecutorKind(): ExecutorKind {
59
+ const raw = parseFlag('executor', 'mock')!;
60
+ if (!VALID_EXECUTORS.has(raw as ExecutorKind)) {
61
+ throw new Error(
62
+ `invalid --executor=${raw}; expected one of: ${[...VALID_EXECUTORS].join(', ')}`,
63
+ );
64
+ }
65
+ return raw as ExecutorKind;
66
+ }
67
+
68
+ function envBinaryFor(kind: ExecutorKind): string | undefined {
69
+ if (kind === 'mock') return undefined;
70
+ const envKey = `KORTEXT_${kind.toUpperCase()}_BIN`;
71
+ return process.env[envKey] || undefined;
72
+ }
73
+
74
+ function hasFlag(name: string): boolean {
75
+ return args.includes(`--${name}`);
76
+ }
77
+
78
+ function parseDaysFlag(name: string, fallback: number): number {
79
+ const raw = parseFlag(name);
80
+ if (!raw) return fallback;
81
+ const m = raw.match(/^(\d+)d?$/);
82
+ if (!m) throw new Error(`invalid --${name}=${raw}; expected '<N>' or '<N>d'`);
83
+ return Number(m[1]);
84
+ }
85
+
86
+ function parseIntFlag(name: string, fallback: number): number {
87
+ const raw = parseFlag(name);
88
+ if (!raw) return fallback;
89
+ const m = raw.match(/^(\d+)$/);
90
+ if (!m) throw new Error(`invalid --${name}=${raw}; expected a positive integer`);
91
+ return Number(m[1]);
92
+ }
93
+
94
+ function packageRoot(): string {
95
+ // Walk up from this file until a package.json is found. Source path is
96
+ // `bin/kortext.ts` (root one level up); compiled path is
97
+ // `dist/bin/kortext.js` (root two levels up). Walking up keeps both
98
+ // layouts working without special-casing.
99
+ const here = dirname(fileURLToPath(import.meta.url));
100
+ let cursor = here;
101
+ for (let i = 0; i < 6; i++) {
102
+ if (existsSync(join(cursor, 'package.json'))) return cursor;
103
+ const parent = resolve(cursor, '..');
104
+ if (parent === cursor) break;
105
+ cursor = parent;
106
+ }
107
+ // Fallback — preserve old behaviour so callers still get a directory.
108
+ return resolve(here, '..');
109
+ }
110
+
111
+ function readVersion(): string {
112
+ try {
113
+ const pkg = JSON.parse(readFileSync(join(packageRoot(), 'package.json'), 'utf8')) as {
114
+ version?: string;
115
+ };
116
+ return pkg.version ?? 'unknown';
117
+ } catch {
118
+ return 'unknown';
119
+ }
120
+ }
121
+
122
+ const HELP_TEXT = [
123
+ 'kortext v3 — autonomous AI agent runtime',
124
+ '',
125
+ ' init [--force] scaffold .kortext/, workflows, agents,',
126
+ ' rules, workspace, AGENTS.md, and DB',
127
+ ' serve [--mode=dev|prod|auto] start backend + dashboard together',
128
+ ' [--port=N] (auto picks prod when dist/ is built)',
129
+ ' start <workflow-id> [--executor=<kind>] [--binary=<path>]',
130
+ ' run a workflow (mock|claude|codex|gemini)',
131
+ ' approve <run-id> [answer] answer the oldest open question for a run',
132
+ ' status show recent runs + open questions',
133
+ ' logs [--limit=N] [--actor=A] [--action=A]',
134
+ ' tail of the audit log',
135
+ ' cleanup [--quarantine-older-than=Nd] [--branches] [--dry-run]',
136
+ ' remove old quarantine + abandoned branches',
137
+ ' doctor workflow / persona / lock consistency scan',
138
+ ' mcp run the MCP server over stdio',
139
+ '',
140
+ ' --help, -h show this help',
141
+ ' --version, -v print version',
142
+ ].join('\n');
143
+
144
+ async function main(): Promise<number> {
145
+ // Top-level flags handled before subcommand dispatch.
146
+ if (cmd === '--version' || cmd === '-v') {
147
+ console.log(readVersion());
148
+ return 0;
149
+ }
150
+ if (cmd === '--help' || cmd === '-h' || cmd === 'help' || cmd === undefined) {
151
+ console.log(HELP_TEXT);
152
+ return 0;
153
+ }
154
+
155
+ // `kortext mcp` owns stdout for JSON-RPC frames — handle BEFORE any
156
+ // other code path runs console.log on it. Runs the server until SIGINT.
157
+ if (cmd === 'mcp') {
158
+ // eslint-disable-next-line no-console
159
+ console.log = console.error;
160
+ await runStdioServer({ cwd: process.cwd() });
161
+ await new Promise<void>(() => undefined); // park until shutdown
162
+ return 0;
163
+ }
164
+
165
+ if (cmd === 'init') {
166
+ const result = initCommand({
167
+ targetDir: process.cwd(),
168
+ force: hasFlag('force'),
169
+ });
170
+ if (!result.ok) {
171
+ console.error(`init failed: ${result.errorMessage}`);
172
+ return 1;
173
+ }
174
+ if (result.created.length > 0) {
175
+ console.log(`created (${result.created.length}):`);
176
+ for (const path of result.created) console.log(` + ${path}`);
177
+ }
178
+ if (result.skipped.length > 0) {
179
+ console.log(`skipped (${result.skipped.length}, already present):`);
180
+ for (const path of result.skipped) console.log(` · ${path}`);
181
+ }
182
+ console.log('');
183
+ console.log(`db schema v${result.schemaVersion} at ${result.dbPath}`);
184
+ console.log("next: 'kortext serve' to launch backend + dashboard");
185
+ return 0;
186
+ }
187
+
188
+ if (cmd === 'serve') {
189
+ const mode = (parseFlag('mode', 'auto') as ServeMode);
190
+ if (!['dev', 'prod', 'auto'].includes(mode)) {
191
+ console.error(`invalid --mode=${mode}; expected dev|prod|auto`);
192
+ return 2;
193
+ }
194
+ const port = parseIntFlag('port', Number(process.env.KORTEXT_PORT ?? 3200));
195
+ const plan = buildServeCommands({
196
+ packageRoot: packageRoot(),
197
+ projectDir: process.cwd(),
198
+ mode,
199
+ port,
200
+ });
201
+ console.error(`[kortext] serve mode=${plan.mode} port=${port}`);
202
+ const children: ChildProcess[] = [];
203
+ let shuttingDown = false;
204
+ const shutdown = (signal: NodeJS.Signals) => {
205
+ if (shuttingDown) return;
206
+ shuttingDown = true;
207
+ console.error(`[kortext] received ${signal}, stopping children`);
208
+ for (const child of children) {
209
+ if (!child.killed) child.kill(signal);
210
+ }
211
+ };
212
+ process.on('SIGINT', () => shutdown('SIGINT'));
213
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
214
+
215
+ for (const c of plan.commands) {
216
+ const child = spawn(c.command, c.args, {
217
+ cwd: c.cwd,
218
+ env: { ...process.env, ...c.env },
219
+ stdio: 'inherit',
220
+ shell: false,
221
+ });
222
+ children.push(child);
223
+ child.on('exit', (code, signal) => {
224
+ if (shuttingDown) return;
225
+ console.error(
226
+ `[kortext] ${c.name} exited (code=${code} signal=${signal}); stopping siblings`,
227
+ );
228
+ shutdown('SIGTERM');
229
+ });
230
+ }
231
+
232
+ // Wait for all children to finish, then propagate worst exit code.
233
+ const codes = await Promise.all(
234
+ children.map(
235
+ (child) =>
236
+ new Promise<number>((res) => {
237
+ child.on('close', (code) => res(code ?? 1));
238
+ }),
239
+ ),
240
+ );
241
+ return codes.reduce((acc, c) => Math.max(acc, c), 0);
242
+ }
243
+
244
+ const { repositories: repos } = getDb();
245
+ const queue = new ApprovalQueue({ repos });
246
+
247
+ switch (cmd) {
248
+ case 'start': {
249
+ const workflowId = args[1];
250
+ if (!workflowId || workflowId.startsWith('--')) {
251
+ console.error('usage: kortext start <workflow-id> [--executor=mock|claude|codex|gemini] [--binary=<path>]');
252
+ return 2;
253
+ }
254
+ const kind = parseExecutorKind();
255
+ const binary = parseFlag('binary', envBinaryFor(kind));
256
+ const result = await startCommand({
257
+ repos,
258
+ workflowsDir: resolve(process.cwd(), 'workflows'),
259
+ workflowId,
260
+ executor: kind,
261
+ executorBinary: binary,
262
+ });
263
+ if (!result.ok) {
264
+ console.error(`start failed: ${result.errorMessage}`);
265
+ return 1;
266
+ }
267
+ console.log(
268
+ `run #${result.runId} (executor=${kind}) → ${result.status}` +
269
+ (result.failedStepKey ? ` (failed step: ${result.failedStepKey})` : ''),
270
+ );
271
+ return result.status === 'succeeded' ? 0 : 1;
272
+ }
273
+
274
+ case 'approve': {
275
+ const runId = Number(args[1]);
276
+ const answer = args[2] ?? 'approve';
277
+ if (!Number.isFinite(runId)) {
278
+ console.error('usage: kortext approve <run-id> [answer]');
279
+ return 2;
280
+ }
281
+ const result = await approveCommand({
282
+ repos,
283
+ queue,
284
+ runId,
285
+ answer,
286
+ answeredBy: process.env.USER ?? 'cli',
287
+ });
288
+ if (!result.ok) {
289
+ console.error(`approve failed: ${result.errorMessage}`);
290
+ return 1;
291
+ }
292
+ console.log(`approved question #${result.questionId} with '${result.answer}'`);
293
+ return 0;
294
+ }
295
+
296
+ case 'status': {
297
+ const result = statusCommand({ repos });
298
+ console.log('Recent runs:');
299
+ for (const r of result.recentRuns) {
300
+ console.log(` #${r.id} ${r.workflow_id} ${r.status} by ${r.triggered_by}`);
301
+ }
302
+ console.log('Open questions:');
303
+ if (result.openQuestions.length === 0) {
304
+ console.log(' (none)');
305
+ } else {
306
+ for (const q of result.openQuestions) {
307
+ console.log(` #${q.id} run=${q.run_id} ${q.question}`);
308
+ }
309
+ }
310
+ return 0;
311
+ }
312
+
313
+ case 'logs': {
314
+ const limit = parseIntFlag('limit', 50);
315
+ const actor = parseFlag('actor');
316
+ const action = parseFlag('action');
317
+ const resourceType = parseFlag('resource-type');
318
+ const resourceId = parseFlag('resource-id');
319
+ const result = logsCommand({
320
+ repos,
321
+ limit,
322
+ actor,
323
+ action,
324
+ resourceType,
325
+ resourceId,
326
+ });
327
+ console.log(formatLogsForCli(result.rows));
328
+ return 0;
329
+ }
330
+
331
+ case 'cleanup': {
332
+ const dryRun = hasFlag('dry-run');
333
+ const olderThanDays = parseDaysFlag('quarantine-older-than', 30);
334
+ const wantsBranches = hasFlag('branches');
335
+ const repoRoot = process.cwd();
336
+ const quarantineRoot = join(repoRoot, '.kortext', 'worktrees-quarantine');
337
+
338
+ const q = await cleanupQuarantine({ quarantineRoot, olderThanDays, dryRun });
339
+ const prefix = dryRun ? '(dry-run) would delete' : 'deleted';
340
+ console.log(`Quarantine — ${prefix} ${q.deleted.length}, kept ${q.kept.length}`);
341
+ for (const path of q.deleted) console.log(` - ${path}`);
342
+
343
+ if (wantsBranches) {
344
+ const b = await cleanupBranches({ repoRoot, repos, dryRun });
345
+ console.log(`Branches — ${prefix} ${b.deleted.length}, kept ${b.kept.length}`);
346
+ for (const name of b.deleted) console.log(` - ${name}`);
347
+ }
348
+ return 0;
349
+ }
350
+
351
+ case 'doctor': {
352
+ const workflows = loadWorkflowsFromDir(resolve(process.cwd(), 'workflows'));
353
+ const personas = loadPersonasFromDir(resolve(process.cwd(), 'agents'));
354
+ const report = runDoctor({ workflows, personas, repos });
355
+ console.log(formatDoctorReport(report));
356
+ console.log('');
357
+ console.log(
358
+ `summary: ${report.summary.workflowsLoaded} workflow(s), ${report.summary.personasLoaded} persona(s), ` +
359
+ `${report.summary.unknownPersonaRefs} unknown ref(s), ${report.summary.staleLocks} stale lock(s), ` +
360
+ `${report.summary.blockedItems} blocked item(s)`,
361
+ );
362
+ return report.hasErrors ? 1 : 0;
363
+ }
364
+
365
+ default:
366
+ console.error(`unknown command: ${cmd}`);
367
+ console.error("run 'kortext --help' for usage");
368
+ return 2;
369
+ }
370
+ }
371
+
372
+ main()
373
+ .then((code) => process.exit(code))
374
+ .catch((err: unknown) => {
375
+ console.error(err);
376
+ process.exit(1);
377
+ });