@zzclub/pipeline 0.4.0 → 0.9.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 (48) hide show
  1. package/README.md +102 -3
  2. package/package.json +1 -1
  3. package/src/adapter-types.ts +6 -0
  4. package/src/adapters/builtin-image-renderer.ts +7 -1
  5. package/src/adapters/builtin-markdown-renderer.ts +1 -0
  6. package/src/commands/analytics.test.ts +155 -0
  7. package/src/commands/analytics.ts +223 -0
  8. package/src/commands/init.ts +42 -3
  9. package/src/commands/publish.ts +44 -67
  10. package/src/commands/republish.ts +146 -0
  11. package/src/commands/sync-blog.ts +19 -3
  12. package/src/commands/topic.test.ts +146 -0
  13. package/src/commands/topic.ts +717 -0
  14. package/src/commands/wechat-export.ts +2 -0
  15. package/src/commands/wx-draft-delete.ts +36 -0
  16. package/src/commands/wx-drafts.ts +47 -0
  17. package/src/config.ts +0 -0
  18. package/src/db.test.ts +47 -0
  19. package/src/db.ts +95 -0
  20. package/src/imgx/assets/icons/ancientone-logo.png +0 -0
  21. package/src/imgx/assets/templates/wechat-cover-split.html +1 -1
  22. package/src/imgx/render-card.ts +1 -5
  23. package/src/plugins.ts +11 -1
  24. package/src/providers/blog.ts +9 -3
  25. package/src/providers/index.test.ts +1 -1
  26. package/src/providers/index.ts +29 -5
  27. package/src/providers/publish-core.ts +124 -0
  28. package/src/providers/wechat.ts +260 -28
  29. package/src/routes.ts +1 -1
  30. package/src/schema/analytics.test.ts +54 -0
  31. package/src/schema/analytics.ts +19 -0
  32. package/src/schema/config.ts +25 -1
  33. package/src/schema/state.ts +10 -0
  34. package/src/schema/topic.test.ts +69 -0
  35. package/src/schema/topic.ts +55 -0
  36. package/src/state.ts +1 -0
  37. package/src/text.test.ts +11 -6
  38. package/src/text.ts +27 -9
  39. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +2 -7
  40. package/src/wechat-preview/assets/browser-dist/assets/{style-BYNXnUvc.css → style-vhHYSP8T.css} +1 -1
  41. package/src/wechat-preview/assets/browser-dist/editor-export.js +27 -27
  42. package/src/wechat-preview/browser/editor-export.css +248 -159
  43. package/src/wechat-preview/index.ts +28 -11
  44. package/src/wechat-preview/themes.ts +70 -24
  45. package/src/wechat-preview/wechat-formatter.test.ts +0 -1
  46. package/src/wechat-preview/wechat-formatter.ts +48 -23
  47. package/src/workflow.test.ts +294 -1
  48. package/src/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
@@ -0,0 +1,717 @@
1
+ import { ensureDb } from "../db";
2
+ import {
3
+ TopicSchema,
4
+ type Topic,
5
+ type TopicStatus,
6
+ type TopicPriority,
7
+ } from "../schema/topic";
8
+ import { parseArgs, requireArg, optionalArg } from "../args";
9
+ import { printResult } from "../output";
10
+
11
+ function generateTopicId(): string {
12
+ const timestamp = Date.now();
13
+ const random = Math.random().toString(36).substring(2, 8);
14
+ return `topic_${timestamp}_${random}`;
15
+ }
16
+
17
+ function rowToTopic(row: Record<string, unknown>): Topic {
18
+ return TopicSchema.parse({
19
+ ...row,
20
+ tags: row.tags ? JSON.parse(row.tags as string) : [],
21
+ retro_metrics_snapshot: row.retro_metrics_snapshot
22
+ ? JSON.parse(row.retro_metrics_snapshot as string)
23
+ : null,
24
+ });
25
+ }
26
+
27
+ export interface AddTopicInput {
28
+ title: string;
29
+ description?: string;
30
+ priority?: TopicPriority;
31
+ tags?: string[];
32
+ notes?: string;
33
+ status?: TopicStatus;
34
+ }
35
+
36
+ export async function addTopic(
37
+ workspace: string,
38
+ input: AddTopicInput,
39
+ ): Promise<Topic> {
40
+ const db = ensureDb(workspace);
41
+
42
+ try {
43
+ const now = new Date().toISOString();
44
+ const topicId = generateTopicId();
45
+
46
+ db.prepare(
47
+ `INSERT INTO topics (
48
+ topic_id, title, description, priority, tags, notes, status,
49
+ created_at, updated_at
50
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
51
+ ).run(
52
+ topicId,
53
+ input.title,
54
+ input.description ?? null,
55
+ input.priority ?? "medium",
56
+ JSON.stringify(input.tags ?? []),
57
+ input.notes ?? null,
58
+ input.status ?? "backlog",
59
+ now,
60
+ now,
61
+ );
62
+
63
+ const row = db
64
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
65
+ .get(topicId) as Record<string, unknown>;
66
+ return rowToTopic(row);
67
+ } finally {
68
+ db.close();
69
+ }
70
+ }
71
+
72
+ export interface ListTopicsFilter {
73
+ status?: TopicStatus;
74
+ priority?: TopicPriority;
75
+ tag?: string;
76
+ limit?: number;
77
+ }
78
+
79
+ export interface UpdateTopicInput {
80
+ status?: TopicStatus;
81
+ priority?: TopicPriority;
82
+ ai_score?: number;
83
+ ai_reason?: string;
84
+ tags?: string[];
85
+ notes?: string;
86
+ }
87
+
88
+ export async function updateTopic(
89
+ workspace: string,
90
+ topicId: string,
91
+ input: UpdateTopicInput,
92
+ ): Promise<Topic> {
93
+ const db = ensureDb(workspace);
94
+
95
+ try {
96
+ const existing = db
97
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
98
+ .get(topicId);
99
+ if (!existing) {
100
+ throw new Error(`Topic not found: ${topicId}`);
101
+ }
102
+
103
+ const updates: string[] = [];
104
+ const params: (string | number | null)[] = [];
105
+
106
+ if (input.status !== undefined) {
107
+ updates.push("status = ?");
108
+ params.push(input.status);
109
+ }
110
+ if (input.priority !== undefined) {
111
+ updates.push("priority = ?");
112
+ params.push(input.priority);
113
+ }
114
+ if (input.ai_score !== undefined) {
115
+ updates.push("ai_score = ?");
116
+ params.push(input.ai_score);
117
+ }
118
+ if (input.ai_reason !== undefined) {
119
+ updates.push("ai_reason = ?");
120
+ params.push(input.ai_reason);
121
+ }
122
+ if (input.tags !== undefined) {
123
+ updates.push("tags = ?");
124
+ params.push(JSON.stringify(input.tags));
125
+ }
126
+ if (input.notes !== undefined) {
127
+ updates.push("notes = ?");
128
+ params.push(input.notes);
129
+ }
130
+
131
+ updates.push("updated_at = ?");
132
+ params.push(new Date().toISOString());
133
+ params.push(topicId);
134
+
135
+ db
136
+ .prepare(`UPDATE topics SET ${updates.join(", ")} WHERE topic_id = ?`)
137
+ .run(...params);
138
+
139
+ const row = db
140
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
141
+ .get(topicId) as Record<string, unknown>;
142
+ return rowToTopic(row);
143
+ } finally {
144
+ db.close();
145
+ }
146
+ }
147
+
148
+ export interface ScheduleTopicInput {
149
+ scheduled_date: string;
150
+ target_account?: string;
151
+ }
152
+
153
+ export async function scheduleTopic(
154
+ workspace: string,
155
+ topicId: string,
156
+ input: ScheduleTopicInput,
157
+ ): Promise<Topic> {
158
+ const db = ensureDb(workspace);
159
+
160
+ try {
161
+ const existing = db
162
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
163
+ .get(topicId);
164
+ if (!existing) {
165
+ throw new Error(`Topic not found: ${topicId}`);
166
+ }
167
+
168
+ const now = new Date().toISOString();
169
+ db.prepare(
170
+ `UPDATE topics
171
+ SET status = 'scheduled',
172
+ scheduled_date = ?,
173
+ target_account = ?,
174
+ updated_at = ?
175
+ WHERE topic_id = ?`,
176
+ ).run(input.scheduled_date, input.target_account ?? null, now, topicId);
177
+
178
+ const row = db
179
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
180
+ .get(topicId) as Record<string, unknown>;
181
+ return rowToTopic(row);
182
+ } finally {
183
+ db.close();
184
+ }
185
+ }
186
+
187
+ export interface RetroTopicInput {
188
+ performance: "excellent" | "good" | "average" | "poor";
189
+ lessons?: string;
190
+ metrics_snapshot?: Record<string, unknown>;
191
+ }
192
+
193
+ export async function retroTopic(
194
+ workspace: string,
195
+ topicId: string,
196
+ input: RetroTopicInput,
197
+ ): Promise<Topic> {
198
+ const db = ensureDb(workspace);
199
+
200
+ try {
201
+ const existing = db
202
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
203
+ .get(topicId);
204
+ if (!existing) {
205
+ throw new Error(`Topic not found: ${topicId}`);
206
+ }
207
+
208
+ const now = new Date().toISOString();
209
+ db.prepare(
210
+ `UPDATE topics
211
+ SET status = 'published',
212
+ retro_performance = ?,
213
+ retro_lessons = ?,
214
+ retro_metrics_snapshot = ?,
215
+ updated_at = ?
216
+ WHERE topic_id = ?`,
217
+ ).run(
218
+ input.performance,
219
+ input.lessons ?? null,
220
+ input.metrics_snapshot ? JSON.stringify(input.metrics_snapshot) : null,
221
+ now,
222
+ topicId,
223
+ );
224
+
225
+ const row = db
226
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
227
+ .get(topicId) as Record<string, unknown>;
228
+ return rowToTopic(row);
229
+ } finally {
230
+ db.close();
231
+ }
232
+ }
233
+
234
+ export interface AbandonTopicInput {
235
+ reason?: string;
236
+ }
237
+
238
+ export async function abandonTopic(
239
+ workspace: string,
240
+ topicId: string,
241
+ input: AbandonTopicInput,
242
+ ): Promise<Topic> {
243
+ const db = ensureDb(workspace);
244
+
245
+ try {
246
+ const existing = db
247
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
248
+ .get(topicId) as Record<string, unknown> | undefined;
249
+ if (!existing) {
250
+ throw new Error(`Topic not found: ${topicId}`);
251
+ }
252
+
253
+ const existingNotes = (existing.notes as string) ?? "";
254
+ const newNotes = input.reason
255
+ ? `${existingNotes}\n[Abandoned] ${input.reason}`.trim()
256
+ : existingNotes;
257
+
258
+ const now = new Date().toISOString();
259
+ db.prepare(
260
+ `UPDATE topics
261
+ SET status = 'abandoned',
262
+ notes = ?,
263
+ updated_at = ?
264
+ WHERE topic_id = ?`,
265
+ ).run(newNotes, now, topicId);
266
+
267
+ const row = db
268
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
269
+ .get(topicId) as Record<string, unknown>;
270
+ return rowToTopic(row);
271
+ } finally {
272
+ db.close();
273
+ }
274
+ }
275
+
276
+ export interface EvaluateTopicInput {
277
+ ai_score: number;
278
+ ai_reason?: string;
279
+ }
280
+
281
+ export async function evaluateTopic(
282
+ workspace: string,
283
+ topicId: string,
284
+ input: EvaluateTopicInput,
285
+ ): Promise<Topic> {
286
+ const db = ensureDb(workspace);
287
+
288
+ try {
289
+ const existing = db
290
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
291
+ .get(topicId);
292
+ if (!existing) {
293
+ throw new Error(`Topic not found: ${topicId}`);
294
+ }
295
+
296
+ const now = new Date().toISOString();
297
+ db.prepare(
298
+ `UPDATE topics
299
+ SET status = 'evaluating',
300
+ ai_score = ?,
301
+ ai_reason = ?,
302
+ updated_at = ?
303
+ WHERE topic_id = ?`,
304
+ ).run(input.ai_score, input.ai_reason ?? null, now, topicId);
305
+
306
+ const row = db
307
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
308
+ .get(topicId) as Record<string, unknown>;
309
+ return rowToTopic(row);
310
+ } finally {
311
+ db.close();
312
+ }
313
+ }
314
+
315
+ export async function listTopics(
316
+ workspace: string,
317
+ filter: ListTopicsFilter,
318
+ ): Promise<Topic[]> {
319
+ const db = ensureDb(workspace);
320
+
321
+ try {
322
+ const conditions: string[] = [];
323
+ const params: (string | number)[] = [];
324
+
325
+ if (filter.status) {
326
+ conditions.push("status = ?");
327
+ params.push(filter.status);
328
+ }
329
+
330
+ if (filter.priority) {
331
+ conditions.push("priority = ?");
332
+ params.push(filter.priority);
333
+ }
334
+
335
+ if (filter.tag) {
336
+ conditions.push(
337
+ "EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)",
338
+ );
339
+ params.push(filter.tag);
340
+ }
341
+
342
+ const where =
343
+ conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
344
+ const limit = filter.limit ?? 50;
345
+
346
+ const sql = `SELECT * FROM topics ${where} ORDER BY created_at DESC LIMIT ?`;
347
+ params.push(limit);
348
+
349
+ const rows = db.prepare(sql).all(...params) as Record<string, unknown>[];
350
+ return rows.map(rowToTopic);
351
+ } finally {
352
+ db.close();
353
+ }
354
+ }
355
+
356
+ export async function topic(args: string[]): Promise<void> {
357
+ const parsed = parseArgs(args);
358
+
359
+ if (parsed.help || args.length === 0) {
360
+ console.log(`
361
+ Usage: zzhub-pipeline topic <subcommand> [options]
362
+
363
+ Subcommands:
364
+ add Add a new topic
365
+ list List topics with filters
366
+ update Update topic fields
367
+ evaluate Evaluate topic with AI score
368
+ schedule Schedule topic for publishing
369
+ promote Promote topic to publish task
370
+ retro Add retrospective to topic
371
+ abandon Abandon a topic
372
+
373
+ Run 'zzhub-pipeline topic <subcommand> --help' for details.
374
+ `.trim());
375
+ return;
376
+ }
377
+
378
+ const subcommand = args[0];
379
+ const subArgs = args.slice(1);
380
+
381
+ switch (subcommand) {
382
+ case "add":
383
+ return topicAdd(subArgs);
384
+ case "list":
385
+ return topicList(subArgs);
386
+ case "update":
387
+ return topicUpdate(subArgs);
388
+ case "evaluate":
389
+ return topicEvaluate(subArgs);
390
+ case "schedule":
391
+ return topicSchedule(subArgs);
392
+ case "promote":
393
+ return topicPromote(subArgs);
394
+ case "retro":
395
+ return topicRetro(subArgs);
396
+ case "abandon":
397
+ return topicAbandon(subArgs);
398
+ default:
399
+ throw new Error(`Unknown topic subcommand: ${subcommand}`);
400
+ }
401
+ }
402
+
403
+ async function topicAdd(args: string[]): Promise<void> {
404
+ const parsed = parseArgs(args);
405
+
406
+ if (parsed.help) {
407
+ console.log(`
408
+ Usage: zzhub-pipeline topic add [options]
409
+
410
+ Options:
411
+ --workspace Workspace path (required)
412
+ --title Topic title (required)
413
+ --description Topic description
414
+ --priority Priority: high/medium/low (default: medium)
415
+ --tags Comma-separated tags
416
+ --notes Notes
417
+ `.trim());
418
+ return;
419
+ }
420
+
421
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
422
+ const title = requireArg(parsed, "title", "Topic title");
423
+ const description = optionalArg(parsed, "description");
424
+ const priority = optionalArg(parsed, "priority") as TopicPriority | undefined;
425
+ const tagsRaw = optionalArg(parsed, "tags");
426
+ const notes = optionalArg(parsed, "notes");
427
+
428
+ const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()) : undefined;
429
+
430
+ const result = await addTopic(workspace, {
431
+ title,
432
+ description,
433
+ priority,
434
+ tags,
435
+ notes,
436
+ });
437
+
438
+ printResult(result);
439
+ }
440
+
441
+ async function topicList(args: string[]): Promise<void> {
442
+ const parsed = parseArgs(args);
443
+
444
+ if (parsed.help) {
445
+ console.log(`
446
+ Usage: zzhub-pipeline topic list [options]
447
+
448
+ Options:
449
+ --workspace Workspace path (required)
450
+ --status Filter by status
451
+ --priority Filter by priority
452
+ --tag Filter by tag
453
+ --sort Sort field: priority/created_at/scheduled_date/ai_score
454
+ --limit Max results (default: 50)
455
+ --view Output format: json/markdown/agent (default: json)
456
+ `.trim());
457
+ return;
458
+ }
459
+
460
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
461
+ const status = optionalArg(parsed, "status") as TopicStatus | undefined;
462
+ const priority = optionalArg(parsed, "priority") as TopicPriority | undefined;
463
+ const tag = optionalArg(parsed, "tag");
464
+ const limitStr = optionalArg(parsed, "limit");
465
+ const limit = limitStr ? parseInt(limitStr, 10) : undefined;
466
+
467
+ const topics = await listTopics(workspace, { status, priority, tag, limit });
468
+
469
+ printResult(topics);
470
+ }
471
+
472
+ async function topicUpdate(args: string[]): Promise<void> {
473
+ const parsed = parseArgs(args);
474
+
475
+ if (parsed.help) {
476
+ console.log(`
477
+ Usage: zzhub-pipeline topic update [options]
478
+
479
+ Options:
480
+ --workspace Workspace path (required)
481
+ --topic Topic ID (required)
482
+ --status New status
483
+ --priority New priority
484
+ --ai-score AI score (0-100)
485
+ --ai-reason AI evaluation reason
486
+ --tags Comma-separated tags
487
+ --notes Notes
488
+ `.trim());
489
+ return;
490
+ }
491
+
492
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
493
+ const topicId = requireArg(parsed, "topic", "Topic ID");
494
+ const status = optionalArg(parsed, "status") as TopicStatus | undefined;
495
+ const priority = optionalArg(parsed, "priority") as TopicPriority | undefined;
496
+ const aiScoreStr = optionalArg(parsed, "ai-score");
497
+ const aiScore = aiScoreStr ? parseInt(aiScoreStr, 10) : undefined;
498
+ const aiReason = optionalArg(parsed, "ai-reason");
499
+ const tagsRaw = optionalArg(parsed, "tags");
500
+ const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()) : undefined;
501
+ const notes = optionalArg(parsed, "notes");
502
+
503
+ const result = await updateTopic(workspace, topicId, {
504
+ status,
505
+ priority,
506
+ ai_score: aiScore,
507
+ ai_reason: aiReason,
508
+ tags,
509
+ notes,
510
+ });
511
+
512
+ printResult(result);
513
+ }
514
+
515
+ async function topicEvaluate(args: string[]): Promise<void> {
516
+ const parsed = parseArgs(args);
517
+
518
+ if (parsed.help) {
519
+ console.log(`
520
+ Usage: zzhub-pipeline topic evaluate [options]
521
+
522
+ Options:
523
+ --workspace Workspace path (required)
524
+ --topic Topic ID (required)
525
+ --ai-score AI score 0-100 (required)
526
+ --ai-reason AI evaluation reason
527
+ `.trim());
528
+ return;
529
+ }
530
+
531
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
532
+ const topicId = requireArg(parsed, "topic", "Topic ID");
533
+ const aiScoreStr = requireArg(parsed, "ai-score", "AI score (0-100)");
534
+ const aiScore = parseInt(aiScoreStr, 10);
535
+ const aiReason = optionalArg(parsed, "ai-reason");
536
+
537
+ const result = await evaluateTopic(workspace, topicId, {
538
+ ai_score: aiScore,
539
+ ai_reason: aiReason,
540
+ });
541
+
542
+ printResult(result);
543
+ }
544
+
545
+ async function topicSchedule(args: string[]): Promise<void> {
546
+ const parsed = parseArgs(args);
547
+
548
+ if (parsed.help) {
549
+ console.log(`
550
+ Usage: zzhub-pipeline topic schedule [options]
551
+
552
+ Options:
553
+ --workspace Workspace path (required)
554
+ --topic Topic ID (required)
555
+ --scheduled-date Scheduled date YYYY-MM-DD (required)
556
+ --target-account Target account
557
+ `.trim());
558
+ return;
559
+ }
560
+
561
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
562
+ const topicId = requireArg(parsed, "topic", "Topic ID");
563
+ const scheduledDate = requireArg(parsed, "scheduled-date", "Scheduled date");
564
+ const targetAccount = optionalArg(parsed, "target-account");
565
+
566
+ const result = await scheduleTopic(workspace, topicId, {
567
+ scheduled_date: scheduledDate,
568
+ target_account: targetAccount,
569
+ });
570
+
571
+ printResult(result);
572
+ }
573
+
574
+ async function topicPromote(args: string[]): Promise<void> {
575
+ const parsed = parseArgs(args);
576
+
577
+ if (parsed.help) {
578
+ console.log(`
579
+ Usage: zzhub-pipeline topic promote [options]
580
+
581
+ Promote a topic to a publish task. This will:
582
+ 1. Create a new publish task using 'init' command
583
+ 2. Update topic status to 'in_progress'
584
+ 3. Link the run_id to the topic
585
+
586
+ Options:
587
+ --workspace Workspace path (required)
588
+ --topic Topic ID (required)
589
+ --intent-text Intent text for init (required)
590
+ --content-form article | newspic (default: article)
591
+ --targets Comma-separated: wechat,blog (default: wechat)
592
+ --account Target account
593
+ `.trim());
594
+ return;
595
+ }
596
+
597
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
598
+ const topicId = requireArg(parsed, "topic", "Topic ID");
599
+ const intentText = requireArg(parsed, "intent-text", "Intent text");
600
+ const contentForm = optionalArg(parsed, "content-form") ?? "article";
601
+ const targets = optionalArg(parsed, "targets") ?? "wechat";
602
+ const account = optionalArg(parsed, "account");
603
+
604
+ // Get topic to verify it exists
605
+ const db = ensureDb(workspace);
606
+ try {
607
+ const topicRow = db
608
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
609
+ .get(topicId) as Record<string, unknown> | undefined;
610
+ if (!topicRow) {
611
+ throw new Error(`Topic not found: ${topicId}`);
612
+ }
613
+
614
+ // Build init command args
615
+ const initArgs = [
616
+ "--workspace", workspace,
617
+ "--task-kind", "publish",
618
+ "--content-form", contentForm,
619
+ "--targets", targets,
620
+ "--content-origin", "user",
621
+ "--intent-text", intentText,
622
+ ];
623
+ if (account) {
624
+ initArgs.push("--account", account);
625
+ }
626
+
627
+ // Call init command
628
+ const { init } = await import("./init");
629
+ await init(initArgs);
630
+
631
+ // Note: We can't easily capture the run_id from init's output
632
+ // For now, just update the topic status
633
+ const now = new Date().toISOString();
634
+ db.prepare(
635
+ `UPDATE topics
636
+ SET status = 'in_progress',
637
+ updated_at = ?
638
+ WHERE topic_id = ?`,
639
+ ).run(now, topicId);
640
+
641
+ const updatedRow = db
642
+ .prepare("SELECT * FROM topics WHERE topic_id = ?")
643
+ .get(topicId) as Record<string, unknown>;
644
+ const updatedTopic = rowToTopic(updatedRow);
645
+
646
+ printResult({
647
+ topic: updatedTopic,
648
+ message: "Topic promoted to in_progress. Run 'zzp find-run --workspace {workspace} --active' to find the new task.",
649
+ init_args: initArgs,
650
+ });
651
+ } finally {
652
+ db.close();
653
+ }
654
+ }
655
+
656
+ async function topicRetro(args: string[]): Promise<void> {
657
+ const parsed = parseArgs(args);
658
+
659
+ if (parsed.help) {
660
+ console.log(`
661
+ Usage: zzhub-pipeline topic retro [options]
662
+
663
+ Options:
664
+ --workspace Workspace path (required)
665
+ --topic Topic ID (required)
666
+ --performance Performance: excellent/good/average/poor (required)
667
+ --lessons Lessons learned
668
+ --metrics-snapshot Metrics snapshot as JSON string
669
+ `.trim());
670
+ return;
671
+ }
672
+
673
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
674
+ const topicId = requireArg(parsed, "topic", "Topic ID");
675
+ const performance = requireArg(parsed, "performance", "Performance rating") as
676
+ | "excellent"
677
+ | "good"
678
+ | "average"
679
+ | "poor";
680
+ const lessons = optionalArg(parsed, "lessons");
681
+ const metricsSnapshotRaw = optionalArg(parsed, "metrics-snapshot");
682
+ const metricsSnapshot = metricsSnapshotRaw
683
+ ? JSON.parse(metricsSnapshotRaw)
684
+ : undefined;
685
+
686
+ const result = await retroTopic(workspace, topicId, {
687
+ performance,
688
+ lessons,
689
+ metrics_snapshot: metricsSnapshot,
690
+ });
691
+
692
+ printResult(result);
693
+ }
694
+
695
+ async function topicAbandon(args: string[]): Promise<void> {
696
+ const parsed = parseArgs(args);
697
+
698
+ if (parsed.help) {
699
+ console.log(`
700
+ Usage: zzhub-pipeline topic abandon [options]
701
+
702
+ Options:
703
+ --workspace Workspace path (required)
704
+ --topic Topic ID (required)
705
+ --reason Reason for abandonment
706
+ `.trim());
707
+ return;
708
+ }
709
+
710
+ const workspace = requireArg(parsed, "workspace", "Workspace path");
711
+ const topicId = requireArg(parsed, "topic", "Topic ID");
712
+ const reason = optionalArg(parsed, "reason");
713
+
714
+ const result = await abandonTopic(workspace, topicId, { reason });
715
+
716
+ printResult(result);
717
+ }