cclaw-cli 0.1.1 → 0.2.1
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/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 +8 -2
- package/dist/config.d.ts +4 -4
- package/dist/config.js +56 -5
- 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 -23
- 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 +2 -0
- 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 +431 -16
- 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 +1 -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.plugin) ? parsed.plugin : [];
|
|
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,13 +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
|
-
|
|
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;
|
|
228
338
|
}
|
|
229
339
|
checks.push({
|
|
230
340
|
name: "agents:cclaw_block",
|
|
231
341
|
ok: agentsBlockOk,
|
|
232
|
-
details: `${agentsFile} must contain cclaw marker block with
|
|
342
|
+
details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
|
|
233
343
|
});
|
|
234
344
|
// Utility commands
|
|
235
345
|
for (const cmd of ["learn", "autoplan"]) {
|
|
@@ -256,14 +366,8 @@ export async function doctorChecks(projectRoot) {
|
|
|
256
366
|
details: skillPath
|
|
257
367
|
});
|
|
258
368
|
}
|
|
259
|
-
//
|
|
260
|
-
for (const folder of
|
|
261
|
-
"security",
|
|
262
|
-
"debugging",
|
|
263
|
-
"performance",
|
|
264
|
-
"ci-cd",
|
|
265
|
-
"docs"
|
|
266
|
-
]) {
|
|
369
|
+
// Extended utility skills generated from utility skill map.
|
|
370
|
+
for (const folder of UTILITY_SKILL_FOLDERS) {
|
|
267
371
|
const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", folder, "SKILL.md");
|
|
268
372
|
checks.push({
|
|
269
373
|
name: `utility_skill:${folder}`,
|
|
@@ -290,6 +394,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
290
394
|
"session-start.sh",
|
|
291
395
|
"stop-checkpoint.sh",
|
|
292
396
|
"prompt-guard.sh",
|
|
397
|
+
"workflow-guard.sh",
|
|
293
398
|
"context-monitor.sh",
|
|
294
399
|
"observe.sh",
|
|
295
400
|
"summarize-observations.sh",
|
|
@@ -343,14 +448,33 @@ export async function doctorChecks(projectRoot) {
|
|
|
343
448
|
ok: hookOk,
|
|
344
449
|
details: fullPath
|
|
345
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
|
+
}
|
|
346
461
|
}
|
|
347
462
|
}
|
|
348
|
-
// OpenCode plugin
|
|
463
|
+
// OpenCode plugin source + deployed path
|
|
349
464
|
checks.push({
|
|
350
|
-
name: "hook:
|
|
465
|
+
name: "hook:opencode_plugin_source",
|
|
351
466
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "hooks", "opencode-plugin.mjs")),
|
|
352
467
|
details: `${RUNTIME_ROOT}/hooks/opencode-plugin.mjs`
|
|
353
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
|
+
});
|
|
354
478
|
if (configuredHarnesses.includes("claude")) {
|
|
355
479
|
const file = path.join(projectRoot, ".claude/hooks/hooks.json");
|
|
356
480
|
const parsed = await readHookDocument(file);
|
|
@@ -368,6 +492,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
368
492
|
const stopCommands = collectHookCommands(hooks.Stop);
|
|
369
493
|
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
370
494
|
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
495
|
+
preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
|
|
371
496
|
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
372
497
|
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
373
498
|
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
@@ -376,7 +501,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
376
501
|
checks.push({
|
|
377
502
|
name: "hook:wiring:claude",
|
|
378
503
|
ok: wiringOk,
|
|
379
|
-
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`
|
|
380
505
|
});
|
|
381
506
|
}
|
|
382
507
|
if (configuredHarnesses.includes("cursor")) {
|
|
@@ -403,6 +528,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
403
528
|
const stopCommands = collectHookCommands(hooks.stop);
|
|
404
529
|
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
405
530
|
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
531
|
+
preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
|
|
406
532
|
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
407
533
|
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
408
534
|
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
@@ -411,7 +537,21 @@ export async function doctorChecks(projectRoot) {
|
|
|
411
537
|
checks.push({
|
|
412
538
|
name: "hook:wiring:cursor",
|
|
413
539
|
ok: wiringOk,
|
|
414
|
-
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`
|
|
415
555
|
});
|
|
416
556
|
}
|
|
417
557
|
if (configuredHarnesses.includes("codex")) {
|
|
@@ -431,6 +571,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
431
571
|
const stopCommands = collectHookCommands(hooks.Stop);
|
|
432
572
|
const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
433
573
|
preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
574
|
+
preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
|
|
434
575
|
preCommands.some((cmd) => cmd.includes("observe.sh pre")) &&
|
|
435
576
|
postCommands.some((cmd) => cmd.includes("observe.sh post")) &&
|
|
436
577
|
postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
@@ -439,25 +580,37 @@ export async function doctorChecks(projectRoot) {
|
|
|
439
580
|
checks.push({
|
|
440
581
|
name: "hook:wiring:codex",
|
|
441
582
|
ok: wiringOk,
|
|
442
|
-
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`
|
|
443
584
|
});
|
|
444
585
|
}
|
|
445
586
|
if (configuredHarnesses.includes("opencode")) {
|
|
446
|
-
const file = path.join(projectRoot,
|
|
587
|
+
const file = path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs");
|
|
447
588
|
let ok = false;
|
|
448
589
|
if (await exists(file)) {
|
|
449
590
|
const content = await fs.readFile(file, "utf8");
|
|
450
591
|
ok =
|
|
451
|
-
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"') &&
|
|
452
600
|
content.includes('"session.resumed"') &&
|
|
453
|
-
content.includes('"session.compacted"') &&
|
|
454
601
|
content.includes('"session.cleared"') &&
|
|
455
602
|
content.includes('"experimental.chat.system.transform"');
|
|
456
603
|
}
|
|
457
604
|
checks.push({
|
|
458
605
|
name: "lifecycle:opencode:rehydration_events",
|
|
459
606
|
ok,
|
|
460
|
-
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
|
|
461
614
|
});
|
|
462
615
|
}
|
|
463
616
|
const hasBash = await commandAvailable("bash");
|
|
@@ -510,7 +663,56 @@ export async function doctorChecks(projectRoot) {
|
|
|
510
663
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
|
|
511
664
|
details: `${RUNTIME_ROOT}/state/suggestion-memory.json must exist for proactive suggestion memory`
|
|
512
665
|
});
|
|
513
|
-
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
|
+
}
|
|
514
716
|
checks.push({
|
|
515
717
|
name: "flow_state:active_run_id",
|
|
516
718
|
ok: typeof flowState.activeRunId === "string" && flowState.activeRunId.trim().length > 0,
|
|
@@ -531,6 +733,55 @@ export async function doctorChecks(projectRoot) {
|
|
|
531
733
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "00-handoff.md")),
|
|
532
734
|
details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/00-handoff.md must exist`
|
|
533
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
|
+
});
|
|
534
785
|
// Utility shims in harness dirs
|
|
535
786
|
for (const harness of configuredHarnesses) {
|
|
536
787
|
const adapter = HARNESS_ADAPTERS[harness];
|
|
@@ -595,7 +846,7 @@ export async function doctorChecks(projectRoot) {
|
|
|
595
846
|
ok: hasRules,
|
|
596
847
|
details: rulesJsonPath
|
|
597
848
|
});
|
|
598
|
-
const policy = await policyChecks(projectRoot);
|
|
849
|
+
const policy = await policyChecks(projectRoot, { harnesses: configuredHarnesses });
|
|
599
850
|
checks.push(...policy);
|
|
600
851
|
return checks;
|
|
601
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>;
|