knoxis-helper 1.8.2 → 1.8.4

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.
@@ -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,
@@ -379,7 +400,11 @@ async function runKitMode(kitMode, task, identity, scaffoldResult) {
379
400
  workspace: identity.workspace,
380
401
  userId: identity.userId,
381
402
  workspaceId: identity.workspaceId,
382
- parentTaskId: (identity.taskIds && identity.taskIds.length) ? identity.taskIds[0] : null
403
+ parentTaskId: (identity.taskIds && identity.taskIds.length) ? identity.taskIds[0] : null,
404
+ activeSession,
405
+ // Auto-doc opt-in flows through env var since the interactive runner
406
+ // sources its identity from the QIG portal rather than CLI args.
407
+ integrate: process.env.KNOXIS_INTEGRATE === 'true' || process.env.KNOXIS_INTEGRATE === '1'
383
408
  });
384
409
  } catch (e) {
385
410
  console.error('Kit template error: ' + e.message);
@@ -406,6 +431,11 @@ async function runKitMode(kitMode, task, identity, scaffoldResult) {
406
431
  console.log(' Workspace: ' + identity.workspace);
407
432
  console.log(' Session: ' + SESSION_ID);
408
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
+ }
409
439
  if (task) console.log(' Hint: ' + task.substring(0, 100) + (task.length > 100 ? '...' : ''));
410
440
  if (scaffoldResult && (scaffoldResult.dirs.length || scaffoldResult.files.length)) {
411
441
  console.log(' Scaffolded: ' + (scaffoldResult.dirs.length + scaffoldResult.files.length) + ' new entries (CODING_RULES + docs/state/)');
@@ -436,6 +466,13 @@ async function runKitMode(kitMode, task, identity, scaffoldResult) {
436
466
  console.log('');
437
467
 
438
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
+ }
439
476
  process.exit(result.code || 0);
440
477
  }
441
478
 
@@ -492,6 +529,7 @@ async function main() {
492
529
  console.log(' Task: ' + task.substring(0, 100) + (task.length > 100 ? '...' : ''));
493
530
  console.log(' Session: ' + SESSION_ID);
494
531
  console.log(' Recorded: ' + recorder.sessionId);
532
+ persistActiveSession(recorder, identity, { mode: 'interactive', taskHint: task });
495
533
  console.log(' Workspace: ' + identity.workspace);
496
534
  console.log(' Pair: ' + (hasGroq ? 'Groq (' + GROQ_MODEL + ')' : 'Disabled (no GROQ_API_KEY)'));
497
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
@@ -804,7 +805,11 @@ async function run() {
804
805
  pattern: recoveryPattern,
805
806
  productSlug: args['product-slug'] || null,
806
807
  projectSlug: args['project-slug'] || null,
807
- workspace
808
+ workspace,
809
+ // Operator opt-in for auto-doc generation. When --integrate is passed
810
+ // alongside --mode feature-kickoff, the kickoff template appends a
811
+ // phase-integrate phase that the session-end ritual will run later.
812
+ integrate: Boolean(args.integrate)
808
813
  });
809
814
  scheduledSteps = [{
810
815
  key: tpl.key,
@@ -887,6 +892,22 @@ async function run() {
887
892
  safeExec,
888
893
  safeExecAsync
889
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
+ }
890
911
  console.log(`Recording: ON`);
891
912
  console.log('');
892
913
  }
@@ -8,6 +8,11 @@
8
8
  // docs/features/<slug>/. Multiple features can coexist; each is independently
9
9
  // trackable from the QIG dashboard via its manifest.json.
10
10
  //
11
+ // Canonical structural constraints (slug pattern, phase counts, manifest shape)
12
+ // live in ./kickoff-schema.json and are also consumed by the server-side
13
+ // KickoffPrepService that turns meeting transcripts into kickoff-ready JSON.
14
+ // Keep this file's prose in sync with that schema if you change the numbers.
15
+ //
11
16
  // State machine (similar shape to resume.js, autonomous-first):
12
17
  // A: docs/features/<slug>/ missing → write the roadmap
13
18
  // B: directory exists with unanswered _(fill in)_ entries → wait
@@ -93,6 +98,36 @@ touches. Steps must be small enough to ship in one pair-program session.
93
98
  **Don't pad.** A 4-step feature is fine. A 30-step feature with 3 phases of
94
99
  filler is not.
95
100
 
101
+ ### Optional final phase: \`phase-integrate\` (operator opt-in)
102
+
103
+ If the session header above includes \`Integrate: yes\`, append one extra
104
+ phase as the **last phase** of the feature, with this exact shape (id is
105
+ load-bearing — the auto-doc generator looks for it):
106
+
107
+ \`\`\`json
108
+ {
109
+ "id": "phase-integrate",
110
+ "title": "Integrate (auto-doc)",
111
+ "goal": "Generate the public-contract API doc and register it for the chatbot",
112
+ "done_when": "docs/features/<slug>/PUBLIC_API.md exists and the integration-docs index has been notified",
113
+ "status": "pending",
114
+ "steps": [
115
+ { "id": "phase-integrate-step-1", "action": "Generate PUBLIC_API.md from manifest + live OpenAPI", "touches": "docs/features/<slug>/PUBLIC_API.md", "status": "pending" },
116
+ { "id": "phase-integrate-step-2", "action": "POST the doc to the backend integration-docs index for vectorization", "touches": "/api/public/integration-docs/index", "status": "pending" }
117
+ ]
118
+ }
119
+ \`\`\`
120
+
121
+ Do NOT include \`phase-integrate\` when the session header does not say
122
+ \`Integrate: yes\`. This is opt-in only — features that don't ship a public
123
+ contract should not have this phase, and adding it half-heartedly creates
124
+ half-baked docs in the vector store.
125
+
126
+ When the operator opts in, mirror the phase into ROADMAP.md (add it to the
127
+ phases list) and PHASES.md (with the two checkbox steps). The session-end
128
+ ritual auto-detects \`phase-integrate\` as the eligible-pending phase and runs
129
+ the doc generator without an extra command.
130
+
96
131
  ## Step 4 — Capture genuine open questions only
97
132
 
98
133
  If something truly cannot be decided autonomously (missing API decision,
@@ -306,7 +341,8 @@ function buildFeatureKickoffPrompt({
306
341
  workspace,
307
342
  userId,
308
343
  workspaceId,
309
- parentTaskId
344
+ parentTaskId,
345
+ integrate
310
346
  } = {}) {
311
347
  const header = ['Mode: feature-kickoff'];
312
348
  if (productSlug) header.push(`Product: ${productSlug}`);
@@ -315,6 +351,7 @@ function buildFeatureKickoffPrompt({
315
351
  if (userId) header.push(`User ID: ${userId}`);
316
352
  if (workspaceId) header.push(`Workspace ID: ${workspaceId}`);
317
353
  if (parentTaskId) header.push(`Parent task ID: ${parentTaskId}`);
354
+ if (integrate) header.push('Integrate: yes');
318
355
  if (taskDescription) header.push(`Feature description (use as-is for the title and summary):\n${taskDescription}`);
319
356
  const ctx = `Session context:\n${header.join('\n')}\n\n`;
320
357
  return ctx + FEATURE_KICKOFF_BODY;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const kickoff = require('./kickoff');
4
4
  const featureKickoff = require('./feature-kickoff');
5
+ const featureIntegrate = require('./integrate');
5
6
  const resume = require('./resume');
6
7
  const sessionEnd = require('./session-end');
7
8
  const recovery = require('./recovery');
@@ -10,6 +11,7 @@ const codingRuleset = require('./coding-ruleset');
10
11
  const MODES = {
11
12
  kickoff,
12
13
  'feature-kickoff': featureKickoff,
14
+ 'feature-integrate': featureIntegrate,
13
15
  resume,
14
16
  'session-end': sessionEnd,
15
17
  recovery
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ // Feature Integrate — single-shot, autonomous (v1)
4
+ //
5
+ // Operator opt-in: a feature includes a phase with id "phase-integrate" at
6
+ // kickoff time, signaling that this feature ships a public-contract surface.
7
+ // When all preceding phases are marked "done" and phase-integrate is the
8
+ // next-pending phase, this template generates docs/features/<slug>/PUBLIC_API.md
9
+ // from the live OpenAPI spec + the feature's manifest, marks phase-integrate
10
+ // done, and (when the backend is reachable) posts the doc to the vectorizer
11
+ // at /api/public/integration-docs/index so the chatbot can serve it as
12
+ // integration guidance.
13
+ //
14
+ // State machine:
15
+ // A: PUBLIC_API.md missing or feature manifest changed since last write → write the doc
16
+ // B: PUBLIC_API.md present and manifest unchanged → no-op (idempotent re-run)
17
+ // C: phase-integrate is not pending or prior phases incomplete → halt with a one-line note
18
+
19
+ const FEATURE_INTEGRATE_BODY = `# Feature Integrate (single-shot, autonomous)
20
+
21
+ You are running in single-shot mode. You produce one response and the process
22
+ exits. Your job is to take the operator's feature slug (in the session header
23
+ above), generate \`docs/features/<slug>/PUBLIC_API.md\` from the live API
24
+ surface, mark the feature's \`phase-integrate\` step done, and (best-effort)
25
+ register the doc with the backend so it can be vectorized for the chatbot.
26
+
27
+ This is **a doc generation pass** — do not change application code, controllers,
28
+ or any file outside \`docs/features/<slug>/\`.
29
+
30
+ ## Step 0 — Resolve the feature slug
31
+
32
+ The slug is in the session header (\`Feature slug: <slug>\`). If absent, abort
33
+ with a one-line note: "Feature integrate requires --prompt <slug>."
34
+
35
+ Verify \`docs/features/<slug>/\` exists and contains \`manifest.json\`. If not,
36
+ abort: "No feature found at docs/features/<slug>/. Run feature-kickoff first."
37
+
38
+ ## Step 1 — Confirm the operator opted in
39
+
40
+ Open \`docs/features/<slug>/manifest.json\`. Look for a phase with
41
+ \`id: "phase-integrate"\`.
42
+
43
+ - **Not found** → halt: "This feature did not opt into integrate phase. To
44
+ enable, re-run feature-kickoff with the --integrate flag, or hand-edit
45
+ manifest.json to add a phase with id 'phase-integrate'."
46
+ - **Found and \`status: "done"\` already** → State B → check whether
47
+ \`PUBLIC_API.md\` exists. If yes, idempotent no-op: print "PUBLIC_API.md
48
+ already up to date." If no, treat as State A.
49
+ - **Found, \`status: "pending"\`, prior phases all \`status: "done"\`** → State A.
50
+ Continue.
51
+ - **Found, \`status: "pending"\`, prior phases NOT all done** → State C → halt:
52
+ "phase-integrate is not yet eligible — phases X, Y, Z still pending."
53
+
54
+ ## Step 2 — Identify the public surface this feature shipped
55
+
56
+ From \`manifest.json\`, walk every \`phases[].steps[].touches\` value.
57
+
58
+ Filter to **Java files under \`src/main/java/com/yourcompany/voice/controller/\`**
59
+ or any file whose path contains \`/controller/\`. These are the candidates for
60
+ the public surface.
61
+
62
+ For each touched controller file:
63
+ - Read its \`@RequestMapping\` base path.
64
+ - Read its \`@PostMapping\`, \`@GetMapping\`, \`@PutMapping\`, \`@DeleteMapping\`
65
+ annotations to enumerate routes.
66
+ - Note the request body shape (look for \`@RequestBody\`, \`@RequestParam\`,
67
+ \`@RequestPart\` types).
68
+ - Note the return type and any \`@Operation(summary=...)\` Swagger annotation
69
+ for prose.
70
+
71
+ If a step's \`touches\` field references a non-controller file (service, DTO,
72
+ config), skip it. Only the public-contract surface goes in PUBLIC_API.md.
73
+
74
+ If after filtering you have zero controllers, this feature does not ship a
75
+ public contract — abort: "No controllers in manifest's touched files; nothing
76
+ to document. Use a different doc kind for this feature."
77
+
78
+ ## Step 3 — Fetch the live OpenAPI spec (best effort)
79
+
80
+ Hit \`http://localhost:8080/v3/api-docs\` (Spring's default) and parse the JSON.
81
+ If the backend is not running locally, look for an env var \`OPENAPI_URL\` and
82
+ use that. If neither is reachable, skip live OpenAPI and rely solely on the
83
+ controller source you read in Step 2 — the doc just won't include exact
84
+ response schemas, only the route + summary + request body shape.
85
+
86
+ For each route from Step 2, look up the matching path in the OpenAPI spec
87
+ (\`paths.<basePath><route>\`) and pull:
88
+ - \`summary\` and \`description\`
89
+ - request schema (\`requestBody.content."application/json".schema\`)
90
+ - response schema (\`responses."200".content.*.schema\`)
91
+ - error responses
92
+
93
+ ## Step 4 — Generate \`docs/features/<slug>/PUBLIC_API.md\`
94
+
95
+ Use this skeleton, populated from Steps 2–3. Keep it tight; integrators will
96
+ read this in 60 seconds.
97
+
98
+ \`\`\`markdown
99
+ ---
100
+ feature_slug: <slug>
101
+ generated_at: <ISO date>
102
+ api_kind: public-contract
103
+ ---
104
+
105
+ # <Feature Title> — Public API
106
+
107
+ <2–3 sentence summary from manifest.feature_title + manifest.proposedSummary
108
+ or the feature's ROADMAP.md Summary section.>
109
+
110
+ **Base URL (deployed):** \`<from PUBLIC_VOICE_API.md or operator-known\`
111
+
112
+ ## 60-second quickstart
113
+
114
+ \\\`\\\`\\\`bash
115
+ # <smallest curl that exercises the headline endpoint>
116
+ \\\`\\\`\\\`
117
+
118
+ ## Endpoints
119
+
120
+ | Path | Method | Use when |
121
+ |---|---|---|
122
+ <one row per route from Step 2-3>
123
+
124
+ ## Authentication
125
+
126
+ <Same shared-key pattern as PUBLIC_VOICE_API.md if these endpoints sit behind
127
+ PublicVoiceGuard, or whatever the controllers actually enforce.>
128
+
129
+ ## <One section per endpoint>
130
+
131
+ ### Request
132
+ \\\`\\\`\\\`json
133
+ <request schema, example values>
134
+ \\\`\\\`\\\`
135
+
136
+ ### Response
137
+ \\\`\\\`\\\`json
138
+ <response schema, example values>
139
+ \\\`\\\`\\\`
140
+
141
+ | Field | Type | Required | Notes |
142
+ |---|---|---|---|
143
+ <rows>
144
+
145
+ ## Error reference
146
+
147
+ <reuse the standard envelope; reference codes the controllers actually emit>
148
+
149
+ ## Limits and defaults
150
+
151
+ <rate limits, concurrency caps, etc. if known>
152
+
153
+ ## Need anything
154
+
155
+ <Match the footer style in docs/PUBLIC_VOICE_API.md.>
156
+ \`\`\`
157
+
158
+ **Don't pad.** If a section has nothing to say, leave it out. Match the tone
159
+ of the existing \`docs/PUBLIC_VOICE_API.md\` (terse, integrator-focused, no
160
+ filler).
161
+
162
+ ## Step 5 — Mark phase-integrate done in the manifest
163
+
164
+ Update \`docs/features/<slug>/manifest.json\`:
165
+ - The phase with \`id: "phase-integrate"\` → \`status: "done"\`
166
+ - All steps within that phase → \`status: "done"\`
167
+ - If \`current_phase\` was \`phase-integrate\`, advance it to \`null\` (or to
168
+ the next pending phase if any exist after integrate).
169
+
170
+ Mirror the change into \`docs/features/<slug>/STATUS.md\` (check off the box).
171
+ Mirror into \`docs/features/<slug>/PHASES.md\` (\`- [x]\` for the integrate
172
+ steps).
173
+
174
+ ## Step 6 — Best-effort: register the doc with the backend vectorizer
175
+
176
+ POST the generated markdown to the backend's integration-docs index endpoint:
177
+
178
+ \`\`\`bash
179
+ curl -s -X POST "$BACKEND/api/public/integration-docs/index" \\
180
+ -H "Content-Type: application/json" \\
181
+ -H "X-Tenant-Id: $TENANT" \\
182
+ -H "X-Public-Voice-Key: $KEY" \\
183
+ -d '{"slug": "<slug>", "markdown": "<contents>"}'
184
+ \`\`\`
185
+
186
+ Read \`BACKEND\` from env or default to \`http://localhost:8080\`. Read
187
+ \`TENANT\` from env (\`KNOXIS_TENANT_ID\`) — if missing, skip vectorization
188
+ with a one-line note "Skipped vectorization: KNOXIS_TENANT_ID not set."
189
+ Read \`KEY\` from env \`PUBLIC_VOICE_API_SHARED_KEY\` (optional).
190
+
191
+ If the curl returns non-2xx, do not fail — the doc was still written to disk.
192
+ Print one line: "Vectorize call returned <code>; doc was generated. The
193
+ operator can re-run vectorization manually."
194
+
195
+ ## Step 7 — Output for the operator
196
+
197
+ End with a copyable block:
198
+
199
+ \`\`\`
200
+ Feature integrate complete: <slug>
201
+
202
+ Files written:
203
+ docs/features/<slug>/PUBLIC_API.md
204
+ docs/features/<slug>/manifest.json (phase-integrate → done)
205
+ docs/features/<slug>/STATUS.md (checkbox)
206
+ docs/features/<slug>/PHASES.md (checkboxes)
207
+
208
+ Vectorization: <success | skipped | failed>
209
+
210
+ Next: the doc is ready to share with integrators and (when registered) to
211
+ serve via the chatbot's "I need a feature for X" intent.
212
+ \`\`\`
213
+
214
+ ## Non-negotiables
215
+
216
+ - Write \`PUBLIC_API.md\` to disk. Do not describe what you would write.
217
+ - Match the tone and structure of \`docs/PUBLIC_VOICE_API.md\` and
218
+ \`docs/PUBLIC_SPEAKER_API.md\` — these are the canonical examples.
219
+ - Keep \`manifest.json\` consistent with the three MD files at all times.
220
+ - Do not modify application code (no edits to anything outside
221
+ \`docs/features/<slug>/\`).
222
+ - One feature per run. The slug is final for this run.
223
+ - Idempotent. Re-running with PUBLIC_API.md already up to date is a no-op.
224
+
225
+ ---
226
+
227
+ `;
228
+
229
+ function buildFeatureIntegratePrompt({
230
+ taskDescription,
231
+ productSlug,
232
+ projectSlug,
233
+ workspace,
234
+ userId,
235
+ workspaceId
236
+ } = {}) {
237
+ const header = ['Mode: feature-integrate'];
238
+ if (productSlug) header.push(`Product: ${productSlug}`);
239
+ if (projectSlug) header.push(`Project: ${projectSlug}`);
240
+ if (workspace) header.push(`Workspace: ${workspace}`);
241
+ if (userId) header.push(`User ID: ${userId}`);
242
+ if (workspaceId) header.push(`Workspace ID: ${workspaceId}`);
243
+ if (taskDescription) header.push(`Feature slug: ${taskDescription}`);
244
+ const ctx = `Session context:\n${header.join('\n')}\n\n`;
245
+ return ctx + FEATURE_INTEGRATE_BODY;
246
+ }
247
+
248
+ module.exports = {
249
+ title: 'Feature Integrate',
250
+ body: FEATURE_INTEGRATE_BODY,
251
+ buildPrompt: buildFeatureIntegratePrompt
252
+ };
@@ -0,0 +1,75 @@
1
+ {
2
+ "version": "1.0",
3
+ "description": "Single source of truth for the feature-kickoff phase shape. Shared by scripts/knoxis-helper/lib/templates/feature-kickoff.js (operator-facing prompt) and src/main/java/com/yourcompany/voice/service/kickoffprep/* (server-side meeting → kickoff-prep generator). Both read these constraints to keep the JSON shape consistent and prevent drift.",
4
+
5
+ "slug": {
6
+ "pattern": "^[a-z0-9][a-z0-9-]{0,39}$",
7
+ "maxLength": 40,
8
+ "rules": [
9
+ "Lowercase alphanumeric and dashes only.",
10
+ "Max 40 characters.",
11
+ "Semantically descriptive of the feature.",
12
+ "Do not include the words 'feature' or 'task'."
13
+ ],
14
+ "examples": [
15
+ { "description": "Add real-time notification toasts", "slug": "realtime-notification-toasts" },
16
+ { "description": "Replace polling with SSE on the dashboard", "slug": "dashboard-sse-replacement" }
17
+ ]
18
+ },
19
+
20
+ "phases": {
21
+ "minCount": 3,
22
+ "maxCount": 6,
23
+ "stepsPerPhase": { "min": 2, "max": 6 },
24
+ "rules": [
25
+ "Phases run sequentially.",
26
+ "Each phase has a clear 'done when' criterion.",
27
+ "Each step is one imperative-voice action plus the file(s) or surface it touches.",
28
+ "Steps must be small enough to ship in one pair-program session.",
29
+ "Don't pad. A 4-step feature is fine. A 30-step feature with 3 phases of filler is not."
30
+ ],
31
+ "defaultShape": [
32
+ { "title": "Discovery", "goal": "Read existing code, find integration points, list constraints" },
33
+ { "title": "Design", "goal": "Data model / API contract / UI shape decisions" },
34
+ { "title": "Implementation", "goal": "Commit-sized steps, each touching named files" },
35
+ { "title": "Testing", "goal": "Unit / integration / manual coverage" },
36
+ { "title": "Rollout", "goal": "Feature flag, docs, telemetry, deprecations" }
37
+ ]
38
+ },
39
+
40
+ "openQuestions": {
41
+ "guidance": "Capture genuine open questions only. Most 'questions' are decisions you should make and capture in DECISIONS instead. If you write more than 3 pending questions you are over-using this.",
42
+ "softMax": 3
43
+ },
44
+
45
+ "manifestShape": {
46
+ "feature_slug": "string (matches slug.pattern)",
47
+ "feature_title": "string",
48
+ "parent_task_id": "string | null",
49
+ "workspace_id": "string | null",
50
+ "user_id": "string | null",
51
+ "product_slug": "string | null",
52
+ "project_slug": "string | null",
53
+ "kicked_off_at": "ISO timestamp",
54
+ "status": "planning | blocked | in-progress | done",
55
+ "current_phase": "string (phase id, e.g. phase-1)",
56
+ "phases": [
57
+ {
58
+ "id": "string (phase-{n})",
59
+ "title": "string",
60
+ "goal": "string",
61
+ "done_when": "string",
62
+ "status": "pending | in-progress | done",
63
+ "steps": [
64
+ {
65
+ "id": "string (phase-{n}-step-{m})",
66
+ "action": "string (imperative)",
67
+ "touches": "string (file or surface)",
68
+ "status": "pending | in-progress | done"
69
+ }
70
+ ]
71
+ }
72
+ ],
73
+ "definition_of_done": ["string"]
74
+ }
75
+ }
@@ -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
 
@@ -345,6 +347,68 @@ For specific archetypes, populate \`archetype_specific_data\`:
345
347
 
346
348
  Show me. Ask: "Paste this into the portal, or wait for portal MCP to upload it automatically (if live)?"
347
349
 
350
+ ## Step 9.5 — Auto-integrate eligible features (opt-in features only)
351
+
352
+ **Orientation:** "If any feature in this project opted into auto-doc generation
353
+ by including a \`phase-integrate\` phase, and that phase is now eligible to
354
+ run, generate its public API doc here so the next session opens with it
355
+ already shipped."
356
+
357
+ For each \`docs/features/*/manifest.json\`, check whether **all three** are true:
358
+
359
+ 1. A phase exists with \`id: "phase-integrate"\`.
360
+ 2. That phase's \`status\` is \`"pending"\`.
361
+ 3. Every phase before it has \`status: "done"\`.
362
+
363
+ If no manifest meets all three, say "No features are eligible for auto-integrate
364
+ this session." and skip to Step 10.
365
+
366
+ For each eligible feature, run the integrate playbook autonomously (you do not
367
+ need a separate command — execute the steps inline now):
368
+
369
+ 1. Read the controllers listed in the feature's \`phases[].steps[].touches\` —
370
+ anything under \`src/main/java/com/yourcompany/voice/controller/\`.
371
+ 2. (Best effort) Fetch \`http://localhost:8080/v3/api-docs\` to enrich with live
372
+ request/response schemas. If unreachable, skip and use the controller source
373
+ directly. Don't fail the step on this.
374
+ 3. Generate \`docs/features/<slug>/PUBLIC_API.md\` matching the structure and
375
+ tone of \`docs/PUBLIC_VOICE_API.md\` and \`docs/PUBLIC_SPEAKER_API.md\`.
376
+ Sections: title + 2-3 sentence summary, 60-second quickstart, endpoint
377
+ table, authentication, per-endpoint request/response, error reference,
378
+ limits, footer.
379
+ 4. Update the feature's \`manifest.json\`: \`phase-integrate\` → \`status: "done"\`,
380
+ each step within → \`status: "done"\`, advance \`current_phase\` past it.
381
+ 5. Mirror the status changes into \`docs/features/<slug>/STATUS.md\` and
382
+ \`docs/features/<slug>/PHASES.md\` (check the boxes).
383
+ 6. **Best-effort vectorization.** If \`KNOXIS_TENANT_ID\` is set in the
384
+ environment, POST the generated markdown to:
385
+
386
+ \`\`\`
387
+ POST $BACKEND/api/public/integration-docs/index
388
+ Headers: Content-Type: application/json
389
+ X-Tenant-Id: $KNOXIS_TENANT_ID
390
+ X-Public-Voice-Key: $PUBLIC_VOICE_API_SHARED_KEY (if set)
391
+ Body: { "slug": "<slug>", "markdown": "<file contents>" }
392
+ \`\`\`
393
+
394
+ Default \`BACKEND\` to \`http://localhost:8080\`. If the call fails with a
395
+ non-2xx, **don't fail the session** — note "Vectorization deferred (HTTP
396
+ <code>); operator can re-run via knoxis-helper --mode feature-integrate
397
+ --prompt <slug>." and move on. The doc is on disk regardless.
398
+
399
+ For each feature you processed, output:
400
+
401
+ \`\`\`
402
+ Auto-integrated: <slug>
403
+ PUBLIC_API.md: <created | refreshed>
404
+ manifest.json: phase-integrate → done
405
+ Vectorization: <success | skipped (no tenant) | deferred (HTTP <code>)>
406
+ \`\`\`
407
+
408
+ Do not run integrate logic on features where the conditions aren't met. Don't
409
+ add a \`phase-integrate\` to features that didn't have one — operator opt-in
410
+ is sticky.
411
+
348
412
  ## Step 10 — Final summary
349
413
 
350
414
  Print a one-paragraph summary:
@@ -378,7 +442,15 @@ Otherwise, end with: "Ready to close. Have a good one."
378
442
 
379
443
  **Begin now.** Start with the preamble.`;
380
444
 
381
- 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
+ } = {}) {
382
454
  const header = [];
383
455
  header.push('Mode: session-end');
384
456
  if (archetype) header.push(`Archetype: ${archetype}`);
@@ -386,6 +458,24 @@ function buildSessionEndPrompt({ archetype, taskDescription, productSlug, projec
386
458
  if (projectSlug) header.push(`Project: ${projectSlug}`);
387
459
  if (workspace) header.push(`Workspace: ${workspace}`);
388
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
+
389
479
  const ctx = `Session context:\n${header.join('\n')}\n\n`;
390
480
  return ctx + SESSION_END_BODY;
391
481
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knoxis-helper",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
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"