cclaw-cli 0.10.1 → 0.12.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 (55) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.js +297 -9
  4. package/dist/config.js +83 -3
  5. package/dist/content/core-agents.d.ts +44 -0
  6. package/dist/content/core-agents.js +225 -0
  7. package/dist/content/doctor-references.d.ts +2 -0
  8. package/dist/content/doctor-references.js +144 -0
  9. package/dist/content/examples.js +1 -1
  10. package/dist/content/harnesses-doc.d.ts +1 -0
  11. package/dist/content/harnesses-doc.js +95 -0
  12. package/dist/content/hook-events.d.ts +4 -0
  13. package/dist/content/hook-events.js +42 -0
  14. package/dist/content/hooks.js +81 -11
  15. package/dist/content/meta-skill.d.ts +0 -8
  16. package/dist/content/meta-skill.js +51 -341
  17. package/dist/content/next-command.js +2 -1
  18. package/dist/content/protocols.d.ts +7 -0
  19. package/dist/content/protocols.js +123 -0
  20. package/dist/content/research-playbooks.d.ts +8 -0
  21. package/dist/content/research-playbooks.js +135 -0
  22. package/dist/content/skills.js +202 -312
  23. package/dist/content/stage-common-guidance.d.ts +2 -0
  24. package/dist/content/stage-common-guidance.js +71 -0
  25. package/dist/content/stage-schema.d.ts +11 -1
  26. package/dist/content/stage-schema.js +155 -52
  27. package/dist/content/start-command.js +19 -13
  28. package/dist/content/subagents.d.ts +1 -1
  29. package/dist/content/subagents.js +23 -38
  30. package/dist/content/templates.d.ts +1 -1
  31. package/dist/content/templates.js +49 -11
  32. package/dist/delegation.d.ts +1 -0
  33. package/dist/delegation.js +27 -1
  34. package/dist/doctor-registry.d.ts +8 -0
  35. package/dist/doctor-registry.js +127 -0
  36. package/dist/doctor.d.ts +5 -0
  37. package/dist/doctor.js +133 -27
  38. package/dist/flow-state.d.ts +4 -0
  39. package/dist/flow-state.js +4 -1
  40. package/dist/gate-evidence.d.ts +9 -1
  41. package/dist/gate-evidence.js +121 -17
  42. package/dist/harness-adapters.d.ts +7 -0
  43. package/dist/harness-adapters.js +53 -9
  44. package/dist/init-detect.d.ts +2 -0
  45. package/dist/init-detect.js +45 -0
  46. package/dist/install.js +73 -1
  47. package/dist/policy.js +21 -13
  48. package/dist/runs.js +21 -4
  49. package/dist/track-heuristics.d.ts +12 -0
  50. package/dist/track-heuristics.js +144 -0
  51. package/dist/types.d.ts +26 -3
  52. package/dist/types.js +6 -3
  53. package/package.json +2 -1
  54. package/dist/content/agents.d.ts +0 -48
  55. package/dist/content/agents.js +0 -411
package/README.md CHANGED
@@ -41,7 +41,7 @@ sequenceDiagram
41
41
  - **Low cognitive load:** one canonical stage flow instead of dozens of competing paths.
42
42
  - **Installer-first architecture:** generates files and hooks; does not run a hidden control plane.
43
43
  - **Hard-gated quality:** each stage has non-skippable constraints that reduce AI drift.
44
- - **Cross-harness parity:** same behavior model across Claude Code, Cursor, Codex, OpenCode.
44
+ - **Tiered harness coverage:** transparent capability tiers across Claude Code, Cursor, Codex, OpenCode.
45
45
  - **Compounding context:** flow state + project knowledge get rehydrated on new sessions automatically.
46
46
  - **Incremental delivery:** active artifacts stay in one place; `cclaw archive` snapshots completed features into dated run folders.
47
47
 
@@ -114,16 +114,17 @@ Required repository secret:
114
114
  ├── commands/
115
115
  ├── hooks/
116
116
  ├── templates/
117
+ ├── references/
117
118
  ├── artifacts/ # active feature artifacts
118
119
  ├── state/
119
- ├── knowledge.md # append-only rule/pattern/lesson log
120
+ ├── knowledge.jsonl # append-only strict-schema rule/pattern/lesson log
120
121
  └── runs/ # archived feature snapshots (YYYY-MM-DD-feature-name)
121
122
  ```
122
123
 
123
124
  ## Harness Integration
124
125
 
125
126
  Supported harnesses: `claude`, `cursor`, `opencode`, `codex`. The full
126
- per-harness install surface, feature matrix, and lifecycle details live in
127
+ per-harness tier/capability matrix, install surface, and lifecycle details live in
127
128
  [docs/harnesses.md](./docs/harnesses.md).
128
129
 
129
130
  ## License
package/dist/cli.d.ts CHANGED
@@ -6,7 +6,13 @@ interface ParsedArgs {
6
6
  harnesses?: HarnessId[];
7
7
  track?: FlowTrack;
8
8
  profile?: InitProfile;
9
+ dryRun?: boolean;
10
+ interactive?: boolean;
9
11
  reconcileGates?: boolean;
12
+ doctorJson?: boolean;
13
+ doctorExplain?: boolean;
14
+ doctorQuiet?: boolean;
15
+ doctorOnly?: string[];
10
16
  archiveName?: string;
11
17
  showHelp?: boolean;
12
18
  showVersion?: boolean;
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync, realpathSync } from "node:fs";
3
3
  import process from "node:process";
4
4
  import path from "node:path";
5
+ import { createInterface } from "node:readline/promises";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { FLOW_TRACKS, HARNESS_IDS, INIT_PROFILES } from "./types.js";
7
8
  import { doctorChecks, doctorSucceeded } from "./doctor.js";
@@ -9,6 +10,9 @@ import { initCclaw, syncCclaw, uninstallCclaw, upgradeCclaw } from "./install.js
9
10
  import { error, info } from "./logger.js";
10
11
  import { archiveRun } from "./runs.js";
11
12
  import { RUNTIME_ROOT } from "./constants.js";
13
+ import { createDefaultConfig, createProfileConfig } from "./config.js";
14
+ import { detectHarnesses } from "./init-detect.js";
15
+ import { HARNESS_ADAPTERS } from "./harness-adapters.js";
12
16
  const INSTALLER_COMMANDS = ["init", "sync", "doctor", "upgrade", "uninstall", "archive"];
13
17
  export function usage() {
14
18
  return `cclaw - installer-first flow toolkit
@@ -22,10 +26,17 @@ Commands:
22
26
  init Bootstrap .cclaw runtime, state, and harness shims in this project.
23
27
  Flags: --profile=<id> Pre-fill defaults. One of: minimal | standard | full. Default: standard.
24
28
  --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex). Overrides the profile default.
25
- --track=<id> Flow track for new runs (standard | quick). Overrides the profile default.
29
+ --track=<id> Flow track for new runs (standard | medium | quick). Overrides the profile default.
30
+ --interactive Force interactive prompts (TTY only).
31
+ --no-interactive Skip interactive prompts even on TTY.
32
+ --dry-run Print resolved config + generated surfaces without writing files.
26
33
  sync Regenerate harness shim files from the current .cclaw config (non-destructive).
27
- doctor Run health checks against the local .cclaw runtime. Exit code 2 on failure.
34
+ doctor Run health checks against the local .cclaw runtime. Exit code 2 when any error-severity check fails.
28
35
  Flags: --reconcile-gates Recompute current-stage gate evidence before checks.
36
+ --json Emit machine-readable JSON output.
37
+ --only=<filter> Comma list of severities/check-name filters (error,warning,info,trace:,hook:...).
38
+ --explain Include fix + doc reference per check in text mode.
39
+ --quiet Print only failing checks (and totals).
29
40
  archive Move .cclaw/artifacts into .cclaw/runs/<date>-<slug> and reset flow state.
30
41
  Flags: --name=<feature> Feature slug (default: inferred from 00-idea.md).
31
42
  upgrade Refresh generated files in .cclaw without modifying user artifacts.
@@ -94,6 +105,208 @@ function parseProfile(raw) {
94
105
  }
95
106
  return trimmed;
96
107
  }
108
+ function isInitPromptAllowed(ctx) {
109
+ return Boolean(process.stdin.isTTY && ctx.stdout.isTTY);
110
+ }
111
+ function buildInitSurfacePreview(harnesses) {
112
+ const lines = [
113
+ ".cclaw/config.yaml",
114
+ ".cclaw/commands/*.md",
115
+ ".cclaw/skills/*/SKILL.md",
116
+ ".cclaw/state/*.json|*.jsonl",
117
+ ".cclaw/references/**",
118
+ "AGENTS.md (managed block)"
119
+ ];
120
+ for (const harness of harnesses) {
121
+ const adapter = HARNESS_ADAPTERS[harness];
122
+ lines.push(`${adapter.commandDir}/cc*.md`);
123
+ if (harness === "claude") {
124
+ lines.push(".claude/hooks/hooks.json");
125
+ }
126
+ if (harness === "cursor") {
127
+ lines.push(".cursor/hooks.json");
128
+ lines.push(".cursor/rules/cclaw-workflow.mdc");
129
+ }
130
+ if (harness === "codex") {
131
+ lines.push(".codex/hooks.json");
132
+ }
133
+ if (harness === "opencode") {
134
+ lines.push(".opencode/plugins/cclaw-plugin.mjs");
135
+ lines.push("opencode.json(.c) plugin registration");
136
+ }
137
+ }
138
+ return lines;
139
+ }
140
+ function inferTrackDefault(profile, track) {
141
+ if (track)
142
+ return track;
143
+ if (!profile)
144
+ return "standard";
145
+ return createProfileConfig(profile).defaultTrack ?? "standard";
146
+ }
147
+ async function promptInitConfig(defaults, ctx) {
148
+ const rl = createInterface({
149
+ input: process.stdin,
150
+ output: ctx.stdout
151
+ });
152
+ const pickSingle = async (label, options, fallback) => {
153
+ while (true) {
154
+ ctx.stdout.write(`\n${label}\n`);
155
+ options.forEach((option, index) => {
156
+ const marker = option === fallback ? " (default)" : "";
157
+ ctx.stdout.write(` ${index + 1}) ${option}${marker}\n`);
158
+ });
159
+ const answer = (await rl.question("> ")).trim();
160
+ if (answer.length === 0) {
161
+ return fallback;
162
+ }
163
+ const numeric = Number(answer);
164
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= options.length) {
165
+ return options[numeric - 1];
166
+ }
167
+ if (options.includes(answer)) {
168
+ return answer;
169
+ }
170
+ ctx.stdout.write("Invalid selection. Use option number or value.\n");
171
+ }
172
+ };
173
+ const pickHarnesses = async (fallback) => {
174
+ const fallbackText = fallback.join(",");
175
+ while (true) {
176
+ const answer = (await rl.question(`\nHarnesses (comma list from ${HARNESS_IDS.join(", ")}) [${fallbackText}]: `)).trim();
177
+ if (answer.length === 0) {
178
+ return fallback;
179
+ }
180
+ try {
181
+ const parsed = parseHarnesses(answer);
182
+ if (parsed.length === 0) {
183
+ ctx.stdout.write("Select at least one harness.\n");
184
+ continue;
185
+ }
186
+ return parsed;
187
+ }
188
+ catch (err) {
189
+ ctx.stdout.write(`${err instanceof Error ? err.message : "Invalid harness list"}\n`);
190
+ }
191
+ }
192
+ };
193
+ try {
194
+ const profile = await pickSingle("Select init profile:", INIT_PROFILES, defaults.profile);
195
+ const trackDefault = inferTrackDefault(profile, defaults.track);
196
+ const track = await pickSingle("Select default flow track:", FLOW_TRACKS, trackDefault);
197
+ const harnesses = await pickHarnesses(defaults.harnesses);
198
+ return { profile, track, harnesses };
199
+ }
200
+ finally {
201
+ rl.close();
202
+ }
203
+ }
204
+ async function resolveInitInputs(parsed, ctx) {
205
+ const detectedHarnesses = parsed.harnesses ? [] : await detectHarnesses(ctx.cwd);
206
+ const autoHarnesses = parsed.harnesses
207
+ ? parsed.harnesses
208
+ : (detectedHarnesses.length > 0 ? detectedHarnesses : undefined);
209
+ const promptRequested = parsed.interactive === true;
210
+ const promptForbidden = parsed.interactive === false;
211
+ const implicitPrompt = !promptForbidden &&
212
+ isInitPromptAllowed(ctx) &&
213
+ parsed.profile === undefined &&
214
+ parsed.track === undefined &&
215
+ parsed.harnesses === undefined;
216
+ const shouldPrompt = promptRequested || implicitPrompt;
217
+ if (!shouldPrompt) {
218
+ return {
219
+ profile: parsed.profile,
220
+ track: parsed.track,
221
+ harnesses: autoHarnesses,
222
+ detectedHarnesses
223
+ };
224
+ }
225
+ if (!isInitPromptAllowed(ctx)) {
226
+ throw new Error("Interactive init requires a TTY. Remove --interactive or run in a terminal.");
227
+ }
228
+ const defaults = {
229
+ profile: parsed.profile ?? "standard",
230
+ track: inferTrackDefault(parsed.profile, parsed.track),
231
+ harnesses: autoHarnesses ?? HARNESS_IDS.slice()
232
+ };
233
+ const prompted = await promptInitConfig(defaults, ctx);
234
+ return {
235
+ profile: prompted.profile,
236
+ track: prompted.track,
237
+ harnesses: prompted.harnesses,
238
+ detectedHarnesses
239
+ };
240
+ }
241
+ function parseDoctorOnly(raw) {
242
+ return raw
243
+ .split(",")
244
+ .map((item) => item.trim().toLowerCase())
245
+ .filter((item) => item.length > 0);
246
+ }
247
+ function filterDoctorChecks(checks, filters) {
248
+ if (!filters || filters.length === 0) {
249
+ return checks;
250
+ }
251
+ return checks.filter((check) => {
252
+ const name = check.name.toLowerCase();
253
+ return filters.some((filter) => {
254
+ if (filter === "error" || filter === "warning" || filter === "info") {
255
+ return check.severity === filter;
256
+ }
257
+ return name.includes(filter);
258
+ });
259
+ });
260
+ }
261
+ function doctorCountsBySeverity(checks) {
262
+ const result = {
263
+ error: { total: 0, failing: 0 },
264
+ warning: { total: 0, failing: 0 },
265
+ info: { total: 0, failing: 0 }
266
+ };
267
+ for (const check of checks) {
268
+ const bucket = result[check.severity];
269
+ bucket.total += 1;
270
+ if (!check.ok) {
271
+ bucket.failing += 1;
272
+ }
273
+ }
274
+ return result;
275
+ }
276
+ function printDoctorText(ctx, checks, options) {
277
+ const orderedSeverities = ["error", "warning", "info"];
278
+ const view = options.quiet ? checks.filter((check) => !check.ok) : checks;
279
+ for (const severity of orderedSeverities) {
280
+ const inBucket = view.filter((check) => check.severity === severity);
281
+ if (inBucket.length === 0)
282
+ continue;
283
+ ctx.stdout.write(`\n[${severity.toUpperCase()}]\n`);
284
+ for (const check of inBucket) {
285
+ const status = check.ok ? "PASS" : "FAIL";
286
+ ctx.stdout.write(`${status} ${check.name} :: ${check.summary}\n`);
287
+ if (!options.quiet) {
288
+ ctx.stdout.write(` details: ${check.details}\n`);
289
+ }
290
+ if (options.explain) {
291
+ ctx.stdout.write(` fix: ${check.fix}\n`);
292
+ if (check.docRef) {
293
+ ctx.stdout.write(` docs: ${check.docRef}\n`);
294
+ }
295
+ }
296
+ }
297
+ }
298
+ const counts = doctorCountsBySeverity(checks);
299
+ const failingErrors = checks.filter((check) => check.severity === "error" && !check.ok).length;
300
+ ctx.stdout.write(`\nTotals: error ${counts.error.failing}/${counts.error.total} failing, ` +
301
+ `warning ${counts.warning.failing}/${counts.warning.total} failing, ` +
302
+ `info ${counts.info.failing}/${counts.info.total} failing\n`);
303
+ if (failingErrors > 0) {
304
+ ctx.stdout.write(`Doctor status: BLOCKED (${failingErrors} failing error checks)\n`);
305
+ }
306
+ else {
307
+ ctx.stdout.write("Doctor status: HEALTHY (no failing error checks)\n");
308
+ }
309
+ }
97
310
  function parseArgs(argv) {
98
311
  const parsed = {};
99
312
  const helpFlag = argv.find((arg) => arg === "--help" || arg === "-h");
@@ -121,10 +334,38 @@ function parseArgs(argv) {
121
334
  parsed.profile = parseProfile(flag.replace("--profile=", ""));
122
335
  continue;
123
336
  }
337
+ if (flag === "--interactive") {
338
+ parsed.interactive = true;
339
+ continue;
340
+ }
341
+ if (flag === "--no-interactive") {
342
+ parsed.interactive = false;
343
+ continue;
344
+ }
345
+ if (flag === "--dry-run") {
346
+ parsed.dryRun = true;
347
+ continue;
348
+ }
124
349
  if (flag === "--reconcile-gates") {
125
350
  parsed.reconcileGates = true;
126
351
  continue;
127
352
  }
353
+ if (flag === "--json") {
354
+ parsed.doctorJson = true;
355
+ continue;
356
+ }
357
+ if (flag === "--explain") {
358
+ parsed.doctorExplain = true;
359
+ continue;
360
+ }
361
+ if (flag === "--quiet") {
362
+ parsed.doctorQuiet = true;
363
+ continue;
364
+ }
365
+ if (flag.startsWith("--only=")) {
366
+ parsed.doctorOnly = parseDoctorOnly(flag.replace("--only=", ""));
367
+ continue;
368
+ }
128
369
  if (flag.startsWith("--name=")) {
129
370
  parsed.archiveName = flag.replace("--name=", "").trim();
130
371
  }
@@ -146,14 +387,44 @@ async function runCommand(parsed, ctx) {
146
387
  return 1;
147
388
  }
148
389
  if (command === "init") {
390
+ const resolved = await resolveInitInputs(parsed, ctx);
391
+ const effectiveProfile = resolved.profile;
392
+ const effectiveTrack = resolved.track;
393
+ const effectiveHarnesses = resolved.harnesses;
394
+ if (parsed.dryRun === true) {
395
+ const previewConfig = effectiveProfile
396
+ ? createProfileConfig(effectiveProfile, {
397
+ harnesses: effectiveHarnesses,
398
+ defaultTrack: effectiveTrack
399
+ })
400
+ : createDefaultConfig(effectiveHarnesses, effectiveTrack);
401
+ const previewSurfaces = buildInitSurfacePreview(previewConfig.harnesses);
402
+ info(ctx, "Dry run: no files were written.");
403
+ if (resolved.detectedHarnesses.length > 0 && parsed.harnesses === undefined) {
404
+ info(ctx, `Detected harnesses from repo: ${resolved.detectedHarnesses.join(", ")}`);
405
+ }
406
+ ctx.stdout.write(`${JSON.stringify({
407
+ profile: effectiveProfile ?? "standard(default)",
408
+ track: previewConfig.defaultTrack ?? "standard",
409
+ harnesses: previewConfig.harnesses,
410
+ promptGuardMode: previewConfig.promptGuardMode,
411
+ gitHookGuards: previewConfig.gitHookGuards,
412
+ languageRulePacks: previewConfig.languageRulePacks,
413
+ generatedSurfaces: previewSurfaces
414
+ }, null, 2)}\n`);
415
+ return 0;
416
+ }
149
417
  await initCclaw({
150
418
  projectRoot: ctx.cwd,
151
- harnesses: parsed.harnesses,
152
- track: parsed.track,
153
- profile: parsed.profile
419
+ harnesses: effectiveHarnesses,
420
+ track: effectiveTrack,
421
+ profile: effectiveProfile
154
422
  });
155
- const profileNote = parsed.profile ? ` profile=${parsed.profile}` : "";
156
- const trackNote = parsed.track ? ` track=${parsed.track}` : "";
423
+ if (resolved.detectedHarnesses.length > 0 && parsed.harnesses === undefined) {
424
+ info(ctx, `Detected harnesses from repo: ${resolved.detectedHarnesses.join(", ")}`);
425
+ }
426
+ const profileNote = effectiveProfile ? ` profile=${effectiveProfile}` : "";
427
+ const trackNote = effectiveTrack ? ` track=${effectiveTrack}` : "";
157
428
  const suffix = profileNote || trackNote ? ` (${(profileNote + trackNote).trim()})` : "";
158
429
  info(ctx, `Initialized .cclaw runtime and generated harness shims${suffix}`);
159
430
  return 0;
@@ -167,8 +438,25 @@ async function runCommand(parsed, ctx) {
167
438
  const checks = await doctorChecks(ctx.cwd, {
168
439
  reconcileCurrentStageGates: parsed.reconcileGates === true
169
440
  });
170
- for (const check of checks) {
171
- ctx.stdout.write(`${check.ok ? "PASS" : "FAIL"} ${check.name} :: ${check.details}\n`);
441
+ const filteredChecks = filterDoctorChecks(checks, parsed.doctorOnly);
442
+ const explain = parsed.doctorExplain === true;
443
+ const quiet = parsed.doctorQuiet === true;
444
+ if (parsed.doctorJson === true) {
445
+ const counts = doctorCountsBySeverity(filteredChecks);
446
+ ctx.stdout.write(`${JSON.stringify({
447
+ ok: doctorSucceeded(checks),
448
+ filters: parsed.doctorOnly ?? [],
449
+ counts,
450
+ checks: filteredChecks
451
+ }, null, 2)}\n`);
452
+ }
453
+ else {
454
+ if (filteredChecks.length === 0) {
455
+ ctx.stdout.write("No checks matched the --only filter.\n");
456
+ }
457
+ else {
458
+ printDoctorText(ctx, filteredChecks, { explain, quiet });
459
+ }
172
460
  }
173
461
  return doctorSucceeded(checks) ? 0 : 2;
174
462
  }
package/dist/config.js CHANGED
@@ -19,7 +19,8 @@ const ALLOWED_CONFIG_KEYS = new Set([
19
19
  "promptGuardMode",
20
20
  "gitHookGuards",
21
21
  "defaultTrack",
22
- "languageRulePacks"
22
+ "languageRulePacks",
23
+ "trackHeuristics"
23
24
  ]);
24
25
  function configFixExample() {
25
26
  return `harnesses:
@@ -34,6 +35,21 @@ function configValidationError(configFilePath, reason) {
34
35
  `Example config:\n${configFixExample()}\n` +
35
36
  `After fixing, run: cclaw sync`);
36
37
  }
38
+ function isRecord(value) {
39
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40
+ }
41
+ function validateStringArray(value, fieldName, configFilePath) {
42
+ if (value === undefined)
43
+ return undefined;
44
+ if (!Array.isArray(value)) {
45
+ throw configValidationError(configFilePath, `"${fieldName}" must be an array of strings`);
46
+ }
47
+ const invalid = value.filter((item) => typeof item !== "string");
48
+ if (invalid.length > 0) {
49
+ throw configValidationError(configFilePath, `"${fieldName}" must contain only strings`);
50
+ }
51
+ return value;
52
+ }
37
53
  export function configPath(projectRoot) {
38
54
  return path.join(projectRoot, CONFIG_PATH);
39
55
  }
@@ -64,7 +80,7 @@ export function createProfileConfig(profile, overrides = {}) {
64
80
  autoAdvance: false,
65
81
  promptGuardMode: "advisory",
66
82
  gitHookGuards: false,
67
- defaultTrack: overrides.defaultTrack ?? "quick",
83
+ defaultTrack: overrides.defaultTrack ?? "medium",
68
84
  languageRulePacks: overrides.languageRulePacks ?? []
69
85
  };
70
86
  case "standard":
@@ -161,6 +177,69 @@ export async function readConfig(projectRoot) {
161
177
  throw configValidationError(fullPath, `unknown languageRulePacks id(s): ${formatted}`);
162
178
  }
163
179
  const languageRulePacks = [...new Set(rawPacks)];
180
+ const trackHeuristicsRaw = parsed.trackHeuristics;
181
+ let trackHeuristics = undefined;
182
+ if (Object.prototype.hasOwnProperty.call(parsed, "trackHeuristics")) {
183
+ if (!isRecord(trackHeuristicsRaw)) {
184
+ throw configValidationError(fullPath, `"trackHeuristics" must be an object`);
185
+ }
186
+ const fallbackRaw = trackHeuristicsRaw.fallback;
187
+ if (fallbackRaw !== undefined && (typeof fallbackRaw !== "string" || !FLOW_TRACK_SET.has(fallbackRaw))) {
188
+ throw configValidationError(fullPath, `"trackHeuristics.fallback" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
189
+ }
190
+ const priorityRaw = trackHeuristicsRaw.priority;
191
+ let priority;
192
+ if (priorityRaw !== undefined) {
193
+ if (!Array.isArray(priorityRaw)) {
194
+ throw configValidationError(fullPath, `"trackHeuristics.priority" must be an array`);
195
+ }
196
+ const invalidPriority = priorityRaw.filter((value) => typeof value !== "string" || !FLOW_TRACK_SET.has(value));
197
+ if (invalidPriority.length > 0) {
198
+ throw configValidationError(fullPath, `"trackHeuristics.priority" must contain only: ${SUPPORTED_TRACKS_TEXT}`);
199
+ }
200
+ priority = [...new Set(priorityRaw)];
201
+ }
202
+ const tracksRaw = trackHeuristicsRaw.tracks;
203
+ let tracks = undefined;
204
+ if (tracksRaw !== undefined) {
205
+ if (!isRecord(tracksRaw)) {
206
+ throw configValidationError(fullPath, `"trackHeuristics.tracks" must be an object`);
207
+ }
208
+ tracks = {};
209
+ for (const [trackName, ruleRaw] of Object.entries(tracksRaw)) {
210
+ if (!FLOW_TRACK_SET.has(trackName)) {
211
+ throw configValidationError(fullPath, `"trackHeuristics.tracks" contains unknown track "${trackName}". Supported: ${SUPPORTED_TRACKS_TEXT}`);
212
+ }
213
+ if (!isRecord(ruleRaw)) {
214
+ throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}" must be an object`);
215
+ }
216
+ const triggers = validateStringArray(ruleRaw.triggers, `trackHeuristics.tracks.${trackName}.triggers`, fullPath);
217
+ const patterns = validateStringArray(ruleRaw.patterns, `trackHeuristics.tracks.${trackName}.patterns`, fullPath);
218
+ const veto = validateStringArray(ruleRaw.veto, `trackHeuristics.tracks.${trackName}.veto`, fullPath);
219
+ if (patterns) {
220
+ for (const pattern of patterns) {
221
+ try {
222
+ // eslint-disable-next-line no-new
223
+ new RegExp(pattern, "iu");
224
+ }
225
+ catch {
226
+ throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}.patterns" contains invalid regex "${pattern}"`);
227
+ }
228
+ }
229
+ }
230
+ tracks[trackName] = {
231
+ triggers,
232
+ patterns,
233
+ veto
234
+ };
235
+ }
236
+ }
237
+ trackHeuristics = {
238
+ fallback: fallbackRaw,
239
+ priority,
240
+ tracks
241
+ };
242
+ }
164
243
  return {
165
244
  version: parsed.version ?? CCLAW_VERSION,
166
245
  flowVersion: parsed.flowVersion ?? FLOW_VERSION,
@@ -169,7 +248,8 @@ export async function readConfig(projectRoot) {
169
248
  promptGuardMode,
170
249
  gitHookGuards,
171
250
  defaultTrack,
172
- languageRulePacks
251
+ languageRulePacks,
252
+ trackHeuristics
173
253
  };
174
254
  }
175
255
  export async function writeConfig(projectRoot, config) {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Agent persona content for cclaw.
3
+ *
4
+ * cclaw materializes markdown agent definitions (`.md` with YAML frontmatter)
5
+ * under `.cclaw/agents/` for harness delegation. Research work that does not
6
+ * need isolated subagent context lives in `.cclaw/skills/research/*.md`
7
+ * playbooks and is executed in-thread by the primary agent.
8
+ */
9
+ export interface AgentDefinition {
10
+ /** Kebab-case identifier, e.g. `"reviewer"`. */
11
+ name: string;
12
+ /** When to invoke — include PROACTIVE / MUST BE USED guidance. */
13
+ description: string;
14
+ /** Allowed tools for this agent (harness-specific names). */
15
+ tools: string[];
16
+ /** Model tier for routing cost/latency vs depth. */
17
+ model: "fast" | "balanced" | "deep";
18
+ /** How the harness should treat activation relative to flow context. */
19
+ activation: "proactive" | "on-demand" | "mandatory";
20
+ /** cclaw flow stages this agent is designed to support. */
21
+ relatedStages: string[];
22
+ /** Markdown body rendered below the YAML frontmatter. */
23
+ body: string;
24
+ }
25
+ /**
26
+ * Canonical specialist roster (core-5) materialized under `.cclaw/agents/`.
27
+ */
28
+ export declare const CCLAW_AGENTS: AgentDefinition[];
29
+ /**
30
+ * Render a complete cclaw agent markdown file (YAML frontmatter + body).
31
+ */
32
+ export declare function agentMarkdown(agent: AgentDefinition): string;
33
+ /**
34
+ * Markdown table mapping cclaw stage entry points to specialist agents.
35
+ */
36
+ export declare function agentRoutingTable(): string;
37
+ /**
38
+ * Cost tier routing for the core-5 agent roster.
39
+ */
40
+ export declare function agentCostTierTable(): string;
41
+ /**
42
+ * AGENTS.md-ready section describing cclaw’s specialist delegation model.
43
+ */
44
+ export declare function agentsAgentsMdBlock(): string;