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.
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +117 -64
- package/dist/codex-feature-flag.d.ts +1 -1
- package/dist/codex-feature-flag.js +1 -1
- package/dist/config.js +3 -0
- package/dist/content/cancel-command.d.ts +2 -0
- package/dist/content/cancel-command.js +25 -0
- package/dist/content/closeout-guidance.js +3 -3
- package/dist/content/core-agents.js +5 -5
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/hooks.js +32 -9
- package/dist/content/ideate-command.js +12 -7
- package/dist/content/meta-skill.js +7 -9
- package/dist/content/next-command.d.ts +2 -2
- package/dist/content/next-command.js +31 -27
- package/dist/content/node-hooks.js +24 -8
- package/dist/content/opencode-plugin.js +1 -1
- package/dist/content/session-hooks.js +1 -1
- package/dist/content/stage-command.js +1 -1
- package/dist/content/stage-common-guidance.js +4 -4
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/review.js +1 -1
- package/dist/content/stages/tdd.js +1 -1
- package/dist/content/start-command.d.ts +2 -2
- package/dist/content/start-command.js +18 -15
- package/dist/content/status-command.js +9 -8
- package/dist/content/subagents.js +1 -1
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +2 -2
- package/dist/content/track-render-context.d.ts +1 -0
- package/dist/content/track-render-context.js +2 -0
- package/dist/doctor-registry.d.ts +2 -0
- package/dist/doctor-registry.js +37 -10
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.js +184 -8
- package/dist/flow-state.d.ts +1 -1
- package/dist/flow-state.js +1 -1
- package/dist/fs-utils.js +6 -0
- package/dist/harness-adapters.d.ts +2 -2
- package/dist/harness-adapters.js +21 -94
- package/dist/harness-selection.d.ts +31 -0
- package/dist/harness-selection.js +214 -0
- package/dist/install.d.ts +4 -1
- package/dist/install.js +47 -10
- package/dist/internal/advance-stage.js +7 -7
- package/dist/managed-resources.d.ts +53 -0
- package/dist/managed-resources.js +289 -0
- package/dist/policy.js +1 -1
- package/dist/run-archive.d.ts +8 -0
- package/dist/run-archive.js +23 -9
- package/dist/run-persistence.js +1 -1
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -1
- package/dist/tdd-cycle.js +10 -10
- package/dist/tdd-verification-evidence.js +4 -4
- package/dist/track-heuristics.d.ts +2 -0
- package/dist/track-heuristics.js +11 -3
- package/package.json +1 -1
package/dist/harness-adapters.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { RUNTIME_ROOT
|
|
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-
|
|
34
|
-
skillName: "cc-
|
|
35
|
-
command: "
|
|
36
|
-
skillFolder: "flow-
|
|
37
|
-
commandFile: "
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
394
|
-
\`/cc-
|
|
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-
|
|
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 "
|
|
476
|
-
return "This is a
|
|
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 "
|
|
557
|
-
return `
|
|
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
|
|
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
|
}
|