@duypham93/openkit 0.2.8 → 0.2.9

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.
@@ -3,7 +3,7 @@
3
3
  "manifestVersion": 1,
4
4
  "kit": {
5
5
  "name": "OpenKit AI Software Factory",
6
- "version": "0.1.0"
6
+ "version": "0.2.9"
7
7
  },
8
8
  "registry": {
9
9
  "path": "registry.json",
@@ -58,7 +58,7 @@ function makeQuickState(overrides = {}) {
58
58
  }
59
59
  }
60
60
 
61
- function writeManifest(projectRoot) {
61
+ function writeManifest(projectRoot, version = "0.2.9") {
62
62
  const opencodeDir = path.join(projectRoot, ".opencode")
63
63
  fs.mkdirSync(opencodeDir, { recursive: true })
64
64
  fs.writeFileSync(
@@ -66,7 +66,7 @@ function writeManifest(projectRoot) {
66
66
  `${JSON.stringify({
67
67
  kit: {
68
68
  name: "OpenKit AI Software Factory",
69
- version: "0.1.0",
69
+ version,
70
70
  entryAgent: "MasterOrchestrator",
71
71
  },
72
72
  }, null, 2)}\n`,
@@ -130,14 +130,6 @@ function writeMetaSkill(projectRoot) {
130
130
  fs.writeFileSync(path.join(skillDir, "SKILL.md"), "# using-skills\n", "utf8")
131
131
  }
132
132
 
133
- function writeFailingPythonShim(projectRoot) {
134
- const binDir = path.join(projectRoot, "bin")
135
- const shimPath = path.join(binDir, "python3-shim")
136
- fs.mkdirSync(binDir, { recursive: true })
137
- fs.writeFileSync(shimPath, "#!/usr/bin/env bash\nexit 1\n", { encoding: "utf8", mode: 0o755 })
138
- return shimPath
139
- }
140
-
141
133
  test("session-start emits mode-aware resume hint for quick tasks", () => {
142
134
  const projectRoot = makeTempProject()
143
135
 
@@ -157,7 +149,7 @@ test("session-start emits mode-aware resume hint for quick tasks", () => {
157
149
 
158
150
  assert.equal(result.status, 0)
159
151
  assert.match(result.stdout, /<openkit_runtime_status>/)
160
- assert.match(result.stdout, /kit: OpenKit AI Software Factory v0\.1\.0/)
152
+ assert.match(result.stdout, /kit: OpenKit AI Software Factory v0\.2\.9/)
161
153
  assert.match(result.stdout, /startup skill: skipped/)
162
154
  assert.match(result.stdout, /node \.opencode\/workflow-state\.js status/)
163
155
  assert.match(result.stdout, /node \.opencode\/workflow-state\.js doctor/)
@@ -265,9 +257,10 @@ test("session-start prints canonical resume guidance and inspection commands", (
265
257
  test("session-start degrades gracefully when the JSON helper fails", () => {
266
258
  const projectRoot = makeTempProject()
267
259
 
260
+ const manifestPath = path.join(projectRoot, ".opencode", "opencode.json")
268
261
  writeManifest(projectRoot)
269
262
  writeState(projectRoot, makeQuickState())
270
- const pythonShim = writeFailingPythonShim(projectRoot)
263
+ fs.writeFileSync(manifestPath, '{"kit":', 'utf8')
271
264
 
272
265
  const result = spawnSync(path.resolve(__dirname, "../../hooks/session-start"), {
273
266
  cwd: projectRoot,
@@ -277,7 +270,6 @@ test("session-start degrades gracefully when the JSON helper fails", () => {
277
270
  OPENKIT_PROJECT_ROOT: projectRoot,
278
271
  OPENKIT_SESSION_START_NO_SKILL: "1",
279
272
  OPENKIT_WORKFLOW_STATE: path.join(projectRoot, ".opencode", "workflow-state.json"),
280
- OPENKIT_PYTHON_BIN: pythonShim,
281
273
  },
282
274
  })
283
275
 
@@ -38,7 +38,7 @@ function writeManifest(projectRoot) {
38
38
  `${JSON.stringify({
39
39
  kit: {
40
40
  name: "OpenKit AI Software Factory",
41
- version: "0.1.0",
41
+ version: "0.2.9",
42
42
  entryAgent: "MasterOrchestrator",
43
43
  },
44
44
  }, null, 2)}\n`,
@@ -39,7 +39,7 @@ function setupTempRuntime(projectRoot) {
39
39
  `${JSON.stringify({
40
40
  kit: {
41
41
  name: "OpenKit AI Software Factory",
42
- version: "0.1.0",
42
+ version: "0.2.9",
43
43
  entryAgent: "MasterOrchestrator",
44
44
  registry: {
45
45
  path: "registry.json",
@@ -30,7 +30,7 @@ function setupTempRuntime(projectRoot) {
30
30
  `${JSON.stringify({
31
31
  kit: {
32
32
  name: "OpenKit AI Software Factory",
33
- version: "0.1.0",
33
+ version: "0.2.9",
34
34
  entryAgent: "MasterOrchestrator",
35
35
  registry: {
36
36
  path: "registry.json",
@@ -239,7 +239,7 @@ test("status command prints workflow and runtime summary", () => {
239
239
 
240
240
  assert.equal(result.status, 0)
241
241
  assert.match(result.stdout, /OpenKit runtime status:/)
242
- assert.match(result.stdout, /kit: OpenKit AI Software Factory v0\.1\.0/)
242
+ assert.match(result.stdout, /kit: OpenKit AI Software Factory v0\.2\.9/)
243
243
  assert.match(result.stdout, /entry agent: MasterOrchestrator/)
244
244
  assert.match(result.stdout, /active profile: openkit-core/)
245
245
  assert.match(result.stdout, /registry: .*registry\.json/)
@@ -577,7 +577,7 @@ test("version command prints kit metadata version", () => {
577
577
  })
578
578
 
579
579
  assert.equal(result.status, 0)
580
- assert.match(result.stdout, /OpenKit version: 0\.1\.0/)
580
+ assert.match(result.stdout, /OpenKit version: 0\.2\.9/)
581
581
  assert.match(result.stdout, /active profile: openkit-core/)
582
582
  })
583
583
 
@@ -3,7 +3,7 @@
3
3
  "stateVersion": 1,
4
4
  "kit": {
5
5
  "name": "OpenKit",
6
- "version": "0.1.0"
6
+ "version": "0.2.8"
7
7
  },
8
8
  "installation": {
9
9
  "profile": "openkit-core",
@@ -40,7 +40,7 @@ Use it to find canonical repository docs and upkeep surfaces quickly. Do not tre
40
40
 
41
41
  - The preferred end-user onboarding path is `npm install -g @duypham93/openkit` followed by `openkit run`.
42
42
  - The first `openkit run` materializes the managed kit into the OpenCode home directory automatically.
43
- - `openkit doctor` is the read-only check for the global install and workspace bootstrap state.
43
+ - `openkit doctor` is a non-mutating check for the global install and current workspace readiness state.
44
44
  - `openkit install-global`, `openkit install`, and `openkit init` remain available as manual or compatibility commands.
45
45
  - The package intentionally avoids npm `postinstall` side effects; setup happens inside the OpenKit CLI where failures and recovery steps are easier to explain.
46
46
 
@@ -80,7 +80,7 @@ If the boundary still feels fuzzy, use the `Lane Decision Matrix` in `context/co
80
80
 
81
81
  ### 1. Check global install and workspace health
82
82
 
83
- Start with read-only checks:
83
+ Start with non-mutating checks:
84
84
 
85
85
  ```bash
86
86
  openkit doctor
@@ -88,7 +88,7 @@ openkit doctor
88
88
 
89
89
  What to look for:
90
90
 
91
- - `doctor` confirms the global kit is installed, the workspace root is available, and the current project can launch with OpenKit cleanly
91
+ - `doctor` confirms the global kit is installed, shows the derived workspace root, and reports whether the current project can launch with OpenKit cleanly without mutating local workspace files
92
92
 
93
93
  If `doctor` reports `install-missing`, run `openkit run` for first-time setup. If `doctor` reports other errors, fix those before trusting resume or task-board behavior.
94
94
 
@@ -1,162 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- # This script runs when an AI session starts.
4
- # It teaches the agent how to use the Skills library by injecting the meta-skill.
5
-
6
- set -e
3
+ set -euo pipefail
7
4
 
8
5
  DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
9
- KIT_ROOT="${OPENKIT_KIT_ROOT:-$(dirname "$DIR")}"
10
- PROJECT_ROOT="${OPENKIT_PROJECT_ROOT:-$(pwd)}"
11
- META_SKILL_PATH="$KIT_ROOT/skills/using-skills/SKILL.md"
12
- STATE_PATH="${OPENKIT_WORKFLOW_STATE:-$PROJECT_ROOT/.opencode/workflow-state.json}"
13
- MANIFEST_PATH="$KIT_ROOT/.opencode/opencode.json"
14
- PYTHON_BIN="${OPENKIT_PYTHON_BIN:-python3}"
15
- RUNTIME_SUMMARY_MODULE="$KIT_ROOT/.opencode/lib/runtime-summary.js"
16
-
17
- KIT_NAME="OpenKit AI Software Factory"
18
- KIT_VERSION="unknown"
19
- ENTRY_AGENT="unknown"
20
- JSON_HELPER_STATUS="ok"
21
-
22
- if [ -f "$MANIFEST_PATH" ] && command -v "$PYTHON_BIN" >/dev/null 2>&1; then
23
- KIT_INFO_RAW=$("$PYTHON_BIN" - "$MANIFEST_PATH" <<'PY' || true
24
- import json
25
- import sys
26
-
27
- manifest_path = sys.argv[1]
28
-
29
- try:
30
- with open(manifest_path, "r", encoding="utf-8") as fh:
31
- manifest = json.load(fh)
32
- except Exception:
33
- sys.exit(0)
34
-
35
- kit = manifest.get("kit") or {}
36
- print(kit.get("name", "OpenKit AI Software Factory"))
37
- print(kit.get("version", "unknown"))
38
- print(kit.get("entryAgent", "unknown"))
39
- PY
40
- )
41
- KIT_INFO_1=$(printf '%s\n' "$KIT_INFO_RAW" | sed -n '1p')
42
- KIT_INFO_2=$(printf '%s\n' "$KIT_INFO_RAW" | sed -n '2p')
43
- KIT_INFO_3=$(printf '%s\n' "$KIT_INFO_RAW" | sed -n '3p')
44
- if [ -n "$KIT_INFO_1" ] && [ -n "$KIT_INFO_2" ] && [ -n "$KIT_INFO_3" ]; then
45
- KIT_NAME="$KIT_INFO_1"
46
- KIT_VERSION="$KIT_INFO_2"
47
- ENTRY_AGENT="$KIT_INFO_3"
48
- else
49
- JSON_HELPER_STATUS="degraded"
50
- fi
51
- elif [ -f "$MANIFEST_PATH" ]; then
52
- JSON_HELPER_STATUS="degraded"
53
- fi
54
-
55
- if [ -n "${OPENKIT_SESSION_START_NO_SKILL:-}" ]; then
56
- SKILL_STATUS="skipped"
57
- elif [ -f "$META_SKILL_PATH" ]; then
58
- SKILL_STATUS="loaded"
59
- else
60
- SKILL_STATUS="missing"
61
- fi
62
-
63
- echo "<openkit_runtime_status>"
64
- echo "kit: $KIT_NAME v$KIT_VERSION"
65
- echo "entry agent: $ENTRY_AGENT"
66
- echo "state file: $STATE_PATH"
67
- echo "startup skill: $SKILL_STATUS"
68
- echo "json helper: $JSON_HELPER_STATUS"
69
- echo "help: node .opencode/workflow-state.js status"
70
- echo "doctor: node .opencode/workflow-state.js doctor"
71
- echo "show: node .opencode/workflow-state.js show"
72
- echo "</openkit_runtime_status>"
73
-
74
- if [ -z "${OPENKIT_SESSION_START_NO_SKILL:-}" ] && [ -f "$META_SKILL_PATH" ]; then
75
- echo "<skill_system_instruction>"
76
- echo "You are running within the Open Kit AI Software Factory framework."
77
- echo "Below are the rules for how you must discover and invoke your skills."
78
- echo ""
79
- cat "$META_SKILL_PATH"
80
- echo "</skill_system_instruction>"
81
- fi
82
-
83
- if [ -f "$STATE_PATH" ] && [ "$JSON_HELPER_STATUS" = "ok" ] && command -v "$PYTHON_BIN" >/dev/null 2>&1; then
84
- "$PYTHON_BIN" - "$STATE_PATH" "$RUNTIME_SUMMARY_MODULE" <<'PY' || true
85
- import json
86
- import os
87
- import pathlib
88
- import subprocess
89
- import sys
90
-
91
- state_path = sys.argv[1]
92
- runtime_summary_module = sys.argv[2]
93
- runtime_root = str(pathlib.Path(state_path).resolve().parents[1])
94
-
95
- try:
96
- with open(state_path, "r", encoding="utf-8") as fh:
97
- state = json.load(fh)
98
- except Exception:
99
- sys.exit(0)
100
-
101
- mode = state.get("mode")
102
- stage = state.get("current_stage")
103
- status = state.get("status")
104
- owner = state.get("current_owner")
105
- feature_id = state.get("feature_id")
106
- feature_slug = state.get("feature_slug")
107
- work_item_id = state.get("work_item_id")
108
-
109
- if not all([mode, stage, status, owner]):
110
- sys.exit(0)
111
-
112
- task_summary = None
113
- active_tasks = []
114
-
115
- if mode == "full" and work_item_id:
116
- try:
117
- node_cmd = [
118
- "node",
119
- "-e",
120
- (
121
- "const { getRuntimeContext } = require(process.argv[1]);"
122
- "const context = getRuntimeContext(process.argv[2], JSON.parse(process.argv[3]));"
123
- "process.stdout.write(JSON.stringify(context));"
124
- ),
125
- runtime_summary_module,
126
- runtime_root,
127
- json.dumps(state),
128
- ]
129
- result = subprocess.run(node_cmd, check=True, capture_output=True, text=True)
130
- runtime_context = json.loads(result.stdout.strip() or "{}")
131
- board_summary = runtime_context.get("taskBoardSummary")
132
- if board_summary:
133
- task_summary = (
134
- f"task board: {board_summary.get('total', 0)} tasks | "
135
- f"ready {board_summary.get('ready', 0)} | active {board_summary.get('active', 0)}"
136
- )
137
- active_tasks = board_summary.get("activeTasks") or []
138
- except Exception:
139
- task_summary = None
140
- active_tasks = []
6
+ NODE_BIN="${OPENKIT_NODE_BIN:-node}"
141
7
 
142
- print("<workflow_resume_hint>")
143
- print("OpenKit workflow resume context detected.")
144
- print(f"mode: {mode}")
145
- print(f"stage: {stage}")
146
- print(f"status: {status}")
147
- print(f"owner: {owner}")
148
- if feature_id and feature_slug:
149
- print(f"work item: {feature_id} ({feature_slug})")
150
- if work_item_id:
151
- print(f"active work item id: {work_item_id}")
152
- if task_summary:
153
- print(task_summary)
154
- if active_tasks:
155
- print(f"active tasks: {'; '.join(active_tasks)}")
156
- print("Read first: AGENTS.md -> context/navigation.md -> context/core/workflow.md -> .opencode/workflow-state.json")
157
- print("Then load resume guidance from context/core/session-resume.md.")
158
- if mode == "full" and active_tasks:
159
- print("Parallel task support is not yet assumed safe by this hook; confirm with `node .opencode/workflow-state.js doctor` before relying on it.")
160
- print("</workflow_resume_hint>")
161
- PY
162
- fi
8
+ exec "$NODE_BIN" "$DIR/session-start.js" "$@"
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createRequire } from 'node:module';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ const DEFAULT_KIT_ROOT = path.resolve(SCRIPT_DIR, '..');
11
+
12
+ function resolveKitRoot(projectRoot, statePath) {
13
+ if (process.env.OPENKIT_KIT_ROOT) {
14
+ return path.resolve(process.env.OPENKIT_KIT_ROOT);
15
+ }
16
+
17
+ const candidates = [
18
+ projectRoot,
19
+ path.dirname(path.resolve(statePath)),
20
+ DEFAULT_KIT_ROOT,
21
+ ];
22
+
23
+ for (const candidate of candidates) {
24
+ const manifestPath = path.join(candidate, '.opencode', 'opencode.json');
25
+ if (fs.existsSync(manifestPath)) {
26
+ return candidate;
27
+ }
28
+ }
29
+
30
+ return DEFAULT_KIT_ROOT;
31
+ }
32
+
33
+ function print(line = '') {
34
+ process.stdout.write(`${line}\n`);
35
+ }
36
+
37
+ function readJsonIfPresent(filePath) {
38
+ if (!fs.existsSync(filePath)) {
39
+ return { exists: false, value: null, malformed: false };
40
+ }
41
+
42
+ try {
43
+ return {
44
+ exists: true,
45
+ value: JSON.parse(fs.readFileSync(filePath, 'utf8')),
46
+ malformed: false,
47
+ };
48
+ } catch {
49
+ return {
50
+ exists: true,
51
+ value: null,
52
+ malformed: true,
53
+ };
54
+ }
55
+ }
56
+
57
+ function resolveRuntimeContext(runtimeSummaryModulePath, runtimeRoot, state) {
58
+ try {
59
+ const { getRuntimeContext } = require(runtimeSummaryModulePath);
60
+ return getRuntimeContext(runtimeRoot, state);
61
+ } catch {
62
+ const workItemId = state?.work_item_id;
63
+ if (!workItemId) {
64
+ return null;
65
+ }
66
+
67
+ const boardPath = path.join(runtimeRoot, '.opencode', 'work-items', workItemId, 'tasks.json');
68
+ if (!fs.existsSync(boardPath)) {
69
+ return null;
70
+ }
71
+
72
+ try {
73
+ const board = JSON.parse(fs.readFileSync(boardPath, 'utf8'));
74
+ const tasks = Array.isArray(board.tasks) ? board.tasks : [];
75
+ const activeStatuses = new Set(['claimed', 'in_progress', 'qa_in_progress']);
76
+ const activeTasks = tasks.filter((task) => activeStatuses.has(task.status));
77
+ const formattedActiveTasks = activeTasks.map((task) => {
78
+ if (task.status === 'qa_in_progress' && task.qa_owner) {
79
+ return `${task.task_id} (${task.status}, qa: ${task.qa_owner})`;
80
+ }
81
+
82
+ if (task.primary_owner) {
83
+ return `${task.task_id} (${task.status}, primary: ${task.primary_owner})`;
84
+ }
85
+
86
+ return `${task.task_id} (${task.status})`;
87
+ });
88
+
89
+ return {
90
+ taskBoardSummary: {
91
+ total: tasks.length,
92
+ ready: tasks.filter((task) => task.status === 'ready').length,
93
+ active: activeTasks.length,
94
+ activeTasks: formattedActiveTasks,
95
+ },
96
+ };
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ }
102
+
103
+ function renderResumeHint(state, runtimeSummaryModulePath, statePath) {
104
+ const mode = state?.mode;
105
+ const stage = state?.current_stage;
106
+ const status = state?.status;
107
+ const owner = state?.current_owner;
108
+ const featureId = state?.feature_id;
109
+ const featureSlug = state?.feature_slug;
110
+ const workItemId = state?.work_item_id;
111
+
112
+ if (!mode || !stage || !status || !owner) {
113
+ return;
114
+ }
115
+
116
+ const runtimeRoot = path.dirname(path.dirname(path.resolve(statePath)));
117
+ const runtimeContext = mode === 'full' && workItemId
118
+ ? resolveRuntimeContext(runtimeSummaryModulePath, runtimeRoot, state)
119
+ : null;
120
+ const boardSummary = runtimeContext?.taskBoardSummary ?? null;
121
+ const activeTasks = boardSummary?.activeTasks ?? [];
122
+
123
+ print('<workflow_resume_hint>');
124
+ print('OpenKit workflow resume context detected.');
125
+ print(`mode: ${mode}`);
126
+ print(`stage: ${stage}`);
127
+ print(`status: ${status}`);
128
+ print(`owner: ${owner}`);
129
+ if (featureId && featureSlug) {
130
+ print(`work item: ${featureId} (${featureSlug})`);
131
+ }
132
+ if (workItemId) {
133
+ print(`active work item id: ${workItemId}`);
134
+ }
135
+ if (boardSummary) {
136
+ print(`task board: ${boardSummary.total ?? 0} tasks | ready ${boardSummary.ready ?? 0} | active ${boardSummary.active ?? 0}`);
137
+ }
138
+ if (activeTasks.length > 0) {
139
+ print(`active tasks: ${activeTasks.join('; ')}`);
140
+ }
141
+ print('Read first: AGENTS.md -> context/navigation.md -> context/core/workflow.md -> .opencode/workflow-state.json');
142
+ print('Then load resume guidance from context/core/session-resume.md.');
143
+ if (mode === 'full' && activeTasks.length > 0) {
144
+ print('Parallel task support is not yet assumed safe by this hook; confirm with `node .opencode/workflow-state.js doctor` before relying on it.');
145
+ }
146
+ print('</workflow_resume_hint>');
147
+ }
148
+
149
+ const projectRoot = process.env.OPENKIT_PROJECT_ROOT ? path.resolve(process.env.OPENKIT_PROJECT_ROOT) : process.cwd();
150
+ const statePath = process.env.OPENKIT_WORKFLOW_STATE
151
+ ? path.resolve(process.env.OPENKIT_WORKFLOW_STATE)
152
+ : path.join(projectRoot, '.opencode', 'workflow-state.json');
153
+ const kitRoot = resolveKitRoot(projectRoot, statePath);
154
+ const metaSkillPath = path.join(kitRoot, 'skills', 'using-skills', 'SKILL.md');
155
+ const manifestPath = path.join(kitRoot, '.opencode', 'opencode.json');
156
+ const runtimeSummaryModulePath = path.join(kitRoot, '.opencode', 'lib', 'runtime-summary.js');
157
+
158
+ let kitName = 'OpenKit AI Software Factory';
159
+ let kitVersion = 'unknown';
160
+ let entryAgent = 'unknown';
161
+ let jsonHelperStatus = 'ok';
162
+
163
+ const manifestResult = readJsonIfPresent(manifestPath);
164
+ if (manifestResult.malformed) {
165
+ jsonHelperStatus = 'degraded';
166
+ } else if (manifestResult.value?.kit) {
167
+ kitName = manifestResult.value.kit.name || kitName;
168
+ kitVersion = manifestResult.value.kit.version || kitVersion;
169
+ entryAgent = manifestResult.value.kit.entryAgent || entryAgent;
170
+ }
171
+
172
+ let skillStatus = 'missing';
173
+ if (process.env.OPENKIT_SESSION_START_NO_SKILL) {
174
+ skillStatus = 'skipped';
175
+ } else if (fs.existsSync(metaSkillPath)) {
176
+ skillStatus = 'loaded';
177
+ }
178
+
179
+ const stateResult = readJsonIfPresent(statePath);
180
+ if (stateResult.malformed) {
181
+ jsonHelperStatus = 'degraded';
182
+ }
183
+
184
+ print('<openkit_runtime_status>');
185
+ print(`kit: ${kitName} v${kitVersion}`);
186
+ print(`entry agent: ${entryAgent}`);
187
+ print(`state file: ${statePath}`);
188
+ print(`startup skill: ${skillStatus}`);
189
+ print(`json helper: ${jsonHelperStatus}`);
190
+ print('help: node .opencode/workflow-state.js status');
191
+ print('doctor: node .opencode/workflow-state.js doctor');
192
+ print('show: node .opencode/workflow-state.js show');
193
+ print('</openkit_runtime_status>');
194
+
195
+ if (!process.env.OPENKIT_SESSION_START_NO_SKILL && fs.existsSync(metaSkillPath)) {
196
+ const metaSkill = fs.readFileSync(metaSkillPath, 'utf8');
197
+ print('<skill_system_instruction>');
198
+ print('You are running within the Open Kit AI Software Factory framework.');
199
+ print('Below are the rules for how you must discover and invoke your skills.');
200
+ print();
201
+ process.stdout.write(metaSkill);
202
+ if (!metaSkill.endsWith('\n')) {
203
+ print();
204
+ }
205
+ print('</skill_system_instruction>');
206
+ }
207
+
208
+ if (jsonHelperStatus === 'ok' && !stateResult.malformed && stateResult.value) {
209
+ renderResumeHint(stateResult.value, runtimeSummaryModulePath, statePath);
210
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duypham93/openkit",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "files": [
6
6
  ".opencode/",
package/registry.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "registryVersion": 1,
4
4
  "kit": {
5
5
  "name": "OpenKit AI Software Factory",
6
- "version": "0.1.0",
6
+ "version": "0.2.9",
7
7
  "description": "Local component metadata for the checked-in OpenKit repository and the global-kit compatibility surface.",
8
8
  "repositoryRoot": ".",
9
9
  "productSurface": {
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ function listWindowsExecutableExtensions(env) {
5
+ const raw = env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD';
6
+ const extensions = raw
7
+ .split(';')
8
+ .map((value) => value.trim().toLowerCase())
9
+ .filter(Boolean);
10
+
11
+ return extensions.length > 0 ? extensions : ['.com', '.exe', '.bat', '.cmd'];
12
+ }
13
+
14
+ export function isCommandAvailable(command, { env = process.env, platform = process.platform } = {}) {
15
+ if (typeof command !== 'string' || command.length === 0) {
16
+ return false;
17
+ }
18
+
19
+ const pathValue = env.PATH ?? '';
20
+ if (pathValue.length === 0) {
21
+ return false;
22
+ }
23
+
24
+ const segments = pathValue.split(path.delimiter).filter(Boolean);
25
+ const hasExtension = path.extname(command).length > 0;
26
+ const suffixes = platform === 'win32' && !hasExtension ? ['', ...listWindowsExecutableExtensions(env)] : [''];
27
+
28
+ for (const segment of segments) {
29
+ for (const suffix of suffixes) {
30
+ if (fs.existsSync(path.join(segment, `${command}${suffix}`))) {
31
+ return true;
32
+ }
33
+ }
34
+ }
35
+
36
+ return false;
37
+ }
@@ -2,12 +2,12 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { readJsonIfPresent, validateGlobalInstallState } from './install-state.js';
5
- import { ensureWorkspaceBootstrap, readWorkspaceMeta } from './workspace-state.js';
5
+ import { inspectWorkspaceMeta } from './workspace-state.js';
6
6
  import { getGlobalPaths, getWorkspacePaths } from './paths.js';
7
+ import { isCommandAvailable } from '../command-detection.js';
7
8
 
8
9
  function isOpenCodeAvailable(env = process.env) {
9
- const pathValue = env.PATH ?? '';
10
- return pathValue.split(path.delimiter).some((segment) => segment && fs.existsSync(path.join(segment, 'opencode')));
10
+ return isCommandAvailable('opencode', { env });
11
11
  }
12
12
 
13
13
  function withGuidance(result, nextStep, recommendedCommand = null) {
@@ -59,8 +59,7 @@ export function inspectGlobalDoctor({ projectRoot = process.cwd(), env = process
59
59
  issues.push('OpenCode executable is not available on PATH.');
60
60
  }
61
61
 
62
- const workspace = readWorkspaceMeta({ projectRoot, env });
63
- ensureWorkspaceBootstrap({ projectRoot, env });
62
+ const workspace = inspectWorkspaceMeta({ projectRoot, env });
64
63
 
65
64
  return withGuidance({
66
65
  status: issues.length === 0 ? 'healthy' : 'workspace-ready-with-issues',
@@ -1,9 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ import { getOpenKitVersion } from '../version.js';
5
+
4
6
  const GLOBAL_INSTALL_SCHEMA = 'openkit/global-install-state@1';
5
7
 
6
- export function createGlobalInstallState({ kitVersion = '0.1.0', installedAt = new Date().toISOString(), profile = 'openkit' } = {}) {
8
+ export function createGlobalInstallState({ kitVersion = getOpenKitVersion(), installedAt = new Date().toISOString(), profile = 'openkit' } = {}) {
7
9
  return {
8
10
  schema: GLOBAL_INSTALL_SCHEMA,
9
11
  stateVersion: 1,
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { createGlobalInstallState, writeJson } from './install-state.js';
6
6
  import { getGlobalPaths } from './paths.js';
7
+ import { getOpenKitVersion } from '../version.js';
7
8
 
8
9
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
9
10
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, '../..');
@@ -65,7 +66,7 @@ function createOpenCodeConfig() {
65
66
  };
66
67
  }
67
68
 
68
- export function materializeGlobalInstall({ env = process.env, kitVersion = '0.1.0' } = {}) {
69
+ export function materializeGlobalInstall({ env = process.env, kitVersion = getOpenKitVersion() } = {}) {
69
70
  const paths = getGlobalPaths({ env });
70
71
 
71
72
  removePathIfPresent(paths.kitRoot);
@@ -92,7 +93,7 @@ export function materializeGlobalInstall({ env = process.env, kitVersion = '0.1.
92
93
  hooks: [
93
94
  {
94
95
  type: 'command',
95
- command: `${path.join(paths.kitRoot, 'hooks', 'session-start')}`,
96
+ command: `${JSON.stringify(process.execPath)} ${JSON.stringify(path.join(paths.kitRoot, 'hooks', 'session-start.js'))}`,
96
97
  async: false,
97
98
  },
98
99
  ],
@@ -29,6 +29,49 @@ function readJson(filePath) {
29
29
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
30
30
  }
31
31
 
32
+ function isSymlink(filePath) {
33
+ try {
34
+ return fs.lstatSync(filePath).isSymbolicLink();
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ function isManagedWorkflowWrapper(content, { requiresAliasMap = false } = {}) {
41
+ if (typeof content !== 'string' || content.length === 0) {
42
+ return false;
43
+ }
44
+
45
+ const sharedMarkers = [
46
+ "require('node:child_process')",
47
+ 'spawnSync(process.execPath',
48
+ "'--state'",
49
+ "process.exit(typeof result.status === 'number' ? result.status : 1);",
50
+ ];
51
+
52
+ if (!sharedMarkers.every((marker) => content.includes(marker))) {
53
+ return false;
54
+ }
55
+
56
+ if (!requiresAliasMap) {
57
+ return true;
58
+ }
59
+
60
+ return content.includes('const aliasMap = new Map([');
61
+ }
62
+
63
+ function shouldRefreshRootWorkflowWrapper(filePath) {
64
+ if (!fs.existsSync(filePath)) {
65
+ return true;
66
+ }
67
+
68
+ try {
69
+ return isManagedWorkflowWrapper(fs.readFileSync(filePath, 'utf8'), { requiresAliasMap: true });
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
32
75
  function relativeTarget(fromPath, toPath) {
33
76
  return path.relative(path.dirname(fromPath), toPath) || '.';
34
77
  }
@@ -102,7 +145,8 @@ export function ensureWorkspaceShim(paths) {
102
145
  }
103
146
 
104
147
  const workflowCli = `#!/usr/bin/env node
105
- import { spawnSync } from 'node:child_process';
148
+ // Generated by OpenKit workspace shim.
149
+ const { spawnSync } = require('node:child_process');
106
150
 
107
151
  const args = process.argv.slice(2);
108
152
  const result = spawnSync(process.execPath, [${JSON.stringify(path.join(paths.kitRoot, '.opencode', 'workflow-state.js'))}, '--state', ${JSON.stringify(paths.workflowStatePath)}, ...args], {
@@ -117,8 +161,9 @@ if (result.error) {
117
161
  process.exit(typeof result.status === 'number' ? result.status : 1);
118
162
  `;
119
163
 
120
- if (!fs.existsSync(paths.workspaceShimWorkflowCliPath)) {
121
- writeFile(paths.workspaceShimWorkflowCliPath, workflowCli, 0o755);
164
+ const workspaceWrapperExists = fs.existsSync(paths.workspaceShimWorkflowCliPath);
165
+ writeFile(paths.workspaceShimWorkflowCliPath, workflowCli, 0o755);
166
+ if (!workspaceWrapperExists) {
122
167
  createdPaths.push(paths.workspaceShimWorkflowCliPath);
123
168
  }
124
169
 
@@ -134,22 +179,20 @@ process.exit(typeof result.status === 'number' ? result.status : 1);
134
179
  type: 'dir',
135
180
  });
136
181
 
137
- createIfMissing(createdPaths, {
182
+ const rootWorkflowStateMode = createIfMissing(createdPaths, {
138
183
  linkPath: path.join(paths.projectRoot, '.opencode', 'workflow-state.json'),
139
184
  targetPath: paths.workspaceShimWorkflowStatePath,
140
185
  type: 'file',
141
186
  });
142
187
 
143
188
  const rootWorkflowStatePath = path.join(paths.projectRoot, '.opencode', 'workflow-state.json');
144
- if (!fs.existsSync(rootWorkflowStatePath)) {
145
- createdPaths.push(rootWorkflowStatePath);
146
- }
147
- if (fs.existsSync(paths.workflowStatePath)) {
189
+ if (fs.existsSync(paths.workflowStatePath) && (rootWorkflowStateMode !== null || isSymlink(rootWorkflowStatePath))) {
148
190
  writeJson(rootWorkflowStatePath, readJson(paths.workflowStatePath));
149
191
  }
150
192
 
151
- if (!fs.existsSync(path.join(paths.projectRoot, '.opencode', 'workflow-state.js'))) {
193
+ if (shouldRefreshRootWorkflowWrapper(path.join(paths.projectRoot, '.opencode', 'workflow-state.js'))) {
152
194
  const rootWorkflowCli = `#!/usr/bin/env node
195
+ // Generated by OpenKit workspace shim.
153
196
  const { spawnSync } = require('node:child_process');
154
197
 
155
198
  const rawArgs = process.argv.slice(2);
@@ -173,8 +216,11 @@ process.exit(typeof result.status === 'number' ? result.status : 1);
173
216
  `;
174
217
 
175
218
  const rootWorkflowCliPath = path.join(paths.projectRoot, '.opencode', 'workflow-state.js');
219
+ const rootWrapperExists = fs.existsSync(rootWorkflowCliPath);
176
220
  writeFile(rootWorkflowCliPath, rootWorkflowCli, 0o755);
177
- createdPaths.push(rootWorkflowCliPath);
221
+ if (!rootWrapperExists) {
222
+ createdPaths.push(rootWorkflowCliPath);
223
+ }
178
224
  }
179
225
 
180
226
  if (!fs.existsSync(path.join(paths.projectRoot, '.opencode', 'work-items'))) {
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  import { getWorkspacePaths } from './paths.js';
5
5
  import { ensureWorkspaceShim } from './workspace-shim.js';
6
+ import { getOpenKitVersion } from '../version.js';
6
7
 
7
8
  const WORKSPACE_STATE_SCHEMA = 'openkit/workspace-state@1';
8
9
 
@@ -115,7 +116,7 @@ function readJson(filePath) {
115
116
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
116
117
  }
117
118
 
118
- export function createWorkspaceMeta({ projectRoot, workspaceId, kitVersion = '0.1.0', profile = 'openkit' }) {
119
+ export function createWorkspaceMeta({ projectRoot, workspaceId, kitVersion = getOpenKitVersion(), profile = 'openkit' }) {
119
120
  return {
120
121
  schema: WORKSPACE_STATE_SCHEMA,
121
122
  stateVersion: 1,
@@ -171,3 +172,13 @@ export function readWorkspaceMeta(options = {}) {
171
172
  index: readJson(paths.workItemIndexPath),
172
173
  };
173
174
  }
175
+
176
+ export function inspectWorkspaceMeta(options = {}) {
177
+ const paths = getWorkspacePaths(options);
178
+
179
+ return {
180
+ paths,
181
+ meta: fs.existsSync(paths.workspaceMetaPath) ? readJson(paths.workspaceMetaPath) : null,
182
+ index: fs.existsSync(paths.workItemIndexPath) ? readJson(paths.workItemIndexPath) : null,
183
+ };
184
+ }
@@ -1,3 +1,5 @@
1
+ import { getOpenKitVersion } from '../version.js'
2
+
1
3
  export const INSTALL_STATE_SCHEMA = "openkit/install-state@1"
2
4
 
3
5
  const MANAGED_STATUSES = new Set(["managed", "materialized"])
@@ -24,7 +26,7 @@ function isArray(value) {
24
26
  }
25
27
 
26
28
  export function createInstallState({
27
- kitVersion = "0.1.0",
29
+ kitVersion = getOpenKitVersion(),
28
30
  profile = "openkit-core",
29
31
  managedAssets = [],
30
32
  adoptedAssets = [],
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url"
5
5
  import { createInstallState } from "./install-state.js"
6
6
  import { applyOpenKitMergePolicy } from "./merge-policy.js"
7
7
  import { createMaterializationConflict, qualifyMergeConflicts } from "./conflicts.js"
8
+ import { getOpenKitVersion } from "../version.js"
8
9
 
9
10
  const ROOT_MANIFEST_ASSET_ID = "runtime.opencode-manifest"
10
11
  const ROOT_MANIFEST_PATH = "opencode.json"
@@ -40,7 +41,7 @@ function removeFileIfPresent(filePath) {
40
41
  }
41
42
  }
42
43
 
43
- export function materializeInstall(projectRoot, { kitVersion = "0.1.0", now } = {}) {
44
+ export function materializeInstall(projectRoot, { kitVersion = getOpenKitVersion(), now } = {}) {
44
45
  const desiredRootManifest = readTemplate("assets/opencode.json.template")
45
46
  const rootManifestPath = path.join(projectRoot, ROOT_MANIFEST_PATH)
46
47
  const installStatePath = path.join(projectRoot, INSTALL_STATE_PATH)
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  import { validateInstallState } from '../install/install-state.js';
5
5
  import { discoverProjectShape } from '../install/discovery.js';
6
+ import { isCommandAvailable } from '../command-detection.js';
6
7
 
7
8
  const EXPECTED_MANAGED_ASSETS = {
8
9
  'runtime.opencode-manifest': {
@@ -52,15 +53,7 @@ function readJsonIfPresent(filePath) {
52
53
  }
53
54
 
54
55
  function defaultIsOpenCodeAvailable() {
55
- const pathValue = process.env.PATH ?? '';
56
- return pathValue.split(path.delimiter).some((segment) => {
57
- if (!segment) {
58
- return false;
59
- }
60
-
61
- const candidate = path.join(segment, 'opencode');
62
- return fs.existsSync(candidate);
63
- });
56
+ return isCommandAvailable('opencode');
64
57
  }
65
58
 
66
59
  export function inspectManagedDoctor({
package/src/version.js ADDED
@@ -0,0 +1,25 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
6
+ const PACKAGE_JSON_PATH = path.resolve(MODULE_DIR, '..', 'package.json');
7
+
8
+ let cachedVersion = null;
9
+
10
+ export function getOpenKitVersion() {
11
+ if (cachedVersion !== null) {
12
+ return cachedVersion;
13
+ }
14
+
15
+ try {
16
+ const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
17
+ cachedVersion = typeof packageJson.version === 'string' && packageJson.version.length > 0
18
+ ? packageJson.version
19
+ : 'unknown';
20
+ } catch {
21
+ cachedVersion = 'unknown';
22
+ }
23
+
24
+ return cachedVersion;
25
+ }
@@ -6,6 +6,8 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
+ import { inspectGlobalDoctor } from '../../src/global/doctor.js';
10
+
9
11
  const __filename = fileURLToPath(import.meta.url);
10
12
  const __dirname = path.dirname(__filename);
11
13
  const worktreeRoot = path.resolve(__dirname, '..', '..');
@@ -96,6 +98,8 @@ test('openkit install-global materializes global kit and profile files', () => {
96
98
  assert.equal(fs.existsSync(path.join(profileRoot, 'opencode.json')), true);
97
99
  assert.equal(readJson(path.join(profileRoot, 'opencode.json')).default_agent, 'master-orchestrator');
98
100
  assert.equal(fs.existsSync(path.join(kitRoot, 'opencode.json')), true);
101
+ assert.match(readJson(path.join(kitRoot, 'install-state.json')).kit.version, /^0\.2\.9$/);
102
+ assert.match(readJson(path.join(profileRoot, 'hooks.json')).hooks.SessionStart[0].hooks[0].command, /session-start\.js/);
99
103
  });
100
104
 
101
105
  test('openkit init and install remain compatibility aliases for install-global', () => {
@@ -140,7 +144,7 @@ test('openkit doctor reports install-missing when global install is absent', ()
140
144
  assert.match(result.stdout, /Recommended command: openkit run/);
141
145
  });
142
146
 
143
- test('openkit doctor reports healthy after global install and bootstraps workspace metadata', () => {
147
+ test('openkit doctor reports healthy without mutating workspace metadata', () => {
144
148
  const tempHome = makeTempDir();
145
149
  const projectRoot = makeTempDir();
146
150
  const fakeBinDir = path.join(tempHome, 'bin');
@@ -168,12 +172,8 @@ test('openkit doctor reports healthy after global install and bootstraps workspa
168
172
  assert.match(result.stdout, /Workspace root:/);
169
173
  assert.match(result.stdout, /Next: Run openkit run/);
170
174
  assert.match(result.stdout, /Recommended command: openkit run/);
171
-
172
- const workspacesRoot = path.join(tempHome, 'workspaces');
173
- const workspaceEntries = fs.readdirSync(workspacesRoot);
174
- assert.equal(workspaceEntries.length, 1);
175
- const workspaceMetaPath = path.join(workspacesRoot, workspaceEntries[0], 'openkit', 'workspace.json');
176
- assert.equal(fs.existsSync(workspaceMetaPath), true);
175
+ assert.equal(fs.existsSync(path.join(tempHome, 'workspaces')), false);
176
+ assert.equal(fs.existsSync(path.join(projectRoot, '.opencode')), false);
177
177
  });
178
178
 
179
179
  test('openkit run launches opencode with the global profile and workspace env', () => {
@@ -336,9 +336,75 @@ test('openkit run does not overwrite existing repo-local workflow files when cre
336
336
  assert.equal(fs.readFileSync(path.join(projectRoot, 'AGENTS.md'), 'utf8'), 'project agents\n');
337
337
  assert.equal(fs.readFileSync(path.join(projectRoot, 'context', 'core', 'workflow.md'), 'utf8'), 'project workflow\n');
338
338
  assert.equal(fs.readFileSync(path.join(projectRoot, '.opencode', 'workflow-state.js'), 'utf8'), '#!/usr/bin/env node\n');
339
+ assert.equal(fs.readFileSync(path.join(projectRoot, '.opencode', 'workflow-state.json'), 'utf8'), '{"project":true}\n');
339
340
  assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'AGENTS.md')), true);
340
341
  });
341
342
 
343
+ test('openkit run refreshes managed wrappers when the workspace location changes', () => {
344
+ const tempHomeA = makeTempDir();
345
+ const tempHomeB = makeTempDir();
346
+ const projectRoot = makeTempDir();
347
+ const fakeBinDirA = path.join(tempHomeA, 'bin');
348
+ const fakeBinDirB = path.join(tempHomeB, 'bin');
349
+
350
+ writeExecutable(path.join(fakeBinDirA, 'opencode'), '#!/bin/sh\nexit 0\n');
351
+ writeExecutable(path.join(fakeBinDirB, 'opencode'), '#!/bin/sh\nexit 0\n');
352
+
353
+ let result = runCli(['run'], {
354
+ cwd: projectRoot,
355
+ env: {
356
+ ...process.env,
357
+ OPENCODE_HOME: tempHomeA,
358
+ PATH: `${fakeBinDirA}${path.delimiter}${process.env.PATH}`,
359
+ },
360
+ });
361
+
362
+ assert.equal(result.status, 0);
363
+
364
+ result = runCli(['run'], {
365
+ cwd: projectRoot,
366
+ env: {
367
+ ...process.env,
368
+ OPENCODE_HOME: tempHomeB,
369
+ PATH: `${fakeBinDirB}${path.delimiter}${process.env.PATH}`,
370
+ },
371
+ });
372
+
373
+ assert.equal(result.status, 0);
374
+
375
+ const workspaceWrapper = fs.readFileSync(path.join(projectRoot, '.opencode', 'openkit', 'workflow-state.js'), 'utf8');
376
+ assert.match(workspaceWrapper, new RegExp(tempHomeB.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
377
+ assert.doesNotMatch(workspaceWrapper, new RegExp(tempHomeA.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
378
+ });
379
+
380
+ test('openkit doctor recognizes opencode.cmd on Windows-style PATH', () => {
381
+ const tempHome = makeTempDir();
382
+ const projectRoot = makeTempDir();
383
+ const fakeBinDir = path.join(tempHome, 'bin');
384
+ writeExecutable(path.join(fakeBinDir, 'opencode.cmd'), '@echo off\nexit /b 0\n');
385
+
386
+ const installResult = runCli(['install-global'], {
387
+ env: {
388
+ ...process.env,
389
+ OPENCODE_HOME: tempHome,
390
+ },
391
+ });
392
+ assert.equal(installResult.status, 0);
393
+
394
+ const doctor = inspectGlobalDoctor({
395
+ projectRoot,
396
+ env: {
397
+ ...process.env,
398
+ OPENCODE_HOME: tempHome,
399
+ PATH: fakeBinDir,
400
+ PATHEXT: '.CMD;.EXE',
401
+ },
402
+ });
403
+
404
+ assert.equal(doctor.status, 'workspace-ready-with-issues');
405
+ assert.match(doctor.issues.join('\n'), /OpenCode executable is not available on PATH/);
406
+ });
407
+
342
408
  test('openkit run cleans root compatibility shims when created files are removed', () => {
343
409
  const tempHome = makeTempDir();
344
410
  const projectRoot = makeTempDir();
@@ -366,7 +432,7 @@ test('openkit run cleans root compatibility shims when created files are removed
366
432
  assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'AGENTS.md')), true);
367
433
  });
368
434
 
369
- test('openkit run creates a module-aware root workflow wrapper with alias support', () => {
435
+ test('openkit run creates CommonJS workflow wrappers without module-boundary warnings', () => {
370
436
  const tempHome = makeTempDir();
371
437
  const projectRoot = makeTempDir();
372
438
  const fakeBinDir = path.join(tempHome, 'bin');
@@ -384,10 +450,23 @@ test('openkit run creates a module-aware root workflow wrapper with alias suppor
384
450
 
385
451
  assert.equal(result.status, 0);
386
452
 
387
- const wrapper = fs.readFileSync(path.join(projectRoot, '.opencode', 'workflow-state.js'), 'utf8');
388
- assert.match(wrapper, /const \{ spawnSync \} = require\('node:child_process'\);/);
389
- assert.match(wrapper, /\['get', 'show'\]/);
390
- assert.match(wrapper, /\['--help', 'help'\]/);
453
+ const rootWrapper = fs.readFileSync(path.join(projectRoot, '.opencode', 'workflow-state.js'), 'utf8');
454
+ assert.match(rootWrapper, /const \{ spawnSync \} = require\('node:child_process'\);/);
455
+ assert.match(rootWrapper, /\['get', 'show'\]/);
456
+ assert.match(rootWrapper, /\['--help', 'help'\]/);
457
+
458
+ const workspaceWrapper = fs.readFileSync(path.join(projectRoot, '.opencode', 'openkit', 'workflow-state.js'), 'utf8');
459
+ assert.match(workspaceWrapper, /const \{ spawnSync \} = require\('node:child_process'\);/);
460
+ assert.doesNotMatch(workspaceWrapper, /import \{ spawnSync \} from 'node:child_process';/);
461
+
462
+ const wrapperRun = spawnSync(process.execPath, ['.opencode/openkit/workflow-state.js', 'help'], {
463
+ cwd: projectRoot,
464
+ encoding: 'utf8',
465
+ });
466
+
467
+ assert.equal(wrapperRun.status, 0);
468
+ assert.match(wrapperRun.stdout, /Usage:/);
469
+ assert.doesNotMatch(wrapperRun.stderr, /MODULE_TYPELESS_PACKAGE_JSON/);
391
470
  });
392
471
 
393
472
  test('openkit run reports missing opencode after first-time setup completes', () => {
@@ -420,7 +499,7 @@ test('openkit run blocks on invalid global install state and recommends upgrade'
420
499
  `${JSON.stringify({
421
500
  schema: 'wrong-schema',
422
501
  stateVersion: 1,
423
- kit: { name: 'OpenKit', version: '0.1.0' },
502
+ kit: { name: 'OpenKit', version: '0.2.9' },
424
503
  installation: {
425
504
  profile: 'openkit',
426
505
  status: 'installed',
@@ -69,6 +69,11 @@ test('global doctor reports next steps for healthy installs', () => {
69
69
  assert.equal(result.status, 'healthy');
70
70
  assert.equal(result.nextStep, 'Run openkit run.');
71
71
  assert.equal(result.recommendedCommand, 'openkit run');
72
+ assert.equal(result.workspace.paths.workspaceRoot.includes('workspaces'), true);
73
+ assert.equal(result.workspace.meta, null);
74
+ assert.equal(result.workspace.index, null);
75
+ assert.equal(fs.existsSync(path.join(projectRoot, '.opencode')), false);
76
+ assert.equal(fs.existsSync(path.join(tempHome, 'workspaces')), false);
72
77
  });
73
78
 
74
79
  test('global doctor recommends upgrade for invalid installs', () => {
@@ -80,7 +85,7 @@ test('global doctor recommends upgrade for invalid installs', () => {
80
85
  stateVersion: 1,
81
86
  kit: {
82
87
  name: 'OpenKit',
83
- version: '0.1.0',
88
+ version: '0.2.9',
84
89
  },
85
90
  installation: {
86
91
  profile: 'openkit',
@@ -127,4 +132,6 @@ test('global doctor reports workspace issues with guidance', () => {
127
132
  assert.equal(result.nextStep, 'Review the issues above before relying on this workspace.');
128
133
  assert.equal(result.recommendedCommand, null);
129
134
  assert.match(result.issues.join('\n'), /OpenCode executable is not available on PATH/);
135
+ assert.equal(result.workspace.meta, null);
136
+ assert.equal(fs.existsSync(path.join(projectRoot, '.opencode')), false);
130
137
  });
@@ -44,6 +44,7 @@ test('ensureGlobalInstall returns none when install is healthy', () => {
44
44
  assert.equal(result.action, 'none');
45
45
  assert.equal(result.installed, false);
46
46
  assert.equal(result.doctor.status, 'healthy');
47
+ assert.equal(fs.existsSync(path.join(tempHome, 'workspaces')), false);
47
48
  });
48
49
 
49
50
  test('ensureGlobalInstall materializes the global install when it is missing', () => {
@@ -80,7 +81,7 @@ test('ensureGlobalInstall returns blocked when install state is invalid', () =>
80
81
  stateVersion: 1,
81
82
  kit: {
82
83
  name: 'OpenKit',
83
- version: '0.1.0',
84
+ version: '0.2.9',
84
85
  },
85
86
  installation: {
86
87
  profile: 'openkit',
@@ -38,7 +38,7 @@ function materializeManagedInstall(projectRoot) {
38
38
  stateVersion: 1,
39
39
  kit: {
40
40
  name: 'OpenKit',
41
- version: '0.1.0',
41
+ version: '0.2.9',
42
42
  },
43
43
  installation: {
44
44
  profile: 'openkit-core',
@@ -110,7 +110,7 @@ test('doctor reports install incomplete for a partial install when install state
110
110
  stateVersion: 1,
111
111
  kit: {
112
112
  name: 'OpenKit',
113
- version: '0.1.0',
113
+ version: '0.2.9',
114
114
  },
115
115
  installation: {
116
116
  profile: 'openkit-core',
@@ -187,7 +187,7 @@ test('doctor reports drift for managed install-state assets it owns in phase 1',
187
187
  stateVersion: 1,
188
188
  kit: {
189
189
  name: 'OpenKit',
190
- version: '0.1.0',
190
+ version: '0.2.9',
191
191
  },
192
192
  installation: {
193
193
  profile: 'custom-profile',
@@ -323,7 +323,7 @@ test('doctor does not report healthy when an adopted root manifest is incompatib
323
323
  stateVersion: 1,
324
324
  kit: {
325
325
  name: 'OpenKit',
326
- version: '0.1.0',
326
+ version: '0.2.9',
327
327
  },
328
328
  installation: {
329
329
  profile: 'openkit-core',
@@ -390,7 +390,7 @@ test('doctor can report healthy when an adopted root manifest still satisfies th
390
390
  stateVersion: 1,
391
391
  kit: {
392
392
  name: 'OpenKit',
393
- version: '0.1.0',
393
+ version: '0.2.9',
394
394
  },
395
395
  installation: {
396
396
  profile: 'openkit-core',