cclaw-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/artifact-linter.d.ts +20 -0
- package/dist/artifact-linter.js +368 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -5
- package/dist/config.d.ts +4 -4
- package/dist/config.js +55 -3
- package/dist/constants.d.ts +4 -4
- package/dist/constants.js +6 -3
- package/dist/content/autoplan.js +51 -4
- package/dist/content/contexts.d.ts +9 -0
- package/dist/content/contexts.js +65 -0
- package/dist/content/hooks.d.ts +6 -2
- package/dist/content/hooks.js +448 -16
- package/dist/content/meta-skill.js +26 -0
- package/dist/content/next-command.d.ts +9 -0
- package/dist/content/next-command.js +138 -0
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +506 -24
- package/dist/content/skills.js +126 -0
- package/dist/content/stage-schema.d.ts +7 -0
- package/dist/content/stage-schema.js +70 -12
- package/dist/content/subagents.js +33 -0
- package/dist/content/templates.d.ts +1 -0
- package/dist/content/templates.js +182 -77
- package/dist/content/utility-skills.d.ts +5 -1
- package/dist/content/utility-skills.js +208 -2
- package/dist/delegation.d.ts +21 -0
- package/dist/delegation.js +94 -0
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +274 -29
- package/dist/fs-utils.d.ts +10 -0
- package/dist/fs-utils.js +47 -0
- package/dist/gate-evidence.d.ts +26 -0
- package/dist/gate-evidence.js +157 -0
- package/dist/harness-adapters.js +10 -26
- package/dist/hook-schema.d.ts +6 -0
- package/dist/hook-schema.js +45 -0
- package/dist/hook-schemas/claude-hooks.v1.json +12 -0
- package/dist/hook-schemas/codex-hooks.v1.json +12 -0
- package/dist/hook-schemas/cursor-hooks.v1.json +15 -0
- package/dist/install.js +395 -15
- package/dist/policy.d.ts +5 -1
- package/dist/policy.js +52 -1
- package/dist/runs.js +8 -3
- package/dist/trace-matrix.d.ts +13 -0
- package/dist/trace-matrix.js +182 -0
- package/dist/types.d.ts +11 -1
- package/package.json +2 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
|
+
import { exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
5
|
+
import { readFlowState } from "./runs.js";
|
|
6
|
+
import { stageSchema } from "./content/stage-schema.js";
|
|
7
|
+
function delegationLogPath(projectRoot, runId) {
|
|
8
|
+
return path.join(projectRoot, RUNTIME_ROOT, "runs", runId, "delegation-log.json");
|
|
9
|
+
}
|
|
10
|
+
function delegationLockPath(projectRoot, runId) {
|
|
11
|
+
return path.join(projectRoot, RUNTIME_ROOT, "runs", runId, ".delegation.lock");
|
|
12
|
+
}
|
|
13
|
+
function isDelegationEntry(value) {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15
|
+
return false;
|
|
16
|
+
const o = value;
|
|
17
|
+
const modeOk = o.mode === "mandatory" || o.mode === "proactive";
|
|
18
|
+
const statusOk = o.status === "scheduled" ||
|
|
19
|
+
o.status === "completed" ||
|
|
20
|
+
o.status === "failed" ||
|
|
21
|
+
o.status === "waived";
|
|
22
|
+
return (typeof o.stage === "string" &&
|
|
23
|
+
typeof o.agent === "string" &&
|
|
24
|
+
modeOk &&
|
|
25
|
+
statusOk &&
|
|
26
|
+
typeof o.ts === "string" &&
|
|
27
|
+
(o.taskId === undefined || typeof o.taskId === "string") &&
|
|
28
|
+
(o.waiverReason === undefined || typeof o.waiverReason === "string"));
|
|
29
|
+
}
|
|
30
|
+
function parseLedger(raw, runId) {
|
|
31
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
32
|
+
return { runId, entries: [] };
|
|
33
|
+
}
|
|
34
|
+
const o = raw;
|
|
35
|
+
const entriesRaw = o.entries;
|
|
36
|
+
const entries = [];
|
|
37
|
+
if (Array.isArray(entriesRaw)) {
|
|
38
|
+
for (const item of entriesRaw) {
|
|
39
|
+
if (isDelegationEntry(item)) {
|
|
40
|
+
entries.push(item);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { runId, entries };
|
|
45
|
+
}
|
|
46
|
+
export async function readDelegationLedger(projectRoot) {
|
|
47
|
+
const { activeRunId } = await readFlowState(projectRoot);
|
|
48
|
+
const filePath = delegationLogPath(projectRoot, activeRunId);
|
|
49
|
+
if (!(await exists(filePath))) {
|
|
50
|
+
return { runId: activeRunId, entries: [] };
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
54
|
+
const parsed = JSON.parse(text);
|
|
55
|
+
return parseLedger(parsed, activeRunId);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { runId: activeRunId, entries: [] };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function appendDelegation(projectRoot, entry) {
|
|
62
|
+
const { activeRunId } = await readFlowState(projectRoot);
|
|
63
|
+
await withDirectoryLock(delegationLockPath(projectRoot, activeRunId), async () => {
|
|
64
|
+
const filePath = delegationLogPath(projectRoot, activeRunId);
|
|
65
|
+
const prior = await readDelegationLedger(projectRoot);
|
|
66
|
+
const ledger = {
|
|
67
|
+
runId: activeRunId,
|
|
68
|
+
entries: [...prior.entries, entry]
|
|
69
|
+
};
|
|
70
|
+
await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
74
|
+
const mandatory = stageSchema(stage).mandatoryDelegations;
|
|
75
|
+
const ledger = await readDelegationLedger(projectRoot);
|
|
76
|
+
const forStage = ledger.entries.filter((e) => e.stage === stage);
|
|
77
|
+
const missing = [];
|
|
78
|
+
const waived = [];
|
|
79
|
+
for (const agent of mandatory) {
|
|
80
|
+
const rows = forStage.filter((e) => e.agent === agent);
|
|
81
|
+
const ok = rows.some((e) => e.status === "completed" || e.status === "waived");
|
|
82
|
+
if (!ok) {
|
|
83
|
+
missing.push(agent);
|
|
84
|
+
}
|
|
85
|
+
else if (rows.some((e) => e.status === "waived")) {
|
|
86
|
+
waived.push(agent);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
satisfied: missing.length === 0,
|
|
91
|
+
missing,
|
|
92
|
+
waived
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -3,5 +3,9 @@ export interface DoctorCheck {
|
|
|
3
3
|
ok: boolean;
|
|
4
4
|
details: string;
|
|
5
5
|
}
|
|
6
|
-
export
|
|
6
|
+
export interface DoctorOptions {
|
|
7
|
+
/** When true, normalize current-stage gate catalog and persist reconciliation before checks. */
|
|
8
|
+
reconcileCurrentStageGates?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function doctorChecks(projectRoot: string, options?: DoctorOptions): Promise<DoctorCheck[]>;
|
|
7
11
|
export declare function doctorSucceeded(checks: DoctorCheck[]): boolean;
|
package/dist/doctor.js
CHANGED
|
@@ -10,7 +10,13 @@ import { gitignoreHasRequiredPatterns } from "./gitignore.js";
|
|
|
10
10
|
import { HARNESS_ADAPTERS, CCLAW_MARKER_START, CCLAW_MARKER_END } from "./harness-adapters.js";
|
|
11
11
|
import { policyChecks } from "./policy.js";
|
|
12
12
|
import { readFlowState } from "./runs.js";
|
|
13
|
+
import { checkMandatoryDelegations } from "./delegation.js";
|
|
14
|
+
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
15
|
+
import { reconcileAndWriteCurrentStageGateCatalog, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
|
|
13
16
|
import { stageSkillFolder } from "./content/skills.js";
|
|
17
|
+
import { UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
|
|
18
|
+
import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
|
|
19
|
+
import { validateHookDocument } from "./hook-schema.js";
|
|
14
20
|
const execFileAsync = promisify(execFile);
|
|
15
21
|
async function isGitRepo(projectRoot) {
|
|
16
22
|
try {
|
|
@@ -21,6 +27,19 @@ async function isGitRepo(projectRoot) {
|
|
|
21
27
|
return false;
|
|
22
28
|
}
|
|
23
29
|
}
|
|
30
|
+
async function resolveGitHooksDir(projectRoot) {
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "hooks"], { cwd: projectRoot });
|
|
33
|
+
const rel = stdout.trim();
|
|
34
|
+
if (rel.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return path.resolve(projectRoot, rel);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
24
43
|
async function gitIgnoresRuntime(projectRoot) {
|
|
25
44
|
try {
|
|
26
45
|
await execFileAsync("git", ["check-ignore", "-q", `${RUNTIME_ROOT}/`], { cwd: projectRoot });
|
|
@@ -128,7 +147,46 @@ async function readHookDocument(filePath) {
|
|
|
128
147
|
return null;
|
|
129
148
|
}
|
|
130
149
|
}
|
|
131
|
-
|
|
150
|
+
function normalizeOpenCodePluginEntry(entry) {
|
|
151
|
+
if (typeof entry === "string" && entry.trim().length > 0)
|
|
152
|
+
return entry.trim();
|
|
153
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
154
|
+
return null;
|
|
155
|
+
const obj = entry;
|
|
156
|
+
for (const key of ["path", "src", "plugin"]) {
|
|
157
|
+
const value = obj[key];
|
|
158
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
159
|
+
return value.trim();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
async function opencodeRegistrationCheck(projectRoot) {
|
|
165
|
+
const expected = ".opencode/plugins/cclaw-plugin.mjs";
|
|
166
|
+
const candidates = [
|
|
167
|
+
path.join(projectRoot, "opencode.json"),
|
|
168
|
+
path.join(projectRoot, "opencode.jsonc"),
|
|
169
|
+
path.join(projectRoot, ".opencode/opencode.json"),
|
|
170
|
+
path.join(projectRoot, ".opencode/opencode.jsonc")
|
|
171
|
+
];
|
|
172
|
+
for (const configPath of candidates) {
|
|
173
|
+
if (!(await exists(configPath))) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const parsed = await readHookDocument(configPath);
|
|
177
|
+
if (!parsed) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const plugins = Array.isArray(parsed.plugins) ? parsed.plugins : [];
|
|
181
|
+
const registered = plugins.some((entry) => normalizeOpenCodePluginEntry(entry) === expected);
|
|
182
|
+
if (registered) {
|
|
183
|
+
return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${expected}` };
|
|
184
|
+
}
|
|
185
|
+
return { ok: false, details: `${path.relative(projectRoot, configPath)} missing plugin ${expected}` };
|
|
186
|
+
}
|
|
187
|
+
return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
|
|
188
|
+
}
|
|
189
|
+
export async function doctorChecks(projectRoot, options = {}) {
|
|
132
190
|
const checks = [];
|
|
133
191
|
for (const dir of REQUIRED_DIRS) {
|
|
134
192
|
const fullPath = path.join(projectRoot, dir);
|
|
@@ -169,8 +227,10 @@ export async function doctorChecks(projectRoot) {
|
|
|
169
227
|
details: ".gitignore must include cclaw ignore block"
|
|
170
228
|
});
|
|
171
229
|
let configuredHarnesses = [];
|
|
230
|
+
let parsedConfig = null;
|
|
172
231
|
try {
|
|
173
232
|
const config = await readConfig(projectRoot);
|
|
233
|
+
parsedConfig = config;
|
|
174
234
|
configuredHarnesses = config.harnesses;
|
|
175
235
|
checks.push({
|
|
176
236
|
name: "config:valid",
|
|
@@ -185,6 +245,55 @@ export async function doctorChecks(projectRoot) {
|
|
|
185
245
|
details: error instanceof Error ? error.message : "Invalid config"
|
|
186
246
|
});
|
|
187
247
|
}
|
|
248
|
+
if (parsedConfig) {
|
|
249
|
+
const expectedMode = parsedConfig.promptGuardMode === "strict" ? "strict" : "advisory";
|
|
250
|
+
const promptGuardPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "prompt-guard.sh");
|
|
251
|
+
let promptGuardModeOk = false;
|
|
252
|
+
if (await exists(promptGuardPath)) {
|
|
253
|
+
const promptGuardContent = await fs.readFile(promptGuardPath, "utf8");
|
|
254
|
+
promptGuardModeOk = promptGuardContent.includes(`PROMPT_GUARD_MODE="${expectedMode}"`);
|
|
255
|
+
}
|
|
256
|
+
checks.push({
|
|
257
|
+
name: "hook:prompt_guard:mode",
|
|
258
|
+
ok: promptGuardModeOk,
|
|
259
|
+
details: `${promptGuardPath} must match promptGuardMode=${expectedMode}`
|
|
260
|
+
});
|
|
261
|
+
if (parsedConfig.gitHookGuards === true) {
|
|
262
|
+
const runtimePreCommit = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-commit.sh");
|
|
263
|
+
const runtimePrePush = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-push.sh");
|
|
264
|
+
const runtimeScriptsOk = (await exists(runtimePreCommit)) && (await exists(runtimePrePush));
|
|
265
|
+
checks.push({
|
|
266
|
+
name: "git_hooks:managed:runtime_scripts",
|
|
267
|
+
ok: runtimeScriptsOk,
|
|
268
|
+
details: `${RUNTIME_ROOT}/hooks/git/pre-commit.sh and pre-push.sh must exist when gitHookGuards=true`
|
|
269
|
+
});
|
|
270
|
+
const gitHooksDir = await resolveGitHooksDir(projectRoot);
|
|
271
|
+
if (!gitHooksDir) {
|
|
272
|
+
checks.push({
|
|
273
|
+
name: "git_hooks:managed:relays",
|
|
274
|
+
ok: true,
|
|
275
|
+
details: "git repository not detected; relay hook check skipped"
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const preCommitHookPath = path.join(gitHooksDir, "pre-commit");
|
|
280
|
+
const prePushHookPath = path.join(gitHooksDir, "pre-push");
|
|
281
|
+
let relaysOk = false;
|
|
282
|
+
if ((await exists(preCommitHookPath)) && (await exists(prePushHookPath))) {
|
|
283
|
+
const preCommitHook = await fs.readFile(preCommitHookPath, "utf8");
|
|
284
|
+
const prePushHook = await fs.readFile(prePushHookPath, "utf8");
|
|
285
|
+
relaysOk =
|
|
286
|
+
preCommitHook.includes("cclaw-managed-git-hook") &&
|
|
287
|
+
prePushHook.includes("cclaw-managed-git-hook");
|
|
288
|
+
}
|
|
289
|
+
checks.push({
|
|
290
|
+
name: "git_hooks:managed:relays",
|
|
291
|
+
ok: relaysOk,
|
|
292
|
+
details: `${path.relative(projectRoot, gitHooksDir)}/pre-commit and pre-push must contain managed relay marker`
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
188
297
|
for (const harness of configuredHarnesses) {
|
|
189
298
|
const adapter = HARNESS_ADAPTERS[harness];
|
|
190
299
|
if (!adapter) {
|
|
@@ -223,19 +332,14 @@ export async function doctorChecks(projectRoot) {
|
|
|
223
332
|
const hasAllCommands = COMMAND_FILE_ORDER.every((stage) => content.includes(`/cc-${stage}`));
|
|
224
333
|
const hasRouting = content.includes("Intent → Stage Routing") || content.includes("Intent → Stage");
|
|
225
334
|
const hasVerification = content.includes("Verification Discipline");
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
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;
|
|
335
|
+
const hasMinimalMarker = content.includes("intentionally minimal for cross-project use");
|
|
336
|
+
const hasMetaSkillPointer = content.includes(".cclaw/skills/using-cclaw/SKILL.md");
|
|
337
|
+
agentsBlockOk = hasMarkers && hasAllCommands && hasRouting && hasVerification && hasMinimalMarker && hasMetaSkillPointer;
|
|
234
338
|
}
|
|
235
339
|
checks.push({
|
|
236
340
|
name: "agents:cclaw_block",
|
|
237
341
|
ok: agentsBlockOk,
|
|
238
|
-
details: `${agentsFile} must contain cclaw marker block with routing, verification, and
|
|
342
|
+
details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
|
|
239
343
|
});
|
|
240
344
|
// Utility commands
|
|
241
345
|
for (const cmd of ["learn", "autoplan"]) {
|
|
@@ -262,14 +366,8 @@ export async function doctorChecks(projectRoot) {
|
|
|
262
366
|
details: skillPath
|
|
263
367
|
});
|
|
264
368
|
}
|
|
265
|
-
//
|
|
266
|
-
for (const folder of
|
|
267
|
-
"security",
|
|
268
|
-
"debugging",
|
|
269
|
-
"performance",
|
|
270
|
-
"ci-cd",
|
|
271
|
-
"docs"
|
|
272
|
-
]) {
|
|
369
|
+
// Extended utility skills generated from utility skill map.
|
|
370
|
+
for (const folder of UTILITY_SKILL_FOLDERS) {
|
|
273
371
|
const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", folder, "SKILL.md");
|
|
274
372
|
checks.push({
|
|
275
373
|
name: `utility_skill:${folder}`,
|
|
@@ -296,6 +394,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
296
394
|
"session-start.sh",
|
|
297
395
|
"stop-checkpoint.sh",
|
|
298
396
|
"prompt-guard.sh",
|
|
397
|
+
"workflow-guard.sh",
|
|
299
398
|
"context-monitor.sh",
|
|
300
399
|
"observe.sh",
|
|
301
400
|
"summarize-observations.sh",
|
|
@@ -349,14 +448,33 @@ export async function doctorChecks(projectRoot) {
|
|
|
349
448
|
ok: hookOk,
|
|
350
449
|
details: fullPath
|
|
351
450
|
});
|
|
451
|
+
if (harness === "claude" || harness === "cursor" || harness === "codex") {
|
|
452
|
+
const schema = validateHookDocument(harness, parsed);
|
|
453
|
+
checks.push({
|
|
454
|
+
name: `hook:schema:${harness}`,
|
|
455
|
+
ok: schema.ok,
|
|
456
|
+
details: schema.ok
|
|
457
|
+
? `${fullPath} matches cclaw hook schema v1`
|
|
458
|
+
: `${fullPath} schema issues: ${schema.errors.join("; ")}`
|
|
459
|
+
});
|
|
460
|
+
}
|
|
352
461
|
}
|
|
353
462
|
}
|
|
354
|
-
// OpenCode plugin
|
|
463
|
+
// OpenCode plugin source + deployed path
|
|
355
464
|
checks.push({
|
|
356
|
-
name: "hook:
|
|
465
|
+
name: "hook:opencode_plugin_source",
|
|
357
466
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "hooks", "opencode-plugin.mjs")),
|
|
358
467
|
details: `${RUNTIME_ROOT}/hooks/opencode-plugin.mjs`
|
|
359
468
|
});
|
|
469
|
+
const opencodeEnabled = configuredHarnesses.includes("opencode");
|
|
470
|
+
const opencodeDeployed = await exists(path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs"));
|
|
471
|
+
checks.push({
|
|
472
|
+
name: "hook:opencode_plugin_deployed",
|
|
473
|
+
ok: opencodeEnabled ? opencodeDeployed : true,
|
|
474
|
+
details: opencodeEnabled
|
|
475
|
+
? ".opencode/plugins/cclaw-plugin.mjs"
|
|
476
|
+
: "opencode harness disabled; deployed plugin check skipped"
|
|
477
|
+
});
|
|
360
478
|
if (configuredHarnesses.includes("claude")) {
|
|
361
479
|
const file = path.join(projectRoot, ".claude/hooks/hooks.json");
|
|
362
480
|
const parsed = await readHookDocument(file);
|
|
@@ -374,6 +492,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
374
492
|
const stopCommands = collectHookCommands(hooks.Stop);
|
|
375
493
|
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
376
494
|
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
495
|
+
preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
|
|
377
496
|
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
378
497
|
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
379
498
|
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
@@ -382,7 +501,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
382
501
|
checks.push({
|
|
383
502
|
name: "hook:wiring:claude",
|
|
384
503
|
ok: wiringOk,
|
|
385
|
-
details: `${file} must wire session-start/prompt-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
504
|
+
details: `${file} must wire session-start/prompt-guard/workflow-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
386
505
|
});
|
|
387
506
|
}
|
|
388
507
|
if (configuredHarnesses.includes("cursor")) {
|
|
@@ -409,6 +528,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
409
528
|
const stopCommands = collectHookCommands(hooks.stop);
|
|
410
529
|
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
411
530
|
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
531
|
+
preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
|
|
412
532
|
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
413
533
|
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
414
534
|
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
@@ -417,7 +537,21 @@ export async function doctorChecks(projectRoot) {
|
|
|
417
537
|
checks.push({
|
|
418
538
|
name: "hook:wiring:cursor",
|
|
419
539
|
ok: wiringOk,
|
|
420
|
-
details: `${file} must wire session-start/prompt-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
540
|
+
details: `${file} must wire session-start/prompt-guard/workflow-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
541
|
+
});
|
|
542
|
+
const cursorRulePath = path.join(projectRoot, ".cursor/rules/cclaw-workflow.mdc");
|
|
543
|
+
let cursorRuleOk = false;
|
|
544
|
+
if (await exists(cursorRulePath)) {
|
|
545
|
+
const content = await fs.readFile(cursorRulePath, "utf8");
|
|
546
|
+
cursorRuleOk =
|
|
547
|
+
content.includes("cclaw-managed-cursor-workflow-rule") &&
|
|
548
|
+
content.includes(".cclaw/state/flow-state.json") &&
|
|
549
|
+
content.includes("/cc-next");
|
|
550
|
+
}
|
|
551
|
+
checks.push({
|
|
552
|
+
name: "rules:cursor:workflow",
|
|
553
|
+
ok: cursorRuleOk,
|
|
554
|
+
details: `${cursorRulePath} must include managed marker and core cclaw workflow guardrails`
|
|
421
555
|
});
|
|
422
556
|
}
|
|
423
557
|
if (configuredHarnesses.includes("codex")) {
|
|
@@ -437,6 +571,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
437
571
|
const stopCommands = collectHookCommands(hooks.Stop);
|
|
438
572
|
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
439
573
|
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
574
|
+
preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
|
|
440
575
|
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
441
576
|
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
442
577
|
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
@@ -445,25 +580,37 @@ export async function doctorChecks(projectRoot) {
|
|
|
445
580
|
checks.push({
|
|
446
581
|
name: "hook:wiring:codex",
|
|
447
582
|
ok: wiringOk,
|
|
448
|
-
details: `${file} must wire session-start/prompt-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
583
|
+
details: `${file} must wire session-start/prompt-guard/workflow-guard/observe/context-monitor/summarize/stop-checkpoint`
|
|
449
584
|
});
|
|
450
585
|
}
|
|
451
586
|
if (configuredHarnesses.includes("opencode")) {
|
|
452
|
-
const file = path.join(projectRoot,
|
|
587
|
+
const file = path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs");
|
|
453
588
|
let ok = false;
|
|
454
589
|
if (await exists(file)) {
|
|
455
590
|
const content = await fs.readFile(file, "utf8");
|
|
456
591
|
ok =
|
|
457
|
-
content.includes(
|
|
592
|
+
content.includes("event: async") &&
|
|
593
|
+
content.includes('"tool.execute.before"') &&
|
|
594
|
+
content.includes('"tool.execute.after"') &&
|
|
595
|
+
content.includes("prompt-guard.sh") &&
|
|
596
|
+
content.includes("workflow-guard.sh") &&
|
|
597
|
+
content.includes("context-monitor.sh") &&
|
|
598
|
+
content.includes('"session.idle"') &&
|
|
599
|
+
content.includes('"session.updated"') &&
|
|
458
600
|
content.includes('"session.resumed"') &&
|
|
459
|
-
content.includes('"session.compacted"') &&
|
|
460
601
|
content.includes('"session.cleared"') &&
|
|
461
602
|
content.includes('"experimental.chat.system.transform"');
|
|
462
603
|
}
|
|
463
604
|
checks.push({
|
|
464
605
|
name: "lifecycle:opencode:rehydration_events",
|
|
465
606
|
ok,
|
|
466
|
-
details: `${file} must include
|
|
607
|
+
details: `${file} must include event lifecycle handler, tool.execute.before/after with prompt/workflow/context hooks, session.idle summarization, and transform rehydration`
|
|
608
|
+
});
|
|
609
|
+
const registration = await opencodeRegistrationCheck(projectRoot);
|
|
610
|
+
checks.push({
|
|
611
|
+
name: "hook:opencode:config_registration",
|
|
612
|
+
ok: registration.ok,
|
|
613
|
+
details: registration.details
|
|
467
614
|
});
|
|
468
615
|
}
|
|
469
616
|
const hasBash = await commandAvailable("bash");
|
|
@@ -516,7 +663,56 @@ export async function doctorChecks(projectRoot) {
|
|
|
516
663
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
|
|
517
664
|
details: `${RUNTIME_ROOT}/state/suggestion-memory.json must exist for proactive suggestion memory`
|
|
518
665
|
});
|
|
519
|
-
const
|
|
666
|
+
const contextModeStatePath = path.join(projectRoot, RUNTIME_ROOT, "state", "context-mode.json");
|
|
667
|
+
checks.push({
|
|
668
|
+
name: "state:context_mode_exists",
|
|
669
|
+
ok: await exists(contextModeStatePath),
|
|
670
|
+
details: `${RUNTIME_ROOT}/state/context-mode.json must exist for context mode switching`
|
|
671
|
+
});
|
|
672
|
+
if (await exists(contextModeStatePath)) {
|
|
673
|
+
let contextModeOk = false;
|
|
674
|
+
try {
|
|
675
|
+
const parsed = JSON.parse(await fs.readFile(contextModeStatePath, "utf8"));
|
|
676
|
+
const activeMode = typeof parsed.activeMode === "string" ? parsed.activeMode : "";
|
|
677
|
+
contextModeOk = activeMode.length > 0 && Object.prototype.hasOwnProperty.call(CONTEXT_MODES, activeMode);
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
contextModeOk = false;
|
|
681
|
+
}
|
|
682
|
+
checks.push({
|
|
683
|
+
name: "state:context_mode_valid",
|
|
684
|
+
ok: contextModeOk,
|
|
685
|
+
details: `${RUNTIME_ROOT}/state/context-mode.json must reference one of: ${Object.keys(CONTEXT_MODES).join(", ")} (default=${DEFAULT_CONTEXT_MODE})`
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
for (const mode of Object.keys(CONTEXT_MODES)) {
|
|
689
|
+
const modePath = path.join(projectRoot, RUNTIME_ROOT, "contexts", `${mode}.md`);
|
|
690
|
+
checks.push({
|
|
691
|
+
name: `contexts:mode:${mode}`,
|
|
692
|
+
ok: await exists(modePath),
|
|
693
|
+
details: modePath
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
let flowState = await readFlowState(projectRoot);
|
|
697
|
+
if (options.reconcileCurrentStageGates === true) {
|
|
698
|
+
const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
|
|
699
|
+
if (reconciliation.wrote) {
|
|
700
|
+
flowState = {
|
|
701
|
+
...flowState,
|
|
702
|
+
stageGateCatalog: {
|
|
703
|
+
...flowState.stageGateCatalog,
|
|
704
|
+
[reconciliation.stage]: reconciliation.after
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
checks.push({
|
|
709
|
+
name: "gates:reconcile:writeback",
|
|
710
|
+
ok: true,
|
|
711
|
+
details: reconciliation.wrote
|
|
712
|
+
? `reconciled gate catalog for stage "${reconciliation.stage}": ${reconciliation.notes.join("; ")}`
|
|
713
|
+
: `no gate reconciliation changes needed for stage "${reconciliation.stage}"`
|
|
714
|
+
});
|
|
715
|
+
}
|
|
520
716
|
checks.push({
|
|
521
717
|
name: "flow_state:active_run_id",
|
|
522
718
|
ok: typeof flowState.activeRunId === "string" && flowState.activeRunId.trim().length > 0,
|
|
@@ -537,6 +733,55 @@ export async function doctorChecks(projectRoot) {
|
|
|
537
733
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "00-handoff.md")),
|
|
538
734
|
details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/00-handoff.md must exist`
|
|
539
735
|
});
|
|
736
|
+
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
|
|
737
|
+
checks.push({
|
|
738
|
+
name: "delegation:mandatory:current_stage",
|
|
739
|
+
ok: delegation.satisfied,
|
|
740
|
+
details: delegation.satisfied
|
|
741
|
+
? `All mandatory delegations satisfied for stage "${flowState.currentStage}"`
|
|
742
|
+
: `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}`
|
|
743
|
+
});
|
|
744
|
+
checks.push({
|
|
745
|
+
name: "warning:delegation:waived",
|
|
746
|
+
ok: true,
|
|
747
|
+
details: delegation.waived.length > 0
|
|
748
|
+
? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}`
|
|
749
|
+
: "no waived mandatory delegations for current stage"
|
|
750
|
+
});
|
|
751
|
+
const trace = await buildTraceMatrix(projectRoot);
|
|
752
|
+
const traceHasSignal = trace.entries.length > 0 ||
|
|
753
|
+
trace.orphanedCriteria.length > 0 ||
|
|
754
|
+
trace.orphanedTasks.length > 0 ||
|
|
755
|
+
trace.orphanedTests.length > 0;
|
|
756
|
+
checks.push({
|
|
757
|
+
name: "trace:criteria_coverage",
|
|
758
|
+
ok: !traceHasSignal || trace.orphanedCriteria.length === 0,
|
|
759
|
+
details: trace.orphanedCriteria.length === 0
|
|
760
|
+
? "all spec criteria are linked to plan tasks"
|
|
761
|
+
: `orphaned criteria: ${trace.orphanedCriteria.join(", ")}`
|
|
762
|
+
});
|
|
763
|
+
checks.push({
|
|
764
|
+
name: "trace:task_to_test_coverage",
|
|
765
|
+
ok: !traceHasSignal || trace.orphanedTasks.length === 0,
|
|
766
|
+
details: trace.orphanedTasks.length === 0
|
|
767
|
+
? "all plan tasks are linked to test slices"
|
|
768
|
+
: `orphaned tasks: ${trace.orphanedTasks.join(", ")}`
|
|
769
|
+
});
|
|
770
|
+
checks.push({
|
|
771
|
+
name: "trace:test_to_criteria_coverage",
|
|
772
|
+
ok: !traceHasSignal || trace.orphanedTests.length === 0,
|
|
773
|
+
details: trace.orphanedTests.length === 0
|
|
774
|
+
? "all test slices map to acceptance-linked tasks"
|
|
775
|
+
: `orphaned test slices: ${trace.orphanedTests.join(", ")}`
|
|
776
|
+
});
|
|
777
|
+
const gateEvidence = await verifyCurrentStageGateEvidence(projectRoot, flowState);
|
|
778
|
+
checks.push({
|
|
779
|
+
name: "gates:evidence:current_stage",
|
|
780
|
+
ok: gateEvidence.ok,
|
|
781
|
+
details: gateEvidence.ok
|
|
782
|
+
? `stage "${gateEvidence.stage}" gate evidence is consistent (required=${gateEvidence.requiredCount}, passed=${gateEvidence.passedCount}, blocked=${gateEvidence.blockedCount})`
|
|
783
|
+
: gateEvidence.issues.join(" ")
|
|
784
|
+
});
|
|
540
785
|
// Utility shims in harness dirs
|
|
541
786
|
for (const harness of configuredHarnesses) {
|
|
542
787
|
const adapter = HARNESS_ADAPTERS[harness];
|
|
@@ -601,7 +846,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
601
846
|
ok: hasRules,
|
|
602
847
|
details: rulesJsonPath
|
|
603
848
|
});
|
|
604
|
-
const policy = await policyChecks(projectRoot);
|
|
849
|
+
const policy = await policyChecks(projectRoot, { harnesses: configuredHarnesses });
|
|
605
850
|
checks.push(...policy);
|
|
606
851
|
return checks;
|
|
607
852
|
}
|
package/dist/fs-utils.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
2
|
+
export interface DirectoryLockOptions {
|
|
3
|
+
retries?: number;
|
|
4
|
+
retryDelayMs?: number;
|
|
5
|
+
staleAfterMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Acquire a lightweight lock by creating a directory.
|
|
9
|
+
* The lock is removed in a finally block.
|
|
10
|
+
*/
|
|
11
|
+
export declare function withDirectoryLock<T>(lockPath: string, fn: () => Promise<T>, options?: DirectoryLockOptions): Promise<T>;
|
|
2
12
|
export declare function writeFileSafe(filePath: string, content: string): Promise<void>;
|
|
3
13
|
export declare function exists(filePath: string): Promise<boolean>;
|
|
4
14
|
export declare function removeIfExists(targetPath: string): Promise<void>;
|
package/dist/fs-utils.js
CHANGED
|
@@ -3,6 +3,53 @@ import path from "node:path";
|
|
|
3
3
|
export async function ensureDir(dirPath) {
|
|
4
4
|
await fs.mkdir(dirPath, { recursive: true });
|
|
5
5
|
}
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Acquire a lightweight lock by creating a directory.
|
|
11
|
+
* The lock is removed in a finally block.
|
|
12
|
+
*/
|
|
13
|
+
export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
14
|
+
const retries = options.retries ?? 200;
|
|
15
|
+
const retryDelayMs = options.retryDelayMs ?? 20;
|
|
16
|
+
const staleAfterMs = options.staleAfterMs ?? 60_000;
|
|
17
|
+
await ensureDir(path.dirname(lockPath));
|
|
18
|
+
let acquired = false;
|
|
19
|
+
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.mkdir(lockPath);
|
|
22
|
+
acquired = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const code = error?.code;
|
|
27
|
+
if (code !== "EEXIST") {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const stat = await fs.stat(lockPath);
|
|
32
|
+
if (Date.now() - stat.mtimeMs > staleAfterMs) {
|
|
33
|
+
await fs.rm(lockPath, { recursive: true, force: true });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Lock directory disappeared between retries.
|
|
39
|
+
}
|
|
40
|
+
await sleep(retryDelayMs);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!acquired) {
|
|
44
|
+
throw new Error(`Failed to acquire lock: ${lockPath}`);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => { });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
6
53
|
export async function writeFileSafe(filePath, content) {
|
|
7
54
|
await ensureDir(path.dirname(filePath));
|
|
8
55
|
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FlowState, StageGateState } from "./flow-state.js";
|
|
2
|
+
import type { FlowStage } from "./types.js";
|
|
3
|
+
export interface GateEvidenceCheckResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
stage: FlowStage;
|
|
6
|
+
issues: string[];
|
|
7
|
+
requiredCount: number;
|
|
8
|
+
passedCount: number;
|
|
9
|
+
blockedCount: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function verifyCurrentStageGateEvidence(projectRoot: string, flowState: FlowState): Promise<GateEvidenceCheckResult>;
|
|
12
|
+
export interface GateReconciliationResult {
|
|
13
|
+
stage: FlowStage;
|
|
14
|
+
changed: boolean;
|
|
15
|
+
before: StageGateState;
|
|
16
|
+
after: StageGateState;
|
|
17
|
+
notes: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface GateReconciliationWritebackResult extends GateReconciliationResult {
|
|
20
|
+
wrote: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function reconcileCurrentStageGateCatalog(flowState: FlowState): {
|
|
23
|
+
nextState: FlowState;
|
|
24
|
+
reconciliation: GateReconciliationResult;
|
|
25
|
+
};
|
|
26
|
+
export declare function reconcileAndWriteCurrentStageGateCatalog(projectRoot: string): Promise<GateReconciliationWritebackResult>;
|