@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.
- package/.opencode/install-manifest.json +1 -1
- package/.opencode/tests/session-start-hook.test.js +5 -13
- package/.opencode/tests/workflow-behavior.test.js +1 -1
- package/.opencode/tests/workflow-contract-consistency.test.js +1 -1
- package/.opencode/tests/workflow-state-cli.test.js +3 -3
- package/assets/openkit-install.json.template +1 -1
- package/docs/maintainer/README.md +1 -1
- package/docs/operations/runbooks/openkit-daily-usage.md +2 -2
- package/hooks/session-start +3 -157
- package/hooks/session-start.js +210 -0
- package/package.json +1 -1
- package/registry.json +1 -1
- package/src/command-detection.js +37 -0
- package/src/global/doctor.js +4 -5
- package/src/global/install-state.js +3 -1
- package/src/global/materialize.js +3 -2
- package/src/global/workspace-shim.js +56 -10
- package/src/global/workspace-state.js +12 -1
- package/src/install/install-state.js +3 -1
- package/src/install/materialize.js +2 -1
- package/src/runtime/doctor.js +2 -9
- package/src/version.js +25 -0
- package/tests/cli/openkit-cli.test.js +92 -13
- package/tests/global/doctor.test.js +8 -1
- package/tests/global/ensure-install.test.js +2 -1
- package/tests/runtime/doctor.test.js +5 -5
|
@@ -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
|
|
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\.
|
|
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
|
-
|
|
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
|
|
|
@@ -30,7 +30,7 @@ function setupTempRuntime(projectRoot) {
|
|
|
30
30
|
`${JSON.stringify({
|
|
31
31
|
kit: {
|
|
32
32
|
name: "OpenKit AI Software Factory",
|
|
33
|
-
version: "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\.
|
|
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\.
|
|
580
|
+
assert.match(result.stdout, /OpenKit version: 0\.2\.9/)
|
|
581
581
|
assert.match(result.stdout, /active profile: openkit-core/)
|
|
582
582
|
})
|
|
583
583
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/hooks/session-start
CHANGED
|
@@ -1,162 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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.
|
|
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
|
+
}
|
package/src/global/doctor.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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)
|
package/src/runtime/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
388
|
-
assert.match(
|
|
389
|
-
assert.match(
|
|
390
|
-
assert.match(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
393
|
+
version: '0.2.9',
|
|
394
394
|
},
|
|
395
395
|
installation: {
|
|
396
396
|
profile: 'openkit-core',
|