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.
- package/README.md +4 -3
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +311 -10
- package/dist/config.js +19 -0
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +13 -1
- package/dist/content/core-agents.d.ts +44 -0
- package/dist/content/core-agents.js +225 -0
- package/dist/content/diff-command.d.ts +2 -0
- package/dist/content/diff-command.js +83 -0
- package/dist/content/doctor-references.d.ts +2 -0
- package/dist/content/doctor-references.js +144 -0
- package/dist/content/examples.js +1 -1
- package/dist/content/feature-command.d.ts +2 -0
- package/dist/content/feature-command.js +120 -0
- package/dist/content/harnesses-doc.d.ts +1 -0
- package/dist/content/harnesses-doc.js +103 -0
- package/dist/content/hook-events.d.ts +4 -0
- package/dist/content/hook-events.js +42 -0
- package/dist/content/hooks.js +47 -1
- package/dist/content/meta-skill.js +3 -2
- package/dist/content/next-command.js +8 -6
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +134 -2
- package/dist/content/protocols.js +34 -6
- package/dist/content/research-playbooks.d.ts +8 -0
- package/dist/content/research-playbooks.js +135 -0
- package/dist/content/retro-command.d.ts +2 -0
- package/dist/content/retro-command.js +77 -0
- package/dist/content/rewind-command.d.ts +3 -0
- package/dist/content/rewind-command.js +120 -0
- package/dist/content/skills.js +20 -0
- package/dist/content/stage-schema.d.ts +3 -1
- package/dist/content/stage-schema.js +20 -51
- package/dist/content/status-command.js +43 -35
- package/dist/content/subagents.d.ts +1 -1
- package/dist/content/subagents.js +23 -38
- package/dist/content/tdd-log-command.d.ts +2 -0
- package/dist/content/tdd-log-command.js +75 -0
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +84 -16
- package/dist/content/tree-command.d.ts +2 -0
- package/dist/content/tree-command.js +91 -0
- package/dist/delegation.d.ts +1 -0
- package/dist/delegation.js +27 -1
- package/dist/doctor-registry.d.ts +8 -0
- package/dist/doctor-registry.js +127 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +261 -7
- package/dist/feature-system.d.ts +18 -0
- package/dist/feature-system.js +247 -0
- package/dist/flow-state.d.ts +25 -0
- package/dist/flow-state.js +8 -1
- package/dist/harness-adapters.d.ts +7 -0
- package/dist/harness-adapters.js +127 -13
- package/dist/init-detect.d.ts +2 -0
- package/dist/init-detect.js +45 -0
- package/dist/install.js +98 -3
- package/dist/policy.js +27 -0
- package/dist/runs.d.ts +33 -1
- package/dist/runs.js +365 -6
- package/dist/tdd-cycle.d.ts +22 -0
- package/dist/tdd-cycle.js +82 -0
- package/dist/types.d.ts +4 -0
- package/package.json +2 -1
- package/dist/content/agents.d.ts +0 -48
- 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
|
+
}
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -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;
|
package/dist/flow-state.js
CHANGED
|
@@ -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>;
|
package/dist/harness-adapters.js
CHANGED
|
@@ -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: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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,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
|
+
}
|