knoxis-helper 1.6.2 → 1.6.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.
- package/bin/knoxis-helper.js +27 -0
- package/lib/framework-version.js +27 -0
- package/lib/knoxis-interactive-pair.js +75 -0
- package/lib/knoxis-pair-program.js +60 -20
- package/lib/session-recorder.js +34 -1
- package/lib/state-scaffold.js +45 -3
- package/lib/templates/coding-ruleset.js +190 -84
- package/lib/templates/kickoff.js +444 -75
- package/lib/templates/recovery.js +126 -37
- package/lib/templates/resume.js +150 -56
- package/lib/templates/session-end.js +271 -55
- package/package.json +1 -1
package/bin/knoxis-helper.js
CHANGED
|
@@ -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.0.0',
|
|
16
|
+
documents: {
|
|
17
|
+
overview: 1,
|
|
18
|
+
kickoff: 7,
|
|
19
|
+
resume: 3,
|
|
20
|
+
sessionEnd: 3,
|
|
21
|
+
recovery: 3,
|
|
22
|
+
codingRuleset: 3,
|
|
23
|
+
marketplaceMcp: 3,
|
|
24
|
+
migration: 3,
|
|
25
|
+
portalContract: '0.3'
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -289,6 +289,52 @@ function readIdentityFromEnv() {
|
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// === STATE-FILE UPDATE PROMPT ===
|
|
293
|
+
// Forces Claude to actually write docs/state/*.md before the session closes.
|
|
294
|
+
// The QIG dashboard reads these files; without this step they stay as scaffold
|
|
295
|
+
// placeholders and the dashboard tiles show "0 done" regardless of what
|
|
296
|
+
// happened in the session.
|
|
297
|
+
function buildUpdateStatePrompt(task) {
|
|
298
|
+
return [
|
|
299
|
+
"Now update the project's state files to reflect what just happened.",
|
|
300
|
+
"This is non-negotiable — the QIG dashboard reads these files and skipping them",
|
|
301
|
+
"means the team loses visibility on what was accomplished.",
|
|
302
|
+
"",
|
|
303
|
+
"**Write the files. Do not describe what you would put in them — actually write them.**",
|
|
304
|
+
"",
|
|
305
|
+
"1. **`docs/state/STATUS.md`** — overwrite. Update `_Last updated:` line. Then:",
|
|
306
|
+
" - `## Done`: one bullet per concrete thing completed in this session.",
|
|
307
|
+
" - `## In flight`: anything started but not finished. Empty if none.",
|
|
308
|
+
" - `## Next`: ONE concrete next step.",
|
|
309
|
+
" - `## Notes`: caveats. Empty if none.",
|
|
310
|
+
"",
|
|
311
|
+
"2. **`docs/state/HANDOFF.md`** — overwrite the whole file:",
|
|
312
|
+
" - `## Where I stopped`: 2-4 sentences (file, function, feature, state).",
|
|
313
|
+
" - `## Why I stopped here`: brief reason.",
|
|
314
|
+
" - `## First thing to do next session`: ONE concrete action with file:line if relevant.",
|
|
315
|
+
" - `## Landmines / gotchas`: anything surprising. \"None.\" if none.",
|
|
316
|
+
" - `## Environment state`: running services / env / branches. \"None.\" if none.",
|
|
317
|
+
"",
|
|
318
|
+
"3. **`docs/state/CHANGELOG.md`** — if code shipped or behavior changed, append a one-line bullet under `## [Unreleased]` in the right section (Added/Changed/Fixed/Removed). Skip if nothing shipped.",
|
|
319
|
+
"",
|
|
320
|
+
"4. **`docs/state/DECISIONS.md`** — if a non-trivial choice was made, append:",
|
|
321
|
+
" ```",
|
|
322
|
+
" ## <today's date>: <one-line title>",
|
|
323
|
+
" **Context:** <2 sentences>",
|
|
324
|
+
" **Decision:** <what you chose>",
|
|
325
|
+
" **Alternatives considered:** <what else and why not>",
|
|
326
|
+
" **Consequences:** <implications>",
|
|
327
|
+
" ```",
|
|
328
|
+
" Skip if no real decisions.",
|
|
329
|
+
"",
|
|
330
|
+
"5. **`docs/state/OPEN_QUESTIONS.md`** — if any genuine unresolved question emerged, append to `## Active`. Skip if none.",
|
|
331
|
+
"",
|
|
332
|
+
"Original task: " + task,
|
|
333
|
+
"",
|
|
334
|
+
"Write the files now. Then briefly summarize which files you wrote and which you skipped."
|
|
335
|
+
].join('\n');
|
|
336
|
+
}
|
|
337
|
+
|
|
292
338
|
// === RECORDER + PORTAL FINALIZATION ===
|
|
293
339
|
// Saves the session JSON locally and POSTs to the portal stub. Called from
|
|
294
340
|
// every exit path so partial / fallback sessions still surface in the UI.
|
|
@@ -517,6 +563,15 @@ async function main() {
|
|
|
517
563
|
appendLog('## Phase 3: Verification\n' + phase3b.stdout.substring(0, 5000) + '\n');
|
|
518
564
|
recorder.completeStep(phase3bIdx, phase3b.stdout, phase3b.code !== 0 ? `exit ${phase3b.code}` : null);
|
|
519
565
|
|
|
566
|
+
// Same Phase 4 (update state) as the normal path so the dashboard sees
|
|
567
|
+
// updates even when session-resume failed mid-flow.
|
|
568
|
+
const updateStatePromptFb = buildUpdateStatePrompt(task);
|
|
569
|
+
const phase4bIdx = recorder.startStep('update-state', 'Claude Code', updateStatePromptFb);
|
|
570
|
+
recorder.setStepPrompt(phase4bIdx, updateStatePromptFb);
|
|
571
|
+
const phase4b = await runClaudeTurn(updateStatePromptFb, true);
|
|
572
|
+
appendLog('## Phase 4: Update state files\n' + phase4b.stdout.substring(0, 5000) + '\n');
|
|
573
|
+
recorder.completeStep(phase4bIdx, phase4b.stdout, phase4b.code !== 0 ? `exit ${phase4b.code}` : null);
|
|
574
|
+
|
|
520
575
|
console.log('');
|
|
521
576
|
console.log('╔══════════════════════════════════════════════════════════════╗');
|
|
522
577
|
console.log('║ PAIR PROGRAMMING SESSION COMPLETE ║');
|
|
@@ -570,6 +625,26 @@ async function main() {
|
|
|
570
625
|
recorder.completeStep(phase3Idx, phase3.stdout, phase3.code !== 0 ? `exit ${phase3.code}` : null);
|
|
571
626
|
|
|
572
627
|
|
|
628
|
+
// ═══════════════════════════════════════════
|
|
629
|
+
// PHASE 4: UPDATE STATE FILES (non-skippable)
|
|
630
|
+
// The QIG dashboard reads docs/state/*.md from the captured session.
|
|
631
|
+
// Without this step, every dashboard tile shows "0 done" no matter what
|
|
632
|
+
// actually happened during the session.
|
|
633
|
+
// ═══════════════════════════════════════════
|
|
634
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
635
|
+
console.log(' PHASE 4: Update state files');
|
|
636
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
637
|
+
console.log('');
|
|
638
|
+
appendLog('## Phase 4: Update state files\n');
|
|
639
|
+
|
|
640
|
+
const updateStatePrompt = buildUpdateStatePrompt(task);
|
|
641
|
+
const phase4Idx = recorder.startStep('update-state', 'Claude Code', updateStatePrompt);
|
|
642
|
+
recorder.setStepPrompt(phase4Idx, updateStatePrompt);
|
|
643
|
+
const phase4 = await runClaudeTurn(updateStatePrompt, true);
|
|
644
|
+
appendLog(phase4.stdout.substring(0, 5000) + '\n');
|
|
645
|
+
recorder.completeStep(phase4Idx, phase4.stdout, phase4.code !== 0 ? `exit ${phase4.code}` : null);
|
|
646
|
+
|
|
647
|
+
|
|
573
648
|
// ═══════════════════════════════════════════
|
|
574
649
|
// DONE
|
|
575
650
|
// ═══════════════════════════════════════════
|
|
@@ -307,6 +307,44 @@ Quick checklist:
|
|
|
307
307
|
- Anything we should test?
|
|
308
308
|
|
|
309
309
|
Give me the summary and any follow-up recommendations.`
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
key: 'update-state',
|
|
313
|
+
title: 'Update state files',
|
|
314
|
+
template: ({ taskDescription }) => `Now update the project's state files to reflect what just happened. This is non-negotiable — the QIG dashboard reads these files and skipping them means the team loses visibility on what was accomplished.
|
|
315
|
+
|
|
316
|
+
**Write the files. Do not describe what you would put in them — actually write them.**
|
|
317
|
+
|
|
318
|
+
1. **\`docs/state/STATUS.md\`** — overwrite it. Update the \`_Last updated:\` line to today's date, then:
|
|
319
|
+
- \`## Done\`: one bullet per concrete thing completed in this session (specific file changes, features wired, bugs fixed). Be terse but specific.
|
|
320
|
+
- \`## In flight\`: anything started but not finished. Empty list if none.
|
|
321
|
+
- \`## Next\`: ONE concrete next step for the next session, not a list of wishes.
|
|
322
|
+
- \`## Notes\`: caveats, gotchas, or context that doesn't fit elsewhere. Empty list if none.
|
|
323
|
+
|
|
324
|
+
2. **\`docs/state/HANDOFF.md\`** — overwrite the whole file:
|
|
325
|
+
- \`## Where I stopped\`: 2-4 sentences. Concrete: file, function, feature, what state.
|
|
326
|
+
- \`## Why I stopped here\`: brief. Finished a chunk / blocked / end of task.
|
|
327
|
+
- \`## First thing to do next session\`: ONE concrete action, not "continue the feature." Reference an exact file:line if applicable.
|
|
328
|
+
- \`## Landmines / gotchas\`: anything that would surprise next-session-me. Write "None." if none.
|
|
329
|
+
- \`## Environment state\`: running services, env vars, open branches, anything not normal config. Write "None." if none.
|
|
330
|
+
|
|
331
|
+
3. **\`docs/state/CHANGELOG.md\`** — if code shipped or a meaningful behavior changed, append a one-line bullet under \`## [Unreleased]\` in the right section (Added / Changed / Fixed / Removed). If nothing shipped (planning, exploration), don't write anything to CHANGELOG.
|
|
332
|
+
|
|
333
|
+
4. **\`docs/state/DECISIONS.md\`** — if a non-trivial choice was made (which library, which pattern, which abstraction), append:
|
|
334
|
+
\`\`\`
|
|
335
|
+
## <today's date>: <one-line decision title>
|
|
336
|
+
**Context:** <2 sentences>
|
|
337
|
+
**Decision:** <what you chose>
|
|
338
|
+
**Alternatives considered:** <what else and why not>
|
|
339
|
+
**Consequences:** <what this implies going forward>
|
|
340
|
+
\`\`\`
|
|
341
|
+
If no real decisions were made, skip this file.
|
|
342
|
+
|
|
343
|
+
5. **\`docs/state/OPEN_QUESTIONS.md\`** — if any genuine unresolved question emerged (not a question you already answered), append to \`## Active\` as \`### N. <question>\`. Skip if no new questions.
|
|
344
|
+
|
|
345
|
+
The original task was: ${taskDescription}
|
|
346
|
+
|
|
347
|
+
Use that as the frame for what to write. Then briefly summarize which files you wrote and which you skipped (with reason).`
|
|
310
348
|
}
|
|
311
349
|
];
|
|
312
350
|
|
|
@@ -694,11 +732,29 @@ async function run() {
|
|
|
694
732
|
const userTask = (task || '').trim();
|
|
695
733
|
task = userTask || `[${mode || 'default'} session]`;
|
|
696
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
|
+
|
|
697
753
|
// Always ensure the standardized layout exists. The pair programmer takes
|
|
698
754
|
// over development across the company, so every task — regardless of mode —
|
|
699
755
|
// must land in a workspace that has CODING_RULES.md and the state files.
|
|
700
756
|
// Scaffolding is idempotent; existing files are preserved.
|
|
701
|
-
const scaffoldResult = scaffoldStateLayout(workspace);
|
|
757
|
+
const scaffoldResult = scaffoldStateLayout(workspace, { productSlug, projectSlug, engineerId });
|
|
702
758
|
|
|
703
759
|
// Mode-specific preconditions: resume / session-end need real state. If the
|
|
704
760
|
// workspace was just scaffolded for them, the placeholder STATUS/HANDOFF
|
|
@@ -784,25 +840,6 @@ async function run() {
|
|
|
784
840
|
process.exit(1);
|
|
785
841
|
}
|
|
786
842
|
|
|
787
|
-
// Resolve operator identity for the session record. CLI flags win, then
|
|
788
|
-
// ~/.knoxis/config.json (where the local-agent stores userId), then null.
|
|
789
|
-
const knoxisConfig = (() => {
|
|
790
|
-
const p = path.join(os.homedir(), '.knoxis', 'config.json');
|
|
791
|
-
try { return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}; } catch (e) { return {}; }
|
|
792
|
-
})();
|
|
793
|
-
const engineerId = args['engineer-id'] || knoxisConfig.userId || null;
|
|
794
|
-
const productSlug = args['product-slug'] || knoxisConfig.productSlug || null;
|
|
795
|
-
const projectSlug = args['project-slug'] || knoxisConfig.projectSlug || path.basename(workspace);
|
|
796
|
-
// QIG portal linkage — passed by the local-agent when the caller (e.g.
|
|
797
|
-
// PairProgramSheet) supplies them.
|
|
798
|
-
const userId = args['user-id'] || knoxisConfig.userId || null;
|
|
799
|
-
const workspaceIdArg = args['workspace-id'] || null;
|
|
800
|
-
const taskIdsArg = (() => {
|
|
801
|
-
const raw = args['task-id'];
|
|
802
|
-
if (!raw) return [];
|
|
803
|
-
return Array.isArray(raw) ? raw : [raw];
|
|
804
|
-
})();
|
|
805
|
-
|
|
806
843
|
console.log('==============================================');
|
|
807
844
|
console.log('Knoxis Pair Programming Session');
|
|
808
845
|
console.log(`Workspace: ${workspace}`);
|
|
@@ -821,6 +858,9 @@ async function run() {
|
|
|
821
858
|
if (scaffoldResult.skipped.length) {
|
|
822
859
|
console.log(`Existing files preserved: ${scaffoldResult.skipped.join(', ')}`);
|
|
823
860
|
}
|
|
861
|
+
if (scaffoldResult.productContext && scaffoldResult.productContext.created) {
|
|
862
|
+
console.log(` + file ${scaffoldResult.productContext.path} (Phase -1 skeleton — fill in via kickoff)`);
|
|
863
|
+
}
|
|
824
864
|
console.log('');
|
|
825
865
|
}
|
|
826
866
|
|
package/lib/session-recorder.js
CHANGED
|
@@ -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
|
};
|
package/lib/state-scaffold.js
CHANGED
|
@@ -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
|
|
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',
|
|
@@ -68,13 +68,13 @@ function ensureDir(p) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function scaffoldStateLayout(workspace) {
|
|
71
|
+
function scaffoldStateLayout(workspace, opts = {}) {
|
|
72
72
|
if (!workspace) {
|
|
73
73
|
throw new Error('scaffoldStateLayout: workspace path required');
|
|
74
74
|
}
|
|
75
75
|
ensureDir(workspace);
|
|
76
76
|
|
|
77
|
-
const created = { dirs: [], files: [], skipped: [] };
|
|
77
|
+
const created = { dirs: [], files: [], skipped: [], productContext: null };
|
|
78
78
|
|
|
79
79
|
for (const rel of STATE_LAYOUT.dirs) {
|
|
80
80
|
const abs = path.join(workspace, rel);
|
|
@@ -95,9 +95,50 @@ function scaffoldStateLayout(workspace) {
|
|
|
95
95
|
created.files.push(spec.path);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// Phase -1 product context (kickoff v7). The framework places product.local.json
|
|
99
|
+
// in the product parent directory — the project's parent. Scaffold a TBD skeleton
|
|
100
|
+
// only when a product slug is known and no file already exists; never overwrite.
|
|
101
|
+
// Kickoff Phase -1 fills in the real values interactively.
|
|
102
|
+
created.productContext = scaffoldProductContext(workspace, opts);
|
|
103
|
+
|
|
98
104
|
return created;
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
function scaffoldProductContext(workspace, { productSlug, projectSlug, engineerId } = {}) {
|
|
108
|
+
if (!productSlug) return null;
|
|
109
|
+
const productDir = path.dirname(workspace);
|
|
110
|
+
const productFile = path.join(productDir, 'product.local.json');
|
|
111
|
+
if (fs.existsSync(productFile)) {
|
|
112
|
+
return { path: productFile, created: false, reason: 'exists' };
|
|
113
|
+
}
|
|
114
|
+
let canWrite = false;
|
|
115
|
+
try {
|
|
116
|
+
fs.accessSync(productDir, fs.constants.W_OK);
|
|
117
|
+
canWrite = true;
|
|
118
|
+
} catch (e) {
|
|
119
|
+
canWrite = false;
|
|
120
|
+
}
|
|
121
|
+
if (!canWrite) {
|
|
122
|
+
return { path: productFile, created: false, reason: 'parent-unwritable' };
|
|
123
|
+
}
|
|
124
|
+
const skeleton = {
|
|
125
|
+
product_id: productSlug,
|
|
126
|
+
name: '<TBD>',
|
|
127
|
+
slug: productSlug,
|
|
128
|
+
internal_classification: '<TBD: low|medium|high>',
|
|
129
|
+
compliance_engineered_for: [],
|
|
130
|
+
approved_agents: [],
|
|
131
|
+
denied_tools: [],
|
|
132
|
+
parent_directory_convention: productDir,
|
|
133
|
+
product_owner_id: null,
|
|
134
|
+
operator: { id: engineerId || null, name: null },
|
|
135
|
+
established_at: new Date().toISOString(),
|
|
136
|
+
_note: 'Skeleton written by knoxis-helper. Run kickoff Phase -1 to fill in real values.'
|
|
137
|
+
};
|
|
138
|
+
fs.writeFileSync(productFile, JSON.stringify(skeleton, null, 2) + '\n', 'utf8');
|
|
139
|
+
return { path: productFile, created: true, projectSlug: projectSlug || null };
|
|
140
|
+
}
|
|
141
|
+
|
|
101
142
|
// Minimum files required for a given mode. Resume and session-end need state files
|
|
102
143
|
// to exist; kickoff has no preconditions; recovery is mode-agnostic.
|
|
103
144
|
const REQUIRED_FILES_BY_MODE = {
|
|
@@ -127,6 +168,7 @@ function assertStateLayout(workspace, mode) {
|
|
|
127
168
|
|
|
128
169
|
module.exports = {
|
|
129
170
|
scaffoldStateLayout,
|
|
171
|
+
scaffoldProductContext,
|
|
130
172
|
assertStateLayout,
|
|
131
173
|
STATE_LAYOUT
|
|
132
174
|
};
|