@duypham93/openkit 0.2.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 (178) hide show
  1. package/.opencode/README.md +47 -0
  2. package/.opencode/install-manifest.json +41 -0
  3. package/.opencode/lib/artifact-scaffolder.js +111 -0
  4. package/.opencode/lib/contract-consistency.js +218 -0
  5. package/.opencode/lib/parallel-execution-rules.js +261 -0
  6. package/.opencode/lib/runtime-paths.js +95 -0
  7. package/.opencode/lib/runtime-summary.js +82 -0
  8. package/.opencode/lib/state-guard.js +99 -0
  9. package/.opencode/lib/task-board-rules.js +375 -0
  10. package/.opencode/lib/work-item-store.js +280 -0
  11. package/.opencode/lib/workflow-state-controller.js +1739 -0
  12. package/.opencode/lib/workflow-state-rules.js +331 -0
  13. package/.opencode/opencode.json +93 -0
  14. package/.opencode/package.json +3 -0
  15. package/.opencode/tests/artifact-scaffolder.test.js +733 -0
  16. package/.opencode/tests/multi-work-item-runtime.test.js +369 -0
  17. package/.opencode/tests/parallel-execution-runtime.test.js +259 -0
  18. package/.opencode/tests/session-start-hook.test.js +357 -0
  19. package/.opencode/tests/state-guard.test.js +124 -0
  20. package/.opencode/tests/task-board-rules.test.js +204 -0
  21. package/.opencode/tests/work-item-store.test.js +380 -0
  22. package/.opencode/tests/workflow-behavior.test.js +149 -0
  23. package/.opencode/tests/workflow-contract-consistency.test.js +387 -0
  24. package/.opencode/tests/workflow-state-cli.test.js +1275 -0
  25. package/.opencode/tests/workflow-state-controller.test.js +1038 -0
  26. package/.opencode/work-items/feature-001/state.json +70 -0
  27. package/.opencode/work-items/index.json +13 -0
  28. package/.opencode/workflow-state.js +489 -0
  29. package/.opencode/workflow-state.json +70 -0
  30. package/AGENTS.md +265 -0
  31. package/README.md +401 -0
  32. package/agents/architect-agent.md +63 -0
  33. package/agents/ba-agent.md +56 -0
  34. package/agents/code-reviewer.md +77 -0
  35. package/agents/fullstack-agent.md +115 -0
  36. package/agents/master-orchestrator.md +60 -0
  37. package/agents/pm-agent.md +56 -0
  38. package/agents/qa-agent.md +124 -0
  39. package/agents/tech-lead-agent.md +60 -0
  40. package/assets/install-bundle/README.md +7 -0
  41. package/assets/install-bundle/opencode/README.md +11 -0
  42. package/assets/install-bundle/opencode/agents/ArchitectAgent.md +63 -0
  43. package/assets/install-bundle/opencode/agents/BAAgent.md +56 -0
  44. package/assets/install-bundle/opencode/agents/CodeReviewer.md +77 -0
  45. package/assets/install-bundle/opencode/agents/FullstackAgent.md +115 -0
  46. package/assets/install-bundle/opencode/agents/MasterOrchestrator.md +60 -0
  47. package/assets/install-bundle/opencode/agents/PMAgent.md +56 -0
  48. package/assets/install-bundle/opencode/agents/QAAgent.md +124 -0
  49. package/assets/install-bundle/opencode/agents/TechLeadAgent.md +60 -0
  50. package/assets/install-bundle/opencode/commands/brainstorm.md +44 -0
  51. package/assets/install-bundle/opencode/commands/delivery.md +45 -0
  52. package/assets/install-bundle/opencode/commands/execute-plan.md +44 -0
  53. package/assets/install-bundle/opencode/commands/migrate.md +61 -0
  54. package/assets/install-bundle/opencode/commands/quick-task.md +45 -0
  55. package/assets/install-bundle/opencode/commands/task.md +46 -0
  56. package/assets/install-bundle/opencode/commands/write-plan.md +50 -0
  57. package/assets/install-bundle/opencode/context/core/lane-selection.md +54 -0
  58. package/assets/install-bundle/opencode/skills/brainstorming/SKILL.md +51 -0
  59. package/assets/install-bundle/opencode/skills/code-review/SKILL.md +48 -0
  60. package/assets/install-bundle/opencode/skills/subagent-driven-development/SKILL.md +79 -0
  61. package/assets/install-bundle/opencode/skills/systematic-debugging/SKILL.md +61 -0
  62. package/assets/install-bundle/opencode/skills/test-driven-development/SKILL.md +48 -0
  63. package/assets/install-bundle/opencode/skills/using-skills/SKILL.md +39 -0
  64. package/assets/install-bundle/opencode/skills/verification-before-completion/SKILL.md +137 -0
  65. package/assets/install-bundle/opencode/skills/writing-plans/SKILL.md +68 -0
  66. package/assets/install-bundle/opencode/skills/writing-specs/SKILL.md +47 -0
  67. package/assets/opencode.json.template +11 -0
  68. package/assets/openkit-install.json.template +19 -0
  69. package/bin/openkit.js +9 -0
  70. package/commands/brainstorm.md +44 -0
  71. package/commands/delivery.md +45 -0
  72. package/commands/execute-plan.md +44 -0
  73. package/commands/migrate.md +61 -0
  74. package/commands/quick-task.md +45 -0
  75. package/commands/task.md +46 -0
  76. package/commands/write-plan.md +50 -0
  77. package/context/core/approval-gates.md +146 -0
  78. package/context/core/code-quality.md +42 -0
  79. package/context/core/issue-routing.md +85 -0
  80. package/context/core/lane-selection.md +54 -0
  81. package/context/core/project-config.md +143 -0
  82. package/context/core/session-resume.md +85 -0
  83. package/context/core/workflow-state-schema.md +224 -0
  84. package/context/core/workflow.md +442 -0
  85. package/context/navigation.md +94 -0
  86. package/docs/adr/README.md +6 -0
  87. package/docs/architecture/2026-03-20-task-intake-dashboard.md +54 -0
  88. package/docs/architecture/README.md +7 -0
  89. package/docs/briefs/2026-03-20-task-intake-dashboard.md +48 -0
  90. package/docs/briefs/README.md +7 -0
  91. package/docs/governance/README.md +25 -0
  92. package/docs/governance/adr-policy.md +27 -0
  93. package/docs/governance/definition-of-done.md +17 -0
  94. package/docs/governance/naming-conventions.md +21 -0
  95. package/docs/governance/severity-levels.md +12 -0
  96. package/docs/maintainer/README.md +51 -0
  97. package/docs/operations/README.md +79 -0
  98. package/docs/operations/internal-records/2026-03-24-release-checklist.md +79 -0
  99. package/docs/operations/internal-records/2026-03-24-simplified-install-ux.md +36 -0
  100. package/docs/operations/internal-records/README.md +18 -0
  101. package/docs/operations/runbooks/README.md +23 -0
  102. package/docs/operations/runbooks/openkit-daily-usage.md +288 -0
  103. package/docs/operations/runbooks/workflow-state-smoke-tests.md +302 -0
  104. package/docs/operator/README.md +50 -0
  105. package/docs/plans/2026-03-20-task-intake-dashboard.md +49 -0
  106. package/docs/plans/2026-03-21-openkit-full-delivery-multi-task-runtime.md +521 -0
  107. package/docs/plans/2026-03-23-openkit-global-install-runtime.md +157 -0
  108. package/docs/plans/README.md +7 -0
  109. package/docs/qa/2026-03-20-task-intake-dashboard.md +41 -0
  110. package/docs/qa/README.md +7 -0
  111. package/docs/specs/2026-03-20-task-intake-dashboard.md +50 -0
  112. package/docs/specs/2026-03-21-openkit-full-delivery-multi-task-runtime.md +462 -0
  113. package/docs/specs/README.md +7 -0
  114. package/docs/templates/README.md +36 -0
  115. package/docs/templates/adr-template.md +18 -0
  116. package/docs/templates/architecture-template.md +31 -0
  117. package/docs/templates/implementation-plan-template.md +32 -0
  118. package/docs/templates/migration-baseline-checklist.md +48 -0
  119. package/docs/templates/migration-plan-template.md +52 -0
  120. package/docs/templates/migration-report-template.md +74 -0
  121. package/docs/templates/migration-verify-checklist.md +39 -0
  122. package/docs/templates/product-brief-template.md +32 -0
  123. package/docs/templates/qa-report-template.md +37 -0
  124. package/docs/templates/quick-task-template.md +36 -0
  125. package/docs/templates/spec-template.md +31 -0
  126. package/hooks/hooks.json +16 -0
  127. package/hooks/session-start +162 -0
  128. package/package.json +24 -0
  129. package/registry.json +328 -0
  130. package/skills/brainstorming/SKILL.md +51 -0
  131. package/skills/code-review/SKILL.md +48 -0
  132. package/skills/subagent-driven-development/SKILL.md +79 -0
  133. package/skills/systematic-debugging/SKILL.md +61 -0
  134. package/skills/test-driven-development/SKILL.md +48 -0
  135. package/skills/using-skills/SKILL.md +39 -0
  136. package/skills/verification-before-completion/SKILL.md +137 -0
  137. package/skills/writing-plans/SKILL.md +68 -0
  138. package/skills/writing-specs/SKILL.md +47 -0
  139. package/src/audit/vietnamese-detection.js +259 -0
  140. package/src/cli/commands/detect-vietnamese.js +24 -0
  141. package/src/cli/commands/doctor.js +33 -0
  142. package/src/cli/commands/help.js +33 -0
  143. package/src/cli/commands/init.js +25 -0
  144. package/src/cli/commands/install-global.js +26 -0
  145. package/src/cli/commands/install.js +25 -0
  146. package/src/cli/commands/run.js +63 -0
  147. package/src/cli/commands/uninstall.js +32 -0
  148. package/src/cli/commands/upgrade.js +25 -0
  149. package/src/cli/conflict-output.js +19 -0
  150. package/src/cli/index.js +56 -0
  151. package/src/global/doctor.js +101 -0
  152. package/src/global/ensure-install.js +32 -0
  153. package/src/global/install-state.js +73 -0
  154. package/src/global/launcher.js +51 -0
  155. package/src/global/materialize.js +123 -0
  156. package/src/global/paths.js +85 -0
  157. package/src/global/uninstall.js +25 -0
  158. package/src/global/workspace-state.js +63 -0
  159. package/src/install/asset-manifest.js +284 -0
  160. package/src/install/conflicts.js +43 -0
  161. package/src/install/discovery.js +138 -0
  162. package/src/install/install-state.js +136 -0
  163. package/src/install/materialize.js +158 -0
  164. package/src/install/merge-policy.js +145 -0
  165. package/src/runtime/doctor.js +281 -0
  166. package/src/runtime/launcher.js +49 -0
  167. package/src/runtime/opencode-layering.js +135 -0
  168. package/src/runtime/openkit-managed-summary.js +27 -0
  169. package/tests/cli/openkit-cli.test.js +417 -0
  170. package/tests/global/doctor.test.js +130 -0
  171. package/tests/global/ensure-install.test.js +105 -0
  172. package/tests/install/discovery.test.js +124 -0
  173. package/tests/install/install-state.test.js +346 -0
  174. package/tests/install/materialize.test.js +244 -0
  175. package/tests/install/merge-policy.test.js +177 -0
  176. package/tests/runtime/doctor.test.js +430 -0
  177. package/tests/runtime/launcher.test.js +230 -0
  178. package/tests/runtime/module-boundary.test.js +16 -0
@@ -0,0 +1,430 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import { inspectManagedDoctor } from '../../src/runtime/doctor.js';
8
+
9
+ function makeTempDir() {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'openkit-doctor-'));
11
+ }
12
+
13
+ function writeJson(filePath, value) {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
16
+ }
17
+
18
+ function writeText(filePath, value) {
19
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
+ fs.writeFileSync(filePath, value, 'utf8');
21
+ }
22
+
23
+ function materializeManagedInstall(projectRoot) {
24
+ writeJson(path.join(projectRoot, 'opencode.json'), {
25
+ installState: {
26
+ path: '.openkit/openkit-install.json',
27
+ schema: 'openkit/install-state@1',
28
+ },
29
+ productSurface: {
30
+ current: 'global-openkit-install',
31
+ installReadiness: 'managed',
32
+ installationMode: 'openkit-managed',
33
+ },
34
+ });
35
+
36
+ writeJson(path.join(projectRoot, '.openkit', 'openkit-install.json'), {
37
+ schema: 'openkit/install-state@1',
38
+ stateVersion: 1,
39
+ kit: {
40
+ name: 'OpenKit',
41
+ version: '0.1.0',
42
+ },
43
+ installation: {
44
+ profile: 'openkit-core',
45
+ status: 'installed',
46
+ installedAt: '2026-03-22T12:00:00.000Z',
47
+ },
48
+ assets: {
49
+ managed: [
50
+ { assetId: 'runtime.opencode-manifest', path: 'opencode.json', status: 'materialized' },
51
+ { assetId: 'runtime.install-state', path: '.openkit/openkit-install.json', status: 'managed' },
52
+ ],
53
+ adopted: [],
54
+ },
55
+ warnings: [],
56
+ conflicts: [],
57
+ });
58
+ }
59
+
60
+ test('doctor reports install missing when managed install files are absent', () => {
61
+ const projectRoot = makeTempDir();
62
+
63
+ const result = inspectManagedDoctor({
64
+ projectRoot,
65
+ env: {},
66
+ isOpenCodeAvailable: () => false,
67
+ });
68
+
69
+ assert.equal(result.status, 'install-missing');
70
+ assert.equal(result.canRunCleanly, false);
71
+ assert.deepEqual(result.ownedAssets.managed, []);
72
+ assert.match(result.summary, /managed install was not found/i);
73
+ assert.match(result.summary, /openkit run cannot proceed cleanly/i);
74
+ });
75
+
76
+ test('doctor reports install incomplete when install state is missing', () => {
77
+ const projectRoot = makeTempDir();
78
+
79
+ writeJson(path.join(projectRoot, 'opencode.json'), {
80
+ installState: {
81
+ path: '.openkit/openkit-install.json',
82
+ schema: 'openkit/install-state@1',
83
+ },
84
+ productSurface: {
85
+ current: 'global-openkit-install',
86
+ installReadiness: 'managed',
87
+ installationMode: 'openkit-managed',
88
+ },
89
+ });
90
+
91
+ const result = inspectManagedDoctor({
92
+ projectRoot,
93
+ env: {},
94
+ isOpenCodeAvailable: () => true,
95
+ });
96
+
97
+ assert.equal(result.status, 'install-incomplete');
98
+ assert.equal(result.canRunCleanly, false);
99
+ assert.match(result.summary, /install is incomplete/i);
100
+ assert.deepEqual(result.issues, [
101
+ 'Missing required managed asset: .openkit/openkit-install.json',
102
+ ]);
103
+ });
104
+
105
+ test('doctor reports install incomplete for a partial install when install state exists but install entrypoint is missing', () => {
106
+ const projectRoot = makeTempDir();
107
+
108
+ writeJson(path.join(projectRoot, '.openkit', 'openkit-install.json'), {
109
+ schema: 'openkit/install-state@1',
110
+ stateVersion: 1,
111
+ kit: {
112
+ name: 'OpenKit',
113
+ version: '0.1.0',
114
+ },
115
+ installation: {
116
+ profile: 'openkit-core',
117
+ status: 'installed',
118
+ installedAt: '2026-03-22T12:00:00.000Z',
119
+ },
120
+ assets: {
121
+ managed: [
122
+ { assetId: 'runtime.opencode-manifest', path: 'opencode.json', status: 'materialized' },
123
+ { assetId: 'runtime.install-state', path: '.openkit/openkit-install.json', status: 'managed' },
124
+ ],
125
+ adopted: [],
126
+ },
127
+ warnings: [],
128
+ conflicts: [],
129
+ });
130
+
131
+ const result = inspectManagedDoctor({
132
+ projectRoot,
133
+ env: {},
134
+ isOpenCodeAvailable: () => true,
135
+ });
136
+
137
+ assert.equal(result.status, 'install-incomplete');
138
+ assert.equal(result.canRunCleanly, false);
139
+ assert.deepEqual(result.ownedAssets.managed, ['opencode.json', '.openkit/openkit-install.json']);
140
+ assert.match(result.summary, /install is incomplete/i);
141
+ assert.match(result.issues.join('\n'), /Missing required managed asset: opencode\.json/);
142
+ });
143
+
144
+ test('doctor reports drift when a managed asset changed on disk', () => {
145
+ const projectRoot = makeTempDir();
146
+
147
+ materializeManagedInstall(projectRoot);
148
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
149
+ model: 'managed-model',
150
+ });
151
+
152
+ writeJson(path.join(projectRoot, 'opencode.json'), {
153
+ installState: {
154
+ path: '.openkit/openkit-install.json',
155
+ schema: 'openkit/install-state@1',
156
+ },
157
+ productSurface: {
158
+ current: 'changed-wrapper-surface',
159
+ installReadiness: 'managed',
160
+ installationMode: 'openkit-managed',
161
+ },
162
+ });
163
+
164
+ const result = inspectManagedDoctor({
165
+ projectRoot,
166
+ env: {},
167
+ isOpenCodeAvailable: () => true,
168
+ });
169
+
170
+ assert.equal(result.status, 'drift-detected');
171
+ assert.equal(result.canRunCleanly, false);
172
+ assert.deepEqual(result.driftedAssets, ['opencode.json']);
173
+ assert.match(result.summary, /managed asset drift was detected/i);
174
+ assert.match(result.issues[0], /Drift detected for managed asset: opencode\.json/);
175
+ });
176
+
177
+ test('doctor reports drift for managed install-state assets it owns in phase 1', () => {
178
+ const projectRoot = makeTempDir();
179
+
180
+ materializeManagedInstall(projectRoot);
181
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
182
+ model: 'managed-model',
183
+ });
184
+
185
+ writeJson(path.join(projectRoot, '.openkit', 'openkit-install.json'), {
186
+ schema: 'openkit/install-state@1',
187
+ stateVersion: 1,
188
+ kit: {
189
+ name: 'OpenKit',
190
+ version: '0.1.0',
191
+ },
192
+ installation: {
193
+ profile: 'custom-profile',
194
+ status: 'installed',
195
+ installedAt: '2026-03-22T12:00:00.000Z',
196
+ },
197
+ assets: {
198
+ managed: [
199
+ { assetId: 'runtime.opencode-manifest', path: 'opencode.json', status: 'materialized' },
200
+ { assetId: 'runtime.install-state', path: '.openkit/openkit-install.json', status: 'managed' },
201
+ ],
202
+ adopted: [],
203
+ },
204
+ warnings: [],
205
+ conflicts: [],
206
+ });
207
+
208
+ const result = inspectManagedDoctor({
209
+ projectRoot,
210
+ env: {},
211
+ isOpenCodeAvailable: () => true,
212
+ });
213
+
214
+ assert.equal(result.status, 'drift-detected');
215
+ assert.equal(result.canRunCleanly, false);
216
+ assert.deepEqual(result.driftedAssets, ['.openkit/openkit-install.json']);
217
+ assert.match(result.summary, /managed asset drift was detected/i);
218
+ assert.match(
219
+ result.issues.join('\n'),
220
+ /Drift detected for managed asset: \.openkit\/openkit-install\.json/
221
+ );
222
+ });
223
+
224
+ test('doctor reports malformed install manifest JSON as diagnosable drift instead of crashing', () => {
225
+ const projectRoot = makeTempDir();
226
+
227
+ materializeManagedInstall(projectRoot);
228
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
229
+ model: 'managed-model',
230
+ });
231
+ writeText(path.join(projectRoot, 'opencode.json'), '{"installState": ');
232
+
233
+ const result = inspectManagedDoctor({
234
+ projectRoot,
235
+ env: {},
236
+ isOpenCodeAvailable: () => true,
237
+ });
238
+
239
+ assert.equal(result.status, 'drift-detected');
240
+ assert.equal(result.canRunCleanly, false);
241
+ assert.deepEqual(result.driftedAssets, ['opencode.json']);
242
+ assert.match(result.issues.join('\n'), /Managed asset JSON is malformed: opencode\.json/);
243
+ });
244
+
245
+ test('doctor reports malformed managed install-state JSON as diagnosable drift instead of crashing', () => {
246
+ const projectRoot = makeTempDir();
247
+
248
+ materializeManagedInstall(projectRoot);
249
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
250
+ model: 'managed-model',
251
+ });
252
+ writeText(path.join(projectRoot, '.openkit', 'openkit-install.json'), '{"schema": ');
253
+
254
+ const result = inspectManagedDoctor({
255
+ projectRoot,
256
+ env: {},
257
+ isOpenCodeAvailable: () => true,
258
+ });
259
+
260
+ assert.equal(result.status, 'drift-detected');
261
+ assert.equal(result.canRunCleanly, false);
262
+ assert.deepEqual(result.driftedAssets, ['.openkit/openkit-install.json']);
263
+ assert.match(
264
+ result.issues.join('\n'),
265
+ /Managed asset JSON is malformed: \.openkit\/openkit-install\.json/
266
+ );
267
+ });
268
+
269
+ test('doctor reports runtime prerequisites missing when install is intact but launcher prerequisites are absent', () => {
270
+ const projectRoot = makeTempDir();
271
+
272
+ materializeManagedInstall(projectRoot);
273
+
274
+ const result = inspectManagedDoctor({
275
+ projectRoot,
276
+ env: {},
277
+ isOpenCodeAvailable: () => false,
278
+ });
279
+
280
+ assert.equal(result.status, 'runtime-prerequisites-missing');
281
+ assert.equal(result.canRunCleanly, false);
282
+ assert.deepEqual(result.driftedAssets, []);
283
+ assert.deepEqual(result.ownedAssets.managed, ['opencode.json', '.openkit/openkit-install.json']);
284
+ assert.match(result.summary, /runtime launch prerequisites are missing/i);
285
+ assert.match(result.issues.join('\n'), /Missing runtime manifest: \.opencode\/opencode\.json/);
286
+ assert.match(result.issues.join('\n'), /OpenCode executable is not available on PATH/);
287
+ });
288
+
289
+ test('doctor reports healthy state when install is intact and launcher prerequisites are available', () => {
290
+ const projectRoot = makeTempDir();
291
+
292
+ materializeManagedInstall(projectRoot);
293
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
294
+ model: 'managed-model',
295
+ });
296
+
297
+ const result = inspectManagedDoctor({
298
+ projectRoot,
299
+ env: {},
300
+ isOpenCodeAvailable: () => true,
301
+ });
302
+
303
+ assert.equal(result.status, 'healthy');
304
+ assert.equal(result.canRunCleanly, true);
305
+ assert.deepEqual(result.issues, []);
306
+ assert.deepEqual(result.driftedAssets, []);
307
+ assert.deepEqual(result.ownedAssets.managed, ['opencode.json', '.openkit/openkit-install.json']);
308
+ assert.match(result.summary, /managed install is healthy/i);
309
+ assert.match(result.summary, /openkit run can proceed cleanly/i);
310
+ });
311
+
312
+ test('doctor does not report healthy when an adopted root manifest is incompatible with the install contract', () => {
313
+ const projectRoot = makeTempDir();
314
+
315
+ writeJson(path.join(projectRoot, 'opencode.json'), {
316
+ plugin: ['existing-plugin'],
317
+ productSurface: {
318
+ current: 'custom-surface',
319
+ },
320
+ });
321
+ writeJson(path.join(projectRoot, '.openkit', 'openkit-install.json'), {
322
+ schema: 'openkit/install-state@1',
323
+ stateVersion: 1,
324
+ kit: {
325
+ name: 'OpenKit',
326
+ version: '0.1.0',
327
+ },
328
+ installation: {
329
+ profile: 'openkit-core',
330
+ status: 'installed',
331
+ installedAt: '2026-03-22T12:00:00.000Z',
332
+ },
333
+ assets: {
334
+ managed: [
335
+ { assetId: 'runtime.install-state', path: '.openkit/openkit-install.json', status: 'managed' },
336
+ ],
337
+ adopted: [
338
+ {
339
+ assetId: 'runtime.opencode-manifest',
340
+ path: 'opencode.json',
341
+ adoptedFrom: 'user-existing',
342
+ status: 'adopted',
343
+ },
344
+ ],
345
+ },
346
+ warnings: [],
347
+ conflicts: [
348
+ {
349
+ assetId: 'runtime.opencode-manifest',
350
+ path: 'opencode.json',
351
+ reason: 'unsupported-top-level-key',
352
+ resolution: 'manual-review-required',
353
+ },
354
+ ],
355
+ });
356
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
357
+ model: 'managed-model',
358
+ });
359
+
360
+ const result = inspectManagedDoctor({
361
+ projectRoot,
362
+ env: {},
363
+ isOpenCodeAvailable: () => true,
364
+ });
365
+
366
+ assert.equal(result.status, 'install-incomplete');
367
+ assert.equal(result.canRunCleanly, false);
368
+ assert.deepEqual(result.ownedAssets.adopted, ['opencode.json']);
369
+ assert.match(result.summary, /install contract is incomplete/i);
370
+ assert.match(result.issues.join('\n'), /adopted root manifest is incompatible with the managed install contract/i);
371
+ });
372
+
373
+ test('doctor can report healthy when an adopted root manifest still satisfies the install contract', () => {
374
+ const projectRoot = makeTempDir();
375
+
376
+ writeJson(path.join(projectRoot, 'opencode.json'), {
377
+ plugin: ['existing-plugin'],
378
+ installState: {
379
+ path: '.openkit/openkit-install.json',
380
+ schema: 'openkit/install-state@1',
381
+ },
382
+ productSurface: {
383
+ current: 'global-openkit-install',
384
+ installReadiness: 'managed',
385
+ installationMode: 'openkit-managed',
386
+ },
387
+ });
388
+ writeJson(path.join(projectRoot, '.openkit', 'openkit-install.json'), {
389
+ schema: 'openkit/install-state@1',
390
+ stateVersion: 1,
391
+ kit: {
392
+ name: 'OpenKit',
393
+ version: '0.1.0',
394
+ },
395
+ installation: {
396
+ profile: 'openkit-core',
397
+ status: 'installed',
398
+ installedAt: '2026-03-22T12:00:00.000Z',
399
+ },
400
+ assets: {
401
+ managed: [
402
+ { assetId: 'runtime.install-state', path: '.openkit/openkit-install.json', status: 'managed' },
403
+ ],
404
+ adopted: [
405
+ {
406
+ assetId: 'runtime.opencode-manifest',
407
+ path: 'opencode.json',
408
+ adoptedFrom: 'user-existing',
409
+ status: 'adopted',
410
+ },
411
+ ],
412
+ },
413
+ warnings: [],
414
+ conflicts: [],
415
+ });
416
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
417
+ model: 'managed-model',
418
+ });
419
+
420
+ const result = inspectManagedDoctor({
421
+ projectRoot,
422
+ env: {},
423
+ isOpenCodeAvailable: () => true,
424
+ });
425
+
426
+ assert.equal(result.status, 'healthy');
427
+ assert.equal(result.canRunCleanly, true);
428
+ assert.deepEqual(result.ownedAssets.adopted, ['opencode.json']);
429
+ assert.deepEqual(result.issues, []);
430
+ });
@@ -0,0 +1,230 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import {
8
+ CONFIG_DIR_RELATIVE_PATHS,
9
+ buildOpenCodeLayering,
10
+ } from '../../src/runtime/opencode-layering.js';
11
+ import { launchManagedOpenCode } from '../../src/runtime/launcher.js';
12
+
13
+ function makeTempDir() {
14
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'openkit-launcher-'));
15
+ }
16
+
17
+ function writeJson(filePath, value) {
18
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
19
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
20
+ }
21
+
22
+ test('buildOpenCodeLayering uses the managed config dir when no baseline config is set', () => {
23
+ const projectRoot = makeTempDir();
24
+
25
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
26
+ model: 'managed-model',
27
+ commands_dir: 'commands',
28
+ });
29
+
30
+ const result = buildOpenCodeLayering({ projectRoot, env: {} });
31
+
32
+ assert.equal(result.env.OPENCODE_CONFIG_DIR, path.join(projectRoot, '.opencode'));
33
+ assert.equal(result.env.OPENCODE_CONFIG_CONTENT, undefined);
34
+ assert.equal(result.managedConfig.runtimeManifestPath, path.join(projectRoot, '.opencode', 'opencode.json'));
35
+ assert.equal(result.baseline.configDir, null);
36
+ assert.equal(result.baseline.hasConfigContent, false);
37
+ });
38
+
39
+ test('buildOpenCodeLayering preserves baseline config while layering managed content', () => {
40
+ const projectRoot = makeTempDir();
41
+ const baselineConfigDir = path.join(projectRoot, 'user-config');
42
+
43
+ writeJson(path.join(baselineConfigDir, 'opencode.json'), {
44
+ commands_dir: 'user-commands',
45
+ hooks: {
46
+ config: 'user-hooks/hooks.json',
47
+ },
48
+ });
49
+
50
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
51
+ model: 'managed-model',
52
+ agents_dir: 'agents',
53
+ commands_dir: 'commands',
54
+ hooks: {
55
+ config: 'hooks/hooks.json',
56
+ },
57
+ });
58
+
59
+ const result = buildOpenCodeLayering({
60
+ projectRoot,
61
+ env: {
62
+ OPENCODE_CONFIG_DIR: baselineConfigDir,
63
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
64
+ model: 'baseline-model',
65
+ customSetting: true,
66
+ skills_dir: 'user-skills',
67
+ hooks: {
68
+ enabled: true,
69
+ },
70
+ }),
71
+ },
72
+ });
73
+
74
+ assert.equal(result.env.OPENCODE_CONFIG_DIR, path.join(projectRoot, '.opencode'));
75
+
76
+ const layeredContent = JSON.parse(result.env.OPENCODE_CONFIG_CONTENT);
77
+ assert.equal(layeredContent.customSetting, true);
78
+ assert.equal(layeredContent.model, 'managed-model');
79
+ assert.equal(layeredContent.commands_dir, 'commands');
80
+ assert.equal(layeredContent.agents_dir, 'agents');
81
+ assert.equal(layeredContent.skills_dir, path.join(baselineConfigDir, 'user-skills'));
82
+ assert.deepEqual(layeredContent.hooks, {
83
+ enabled: true,
84
+ config: 'hooks/hooks.json',
85
+ });
86
+ assert.equal(result.baseline.configDir, baselineConfigDir);
87
+ assert.equal(result.baseline.config.commands_dir, path.join(baselineConfigDir, 'user-commands'));
88
+ assert.equal(
89
+ result.baseline.config.hooks.config,
90
+ path.join(baselineConfigDir, 'user-hooks', 'hooks.json')
91
+ );
92
+ assert.equal(result.baseline.config.model, 'baseline-model');
93
+ });
94
+
95
+ test('buildOpenCodeLayering only normalizes the documented config-dir-relative keys', () => {
96
+ const projectRoot = makeTempDir();
97
+ const baselineConfigDir = path.join(projectRoot, 'user-config');
98
+
99
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
100
+ model: 'managed-model',
101
+ });
102
+
103
+ const result = buildOpenCodeLayering({
104
+ projectRoot,
105
+ env: {
106
+ OPENCODE_CONFIG_DIR: baselineConfigDir,
107
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
108
+ model: 'baseline-model',
109
+ agents_dir: 'agents',
110
+ skills_dir: 'skills',
111
+ commands_dir: 'commands',
112
+ hooks: {
113
+ config: 'hooks/hooks.json',
114
+ },
115
+ otherRelativePath: 'notes/local.md',
116
+ }),
117
+ },
118
+ });
119
+
120
+ assert.deepEqual(CONFIG_DIR_RELATIVE_PATHS, ['agents_dir', 'commands_dir', 'skills_dir', 'hooks.config']);
121
+ assert.equal(result.baseline.config.agents_dir, path.join(baselineConfigDir, 'agents'));
122
+ assert.equal(result.baseline.config.skills_dir, path.join(baselineConfigDir, 'skills'));
123
+ assert.equal(result.baseline.config.commands_dir, path.join(baselineConfigDir, 'commands'));
124
+ assert.equal(result.baseline.config.hooks.config, path.join(baselineConfigDir, 'hooks', 'hooks.json'));
125
+ assert.equal(result.baseline.config.otherRelativePath, 'notes/local.md');
126
+ });
127
+
128
+ test('launchManagedOpenCode reports a clear error when opencode is unavailable', () => {
129
+ const projectRoot = makeTempDir();
130
+
131
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
132
+ model: 'managed-model',
133
+ });
134
+
135
+ const result = launchManagedOpenCode([], {
136
+ projectRoot,
137
+ env: {},
138
+ spawn: () => ({
139
+ error: Object.assign(new Error('spawn opencode ENOENT'), { code: 'ENOENT' }),
140
+ status: null,
141
+ stdout: '',
142
+ stderr: '',
143
+ }),
144
+ });
145
+
146
+ assert.equal(result.exitCode, 1);
147
+ assert.match(result.stderr, /Could not find `opencode` on your PATH/i);
148
+ assert.match(result.stderr, /supported launcher path is `openkit run`/i);
149
+ });
150
+
151
+ test('launchManagedOpenCode uses interactive stdio by default for the real launcher path', () => {
152
+ const projectRoot = makeTempDir();
153
+ let spawnCall = null;
154
+
155
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
156
+ model: 'managed-model',
157
+ });
158
+
159
+ const result = launchManagedOpenCode(['status'], {
160
+ projectRoot,
161
+ env: {},
162
+ spawn: (command, args, options) => {
163
+ spawnCall = { command, args, options };
164
+ return { status: 0 };
165
+ },
166
+ });
167
+
168
+ assert.equal(result.exitCode, 0);
169
+ assert.equal(spawnCall.command, 'opencode');
170
+ assert.deepEqual(spawnCall.args, ['status']);
171
+ assert.equal(spawnCall.options.stdio, 'inherit');
172
+ });
173
+
174
+ test('launchManagedOpenCode forwards layered config to opencode on the supported path', () => {
175
+ const projectRoot = makeTempDir();
176
+ const fakeBinDir = path.join(projectRoot, 'bin');
177
+ const fakeOpencodePath = path.join(fakeBinDir, 'opencode');
178
+ const baselineConfigDir = path.join(projectRoot, 'user-config');
179
+
180
+ writeJson(path.join(baselineConfigDir, 'opencode.json'), {
181
+ skills_dir: 'baseline-skills',
182
+ });
183
+
184
+ writeJson(path.join(projectRoot, '.opencode', 'opencode.json'), {
185
+ model: 'managed-model',
186
+ commands_dir: 'commands',
187
+ agents_dir: 'agents',
188
+ });
189
+
190
+ fs.mkdirSync(fakeBinDir, { recursive: true });
191
+ fs.writeFileSync(
192
+ fakeOpencodePath,
193
+ [
194
+ '#!/usr/bin/env node',
195
+ 'const payload = {',
196
+ ' args: process.argv.slice(2),',
197
+ ' configDir: process.env.OPENCODE_CONFIG_DIR ?? null,',
198
+ ' configContent: process.env.OPENCODE_CONFIG_CONTENT ?? null,',
199
+ '};',
200
+ 'process.stdout.write(JSON.stringify(payload));',
201
+ ].join('\n'),
202
+ 'utf8'
203
+ );
204
+ fs.chmodSync(fakeOpencodePath, 0o755);
205
+
206
+ const result = launchManagedOpenCode(['status'], {
207
+ projectRoot,
208
+ env: {
209
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH ?? ''}`,
210
+ OPENCODE_CONFIG_DIR: baselineConfigDir,
211
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
212
+ customSetting: true,
213
+ }),
214
+ },
215
+ stdio: 'pipe',
216
+ });
217
+
218
+ assert.equal(result.exitCode, 0);
219
+
220
+ const payload = JSON.parse(result.stdout);
221
+ assert.deepEqual(payload.args, ['status']);
222
+ assert.equal(payload.configDir, path.join(projectRoot, '.opencode'));
223
+
224
+ const layeredContent = JSON.parse(payload.configContent);
225
+ assert.equal(layeredContent.customSetting, true);
226
+ assert.equal(layeredContent.model, 'managed-model');
227
+ assert.equal(layeredContent.commands_dir, 'commands');
228
+ assert.equal(layeredContent.agents_dir, 'agents');
229
+ assert.equal(layeredContent.skills_dir, path.join(baselineConfigDir, 'baseline-skills'));
230
+ });
@@ -0,0 +1,16 @@
1
+ import test from "node:test"
2
+ import assert from "node:assert/strict"
3
+ import fs from "node:fs"
4
+ import path from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+
7
+ const __filename = fileURLToPath(import.meta.url)
8
+ const __dirname = path.dirname(__filename)
9
+ const worktreeRoot = path.resolve(__dirname, "..", "..")
10
+
11
+ test(".opencode declares a CommonJS boundary for the legacy runtime", () => {
12
+ const packageJsonPath = path.join(worktreeRoot, ".opencode", "package.json")
13
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
14
+
15
+ assert.equal(packageJson.type, "commonjs")
16
+ })