knoxis-helper 1.8.6 → 1.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.
@@ -46,7 +46,52 @@ const ALLOWED_ORIGINS = (process.env.KNOXIS_ALLOWED_ORIGINS || '')
46
46
  .map(origin => origin.trim())
47
47
  .filter(Boolean);
48
48
  // Merge custom origins with trusted ones
49
- const ALL_ALLOWED_ORIGINS = [...new Set([...TRUSTED_ORIGINS, ...ALLOWED_ORIGINS])];
49
+ const ALL_ALLOWED_ORIGINS = Array.from(new Set([...TRUSTED_ORIGINS, ...ALLOWED_ORIGINS]));
50
+
51
+ /**
52
+ * Collect Maker-related Markdown documents for persistence and syncing.
53
+ * These include backlog, PRD, glossary, product, and strategy files.
54
+ */
55
+ function collectMakerDocs(workspaceDir) {
56
+ try {
57
+ if (!workspaceDir || !fs.existsSync(workspaceDir)) return [];
58
+ const makerFiles = [];
59
+ const candidates = [
60
+ 'BACKLOG.md',
61
+ 'GLOSSARY.md',
62
+ 'PRD.md',
63
+ 'PRODUCT.md',
64
+ 'STRATEGY.md'
65
+ ];
66
+ candidates.forEach(name => {
67
+ const full = path.join(workspaceDir, name);
68
+ if (fs.existsSync(full)) makerFiles.push(full);
69
+ });
70
+ const docsDir = path.join(workspaceDir, 'docs');
71
+ if (fs.existsSync(docsDir)) {
72
+ fs.readdirSync(docsDir).forEach(f => {
73
+ if (f.endsWith('.md')) makerFiles.push(path.join(docsDir, f));
74
+ });
75
+ }
76
+ const mvpDir = path.join(workspaceDir, '.mvp');
77
+ if (fs.existsSync(mvpDir)) {
78
+ const walk = dir => {
79
+ fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => {
80
+ const res = path.join(dir, entry.name);
81
+ if (entry.isDirectory()) walk(res);
82
+ else if (entry.name.endsWith('.md')) makerFiles.push(res);
83
+ });
84
+ };
85
+ walk(mvpDir);
86
+ }
87
+ console.log(`📁 Maker-aware discovery found ${makerFiles.length} planning docs.`);
88
+ return makerFiles;
89
+ } catch (err) {
90
+ console.warn('⚠️ collectMakerDocs failed:', err.message);
91
+ return [];
92
+ }
93
+ }
94
+
50
95
 
51
96
  const serverMeta = { secure: false, port: DEFAULT_PORT };
52
97
 
@@ -580,55 +625,6 @@ function resolveInteractiveScript() {
580
625
  return null;
581
626
  }
582
627
 
583
- // Resolve the kit-aware pair-program runner (used when caller specifies --mode)
584
- function resolvePairProgramScript() {
585
- const candidates = [
586
- path.join(__dirname, 'knoxis-pair-program.js'),
587
- path.join(__dirname, '..', 'knoxis-pair-program.js'),
588
- path.join(os.homedir(), '.knoxis', 'agent', 'knoxis-pair-program.js'),
589
- path.join(__dirname, '..', '..', 'knoxis-pair-program.js'),
590
- ];
591
- for (const candidate of candidates) {
592
- if (fs.existsSync(candidate)) return candidate;
593
- }
594
- return null;
595
- }
596
-
597
- const KIT_MODES = new Set(['kickoff', 'feature-kickoff', 'resume', 'session-end', 'recovery']);
598
- // Every kit mode rewrites files on disk — STATUS / HANDOFF / DECISIONS for
599
- // session-end, the docs/state/* + docs/features/<slug>/* bag for kickoff /
600
- // feature-kickoff / resume, and recovery's triage notes. The interactive
601
- // runner invokes `claude -p --session-id`, which gives Claude proper tool
602
- // access; the older pair-program.js pipes to `claude` without -p and silently
603
- // skips the file writes. Route every kit mode through the interactive runner
604
- // so their kit prompts actually do their work.
605
- const KIT_MODES_VIA_INTERACTIVE = new Set(['kickoff', 'feature-kickoff', 'resume', 'session-end', 'recovery']);
606
-
607
- function buildPairProgramCommand({ scriptPath, workspace, mode, archetype, pattern, productSlug, projectSlug, taskPrompt, userId, workspaceId, taskIds }) {
608
- const q = v => `"${escapeForDoubleQuotedShellArg(v)}"`;
609
- // Mode/archetype/pattern are constrained to known short tokens — no need to quote.
610
- // Slugs and paths can contain spaces or special chars — always quote.
611
- const parts = [`node ${q(scriptPath)}`, `--workspace ${q(workspace)}`];
612
- if (mode) parts.push(`--mode ${mode}`);
613
- if (archetype) parts.push(`--archetype ${archetype}`);
614
- if (pattern) parts.push(`--pattern ${pattern}`);
615
- if (productSlug) parts.push(`--product-slug ${q(productSlug)}`);
616
- if (projectSlug) parts.push(`--project-slug ${q(projectSlug)}`);
617
- if (userId) parts.push(`--user-id ${q(userId)}`);
618
- if (workspaceId) parts.push(`--workspace-id ${q(workspaceId)}`);
619
- if (Array.isArray(taskIds)) {
620
- for (const t of taskIds) {
621
- if (t) parts.push(`--task-id ${q(t)}`);
622
- }
623
- }
624
- if (taskPrompt && taskPrompt.length) {
625
- // Use base64 to avoid shell-quoting hazards (newlines, quotes, backticks).
626
- const b64 = Buffer.from(taskPrompt, 'utf8').toString('base64');
627
- parts.push(`--prompt-base64 ${b64}`);
628
- }
629
- return parts.join(' ');
630
- }
631
-
632
628
  // Request handler
633
629
  async function handleRequest(req, res) {
634
630
  const parsedUrl = url.parse(req.url, true);
@@ -685,7 +681,14 @@ async function handleRequest(req, res) {
685
681
  if (body.claudeMdContent && workspace && fs.existsSync(workspace)) {
686
682
  try {
687
683
  fs.writeFileSync(path.join(workspace, 'CLAUDE.md'), body.claudeMdContent, 'utf8');
688
- console.log('📄 Wrote CLAUDE.md (' + body.claudeMdContent.length + ' chars)');
684
+ console.log('📄 Wrote CLAUDE.md (' + body.claudeMdContent.length + ' chars)');
685
+ // Maker-aware discovery integration
686
+ try {
687
+ const makerDocs = collectMakerDocs(workspace);
688
+ console.log(` Maker doc sync: ${makerDocs.length} files found.`);
689
+ } catch (err) {
690
+ console.warn('⚠️ Maker doc sync failed:', err.message);
691
+ }
689
692
  } catch (writeErr) {
690
693
  console.warn('⚠️ Failed to write CLAUDE.md:', writeErr.message);
691
694
  }
@@ -916,22 +919,8 @@ async function handleRequest(req, res) {
916
919
  const body = await parseBody(req);
917
920
  const { workspace, task, file, provider, headless, sessionId, claudeMdContent } = body;
918
921
  const interactive = body.interactive === true || body.interactive === 'true';
919
- const kitMode = typeof body.mode === 'string' && KIT_MODES.has(body.mode) ? body.mode : null;
920
- const archetype = typeof body.archetype === 'string' ? body.archetype : null;
921
- const pattern = typeof body.pattern === 'string' ? body.pattern : null;
922
- const productSlug = typeof body.productSlug === 'string' ? body.productSlug : null;
923
- const projectSlug = typeof body.projectSlug === 'string' ? body.projectSlug : null;
924
- // QIG portal linkage — let session records get tied back to the user / workspace / tasks.
925
- const portalUserId = typeof body.userId === 'string' ? body.userId : null;
926
- const portalWorkspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : null;
927
- const portalTaskIds = Array.isArray(body.taskIds)
928
- ? body.taskIds.filter(t => typeof t === 'string' && t)
929
- : (typeof body.taskId === 'string' ? [body.taskId] : []);
930
-
931
- // Kickoff and session-end modes don't strictly need a task — the kit prompt
932
- // drives the interaction. For all other paths a task is still required.
933
- const taskOptionalForMode = kitMode === 'kickoff' || kitMode === 'session-end';
934
- if (!task && !taskOptionalForMode) {
922
+
923
+ if (!task) {
935
924
  return sendJSON(res, 400, { success: false, error: 'Task description required' }, requestOrigin);
936
925
  }
937
926
 
@@ -953,80 +942,32 @@ async function handleRequest(req, res) {
953
942
  try {
954
943
  fs.writeFileSync(path.join(workspaceDir, 'CLAUDE.md'), effectiveClaudeMd, 'utf8');
955
944
  console.log(`📄 Wrote CLAUDE.md to ${workspaceDir} (${effectiveClaudeMd.length} chars)`);
945
+ // Maker-aware discovery integration
946
+ try {
947
+ const makerDocs = collectMakerDocs(workspaceDir);
948
+ console.log(` Maker doc sync: ${makerDocs.length} files found.`);
949
+ } catch (err) {
950
+ console.warn('⚠️ Maker doc sync failed:', err.message);
951
+ }
956
952
  } catch (e) {
957
953
  console.warn(`⚠️ Failed to write CLAUDE.md: ${e.message}`);
958
954
  }
959
955
  }
960
956
 
961
- // Write the actual task to a temp file (used by single-shot and interactive paths)
957
+ // Write the actual task to a temp file
962
958
  const promptFile = path.join(os.tmpdir(), `knoxis-task-${sessionId || Date.now()}.txt`);
963
- const promptText = task ? (file ? `Working on file: ${file}\n\nTask: ${task}` : task) : '';
964
- if (promptText) fs.writeFileSync(promptFile, promptText, 'utf8');
959
+ const promptText = file ? `Working on file: ${file}\n\nTask: ${task}` : task;
960
+ fs.writeFileSync(promptFile, promptText, 'utf8');
965
961
 
966
- // Determine the command to run.
962
+ // Determine the command to run
967
963
  let command;
968
964
  let mode = 'single-shot';
969
965
 
970
- // Route through knoxis-pair-program.js whenever it's installed — gives
971
- // every task auto-scaffold, the standards systemIntro, auto-context, and
972
- // portal-sync. Kit mode adds --mode; default mode runs the standard
973
- // 4-step pipeline. Interactive (Groq) mode and the legacy claude-pipe
974
- // fallback remain available when the runner isn't present.
975
- const ppScript = resolvePairProgramScript();
976
- const kitViaInteractive = kitMode && KIT_MODES_VIA_INTERACTIVE.has(kitMode);
977
- if (kitMode && !kitViaInteractive && !ppScript) {
978
- return sendJSON(res, 500, { success: false, error: 'knoxis-pair-program.js not found — reinstall knoxis-helper.' }, requestOrigin);
979
- }
980
- if (kitViaInteractive) {
981
- const scriptPath = resolveInteractiveScript();
982
- if (!scriptPath) {
983
- return sendJSON(res, 500, { success: false, error: 'knoxis-interactive-pair.js not found — reinstall knoxis-helper.' }, requestOrigin);
984
- }
985
- const kitEnv = { KNOXIS_WORKSPACE_PATH: workspaceDir, KNOXIS_KIT_MODE: kitMode };
986
- if (promptText) kitEnv.KNOXIS_TASK_FILE = promptFile;
987
- if (archetype) kitEnv.KNOXIS_KIT_ARCHETYPE = archetype;
988
- if (pattern) kitEnv.KNOXIS_KIT_PATTERN = pattern;
989
- if (productSlug) kitEnv.KNOXIS_PRODUCT_SLUG = productSlug;
990
- if (projectSlug) kitEnv.KNOXIS_PROJECT_SLUG = projectSlug;
991
- if (portalUserId) kitEnv.KNOXIS_USER_ID = portalUserId;
992
- if (portalWorkspaceId) kitEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
993
- if (portalTaskIds && portalTaskIds.length) kitEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
994
- if (sessionId) kitEnv.KNOXIS_BACKEND_SESSION_ID = sessionId;
995
- command = buildEnvCommand(kitEnv, `node "${scriptPath}"`);
996
- mode = `kit:${kitMode}${archetype ? `/${archetype}` : ''}`;
997
- console.log(`🧰 Kit (interactive runner): ${mode} — ${scriptPath}`);
998
- } else if (kitMode || (ppScript && !interactive && promptText)) {
999
- command = buildPairProgramCommand({
1000
- scriptPath: ppScript,
1001
- workspace: workspaceDir,
1002
- mode: kitMode || null,
1003
- archetype,
1004
- pattern,
1005
- productSlug,
1006
- projectSlug,
1007
- taskPrompt: promptText,
1008
- userId: portalUserId,
1009
- workspaceId: portalWorkspaceId,
1010
- taskIds: portalTaskIds
1011
- });
1012
- mode = kitMode
1013
- ? `kit:${kitMode}${archetype ? `/${archetype}` : ''}${pattern ? `/${pattern}` : ''}`
1014
- : 'pair-program:default';
1015
- console.log(`🧰 ${kitMode ? 'Kit' : 'Pair-program (default pipeline)'}: ${mode}`);
1016
- } else if (interactive) {
966
+ if (interactive) {
1017
967
  const scriptPath = resolveInteractiveScript();
1018
968
  if (scriptPath) {
1019
- // Interactive mode: multi-turn with Groq pair programmer.
1020
- // Pass identity env vars so the script can build a schema-aligned
1021
- // session record and POST it via portal-sync.
1022
- const interactiveEnv = { KNOXIS_TASK_FILE: promptFile, KNOXIS_WORKSPACE_PATH: workspaceDir };
1023
- if (portalUserId) interactiveEnv.KNOXIS_USER_ID = portalUserId;
1024
- if (portalWorkspaceId) interactiveEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
1025
- if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
1026
- if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
1027
- if (projectSlug) interactiveEnv.KNOXIS_PROJECT_SLUG = projectSlug;
1028
- if (sessionId) interactiveEnv.KNOXIS_BACKEND_SESSION_ID = sessionId;
1029
- command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
969
+ // Interactive mode: multi-turn with Groq pair programmer
970
+ command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
1030
971
  mode = 'interactive';
1031
972
  console.log(`🤝 Interactive mode: ${scriptPath}`);
1032
973
  } else {
@@ -1035,19 +976,17 @@ async function handleRequest(req, res) {
1035
976
  command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
1036
977
  }
1037
978
  } else {
1038
- // Legacy fallback when neither pair-program nor interactive scripts are installed.
979
+ // Standard single-shot mode: pipe task to Claude
1039
980
  command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
1040
981
  }
1041
982
 
1042
983
  if (headless) {
1043
- // Anything routed through pair-program.js or knoxis-interactive-pair.js
1044
- // is a script invocation run the assembled command verbatim.
1045
- const isScriptInvocation = mode === 'interactive' || mode === 'pair-program:default' || (kitMode != null);
1046
- if (isScriptInvocation) {
984
+ if (interactive && mode === 'interactive') {
985
+ // Headless interactive: run the script directly as a process
1047
986
  const result = await runHeadlessProcess({
1048
987
  workspace: workspaceDir,
1049
988
  command,
1050
- sessionLabel: sessionId || (kitMode ? `kit-${kitMode}` : (mode === 'interactive' ? 'interactive-pair' : 'pair-program'))
989
+ sessionLabel: sessionId || 'interactive-pair'
1051
990
  });
1052
991
  return sendJSON(res, result.success ? 200 : 500, { ...result, mode }, requestOrigin);
1053
992
  }
@@ -1076,15 +1015,10 @@ async function handleRequest(req, res) {
1076
1015
 
1077
1016
  return sendJSON(res, 200, {
1078
1017
  success: true,
1079
- message: kitMode ? `Pair programming session started (kit mode: ${kitMode})`
1080
- : interactive ? 'Interactive pair programming session started'
1081
- : 'Pair programming session started',
1018
+ message: interactive ? 'Interactive pair programming session started' : 'Pair programming session started',
1082
1019
  mode,
1083
- kitMode: kitMode || null,
1084
- archetype: archetype || null,
1085
- pattern: pattern || null,
1086
1020
  workspace: workspaceDir,
1087
- task: task || null,
1021
+ task,
1088
1022
  file: file || null
1089
1023
  }, requestOrigin);
1090
1024
  } catch (error) {
@@ -1437,6 +1371,13 @@ function connectRelayWebSocket() {
1437
1371
  try {
1438
1372
  fs.writeFileSync(path.join(wsDir, 'CLAUDE.md'), msg.claudeMdContent, 'utf8');
1439
1373
  console.log(` 📄 Wrote CLAUDE.md (${msg.claudeMdContent.length} chars)`);
1374
+ // Maker-aware discovery integration
1375
+ try {
1376
+ const makerDocs = collectMakerDocs(wsDir);
1377
+ console.log(` Maker doc sync: ${makerDocs.length} files found.`);
1378
+ } catch (err) {
1379
+ console.warn('⚠️ Maker doc sync failed:', err.message);
1380
+ }
1440
1381
  } catch (writeErr) {
1441
1382
  console.warn(` ⚠️ Failed to write CLAUDE.md: ${writeErr.message}`);
1442
1383
  }
@@ -1463,83 +1404,16 @@ function connectRelayWebSocket() {
1463
1404
  }
1464
1405
 
1465
1406
  const interactive = msg.interactive === true;
1466
- const kitMode = typeof msg.mode === 'string' && KIT_MODES.has(msg.mode) ? msg.mode : null;
1467
- const archetype = typeof msg.archetype === 'string' ? msg.archetype : null;
1468
- const pattern = typeof msg.pattern === 'string' ? msg.pattern : null;
1469
- const productSlug = typeof msg.productSlug === 'string' ? msg.productSlug : null;
1470
- const projectSlug = typeof msg.projectSlug === 'string' ? msg.projectSlug : null;
1471
- const portalUserId = typeof msg.userId === 'string' ? msg.userId : null;
1472
- const portalWorkspaceId = typeof msg.workspaceId === 'string' ? msg.workspaceId : null;
1473
- const portalTaskIds = Array.isArray(msg.taskIds)
1474
- ? msg.taskIds.filter(t => typeof t === 'string' && t)
1475
- : (typeof msg.taskId === 'string' ? [msg.taskId] : []);
1476
- const ppScript = resolvePairProgramScript();
1477
- const kitViaInteractive = kitMode && KIT_MODES_VIA_INTERACTIVE.has(kitMode);
1478
- const routeViaPairProgram = !kitViaInteractive && (kitMode || (ppScript && !interactive && taskPrompt));
1479
-
1480
- if (kitViaInteractive) {
1481
- const scriptPath = resolveInteractiveScript();
1482
- if (scriptPath) {
1483
- // kickoff/resume need Claude's tool path (claude -p --session-id),
1484
- // which the interactive runner provides. Pass the kit mode + identity
1485
- // via env so the runner picks the kit branch instead of the 4-phase
1486
- // Groq flow.
1487
- if (taskPrompt) {
1488
- promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
1489
- fs.writeFileSync(promptFile, taskPrompt, 'utf8');
1490
- }
1491
- const kitEnv = { KNOXIS_WORKSPACE_PATH: wsDir, KNOXIS_KIT_MODE: kitMode };
1492
- if (promptFile) kitEnv.KNOXIS_TASK_FILE = promptFile;
1493
- if (archetype) kitEnv.KNOXIS_KIT_ARCHETYPE = archetype;
1494
- if (pattern) kitEnv.KNOXIS_KIT_PATTERN = pattern;
1495
- if (productSlug) kitEnv.KNOXIS_PRODUCT_SLUG = productSlug;
1496
- if (projectSlug) kitEnv.KNOXIS_PROJECT_SLUG = projectSlug;
1497
- if (portalUserId) kitEnv.KNOXIS_USER_ID = portalUserId;
1498
- if (portalWorkspaceId) kitEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
1499
- if (portalTaskIds && portalTaskIds.length) kitEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
1500
- const relaySessionId = typeof msg.sessionId === 'string' ? msg.sessionId : null;
1501
- if (relaySessionId) kitEnv.KNOXIS_BACKEND_SESSION_ID = relaySessionId;
1502
- command = buildEnvCommand(kitEnv, `node "${scriptPath}"`);
1503
- console.log(` 🧰 Kit (interactive runner): ${kitMode}${archetype ? `/${archetype}` : ''} — ${scriptPath}`);
1504
- } else {
1505
- console.warn(` ⚠️ knoxis-interactive-pair.js not found — cannot run kit mode ${kitMode}`);
1506
- }
1507
- } else if (routeViaPairProgram && ppScript) {
1508
- command = buildPairProgramCommand({
1509
- scriptPath: ppScript,
1510
- workspace: wsDir,
1511
- mode: kitMode || null,
1512
- archetype,
1513
- pattern,
1514
- productSlug,
1515
- projectSlug,
1516
- taskPrompt: taskPrompt || '',
1517
- userId: portalUserId,
1518
- workspaceId: portalWorkspaceId,
1519
- taskIds: portalTaskIds
1520
- });
1521
- console.log(` 🧰 ${kitMode ? `Kit mode: ${kitMode}` : 'Pair-program (default pipeline)'}${archetype ? `/${archetype}` : ''}${pattern ? `/${pattern}` : ''}`);
1522
- } else if (kitMode) {
1523
- console.warn(` ⚠️ knoxis-pair-program.js not found — cannot run kit mode`);
1524
- } else if (taskPrompt) {
1407
+
1408
+ if (taskPrompt) {
1525
1409
  promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
1526
1410
  fs.writeFileSync(promptFile, taskPrompt, 'utf8');
1527
1411
 
1528
1412
  if (interactive) {
1529
- // Interactive mode: use multi-turn pair programming script.
1530
- // Pass identity env vars so the script can build a schema-aligned
1531
- // session record and POST it via portal-sync.
1413
+ // Interactive mode: use multi-turn pair programming script
1532
1414
  const scriptPath = resolveInteractiveScript();
1533
1415
  if (scriptPath) {
1534
- const interactiveEnv = { KNOXIS_TASK_FILE: promptFile, KNOXIS_WORKSPACE_PATH: wsDir };
1535
- if (portalUserId) interactiveEnv.KNOXIS_USER_ID = portalUserId;
1536
- if (portalWorkspaceId) interactiveEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
1537
- if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
1538
- if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
1539
- if (projectSlug) interactiveEnv.KNOXIS_PROJECT_SLUG = projectSlug;
1540
- const relaySessionId = typeof msg.sessionId === 'string' ? msg.sessionId : null;
1541
- if (relaySessionId) interactiveEnv.KNOXIS_BACKEND_SESSION_ID = relaySessionId;
1542
- command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
1416
+ command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
1543
1417
  console.log(` 🤝 Interactive mode: ${scriptPath}`);
1544
1418
  } else {
1545
1419
  command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
@@ -1598,28 +1472,6 @@ function connectRelayWebSocket() {
1598
1472
  }
1599
1473
  } else if (msg.type === 'connected') {
1600
1474
  console.log(`🤝 Backend acknowledged: ${msg.message}`);
1601
- } else if (msg.type === 'portal_config') {
1602
- // Backend is auto-distributing the portal-sync token + URL so devs
1603
- // don't paste them by hand. Merge into ~/.knoxis/config.json so
1604
- // portal-sync.js picks them up on the next session save.
1605
- try {
1606
- ensureKnoxisDir();
1607
- const cfgPath = path.join(KNOXIS_DIR, 'config.json');
1608
- let existing = {};
1609
- try {
1610
- if (fs.existsSync(cfgPath)) existing = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) || {};
1611
- } catch (_) {}
1612
- if (typeof msg.portalToken === 'string' && msg.portalToken) existing.portalToken = msg.portalToken;
1613
- if (typeof msg.portalUrl === 'string' && msg.portalUrl) existing.portalUrl = msg.portalUrl;
1614
- if (typeof msg.portalSessionsPath === 'string' && msg.portalSessionsPath) {
1615
- existing.portalSessionsPath = msg.portalSessionsPath;
1616
- }
1617
- fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));
1618
- // Don't log the token itself.
1619
- console.log(`🔐 Portal-sync config received (token len ${(msg.portalToken || '').length})`);
1620
- } catch (cfgErr) {
1621
- console.warn(`⚠️ Failed to persist portal_config: ${cfgErr.message}`);
1622
- }
1623
1475
  }
1624
1476
  } catch (e) {
1625
1477
  console.error('❌ Relay message handling error:', e.message);
@@ -1803,4 +1655,4 @@ server.listen(serverMeta.port, () => {
1803
1655
  console.log('✅ HTTPS enabled - ready for secure connections from deployed frontends');
1804
1656
  console.log('');
1805
1657
  }
1806
- });
1658
+ });
@@ -34,89 +34,119 @@ function extractFilesTouched(diff) {
34
34
  return Array.from(seen);
35
35
  }
36
36
 
37
- // Snapshot the canonical state files so the session record carries the
38
- // latest STATUS / HANDOFF / DECISIONS / etc. up to the portal for display
39
- // on qig.ai. Three groups are captured into a single bag keyed by relative
40
- // path; the frontend distinguishes them by path prefix.
37
+ // Snapshot the project's maker-framework markdown docs so the session record
38
+ // carries the latest STATUS / HANDOFF / ARCHITECTURE / PRD / etc. up to the
39
+ // portal for display on qig.ai. Captured as a single bag keyed by relative
40
+ // path; the frontend distinguishes by path prefix.
41
41
  //
42
- // docs/state/*.md — project-level state (always present after scaffold)
43
- // docs/architecture/*.md — ARCHITECTURE.md + ADRs from project-kickoff
44
- // docs/features/<slug>/* — per-feature ROADMAP / PHASES / STATUS / manifest
45
- // from feature-kickoff (one entry per feature)
42
+ // All 11 dirs under `docs/` listed in the maker framework are walked
43
+ // recursively for `.md` files in priority order, until a total-bag budget
44
+ // is hit. The Java PairProgrammerSessionService persists the bag to the
45
+ // `state_files` JSONB column.
46
46
  //
47
- // All three flow through `stateFiles` on the session record. The Java
48
- // PairProgrammerSessionService persists the bag to the `state_files` JSONB
49
- // column; no schema change is needed to add new path prefixes.
50
- const STATE_FILES_TO_CAPTURE = [
51
- 'docs/state/STATUS.md',
52
- 'docs/state/HANDOFF.md',
53
- 'docs/state/DECISIONS.md',
54
- 'docs/state/CHANGELOG.md',
55
- 'docs/state/OPEN_QUESTIONS.md',
56
- 'docs/state/RISKS.md',
57
- // Project architecture pack from kickoff v7.
58
- 'docs/architecture/ARCHITECTURE.md'
47
+ // Priority order matters: when the budget is exhausted, the high-value docs
48
+ // must already be in. State first, then architecture, then product / planning,
49
+ // then features, then the rest alphabetical.
50
+ const STATE_DOC_DIRS = [
51
+ 'state',
52
+ 'architecture',
53
+ 'product',
54
+ 'planning',
55
+ 'features',
56
+ 'compliance',
57
+ 'design',
58
+ 'operations',
59
+ 'reports',
60
+ 'requirements',
61
+ 'standards'
59
62
  ];
60
- const FEATURE_FILE_NAMES = [
61
- 'ROADMAP.md',
62
- 'PHASES.md',
63
- 'STATUS.md',
64
- 'OPEN_QUESTIONS.md',
65
- 'manifest.json'
66
- ];
67
- const MAX_STATE_FILE_BYTES = 256 * 1024; // 256KB per file
68
-
69
- function readSafe(absPath, relPath, out) {
70
- try {
71
- const stat = fs.statSync(absPath);
72
- if (!stat.isFile()) return;
73
- if (stat.size > MAX_STATE_FILE_BYTES) {
74
- out[relPath] = `[File omitted: ${stat.size} bytes exceeds ${MAX_STATE_FILE_BYTES}-byte cap]`;
75
- return;
76
- }
77
- out[relPath] = fs.readFileSync(absPath, 'utf8');
78
- } catch (e) {
79
- // Missing file is normal — skip silently.
80
- }
81
- }
63
+ const MAX_STATE_FILE_BYTES = 256 * 1024; // 256KB per file
64
+ const MAX_BAG_BYTES = 2 * 1024 * 1024; // 2MB total — fits Postgres JSONB row comfortably + keeps frontend fast.
65
+ const MAX_FILES_PER_DIR = 50; // Bounds runaway dirs (e.g. archive of old reports).
66
+ const MAX_WALK_DEPTH = 5;
82
67
 
83
68
  function readStateFiles(workspace) {
84
69
  const out = {};
70
+ const skipped = [];
71
+ const state = { bytes: 0, halted: false };
85
72
  if (!workspace) return out;
86
73
 
87
- // Fixed list (state pack + ARCHITECTURE).
88
- for (const rel of STATE_FILES_TO_CAPTURE) {
89
- readSafe(path.join(workspace, rel), rel, out);
74
+ for (const dir of STATE_DOC_DIRS) {
75
+ walkMdFiles(path.join(workspace, 'docs', dir), `docs/${dir}`, out, 0, state, skipped);
76
+ if (state.halted) break;
77
+ }
78
+
79
+ if (skipped.length > 0) {
80
+ out['__truncated__'] = JSON.stringify({
81
+ reason: `state_files bag hit ${MAX_BAG_BYTES}-byte budget`,
82
+ capturedBytes: state.bytes,
83
+ skippedFiles: skipped.slice(0, 200)
84
+ }, null, 2);
90
85
  }
91
86
 
92
- // ADRs — docs/architecture/adr/*.md.
93
- const adrDir = path.join(workspace, 'docs', 'architecture', 'adr');
87
+ return out;
88
+ }
89
+
90
+ function walkMdFiles(absDir, relDir, out, depth, state, skipped) {
91
+ if (state.halted || depth > MAX_WALK_DEPTH) return;
92
+ let entries;
94
93
  try {
95
- for (const entry of fs.readdirSync(adrDir, { withFileTypes: true })) {
96
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
97
- const rel = `docs/architecture/adr/${entry.name}`;
98
- readSafe(path.join(adrDir, entry.name), rel, out);
99
- }
94
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
100
95
  } catch (e) {
101
- // No ADR dir is normal.
96
+ return; // Missing or unreadable dir skip silently.
102
97
  }
103
98
 
104
- // Per-feature files docs/features/<slug>/{ROADMAP,PHASES,STATUS,OPEN_QUESTIONS}.md
105
- // and manifest.json. One slug = one folder.
106
- const featuresDir = path.join(workspace, 'docs', 'features');
107
- try {
108
- for (const entry of fs.readdirSync(featuresDir, { withFileTypes: true })) {
109
- if (!entry.isDirectory()) continue;
110
- for (const filename of FEATURE_FILE_NAMES) {
111
- const rel = `docs/features/${entry.name}/${filename}`;
112
- readSafe(path.join(featuresDir, entry.name, filename), rel, out);
99
+ // Sort: directories last, files alphabetically. Gives stable, predictable
100
+ // capture order so truncation skips the same low-priority files each run.
101
+ entries.sort((a, b) => {
102
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? 1 : -1;
103
+ return a.name.localeCompare(b.name);
104
+ });
105
+
106
+ let filesInDir = 0;
107
+ for (const entry of entries) {
108
+ if (state.halted) return;
109
+ if (entry.name.startsWith('.')) continue; // Skip hidden entries.
110
+ const absPath = path.join(absDir, entry.name);
111
+ const relPath = `${relDir}/${entry.name}`;
112
+ if (entry.isDirectory()) {
113
+ walkMdFiles(absPath, relPath, out, depth + 1, state, skipped);
114
+ } else if (entry.isFile()) {
115
+ if (!entry.name.endsWith('.md')) continue;
116
+ if (entry.name.includes('.bak-')) continue; // Skip *.bak-<timestamp> backups.
117
+ if (filesInDir >= MAX_FILES_PER_DIR) {
118
+ skipped.push(`${relPath} (per-dir cap)`);
119
+ continue;
113
120
  }
121
+ filesInDir++;
122
+ readSafe(absPath, relPath, out, state, skipped);
114
123
  }
115
- } catch (e) {
116
- // No features dir is normal.
117
124
  }
125
+ }
118
126
 
119
- return out;
127
+ function readSafe(absPath, relPath, out, state, skipped) {
128
+ let stat;
129
+ try {
130
+ stat = fs.statSync(absPath);
131
+ } catch (e) {
132
+ return; // Missing file is normal.
133
+ }
134
+ if (!stat.isFile()) return;
135
+ if (stat.size > MAX_STATE_FILE_BYTES) {
136
+ out[relPath] = `[File omitted: ${stat.size} bytes exceeds ${MAX_STATE_FILE_BYTES}-byte cap]`;
137
+ return;
138
+ }
139
+ if (state.bytes + stat.size > MAX_BAG_BYTES) {
140
+ skipped.push(`${relPath} (bag budget)`);
141
+ state.halted = true;
142
+ return;
143
+ }
144
+ try {
145
+ out[relPath] = fs.readFileSync(absPath, 'utf8');
146
+ state.bytes += stat.size;
147
+ } catch (e) {
148
+ // Read failure (perms etc.) — skip silently.
149
+ }
120
150
  }
121
151
 
122
152
  // Enumerate feature slugs from the workspace so the frontend can list features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knoxis-helper",
3
- "version": "1.8.6",
3
+ "version": "1.9.0",
4
4
  "description": "Local helper for Knoxis pair programming - connects your machine to Knoxis on qig.ai",
5
5
  "bin": {
6
6
  "knoxis-helper": "./bin/knoxis-helper.js"