knoxis-helper 1.6.3 → 1.7.1

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.
@@ -59,6 +59,7 @@ function parseArgs() {
59
59
  if (arg === '--update') { args.update = true; continue; }
60
60
  if (arg === '--uninstall') { args.uninstall = true; continue; }
61
61
  if (arg === '--restart') { args.restart = true; continue; }
62
+ if (arg === '--version' || arg === '-v') { args.version = true; continue; }
62
63
  if (arg.startsWith('--')) {
63
64
  const key = arg.slice(2);
64
65
  const next = process.argv[i + 1];
@@ -100,6 +101,7 @@ function installAgentLocally(force) {
100
101
  const sourceStateScaffold = path.join(libDir, 'state-scaffold.js');
101
102
  const sourcePortalSync = path.join(libDir, 'portal-sync.js');
102
103
  const sourceSessionRecorder = path.join(libDir, 'session-recorder.js');
104
+ const sourceFrameworkVersion = path.join(libDir, 'framework-version.js');
103
105
  const sourceTemplatesDir = path.join(libDir, 'templates');
104
106
  const sourcePackage = path.join(__dirname, '..', 'package.json');
105
107
 
@@ -156,6 +158,11 @@ function installAgentLocally(force) {
156
158
  console.log(' Installed: session-recorder.js');
157
159
  }
158
160
 
161
+ if (fs.existsSync(sourceFrameworkVersion)) {
162
+ fs.copyFileSync(sourceFrameworkVersion, path.join(AGENT_DIR, 'framework-version.js'));
163
+ console.log(' Installed: framework-version.js');
164
+ }
165
+
159
166
  if (fs.existsSync(sourceTemplatesDir)) {
160
167
  copyDirRecursive(sourceTemplatesDir, path.join(AGENT_DIR, 'templates'));
161
168
  console.log(' Installed: templates/');
@@ -375,6 +382,26 @@ function restart() {
375
382
  async function main() {
376
383
  const args = parseArgs();
377
384
 
385
+ // Version — print npm release version and embedded framework bundle version.
386
+ if (args.version) {
387
+ let pkgVersion = 'unknown';
388
+ try {
389
+ pkgVersion = require(path.join(__dirname, '..', 'package.json')).version;
390
+ } catch (e) {}
391
+ let fw;
392
+ try {
393
+ fw = require(path.join(__dirname, '..', 'lib', 'framework-version.js'));
394
+ } catch (e) {
395
+ fw = null;
396
+ }
397
+ console.log(`knoxis-helper@${pkgVersion}`);
398
+ if (fw) {
399
+ console.log(`framework bundle: v${fw.bundle}`);
400
+ console.log(` kickoff v${fw.documents.kickoff}, resume v${fw.documents.resume}, session-end v${fw.documents.sessionEnd}, recovery v${fw.documents.recovery}, ruleset v${fw.documents.codingRuleset}, portal-contract v${fw.documents.portalContract}`);
401
+ }
402
+ process.exit(0);
403
+ }
404
+
378
405
  // Uninstall
379
406
  if (args.uninstall) {
380
407
  uninstall();
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ // Framework bundle version embedded in this knoxis-helper release.
4
+ // Source: docs/Pair Programmer Docs/pair-programmer/VERSION (+ CHANGELOG.md)
5
+ // and the per-document version headers in pair-programmer/framework/*.md.
6
+ //
7
+ // Why this is separate from package.json: knoxis-helper version (npm release)
8
+ // bumps for helper-side fixes — sync bugs, scaffold tweaks, CLI changes —
9
+ // that don't change the methodology. The framework bundle version bumps when
10
+ // the canonical docs change. One npm package, two version labels: the package
11
+ // version answers "what release am I running"; the framework version answers
12
+ // "what methodology version is baked into it." Both ride together in every
13
+ // session record so the portal can render them.
14
+ module.exports = {
15
+ bundle: '1.1.0',
16
+ documents: {
17
+ overview: 1,
18
+ kickoff: 8,
19
+ resume: 4,
20
+ sessionEnd: 4,
21
+ recovery: 3,
22
+ codingRuleset: 3,
23
+ marketplaceMcp: 3,
24
+ migration: 3,
25
+ portalContract: '0.3'
26
+ }
27
+ };
@@ -732,11 +732,29 @@ async function run() {
732
732
  const userTask = (task || '').trim();
733
733
  task = userTask || `[${mode || 'default'} session]`;
734
734
 
735
+ // Resolve operator + product/project identity early so the scaffolder can
736
+ // also drop a product.local.json skeleton in the product parent directory
737
+ // when the framework's Phase -1 product context isn't already written.
738
+ const knoxisConfig = (() => {
739
+ const p = path.join(os.homedir(), '.knoxis', 'config.json');
740
+ try { return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}; } catch (e) { return {}; }
741
+ })();
742
+ const engineerId = args['engineer-id'] || knoxisConfig.userId || null;
743
+ const productSlug = args['product-slug'] || knoxisConfig.productSlug || null;
744
+ const projectSlug = args['project-slug'] || knoxisConfig.projectSlug || path.basename(workspace);
745
+ const userId = args['user-id'] || knoxisConfig.userId || null;
746
+ const workspaceIdArg = args['workspace-id'] || null;
747
+ const taskIdsArg = (() => {
748
+ const raw = args['task-id'];
749
+ if (!raw) return [];
750
+ return Array.isArray(raw) ? raw : [raw];
751
+ })();
752
+
735
753
  // Always ensure the standardized layout exists. The pair programmer takes
736
754
  // over development across the company, so every task — regardless of mode —
737
755
  // must land in a workspace that has CODING_RULES.md and the state files.
738
756
  // Scaffolding is idempotent; existing files are preserved.
739
- const scaffoldResult = scaffoldStateLayout(workspace);
757
+ const scaffoldResult = scaffoldStateLayout(workspace, { productSlug, projectSlug, engineerId });
740
758
 
741
759
  // Mode-specific preconditions: resume / session-end need real state. If the
742
760
  // workspace was just scaffolded for them, the placeholder STATUS/HANDOFF
@@ -822,25 +840,6 @@ async function run() {
822
840
  process.exit(1);
823
841
  }
824
842
 
825
- // Resolve operator identity for the session record. CLI flags win, then
826
- // ~/.knoxis/config.json (where the local-agent stores userId), then null.
827
- const knoxisConfig = (() => {
828
- const p = path.join(os.homedir(), '.knoxis', 'config.json');
829
- try { return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}; } catch (e) { return {}; }
830
- })();
831
- const engineerId = args['engineer-id'] || knoxisConfig.userId || null;
832
- const productSlug = args['product-slug'] || knoxisConfig.productSlug || null;
833
- const projectSlug = args['project-slug'] || knoxisConfig.projectSlug || path.basename(workspace);
834
- // QIG portal linkage — passed by the local-agent when the caller (e.g.
835
- // PairProgramSheet) supplies them.
836
- const userId = args['user-id'] || knoxisConfig.userId || null;
837
- const workspaceIdArg = args['workspace-id'] || null;
838
- const taskIdsArg = (() => {
839
- const raw = args['task-id'];
840
- if (!raw) return [];
841
- return Array.isArray(raw) ? raw : [raw];
842
- })();
843
-
844
843
  console.log('==============================================');
845
844
  console.log('Knoxis Pair Programming Session');
846
845
  console.log(`Workspace: ${workspace}`);
@@ -859,6 +858,9 @@ async function run() {
859
858
  if (scaffoldResult.skipped.length) {
860
859
  console.log(`Existing files preserved: ${scaffoldResult.skipped.join(', ')}`);
861
860
  }
861
+ if (scaffoldResult.productContext && scaffoldResult.productContext.created) {
862
+ console.log(` + file ${scaffoldResult.productContext.path} (Phase -1 skeleton — fill in via kickoff)`);
863
+ }
862
864
  console.log('');
863
865
  }
864
866
 
@@ -7,6 +7,8 @@ const { execSync } = require('child_process');
7
7
  const util = require('util');
8
8
  const { exec } = require('child_process');
9
9
 
10
+ const FRAMEWORK_VERSION = require('./framework-version');
11
+
10
12
  const SESSIONS_DIR = path.join(os.homedir(), '.knoxis', 'sessions');
11
13
 
12
14
  function ensureSessionDir() {
@@ -65,6 +67,31 @@ function readStateFiles(workspace) {
65
67
  return out;
66
68
  }
67
69
 
70
+ // Snapshot product.local.json from the project's parent directory (per
71
+ // kickoff v7 Phase -1) so the session record carries product context up to
72
+ // the portal. Multiple projects under one product share this file; the
73
+ // snapshot also lets the frontend visualize how directories and repos under
74
+ // a product are connected.
75
+ function readProductContext(workspace) {
76
+ if (!workspace) return null;
77
+ const productFile = path.join(path.dirname(workspace), 'product.local.json');
78
+ try {
79
+ const stat = fs.statSync(productFile);
80
+ if (!stat.isFile()) return null;
81
+ if (stat.size > MAX_STATE_FILE_BYTES) {
82
+ return { path: productFile, data: null, error: `File ${stat.size} bytes exceeds ${MAX_STATE_FILE_BYTES}-byte cap` };
83
+ }
84
+ const raw = fs.readFileSync(productFile, 'utf8');
85
+ try {
86
+ return { path: productFile, data: JSON.parse(raw) };
87
+ } catch (e) {
88
+ return { path: productFile, data: null, error: `JSON parse error: ${e.message}` };
89
+ }
90
+ } catch (e) {
91
+ return null;
92
+ }
93
+ }
94
+
68
95
  // Default git-call helpers — return null on any failure (callers want a string
69
96
  // or null). stdio: 'pipe' prevents stderr from leaking to the parent process
70
97
  // (e.g. "fatal: Needed a single revision" when the repo has no commits).
@@ -223,6 +250,7 @@ class SessionRecorder {
223
250
  return {
224
251
  sessionId: this.sessionId,
225
252
  version: '1.1.0',
253
+ frameworkVersion: FRAMEWORK_VERSION,
226
254
  mode: this.mode,
227
255
  archetype: this.archetype,
228
256
  productSlug: this.productSlug,
@@ -246,6 +274,9 @@ class SessionRecorder {
246
274
  // display the latest STATUS / HANDOFF / DECISIONS / etc. without
247
275
  // needing live filesystem access on the dev's machine.
248
276
  stateFiles: readStateFiles(this.workspace),
277
+ // Snapshot of product.local.json from the parent directory so the
278
+ // frontend can render product context alongside the session record.
279
+ productContext: readProductContext(this.workspace),
249
280
  decisionsLogged: [],
250
281
  waiversRequested: [],
251
282
  incidentsFlagged: [],
@@ -266,5 +297,7 @@ module.exports = {
266
297
  SESSIONS_DIR,
267
298
  ensureSessionDir,
268
299
  slugify,
269
- extractFilesTouched
300
+ extractFilesTouched,
301
+ readStateFiles,
302
+ readProductContext
270
303
  };
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { codingRuleset } = require('./templates');
6
6
 
7
- // Standardized layout per the kit (docs/Pair Programmer Docs/01-kickoff.md Phase 0.3).
7
+ // Standardized layout per the framework (docs/Pair Programmer Docs/pair-programmer/framework/01-kickoff.md Phase 0.3, v7).
8
8
  const STATE_LAYOUT = {
9
9
  dirs: [
10
10
  'docs/product',
@@ -46,8 +46,29 @@ const STATE_LAYOUT = {
46
46
  overwrite: false
47
47
  },
48
48
  {
49
+ // Three sections: Pending Answers (kickoff/resume interview questions
50
+ // awaiting operator input — the question bus), Active (project-level
51
+ // unresolved questions), Resolved. Kickoff/resume detect "Pending
52
+ // Answers" with `_(fill in)_` placeholders to decide whether to wait
53
+ // or proceed.
49
54
  path: 'docs/state/OPEN_QUESTIONS.md',
50
- content: () => `# Open Questions\n\n## Active\n\n## Resolved\n`,
55
+ content: () => `# Open Questions
56
+
57
+ > **How this file works**
58
+ >
59
+ > - \`## Pending Answers\` — kickoff and resume use this section as a question bus. When the AI needs operator input, it appends numbered entries here with \`**Answer:** _(fill in)_\` placeholders.
60
+ > - To answer: open this file, replace each \`_(fill in)_\` with your answer (plain text, any length), save, then re-run the same kickoff/resume command. The AI consumes the answers, generates the framework files, and moves the entries to \`## Resolved\`.
61
+ > - \`## Active\` — project-level questions you want to track over time but that don't block this session.
62
+ > - \`## Resolved\` — answered questions, kept for history.
63
+
64
+ ## Pending Answers
65
+
66
+ _(Empty — kickoff or resume will populate this when it needs your input.)_
67
+
68
+ ## Active
69
+
70
+ ## Resolved
71
+ `,
51
72
  overwrite: false
52
73
  },
53
74
  {
@@ -68,13 +89,13 @@ function ensureDir(p) {
68
89
  }
69
90
  }
70
91
 
71
- function scaffoldStateLayout(workspace) {
92
+ function scaffoldStateLayout(workspace, opts = {}) {
72
93
  if (!workspace) {
73
94
  throw new Error('scaffoldStateLayout: workspace path required');
74
95
  }
75
96
  ensureDir(workspace);
76
97
 
77
- const created = { dirs: [], files: [], skipped: [] };
98
+ const created = { dirs: [], files: [], skipped: [], productContext: null };
78
99
 
79
100
  for (const rel of STATE_LAYOUT.dirs) {
80
101
  const abs = path.join(workspace, rel);
@@ -95,9 +116,50 @@ function scaffoldStateLayout(workspace) {
95
116
  created.files.push(spec.path);
96
117
  }
97
118
 
119
+ // Phase -1 product context (kickoff v7). The framework places product.local.json
120
+ // in the product parent directory — the project's parent. Scaffold a TBD skeleton
121
+ // only when a product slug is known and no file already exists; never overwrite.
122
+ // Kickoff Phase -1 fills in the real values interactively.
123
+ created.productContext = scaffoldProductContext(workspace, opts);
124
+
98
125
  return created;
99
126
  }
100
127
 
128
+ function scaffoldProductContext(workspace, { productSlug, projectSlug, engineerId } = {}) {
129
+ if (!productSlug) return null;
130
+ const productDir = path.dirname(workspace);
131
+ const productFile = path.join(productDir, 'product.local.json');
132
+ if (fs.existsSync(productFile)) {
133
+ return { path: productFile, created: false, reason: 'exists' };
134
+ }
135
+ let canWrite = false;
136
+ try {
137
+ fs.accessSync(productDir, fs.constants.W_OK);
138
+ canWrite = true;
139
+ } catch (e) {
140
+ canWrite = false;
141
+ }
142
+ if (!canWrite) {
143
+ return { path: productFile, created: false, reason: 'parent-unwritable' };
144
+ }
145
+ const skeleton = {
146
+ product_id: productSlug,
147
+ name: '<TBD>',
148
+ slug: productSlug,
149
+ internal_classification: '<TBD: low|medium|high>',
150
+ compliance_engineered_for: [],
151
+ approved_agents: [],
152
+ denied_tools: [],
153
+ parent_directory_convention: productDir,
154
+ product_owner_id: null,
155
+ operator: { id: engineerId || null, name: null },
156
+ established_at: new Date().toISOString(),
157
+ _note: 'Skeleton written by knoxis-helper. Run kickoff Phase -1 to fill in real values.'
158
+ };
159
+ fs.writeFileSync(productFile, JSON.stringify(skeleton, null, 2) + '\n', 'utf8');
160
+ return { path: productFile, created: true, projectSlug: projectSlug || null };
161
+ }
162
+
101
163
  // Minimum files required for a given mode. Resume and session-end need state files
102
164
  // to exist; kickoff has no preconditions; recovery is mode-agnostic.
103
165
  const REQUIRED_FILES_BY_MODE = {
@@ -127,6 +189,7 @@ function assertStateLayout(workspace, mode) {
127
189
 
128
190
  module.exports = {
129
191
  scaffoldStateLayout,
192
+ scaffoldProductContext,
130
193
  assertStateLayout,
131
194
  STATE_LAYOUT
132
195
  };