cclaw-cli 0.51.22 → 0.51.24

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/doctor.js CHANGED
@@ -18,11 +18,13 @@ 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";
24
25
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
25
26
  import { validateHookDocument } from "./hook-schema.js";
27
+ import { HOOK_EVENTS_BY_HARNESS } from "./content/hook-events.js";
26
28
  import { validateKnowledgeEntry } from "./knowledge-store.js";
27
29
  import { readSeedShelf } from "./content/seed-shelf.js";
28
30
  import { evaluateRetroGate } from "./retro-gate.js";
@@ -289,17 +291,27 @@ function normalizeOpenCodePluginEntry(entry) {
289
291
  }
290
292
  return null;
291
293
  }
292
- async function opencodeRegistrationCheck(projectRoot) {
293
- const expected = ".opencode/plugins/cclaw-plugin.mjs";
294
- const candidates = [
294
+ const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
295
+ function opencodeConfigCandidates(projectRoot) {
296
+ return [
295
297
  path.join(projectRoot, "opencode.json"),
296
298
  path.join(projectRoot, "opencode.jsonc"),
299
+ path.join(projectRoot, "oh-my-opencode.jsonc"),
300
+ path.join(projectRoot, "oh-my-openagent.jsonc"),
297
301
  path.join(projectRoot, ".opencode/opencode.json"),
298
- path.join(projectRoot, ".opencode/opencode.jsonc")
302
+ path.join(projectRoot, ".opencode/opencode.jsonc"),
303
+ path.join(projectRoot, ".opencode/oh-my-opencode.jsonc"),
304
+ path.join(projectRoot, ".opencode/oh-my-openagent.jsonc")
299
305
  ];
306
+ }
307
+ function openCodeConfigRegistersPlugin(parsed) {
308
+ const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
309
+ return plugins.some((entry) => normalizeOpenCodePluginEntry(entry) === OPENCODE_PLUGIN_REL_PATH);
310
+ }
311
+ async function opencodeRegistrationCheck(projectRoot) {
300
312
  const mismatches = [];
301
313
  let foundAnyConfig = false;
302
- for (const configPath of candidates) {
314
+ for (const configPath of opencodeConfigCandidates(projectRoot)) {
303
315
  if (!(await exists(configPath))) {
304
316
  continue;
305
317
  }
@@ -309,17 +321,130 @@ async function opencodeRegistrationCheck(projectRoot) {
309
321
  mismatches.push(`${path.relative(projectRoot, configPath)} is unreadable or invalid JSON`);
310
322
  continue;
311
323
  }
312
- const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
313
- const registered = plugins.some((entry) => normalizeOpenCodePluginEntry(entry) === expected);
314
- if (registered) {
315
- return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${expected}` };
324
+ if (openCodeConfigRegistersPlugin(parsed)) {
325
+ return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${OPENCODE_PLUGIN_REL_PATH}` };
316
326
  }
317
- mismatches.push(`${path.relative(projectRoot, configPath)} missing plugin ${expected}`);
327
+ mismatches.push(`${path.relative(projectRoot, configPath)} missing plugin ${OPENCODE_PLUGIN_REL_PATH}`);
318
328
  }
319
329
  if (foundAnyConfig) {
320
330
  return { ok: false, details: mismatches.join(" | ") };
321
331
  }
322
- return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
332
+ return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${OPENCODE_PLUGIN_REL_PATH}` };
333
+ }
334
+ async function opencodeQuestionPermissionCheck(projectRoot) {
335
+ const mismatches = [];
336
+ for (const configPath of opencodeConfigCandidates(projectRoot)) {
337
+ if (!(await exists(configPath)))
338
+ continue;
339
+ const parsed = await readHookDocument(configPath);
340
+ if (!parsed || !openCodeConfigRegistersPlugin(parsed))
341
+ continue;
342
+ const permission = toObject(parsed.permission) ?? {};
343
+ if (permission.question === "allow") {
344
+ return {
345
+ ok: true,
346
+ details: `${path.relative(projectRoot, configPath)} sets permission.question to "allow" for structured questions`
347
+ };
348
+ }
349
+ mismatches.push(`${path.relative(projectRoot, configPath)} registers ${OPENCODE_PLUGIN_REL_PATH} but must set permission.question to "allow"`);
350
+ }
351
+ if (mismatches.length > 0) {
352
+ return { ok: false, details: mismatches.join(" | ") };
353
+ }
354
+ return {
355
+ ok: false,
356
+ details: `No opencode config with ${OPENCODE_PLUGIN_REL_PATH} registration found; cannot verify permission.question = "allow"`
357
+ };
358
+ }
359
+ function opencodeQuestionEnvCheck() {
360
+ if (process.env.OPENCODE_ENABLE_QUESTION_TOOL === "1") {
361
+ return { ok: true, details: "OPENCODE_ENABLE_QUESTION_TOOL=1 is set for ACP question tooling" };
362
+ }
363
+ return {
364
+ ok: false,
365
+ details: "Set OPENCODE_ENABLE_QUESTION_TOOL=1 for OpenCode ACP clients so permission-gated structured questions can use the question tool."
366
+ };
367
+ }
368
+ function codexFlagInactiveDetail(configPath, state, error) {
369
+ if (state === "enabled") {
370
+ return `codex_hooks feature flag is enabled in ${configPath}; Codex hooks are active.`;
371
+ }
372
+ if (state === "read-error") {
373
+ return `Codex hooks are inactive: could not read ${configPath} (${error instanceof Error ? error.message : String(error)}).`;
374
+ }
375
+ if (state === "missing-file") {
376
+ return `Codex hooks are inactive: ${configPath} does not exist; .codex/hooks.json is ignored until [features] codex_hooks = true is configured.`;
377
+ }
378
+ if (state === "missing-section") {
379
+ return `Codex hooks are inactive: ${configPath} has no [features] section; add codex_hooks = true to activate configured hooks.`;
380
+ }
381
+ if (state === "missing-key") {
382
+ return `Codex hooks are inactive: ${configPath} is missing codex_hooks under [features]; add codex_hooks = true to activate configured hooks.`;
383
+ }
384
+ return `Codex hooks are inactive: ${configPath} sets codex_hooks to a non-true value; set codex_hooks = true under [features].`;
385
+ }
386
+ function hookCommandsWithMatchers(value) {
387
+ if (!Array.isArray(value)) {
388
+ return [];
389
+ }
390
+ const out = [];
391
+ for (const item of value) {
392
+ const obj = toObject(item);
393
+ if (!obj)
394
+ continue;
395
+ const matcher = typeof obj.matcher === "string" ? obj.matcher : undefined;
396
+ if (typeof obj.command === "string") {
397
+ out.push({ command: obj.command, matcher });
398
+ }
399
+ const nested = hookCommandsWithMatchers(obj.hooks);
400
+ for (const child of nested) {
401
+ out.push({ ...child, matcher: child.matcher ?? matcher });
402
+ }
403
+ }
404
+ return out;
405
+ }
406
+ function commandHasHandler(entries, handler) {
407
+ return entries.some((entry) => entry.command.includes(`run-hook.cmd ${handler}`) || entry.command.includes(`run-hook.mjs ${handler}`));
408
+ }
409
+ function codexBashOnly(entries, handler) {
410
+ const matches = entries.filter((entry) => entry.command.includes(`run-hook.cmd ${handler}`) || entry.command.includes(`run-hook.mjs ${handler}`));
411
+ return matches.length > 0 && matches.every((entry) => entry.matcher === "Bash|bash");
412
+ }
413
+ function codexStructuralWiringCheck(codexHooks) {
414
+ const problems = [];
415
+ const expectedSession = HOOK_EVENTS_BY_HARNESS.codex.session_rehydrate;
416
+ if (expectedSession !== "SessionStart matcher=startup|resume") {
417
+ problems.push("semantic session_rehydrate mapping must remain SessionStart matcher=startup|resume");
418
+ }
419
+ const session = hookCommandsWithMatchers(codexHooks.SessionStart);
420
+ if (!commandHasHandler(session, "session-start") || !session.some((entry) => entry.matcher === "startup|resume")) {
421
+ problems.push("SessionStart must run session-start with matcher startup|resume");
422
+ }
423
+ const userPrompt = hookCommandsWithMatchers(codexHooks.UserPromptSubmit);
424
+ if (!commandHasHandler(userPrompt, "prompt-guard")) {
425
+ problems.push("UserPromptSubmit must run prompt-guard");
426
+ }
427
+ if (!commandHasHandler(userPrompt, "verify-current-state")) {
428
+ problems.push("UserPromptSubmit must run verify-current-state");
429
+ }
430
+ const pre = hookCommandsWithMatchers(codexHooks.PreToolUse);
431
+ if (!codexBashOnly(pre, "prompt-guard")) {
432
+ problems.push("PreToolUse prompt-guard must be Bash-only matcher Bash|bash");
433
+ }
434
+ if (!codexBashOnly(pre, "workflow-guard")) {
435
+ problems.push("PreToolUse workflow-guard must be Bash-only matcher Bash|bash");
436
+ }
437
+ const post = hookCommandsWithMatchers(codexHooks.PostToolUse);
438
+ if (!codexBashOnly(post, "context-monitor")) {
439
+ problems.push("PostToolUse context-monitor must be Bash-only matcher Bash|bash");
440
+ }
441
+ const stop = hookCommandsWithMatchers(codexHooks.Stop);
442
+ if (!commandHasHandler(stop, "stop-handoff")) {
443
+ problems.push("Stop must run stop-handoff");
444
+ }
445
+ return problems.length === 0
446
+ ? { ok: true, details: "Codex hook events, matchers, and manifest semantic mappings are structurally valid" }
447
+ : { ok: false, details: problems.join("; ") };
323
448
  }
324
449
  async function initRecoveryCheck(projectRoot) {
325
450
  const sentinelPath = path.join(projectRoot, RUNTIME_ROOT, "state", ".init-in-progress");
@@ -667,7 +792,6 @@ export async function doctorChecks(projectRoot, options = {}) {
667
792
  ok: agentsBlockOk,
668
793
  details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
669
794
  });
670
- // User-facing entry commands only. Stage and view subcommands live in skills.
671
795
  for (const cmd of ["start", "next", "ideate", "view"]) {
672
796
  const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
673
797
  checks.push({
@@ -676,6 +800,19 @@ export async function doctorChecks(projectRoot, options = {}) {
676
800
  details: cmdPath
677
801
  });
678
802
  }
803
+ for (const stage of FLOW_STAGES) {
804
+ const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${stage}.md`);
805
+ let stageCommandOk = false;
806
+ if (await exists(cmdPath)) {
807
+ const content = await fs.readFile(cmdPath, "utf8");
808
+ stageCommandOk = content === stageCommandShimMarkdown(stage);
809
+ }
810
+ checks.push({
811
+ name: `stage_command:${stage}`,
812
+ ok: stageCommandOk,
813
+ details: `${cmdPath} must be a thin shim to ${RUNTIME_ROOT}/skills/${stageSkillFolder(stage)}/SKILL.md and /cc-next`
814
+ });
815
+ }
679
816
  // Utility skills
680
817
  for (const [folder, label] of [
681
818
  ["learnings", "learnings"],
@@ -942,7 +1079,6 @@ export async function doctorChecks(projectRoot, options = {}) {
942
1079
  const codexStopCmds = collectHookCommands(codexHooks.Stop);
943
1080
  const codexWiringOk = codexSessionCmds.some((cmd) => cmd.includes("session-start")) &&
944
1081
  codexUserPromptCmds.some((cmd) => cmd.includes("prompt-guard")) &&
945
- codexUserPromptCmds.some((cmd) => cmd.includes("workflow-guard")) &&
946
1082
  codexUserPromptCmds.some((cmd) => cmd.includes("verify-current-state")) &&
947
1083
  codexPreCmds.some((cmd) => cmd.includes("prompt-guard")) &&
948
1084
  codexPreCmds.some((cmd) => cmd.includes("workflow-guard")) &&
@@ -951,36 +1087,50 @@ export async function doctorChecks(projectRoot, options = {}) {
951
1087
  checks.push({
952
1088
  name: "hook:wiring:codex",
953
1089
  ok: codexWiringOk,
954
- 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+`
1090
+ 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.`
1091
+ });
1092
+ const codexStructural = codexStructuralWiringCheck(codexHooks);
1093
+ checks.push({
1094
+ name: "hook:wiring:codex:structure",
1095
+ ok: codexStructural.ok,
1096
+ details: codexStructural.details
955
1097
  });
956
- // Feature flag warning: Codex ignores `.codex/hooks.json` unless the
957
- // user has `[features] codex_hooks = true` in `~/.codex/config.toml`.
958
- // Advisory warning — not a hard failure, because the skills still
959
- // work without the flag.
1098
+ // Codex ignores `.codex/hooks.json` unless the user has
1099
+ // `[features] codex_hooks = true` in `~/.codex/config.toml`.
960
1100
  const codexConfig = codexConfigPath();
961
- let featureFlagNote = "";
1101
+ let codexFlagState = "read-error";
1102
+ let codexFlagReadError;
962
1103
  try {
963
1104
  const content = await readCodexConfig(codexConfig);
964
- const state = classifyCodexHooksFlag(content);
965
- featureFlagNote =
966
- state === "enabled"
967
- ? `codex_hooks feature flag is enabled in ${codexConfig}`
968
- : state === "missing-file"
969
- ? `warning: ${codexConfig} does not exist; .codex/hooks.json will be ignored until you create it with \`[features]\\ncodex_hooks = true\\n\`.`
970
- : state === "missing-section"
971
- ? `warning: ${codexConfig} has no [features] section; add \`[features]\\ncodex_hooks = true\\n\` to enable cclaw hooks.`
972
- : state === "missing-key"
973
- ? `warning: ${codexConfig} is missing the codex_hooks key under [features]. Add \`codex_hooks = true\` to enable cclaw hooks.`
974
- : `warning: ${codexConfig} sets codex_hooks to a non-true value; set \`codex_hooks = true\` under [features] to enable cclaw hooks.`;
1105
+ codexFlagState = classifyCodexHooksFlag(content);
975
1106
  }
976
1107
  catch (err) {
977
- featureFlagNote = `warning: could not read ${codexConfig}: ${err instanceof Error ? err.message : String(err)}`;
1108
+ codexFlagReadError = err;
978
1109
  }
1110
+ const featureFlagNote = codexFlagInactiveDetail(codexConfig, codexFlagState, codexFlagReadError);
1111
+ const featureFlagOk = codexFlagState === "enabled";
979
1112
  checks.push({
980
1113
  name: "warning:codex:feature_flag",
981
- ok: true,
982
- details: featureFlagNote
1114
+ ok: featureFlagOk,
1115
+ details: featureFlagNote,
1116
+ summary: featureFlagOk
1117
+ ? "Codex hooks are active."
1118
+ : "Codex hooks are inactive; configured hooks will be ignored.",
1119
+ fix: "Set `[features] codex_hooks = true` in the Codex config or run cclaw init/sync with Codex flag repair.",
1120
+ docRef: "docs/harnesses.md"
983
1121
  });
1122
+ if (parsedConfig?.strictness === "strict") {
1123
+ checks.push({
1124
+ name: "hook:codex:feature_flag_active",
1125
+ ok: featureFlagOk,
1126
+ details: featureFlagNote,
1127
+ summary: featureFlagOk
1128
+ ? "Codex hooks are active for strict runtime enforcement."
1129
+ : "Codex hooks are inactive; strict Codex hook enforcement is not ready.",
1130
+ fix: "Set `[features] codex_hooks = true` in the Codex config so strict Codex hooks can run.",
1131
+ docRef: "docs/harnesses.md"
1132
+ });
1133
+ }
984
1134
  // Legacy `.codex/commands/*` must not linger from older cclaw installs.
985
1135
  // (The `.codex/hooks.json` path is now managed and is validated above,
986
1136
  // so there is no longer a legacy_hooks_json warning.)
@@ -1074,6 +1224,18 @@ export async function doctorChecks(projectRoot, options = {}) {
1074
1224
  ok: registration.ok,
1075
1225
  details: registration.details
1076
1226
  });
1227
+ const questionPermission = await opencodeQuestionPermissionCheck(projectRoot);
1228
+ checks.push({
1229
+ name: "hook:opencode:question_permission",
1230
+ ok: questionPermission.ok,
1231
+ details: questionPermission.details
1232
+ });
1233
+ const questionEnv = opencodeQuestionEnvCheck();
1234
+ checks.push({
1235
+ name: "warning:opencode:question_tool_env",
1236
+ ok: questionEnv.ok,
1237
+ details: questionEnv.details
1238
+ });
1077
1239
  }
1078
1240
  const nodeVersion = await commandVersion("node");
1079
1241
  const nodeMajor = parseNodeMajor(nodeVersion.output);
@@ -63,6 +63,7 @@ export interface CloseoutState {
63
63
  retroSkipReason?: string;
64
64
  compoundCompletedAt?: string;
65
65
  compoundSkipped?: boolean;
66
+ compoundSkipReason?: string;
66
67
  compoundPromoted: number;
67
68
  }
68
69
  export declare function createInitialCloseoutState(): CloseoutState;
@@ -42,6 +42,7 @@ export function createInitialCloseoutState() {
42
42
  retroSkipReason: undefined,
43
43
  compoundCompletedAt: undefined,
44
44
  compoundSkipped: undefined,
45
+ compoundSkipReason: undefined,
45
46
  compoundPromoted: 0
46
47
  };
47
48
  }
@@ -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
  /**
@@ -1,10 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { RUNTIME_ROOT } from "./constants.js";
3
+ import { RUNTIME_ROOT, STAGE_TO_SKILL_FOLDER } from "./constants.js";
4
4
  import { conversationLanguagePolicyMarkdown } from "./content/language-policy.js";
5
5
  import { CCLAW_AGENTS, agentMarkdown } from "./content/core-agents.js";
6
6
  import { ironLawsAgentsMdBlock } from "./content/iron-laws.js";
7
7
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
8
+ import { FLOW_STAGES } from "./types.js";
8
9
  export const CCLAW_MARKER_START = "<!-- cclaw-start -->";
9
10
  export const CCLAW_MARKER_END = "<!-- cclaw-end -->";
10
11
  function escapeRegExp(value) {
@@ -46,12 +47,26 @@ const LEGACY_CODEX_SKILL_PREFIX = "cclaw-cc";
46
47
  * harness command directories so `/cc-learn` etc. do not linger.
47
48
  */
48
49
  const LEGACY_HARNESS_SHIMS = ["cc-learn.md"];
50
+ function stageShimFileName(stage) {
51
+ return `cc-${stage}.md`;
52
+ }
53
+ function stageShimSkillName(stage) {
54
+ return `cc-${stage}`;
55
+ }
49
56
  export function harnessShimFileNames() {
50
- return ["cc.md", ...UTILITY_SHIMS.map((shim) => shim.fileName)];
57
+ return [
58
+ "cc.md",
59
+ ...UTILITY_SHIMS.map((shim) => shim.fileName),
60
+ ...FLOW_STAGES.map((stage) => stageShimFileName(stage))
61
+ ];
51
62
  }
52
63
  /** Skill folder names cclaw writes under `<commandDir>` for skill-kind harnesses. */
53
64
  export function harnessShimSkillNames() {
54
- return [ENTRY_SHIM_SKILL_NAME, ...UTILITY_SHIMS.map((shim) => shim.skillName)];
65
+ return [
66
+ ENTRY_SHIM_SKILL_NAME,
67
+ ...UTILITY_SHIMS.map((shim) => shim.skillName),
68
+ ...FLOW_STAGES.map((stage) => stageShimSkillName(stage))
69
+ ];
55
70
  }
56
71
  export const HARNESS_ADAPTERS = {
57
72
  claude: {
@@ -85,7 +100,11 @@ export const HARNESS_ADAPTERS = {
85
100
  commandDir: ".opencode/commands",
86
101
  shimKind: "command",
87
102
  capabilities: {
88
- nativeSubagentDispatch: "partial",
103
+ // OpenCode supports project-local markdown subagents under
104
+ // `.opencode/agents/`; primary agents can invoke them via the Task
105
+ // tool or explicit `@agent` mention. cclaw materializes its core
106
+ // roster there, so mandatory delegations are real isolated subagents.
107
+ nativeSubagentDispatch: "full",
89
108
  hookSurface: "plugin",
90
109
  // OpenCode exposes a native `question` tool (header + options +
91
110
  // custom-answer fallback, multi-question navigation). It is
@@ -95,7 +114,7 @@ export const HARNESS_ADAPTERS = {
95
114
  // in generated harness guidance; skills fall back to the shared
96
115
  // plain-text lettered list when the tool is denied or unavailable.
97
116
  structuredAsk: "question",
98
- subagentFallback: "role-switch"
117
+ subagentFallback: "native"
99
118
  }
100
119
  },
101
120
  codex: {
@@ -103,8 +122,10 @@ export const HARNESS_ADAPTERS = {
103
122
  // Codex CLI reads skills from the universal `.agents/skills/` path
104
123
  // (OpenAI Codex 0.89, Jan 2026). It does NOT have a native
105
124
  // `.codex/commands/*` slash-command discovery — cclaw installs
106
- // its entry points as skills here. Since v0.114 (Mar 2026) Codex
107
- // also exposes lifecycle hooks via `.codex/hooks.json`, behind
125
+ // its entry points as skills here. Current Codex releases also support
126
+ // native parallel subagents and project-local `.codex/agents/*.toml`
127
+ // custom agents; cclaw materializes its core roster there. Since v0.114
128
+ // (Mar 2026) Codex also exposes lifecycle hooks via `.codex/hooks.json`, behind
108
129
  // the `[features] codex_hooks = true` feature flag in
109
130
  // `~/.codex/config.toml`. cclaw writes that file on sync and
110
131
  // `hookSurface: "limited"` records the reality: SessionStart /
@@ -113,7 +134,7 @@ export const HARNESS_ADAPTERS = {
113
134
  commandDir: ".agents/skills",
114
135
  shimKind: "skill",
115
136
  capabilities: {
116
- nativeSubagentDispatch: "none",
137
+ nativeSubagentDispatch: "full",
117
138
  hookSurface: "limited",
118
139
  // Codex CLI exposes `request_user_input` — an experimental tool
119
140
  // that asks 1-3 short questions and returns the user's answers.
@@ -123,10 +144,29 @@ export const HARNESS_ADAPTERS = {
123
144
  // it into generated harness guidance. The shared plain-text
124
145
  // lettered list is the documented fallback when the tool is unavailable.
125
146
  structuredAsk: "request_user_input",
126
- subagentFallback: "role-switch"
147
+ subagentFallback: "native"
127
148
  }
128
149
  }
129
150
  };
151
+ export function harnessDispatchSurface(harnessId) {
152
+ switch (harnessId) {
153
+ case "claude":
154
+ return "Use Claude Code Task with the cclaw agent name as subagent_type; record fulfillmentMode: \"isolated\".";
155
+ case "cursor":
156
+ return "Use Cursor Subagent/Task with a generic subagent_type (explore for read-only mapping, generalPurpose for broader work, shell/browser-use when specifically needed) and paste the cclaw role prompt; record fulfillmentMode: \"generic-dispatch\" with evidenceRefs.";
157
+ case "opencode":
158
+ return "Use OpenCode subagents: invoke the generated .opencode/agents/<agent>.md agent via Task or @<agent>, run independent agents in parallel when safe, then record fulfillmentMode: \"isolated\".";
159
+ case "codex":
160
+ return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name, wait for all results, then record fulfillmentMode: \"isolated\".";
161
+ }
162
+ }
163
+ export function harnessDispatchFallback(harnessId) {
164
+ const adapter = HARNESS_ADAPTERS[harnessId];
165
+ if (adapter.capabilities.subagentFallback !== "role-switch") {
166
+ return "Role-switch is only a degradation path if the active runtime cannot expose the declared dispatch surface; include non-empty evidenceRefs when used.";
167
+ }
168
+ return "Use a visible role-switch pass with non-empty evidenceRefs because this harness has no true dispatch surface.";
169
+ }
130
170
  export function harnessTier(harnessId) {
131
171
  const capabilities = HARNESS_ADAPTERS[harnessId].capabilities;
132
172
  if (capabilities.nativeSubagentDispatch === "full" &&
@@ -223,7 +263,7 @@ If the same approach fails three times in a row (same command, same finding, sam
223
263
  ### Detail Level
224
264
 
225
265
  - This managed AGENTS block is intentionally minimal for cross-project use.
226
- - Harness coverage is tiered: Tier1 (claude), Tier2 (cursor/opencode/codex codex has Bash-only tool hooks), Tier3 (fallback/manual-only).
266
+ - Subagent dispatch coverage: Claude/OpenCode/Codex support native isolated workers; Cursor uses generic Task dispatch. Codex still has Bash-only tool hooks.
227
267
  - Detailed operating procedures live in \`.cclaw/skills/using-cclaw/SKILL.md\`.
228
268
  - Keep preambles brief; re-announce role/stage only when either changes.
229
269
  - Subagent orchestration patterns: \`.cclaw/skills/subagent-dev/SKILL.md\` and \`.cclaw/skills/parallel-dispatch/SKILL.md\`.
@@ -336,6 +376,50 @@ Load and execute:
336
376
  ${utilityShimBehavior(command)}
337
377
  `;
338
378
  }
379
+ function stageShimContent(harness, stage) {
380
+ const shimName = stageShimSkillName(stage);
381
+ const skillPath = `${RUNTIME_ROOT}/skills/${STAGE_TO_SKILL_FOLDER[stage]}/SKILL.md`;
382
+ return `---
383
+ name: ${shimName}
384
+ description: Generated shim for ${harness}. Flow stage pointer; normal advancement uses /cc-next.
385
+ source: generated-by-cclaw
386
+ ---
387
+
388
+ # cclaw ${stage}
389
+
390
+ This is a thin compatibility shim for the \`${stage}\` flow stage.
391
+
392
+ Load and follow the authoritative stage skill:
393
+
394
+ - \`${skillPath}\`
395
+
396
+ Normal stage resume and advancement uses \`/cc-next\`. Use \`/cc-next\` to read
397
+ \`.cclaw/state/flow-state.json\`, select the active stage, and advance only after
398
+ that stage's gates pass. Do not duplicate the stage protocol here.
399
+ `;
400
+ }
401
+ function codexStageSkillMarkdown(stage) {
402
+ const skillName = stageShimSkillName(stage);
403
+ const skillPath = `${RUNTIME_ROOT}/skills/${STAGE_TO_SKILL_FOLDER[stage]}/SKILL.md`;
404
+ return `---
405
+ name: ${skillName}
406
+ description: Thin cclaw stage shim for /cc-${stage}. Load ${skillPath}; normal stage resume and advancement uses /cc-next.
407
+ source: generated-by-cclaw
408
+ ---
409
+
410
+ # cclaw /cc-${stage} (Codex adapter)
411
+
412
+ This is a thin compatibility shim for the \`${stage}\` flow stage.
413
+
414
+ Load and follow the authoritative stage skill:
415
+
416
+ - \`${skillPath}\`
417
+
418
+ Normal stage resume and advancement uses \`/cc-next\`. Use \`/cc-next\` to read
419
+ \`.cclaw/state/flow-state.json\`, select the active stage, and advance only after
420
+ that stage's gates pass. Do not duplicate the stage protocol here.
421
+ `;
422
+ }
339
423
  /**
340
424
  * Frontmatter `description` that triggers the skill when the user types any
341
425
  * of the classic cclaw slash-tokens. Codex's skill matcher runs on the skill
@@ -398,11 +482,12 @@ for the current hook surface and limitations.
398
482
 
399
483
  ## Honest caveats
400
484
 
401
- - Codex has no subagent dispatch primitive. Mandatory delegations
402
- fall back to **role-switch** announce the role, act in-session,
403
- append a completed row with \`evidenceRefs\` to
404
- \`.cclaw/state/delegation-log.json\`. Silent auto-waiver is disabled
405
- (v0.33+).
485
+ - Codex has native parallel subagents. cclaw writes project custom agents
486
+ under \`.codex/agents/*.toml\`; ask Codex to spawn the relevant cclaw
487
+ agent(s) by name, wait for their results, write evidence into the active
488
+ artifact, then append completed delegation rows with \`fulfillmentMode:
489
+ "isolated"\`. Use role-switch only if this Codex build has subagents
490
+ unavailable or disabled, and then include non-empty \`evidenceRefs\`.
406
491
  - Codex's \`PreToolUse\` / \`PostToolUse\` hooks currently only intercept
407
492
  the \`Bash\` tool. \`Write\`, \`Edit\`, \`WebSearch\`, and MCP tool calls
408
493
  are **not** gated by hooks — use \`cclaw doctor --explain\` for what cclaw
@@ -432,6 +517,9 @@ async function writeCommandKindShims(commandDir, harness) {
432
517
  for (const shim of UTILITY_SHIMS) {
433
518
  await writeFileSafe(path.join(commandDir, shim.fileName), utilityShimContent(harness, shim.command, shim.skillFolder, shim.commandFile));
434
519
  }
520
+ for (const stage of FLOW_STAGES) {
521
+ await writeFileSafe(path.join(commandDir, stageShimFileName(stage)), stageShimContent(harness, stage));
522
+ }
435
523
  for (const legacy of LEGACY_HARNESS_SHIMS) {
436
524
  const legacyPath = path.join(commandDir, legacy);
437
525
  try {
@@ -448,6 +536,9 @@ async function writeSkillKindShims(commandDir) {
448
536
  for (const shim of UTILITY_SHIMS) {
449
537
  await writeFileSafe(path.join(commandDir, shim.skillName, "SKILL.md"), codexSkillMarkdown(shim.command, shim.skillName, shim.skillFolder, shim.commandFile));
450
538
  }
539
+ for (const stage of FLOW_STAGES) {
540
+ await writeFileSafe(path.join(commandDir, stageShimSkillName(stage), "SKILL.md"), codexStageSkillMarkdown(stage));
541
+ }
451
542
  }
452
543
  /**
453
544
  * Legacy codex surfaces cclaw wrote before v0.39.0 that Codex CLI never
@@ -505,12 +596,57 @@ async function cleanupLegacyCodexSurfaces(projectRoot) {
505
596
  // directory absent or non-empty
506
597
  }
507
598
  }
508
- async function syncAgentFiles(projectRoot) {
599
+ function codexAgentToml(agent) {
600
+ const instructions = `${agent.body}\n\n${enhancedAgentInstruction(agent.name)}`.trim();
601
+ const sandboxMode = agent.tools.some((tool) => ["Write", "Edit", "Bash"].includes(tool))
602
+ ? "workspace-write"
603
+ : "read-only";
604
+ return [
605
+ `name = ${JSON.stringify(agent.name)}`,
606
+ `description = ${JSON.stringify(agent.description)}`,
607
+ `sandbox_mode = ${JSON.stringify(sandboxMode)}`,
608
+ 'developer_instructions = """',
609
+ instructions.replace(/"""/gu, '\"\"\"'),
610
+ '"""',
611
+ ""
612
+ ].join("\n");
613
+ }
614
+ function opencodeAgentMarkdown(agent) {
615
+ const editPermission = agent.tools.some((tool) => ["Write", "Edit"].includes(tool)) ? "ask" : "deny";
616
+ const bashPermission = agent.tools.includes("Bash") ? "ask" : "deny";
617
+ return `---
618
+ description: ${JSON.stringify(agent.description)}
619
+ mode: subagent
620
+ permission:
621
+ edit: ${editPermission}
622
+ bash: ${bashPermission}
623
+ ---
624
+
625
+ ${agentMarkdown(agent)}`;
626
+ }
627
+ function enhancedAgentInstruction(agentName) {
628
+ return `You are the cclaw ${agentName} subagent. Follow the parent prompt as the task boundary, produce evidence suitable for .cclaw/state/delegation-log.json, and do not recursively orchestrate other agents unless the parent explicitly asks.`;
629
+ }
630
+ async function syncAgentFiles(projectRoot, harnesses) {
509
631
  const agentsDir = path.join(projectRoot, RUNTIME_ROOT, "agents");
510
632
  await ensureDir(agentsDir);
511
633
  for (const agent of CCLAW_AGENTS) {
512
634
  await writeFileSafe(path.join(agentsDir, `${agent.name}.md`), agentMarkdown(agent));
513
635
  }
636
+ if (harnesses.includes("opencode")) {
637
+ const opencodeAgentsDir = path.join(projectRoot, ".opencode/agents");
638
+ await ensureDir(opencodeAgentsDir);
639
+ for (const agent of CCLAW_AGENTS) {
640
+ await writeFileSafe(path.join(opencodeAgentsDir, `${agent.name}.md`), opencodeAgentMarkdown(agent));
641
+ }
642
+ }
643
+ if (harnesses.includes("codex")) {
644
+ const codexAgentsDir = path.join(projectRoot, ".codex/agents");
645
+ await ensureDir(codexAgentsDir);
646
+ for (const agent of CCLAW_AGENTS) {
647
+ await writeFileSafe(path.join(codexAgentsDir, `${agent.name}.toml`), codexAgentToml(agent));
648
+ }
649
+ }
514
650
  }
515
651
  export async function syncHarnessShims(projectRoot, harnesses) {
516
652
  // Legacy codex cleanup is unconditional — even installs that never enabled
@@ -529,6 +665,6 @@ export async function syncHarnessShims(projectRoot, harnesses) {
529
665
  await writeCommandKindShims(commandDir, harness);
530
666
  }
531
667
  }
532
- await syncAgentFiles(projectRoot);
668
+ await syncAgentFiles(projectRoot, harnesses);
533
669
  await syncAgentsMd(projectRoot, harnesses);
534
670
  }