kortext 2.2.3 → 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 (208) hide show
  1. package/AGENTS.md +0 -4
  2. package/CHANGELOG.md +81 -0
  3. package/LICENSE +21 -0
  4. package/README.md +145 -55
  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/scripts/copy-migrations.mjs +29 -0
  156. package/workspace/.locks/workspace_handover.md.lock +3 -0
  157. package/hooks/audit-logger.sh +0 -25
  158. package/hooks/auto-locker.sh +0 -74
  159. package/hooks/auto-unlocker.sh +0 -17
  160. package/hooks/backlog-sync-guard.sh +0 -12
  161. package/hooks/branch-guard.sh +0 -32
  162. package/hooks/commit-msg-guard.sh +0 -43
  163. package/hooks/git-pre-commit.sh +0 -76
  164. package/hooks/git-pre-push.sh +0 -17
  165. package/hooks/handover-guard.sh +0 -33
  166. package/hooks/kortext-init.sh +0 -194
  167. package/hooks/kortext-lib.sh +0 -151
  168. package/hooks/lint-guard.sh +0 -50
  169. package/hooks/secret-scanner.sh +0 -89
  170. package/hooks/size-guard.sh +0 -48
  171. package/hooks/snapshot-guard.sh +0 -39
  172. package/hooks/write-guard.sh +0 -77
  173. package/scripts/kortext-backlog-add.py +0 -123
  174. package/scripts/kortext-backlog-health.py +0 -112
  175. package/scripts/kortext-backlog-sync.py +0 -117
  176. package/scripts/kortext-bulk-plan.py +0 -63
  177. package/scripts/kortext-cli.py +0 -376
  178. package/scripts/kortext-consistency-check.py +0 -74
  179. package/scripts/kortext-context-check.py +0 -146
  180. package/scripts/kortext-handover.py +0 -107
  181. package/scripts/kortext-item-check.py +0 -61
  182. package/scripts/kortext-item-start.py +0 -70
  183. package/scripts/kortext-item-transition.py +0 -78
  184. package/scripts/kortext-lock.py +0 -174
  185. package/scripts/kortext-session-start.py +0 -127
  186. package/scripts/lock_kortext.sh +0 -34
  187. package/settings/.claude-settings.template.json +0 -48
  188. package/settings/CHANGELOG.md +0 -176
  189. package/settings/INTEGRATION-MAP.md +0 -276
  190. package/settings/README.md +0 -255
  191. package/settings/USER-GUIDE.md +0 -502
  192. package/settings/VERSION +0 -1
  193. package/settings/config.md +0 -8
  194. package/settings/runtime-adapters.md +0 -104
  195. package/skills/backend-developer/.gitkeep +0 -0
  196. package/skills/compliance-expert/.gitkeep +0 -0
  197. package/skills/copywriter/.gitkeep +0 -0
  198. package/skills/db-admin/.gitkeep +0 -0
  199. package/skills/delivery-manager/.gitkeep +0 -0
  200. package/skills/designer/.gitkeep +0 -0
  201. package/skills/devops-engineer/.gitkeep +0 -0
  202. package/skills/engineering-manager/.gitkeep +0 -0
  203. package/skills/frontend-developer/.gitkeep +0 -0
  204. package/skills/growth-expert/.gitkeep +0 -0
  205. package/skills/operation-manager/.gitkeep +0 -0
  206. package/skills/product-manager/.gitkeep +0 -0
  207. package/skills/qa-engineer/.gitkeep +0 -0
  208. 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
+ });