cclaw-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +101 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +70 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +50 -0
- package/dist/content/agents.d.ts +39 -0
- package/dist/content/agents.js +244 -0
- package/dist/content/autoplan.d.ts +7 -0
- package/dist/content/autoplan.js +297 -0
- package/dist/content/contracts.d.ts +2 -0
- package/dist/content/contracts.js +50 -0
- package/dist/content/examples.d.ts +2 -0
- package/dist/content/examples.js +327 -0
- package/dist/content/hooks.d.ts +16 -0
- package/dist/content/hooks.js +753 -0
- package/dist/content/learnings.d.ts +5 -0
- package/dist/content/learnings.js +265 -0
- package/dist/content/meta-skill.d.ts +10 -0
- package/dist/content/meta-skill.js +137 -0
- package/dist/content/observe.d.ts +21 -0
- package/dist/content/observe.js +1110 -0
- package/dist/content/session-hooks.d.ts +7 -0
- package/dist/content/session-hooks.js +137 -0
- package/dist/content/skills.d.ts +3 -0
- package/dist/content/skills.js +257 -0
- package/dist/content/stage-schema.d.ts +78 -0
- package/dist/content/stage-schema.js +1453 -0
- package/dist/content/subagents.d.ts +13 -0
- package/dist/content/subagents.js +616 -0
- package/dist/content/templates.d.ts +3 -0
- package/dist/content/templates.js +272 -0
- package/dist/content/utility-skills.d.ts +12 -0
- package/dist/content/utility-skills.js +467 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +610 -0
- package/dist/flow-state.d.ts +19 -0
- package/dist/flow-state.js +41 -0
- package/dist/fs-utils.d.ts +5 -0
- package/dist/fs-utils.js +28 -0
- package/dist/gitignore.d.ts +3 -0
- package/dist/gitignore.js +43 -0
- package/dist/harness-adapters.d.ts +12 -0
- package/dist/harness-adapters.js +175 -0
- package/dist/install.d.ts +9 -0
- package/dist/install.js +562 -0
- package/dist/learnings-summarizer.d.ts +25 -0
- package/dist/learnings-summarizer.js +201 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +6 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +179 -0
- package/dist/runs.d.ts +18 -0
- package/dist/runs.js +446 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +12 -0
- package/package.json +47 -0
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
|
|
6
|
+
import { CCLAW_AGENTS } from "./content/agents.js";
|
|
7
|
+
import { readConfig } from "./config.js";
|
|
8
|
+
import { exists } from "./fs-utils.js";
|
|
9
|
+
import { gitignoreHasRequiredPatterns } from "./gitignore.js";
|
|
10
|
+
import { HARNESS_ADAPTERS, CCLAW_MARKER_START, CCLAW_MARKER_END } from "./harness-adapters.js";
|
|
11
|
+
import { policyChecks } from "./policy.js";
|
|
12
|
+
import { readFlowState } from "./runs.js";
|
|
13
|
+
import { stageSkillFolder } from "./content/skills.js";
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
async function isGitRepo(projectRoot) {
|
|
16
|
+
try {
|
|
17
|
+
await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectRoot });
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function gitIgnoresRuntime(projectRoot) {
|
|
25
|
+
try {
|
|
26
|
+
await execFileAsync("git", ["check-ignore", "-q", `${RUNTIME_ROOT}/`], { cwd: projectRoot });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function toObject(value) {
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
35
|
+
return null;
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function collectHookCommands(value) {
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return value.flatMap((item) => collectHookCommands(item));
|
|
41
|
+
}
|
|
42
|
+
const obj = toObject(value);
|
|
43
|
+
if (!obj)
|
|
44
|
+
return [];
|
|
45
|
+
const direct = typeof obj.command === "string" ? [obj.command] : [];
|
|
46
|
+
const nested = collectHookCommands(obj.hooks);
|
|
47
|
+
return [...direct, ...nested];
|
|
48
|
+
}
|
|
49
|
+
async function commandAvailable(command) {
|
|
50
|
+
try {
|
|
51
|
+
await execFileAsync("bash", ["-lc", `command -v ${command} >/dev/null 2>&1`]);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function stripJsonCommentsOutsideStrings(input) {
|
|
59
|
+
let out = "";
|
|
60
|
+
let i = 0;
|
|
61
|
+
let inString = false;
|
|
62
|
+
let escape = false;
|
|
63
|
+
while (i < input.length) {
|
|
64
|
+
const c = input[i];
|
|
65
|
+
if (inString) {
|
|
66
|
+
out += c;
|
|
67
|
+
if (escape) {
|
|
68
|
+
escape = false;
|
|
69
|
+
}
|
|
70
|
+
else if (c === "\\") {
|
|
71
|
+
escape = true;
|
|
72
|
+
}
|
|
73
|
+
else if (c === "\"") {
|
|
74
|
+
inString = false;
|
|
75
|
+
}
|
|
76
|
+
i += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (c === "\"") {
|
|
80
|
+
inString = true;
|
|
81
|
+
out += c;
|
|
82
|
+
i += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const next = input[i + 1];
|
|
86
|
+
if (c === "/" && next === "/") {
|
|
87
|
+
while (i < input.length && input[i] !== "\n" && input[i] !== "\r")
|
|
88
|
+
i += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (c === "/" && next === "*") {
|
|
92
|
+
i += 2;
|
|
93
|
+
while (i < input.length - 1 && !(input[i] === "*" && input[i + 1] === "/"))
|
|
94
|
+
i += 1;
|
|
95
|
+
i = Math.min(i + 2, input.length);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
out += c;
|
|
99
|
+
i += 1;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function parseJsonLike(raw) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// fall through
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const normalized = stripJsonCommentsOutsideStrings(raw).replace(/,\s*([}\]])/gu, "$1");
|
|
112
|
+
return JSON.parse(normalized);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function readHookDocument(filePath) {
|
|
119
|
+
if (!(await exists(filePath)))
|
|
120
|
+
return null;
|
|
121
|
+
try {
|
|
122
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
123
|
+
const parsed = parseJsonLike(raw);
|
|
124
|
+
const obj = toObject(parsed);
|
|
125
|
+
return obj ?? null;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function doctorChecks(projectRoot) {
|
|
132
|
+
const checks = [];
|
|
133
|
+
for (const dir of REQUIRED_DIRS) {
|
|
134
|
+
const fullPath = path.join(projectRoot, dir);
|
|
135
|
+
checks.push({
|
|
136
|
+
name: `dir:${dir}`,
|
|
137
|
+
ok: await exists(fullPath),
|
|
138
|
+
details: fullPath
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const stage of COMMAND_FILE_ORDER) {
|
|
142
|
+
const commandPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${stage}.md`);
|
|
143
|
+
checks.push({
|
|
144
|
+
name: `command:${stage}`,
|
|
145
|
+
ok: await exists(commandPath),
|
|
146
|
+
details: commandPath
|
|
147
|
+
});
|
|
148
|
+
const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", stageSkillFolder(stage), "SKILL.md");
|
|
149
|
+
const skillExists = await exists(skillPath);
|
|
150
|
+
checks.push({
|
|
151
|
+
name: `skill:${stage}`,
|
|
152
|
+
ok: skillExists,
|
|
153
|
+
details: skillPath
|
|
154
|
+
});
|
|
155
|
+
if (skillExists) {
|
|
156
|
+
const skillContent = await fs.readFile(skillPath, "utf8");
|
|
157
|
+
const lineCount = skillContent.split("\n").length;
|
|
158
|
+
const MIN_SKILL_LINES = 110;
|
|
159
|
+
checks.push({
|
|
160
|
+
name: `skill:${stage}:min_lines`,
|
|
161
|
+
ok: lineCount >= MIN_SKILL_LINES,
|
|
162
|
+
details: `${skillPath} has ${lineCount} lines (minimum ${MIN_SKILL_LINES})`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
checks.push({
|
|
167
|
+
name: "gitignore:required_patterns",
|
|
168
|
+
ok: await gitignoreHasRequiredPatterns(projectRoot),
|
|
169
|
+
details: ".gitignore must include cclaw ignore block"
|
|
170
|
+
});
|
|
171
|
+
let configuredHarnesses = [];
|
|
172
|
+
try {
|
|
173
|
+
const config = await readConfig(projectRoot);
|
|
174
|
+
configuredHarnesses = config.harnesses;
|
|
175
|
+
checks.push({
|
|
176
|
+
name: "config:valid",
|
|
177
|
+
ok: true,
|
|
178
|
+
details: `${RUNTIME_ROOT}/config.yaml parsed successfully`
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
checks.push({
|
|
183
|
+
name: "config:valid",
|
|
184
|
+
ok: false,
|
|
185
|
+
details: error instanceof Error ? error.message : "Invalid config"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
for (const harness of configuredHarnesses) {
|
|
189
|
+
const adapter = HARNESS_ADAPTERS[harness];
|
|
190
|
+
if (!adapter) {
|
|
191
|
+
checks.push({
|
|
192
|
+
name: `harness:${harness}:supported`,
|
|
193
|
+
ok: false,
|
|
194
|
+
details: `Unsupported harness "${harness}" in ${RUNTIME_ROOT}/config.yaml`
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
for (const stage of COMMAND_FILE_ORDER) {
|
|
199
|
+
const shimPath = path.join(projectRoot, adapter.commandDir, `cc-${stage}.md`);
|
|
200
|
+
let shimOk = await exists(shimPath);
|
|
201
|
+
let details = shimPath;
|
|
202
|
+
if (shimOk) {
|
|
203
|
+
const content = await fs.readFile(shimPath, "utf8");
|
|
204
|
+
const hasSkillReference = content.includes(`.cclaw/skills/${stageSkillFolder(stage)}/SKILL.md`);
|
|
205
|
+
const hasCommandReference = content.includes(`.cclaw/commands/${stage}.md`);
|
|
206
|
+
shimOk = hasSkillReference && hasCommandReference;
|
|
207
|
+
details = hasSkillReference && hasCommandReference
|
|
208
|
+
? `${shimPath} aligned`
|
|
209
|
+
: `${shimPath} missing stage references`;
|
|
210
|
+
}
|
|
211
|
+
checks.push({
|
|
212
|
+
name: `shim:${harness}:${stage}`,
|
|
213
|
+
ok: shimOk,
|
|
214
|
+
details
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const agentsFile = path.join(projectRoot, "AGENTS.md");
|
|
219
|
+
let agentsBlockOk = false;
|
|
220
|
+
if (await exists(agentsFile)) {
|
|
221
|
+
const content = await fs.readFile(agentsFile, "utf8");
|
|
222
|
+
const hasMarkers = content.includes(CCLAW_MARKER_START) && content.includes(CCLAW_MARKER_END);
|
|
223
|
+
const hasAllCommands = COMMAND_FILE_ORDER.every((stage) => content.includes(`/cc-${stage}`));
|
|
224
|
+
const hasRouting = content.includes("Intent → Stage Routing") || content.includes("Intent → Stage");
|
|
225
|
+
const hasVerification = content.includes("Verification Discipline");
|
|
226
|
+
const hasFileMap = content.includes("File Map");
|
|
227
|
+
const hasLearnings = content.includes("Learnings Store");
|
|
228
|
+
const hasAutoplan = content.includes("Autoplan Orchestrator");
|
|
229
|
+
const hasAgents = content.includes("Agent Specialists");
|
|
230
|
+
const hasSubagents = content.includes("Subagent Orchestration");
|
|
231
|
+
const hasSessionProtocols = content.includes("Session Guidelines");
|
|
232
|
+
const hasHooks = content.includes("Hooks");
|
|
233
|
+
agentsBlockOk = hasMarkers && hasAllCommands && hasRouting && hasVerification && hasFileMap && hasLearnings && hasAutoplan && hasAgents && hasSubagents && hasSessionProtocols && hasHooks;
|
|
234
|
+
}
|
|
235
|
+
checks.push({
|
|
236
|
+
name: "agents:cclaw_block",
|
|
237
|
+
ok: agentsBlockOk,
|
|
238
|
+
details: `${agentsFile} must contain cclaw marker block with routing, verification, and file map`
|
|
239
|
+
});
|
|
240
|
+
// Utility commands
|
|
241
|
+
for (const cmd of ["learn", "autoplan"]) {
|
|
242
|
+
const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
|
|
243
|
+
checks.push({
|
|
244
|
+
name: `utility_command:${cmd}`,
|
|
245
|
+
ok: await exists(cmdPath),
|
|
246
|
+
details: cmdPath
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// Utility skills
|
|
250
|
+
for (const [folder, label] of [
|
|
251
|
+
["learnings", "learnings"],
|
|
252
|
+
["autoplan", "autoplan"],
|
|
253
|
+
["subagent-dev", "sdd"],
|
|
254
|
+
["parallel-dispatch", "parallel-agents"],
|
|
255
|
+
["session", "session"],
|
|
256
|
+
["using-cclaw", "meta-skill"]
|
|
257
|
+
]) {
|
|
258
|
+
const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", folder, "SKILL.md");
|
|
259
|
+
checks.push({
|
|
260
|
+
name: `utility_skill:${label}`,
|
|
261
|
+
ok: await exists(skillPath),
|
|
262
|
+
details: skillPath
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// New utility skills (security, debugging, performance, ci-cd, docs)
|
|
266
|
+
for (const folder of [
|
|
267
|
+
"security",
|
|
268
|
+
"debugging",
|
|
269
|
+
"performance",
|
|
270
|
+
"ci-cd",
|
|
271
|
+
"docs"
|
|
272
|
+
]) {
|
|
273
|
+
const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", folder, "SKILL.md");
|
|
274
|
+
checks.push({
|
|
275
|
+
name: `utility_skill:${folder}`,
|
|
276
|
+
ok: await exists(skillPath),
|
|
277
|
+
details: skillPath
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Agent definition files
|
|
281
|
+
for (const agent of CCLAW_AGENTS) {
|
|
282
|
+
const agentPath = path.join(projectRoot, RUNTIME_ROOT, "agents", `${agent.name}.md`);
|
|
283
|
+
let agentOk = await exists(agentPath);
|
|
284
|
+
if (agentOk) {
|
|
285
|
+
const content = await fs.readFile(agentPath, "utf8");
|
|
286
|
+
agentOk = content.includes(`name: ${agent.name}`) && content.includes("tools:");
|
|
287
|
+
}
|
|
288
|
+
checks.push({
|
|
289
|
+
name: `agent:${agent.name}`,
|
|
290
|
+
ok: agentOk,
|
|
291
|
+
details: agentPath
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// Hook scripts
|
|
295
|
+
for (const script of [
|
|
296
|
+
"session-start.sh",
|
|
297
|
+
"stop-checkpoint.sh",
|
|
298
|
+
"prompt-guard.sh",
|
|
299
|
+
"context-monitor.sh",
|
|
300
|
+
"observe.sh",
|
|
301
|
+
"summarize-observations.sh",
|
|
302
|
+
"summarize-observations.mjs"
|
|
303
|
+
]) {
|
|
304
|
+
const scriptPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", script);
|
|
305
|
+
const scriptExists = await exists(scriptPath);
|
|
306
|
+
checks.push({
|
|
307
|
+
name: `hook:script:${script}`,
|
|
308
|
+
ok: scriptExists,
|
|
309
|
+
details: scriptPath
|
|
310
|
+
});
|
|
311
|
+
if (scriptExists) {
|
|
312
|
+
let executable = false;
|
|
313
|
+
try {
|
|
314
|
+
const stat = await fs.stat(scriptPath);
|
|
315
|
+
executable = (stat.mode & 0o111) !== 0;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
executable = false;
|
|
319
|
+
}
|
|
320
|
+
checks.push({
|
|
321
|
+
name: `hook:script:${script}:executable`,
|
|
322
|
+
ok: executable,
|
|
323
|
+
details: `${scriptPath} must be executable`
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Hook JSON files per harness
|
|
328
|
+
const hookPaths = {
|
|
329
|
+
claude: ".claude/hooks/hooks.json",
|
|
330
|
+
cursor: ".cursor/hooks.json",
|
|
331
|
+
codex: ".codex/hooks.json"
|
|
332
|
+
};
|
|
333
|
+
for (const harness of configuredHarnesses) {
|
|
334
|
+
const hp = hookPaths[harness];
|
|
335
|
+
if (!hp && harness !== "opencode") {
|
|
336
|
+
checks.push({
|
|
337
|
+
name: `hook:json:${harness}`,
|
|
338
|
+
ok: false,
|
|
339
|
+
details: `Unsupported harness "${harness}" in ${RUNTIME_ROOT}/config.yaml`
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (hp) {
|
|
344
|
+
const fullPath = path.join(projectRoot, hp);
|
|
345
|
+
const parsed = await readHookDocument(fullPath);
|
|
346
|
+
const hookOk = !!(parsed && typeof parsed.hooks === "object" && parsed.hooks !== null);
|
|
347
|
+
checks.push({
|
|
348
|
+
name: `hook:json:${harness}`,
|
|
349
|
+
ok: hookOk,
|
|
350
|
+
details: fullPath
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// OpenCode plugin
|
|
355
|
+
checks.push({
|
|
356
|
+
name: "hook:opencode_plugin",
|
|
357
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "hooks", "opencode-plugin.mjs")),
|
|
358
|
+
details: `${RUNTIME_ROOT}/hooks/opencode-plugin.mjs`
|
|
359
|
+
});
|
|
360
|
+
if (configuredHarnesses.includes("claude")) {
|
|
361
|
+
const file = path.join(projectRoot, ".claude/hooks/hooks.json");
|
|
362
|
+
const parsed = await readHookDocument(file);
|
|
363
|
+
const hooks = toObject(parsed?.hooks) ?? {};
|
|
364
|
+
const sessionStart = hooks.SessionStart;
|
|
365
|
+
const ok = JSON.stringify(sessionStart ?? "").includes("startup|resume|clear|compact");
|
|
366
|
+
checks.push({
|
|
367
|
+
name: "lifecycle:claude:rehydration_matcher",
|
|
368
|
+
ok,
|
|
369
|
+
details: `${file} must include SessionStart matcher startup|resume|clear|compact`
|
|
370
|
+
});
|
|
371
|
+
const sessionCommands = collectHookCommands(hooks.SessionStart);
|
|
372
|
+
const preCommands = collectHookCommands(hooks.PreToolUse);
|
|
373
|
+
const postCommands = collectHookCommands(hooks.PostToolUse);
|
|
374
|
+
const stopCommands = collectHookCommands(hooks.Stop);
|
|
375
|
+
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
376
|
+
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
377
|
+
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
378
|
+
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
379
|
+
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
380
|
+
stopCommands.some((cmd) => cmd.includes("summarize-observations.sh")) &&
|
|
381
|
+
stopCommands.some((cmd) => cmd.includes("stop-checkpoint.sh"));
|
|
382
|
+
checks.push({
|
|
383
|
+
name: "hook:wiring:claude",
|
|
384
|
+
ok: wiringOk,
|
|
385
|
+
details: `${file} must wire session-start/prompt-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (configuredHarnesses.includes("cursor")) {
|
|
389
|
+
const file = path.join(projectRoot, ".cursor/hooks.json");
|
|
390
|
+
const parsed = await readHookDocument(file);
|
|
391
|
+
const hooks = toObject(parsed?.hooks) ?? {};
|
|
392
|
+
const hasLifecycleKeys = Array.isArray(hooks.sessionStart) &&
|
|
393
|
+
Array.isArray(hooks.sessionResume) &&
|
|
394
|
+
Array.isArray(hooks.sessionClear) &&
|
|
395
|
+
Array.isArray(hooks.sessionCompact);
|
|
396
|
+
checks.push({
|
|
397
|
+
name: "lifecycle:cursor:rehydration_events",
|
|
398
|
+
ok: hasLifecycleKeys,
|
|
399
|
+
details: `${file} must include sessionStart/sessionResume/sessionClear/sessionCompact hooks`
|
|
400
|
+
});
|
|
401
|
+
const sessionCommands = [
|
|
402
|
+
...collectHookCommands(hooks.sessionStart),
|
|
403
|
+
...collectHookCommands(hooks.sessionResume),
|
|
404
|
+
...collectHookCommands(hooks.sessionClear),
|
|
405
|
+
...collectHookCommands(hooks.sessionCompact)
|
|
406
|
+
];
|
|
407
|
+
const preCommands = collectHookCommands(hooks.preToolUse);
|
|
408
|
+
const postCommands = collectHookCommands(hooks.postToolUse);
|
|
409
|
+
const stopCommands = collectHookCommands(hooks.stop);
|
|
410
|
+
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
411
|
+
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
412
|
+
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
413
|
+
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
414
|
+
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
415
|
+
stopCommands.some((cmd) => cmd.includes("summarize-observations.sh")) &&
|
|
416
|
+
stopCommands.some((cmd) => cmd.includes("stop-checkpoint.sh"));
|
|
417
|
+
checks.push({
|
|
418
|
+
name: "hook:wiring:cursor",
|
|
419
|
+
ok: wiringOk,
|
|
420
|
+
details: `${file} must wire session-start/prompt-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (configuredHarnesses.includes("codex")) {
|
|
424
|
+
const file = path.join(projectRoot, ".codex/hooks.json");
|
|
425
|
+
const parsed = await readHookDocument(file);
|
|
426
|
+
const hooks = toObject(parsed?.hooks) ?? {};
|
|
427
|
+
const sessionStart = hooks.SessionStart;
|
|
428
|
+
const ok = JSON.stringify(sessionStart ?? "").includes("startup|resume|clear|compact");
|
|
429
|
+
checks.push({
|
|
430
|
+
name: "lifecycle:codex:rehydration_matcher",
|
|
431
|
+
ok,
|
|
432
|
+
details: `${file} must include SessionStart matcher startup|resume|clear|compact`
|
|
433
|
+
});
|
|
434
|
+
const sessionCommands = collectHookCommands(hooks.SessionStart);
|
|
435
|
+
const preCommands = collectHookCommands(hooks.PreToolUse);
|
|
436
|
+
const postCommands = collectHookCommands(hooks.PostToolUse);
|
|
437
|
+
const stopCommands = collectHookCommands(hooks.Stop);
|
|
438
|
+
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
439
|
+
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
440
|
+
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
441
|
+
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
442
|
+
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
443
|
+
stopCommands.some((cmd) => cmd.includes("summarize-observations.sh")) &&
|
|
444
|
+
stopCommands.some((cmd) => cmd.includes("stop-checkpoint.sh"));
|
|
445
|
+
checks.push({
|
|
446
|
+
name: "hook:wiring:codex",
|
|
447
|
+
ok: wiringOk,
|
|
448
|
+
details: `${file} must wire session-start/prompt-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (configuredHarnesses.includes("opencode")) {
|
|
452
|
+
const file = path.join(projectRoot, RUNTIME_ROOT, "hooks", "opencode-plugin.mjs");
|
|
453
|
+
let ok = false;
|
|
454
|
+
if (await exists(file)) {
|
|
455
|
+
const content = await fs.readFile(file, "utf8");
|
|
456
|
+
ok =
|
|
457
|
+
content.includes('"session.created"') &&
|
|
458
|
+
content.includes('"session.resumed"') &&
|
|
459
|
+
content.includes('"session.compacted"') &&
|
|
460
|
+
content.includes('"session.cleared"') &&
|
|
461
|
+
content.includes('"experimental.chat.system.transform"');
|
|
462
|
+
}
|
|
463
|
+
checks.push({
|
|
464
|
+
name: "lifecycle:opencode:rehydration_events",
|
|
465
|
+
ok,
|
|
466
|
+
details: `${file} must include created/resumed/compacted/cleared and transform rehydration handlers`
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const hasBash = await commandAvailable("bash");
|
|
470
|
+
const hasNode = await commandAvailable("node");
|
|
471
|
+
const hasPython = await commandAvailable("python3");
|
|
472
|
+
const hasJq = await commandAvailable("jq");
|
|
473
|
+
checks.push({
|
|
474
|
+
name: "capability:required:bash",
|
|
475
|
+
ok: hasBash,
|
|
476
|
+
details: "bash is required to execute cclaw hook scripts"
|
|
477
|
+
});
|
|
478
|
+
checks.push({
|
|
479
|
+
name: "capability:required:node",
|
|
480
|
+
ok: hasNode,
|
|
481
|
+
details: "node is required for cclaw runtime scripts and CLI wiring"
|
|
482
|
+
});
|
|
483
|
+
checks.push({
|
|
484
|
+
name: "capability:runtime:json_parser",
|
|
485
|
+
ok: hasPython || hasJq,
|
|
486
|
+
details: "at least one of python3 or jq must be available for hook JSON parsing fallbacks"
|
|
487
|
+
});
|
|
488
|
+
checks.push({
|
|
489
|
+
name: "warning:capability:jq",
|
|
490
|
+
ok: true,
|
|
491
|
+
details: hasJq ? "jq available" : "warning: jq not found, python/node fallbacks will be used"
|
|
492
|
+
});
|
|
493
|
+
checks.push({
|
|
494
|
+
name: "warning:capability:python3",
|
|
495
|
+
ok: true,
|
|
496
|
+
details: hasPython ? "python3 available" : "warning: python3 not found, jq/node paths must stay healthy"
|
|
497
|
+
});
|
|
498
|
+
// Learnings store exists
|
|
499
|
+
checks.push({
|
|
500
|
+
name: "learnings:store_exists",
|
|
501
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "learnings.jsonl")),
|
|
502
|
+
details: `${RUNTIME_ROOT}/learnings.jsonl must exist (can be empty)`
|
|
503
|
+
});
|
|
504
|
+
checks.push({
|
|
505
|
+
name: "state:checkpoint_exists",
|
|
506
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "checkpoint.json")),
|
|
507
|
+
details: `${RUNTIME_ROOT}/state/checkpoint.json must exist`
|
|
508
|
+
});
|
|
509
|
+
checks.push({
|
|
510
|
+
name: "state:stage_activity_exists",
|
|
511
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl")),
|
|
512
|
+
details: `${RUNTIME_ROOT}/state/stage-activity.jsonl must exist`
|
|
513
|
+
});
|
|
514
|
+
checks.push({
|
|
515
|
+
name: "state:suggestion_memory_exists",
|
|
516
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
|
|
517
|
+
details: `${RUNTIME_ROOT}/state/suggestion-memory.json must exist for proactive suggestion memory`
|
|
518
|
+
});
|
|
519
|
+
const flowState = await readFlowState(projectRoot);
|
|
520
|
+
checks.push({
|
|
521
|
+
name: "flow_state:active_run_id",
|
|
522
|
+
ok: typeof flowState.activeRunId === "string" && flowState.activeRunId.trim().length > 0,
|
|
523
|
+
details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
|
|
524
|
+
});
|
|
525
|
+
checks.push({
|
|
526
|
+
name: "run:active_artifacts",
|
|
527
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "artifacts")),
|
|
528
|
+
details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/artifacts must exist`
|
|
529
|
+
});
|
|
530
|
+
checks.push({
|
|
531
|
+
name: "run:active_metadata",
|
|
532
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "run.json")),
|
|
533
|
+
details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/run.json must exist`
|
|
534
|
+
});
|
|
535
|
+
checks.push({
|
|
536
|
+
name: "run:active_handoff",
|
|
537
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "00-handoff.md")),
|
|
538
|
+
details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/00-handoff.md must exist`
|
|
539
|
+
});
|
|
540
|
+
// Utility shims in harness dirs
|
|
541
|
+
for (const harness of configuredHarnesses) {
|
|
542
|
+
const adapter = HARNESS_ADAPTERS[harness];
|
|
543
|
+
if (!adapter) {
|
|
544
|
+
checks.push({
|
|
545
|
+
name: `harness:${harness}:supported`,
|
|
546
|
+
ok: false,
|
|
547
|
+
details: `Unsupported harness "${harness}" in ${RUNTIME_ROOT}/config.yaml`
|
|
548
|
+
});
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
for (const cmd of ["learn", "autoplan"]) {
|
|
552
|
+
const shimPath = path.join(projectRoot, adapter.commandDir, `cc-${cmd}.md`);
|
|
553
|
+
checks.push({
|
|
554
|
+
name: `shim:${harness}:${cmd}`,
|
|
555
|
+
ok: await exists(shimPath),
|
|
556
|
+
details: shimPath
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Self-improvement block in stage skills
|
|
561
|
+
for (const stage of COMMAND_FILE_ORDER) {
|
|
562
|
+
const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", stageSkillFolder(stage), "SKILL.md");
|
|
563
|
+
if (await exists(skillPath)) {
|
|
564
|
+
const content = await fs.readFile(skillPath, "utf8");
|
|
565
|
+
checks.push({
|
|
566
|
+
name: `skill:${stage}:self_improvement`,
|
|
567
|
+
ok: content.includes("## Operational Self-Improvement"),
|
|
568
|
+
details: `${skillPath} must contain self-improvement block`
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const isRepo = await isGitRepo(projectRoot);
|
|
573
|
+
checks.push({
|
|
574
|
+
name: "git:cclaw_ignored_runtime",
|
|
575
|
+
ok: isRepo ? await gitIgnoresRuntime(projectRoot) : true,
|
|
576
|
+
details: isRepo
|
|
577
|
+
? `git check-ignore must pass for ${RUNTIME_ROOT}/`
|
|
578
|
+
: "repository not initialized; check skipped"
|
|
579
|
+
});
|
|
580
|
+
const rulesJsonPath = path.join(projectRoot, RUNTIME_ROOT, "rules", "rules.json");
|
|
581
|
+
let hasRules = false;
|
|
582
|
+
if (await exists(rulesJsonPath)) {
|
|
583
|
+
try {
|
|
584
|
+
const parsed = JSON.parse(await fs.readFile(rulesJsonPath, "utf8"));
|
|
585
|
+
const hasCoreLists = Array.isArray(parsed.MUST_ALWAYS) && Array.isArray(parsed.MUST_NEVER);
|
|
586
|
+
const stageOrder = parsed.stage_order;
|
|
587
|
+
const stageGates = parsed.stage_gates;
|
|
588
|
+
const hasStageOrder = Array.isArray(stageOrder) &&
|
|
589
|
+
COMMAND_FILE_ORDER.every((stage) => stageOrder.includes(stage));
|
|
590
|
+
const hasStageGates = typeof stageGates === "object" &&
|
|
591
|
+
stageGates !== null &&
|
|
592
|
+
COMMAND_FILE_ORDER.every((stage) => Array.isArray(stageGates[stage]));
|
|
593
|
+
hasRules = hasCoreLists && hasStageOrder && hasStageGates;
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
hasRules = false;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
checks.push({
|
|
600
|
+
name: "rules:policy_schema",
|
|
601
|
+
ok: hasRules,
|
|
602
|
+
details: rulesJsonPath
|
|
603
|
+
});
|
|
604
|
+
const policy = await policyChecks(projectRoot);
|
|
605
|
+
checks.push(...policy);
|
|
606
|
+
return checks;
|
|
607
|
+
}
|
|
608
|
+
export function doctorSucceeded(checks) {
|
|
609
|
+
return checks.every((check) => check.ok);
|
|
610
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FlowStage, TransitionRule } from "./types.js";
|
|
2
|
+
export declare const TRANSITION_RULES: TransitionRule[];
|
|
3
|
+
export interface StageGateState {
|
|
4
|
+
required: string[];
|
|
5
|
+
passed: string[];
|
|
6
|
+
blocked: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface FlowState {
|
|
9
|
+
activeRunId: string;
|
|
10
|
+
currentStage: FlowStage;
|
|
11
|
+
completedStages: FlowStage[];
|
|
12
|
+
guardEvidence: Record<string, string>;
|
|
13
|
+
stageGateCatalog: Record<FlowStage, StageGateState>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createInitialFlowState(activeRunId?: string): FlowState;
|
|
16
|
+
export declare function canTransition(from: FlowStage, to: FlowStage): boolean;
|
|
17
|
+
export declare function getTransitionGuards(from: FlowStage, to: FlowStage): string[];
|
|
18
|
+
export declare function nextStage(stage: FlowStage): FlowStage | null;
|
|
19
|
+
export declare function previousStage(stage: FlowStage): FlowStage | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { COMMAND_FILE_ORDER } from "./constants.js";
|
|
2
|
+
import { buildTransitionRules, orderedStageSchemas, stageGateIds } from "./content/stage-schema.js";
|
|
3
|
+
export const TRANSITION_RULES = buildTransitionRules();
|
|
4
|
+
export function createInitialFlowState(activeRunId = "run-pending") {
|
|
5
|
+
const stageGateCatalog = {};
|
|
6
|
+
for (const schema of orderedStageSchemas()) {
|
|
7
|
+
stageGateCatalog[schema.stage] = {
|
|
8
|
+
required: stageGateIds(schema.stage),
|
|
9
|
+
passed: [],
|
|
10
|
+
blocked: []
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
activeRunId,
|
|
15
|
+
currentStage: "brainstorm",
|
|
16
|
+
completedStages: [],
|
|
17
|
+
guardEvidence: {},
|
|
18
|
+
stageGateCatalog
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function canTransition(from, to) {
|
|
22
|
+
return TRANSITION_RULES.some((rule) => rule.from === from && rule.to === to);
|
|
23
|
+
}
|
|
24
|
+
export function getTransitionGuards(from, to) {
|
|
25
|
+
const match = TRANSITION_RULES.find((rule) => rule.from === from && rule.to === to);
|
|
26
|
+
return match ? [...match.guards] : [];
|
|
27
|
+
}
|
|
28
|
+
export function nextStage(stage) {
|
|
29
|
+
const index = COMMAND_FILE_ORDER.indexOf(stage);
|
|
30
|
+
if (index < 0 || index === COMMAND_FILE_ORDER.length - 1) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return COMMAND_FILE_ORDER[index + 1];
|
|
34
|
+
}
|
|
35
|
+
export function previousStage(stage) {
|
|
36
|
+
const index = COMMAND_FILE_ORDER.indexOf(stage);
|
|
37
|
+
if (index <= 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return COMMAND_FILE_ORDER[index - 1];
|
|
41
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
2
|
+
export declare function writeFileSafe(filePath: string, content: string): Promise<void>;
|
|
3
|
+
export declare function exists(filePath: string): Promise<boolean>;
|
|
4
|
+
export declare function removeIfExists(targetPath: string): Promise<void>;
|
|
5
|
+
export declare function resolveProjectPath(cwd: string, relativePath: string): string;
|