@assistkick/create 1.2.0 → 1.3.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 (49) hide show
  1. package/package.json +2 -1
  2. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +231 -0
  3. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +4 -4
  4. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +49 -2
  5. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +82 -0
  6. package/templates/assistkick-product-system/packages/backend/src/server.ts +19 -6
  7. package/templates/assistkick-product-system/packages/backend/src/services/github_app_service.ts +146 -0
  8. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +69 -2
  9. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +71 -0
  10. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +87 -0
  11. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +194 -0
  12. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -17
  13. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +114 -39
  14. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +28 -14
  15. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +1 -1
  16. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +151 -0
  17. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +352 -0
  18. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +208 -95
  19. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +17 -1
  20. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +238 -105
  21. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +15 -13
  22. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +1 -0
  23. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +4 -0
  24. package/templates/assistkick-product-system/packages/frontend/src/routes/dashboard.tsx +22 -4
  25. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +486 -38
  26. package/templates/assistkick-product-system/packages/shared/db/migrations/0001_vengeful_wallop.sql +1 -0
  27. package/templates/assistkick-product-system/packages/shared/db/migrations/0002_greedy_excalibur.sql +4 -0
  28. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +826 -0
  29. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +854 -0
  30. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +14 -0
  31. package/templates/assistkick-product-system/packages/shared/db/schema.ts +5 -0
  32. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +54 -1
  33. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +25 -0
  34. package/templates/assistkick-product-system/packages/shared/lib/pipeline-state-store.ts +4 -0
  35. package/templates/assistkick-product-system/packages/shared/lib/pipeline.ts +329 -89
  36. package/templates/assistkick-product-system/packages/shared/lib/pipeline_orchestrator.ts +186 -0
  37. package/templates/assistkick-product-system/packages/shared/tools/db_explorer.ts +275 -0
  38. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +2 -1
  39. package/templates/assistkick-product-system/packages/shared/tools/move_card.ts +3 -2
  40. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  41. package/templates/assistkick-product-system/tests/kanban.test.ts +1 -1
  42. package/templates/assistkick-product-system/tests/pipeline_stats_all_cards.test.ts +1 -1
  43. package/templates/assistkick-product-system/tests/web_terminal.test.ts +189 -150
  44. package/templates/skills/assistkick-bootstrap/SKILL.md +33 -25
  45. package/templates/skills/assistkick-code-reviewer/SKILL.md +23 -15
  46. package/templates/skills/assistkick-db-explorer/SKILL.md +86 -0
  47. package/templates/skills/assistkick-debugger/SKILL.md +30 -22
  48. package/templates/skills/assistkick-developer/SKILL.md +37 -29
  49. package/templates/skills/assistkick-interview/SKILL.md +34 -26
@@ -0,0 +1,186 @@
1
+ /**
2
+ * PipelineOrchestrator — runs the Play All loop server-side.
3
+ * Iterates through TODO features sequentially, respecting dependency ordering.
4
+ * Continues across browser sessions. On server restart, does NOT auto-resume.
5
+ */
6
+
7
+ export class PipelineOrchestrator {
8
+ private pipeline: any;
9
+ private loadKanban: (projectId?: string) => Promise<any>;
10
+ private readGraph: (projectId?: string) => Promise<any>;
11
+ private log: (tag: string, ...args: any[]) => void;
12
+
13
+ private active = false;
14
+ private aborted = false;
15
+ private currentFeatureId: string | null = null;
16
+ private projectId: string | null = null;
17
+ private processedCount = 0;
18
+ private skippedFeatures: string[] = [];
19
+
20
+ constructor({ pipeline, loadKanban, readGraph, log }: {
21
+ pipeline: any;
22
+ loadKanban: (projectId?: string) => Promise<any>;
23
+ readGraph: (projectId?: string) => Promise<any>;
24
+ log: (tag: string, ...args: any[]) => void;
25
+ }) {
26
+ this.pipeline = pipeline;
27
+ this.loadKanban = loadKanban;
28
+ this.readGraph = readGraph;
29
+ this.log = log;
30
+ }
31
+
32
+ startPlayAll = async (projectId?: string) => {
33
+ if (this.active) {
34
+ return { error: 'Play All is already running', status: 409 };
35
+ }
36
+
37
+ this.active = true;
38
+ this.aborted = false;
39
+ this.currentFeatureId = null;
40
+ this.projectId = projectId || null;
41
+ this.processedCount = 0;
42
+ this.skippedFeatures = [];
43
+
44
+ this.log('ORCHESTRATOR', `Play All started${projectId ? ` for project ${projectId}` : ''}`);
45
+
46
+ // Fire-and-forget — loop runs in the background
47
+ this.runLoop().catch(err => {
48
+ this.log('ORCHESTRATOR', `Play All UNCAUGHT ERROR: ${err.message}`);
49
+ }).finally(() => {
50
+ this.active = false;
51
+ this.currentFeatureId = null;
52
+ this.log('ORCHESTRATOR', `Play All ended. Processed ${this.processedCount} features.`);
53
+ });
54
+
55
+ return { started: true, status: 200 };
56
+ };
57
+
58
+ stopPlayAll = () => {
59
+ if (!this.active) {
60
+ return { error: 'Play All is not running', status: 400 };
61
+ }
62
+ this.aborted = true;
63
+ this.log('ORCHESTRATOR', 'Play All stop requested');
64
+ return { stopped: true, status: 200 };
65
+ };
66
+
67
+ getStatus = () => {
68
+ return {
69
+ active: this.active,
70
+ currentFeatureId: this.currentFeatureId,
71
+ projectId: this.projectId,
72
+ processedCount: this.processedCount,
73
+ skippedFeatures: this.skippedFeatures,
74
+ };
75
+ };
76
+
77
+ private runLoop = async () => {
78
+ while (!this.aborted) {
79
+ // Fetch fresh kanban and graph data each iteration
80
+ let kanbanData: any;
81
+ let graphData: any;
82
+ try {
83
+ kanbanData = await this.loadKanban(this.projectId || undefined);
84
+ graphData = await this.readGraph(this.projectId || undefined);
85
+ } catch (err: any) {
86
+ this.log('ORCHESTRATOR', `Failed to fetch data: ${err.message}`);
87
+ break;
88
+ }
89
+
90
+ // Build feature node lookup
91
+ const featureNodes = new Map<string, any>();
92
+ graphData.nodes
93
+ .filter((n: any) => n.type === 'feature')
94
+ .forEach((n: any) => featureNodes.set(n.id, n));
95
+
96
+ // Get TODO cards that aren't dev_blocked, sorted by completeness (highest first)
97
+ const todoCards = Object.entries(kanbanData)
98
+ .filter(([id, entry]: [string, any]) =>
99
+ entry.column === 'todo' && featureNodes.has(id) && !entry.dev_blocked
100
+ )
101
+ .map(([id]: [string, any]) => ({
102
+ id,
103
+ completeness: featureNodes.get(id).completeness || 0,
104
+ }))
105
+ .sort((a, b) => b.completeness - a.completeness);
106
+
107
+ if (todoCards.length === 0) {
108
+ this.log('ORCHESTRATOR', 'No TODO cards remaining — stopping');
109
+ break;
110
+ }
111
+
112
+ // Try to find an unblocked card
113
+ let processed = false;
114
+ this.skippedFeatures = [];
115
+
116
+ for (const card of todoCards) {
117
+ if (this.aborted) return;
118
+
119
+ // Check dependency ordering
120
+ const deps = graphData.edges
121
+ .filter((e: any) => e.from === card.id && e.relation === 'depends_on')
122
+ .map((e: any) => e.to)
123
+ .filter((depId: string) => graphData.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
124
+ const blocked = deps.some((depId: string) =>
125
+ !kanbanData[depId] || kanbanData[depId].column !== 'done'
126
+ );
127
+
128
+ if (blocked) {
129
+ this.skippedFeatures.push(card.id);
130
+ continue;
131
+ }
132
+
133
+ // Start pipeline for this card
134
+ this.currentFeatureId = card.id;
135
+ this.log('ORCHESTRATOR', `Starting pipeline for ${card.id}`);
136
+
137
+ try {
138
+ const result = await this.pipeline.start(card.id);
139
+ if (result.error) {
140
+ this.log('ORCHESTRATOR', `Failed to start pipeline for ${card.id}: ${result.error}`);
141
+ // Try the next card if this one failed to start
142
+ continue;
143
+ }
144
+ } catch (err: any) {
145
+ this.log('ORCHESTRATOR', `Failed to start pipeline for ${card.id}: ${err.message}`);
146
+ continue;
147
+ }
148
+
149
+ // Wait for pipeline to reach a terminal status
150
+ await this.waitForPipelineCompletion(card.id);
151
+
152
+ if (this.aborted) return;
153
+
154
+ this.processedCount++;
155
+ processed = true;
156
+ break; // Process one card per iteration, then re-fetch data
157
+ }
158
+
159
+ // Deadlock: a full pass with zero features processable
160
+ if (!processed) {
161
+ this.log('ORCHESTRATOR', `Deadlock detected — ${this.skippedFeatures.length} features blocked: ${this.skippedFeatures.join(', ')}`);
162
+ break;
163
+ }
164
+ }
165
+ };
166
+
167
+ private waitForPipelineCompletion = async (featureId: string): Promise<void> => {
168
+ const TERMINAL_STATUSES = ['idle', 'completed', 'blocked', 'failed', 'interrupted'];
169
+ const POLL_INTERVAL_MS = 5000;
170
+
171
+ while (!this.aborted) {
172
+ try {
173
+ const status = await this.pipeline.getStatus(featureId);
174
+ if (TERMINAL_STATUSES.includes(status.status)) {
175
+ this.log('ORCHESTRATOR', `Pipeline for ${featureId} reached terminal status: ${status.status}`);
176
+ return;
177
+ }
178
+ } catch (err: any) {
179
+ this.log('ORCHESTRATOR', `Error polling status for ${featureId}: ${err.message}`);
180
+ return;
181
+ }
182
+
183
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
184
+ }
185
+ };
186
+ }
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * db_explorer — Read-only database explorer for AI assistants.
5
+ * Provides safe, read-only access to inspect database tables, schemas, and data.
6
+ * Absolutely NO write capabilities — only SELECT, PRAGMA, and EXPLAIN queries allowed.
7
+ * Sensitive columns (password_hash, token_hash) are automatically redacted.
8
+ *
9
+ * Usage:
10
+ * npx tsx tools/db_explorer.ts tables
11
+ * npx tsx tools/db_explorer.ts describe <table>
12
+ * npx tsx tools/db_explorer.ts count <table> [--where "..."]
13
+ * npx tsx tools/db_explorer.ts sample <table> [--limit N] [--where "..."] [--order-by <col>]
14
+ * npx tsx tools/db_explorer.ts query "SELECT ..."
15
+ *
16
+ * Note: --project-id is optional. Use --where "project_id = '...'" to filter by project.
17
+ */
18
+
19
+ import { program } from 'commander';
20
+ import chalk from 'chalk';
21
+ import { createClient } from '@libsql/client';
22
+ import { config } from 'dotenv';
23
+ import { dirname, join } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { existsSync } from 'node:fs';
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ let PROJECT_ROOT = join(__dirname, '..');
29
+ while (PROJECT_ROOT !== dirname(PROJECT_ROOT)) {
30
+ if (existsSync(join(PROJECT_ROOT, 'pnpm-workspace.yaml'))) break;
31
+ PROJECT_ROOT = dirname(PROJECT_ROOT);
32
+ }
33
+ const ENV_PATH = join(PROJECT_ROOT, '.env');
34
+
35
+ const SENSITIVE_COLUMNS = ['password_hash', 'token_hash'];
36
+
37
+ function getClient() {
38
+ if (existsSync(ENV_PATH)) {
39
+ config({ path: ENV_PATH, quiet: true });
40
+ }
41
+
42
+ const tursoUrl = process.env.TURSO_DATABASE_URL;
43
+ const tursoAuthToken = process.env.TURSO_AUTH_TOKEN;
44
+
45
+ if (tursoUrl && tursoAuthToken) {
46
+ return createClient({ url: tursoUrl, authToken: tursoAuthToken });
47
+ }
48
+
49
+ const dataDir = join(PROJECT_ROOT, 'data');
50
+ return createClient({ url: `file:${join(dataDir, 'local.db')}` });
51
+ }
52
+
53
+ /**
54
+ * Validates that a SQL query is strictly read-only.
55
+ * Returns true only for SELECT, PRAGMA, and EXPLAIN queries.
56
+ */
57
+ function isReadOnly(query: string): boolean {
58
+ // Strip SQL comments
59
+ let cleaned = query
60
+ .replace(/--[^\n]*/g, '')
61
+ .replace(/\/\*[\s\S]*?\*\//g, '')
62
+ .trim();
63
+
64
+ // Remove trailing semicolon
65
+ cleaned = cleaned.replace(/;\s*$/, '');
66
+
67
+ // Strip string literals to avoid false positives
68
+ const noStrings = cleaned.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
69
+
70
+ // No multiple statements
71
+ if (noStrings.includes(';')) return false;
72
+
73
+ const upper = noStrings.toUpperCase().replace(/\s+/g, ' ');
74
+
75
+ // Must start with SELECT, PRAGMA, or EXPLAIN
76
+ if (!/^(SELECT|PRAGMA|EXPLAIN)\b/.test(upper)) return false;
77
+
78
+ // Block write/DDL/dangerous keywords
79
+ if (/\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE|ATTACH|DETACH|VACUUM|REINDEX|GRANT|REVOKE|LOAD_EXTENSION)\b/.test(upper)) {
80
+ return false;
81
+ }
82
+
83
+ return true;
84
+ }
85
+
86
+ function redactRows(rows: Record<string, unknown>[], columns: string[]): Record<string, unknown>[] {
87
+ const hasSensitive = columns.some(c => SENSITIVE_COLUMNS.includes(c.toLowerCase()));
88
+ if (!hasSensitive) return rows;
89
+
90
+ return rows.map(row => {
91
+ const copy = { ...row };
92
+ for (const col of SENSITIVE_COLUMNS) {
93
+ if (col in copy) copy[col] = '[REDACTED]';
94
+ }
95
+ return copy;
96
+ });
97
+ }
98
+
99
+ function printRows(rows: Record<string, unknown>[], columns: string[]) {
100
+ if (rows.length === 0) {
101
+ console.log(chalk.yellow(' (no rows)'));
102
+ return;
103
+ }
104
+ for (const row of rows) {
105
+ console.log(chalk.gray(' ─────────'));
106
+ for (const col of columns) {
107
+ const val = row[col];
108
+ const display = val === null
109
+ ? chalk.gray('NULL')
110
+ : typeof val === 'string' && val.length > 200
111
+ ? val.substring(0, 200) + '...'
112
+ : String(val);
113
+ console.log(` ${chalk.bold(col)}: ${display}`);
114
+ }
115
+ }
116
+ }
117
+
118
+ function sanitizeTableName(name: string): string {
119
+ return name.replace(/[^a-zA-Z0-9_]/g, '');
120
+ }
121
+
122
+ program
123
+ .argument('<action>', 'Action: tables, describe, count, sample, query')
124
+ .argument('[target]', 'Table name or SQL query (for query action)')
125
+ .option('--project-id <id>', 'Project ID (optional — use --where to filter by project)')
126
+ .option('--limit <n>', 'Row limit for sample action', '10')
127
+ .option('--where <condition>', 'WHERE clause for count/sample')
128
+ .option('--order-by <column>', 'ORDER BY clause for sample')
129
+ .parse();
130
+
131
+ const [action, target] = program.args;
132
+ const opts = program.opts();
133
+
134
+ (async () => {
135
+ const client = getClient();
136
+
137
+ try {
138
+ switch (action) {
139
+ case 'tables': {
140
+ const result = await client.execute(
141
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle_%' AND name NOT LIKE '_litestream_%' ORDER BY name"
142
+ );
143
+ const tableNames = result.rows.map(r => r.name as string);
144
+
145
+ console.log(chalk.cyan.bold('Database Tables:\n'));
146
+ for (const t of tableNames) {
147
+ const countResult = await client.execute(`SELECT COUNT(*) as cnt FROM "${t}"`);
148
+ const cnt = countResult.rows[0]?.cnt ?? 0;
149
+ console.log(` ${chalk.bold(t)} (${cnt} rows)`);
150
+ }
151
+ console.log('\n' + JSON.stringify({ tables: tableNames }));
152
+ break;
153
+ }
154
+
155
+ case 'describe': {
156
+ if (!target) throw new Error('Usage: db_explorer describe <table_name>');
157
+ const table = sanitizeTableName(target);
158
+
159
+ const result = await client.execute(`PRAGMA table_info("${table}")`);
160
+ if (result.rows.length === 0) throw new Error(`Table not found: ${target}`);
161
+
162
+ console.log(chalk.cyan.bold(`\nTable: ${table}\n`));
163
+ console.log(chalk.gray(' # Name Type Nullable Default PK'));
164
+ console.log(chalk.gray(' ' + '─'.repeat(75)));
165
+
166
+ for (const col of result.rows) {
167
+ const nullable = col.notnull ? 'NOT NULL' : 'NULL ';
168
+ const pk = col.pk ? chalk.yellow('PK') : ' ';
169
+ const def = col.dflt_value !== null ? String(col.dflt_value) : '';
170
+ console.log(` ${String(col.cid).padEnd(3)} ${chalk.bold(String(col.name).padEnd(25))} ${String(col.type).padEnd(12)} ${nullable} ${def.padEnd(14)} ${pk}`);
171
+ }
172
+
173
+ const idxResult = await client.execute(`PRAGMA index_list("${table}")`);
174
+ if (idxResult.rows.length > 0) {
175
+ console.log(chalk.cyan.bold('\n Indexes:'));
176
+ for (const idx of idxResult.rows) {
177
+ const unique = idx.unique ? chalk.yellow(' UNIQUE') : '';
178
+ const idxName = sanitizeTableName(String(idx.name));
179
+ const idxInfoResult = await client.execute(`PRAGMA index_info("${idxName}")`);
180
+ const cols = idxInfoResult.rows.map(r => r.name).join(', ');
181
+ console.log(` ${idx.name}${unique} (${cols})`);
182
+ }
183
+ }
184
+
185
+ const fkResult = await client.execute(`PRAGMA foreign_key_list("${table}")`);
186
+ if (fkResult.rows.length > 0) {
187
+ console.log(chalk.cyan.bold('\n Foreign Keys:'));
188
+ for (const fk of fkResult.rows) {
189
+ console.log(` ${fk.from} -> ${fk.table}.${fk.to}`);
190
+ }
191
+ }
192
+
193
+ console.log('\n' + JSON.stringify({
194
+ table,
195
+ columns: result.rows.map(c => ({
196
+ name: c.name, type: c.type, nullable: !c.notnull,
197
+ default: c.dflt_value, pk: !!c.pk,
198
+ })),
199
+ }));
200
+ break;
201
+ }
202
+
203
+ case 'count': {
204
+ if (!target) throw new Error('Usage: db_explorer count <table_name>');
205
+ const table = sanitizeTableName(target);
206
+ let query = `SELECT COUNT(*) as cnt FROM "${table}"`;
207
+ if (opts.where) query += ` WHERE ${opts.where}`;
208
+
209
+ if (!isReadOnly(query)) throw new Error('Query validation failed — only read-only queries are allowed');
210
+
211
+ const result = await client.execute(query);
212
+ const count = result.rows[0]?.cnt ?? 0;
213
+ console.log(chalk.cyan(`Row count for ${chalk.bold(table)}: ${chalk.green(String(count))}`));
214
+ console.log(JSON.stringify({ table, count }));
215
+ break;
216
+ }
217
+
218
+ case 'sample': {
219
+ if (!target) throw new Error('Usage: db_explorer sample <table_name> [--limit N] [--where "..."]');
220
+ const table = sanitizeTableName(target);
221
+ const limit = parseInt(opts.limit) || 10;
222
+
223
+ let query = `SELECT * FROM "${table}"`;
224
+ if (opts.where) query += ` WHERE ${opts.where}`;
225
+ if (opts.orderBy) query += ` ORDER BY ${opts.orderBy}`;
226
+ query += ` LIMIT ${limit}`;
227
+
228
+ if (!isReadOnly(query)) throw new Error('Query validation failed — only read-only queries are allowed');
229
+
230
+ const result = await client.execute(query);
231
+ const columns = result.columns.length > 0
232
+ ? result.columns
233
+ : result.rows.length > 0 ? Object.keys(result.rows[0]) : [];
234
+ const rows = redactRows(result.rows as Record<string, unknown>[], columns);
235
+
236
+ console.log(chalk.cyan.bold(`\nSample from ${table} (${rows.length} rows):\n`));
237
+ printRows(rows, columns);
238
+ console.log('\n' + JSON.stringify({ table, count: rows.length, columns, rows }));
239
+ break;
240
+ }
241
+
242
+ case 'query': {
243
+ if (!target) throw new Error('Usage: db_explorer query "SELECT ..."');
244
+ if (!isReadOnly(target)) {
245
+ throw new Error('Only read-only queries are allowed (SELECT, PRAGMA, EXPLAIN). Write operations are blocked.');
246
+ }
247
+
248
+ // Add safety limit if none specified
249
+ const upper = target.toUpperCase();
250
+ const query = upper.startsWith('SELECT') && !upper.includes('LIMIT')
251
+ ? `${target} LIMIT 100`
252
+ : target;
253
+
254
+ const result = await client.execute(query);
255
+ const columns = result.columns.length > 0
256
+ ? result.columns
257
+ : result.rows.length > 0 ? Object.keys(result.rows[0]) : [];
258
+ const rows = redactRows(result.rows as Record<string, unknown>[], columns);
259
+
260
+ console.log(chalk.cyan(`\nQuery returned ${rows.length} row(s):\n`));
261
+ printRows(rows, columns);
262
+ console.log('\n' + JSON.stringify({ count: rows.length, columns, rows }));
263
+ break;
264
+ }
265
+
266
+ default:
267
+ throw new Error(`Unknown action "${action}". Valid actions: tables, describe, count, sample, query`);
268
+ }
269
+ } catch (err) {
270
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
271
+ process.exit(1);
272
+ } finally {
273
+ client.close();
274
+ }
275
+ })();
@@ -14,9 +14,10 @@ import chalk from 'chalk';
14
14
  import { readGraph } from '../lib/graph.js';
15
15
  import { loadKanban, kanbanExists } from '../lib/kanban.js';
16
16
 
17
- const VALID_COLUMNS = ['todo', 'in_progress', 'in_review', 'qa', 'done'];
17
+ const VALID_COLUMNS = ['backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done'];
18
18
 
19
19
  const COLUMN_COLORS = {
20
+ backlog: chalk.dim,
20
21
  todo: chalk.gray,
21
22
  in_progress: chalk.blue,
22
23
  in_review: chalk.magenta,
@@ -25,11 +25,12 @@ import { program } from 'commander';
25
25
  import chalk from 'chalk';
26
26
  import { getKanbanEntry, saveKanbanEntry, kanbanExists } from '../lib/kanban.js';
27
27
 
28
- const VALID_COLUMNS = ['todo', 'in_progress', 'in_review', 'qa', 'done'];
28
+ const VALID_COLUMNS = ['backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done'];
29
29
 
30
30
  // AI-allowed transitions (via CLI)
31
31
  const ALLOWED_TRANSITIONS = {
32
- 'todo': ['in_progress'],
32
+ 'backlog': ['todo'],
33
+ 'todo': ['in_progress', 'backlog'],
33
34
  'in_progress': ['in_review'],
34
35
  'in_review': ['qa', 'todo'],
35
36
  'qa': ['todo'],
@@ -114,8 +114,8 @@ const opts = program.opts();
114
114
  if (nodeMeta.type === 'feature' && finalStatus === 'defined' && meta.completeness >= 1) {
115
115
  const existing = await getKanbanEntry(id);
116
116
  if (!existing) {
117
- await saveKanbanEntry(id, { column: 'todo', rejection_count: 0, notes: [] }, opts.projectId);
118
- console.log(chalk.green(`✓ Auto-added ${id} to kanban board (todo)`));
117
+ await saveKanbanEntry(id, { column: 'backlog', rejection_count: 0, notes: [] }, opts.projectId);
118
+ console.log(chalk.green(`✓ Auto-added ${id} to kanban board (backlog)`));
119
119
  }
120
120
  }
121
121
 
@@ -12,7 +12,7 @@ import { randomUUID } from 'node:crypto';
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
 
14
14
  // Valid kanban columns — users can move cards between any columns (mirrors server logic)
15
- const VALID_COLUMNS = ['todo', 'in_progress', 'in_review', 'qa', 'done'];
15
+ const VALID_COLUMNS = ['backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done'];
16
16
 
17
17
  // In-memory kanban data used by the test server
18
18
  let kanbanData;
@@ -96,7 +96,7 @@ describe('Pipeline stats on all kanban cards', () => {
96
96
  });
97
97
 
98
98
  describe('shouldShowStats — stats visibility for all columns', () => {
99
- const columns = ['todo', 'in_progress', 'in_review', 'qa', 'done'];
99
+ const columns = ['backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done'];
100
100
 
101
101
  for (const column of columns) {
102
102
  it(`shows stats for ${column} card with completed pipeline`, () => {