project-knowledge 0.1.0

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INDEX.md +53 -0
  3. package/README.md +79 -0
  4. package/_site/README.md +63 -0
  5. package/_site/_test/ai-profile-test.js +199 -0
  6. package/_site/_test/baseline-schema-test.js +132 -0
  7. package/_site/_test/commit-analysis-test.js +184 -0
  8. package/_site/_test/context-pack-test.js +199 -0
  9. package/_site/_test/draft-apply-test.js +363 -0
  10. package/_site/_test/git-validation-test.js +171 -0
  11. package/_site/_test/hook-trigger-test.js +257 -0
  12. package/_site/_test/initial-analysis-test.js +228 -0
  13. package/_site/_test/job-orchestrator-test.js +297 -0
  14. package/_site/_test/kb-v2-templates-test.js +189 -0
  15. package/_site/_test/pr-consumer-contract-test.js +236 -0
  16. package/_site/_test/run-all-tests.js +135 -0
  17. package/_site/_test/scanner-test.js +206 -0
  18. package/_site/_test/ui-smoke-test.js +237 -0
  19. package/_site/_test/ui-test.js +237 -0
  20. package/_site/index.html +1166 -0
  21. package/_site/lib/ai-adapter.js +287 -0
  22. package/_site/lib/analysis-orchestrator.js +433 -0
  23. package/_site/lib/context-pack-builder.js +290 -0
  24. package/_site/lib/draft-apply.js +219 -0
  25. package/_site/lib/git-runner.js +26 -0
  26. package/_site/lib/hook-manager.js +148 -0
  27. package/_site/lib/job-orchestrator.js +231 -0
  28. package/_site/lib/kb-validator.js +224 -0
  29. package/_site/lib/llm-client.js +126 -0
  30. package/_site/lib/scanner.js +94 -0
  31. package/_site/scripts/hook-trigger.js +133 -0
  32. package/_site/scripts/safe-runner.js +151 -0
  33. package/_site/server.js +1058 -0
  34. package/_site/start.bat +26 -0
  35. package/_site/stop.bat +11 -0
  36. package/ai-profiles.json +18 -0
  37. package/docs/ai-knowledge-base-system-design.md +395 -0
  38. package/docs/pr-consumer-contract.md +198 -0
  39. package/docs/project-goal.md +72 -0
  40. package/docs/project-registry-schema.md +46 -0
  41. package/docs/testing-strategy.md +169 -0
  42. package/iterations.json +23 -0
  43. package/package.json +47 -0
  44. package/scripts/gen-commit-doc.ps1 +178 -0
  45. package/scripts/gen-commit-doc.sh +197 -0
  46. package/scripts/list-features.ps1 +41 -0
  47. package/scripts/register-scheduled-task.bat +5 -0
  48. package/templates/change.md +59 -0
  49. package/templates/commit-feature.md +56 -0
  50. package/templates/feature.md +44 -0
  51. package/templates/framework.md +80 -0
  52. package/templates/index-header.md +3 -0
  53. package/templates/kb-manifest.json +38 -0
  54. package/templates/module.md +58 -0
  55. package/templates/project-analysis.md +48 -0
  56. package/templates/project-goal.md +55 -0
  57. package/templates/project-readme.md +60 -0
  58. package/templates/quality-review-rules.md +37 -0
  59. package/templates/update-entry.md +7 -0
@@ -0,0 +1,184 @@
1
+ // TASK-008: Incremental commit analysis test
2
+ // Run: node _site/_test/commit-analysis-test.js
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+ const { makeRepo, git } = require('./fixtures/make-git-repos');
7
+ const { runCommitAnalysis, readRun, listDrafts } = require('../lib/analysis-orchestrator');
8
+
9
+ const ROOT = path.resolve(__dirname, '..', '..');
10
+ const SERVER = path.join(ROOT, '_site', 'server.js');
11
+ const PROJECTS_JSON = path.join(ROOT, 'projects.json');
12
+ const PORT = process.env.KB_COMMIT_TEST_PORT || '7798';
13
+ const BASE_URL = `http://127.0.0.1:${PORT}`;
14
+ const TEMP_SLUG = 'task-008-temp';
15
+
16
+ function assert(cond, msg) { if (!cond) throw new Error(msg); }
17
+
18
+ async function waitForServer() {
19
+ const deadline = Date.now() + 15000;
20
+ let lastError;
21
+ while (Date.now() < deadline) {
22
+ try {
23
+ const res = await fetch(`${BASE_URL}/api/state`);
24
+ if (res.ok) return;
25
+ lastError = new Error(`HTTP ${res.status}`);
26
+ } catch (e) { lastError = e; }
27
+ await new Promise(r => setTimeout(r, 250));
28
+ }
29
+ throw lastError || new Error('server did not start');
30
+ }
31
+
32
+ async function json(method, url, body) {
33
+ const res = await fetch(`${BASE_URL}${url}`, {
34
+ method,
35
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
36
+ body: body ? JSON.stringify(body) : undefined,
37
+ });
38
+ const text = await res.text();
39
+ let data = {};
40
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
41
+ return { res, data };
42
+ }
43
+
44
+ async function cleanup() {
45
+ const base = path.join(ROOT, 'projects', TEMP_SLUG);
46
+ fs.rmSync(base, { recursive: true, force: true });
47
+ const cur = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
48
+ if (cur[TEMP_SLUG]) {
49
+ delete cur[TEMP_SLUG];
50
+ fs.writeFileSync(PROJECTS_JSON, JSON.stringify(cur, null, 2) + '\n', 'utf-8');
51
+ }
52
+ }
53
+
54
+ (async () => {
55
+ // Unit: feature commit creates feature + change drafts
56
+ const repo = makeRepo({ kind: 'feature-commit' });
57
+ const kbBase = path.join(ROOT, 'projects', TEMP_SLUG);
58
+ fs.mkdirSync(kbBase, { recursive: true });
59
+ fs.writeFileSync(path.join(kbBase, 'project-goal.md'), '# Goal — preserve\n');
60
+ fs.mkdirSync(path.join(kbBase, 'features'), { recursive: true });
61
+
62
+ const project = {
63
+ slug: TEMP_SLUG,
64
+ kbPath: kbBase,
65
+ gitPath: repo.path,
66
+ localPath: repo.path,
67
+ aiProfileId: 'mock-agent',
68
+ };
69
+
70
+ let r = await runCommitAnalysis(project);
71
+ assert(r.ok, `commit analysis should succeed: ${r.error || ''}`);
72
+ assert(r.runRecord.commitCount === 2, 'fixture has 2 commits, run should see 2');
73
+ assert(r.runRecord.status === 'succeeded', 'run should be succeeded');
74
+
75
+ // 1. New-feature commit (the second of the two) should produce a feature draft
76
+ const drafts = listDrafts(kbBase, r.runId);
77
+ const changeDrafts = drafts.filter(d => d.path.startsWith('changes/'));
78
+ const featureDrafts = drafts.filter(d => d.path.startsWith('features/'));
79
+ assert(changeDrafts.length === 2, `expected 2 change drafts, got ${changeDrafts.length}`);
80
+ assert(featureDrafts.length >= 1, `expected at least 1 feature draft, got ${featureDrafts.length}`);
81
+
82
+ // 2. Existing pre-existing project-goal.md is preserved
83
+ const goalText = fs.readFileSync(path.join(kbBase, 'project-goal.md'), 'utf-8');
84
+ assert(goalText.includes('preserve'), 'pre-existing project-goal.md must not be overwritten');
85
+
86
+ // 3. lastAnalyzedCommit is unchanged in the project input
87
+ assert(r.runRecord.lastAnalyzedCommitBefore === null, 'before-state was null');
88
+
89
+ // 4. Refactor commit explains architecture impact
90
+ const repo2 = makeRepo({ kind: 'refactor-commit' });
91
+ const kbBase2 = path.join(ROOT, 'projects', TEMP_SLUG + '-refactor');
92
+ fs.mkdirSync(kbBase2, { recursive: true });
93
+ fs.writeFileSync(path.join(kbBase2, 'project-goal.md'), '# Goal\n');
94
+ const project2 = { ...project, slug: TEMP_SLUG + '-refactor', kbPath: kbBase2, gitPath: repo2.path, localPath: repo2.path };
95
+ r = await runCommitAnalysis(project2);
96
+ assert(r.ok, 'refactor analysis should succeed');
97
+ const refactorChanges = (r.runRecord.drafts || []).filter(d => d.path.startsWith('changes/'));
98
+ assert(refactorChanges.length === 2, 'should draft both refactor commits');
99
+ // The refactor's change draft should reference architecture or modules
100
+ const changePath = path.join(kbBase2, '_ai', 'drafts', r.runId, 'changes', `${(repo2.commits[0].short || repo2.commits[0].hash).slice(0, 7)}.md`);
101
+ if (fs.existsSync(changePath)) {
102
+ const txt = fs.readFileSync(changePath, 'utf-8');
103
+ assert(txt.includes('classification') || txt.includes('refactor') || txt.includes('AI Proposal'),
104
+ 'refactor change draft should reference classification');
105
+ }
106
+ repo2.cleanup();
107
+ fs.rmSync(kbBase2, { recursive: true, force: true });
108
+
109
+ // 5. Test-only commit does NOT invent a user feature
110
+ const repo3 = makeRepo({ kind: 'pure-test-only' });
111
+ const kbBase3 = path.join(ROOT, 'projects', TEMP_SLUG + '-test');
112
+ fs.mkdirSync(kbBase3, { recursive: true });
113
+ fs.writeFileSync(path.join(kbBase3, 'project-goal.md'), '# Goal\n');
114
+ const project3 = { ...project, slug: TEMP_SLUG + '-test', kbPath: kbBase3, gitPath: repo3.path, localPath: repo3.path };
115
+ r = await runCommitAnalysis(project3);
116
+ assert(r.ok, 'test analysis should succeed');
117
+ const testFeatureDrafts = listDrafts(kbBase3, r.runId).filter(d => d.path.startsWith('features/'));
118
+ assert(testFeatureDrafts.length === 0, `test-only commit must NOT produce a feature draft, got ${testFeatureDrafts.length}`);
119
+ repo3.cleanup();
120
+ fs.rmSync(kbBase3, { recursive: true, force: true });
121
+
122
+ // 6. Failed analysis leaves lastAnalyzedCommit unchanged
123
+ // We can't easily make the mock fail; simulate by giving an invalid project path.
124
+ const badProject = { ...project, gitPath: 'D:\\__no_such_repo__' };
125
+ r = await runCommitAnalysis(badProject);
126
+ assert(!r.ok, 'bad git path should fail');
127
+ // The bad project's projects.json is untouched.
128
+
129
+ repo.cleanup();
130
+ fs.rmSync(kbBase, { recursive: true, force: true });
131
+
132
+ // 7. Server tests
133
+ const child = spawn(process.execPath, [SERVER], {
134
+ cwd: ROOT,
135
+ env: { ...process.env, KB_SITE_PORT: PORT, KB_SITE_HOST: '127.0.0.1' },
136
+ stdio: ['ignore', 'pipe', 'pipe'],
137
+ windowsHide: true,
138
+ });
139
+ let serverOutput = '';
140
+ child.stdout.on('data', d => { serverOutput += d.toString(); });
141
+ child.stderr.on('data', d => { serverOutput += d.toString(); });
142
+
143
+ try {
144
+ await cleanup();
145
+ await waitForServer();
146
+
147
+ const repo4 = makeRepo({ kind: 'multi-commit' });
148
+ const slug = TEMP_SLUG;
149
+ const kbPath = path.join(ROOT, 'projects', slug);
150
+ fs.mkdirSync(kbPath, { recursive: true });
151
+ fs.writeFileSync(path.join(kbPath, 'project-goal.md'), '# Goal — server\n');
152
+
153
+ r = await json('PUT', '/api/projects', {
154
+ slug,
155
+ config: { displayName: 'TASK-008', localPath: repo4.path, gitPath: repo4.path, kbPath },
156
+ });
157
+ assert(r.res.ok, 'upsert should succeed');
158
+
159
+ r = await json('POST', `/api/projects/${slug}/analyze/commits`, {});
160
+ assert(r.res.ok, `analyze commits should succeed: ${JSON.stringify(r.data)}`);
161
+ assert(r.data.runId, 'should return runId');
162
+ assert(r.data.run && r.data.run.status === 'succeeded', 'run should succeed');
163
+ const serverRunId = r.data.runId;
164
+
165
+ // Drafts should be on disk
166
+ r = await json('GET', `/api/projects/${slug}/drafts/${serverRunId}`);
167
+ assert(r.res.ok, 'list drafts should succeed');
168
+ assert(r.data.drafts.length >= 3, `expected 3+ drafts (2 changes + 1 feature), got ${r.data.drafts.length}`);
169
+
170
+ // 8. Bad slug
171
+ r = await json('POST', '/api/projects/INVALID../analyze/commits', {});
172
+ assert(!r.res.ok && r.res.status === 400, 'bad slug should 400');
173
+
174
+ repo4.cleanup();
175
+ console.log('TASK-008 commit analysis test passed');
176
+ } catch (e) {
177
+ console.error('TASK-008 commit analysis test failed:', e.message);
178
+ if (serverOutput) console.error(serverOutput);
179
+ process.exitCode = 1;
180
+ } finally {
181
+ await cleanup().catch(() => {});
182
+ child.kill();
183
+ }
184
+ })();
@@ -0,0 +1,199 @@
1
+ // TASK-006: Context pack builder test
2
+ // Run: node _site/_test/context-pack-test.js
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+ const { makeRepo, git } = require('./fixtures/make-git-repos');
7
+ const { buildContextPack, isSafePath, PACKAGE_CONFIG_FILES } = require('../lib/context-pack-builder');
8
+
9
+ const ROOT = path.resolve(__dirname, '..', '..');
10
+ const SERVER = path.join(ROOT, '_site', 'server.js');
11
+ const PROJECTS_JSON = path.join(ROOT, 'projects.json');
12
+ const PORT = process.env.KB_CTX_TEST_PORT || '7796';
13
+ const BASE_URL = `http://127.0.0.1:${PORT}`;
14
+ const TEMP_SLUG = 'task-006-temp';
15
+
16
+ function assert(cond, msg) { if (!cond) throw new Error(msg); }
17
+
18
+ async function waitForServer() {
19
+ const deadline = Date.now() + 15000;
20
+ let lastError;
21
+ while (Date.now() < deadline) {
22
+ try {
23
+ const res = await fetch(`${BASE_URL}/api/state`);
24
+ if (res.ok) return;
25
+ lastError = new Error(`HTTP ${res.status}`);
26
+ } catch (e) { lastError = e; }
27
+ await new Promise(r => setTimeout(r, 250));
28
+ }
29
+ throw lastError || new Error('server did not start');
30
+ }
31
+
32
+ async function json(method, url, body) {
33
+ const res = await fetch(`${BASE_URL}${url}`, {
34
+ method,
35
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
36
+ body: body ? JSON.stringify(body) : undefined,
37
+ });
38
+ const text = await res.text();
39
+ let data = {};
40
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
41
+ return { res, data };
42
+ }
43
+
44
+ async function cleanup() {
45
+ const base = path.join(ROOT, 'projects', TEMP_SLUG);
46
+ fs.rmSync(base, { recursive: true, force: true });
47
+ const cur = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
48
+ if (cur[TEMP_SLUG]) {
49
+ delete cur[TEMP_SLUG];
50
+ fs.writeFileSync(PROJECTS_JSON, JSON.stringify(cur, null, 2) + '\n', 'utf-8');
51
+ }
52
+ }
53
+
54
+ (async () => {
55
+ // 1. Path safety
56
+ assert(isSafePath('/tmp/proj', 'a/b.md'), 'safe path should pass');
57
+ assert(!isSafePath('/tmp/proj', '../escape.md'), 'parent traversal should fail');
58
+ assert(!isSafePath('/tmp/proj', '/etc/passwd'), 'absolute outside should fail');
59
+ // Sibling that is not a sub-path: resolve it relative to the project root and check.
60
+ const siblingResolved = require('path').resolve('/tmp/proj', '../proj-other/x.md');
61
+ assert(!siblingResolved.startsWith('/tmp/proj/') && siblingResolved !== '/tmp/proj',
62
+ 'sibling-of-project should not pass when constructed as a traversal');
63
+ assert(Array.isArray(PACKAGE_CONFIG_FILES) && PACKAGE_CONFIG_FILES.includes('package.json'), 'package config list ok');
64
+
65
+ // 2. Unit test: build a context pack for a feature-commit fixture
66
+ const repo = makeRepo({ kind: 'feature-commit' });
67
+ const kbBase = path.join(ROOT, 'projects', TEMP_SLUG);
68
+ fs.mkdirSync(kbBase, { recursive: true });
69
+ // Lay down v2 structure with project-goal.md and a feature doc that references a changed file
70
+ fs.mkdirSync(path.join(kbBase, 'features'), { recursive: true });
71
+ fs.mkdirSync(path.join(kbBase, 'modules'), { recursive: true });
72
+ fs.writeFileSync(path.join(kbBase, 'project-goal.md'), '# Goal — test\n\nGoal text for tests.\n');
73
+ fs.writeFileSync(path.join(kbBase, 'project-analysis.md'), '# Analysis — test\n');
74
+ fs.writeFileSync(path.join(kbBase, 'features', 'feature.md'), 'See `src/feature.ts` for the implementation.\n');
75
+ fs.writeFileSync(path.join(kbBase, 'README.md'), '# Project — test\n');
76
+ // package.json lives in the source repo (the fixture already has a src/, but no package.json)
77
+ fs.writeFileSync(path.join(repo.path, 'package.json'), '{"name":"fixture"}');
78
+
79
+ const commits = repo.commits.slice().reverse(); // chronological
80
+ const pack = await buildContextPack({
81
+ project: {
82
+ slug: TEMP_SLUG,
83
+ kbPath: kbBase,
84
+ gitPath: repo.path,
85
+ localPath: repo.path,
86
+ },
87
+ runId: 'run-unit-1',
88
+ trigger: 'commits',
89
+ commits,
90
+ });
91
+
92
+ assert(pack.schema === 'context-pack/v1', 'pack schema should be v1');
93
+ assert(pack.runId === 'run-unit-1', 'runId should be honored');
94
+ assert(pack.trigger === 'commits', 'trigger should be commits');
95
+ assert(pack.commitCount === commits.length, 'commitCount should match');
96
+ assert(pack.range, 'range should be set');
97
+
98
+ // Required entries
99
+ const goalEntry = pack.entries.find(e => e.path === 'project-goal.md');
100
+ assert(goalEntry, 'pack should include project-goal.md');
101
+ assert(goalEntry.kind === 'goal', 'goal entry kind should be goal');
102
+ assert(goalEntry.reason, 'goal entry should have a reason');
103
+ const pkg = pack.entries.find(e => e.path === 'package.json');
104
+ assert(pkg && pkg.kind === 'package-config', 'pack should include package.json as package-config');
105
+ const changed = pack.entries.find(e => e.path === 'src/feature.ts');
106
+ assert(changed && changed.kind === 'git-changed', 'pack should include changed file src/feature.ts');
107
+ const related = pack.entries.find(e => e.path === 'features/feature.md');
108
+ assert(related && related.kind === 'feature-doc', 'pack should include related feature doc');
109
+ // Every entry must have a reason
110
+ for (const e of pack.entries) {
111
+ assert(typeof e.reason === 'string' && e.reason.length > 0, `entry ${e.path} missing reason`);
112
+ }
113
+
114
+ // Disk artifacts
115
+ const onDisk = JSON.parse(fs.readFileSync(path.join(kbBase, '_ai', 'context-packs', 'run-unit-1', 'context-pack.json'), 'utf-8'));
116
+ assert(onDisk.runId === 'run-unit-1', 'on-disk runId should match');
117
+
118
+ // 3. Large / binary file is summarized or skipped
119
+ const repo2 = makeRepo({ kind: 'binary-commit' });
120
+ const kbBase2 = path.join(ROOT, 'projects', TEMP_SLUG + '-2');
121
+ fs.mkdirSync(kbBase2, { recursive: true });
122
+ fs.writeFileSync(path.join(kbBase2, 'project-goal.md'), '# Goal\n');
123
+ const commits2 = repo2.commits.slice().reverse();
124
+ const pack2 = await buildContextPack({
125
+ project: { slug: TEMP_SLUG + '-2', kbPath: kbBase2, gitPath: repo2.path, localPath: repo2.path },
126
+ runId: 'run-binary-1',
127
+ trigger: 'commits',
128
+ commits: commits2,
129
+ });
130
+ const big = pack2.entries.find(e => e.path === 'big.bin');
131
+ if (big) {
132
+ assert(big.binary || big.skipped === 'too-large' || big.truncated, 'big file should be marked binary/skipped/truncated');
133
+ }
134
+ fs.rmSync(kbBase2, { recursive: true, force: true });
135
+ repo2.cleanup();
136
+
137
+ // 4. Path-traversal attempt is rejected
138
+ const ok = isSafePath(kbBase, '..\\..\\escape.md');
139
+ // On Windows, ..\\ becomes normalized. The resolved path should still be inside kbBase.
140
+ // Test the explicit scenario:
141
+ assert(!isSafePath(kbBase, '..\\..\\..\\..\\Windows\\evil.txt') || path.resolve(kbBase, '..\\..\\..\\..\\Windows\\evil.txt').startsWith(path.resolve(kbBase)),
142
+ 'path traversal must be rejected');
143
+
144
+ repo.cleanup();
145
+
146
+ // 5. Server tests
147
+ assert(fs.existsSync(SERVER), 'server.js missing');
148
+ JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
149
+ const child = spawn(process.execPath, [SERVER], {
150
+ cwd: ROOT,
151
+ env: { ...process.env, KB_SITE_PORT: PORT, KB_SITE_HOST: '127.0.0.1' },
152
+ stdio: ['ignore', 'pipe', 'pipe'],
153
+ windowsHide: true,
154
+ });
155
+ let serverOutput = '';
156
+ child.stdout.on('data', d => { serverOutput += d.toString(); });
157
+ child.stderr.on('data', d => { serverOutput += d.toString(); });
158
+
159
+ try {
160
+ await cleanup();
161
+ await waitForServer();
162
+
163
+ // 6. POST /api/projects/:slug/context-pack for an initial pack
164
+ const initRepo = makeRepo({ kind: 'one-commit' });
165
+ const slug = TEMP_SLUG;
166
+ const kbPath = path.join(ROOT, 'projects', slug);
167
+ fs.mkdirSync(kbPath, { recursive: true });
168
+ fs.writeFileSync(path.join(kbPath, 'project-goal.md'), '# Goal — server test\n');
169
+
170
+ r = await json('PUT', '/api/projects', {
171
+ slug,
172
+ config: { displayName: 'TASK-006', localPath: initRepo.path, gitPath: initRepo.path, kbPath },
173
+ });
174
+ assert(r.res.ok, 'upsert should succeed');
175
+
176
+ r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'initial' });
177
+ assert(r.res.ok, 'context-pack initial should succeed');
178
+ assert(r.data.contextPack.entries.length > 0, 'context pack should have entries');
179
+ assert(r.data.contextPack.entries.find(e => e.path === 'project-goal.md'), 'goal should be in pack');
180
+
181
+ r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'commits' });
182
+ assert(r.res.ok, 'context-pack commits should succeed');
183
+ assert(r.data.contextPack.commitCount > 0, 'commits trigger should report commits');
184
+
185
+ // 7. Bad slug
186
+ r = await json('POST', '/api/projects/INVALID../context-pack', {});
187
+ assert(!r.res.ok && r.res.status === 400, 'bad slug should 400');
188
+
189
+ initRepo.cleanup();
190
+ console.log('TASK-006 context pack test passed');
191
+ } catch (e) {
192
+ console.error('TASK-006 context pack test failed:', e.message);
193
+ if (serverOutput) console.error(serverOutput);
194
+ process.exitCode = 1;
195
+ } finally {
196
+ await cleanup().catch(() => {});
197
+ child.kill();
198
+ }
199
+ })();