knoxis-helper 1.8.3 → 1.8.5

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.
@@ -101,6 +101,7 @@ function installAgentLocally(force) {
101
101
  const sourceStateScaffold = path.join(libDir, 'state-scaffold.js');
102
102
  const sourcePortalSync = path.join(libDir, 'portal-sync.js');
103
103
  const sourceSessionRecorder = path.join(libDir, 'session-recorder.js');
104
+ const sourceActiveSession = path.join(libDir, 'active-session.js');
104
105
  const sourceFrameworkVersion = path.join(libDir, 'framework-version.js');
105
106
  const sourceTemplatesDir = path.join(libDir, 'templates');
106
107
  const sourcePackage = path.join(__dirname, '..', 'package.json');
@@ -158,6 +159,11 @@ function installAgentLocally(force) {
158
159
  console.log(' Installed: session-recorder.js');
159
160
  }
160
161
 
162
+ if (fs.existsSync(sourceActiveSession)) {
163
+ fs.copyFileSync(sourceActiveSession, path.join(AGENT_DIR, 'active-session.js'));
164
+ console.log(' Installed: active-session.js');
165
+ }
166
+
161
167
  if (fs.existsSync(sourceFrameworkVersion)) {
162
168
  fs.copyFileSync(sourceFrameworkVersion, path.join(AGENT_DIR, 'framework-version.js'));
163
169
  console.log(' Installed: framework-version.js');
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const FILENAME = 'active-session.json';
7
+
8
+ function activeSessionPath(workspace) {
9
+ return path.join(workspace, '.knoxis', FILENAME);
10
+ }
11
+
12
+ /**
13
+ * Persist the in-progress pair-programming session so session-end can
14
+ * reference the same IDs (portal recorder, Claude resume, backend relay).
15
+ */
16
+ function writeActiveSession(workspace, data) {
17
+ const fp = activeSessionPath(workspace);
18
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
19
+ const payload = {
20
+ ...data,
21
+ updatedAt: new Date().toISOString()
22
+ };
23
+ fs.writeFileSync(fp, JSON.stringify(payload, null, 2), 'utf8');
24
+ return payload;
25
+ }
26
+
27
+ function readActiveSession(workspace) {
28
+ try {
29
+ const raw = fs.readFileSync(activeSessionPath(workspace), 'utf8');
30
+ return JSON.parse(raw);
31
+ } catch (_) {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function clearActiveSession(workspace) {
37
+ try {
38
+ fs.unlinkSync(activeSessionPath(workspace));
39
+ } catch (_) {}
40
+ }
41
+
42
+ module.exports = {
43
+ writeActiveSession,
44
+ readActiveSession,
45
+ clearActiveSession
46
+ };
@@ -36,6 +36,7 @@ const os = require('os');
36
36
  const { SessionRecorder } = require('./session-recorder');
37
37
  const { scaffoldStateLayout } = require('./state-scaffold');
38
38
  const { syncSessionToPortal } = require('./portal-sync');
39
+ const { writeActiveSession, readActiveSession, clearActiveSession } = require('./active-session');
39
40
  const kitTemplates = require('./templates');
40
41
 
41
42
  // === CONFIG ===
@@ -290,6 +291,24 @@ function readIdentityFromEnv() {
290
291
  };
291
292
  }
292
293
 
294
+ function persistActiveSession(recorder, identity, extra = {}) {
295
+ if (!recorder || !identity || !identity.workspace) return;
296
+ try {
297
+ writeActiveSession(identity.workspace, {
298
+ claudeSessionId: SESSION_ID,
299
+ recordedSessionId: recorder.sessionId,
300
+ backendSessionId: process.env.KNOXIS_BACKEND_SESSION_ID || null,
301
+ operatorId: identity.userId || identity.engineerId || null,
302
+ startedAt: recorder.startedAt,
303
+ mode: extra.mode ?? null,
304
+ kitMode: extra.kitMode ?? null,
305
+ taskHint: extra.taskHint ?? null
306
+ });
307
+ } catch (e) {
308
+ console.warn(' Could not persist active session: ' + e.message);
309
+ }
310
+ }
311
+
293
312
  // === STATE-FILE UPDATE PROMPT ===
294
313
  // Forces Claude to actually write docs/state/*.md before the session closes.
295
314
  // The QIG dashboard reads these files; without this step they stay as scaffold
@@ -365,6 +384,8 @@ async function finalizeSession(recorder) {
365
384
  async function runKitMode(kitMode, task, identity, scaffoldResult) {
366
385
  const archetype = process.env.KNOXIS_KIT_ARCHETYPE || null;
367
386
  const pattern = process.env.KNOXIS_KIT_PATTERN || null;
387
+ const closingSession = kitMode === 'session-end';
388
+ const activeSession = closingSession ? readActiveSession(identity.workspace) : null;
368
389
  let tpl;
369
390
  try {
370
391
  // feature-kickoff additionally consumes identity (userId, workspaceId,
@@ -380,6 +401,7 @@ async function runKitMode(kitMode, task, identity, scaffoldResult) {
380
401
  userId: identity.userId,
381
402
  workspaceId: identity.workspaceId,
382
403
  parentTaskId: (identity.taskIds && identity.taskIds.length) ? identity.taskIds[0] : null,
404
+ activeSession,
383
405
  // Auto-doc opt-in flows through env var since the interactive runner
384
406
  // sources its identity from the QIG portal rather than CLI args.
385
407
  integrate: process.env.KNOXIS_INTEGRATE === 'true' || process.env.KNOXIS_INTEGRATE === '1'
@@ -409,6 +431,11 @@ async function runKitMode(kitMode, task, identity, scaffoldResult) {
409
431
  console.log(' Workspace: ' + identity.workspace);
410
432
  console.log(' Session: ' + SESSION_ID);
411
433
  console.log(' Recorded: ' + recorder.sessionId);
434
+ if (closingSession && activeSession) {
435
+ console.log(' Closing: ' + (activeSession.recordedSessionId || activeSession.claudeSessionId || '(see .knoxis/active-session.json)'));
436
+ } else if (!closingSession) {
437
+ persistActiveSession(recorder, identity, { kitMode, taskHint: task || null });
438
+ }
412
439
  if (task) console.log(' Hint: ' + task.substring(0, 100) + (task.length > 100 ? '...' : ''));
413
440
  if (scaffoldResult && (scaffoldResult.dirs.length || scaffoldResult.files.length)) {
414
441
  console.log(' Scaffolded: ' + (scaffoldResult.dirs.length + scaffoldResult.files.length) + ' new entries (CODING_RULES + docs/state/)');
@@ -439,6 +466,13 @@ async function runKitMode(kitMode, task, identity, scaffoldResult) {
439
466
  console.log('');
440
467
 
441
468
  await finalizeSession(recorder);
469
+ if (closingSession) {
470
+ try {
471
+ clearActiveSession(identity.workspace);
472
+ } catch (e) {
473
+ console.warn(' Could not clear active session: ' + e.message);
474
+ }
475
+ }
442
476
  process.exit(result.code || 0);
443
477
  }
444
478
 
@@ -495,6 +529,7 @@ async function main() {
495
529
  console.log(' Task: ' + task.substring(0, 100) + (task.length > 100 ? '...' : ''));
496
530
  console.log(' Session: ' + SESSION_ID);
497
531
  console.log(' Recorded: ' + recorder.sessionId);
532
+ persistActiveSession(recorder, identity, { mode: 'interactive', taskHint: task });
498
533
  console.log(' Workspace: ' + identity.workspace);
499
534
  console.log(' Pair: ' + (hasGroq ? 'Groq (' + GROQ_MODEL + ')' : 'Disabled (no GROQ_API_KEY)'));
500
535
  console.log(' Timeout: ' + (MAX_PHASE_MS / 60000).toFixed(0) + ' min per phase');
@@ -991,6 +991,7 @@ async function handleRequest(req, res) {
991
991
  if (portalUserId) kitEnv.KNOXIS_USER_ID = portalUserId;
992
992
  if (portalWorkspaceId) kitEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
993
993
  if (portalTaskIds && portalTaskIds.length) kitEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
994
+ if (sessionId) kitEnv.KNOXIS_BACKEND_SESSION_ID = sessionId;
994
995
  command = buildEnvCommand(kitEnv, `node "${scriptPath}"`);
995
996
  mode = `kit:${kitMode}${archetype ? `/${archetype}` : ''}`;
996
997
  console.log(`🧰 Kit (interactive runner): ${mode} — ${scriptPath}`);
@@ -1024,6 +1025,7 @@ async function handleRequest(req, res) {
1024
1025
  if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
1025
1026
  if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
1026
1027
  if (projectSlug) interactiveEnv.KNOXIS_PROJECT_SLUG = projectSlug;
1028
+ if (sessionId) interactiveEnv.KNOXIS_BACKEND_SESSION_ID = sessionId;
1027
1029
  command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
1028
1030
  mode = 'interactive';
1029
1031
  console.log(`🤝 Interactive mode: ${scriptPath}`);
@@ -1495,6 +1497,8 @@ function connectRelayWebSocket() {
1495
1497
  if (portalUserId) kitEnv.KNOXIS_USER_ID = portalUserId;
1496
1498
  if (portalWorkspaceId) kitEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
1497
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;
1498
1502
  command = buildEnvCommand(kitEnv, `node "${scriptPath}"`);
1499
1503
  console.log(` 🧰 Kit (interactive runner): ${kitMode}${archetype ? `/${archetype}` : ''} — ${scriptPath}`);
1500
1504
  } else {
@@ -1533,6 +1537,8 @@ function connectRelayWebSocket() {
1533
1537
  if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
1534
1538
  if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
1535
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;
1536
1542
  command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
1537
1543
  console.log(` 🤝 Interactive mode: ${scriptPath}`);
1538
1544
  } else {
@@ -8,6 +8,7 @@ const kitTemplates = require('./templates');
8
8
  const { scaffoldStateLayout, assertStateLayout } = require('./state-scaffold');
9
9
  const { syncSessionToPortal } = require('./portal-sync');
10
10
  const { SessionRecorder } = require('./session-recorder');
11
+ const { writeActiveSession } = require('./active-session');
11
12
 
12
13
  // ===== RETRY CONFIGURATION =====
13
14
  // Can be overridden via environment variables
@@ -891,6 +892,22 @@ async function run() {
891
892
  safeExec,
892
893
  safeExecAsync
893
894
  });
895
+ if (mode !== 'session-end') {
896
+ try {
897
+ writeActiveSession(workspace, {
898
+ claudeSessionId: null,
899
+ recordedSessionId: recorder.sessionId,
900
+ backendSessionId: process.env.KNOXIS_BACKEND_SESSION_ID || null,
901
+ operatorId: userId || engineerId || null,
902
+ startedAt: recorder.startedAt,
903
+ mode: mode || 'pair-program',
904
+ kitMode: mode || null,
905
+ taskHint: task || null
906
+ });
907
+ } catch (e) {
908
+ console.warn(`Could not persist active session: ${e.message}`);
909
+ }
910
+ }
894
911
  console.log(`Recording: ON`);
895
912
  console.log('');
896
913
  }
@@ -28,9 +28,11 @@ Coaching reminders that apply throughout:
28
28
 
29
29
  Before starting, confirm:
30
30
  - **Operator ID** for this session (should match what was set in resume Step 0).
31
- - **Session ID** held since session start.
31
+ - **Session ID** for the working session you are closing (not the session-end invocation).
32
32
 
33
- If you're unsure of either, surface it before proceeding. Author tags on artifacts must be accurate.
33
+ If **Session context** above includes a **Working session** block, use those IDs directly — do not ask the operator to confirm them. Use **Portal recorded session** as \`session_id\` in the Step 9 SESSION record and in \`last_session_id\` frontmatter unless the framework calls for a new semantic slug for this closeout. Use **Operator ID** from that block for author tags.
34
+
35
+ If no Working session block is present and you're unsure of either ID, surface it before proceeding. Author tags on artifacts must be accurate.
34
36
 
35
37
  ---
36
38
 
@@ -440,7 +442,15 @@ Otherwise, end with: "Ready to close. Have a good one."
440
442
 
441
443
  **Begin now.** Start with the preamble.`;
442
444
 
443
- function buildSessionEndPrompt({ archetype, taskDescription, productSlug, projectSlug, workspace } = {}) {
445
+ function buildSessionEndPrompt({
446
+ archetype,
447
+ taskDescription,
448
+ productSlug,
449
+ projectSlug,
450
+ workspace,
451
+ activeSession,
452
+ userId
453
+ } = {}) {
444
454
  const header = [];
445
455
  header.push('Mode: session-end');
446
456
  if (archetype) header.push(`Archetype: ${archetype}`);
@@ -448,6 +458,24 @@ function buildSessionEndPrompt({ archetype, taskDescription, productSlug, projec
448
458
  if (projectSlug) header.push(`Project: ${projectSlug}`);
449
459
  if (workspace) header.push(`Workspace: ${workspace}`);
450
460
  if (taskDescription) header.push(`Session task: ${taskDescription}`);
461
+
462
+ const work = activeSession || null;
463
+ const operatorId = (work && work.operatorId) || userId || null;
464
+ if (work) {
465
+ header.push('');
466
+ header.push('Working session (close this one — ignore the session-end runner banner IDs):');
467
+ if (work.recordedSessionId) header.push(` Portal recorded session: ${work.recordedSessionId}`);
468
+ if (work.claudeSessionId) header.push(` Claude session: ${work.claudeSessionId}`);
469
+ if (work.collabSessionId) header.push(` Collab session: ${work.collabSessionId}`);
470
+ if (work.backendSessionId) header.push(` Knoxis backend session: ${work.backendSessionId}`);
471
+ if (operatorId) header.push(` Operator ID: ${operatorId}`);
472
+ if (work.startedAt) header.push(` Started at: ${work.startedAt}`);
473
+ if (work.kitMode) header.push(` Kit mode: ${work.kitMode}`);
474
+ if (work.mode) header.push(` Runner mode: ${work.mode}`);
475
+ } else if (operatorId) {
476
+ header.push(`Operator ID (from portal): ${operatorId}`);
477
+ }
478
+
451
479
  const ctx = `Session context:\n${header.join('\n')}\n\n`;
452
480
  return ctx + SESSION_END_BODY;
453
481
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knoxis-helper",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
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"