@sanqianx/project-knowledge 5.0.18 → 5.0.20-rc.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.
Files changed (209) hide show
  1. package/.agents/plugins/marketplace.json +3 -3
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +349 -349
  7. package/_modules/knowledge-engine/README.md +4 -0
  8. package/_modules/knowledge-engine/src/analysis-contract.js +4 -3
  9. package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
  10. package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
  11. package/_modules/knowledge-engine/src/coordinator.js +8 -0
  12. package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
  13. package/_modules/knowledge-engine/src/memories.js +10 -5
  14. package/_modules/knowledge-engine/src/memory-eligibility.js +74 -0
  15. package/_modules/knowledge-engine/src/retrieval-ranking.js +19 -1
  16. package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +1 -0
  17. package/_modules/knowledge-engine/src/runtime/commit-prompt.js +12 -9
  18. package/_modules/knowledge-engine/src/runtime/conversation-store.js +2 -0
  19. package/_modules/knowledge-engine/src/server.js +1 -1
  20. package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
  21. package/_site/_test/ai-profile-test.js +75 -75
  22. package/_site/_test/ai-vendor-presets-test.js +91 -91
  23. package/_site/_test/automatic-context-test.js +70 -70
  24. package/_site/_test/bin-cli-test.js +230 -230
  25. package/_site/_test/bridge-adapter-test.js +95 -95
  26. package/_site/_test/bridge-consumer-service-test.js +310 -310
  27. package/_site/_test/candidate-bin-content-test.js +35 -0
  28. package/_site/_test/candidate-product-assets-test.js +13 -0
  29. package/_site/_test/candidate-release-contract-test.js +14 -14
  30. package/_site/_test/capture-hook-dedup-test.js +158 -158
  31. package/_site/_test/claude-executable-discovery-test.js +28 -28
  32. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
  33. package/_site/_test/cli-instance-management-test.js +62 -62
  34. package/_site/_test/client-capture-restart-e2e-test.js +189 -189
  35. package/_site/_test/codex-conversation-projection-test.js +60 -60
  36. package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
  37. package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
  38. package/_site/_test/codex-notify-fanout-test.js +134 -134
  39. package/_site/_test/codex-notify-upgrade-test.js +253 -253
  40. package/_site/_test/commit-boundary-freeze-test.js +147 -147
  41. package/_site/_test/commit-conversation-binding-test.js +4 -4
  42. package/_site/_test/configuration-generation-test.js +22 -22
  43. package/_site/_test/context-pack-test.js +200 -200
  44. package/_site/_test/conversation-api-test.js +10 -10
  45. package/_site/_test/conversation-origin-test.js +15 -0
  46. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
  47. package/_site/_test/embedding-config-test.js +32 -32
  48. package/_site/_test/fixtures/fake-browser-exit.js +10 -10
  49. package/_site/_test/fixtures/fake-browser-hang.js +10 -10
  50. package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
  51. package/_site/_test/fixtures/make-git-repos.js +92 -92
  52. package/_site/_test/fixtures/retrieval-gold-queries.json +32 -0
  53. package/_site/_test/fresh-hook-reimport-test.js +53 -53
  54. package/_site/_test/fresh-product-data-test.js +56 -56
  55. package/_site/_test/git-path-compatibility-test.js +80 -80
  56. package/_site/_test/git-validation-test.js +81 -81
  57. package/_site/_test/hook-worktree-test.js +42 -42
  58. package/_site/_test/index-builder-test.js +41 -41
  59. package/_site/_test/index-error-display-contract-test.js +10 -10
  60. package/_site/_test/integration-adapters-test.js +67 -67
  61. package/_site/_test/integration-capture-setup-test.js +118 -118
  62. package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
  63. package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
  64. package/_site/_test/integration-runtime-binding-test.js +473 -473
  65. package/_site/_test/integration-status-scan-test.js +29 -29
  66. package/_site/_test/kb-framework-test.js +57 -57
  67. package/_site/_test/kbpath-follow-test.js +63 -63
  68. package/_site/_test/knowledge-db-test.js +144 -144
  69. package/_site/_test/knowledge-engine-client-test.js +109 -109
  70. package/_site/_test/knowledge-maintenance-test.js +40 -40
  71. package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
  72. package/_site/_test/knowledge-scopes-test.js +37 -37
  73. package/_site/_test/knowledge-storage-location-test.js +28 -28
  74. package/_site/_test/launcher-readiness-test.js +80 -80
  75. package/_site/_test/legacy-claude-session-alias-test.js +106 -106
  76. package/_site/_test/legacy-routes-removed-test.js +47 -47
  77. package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
  78. package/_site/_test/live-gateway-test.js +48 -48
  79. package/_site/_test/local-marketplace-test.js +16 -16
  80. package/_site/_test/log-redaction-test.js +58 -58
  81. package/_site/_test/markdown-maintenance-test.js +11 -11
  82. package/_site/_test/model-context-windows-test.js +89 -89
  83. package/_site/_test/module-agent-start-proxy-test.js +65 -65
  84. package/_site/_test/module-artifact-integrity-test.js +27 -27
  85. package/_site/_test/module-boundary-test.js +30 -30
  86. package/_site/_test/module-bridge-eventbridge-test.js +202 -202
  87. package/_site/_test/module-dynamic-port-test.js +118 -118
  88. package/_site/_test/module-index-proxy-test.js +33 -0
  89. package/_site/_test/module-model-configuration-test.js +44 -44
  90. package/_site/_test/module-process-lifecycle-test.js +35 -35
  91. package/_site/_test/module-stream-proxy-test.js +34 -34
  92. package/_site/_test/module-terminal-restart-test.js +63 -0
  93. package/_site/_test/module-wiring-unbind-race-test.js +59 -59
  94. package/_site/_test/native-context-integrations-test.js +86 -86
  95. package/_site/_test/orphaned-claim-recovery-test.js +80 -80
  96. package/_site/_test/package-boundary-test.js +43 -42
  97. package/_site/_test/package-startup-test.js +56 -56
  98. package/_site/_test/packaged-ui-smoke-test.js +88 -88
  99. package/_site/_test/pr-consumer-contract-test.js +55 -55
  100. package/_site/_test/product-brand-contract-test.js +25 -25
  101. package/_site/_test/product-diagnostics-ui-test.js +93 -93
  102. package/_site/_test/product-import-ui-test.js +104 -104
  103. package/_site/_test/product-progress-status-test.js +12 -1
  104. package/_site/_test/project-remove-running-guard-test.js +79 -79
  105. package/_site/_test/project-store-test.js +90 -90
  106. package/_site/_test/registry-events-test.js +38 -0
  107. package/_site/_test/release-version-sync-test.js +115 -115
  108. package/_site/_test/requirement-binding-test.js +113 -113
  109. package/_site/_test/runtime-endpoint-test.js +64 -64
  110. package/_site/_test/scanner-test.js +83 -83
  111. package/_site/_test/server-security-test.js +96 -96
  112. package/_site/_test/sessions-stream-test.js +93 -93
  113. package/_site/_test/shared-scope-root-binding-test.js +67 -67
  114. package/_site/_test/simple-import-test.js +98 -98
  115. package/_site/_test/startup-analysis-disabled-test.js +6 -6
  116. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
  117. package/_site/_test/team-knowledge-store-test.js +132 -132
  118. package/_site/_test/test-server-cleanup-test.js +35 -35
  119. package/_site/_test/test-server-module-isolation-test.js +37 -37
  120. package/_site/_test/tracking-start-test.js +35 -35
  121. package/_site/_test/workbench-context-receipts-test.js +22 -0
  122. package/_site/_test/workspace-ui-contract-test.js +31 -31
  123. package/_site/_test/zcode-capture-integration-test.js +108 -108
  124. package/_site/lib/ai-vendor-presets.js +83 -83
  125. package/_site/lib/ai-workspace.js +3 -3
  126. package/_site/lib/atomic-file.js +204 -204
  127. package/_site/lib/bridge-adapter.js +260 -260
  128. package/_site/lib/capture-hook-config.js +108 -108
  129. package/_site/lib/claude-cli-runner.js +89 -89
  130. package/_site/lib/cli-instances.js +43 -43
  131. package/_site/lib/codex-context-status.js +17 -17
  132. package/_site/lib/codex-notify-config.js +193 -193
  133. package/_site/lib/commit-conversation-binder.js +13 -12
  134. package/_site/lib/commit-reconciler.js +38 -38
  135. package/_site/lib/context-pack-builder.js +299 -299
  136. package/_site/lib/conversation-exclusions.js +204 -204
  137. package/_site/lib/conversation-query-service.js +17 -17
  138. package/_site/lib/conversation-store.js +2 -0
  139. package/_site/lib/data-dir.js +13 -13
  140. package/_site/lib/embedding-service.js +85 -85
  141. package/_site/lib/engine-connection.js +93 -90
  142. package/_site/lib/engine-knowledge-client.js +72 -72
  143. package/_site/lib/folder-picker-output.js +3 -3
  144. package/_site/lib/git-path.js +14 -14
  145. package/_site/lib/github-team-store.js +3 -3
  146. package/_site/lib/index-builder.js +157 -157
  147. package/_site/lib/integration-status-scan.js +99 -99
  148. package/_site/lib/integration-status-worker.js +14 -14
  149. package/_site/lib/kb-framework.js +15 -15
  150. package/_site/lib/knowledge-db.js +363 -363
  151. package/_site/lib/knowledge-schema.js +121 -121
  152. package/_site/lib/live-gateway.js +78 -76
  153. package/_site/lib/llm-client.js +12 -12
  154. package/_site/lib/model-context-windows.js +82 -82
  155. package/_site/lib/module-bridge.js +874 -865
  156. package/_site/lib/module-port-resolver.js +57 -57
  157. package/_site/lib/post-commit-automation.js +54 -54
  158. package/_site/lib/product-data.js +35 -35
  159. package/_site/lib/project-lifecycle-service.js +14 -14
  160. package/_site/lib/project-registry-store.js +129 -129
  161. package/_site/lib/prompt-registry.js +148 -148
  162. package/_site/lib/registry-events.js +16 -0
  163. package/_site/lib/requirement-adapters.js +10 -10
  164. package/_site/lib/requirement-binder.js +112 -112
  165. package/_site/lib/runtime-control.js +89 -89
  166. package/_site/lib/runtime-endpoint.js +5 -5
  167. package/_site/lib/server-app.js +10 -1
  168. package/_site/lib/zcode-context-integration.js +60 -60
  169. package/_site/scripts/codex-notify-fanout.cjs +95 -95
  170. package/_site/scripts/install-module-candidates.js +69 -69
  171. package/_site/scripts/pack-module-candidates.js +89 -72
  172. package/_site/scripts/sync-product-content.js +28 -28
  173. package/_site/scripts/sync-release-version.js +164 -164
  174. package/_site/scripts/vendor-modules.js +49 -49
  175. package/_site/scripts/verify-module-runtime.js +50 -50
  176. package/_site/scripts/verify-product-runtime.js +36 -36
  177. package/_site/scripts/verify-retrieval-gold.js +83 -0
  178. package/_site/server.js +15 -15
  179. package/_site/start.bat +26 -26
  180. package/_site/stop.bat +11 -11
  181. package/bin/project-knowledge-integrations.js +254 -254
  182. package/bin/project-knowledge-kb.js +67 -67
  183. package/bin/project-knowledge-mcp.js +8 -8
  184. package/bin/project-knowledge.js +170 -170
  185. package/claude-prompts.json +21 -21
  186. package/docs/README.zh-CN.md +205 -205
  187. package/docs/assets/architecture.svg +181 -181
  188. package/docs/assets/logo.svg +49 -49
  189. package/docs/fresh-v5-start.md +42 -42
  190. package/docs/leader-glm-workbench-playbook.md +122 -119
  191. package/docs/project-registry-schema.md +11 -11
  192. package/docs/release-5.0.16-acceptance.md +176 -176
  193. package/docs/release-5.0.17-acceptance.md +117 -117
  194. package/docs/release-5.0.19-acceptance.md +39 -0
  195. package/docs/team-knowledge-mode-a-plan.md +73 -73
  196. package/module-runtime-manifest.json +126 -104
  197. package/package.json +110 -108
  198. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  199. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  200. package/plugins/project-knowledge/.mcp.json +1 -1
  201. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  202. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  203. package/templates/change.md +30 -30
  204. package/templates/project-readme.md +18 -18
  205. package/ui/index.html +1 -1
  206. package/ui/knowledge-panel.html +118 -0
  207. package/ui/product.css +8 -0
  208. package/ui/product.js +175 -8
  209. package/vendor-manifest.json +55 -53
@@ -1,81 +1,81 @@
1
- // Git status remains a read-only v2 API; manual validation/state mutation is removed.
2
- const assert = require('assert');
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { execFileSync } = require('child_process');
7
- const { spawnServer } = require('./helpers/spawn-server');
8
- const { makeRepo } = require('./fixtures/make-git-repos');
9
-
10
- const ROOT = path.resolve(__dirname, '..', '..');
11
- const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), `kb-data-git-validation-${process.pid}-`));
12
- const PORT = Number(process.env.KB_GIT_TEST_PORT || 7792);
13
- const BASE_URL = `http://127.0.0.1:${PORT}`;
14
- const PROJECT_ID = 'task-002-temp';
15
-
16
- async function waitForServer() {
17
- for (let attempt = 0; attempt < 100; attempt += 1) {
18
- try { if ((await fetch(`${BASE_URL}/api/health`)).ok) return; } catch {}
19
- await new Promise(resolve => setTimeout(resolve, 100));
20
- }
21
- throw new Error('server did not start');
22
- }
23
-
24
- async function json(method, route, body) {
25
- const response = await fetch(`${BASE_URL}${route}`, {
26
- method,
27
- headers: body ? { 'Content-Type': 'application/json' } : undefined,
28
- body: body ? JSON.stringify(body) : undefined,
29
- });
30
- return { response, body: await response.json() };
31
- }
32
-
33
- (async () => {
34
- const repo = makeRepo({ kind: 'one-commit' });
35
- const spawned = spawnServer({ root: ROOT, port: PORT, dataDir: DATA_DIR, tag: 'git-validation', extraEnv: { KB_AUTOMATION_FAKE_CLAUDE: '1' } });
36
- let output = '';
37
- spawned.child.stdout.on('data', chunk => { output += chunk; });
38
- spawned.child.stderr.on('data', chunk => { output += chunk; });
39
- try {
40
- await waitForServer();
41
- let result = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(DATA_DIR, 'knowledge') } });
42
- assert(result.response.ok, JSON.stringify(result.body));
43
- result = await json('POST', '/api/projects/import', { projectId: PROJECT_ID, localPath: repo.path, displayName: 'Git status fixture' });
44
- assert(result.response.status === 201, JSON.stringify(result.body));
45
-
46
- const statePath = path.join(DATA_DIR, 'projects', PROJECT_ID, 'state.json');
47
- const stateBefore = fs.readFileSync(statePath, 'utf8');
48
- result = await json('GET', `/api/projects/${PROJECT_ID}/git-status`);
49
- assert(result.response.ok, JSON.stringify(result.body));
50
- assert.strictEqual(result.body.repoStatus, 'ok');
51
- assert.strictEqual(result.body.branch, 'main');
52
- assert.strictEqual(result.body.dirty, false);
53
- assert.match(result.body.headCommit, /^[a-f0-9]{40}$/i);
54
- assert.strictEqual(fs.readFileSync(statePath, 'utf8'), stateBefore, 'read-only status must not mutate project state');
55
-
56
- fs.writeFileSync(path.join(repo.path, 'untracked.txt'), 'working tree change\n', 'utf8');
57
- result = await json('GET', `/api/projects/${PROJECT_ID}/git-status`);
58
- assert.strictEqual(result.body.dirty, true);
59
- assert(result.body.changes.some(line => line.includes('untracked.txt')));
60
-
61
- execFileSync('git', ['add', 'untracked.txt'], { cwd: repo.path });
62
- execFileSync('git', ['commit', '-m', 'status fixture'], { cwd: repo.path });
63
- result = await json('GET', `/api/projects/${PROJECT_ID}/git-status`);
64
- assert.strictEqual(result.body.repoStatus, 'ok');
65
- assert.strictEqual(result.body.dirty, false);
66
-
67
- result = await json('POST', `/api/projects/${PROJECT_ID}/validate-git`, {});
68
- assert.strictEqual(result.response.status, 404, 'manual validation route must remain deleted');
69
- result = await json('GET', '/api/projects/%5Ebad/git-status');
70
- assert.strictEqual(result.response.status, 400);
71
- result = await json('GET', '/api/projects/project-missing/git-status');
72
- assert.strictEqual(result.response.status, 404);
73
- console.log('TASK-002 git validation test passed');
74
- } catch (error) {
75
- if (output) process.stderr.write(output);
76
- throw error;
77
- } finally {
78
- await spawned.cleanup();
79
- repo.cleanup();
80
- }
81
- })().catch(error => { console.error(error); process.exitCode = 1; });
1
+ // Git status remains a read-only v2 API; manual validation/state mutation is removed.
2
+ const assert = require('assert');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { execFileSync } = require('child_process');
7
+ const { spawnServer } = require('./helpers/spawn-server');
8
+ const { makeRepo } = require('./fixtures/make-git-repos');
9
+
10
+ const ROOT = path.resolve(__dirname, '..', '..');
11
+ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), `kb-data-git-validation-${process.pid}-`));
12
+ const PORT = Number(process.env.KB_GIT_TEST_PORT || 7792);
13
+ const BASE_URL = `http://127.0.0.1:${PORT}`;
14
+ const PROJECT_ID = 'task-002-temp';
15
+
16
+ async function waitForServer() {
17
+ for (let attempt = 0; attempt < 100; attempt += 1) {
18
+ try { if ((await fetch(`${BASE_URL}/api/health`)).ok) return; } catch {}
19
+ await new Promise(resolve => setTimeout(resolve, 100));
20
+ }
21
+ throw new Error('server did not start');
22
+ }
23
+
24
+ async function json(method, route, body) {
25
+ const response = await fetch(`${BASE_URL}${route}`, {
26
+ method,
27
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
28
+ body: body ? JSON.stringify(body) : undefined,
29
+ });
30
+ return { response, body: await response.json() };
31
+ }
32
+
33
+ (async () => {
34
+ const repo = makeRepo({ kind: 'one-commit' });
35
+ const spawned = spawnServer({ root: ROOT, port: PORT, dataDir: DATA_DIR, tag: 'git-validation', extraEnv: { KB_AUTOMATION_FAKE_CLAUDE: '1' } });
36
+ let output = '';
37
+ spawned.child.stdout.on('data', chunk => { output += chunk; });
38
+ spawned.child.stderr.on('data', chunk => { output += chunk; });
39
+ try {
40
+ await waitForServer();
41
+ let result = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(DATA_DIR, 'knowledge') } });
42
+ assert(result.response.ok, JSON.stringify(result.body));
43
+ result = await json('POST', '/api/projects/import', { projectId: PROJECT_ID, localPath: repo.path, displayName: 'Git status fixture' });
44
+ assert(result.response.status === 201, JSON.stringify(result.body));
45
+
46
+ const statePath = path.join(DATA_DIR, 'projects', PROJECT_ID, 'state.json');
47
+ const stateBefore = fs.readFileSync(statePath, 'utf8');
48
+ result = await json('GET', `/api/projects/${PROJECT_ID}/git-status`);
49
+ assert(result.response.ok, JSON.stringify(result.body));
50
+ assert.strictEqual(result.body.repoStatus, 'ok');
51
+ assert.strictEqual(result.body.branch, 'main');
52
+ assert.strictEqual(result.body.dirty, false);
53
+ assert.match(result.body.headCommit, /^[a-f0-9]{40}$/i);
54
+ assert.strictEqual(fs.readFileSync(statePath, 'utf8'), stateBefore, 'read-only status must not mutate project state');
55
+
56
+ fs.writeFileSync(path.join(repo.path, 'untracked.txt'), 'working tree change\n', 'utf8');
57
+ result = await json('GET', `/api/projects/${PROJECT_ID}/git-status`);
58
+ assert.strictEqual(result.body.dirty, true);
59
+ assert(result.body.changes.some(line => line.includes('untracked.txt')));
60
+
61
+ execFileSync('git', ['add', 'untracked.txt'], { cwd: repo.path });
62
+ execFileSync('git', ['commit', '-m', 'status fixture'], { cwd: repo.path });
63
+ result = await json('GET', `/api/projects/${PROJECT_ID}/git-status`);
64
+ assert.strictEqual(result.body.repoStatus, 'ok');
65
+ assert.strictEqual(result.body.dirty, false);
66
+
67
+ result = await json('POST', `/api/projects/${PROJECT_ID}/validate-git`, {});
68
+ assert.strictEqual(result.response.status, 404, 'manual validation route must remain deleted');
69
+ result = await json('GET', '/api/projects/%5Ebad/git-status');
70
+ assert.strictEqual(result.response.status, 400);
71
+ result = await json('GET', '/api/projects/project-missing/git-status');
72
+ assert.strictEqual(result.response.status, 404);
73
+ console.log('TASK-002 git validation test passed');
74
+ } catch (error) {
75
+ if (output) process.stderr.write(output);
76
+ throw error;
77
+ } finally {
78
+ await spawned.cleanup();
79
+ repo.cleanup();
80
+ }
81
+ })().catch(error => { console.error(error); process.exitCode = 1; });
@@ -1,42 +1,42 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
6
- const { installHook, readHookStatus, migrateManagedHook, LEGACY_HOOK_MARKER } = require('../lib/hook-manager');
7
-
8
- const ROOT = path.resolve(__dirname, '..', '..');
9
- const TRIGGER = path.join(ROOT, '_site', 'scripts', 'hook-trigger.js');
10
-
11
- function git(repo, args) {
12
- const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
13
- if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
14
- return String(result.stdout || '').trim();
15
- }
16
-
17
- const base = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-hook-worktree-'));
18
- const main = path.join(base, 'main repo');
19
- const linked = path.join(base, 'linked worktree');
20
- fs.mkdirSync(main, { recursive: true });
21
- git(main, ['init', '--initial-branch=main']);
22
- git(main, ['config', 'user.email', 'hook@example.invalid']);
23
- git(main, ['config', 'user.name', 'Hook Test']);
24
- fs.writeFileSync(path.join(main, 'README.md'), '# main\n');
25
- git(main, ['add', 'README.md']);
26
- git(main, ['commit', '-m', 'initial']);
27
- git(main, ['worktree', 'add', '-b', 'linked', linked]);
28
-
29
- git(linked, ['config', 'core.hooksPath', '.custom-hooks']);
30
- const installed = installHook({ repoPath: linked, projectId: 'project-worktree', triggerScriptPath: TRIGGER });
31
- const authoritativeHooks = git(linked, ['rev-parse', '--path-format=absolute', '--git-path', 'hooks']);
32
- assert.strictEqual(path.resolve(path.dirname(installed.hookPath)), path.resolve(authoritativeHooks));
33
- assert.strictEqual(readHookStatus({ repoPath: linked, projectId: 'project-worktree' }).installed, true);
34
-
35
- fs.writeFileSync(installed.hookPath, `#!/bin/sh\n${LEGACY_HOOK_MARKER} — old\nexit 0\n`);
36
- const migrated = migrateManagedHook({ repoPath: linked, projectId: 'project-worktree', triggerScriptPath: TRIGGER });
37
- assert.strictEqual(migrated.migrated, true);
38
- const second = migrateManagedHook({ repoPath: linked, projectId: 'project-worktree', triggerScriptPath: TRIGGER });
39
- assert.strictEqual(second.migrated, false);
40
- assert.strictEqual(second.reason, 'current');
41
-
42
- console.log('hook-worktree-test PASS');
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const { installHook, readHookStatus, migrateManagedHook, LEGACY_HOOK_MARKER } = require('../lib/hook-manager');
7
+
8
+ const ROOT = path.resolve(__dirname, '..', '..');
9
+ const TRIGGER = path.join(ROOT, '_site', 'scripts', 'hook-trigger.js');
10
+
11
+ function git(repo, args) {
12
+ const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
13
+ if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
14
+ return String(result.stdout || '').trim();
15
+ }
16
+
17
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-hook-worktree-'));
18
+ const main = path.join(base, 'main repo');
19
+ const linked = path.join(base, 'linked worktree');
20
+ fs.mkdirSync(main, { recursive: true });
21
+ git(main, ['init', '--initial-branch=main']);
22
+ git(main, ['config', 'user.email', 'hook@example.invalid']);
23
+ git(main, ['config', 'user.name', 'Hook Test']);
24
+ fs.writeFileSync(path.join(main, 'README.md'), '# main\n');
25
+ git(main, ['add', 'README.md']);
26
+ git(main, ['commit', '-m', 'initial']);
27
+ git(main, ['worktree', 'add', '-b', 'linked', linked]);
28
+
29
+ git(linked, ['config', 'core.hooksPath', '.custom-hooks']);
30
+ const installed = installHook({ repoPath: linked, projectId: 'project-worktree', triggerScriptPath: TRIGGER });
31
+ const authoritativeHooks = git(linked, ['rev-parse', '--path-format=absolute', '--git-path', 'hooks']);
32
+ assert.strictEqual(path.resolve(path.dirname(installed.hookPath)), path.resolve(authoritativeHooks));
33
+ assert.strictEqual(readHookStatus({ repoPath: linked, projectId: 'project-worktree' }).installed, true);
34
+
35
+ fs.writeFileSync(installed.hookPath, `#!/bin/sh\n${LEGACY_HOOK_MARKER} — old\nexit 0\n`);
36
+ const migrated = migrateManagedHook({ repoPath: linked, projectId: 'project-worktree', triggerScriptPath: TRIGGER });
37
+ assert.strictEqual(migrated.migrated, true);
38
+ const second = migrateManagedHook({ repoPath: linked, projectId: 'project-worktree', triggerScriptPath: TRIGGER });
39
+ assert.strictEqual(second.migrated, false);
40
+ assert.strictEqual(second.reason, 'current');
41
+
42
+ console.log('hook-worktree-test PASS');
@@ -1,41 +1,41 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { regenerateIndexes } = require('../lib/index-builder');
6
-
7
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'index-builder-test-'));
8
-
9
- try {
10
- const modules = path.join(temp, 'modules');
11
- const changes = path.join(temp, 'changes');
12
- fs.mkdirSync(modules, { recursive: true });
13
- fs.mkdirSync(changes, { recursive: true });
14
-
15
- const manyTags = Array.from({ length: 40 }, (_, index) => `tag-${index}`);
16
- fs.writeFileSync(path.join(modules, 'core.md'), `---\ntitle: Core | API\ntags: [${manyTags.join(', ')}]\nsourcePaths: [${manyTags.map(tag => `src/${tag}.js`).join(', ')}]\nsymbols: [${manyTags.map(tag => `symbol-${tag}`).join(', ')}]\n---\n# Core\n`, 'utf8');
17
-
18
- for (let index = 0; index < 105; index++) {
19
- const day = String((index % 28) + 1).padStart(2, '0');
20
- fs.writeFileSync(path.join(changes, `change-${String(index).padStart(3, '0')}.md`), `---\ntitle: Change ${index}\ntags: [${manyTags.join(', ')}]\naffectedModules: [core, api, ui, worker, storage, release]\nupdatedAt: 2026-07-${day}\ndevelopmentIntent: ${'Long intent '.repeat(80)}\n---\n# Change ${index}\n`, 'utf8');
21
- }
22
-
23
- regenerateIndexes(temp);
24
- const moduleIndex = fs.readFileSync(path.join(modules, '00-index.md'), 'utf8');
25
- const changeIndex = fs.readFileSync(path.join(changes, '00-index.md'), 'utf8');
26
-
27
- assert(!/^Tags:/m.test(moduleIndex), 'module index must not aggregate all tags');
28
- assert(!/^Tags:/m.test(changeIndex), 'change index must not aggregate all tags');
29
- assert(moduleIndex.startsWith('<!-- Generated by project-knowledge.'), 'generated indexes should identify themselves as derived files');
30
- assert(moduleIndex.includes('[Core \\| API](./core.md)'), 'Markdown table links should escape title separators');
31
- assert(moduleIndex.includes('tag-0, tag-1, tag-2, tag-3 (+36)'), 'tags should be capped with a remainder count');
32
- assert(changeIndex.includes('Items: 105 (showing 40;'), 'large change indexes should be bounded');
33
- assert.equal((changeIndex.match(/^\| \[/gm) || []).length, 40, 'only the most recent 40 entries should be listed');
34
- assert(Buffer.byteLength(changeIndex, 'utf8') < 30000, 'large generated indexes should stay below 30 KiB');
35
- assert(Math.max(...moduleIndex.split(/\r?\n/).map(line => line.length)) < 900, 'module index lines should stay compact');
36
- assert(Math.max(...changeIndex.split(/\r?\n/).map(line => line.length)) < 900, 'change index lines should stay compact');
37
-
38
- console.log('index-builder-test: PASS');
39
- } finally {
40
- fs.rmSync(temp, { recursive: true, force: true });
41
- }
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { regenerateIndexes } = require('../lib/index-builder');
6
+
7
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'index-builder-test-'));
8
+
9
+ try {
10
+ const modules = path.join(temp, 'modules');
11
+ const changes = path.join(temp, 'changes');
12
+ fs.mkdirSync(modules, { recursive: true });
13
+ fs.mkdirSync(changes, { recursive: true });
14
+
15
+ const manyTags = Array.from({ length: 40 }, (_, index) => `tag-${index}`);
16
+ fs.writeFileSync(path.join(modules, 'core.md'), `---\ntitle: Core | API\ntags: [${manyTags.join(', ')}]\nsourcePaths: [${manyTags.map(tag => `src/${tag}.js`).join(', ')}]\nsymbols: [${manyTags.map(tag => `symbol-${tag}`).join(', ')}]\n---\n# Core\n`, 'utf8');
17
+
18
+ for (let index = 0; index < 105; index++) {
19
+ const day = String((index % 28) + 1).padStart(2, '0');
20
+ fs.writeFileSync(path.join(changes, `change-${String(index).padStart(3, '0')}.md`), `---\ntitle: Change ${index}\ntags: [${manyTags.join(', ')}]\naffectedModules: [core, api, ui, worker, storage, release]\nupdatedAt: 2026-07-${day}\ndevelopmentIntent: ${'Long intent '.repeat(80)}\n---\n# Change ${index}\n`, 'utf8');
21
+ }
22
+
23
+ regenerateIndexes(temp);
24
+ const moduleIndex = fs.readFileSync(path.join(modules, '00-index.md'), 'utf8');
25
+ const changeIndex = fs.readFileSync(path.join(changes, '00-index.md'), 'utf8');
26
+
27
+ assert(!/^Tags:/m.test(moduleIndex), 'module index must not aggregate all tags');
28
+ assert(!/^Tags:/m.test(changeIndex), 'change index must not aggregate all tags');
29
+ assert(moduleIndex.startsWith('<!-- Generated by project-knowledge.'), 'generated indexes should identify themselves as derived files');
30
+ assert(moduleIndex.includes('[Core \\| API](./core.md)'), 'Markdown table links should escape title separators');
31
+ assert(moduleIndex.includes('tag-0, tag-1, tag-2, tag-3 (+36)'), 'tags should be capped with a remainder count');
32
+ assert(changeIndex.includes('Items: 105 (showing 40;'), 'large change indexes should be bounded');
33
+ assert.equal((changeIndex.match(/^\| \[/gm) || []).length, 40, 'only the most recent 40 entries should be listed');
34
+ assert(Buffer.byteLength(changeIndex, 'utf8') < 30000, 'large generated indexes should stay below 30 KiB');
35
+ assert(Math.max(...moduleIndex.split(/\r?\n/).map(line => line.length)) < 900, 'module index lines should stay compact');
36
+ assert(Math.max(...changeIndex.split(/\r?\n/).map(line => line.length)) < 900, 'change index lines should stay compact');
37
+
38
+ console.log('index-builder-test: PASS');
39
+ } finally {
40
+ fs.rmSync(temp, { recursive: true, force: true });
41
+ }
@@ -1,10 +1,10 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const source = fs.readFileSync(path.resolve(__dirname, '../../ui/product.js'), 'utf8');
6
- for (const field of ['upstreamMessage', 'failureKind', 'upstreamCode', 'requestId', 'attempt', 'httpStatus', 'durationMs']) {
7
- assert.ok(source.includes(`esc(r.error.details.${field}`), `index detail must be escaped: ${field}`);
8
- }
9
- assert.ok(source.includes("['failed', 'index-pending'].includes(r.state)"));
10
- console.log('index-error-display-contract-test PASS (real rendering/export covered by engine test:web:faults)');
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const source = fs.readFileSync(path.resolve(__dirname, '../../ui/product.js'), 'utf8');
6
+ for (const field of ['upstreamMessage', 'failureKind', 'upstreamCode', 'requestId', 'attempt', 'httpStatus', 'durationMs']) {
7
+ assert.ok(source.includes(`esc(r.error.details.${field}`), `index detail must be escaped: ${field}`);
8
+ }
9
+ assert.ok(source.includes("['failed', 'index-pending'].includes(r.state)"));
10
+ console.log('index-error-display-contract-test PASS (real rendering/export covered by engine test:web:faults)');
@@ -139,85 +139,85 @@ async function main() {
139
139
  });
140
140
  const codexResult = await codexManager.operate('update', ['codex']);
141
141
  assert(codexResult[0].ok, 'Codex update should complete');
142
- assert(codexCalls.some(call => call.args.join(' ') === 'plugin marketplace upgrade project-knowledge'), 'Codex should refresh its marketplace');
143
- assert(codexCalls.some(call => call.args.join(' ') === 'plugin remove project-knowledge@project-knowledge'), 'Codex should remove the cached plugin during update');
144
- assert(codexCalls.some(call => call.args.join(' ') === 'plugin add project-knowledge@project-knowledge'), 'Codex should install the refreshed plugin');
145
-
146
- const chainedCodexConfig = path.join(TEMP, 'codex-config.toml');
147
- const oldNotifier = Buffer.from(JSON.stringify(['node', 'C:/existing-notify.js'])).toString('base64');
148
- fs.writeFileSync(chainedCodexConfig, `notify = ["node", "C:/tools/devtask-radar/connectors/codex/notify-hook.js", "--previous-notify", "${oldNotifier}"]\n`, 'utf8');
149
- const chainedManager = new IntegrationManager({
150
- rootDir: ROOT,
151
- homeDir: path.join(TEMP, 'codex-home'),
152
- bridgeHomeDir: path.join(TEMP, 'bridge-home'),
153
- codexConfigFile: chainedCodexConfig,
154
- });
155
- const chained = await chainedManager.installCaptureCodex();
156
- assert(chained.installed && !chained.conflict, 'a compatible DevTask Radar notifier should chain Bridge instead of blocking capture');
157
- const chainedConfig = JSON.parse(fs.readFileSync(chainedCodexConfig, 'utf8').replace(/^\s*notify\s*=\s*/, '').trim());
158
- const nextIndex = chainedConfig.indexOf('--next-base64');
142
+ assert(codexCalls.some(call => call.args.join(' ') === 'plugin marketplace upgrade project-knowledge'), 'Codex should refresh its marketplace');
143
+ assert(codexCalls.some(call => call.args.join(' ') === 'plugin remove project-knowledge@project-knowledge'), 'Codex should remove the cached plugin during update');
144
+ assert(codexCalls.some(call => call.args.join(' ') === 'plugin add project-knowledge@project-knowledge'), 'Codex should install the refreshed plugin');
145
+
146
+ const chainedCodexConfig = path.join(TEMP, 'codex-config.toml');
147
+ const oldNotifier = Buffer.from(JSON.stringify(['node', 'C:/existing-notify.js'])).toString('base64');
148
+ fs.writeFileSync(chainedCodexConfig, `notify = ["node", "C:/tools/devtask-radar/connectors/codex/notify-hook.js", "--previous-notify", "${oldNotifier}"]\n`, 'utf8');
149
+ const chainedManager = new IntegrationManager({
150
+ rootDir: ROOT,
151
+ homeDir: path.join(TEMP, 'codex-home'),
152
+ bridgeHomeDir: path.join(TEMP, 'bridge-home'),
153
+ codexConfigFile: chainedCodexConfig,
154
+ });
155
+ const chained = await chainedManager.installCaptureCodex();
156
+ assert(chained.installed && !chained.conflict, 'a compatible DevTask Radar notifier should chain Bridge instead of blocking capture');
157
+ const chainedConfig = JSON.parse(fs.readFileSync(chainedCodexConfig, 'utf8').replace(/^\s*notify\s*=\s*/, '').trim());
158
+ const nextIndex = chainedConfig.indexOf('--next-base64');
159
159
  assert(nextIndex >= 0, 'managed fan-out must preserve the compatible notifier as its independent branch');
160
- const nextNotify = JSON.parse(Buffer.from(chainedConfig[nextIndex + 1], 'base64').toString('utf8'));
160
+ const nextNotify = JSON.parse(Buffer.from(chainedConfig[nextIndex + 1], 'base64').toString('utf8'));
161
161
  assert(nextNotify[1] === 'C:/tools/devtask-radar/connectors/codex/notify-hook.js', 'Radar remains the existing independent notifier');
162
162
  const bridgeIndex = chainedConfig.indexOf('--bridge-base64');
163
163
  const bridgeNotify = JSON.parse(Buffer.from(chainedConfig[bridgeIndex + 1], 'base64').toString('utf8'));
164
164
  assert(bridgeNotify[1] === path.join(TEMP, 'bridge-home', 'bin', 'bridge-hook.cjs'), 'capture must receive the payload through the current Bridge shim');
165
- const chainedStatus = await chainedManager.statusCaptureCodex();
166
- assert(chainedStatus.installed && !chainedStatus.conflict, 'status should recognize the compatible chained notifier');
167
-
168
- const nestedCodexConfig = path.join(TEMP, 'nested-codex-config.toml');
169
- const nestedDevTask = ['node', 'C:/tools/devtask-radar/connectors/codex/notify-hook.js', '--next-base64', oldNotifier];
165
+ const chainedStatus = await chainedManager.statusCaptureCodex();
166
+ assert(chainedStatus.installed && !chainedStatus.conflict, 'status should recognize the compatible chained notifier');
167
+
168
+ const nestedCodexConfig = path.join(TEMP, 'nested-codex-config.toml');
169
+ const nestedDevTask = ['node', 'C:/tools/devtask-radar/connectors/codex/notify-hook.js', '--next-base64', oldNotifier];
170
170
  const desktopExecutable = path.join(TEMP, 'OpenAI', 'Codex', 'runtimes', 'cua_node', 'test-runtime', 'bin', 'node_modules', '@oai', 'sky', 'bin', 'windows', 'codex-computer-use.exe');
171
171
  fs.mkdirSync(path.dirname(desktopExecutable), { recursive: true });
172
172
  fs.writeFileSync(desktopExecutable, '');
173
173
  fs.writeFileSync(nestedCodexConfig, `notify = ${JSON.stringify([desktopExecutable, 'turn-ended', '--previous-notify', JSON.stringify(nestedDevTask)])}\n`, 'utf8');
174
- const nestedManager = new IntegrationManager({
175
- rootDir: ROOT,
176
- homeDir: path.join(TEMP, 'nested-codex-home'),
177
- bridgeHomeDir: path.join(TEMP, 'nested-bridge-home'),
178
- codexConfigFile: nestedCodexConfig,
179
- });
180
- const nested = await nestedManager.installCaptureCodex();
181
- assert(nested.installed && !nested.conflict, 'a nested DevTask Radar notifier should be preserved through the fan-out wrapper');
182
- const nestedConfig = JSON.parse(fs.readFileSync(nestedCodexConfig, 'utf8').replace(/^\s*notify\s*=\s*/, '').trim());
174
+ const nestedManager = new IntegrationManager({
175
+ rootDir: ROOT,
176
+ homeDir: path.join(TEMP, 'nested-codex-home'),
177
+ bridgeHomeDir: path.join(TEMP, 'nested-bridge-home'),
178
+ codexConfigFile: nestedCodexConfig,
179
+ });
180
+ const nested = await nestedManager.installCaptureCodex();
181
+ assert(nested.installed && !nested.conflict, 'a nested DevTask Radar notifier should be preserved through the fan-out wrapper');
182
+ const nestedConfig = JSON.parse(fs.readFileSync(nestedCodexConfig, 'utf8').replace(/^\s*notify\s*=\s*/, '').trim());
183
183
  assert(nestedConfig[0] === desktopExecutable, 'the active Codex desktop wrapper keeps its position');
184
184
  assert(nestedConfig.join(' ').includes('codex-notify-fanout.cjs'), 'nested notifier chain should use the Project Knowledge fan-out wrapper');
185
- const nestedStatus = await nestedManager.statusCaptureCodex();
186
- assert(nestedStatus.installed && !nestedStatus.conflict, 'status should recognize the managed nested fan-out wrapper');
187
-
188
- // Codex Desktop computer-use wraps the existing notifier as a JSON array in
189
- // --previous-notify. The managed fan-out is still installed and must not be
190
- // misreported as a third-party conflict or wrapped a second time.
191
- const desktopWrappedConfig = path.join(TEMP, 'desktop-wrapped-codex-config.toml');
185
+ const nestedStatus = await nestedManager.statusCaptureCodex();
186
+ assert(nestedStatus.installed && !nestedStatus.conflict, 'status should recognize the managed nested fan-out wrapper');
187
+
188
+ // Codex Desktop computer-use wraps the existing notifier as a JSON array in
189
+ // --previous-notify. The managed fan-out is still installed and must not be
190
+ // misreported as a third-party conflict or wrapped a second time.
191
+ const desktopWrappedConfig = path.join(TEMP, 'desktop-wrapped-codex-config.toml');
192
192
  const desktopWrappedArgs = [desktopExecutable, 'turn-ended', '--previous-notify', JSON.stringify(nestedConfig)];
193
- fs.writeFileSync(desktopWrappedConfig, `notify = ${JSON.stringify(desktopWrappedArgs)}\n`, 'utf8');
194
- const desktopWrappedManager = new IntegrationManager({
195
- rootDir: ROOT,
196
- homeDir: path.join(TEMP, 'desktop-wrapped-codex-home'),
197
- bridgeHomeDir: path.join(TEMP, 'nested-bridge-home'),
198
- codexConfigFile: desktopWrappedConfig,
199
- });
200
- const desktopWrappedStatus = await desktopWrappedManager.statusCaptureCodex();
201
- assert(desktopWrappedStatus.installed && !desktopWrappedStatus.conflict,
202
- 'Codex Desktop outer wrapper should preserve managed capture health');
203
- const beforeDesktopInstall = fs.readFileSync(desktopWrappedConfig, 'utf8');
204
- const desktopWrappedInstall = await desktopWrappedManager.installCaptureCodex();
205
- assert(desktopWrappedInstall.installed && !desktopWrappedInstall.conflict,
206
- 'one-click repair should recognize the nested managed fan-out');
207
- assert(fs.readFileSync(desktopWrappedConfig, 'utf8') === beforeDesktopInstall,
208
- 'healthy Codex Desktop wrapper must remain byte-for-byte unchanged');
209
-
210
- const bridgeOnlyConfig = path.join(TEMP, 'bridge-only-codex-config.toml');
211
- const bridgeOnlyManager = new IntegrationManager({
212
- rootDir: ROOT,
213
- homeDir: path.join(TEMP, 'bridge-only-codex-home'),
214
- bridgeHomeDir: path.join(TEMP, 'bridge-only-bridge-home'),
215
- codexConfigFile: bridgeOnlyConfig,
216
- });
217
- const bridgeOnly = await bridgeOnlyManager.installCaptureCodex();
218
- assert(bridgeOnly.installed && !bridgeOnly.conflict, 'a fresh Codex install should use the payload-preserving fan-out wrapper');
219
- const bridgeOnlyArgs = JSON.parse(fs.readFileSync(bridgeOnlyConfig, 'utf8').replace(/^\s*notify\s*=\s*/, '').trim());
220
- assert(bridgeOnlyArgs.join(' ').includes('codex-notify-fanout.cjs'), 'fresh Codex install should use the Project Knowledge fan-out wrapper');
193
+ fs.writeFileSync(desktopWrappedConfig, `notify = ${JSON.stringify(desktopWrappedArgs)}\n`, 'utf8');
194
+ const desktopWrappedManager = new IntegrationManager({
195
+ rootDir: ROOT,
196
+ homeDir: path.join(TEMP, 'desktop-wrapped-codex-home'),
197
+ bridgeHomeDir: path.join(TEMP, 'nested-bridge-home'),
198
+ codexConfigFile: desktopWrappedConfig,
199
+ });
200
+ const desktopWrappedStatus = await desktopWrappedManager.statusCaptureCodex();
201
+ assert(desktopWrappedStatus.installed && !desktopWrappedStatus.conflict,
202
+ 'Codex Desktop outer wrapper should preserve managed capture health');
203
+ const beforeDesktopInstall = fs.readFileSync(desktopWrappedConfig, 'utf8');
204
+ const desktopWrappedInstall = await desktopWrappedManager.installCaptureCodex();
205
+ assert(desktopWrappedInstall.installed && !desktopWrappedInstall.conflict,
206
+ 'one-click repair should recognize the nested managed fan-out');
207
+ assert(fs.readFileSync(desktopWrappedConfig, 'utf8') === beforeDesktopInstall,
208
+ 'healthy Codex Desktop wrapper must remain byte-for-byte unchanged');
209
+
210
+ const bridgeOnlyConfig = path.join(TEMP, 'bridge-only-codex-config.toml');
211
+ const bridgeOnlyManager = new IntegrationManager({
212
+ rootDir: ROOT,
213
+ homeDir: path.join(TEMP, 'bridge-only-codex-home'),
214
+ bridgeHomeDir: path.join(TEMP, 'bridge-only-bridge-home'),
215
+ codexConfigFile: bridgeOnlyConfig,
216
+ });
217
+ const bridgeOnly = await bridgeOnlyManager.installCaptureCodex();
218
+ assert(bridgeOnly.installed && !bridgeOnly.conflict, 'a fresh Codex install should use the payload-preserving fan-out wrapper');
219
+ const bridgeOnlyArgs = JSON.parse(fs.readFileSync(bridgeOnlyConfig, 'utf8').replace(/^\s*notify\s*=\s*/, '').trim());
220
+ assert(bridgeOnlyArgs.join(' ').includes('codex-notify-fanout.cjs'), 'fresh Codex install should use the Project Knowledge fan-out wrapper');
221
221
 
222
222
  const integrationLogs = [];
223
223
  const failingManager = new IntegrationManager({