cclaw-cli 0.46.5 → 0.46.7

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.
@@ -15,10 +15,7 @@ export interface WorkflowGuardOptions {
15
15
  tddTestGlobs?: string[];
16
16
  }
17
17
  export declare function workflowGuardScript(options?: WorkflowGuardOptions): string;
18
- export declare function observeScript(): string;
19
18
  export declare function contextMonitorScript(): string;
20
- export declare function summarizeObservationsRuntimeModule(): string;
21
- export declare function summarizeObservationsScript(): string;
22
19
  export declare function claudeHooksJsonWithObservation(): string;
23
20
  export declare function cursorHooksJsonWithObservation(): string;
24
21
  /**
@@ -693,260 +693,6 @@ if [ -n "$REASONS" ]; then
693
693
  printf '[cclaw] %s\n' "$NOTE" >&2
694
694
  fi
695
695
 
696
- exit 0
697
- `;
698
- }
699
- export function observeScript() {
700
- return `#!/usr/bin/env bash
701
- # cclaw observe hook — generated by cclaw sync
702
- # Captures PreToolUse/PostToolUse events to ${RUNTIME_ROOT}/observations.jsonl
703
- # Reads hook JSON from stdin, extracts tool + truncated I/O, appends JSONL.
704
- # Always exits 0 to never block the agent.
705
- set -uo pipefail
706
-
707
- # Phase: "pre" or "post" passed as $1 by the hook runner
708
- PHASE="\${1:-post}"
709
-
710
- ${RUNTIME_SHELL_DETECT_ROOT}
711
-
712
- OBS_FILE="$ROOT/${RUNTIME_ROOT}/observations.jsonl"
713
- STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
714
- ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
715
- LOCK_DIR="$ROOT/${RUNTIME_ROOT}/state/.observe.lock"
716
- MAX_LEN=2000
717
-
718
- # Guard: skip if disabled or observations dir missing
719
- [ -f "$ROOT/${RUNTIME_ROOT}/.observe-disabled" ] && exit 0
720
- [ -d "$ROOT/${RUNTIME_ROOT}" ] || exit 0
721
- mkdir -p "$ROOT/${RUNTIME_ROOT}/state" 2>/dev/null || true
722
-
723
- escape_json() {
724
- local str="$1"
725
- str=\${str//\\\\/\\\\\\\\}
726
- str=\${str//\\"/\\\\\\"}
727
- str=\${str//$'\\t'/\\\\t}
728
- str=\${str//$'\\n'/\\\\n}
729
- printf '%s' "$str"
730
- }
731
-
732
- acquire_lock() {
733
- local attempt=0
734
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do
735
- attempt=$((attempt + 1))
736
- if [ "$attempt" -ge 200 ]; then
737
- return 1
738
- fi
739
- sleep 0.02
740
- done
741
- return 0
742
- }
743
-
744
- release_lock() {
745
- rmdir "$LOCK_DIR" 2>/dev/null || true
746
- }
747
-
748
- rotate_file() {
749
- local file_path="$1"
750
- local keep_lines="$2"
751
- if [ ! -f "$file_path" ]; then
752
- return
753
- fi
754
- local line_count
755
- line_count=$(wc -l < "$file_path" 2>/dev/null | tr -d ' ')
756
- if [ -z "$line_count" ]; then
757
- return
758
- fi
759
- if [ "$line_count" -gt $((keep_lines * 2)) ]; then
760
- local tmp_path="\${file_path}.tmp.$$"
761
- if tail -n "$keep_lines" "$file_path" > "$tmp_path" 2>/dev/null; then
762
- mv "$tmp_path" "$file_path" 2>/dev/null || rm -f "$tmp_path" 2>/dev/null || true
763
- fi
764
- fi
765
- }
766
-
767
- sync_run_artifacts() {
768
- if [ "$PHASE" != "post" ]; then
769
- return
770
- fi
771
- [ -n "$ACTIVE_RUN" ] || return
772
- if [ "$ACTIVE_RUN" = "none" ]; then
773
- return
774
- fi
775
-
776
- local tool_lower
777
- tool_lower=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
778
- case "$tool_lower" in
779
- write|edit|multiedit|multi_edit|delete|applypatch|runcommand|shell|terminal|execcommand) ;;
780
- *) return ;;
781
- esac
782
-
783
- local active_dir="$ROOT/${RUNTIME_ROOT}/artifacts"
784
- local run_dir="$ROOT/${RUNTIME_ROOT}/runs/$ACTIVE_RUN/artifacts"
785
- [ -d "$active_dir" ] || return
786
- mkdir -p "$run_dir" 2>/dev/null || return
787
-
788
- for run_file in "$run_dir"/*; do
789
- [ -e "$run_file" ] || continue
790
- [ -f "$run_file" ] || continue
791
- local base_name
792
- base_name=$(basename "$run_file")
793
- local active_file="$active_dir/$base_name"
794
- if [ ! -f "$active_file" ]; then
795
- rm -f "$run_file" 2>/dev/null || true
796
- continue
797
- fi
798
- if command -v cmp >/dev/null 2>&1 && cmp -s "$active_file" "$run_file" 2>/dev/null; then
799
- continue
800
- fi
801
- cp "$active_file" "$run_file" 2>/dev/null || true
802
- done
803
-
804
- for active_file in "$active_dir"/*; do
805
- [ -e "$active_file" ] || continue
806
- [ -f "$active_file" ] || continue
807
- local base_name
808
- base_name=$(basename "$active_file")
809
- local run_file="$run_dir/$base_name"
810
- if [ -f "$run_file" ] && command -v cmp >/dev/null 2>&1 && cmp -s "$active_file" "$run_file" 2>/dev/null; then
811
- continue
812
- fi
813
- cp "$active_file" "$run_file" 2>/dev/null || true
814
- done
815
- }
816
-
817
- # Read stdin (hook JSON)
818
- INPUT=$(cat 2>/dev/null || echo '{}')
819
- [ -z "$INPUT" ] && exit 0
820
-
821
- # Extract fields from hook payload.
822
- TOOL="unknown"
823
- PAYLOAD=""
824
- if command -v jq >/dev/null 2>&1; then
825
- TOOL=$(echo "$INPUT" | jq -r '.tool_name // .tool // .toolName // .name // .id // .command // .tool.name // .tool.id // .input.tool_name // .input.tool // .input.toolName // .input.name // .input.id // .input.command // .input.tool.name // .input.tool.id // "unknown"' 2>/dev/null || echo "unknown")
826
- if [ "$PHASE" = "pre" ]; then
827
- PAYLOAD=$(echo "$INPUT" | jq -r --arg max "$MAX_LEN" '.tool_input // .input // {} | tostring | .[0:($max|tonumber)]' 2>/dev/null || echo "")
828
- else
829
- PAYLOAD=$(echo "$INPUT" | jq -r --arg max "$MAX_LEN" '.tool_response // .tool_output // .output // .result_json // "" | tostring | .[0:($max|tonumber)]' 2>/dev/null || echo "")
830
- fi
831
- else
832
- TOOL=$(printf '%s' "$INPUT" | sed -n -E 's/.*"tool_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\\1/p' | head -1)
833
- if [ -z "$TOOL" ]; then
834
- TOOL=$(printf '%s' "$INPUT" | sed -n -E 's/.*"tool"[[:space:]]*:[[:space:]]*"([^"]+)".*/\\1/p' | head -1)
835
- fi
836
- [ -n "$TOOL" ] || TOOL="unknown"
837
- PAYLOAD=$(printf '%s' "$INPUT" | cut -c1-"$MAX_LEN")
838
- fi
839
-
840
- TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
841
- STAGE="none"
842
- ACTIVE_RUN="none"
843
- if [ -f "$STATE_FILE" ]; then
844
- if command -v jq >/dev/null 2>&1; then
845
- STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
846
- ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
847
- elif command -v python3 >/dev/null 2>&1; then
848
- STAGE=$(python3 - "$STATE_FILE" <<'PY'
849
- import json
850
- import sys
851
-
852
- stage = "none"
853
- try:
854
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
855
- data = json.load(fh)
856
- value = data.get("currentStage")
857
- if isinstance(value, str) and value:
858
- stage = value
859
- except Exception:
860
- pass
861
- print(stage)
862
- PY
863
- )
864
- ACTIVE_RUN=$(python3 - "$STATE_FILE" <<'PY'
865
- import json
866
- import sys
867
-
868
- run_id = "none"
869
- try:
870
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
871
- data = json.load(fh)
872
- value = data.get("activeRunId")
873
- if isinstance(value, str) and value:
874
- run_id = value
875
- except Exception:
876
- pass
877
- print(run_id)
878
- PY
879
- )
880
- else
881
- STAGE=$(grep -o '"currentStage"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
882
- ACTIVE_RUN=$(grep -o '"activeRunId"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
883
- fi
884
- fi
885
-
886
- # Skip observation of cclaw hooks to avoid recursion
887
- case "$TOOL" in
888
- cclaw*|*cclaw-hook*|observe) exit 0 ;;
889
- esac
890
-
891
- if [ "$PHASE" = "pre" ]; then
892
- EVENT="tool_start"
893
- else
894
- EVENT="tool_complete"
895
- fi
896
-
897
- # Scrub potential secrets (env vars, tokens, keys) — BSD/macOS sed compatible
898
- PAYLOAD=$(echo "$PAYLOAD" | sed -E 's/[A-Za-z0-9_]*([Kk][Ee][Yy]|[Tt][Oo][Kk][Ee][Nn]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Cc][Rr][Ee][Dd][Ee][Nn][Tt][Ii][Aa][Ll])[A-Za-z0-9_]*[=:][^ ",}]+/[REDACTED]/g' 2>/dev/null || echo "$PAYLOAD")
899
-
900
- # Build JSONL lines.
901
- if command -v jq >/dev/null 2>&1; then
902
- EVENT_JSON=$(jq -n -c \\
903
- --arg ts "$TS" \\
904
- --arg event "$EVENT" \\
905
- --arg tool "$TOOL" \\
906
- --arg phase "$PHASE" \\
907
- --arg stage "$STAGE" \\
908
- --arg runId "$ACTIVE_RUN" \\
909
- --arg payload "$PAYLOAD" \\
910
- '{ts:$ts,event:$event,tool:$tool,phase:$phase,stage:$stage,runId:$runId,data:$payload}' 2>/dev/null || echo "")
911
- ACTIVITY_JSON=$(jq -n -c \\
912
- --arg ts "$TS" \\
913
- --arg event "$EVENT" \\
914
- --arg tool "$TOOL" \\
915
- --arg phase "$PHASE" \\
916
- --arg stage "$STAGE" \\
917
- --arg runId "$ACTIVE_RUN" \\
918
- --arg schemaVersion "1" \\
919
- '{ts:$ts,event:$event,tool:$tool,phase:$phase,stage:$stage,runId:$runId,schemaVersion:($schemaVersion|tonumber)}' 2>/dev/null || echo "")
920
- else
921
- EVENT_JSON=$(printf '{"ts":"%s","event":"%s","tool":"%s","phase":"%s","stage":"%s","runId":"%s","data":"%s"}' \\
922
- "$(escape_json "$TS")" \\
923
- "$(escape_json "$EVENT")" \\
924
- "$(escape_json "$TOOL")" \\
925
- "$(escape_json "$PHASE")" \\
926
- "$(escape_json "$STAGE")" \\
927
- "$(escape_json "$ACTIVE_RUN")" \\
928
- "$(escape_json "$PAYLOAD")")
929
- ACTIVITY_JSON=$(printf '{"ts":"%s","event":"%s","tool":"%s","phase":"%s","stage":"%s","runId":"%s","schemaVersion":1}' \\
930
- "$(escape_json "$TS")" \\
931
- "$(escape_json "$EVENT")" \\
932
- "$(escape_json "$TOOL")" \\
933
- "$(escape_json "$PHASE")" \\
934
- "$(escape_json "$STAGE")" \\
935
- "$(escape_json "$ACTIVE_RUN")")
936
- fi
937
-
938
- if acquire_lock; then
939
- trap release_lock EXIT INT TERM
940
- [ -n "$EVENT_JSON" ] && printf '%s\\n' "$EVENT_JSON" >> "$OBS_FILE" 2>/dev/null
941
- [ -n "$ACTIVITY_JSON" ] && printf '%s\\n' "$ACTIVITY_JSON" >> "$ACTIVITY_FILE" 2>/dev/null
942
- rotate_file "$OBS_FILE" 4000
943
- rotate_file "$ACTIVITY_FILE" 3000
944
- release_lock
945
- trap - EXIT INT TERM
946
- fi
947
-
948
- sync_run_artifacts
949
-
950
696
  exit 0
951
697
  `;
952
698
  }
@@ -1167,641 +913,6 @@ if [ -s "$TMP_STATE" ]; then
1167
913
  mv "$TMP_STATE" "$MONITOR_STATE" 2>/dev/null || rm -f "$TMP_STATE" 2>/dev/null || true
1168
914
  fi
1169
915
 
1170
- exit 0
1171
- `;
1172
- }
1173
- export function summarizeObservationsRuntimeModule() {
1174
- return `#!/usr/bin/env node
1175
- import fs from "node:fs";
1176
-
1177
- const [, , observationsPath, learningsPath, timestampArg] = process.argv;
1178
-
1179
- function readTailText(filePath, maxBytes = 524288) {
1180
- let fd;
1181
- try {
1182
- fd = fs.openSync(filePath, "r");
1183
- const size = fs.fstatSync(fd).size;
1184
- if (!Number.isFinite(size) || size <= 0) return "";
1185
- const bytesToRead = Math.min(size, maxBytes);
1186
- const buffer = Buffer.allocUnsafe(bytesToRead);
1187
- fs.readSync(fd, buffer, 0, bytesToRead, size - bytesToRead);
1188
- return buffer.toString("utf8");
1189
- } catch {
1190
- return "";
1191
- } finally {
1192
- if (fd !== undefined) {
1193
- try {
1194
- fs.closeSync(fd);
1195
- } catch {
1196
- // ignore
1197
- }
1198
- }
1199
- }
1200
- }
1201
-
1202
- function readTailLines(filePath, maxLines, maxBytes = 524288) {
1203
- const raw = readTailText(filePath, maxBytes).trim();
1204
- if (!raw) return [];
1205
- return raw.split(/\\r?\\n/).slice(-maxLines);
1206
- }
1207
-
1208
- function writeAppend(filePath, lines) {
1209
- if (!lines.length) return;
1210
- try {
1211
- fs.appendFileSync(filePath, lines.join("\\n") + "\\n", "utf8");
1212
- } catch {
1213
- // advisory-only runtime path
1214
- }
1215
- }
1216
-
1217
- function parseLine(line) {
1218
- const trimmed = line.trim();
1219
- if (!trimmed) return null;
1220
- try {
1221
- const parsed = JSON.parse(trimmed);
1222
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1223
- return parsed;
1224
- }
1225
- return null;
1226
- } catch {
1227
- return null;
1228
- }
1229
- }
1230
-
1231
- function toText(value) {
1232
- if (typeof value === "string") return value;
1233
- try {
1234
- return JSON.stringify(value);
1235
- } catch {
1236
- return "";
1237
- }
1238
- }
1239
-
1240
- const keyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1241
- const errorPattern = /(error|fail|timeout|exception)/i;
1242
- const timestamp = timestampArg || new Date().toISOString();
1243
- const observationLines = readTailLines(observationsPath, 4000, 1024 * 1024);
1244
- const learningLines = readTailLines(learningsPath, 6000, 1024 * 1024);
1245
-
1246
- const observations = observationLines
1247
- .map(parseLine)
1248
- .filter(Boolean);
1249
-
1250
- if (observations.length < 5) {
1251
- process.exit(0);
1252
- }
1253
-
1254
- const toolUsage = new Map();
1255
- const toolErrors = new Map();
1256
- const stageErrors = new Map();
1257
- const longPayload = new Map();
1258
-
1259
- for (const obs of observations) {
1260
- const toolRaw = typeof obs.tool === "string" ? obs.tool : "unknown";
1261
- const stageRaw = typeof obs.stage === "string" ? obs.stage : "none";
1262
- const tool = toolRaw.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "unknown";
1263
- const stage = stageRaw.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "none";
1264
- const payload = toText(obs.data);
1265
- toolUsage.set(tool, (toolUsage.get(tool) || 0) + 1);
1266
- if (payload.length >= 1500) {
1267
- longPayload.set(tool, (longPayload.get(tool) || 0) + 1);
1268
- }
1269
- if (obs.event === "tool_complete" && errorPattern.test(payload)) {
1270
- toolErrors.set(tool, (toolErrors.get(tool) || 0) + 1);
1271
- stageErrors.set(stage, (stageErrors.get(stage) || 0) + 1);
1272
- }
1273
- }
1274
-
1275
- const candidates = [];
1276
-
1277
- for (const [tool, errors] of toolErrors.entries()) {
1278
- if (errors < 3) continue;
1279
- candidates.push({
1280
- ts: timestamp,
1281
- skill: "observation",
1282
- type: "pitfall",
1283
- key: "frequent-errors-" + tool,
1284
- insight: "Tool " + tool + " produced " + errors + " error-like completions in a single session; add a preflight checklist before using it.",
1285
- confidence: Math.min(9, 4 + Math.floor(errors / 2)),
1286
- source: "observed"
1287
- });
1288
- }
1289
-
1290
- for (const [tool, total] of toolUsage.entries()) {
1291
- if (total < 8) continue;
1292
- const errors = toolErrors.get(tool) || 0;
1293
- if (errors > Math.max(1, Math.floor(total * 0.15))) continue;
1294
- candidates.push({
1295
- ts: timestamp,
1296
- skill: "observation",
1297
- type: "pattern",
1298
- key: "reliable-tool-" + tool,
1299
- insight: "Tool " + tool + " was used " + total + " times with low failure rate; prefer it as a first option for similar tasks.",
1300
- confidence: Math.min(8, 3 + Math.floor(total / 3)),
1301
- source: "observed"
1302
- });
1303
- }
1304
-
1305
- for (const [stage, errors] of stageErrors.entries()) {
1306
- if (stage === "none" || errors < 4) continue;
1307
- candidates.push({
1308
- ts: timestamp,
1309
- skill: "observation",
1310
- type: "pitfall",
1311
- key: "stage-hotspot-" + stage,
1312
- insight: "Stage " + stage + " produced " + errors + " error-like tool completions in one session; add stage-specific checks before execution.",
1313
- confidence: Math.min(8, 3 + Math.floor(errors / 2)),
1314
- source: "observed"
1315
- });
1316
- }
1317
-
1318
- for (const [tool, count] of longPayload.entries()) {
1319
- if (count < 3) continue;
1320
- candidates.push({
1321
- ts: timestamp,
1322
- skill: "observation",
1323
- type: "preference",
1324
- key: "truncate-heavy-payloads-" + tool,
1325
- insight: "Tool " + tool + " produced large payloads repeatedly; summarize outputs earlier to avoid context pressure.",
1326
- confidence: Math.min(7, 3 + Math.floor(count / 2)),
1327
- source: "observed"
1328
- });
1329
- }
1330
-
1331
- const toolFilePathCounts = new Map();
1332
- function extractFilePathsFromPayload(dataVal) {
1333
- const text = toText(dataVal);
1334
- if (!text) return [];
1335
- const found = [];
1336
- const seen = new Set();
1337
- try {
1338
- const obj = JSON.parse(text);
1339
- if (obj && typeof obj === "object" && !Array.isArray(obj)) {
1340
- const keys = [
1341
- "path",
1342
- "file_path",
1343
- "filePath",
1344
- "target_file",
1345
- "file",
1346
- "filepath",
1347
- "old_path",
1348
- "new_path",
1349
- "targetPath"
1350
- ];
1351
- for (const k of keys) {
1352
- if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
1353
- const v = obj[k];
1354
- if (typeof v === "string" && v.length > 1 && (v.includes("/") || v.includes("\\\\"))) {
1355
- const norm = v.replace(/\\\\/g, "/").replace(/\\/+/g, "/");
1356
- if (!seen.has(norm)) {
1357
- seen.add(norm);
1358
- found.push(norm);
1359
- }
1360
- }
1361
- }
1362
- }
1363
- } catch {
1364
- // ignore JSON parse errors
1365
- }
1366
- return found;
1367
- }
1368
-
1369
- for (const obs of observations) {
1370
- const toolRaw = typeof obs.tool === "string" ? obs.tool : "unknown";
1371
- const tool = toolRaw.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "unknown";
1372
- for (const filePath of extractFilePathsFromPayload(obs.data)) {
1373
- const pairKey = JSON.stringify([tool, filePath]);
1374
- toolFilePathCounts.set(pairKey, (toolFilePathCounts.get(pairKey) || 0) + 1);
1375
- }
1376
- }
1377
-
1378
- for (const [pairKey, uses] of toolFilePathCounts.entries()) {
1379
- if (uses < 5) continue;
1380
- let tool = "unknown";
1381
- let filePath = "";
1382
- try {
1383
- const parsed = JSON.parse(pairKey);
1384
- if (!Array.isArray(parsed) || parsed.length !== 2) continue;
1385
- if (typeof parsed[0] === "string") tool = parsed[0];
1386
- if (typeof parsed[1] === "string") filePath = parsed[1];
1387
- } catch {
1388
- continue;
1389
- }
1390
- if (!filePath) continue;
1391
- const parts = filePath.split(/[/\\\\]/);
1392
- let basename = parts[parts.length - 1] || filePath;
1393
- basename = basename.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "file";
1394
- const key = "file-affinity-" + tool + "-" + basename;
1395
- if (!keyPattern.test(key)) continue;
1396
- candidates.push({
1397
- ts: timestamp,
1398
- skill: "observation",
1399
- type: "pattern",
1400
- key: key,
1401
- insight:
1402
- "Tool " +
1403
- tool +
1404
- " frequently targets " +
1405
- filePath +
1406
- "; consider pre-loading it for this stage.",
1407
- confidence: 4,
1408
- source: "observed"
1409
- });
1410
- }
1411
-
1412
- if (observations.length >= 10) {
1413
- const stages = new Set();
1414
- for (const obs of observations) {
1415
- const s = typeof obs.stage === "string" ? obs.stage.trim() : "none";
1416
- stages.add(s || "none");
1417
- }
1418
- if (stages.size === 1) {
1419
- const onlyStage = [...stages][0];
1420
- if (onlyStage && onlyStage !== "none") {
1421
- const stageSan = onlyStage.replace(/[^A-Za-z0-9._-]+/g, "-") || "none";
1422
- const times = [];
1423
- for (const obs of observations) {
1424
- if (typeof obs.ts === "string") {
1425
- const ms = Date.parse(obs.ts);
1426
- if (!Number.isNaN(ms)) times.push(ms);
1427
- }
1428
- }
1429
- times.sort((a, b) => a - b);
1430
- if (times.length >= 2) {
1431
- const spanMin = Math.max(
1432
- 1,
1433
- Math.round((times[times.length - 1] - times[0]) / 60000)
1434
- );
1435
- const M = observations.length;
1436
- const velKey = "stage-velocity-" + stageSan;
1437
- if (keyPattern.test(velKey)) {
1438
- candidates.push({
1439
- ts: timestamp,
1440
- skill: "observation",
1441
- type: "operational",
1442
- key: velKey,
1443
- insight:
1444
- "Stage " +
1445
- onlyStage +
1446
- " took approximately " +
1447
- spanMin +
1448
- " minutes with " +
1449
- M +
1450
- " tool calls.",
1451
- confidence: 3,
1452
- source: "observed"
1453
- });
1454
- }
1455
- }
1456
- }
1457
- }
1458
- }
1459
-
1460
- const valid = [];
1461
- const bestCandidate = new Map();
1462
- for (const candidate of candidates) {
1463
- if (typeof candidate.key !== "string" || !keyPattern.test(candidate.key)) continue;
1464
- if (typeof candidate.insight !== "string" || candidate.insight.trim().length < 16) continue;
1465
- if (!Number.isInteger(candidate.confidence) || candidate.confidence < 1 || candidate.confidence > 10) continue;
1466
- const token = candidate.key + ":" + candidate.type;
1467
- const current = bestCandidate.get(token);
1468
- if (!current || candidate.confidence > current.confidence) {
1469
- bestCandidate.set(token, candidate);
1470
- }
1471
- }
1472
- for (const value of bestCandidate.values()) {
1473
- valid.push(value);
1474
- }
1475
-
1476
- const bestExisting = new Map();
1477
- for (const line of learningLines) {
1478
- const parsed = parseLine(line);
1479
- if (!parsed) continue;
1480
- if (typeof parsed.key !== "string" || typeof parsed.type !== "string") continue;
1481
- if (typeof parsed.confidence !== "number" || !Number.isInteger(parsed.confidence)) continue;
1482
- const token = parsed.key + ":" + parsed.type;
1483
- const current = bestExisting.get(token) || 0;
1484
- if (parsed.confidence > current) {
1485
- bestExisting.set(token, parsed.confidence);
1486
- }
1487
- }
1488
-
1489
- const appended = [];
1490
- for (const candidate of valid) {
1491
- const token = candidate.key + ":" + candidate.type;
1492
- const current = bestExisting.get(token) || 0;
1493
- if (candidate.confidence > current) {
1494
- appended.push(JSON.stringify(candidate));
1495
- bestExisting.set(token, candidate.confidence);
1496
- }
1497
- }
1498
-
1499
- writeAppend(learningsPath, appended);
1500
- process.exit(0);
1501
- `;
1502
- }
1503
- export function summarizeObservationsScript() {
1504
- return `#!/usr/bin/env bash
1505
- # cclaw stop-summarize hook — generated by cclaw sync
1506
- # Analyzes recent observations and creates learnings entries.
1507
- # Runs as part of the stop hook chain.
1508
- set -uo pipefail
1509
-
1510
- ${RUNTIME_SHELL_DETECT_ROOT}
1511
-
1512
- OBS_FILE="$ROOT/${RUNTIME_ROOT}/observations.jsonl"
1513
- LEARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/learnings.jsonl"
1514
- ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
1515
- LOCK_DIR="$ROOT/${RUNTIME_ROOT}/state/.observe.lock"
1516
- mkdir -p "$ROOT/${RUNTIME_ROOT}/state" 2>/dev/null || true
1517
- [ -f "$LEARNINGS_FILE" ] || : > "$LEARNINGS_FILE" 2>/dev/null || true
1518
-
1519
- # Guard
1520
- [ -f "$OBS_FILE" ] || exit 0
1521
- [ -s "$OBS_FILE" ] || exit 0
1522
-
1523
- acquire_lock() {
1524
- local attempt=0
1525
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do
1526
- attempt=$((attempt + 1))
1527
- if [ "$attempt" -ge 200 ]; then
1528
- return 1
1529
- fi
1530
- sleep 0.02
1531
- done
1532
- return 0
1533
- }
1534
-
1535
- release_lock() {
1536
- rmdir "$LOCK_DIR" 2>/dev/null || true
1537
- }
1538
-
1539
- rotate_file() {
1540
- local file_path="$1"
1541
- local keep_lines="$2"
1542
- if [ ! -f "$file_path" ]; then
1543
- return
1544
- fi
1545
- local line_count
1546
- line_count=$(wc -l < "$file_path" 2>/dev/null | tr -d ' ')
1547
- if [ -z "$line_count" ]; then
1548
- return
1549
- fi
1550
- if [ "$line_count" -gt $((keep_lines * 2)) ]; then
1551
- local tmp_path="\${file_path}.tmp.$$"
1552
- if tail -n "$keep_lines" "$file_path" > "$tmp_path" 2>/dev/null; then
1553
- mv "$tmp_path" "$file_path" 2>/dev/null || rm -f "$tmp_path" 2>/dev/null || true
1554
- fi
1555
- fi
1556
- }
1557
-
1558
- acquire_lock || exit 0
1559
- trap release_lock EXIT INT TERM
1560
-
1561
- # Count observations in this session
1562
- OBS_COUNT=$(wc -l < "$OBS_FILE" 2>/dev/null | tr -d ' ')
1563
- [ "$OBS_COUNT" -lt 5 ] && exit 0
1564
-
1565
- TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
1566
- RUNTIME_SUMMARIZER="$ROOT/${RUNTIME_ROOT}/hooks/summarize-observations.mjs"
1567
- if command -v node >/dev/null 2>&1 && [ -f "$RUNTIME_SUMMARIZER" ]; then
1568
- node "$RUNTIME_SUMMARIZER" "$OBS_FILE" "$LEARNINGS_FILE" "$TS" >/dev/null 2>&1 || true
1569
- elif command -v python3 >/dev/null 2>&1; then
1570
- python3 - "$OBS_FILE" "$LEARNINGS_FILE" "$TS" <<'PY' >/dev/null 2>&1
1571
- import json
1572
- import re
1573
- import sys
1574
- from collections import Counter
1575
- from pathlib import Path
1576
-
1577
- obs_path = Path(sys.argv[1])
1578
- learnings_path = Path(sys.argv[2])
1579
- timestamp = sys.argv[3]
1580
-
1581
- error_pattern = re.compile(r"(error|fail)", re.IGNORECASE)
1582
- key_pattern = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
1583
-
1584
- observations = []
1585
- for line in obs_path.read_text(encoding="utf-8").splitlines():
1586
- line = line.strip()
1587
- if not line:
1588
- continue
1589
- try:
1590
- value = json.loads(line)
1591
- except Exception:
1592
- continue
1593
- if isinstance(value, dict):
1594
- observations.append(value)
1595
-
1596
- if len(observations) < 5:
1597
- raise SystemExit(0)
1598
-
1599
- tool_counts: Counter[str] = Counter()
1600
- error_counts: Counter[str] = Counter()
1601
- stage_error_counts: Counter[str] = Counter()
1602
-
1603
- for event in observations:
1604
- tool = event.get("tool")
1605
- if not isinstance(tool, str) or not tool:
1606
- tool = "unknown"
1607
- stage = event.get("stage")
1608
- if not isinstance(stage, str) or not stage:
1609
- stage = "none"
1610
-
1611
- tool_counts[tool] += 1
1612
-
1613
- payload = event.get("data")
1614
- if isinstance(payload, str):
1615
- payload_text = payload
1616
- else:
1617
- try:
1618
- payload_text = json.dumps(payload, ensure_ascii=False)
1619
- except Exception:
1620
- payload_text = ""
1621
-
1622
- if event.get("event") == "tool_complete" and error_pattern.search(payload_text):
1623
- error_counts[tool] += 1
1624
- stage_error_counts[stage] += 1
1625
-
1626
- candidates: list[dict[str, object]] = []
1627
- if error_counts:
1628
- top_tool, top_count = error_counts.most_common(1)[0]
1629
- if top_count >= 3:
1630
- candidates.append({
1631
- "ts": timestamp,
1632
- "skill": "observation",
1633
- "type": "pitfall",
1634
- "key": f"frequent-errors-{top_tool}",
1635
- "insight": f"Tool {top_tool} had {top_count} error-like outputs in one session; add a preflight checklist before using it.",
1636
- "confidence": min(9, 4 + (top_count // 2)),
1637
- "source": "observed"
1638
- })
1639
-
1640
- if tool_counts:
1641
- top_tool, top_total = tool_counts.most_common(1)[0]
1642
- if top_total >= 8 and error_counts.get(top_tool, 0) <= 1:
1643
- candidates.append({
1644
- "ts": timestamp,
1645
- "skill": "observation",
1646
- "type": "pattern",
1647
- "key": f"reliable-tool-{top_tool}",
1648
- "insight": f"Tool {top_tool} was used {top_total} times with low failure rate; prefer it as first option for similar tasks.",
1649
- "confidence": min(8, 3 + (top_total // 3)),
1650
- "source": "observed"
1651
- })
1652
-
1653
- if stage_error_counts:
1654
- top_stage, top_stage_count = stage_error_counts.most_common(1)[0]
1655
- if top_stage != "none" and top_stage_count >= 4:
1656
- candidates.append({
1657
- "ts": timestamp,
1658
- "skill": "observation",
1659
- "type": "pitfall",
1660
- "key": f"stage-hotspot-{top_stage}",
1661
- "insight": f"Stage {top_stage} produced {top_stage_count} error-like tool completions in one session; stage-specific guardrails are needed.",
1662
- "confidence": min(8, 3 + (top_stage_count // 2)),
1663
- "source": "observed"
1664
- })
1665
-
1666
- def is_valid(entry: dict[str, object]) -> bool:
1667
- ts = entry.get("ts")
1668
- key = entry.get("key")
1669
- insight = entry.get("insight")
1670
- conf = entry.get("confidence")
1671
- typ = entry.get("type")
1672
- source = entry.get("source")
1673
-
1674
- if not isinstance(ts, str) or not ts:
1675
- return False
1676
- if not isinstance(key, str) or not key_pattern.match(key):
1677
- return False
1678
- if not isinstance(insight, str) or len(insight.strip()) < 16:
1679
- return False
1680
- if not isinstance(conf, int) or conf < 1 or conf > 10:
1681
- return False
1682
- if typ not in {"pitfall", "pattern", "preference"}:
1683
- return False
1684
- if source not in {"observed", "user-stated"}:
1685
- return False
1686
- return True
1687
-
1688
- best_existing: dict[str, int] = {}
1689
- if learnings_path.exists():
1690
- for line in learnings_path.read_text(encoding="utf-8").splitlines():
1691
- line = line.strip()
1692
- if not line:
1693
- continue
1694
- try:
1695
- entry = json.loads(line)
1696
- except Exception:
1697
- continue
1698
- if not isinstance(entry, dict):
1699
- continue
1700
- key = entry.get("key")
1701
- typ = entry.get("type")
1702
- conf = entry.get("confidence")
1703
- if isinstance(key, str) and isinstance(typ, str) and isinstance(conf, int):
1704
- token = f"{key}:{typ}"
1705
- best_existing[token] = max(best_existing.get(token, 0), conf)
1706
-
1707
- appended: list[str] = []
1708
- with learnings_path.open("a", encoding="utf-8") as handle:
1709
- for candidate in candidates:
1710
- if not is_valid(candidate):
1711
- continue
1712
- token = f"{candidate['key']}:{candidate['type']}"
1713
- confidence = int(candidate["confidence"])
1714
- if confidence <= best_existing.get(token, 0):
1715
- continue
1716
- handle.write(json.dumps(candidate, ensure_ascii=False) + "\\n")
1717
- best_existing[token] = confidence
1718
- appended.append(str(candidate["key"]))
1719
-
1720
- raise SystemExit(0)
1721
- PY
1722
- elif command -v jq >/dev/null 2>&1; then
1723
- ERROR_PATTERNS=$(jq -r 'select(.event=="tool_complete") | select(.data | test("error|Error|ERROR|fail|Fail|FAIL"; "g")) | .tool' "$OBS_FILE" 2>/dev/null | sort | uniq -c | sort -rn | head -3)
1724
- if [ -n "$ERROR_PATTERNS" ]; then
1725
- TOP_ERROR_TOOL=$(echo "$ERROR_PATTERNS" | head -1 | awk '{print $2}')
1726
- TOP_ERROR_COUNT=$(echo "$ERROR_PATTERNS" | head -1 | awk '{print $1}')
1727
- if [ "$TOP_ERROR_COUNT" -ge 3 ]; then
1728
- CANDIDATE=$(jq -n -c \\
1729
- --arg ts "$TS" \\
1730
- --arg tool "$TOP_ERROR_TOOL" \\
1731
- --arg count "$TOP_ERROR_COUNT" \\
1732
- '{ts:$ts,skill:"observation",type:"pitfall",key:("frequent-errors-"+$tool),insight:("Tool "+$tool+" had "+$count+" error-like outputs in one session; add a preflight checklist before using it."),confidence:(5 + (($count|tonumber) / 2 | floor)),source:"observed"}' 2>/dev/null || echo "")
1733
- if [ -n "$CANDIDATE" ]; then
1734
- CANDIDATE=$(echo "$CANDIDATE" | jq -c 'if .confidence > 10 then .confidence = 10 else . end' 2>/dev/null || echo "")
1735
- fi
1736
-
1737
- if [ -n "$CANDIDATE" ]; then
1738
- CANDIDATE_OK=$(echo "$CANDIDATE" | jq -r '
1739
- (.ts|type=="string") and
1740
- (.key|type=="string") and
1741
- (.type|type=="string") and
1742
- (.insight|type=="string" and (length >= 16)) and
1743
- (.confidence|type=="number" and . >= 1 and . <= 10) and
1744
- (.source|type=="string")
1745
- ' 2>/dev/null || echo "false")
1746
- if [ "$CANDIDATE_OK" = "true" ]; then
1747
- CANDIDATE_KEY=$(echo "$CANDIDATE" | jq -r '.key' 2>/dev/null || echo "")
1748
- CANDIDATE_TYPE=$(echo "$CANDIDATE" | jq -r '.type' 2>/dev/null || echo "")
1749
- CANDIDATE_CONF=$(echo "$CANDIDATE" | jq -r '.confidence' 2>/dev/null || echo "0")
1750
- EXISTING_CONF=$(jq -r --arg key "$CANDIDATE_KEY" --arg type "$CANDIDATE_TYPE" '
1751
- select(.key == $key and .type == $type) | (.confidence // 0)
1752
- ' "$LEARNINGS_FILE" 2>/dev/null | sort -nr | head -1)
1753
- [ -n "$EXISTING_CONF" ] || EXISTING_CONF=0
1754
- if [ "$CANDIDATE_CONF" -gt "$EXISTING_CONF" ]; then
1755
- printf '%s\\n' "$CANDIDATE" >> "$LEARNINGS_FILE" 2>/dev/null || true
1756
- fi
1757
- fi
1758
- fi
1759
- fi
1760
- fi
1761
- fi
1762
-
1763
- # Archive observations (rotate to prevent unbounded growth)
1764
- ARCHIVE_DIR="$ROOT/${RUNTIME_ROOT}/observations.archive"
1765
- mkdir -p "$ARCHIVE_DIR" 2>/dev/null
1766
- ARCHIVE_FILE="$ARCHIVE_DIR/$(date -u +"%Y%m%d-%H%M%S").jsonl"
1767
- cp "$OBS_FILE" "$ARCHIVE_FILE" 2>/dev/null || true
1768
- : > "$OBS_FILE" 2>/dev/null || true
1769
-
1770
- # Retain only the most recent 30 archives.
1771
- ARCHIVE_LIST=$(ls -1t "$ARCHIVE_DIR"/*.jsonl 2>/dev/null || true)
1772
- COUNT=0
1773
- for file in $ARCHIVE_LIST; do
1774
- COUNT=$((COUNT + 1))
1775
- if [ "$COUNT" -gt 30 ]; then
1776
- rm -f "$file" 2>/dev/null || true
1777
- fi
1778
- done
1779
-
1780
- # Write session digest for cross-session context
1781
- STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
1782
- DIGEST_FILE="$ROOT/${RUNTIME_ROOT}/state/session-digest.md"
1783
- STAGE_NOW="none"
1784
- RUN_DIGEST_ID="none"
1785
- if [ -f "$STATE_FILE" ]; then
1786
- if command -v jq >/dev/null 2>&1; then
1787
- STAGE_NOW=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
1788
- RUN_DIGEST_ID=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
1789
- fi
1790
- fi
1791
- {
1792
- printf '%s\\n' "# Session Digest"
1793
- printf '%s\\n' "- Stage: $STAGE_NOW"
1794
- printf '%s\\n' "- Observations: $OBS_COUNT"
1795
- printf '%s\\n' "- Timestamp: $TS"
1796
- printf '%s\\n' "- Run: $RUN_DIGEST_ID"
1797
- } > "$DIGEST_FILE" 2>/dev/null || true
1798
-
1799
- # Keep stage activity bounded by line count.
1800
- rotate_file "$ACTIVITY_FILE" 2000
1801
-
1802
- release_lock
1803
- trap - EXIT INT TERM
1804
-
1805
916
  exit 0
1806
917
  `;
1807
918
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.46.5",
3
+ "version": "0.46.7",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {