cclaw-cli 0.36.0 → 0.38.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 CHANGED
@@ -229,27 +229,30 @@ Each critical-path stage produces a dated artifact under
229
229
  bundle into `.cclaw/runs/<YYYY-MM-DD-slug>/` and resets the active flow
230
230
  for the next feature.
231
231
 
232
- ### Track heuristics are configurable
232
+ ### Track heuristics are configurable (advisory)
233
233
 
234
234
  Every team has its own vocabulary. Override the built-in trigger lists in
235
235
  `.cclaw/config.yaml`:
236
236
 
237
237
  ```yaml
238
238
  trackHeuristics:
239
- priority: [standard, medium, quick]
240
239
  fallback: standard
241
240
  tracks:
242
241
  quick:
243
242
  triggers: [hotfix, rollback, prod-incident]
244
- veto: [schema, migration] # never route quick even if one trigger hits
243
+ veto: [schema, migration] # never route quick even if a trigger hits
245
244
  standard:
246
- patterns:
247
- - "^epic:"
248
- - "platform-team|core-infra"
245
+ triggers: [epic, platform-team, core-infra]
249
246
  ```
250
247
 
251
- `priority` + `veto` + regex `patterns` give you deterministic routing
252
- without touching any code.
248
+ Honest caveat: this config is **advisory**. cclaw surfaces these lists in
249
+ the `/cc` skill and contract prose so the LLM applies them during
250
+ classification — there is no Node-level router that mechanically enforces
251
+ the outcome. That is why the knobs are deliberately minimal: per-track
252
+ `triggers` + `veto` on top of defaults, plus `fallback`. Evaluation order is
253
+ fixed (`standard -> medium -> quick`, narrow-to-broad); regex `patterns`
254
+ and a `priority` override were removed in v0.38.0 because nothing in
255
+ runtime consumed them.
253
256
 
254
257
  ### Mid-flow reclassification
255
258
 
package/dist/config.js CHANGED
@@ -160,17 +160,8 @@ export async function readConfig(projectRoot) {
160
160
  if (fallbackRaw !== undefined && (typeof fallbackRaw !== "string" || !FLOW_TRACK_SET.has(fallbackRaw))) {
161
161
  throw configValidationError(fullPath, `"trackHeuristics.fallback" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
162
162
  }
163
- const priorityRaw = trackHeuristicsRaw.priority;
164
- let priority;
165
- if (priorityRaw !== undefined) {
166
- if (!Array.isArray(priorityRaw)) {
167
- throw configValidationError(fullPath, `"trackHeuristics.priority" must be an array`);
168
- }
169
- const invalidPriority = priorityRaw.filter((value) => typeof value !== "string" || !FLOW_TRACK_SET.has(value));
170
- if (invalidPriority.length > 0) {
171
- throw configValidationError(fullPath, `"trackHeuristics.priority" must contain only: ${SUPPORTED_TRACKS_TEXT}`);
172
- }
173
- priority = [...new Set(priorityRaw)];
163
+ if (Object.prototype.hasOwnProperty.call(trackHeuristicsRaw, "priority")) {
164
+ throw configValidationError(fullPath, `"trackHeuristics.priority" is no longer supported (removed in v0.38.0). Track evaluation order is always standard -> medium -> quick. Remove the field to upgrade.`);
174
165
  }
175
166
  const tracksRaw = trackHeuristicsRaw.tracks;
176
167
  let tracks = undefined;
@@ -186,30 +177,19 @@ export async function readConfig(projectRoot) {
186
177
  if (!isRecord(ruleRaw)) {
187
178
  throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}" must be an object`);
188
179
  }
180
+ if (Object.prototype.hasOwnProperty.call(ruleRaw, "patterns")) {
181
+ throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}.patterns" is no longer supported (removed in v0.38.0). Regex patterns were never wired into runtime routing. Move the intent into "triggers" (substrings) or "veto".`);
182
+ }
189
183
  const triggers = validateStringArray(ruleRaw.triggers, `trackHeuristics.tracks.${trackName}.triggers`, fullPath);
190
- const patterns = validateStringArray(ruleRaw.patterns, `trackHeuristics.tracks.${trackName}.patterns`, fullPath);
191
184
  const veto = validateStringArray(ruleRaw.veto, `trackHeuristics.tracks.${trackName}.veto`, fullPath);
192
- if (patterns) {
193
- for (const pattern of patterns) {
194
- try {
195
- // eslint-disable-next-line no-new
196
- new RegExp(pattern, "iu");
197
- }
198
- catch {
199
- throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}.patterns" contains invalid regex "${pattern}"`);
200
- }
201
- }
202
- }
203
185
  tracks[trackName] = {
204
186
  triggers,
205
- patterns,
206
187
  veto
207
188
  };
208
189
  }
209
190
  }
210
191
  trackHeuristics = {
211
192
  fallback: fallbackRaw,
212
- priority,
213
193
  tracks
214
194
  };
215
195
  }
@@ -35,20 +35,24 @@ the user can approve individual lifts, accept-all, or skip.
35
35
  - set \`closeout.compoundPromoted = 0\`,
36
36
  - set \`closeout.shipSubstate = "ready_to_archive"\`,
37
37
  - emit \`compound: no candidates | next: /cc-next\` and stop.
38
- 5. Otherwise, present **one** structured ask (AskUserQuestion / AskQuestion /
38
+ 5. **Drift check** each surviving candidate before presenting it (see
39
+ "Drift check" section in the skill): confirm the lift target file is
40
+ current, spot-check the repo for contradictions, demote stale clusters
41
+ into a new superseding entry instead of a lift.
42
+ 6. Otherwise, present **one** structured ask (AskUserQuestion / AskQuestion /
39
43
  plain text) summarising all candidates at once:
40
44
  - \`apply-all\` (default) — apply every listed lift,
41
45
  - \`apply-selected\` — prompt per-candidate,
42
46
  - \`skip\` — record a skip reason and advance without changes.
43
- 6. Apply approved lifts to the target file(s). Each lift also appends a
47
+ 7. Apply approved lifts to the target file(s). Each lift also appends a
44
48
  \`type: "compound"\` entry back to \`${RUNTIME_ROOT}/knowledge.jsonl\`
45
49
  summarising what was lifted.
46
- 7. Update flow-state:
50
+ 8. Update flow-state:
47
51
  - \`closeout.compoundCompletedAt = <ISO>\`,
48
52
  - \`closeout.compoundPromoted = <count>\`,
49
53
  - \`closeout.compoundSkipped = true\` if user picked skip,
50
54
  - \`closeout.shipSubstate = "ready_to_archive"\`.
51
- 8. Emit one-line summary: \`compound: promoted=<N> skipped=<bool> | next: /cc-next\`.
55
+ 9. Emit one-line summary: \`compound: promoted=<N> skipped=<bool> | next: /cc-next\`.
52
56
 
53
57
  ## Primary skill
54
58
 
@@ -83,27 +87,53 @@ empty pass is allowed and must advance \`closeout.shipSubstate\` to
83
87
  - \`closeout.compoundPromoted = 0\`,
84
88
  - \`closeout.shipSubstate = "ready_to_archive"\`,
85
89
  - announce \`compound: no candidates\` and stop.
86
- 4. Otherwise, render each candidate as:
90
+ 4. **Drift check run before presenting any candidate.** Knowledge lines
91
+ are append-only, so textual repetition alone does not prove the rule is
92
+ still true. For every cluster that survives the recurrence filter:
93
+
94
+ - **Read the lift target.** Open the rule/protocol/skill file you would
95
+ edit. If the current contents already encode a stronger version of
96
+ the cluster's \`action\`, drop the candidate (nothing to lift).
97
+ - **Grep for contradictions.** Run a quick repo search on the cluster's
98
+ \`trigger\` keywords. If recent code or docs contradict the cluster,
99
+ treat the cluster as stale.
100
+ - **Check age.** Inspect \`last_seen_ts\` across the cluster's lines. If
101
+ every contributing line is older than ~90 days with no fresh
102
+ observation, treat the cluster as stale.
103
+ - **Handle stale clusters correctly.** Do **not** silently skip them.
104
+ Append a new superseding \`type: "lesson"\` line to
105
+ \`.cclaw/knowledge.jsonl\` whose \`trigger\` explicitly references the
106
+ old pattern (e.g. \`"when previous rule about X no longer holds: ..."\`)
107
+ and whose \`action\` documents the replacement or archive reason.
108
+ Then drop the candidate from the lift list.
109
+ - **Cite line IDs.** Every surviving candidate must list the concrete
110
+ knowledge line indices (1-based) that back it, not just a
111
+ summary string. This is what makes the lift auditable.
112
+ - Optionally invoke the \`knowledge-curation\` utility skill's
113
+ stale/duplicate/supersede heuristics if you want a second pass.
114
+
115
+ 5. Otherwise, render each candidate as:
87
116
 
88
117
  \`\`\`
89
118
  Candidate: <short title>
90
- Evidence: <knowledge refs>
119
+ Evidence: <knowledge line-ids>
120
+ Freshness: <newest last_seen_ts among evidence lines>
91
121
  Lift target: <rule/protocol/skill file>
92
122
  Change type: <add/update/remove>
93
123
  Expected benefit: <what regressions this prevents>
94
124
  \`\`\`
95
125
 
96
- 5. Present **one** structured question with three options:
126
+ 6. Present **one** structured question with three options:
97
127
  - \`apply-all\` (default) — apply every candidate,
98
128
  - \`apply-selected\` — prompt per-candidate approval next,
99
129
  - \`skip\` — record a skip reason and advance.
100
130
 
101
- 6. For approved candidates:
131
+ 7. For approved candidates:
102
132
  - edit the target file(s) with the lift,
103
133
  - append a \`type: "compound"\` entry to \`.cclaw/knowledge.jsonl\`
104
- describing what was promoted.
134
+ describing what was promoted, including the source line IDs.
105
135
 
106
- 7. Update flow-state \`closeout\`:
136
+ 8. Update flow-state \`closeout\`:
107
137
  - \`compoundCompletedAt\`,
108
138
  - \`compoundPromoted\` (count),
109
139
  - \`compoundSkipped\` (boolean) + \`compoundSkipReason\` when applicable,
@@ -122,6 +152,9 @@ closeout chain's perspective.
122
152
  - \`closeout.compoundCompletedAt\` is set.
123
153
  - \`closeout.shipSubstate === "ready_to_archive"\`.
124
154
  - If lifts were applied, the target files show the edit and at least one
125
- new \`compound\` line exists in \`.cclaw/knowledge.jsonl\`.
155
+ new \`compound\` line exists in \`.cclaw/knowledge.jsonl\`, and the new
156
+ line references the source knowledge line IDs.
157
+ - If drift check demoted any cluster, a new superseding \`lesson\` line
158
+ exists on the same run documenting the replacement.
126
159
  `;
127
160
  }
@@ -65,7 +65,7 @@ This is the **recommended way to start** working with cclaw. Use \`/cc-next\` fo
65
65
  4. Read \`${flowPath}\`.
66
66
  5. If flow already has completed stages beyond brainstorm, warn the user that starting a new brainstorm will reset progress. Ask for confirmation before proceeding.
67
67
  6. **Track heuristic** — classify the idea text and **recommend** a track (the user can override before any state mutation):
68
- - First, load \`${RUNTIME_ROOT}/config.yaml\`. If \`trackHeuristics\` is defined, apply those per-track rules (\`priority\`, \`fallback\`, \`tracks.<id>.{triggers,patterns,veto}\`) before built-in defaults.
68
+ - First, load \`${RUNTIME_ROOT}/config.yaml\`. If \`trackHeuristics\` is defined, apply those per-track vocabulary hints (\`fallback\`, \`tracks.<id>.{triggers,veto}\`) on top of the built-in defaults. Evaluation order is always \`standard -> medium -> quick\` (narrow-to-broad).
69
69
  - **quick** (\`spec → tdd → review → ship\`) — single-purpose work where the spec is essentially already known.
70
70
  Triggers (case-insensitive substring or close variant): \`bug\`, \`bugfix\`, \`fix\`, \`hotfix\`, \`patch\`, \`typo\`, \`regression\`, \`copy change\`, \`rename\`, \`bump\`, \`upgrade dep\`, \`config tweak\`, \`docs only\`, \`comment\`, \`lint\`, \`format\`, \`small\`, \`tiny\`, \`one-liner\`, \`revert\`.
71
71
  - **medium** (\`brainstorm → spec → plan → tdd → review → ship\`) — additive work that fits existing architecture and still needs product framing.
@@ -141,7 +141,7 @@ Do **not** silently discard an existing flow when the user provides a prompt. If
141
141
  - Ask: "Continue with reset? (A) Yes, start fresh (B) No, resume current flow"
142
142
  - If (B) → switch to Path B behavior.
143
143
  6. **Classify the idea** using the heuristic below and present a single track recommendation. Wait for explicit confirmation or override before mutating any state.
144
- - If \`${RUNTIME_ROOT}/config.yaml\` defines \`trackHeuristics\`, apply that override first (priority/fallback/rules), then use built-in defaults only as fallback.
144
+ - If \`${RUNTIME_ROOT}/config.yaml\` defines \`trackHeuristics\`, apply those vocabulary hints (\`fallback\`, \`tracks.<id>.{triggers,veto}\`) on top of built-in defaults. Evaluation order is fixed: \`standard -> medium -> quick\`. (Honest note: this is advisory prose; the LLM applies it, not a Node-level router.)
145
145
 
146
146
  **Track heuristic** (lowercase substring match against the user prompt):
147
147
 
package/dist/doctor.js CHANGED
@@ -873,7 +873,13 @@ export async function doctorChecks(projectRoot, options = {}) {
873
873
  let missingSchemaV2Fields = 0;
874
874
  let parsedKnowledgeLines = 0;
875
875
  let lowConfidenceLines = 0;
876
+ let staleRawEntries = 0;
876
877
  const triggerActionCounts = new Map();
878
+ // Stale threshold for raw entries: ~90 days with no re-observation.
879
+ // Chosen to match the compound drift checklist language; anything newer is
880
+ // recent enough to trust, anything older deserves a curate/supersede pass.
881
+ const STALE_RAW_THRESHOLD_MS = 90 * 24 * 60 * 60 * 1000;
882
+ const now = Date.now();
877
883
  const requiredV2Fields = [
878
884
  "type",
879
885
  "trigger",
@@ -919,6 +925,14 @@ export async function doctorChecks(projectRoot, options = {}) {
919
925
  if (missing) {
920
926
  missingSchemaV2Fields += 1;
921
927
  }
928
+ const maturity = typeof parsed.maturity === "string" ? parsed.maturity.toLowerCase() : "";
929
+ const lastSeenRaw = typeof parsed.last_seen_ts === "string" ? parsed.last_seen_ts : "";
930
+ if (maturity === "raw" && lastSeenRaw.length > 0) {
931
+ const lastSeenMs = Date.parse(lastSeenRaw);
932
+ if (Number.isFinite(lastSeenMs) && now - lastSeenMs > STALE_RAW_THRESHOLD_MS) {
933
+ staleRawEntries += 1;
934
+ }
935
+ }
922
936
  }
923
937
  catch {
924
938
  malformedKnowledgeLines += 1;
@@ -962,6 +976,15 @@ export async function doctorChecks(projectRoot, options = {}) {
962
976
  ? "no high-frequency repeated trigger/action clusters detected"
963
977
  : `warning: ${repeatedClusters.length} repeated learning cluster(s) detected (>=3 repeats). Consider /cc-ops compound to lift them into rules/skills.`
964
978
  });
979
+ checks.push({
980
+ name: "warning:knowledge:stale_raw_entries",
981
+ ok: true,
982
+ details: parsedKnowledgeLines === 0
983
+ ? "knowledge.jsonl is empty"
984
+ : staleRawEntries === 0
985
+ ? `no raw knowledge entries older than 90 days`
986
+ : `warning: ${staleRawEntries} raw knowledge entry(ies) have last_seen_ts older than 90 days. Run /cc-learn curate or append a superseding entry before the next /cc-ops compound pass.`
987
+ });
965
988
  }
966
989
  checks.push({
967
990
  name: "state:checkpoint_exists",
@@ -4,9 +4,16 @@ export interface TrackResolution {
4
4
  reason: string;
5
5
  matchedTokens: string[];
6
6
  }
7
+ /**
8
+ * Reference implementation of the track classifier the /cc skill prose
9
+ * describes. Tests pin its behavior so the built-in defaults stay honest.
10
+ * This function is not called from cclaw runtime — `/cc` routing happens in
11
+ * the LLM. If you wire this in later, update README to drop the
12
+ * "advisory" language.
13
+ */
7
14
  export declare function resolveTrackFromPrompt(prompt: string, config: TrackHeuristicsConfig | undefined): TrackResolution;
8
15
  export declare const TRACK_HEURISTICS_DEFAULTS: {
9
16
  readonly fallback: "standard";
10
- readonly priority: ("quick" | "medium" | "standard")[];
17
+ readonly evaluationOrder: readonly ("quick" | "medium" | "standard")[];
11
18
  readonly tracks: Record<"quick" | "medium" | "standard", TrackHeuristicRule>;
12
19
  };
@@ -1,4 +1,6 @@
1
1
  import { FLOW_TRACKS } from "./types.js";
2
+ // Built-in vocabulary per track. Kept in one place so tests, docs, and the
3
+ // /cc skill prose can snapshot the exact same strings.
2
4
  const DEFAULT_RULES = {
3
5
  quick: {
4
6
  triggers: [
@@ -48,7 +50,9 @@ const DEFAULT_RULES = {
48
50
  ]
49
51
  }
50
52
  };
51
- const DEFAULT_PRIORITY = ["standard", "medium", "quick"];
53
+ // Fixed evaluation order: narrow-to-broad. Overriding this was never wired
54
+ // into runtime, so cclaw stopped offering the knob in v0.38.0.
55
+ const EVALUATION_ORDER = ["standard", "medium", "quick"];
52
56
  const DEFAULT_FALLBACK = "standard";
53
57
  function hasToken(promptLower, token) {
54
58
  return promptLower.includes(token.toLowerCase());
@@ -62,17 +66,6 @@ function matchRule(promptLower, rule) {
62
66
  matches.push(trigger);
63
67
  }
64
68
  }
65
- for (const pattern of rule.patterns ?? []) {
66
- try {
67
- const regex = new RegExp(pattern, "iu");
68
- if (regex.test(promptLower)) {
69
- matches.push(`/${pattern}/`);
70
- }
71
- }
72
- catch {
73
- // Ignore invalid custom regex entries; config validation should catch these.
74
- }
75
- }
76
69
  return [...new Set(matches)];
77
70
  }
78
71
  function isValidTrack(value) {
@@ -89,34 +82,26 @@ function mergeRules(base, overrides) {
89
82
  continue;
90
83
  merged[track] = {
91
84
  triggers: rule.triggers ?? merged[track].triggers,
92
- patterns: rule.patterns ?? merged[track].patterns,
93
85
  veto: rule.veto ?? merged[track].veto
94
86
  };
95
87
  }
96
88
  return merged;
97
89
  }
98
- function resolvePriority(config) {
99
- const configured = config?.priority ?? [];
100
- const filtered = configured.filter((track) => isValidTrack(track));
101
- const unique = [...new Set(filtered)];
102
- if (unique.length === 0)
103
- return [...DEFAULT_PRIORITY];
104
- // Ensure all tracks are still represented in deterministic order.
105
- for (const track of FLOW_TRACKS) {
106
- if (!unique.includes(track))
107
- unique.push(track);
108
- }
109
- return unique;
110
- }
111
90
  function resolveFallback(config) {
112
91
  return config?.fallback && isValidTrack(config.fallback) ? config.fallback : DEFAULT_FALLBACK;
113
92
  }
93
+ /**
94
+ * Reference implementation of the track classifier the /cc skill prose
95
+ * describes. Tests pin its behavior so the built-in defaults stay honest.
96
+ * This function is not called from cclaw runtime — `/cc` routing happens in
97
+ * the LLM. If you wire this in later, update README to drop the
98
+ * "advisory" language.
99
+ */
114
100
  export function resolveTrackFromPrompt(prompt, config) {
115
101
  const promptLower = prompt.toLowerCase();
116
102
  const rules = mergeRules(DEFAULT_RULES, config);
117
- const priority = resolvePriority(config);
118
103
  const fallback = resolveFallback(config);
119
- for (const track of priority) {
104
+ for (const track of EVALUATION_ORDER) {
120
105
  const rule = rules[track];
121
106
  const vetoes = rule.veto ?? [];
122
107
  if (vetoes.some((token) => hasToken(promptLower, token))) {
@@ -139,6 +124,6 @@ export function resolveTrackFromPrompt(prompt, config) {
139
124
  }
140
125
  export const TRACK_HEURISTICS_DEFAULTS = {
141
126
  fallback: DEFAULT_FALLBACK,
142
- priority: DEFAULT_PRIORITY,
127
+ evaluationOrder: EVALUATION_ORDER,
143
128
  tracks: DEFAULT_RULES
144
129
  };
package/dist/types.d.ts CHANGED
@@ -25,20 +25,38 @@ export type HarnessId = (typeof HARNESS_IDS)[number];
25
25
  */
26
26
  export declare const LANGUAGE_RULE_PACKS: readonly ["typescript", "python", "go"];
27
27
  export type LanguageRulePack = (typeof LANGUAGE_RULE_PACKS)[number];
28
+ /**
29
+ * Per-track vocabulary hints the LLM applies when classifying a /cc prompt.
30
+ *
31
+ * Intentionally minimal:
32
+ * - `triggers`: additional substrings that push a prompt toward this track.
33
+ * - `veto`: substrings that forbid this track even if a trigger matches.
34
+ *
35
+ * Removed in v0.38.0:
36
+ * - `patterns` (regex): no runtime ever consumed them; kept authors honest
37
+ * about what cclaw actually enforces.
38
+ */
28
39
  export interface TrackHeuristicRule {
29
40
  triggers?: string[];
30
- patterns?: string[];
31
41
  veto?: string[];
32
42
  }
43
+ /**
44
+ * Optional prompt-to-track overrides for /cc classification.
45
+ *
46
+ * Honesty note: this config is **advisory**. cclaw surfaces these lists in
47
+ * the /cc skill and contract prose so the LLM can apply them when picking a
48
+ * track. There is no Node-level routing layer that mechanically enforces the
49
+ * result — which is why we only ship `triggers`, `veto`, and `fallback`, not
50
+ * regex patterns or priority overrides.
51
+ *
52
+ * Removed in v0.38.0:
53
+ * - `priority`: track evaluation order is always `standard -> medium -> quick`
54
+ * (narrow-to-broad matching). Overriding it was never wired.
55
+ */
33
56
  export interface TrackHeuristicsConfig {
34
- /** Track used when no trigger/pattern matches. */
57
+ /** Track used when no trigger matches. Defaults to `standard`. */
35
58
  fallback?: FlowTrack;
36
- /**
37
- * Track evaluation order. First matching track wins.
38
- * Example: ["standard", "medium", "quick"].
39
- */
40
- priority?: FlowTrack[];
41
- /** Per-track matching rules. */
59
+ /** Per-track vocabulary hints. */
42
60
  tracks?: Partial<Record<FlowTrack, TrackHeuristicRule>>;
43
61
  }
44
62
  /**
@@ -92,7 +110,8 @@ export interface VibyConfig {
92
110
  */
93
111
  languageRulePacks?: LanguageRulePack[];
94
112
  /**
95
- * Optional prompt-to-track mapping overrides for /cc classification.
113
+ * Optional prompt-to-track vocabulary overrides for /cc classification.
114
+ * Advisory (surfaced in the /cc skill prose), not machine-enforced.
96
115
  * If omitted, cclaw uses built-in defaults.
97
116
  */
98
117
  trackHeuristics?: TrackHeuristicsConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {