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.
package/bin/knoxis-helper.js
CHANGED
|
@@ -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**
|
|
31
|
+
- **Session ID** for the working session you are closing (not the session-end invocation).
|
|
32
32
|
|
|
33
|
-
If
|
|
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({
|
|
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
|
}
|