cclaw-cli 0.5.17 → 0.7.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.
@@ -2,7 +2,7 @@ import type { FlowStage } from "./types.js";
2
2
  export type DelegationEntry = {
3
3
  stage: string;
4
4
  agent: string;
5
- mode: "mandatory" | "proactive";
5
+ mode: "mandatory" | "proactive" | "conditional";
6
6
  status: "scheduled" | "completed" | "failed" | "waived";
7
7
  taskId?: string;
8
8
  waiverReason?: string;
@@ -12,6 +12,11 @@ export type DelegationEntry = {
12
12
  * consumers treat missing runId as unscoped (conservatively excluded from current-run checks).
13
13
  */
14
14
  runId?: string;
15
+ /**
16
+ * For `conditional` rows: the trigger predicate that fired (e.g. `diff_lines_gt:100`).
17
+ * Recorded for audit so reviewers can see why the second pass was required.
18
+ */
19
+ conditionTrigger?: string;
15
20
  };
16
21
  export type DelegationLedger = {
17
22
  runId: string;
@@ -14,7 +14,7 @@ function isDelegationEntry(value) {
14
14
  if (!value || typeof value !== "object" || Array.isArray(value))
15
15
  return false;
16
16
  const o = value;
17
- const modeOk = o.mode === "mandatory" || o.mode === "proactive";
17
+ const modeOk = o.mode === "mandatory" || o.mode === "proactive" || o.mode === "conditional";
18
18
  const statusOk = o.status === "scheduled" ||
19
19
  o.status === "completed" ||
20
20
  o.status === "failed" ||
@@ -26,7 +26,8 @@ function isDelegationEntry(value) {
26
26
  typeof o.ts === "string" &&
27
27
  (o.taskId === undefined || typeof o.taskId === "string") &&
28
28
  (o.waiverReason === undefined || typeof o.waiverReason === "string") &&
29
- (o.runId === undefined || typeof o.runId === "string"));
29
+ (o.runId === undefined || typeof o.runId === "string") &&
30
+ (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string"));
30
31
  }
31
32
  function parseLedger(raw, runId) {
32
33
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
package/dist/doctor.js CHANGED
@@ -11,11 +11,13 @@ import { gitignoreHasRequiredPatterns } from "./gitignore.js";
11
11
  import { HARNESS_ADAPTERS, CCLAW_MARKER_START, CCLAW_MARKER_END } from "./harness-adapters.js";
12
12
  import { policyChecks } from "./policy.js";
13
13
  import { readFlowState } from "./runs.js";
14
+ import { skippedStagesForTrack } from "./flow-state.js";
15
+ import { TRACK_STAGES } from "./types.js";
14
16
  import { checkMandatoryDelegations } from "./delegation.js";
15
17
  import { buildTraceMatrix } from "./trace-matrix.js";
16
18
  import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
17
19
  import { stageSkillFolder } from "./content/skills.js";
18
- import { UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
20
+ import { LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
19
21
  import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
20
22
  import { validateHookDocument } from "./hook-schema.js";
21
23
  const execFileAsync = promisify(execFile);
@@ -406,6 +408,18 @@ export async function doctorChecks(projectRoot, options = {}) {
406
408
  details: skillPath
407
409
  });
408
410
  }
411
+ // Opt-in language rule packs: only check presence for packs the user enabled.
412
+ for (const pack of parsedConfig?.languageRulePacks ?? []) {
413
+ const folder = LANGUAGE_RULE_PACK_FOLDERS[pack];
414
+ if (!folder)
415
+ continue;
416
+ const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", folder, "SKILL.md");
417
+ checks.push({
418
+ name: `language_rule_pack:${pack}`,
419
+ ok: await exists(skillPath),
420
+ details: skillPath
421
+ });
422
+ }
409
423
  // Agent definition files
410
424
  for (const agent of CCLAW_AGENTS) {
411
425
  const agentPath = path.join(projectRoot, RUNTIME_ROOT, "agents", `${agent.name}.md`);
@@ -743,6 +757,29 @@ export async function doctorChecks(projectRoot, options = {}) {
743
757
  ok: activeRunId.length > 0,
744
758
  details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
745
759
  });
760
+ const activeTrack = flowState.track ?? "standard";
761
+ const trackStageList = TRACK_STAGES[activeTrack];
762
+ const skippedFromState = Array.isArray(flowState.skippedStages) ? flowState.skippedStages : [];
763
+ const expectedSkipped = skippedStagesForTrack(activeTrack);
764
+ const skippedConsistent = expectedSkipped.length === skippedFromState.length &&
765
+ expectedSkipped.every((stage) => skippedFromState.includes(stage));
766
+ checks.push({
767
+ name: "flow_state:track",
768
+ ok: skippedConsistent,
769
+ details: skippedConsistent
770
+ ? `active track "${activeTrack}" (${trackStageList.length}/${COMMAND_FILE_ORDER.length} stages: ${trackStageList.join(" → ")})${expectedSkipped.length > 0 ? `; skippedStages=${expectedSkipped.join(", ")}` : ""}`
771
+ : `track "${activeTrack}" expects skippedStages=[${expectedSkipped.join(", ")}] but flow-state has [${skippedFromState.join(", ")}] — run \`cclaw sync\` to repair`
772
+ });
773
+ checks.push({
774
+ name: "flow_state:track_completed_in_track",
775
+ ok: flowState.completedStages.every((stage) => trackStageList.includes(stage) || expectedSkipped.includes(stage)),
776
+ details: (() => {
777
+ const offTrack = flowState.completedStages.filter((stage) => !trackStageList.includes(stage) && !expectedSkipped.includes(stage));
778
+ return offTrack.length === 0
779
+ ? `every completed stage belongs to track "${activeTrack}" or its skipped set`
780
+ : `completed stages contain entries outside track "${activeTrack}" and not in skipped set: ${offTrack.join(", ")}`;
781
+ })()
782
+ });
746
783
  checks.push({
747
784
  name: "artifacts:active_root",
748
785
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "artifacts")),
@@ -1,4 +1,4 @@
1
- import type { FlowStage, TransitionRule } from "./types.js";
1
+ import type { FlowStage, FlowTrack, TransitionRule } from "./types.js";
2
2
  export declare const TRANSITION_RULES: TransitionRule[];
3
3
  export interface StageGateState {
4
4
  required: string[];
@@ -11,9 +11,21 @@ export interface FlowState {
11
11
  completedStages: FlowStage[];
12
12
  guardEvidence: Record<string, string>;
13
13
  stageGateCatalog: Record<FlowStage, StageGateState>;
14
+ /** Active flow track (determines which stages are in the critical path for this run). */
15
+ track: FlowTrack;
16
+ /** Stages explicitly skipped for this track (empty for standard; populated for quick). */
17
+ skippedStages: FlowStage[];
14
18
  }
15
- export declare function createInitialFlowState(activeRunId?: string): FlowState;
19
+ export interface InitialFlowStateOptions {
20
+ activeRunId?: string;
21
+ track?: FlowTrack;
22
+ }
23
+ export declare function isFlowTrack(value: unknown): value is FlowTrack;
24
+ export declare function trackStages(track: FlowTrack): FlowStage[];
25
+ export declare function skippedStagesForTrack(track: FlowTrack): FlowStage[];
26
+ export declare function firstStageForTrack(track: FlowTrack): FlowStage;
27
+ export declare function createInitialFlowState(activeRunIdOrOptions?: string | InitialFlowStateOptions, maybeTrack?: FlowTrack): FlowState;
16
28
  export declare function canTransition(from: FlowStage, to: FlowStage): boolean;
17
29
  export declare function getTransitionGuards(from: FlowStage, to: FlowStage): string[];
18
- export declare function nextStage(stage: FlowStage): FlowStage | null;
19
- export declare function previousStage(stage: FlowStage): FlowStage | null;
30
+ export declare function nextStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
31
+ export declare function previousStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
@@ -1,7 +1,28 @@
1
1
  import { COMMAND_FILE_ORDER } from "./constants.js";
2
2
  import { buildTransitionRules, orderedStageSchemas, stageGateIds } from "./content/stage-schema.js";
3
+ import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "./types.js";
3
4
  export const TRANSITION_RULES = buildTransitionRules();
4
- export function createInitialFlowState(activeRunId = "active") {
5
+ export function isFlowTrack(value) {
6
+ return typeof value === "string" && FLOW_TRACKS.includes(value);
7
+ }
8
+ export function trackStages(track) {
9
+ return [...TRACK_STAGES[track]];
10
+ }
11
+ export function skippedStagesForTrack(track) {
12
+ const inTrack = new Set(TRACK_STAGES[track]);
13
+ return FLOW_STAGES.filter((stage) => !inTrack.has(stage));
14
+ }
15
+ export function firstStageForTrack(track) {
16
+ const stages = TRACK_STAGES[track];
17
+ return stages[0] ?? "brainstorm";
18
+ }
19
+ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTrack) {
20
+ const options = typeof activeRunIdOrOptions === "string"
21
+ ? { activeRunId: activeRunIdOrOptions, track: maybeTrack }
22
+ : activeRunIdOrOptions;
23
+ const activeRunId = options.activeRunId ?? "active";
24
+ const track = options.track ?? "standard";
25
+ const skippedStages = skippedStagesForTrack(track);
5
26
  const stageGateCatalog = {};
6
27
  for (const schema of orderedStageSchemas()) {
7
28
  stageGateCatalog[schema.stage] = {
@@ -12,10 +33,12 @@ export function createInitialFlowState(activeRunId = "active") {
12
33
  }
13
34
  return {
14
35
  activeRunId,
15
- currentStage: "brainstorm",
36
+ currentStage: firstStageForTrack(track),
16
37
  completedStages: [],
17
38
  guardEvidence: {},
18
- stageGateCatalog
39
+ stageGateCatalog,
40
+ track,
41
+ skippedStages
19
42
  };
20
43
  }
21
44
  export function canTransition(from, to) {
@@ -25,17 +48,33 @@ export function getTransitionGuards(from, to) {
25
48
  const match = TRANSITION_RULES.find((rule) => rule.from === from && rule.to === to);
26
49
  return match ? [...match.guards] : [];
27
50
  }
28
- export function nextStage(stage) {
29
- const index = COMMAND_FILE_ORDER.indexOf(stage);
30
- if (index < 0 || index === COMMAND_FILE_ORDER.length - 1) {
51
+ export function nextStage(stage, track = "standard") {
52
+ const ordered = TRACK_STAGES[track];
53
+ const index = ordered.indexOf(stage);
54
+ if (index < 0) {
55
+ const fallback = COMMAND_FILE_ORDER.indexOf(stage);
56
+ if (fallback < 0 || fallback === COMMAND_FILE_ORDER.length - 1) {
57
+ return null;
58
+ }
59
+ return COMMAND_FILE_ORDER[fallback + 1];
60
+ }
61
+ if (index === ordered.length - 1) {
31
62
  return null;
32
63
  }
33
- return COMMAND_FILE_ORDER[index + 1];
64
+ return ordered[index + 1];
34
65
  }
35
- export function previousStage(stage) {
36
- const index = COMMAND_FILE_ORDER.indexOf(stage);
37
- if (index <= 0) {
66
+ export function previousStage(stage, track = "standard") {
67
+ const ordered = TRACK_STAGES[track];
68
+ const index = ordered.indexOf(stage);
69
+ if (index === 0) {
38
70
  return null;
39
71
  }
40
- return COMMAND_FILE_ORDER[index - 1];
72
+ if (index < 0) {
73
+ const fallback = COMMAND_FILE_ORDER.indexOf(stage);
74
+ if (fallback <= 0) {
75
+ return null;
76
+ }
77
+ return COMMAND_FILE_ORDER[fallback - 1];
78
+ }
79
+ return ordered[index - 1];
41
80
  }
@@ -124,6 +124,7 @@ export async function syncHarnessShims(projectRoot, harnesses) {
124
124
  await writeFileSafe(path.join(commandDir, "cc.md"), utilityShimContent(harness, "cc", "flow-start", "start.md"));
125
125
  await writeFileSafe(path.join(commandDir, "cc-next.md"), utilityShimContent(harness, "next", "flow-next-step", "next.md"));
126
126
  await writeFileSafe(path.join(commandDir, "cc-learn.md"), utilityShimContent(harness, "learn", "learnings", "learn.md"));
127
+ await writeFileSafe(path.join(commandDir, "cc-status.md"), utilityShimContent(harness, "status", "flow-status", "status.md"));
127
128
  }
128
129
  await syncAgentFiles(projectRoot);
129
130
  await syncAgentsMd(projectRoot);
package/dist/install.d.ts CHANGED
@@ -1,7 +1,10 @@
1
- import type { HarnessId } from "./types.js";
1
+ import type { FlowTrack, HarnessId, InitProfile } from "./types.js";
2
2
  export interface InitOptions {
3
3
  projectRoot: string;
4
4
  harnesses?: HarnessId[];
5
+ track?: FlowTrack;
6
+ /** When set, pre-fills config defaults from the named profile before applying flag overrides. */
7
+ profile?: InitProfile;
5
8
  }
6
9
  export declare function initCclaw(options: InitOptions): Promise<void>;
7
10
  export declare function syncCclaw(projectRoot: string): Promise<void>;
package/dist/install.js CHANGED
@@ -3,20 +3,21 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
6
- import { writeConfig, createDefaultConfig, readConfig, configPath } from "./config.js";
6
+ import { writeConfig, createDefaultConfig, createProfileConfig, readConfig, configPath } from "./config.js";
7
7
  import { commandContract } from "./content/contracts.js";
8
8
  import { contextModeFiles, createInitialContextModeState } from "./content/contexts.js";
9
9
  import { learnSkillMarkdown, learnCommandContract } from "./content/learnings.js";
10
10
  import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
11
11
  import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
12
+ import { statusCommandContract, statusCommandSkillMarkdown } from "./content/status-command.js";
12
13
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
13
14
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
14
- import { sessionStartScript, stopCheckpointScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
15
+ import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
15
16
  import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
16
17
  import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
17
18
  import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
18
19
  import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
19
- import { UTILITY_SKILL_FOLDERS, UTILITY_SKILL_MAP } from "./content/utility-skills.js";
20
+ import { LANGUAGE_RULE_PACK_FOLDERS, LANGUAGE_RULE_PACK_GENERATORS, UTILITY_SKILL_FOLDERS, UTILITY_SKILL_MAP } from "./content/utility-skills.js";
20
21
  import { createInitialFlowState } from "./flow-state.js";
21
22
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
22
23
  import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
@@ -164,7 +165,7 @@ async function writeArtifactTemplates(projectRoot) {
164
165
  await writeFileSafe(runtimePath(projectRoot, "templates", fileName), content);
165
166
  }
166
167
  }
167
- async function writeSkills(projectRoot) {
168
+ async function writeSkills(projectRoot, config) {
168
169
  for (const stage of COMMAND_FILE_ORDER) {
169
170
  const folder = stageSkillFolder(stage);
170
171
  await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), stageSkillMarkdown(stage));
@@ -173,6 +174,7 @@ async function writeSkills(projectRoot) {
173
174
  await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
174
175
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-next-step", "SKILL.md"), nextCommandSkillMarkdown());
175
176
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-start", "SKILL.md"), startCommandSkillMarkdown());
177
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-status", "SKILL.md"), statusCommandSkillMarkdown());
176
178
  await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
177
179
  await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
178
180
  await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
@@ -181,11 +183,20 @@ async function writeSkills(projectRoot) {
181
183
  const generator = UTILITY_SKILL_MAP[folder];
182
184
  await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), generator());
183
185
  }
186
+ const enabledPacks = config?.languageRulePacks ?? [];
187
+ for (const pack of enabledPacks) {
188
+ const folder = LANGUAGE_RULE_PACK_FOLDERS[pack];
189
+ const generator = LANGUAGE_RULE_PACK_GENERATORS[folder];
190
+ if (!folder || !generator)
191
+ continue;
192
+ await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), generator());
193
+ }
184
194
  }
185
195
  async function writeUtilityCommands(projectRoot) {
186
196
  await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
187
197
  await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
188
198
  await writeFileSafe(runtimePath(projectRoot, "commands", "start.md"), startCommandContract());
199
+ await writeFileSafe(runtimePath(projectRoot, "commands", "status.md"), statusCommandContract());
189
200
  }
190
201
  function toObject(value) {
191
202
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -482,6 +493,7 @@ async function writeHooks(projectRoot, config) {
482
493
  await ensureDir(hooksDir);
483
494
  await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
484
495
  await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
496
+ await writeFileSafe(path.join(hooksDir, "pre-compact.sh"), preCompactScript());
485
497
  await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
486
498
  strictMode: config.promptGuardMode === "strict"
487
499
  }));
@@ -493,6 +505,7 @@ async function writeHooks(projectRoot, config) {
493
505
  for (const script of [
494
506
  "session-start.sh",
495
507
  "stop-checkpoint.sh",
508
+ "pre-compact.sh",
496
509
  "prompt-guard.sh",
497
510
  "workflow-guard.sh",
498
511
  "context-monitor.sh",
@@ -542,6 +555,151 @@ async function ensureKnowledgeStore(projectRoot) {
542
555
  await writeFileSafe(storePath, "# Project Knowledge\n\n");
543
556
  }
544
557
  }
558
+ async function ensureCustomSkillsScaffold(projectRoot) {
559
+ const customDir = runtimePath(projectRoot, "custom-skills");
560
+ await ensureDir(customDir);
561
+ const readmePath = path.join(customDir, "README.md");
562
+ if (!(await exists(readmePath))) {
563
+ await writeFileSafe(readmePath, CUSTOM_SKILLS_README);
564
+ }
565
+ const examplePath = path.join(customDir, "example", "SKILL.md");
566
+ if (!(await exists(examplePath))) {
567
+ await writeFileSafe(examplePath, CUSTOM_SKILLS_EXAMPLE);
568
+ }
569
+ }
570
+ const CUSTOM_SKILLS_README = `# Custom Skills (sync-safe)
571
+
572
+ This directory is **never overwritten** by \`cclaw sync\` or \`cclaw upgrade\`. Use it
573
+ to add project-specific skills that complement the managed skills under
574
+ \`.cclaw/skills/\`.
575
+
576
+ ## When to add a custom skill
577
+
578
+ - A repeatable lens specific to **this project** (e.g. "billing-domain", "kafka-message-contracts").
579
+ - A team convention you want every agent session to load.
580
+ - A domain checklist that does not generalize to other projects.
581
+
582
+ If the skill is general (security, performance, accessibility, etc.) prefer
583
+ contributing it upstream instead — the managed skills receive maintenance.
584
+
585
+ ## File format — public API (stable contract)
586
+
587
+ Each skill lives at \`.cclaw/custom-skills/<folder>/SKILL.md\`. The format is a
588
+ **stable public API**: \`cclaw sync\` and \`cclaw upgrade\` will not rewrite
589
+ custom skills, and the fields below are guaranteed to be respected by the
590
+ meta-skill router and the stage hooks.
591
+
592
+ ### Frontmatter (YAML, required)
593
+
594
+ \`\`\`yaml
595
+ ---
596
+ # Required fields
597
+ name: <kebab-case-skill-name>
598
+ description: >
599
+ One sentence (≤180 chars) that triggers semantic routing. Include the
600
+ concrete situation and the expected action
601
+ (e.g. "Audit Kafka topic contracts when a producer or consumer signature changes").
602
+
603
+ # Optional fields (omit when not applicable)
604
+ stages: [design, spec, tdd, review] # flow stages this skill applies to
605
+ triggers:
606
+ - "kafka topic"
607
+ - "producer.schema"
608
+ - "consumer.schema"
609
+ hardGate: false # true => skill body MUST include a ## HARD-GATE section
610
+ owners: ["@team-messaging"] # informational routing hint, not enforced
611
+ version: 0.1.0 # semver; bump when hardGate or algorithm changes
612
+ ---
613
+ \`\`\`
614
+
615
+ ### Field contract
616
+
617
+ | Field | Type | Required | Meaning |
618
+ |---|---|---|---|
619
+ | \`name\` | string (kebab-case) | yes | Unique id used by the router and by \`/cc-status\` diagnostics. |
620
+ | \`description\` | string ≤180 chars (single line OR YAML \`>\` folded) | yes | Drives semantic routing. Include trigger + action. |
621
+ | \`stages\` | array of flow stages | no | When present, the meta-skill only surfaces this skill during those stages. Omit for "any stage". |
622
+ | \`triggers\` | array of strings | no | Extra literal substrings that route to this skill when found in the user prompt or the active artifact. |
623
+ | \`hardGate\` | boolean | no | When \`true\`, the body MUST include a \`## HARD-GATE\` section; the agent treats the rule as non-skippable. |
624
+ | \`owners\` | array of strings | no | Informational only — surfaced to the user, never enforced. |
625
+ | \`version\` | semver string | no | Bump when you change the HARD-GATE or algorithm so reviewers can spot changes. |
626
+
627
+ ### Body sections (markdown, recommended order)
628
+
629
+ \`\`\`markdown
630
+ # <Skill title>
631
+
632
+ ## Overview
633
+ One-paragraph summary; context for when this skill is loaded.
634
+
635
+ ## When to use
636
+ - Bullet list of situations where this skill adds value.
637
+
638
+ ## When NOT to use
639
+ - Situations where loading this skill is context bloat or wrong.
640
+
641
+ ## HARD-GATE (REQUIRED when frontmatter hardGate: true)
642
+ Phrase it as a refusal:
643
+ > Do not <X> while <Y>.
644
+
645
+ ## Algorithm / checklist
646
+ 1. Concrete, observable steps with evidence (file:line, artifact, or knowledge entry).
647
+
648
+ ## Output protocol
649
+ Where the artifact / chat output lives and what shape it takes.
650
+
651
+ ## Anti-patterns
652
+ - Common failure modes to reject.
653
+ \`\`\`
654
+
655
+ ### Stage association semantics
656
+
657
+ - \`stages: []\` or missing → skill is available at any stage. The meta-skill still only surfaces it when \`description\` or \`triggers\` match the prompt.
658
+ - \`stages: [review]\` → skill is offered only during the review stage.
659
+ - Custom skills **never** become mandatory delegations. They are opt-in lenses. If you need a mandatory dispatch, add a proper managed specialist under \`.cclaw/skills/\` instead.
660
+
661
+ ## Routing
662
+
663
+ Custom skills are surfaced via the \`using-cclaw\` meta-skill at session start.
664
+ Mention the skill name in your prompt or let the agent semantic-route to it
665
+ based on the description + triggers + stages frontmatter.
666
+
667
+ ## Versioning & removal
668
+
669
+ Custom skills are user-owned. Bump \`version\` when you change the HARD-GATE or
670
+ algorithm; delete or edit them at any time — \`cclaw sync\` will not touch them.
671
+ `;
672
+ const CUSTOM_SKILLS_EXAMPLE = `---
673
+ name: example-custom-skill
674
+ description: "Replace this with a one-sentence description that triggers when the skill should be used. Delete or rename this folder when you add a real skill."
675
+ ---
676
+
677
+ # Example Custom Skill
678
+
679
+ This is a placeholder. Use it as a starting template, then delete or rename
680
+ the \`example/\` folder.
681
+
682
+ ## When to use
683
+
684
+ - A real, repeatable situation in **this** project that needs a consistent lens.
685
+
686
+ ## HARD-GATE (optional)
687
+
688
+ Drop this section if no hard rule applies. Keep it crisp:
689
+
690
+ > Do not <X> while <Y>.
691
+
692
+ ## Algorithm
693
+
694
+ 1. Step one — observable, file:line evidence required.
695
+ 2. Step two — produce a named artifact, not a vibe.
696
+ 3. Step three — escalate / hand off / record knowledge entry.
697
+
698
+ ## Anti-patterns
699
+
700
+ - Treating this skill as advisory when the situation matches the trigger.
701
+ - Loading this skill when the situation clearly does not match (context bloat).
702
+ `;
545
703
  async function ensureSessionStateFiles(projectRoot) {
546
704
  const stateDir = runtimePath(projectRoot, "state");
547
705
  await ensureDir(stateDir);
@@ -623,12 +781,12 @@ async function syncDisabledHarnessArtifacts(projectRoot, harnesses) {
623
781
  await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
624
782
  }
625
783
  }
626
- async function writeState(projectRoot, forceReset = false) {
784
+ async function writeState(projectRoot, config, forceReset = false) {
627
785
  const statePath = runtimePath(projectRoot, "state", "flow-state.json");
628
786
  if (!forceReset && (await exists(statePath))) {
629
787
  return;
630
788
  }
631
- const state = createInitialFlowState();
789
+ const state = createInitialFlowState("active", config.defaultTrack ?? "standard");
632
790
  await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`);
633
791
  }
634
792
  async function writeAdapterManifest(projectRoot, harnesses) {
@@ -736,15 +894,16 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
736
894
  await cleanStaleFiles(projectRoot);
737
895
  await writeCommandContracts(projectRoot);
738
896
  await writeUtilityCommands(projectRoot);
739
- await writeSkills(projectRoot);
897
+ await writeSkills(projectRoot, config);
740
898
  await writeContextModes(projectRoot);
741
899
  await writeArtifactTemplates(projectRoot);
742
900
  await writeRulebook(projectRoot);
743
- await writeState(projectRoot, forceStateReset);
901
+ await writeState(projectRoot, config, forceStateReset);
744
902
  await ensureRunSystem(projectRoot, { createIfMissing: false });
745
903
  await ensureSessionStateFiles(projectRoot);
746
904
  await writeAdapterManifest(projectRoot, harnesses);
747
905
  await ensureKnowledgeStore(projectRoot);
906
+ await ensureCustomSkillsScaffold(projectRoot);
748
907
  await writeHooks(projectRoot, config);
749
908
  await syncDisabledHarnessArtifacts(projectRoot, harnesses);
750
909
  await syncManagedGitHooks(projectRoot, config);
@@ -753,7 +912,12 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
753
912
  await ensureGitignore(projectRoot);
754
913
  }
755
914
  export async function initCclaw(options) {
756
- const config = createDefaultConfig(options.harnesses);
915
+ const config = options.profile
916
+ ? createProfileConfig(options.profile, {
917
+ harnesses: options.harnesses,
918
+ defaultTrack: options.track
919
+ })
920
+ : createDefaultConfig(options.harnesses, options.track);
757
921
  await writeConfig(options.projectRoot, config);
758
922
  await materializeRuntime(options.projectRoot, config, true);
759
923
  }
@@ -828,7 +992,7 @@ function stripManagedHookCommands(value) {
828
992
  }
829
993
  function isManagedRuntimeHookCommand(command) {
830
994
  const normalized = command.trim().replace(/\s+/gu, " ");
831
- return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized);
995
+ return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized);
832
996
  }
833
997
  async function removeManagedHookEntries(hookFilePath) {
834
998
  if (!(await exists(hookFilePath)))
package/dist/policy.js CHANGED
@@ -85,7 +85,8 @@ export async function policyChecks(projectRoot, options = {}) {
85
85
  // --- utility skill checks ---
86
86
  const runtimeFile = (relativePath) => `${RUNTIME_ROOT}/${relativePath}`;
87
87
  const utilitySkillChecks = [
88
- { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Entry format (append-only)", name: "utility_skill:learnings:entry_format" },
88
+ { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Entry format", name: "utility_skill:learnings:entry_format" },
89
+ { file: runtimeFile("skills/learnings/SKILL.md"), needle: "knowledge.jsonl", name: "utility_skill:learnings:jsonl_mirror" },
89
90
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Subcommands", name: "utility_skill:learnings:subcommands" },
90
91
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:learnings:hard_gate" },
91
92
  { file: runtimeFile("commands/learn.md"), needle: "## Subcommands", name: "utility_command:learn:subcommands" },
package/dist/runs.d.ts CHANGED
@@ -24,6 +24,13 @@ export interface ArchiveRunResult {
24
24
  featureName: string;
25
25
  resetState: FlowState;
26
26
  snapshottedStateFiles: string[];
27
+ /** Knowledge curation hint: total active entries + soft threshold (50). */
28
+ knowledge: {
29
+ activeEntryCount: number;
30
+ softThreshold: number;
31
+ overThreshold: boolean;
32
+ knowledgePath: string;
33
+ };
27
34
  }
28
35
  export interface ArchiveManifest {
29
36
  version: 1;
@@ -48,4 +55,12 @@ export declare function writeFlowState(projectRoot: string, state: FlowState, op
48
55
  export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
49
56
  export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
50
57
  export declare function archiveRun(projectRoot: string, featureName?: string): Promise<ArchiveRunResult>;
58
+ /**
59
+ * Counts active (non-superseded) knowledge entries.
60
+ * An entry is a markdown H3 heading with the canonical timestamped format produced by
61
+ * `learn add` / `learn curate`. Entries marked `Supersedes:` themselves are still active;
62
+ * this helper does not currently follow supersession chains beyond raw count, which is
63
+ * deliberate — the curator reads the file directly to make the soft-archive plan.
64
+ */
65
+ export declare function countActiveKnowledgeEntries(text: string): number;
51
66
  export {};
package/dist/runs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { COMMAND_FILE_ORDER, RUNTIME_ROOT } from "./constants.js";
4
- import { canTransition, createInitialFlowState } from "./flow-state.js";
4
+ import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack } from "./flow-state.js";
5
5
  import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
6
6
  export class InvalidStageTransitionError extends Error {
7
7
  from;
@@ -156,8 +156,27 @@ function sanitizeStageGateCatalog(value, fallback) {
156
156
  }
157
157
  return next;
158
158
  }
159
+ function coerceTrack(value) {
160
+ return isFlowTrack(value) ? value : "standard";
161
+ }
162
+ function sanitizeSkippedStages(value, track) {
163
+ const trackDefault = skippedStagesForTrack(track);
164
+ if (!Array.isArray(value)) {
165
+ return trackDefault;
166
+ }
167
+ const seen = new Set();
168
+ const out = [];
169
+ for (const raw of value) {
170
+ if (isFlowStage(raw) && !seen.has(raw)) {
171
+ seen.add(raw);
172
+ out.push(raw);
173
+ }
174
+ }
175
+ return out.length > 0 ? out : trackDefault;
176
+ }
159
177
  function coerceFlowState(parsed) {
160
- const next = createInitialFlowState();
178
+ const track = coerceTrack(parsed.track);
179
+ const next = createInitialFlowState("active", track);
161
180
  const activeRunIdRaw = parsed.activeRunId;
162
181
  const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
163
182
  ? activeRunIdRaw.trim()
@@ -167,7 +186,9 @@ function coerceFlowState(parsed) {
167
186
  currentStage: isFlowStage(parsed.currentStage) ? parsed.currentStage : next.currentStage,
168
187
  completedStages: sanitizeCompletedStages(parsed.completedStages),
169
188
  guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
170
- stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog)
189
+ stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
190
+ track,
191
+ skippedStages: sanitizeSkippedStages(parsed.skippedStages, track)
171
192
  };
172
193
  }
173
194
  function toArchiveDate(date = new Date()) {
@@ -373,12 +394,46 @@ export async function archiveRun(projectRoot, featureName) {
373
394
  snapshottedStateFiles
374
395
  };
375
396
  await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
397
+ const knowledgeStats = await readKnowledgeStats(projectRoot);
376
398
  return {
377
399
  archiveId,
378
400
  archivePath,
379
401
  archivedAt,
380
402
  featureName: feature,
381
403
  resetState,
382
- snapshottedStateFiles
404
+ snapshottedStateFiles,
405
+ knowledge: knowledgeStats
383
406
  };
384
407
  }
408
+ const KNOWLEDGE_SOFT_THRESHOLD = 50;
409
+ async function readKnowledgeStats(projectRoot) {
410
+ const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.md");
411
+ let activeEntryCount = 0;
412
+ if (await exists(knowledgePath)) {
413
+ const text = await fs.readFile(knowledgePath, "utf8");
414
+ activeEntryCount = countActiveKnowledgeEntries(text);
415
+ }
416
+ return {
417
+ activeEntryCount,
418
+ softThreshold: KNOWLEDGE_SOFT_THRESHOLD,
419
+ overThreshold: activeEntryCount > KNOWLEDGE_SOFT_THRESHOLD,
420
+ knowledgePath: `${RUNTIME_ROOT}/knowledge.md`
421
+ };
422
+ }
423
+ /**
424
+ * Counts active (non-superseded) knowledge entries.
425
+ * An entry is a markdown H3 heading with the canonical timestamped format produced by
426
+ * `learn add` / `learn curate`. Entries marked `Supersedes:` themselves are still active;
427
+ * this helper does not currently follow supersession chains beyond raw count, which is
428
+ * deliberate — the curator reads the file directly to make the soft-archive plan.
429
+ */
430
+ export function countActiveKnowledgeEntries(text) {
431
+ const lines = text.split(/\r?\n/);
432
+ let count = 0;
433
+ for (const line of lines) {
434
+ if (/^###\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\s+\[(rule|pattern|lesson|compound)\]/u.test(line)) {
435
+ count += 1;
436
+ }
437
+ }
438
+ return count;
439
+ }