cclaw-cli 0.10.0 → 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) {
@@ -714,6 +714,72 @@ const DOMAIN_LABELS = {
714
714
  "data-pipeline": "Data pipeline / ETL"
715
715
  };
716
716
  const STAGE_DOMAIN_SAMPLES = {
717
+ brainstorm: [
718
+ {
719
+ domain: "web",
720
+ label: "Direction",
721
+ body: "Problem: admin dashboard orders table requires manual refresh to see new orders. Success: admins see new rows within 2s of server-side status change, no full navigation. Anti-success: WebSocket rewrite of the whole table stack when only one view needs live updates."
722
+ },
723
+ {
724
+ domain: "cli",
725
+ label: "Direction",
726
+ body: "Problem: `cclaw archive` silently deletes 30+ day runs with no preview. Success: a `--dry-run` flag prints would-be-archived run IDs to stdout and exits 0; current behavior is unchanged without the flag. Anti-success: adding an interactive confirmation prompt that breaks CI scripts."
727
+ },
728
+ {
729
+ domain: "library",
730
+ label: "Direction",
731
+ body: "Problem: consumers cannot validate hook JSON without importing internal modules. Success: `validateHookDocument(obj)` exported from the package root with typed result `{ ok, errors? }`. Anti-success: exposing the full Zod schema and forcing consumers to depend on Zod."
732
+ },
733
+ {
734
+ domain: "data-pipeline",
735
+ label: "Direction",
736
+ body: "Problem: reruns of the orders job create duplicate `fact_orders` rows. Success: running the job twice on the same input leaves row count unchanged and `dbt test --select fact_orders` green. Anti-success: introducing a nightly dedup job that hides the underlying non-idempotency."
737
+ }
738
+ ],
739
+ scope: [
740
+ {
741
+ domain: "web",
742
+ label: "Scope line",
743
+ body: "In: live-update `/dashboard/orders` table via SSE; out: notification drawer, mobile PWA, dashboards other than `orders`. Discretion: choice of SSE vs long-polling for legacy Safari. NOT in scope: rewriting the auth layer or the existing REST endpoints."
744
+ },
745
+ {
746
+ domain: "cli",
747
+ label: "Scope line",
748
+ body: "In: add `--dry-run` to `cclaw archive`; out: redesigning archive formats, adding retention flags, or changing the default. Discretion: exact wording of stdout lines. NOT in scope: touching `init` / `sync` / `doctor` subcommands."
749
+ },
750
+ {
751
+ domain: "library",
752
+ label: "Scope line",
753
+ body: "In: expose `validateHookDocument` + types from package root; out: rewriting hook schema, adding new hook kinds, dropping old ones. Discretion: whether to re-export `HookDocument` as type-only. NOT in scope: migrating consumers."
754
+ },
755
+ {
756
+ domain: "data-pipeline",
757
+ label: "Scope line",
758
+ body: "In: dedup step between `raw.orders` and `fact_orders` keyed on `(order_id, event_ts)`; out: redesigning ingestion, adding new partitions, or touching downstream marts. Discretion: `row_number()` vs `qualify`-style dedup. NOT in scope: backfilling historical partitions."
759
+ }
760
+ ],
761
+ design: [
762
+ {
763
+ domain: "web",
764
+ label: "Architecture note",
765
+ body: "Data flow: server-side order update → publish to `orders-updates` channel → SSE endpoint `/api/orders/stream` → `useOrderFeed` hook merges into React state → row rerenders. Failure mode: SSE connection drop → exponential-backoff reconnect + on-reconnect REST snapshot fallback. Trade-off accepted: no client→server channel (SSE one-way); existing REST mutations cover it."
766
+ },
767
+ {
768
+ domain: "cli",
769
+ label: "Architecture note",
770
+ body: "Flag is parsed by the existing Zod CLI parser; `--dry-run` short-circuits before any filesystem mutation, shares formatter `src/cli/format.ts` with `status`. Failure mode: formatter output differs between `status` and `archive --dry-run` → centralize format. Trade-off: we print run IDs unsorted to keep the code path identical to the real archive path."
771
+ },
772
+ {
773
+ domain: "library",
774
+ label: "Architecture note",
775
+ body: "Re-export `validateHookDocument` from package root; rename internal `__validate` to match the exported name so callsites and the export converge. Failure mode: consumers importing from `/dist/internal` break on the rename → add a deprecation re-export shim for one minor. Trade-off: slightly wider public surface today buys us a smaller public surface tomorrow."
776
+ },
777
+ {
778
+ domain: "data-pipeline",
779
+ label: "Architecture note",
780
+ body: "Insert `int_orders_deduped` CTE between staging and fact, keyed on `(order_id, event_ts)` with `row_number() = 1` per key; `fact_orders` reads from the deduped model only. Failure mode: late-arriving events with an earlier `event_ts` would flap the chosen row → tiebreak on `ingest_ts DESC`. Trade-off: the job now does one extra pass; measured +8% runtime, within budget."
781
+ }
782
+ ],
717
783
  spec: [
718
784
  {
719
785
  domain: "web",
@@ -780,6 +846,28 @@ const STAGE_DOMAIN_SAMPLES = {
780
846
  body: "RED: `dbt test --select fact_orders` → `unique test on (order_id, event_ts)` fails on re-run. GREEN: added `row_number()` dedup in the staging model. REFACTOR: extracted the dedup CTE into `int_orders_deduped` for reuse by `fact_returns`."
781
847
  }
782
848
  ],
849
+ review: [
850
+ {
851
+ domain: "web",
852
+ label: "Finding",
853
+ body: "R-W-1 (Critical, correctness): `useOrderFeed` does not unsubscribe from the SSE channel on unmount — two mounts on the same page double-count rows. Evidence: `tests/unit/order-feed-hook.test.ts > unmount` fails. Fix owner: frontend; blocks ship."
854
+ },
855
+ {
856
+ domain: "cli",
857
+ label: "Finding",
858
+ body: "R-C-2 (Suggestion, UX): `cclaw archive --dry-run` prints run IDs without a trailing newline, breaking downstream `xargs` pipelines. Evidence: `echo '' | xargs -I{} printf '%s\\n' {}` contrast. Fix owner: CLI; non-blocking."
859
+ },
860
+ {
861
+ domain: "library",
862
+ label: "Finding",
863
+ body: "R-L-1 (Important, surface-area): the new `validateHookDocument` export is documented in README but missing from `src/index.ts` — `import { validateHookDocument } from 'cclaw'` fails despite the docs. Evidence: `pnpm build && node -e \"require('./dist').validateHookDocument\"` prints `undefined`. Fix owner: library; blocks ship."
864
+ },
865
+ {
866
+ domain: "data-pipeline",
867
+ label: "Finding",
868
+ body: "R-D-1 (Critical, correctness): dedup CTE orders by `event_ts ASC` instead of `event_ts DESC` — on duplicate events we keep the older row. Evidence: `dbt test --select fact_orders` green but fixture `tests/fixtures/orders-dupes.csv` shows wrong survivor. Fix owner: analytics-eng; blocks ship."
869
+ }
870
+ ],
783
871
  ship: [
784
872
  {
785
873
  domain: "web",
@@ -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;