cclaw-cli 0.10.1 → 0.11.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/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) {
@@ -39,7 +39,7 @@ export const RUNTIME_SHELL_DETECT_ROOT = DETECT_ROOT;
39
39
  export function sessionStartScript(_options = {}) {
40
40
  return `#!/usr/bin/env bash
41
41
  # cclaw session-start hook — generated by cclaw sync
42
- # Injects using-cclaw + flow status + active artifacts + knowledge snapshot + checkpoint/activity summary.
42
+ # Injects using-cclaw + flow status + active artifacts + compact knowledge digest + checkpoint/activity summary.
43
43
  set -euo pipefail
44
44
 
45
45
  ${DETECT_ROOT}
@@ -52,6 +52,7 @@ CONTEXT_WARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/state/context-warnings.jsonl"
52
52
  CONTEXT_MODE_FILE="$ROOT/${RUNTIME_ROOT}/state/context-mode.json"
53
53
  CONTEXTS_DIR="$ROOT/${RUNTIME_ROOT}/contexts"
54
54
  KNOWLEDGE_FILE="$ROOT/${RUNTIME_ROOT}/knowledge.jsonl"
55
+ KNOWLEDGE_DIGEST_FILE="$ROOT/${RUNTIME_ROOT}/state/knowledge-digest.md"
55
56
  META_SKILL="$ROOT/${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"
56
57
 
57
58
  # --- Read flow state ---
@@ -309,12 +310,72 @@ if [ -f "$META_SKILL" ]; then
309
310
  META_CONTENT=$(cat "$META_SKILL" 2>/dev/null || echo "")
310
311
  fi
311
312
 
312
- # --- Load knowledge snapshot (canonical JSONL tail + total count) ---
313
- KNOWLEDGE_SUMMARY=""
313
+ # --- Build compact knowledge digest (stage-biased, top entries only) ---
314
+ KNOWLEDGE_DIGEST=""
314
315
  LEARNINGS_COUNT=0
315
316
  if [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
316
- KNOWLEDGE_SUMMARY=$(tail -n 30 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
317
317
  LEARNINGS_COUNT=$(grep -c '^{' "$KNOWLEDGE_FILE" 2>/dev/null || echo "0")
318
+ if command -v jq >/dev/null 2>&1; then
319
+ KNOWLEDGE_DIGEST=$(tail -n 200 "$KNOWLEDGE_FILE" 2>/dev/null | jq -Rsc --arg stage "$STAGE" '
320
+ split("\\n")
321
+ | map(select(length > 0))
322
+ | map(try fromjson catch null)
323
+ | map(select(type == "object"))
324
+ | map(select((.stage // null) == $stage or (.stage // null) == null))
325
+ | reverse
326
+ | .[0:8]
327
+ | map("- [" + ((.confidence // "unknown")|tostring) + " • " + ((.stage // "global")|tostring) + " • " + ((.domain // "general")|tostring) + "] " + ((.trigger // "trigger")|tostring) + " -> " + ((.action // "action")|tostring))
328
+ | join("\\n")
329
+ ' 2>/dev/null || echo "")
330
+ elif command -v python3 >/dev/null 2>&1; then
331
+ KNOWLEDGE_DIGEST=$(python3 - "$KNOWLEDGE_FILE" "$STAGE" <<'PY'
332
+ import json
333
+ import sys
334
+
335
+ path = sys.argv[1]
336
+ stage = sys.argv[2]
337
+ entries = []
338
+ try:
339
+ with open(path, "r", encoding="utf-8") as fh:
340
+ lines = fh.readlines()[-200:]
341
+ for raw in lines:
342
+ raw = raw.strip()
343
+ if not raw:
344
+ continue
345
+ try:
346
+ obj = json.loads(raw)
347
+ except Exception:
348
+ continue
349
+ if not isinstance(obj, dict):
350
+ continue
351
+ row_stage = obj.get("stage")
352
+ if row_stage not in (stage, None):
353
+ continue
354
+ entries.append(obj)
355
+ except Exception:
356
+ entries = []
357
+
358
+ entries = list(reversed(entries))[:8]
359
+ out = []
360
+ for obj in entries:
361
+ conf = str(obj.get("confidence", "unknown"))
362
+ row_stage = str(obj.get("stage", "global"))
363
+ domain = str(obj.get("domain", "general"))
364
+ trigger = str(obj.get("trigger", "trigger"))
365
+ action = str(obj.get("action", "action"))
366
+ out.append(f"- [{conf} • {row_stage} • {domain}] {trigger} -> {action}")
367
+ print("\\n".join(out))
368
+ PY
369
+ )
370
+ else
371
+ KNOWLEDGE_DIGEST=$(tail -n 8 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
372
+ fi
373
+ fi
374
+
375
+ if [ -n "$KNOWLEDGE_DIGEST" ]; then
376
+ printf '# Knowledge digest (auto-generated)\\n\\n%s\\n' "$KNOWLEDGE_DIGEST" > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
377
+ elif [ -f "$KNOWLEDGE_DIGEST_FILE" ]; then
378
+ printf '# Knowledge digest (auto-generated)\\n\\n(no matching entries for current stage)\\n' > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
318
379
  fi
319
380
 
320
381
  # --- Installed cclaw-cli version vs. project's recorded version (one-block
@@ -391,10 +452,10 @@ if [ -n "$STAGE_SUGGESTION" ]; then
391
452
  $STAGE_SUGGESTION
392
453
  To disable suggestions persistently set ${RUNTIME_ROOT}/state/suggestion-memory.json -> enabled=false."
393
454
  fi
394
- if [ -n "$KNOWLEDGE_SUMMARY" ]; then
455
+ if [ -n "$KNOWLEDGE_DIGEST" ]; then
395
456
  CTX="$CTX
396
- Knowledge snapshot (latest entries):
397
- $KNOWLEDGE_SUMMARY"
457
+ Knowledge digest (top relevant entries):
458
+ $KNOWLEDGE_DIGEST"
398
459
  fi
399
460
  if [ -n "$META_CONTENT" ]; then
400
461
  CTX="$CTX
@@ -833,6 +894,7 @@ export default function cclawPlugin(ctx) {
833
894
  const contextsDir = join(runtimeDir, "contexts");
834
895
  const sessionDigestPath = join(stateDir, "session-digest.md");
835
896
  const knowledgePath = join(runtimeDir, "knowledge.jsonl");
897
+ const knowledgeDigestPath = join(stateDir, "knowledge-digest.md");
836
898
  const metaSkillPath = join(runtimeDir, "skills/${META_SKILL_NAME}/SKILL.md");
837
899
 
838
900
  function ensureRuntimeDirs() {
@@ -937,8 +999,16 @@ export default function cclawPlugin(ctx) {
937
999
  }
938
1000
  }
939
1001
 
940
- function readKnowledgeSnapshot() {
941
- return readTailLines(knowledgePath, 30);
1002
+ function readKnowledgeDigest() {
1003
+ const digest = readFileText(knowledgeDigestPath).trim();
1004
+ if (!digest) {
1005
+ return readTailLines(knowledgePath, 12);
1006
+ }
1007
+ return digest
1008
+ .split(/\\r?\\n/)
1009
+ .map((line) => line.trim())
1010
+ .filter((line) => line.length > 0)
1011
+ .filter((line) => !line.startsWith("#"));
942
1012
  }
943
1013
 
944
1014
  function buildBootstrap() {
@@ -965,8 +1035,8 @@ export default function cclawPlugin(ctx) {
965
1035
  const warning = readLatestContextWarning();
966
1036
  if (warning) parts.push("Latest context warning:", warning);
967
1037
 
968
- const knowledge = readKnowledgeSnapshot();
969
- if (knowledge.length > 0) parts.push("Knowledge snapshot (latest entries):", ...knowledge);
1038
+ const knowledge = readKnowledgeDigest();
1039
+ if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
970
1040
 
971
1041
  parts.push(
972
1042
  "If you discover a non-obvious rule or pattern, append one strict-schema JSON line to .cclaw/knowledge.jsonl using type: rule, pattern, lesson, or compound."
@@ -1,10 +1,2 @@
1
- /**
2
- * using-cclaw meta-skill — injected at SessionStart via hooks.
3
- *
4
- * Like agent-skills' using-agent-skills, this teaches the agent HOW to use
5
- * cclaw: skill discovery flowchart, activation rules, skill behaviors.
6
- * The full text is injected by session-start.sh so the agent always has
7
- * routing context without needing to read files first.
8
- */
9
1
  export declare const META_SKILL_NAME = "using-cclaw";
10
2
  export declare function usingCclawSkillMarkdown(): string;