cclaw-cli 0.46.6 → 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.
@@ -16,8 +16,6 @@ export interface WorkflowGuardOptions {
16
16
  }
17
17
  export declare function workflowGuardScript(options?: WorkflowGuardOptions): string;
18
18
  export declare function contextMonitorScript(): string;
19
- export declare function summarizeObservationsRuntimeModule(): string;
20
- export declare function summarizeObservationsScript(): string;
21
19
  export declare function claudeHooksJsonWithObservation(): string;
22
20
  export declare function cursorHooksJsonWithObservation(): string;
23
21
  /**
@@ -913,641 +913,6 @@ if [ -s "$TMP_STATE" ]; then
913
913
  mv "$TMP_STATE" "$MONITOR_STATE" 2>/dev/null || rm -f "$TMP_STATE" 2>/dev/null || true
914
914
  fi
915
915
 
916
- exit 0
917
- `;
918
- }
919
- export function summarizeObservationsRuntimeModule() {
920
- return `#!/usr/bin/env node
921
- import fs from "node:fs";
922
-
923
- const [, , observationsPath, learningsPath, timestampArg] = process.argv;
924
-
925
- function readTailText(filePath, maxBytes = 524288) {
926
- let fd;
927
- try {
928
- fd = fs.openSync(filePath, "r");
929
- const size = fs.fstatSync(fd).size;
930
- if (!Number.isFinite(size) || size <= 0) return "";
931
- const bytesToRead = Math.min(size, maxBytes);
932
- const buffer = Buffer.allocUnsafe(bytesToRead);
933
- fs.readSync(fd, buffer, 0, bytesToRead, size - bytesToRead);
934
- return buffer.toString("utf8");
935
- } catch {
936
- return "";
937
- } finally {
938
- if (fd !== undefined) {
939
- try {
940
- fs.closeSync(fd);
941
- } catch {
942
- // ignore
943
- }
944
- }
945
- }
946
- }
947
-
948
- function readTailLines(filePath, maxLines, maxBytes = 524288) {
949
- const raw = readTailText(filePath, maxBytes).trim();
950
- if (!raw) return [];
951
- return raw.split(/\\r?\\n/).slice(-maxLines);
952
- }
953
-
954
- function writeAppend(filePath, lines) {
955
- if (!lines.length) return;
956
- try {
957
- fs.appendFileSync(filePath, lines.join("\\n") + "\\n", "utf8");
958
- } catch {
959
- // advisory-only runtime path
960
- }
961
- }
962
-
963
- function parseLine(line) {
964
- const trimmed = line.trim();
965
- if (!trimmed) return null;
966
- try {
967
- const parsed = JSON.parse(trimmed);
968
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
969
- return parsed;
970
- }
971
- return null;
972
- } catch {
973
- return null;
974
- }
975
- }
976
-
977
- function toText(value) {
978
- if (typeof value === "string") return value;
979
- try {
980
- return JSON.stringify(value);
981
- } catch {
982
- return "";
983
- }
984
- }
985
-
986
- const keyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
987
- const errorPattern = /(error|fail|timeout|exception)/i;
988
- const timestamp = timestampArg || new Date().toISOString();
989
- const observationLines = readTailLines(observationsPath, 4000, 1024 * 1024);
990
- const learningLines = readTailLines(learningsPath, 6000, 1024 * 1024);
991
-
992
- const observations = observationLines
993
- .map(parseLine)
994
- .filter(Boolean);
995
-
996
- if (observations.length < 5) {
997
- process.exit(0);
998
- }
999
-
1000
- const toolUsage = new Map();
1001
- const toolErrors = new Map();
1002
- const stageErrors = new Map();
1003
- const longPayload = new Map();
1004
-
1005
- for (const obs of observations) {
1006
- const toolRaw = typeof obs.tool === "string" ? obs.tool : "unknown";
1007
- const stageRaw = typeof obs.stage === "string" ? obs.stage : "none";
1008
- const tool = toolRaw.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "unknown";
1009
- const stage = stageRaw.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "none";
1010
- const payload = toText(obs.data);
1011
- toolUsage.set(tool, (toolUsage.get(tool) || 0) + 1);
1012
- if (payload.length >= 1500) {
1013
- longPayload.set(tool, (longPayload.get(tool) || 0) + 1);
1014
- }
1015
- if (obs.event === "tool_complete" && errorPattern.test(payload)) {
1016
- toolErrors.set(tool, (toolErrors.get(tool) || 0) + 1);
1017
- stageErrors.set(stage, (stageErrors.get(stage) || 0) + 1);
1018
- }
1019
- }
1020
-
1021
- const candidates = [];
1022
-
1023
- for (const [tool, errors] of toolErrors.entries()) {
1024
- if (errors < 3) continue;
1025
- candidates.push({
1026
- ts: timestamp,
1027
- skill: "observation",
1028
- type: "pitfall",
1029
- key: "frequent-errors-" + tool,
1030
- insight: "Tool " + tool + " produced " + errors + " error-like completions in a single session; add a preflight checklist before using it.",
1031
- confidence: Math.min(9, 4 + Math.floor(errors / 2)),
1032
- source: "observed"
1033
- });
1034
- }
1035
-
1036
- for (const [tool, total] of toolUsage.entries()) {
1037
- if (total < 8) continue;
1038
- const errors = toolErrors.get(tool) || 0;
1039
- if (errors > Math.max(1, Math.floor(total * 0.15))) continue;
1040
- candidates.push({
1041
- ts: timestamp,
1042
- skill: "observation",
1043
- type: "pattern",
1044
- key: "reliable-tool-" + tool,
1045
- insight: "Tool " + tool + " was used " + total + " times with low failure rate; prefer it as a first option for similar tasks.",
1046
- confidence: Math.min(8, 3 + Math.floor(total / 3)),
1047
- source: "observed"
1048
- });
1049
- }
1050
-
1051
- for (const [stage, errors] of stageErrors.entries()) {
1052
- if (stage === "none" || errors < 4) continue;
1053
- candidates.push({
1054
- ts: timestamp,
1055
- skill: "observation",
1056
- type: "pitfall",
1057
- key: "stage-hotspot-" + stage,
1058
- insight: "Stage " + stage + " produced " + errors + " error-like tool completions in one session; add stage-specific checks before execution.",
1059
- confidence: Math.min(8, 3 + Math.floor(errors / 2)),
1060
- source: "observed"
1061
- });
1062
- }
1063
-
1064
- for (const [tool, count] of longPayload.entries()) {
1065
- if (count < 3) continue;
1066
- candidates.push({
1067
- ts: timestamp,
1068
- skill: "observation",
1069
- type: "preference",
1070
- key: "truncate-heavy-payloads-" + tool,
1071
- insight: "Tool " + tool + " produced large payloads repeatedly; summarize outputs earlier to avoid context pressure.",
1072
- confidence: Math.min(7, 3 + Math.floor(count / 2)),
1073
- source: "observed"
1074
- });
1075
- }
1076
-
1077
- const toolFilePathCounts = new Map();
1078
- function extractFilePathsFromPayload(dataVal) {
1079
- const text = toText(dataVal);
1080
- if (!text) return [];
1081
- const found = [];
1082
- const seen = new Set();
1083
- try {
1084
- const obj = JSON.parse(text);
1085
- if (obj && typeof obj === "object" && !Array.isArray(obj)) {
1086
- const keys = [
1087
- "path",
1088
- "file_path",
1089
- "filePath",
1090
- "target_file",
1091
- "file",
1092
- "filepath",
1093
- "old_path",
1094
- "new_path",
1095
- "targetPath"
1096
- ];
1097
- for (const k of keys) {
1098
- if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
1099
- const v = obj[k];
1100
- if (typeof v === "string" && v.length > 1 && (v.includes("/") || v.includes("\\\\"))) {
1101
- const norm = v.replace(/\\\\/g, "/").replace(/\\/+/g, "/");
1102
- if (!seen.has(norm)) {
1103
- seen.add(norm);
1104
- found.push(norm);
1105
- }
1106
- }
1107
- }
1108
- }
1109
- } catch {
1110
- // ignore JSON parse errors
1111
- }
1112
- return found;
1113
- }
1114
-
1115
- for (const obs of observations) {
1116
- const toolRaw = typeof obs.tool === "string" ? obs.tool : "unknown";
1117
- const tool = toolRaw.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "unknown";
1118
- for (const filePath of extractFilePathsFromPayload(obs.data)) {
1119
- const pairKey = JSON.stringify([tool, filePath]);
1120
- toolFilePathCounts.set(pairKey, (toolFilePathCounts.get(pairKey) || 0) + 1);
1121
- }
1122
- }
1123
-
1124
- for (const [pairKey, uses] of toolFilePathCounts.entries()) {
1125
- if (uses < 5) continue;
1126
- let tool = "unknown";
1127
- let filePath = "";
1128
- try {
1129
- const parsed = JSON.parse(pairKey);
1130
- if (!Array.isArray(parsed) || parsed.length !== 2) continue;
1131
- if (typeof parsed[0] === "string") tool = parsed[0];
1132
- if (typeof parsed[1] === "string") filePath = parsed[1];
1133
- } catch {
1134
- continue;
1135
- }
1136
- if (!filePath) continue;
1137
- const parts = filePath.split(/[/\\\\]/);
1138
- let basename = parts[parts.length - 1] || filePath;
1139
- basename = basename.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "file";
1140
- const key = "file-affinity-" + tool + "-" + basename;
1141
- if (!keyPattern.test(key)) continue;
1142
- candidates.push({
1143
- ts: timestamp,
1144
- skill: "observation",
1145
- type: "pattern",
1146
- key: key,
1147
- insight:
1148
- "Tool " +
1149
- tool +
1150
- " frequently targets " +
1151
- filePath +
1152
- "; consider pre-loading it for this stage.",
1153
- confidence: 4,
1154
- source: "observed"
1155
- });
1156
- }
1157
-
1158
- if (observations.length >= 10) {
1159
- const stages = new Set();
1160
- for (const obs of observations) {
1161
- const s = typeof obs.stage === "string" ? obs.stage.trim() : "none";
1162
- stages.add(s || "none");
1163
- }
1164
- if (stages.size === 1) {
1165
- const onlyStage = [...stages][0];
1166
- if (onlyStage && onlyStage !== "none") {
1167
- const stageSan = onlyStage.replace(/[^A-Za-z0-9._-]+/g, "-") || "none";
1168
- const times = [];
1169
- for (const obs of observations) {
1170
- if (typeof obs.ts === "string") {
1171
- const ms = Date.parse(obs.ts);
1172
- if (!Number.isNaN(ms)) times.push(ms);
1173
- }
1174
- }
1175
- times.sort((a, b) => a - b);
1176
- if (times.length >= 2) {
1177
- const spanMin = Math.max(
1178
- 1,
1179
- Math.round((times[times.length - 1] - times[0]) / 60000)
1180
- );
1181
- const M = observations.length;
1182
- const velKey = "stage-velocity-" + stageSan;
1183
- if (keyPattern.test(velKey)) {
1184
- candidates.push({
1185
- ts: timestamp,
1186
- skill: "observation",
1187
- type: "operational",
1188
- key: velKey,
1189
- insight:
1190
- "Stage " +
1191
- onlyStage +
1192
- " took approximately " +
1193
- spanMin +
1194
- " minutes with " +
1195
- M +
1196
- " tool calls.",
1197
- confidence: 3,
1198
- source: "observed"
1199
- });
1200
- }
1201
- }
1202
- }
1203
- }
1204
- }
1205
-
1206
- const valid = [];
1207
- const bestCandidate = new Map();
1208
- for (const candidate of candidates) {
1209
- if (typeof candidate.key !== "string" || !keyPattern.test(candidate.key)) continue;
1210
- if (typeof candidate.insight !== "string" || candidate.insight.trim().length < 16) continue;
1211
- if (!Number.isInteger(candidate.confidence) || candidate.confidence < 1 || candidate.confidence > 10) continue;
1212
- const token = candidate.key + ":" + candidate.type;
1213
- const current = bestCandidate.get(token);
1214
- if (!current || candidate.confidence > current.confidence) {
1215
- bestCandidate.set(token, candidate);
1216
- }
1217
- }
1218
- for (const value of bestCandidate.values()) {
1219
- valid.push(value);
1220
- }
1221
-
1222
- const bestExisting = new Map();
1223
- for (const line of learningLines) {
1224
- const parsed = parseLine(line);
1225
- if (!parsed) continue;
1226
- if (typeof parsed.key !== "string" || typeof parsed.type !== "string") continue;
1227
- if (typeof parsed.confidence !== "number" || !Number.isInteger(parsed.confidence)) continue;
1228
- const token = parsed.key + ":" + parsed.type;
1229
- const current = bestExisting.get(token) || 0;
1230
- if (parsed.confidence > current) {
1231
- bestExisting.set(token, parsed.confidence);
1232
- }
1233
- }
1234
-
1235
- const appended = [];
1236
- for (const candidate of valid) {
1237
- const token = candidate.key + ":" + candidate.type;
1238
- const current = bestExisting.get(token) || 0;
1239
- if (candidate.confidence > current) {
1240
- appended.push(JSON.stringify(candidate));
1241
- bestExisting.set(token, candidate.confidence);
1242
- }
1243
- }
1244
-
1245
- writeAppend(learningsPath, appended);
1246
- process.exit(0);
1247
- `;
1248
- }
1249
- export function summarizeObservationsScript() {
1250
- return `#!/usr/bin/env bash
1251
- # cclaw stop-summarize hook — generated by cclaw sync
1252
- # Analyzes recent observations and creates learnings entries.
1253
- # Runs as part of the stop hook chain.
1254
- set -uo pipefail
1255
-
1256
- ${RUNTIME_SHELL_DETECT_ROOT}
1257
-
1258
- OBS_FILE="$ROOT/${RUNTIME_ROOT}/observations.jsonl"
1259
- LEARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/learnings.jsonl"
1260
- ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
1261
- LOCK_DIR="$ROOT/${RUNTIME_ROOT}/state/.observe.lock"
1262
- mkdir -p "$ROOT/${RUNTIME_ROOT}/state" 2>/dev/null || true
1263
- [ -f "$LEARNINGS_FILE" ] || : > "$LEARNINGS_FILE" 2>/dev/null || true
1264
-
1265
- # Guard
1266
- [ -f "$OBS_FILE" ] || exit 0
1267
- [ -s "$OBS_FILE" ] || exit 0
1268
-
1269
- acquire_lock() {
1270
- local attempt=0
1271
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do
1272
- attempt=$((attempt + 1))
1273
- if [ "$attempt" -ge 200 ]; then
1274
- return 1
1275
- fi
1276
- sleep 0.02
1277
- done
1278
- return 0
1279
- }
1280
-
1281
- release_lock() {
1282
- rmdir "$LOCK_DIR" 2>/dev/null || true
1283
- }
1284
-
1285
- rotate_file() {
1286
- local file_path="$1"
1287
- local keep_lines="$2"
1288
- if [ ! -f "$file_path" ]; then
1289
- return
1290
- fi
1291
- local line_count
1292
- line_count=$(wc -l < "$file_path" 2>/dev/null | tr -d ' ')
1293
- if [ -z "$line_count" ]; then
1294
- return
1295
- fi
1296
- if [ "$line_count" -gt $((keep_lines * 2)) ]; then
1297
- local tmp_path="\${file_path}.tmp.$$"
1298
- if tail -n "$keep_lines" "$file_path" > "$tmp_path" 2>/dev/null; then
1299
- mv "$tmp_path" "$file_path" 2>/dev/null || rm -f "$tmp_path" 2>/dev/null || true
1300
- fi
1301
- fi
1302
- }
1303
-
1304
- acquire_lock || exit 0
1305
- trap release_lock EXIT INT TERM
1306
-
1307
- # Count observations in this session
1308
- OBS_COUNT=$(wc -l < "$OBS_FILE" 2>/dev/null | tr -d ' ')
1309
- [ "$OBS_COUNT" -lt 5 ] && exit 0
1310
-
1311
- TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
1312
- RUNTIME_SUMMARIZER="$ROOT/${RUNTIME_ROOT}/hooks/summarize-observations.mjs"
1313
- if command -v node >/dev/null 2>&1 && [ -f "$RUNTIME_SUMMARIZER" ]; then
1314
- node "$RUNTIME_SUMMARIZER" "$OBS_FILE" "$LEARNINGS_FILE" "$TS" >/dev/null 2>&1 || true
1315
- elif command -v python3 >/dev/null 2>&1; then
1316
- python3 - "$OBS_FILE" "$LEARNINGS_FILE" "$TS" <<'PY' >/dev/null 2>&1
1317
- import json
1318
- import re
1319
- import sys
1320
- from collections import Counter
1321
- from pathlib import Path
1322
-
1323
- obs_path = Path(sys.argv[1])
1324
- learnings_path = Path(sys.argv[2])
1325
- timestamp = sys.argv[3]
1326
-
1327
- error_pattern = re.compile(r"(error|fail)", re.IGNORECASE)
1328
- key_pattern = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
1329
-
1330
- observations = []
1331
- for line in obs_path.read_text(encoding="utf-8").splitlines():
1332
- line = line.strip()
1333
- if not line:
1334
- continue
1335
- try:
1336
- value = json.loads(line)
1337
- except Exception:
1338
- continue
1339
- if isinstance(value, dict):
1340
- observations.append(value)
1341
-
1342
- if len(observations) < 5:
1343
- raise SystemExit(0)
1344
-
1345
- tool_counts: Counter[str] = Counter()
1346
- error_counts: Counter[str] = Counter()
1347
- stage_error_counts: Counter[str] = Counter()
1348
-
1349
- for event in observations:
1350
- tool = event.get("tool")
1351
- if not isinstance(tool, str) or not tool:
1352
- tool = "unknown"
1353
- stage = event.get("stage")
1354
- if not isinstance(stage, str) or not stage:
1355
- stage = "none"
1356
-
1357
- tool_counts[tool] += 1
1358
-
1359
- payload = event.get("data")
1360
- if isinstance(payload, str):
1361
- payload_text = payload
1362
- else:
1363
- try:
1364
- payload_text = json.dumps(payload, ensure_ascii=False)
1365
- except Exception:
1366
- payload_text = ""
1367
-
1368
- if event.get("event") == "tool_complete" and error_pattern.search(payload_text):
1369
- error_counts[tool] += 1
1370
- stage_error_counts[stage] += 1
1371
-
1372
- candidates: list[dict[str, object]] = []
1373
- if error_counts:
1374
- top_tool, top_count = error_counts.most_common(1)[0]
1375
- if top_count >= 3:
1376
- candidates.append({
1377
- "ts": timestamp,
1378
- "skill": "observation",
1379
- "type": "pitfall",
1380
- "key": f"frequent-errors-{top_tool}",
1381
- "insight": f"Tool {top_tool} had {top_count} error-like outputs in one session; add a preflight checklist before using it.",
1382
- "confidence": min(9, 4 + (top_count // 2)),
1383
- "source": "observed"
1384
- })
1385
-
1386
- if tool_counts:
1387
- top_tool, top_total = tool_counts.most_common(1)[0]
1388
- if top_total >= 8 and error_counts.get(top_tool, 0) <= 1:
1389
- candidates.append({
1390
- "ts": timestamp,
1391
- "skill": "observation",
1392
- "type": "pattern",
1393
- "key": f"reliable-tool-{top_tool}",
1394
- "insight": f"Tool {top_tool} was used {top_total} times with low failure rate; prefer it as first option for similar tasks.",
1395
- "confidence": min(8, 3 + (top_total // 3)),
1396
- "source": "observed"
1397
- })
1398
-
1399
- if stage_error_counts:
1400
- top_stage, top_stage_count = stage_error_counts.most_common(1)[0]
1401
- if top_stage != "none" and top_stage_count >= 4:
1402
- candidates.append({
1403
- "ts": timestamp,
1404
- "skill": "observation",
1405
- "type": "pitfall",
1406
- "key": f"stage-hotspot-{top_stage}",
1407
- "insight": f"Stage {top_stage} produced {top_stage_count} error-like tool completions in one session; stage-specific guardrails are needed.",
1408
- "confidence": min(8, 3 + (top_stage_count // 2)),
1409
- "source": "observed"
1410
- })
1411
-
1412
- def is_valid(entry: dict[str, object]) -> bool:
1413
- ts = entry.get("ts")
1414
- key = entry.get("key")
1415
- insight = entry.get("insight")
1416
- conf = entry.get("confidence")
1417
- typ = entry.get("type")
1418
- source = entry.get("source")
1419
-
1420
- if not isinstance(ts, str) or not ts:
1421
- return False
1422
- if not isinstance(key, str) or not key_pattern.match(key):
1423
- return False
1424
- if not isinstance(insight, str) or len(insight.strip()) < 16:
1425
- return False
1426
- if not isinstance(conf, int) or conf < 1 or conf > 10:
1427
- return False
1428
- if typ not in {"pitfall", "pattern", "preference"}:
1429
- return False
1430
- if source not in {"observed", "user-stated"}:
1431
- return False
1432
- return True
1433
-
1434
- best_existing: dict[str, int] = {}
1435
- if learnings_path.exists():
1436
- for line in learnings_path.read_text(encoding="utf-8").splitlines():
1437
- line = line.strip()
1438
- if not line:
1439
- continue
1440
- try:
1441
- entry = json.loads(line)
1442
- except Exception:
1443
- continue
1444
- if not isinstance(entry, dict):
1445
- continue
1446
- key = entry.get("key")
1447
- typ = entry.get("type")
1448
- conf = entry.get("confidence")
1449
- if isinstance(key, str) and isinstance(typ, str) and isinstance(conf, int):
1450
- token = f"{key}:{typ}"
1451
- best_existing[token] = max(best_existing.get(token, 0), conf)
1452
-
1453
- appended: list[str] = []
1454
- with learnings_path.open("a", encoding="utf-8") as handle:
1455
- for candidate in candidates:
1456
- if not is_valid(candidate):
1457
- continue
1458
- token = f"{candidate['key']}:{candidate['type']}"
1459
- confidence = int(candidate["confidence"])
1460
- if confidence <= best_existing.get(token, 0):
1461
- continue
1462
- handle.write(json.dumps(candidate, ensure_ascii=False) + "\\n")
1463
- best_existing[token] = confidence
1464
- appended.append(str(candidate["key"]))
1465
-
1466
- raise SystemExit(0)
1467
- PY
1468
- elif command -v jq >/dev/null 2>&1; then
1469
- 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)
1470
- if [ -n "$ERROR_PATTERNS" ]; then
1471
- TOP_ERROR_TOOL=$(echo "$ERROR_PATTERNS" | head -1 | awk '{print $2}')
1472
- TOP_ERROR_COUNT=$(echo "$ERROR_PATTERNS" | head -1 | awk '{print $1}')
1473
- if [ "$TOP_ERROR_COUNT" -ge 3 ]; then
1474
- CANDIDATE=$(jq -n -c \\
1475
- --arg ts "$TS" \\
1476
- --arg tool "$TOP_ERROR_TOOL" \\
1477
- --arg count "$TOP_ERROR_COUNT" \\
1478
- '{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 "")
1479
- if [ -n "$CANDIDATE" ]; then
1480
- CANDIDATE=$(echo "$CANDIDATE" | jq -c 'if .confidence > 10 then .confidence = 10 else . end' 2>/dev/null || echo "")
1481
- fi
1482
-
1483
- if [ -n "$CANDIDATE" ]; then
1484
- CANDIDATE_OK=$(echo "$CANDIDATE" | jq -r '
1485
- (.ts|type=="string") and
1486
- (.key|type=="string") and
1487
- (.type|type=="string") and
1488
- (.insight|type=="string" and (length >= 16)) and
1489
- (.confidence|type=="number" and . >= 1 and . <= 10) and
1490
- (.source|type=="string")
1491
- ' 2>/dev/null || echo "false")
1492
- if [ "$CANDIDATE_OK" = "true" ]; then
1493
- CANDIDATE_KEY=$(echo "$CANDIDATE" | jq -r '.key' 2>/dev/null || echo "")
1494
- CANDIDATE_TYPE=$(echo "$CANDIDATE" | jq -r '.type' 2>/dev/null || echo "")
1495
- CANDIDATE_CONF=$(echo "$CANDIDATE" | jq -r '.confidence' 2>/dev/null || echo "0")
1496
- EXISTING_CONF=$(jq -r --arg key "$CANDIDATE_KEY" --arg type "$CANDIDATE_TYPE" '
1497
- select(.key == $key and .type == $type) | (.confidence // 0)
1498
- ' "$LEARNINGS_FILE" 2>/dev/null | sort -nr | head -1)
1499
- [ -n "$EXISTING_CONF" ] || EXISTING_CONF=0
1500
- if [ "$CANDIDATE_CONF" -gt "$EXISTING_CONF" ]; then
1501
- printf '%s\\n' "$CANDIDATE" >> "$LEARNINGS_FILE" 2>/dev/null || true
1502
- fi
1503
- fi
1504
- fi
1505
- fi
1506
- fi
1507
- fi
1508
-
1509
- # Archive observations (rotate to prevent unbounded growth)
1510
- ARCHIVE_DIR="$ROOT/${RUNTIME_ROOT}/observations.archive"
1511
- mkdir -p "$ARCHIVE_DIR" 2>/dev/null
1512
- ARCHIVE_FILE="$ARCHIVE_DIR/$(date -u +"%Y%m%d-%H%M%S").jsonl"
1513
- cp "$OBS_FILE" "$ARCHIVE_FILE" 2>/dev/null || true
1514
- : > "$OBS_FILE" 2>/dev/null || true
1515
-
1516
- # Retain only the most recent 30 archives.
1517
- ARCHIVE_LIST=$(ls -1t "$ARCHIVE_DIR"/*.jsonl 2>/dev/null || true)
1518
- COUNT=0
1519
- for file in $ARCHIVE_LIST; do
1520
- COUNT=$((COUNT + 1))
1521
- if [ "$COUNT" -gt 30 ]; then
1522
- rm -f "$file" 2>/dev/null || true
1523
- fi
1524
- done
1525
-
1526
- # Write session digest for cross-session context
1527
- STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
1528
- DIGEST_FILE="$ROOT/${RUNTIME_ROOT}/state/session-digest.md"
1529
- STAGE_NOW="none"
1530
- RUN_DIGEST_ID="none"
1531
- if [ -f "$STATE_FILE" ]; then
1532
- if command -v jq >/dev/null 2>&1; then
1533
- STAGE_NOW=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
1534
- RUN_DIGEST_ID=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
1535
- fi
1536
- fi
1537
- {
1538
- printf '%s\\n' "# Session Digest"
1539
- printf '%s\\n' "- Stage: $STAGE_NOW"
1540
- printf '%s\\n' "- Observations: $OBS_COUNT"
1541
- printf '%s\\n' "- Timestamp: $TS"
1542
- printf '%s\\n' "- Run: $RUN_DIGEST_ID"
1543
- } > "$DIGEST_FILE" 2>/dev/null || true
1544
-
1545
- # Keep stage activity bounded by line count.
1546
- rotate_file "$ACTIVITY_FILE" 2000
1547
-
1548
- release_lock
1549
- trap - EXIT INT TERM
1550
-
1551
916
  exit 0
1552
917
  `;
1553
918
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.46.6",
3
+ "version": "0.46.7",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {