cclaw-cli 0.51.28 → 0.51.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/cli.d.ts +6 -1
  2. package/dist/cli.js +117 -64
  3. package/dist/codex-feature-flag.d.ts +1 -1
  4. package/dist/codex-feature-flag.js +1 -1
  5. package/dist/config.js +3 -0
  6. package/dist/content/cancel-command.d.ts +2 -0
  7. package/dist/content/cancel-command.js +25 -0
  8. package/dist/content/closeout-guidance.js +3 -3
  9. package/dist/content/core-agents.js +5 -5
  10. package/dist/content/harness-doc.js +1 -1
  11. package/dist/content/hooks.js +32 -9
  12. package/dist/content/ideate-command.js +12 -7
  13. package/dist/content/meta-skill.js +7 -9
  14. package/dist/content/next-command.d.ts +2 -2
  15. package/dist/content/next-command.js +31 -27
  16. package/dist/content/node-hooks.js +24 -8
  17. package/dist/content/opencode-plugin.js +1 -1
  18. package/dist/content/session-hooks.js +1 -1
  19. package/dist/content/stage-command.js +1 -1
  20. package/dist/content/stage-common-guidance.js +4 -4
  21. package/dist/content/stages/plan.js +2 -2
  22. package/dist/content/stages/review.js +1 -1
  23. package/dist/content/stages/tdd.js +1 -1
  24. package/dist/content/start-command.d.ts +2 -2
  25. package/dist/content/start-command.js +18 -15
  26. package/dist/content/status-command.js +9 -8
  27. package/dist/content/subagents.js +1 -1
  28. package/dist/content/templates.d.ts +1 -1
  29. package/dist/content/templates.js +2 -2
  30. package/dist/content/track-render-context.d.ts +1 -0
  31. package/dist/content/track-render-context.js +2 -0
  32. package/dist/doctor-registry.d.ts +2 -0
  33. package/dist/doctor-registry.js +37 -10
  34. package/dist/doctor.d.ts +2 -1
  35. package/dist/doctor.js +184 -8
  36. package/dist/flow-state.d.ts +1 -1
  37. package/dist/flow-state.js +1 -1
  38. package/dist/fs-utils.js +6 -0
  39. package/dist/harness-adapters.d.ts +2 -2
  40. package/dist/harness-adapters.js +21 -94
  41. package/dist/harness-selection.d.ts +31 -0
  42. package/dist/harness-selection.js +214 -0
  43. package/dist/install.d.ts +4 -1
  44. package/dist/install.js +47 -10
  45. package/dist/internal/advance-stage.js +7 -7
  46. package/dist/managed-resources.d.ts +53 -0
  47. package/dist/managed-resources.js +289 -0
  48. package/dist/policy.js +1 -1
  49. package/dist/run-archive.d.ts +8 -0
  50. package/dist/run-archive.js +23 -9
  51. package/dist/run-persistence.js +1 -1
  52. package/dist/runs.d.ts +1 -1
  53. package/dist/runs.js +1 -1
  54. package/dist/tdd-cycle.js +10 -10
  55. package/dist/tdd-verification-evidence.js +4 -4
  56. package/dist/track-heuristics.d.ts +2 -0
  57. package/dist/track-heuristics.js +11 -3
  58. package/package.json +1 -1
@@ -1,11 +1,10 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { RUNTIME_ROOT, STAGE_TO_SKILL_FOLDER } from "./constants.js";
3
+ import { RUNTIME_ROOT } 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";
9
8
  export const CCLAW_MARKER_START = "<!-- cclaw-start -->";
10
9
  export const CCLAW_MARKER_END = "<!-- cclaw-end -->";
11
10
  function escapeRegExp(value) {
@@ -15,13 +14,6 @@ const RUNTIME_AGENTS_BLOCK_SOURCE = `${escapeRegExp(CCLAW_MARKER_START)}[\\s\\S]
15
14
  const RUNTIME_AGENTS_BLOCK_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "u");
16
15
  const RUNTIME_AGENTS_BLOCK_GLOBAL_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "gu");
17
16
  const UTILITY_SHIMS = [
18
- {
19
- fileName: "cc-next.md",
20
- skillName: "cc-next",
21
- command: "next",
22
- skillFolder: "flow-next-step",
23
- commandFile: "next.md"
24
- },
25
17
  {
26
18
  fileName: "cc-ideate.md",
27
19
  skillName: "cc-ideate",
@@ -30,11 +22,11 @@ const UTILITY_SHIMS = [
30
22
  commandFile: "ideate.md"
31
23
  },
32
24
  {
33
- fileName: "cc-view.md",
34
- skillName: "cc-view",
35
- command: "view",
36
- skillFolder: "flow-view",
37
- commandFile: "view.md"
25
+ fileName: "cc-cancel.md",
26
+ skillName: "cc-cancel",
27
+ command: "cancel",
28
+ skillFolder: "flow-cancel",
29
+ commandFile: "cancel.md"
38
30
  }
39
31
  ];
40
32
  /** Skill-kind shim name for the root `/cc` entry point. */
@@ -47,25 +39,17 @@ const LEGACY_CODEX_SKILL_PREFIX = "cclaw-cc";
47
39
  * harness command directories so `/cc-learn` etc. do not linger.
48
40
  */
49
41
  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
- }
56
42
  export function harnessShimFileNames() {
57
43
  return [
58
44
  "cc.md",
59
- ...UTILITY_SHIMS.map((shim) => shim.fileName),
60
- ...FLOW_STAGES.map((stage) => stageShimFileName(stage))
45
+ ...UTILITY_SHIMS.map((shim) => shim.fileName)
61
46
  ];
62
47
  }
63
48
  /** Skill folder names cclaw writes under `<commandDir>` for skill-kind harnesses. */
64
49
  export function harnessShimSkillNames() {
65
50
  return [
66
51
  ENTRY_SHIM_SKILL_NAME,
67
- ...UTILITY_SHIMS.map((shim) => shim.skillName),
68
- ...FLOW_STAGES.map((stage) => stageShimSkillName(stage))
52
+ ...UTILITY_SHIMS.map((shim) => shim.skillName)
69
53
  ];
70
54
  }
71
55
  export const HARNESS_ADAPTERS = {
@@ -179,9 +163,9 @@ export function harnessDispatchSurface(harnessId) {
179
163
  case "cursor":
180
164
  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.";
181
165
  case "opencode":
182
- return "Use OpenCode subagents: invoke the generated .opencode/agents/<agent>.md agent via Task or @<agent>; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
166
+ return "Use OpenCode subagents: invoke the generated .opencode/agents/<agent>.md agent via Task or @<agent>; if agents or plugin registration are missing, run `cclaw sync` and check opencode.json(.c) plugin registration with `cclaw doctor --explain`; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
183
167
  case "codex":
184
- return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
168
+ return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name; if hooks are inert, set `[features] codex_hooks = true` in ~/.codex/config.toml or rerun init/sync repair, then `cclaw doctor --explain`; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
185
169
  }
186
170
  }
187
171
  /**
@@ -329,7 +313,7 @@ Treat quality as a hard requirement, not style preference:
329
313
 
330
314
  Before responding to a coding request:
331
315
  1. Read \`.cclaw/state/flow-state.json\` for the current stage.
332
- 2. Use \`/cc\` to start or \`/cc-next\` to continue the flow.
316
+ 2. Use \`/cc\` to start, resume, or continue the flow.
333
317
  3. If no stage applies, respond normally.
334
318
 
335
319
  ${ironLawsAgentsMdBlock()}
@@ -358,18 +342,16 @@ When in doubt, prefer **non-trivial** — the quick track is opt-in and only saf
358
342
 
359
343
  | Command | Purpose |
360
344
  |---|---|
361
- | \`/cc\` | **Entry point.** No args = resume current stage. With prompt = classify task and start the right flow. |
362
- | \`/cc-next\` | **Progression.** Advances to the next stage when current is complete. |
345
+ | \`/cc\` | **Entry point.** No args = resume or progress current flow. With prompt = classify task and start the right flow. |
363
346
  | \`/cc-ideate\` | **Ideate mode.** Generates a ranked repo-improvement backlog before implementation. |
364
- | \`/cc-view\` | **Read-only router.** Unified entry for status/tree/diff views. |
347
+ | \`/cc-cancel\` | **Non-completion closeout.** Archives a cancelled/abandoned run with a required reason. |
365
348
 
366
349
  Knowledge capture and curation run automatically as part of stage completion
367
350
  protocols via the internal \`learnings\` skill — no user-facing command.
368
351
  Reusable entries land in \`.cclaw/knowledge.jsonl\` as strict JSONL with
369
352
  \`type\`, \`trigger\`, \`action\`, and \`origin_run\` metadata.
370
353
 
371
- **Stage order:** brainstorm > scope > design > spec > plan > tdd > review > ship, then closeout: retro > compound > archive.
372
- \`/cc-next\` loads the right stage skill automatically and also drives post-ship closeout. Gates must pass before handoff.
354
+ **Stage order:** brainstorm > scope > design > spec > plan > tdd > review > ship, then closeout: retro > compound > archive. Use \`/cc\` to keep moving through normal work and post-ship closeout; use \`/cc-cancel\` for cancelled/abandoned runs. Gates must pass before handoff.
373
355
 
374
356
  ### Verification Discipline
375
357
 
@@ -390,12 +372,11 @@ If the same approach fails three times in a row (same command, same finding, sam
390
372
  ### Codex users
391
373
 
392
374
  OpenAI Codex CLI has **no native \`/cc\` slash command** (custom prompts
393
- were deprecated in v0.89, Jan 2026). The \`/cc\`, \`/cc-next\`,
394
- \`/cc-ideate\`, \`/cc-view\` tokens above describe intent — in
395
- Codex they map onto skills cclaw installs at
375
+ were deprecated in v0.89, Jan 2026). The \`/cc\`, \`/cc-ideate\`, and
376
+ \`/cc-cancel\` tokens above describe intent — in Codex they map onto skills cclaw installs at
396
377
  \`.agents/skills/cc*/SKILL.md\`. Activate one of two ways:
397
378
 
398
- - Type \`/use cc\` (or \`cc-next\`, etc.) at Codex's prompt.
379
+ - Type \`/use cc\` (or \`cc-ideate\` / \`cc-cancel\`) at Codex's prompt.
399
380
  - Type \`/cc …\` as plain text — Codex matches the skill \`description\`
400
381
  frontmatter (which spells out the token verbatim) and loads the right
401
382
  skill body automatically.
@@ -468,12 +449,10 @@ function utilityShimBehavior(command) {
468
449
  switch (command) {
469
450
  case "cc":
470
451
  return "This is the entry command, not a flow stage. It may initialize or resume flow state after confirmation.";
471
- case "next":
472
- return "This is the progression command, not a flow stage. It may advance stages and post-ship closeout through managed helpers.";
473
452
  case "ideate":
474
453
  return "This is an ideation command, not a flow stage. It may write ideation artifacts/seeds but does not advance flow state.";
475
- case "view":
476
- return "This is a read-only view command, not a flow stage. It never mutates flow state.";
454
+ case "cancel":
455
+ return "This is a non-completion closeout utility, not a flow stage. It requires a reason and archives cancelled or abandoned work without presenting it as completed.";
477
456
  default:
478
457
  return "This is a utility command, not a flow stage.";
479
458
  }
@@ -495,50 +474,6 @@ Load and execute:
495
474
  ${utilityShimBehavior(command)}
496
475
  `;
497
476
  }
498
- function stageShimContent(harness, stage) {
499
- const shimName = stageShimSkillName(stage);
500
- const skillPath = `${RUNTIME_ROOT}/skills/${STAGE_TO_SKILL_FOLDER[stage]}/SKILL.md`;
501
- return `---
502
- name: ${shimName}
503
- description: Generated shim for ${harness}. Flow stage pointer; normal advancement uses /cc-next.
504
- source: generated-by-cclaw
505
- ---
506
-
507
- # cclaw ${stage}
508
-
509
- This is a thin compatibility shim for the \`${stage}\` flow stage.
510
-
511
- Load and follow the authoritative stage skill:
512
-
513
- - \`${skillPath}\`
514
-
515
- Normal stage resume and advancement uses \`/cc-next\`. Use \`/cc-next\` to read
516
- \`.cclaw/state/flow-state.json\`, select the active stage, and advance only after
517
- that stage's gates pass. Do not duplicate the stage protocol here.
518
- `;
519
- }
520
- function codexStageSkillMarkdown(stage) {
521
- const skillName = stageShimSkillName(stage);
522
- const skillPath = `${RUNTIME_ROOT}/skills/${STAGE_TO_SKILL_FOLDER[stage]}/SKILL.md`;
523
- return `---
524
- name: ${skillName}
525
- description: Thin cclaw stage shim for /cc-${stage}. Load ${skillPath}; normal stage resume and advancement uses /cc-next.
526
- source: generated-by-cclaw
527
- ---
528
-
529
- # cclaw /cc-${stage} (Codex adapter)
530
-
531
- This is a thin compatibility shim for the \`${stage}\` flow stage.
532
-
533
- Load and follow the authoritative stage skill:
534
-
535
- - \`${skillPath}\`
536
-
537
- Normal stage resume and advancement uses \`/cc-next\`. Use \`/cc-next\` to read
538
- \`.cclaw/state/flow-state.json\`, select the active stage, and advance only after
539
- that stage's gates pass. Do not duplicate the stage protocol here.
540
- `;
541
- }
542
477
  /**
543
478
  * Frontmatter `description` that triggers the skill when the user types any
544
479
  * of the classic cclaw slash-tokens. Codex's skill matcher runs on the skill
@@ -549,12 +484,10 @@ function codexSkillDescription(command) {
549
484
  switch (command) {
550
485
  case "cc":
551
486
  return `Entry point for the cclaw track-aware workflow ending in ship plus auto-closeout (retro → compound → archive). Use whenever the user types \`/cc\`, \`/cclaw\`, or asks to "start the flow", "begin cclaw", "kick off the workflow", "classify this task", or wants to start/resume a non-trivial software change. No args = resume the active stage from \`.cclaw/state/flow-state.json\`. With a prompt = classify and pick a track (quick/medium/standard).`;
552
- case "next":
553
- return `Advance the cclaw flow to the next stage or post-ship closeout substate. Use when the user types \`/cc-next\` or asks to "move to the next stage", "continue the flow", "advance cclaw", "progress the workflow", or when the current stage skill reports completion and gates have passed.`;
554
487
  case "ideate":
555
488
  return `Read-only repo-improvement ideate mode for cclaw. Use when the user types \`/cc-ideate\` or asks to "ideate", "scan the repo for TODOs/tech debt", "generate a backlog", or wants a ranked list of candidate ideas before committing to a single flow. Does not mutate \`.cclaw/state/flow-state.json\`.`;
556
- case "view":
557
- return `Read-only router for cclaw flow views. Use when the user types \`/cc-view\`, \`/cc-view status\`, \`/cc-view tree\`, \`/cc-view diff\`, or asks to "show cclaw status", "show the flow tree", "diff flow state", or wants a snapshot without mutation.`;
489
+ case "cancel":
490
+ return `Cancel or abandon the active cclaw run. Use when the user types \`/cc-cancel\` or asks to cancel, abandon, stop, discard, or reset an unfinished run. Requires a reason and archives with cancelled/abandoned disposition.`;
558
491
  default:
559
492
  return `Generated cclaw skill for ${command}.`;
560
493
  }
@@ -636,9 +569,6 @@ async function writeCommandKindShims(commandDir, harness) {
636
569
  for (const shim of UTILITY_SHIMS) {
637
570
  await writeFileSafe(path.join(commandDir, shim.fileName), utilityShimContent(harness, shim.command, shim.skillFolder, shim.commandFile));
638
571
  }
639
- for (const stage of FLOW_STAGES) {
640
- await writeFileSafe(path.join(commandDir, stageShimFileName(stage)), stageShimContent(harness, stage));
641
- }
642
572
  for (const legacy of LEGACY_HARNESS_SHIMS) {
643
573
  const legacyPath = path.join(commandDir, legacy);
644
574
  try {
@@ -655,9 +585,6 @@ async function writeSkillKindShims(commandDir) {
655
585
  for (const shim of UTILITY_SHIMS) {
656
586
  await writeFileSafe(path.join(commandDir, shim.skillName, "SKILL.md"), codexSkillMarkdown(shim.command, shim.skillName, shim.skillFolder, shim.commandFile));
657
587
  }
658
- for (const stage of FLOW_STAGES) {
659
- await writeFileSafe(path.join(commandDir, stageShimSkillName(stage), "SKILL.md"), codexStageSkillMarkdown(stage));
660
- }
661
588
  }
662
589
  /**
663
590
  * Legacy codex surfaces cclaw wrote before v0.39.0 that Codex CLI never
@@ -0,0 +1,31 @@
1
+ import { type CliContext, type HarnessId } from "./types.js";
2
+ export type HarnessSelectionAnswer = {
3
+ kind: "accept";
4
+ } | {
5
+ kind: "all";
6
+ } | {
7
+ kind: "toggle";
8
+ indexes: number[];
9
+ } | {
10
+ kind: "invalid";
11
+ message: string;
12
+ };
13
+ export interface HarnessChecklistState {
14
+ choices: readonly HarnessId[];
15
+ selected: readonly HarnessId[];
16
+ cursor: number;
17
+ message?: string;
18
+ }
19
+ export type HarnessChecklistOutcome = "confirm" | "cancel";
20
+ export interface HarnessChecklistUpdate {
21
+ state: HarnessChecklistState;
22
+ outcome?: HarnessChecklistOutcome;
23
+ }
24
+ export declare function parseHarnessSelectionAnswer(raw: string, total?: 4): HarnessSelectionAnswer;
25
+ export declare function createHarnessChecklistState(selected: readonly HarnessId[], choices?: readonly HarnessId[]): HarnessChecklistState;
26
+ export declare function updateHarnessChecklistState(state: HarnessChecklistState, key: string): HarnessChecklistUpdate;
27
+ export declare function promptHarnessSelectionChecklist(defaults: {
28
+ harnesses: HarnessId[];
29
+ detectedHarnesses?: HarnessId[];
30
+ currentHarnesses?: HarnessId[];
31
+ }, ctx: CliContext, label?: string): Promise<HarnessId[]>;
@@ -0,0 +1,214 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import process from "node:process";
3
+ import { HARNESS_ADAPTERS } from "./harness-adapters.js";
4
+ import { HARNESS_IDS } from "./types.js";
5
+ export function parseHarnessSelectionAnswer(raw, total = HARNESS_IDS.length) {
6
+ const answer = raw.trim().toLowerCase();
7
+ if (answer.length === 0)
8
+ return { kind: "accept" };
9
+ if (answer === "all")
10
+ return { kind: "all" };
11
+ if (answer === "none") {
12
+ return { kind: "invalid", message: "Zero harnesses is not supported. Select at least one harness." };
13
+ }
14
+ const parts = answer.split(",").map((part) => part.trim()).filter(Boolean);
15
+ const indexes = parts.map((part) => Number.parseInt(part, 10));
16
+ if (indexes.some((value) => !Number.isInteger(value) || value < 1 || value > total)) {
17
+ return { kind: "invalid", message: `Invalid selection. Use numbers 1-${total}, comma-separated.` };
18
+ }
19
+ return { kind: "toggle", indexes };
20
+ }
21
+ export function createHarnessChecklistState(selected, choices = HARNESS_IDS) {
22
+ const validSelected = choices.filter((harness) => selected.includes(harness));
23
+ return {
24
+ choices,
25
+ selected: validSelected.length > 0 ? validSelected : choices.slice(),
26
+ cursor: 0
27
+ };
28
+ }
29
+ function moveCursor(state, delta) {
30
+ const next = (state.cursor + delta + state.choices.length) % state.choices.length;
31
+ return { ...state, cursor: next, message: undefined };
32
+ }
33
+ function toggleCurrent(state) {
34
+ const current = state.choices[state.cursor];
35
+ if (!current)
36
+ return state;
37
+ const selected = state.selected.includes(current)
38
+ ? state.selected.filter((harness) => harness !== current)
39
+ : [...state.selected, current];
40
+ return { ...state, selected: state.choices.filter((harness) => selected.includes(harness)), message: undefined };
41
+ }
42
+ export function updateHarnessChecklistState(state, key) {
43
+ if (key === "\u0003" || key === "\u001b") {
44
+ return { state: { ...state, message: "Cancelled." }, outcome: "cancel" };
45
+ }
46
+ if (key === "\u001b[A" || key === "k" || key === "K") {
47
+ return { state: moveCursor(state, -1) };
48
+ }
49
+ if (key === "\u001b[B" || key === "j" || key === "J") {
50
+ return { state: moveCursor(state, 1) };
51
+ }
52
+ if (key === " ") {
53
+ return { state: toggleCurrent(state) };
54
+ }
55
+ if (key === "a" || key === "A") {
56
+ return { state: { ...state, selected: state.choices.slice(), message: undefined } };
57
+ }
58
+ if (key === "\r" || key === "\n") {
59
+ if (state.selected.length === 0) {
60
+ return { state: { ...state, message: "Select at least one harness." } };
61
+ }
62
+ return { state, outcome: "confirm" };
63
+ }
64
+ return { state };
65
+ }
66
+ function selectedHarnessPreview(harnesses) {
67
+ return harnesses.length > 0 ? harnesses.join(", ") : "none";
68
+ }
69
+ function harnessLabel(harness) {
70
+ const adapter = HARNESS_ADAPTERS[harness];
71
+ const tier = adapter ? `${adapter.reality.declaredSupport}, ${adapter.capabilities.hookSurface} hooks` : "supported";
72
+ return `${harness} (${tier})`;
73
+ }
74
+ function renderChecklist(state, defaults, ctx, label) {
75
+ const detected = new Set(defaults.detectedHarnesses ?? []);
76
+ const current = new Set(defaults.currentHarnesses ?? []);
77
+ const defaultSet = new Set(defaults.defaultHarnesses ?? []);
78
+ ctx.stdout.write("\x1b[2J\x1b[H");
79
+ ctx.stdout.write(`${label}\n`);
80
+ ctx.stdout.write(`Detected: ${selectedHarnessPreview(defaults.detectedHarnesses ?? [])}\n`);
81
+ ctx.stdout.write(`Current: ${selectedHarnessPreview(defaults.currentHarnesses ?? [])}\n`);
82
+ ctx.stdout.write("Use Up/Down or k/j to move, Space to toggle, a to select all, Enter to confirm, Esc to cancel.\n\n");
83
+ state.choices.forEach((harness, index) => {
84
+ const adapter = HARNESS_ADAPTERS[harness];
85
+ const markers = [
86
+ detected.has(harness) ? "detected" : "",
87
+ current.has(harness) ? "current" : "",
88
+ defaultSet.has(harness) ? "default" : ""
89
+ ].filter(Boolean).join(", ");
90
+ const pointer = index === state.cursor ? ">" : " ";
91
+ const checked = state.selected.includes(harness) ? "x" : " ";
92
+ ctx.stdout.write(`${pointer} [${checked}] ${harnessLabel(harness)} -> ${adapter.commandDir}${markers ? ` (${markers})` : ""}\n`);
93
+ });
94
+ if (state.message) {
95
+ ctx.stdout.write(`\n${state.message}\n`);
96
+ }
97
+ }
98
+ function rawModeAvailable(ctx) {
99
+ return Boolean(process.stdin.isTTY &&
100
+ ctx.stdout.isTTY &&
101
+ typeof process.stdin.setRawMode === "function");
102
+ }
103
+ async function promptHarnessSelectionRaw(defaults, ctx, label) {
104
+ let state = createHarnessChecklistState(defaults.harnesses);
105
+ const input = process.stdin;
106
+ const wasRaw = Boolean(input.isRaw);
107
+ let settle;
108
+ let rejectSelection;
109
+ const done = new Promise((resolve, reject) => {
110
+ settle = resolve;
111
+ rejectSelection = reject;
112
+ });
113
+ const onData = (chunk) => {
114
+ const key = chunk.toString("utf8");
115
+ const update = updateHarnessChecklistState(state, key);
116
+ state = update.state;
117
+ renderChecklist(state, {
118
+ detectedHarnesses: defaults.detectedHarnesses,
119
+ currentHarnesses: defaults.currentHarnesses,
120
+ defaultHarnesses: defaults.harnesses
121
+ }, ctx, label);
122
+ if (update.outcome === "confirm") {
123
+ settle?.(HARNESS_IDS.filter((harness) => state.selected.includes(harness)));
124
+ }
125
+ else if (update.outcome === "cancel") {
126
+ rejectSelection?.(new Error("Harness selection cancelled."));
127
+ }
128
+ };
129
+ try {
130
+ input.setRawMode?.(true);
131
+ input.resume();
132
+ input.on("data", onData);
133
+ renderChecklist(state, {
134
+ detectedHarnesses: defaults.detectedHarnesses,
135
+ currentHarnesses: defaults.currentHarnesses,
136
+ defaultHarnesses: defaults.harnesses
137
+ }, ctx, label);
138
+ return await done;
139
+ }
140
+ finally {
141
+ input.off("data", onData);
142
+ input.setRawMode?.(wasRaw);
143
+ if (!wasRaw)
144
+ input.pause();
145
+ ctx.stdout.write("\n");
146
+ }
147
+ }
148
+ async function promptHarnessSelectionText(defaults, ctx, label) {
149
+ const rl = createInterface({
150
+ input: process.stdin,
151
+ output: ctx.stdout
152
+ });
153
+ const defaultSet = new Set(defaults.harnesses);
154
+ const selected = new Set(defaults.harnesses.length > 0 ? defaults.harnesses : HARNESS_IDS);
155
+ const detected = new Set(defaults.detectedHarnesses ?? []);
156
+ const current = new Set(defaults.currentHarnesses ?? []);
157
+ const printMenu = () => {
158
+ ctx.stdout.write(`\n${label}\n`);
159
+ ctx.stdout.write(`Detected: ${selectedHarnessPreview(defaults.detectedHarnesses ?? [])}\n`);
160
+ ctx.stdout.write(`Current: ${selectedHarnessPreview(defaults.currentHarnesses ?? [])}\n`);
161
+ ctx.stdout.write("Supported harnesses and target paths:\n");
162
+ HARNESS_IDS.forEach((harness, index) => {
163
+ const adapter = HARNESS_ADAPTERS[harness];
164
+ const markers = [
165
+ detected.has(harness) ? "detected" : "",
166
+ current.has(harness) ? "current" : "",
167
+ defaultSet.has(harness) ? "default" : ""
168
+ ].filter(Boolean).join(", ");
169
+ const checked = selected.has(harness) ? "x" : " ";
170
+ ctx.stdout.write(` ${index + 1}. [${checked}] ${harnessLabel(harness)} -> ${adapter.commandDir}${markers ? ` (${markers})` : ""}\n`);
171
+ });
172
+ ctx.stdout.write("Enter numbers to toggle (for example 1,3), 'all', or press Enter to accept.\n");
173
+ };
174
+ try {
175
+ while (true) {
176
+ printMenu();
177
+ const answer = await rl.question(`Selected [${[...selected].join(",") || "select at least one"}]: `);
178
+ const parsedAnswer = parseHarnessSelectionAnswer(answer);
179
+ if (parsedAnswer.kind === "accept") {
180
+ if (selected.size === 0) {
181
+ ctx.stdout.write("Select at least one harness.\n");
182
+ continue;
183
+ }
184
+ return HARNESS_IDS.filter((harness) => selected.has(harness));
185
+ }
186
+ if (parsedAnswer.kind === "all") {
187
+ HARNESS_IDS.forEach((harness) => selected.add(harness));
188
+ continue;
189
+ }
190
+ if (parsedAnswer.kind === "invalid") {
191
+ ctx.stdout.write(`${parsedAnswer.message}\n`);
192
+ continue;
193
+ }
194
+ for (const index of parsedAnswer.indexes) {
195
+ const harness = HARNESS_IDS[index - 1];
196
+ if (!harness)
197
+ continue;
198
+ if (selected.has(harness))
199
+ selected.delete(harness);
200
+ else
201
+ selected.add(harness);
202
+ }
203
+ }
204
+ }
205
+ finally {
206
+ rl.close();
207
+ }
208
+ }
209
+ export async function promptHarnessSelectionChecklist(defaults, ctx, label = "Harness selection") {
210
+ if (rawModeAvailable(ctx)) {
211
+ return promptHarnessSelectionRaw(defaults, ctx, label);
212
+ }
213
+ return promptHarnessSelectionText(defaults, ctx, label);
214
+ }
package/dist/install.d.ts CHANGED
@@ -4,8 +4,11 @@ export interface InitOptions {
4
4
  harnesses?: HarnessId[];
5
5
  track?: FlowTrack;
6
6
  }
7
+ export interface SyncOptions {
8
+ harnesses?: HarnessId[];
9
+ }
7
10
  export declare function initCclaw(options: InitOptions): Promise<void>;
8
- export declare function syncCclaw(projectRoot: string): Promise<void>;
11
+ export declare function syncCclaw(projectRoot: string, options?: SyncOptions): Promise<void>;
9
12
  /**
10
13
  * Refresh generated files in `.cclaw/` without touching user-authored
11
14
  * artifacts, state, or custom config keys. Only the `version` + `flowVersion`
package/dist/install.js CHANGED
@@ -10,6 +10,7 @@ import { stageCommandShimMarkdown } from "./content/stage-command.js";
10
10
  import { ideateCommandContract, ideateCommandSkillMarkdown } from "./content/ideate-command.js";
11
11
  import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
12
12
  import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-command.js";
13
+ import { cancelCommandContract, cancelCommandSkillMarkdown } from "./content/cancel-command.js";
13
14
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
14
15
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
15
16
  import { ironLawRuntimeDocument, ironLawsSkillMarkdown } from "./content/iron-laws.js";
@@ -26,6 +27,7 @@ import { SUBAGENT_CONTEXT_SKILLS } from "./content/subagent-context-skills.js";
26
27
  import { CCLAW_AGENTS } from "./content/core-agents.js";
27
28
  import { createInitialFlowState } from "./flow-state.js";
28
29
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
30
+ import { ManagedResourceSession, setActiveManagedResourceSession } from "./managed-resources.js";
29
31
  import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
30
32
  import { HARNESS_ADAPTERS, harnessShimFileNames, syncHarnessShims, removeCclawFromAgentsMd } from "./harness-adapters.js";
31
33
  import { validateHookDocument } from "./hook-schema.js";
@@ -99,6 +101,7 @@ const DEPRECATED_AGENT_FILES = [
99
101
  ];
100
102
  const DEPRECATED_COMMAND_FILES = [
101
103
  "learn.md",
104
+ "finish.md",
102
105
  "status.md",
103
106
  "tree.md",
104
107
  "diff.md",
@@ -111,6 +114,7 @@ const DEPRECATED_COMMAND_FILES = [
111
114
  "rewind.md"
112
115
  ];
113
116
  const DEPRECATED_SKILL_FILES = [
117
+ ["flow-finish", "SKILL.md"],
114
118
  ["flow-ops", "SKILL.md"],
115
119
  ["tdd-cycle-log", "SKILL.md"],
116
120
  ["flow-retro", "SKILL.md"],
@@ -451,6 +455,7 @@ async function writeSkills(projectRoot, config) {
451
455
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-ideate", "SKILL.md"), ideateCommandSkillMarkdown());
452
456
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-start", "SKILL.md"), startCommandSkillMarkdown());
453
457
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-view", "SKILL.md"), viewCommandSkillMarkdown());
458
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-cancel", "SKILL.md"), cancelCommandSkillMarkdown());
454
459
  await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
455
460
  await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
456
461
  await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
@@ -514,6 +519,7 @@ async function writeEntryCommands(projectRoot) {
514
519
  await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
515
520
  await writeFileSafe(runtimePath(projectRoot, "commands", "ideate.md"), ideateCommandContract());
516
521
  await writeFileSafe(runtimePath(projectRoot, "commands", "view.md"), viewCommandContract());
522
+ await writeFileSafe(runtimePath(projectRoot, "commands", "cancel.md"), cancelCommandContract());
517
523
  for (const stage of FLOW_STAGES) {
518
524
  await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), stageCommandShimMarkdown(stage));
519
525
  }
@@ -1055,13 +1061,8 @@ async function cleanLegacyArtifacts(projectRoot) {
1055
1061
  }
1056
1062
  async function cleanStaleFiles(projectRoot) {
1057
1063
  const expectedShimFiles = new Set(harnessShimFileNames());
1064
+ const expectedShimSkills = new Set(harnessShimFileNames().map((fileName) => fileName.replace(/\.md$/u, "")));
1058
1065
  for (const adapter of Object.values(HARNESS_ADAPTERS)) {
1059
- // Skill-kind shims (Codex) live in per-skill directories, not flat
1060
- // markdown files, so the regex-based stale sweep below would never
1061
- // match them anyway. The legacy `.codex/commands/` cleanup happens in
1062
- // `cleanupLegacyCodexSurfaces` inside syncHarnessShims().
1063
- if (adapter.shimKind === "skill")
1064
- continue;
1065
1066
  const commandDir = path.join(projectRoot, adapter.commandDir);
1066
1067
  if (!(await exists(commandDir)))
1067
1068
  continue;
@@ -1072,6 +1073,16 @@ async function cleanStaleFiles(projectRoot) {
1072
1073
  catch {
1073
1074
  entries = [];
1074
1075
  }
1076
+ if (adapter.shimKind === "skill") {
1077
+ for (const entry of entries) {
1078
+ if (!/^cc(?:-.*)?$/u.test(entry))
1079
+ continue;
1080
+ if (expectedShimSkills.has(entry))
1081
+ continue;
1082
+ await fs.rm(path.join(commandDir, entry), { recursive: true, force: true });
1083
+ }
1084
+ continue;
1085
+ }
1075
1086
  for (const entry of entries) {
1076
1087
  if (!/^cc(?:-.*)?\.md$/u.test(entry))
1077
1088
  continue;
@@ -1085,6 +1096,8 @@ async function cleanStaleFiles(projectRoot) {
1085
1096
  }
1086
1097
  async function materializeRuntime(projectRoot, config, forceStateReset, operation = "sync") {
1087
1098
  const sentinelPath = await writeInitSentinel(projectRoot, operation);
1099
+ const managedSession = await ManagedResourceSession.create({ projectRoot, operation });
1100
+ setActiveManagedResourceSession(managedSession);
1088
1101
  try {
1089
1102
  const harnesses = config.harnesses;
1090
1103
  await ensureStructure(projectRoot);
@@ -1105,14 +1118,21 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
1105
1118
  await syncHarnessShims(projectRoot, harnesses);
1106
1119
  await writeCursorWorkflowRule(projectRoot, harnesses);
1107
1120
  await ensureGitignore(projectRoot);
1121
+ await managedSession.commit();
1108
1122
  await fs.unlink(sentinelPath).catch(() => undefined);
1109
1123
  }
1110
1124
  catch (error) {
1111
1125
  // Leave the sentinel in place so doctor can surface the interrupted run.
1112
1126
  throw error;
1113
1127
  }
1128
+ finally {
1129
+ setActiveManagedResourceSession(null);
1130
+ }
1114
1131
  }
1115
1132
  export async function initCclaw(options) {
1133
+ if (options.harnesses !== undefined && options.harnesses.length === 0) {
1134
+ throw new Error("Select at least one harness.");
1135
+ }
1116
1136
  const baseConfig = createDefaultConfig(options.harnesses, options.track);
1117
1137
  // Best-effort auto-detect: a Node project gets `typescript`, a Go module
1118
1138
  // gets `go`, etc. Skipped entirely when the project root has no manifests.
@@ -1127,7 +1147,10 @@ export async function initCclaw(options) {
1127
1147
  await writeConfig(options.projectRoot, config, { mode: "minimal" });
1128
1148
  await materializeRuntime(options.projectRoot, config, true, "init");
1129
1149
  }
1130
- export async function syncCclaw(projectRoot) {
1150
+ export async function syncCclaw(projectRoot, options = {}) {
1151
+ if (options.harnesses !== undefined && options.harnesses.length === 0) {
1152
+ throw new Error("Select at least one harness.");
1153
+ }
1131
1154
  const configExists = await exists(configPath(projectRoot));
1132
1155
  let config = await readConfig(projectRoot);
1133
1156
  if (!configExists) {
@@ -1138,11 +1161,21 @@ export async function syncCclaw(projectRoot) {
1138
1161
  // Fall back to the previous default (config.harnesses) if no markers
1139
1162
  // are found so brand-new projects still bootstrap cleanly.
1140
1163
  const detected = await detectHarnesses(projectRoot);
1141
- const harnesses = detected.length > 0 ? detected : config.harnesses;
1164
+ const harnesses = options.harnesses ?? (detected.length > 0 ? detected : config.harnesses);
1142
1165
  const defaultConfig = createDefaultConfig(harnesses);
1143
1166
  await writeConfig(projectRoot, defaultConfig);
1144
1167
  config = defaultConfig;
1145
1168
  }
1169
+ else if (options.harnesses !== undefined) {
1170
+ config = {
1171
+ ...config,
1172
+ harnesses: options.harnesses
1173
+ };
1174
+ await writeConfig(projectRoot, config, {
1175
+ mode: "minimal",
1176
+ advancedKeysPresent: await detectAdvancedKeys(projectRoot)
1177
+ });
1178
+ }
1146
1179
  await materializeRuntime(projectRoot, config, false, "sync");
1147
1180
  }
1148
1181
  /**
@@ -1156,8 +1189,12 @@ export async function syncCclaw(projectRoot) {
1156
1189
  * minimal — advanced knobs are never silently added.
1157
1190
  */
1158
1191
  export async function upgradeCclaw(projectRoot) {
1192
+ const configExists = await exists(configPath(projectRoot));
1159
1193
  const advancedKeysPresent = await detectAdvancedKeys(projectRoot);
1160
- const existing = await readConfig(projectRoot);
1194
+ const detectedHarnesses = configExists ? [] : await detectHarnesses(projectRoot);
1195
+ const existing = configExists
1196
+ ? await readConfig(projectRoot)
1197
+ : createDefaultConfig(detectedHarnesses.length > 0 ? detectedHarnesses : undefined);
1161
1198
  const upgraded = {
1162
1199
  ...existing,
1163
1200
  version: CCLAW_VERSION,
@@ -1332,7 +1369,7 @@ export async function uninstallCclaw(projectRoot) {
1332
1369
  try {
1333
1370
  const entries = await fs.readdir(codexSkillsRoot);
1334
1371
  for (const entry of entries) {
1335
- if (/^(?:cclaw-)?cc(?:-(?:next|view|ops|ideate|brainstorm|scope|design|spec|plan|tdd|review|ship))?$/u.test(entry)) {
1372
+ if (/^(?:cclaw-)?cc(?:-(?:next|view|finish|cancel|ops|ideate|brainstorm|scope|design|spec|plan|tdd|review|ship))?$/u.test(entry)) {
1336
1373
  await fs.rm(path.join(codexSkillsRoot, entry), { recursive: true, force: true });
1337
1374
  }
1338
1375
  }