cclaw-cli 0.51.21 → 0.51.23
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 +14 -13
- package/dist/config.d.ts +8 -1
- package/dist/config.js +9 -6
- package/dist/content/examples.js +2 -2
- package/dist/content/hook-manifest.d.ts +2 -4
- package/dist/content/hook-manifest.js +5 -7
- package/dist/content/learnings.js +5 -2
- package/dist/content/meta-skill.d.ts +1 -0
- package/dist/content/meta-skill.js +16 -9
- package/dist/content/next-command.js +2 -2
- package/dist/content/node-hooks.js +14 -4
- package/dist/content/review-loop.js +15 -5
- package/dist/content/review-prompts.js +1 -1
- package/dist/content/skills.js +16 -11
- package/dist/content/stage-command.d.ts +2 -0
- package/dist/content/stage-command.js +17 -0
- package/dist/content/stage-schema.js +1 -0
- package/dist/content/stages/brainstorm.js +3 -3
- package/dist/content/stages/design.js +18 -17
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/review.js +15 -15
- package/dist/content/stages/scope.js +14 -14
- package/dist/content/stages/spec.js +7 -5
- package/dist/content/stages/tdd.js +11 -4
- package/dist/content/start-command.d.ts +4 -3
- package/dist/content/start-command.js +21 -17
- package/dist/content/subagents.js +14 -4
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +49 -29
- package/dist/content/track-render-context.js +7 -0
- package/dist/content/view-command.js +3 -1
- package/dist/delegation.d.ts +2 -2
- package/dist/delegation.js +40 -13
- package/dist/doctor-registry.js +1 -1
- package/dist/doctor.js +222 -34
- package/dist/gate-evidence.js +19 -7
- package/dist/harness-adapters.d.ts +14 -11
- package/dist/harness-adapters.js +154 -22
- package/dist/install.js +116 -28
- package/dist/internal/advance-stage.js +90 -11
- package/dist/knowledge-store.d.ts +4 -1
- package/dist/knowledge-store.js +24 -14
- package/dist/retro-gate.d.ts +1 -0
- package/dist/retro-gate.js +9 -9
- package/dist/run-archive.js +19 -1
- package/dist/run-persistence.js +6 -2
- package/dist/tdd-cycle.js +6 -3
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -18,6 +18,7 @@ import { buildTraceMatrix } from "./trace-matrix.js";
|
|
|
18
18
|
import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
|
|
19
19
|
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
20
20
|
import { stageSkillFolder } from "./content/skills.js";
|
|
21
|
+
import { stageCommandShimMarkdown } from "./content/stage-command.js";
|
|
21
22
|
import { doctorCheckMetadata } from "./doctor-registry.js";
|
|
22
23
|
import { resolveTrackFromPrompt } from "./track-heuristics.js";
|
|
23
24
|
import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
|
|
@@ -25,6 +26,7 @@ import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_
|
|
|
25
26
|
import { validateHookDocument } from "./hook-schema.js";
|
|
26
27
|
import { validateKnowledgeEntry } from "./knowledge-store.js";
|
|
27
28
|
import { readSeedShelf } from "./content/seed-shelf.js";
|
|
29
|
+
import { evaluateRetroGate } from "./retro-gate.js";
|
|
28
30
|
const execFileAsync = promisify(execFile);
|
|
29
31
|
async function isGitRepo(projectRoot) {
|
|
30
32
|
try {
|
|
@@ -149,18 +151,30 @@ function knowledgeRoutingSurfaceIsDiscoverable(content) {
|
|
|
149
151
|
return ["trigger", "action", "origin_run"].every((term) => normalized.includes(term));
|
|
150
152
|
}
|
|
151
153
|
async function commandAvailable(command) {
|
|
154
|
+
const version = await commandVersion(command);
|
|
155
|
+
return version.available;
|
|
156
|
+
}
|
|
157
|
+
async function commandVersion(command, args = ["--version"]) {
|
|
152
158
|
try {
|
|
153
159
|
if (process.platform === "win32") {
|
|
154
160
|
await execFileAsync("where", [command]);
|
|
155
|
-
return true;
|
|
156
161
|
}
|
|
157
|
-
await execFileAsync(command,
|
|
158
|
-
return true;
|
|
162
|
+
const { stdout, stderr } = await execFileAsync(command, args);
|
|
163
|
+
return { available: true, output: `${stdout}${stderr}`.trim() };
|
|
159
164
|
}
|
|
160
165
|
catch {
|
|
161
|
-
return false;
|
|
166
|
+
return { available: false, output: "" };
|
|
162
167
|
}
|
|
163
168
|
}
|
|
169
|
+
function parseNodeMajor(versionOutput) {
|
|
170
|
+
const match = /v?(\d+)\./u.exec(versionOutput);
|
|
171
|
+
if (!match)
|
|
172
|
+
return null;
|
|
173
|
+
return Number(match[1]);
|
|
174
|
+
}
|
|
175
|
+
function gitVersionLooksUsable(versionOutput) {
|
|
176
|
+
return /git version \d+\.\d+/iu.test(versionOutput);
|
|
177
|
+
}
|
|
164
178
|
function stripJsonCommentsOutsideStrings(input) {
|
|
165
179
|
let out = "";
|
|
166
180
|
let i = 0;
|
|
@@ -276,17 +290,23 @@ function normalizeOpenCodePluginEntry(entry) {
|
|
|
276
290
|
}
|
|
277
291
|
return null;
|
|
278
292
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
293
|
+
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
294
|
+
function opencodeConfigCandidates(projectRoot) {
|
|
295
|
+
return [
|
|
282
296
|
path.join(projectRoot, "opencode.json"),
|
|
283
297
|
path.join(projectRoot, "opencode.jsonc"),
|
|
284
298
|
path.join(projectRoot, ".opencode/opencode.json"),
|
|
285
299
|
path.join(projectRoot, ".opencode/opencode.jsonc")
|
|
286
300
|
];
|
|
301
|
+
}
|
|
302
|
+
function openCodeConfigRegistersPlugin(parsed) {
|
|
303
|
+
const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
304
|
+
return plugins.some((entry) => normalizeOpenCodePluginEntry(entry) === OPENCODE_PLUGIN_REL_PATH);
|
|
305
|
+
}
|
|
306
|
+
async function opencodeRegistrationCheck(projectRoot) {
|
|
287
307
|
const mismatches = [];
|
|
288
308
|
let foundAnyConfig = false;
|
|
289
|
-
for (const configPath of
|
|
309
|
+
for (const configPath of opencodeConfigCandidates(projectRoot)) {
|
|
290
310
|
if (!(await exists(configPath))) {
|
|
291
311
|
continue;
|
|
292
312
|
}
|
|
@@ -296,17 +316,130 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
296
316
|
mismatches.push(`${path.relative(projectRoot, configPath)} is unreadable or invalid JSON`);
|
|
297
317
|
continue;
|
|
298
318
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (registered) {
|
|
302
|
-
return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${expected}` };
|
|
319
|
+
if (openCodeConfigRegistersPlugin(parsed)) {
|
|
320
|
+
return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${OPENCODE_PLUGIN_REL_PATH}` };
|
|
303
321
|
}
|
|
304
|
-
mismatches.push(`${path.relative(projectRoot, configPath)} missing plugin ${
|
|
322
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} missing plugin ${OPENCODE_PLUGIN_REL_PATH}`);
|
|
305
323
|
}
|
|
306
324
|
if (foundAnyConfig) {
|
|
307
325
|
return { ok: false, details: mismatches.join(" | ") };
|
|
308
326
|
}
|
|
309
|
-
return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${
|
|
327
|
+
return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${OPENCODE_PLUGIN_REL_PATH}` };
|
|
328
|
+
}
|
|
329
|
+
async function opencodeQuestionPermissionCheck(projectRoot) {
|
|
330
|
+
const mismatches = [];
|
|
331
|
+
for (const configPath of opencodeConfigCandidates(projectRoot)) {
|
|
332
|
+
if (!(await exists(configPath)))
|
|
333
|
+
continue;
|
|
334
|
+
const parsed = await readHookDocument(configPath);
|
|
335
|
+
if (!parsed || !openCodeConfigRegistersPlugin(parsed))
|
|
336
|
+
continue;
|
|
337
|
+
const permission = toObject(parsed.permission) ?? {};
|
|
338
|
+
if (permission.question === "allow") {
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
details: `${path.relative(projectRoot, configPath)} sets permission.question to "allow" for structured questions`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} registers ${OPENCODE_PLUGIN_REL_PATH} but must set permission.question to "allow"`);
|
|
345
|
+
}
|
|
346
|
+
if (mismatches.length > 0) {
|
|
347
|
+
return { ok: false, details: mismatches.join(" | ") };
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
details: `No opencode config with ${OPENCODE_PLUGIN_REL_PATH} registration found; cannot verify permission.question = "allow"`
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function opencodeQuestionEnvCheck() {
|
|
355
|
+
if (process.env.OPENCODE_ENABLE_QUESTION_TOOL === "1") {
|
|
356
|
+
return { ok: true, details: "OPENCODE_ENABLE_QUESTION_TOOL=1 is set for ACP question tooling" };
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
details: "Set OPENCODE_ENABLE_QUESTION_TOOL=1 for OpenCode ACP clients so permission-gated structured questions can use the question tool."
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function initRecoveryCheck(projectRoot) {
|
|
364
|
+
const sentinelPath = path.join(projectRoot, RUNTIME_ROOT, "state", ".init-in-progress");
|
|
365
|
+
if (!(await exists(sentinelPath))) {
|
|
366
|
+
return { ok: true, details: "no partial init/sync sentinel found" };
|
|
367
|
+
}
|
|
368
|
+
let summary = `${RUNTIME_ROOT}/state/.init-in-progress sentinel present`;
|
|
369
|
+
try {
|
|
370
|
+
const parsed = JSON.parse(await fs.readFile(sentinelPath, "utf8"));
|
|
371
|
+
const operation = typeof parsed.operation === "string" ? parsed.operation : "unknown";
|
|
372
|
+
const startedAt = typeof parsed.startedAt === "string" ? parsed.startedAt : "unknown";
|
|
373
|
+
summary = `${summary} (operation=${operation}, startedAt=${startedAt})`;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
summary = `${summary} (unreadable sentinel payload)`;
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
details: `${summary}. Fix: inspect generated runtime files, then rerun cclaw sync or remove the sentinel only after confirming the runtime is complete.`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async function archiveIntegrityCheck(projectRoot) {
|
|
384
|
+
const runsDir = path.join(projectRoot, RUNTIME_ROOT, "runs");
|
|
385
|
+
if (!(await exists(runsDir))) {
|
|
386
|
+
return { ok: true, details: `${RUNTIME_ROOT}/runs is absent; no archives to inspect yet` };
|
|
387
|
+
}
|
|
388
|
+
let entries;
|
|
389
|
+
try {
|
|
390
|
+
entries = await fs.readdir(runsDir, { withFileTypes: true });
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
394
|
+
return { ok: false, details: `unable to inspect ${RUNTIME_ROOT}/runs (${reason})` };
|
|
395
|
+
}
|
|
396
|
+
const problems = [];
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
if (!entry.isDirectory())
|
|
399
|
+
continue;
|
|
400
|
+
const runId = entry.name;
|
|
401
|
+
const runPath = path.join(runsDir, runId);
|
|
402
|
+
const relRunPath = `${RUNTIME_ROOT}/runs/${runId}`;
|
|
403
|
+
if (await exists(path.join(runPath, ".archive-in-progress"))) {
|
|
404
|
+
problems.push(`${relRunPath}/.archive-in-progress sentinel present`);
|
|
405
|
+
}
|
|
406
|
+
const manifestPath = path.join(runPath, "archive-manifest.json");
|
|
407
|
+
if (!(await exists(manifestPath))) {
|
|
408
|
+
problems.push(`${relRunPath} missing archive-manifest.json`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
let manifest;
|
|
412
|
+
try {
|
|
413
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
417
|
+
problems.push(`${relRunPath}/archive-manifest.json unreadable (${reason})`);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const stateFiles = Array.isArray(manifest.snapshottedStateFiles)
|
|
421
|
+
? manifest.snapshottedStateFiles.filter((value) => typeof value === "string")
|
|
422
|
+
: [];
|
|
423
|
+
const stateDir = path.join(runPath, "state");
|
|
424
|
+
if (stateFiles.length > 0 && !(await exists(stateDir))) {
|
|
425
|
+
problems.push(`${relRunPath} manifest lists state snapshot files but state/ is missing`);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
for (const stateFile of stateFiles) {
|
|
429
|
+
if (stateFile.endsWith("/"))
|
|
430
|
+
continue;
|
|
431
|
+
if (!(await exists(path.join(stateDir, stateFile)))) {
|
|
432
|
+
problems.push(`${relRunPath}/state missing ${stateFile} listed in manifest`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (problems.length === 0) {
|
|
437
|
+
return { ok: true, details: "no partial archive sentinels or incomplete archive snapshots found" };
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
details: `${problems.join("; ")}. Fix: inspect the archive directory, retry archive if the active run was restored, or recover/rollback artifacts and state from the snapshot before removing the sentinel.`
|
|
442
|
+
};
|
|
310
443
|
}
|
|
311
444
|
async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
312
445
|
const pluginPath = path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs");
|
|
@@ -573,7 +706,6 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
573
706
|
ok: agentsBlockOk,
|
|
574
707
|
details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
|
|
575
708
|
});
|
|
576
|
-
// User-facing entry commands only. Stage and view subcommands live in skills.
|
|
577
709
|
for (const cmd of ["start", "next", "ideate", "view"]) {
|
|
578
710
|
const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
|
|
579
711
|
checks.push({
|
|
@@ -582,6 +714,19 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
582
714
|
details: cmdPath
|
|
583
715
|
});
|
|
584
716
|
}
|
|
717
|
+
for (const stage of FLOW_STAGES) {
|
|
718
|
+
const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${stage}.md`);
|
|
719
|
+
let stageCommandOk = false;
|
|
720
|
+
if (await exists(cmdPath)) {
|
|
721
|
+
const content = await fs.readFile(cmdPath, "utf8");
|
|
722
|
+
stageCommandOk = content === stageCommandShimMarkdown(stage);
|
|
723
|
+
}
|
|
724
|
+
checks.push({
|
|
725
|
+
name: `stage_command:${stage}`,
|
|
726
|
+
ok: stageCommandOk,
|
|
727
|
+
details: `${cmdPath} must be a thin shim to ${RUNTIME_ROOT}/skills/${stageSkillFolder(stage)}/SKILL.md and /cc-next`
|
|
728
|
+
});
|
|
729
|
+
}
|
|
585
730
|
// Utility skills
|
|
586
731
|
for (const [folder, label] of [
|
|
587
732
|
["learnings", "learnings"],
|
|
@@ -848,7 +993,6 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
848
993
|
const codexStopCmds = collectHookCommands(codexHooks.Stop);
|
|
849
994
|
const codexWiringOk = codexSessionCmds.some((cmd) => cmd.includes("session-start")) &&
|
|
850
995
|
codexUserPromptCmds.some((cmd) => cmd.includes("prompt-guard")) &&
|
|
851
|
-
codexUserPromptCmds.some((cmd) => cmd.includes("workflow-guard")) &&
|
|
852
996
|
codexUserPromptCmds.some((cmd) => cmd.includes("verify-current-state")) &&
|
|
853
997
|
codexPreCmds.some((cmd) => cmd.includes("prompt-guard")) &&
|
|
854
998
|
codexPreCmds.some((cmd) => cmd.includes("workflow-guard")) &&
|
|
@@ -857,7 +1001,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
857
1001
|
checks.push({
|
|
858
1002
|
name: "hook:wiring:codex",
|
|
859
1003
|
ok: codexWiringOk,
|
|
860
|
-
details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt/
|
|
1004
|
+
details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt/verify-current-state), Bash-only PreToolUse(prompt/workflow), Bash-only PostToolUse(context-monitor), and Stop(stop-handoff). Codex workflow-guard is intentionally strict Bash-only.`
|
|
861
1005
|
});
|
|
862
1006
|
// Feature flag warning: Codex ignores `.codex/hooks.json` unless the
|
|
863
1007
|
// user has `[features] codex_hooks = true` in `~/.codex/config.toml`.
|
|
@@ -980,12 +1124,49 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
980
1124
|
ok: registration.ok,
|
|
981
1125
|
details: registration.details
|
|
982
1126
|
});
|
|
1127
|
+
const questionPermission = await opencodeQuestionPermissionCheck(projectRoot);
|
|
1128
|
+
checks.push({
|
|
1129
|
+
name: "hook:opencode:question_permission",
|
|
1130
|
+
ok: questionPermission.ok,
|
|
1131
|
+
details: questionPermission.details
|
|
1132
|
+
});
|
|
1133
|
+
const questionEnv = opencodeQuestionEnvCheck();
|
|
1134
|
+
checks.push({
|
|
1135
|
+
name: "warning:opencode:question_tool_env",
|
|
1136
|
+
ok: questionEnv.ok,
|
|
1137
|
+
details: questionEnv.details
|
|
1138
|
+
});
|
|
983
1139
|
}
|
|
984
|
-
const
|
|
1140
|
+
const nodeVersion = await commandVersion("node");
|
|
1141
|
+
const nodeMajor = parseNodeMajor(nodeVersion.output);
|
|
985
1142
|
checks.push({
|
|
986
1143
|
name: "capability:required:node",
|
|
987
|
-
ok:
|
|
988
|
-
details:
|
|
1144
|
+
ok: nodeVersion.available,
|
|
1145
|
+
details: nodeVersion.available
|
|
1146
|
+
? `node binary available (${nodeVersion.output || "version unknown"})`
|
|
1147
|
+
: "node is required for cclaw runtime scripts and CLI wiring"
|
|
1148
|
+
});
|
|
1149
|
+
checks.push({
|
|
1150
|
+
name: "capability:required:node_version",
|
|
1151
|
+
ok: nodeVersion.available && nodeMajor !== null && nodeMajor >= 20,
|
|
1152
|
+
details: nodeVersion.available
|
|
1153
|
+
? `node >=20 required; detected ${nodeVersion.output || "unknown version"}`
|
|
1154
|
+
: "node version check skipped because node binary is unavailable"
|
|
1155
|
+
});
|
|
1156
|
+
const gitVersion = await commandVersion("git");
|
|
1157
|
+
checks.push({
|
|
1158
|
+
name: "capability:required:git",
|
|
1159
|
+
ok: gitVersion.available,
|
|
1160
|
+
details: gitVersion.available
|
|
1161
|
+
? `git binary available (${gitVersion.output || "version unknown"})`
|
|
1162
|
+
: "git is required for repository detection, hook setup, and doctor checks"
|
|
1163
|
+
});
|
|
1164
|
+
checks.push({
|
|
1165
|
+
name: "capability:required:git_version",
|
|
1166
|
+
ok: gitVersion.available && gitVersionLooksUsable(gitVersion.output),
|
|
1167
|
+
details: gitVersion.available
|
|
1168
|
+
? `git version output: ${gitVersion.output || "unknown version"}`
|
|
1169
|
+
: "git version check skipped because git binary is unavailable"
|
|
989
1170
|
});
|
|
990
1171
|
const windowsHookConfigCandidates = [
|
|
991
1172
|
path.join(projectRoot, ".claude/hooks/hooks.json"),
|
|
@@ -1083,12 +1264,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1083
1264
|
const key = `${trigger} => ${action}`;
|
|
1084
1265
|
triggerActionCounts.set(key, (triggerActionCounts.get(key) ?? 0) + 1);
|
|
1085
1266
|
}
|
|
1086
|
-
const missing = requiredV2Fields.some((field) =>
|
|
1087
|
-
if (field === "origin_run" && Object.prototype.hasOwnProperty.call(parsed, "origin_feature")) {
|
|
1088
|
-
return false;
|
|
1089
|
-
}
|
|
1090
|
-
return !Object.prototype.hasOwnProperty.call(parsed, field);
|
|
1091
|
-
});
|
|
1267
|
+
const missing = requiredV2Fields.some((field) => !Object.prototype.hasOwnProperty.call(parsed, field));
|
|
1092
1268
|
if (missing) {
|
|
1093
1269
|
missingSchemaV2Fields += 1;
|
|
1094
1270
|
}
|
|
@@ -1388,17 +1564,17 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1388
1564
|
? "no stale stages pending acknowledgement"
|
|
1389
1565
|
: `stale stages pending acknowledgement: ${staleStages.join(", ")}`
|
|
1390
1566
|
});
|
|
1391
|
-
const
|
|
1392
|
-
const retroComplete = !retroRequired ||
|
|
1393
|
-
(typeof flowState.retro.completedAt === "string" && flowState.retro.compoundEntries > 0);
|
|
1567
|
+
const retroGateStatus = await evaluateRetroGate(projectRoot, flowState);
|
|
1394
1568
|
checks.push({
|
|
1395
1569
|
name: "state:retro_gate",
|
|
1396
|
-
ok:
|
|
1397
|
-
details:
|
|
1398
|
-
?
|
|
1399
|
-
?
|
|
1570
|
+
ok: retroGateStatus.completed,
|
|
1571
|
+
details: retroGateStatus.completed
|
|
1572
|
+
? retroGateStatus.required
|
|
1573
|
+
? retroGateStatus.skipped
|
|
1574
|
+
? "retro gate complete (retro skipped with recorded closeout decision)"
|
|
1575
|
+
: `retro gate complete (${retroGateStatus.compoundEntries} compound entries)`
|
|
1400
1576
|
: "retro gate not required yet (ship not completed)"
|
|
1401
|
-
: "retro gate incomplete: ship flow requires recorded retrospective evidence."
|
|
1577
|
+
: "retro gate incomplete: ship flow requires recorded retrospective evidence or an explicit retro skip."
|
|
1402
1578
|
});
|
|
1403
1579
|
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
1404
1580
|
const tddLogExists = await exists(tddLogPath);
|
|
@@ -1444,6 +1620,18 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1444
1620
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
|
|
1445
1621
|
details: `${RUNTIME_ROOT}/runs must exist for archived run snapshots`
|
|
1446
1622
|
});
|
|
1623
|
+
const initRecovery = await initRecoveryCheck(projectRoot);
|
|
1624
|
+
checks.push({
|
|
1625
|
+
name: "state:init_recovery",
|
|
1626
|
+
ok: initRecovery.ok,
|
|
1627
|
+
details: initRecovery.details
|
|
1628
|
+
});
|
|
1629
|
+
const archiveIntegrity = await archiveIntegrityCheck(projectRoot);
|
|
1630
|
+
checks.push({
|
|
1631
|
+
name: "runs:archive_integrity",
|
|
1632
|
+
ok: archiveIntegrity.ok,
|
|
1633
|
+
details: archiveIntegrity.details
|
|
1634
|
+
});
|
|
1447
1635
|
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
|
|
1448
1636
|
repairFeatureSystem: false
|
|
1449
1637
|
});
|
package/dist/gate-evidence.js
CHANGED
|
@@ -336,11 +336,27 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
336
336
|
if (stage === "design") {
|
|
337
337
|
const researchGateRequired = schema.requiredGates.some((gate) => gate.id === "design_research_complete" && gate.tier === "required");
|
|
338
338
|
if (researchGateRequired) {
|
|
339
|
+
const designMarkdown = await readArtifactMarkdown(projectRoot, "03-design.md");
|
|
340
|
+
const inlineResearchBody = designMarkdown
|
|
341
|
+
? extractMarkdownSectionBody(designMarkdown, "Research Fleet Synthesis")
|
|
342
|
+
: null;
|
|
343
|
+
const inlineResearchLines = inlineResearchBody
|
|
344
|
+
? inlineResearchBody
|
|
345
|
+
.split(/\r?\n/gu)
|
|
346
|
+
.map((line) => line.trim())
|
|
347
|
+
.filter((line) => line.length > 0)
|
|
348
|
+
.filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line))
|
|
349
|
+
.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
|
|
350
|
+
!/<fill-in>/iu.test(line) &&
|
|
351
|
+
!/^>\s*Default path:/iu.test(line) &&
|
|
352
|
+
!/^\|\s*compact inline synthesis\s*\|\s*\|\s*\|\s*\|?\s*$/iu.test(line))
|
|
353
|
+
: [];
|
|
354
|
+
const inlineResearchComplete = inlineResearchLines.length > 0;
|
|
339
355
|
const researchMarkdown = await readArtifactMarkdown(projectRoot, "02a-research.md");
|
|
340
|
-
if (!researchMarkdown) {
|
|
341
|
-
issues.push("design research gate blocked (design_research_complete):
|
|
356
|
+
if (!inlineResearchComplete && !researchMarkdown) {
|
|
357
|
+
issues.push("design research gate blocked (design_research_complete): fill `Research Fleet Synthesis` in `.cclaw/artifacts/03-design.md`, or write `.cclaw/artifacts/02a-research.md` for deep/high-risk research.");
|
|
342
358
|
}
|
|
343
|
-
else {
|
|
359
|
+
else if (researchMarkdown) {
|
|
344
360
|
const missingSections = [];
|
|
345
361
|
for (const section of DESIGN_RESEARCH_REQUIRED_SECTIONS) {
|
|
346
362
|
const body = extractMarkdownSectionBody(researchMarkdown, section);
|
|
@@ -353,10 +369,6 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
353
369
|
.map((line) => line.trim())
|
|
354
370
|
.filter((line) => line.length > 0)
|
|
355
371
|
.filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
|
|
356
|
-
// `<fill-in>` needs its own check because `\b` does not match
|
|
357
|
-
// around `<`/`>` (non-word characters), so the previous combined
|
|
358
|
-
// pattern `\b(?:...|<fill-in>)\b` silently never matched placeholder
|
|
359
|
-
// templates that used angle-bracket form.
|
|
360
372
|
const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
|
|
361
373
|
!/<fill-in>/iu.test(line));
|
|
362
374
|
if (nonPlaceholder.length === 0) {
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type HarnessId } from "./types.js";
|
|
2
2
|
export declare const CCLAW_MARKER_START = "<!-- cclaw-start -->";
|
|
3
3
|
export declare const CCLAW_MARKER_END = "<!-- cclaw-end -->";
|
|
4
4
|
export type SubagentFallback =
|
|
5
|
-
/** Harness has real, isolated subagent dispatch; no fallback needed. */
|
|
5
|
+
/** Harness has real, isolated named subagent dispatch; no fallback needed. */
|
|
6
6
|
"native"
|
|
7
7
|
/**
|
|
8
|
-
* Harness has
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Harness has a real dispatcher but not cclaw-named agents. cclaw maps each
|
|
9
|
+
* named role to the available built-in/generic subagent surface with a
|
|
10
|
+
* structured role prompt.
|
|
11
11
|
*/
|
|
12
12
|
| "generic-dispatch"
|
|
13
13
|
/**
|
|
14
14
|
* No isolated dispatch — the agent performs the named subagent's role
|
|
15
15
|
* in-session with an explicit role announce + delegation-log entry
|
|
16
|
-
* carrying evidenceRefs. Accepted as `completed`
|
|
16
|
+
* carrying evidenceRefs. Accepted as `completed` only when no true dispatch
|
|
17
|
+
* surface exists.
|
|
17
18
|
*/
|
|
18
19
|
| "role-switch"
|
|
19
20
|
/**
|
|
@@ -50,11 +51,11 @@ export interface HarnessAdapter {
|
|
|
50
51
|
capabilities: {
|
|
51
52
|
/**
|
|
52
53
|
* Level of native subagent dispatch:
|
|
53
|
-
* - `full` — isolated workers + user-defined named subagents (Claude
|
|
54
|
-
*
|
|
55
|
-
* - `
|
|
56
|
-
*
|
|
57
|
-
* - `none` — no dispatch primitive at all
|
|
54
|
+
* - `full` — isolated workers + user-defined named subagents (Claude,
|
|
55
|
+
* OpenCode, Codex custom agents).
|
|
56
|
+
* - `generic` — generic dispatcher without cclaw-named agents (Cursor).
|
|
57
|
+
* - `partial` — limited or plugin-only dispatch surface.
|
|
58
|
+
* - `none` — no dispatch primitive at all.
|
|
58
59
|
*/
|
|
59
60
|
nativeSubagentDispatch: "full" | "generic" | "partial" | "none";
|
|
60
61
|
hookSurface: "full" | "plugin" | "limited" | "none";
|
|
@@ -87,6 +88,8 @@ export declare function harnessShimFileNames(): string[];
|
|
|
87
88
|
/** Skill folder names cclaw writes under `<commandDir>` for skill-kind harnesses. */
|
|
88
89
|
export declare function harnessShimSkillNames(): string[];
|
|
89
90
|
export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
|
|
91
|
+
export declare function harnessDispatchSurface(harnessId: HarnessId): string;
|
|
92
|
+
export declare function harnessDispatchFallback(harnessId: HarnessId): string;
|
|
90
93
|
export type HarnessTier = "tier1" | "tier2" | "tier3";
|
|
91
94
|
export declare function harnessTier(harnessId: HarnessId): HarnessTier;
|
|
92
95
|
/**
|