cclaw-cli 0.51.21 → 0.51.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/config.d.ts +8 -1
- package/dist/config.js +9 -6
- package/dist/content/examples.js +2 -2
- package/dist/content/hook-manifest.d.ts +2 -4
- package/dist/content/hook-manifest.js +5 -7
- package/dist/content/learnings.js +5 -2
- package/dist/content/meta-skill.d.ts +1 -0
- package/dist/content/meta-skill.js +16 -9
- package/dist/content/next-command.js +2 -2
- package/dist/content/node-hooks.js +14 -4
- package/dist/content/review-loop.js +15 -5
- package/dist/content/review-prompts.js +1 -1
- package/dist/content/skills.js +16 -11
- package/dist/content/stage-command.d.ts +2 -0
- package/dist/content/stage-command.js +17 -0
- package/dist/content/stage-schema.js +1 -0
- package/dist/content/stages/brainstorm.js +3 -3
- package/dist/content/stages/design.js +18 -17
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/review.js +15 -15
- package/dist/content/stages/scope.js +14 -14
- package/dist/content/stages/spec.js +7 -5
- package/dist/content/stages/tdd.js +11 -4
- package/dist/content/start-command.d.ts +4 -3
- package/dist/content/start-command.js +21 -17
- package/dist/content/subagents.js +14 -4
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +49 -29
- package/dist/content/track-render-context.js +7 -0
- package/dist/content/view-command.js +3 -1
- package/dist/delegation.d.ts +2 -2
- package/dist/delegation.js +40 -13
- package/dist/doctor-registry.js +1 -1
- package/dist/doctor.js +222 -34
- package/dist/gate-evidence.js +19 -7
- package/dist/harness-adapters.d.ts +14 -11
- package/dist/harness-adapters.js +154 -22
- package/dist/install.js +116 -28
- package/dist/internal/advance-stage.js +90 -11
- package/dist/knowledge-store.d.ts +4 -1
- package/dist/knowledge-store.js +24 -14
- package/dist/retro-gate.d.ts +1 -0
- package/dist/retro-gate.js +9 -9
- package/dist/run-archive.js +19 -1
- package/dist/run-persistence.js +6 -2
- package/dist/tdd-cycle.js +6 -3
- package/package.json +1 -1
package/dist/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" &&
|
|
@@ -134,11 +174,7 @@ export function harnessTier(harnessId) {
|
|
|
134
174
|
capabilities.hookSurface === "full") {
|
|
135
175
|
return "tier1";
|
|
136
176
|
}
|
|
137
|
-
if (capabilities.hookSurface
|
|
138
|
-
capabilities.hookSurface === "plugin" ||
|
|
139
|
-
capabilities.hookSurface === "limited" ||
|
|
140
|
-
capabilities.nativeSubagentDispatch === "generic" ||
|
|
141
|
-
capabilities.nativeSubagentDispatch === "partial") {
|
|
177
|
+
if (capabilities.hookSurface !== "none" || capabilities.nativeSubagentDispatch !== "none") {
|
|
142
178
|
return "tier2";
|
|
143
179
|
}
|
|
144
180
|
return "tier3";
|
|
@@ -227,7 +263,7 @@ If the same approach fails three times in a row (same command, same finding, sam
|
|
|
227
263
|
### Detail Level
|
|
228
264
|
|
|
229
265
|
- This managed AGENTS block is intentionally minimal for cross-project use.
|
|
230
|
-
-
|
|
266
|
+
- Subagent dispatch coverage: Claude/OpenCode/Codex support native isolated workers; Cursor uses generic Task dispatch. Codex still has Bash-only tool hooks.
|
|
231
267
|
- Detailed operating procedures live in \`.cclaw/skills/using-cclaw/SKILL.md\`.
|
|
232
268
|
- Keep preambles brief; re-announce role/stage only when either changes.
|
|
233
269
|
- Subagent orchestration patterns: \`.cclaw/skills/subagent-dev/SKILL.md\` and \`.cclaw/skills/parallel-dispatch/SKILL.md\`.
|
|
@@ -340,6 +376,50 @@ Load and execute:
|
|
|
340
376
|
${utilityShimBehavior(command)}
|
|
341
377
|
`;
|
|
342
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
|
+
}
|
|
343
423
|
/**
|
|
344
424
|
* Frontmatter `description` that triggers the skill when the user types any
|
|
345
425
|
* of the classic cclaw slash-tokens. Codex's skill matcher runs on the skill
|
|
@@ -402,11 +482,12 @@ for the current hook surface and limitations.
|
|
|
402
482
|
|
|
403
483
|
## Honest caveats
|
|
404
484
|
|
|
405
|
-
- Codex has
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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\`.
|
|
410
491
|
- Codex's \`PreToolUse\` / \`PostToolUse\` hooks currently only intercept
|
|
411
492
|
the \`Bash\` tool. \`Write\`, \`Edit\`, \`WebSearch\`, and MCP tool calls
|
|
412
493
|
are **not** gated by hooks — use \`cclaw doctor --explain\` for what cclaw
|
|
@@ -436,6 +517,9 @@ async function writeCommandKindShims(commandDir, harness) {
|
|
|
436
517
|
for (const shim of UTILITY_SHIMS) {
|
|
437
518
|
await writeFileSafe(path.join(commandDir, shim.fileName), utilityShimContent(harness, shim.command, shim.skillFolder, shim.commandFile));
|
|
438
519
|
}
|
|
520
|
+
for (const stage of FLOW_STAGES) {
|
|
521
|
+
await writeFileSafe(path.join(commandDir, stageShimFileName(stage)), stageShimContent(harness, stage));
|
|
522
|
+
}
|
|
439
523
|
for (const legacy of LEGACY_HARNESS_SHIMS) {
|
|
440
524
|
const legacyPath = path.join(commandDir, legacy);
|
|
441
525
|
try {
|
|
@@ -452,6 +536,9 @@ async function writeSkillKindShims(commandDir) {
|
|
|
452
536
|
for (const shim of UTILITY_SHIMS) {
|
|
453
537
|
await writeFileSafe(path.join(commandDir, shim.skillName, "SKILL.md"), codexSkillMarkdown(shim.command, shim.skillName, shim.skillFolder, shim.commandFile));
|
|
454
538
|
}
|
|
539
|
+
for (const stage of FLOW_STAGES) {
|
|
540
|
+
await writeFileSafe(path.join(commandDir, stageShimSkillName(stage), "SKILL.md"), codexStageSkillMarkdown(stage));
|
|
541
|
+
}
|
|
455
542
|
}
|
|
456
543
|
/**
|
|
457
544
|
* Legacy codex surfaces cclaw wrote before v0.39.0 that Codex CLI never
|
|
@@ -509,12 +596,57 @@ async function cleanupLegacyCodexSurfaces(projectRoot) {
|
|
|
509
596
|
// directory absent or non-empty
|
|
510
597
|
}
|
|
511
598
|
}
|
|
512
|
-
|
|
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) {
|
|
513
631
|
const agentsDir = path.join(projectRoot, RUNTIME_ROOT, "agents");
|
|
514
632
|
await ensureDir(agentsDir);
|
|
515
633
|
for (const agent of CCLAW_AGENTS) {
|
|
516
634
|
await writeFileSafe(path.join(agentsDir, `${agent.name}.md`), agentMarkdown(agent));
|
|
517
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
|
+
}
|
|
518
650
|
}
|
|
519
651
|
export async function syncHarnessShims(projectRoot, harnesses) {
|
|
520
652
|
// Legacy codex cleanup is unconditional — even installs that never enabled
|
|
@@ -533,6 +665,6 @@ export async function syncHarnessShims(projectRoot, harnesses) {
|
|
|
533
665
|
await writeCommandKindShims(commandDir, harness);
|
|
534
666
|
}
|
|
535
667
|
}
|
|
536
|
-
await syncAgentFiles(projectRoot);
|
|
668
|
+
await syncAgentFiles(projectRoot, harnesses);
|
|
537
669
|
await syncAgentsMd(projectRoot, harnesses);
|
|
538
670
|
}
|
package/dist/install.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CCLAW_VERSION, FLOW_VERSION, REQUIRED_DIRS, RUNTIME_ROOT } from "./cons
|
|
|
6
6
|
import { writeConfig, createDefaultConfig, readConfig, configPath, detectLanguageRulePacks, detectAdvancedKeys } from "./config.js";
|
|
7
7
|
import { learnSkillMarkdown } from "./content/learnings.js";
|
|
8
8
|
import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
|
|
9
|
+
import { stageCommandShimMarkdown } from "./content/stage-command.js";
|
|
9
10
|
import { ideateCommandContract, ideateCommandSkillMarkdown } from "./content/ideate-command.js";
|
|
10
11
|
import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
|
|
11
12
|
import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-command.js";
|
|
@@ -34,10 +35,17 @@ const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
|
34
35
|
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
35
36
|
const GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
|
|
36
37
|
const GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
|
|
38
|
+
const INIT_SENTINEL_FILE = ".init-in-progress";
|
|
37
39
|
const execFileAsync = promisify(execFile);
|
|
38
40
|
function runtimePath(projectRoot, ...segments) {
|
|
39
41
|
return path.join(projectRoot, RUNTIME_ROOT, ...segments);
|
|
40
42
|
}
|
|
43
|
+
async function writeInitSentinel(projectRoot, operation) {
|
|
44
|
+
const sentinelPath = runtimePath(projectRoot, "state", INIT_SENTINEL_FILE);
|
|
45
|
+
await ensureDir(path.dirname(sentinelPath));
|
|
46
|
+
await writeFileSafe(sentinelPath, `${JSON.stringify({ operation, startedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
47
|
+
return sentinelPath;
|
|
48
|
+
}
|
|
41
49
|
async function removeBestEffort(targetPath, recursive = false) {
|
|
42
50
|
try {
|
|
43
51
|
await fs.rm(targetPath, { recursive, force: true });
|
|
@@ -436,6 +444,9 @@ async function writeEntryCommands(projectRoot) {
|
|
|
436
444
|
await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
|
|
437
445
|
await writeFileSafe(runtimePath(projectRoot, "commands", "ideate.md"), ideateCommandContract());
|
|
438
446
|
await writeFileSafe(runtimePath(projectRoot, "commands", "view.md"), viewCommandContract());
|
|
447
|
+
for (const stage of FLOW_STAGES) {
|
|
448
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), stageCommandShimMarkdown(stage));
|
|
449
|
+
}
|
|
439
450
|
}
|
|
440
451
|
function toObject(value) {
|
|
441
452
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -540,11 +551,20 @@ function mergeOpenCodePluginConfig(existingDoc, pluginRelPath) {
|
|
|
540
551
|
if (!normalized.has(pluginRelPath)) {
|
|
541
552
|
pluginsRaw.push(pluginRelPath);
|
|
542
553
|
}
|
|
543
|
-
const
|
|
554
|
+
const permission = toObject(root.permission) ?? {};
|
|
555
|
+
const permissionChanged = permission.question !== "allow";
|
|
556
|
+
const changed = !normalized.has(pluginRelPath) ||
|
|
557
|
+
!Array.isArray(root.plugin) ||
|
|
558
|
+
permissionChanged ||
|
|
559
|
+
!toObject(root.permission);
|
|
544
560
|
return {
|
|
545
561
|
merged: {
|
|
546
562
|
...root,
|
|
547
|
-
plugin: pluginsRaw
|
|
563
|
+
plugin: pluginsRaw,
|
|
564
|
+
permission: {
|
|
565
|
+
...permission,
|
|
566
|
+
question: "allow"
|
|
567
|
+
}
|
|
548
568
|
},
|
|
549
569
|
changed
|
|
550
570
|
};
|
|
@@ -653,6 +673,54 @@ async function backupHookFile(projectRoot, hookFilePath, rawContent) {
|
|
|
653
673
|
await pruneOldHookBackups(backupsDir);
|
|
654
674
|
return backupPath;
|
|
655
675
|
}
|
|
676
|
+
function normalizeHookCommandForDedupe(command) {
|
|
677
|
+
return command.trim().replace(/\s+/gu, " ").replace(/\\/gu, "/");
|
|
678
|
+
}
|
|
679
|
+
function dedupeHookEntryByCommand(entry, seenCommands) {
|
|
680
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
681
|
+
return entry;
|
|
682
|
+
}
|
|
683
|
+
const obj = entry;
|
|
684
|
+
let changed = false;
|
|
685
|
+
if (typeof obj.command === "string") {
|
|
686
|
+
const normalized = normalizeHookCommandForDedupe(obj.command);
|
|
687
|
+
if (seenCommands.has(normalized)) {
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
seenCommands.add(normalized);
|
|
691
|
+
}
|
|
692
|
+
if (Array.isArray(obj.hooks)) {
|
|
693
|
+
const hooks = [];
|
|
694
|
+
for (const nested of obj.hooks) {
|
|
695
|
+
const deduped = dedupeHookEntryByCommand(nested, seenCommands);
|
|
696
|
+
if (deduped !== undefined) {
|
|
697
|
+
hooks.push(deduped);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
changed = true;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (hooks.length !== obj.hooks.length) {
|
|
704
|
+
changed = true;
|
|
705
|
+
}
|
|
706
|
+
if (hooks.length === 0 && typeof obj.command !== "string") {
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
return changed ? { ...obj, hooks } : entry;
|
|
710
|
+
}
|
|
711
|
+
return entry;
|
|
712
|
+
}
|
|
713
|
+
function dedupeHookEntriesByCommand(entries) {
|
|
714
|
+
const seenCommands = new Set();
|
|
715
|
+
const deduped = [];
|
|
716
|
+
for (const entry of entries) {
|
|
717
|
+
const next = dedupeHookEntryByCommand(entry, seenCommands);
|
|
718
|
+
if (next !== undefined) {
|
|
719
|
+
deduped.push(next);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return deduped;
|
|
723
|
+
}
|
|
656
724
|
function mergeHookDocuments(existingDoc, generatedDoc) {
|
|
657
725
|
const generatedRoot = toObject(generatedDoc) ?? {};
|
|
658
726
|
const generatedHooks = toObject(generatedRoot.hooks) ?? {};
|
|
@@ -664,7 +732,7 @@ function mergeHookDocuments(existingDoc, generatedDoc) {
|
|
|
664
732
|
const existingEntries = existingHooks[eventName];
|
|
665
733
|
if (Array.isArray(generatedEntries)) {
|
|
666
734
|
const preservedEntries = Array.isArray(existingEntries) ? existingEntries : [];
|
|
667
|
-
mergedHooks[eventName] = [...generatedEntries, ...preservedEntries];
|
|
735
|
+
mergedHooks[eventName] = dedupeHookEntriesByCommand([...generatedEntries, ...preservedEntries]);
|
|
668
736
|
continue;
|
|
669
737
|
}
|
|
670
738
|
// Defensive: malformed generated event payload must not wipe user hooks.
|
|
@@ -878,7 +946,6 @@ async function cleanLegacyArtifacts(projectRoot) {
|
|
|
878
946
|
await removeBestEffort(legacyPlugin);
|
|
879
947
|
}
|
|
880
948
|
for (const legacyRuntimeFile of [
|
|
881
|
-
...FLOW_STAGES.map((stage) => runtimePath(projectRoot, "commands", `${stage}.md`)),
|
|
882
949
|
...DEPRECATED_COMMAND_FILES.map((file) => runtimePath(projectRoot, "commands", file)),
|
|
883
950
|
...DEPRECATED_SKILL_FILES.map((segments) => runtimePath(projectRoot, "skills", ...segments)),
|
|
884
951
|
...DEPRECATED_STATE_FILES.map((file) => runtimePath(projectRoot, "state", file)),
|
|
@@ -944,26 +1011,34 @@ async function cleanStaleFiles(projectRoot) {
|
|
|
944
1011
|
// Keep user-owned custom assets under .cclaw/agents and .cclaw/skills.
|
|
945
1012
|
// Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
|
|
946
1013
|
}
|
|
947
|
-
async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1014
|
+
async function materializeRuntime(projectRoot, config, forceStateReset, operation = "sync") {
|
|
1015
|
+
const sentinelPath = await writeInitSentinel(projectRoot, operation);
|
|
1016
|
+
try {
|
|
1017
|
+
const harnesses = config.harnesses;
|
|
1018
|
+
await ensureStructure(projectRoot);
|
|
1019
|
+
await cleanLegacyArtifacts(projectRoot);
|
|
1020
|
+
await cleanStaleFiles(projectRoot);
|
|
1021
|
+
await Promise.all([
|
|
1022
|
+
writeEntryCommands(projectRoot),
|
|
1023
|
+
writeSkills(projectRoot, config),
|
|
1024
|
+
writeArtifactTemplates(projectRoot),
|
|
1025
|
+
writeRulebook(projectRoot)
|
|
1026
|
+
]);
|
|
1027
|
+
await writeState(projectRoot, config, forceStateReset);
|
|
1028
|
+
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
1029
|
+
await ensureKnowledgeStore(projectRoot);
|
|
1030
|
+
await writeHooks(projectRoot, config);
|
|
1031
|
+
await syncDisabledHarnessArtifacts(projectRoot, harnesses);
|
|
1032
|
+
await syncManagedGitHooks(projectRoot, config);
|
|
1033
|
+
await syncHarnessShims(projectRoot, harnesses);
|
|
1034
|
+
await writeCursorWorkflowRule(projectRoot, harnesses);
|
|
1035
|
+
await ensureGitignore(projectRoot);
|
|
1036
|
+
await fs.unlink(sentinelPath).catch(() => undefined);
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
// Leave the sentinel in place so doctor can surface the interrupted run.
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
967
1042
|
}
|
|
968
1043
|
export async function initCclaw(options) {
|
|
969
1044
|
const baseConfig = createDefaultConfig(options.harnesses, options.track);
|
|
@@ -978,7 +1053,7 @@ export async function initCclaw(options) {
|
|
|
978
1053
|
// and only appear in the on-disk file when the user sets them explicitly
|
|
979
1054
|
// or a non-default value was detected (e.g. languageRulePacks).
|
|
980
1055
|
await writeConfig(options.projectRoot, config, { mode: "minimal" });
|
|
981
|
-
await materializeRuntime(options.projectRoot, config, true);
|
|
1056
|
+
await materializeRuntime(options.projectRoot, config, true, "init");
|
|
982
1057
|
}
|
|
983
1058
|
export async function syncCclaw(projectRoot) {
|
|
984
1059
|
const configExists = await exists(configPath(projectRoot));
|
|
@@ -996,7 +1071,7 @@ export async function syncCclaw(projectRoot) {
|
|
|
996
1071
|
await writeConfig(projectRoot, defaultConfig);
|
|
997
1072
|
config = defaultConfig;
|
|
998
1073
|
}
|
|
999
|
-
await materializeRuntime(projectRoot, config, false);
|
|
1074
|
+
await materializeRuntime(projectRoot, config, false, "sync");
|
|
1000
1075
|
}
|
|
1001
1076
|
/**
|
|
1002
1077
|
* Refresh generated files in `.cclaw/` without touching user-authored
|
|
@@ -1020,7 +1095,7 @@ export async function upgradeCclaw(projectRoot) {
|
|
|
1020
1095
|
mode: "minimal",
|
|
1021
1096
|
advancedKeysPresent
|
|
1022
1097
|
});
|
|
1023
|
-
await materializeRuntime(projectRoot, upgraded, false);
|
|
1098
|
+
await materializeRuntime(projectRoot, upgraded, false, "upgrade");
|
|
1024
1099
|
}
|
|
1025
1100
|
function stripManagedHookCommands(value) {
|
|
1026
1101
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -1185,7 +1260,7 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
1185
1260
|
try {
|
|
1186
1261
|
const entries = await fs.readdir(codexSkillsRoot);
|
|
1187
1262
|
for (const entry of entries) {
|
|
1188
|
-
if (/^(?:cclaw-)?cc(?:-(?:next|view|ops|ideate))?$/u.test(entry)) {
|
|
1263
|
+
if (/^(?:cclaw-)?cc(?:-(?:next|view|ops|ideate|brainstorm|scope|design|spec|plan|tdd|review|ship))?$/u.test(entry)) {
|
|
1189
1264
|
await fs.rm(path.join(codexSkillsRoot, entry), { recursive: true, force: true });
|
|
1190
1265
|
}
|
|
1191
1266
|
}
|
|
@@ -1195,6 +1270,17 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
1195
1270
|
}
|
|
1196
1271
|
await removeIfEmpty(codexSkillsRoot);
|
|
1197
1272
|
await removeIfEmpty(path.join(projectRoot, ".agents"));
|
|
1273
|
+
const managedAgentNames = [
|
|
1274
|
+
"planner",
|
|
1275
|
+
"reviewer",
|
|
1276
|
+
"security-reviewer",
|
|
1277
|
+
"test-author",
|
|
1278
|
+
"doc-updater"
|
|
1279
|
+
];
|
|
1280
|
+
for (const agentName of managedAgentNames) {
|
|
1281
|
+
await removeBestEffort(path.join(projectRoot, ".opencode/agents", `${agentName}.md`));
|
|
1282
|
+
await removeBestEffort(path.join(projectRoot, ".codex/agents", `${agentName}.toml`));
|
|
1283
|
+
}
|
|
1198
1284
|
for (const pluginPath of [
|
|
1199
1285
|
path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
|
|
1200
1286
|
path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
|
|
@@ -1221,8 +1307,10 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
1221
1307
|
".cursor/rules",
|
|
1222
1308
|
".cursor/commands",
|
|
1223
1309
|
".cursor",
|
|
1310
|
+
".codex/agents",
|
|
1224
1311
|
".codex/commands",
|
|
1225
1312
|
".codex",
|
|
1313
|
+
".opencode/agents",
|
|
1226
1314
|
".opencode/plugins",
|
|
1227
1315
|
".opencode/commands",
|
|
1228
1316
|
".opencode"
|