cclaw-cli 0.11.0 → 0.13.0

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 (67) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.d.ts +8 -0
  3. package/dist/cli.js +311 -10
  4. package/dist/config.js +19 -0
  5. package/dist/constants.d.ts +2 -2
  6. package/dist/constants.js +13 -1
  7. package/dist/content/core-agents.d.ts +44 -0
  8. package/dist/content/core-agents.js +225 -0
  9. package/dist/content/diff-command.d.ts +2 -0
  10. package/dist/content/diff-command.js +83 -0
  11. package/dist/content/doctor-references.d.ts +2 -0
  12. package/dist/content/doctor-references.js +144 -0
  13. package/dist/content/examples.js +1 -1
  14. package/dist/content/feature-command.d.ts +2 -0
  15. package/dist/content/feature-command.js +120 -0
  16. package/dist/content/harnesses-doc.d.ts +1 -0
  17. package/dist/content/harnesses-doc.js +103 -0
  18. package/dist/content/hook-events.d.ts +4 -0
  19. package/dist/content/hook-events.js +42 -0
  20. package/dist/content/hooks.js +47 -1
  21. package/dist/content/meta-skill.js +3 -2
  22. package/dist/content/next-command.js +8 -6
  23. package/dist/content/observe.d.ts +5 -1
  24. package/dist/content/observe.js +134 -2
  25. package/dist/content/protocols.js +34 -6
  26. package/dist/content/research-playbooks.d.ts +8 -0
  27. package/dist/content/research-playbooks.js +135 -0
  28. package/dist/content/retro-command.d.ts +2 -0
  29. package/dist/content/retro-command.js +77 -0
  30. package/dist/content/rewind-command.d.ts +3 -0
  31. package/dist/content/rewind-command.js +120 -0
  32. package/dist/content/skills.js +20 -0
  33. package/dist/content/stage-schema.d.ts +3 -1
  34. package/dist/content/stage-schema.js +20 -51
  35. package/dist/content/status-command.js +43 -35
  36. package/dist/content/subagents.d.ts +1 -1
  37. package/dist/content/subagents.js +23 -38
  38. package/dist/content/tdd-log-command.d.ts +2 -0
  39. package/dist/content/tdd-log-command.js +75 -0
  40. package/dist/content/templates.d.ts +1 -1
  41. package/dist/content/templates.js +84 -16
  42. package/dist/content/tree-command.d.ts +2 -0
  43. package/dist/content/tree-command.js +91 -0
  44. package/dist/delegation.d.ts +1 -0
  45. package/dist/delegation.js +27 -1
  46. package/dist/doctor-registry.d.ts +8 -0
  47. package/dist/doctor-registry.js +127 -0
  48. package/dist/doctor.d.ts +5 -0
  49. package/dist/doctor.js +261 -7
  50. package/dist/feature-system.d.ts +18 -0
  51. package/dist/feature-system.js +247 -0
  52. package/dist/flow-state.d.ts +25 -0
  53. package/dist/flow-state.js +8 -1
  54. package/dist/harness-adapters.d.ts +7 -0
  55. package/dist/harness-adapters.js +127 -13
  56. package/dist/init-detect.d.ts +2 -0
  57. package/dist/init-detect.js +45 -0
  58. package/dist/install.js +98 -3
  59. package/dist/policy.js +27 -0
  60. package/dist/runs.d.ts +33 -1
  61. package/dist/runs.js +365 -6
  62. package/dist/tdd-cycle.d.ts +22 -0
  63. package/dist/tdd-cycle.js +82 -0
  64. package/dist/types.d.ts +4 -0
  65. package/package.json +2 -1
  66. package/dist/content/agents.d.ts +0 -48
  67. package/dist/content/agents.js +0 -411
@@ -0,0 +1,247 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_ROOT } from "./constants.js";
4
+ import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
5
+ const FEATURES_DIR_REL_PATH = `${RUNTIME_ROOT}/features`;
6
+ const ACTIVE_FEATURE_META_REL_PATH = `${RUNTIME_ROOT}/state/active-feature.json`;
7
+ const DEFAULT_FEATURE_ID = "default";
8
+ const FEATURE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
9
+ const FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT = new Set([
10
+ "active-feature.json",
11
+ ".flow-state.lock",
12
+ ".delegation.lock"
13
+ ]);
14
+ function featuresRoot(projectRoot) {
15
+ return path.join(projectRoot, FEATURES_DIR_REL_PATH);
16
+ }
17
+ function runtimeArtifactsRoot(projectRoot) {
18
+ return path.join(projectRoot, RUNTIME_ROOT, "artifacts");
19
+ }
20
+ function runtimeStateRoot(projectRoot) {
21
+ return path.join(projectRoot, RUNTIME_ROOT, "state");
22
+ }
23
+ export function activeFeatureMetaPath(projectRoot) {
24
+ return path.join(projectRoot, ACTIVE_FEATURE_META_REL_PATH);
25
+ }
26
+ export function featureRootPath(projectRoot, featureId) {
27
+ return path.join(featuresRoot(projectRoot), featureId);
28
+ }
29
+ export function featureArtifactsPath(projectRoot, featureId) {
30
+ return path.join(featureRootPath(projectRoot, featureId), "artifacts");
31
+ }
32
+ export function featureStatePath(projectRoot, featureId) {
33
+ return path.join(featureRootPath(projectRoot, featureId), "state");
34
+ }
35
+ function normalizedFeatureId(value) {
36
+ const candidate = value
37
+ .trim()
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9]+/gu, "-")
40
+ .replace(/^-+/u, "")
41
+ .replace(/-+$/u, "");
42
+ if (!candidate) {
43
+ return DEFAULT_FEATURE_ID;
44
+ }
45
+ const clipped = candidate.slice(0, 64);
46
+ return FEATURE_ID_PATTERN.test(clipped) ? clipped : DEFAULT_FEATURE_ID;
47
+ }
48
+ async function clearDirectory(dirPath, preserveTargetEntries = new Set()) {
49
+ await ensureDir(dirPath);
50
+ let entries;
51
+ try {
52
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ for (const entry of entries) {
58
+ if (preserveTargetEntries.has(entry.name)) {
59
+ continue;
60
+ }
61
+ await fs.rm(path.join(dirPath, entry.name), { recursive: true, force: true });
62
+ }
63
+ }
64
+ async function copyDirectoryContents(sourceDir, targetDir, options = {}) {
65
+ const exclude = options.exclude ?? new Set();
66
+ const preserveTargetEntries = options.preserveTargetEntries ?? new Set();
67
+ await ensureDir(targetDir);
68
+ await clearDirectory(targetDir, preserveTargetEntries);
69
+ if (!(await exists(sourceDir))) {
70
+ return;
71
+ }
72
+ let entries;
73
+ try {
74
+ entries = await fs.readdir(sourceDir, { withFileTypes: true });
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ for (const entry of entries) {
80
+ if (exclude.has(entry.name)) {
81
+ continue;
82
+ }
83
+ const from = path.join(sourceDir, entry.name);
84
+ const to = path.join(targetDir, entry.name);
85
+ if (entry.isDirectory()) {
86
+ await fs.cp(from, to, { recursive: true, force: true });
87
+ continue;
88
+ }
89
+ if (entry.isFile()) {
90
+ await fs.copyFile(from, to);
91
+ }
92
+ }
93
+ }
94
+ async function dirHasEntries(dirPath, exclude = new Set()) {
95
+ if (!(await exists(dirPath))) {
96
+ return false;
97
+ }
98
+ try {
99
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
100
+ return entries.some((entry) => !exclude.has(entry.name));
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ async function readActiveFeatureMetaInternal(projectRoot) {
107
+ const filePath = activeFeatureMetaPath(projectRoot);
108
+ if (!(await exists(filePath))) {
109
+ return {
110
+ activeFeature: DEFAULT_FEATURE_ID,
111
+ updatedAt: new Date().toISOString()
112
+ };
113
+ }
114
+ try {
115
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
116
+ const activeFeatureRaw = typeof parsed.activeFeature === "string"
117
+ ? parsed.activeFeature
118
+ : DEFAULT_FEATURE_ID;
119
+ const updatedAtRaw = typeof parsed.updatedAt === "string"
120
+ ? parsed.updatedAt
121
+ : new Date().toISOString();
122
+ return {
123
+ activeFeature: normalizedFeatureId(activeFeatureRaw),
124
+ updatedAt: updatedAtRaw
125
+ };
126
+ }
127
+ catch {
128
+ return {
129
+ activeFeature: DEFAULT_FEATURE_ID,
130
+ updatedAt: new Date().toISOString()
131
+ };
132
+ }
133
+ }
134
+ async function writeActiveFeatureMeta(projectRoot, meta) {
135
+ const normalized = {
136
+ activeFeature: normalizedFeatureId(meta.activeFeature),
137
+ updatedAt: meta.updatedAt
138
+ };
139
+ await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`);
140
+ }
141
+ async function ensureFeatureSnapshot(projectRoot, featureId) {
142
+ const id = normalizedFeatureId(featureId);
143
+ await ensureDir(featureArtifactsPath(projectRoot, id));
144
+ await ensureDir(featureStatePath(projectRoot, id));
145
+ }
146
+ export async function readActiveFeature(projectRoot) {
147
+ const meta = await readActiveFeatureMetaInternal(projectRoot);
148
+ return normalizedFeatureId(meta.activeFeature);
149
+ }
150
+ export async function listFeatures(projectRoot) {
151
+ const root = featuresRoot(projectRoot);
152
+ if (!(await exists(root))) {
153
+ return [];
154
+ }
155
+ let entries;
156
+ try {
157
+ entries = await fs.readdir(root, { withFileTypes: true });
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ return entries
163
+ .filter((entry) => entry.isDirectory() && FEATURE_ID_PATTERN.test(entry.name))
164
+ .map((entry) => entry.name)
165
+ .sort((a, b) => a.localeCompare(b));
166
+ }
167
+ export async function ensureFeatureSystem(projectRoot) {
168
+ await ensureDir(featuresRoot(projectRoot));
169
+ await ensureDir(runtimeArtifactsRoot(projectRoot));
170
+ await ensureDir(runtimeStateRoot(projectRoot));
171
+ const existing = await readActiveFeatureMetaInternal(projectRoot);
172
+ const activeFeature = normalizedFeatureId(existing.activeFeature);
173
+ await ensureFeatureSnapshot(projectRoot, activeFeature);
174
+ const runtimeArtifactsHasData = await dirHasEntries(runtimeArtifactsRoot(projectRoot));
175
+ const runtimeStateHasData = await dirHasEntries(runtimeStateRoot(projectRoot), new Set(["active-feature.json"]));
176
+ const featureArtifactsHasData = await dirHasEntries(featureArtifactsPath(projectRoot, activeFeature));
177
+ const featureStateHasData = await dirHasEntries(featureStatePath(projectRoot, activeFeature));
178
+ if ((runtimeArtifactsHasData || runtimeStateHasData) && !featureArtifactsHasData && !featureStateHasData) {
179
+ await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
180
+ await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), { exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT });
181
+ }
182
+ else if ((!runtimeArtifactsHasData && !runtimeStateHasData) && (featureArtifactsHasData || featureStateHasData)) {
183
+ await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), runtimeArtifactsRoot(projectRoot));
184
+ await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), runtimeStateRoot(projectRoot), { preserveTargetEntries: new Set(["active-feature.json"]) });
185
+ }
186
+ const normalized = {
187
+ activeFeature,
188
+ updatedAt: new Date().toISOString()
189
+ };
190
+ await writeActiveFeatureMeta(projectRoot, normalized);
191
+ return normalized;
192
+ }
193
+ export async function syncActiveFeatureSnapshot(projectRoot) {
194
+ const activeFeature = await readActiveFeature(projectRoot);
195
+ await ensureFeatureSnapshot(projectRoot, activeFeature);
196
+ await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
197
+ await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), {
198
+ exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT
199
+ });
200
+ }
201
+ export async function switchActiveFeature(projectRoot, featureId) {
202
+ await ensureFeatureSystem(projectRoot);
203
+ const current = await readActiveFeature(projectRoot);
204
+ const target = normalizedFeatureId(featureId);
205
+ if (current === target) {
206
+ const unchanged = {
207
+ activeFeature: current,
208
+ updatedAt: new Date().toISOString()
209
+ };
210
+ await writeActiveFeatureMeta(projectRoot, unchanged);
211
+ return unchanged;
212
+ }
213
+ await syncActiveFeatureSnapshot(projectRoot);
214
+ await ensureFeatureSnapshot(projectRoot, target);
215
+ await copyDirectoryContents(featureArtifactsPath(projectRoot, target), runtimeArtifactsRoot(projectRoot));
216
+ await copyDirectoryContents(featureStatePath(projectRoot, target), runtimeStateRoot(projectRoot), {
217
+ preserveTargetEntries: new Set(["active-feature.json"])
218
+ });
219
+ const nextMeta = {
220
+ activeFeature: target,
221
+ updatedAt: new Date().toISOString()
222
+ };
223
+ await writeActiveFeatureMeta(projectRoot, nextMeta);
224
+ return nextMeta;
225
+ }
226
+ export async function createFeature(projectRoot, rawFeatureId, options = {}) {
227
+ await ensureFeatureSystem(projectRoot);
228
+ const featureId = normalizedFeatureId(rawFeatureId);
229
+ if (featureId === DEFAULT_FEATURE_ID && rawFeatureId.trim().length > 0 && rawFeatureId.trim().toLowerCase() !== "default") {
230
+ throw new Error(`Unable to create feature from "${rawFeatureId}" — use letters, numbers, and dashes.`);
231
+ }
232
+ const featureDir = featureRootPath(projectRoot, featureId);
233
+ if (await exists(featureDir)) {
234
+ throw new Error(`Feature "${featureId}" already exists.`);
235
+ }
236
+ await ensureFeatureSnapshot(projectRoot, featureId);
237
+ if (options.cloneActive === true) {
238
+ const activeFeature = await readActiveFeature(projectRoot);
239
+ await syncActiveFeatureSnapshot(projectRoot);
240
+ await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), featureArtifactsPath(projectRoot, featureId));
241
+ await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), featureStatePath(projectRoot, featureId));
242
+ }
243
+ if (options.switchTo === true) {
244
+ await switchActiveFeature(projectRoot, featureId);
245
+ }
246
+ return featureId;
247
+ }
@@ -9,6 +9,25 @@ export interface StageGateState {
9
9
  passed: string[];
10
10
  blocked: string[];
11
11
  }
12
+ export interface RewindRecord {
13
+ id: string;
14
+ fromStage: FlowStage;
15
+ toStage: FlowStage;
16
+ reason: string;
17
+ timestamp: string;
18
+ invalidatedStages: FlowStage[];
19
+ }
20
+ export interface StaleStageMarker {
21
+ rewindId: string;
22
+ reason: string;
23
+ markedAt: string;
24
+ acknowledgedAt?: string;
25
+ }
26
+ export interface RetroState {
27
+ required: boolean;
28
+ completedAt?: string;
29
+ compoundEntries: number;
30
+ }
12
31
  export interface FlowState {
13
32
  activeRunId: string;
14
33
  currentStage: FlowStage;
@@ -19,6 +38,12 @@ export interface FlowState {
19
38
  track: FlowTrack;
20
39
  /** Stages explicitly skipped for this track (empty for standard; populated for quick). */
21
40
  skippedStages: FlowStage[];
41
+ /** Stages invalidated by rewind operations and awaiting explicit acknowledgement. */
42
+ staleStages: Partial<Record<FlowStage, StaleStageMarker>>;
43
+ /** Chronological rewind operations for the active run. */
44
+ rewinds: RewindRecord[];
45
+ /** Mandatory retrospective gate status before archive. */
46
+ retro: RetroState;
22
47
  }
23
48
  export interface InitialFlowStateOptions {
24
49
  activeRunId?: string;
@@ -41,7 +41,14 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
41
41
  guardEvidence: {},
42
42
  stageGateCatalog,
43
43
  track,
44
- skippedStages
44
+ skippedStages,
45
+ staleStages: {},
46
+ rewinds: [],
47
+ retro: {
48
+ required: false,
49
+ completedAt: undefined,
50
+ compoundEntries: 0
51
+ }
45
52
  };
46
53
  }
47
54
  export function canTransition(from, to) {
@@ -4,8 +4,15 @@ export declare const CCLAW_MARKER_END = "<!-- cclaw-end -->";
4
4
  export interface HarnessAdapter {
5
5
  id: HarnessId;
6
6
  commandDir: string;
7
+ capabilities: {
8
+ nativeSubagentDispatch: "full" | "partial" | "none";
9
+ hookSurface: "full" | "plugin" | "limited" | "none";
10
+ structuredAsk: "AskUserQuestion" | "AskQuestion" | "plain-text";
11
+ };
7
12
  }
8
13
  export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
14
+ export type HarnessTier = "tier1" | "tier2" | "tier3";
15
+ export declare function harnessTier(harnessId: HarnessId): HarnessTier;
9
16
  /** Removes the cclaw AGENTS.md block. */
10
17
  export declare function stripCclawBlock(content: string): string;
11
18
  export declare function removeCclawFromAgentsMd(projectRoot: string): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "./constants.js";
4
- import { CCLAW_AGENTS, agentMarkdown } from "./content/agents.js";
4
+ import { CCLAW_AGENTS, agentMarkdown } from "./content/core-agents.js";
5
5
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
6
6
  export const CCLAW_MARKER_START = "<!-- cclaw-start -->";
7
7
  export const CCLAW_MARKER_END = "<!-- cclaw-end -->";
@@ -11,12 +11,120 @@ function escapeRegExp(value) {
11
11
  const RUNTIME_AGENTS_BLOCK_SOURCE = `${escapeRegExp(CCLAW_MARKER_START)}[\\s\\S]*?${escapeRegExp(CCLAW_MARKER_END)}`;
12
12
  const RUNTIME_AGENTS_BLOCK_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "u");
13
13
  const RUNTIME_AGENTS_BLOCK_GLOBAL_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "gu");
14
+ const UTILITY_SHIMS = [
15
+ {
16
+ fileName: "cc-next.md",
17
+ command: "next",
18
+ skillFolder: "flow-next-step",
19
+ commandFile: "next.md"
20
+ },
21
+ {
22
+ fileName: "cc-learn.md",
23
+ command: "learn",
24
+ skillFolder: "learnings",
25
+ commandFile: "learn.md"
26
+ },
27
+ {
28
+ fileName: "cc-status.md",
29
+ command: "status",
30
+ skillFolder: "flow-status",
31
+ commandFile: "status.md"
32
+ },
33
+ {
34
+ fileName: "cc-tree.md",
35
+ command: "tree",
36
+ skillFolder: "flow-tree",
37
+ commandFile: "tree.md"
38
+ },
39
+ {
40
+ fileName: "cc-diff.md",
41
+ command: "diff",
42
+ skillFolder: "flow-diff",
43
+ commandFile: "diff.md"
44
+ },
45
+ {
46
+ fileName: "cc-feature.md",
47
+ command: "feature",
48
+ skillFolder: "feature-workspaces",
49
+ commandFile: "feature.md"
50
+ },
51
+ {
52
+ fileName: "cc-tdd-log.md",
53
+ command: "tdd-log",
54
+ skillFolder: "tdd-cycle-log",
55
+ commandFile: "tdd-log.md"
56
+ },
57
+ {
58
+ fileName: "cc-retro.md",
59
+ command: "retro",
60
+ skillFolder: "flow-retro",
61
+ commandFile: "retro.md"
62
+ },
63
+ {
64
+ fileName: "cc-rewind.md",
65
+ command: "rewind",
66
+ skillFolder: "flow-rewind",
67
+ commandFile: "rewind.md"
68
+ },
69
+ {
70
+ fileName: "cc-rewind-ack.md",
71
+ command: "rewind-ack",
72
+ skillFolder: "flow-rewind",
73
+ commandFile: "rewind-ack.md"
74
+ }
75
+ ];
14
76
  export const HARNESS_ADAPTERS = {
15
- claude: { id: "claude", commandDir: ".claude/commands" },
16
- cursor: { id: "cursor", commandDir: ".cursor/commands" },
17
- opencode: { id: "opencode", commandDir: ".opencode/commands" },
18
- codex: { id: "codex", commandDir: ".codex/commands" }
77
+ claude: {
78
+ id: "claude",
79
+ commandDir: ".claude/commands",
80
+ capabilities: {
81
+ nativeSubagentDispatch: "full",
82
+ hookSurface: "full",
83
+ structuredAsk: "AskUserQuestion"
84
+ }
85
+ },
86
+ cursor: {
87
+ id: "cursor",
88
+ commandDir: ".cursor/commands",
89
+ capabilities: {
90
+ nativeSubagentDispatch: "partial",
91
+ hookSurface: "full",
92
+ structuredAsk: "AskQuestion"
93
+ }
94
+ },
95
+ opencode: {
96
+ id: "opencode",
97
+ commandDir: ".opencode/commands",
98
+ capabilities: {
99
+ nativeSubagentDispatch: "partial",
100
+ hookSurface: "plugin",
101
+ structuredAsk: "plain-text"
102
+ }
103
+ },
104
+ codex: {
105
+ id: "codex",
106
+ commandDir: ".codex/commands",
107
+ capabilities: {
108
+ nativeSubagentDispatch: "none",
109
+ hookSurface: "full",
110
+ structuredAsk: "plain-text"
111
+ }
112
+ }
19
113
  };
114
+ export function harnessTier(harnessId) {
115
+ const capabilities = HARNESS_ADAPTERS[harnessId].capabilities;
116
+ if (capabilities.nativeSubagentDispatch === "full" &&
117
+ capabilities.structuredAsk !== "plain-text" &&
118
+ capabilities.hookSurface === "full") {
119
+ return "tier1";
120
+ }
121
+ if (capabilities.hookSurface === "full" ||
122
+ capabilities.hookSurface === "plugin" ||
123
+ capabilities.nativeSubagentDispatch === "partial") {
124
+ return "tier2";
125
+ }
126
+ return "tier3";
127
+ }
20
128
  function agentsMdBlock() {
21
129
  return `${CCLAW_MARKER_START}
22
130
  ## Cclaw — Workflow Adapter
@@ -52,21 +160,25 @@ When in doubt, prefer **non-trivial** — the quick track is opt-in and only saf
52
160
  5. Contextual utility skills.
53
161
  6. Training priors.
54
162
 
55
- ### Commands (3 total)
163
+ ### Commands
56
164
 
57
165
  | Command | Purpose |
58
166
  |---|---|
59
167
  | \`/cc\` | **Entry point.** No args = resume current stage. With prompt = classify task and start the right flow. |
60
168
  | \`/cc-next\` | **Progression.** Advances to the next stage when current is complete. |
61
169
  | \`/cc-learn\` | **Cross-cutting.** Capture or review project knowledge (append-only JSONL). |
170
+ | \`/cc-status\` | **Read-only.** Visual snapshot with progress bar, gate delta, and delegations. |
171
+ | \`/cc-tree\` | **Read-only.** Deep flow tree for stages, artifacts, and stale markers. |
172
+ | \`/cc-diff\` | **Delta map.** Compare current flow-state with saved baseline snapshot. |
173
+ | \`/cc-feature\` | **Workspace.** Manage active feature snapshots for parallel tracks. |
174
+ | \`/cc-tdd-log\` | **Evidence.** Record RED/GREEN/REFACTOR cycle events for enforcement. |
175
+ | \`/cc-retro\` | **Learning gate.** Mandatory retrospective before archive after ship. |
176
+ | \`/cc-rewind\` | **Recovery.** Rewind flow to an earlier stage and invalidate downstream work. |
177
+ | \`/cc-rewind-ack\` | **Recovery.** Clear stale-stage markers after redo. |
62
178
 
63
179
  **Stage order:** brainstorm > scope > design > spec > plan > tdd > review > ship.
64
180
  \`/cc-next\` loads the right stage skill automatically. Gates must pass before handoff.
65
181
 
66
- ### Invocation Preamble (non-trivial turns)
67
-
68
- Before starting substantive work, emit a one-paragraph preamble: **Stage**, **Goal**, **Plan** (next 1–3 actions), **Guardrails**. Skip for pure questions, trivial edits, and dispatched subagent invocations.
69
-
70
182
  ### Verification Discipline
71
183
 
72
184
  No completion claims without fresh evidence. No "Done" / "All good" / "Tests pass" without running the command in this message. Failed tool calls are diagnostic data, not instructions.
@@ -78,7 +190,9 @@ If the same approach fails three times in a row (same command, same finding, sam
78
190
  ### Detail Level
79
191
 
80
192
  - This managed AGENTS block is intentionally minimal for cross-project use.
193
+ - Harness coverage is tiered: Tier1 (claude), Tier2 (cursor/opencode/codex), Tier3 (fallback/manual-only).
81
194
  - Detailed operating procedures live in \`.cclaw/skills/using-cclaw/SKILL.md\`.
195
+ - Preamble budget and cooldown rules live in \`.cclaw/references/protocols/ethos.md\`.
82
196
  - Subagent orchestration patterns: \`.cclaw/skills/subagent-dev/SKILL.md\` and \`.cclaw/skills/parallel-dispatch/SKILL.md\`.
83
197
  ${CCLAW_MARKER_END}`;
84
198
  }
@@ -169,9 +283,9 @@ export async function syncHarnessShims(projectRoot, harnesses) {
169
283
  const commandDir = path.join(projectRoot, adapter.commandDir);
170
284
  await ensureDir(commandDir);
171
285
  await writeFileSafe(path.join(commandDir, "cc.md"), utilityShimContent(harness, "cc", "flow-start", "start.md"));
172
- await writeFileSafe(path.join(commandDir, "cc-next.md"), utilityShimContent(harness, "next", "flow-next-step", "next.md"));
173
- await writeFileSafe(path.join(commandDir, "cc-learn.md"), utilityShimContent(harness, "learn", "learnings", "learn.md"));
174
- await writeFileSafe(path.join(commandDir, "cc-status.md"), utilityShimContent(harness, "status", "flow-status", "status.md"));
286
+ for (const shim of UTILITY_SHIMS) {
287
+ await writeFileSafe(path.join(commandDir, shim.fileName), utilityShimContent(harness, shim.command, shim.skillFolder, shim.commandFile));
288
+ }
175
289
  }
176
290
  await syncAgentFiles(projectRoot);
177
291
  await syncAgentsMd(projectRoot, harnesses);
@@ -0,0 +1,2 @@
1
+ import type { HarnessId } from "./types.js";
2
+ export declare function detectHarnesses(projectRoot: string): Promise<HarnessId[]>;
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { exists } from "./fs-utils.js";
3
+ export async function detectHarnesses(projectRoot) {
4
+ const detected = [];
5
+ const claudeHints = [
6
+ path.join(projectRoot, ".claude"),
7
+ path.join(projectRoot, "CLAUDE.md")
8
+ ];
9
+ if (await anyExists(claudeHints)) {
10
+ detected.push("claude");
11
+ }
12
+ const cursorHints = [
13
+ path.join(projectRoot, ".cursor"),
14
+ path.join(projectRoot, ".cursor/rules")
15
+ ];
16
+ if (await anyExists(cursorHints)) {
17
+ detected.push("cursor");
18
+ }
19
+ const opencodeHints = [
20
+ path.join(projectRoot, ".opencode"),
21
+ path.join(projectRoot, "opencode.json"),
22
+ path.join(projectRoot, "opencode.jsonc"),
23
+ path.join(projectRoot, ".opencode/opencode.json"),
24
+ path.join(projectRoot, ".opencode/opencode.jsonc")
25
+ ];
26
+ if (await anyExists(opencodeHints)) {
27
+ detected.push("opencode");
28
+ }
29
+ const codexHints = [
30
+ path.join(projectRoot, ".codex"),
31
+ path.join(projectRoot, ".codex/hooks.json")
32
+ ];
33
+ if (await anyExists(codexHints)) {
34
+ detected.push("codex");
35
+ }
36
+ return detected;
37
+ }
38
+ async function anyExists(paths) {
39
+ for (const candidate of paths) {
40
+ if (await exists(candidate)) {
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ }