yaml-flow 5.2.5 → 5.2.8

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 (84) hide show
  1. package/README.md +6 -6
  2. package/board-livecards-server-runtime.js +260 -35
  3. package/browser/board-livegraph-engine.js +57 -32
  4. package/browser/board-livegraph-engine.js.map +1 -1
  5. package/browser/card-compute.js +17 -17
  6. package/browser/live-cards.js +139 -12
  7. package/browser/live-cards.schema.json +14 -9
  8. package/dist/board-livegraph-runtime/index.cjs +57 -32
  9. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  10. package/dist/board-livegraph-runtime/index.d.cts +1 -1
  11. package/dist/board-livegraph-runtime/index.d.ts +1 -1
  12. package/dist/board-livegraph-runtime/index.js +57 -32
  13. package/dist/board-livegraph-runtime/index.js.map +1 -1
  14. package/dist/card-compute/index.cjs +96 -38
  15. package/dist/card-compute/index.cjs.map +1 -1
  16. package/dist/card-compute/index.d.cts +13 -8
  17. package/dist/card-compute/index.d.ts +13 -8
  18. package/dist/card-compute/index.js +96 -38
  19. package/dist/card-compute/index.js.map +1 -1
  20. package/dist/cli/board-live-cards-cli.cjs +7200 -201
  21. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  22. package/dist/cli/board-live-cards-cli.d.cts +6 -6
  23. package/dist/cli/board-live-cards-cli.d.ts +6 -6
  24. package/dist/cli/board-live-cards-cli.js +7199 -201
  25. package/dist/cli/board-live-cards-cli.js.map +1 -1
  26. package/dist/continuous-event-graph/index.cjs +55 -30
  27. package/dist/continuous-event-graph/index.cjs.map +1 -1
  28. package/dist/continuous-event-graph/index.d.cts +2 -2
  29. package/dist/continuous-event-graph/index.d.ts +2 -2
  30. package/dist/continuous-event-graph/index.js +55 -30
  31. package/dist/continuous-event-graph/index.js.map +1 -1
  32. package/dist/index.cjs +121 -53
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +121 -53
  37. package/dist/index.js.map +1 -1
  38. package/dist/{live-cards-bridge-CeNxiVcm.d.ts → live-cards-bridge-EQjytzI_.d.ts} +10 -5
  39. package/dist/{live-cards-bridge-z_rJCSbi.d.cts → live-cards-bridge-x5XREkXm.d.cts} +10 -5
  40. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
  41. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +1 -1
  42. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +1 -1
  43. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
  44. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +2 -2
  45. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +1 -1
  46. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +10 -10
  47. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  48. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
  49. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  50. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +2 -2
  51. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
  52. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
  53. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
  54. package/examples/example-board/agent-instructions-cardlayout.md +1 -1
  55. package/examples/example-board/agent-instructions.md +271 -45
  56. package/examples/example-board/cards/card-concentration.json +8 -5
  57. package/examples/example-board/cards/card-market-prices.json +14 -9
  58. package/examples/example-board/cards/card-my-identity.json +28 -0
  59. package/examples/example-board/cards/card-portfolio-value.json +1 -1
  60. package/examples/example-board/cards/card-portfolio.json +1 -1
  61. package/examples/example-board/cards/card-rebalance-impact.json +65 -0
  62. package/examples/example-board/cards/card-rebalance-sim.json +57 -0
  63. package/examples/example-board/demo-chat-handler.js +2 -1
  64. package/examples/example-board/demo-server-config.json +6 -1
  65. package/examples/example-board/demo-server.js +79 -8
  66. package/examples/example-board/demo-shell-browser.html +6 -6
  67. package/examples/example-board/demo-shell-with-server.html +4 -4
  68. package/examples/example-board/demo-task-executor.js +436 -246
  69. package/examples/example-board/scripts/copilot_wrapper.bat +157 -0
  70. package/examples/example-board/scripts/copilot_wrapper_helper.ps1 +190 -0
  71. package/examples/example-board/scripts/workiq_wrapper.mjs +66 -0
  72. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +5 -5
  73. package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +3 -3
  74. package/examples/npm-libs/event-graph/research-pipeline.ts +5 -5
  75. package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +9 -9
  76. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  77. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
  78. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  79. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
  80. package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
  81. package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
  82. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
  83. package/package.json +2 -2
  84. package/schema/live-cards.schema.json +14 -9
package/README.md CHANGED
@@ -263,14 +263,14 @@ settings:
263
263
 
264
264
  tasks:
265
265
  fetch_sources:
266
- provides: [raw-sources]
266
+ provides: [raw-asources]
267
267
 
268
268
  analyse_sentiment:
269
- requires: [raw-sources]
269
+ requires: [raw-asources]
270
270
  provides: [sentiment-result]
271
271
 
272
272
  analyse_entities:
273
- requires: [raw-sources]
273
+ requires: [raw-asources]
274
274
  provides: [entity-result]
275
275
 
276
276
  merge_analysis:
@@ -384,7 +384,7 @@ When multiple eligible tasks produce the same output token, only one should run
384
384
 
385
385
  ### Pattern: AI Agent Tool Orchestration (Event Graph)
386
386
 
387
- An agent needs to gather evidence from multiple sources, then synthesize.
387
+ An agent needs to gather evidence from multiple source_defs, then synthesize.
388
388
 
389
389
  ```yaml
390
390
  settings:
@@ -735,7 +735,7 @@ Templates first (expands references), then variables (fills in `${...}` placehol
735
735
  ```typescript
736
736
  import { resolveConfigTemplates, resolveVariables } from 'yaml-flow/config';
737
737
 
738
- const raw = loadYaml('pipeline.yaml'); // has configTemplates + ${VAR} refs
738
+ const raw = loadYaml('pipeline.yaml'); // has configTemplates + ${VAR} projections
739
739
  const resolved = resolveVariables(
740
740
  resolveConfigTemplates(raw),
741
741
  { ENTITY_ID: 'url-42', TOOLS_DIR: '/opt/tools' },
@@ -971,7 +971,7 @@ const r3 = validateLiveCardSchema(config);
971
971
  |---|---|---|
972
972
  | `validateGraphSchema` | `schema/event-graph.schema.json` | Tasks, settings, refreshStrategy, retry, circuit_breaker, inference hints |
973
973
  | `validateFlowSchema` | `schema/flow.schema.json` | Steps, transitions, retry, terminal states |
974
- | `validateLiveCardSchema` | `schema/live-cards.schema.json` | Cards, sources, elements, compute, data bindings |
974
+ | `validateLiveCardSchema` | `schema/live-cards.schema.json` | Cards, source_defs, elements, compute, data bindings |
975
975
 
976
976
  All validators are synchronous, pure functions. They return `{ ok: boolean, errors?: ErrorObject[] }`.
977
977
 
@@ -38,6 +38,22 @@ function parseUrl(urlString) {
38
38
  return new URL(urlString, 'http://localhost');
39
39
  }
40
40
 
41
+ /**
42
+ * Merges `extraFields` into the `extra` object inside a `.task-executor` JSON file.
43
+ * No-op if the file doesn't exist or isn't valid JSON.
44
+ */
45
+ function refreshTaskExecutorExtra(runtimeDir, extraFields) {
46
+ const taskExecutorFile = path.join(runtimeDir, '.task-executor');
47
+ if (!fs.existsSync(taskExecutorFile)) return;
48
+ try {
49
+ const current = JSON.parse(fs.readFileSync(taskExecutorFile, 'utf-8'));
50
+ const merged = { ...current, extra: { ...(current.extra || {}), ...extraFields } };
51
+ fs.writeFileSync(taskExecutorFile, JSON.stringify(merged, null, 2), 'utf-8');
52
+ } catch {
53
+ // Silently ignore — board will still function, extra is best-effort
54
+ }
55
+ }
56
+
41
57
  export function createRuntimeRequestDispatcher(runtime) {
42
58
  if (!runtime || typeof runtime !== 'object') {
43
59
  throw new Error('runtime is required');
@@ -147,6 +163,18 @@ export function createMultiBoardServerRuntime(options = {}) {
147
163
  const defaultInferenceAdapterPath = typeof entry.inferenceAdapterPath === 'string'
148
164
  ? entry.inferenceAdapterPath
149
165
  : options.defaultInferenceAdapterPath;
166
+ const gandalfCardsDir = typeof entry.gandalfCardsDir === 'string'
167
+ ? entry.gandalfCardsDir
168
+ : (options.defaultGandalfCardsDir || null);
169
+ const gandalfTaskExecutorPath = typeof entry.gandalfTaskExecutorPath === 'string'
170
+ ? entry.gandalfTaskExecutorPath
171
+ : (options.defaultGandalfTaskExecutorPath || null);
172
+ const gandalfChatHandlerPath = typeof entry.gandalfChatHandlerPath === 'string'
173
+ ? entry.gandalfChatHandlerPath
174
+ : (options.defaultGandalfChatHandlerPath || null);
175
+ const gandalfInferenceAdapterPath = typeof entry.gandalfInferenceAdapterPath === 'string'
176
+ ? entry.gandalfInferenceAdapterPath
177
+ : (options.defaultGandalfInferenceAdapterPath || null);
150
178
 
151
179
  const service = createExampleBoardServerRuntime({
152
180
  apiBasePath: `${apiBasePath}/${boardId}`,
@@ -160,7 +188,14 @@ export function createMultiBoardServerRuntime(options = {}) {
160
188
  defaultStepMachineCliPath,
161
189
  defaultChatHandlerPath,
162
190
  defaultInferenceAdapterPath,
191
+ gandalfCardsDir,
192
+ gandalfRuntimeDir: path.join(boardRoot, 'gandalf-runtime'),
193
+ gandalfRuntimeOutDir: path.join(boardRoot, 'gandalf-runtime-out'),
194
+ gandalfTaskExecutorPath,
195
+ gandalfChatHandlerPath,
196
+ gandalfInferenceAdapterPath,
163
197
  boardLiveCardsCliJs: options.boardLiveCardsCliJs,
198
+ serverUrl: options.serverUrl || null,
164
199
  });
165
200
 
166
201
  boardServiceCache.set(boardId, service);
@@ -346,6 +381,49 @@ export function createExampleBoardServerRuntime(options = {}) {
346
381
  const boardFile = path.join(boardDir, 'board-graph.json');
347
382
  const inventoryFile = path.join(boardDir, 'cards-inventory.jsonl');
348
383
 
384
+ // Board-cards: parallel runtime dirs for the board-manager board.
385
+ const gandalfCardsDir = options.gandalfCardsDir ? path.resolve(options.gandalfCardsDir) : null;
386
+ const gandalfRuntimeDir = path.resolve(options.gandalfRuntimeDir || path.join(path.dirname(boardDir), 'gandalf-runtime'));
387
+ const gandalfRuntimeOutDir = path.resolve(options.gandalfRuntimeOutDir || path.join(path.dirname(boardDir), 'gandalf-runtime-out'));
388
+ const tmpGandalfCardsDir = path.join(tmpSurfaceDir, 'tmp-gandalf-cards');
389
+ const gandalfInventoryFile = path.join(gandalfRuntimeDir, 'cards-inventory.jsonl');
390
+ const gandalfBoardFile = path.join(gandalfRuntimeDir, 'board-graph.json');
391
+ const gandalfStatusSnapshotFile = path.join(gandalfRuntimeOutDir, 'board-livegraph-status.json');
392
+
393
+ // Explicit gandalf-card executor paths — no fallback to regular-card paths.
394
+ const configuredGandalfTaskExecutorPath = typeof options.gandalfTaskExecutorPath === 'string' && options.gandalfTaskExecutorPath.trim()
395
+ ? (path.isAbsolute(options.gandalfTaskExecutorPath) ? options.gandalfTaskExecutorPath : path.resolve(process.cwd(), options.gandalfTaskExecutorPath))
396
+ : null;
397
+ const configuredGandalfChatHandlerPath = typeof options.gandalfChatHandlerPath === 'string' && options.gandalfChatHandlerPath.trim()
398
+ ? (path.isAbsolute(options.gandalfChatHandlerPath) ? options.gandalfChatHandlerPath : path.resolve(process.cwd(), options.gandalfChatHandlerPath))
399
+ : null;
400
+ const configuredGandalfInferenceAdapterPath = typeof options.gandalfInferenceAdapterPath === 'string' && options.gandalfInferenceAdapterPath.trim()
401
+ ? (path.isAbsolute(options.gandalfInferenceAdapterPath) ? options.gandalfInferenceAdapterPath : path.resolve(process.cwd(), options.gandalfInferenceAdapterPath))
402
+ : null;
403
+
404
+ // Server URL passed down from the hosting server (e.g. demo-server) so executors/handlers
405
+ // can call back to server-side proxy endpoints (e.g. /api/workiq/ask).
406
+ const serverUrl = typeof options.serverUrl === 'string' && options.serverUrl.trim()
407
+ ? options.serverUrl.trim().replace(/\/$/, '')
408
+ : null;
409
+
410
+ // Board-card ID cache: O(1) lookup, mtime-refreshed each SSE tick.
411
+ let _gandalfCardIds = new Set();
412
+ let _gandalfInventoryMtime = 0;
413
+ function _refreshGandalfCardCache() {
414
+ if (!fs.existsSync(gandalfInventoryFile)) { _gandalfCardIds = new Set(); return; }
415
+ const mtime = fs.statSync(gandalfInventoryFile).mtimeMs;
416
+ if (mtime === _gandalfInventoryMtime) return;
417
+ _gandalfInventoryMtime = mtime;
418
+ _gandalfCardIds = new Set(
419
+ fs.readFileSync(gandalfInventoryFile, 'utf-8')
420
+ .split('\n').filter(Boolean)
421
+ .map(l => { try { return JSON.parse(l).cardId; } catch { return null; } })
422
+ .filter(Boolean)
423
+ );
424
+ }
425
+ function isGandalfCard(cardId) { return _gandalfCardIds.has(cardId); }
426
+
349
427
  let didDemoSetup = false;
350
428
 
351
429
  function resolveCliJsPath() {
@@ -380,7 +458,8 @@ export function createExampleBoardServerRuntime(options = {}) {
380
458
 
381
459
  function ensureCardStorageDirs(cardId) {
382
460
  const safeCardId = String(cardId || '').replace(/[^a-zA-Z0-9_-]/g, '_') || 'unknown-card';
383
- const cardDir = path.join(tmpCardsDir, safeCardId);
461
+ const baseDir = isGandalfCard(cardId) ? tmpGandalfCardsDir : tmpCardsDir;
462
+ const cardDir = path.join(baseDir, safeCardId);
384
463
  const filesDir = path.join(cardDir, 'files');
385
464
  const chatsDir = path.join(cardDir, 'chats');
386
465
  fs.mkdirSync(filesDir, { recursive: true });
@@ -491,41 +570,60 @@ export function createExampleBoardServerRuntime(options = {}) {
491
570
  .map((l) => JSON.parse(l));
492
571
  }
493
572
 
573
+ function readGandalfInventory() {
574
+ if (!fs.existsSync(gandalfInventoryFile)) return [];
575
+ return fs
576
+ .readFileSync(gandalfInventoryFile, 'utf-8')
577
+ .split('\n')
578
+ .map((l) => l.trim())
579
+ .filter(Boolean)
580
+ .map((l) => JSON.parse(l));
581
+ }
582
+
494
583
  function readStatusSnapshot() {
495
- if (!fs.existsSync(statusSnapshotFile)) return null;
496
- return readJson(statusSnapshotFile);
584
+ const base = fs.existsSync(statusSnapshotFile) ? readJson(statusSnapshotFile) : null;
585
+ const boardSnap = fs.existsSync(gandalfStatusSnapshotFile) ? readJson(gandalfStatusSnapshotFile) : null;
586
+ if (!base && !boardSnap) return null;
587
+ if (!boardSnap) return base;
588
+ if (!base) return boardSnap;
589
+ return { ...base, tasks: { ...(base.tasks || {}), ...(boardSnap.tasks || {}) } };
497
590
  }
498
591
 
499
592
  function readCardDefinitions() {
500
- const inv = readInventory();
501
- const out = [];
502
- for (const entry of inv) {
503
- if (!entry || !entry.cardId || !entry.cardFilePath) continue;
504
- if (!fs.existsSync(entry.cardFilePath)) continue;
505
- out.push(readJson(entry.cardFilePath));
593
+ function readFromInventory(invFile) {
594
+ if (!fs.existsSync(invFile)) return [];
595
+ const inv = fs.readFileSync(invFile, 'utf-8').split('\n').map(l => l.trim()).filter(Boolean).map(l => JSON.parse(l));
596
+ const out = [];
597
+ for (const entry of inv) {
598
+ if (!entry || !entry.cardId || !entry.cardFilePath) continue;
599
+ if (!fs.existsSync(entry.cardFilePath)) continue;
600
+ out.push(readJson(entry.cardFilePath));
601
+ }
602
+ return out;
506
603
  }
507
- return out;
604
+ return [...readFromInventory(inventoryFile), ...readFromInventory(gandalfInventoryFile)];
508
605
  }
509
606
 
510
607
  function readCardRuntimeArtifacts() {
511
- const cardsOutDir = path.join(runtimeOutDir, 'cards');
512
- if (!fs.existsSync(cardsOutDir)) return {};
513
-
514
- const out = {};
515
- for (const entry of fs.readdirSync(cardsOutDir, { withFileTypes: true })) {
516
- if (!entry.isFile()) continue;
517
- if (!entry.name.endsWith('.computed.json')) continue;
518
- const cardId = entry.name.slice(0, -'.computed.json'.length);
519
- out[cardId] = readJson(path.join(cardsOutDir, entry.name));
608
+ function readFromDir(dir) {
609
+ const cardsOutDir = path.join(dir, 'cards');
610
+ if (!fs.existsSync(cardsOutDir)) return {};
611
+ const out = {};
612
+ for (const entry of fs.readdirSync(cardsOutDir, { withFileTypes: true })) {
613
+ if (!entry.isFile() || !entry.name.endsWith('.computed.json')) continue;
614
+ const cardId = entry.name.slice(0, -'.computed.json'.length);
615
+ out[cardId] = readJson(path.join(cardsOutDir, entry.name));
616
+ }
617
+ return out;
520
618
  }
521
- return out;
619
+ return { ...readFromDir(runtimeOutDir), ...readFromDir(gandalfRuntimeOutDir) };
522
620
  }
523
621
 
524
622
  function readSourcePayloads(cardDefinition) {
525
623
  const out = {};
526
- if (!cardDefinition || !Array.isArray(cardDefinition.sources)) return out;
624
+ if (!cardDefinition || !Array.isArray(cardDefinition.source_defs)) return out;
527
625
 
528
- for (const sourceDef of cardDefinition.sources) {
626
+ for (const sourceDef of cardDefinition.source_defs) {
529
627
  if (!sourceDef || !sourceDef.bindTo || !sourceDef.outputFile) continue;
530
628
  const filePath = path.join(boardDir, cardDefinition.id, sourceDef.outputFile);
531
629
  if (!fs.existsSync(filePath)) continue;
@@ -561,7 +659,8 @@ export function createExampleBoardServerRuntime(options = {}) {
561
659
  }
562
660
 
563
661
  function readChatSignal(cardId) {
564
- const chatsDir = path.join(tmpCardsDir, cardId, 'chats');
662
+ const baseDir = isGandalfCard(cardId) ? tmpGandalfCardsDir : tmpCardsDir;
663
+ const chatsDir = path.join(baseDir, cardId, 'chats');
565
664
  if (!fs.existsSync(chatsDir)) {
566
665
  return { count: 0, latest_mtime_ms: 0, processing: false };
567
666
  }
@@ -644,6 +743,16 @@ export function createExampleBoardServerRuntime(options = {}) {
644
743
  fs.copyFileSync(src, dst);
645
744
  }
646
745
 
746
+ // Copy gandalf-card templates if gandalfCardsDir is configured.
747
+ if (gandalfCardsDir && fs.existsSync(gandalfCardsDir)) {
748
+ fs.rmSync(tmpGandalfCardsDir, { recursive: true, force: true });
749
+ fs.mkdirSync(tmpGandalfCardsDir, { recursive: true });
750
+ for (const entry of fs.readdirSync(gandalfCardsDir, { withFileTypes: true })) {
751
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.json')) continue;
752
+ fs.copyFileSync(path.join(gandalfCardsDir, entry.name), path.join(tmpGandalfCardsDir, entry.name));
753
+ }
754
+ }
755
+
647
756
  // Concatenate agent-instructions*.md files into copilot-instructions.md at boardSetupRoot
648
757
  const boardSetupRoot = path.dirname(boardDir);
649
758
  const agentInstructionFiles = ['agent-instructions.md', 'agent-instructions-cardlayout.md'];
@@ -662,8 +771,12 @@ export function createExampleBoardServerRuntime(options = {}) {
662
771
  didDemoSetup = true;
663
772
  }
664
773
 
774
+ function isDemoSetupDone() {
775
+ return didDemoSetup && fs.existsSync(tmpCardsDir);
776
+ }
777
+
665
778
  function ensureDemoSetup() {
666
- if (didDemoSetup && fs.existsSync(tmpCardsDir)) return;
779
+ if (isDemoSetupDone()) return;
667
780
  demoPrepSetup();
668
781
  }
669
782
 
@@ -713,6 +826,69 @@ export function createExampleBoardServerRuntime(options = {}) {
713
826
  return resolved;
714
827
  }
715
828
 
829
+ function resolveGandalfTaskExecutorPath() {
830
+ if (!configuredGandalfTaskExecutorPath) return null;
831
+ if (!fs.existsSync(configuredGandalfTaskExecutorPath)) {
832
+ const err = new Error(`Gandalf task executor script not found: ${configuredGandalfTaskExecutorPath}`);
833
+ err.statusCode = 400;
834
+ throw err;
835
+ }
836
+ return configuredGandalfTaskExecutorPath;
837
+ }
838
+
839
+ function resolveGandalfChatHandlerPath() {
840
+ if (!configuredGandalfChatHandlerPath) return null;
841
+ if (!fs.existsSync(configuredGandalfChatHandlerPath)) {
842
+ const err = new Error(`Gandalf chat handler script not found: ${configuredGandalfChatHandlerPath}`);
843
+ err.statusCode = 400;
844
+ throw err;
845
+ }
846
+ return configuredGandalfChatHandlerPath;
847
+ }
848
+
849
+ function resolveGandalfInferenceAdapterPath() {
850
+ if (!configuredGandalfInferenceAdapterPath) return null;
851
+ if (!fs.existsSync(configuredGandalfInferenceAdapterPath)) {
852
+ const err = new Error(`Gandalf inference adapter script not found: ${configuredGandalfInferenceAdapterPath}`);
853
+ err.statusCode = 400;
854
+ throw err;
855
+ }
856
+ return configuredGandalfInferenceAdapterPath;
857
+ }
858
+
859
+ function initGandalfCards() {
860
+ const taskExecutorPath = resolveGandalfTaskExecutorPath();
861
+ if (!taskExecutorPath) return; // gandalf-cards not configured; skip.
862
+ fs.mkdirSync(gandalfRuntimeDir, { recursive: true });
863
+ const chatHandlerPath = resolveGandalfChatHandlerPath();
864
+ const inferenceAdapterPath = resolveGandalfInferenceAdapterPath();
865
+ const taskExecutorCmd = `${shellQuote(process.execPath)} ${shellQuote(taskExecutorPath)}`;
866
+ const chatHandlerCmd = chatHandlerPath ? `${shellQuote(process.execPath)} ${shellQuote(chatHandlerPath)}` : null;
867
+ const inferenceAdapterCmd = inferenceAdapterPath ? `${shellQuote(process.execPath)} ${shellQuote(inferenceAdapterPath)}` : null;
868
+ const boardSetupRoot = path.dirname(boardDir);
869
+ const taskExecutorExtra = JSON.stringify({
870
+ boardSetupRoot,
871
+ boardId,
872
+ boardRuntimeDir: path.relative(boardSetupRoot, gandalfRuntimeDir),
873
+ runtimeStatusDir: path.relative(boardSetupRoot, gandalfRuntimeOutDir),
874
+ cardsDir: path.relative(boardSetupRoot, tmpGandalfCardsDir),
875
+ ...(serverUrl ? { serverUrl } : {}),
876
+ });
877
+ const initArgs = ['init', gandalfRuntimeDir, '--task-executor', taskExecutorCmd, '--task-executor-extra', taskExecutorExtra];
878
+ if (chatHandlerCmd) initArgs.push('--chat-handler', chatHandlerCmd);
879
+ if (inferenceAdapterCmd) initArgs.push('--inference-adapter', inferenceAdapterCmd);
880
+ initArgs.push('--runtime-out', gandalfRuntimeOutDir);
881
+ try {
882
+ runCli(initArgs);
883
+ } catch (err) {
884
+ const msg = String((err && err.message) || err);
885
+ if (!msg.includes('no valid board-graph.json')) throw err;
886
+ clearDirContents(gandalfRuntimeDir);
887
+ fs.mkdirSync(gandalfRuntimeDir, { recursive: true });
888
+ runCli(initArgs);
889
+ }
890
+ }
891
+
716
892
  function initBoard(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
717
893
  fs.mkdirSync(boardDir, { recursive: true });
718
894
 
@@ -727,7 +903,17 @@ export function createExampleBoardServerRuntime(options = {}) {
727
903
  ? `${shellQuote(process.execPath)} ${shellQuote(inferenceAdapterPath)}`
728
904
  : null;
729
905
 
730
- const initArgs = ['init', boardDir, '--task-executor', taskExecutorCmd];
906
+ const boardSetupRoot = path.dirname(boardDir);
907
+ const taskExecutorExtra = JSON.stringify({
908
+ boardSetupRoot,
909
+ boardId,
910
+ boardRuntimeDir: path.relative(boardSetupRoot, boardDir),
911
+ runtimeStatusDir: path.relative(boardSetupRoot, runtimeOutDir),
912
+ cardsDir: path.relative(boardSetupRoot, tmpCardsDir),
913
+ ...(serverUrl ? { serverUrl } : {}),
914
+ });
915
+
916
+ const initArgs = ['init', boardDir, '--task-executor', taskExecutorCmd, '--task-executor-extra', taskExecutorExtra];
731
917
  if (chatHandlerCmd) initArgs.push('--chat-handler', chatHandlerCmd);
732
918
  if (inferenceAdapterCmd) initArgs.push('--inference-adapter', inferenceAdapterCmd);
733
919
  initArgs.push('--runtime-out', runtimeOutDir);
@@ -762,6 +948,31 @@ export function createExampleBoardServerRuntime(options = {}) {
762
948
  clearDirContents(boardDir);
763
949
  initBoard(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam);
764
950
  }
951
+
952
+ // Always refresh the extra in .task-executor so serverUrl and other runtime fields stay current
953
+ // even when initBoard is skipped (board already initialized from a previous run).
954
+ refreshTaskExecutorExtra(boardDir, {
955
+ boardSetupRoot: path.dirname(boardDir),
956
+ boardId,
957
+ boardRuntimeDir: path.relative(path.dirname(boardDir), boardDir),
958
+ runtimeStatusDir: path.relative(path.dirname(boardDir), runtimeOutDir),
959
+ cardsDir: path.relative(path.dirname(boardDir), tmpCardsDir),
960
+ ...(serverUrl ? { serverUrl } : {}),
961
+ });
962
+
963
+ // Board-cards runtime: init if configured but not yet initialized.
964
+ if (resolveGandalfTaskExecutorPath() && !fs.existsSync(gandalfBoardFile)) {
965
+ initGandalfCards();
966
+ }
967
+ }
968
+
969
+ function bootstrapGandalfCards() {
970
+ if (!fs.existsSync(tmpGandalfCardsDir)) return;
971
+ if (!fs.existsSync(gandalfBoardFile)) return; // runtime not initialized; initBoardAndSetup handles it
972
+ const jsonFiles = (fs.readdirSync(tmpGandalfCardsDir)).filter(f => f.endsWith('.json'));
973
+ if (!jsonFiles.length) return;
974
+ runCli(['upsert-card', '--rg', gandalfRuntimeDir, '--card-glob', path.join(tmpGandalfCardsDir, '*.json')]);
975
+ _refreshGandalfCardCache();
765
976
  }
766
977
 
767
978
  function bootstrapCards() {
@@ -772,9 +983,14 @@ export function createExampleBoardServerRuntime(options = {}) {
772
983
  function bootstrapBoard() {
773
984
  initBoardAndSetup();
774
985
  bootstrapCards();
986
+ bootstrapGandalfCards();
775
987
  }
776
988
 
777
989
  function findCardPath(cardId) {
990
+ if (isGandalfCard(cardId)) {
991
+ const found = readGandalfInventory().find((e) => e.cardId === cardId);
992
+ return found ? found.cardFilePath : null;
993
+ }
778
994
  const inv = readInventory();
779
995
  const found = inv.find((e) => e.cardId === cardId);
780
996
  return found ? found.cardFilePath : null;
@@ -795,7 +1011,8 @@ export function createExampleBoardServerRuntime(options = {}) {
795
1011
  fs.writeFileSync(cardPath, JSON.stringify(nextCard, null, 2));
796
1012
 
797
1013
  if (syncBoard) {
798
- runCli(['upsert-card', '--rg', boardDir, '--card', cardPath, '--restart']);
1014
+ const rg = isGandalfCard(cardId) ? gandalfRuntimeDir : boardDir;
1015
+ runCli(['upsert-card', '--rg', rg, '--card', cardPath, '--restart']);
799
1016
  }
800
1017
  }
801
1018
 
@@ -992,18 +1209,20 @@ export function createExampleBoardServerRuntime(options = {}) {
992
1209
  }
993
1210
 
994
1211
  // Fire-and-forget invocation of .chat-handler after a user chat message is persisted.
995
- // boardDir/.chat-handler must contain the handler command as a single-line string.
1212
+ // The handler file lives in the appropriate runtime dir (.chat-handler).
996
1213
  // Called with: --boardId <id> --cardId <id> --extraEncJson <base64json>
997
1214
  // extraEncJson decodes to:
998
1215
  // boardSetupRoot — absolute path to board root (parent of runtime/, surface/, runtime-out/)
999
- // boardRuntimeDir — relative: 'runtime'
1216
+ // boardRuntimeDir — relative: 'runtime' (or 'gandalf-runtime' for gandalf cards)
1000
1217
  // runtimeStatusDir— relative: 'runtime-out'
1001
- // cardsDir — relative: 'surface/tmp-cards'
1218
+ // cardsDir — relative: 'surface/tmp-cards' (or 'surface/tmp-gandalf-cards')
1002
1219
  // chatDir — relative (from cardsDir): e.g. 'card-portfolio/chats'
1003
1220
  // lastChatFile — filename of the just-written user message, e.g. '001_user.txt'
1004
1221
  // Handler failures are logged and silently ignored — chat-send response is never affected.
1005
1222
  function invokeChatHandler(cardId, chatsDir, lastChatFile) {
1006
- const handlerFile = path.join(boardDir, '.chat-handler');
1223
+ const isGandalf = isGandalfCard(cardId);
1224
+ const runtimeDir = isGandalf ? gandalfRuntimeDir : boardDir;
1225
+ const handlerFile = path.join(runtimeDir, '.chat-handler');
1007
1226
  if (!fs.existsSync(handlerFile)) return;
1008
1227
  const handlerCmd = fs.readFileSync(handlerFile, 'utf-8').trim();
1009
1228
  if (!handlerCmd) return;
@@ -1012,11 +1231,12 @@ export function createExampleBoardServerRuntime(options = {}) {
1012
1231
  try { fs.mkdirSync(chatsDir, { recursive: true }); fs.writeFileSync(processingFile, '', 'utf-8'); } catch {}
1013
1232
  const extra = Buffer.from(JSON.stringify({
1014
1233
  boardSetupRoot,
1015
- boardRuntimeDir: path.relative(boardSetupRoot, boardDir),
1016
- runtimeStatusDir: path.relative(boardSetupRoot, runtimeOutDir),
1017
- cardsDir: path.relative(boardSetupRoot, tmpCardsDir),
1234
+ boardRuntimeDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeDir : boardDir),
1235
+ runtimeStatusDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeOutDir : runtimeOutDir),
1236
+ cardsDir: path.relative(boardSetupRoot, isGandalf ? tmpGandalfCardsDir : tmpCardsDir),
1018
1237
  chatDir: chatsDir,
1019
1238
  lastChatFile,
1239
+ ...(serverUrl ? { serverUrl } : {}),
1020
1240
  })).toString('base64');
1021
1241
  try {
1022
1242
  const proc = spawn(handlerCmd, [
@@ -1180,6 +1400,10 @@ export function createExampleBoardServerRuntime(options = {}) {
1180
1400
  const poll = setInterval(() => {
1181
1401
  try {
1182
1402
  runCli(['process-accumulated-events', '--rg', boardDir]);
1403
+ if (fs.existsSync(gandalfBoardFile)) {
1404
+ runCli(['process-accumulated-events', '--rg', gandalfRuntimeDir]);
1405
+ }
1406
+ _refreshGandalfCardCache();
1183
1407
 
1184
1408
  const nextPayload = buildPublishedRuntimePayload();
1185
1409
  const nextHash = stablePayloadString(nextPayload);
@@ -1318,8 +1542,8 @@ export function createExampleBoardServerRuntime(options = {}) {
1318
1542
  const idx = parseInt(cardFileDownloadMatch[2], 10);
1319
1543
  const expectedStoredName = url.searchParams.get('sn');
1320
1544
 
1321
- const cardPath = path.join(tmpCardsDir, `${cardId}.json`);
1322
- if (!fs.existsSync(cardPath)) {
1545
+ const cardPath = findCardPath(cardId);
1546
+ if (!cardPath || !fs.existsSync(cardPath)) {
1323
1547
  json(res, 404, { error: 'Card not found' });
1324
1548
  return true;
1325
1549
  }
@@ -1395,6 +1619,7 @@ export function createExampleBoardServerRuntime(options = {}) {
1395
1619
  runCli,
1396
1620
  demoPrepSetup,
1397
1621
  ensureDemoSetup,
1622
+ isDemoSetupDone,
1398
1623
  buildPublishedRuntimePayload,
1399
1624
  handleRuntimeApi,
1400
1625
  clearChatRecords,