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.
Files changed (48) hide show
  1. package/README.md +14 -13
  2. package/dist/config.d.ts +8 -1
  3. package/dist/config.js +9 -6
  4. package/dist/content/examples.js +2 -2
  5. package/dist/content/hook-manifest.d.ts +2 -4
  6. package/dist/content/hook-manifest.js +5 -7
  7. package/dist/content/learnings.js +5 -2
  8. package/dist/content/meta-skill.d.ts +1 -0
  9. package/dist/content/meta-skill.js +16 -9
  10. package/dist/content/next-command.js +2 -2
  11. package/dist/content/node-hooks.js +14 -4
  12. package/dist/content/review-loop.js +15 -5
  13. package/dist/content/review-prompts.js +1 -1
  14. package/dist/content/skills.js +16 -11
  15. package/dist/content/stage-command.d.ts +2 -0
  16. package/dist/content/stage-command.js +17 -0
  17. package/dist/content/stage-schema.js +1 -0
  18. package/dist/content/stages/brainstorm.js +3 -3
  19. package/dist/content/stages/design.js +18 -17
  20. package/dist/content/stages/plan.js +2 -1
  21. package/dist/content/stages/review.js +15 -15
  22. package/dist/content/stages/scope.js +14 -14
  23. package/dist/content/stages/spec.js +7 -5
  24. package/dist/content/stages/tdd.js +11 -4
  25. package/dist/content/start-command.d.ts +4 -3
  26. package/dist/content/start-command.js +21 -17
  27. package/dist/content/subagents.js +14 -4
  28. package/dist/content/templates.d.ts +1 -1
  29. package/dist/content/templates.js +49 -29
  30. package/dist/content/track-render-context.js +7 -0
  31. package/dist/content/view-command.js +3 -1
  32. package/dist/delegation.d.ts +2 -2
  33. package/dist/delegation.js +40 -13
  34. package/dist/doctor-registry.js +1 -1
  35. package/dist/doctor.js +222 -34
  36. package/dist/gate-evidence.js +19 -7
  37. package/dist/harness-adapters.d.ts +14 -11
  38. package/dist/harness-adapters.js +154 -22
  39. package/dist/install.js +116 -28
  40. package/dist/internal/advance-stage.js +90 -11
  41. package/dist/knowledge-store.d.ts +4 -1
  42. package/dist/knowledge-store.js +24 -14
  43. package/dist/retro-gate.d.ts +1 -0
  44. package/dist/retro-gate.js +9 -9
  45. package/dist/run-archive.js +19 -1
  46. package/dist/run-persistence.js +6 -2
  47. package/dist/tdd-cycle.js +6 -3
  48. 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, ["--version"]);
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
- async function opencodeRegistrationCheck(projectRoot) {
280
- const expected = ".opencode/plugins/cclaw-plugin.mjs";
281
- const candidates = [
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 candidates) {
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
- const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
300
- const registered = plugins.some((entry) => normalizeOpenCodePluginEntry(entry) === expected);
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 ${expected}`);
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 ${expected}` };
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/workflow/verify-current-state), PreToolUse(prompt/workflow), PostToolUse(context-monitor), and Stop(stop-handoff). PreToolUse/PostToolUse run Bash-only in Codex v0.114+`
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 hasNode = await commandAvailable("node");
1140
+ const nodeVersion = await commandVersion("node");
1141
+ const nodeMajor = parseNodeMajor(nodeVersion.output);
985
1142
  checks.push({
986
1143
  name: "capability:required:node",
987
- ok: hasNode,
988
- details: "node is required for cclaw runtime scripts and CLI wiring"
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 retroRequired = flowState.completedStages.includes("ship");
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: retroComplete,
1397
- details: retroComplete
1398
- ? retroRequired
1399
- ? `retro gate complete (${flowState.retro.compoundEntries} compound entries)`
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
  });
@@ -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): missing `.cclaw/artifacts/02a-research.md`.");
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 { HarnessId } from "./types.js";
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 generic dispatch (e.g. Cursor's Task tool with
9
- * `subagent_type`) but not user-defined named subagents; cclaw maps each
10
- * named agent to the generic dispatcher with a structured role prompt.
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` in delegation checks.
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
- * - `generic` — generic dispatcher (Task) without named agents (Cursor).
55
- * - `partial` — plugin-based dispatch, not a first-class primitive
56
- * (OpenCode).
57
- * - `none` — no dispatch primitive at all (Codex).
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
  /**