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/README.md +14 -13
- package/dist/content/core-agents.d.ts +18 -2
- package/dist/content/core-agents.js +59 -13
- package/dist/content/examples.js +15 -7
- package/dist/content/hook-manifest.js +1 -4
- package/dist/content/learnings.js +5 -2
- package/dist/content/meta-skill.d.ts +1 -0
- package/dist/content/meta-skill.js +10 -1
- package/dist/content/node-hooks.js +1 -1
- package/dist/content/seed-shelf.js +73 -8
- package/dist/content/skills.js +14 -10
- package/dist/content/stage-command.d.ts +2 -0
- package/dist/content/stage-command.js +17 -0
- package/dist/content/stage-schema.js +50 -6
- package/dist/content/stages/brainstorm.js +20 -15
- package/dist/content/stages/design.js +16 -16
- package/dist/content/stages/review.js +20 -11
- package/dist/content/stages/schema-types.d.ts +1 -1
- package/dist/content/stages/scope.js +16 -11
- package/dist/content/stages/tdd.js +10 -3
- package/dist/content/subagents.js +73 -7
- package/dist/content/templates.js +127 -31
- package/dist/content/track-render-context.js +7 -0
- package/dist/delegation.d.ts +2 -2
- package/dist/delegation.js +16 -9
- package/dist/doctor-registry.js +1 -1
- package/dist/doctor.js +195 -33
- package/dist/flow-state.d.ts +1 -0
- package/dist/flow-state.js +1 -0
- package/dist/harness-adapters.d.ts +14 -11
- package/dist/harness-adapters.js +153 -17
- package/dist/install.js +101 -5
- package/dist/knowledge-store.js +30 -6
- package/dist/run-archive.js +11 -0
- package/dist/run-persistence.js +14 -7
- package/package.json +1 -1
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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
|
-
|
|
313
|
-
|
|
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 ${
|
|
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 ${
|
|
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/
|
|
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
|
-
//
|
|
957
|
-
//
|
|
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
|
|
1101
|
+
let codexFlagState = "read-error";
|
|
1102
|
+
let codexFlagReadError;
|
|
962
1103
|
try {
|
|
963
1104
|
const content = await readCodexConfig(codexConfig);
|
|
964
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -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;
|
package/dist/flow-state.js
CHANGED
|
@@ -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
|
/**
|
package/dist/harness-adapters.js
CHANGED
|
@@ -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 [
|
|
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 [
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
107
|
-
//
|
|
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: "
|
|
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: "
|
|
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
|
-
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
}
|