@abarcenas/ai-workflow-template 1.34.0 → 1.39.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 (34) hide show
  1. package/.agents/instructions/knowledgebase.instructions.md +238 -0
  2. package/.agents/instructions/learned-knowledge.instructions.md +91 -0
  3. package/.husky/post-commit +10 -0
  4. package/.husky/post-merge +0 -1
  5. package/.opencode/agents/architect.agent.md +1 -0
  6. package/.opencode/agents/coder.agent.md +1 -0
  7. package/.opencode/agents/deployer.agent.md +1 -0
  8. package/.opencode/agents/designer.agent.md +1 -0
  9. package/.opencode/agents/e2e-tester.agent.md +1 -0
  10. package/.opencode/agents/implementer.agent.md +1 -0
  11. package/.opencode/agents/orchestrator/feature-pipeline.agent.md +1 -0
  12. package/.opencode/agents/orchestrator/orchestrator.agent.md +1 -0
  13. package/.opencode/agents/orchestrator/tdd-orchestrator.agent.md +1 -0
  14. package/.opencode/agents/researcher.agent.md +1 -0
  15. package/.opencode/agents/reviewer.agent.md +1 -0
  16. package/.opencode/agents/tracker.agent.md +2 -0
  17. package/.opencode/agents/unit-tester.agent.md +1 -0
  18. package/README.md +1 -1
  19. package/memory-bank/.vocabulary.json +8 -2
  20. package/package.json +12 -6
  21. package/scripts/knowledgebase-cli.js +228 -0
  22. package/scripts/knowledgebase-cli.test.js +576 -0
  23. package/scripts/knowledgebase-index.js +786 -0
  24. package/scripts/knowledgebase-index.test.js +759 -0
  25. package/scripts/knowledgebase-init.sql +193 -0
  26. package/scripts/mcp-knowledgebase-server.js +330 -0
  27. package/scripts/setup/constants.js +36 -3
  28. package/scripts/setup/hooks.js +2 -2
  29. package/scripts/setup/hooks.test.js +13 -5
  30. package/scripts/setup/index.js +29 -2
  31. package/scripts/setup/index.test.js +36 -1
  32. package/scripts/setup/knowledgebase.js +204 -0
  33. package/scripts/setup/knowledgebase.test.js +248 -0
  34. package/scripts/sync.js +40 -6
@@ -11,8 +11,9 @@
11
11
  // 6. Phase 3 — Prepare (configure consumer package.json prepare script)
12
12
  // 7. Phase 4 — Husky Init (generate .husky/_/ shims, unless --skip-hooks)
13
13
  // 8. Phase 5 — Sync (spawn sync.js + normalize-memory.js, unless --skip-sync)
14
- // 9. Render summary table via ui.summary()
15
- // 10. Determine exit code and return it
14
+ // 9. Phase 6 — Knowledgebase (register with centralized knowledgebase, unless --skip-knowledgebase)
15
+ // 10. Render summary table via ui.summary()
16
+ // 11. Determine exit code and return it
16
17
  //
17
18
  // Each phase is wrapped in try/catch. Only discovery failure is fatal.
18
19
  // All other phase errors are logged and the pipeline continues (REQ-04).
@@ -38,6 +39,7 @@ import { installHooks } from './hooks.js'
38
39
  import { handlePrepare } from './prepare.js'
39
40
  import { initHusky } from './husky-init.js'
40
41
  import { runSyncPhase } from './sync-phase.js'
42
+ import { registerKnowledgebase } from './knowledgebase.js'
41
43
 
42
44
  // ── Package version (lazy, cached) ───────────────────────────────────────────
43
45
 
@@ -85,6 +87,7 @@ function actionStatus(action) {
85
87
  case 'dry-run':
86
88
  case 'overwritten':
87
89
  case 'merged':
90
+ case 'indexed':
88
91
  return 'success'
89
92
  // These actions indicate the phase was intentionally skipped or
90
93
  // only partially completed — not an error but worth noting
@@ -331,6 +334,30 @@ export async function main(argv) {
331
334
  }
332
335
  }
333
336
 
337
+ // ═════════════════════════════════════════════════════════════════════════
338
+ // Phase 6 — Knowledgebase (unless --skip-knowledgebase)
339
+ // ═════════════════════════════════════════════════════════════════════════
340
+ if (!flags.skipKnowledgebase) {
341
+ section('Phase 6: Knowledgebase')
342
+
343
+ try {
344
+ const kbResult = await registerKnowledgebase(context)
345
+ context.kbResult = kbResult
346
+ phases.push({
347
+ phase: 'knowledgebase',
348
+ status: actionStatus(kbResult.action),
349
+ message: kbResult.message,
350
+ })
351
+ } catch (err) {
352
+ stepWarn(`Knowledgebase registration skipped: ${err.message}`)
353
+ phases.push({
354
+ phase: 'knowledgebase',
355
+ status: 'warn',
356
+ message: `Skipped \u2014 ${err.message}`,
357
+ })
358
+ }
359
+ }
360
+
334
361
  // ═════════════════════════════════════════════════════════════════════════
335
362
  // Summary
336
363
  // ═════════════════════════════════════════════════════════════════════════
@@ -72,6 +72,10 @@ vi.mock('./sync-phase.js', () => ({
72
72
  runSyncPhase: vi.fn(),
73
73
  }))
74
74
 
75
+ vi.mock('./knowledgebase.js', () => ({
76
+ registerKnowledgebase: vi.fn(),
77
+ }))
78
+
75
79
  // ═════════════════════════════════════════════════════════════════════════════
76
80
  // Imports — these resolve to the mocked modules above
77
81
  // ═════════════════════════════════════════════════════════════════════════════
@@ -93,6 +97,7 @@ import { installHooks } from './hooks.js'
93
97
  import { handlePrepare } from './prepare.js'
94
98
  import { initHusky } from './husky-init.js'
95
99
  import { runSyncPhase } from './sync-phase.js'
100
+ import { registerKnowledgebase } from './knowledgebase.js'
96
101
  import { EXIT_CODES } from './constants.js'
97
102
 
98
103
  // ═════════════════════════════════════════════════════════════════════════════
@@ -110,6 +115,7 @@ const defaultFlags = {
110
115
  skipHooks: false,
111
116
  skipPrepare: false,
112
117
  skipSync: false,
118
+ skipKnowledgebase: false,
113
119
  help: false,
114
120
  version: false,
115
121
  quiet: false,
@@ -190,6 +196,12 @@ describe('main() — setup orchestrator', () => {
190
196
  handlePrepare.mockResolvedValue(defaultPrepareResult)
191
197
  initHusky.mockResolvedValue(defaultHuskyResult)
192
198
  runSyncPhase.mockResolvedValue(defaultSyncResult)
199
+ registerKnowledgebase.mockResolvedValue({
200
+ action: 'indexed',
201
+ message: 'Registered "test-consumer" — 5 chunks indexed, 0 skipped',
202
+ chunks_indexed: 5,
203
+ chunks_skipped: 0,
204
+ })
193
205
 
194
206
  // Spy on console.log for --version tests
195
207
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
@@ -214,6 +226,7 @@ describe('main() — setup orchestrator', () => {
214
226
  expect(handlePrepare).not.toHaveBeenCalled()
215
227
  expect(initHusky).not.toHaveBeenCalled()
216
228
  expect(runSyncPhase).not.toHaveBeenCalled()
229
+ expect(registerKnowledgebase).not.toHaveBeenCalled()
217
230
  // No header or summary for early-exit flags
218
231
  expect(header).not.toHaveBeenCalled()
219
232
  expect(summary).not.toHaveBeenCalled()
@@ -231,6 +244,7 @@ describe('main() — setup orchestrator', () => {
231
244
  expect(discover).not.toHaveBeenCalled()
232
245
  expect(installHooks).not.toHaveBeenCalled()
233
246
  expect(header).not.toHaveBeenCalled()
247
+ expect(registerKnowledgebase).not.toHaveBeenCalled()
234
248
  })
235
249
 
236
250
  // ── 3. --version flag ────────────────────────────────────────────────────
@@ -246,6 +260,7 @@ describe('main() — setup orchestrator', () => {
246
260
  // Early exit means no pipeline phases run
247
261
  expect(discover).not.toHaveBeenCalled()
248
262
  expect(help).not.toHaveBeenCalled()
263
+ expect(registerKnowledgebase).not.toHaveBeenCalled()
249
264
  })
250
265
 
251
266
  // ── 4. -v short flag ─────────────────────────────────────────────────────
@@ -259,6 +274,7 @@ describe('main() — setup orchestrator', () => {
259
274
  expect(logSpy).toHaveBeenCalled()
260
275
  expect(logSpy.mock.calls[0][0]).toEqual(expect.any(String))
261
276
  expect(discover).not.toHaveBeenCalled()
277
+ expect(registerKnowledgebase).not.toHaveBeenCalled()
262
278
  })
263
279
 
264
280
  // ── 5. --dry-run flag ────────────────────────────────────────────────────
@@ -287,6 +303,7 @@ describe('main() — setup orchestrator', () => {
287
303
  expect(handlePrepare).toHaveBeenCalledTimes(1)
288
304
  expect(initHusky).toHaveBeenCalledTimes(1)
289
305
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
306
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
290
307
  // Header and summary shown (not quiet)
291
308
  expect(header).toHaveBeenCalledTimes(1)
292
309
  expect(summary).toHaveBeenCalledTimes(1)
@@ -307,6 +324,7 @@ describe('main() — setup orchestrator', () => {
307
324
  expect(discover).toHaveBeenCalledTimes(1)
308
325
  expect(handlePrepare).toHaveBeenCalledTimes(1)
309
326
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
327
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
310
328
  })
311
329
 
312
330
  // ── 7. --skip-sync flag ──────────────────────────────────────────────────
@@ -323,6 +341,7 @@ describe('main() — setup orchestrator', () => {
323
341
  expect(installHooks).toHaveBeenCalledTimes(1)
324
342
  expect(handlePrepare).toHaveBeenCalledTimes(1)
325
343
  expect(initHusky).toHaveBeenCalledTimes(1)
344
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
326
345
  })
327
346
 
328
347
  // ── 8. --skip-prepare flag ───────────────────────────────────────────────
@@ -339,6 +358,7 @@ describe('main() — setup orchestrator', () => {
339
358
  expect(installHooks).toHaveBeenCalledTimes(1)
340
359
  expect(initHusky).toHaveBeenCalledTimes(1)
341
360
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
361
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
342
362
  })
343
363
 
344
364
  // ── 9. Unknown flag ──────────────────────────────────────────────────────
@@ -362,7 +382,7 @@ describe('main() — setup orchestrator', () => {
362
382
 
363
383
  // ── 10. Full pipeline (default flags) ────────────────────────────────────
364
384
 
365
- it('full pipeline: all 5 phases called in order, exits 0', async () => {
385
+ it('full pipeline: all 6 phases called in order, exits 0', async () => {
366
386
  const code = await main([])
367
387
 
368
388
  expect(code).toBe(EXIT_CODES.SUCCESS)
@@ -372,6 +392,7 @@ describe('main() — setup orchestrator', () => {
372
392
  expect(handlePrepare).toHaveBeenCalledTimes(1)
373
393
  expect(initHusky).toHaveBeenCalledTimes(1)
374
394
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
395
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
375
396
  // Header displayed
376
397
  expect(header).toHaveBeenCalledTimes(1)
377
398
  // Summary displayed
@@ -398,6 +419,7 @@ describe('main() — setup orchestrator', () => {
398
419
  expect(handlePrepare).not.toHaveBeenCalled()
399
420
  expect(initHusky).not.toHaveBeenCalled()
400
421
  expect(runSyncPhase).not.toHaveBeenCalled()
422
+ expect(registerKnowledgebase).not.toHaveBeenCalled()
401
423
  // Summary is shown even on fatal (not quiet mode)
402
424
  expect(summary).toHaveBeenCalledTimes(1)
403
425
  })
@@ -419,6 +441,7 @@ describe('main() — setup orchestrator', () => {
419
441
  expect(handlePrepare).toHaveBeenCalledTimes(1)
420
442
  expect(initHusky).toHaveBeenCalledTimes(1)
421
443
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
444
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
422
445
  // Summary is shown
423
446
  expect(summary).toHaveBeenCalledTimes(1)
424
447
  })
@@ -445,6 +468,7 @@ describe('main() — setup orchestrator', () => {
445
468
  expect(discover).toHaveBeenCalledTimes(1)
446
469
  expect(handlePrepare).toHaveBeenCalledTimes(1)
447
470
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
471
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
448
472
  // Summary shown
449
473
  expect(summary).toHaveBeenCalledTimes(1)
450
474
  })
@@ -463,6 +487,7 @@ describe('main() — setup orchestrator', () => {
463
487
  expect(ciModeBanner).toHaveBeenCalledTimes(1)
464
488
  // CI mode doesn't skip phases (that's done by --skip-hooks)
465
489
  expect(discover).toHaveBeenCalledTimes(1)
490
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
466
491
  })
467
492
 
468
493
  it('CI mode: ciModeBanner NOT shown in quiet mode even when isCI is true', async () => {
@@ -496,6 +521,7 @@ describe('main() — setup orchestrator', () => {
496
521
  expect(handlePrepare).toHaveBeenCalledTimes(1)
497
522
  expect(initHusky).toHaveBeenCalledTimes(1)
498
523
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
524
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
499
525
  })
500
526
 
501
527
  // ── 16. Multiple skips combined ─────────────────────────────────────────
@@ -520,6 +546,7 @@ describe('main() — setup orchestrator', () => {
520
546
  expect(handlePrepare).not.toHaveBeenCalled()
521
547
  expect(initHusky).not.toHaveBeenCalled()
522
548
  expect(runSyncPhase).not.toHaveBeenCalled()
549
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
523
550
  })
524
551
 
525
552
  // ── 17. Verify parseCliArgs receives the argv ───────────────────────────
@@ -536,6 +563,7 @@ describe('main() — setup orchestrator', () => {
536
563
  await main(argv)
537
564
 
538
565
  expect(parseCliArgs).toHaveBeenCalledWith(argv)
566
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
539
567
  })
540
568
 
541
569
  // ── 18. Discovery context is passed to each phase ───────────────────────
@@ -556,6 +584,7 @@ describe('main() — setup orchestrator', () => {
556
584
  expect(handlePrepare).toHaveBeenCalledWith(ctx)
557
585
  expect(initHusky).toHaveBeenCalledWith(ctx)
558
586
  expect(runSyncPhase).toHaveBeenCalledWith(ctx)
587
+ expect(registerKnowledgebase).toHaveBeenCalledWith(ctx)
559
588
  })
560
589
 
561
590
  // ── 19a. --verbose flag passthrough to Context ──────────────────────────
@@ -577,6 +606,7 @@ describe('main() — setup orchestrator', () => {
577
606
  expect(handlePrepare).toHaveBeenCalledWith(ctx)
578
607
  expect(initHusky).toHaveBeenCalledWith(ctx)
579
608
  expect(runSyncPhase).toHaveBeenCalledWith(ctx)
609
+ expect(registerKnowledgebase).toHaveBeenCalledWith(ctx)
580
610
  // Normal pipeline — header and summary shown
581
611
  expect(header).toHaveBeenCalledTimes(1)
582
612
  expect(summary).toHaveBeenCalledTimes(1)
@@ -591,6 +621,7 @@ describe('main() — setup orchestrator', () => {
591
621
  await main(['--dry-run'])
592
622
 
593
623
  expect(discover).toHaveBeenCalledWith(flags)
624
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
594
625
  })
595
626
 
596
627
  // ── 20. Verify --help takes priority over --version ─────────────────────
@@ -608,6 +639,7 @@ describe('main() — setup orchestrator', () => {
608
639
  expect(help).toHaveBeenCalledTimes(1)
609
640
  expect(logSpy).not.toHaveBeenCalled() // console.log NOT called for version
610
641
  expect(discover).not.toHaveBeenCalled()
642
+ expect(registerKnowledgebase).not.toHaveBeenCalled()
611
643
  })
612
644
 
613
645
  // ── 21. Error in prepare phase → continues ──────────────────────────────
@@ -626,6 +658,7 @@ describe('main() — setup orchestrator', () => {
626
658
  expect(installHooks).toHaveBeenCalledTimes(1)
627
659
  expect(initHusky).toHaveBeenCalledTimes(1)
628
660
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
661
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
629
662
  })
630
663
 
631
664
  // ── 22. Error in husky-init → continues ────────────────────────────────
@@ -644,6 +677,7 @@ describe('main() — setup orchestrator', () => {
644
677
  expect(installHooks).toHaveBeenCalledTimes(1)
645
678
  expect(handlePrepare).toHaveBeenCalledTimes(1)
646
679
  expect(runSyncPhase).toHaveBeenCalledTimes(1)
680
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
647
681
  })
648
682
 
649
683
  // ── 23. Error in sync phase → continues ────────────────────────────────
@@ -660,5 +694,6 @@ describe('main() — setup orchestrator', () => {
660
694
  expect(installHooks).toHaveBeenCalledTimes(1)
661
695
  expect(handlePrepare).toHaveBeenCalledTimes(1)
662
696
  expect(initHusky).toHaveBeenCalledTimes(1)
697
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
663
698
  })
664
699
  })
@@ -0,0 +1,204 @@
1
+ // ═════════════════════════════════════════════════════════════════════════════
2
+ // knowledgebase.js — Setup Phase 6: register consumer project in the
3
+ // centralized knowledgebase (PostgreSQL + pgvector)
4
+ // ═════════════════════════════════════════════════════════════════════════════
5
+ //
6
+ // Spawns `knowledgebase-cli.js sync --project <id>` as a child process to
7
+ // register the consumer project and index its learned-knowledge chunks.
8
+ //
9
+ // Gracefully degrades when DATABASE_URL is not configured or when the CLI
10
+ // script is unavailable — never blocks the setup pipeline.
11
+ // ═════════════════════════════════════════════════════════════════════════════
12
+
13
+ import { spawn } from 'child_process'
14
+ import { join } from 'path'
15
+
16
+ import { UNICODE_CHARS } from './constants.js'
17
+ import { verbose } from './ui.js'
18
+ import { logInfo, logWarn, resolvePackageRoot } from './utils.js'
19
+
20
+ // ── Main export ─────────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Registers the consumer project in the centralized knowledgebase.
24
+ *
25
+ * Spawns `scripts/knowledgebase-cli.js sync --project <projectId>` as a child
26
+ * process so the CLI's own graceful degradation (DATABASE_URL check, optional
27
+ * dependency loading) is reused rather than duplicated.
28
+ *
29
+ * **Early exits (all return `{ action: 'skipped' }`):**
30
+ * - `flags.skipKnowledgebase` is `true` — user opted out via flag
31
+ * - `process.env.DATABASE_URL` is not set — not configured yet
32
+ * - `consumerPackageJson` has no `name` field — can't identify the project
33
+ *
34
+ * **Normal execution:**
35
+ * The CLI is spawned with `INIT_CWD` set to `consumerRoot`. If the child
36
+ * process exits with a non-zero code, the action becomes `'failed'` but the
37
+ * pipeline continues (graceful degradation per REQ-08).
38
+ *
39
+ * @param {object} context - Setup context object.
40
+ * @param {object} [context.flags={}] - Parsed CLI flags.
41
+ * @param {boolean} [context.flags.skipKnowledgebase=false] - If `true`, skip.
42
+ * @param {boolean} [context.verbose=false] - Enable verbose child output.
43
+ * @param {string} [context.consumerRoot] - Absolute consumer project root.
44
+ * @param {object} [context.consumerPackageJson] - Consumer's package.json.
45
+ * @returns {Promise<{
46
+ * action: 'indexed'|'skipped'|'failed',
47
+ * message: string,
48
+ * chunks_indexed?: number,
49
+ * chunks_skipped?: number
50
+ * }>}
51
+ */
52
+ export async function registerKnowledgebase(context) {
53
+ const { flags = {}, verbose: verboseFlag = false } = context
54
+
55
+ // ── Skip flag check ──────────────────────────────────────────────────
56
+ if (flags.skipKnowledgebase) {
57
+ const message =
58
+ '[knowledgebase] Skipping Phase 6 \u2014 --skip-knowledgebase flag set'
59
+ logInfo(message)
60
+ return { action: 'skipped', message }
61
+ }
62
+
63
+ // ── DATABASE_URL check ───────────────────────────────────────────────
64
+ if (!process.env.DATABASE_URL) {
65
+ const message =
66
+ 'DATABASE_URL not configured. Set up later with: ' +
67
+ 'npx ai-workflow-template setup --knowledgebase'
68
+ logWarn(
69
+ `[knowledgebase] Skipping Phase 6 \u2014 ${message}`,
70
+ )
71
+ return { action: 'skipped', message }
72
+ }
73
+
74
+ // ── Resolve project ID from consumer package.json ────────────────────
75
+ const projectId = context.consumerPackageJson?.name
76
+ if (!projectId) {
77
+ const message = 'No project name in consumer package.json'
78
+ logWarn(
79
+ `[knowledgebase] Skipping Phase 6 \u2014 ${message}`,
80
+ )
81
+ return { action: 'skipped', message }
82
+ }
83
+
84
+ // ── Spawn knowledgebase-cli.js sync ──────────────────────────────────
85
+ const packageRoot = resolvePackageRoot()
86
+ const cliPath = join(packageRoot, 'scripts', 'knowledgebase-cli.js')
87
+ const cwd = context.consumerRoot || process.cwd()
88
+
89
+ verbose(verboseFlag, `Spawning knowledgebase-cli.js sync for project "${projectId}"\u2026`)
90
+
91
+ try {
92
+ const result = await spawnScript(
93
+ 'knowledgebase-cli.js',
94
+ cliPath,
95
+ cwd,
96
+ ['sync', '--project', projectId],
97
+ verboseFlag,
98
+ )
99
+
100
+ // Non-zero exit from the CLI is a warning, not a blocker
101
+ if (!result.success) {
102
+ const trimmedOutput = result.output
103
+ const message =
104
+ `Knowledgebase sync failed (exit code ${result.exitCode}): ${trimmedOutput}`
105
+ logWarn(`[knowledgebase] ${message}`)
106
+ return { action: 'failed', message }
107
+ }
108
+
109
+ // Parse the human-readable output for chunk counts.
110
+ // The CLI outputs: "[knowledgebase] Indexed X new, updated Y, skipped Z chunks from project <name>"
111
+ let chunksIndexed = 0
112
+ let chunksSkipped = 0
113
+ const outputLine = result.output
114
+ const match = outputLine.match(
115
+ /Indexed (\d+) new, updated (\d+), skipped (\d+) chunks/,
116
+ )
117
+ if (match) {
118
+ // "indexed" = new chunks + updated chunks (both were processed)
119
+ chunksIndexed = parseInt(match[1], 10) + parseInt(match[2], 10)
120
+ chunksSkipped = parseInt(match[3], 10)
121
+ }
122
+
123
+ const message =
124
+ `Registered "${projectId}" \u2014 ${chunksIndexed} chunks indexed, ` +
125
+ `${chunksSkipped} skipped`
126
+ logInfo(`[knowledgebase] ${UNICODE_CHARS.CHECK} ${message}`)
127
+
128
+ return {
129
+ action: 'indexed',
130
+ message,
131
+ chunks_indexed: chunksIndexed,
132
+ chunks_skipped: chunksSkipped,
133
+ }
134
+ } catch (err) {
135
+ // Catch spawn-level errors (ENOENT when script doesn't exist, etc.)
136
+ const message = `knowledgebase-cli.js not available: ${err.message}`
137
+ logWarn(
138
+ `[knowledgebase] Skipping Phase 6 \u2014 ${message}`,
139
+ )
140
+ return { action: 'skipped', message }
141
+ }
142
+ }
143
+
144
+ // ── Internal helpers ───────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Spawns a Node.js script as a child process and waits for completion.
148
+ *
149
+ * Matches the `spawnScript` pattern from `sync-phase.js`: uses
150
+ * `process.execPath` so the child runs the same Node binary as the parent,
151
+ * streams output in real-time when `verbose` is `true`, and captures
152
+ * interleaved stdout + stderr when not.
153
+ *
154
+ * @param {string} name - Display name for the script.
155
+ * @param {string} scriptPath - Absolute path to the script to execute.
156
+ * @param {string} cwd - Working directory for the child process.
157
+ * @param {string[]} args - CLI arguments forwarded to the script.
158
+ * @param {boolean} [verbose=false] - Stream output in real-time when `true`.
159
+ * @returns {Promise<{
160
+ * script: string,
161
+ * success: boolean,
162
+ * exitCode: number,
163
+ * output: string
164
+ * }>}
165
+ */
166
+ function spawnScript(name, scriptPath, cwd, args, verbose = false) {
167
+ return new Promise((resolve) => {
168
+ const child = spawn(process.execPath, [scriptPath, ...args], {
169
+ cwd,
170
+ env: { ...process.env, INIT_CWD: cwd },
171
+ stdio: verbose ? 'inherit' : 'pipe',
172
+ })
173
+
174
+ let output = ''
175
+
176
+ if (!verbose) {
177
+ child.stdout.on('data', (data) => {
178
+ output += data.toString()
179
+ })
180
+
181
+ child.stderr.on('data', (data) => {
182
+ output += data.toString()
183
+ })
184
+ }
185
+
186
+ child.on('close', (exitCode) => {
187
+ resolve({
188
+ script: name,
189
+ success: exitCode === 0,
190
+ exitCode: exitCode ?? -1,
191
+ output: verbose ? '(streamed to terminal)' : output.trim(),
192
+ })
193
+ })
194
+
195
+ child.on('error', (err) => {
196
+ resolve({
197
+ script: name,
198
+ success: false,
199
+ exitCode: -1,
200
+ output: `Failed to spawn child process: ${err.message}`,
201
+ })
202
+ })
203
+ })
204
+ }