cclaw-cli 0.5.4 → 0.5.5

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.
@@ -1,19 +1,12 @@
1
1
  /**
2
2
  * Hook generators for all supported harnesses.
3
3
  *
4
- * SessionStart: injects using-cclaw + flow state + learnings + checkpoint/activity summary.
4
+ * SessionStart: injects using-cclaw + flow state + knowledge + checkpoint/activity summary.
5
5
  * Stop: writes checkpoint.json and reminds about flow consistency.
6
- * PreToolUse/PostToolUse and summarize chain are generated in observe.ts.
6
+ * Harness hook JSON wiring is generated in observe.ts.
7
7
  */
8
8
  import { RUNTIME_ROOT } from "../constants.js";
9
9
  import { META_SKILL_NAME } from "./meta-skill.js";
10
- function shellLiteral(value) {
11
- return value
12
- .replace(/\\/gu, "\\\\")
13
- .replace(/"/gu, '\\"')
14
- .replace(/\$/gu, "\\$")
15
- .replace(/`/gu, "\\`");
16
- }
17
10
  const ESCAPE_FN = `escape_json() {
18
11
  local str="$1"
19
12
  str=\${str//\\\\/\\\\\\\\}
@@ -43,14 +36,10 @@ if [ -z "$ROOT" ]; then
43
36
  fi`;
44
37
  /** Shared bash preamble for generated hook scripts. */
45
38
  export const RUNTIME_SHELL_DETECT_ROOT = DETECT_ROOT;
46
- export function sessionStartScript(options = {}) {
47
- const globalLearningsEnabled = options.globalLearningsEnabled === true;
48
- const globalLearningsPath = options.globalLearningsPath?.trim() ?? "";
49
- const globalLearningsPathLiteral = shellLiteral(globalLearningsPath);
50
- const globalLearningsEnabledLiteral = globalLearningsEnabled ? "true" : "false";
39
+ export function sessionStartScript(_options = {}) {
51
40
  return `#!/usr/bin/env bash
52
41
  # cclaw session-start hook — generated by cclaw sync
53
- # Injects using-cclaw + flow status + run pointer + top learnings + checkpoint/activity summary.
42
+ # Injects using-cclaw + flow status + active artifacts + knowledge snapshot + checkpoint/activity summary.
54
43
  set -euo pipefail
55
44
 
56
45
  ${DETECT_ROOT}
@@ -62,12 +51,7 @@ SUGGESTION_MEMORY_FILE="$ROOT/${RUNTIME_ROOT}/state/suggestion-memory.json"
62
51
  CONTEXT_WARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/state/context-warnings.jsonl"
63
52
  CONTEXT_MODE_FILE="$ROOT/${RUNTIME_ROOT}/state/context-mode.json"
64
53
  CONTEXTS_DIR="$ROOT/${RUNTIME_ROOT}/contexts"
65
- LEARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/learnings.jsonl"
66
- GLOBAL_LEARNINGS_ENABLED="${globalLearningsEnabledLiteral}"
67
- GLOBAL_LEARNINGS_FILE="${globalLearningsPathLiteral}"
68
- if [ "$GLOBAL_LEARNINGS_ENABLED" = "true" ] && [ -z "$GLOBAL_LEARNINGS_FILE" ]; then
69
- GLOBAL_LEARNINGS_FILE="$HOME/.cclaw-global-learnings.jsonl"
70
- fi
54
+ KNOWLEDGE_FILE="$ROOT/${RUNTIME_ROOT}/knowledge.md"
71
55
  META_SKILL="$ROOT/${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"
72
56
 
73
57
  # --- Read flow state ---
@@ -325,82 +309,14 @@ if [ -f "$META_SKILL" ]; then
325
309
  META_CONTENT=$(cat "$META_SKILL" 2>/dev/null || echo "")
326
310
  fi
327
311
 
328
- # --- Read top learnings with decay ---
329
- LEARNINGS_SUMMARY=""
330
- LEARNING_LINES=""
331
- for source_file in "$LEARNINGS_FILE" "$GLOBAL_LEARNINGS_FILE"; do
332
- [ -n "$source_file" ] || continue
333
- if [ "$source_file" = "$GLOBAL_LEARNINGS_FILE" ] && [ "$GLOBAL_LEARNINGS_ENABLED" != "true" ]; then
334
- continue
335
- fi
336
- [ -f "$source_file" ] || continue
337
- [ -s "$source_file" ] || continue
338
- CHUNK=$(tail -n 30 "$source_file" 2>/dev/null || echo "")
339
- [ -n "$CHUNK" ] || continue
340
- if [ -n "$LEARNING_LINES" ]; then
341
- LEARNING_LINES="$LEARNING_LINES
342
- $CHUNK"
343
- else
344
- LEARNING_LINES="$CHUNK"
345
- fi
346
- done
347
-
348
- if [ -n "$LEARNING_LINES" ]; then
349
- if command -v jq >/dev/null 2>&1; then
350
- NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
351
- LEARNINGS_SUMMARY=$(printf '%s\n' "$LEARNING_LINES" | jq -r -s --arg now "$NOW_EPOCH" '
352
- [.[] | select(.key != null)]
353
- | group_by([.key, .type])
354
- | map(sort_by(.ts) | last)
355
- | map(
356
- . as $e |
357
- (if $e.source == "user-stated" then 0
358
- else (try ((($now|tonumber) - ($e.ts // "" | fromdateiso8601)) / 2592000 | floor) catch 0)
359
- end) as $months |
360
- ($e.confidence - $months) as $eff |
361
- . + {effective_confidence: (if $eff < 0 then 0 else $eff end)}
362
- )
363
- | sort_by(-.effective_confidence)
364
- | .[0:3]
365
- | map("- " + .key + " (conf " + (.effective_confidence|tostring) + "): " + .insight)
366
- | join("\\n")
367
- ' 2>/dev/null || echo "")
368
- else
369
- LEARNINGS_SUMMARY=$(printf '%s\n' "$LEARNING_LINES" | tail -n 3 2>/dev/null || echo "")
370
- fi
371
- fi
372
-
373
- STAGE_LEARNINGS=""
374
- STAGE_LEARNING_LINES=""
375
- for source_file in "$LEARNINGS_FILE" "$GLOBAL_LEARNINGS_FILE"; do
376
- [ -n "$source_file" ] || continue
377
- if [ "$source_file" = "$GLOBAL_LEARNINGS_FILE" ] && [ "$GLOBAL_LEARNINGS_ENABLED" != "true" ]; then
378
- continue
379
- fi
380
- [ -f "$source_file" ] || continue
381
- [ -s "$source_file" ] || continue
382
- CHUNK=$(tail -n 50 "$source_file" 2>/dev/null || echo "")
383
- [ -n "$CHUNK" ] || continue
384
- if [ -n "$STAGE_LEARNING_LINES" ]; then
385
- STAGE_LEARNING_LINES="$STAGE_LEARNING_LINES
386
- $CHUNK"
387
- else
388
- STAGE_LEARNING_LINES="$CHUNK"
389
- fi
390
- done
391
-
392
- if [ "$STAGE" != "none" ] && [ -n "$STAGE_LEARNING_LINES" ] && command -v jq >/dev/null 2>&1; then
393
- STAGE_LEARNINGS=$(printf '%s\n' "$STAGE_LEARNING_LINES" | jq -r -s --arg stage "$STAGE" '
394
- [.[] | select(.key != null and (.key | contains($stage)))]
395
- | sort_by(-.confidence)
396
- | .[0:2]
397
- | map("- [stage] " + .key + ": " + .insight)
398
- | join("\\n")
399
- ' 2>/dev/null || echo "")
312
+ # --- Load knowledge snapshot (append-only markdown) ---
313
+ KNOWLEDGE_SUMMARY=""
314
+ if [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
315
+ KNOWLEDGE_SUMMARY=$(tail -n 30 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
400
316
  fi
401
317
 
402
318
  # --- Build context message ---
403
- CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/8 completed, run=$ACTIVE_RUN). Active run artifacts: ${RUNTIME_ROOT}/runs/$ACTIVE_RUN/artifacts/"
319
+ CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/8 completed, run=$ACTIVE_RUN). Active artifacts: ${RUNTIME_ROOT}/artifacts/"
404
320
  if [ -n "$CONTEXT_MODE_NOTE" ]; then
405
321
  CTX="$CTX
406
322
  $CONTEXT_MODE_NOTE"
@@ -429,15 +345,10 @@ if [ -n "$STAGE_SUGGESTION" ]; then
429
345
  $STAGE_SUGGESTION
430
346
  To disable suggestions persistently set ${RUNTIME_ROOT}/state/suggestion-memory.json -> enabled=false."
431
347
  fi
432
- if [ -n "$LEARNINGS_SUMMARY" ]; then
433
- CTX="$CTX
434
- Top learnings:
435
- $LEARNINGS_SUMMARY"
436
- fi
437
- if [ -n "$STAGE_LEARNINGS" ]; then
348
+ if [ -n "$KNOWLEDGE_SUMMARY" ]; then
438
349
  CTX="$CTX
439
- Stage learnings ($STAGE):
440
- $STAGE_LEARNINGS"
350
+ Knowledge snapshot (latest entries):
351
+ $KNOWLEDGE_SUMMARY"
441
352
  fi
442
353
  if [ -n "$META_CONTENT" ]; then
443
354
  CTX="$CTX
@@ -678,98 +589,11 @@ if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
678
589
  CHECKPOINT_NOTE="Checkpoint update failed. Review ${RUNTIME_ROOT}/state/checkpoint.json manually."
679
590
  fi
680
591
 
681
- RUN_SYNC_NOTE="Run metadata sync skipped."
682
- if [ -n "$ACTIVE_RUN" ] && [ "$ACTIVE_RUN" != "none" ] && [ "$ACTIVE_RUN" != "run-pending" ]; then
683
- RUN_DIR="$ROOT/${RUNTIME_ROOT}/runs/$ACTIVE_RUN"
684
- RUN_META_FILE="$RUN_DIR/run.json"
685
- RUN_HANDOFF_FILE="$RUN_DIR/handoff.md"
686
- if [ -f "$RUN_META_FILE" ] && [ -f "$STATE_FILE" ] && command -v python3 >/dev/null 2>&1; then
687
- if python3 - "$STATE_FILE" "$RUN_META_FILE" "$RUN_HANDOFF_FILE" "$ACTIVE_RUN" "$TS" <<'PY'
688
- import json
689
- import sys
690
- from pathlib import Path
691
-
692
- state_path, run_meta_path, handoff_path, active_run, timestamp = sys.argv[1:6]
693
-
694
- try:
695
- state = json.loads(Path(state_path).read_text(encoding="utf-8"))
696
- except Exception:
697
- raise SystemExit(1)
698
-
699
- try:
700
- meta = json.loads(Path(run_meta_path).read_text(encoding="utf-8"))
701
- except Exception:
702
- raise SystemExit(1)
703
-
704
- if not isinstance(state, dict) or not isinstance(meta, dict):
705
- raise SystemExit(1)
706
-
707
- completed = state.get("completedStages")
708
- if not isinstance(completed, list):
709
- completed = []
710
- completed = [stage for stage in completed if isinstance(stage, str)]
711
-
712
- guard_evidence = state.get("guardEvidence")
713
- if not isinstance(guard_evidence, dict):
714
- guard_evidence = {}
715
- guard_evidence = {k: v for k, v in guard_evidence.items() if isinstance(k, str) and isinstance(v, str)}
716
-
717
- stage_gate_catalog = state.get("stageGateCatalog")
718
- if not isinstance(stage_gate_catalog, dict):
719
- stage_gate_catalog = {}
720
-
721
- snapshot = {
722
- "currentStage": state.get("currentStage") if isinstance(state.get("currentStage"), str) else "brainstorm",
723
- "completedStages": completed,
724
- "guardEvidence": guard_evidence,
725
- "stageGateCatalog": stage_gate_catalog,
726
- }
727
- meta["stateSnapshot"] = snapshot
728
- Path(run_meta_path).write_text(json.dumps(meta, indent=2) + "\\n", encoding="utf-8")
729
-
730
- title = meta.get("title") if isinstance(meta.get("title"), str) and meta.get("title") else active_run
731
- created_at = meta.get("createdAt") if isinstance(meta.get("createdAt"), str) and meta.get("createdAt") else "unknown"
732
- archived_at = meta.get("archivedAt") if isinstance(meta.get("archivedAt"), str) and meta.get("archivedAt") else None
733
- completed_display = ", ".join(completed) if completed else "(none)"
734
-
735
- handoff_lines = [
736
- "# Run Handoff",
737
- "",
738
- f"- ID: {active_run}",
739
- f"- Title: {title}",
740
- f"- Created: {created_at}",
741
- f"- Archived: {archived_at if archived_at else 'active'}",
742
- "",
743
- "## Flow Snapshot",
744
- f"- Active stage: {snapshot['currentStage']}",
745
- f"- Completed stages: {completed_display}",
746
- f"- Active run ID in flow-state: {state.get('activeRunId') if isinstance(state.get('activeRunId'), str) else active_run}",
747
- "",
748
- "## Paths",
749
- f"- Active artifacts: ${RUNTIME_ROOT}/artifacts/",
750
- f"- Run artifacts snapshot: ${RUNTIME_ROOT}/runs/{active_run}/artifacts/",
751
- f"- Flow state: ${RUNTIME_ROOT}/state/flow-state.json",
752
- "",
753
- "## Resume",
754
- "1. Open ${RUNTIME_ROOT}/state/flow-state.json and verify current stage.",
755
- "2. Review ${RUNTIME_ROOT}/artifacts/ for the latest working artifacts.",
756
- "3. Continue using /cc or /cc-next from the current stage.",
757
- "",
758
- f"- Last updated: {timestamp}",
759
- ]
760
- Path(handoff_path).write_text("\\n".join(handoff_lines) + "\\n", encoding="utf-8")
761
- PY
762
- then
763
- RUN_SYNC_NOTE="Run metadata synchronized for $ACTIVE_RUN."
764
- else
765
- RUN_SYNC_NOTE="Run metadata sync failed for $ACTIVE_RUN."
766
- fi
767
- fi
768
- fi
592
+ RUN_SYNC_NOTE="Run metadata sync removed; active artifacts stay in ${RUNTIME_ROOT}/artifacts until cclaw archive."
769
593
 
770
594
  # --- Escape for JSON ---
771
595
  ${ESCAPE_FN}
772
- MSG=$(escape_json "Cclaw: session ending (stage=$STAGE, run=$ACTIVE_RUN). $CHECKPOINT_NOTE $RUN_SYNC_NOTE Before stopping: (1) confirm flow-state reflects reality, (2) ensure artifact changes match active run intent, (3) log reusable learnings, (4) commit or revert pending changes.")
596
+ MSG=$(escape_json "Cclaw: session ending (stage=$STAGE, run=$ACTIVE_RUN). $CHECKPOINT_NOTE $RUN_SYNC_NOTE Before stopping: (1) confirm flow-state reflects reality, (2) ensure artifact changes match current feature intent, (3) if you discovered a non-obvious rule/pattern, append it to ${RUNTIME_ROOT}/knowledge.md, (4) commit or revert pending changes.")
773
597
 
774
598
  # --- Output harness-specific JSON ---
775
599
  case "$HARNESS" in
@@ -793,8 +617,7 @@ esac
793
617
  `;
794
618
  }
795
619
  // ---------------------------------------------------------------------------
796
- // hooks.json generators NOW use observe.ts versions with PreToolUse/PostToolUse
797
- // These are kept as fallbacks for when observation is disabled.
620
+ // hooks.json generators are defined in observe.ts (shared across harnesses).
798
621
  // ---------------------------------------------------------------------------
799
622
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
800
623
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
@@ -802,151 +625,70 @@ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
802
625
  // ---------------------------------------------------------------------------
803
626
  // OpenCode plugin — JS module
804
627
  // ---------------------------------------------------------------------------
805
- export function opencodePluginJs(options = {}) {
806
- const globalLearningsEnabledLiteral = options.globalLearningsEnabled === true ? "true" : "false";
807
- const globalLearningsPathLiteral = JSON.stringify(options.globalLearningsPath?.trim() ?? "");
628
+ export function opencodePluginJs(_options = {}) {
808
629
  return `// cclaw OpenCode plugin — generated by cclaw sync
809
- import {
810
- appendFileSync,
811
- closeSync,
812
- existsSync,
813
- fstatSync,
814
- mkdirSync,
815
- openSync,
816
- readFileSync,
817
- readSync
818
- } from "node:fs";
819
- import { homedir } from "node:os";
820
- import { isAbsolute, join } from "node:path";
630
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
631
+ import { join } from "node:path";
821
632
 
822
633
  export default function cclawPlugin(ctx) {
823
634
  const root = ctx.directory || process.cwd();
824
635
  const runtimeDir = join(root, "${RUNTIME_ROOT}");
825
- const stateDir = join(root, "${RUNTIME_ROOT}/state");
826
- const observationsPath = join(root, "${RUNTIME_ROOT}/observations.jsonl");
827
- const learningsPath = join(root, "${RUNTIME_ROOT}/learnings.jsonl");
828
- const checkpointPath = join(root, "${RUNTIME_ROOT}/state/checkpoint.json");
829
- const activityPath = join(root, "${RUNTIME_ROOT}/state/stage-activity.jsonl");
830
- const suggestionMemoryPath = join(root, "${RUNTIME_ROOT}/state/suggestion-memory.json");
831
- const contextWarningsPath = join(root, "${RUNTIME_ROOT}/state/context-warnings.jsonl");
832
- const contextModePath = join(root, "${RUNTIME_ROOT}/state/context-mode.json");
833
- const contextsDir = join(root, "${RUNTIME_ROOT}/contexts");
834
- const sessionDigestPath = join(root, "${RUNTIME_ROOT}/state/session-digest.md");
835
- const observeDisabledPath = join(root, "${RUNTIME_ROOT}/.observe-disabled");
836
- const globalLearningsEnabled = ${globalLearningsEnabledLiteral};
837
- const globalLearningsPathRaw = ${globalLearningsPathLiteral};
636
+ const stateDir = join(runtimeDir, "state");
637
+ const flowStatePath = join(stateDir, "flow-state.json");
638
+ const checkpointPath = join(stateDir, "checkpoint.json");
639
+ const activityPath = join(stateDir, "stage-activity.jsonl");
640
+ const contextWarningsPath = join(stateDir, "context-warnings.jsonl");
641
+ const contextModePath = join(stateDir, "context-mode.json");
642
+ const contextsDir = join(runtimeDir, "contexts");
643
+ const sessionDigestPath = join(stateDir, "session-digest.md");
644
+ const knowledgePath = join(runtimeDir, "knowledge.md");
645
+ const metaSkillPath = join(runtimeDir, "skills/${META_SKILL_NAME}/SKILL.md");
838
646
 
839
- function readFlowState() {
647
+ function ensureRuntimeDirs() {
840
648
  try {
841
- const raw = readFileSync(join(root, "${RUNTIME_ROOT}/state/flow-state.json"), "utf8");
842
- const state = JSON.parse(raw);
843
- return {
844
- stage: state.currentStage || "none",
845
- completed: (state.completedStages || []).length,
846
- activeRunId: state.activeRunId || "none",
847
- };
649
+ mkdirSync(runtimeDir, { recursive: true });
848
650
  } catch {
849
- return { stage: "none", completed: 0, activeRunId: "none" };
651
+ // ignore
850
652
  }
851
- }
852
-
853
- function readMetaSkill() {
854
653
  try {
855
- return readFileSync(join(root, "${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"), "utf8");
654
+ mkdirSync(stateDir, { recursive: true });
856
655
  } catch {
857
- return "";
656
+ // ignore
858
657
  }
859
658
  }
860
659
 
861
- function readTailText(filePath, maxBytes = 65536) {
862
- let fd;
660
+ function readFlowState() {
863
661
  try {
864
- fd = openSync(filePath, "r");
865
- const size = fstatSync(fd).size;
866
- if (!Number.isFinite(size) || size <= 0) return "";
867
- const bytesToRead = Math.min(size, maxBytes);
868
- const buffer = Buffer.allocUnsafe(bytesToRead);
869
- readSync(fd, buffer, 0, bytesToRead, size - bytesToRead);
870
- return buffer.toString("utf8");
662
+ const raw = readFileSync(flowStatePath, "utf8");
663
+ const state = JSON.parse(raw);
664
+ return {
665
+ stage: typeof state.currentStage === "string" ? state.currentStage : "none",
666
+ completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
667
+ activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
668
+ };
871
669
  } catch {
872
- return "";
873
- } finally {
874
- if (fd !== undefined) {
875
- try {
876
- closeSync(fd);
877
- } catch {
878
- // ignore
879
- }
880
- }
881
- }
882
- }
883
-
884
- function readTailLines(filePath, maxLines, maxBytes = 65536) {
885
- const raw = readTailText(filePath, maxBytes).trim();
886
- if (!raw) return [];
887
- return raw.split(/\\r?\\n/).slice(-maxLines);
888
- }
889
-
890
- function resolveGlobalLearningsPath(rawPath) {
891
- if (!rawPath || typeof rawPath !== "string" || rawPath.trim().length === 0) {
892
- return join(homedir(), ".cclaw-global-learnings.jsonl");
893
- }
894
- const trimmed = rawPath.trim();
895
- if (trimmed.startsWith("~/")) {
896
- return join(homedir(), trimmed.slice(2));
897
- }
898
- if (isAbsolute(trimmed)) {
899
- return trimmed;
900
- }
901
- return join(root, trimmed);
902
- }
903
-
904
- function learningFiles() {
905
- const files = [learningsPath];
906
- if (globalLearningsEnabled) {
907
- const globalPath = resolveGlobalLearningsPath(globalLearningsPathRaw);
908
- if (globalPath !== learningsPath) {
909
- files.push(globalPath);
910
- }
911
- }
912
- return files;
913
- }
914
-
915
- function readLearningLines(maxLines, maxBytes) {
916
- const combined = [];
917
- for (const filePath of learningFiles()) {
918
- combined.push(...readTailLines(filePath, maxLines, maxBytes));
670
+ return { stage: "none", completed: 0, activeRunId: "none" };
919
671
  }
920
- return combined;
921
672
  }
922
673
 
923
- function readTopLearnings() {
674
+ function readFileText(filePath) {
924
675
  try {
925
- const lines = readLearningLines(30, 65536);
926
- if (lines.length === 0) return [];
927
- const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
928
- const deduped = new Map();
929
- for (const e of entries) {
930
- const key = e.key + ":" + e.type;
931
- if (!deduped.has(key) || e.ts > deduped.get(key).ts) deduped.set(key, e);
932
- }
933
- const now = Date.now();
934
- return [...deduped.values()]
935
- .map(e => {
936
- const months = e.source === "user-stated" ? 0 : Math.floor((now - new Date(e.ts).getTime()) / (30 * 86400000));
937
- const eff = Math.max(0, e.confidence - months);
938
- return { ...e, effective_confidence: eff };
939
- })
940
- .sort((a, b) => b.effective_confidence - a.effective_confidence)
941
- .slice(0, 3);
676
+ return readFileSync(filePath, "utf8");
942
677
  } catch {
943
- return [];
678
+ return "";
944
679
  }
945
680
  }
946
681
 
682
+ function readTailLines(filePath, maxLines) {
683
+ const text = readFileText(filePath).trim();
684
+ if (!text) return [];
685
+ return text.split(/\\r?\\n/).slice(-maxLines);
686
+ }
687
+
947
688
  function readCheckpointSummary() {
948
689
  try {
949
- const raw = readFileSync(checkpointPath, "utf8");
690
+ const raw = readFileText(checkpointPath);
691
+ if (!raw) return "";
950
692
  const cp = JSON.parse(raw);
951
693
  return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
952
694
  } catch {
@@ -957,13 +699,12 @@ export default function cclawPlugin(ctx) {
957
699
  function readContextMode() {
958
700
  let mode = "default";
959
701
  try {
960
- const raw = readFileSync(contextModePath, "utf8");
961
- const parsed = JSON.parse(raw);
702
+ const parsed = JSON.parse(readFileText(contextModePath));
962
703
  if (parsed && typeof parsed.activeMode === "string" && parsed.activeMode.trim().length > 0) {
963
704
  mode = parsed.activeMode.trim();
964
705
  }
965
706
  } catch {
966
- // use default mode
707
+ // keep default
967
708
  }
968
709
  const guidePath = join(contextsDir, mode + ".md");
969
710
  const guide = existsSync(guidePath) ? "${RUNTIME_ROOT}/contexts/" + mode + ".md" : "";
@@ -972,40 +713,32 @@ export default function cclawPlugin(ctx) {
972
713
 
973
714
  function readRecentActivity() {
974
715
  try {
975
- const lines = readTailLines(activityPath, 5, 32768);
716
+ const lines = readTailLines(activityPath, 5);
976
717
  if (lines.length === 0) return [];
977
- return lines.map((line) => {
978
- try {
979
- return JSON.parse(line);
980
- } catch {
981
- return null;
982
- }
983
- }).filter(Boolean).map((entry) => \`- \${entry.ts || "unknown"} [\${entry.phase || "unknown"}] \${entry.tool || "unknown"} (stage=\${entry.stage || "unknown"}, run=\${entry.runId || "none"})\`);
718
+ return lines
719
+ .map((line) => {
720
+ try {
721
+ return JSON.parse(line);
722
+ } catch {
723
+ return null;
724
+ }
725
+ })
726
+ .filter(Boolean)
727
+ .map((entry) => \`- \${entry.ts || "unknown"} [\${entry.phase || "unknown"}] \${entry.tool || "unknown"} (stage=\${entry.stage || "unknown"}, run=\${entry.runId || "none"})\`);
984
728
  } catch {
985
729
  return [];
986
730
  }
987
731
  }
988
732
 
989
- function readSessionDigest() {
990
- try {
991
- const digest = readFileSync(sessionDigestPath, "utf8").trim();
992
- return digest;
993
- } catch {
994
- return "";
995
- }
996
- }
997
-
998
733
  function readLatestContextWarning() {
999
734
  try {
1000
- const line = readTailLines(contextWarningsPath, 1, 8192)[0];
735
+ const line = readTailLines(contextWarningsPath, 1)[0];
1001
736
  if (!line) return "";
1002
737
  try {
1003
738
  const parsed = JSON.parse(line);
1004
- if (parsed && typeof parsed.note === "string") {
1005
- return parsed.note;
1006
- }
739
+ if (parsed && typeof parsed.note === "string") return parsed.note;
1007
740
  } catch {
1008
- // non-json line fallback
741
+ // non-json fallback
1009
742
  }
1010
743
  return line;
1011
744
  } catch {
@@ -1013,95 +746,42 @@ export default function cclawPlugin(ctx) {
1013
746
  }
1014
747
  }
1015
748
 
1016
- function stageSuggestionFor(stage) {
1017
- const map = {
1018
- brainstorm: "Suggestion: list 2-3 alternatives and ask a single focused clarifying question before direction lock.",
1019
- scope: "Suggestion: lock explicit in-scope/out-of-scope boundaries and choose one scope mode.",
1020
- design: "Suggestion: map failure modes per new codepath and confirm architecture boundaries before moving forward.",
1021
- spec: "Suggestion: ensure every acceptance criterion is measurable and mapped to a concrete test.",
1022
- plan: "Suggestion: group tasks into dependency waves and keep WAIT_FOR_CONFIRM pending until approval.",
1023
- tdd: "Suggestion: execute RED → GREEN → REFACTOR for each selected slice and capture evidence per cycle.",
1024
- review: "Suggestion: run Layer 1 before Layer 2 and reconcile findings into 07-review-army.json.",
1025
- ship: "Suggestion: verify preflight + rollback plan before selecting exactly one finalization mode."
1026
- };
1027
- return map[stage] || "";
1028
- }
1029
-
1030
- function readStageSuggestion(stage) {
1031
- let memory = { enabled: true, mutedStages: [] };
1032
- try {
1033
- const parsed = JSON.parse(readFileSync(suggestionMemoryPath, "utf8"));
1034
- if (parsed && typeof parsed === "object") {
1035
- memory = {
1036
- enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : true,
1037
- mutedStages: Array.isArray(parsed.mutedStages)
1038
- ? parsed.mutedStages.filter((item) => typeof item === "string")
1039
- : []
1040
- };
1041
- }
1042
- } catch {
1043
- // use defaults
1044
- }
1045
- if (!memory.enabled || memory.mutedStages.includes(stage)) {
1046
- return "";
1047
- }
1048
- return stageSuggestionFor(stage);
1049
- }
1050
-
1051
- function readStageLearnings(stage) {
1052
- try {
1053
- const lines = readLearningLines(60, 131072);
1054
- const entries = lines
1055
- .map((line) => {
1056
- try {
1057
- return JSON.parse(line);
1058
- } catch {
1059
- return null;
1060
- }
1061
- })
1062
- .filter(Boolean);
1063
- if (entries.length === 0) return [];
1064
- return entries
1065
- .filter((entry) => typeof entry.key === "string" && entry.key.toLowerCase().includes(stage.toLowerCase()))
1066
- .sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0))
1067
- .slice(0, 2)
1068
- .map((entry) => "- [stage] " + entry.key + ": " + entry.insight);
1069
- } catch {
1070
- return [];
1071
- }
749
+ function readKnowledgeSnapshot() {
750
+ return readTailLines(knowledgePath, 30);
1072
751
  }
1073
752
 
1074
753
  function buildBootstrap() {
1075
- const { stage, completed, activeRunId } = readFlowState();
1076
- const meta = readMetaSkill();
1077
- const learnings = readTopLearnings();
1078
- const checkpoint = readCheckpointSummary();
1079
- const activity = readRecentActivity();
1080
- const digest = readSessionDigest();
1081
- const warning = readLatestContextWarning();
754
+ const flow = readFlowState();
755
+ const parts = [
756
+ \`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: ${RUNTIME_ROOT}/artifacts/\`
757
+ ];
1082
758
  const contextMode = readContextMode();
1083
- const stageSuggestion = readStageSuggestion(stage);
1084
- const stageLearnings = readStageLearnings(stage);
1085
- const parts = [\`cclaw loaded. Flow: stage=\${stage} (\${completed}/8 completed, run=\${activeRunId}). Active run artifacts: ${RUNTIME_ROOT}/runs/\${activeRunId}/artifacts/\`];
1086
759
  parts.push(
1087
760
  contextMode.guide
1088
761
  ? \`Context mode: \${contextMode.mode} (guide: \${contextMode.guide})\`
1089
762
  : \`Context mode: \${contextMode.mode}\`
1090
763
  );
764
+
765
+ const checkpoint = readCheckpointSummary();
1091
766
  if (checkpoint) parts.push(checkpoint);
767
+
768
+ const digest = readFileText(sessionDigestPath).trim();
1092
769
  if (digest) parts.push("Last session:", digest);
770
+
771
+ const activity = readRecentActivity();
1093
772
  if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
773
+
774
+ const warning = readLatestContextWarning();
1094
775
  if (warning) parts.push("Latest context warning:", warning);
1095
- if (stageSuggestion) {
1096
- parts.push(stageSuggestion, "To disable suggestions persistently set .cclaw/state/suggestion-memory.json -> enabled=false.");
1097
- }
1098
- if (learnings.length > 0) {
1099
- parts.push("Top learnings:");
1100
- for (const l of learnings) parts.push(\`- \${l.key} (conf \${l.effective_confidence}): \${l.insight}\`);
1101
- }
1102
- if (stageLearnings.length > 0) {
1103
- parts.push(\`Stage learnings (\${stage}):\`, ...stageLearnings);
1104
- }
776
+
777
+ const knowledge = readKnowledgeSnapshot();
778
+ if (knowledge.length > 0) parts.push("Knowledge snapshot (latest entries):", ...knowledge);
779
+
780
+ parts.push(
781
+ "If you discover a non-obvious rule or pattern, append it to .cclaw/knowledge.md using type: rule, pattern, or lesson."
782
+ );
783
+
784
+ const meta = readFileText(metaSkillPath).trim();
1105
785
  if (meta) parts.push("", meta);
1106
786
  return parts.join("\\n");
1107
787
  }
@@ -1110,23 +790,6 @@ export default function cclawPlugin(ctx) {
1110
790
  console.log(buildBootstrap());
1111
791
  }
1112
792
 
1113
- function observationEnabled() {
1114
- return !existsSync(observeDisabledPath);
1115
- }
1116
-
1117
- function ensureRuntimeDirs() {
1118
- try {
1119
- mkdirSync(runtimeDir, { recursive: true });
1120
- } catch {
1121
- // ignore
1122
- }
1123
- try {
1124
- mkdirSync(stateDir, { recursive: true });
1125
- } catch {
1126
- // ignore
1127
- }
1128
- }
1129
-
1130
793
  async function runHookScript(scriptFileName, payload = {}) {
1131
794
  const { spawnSync } = await import("node:child_process");
1132
795
  const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
@@ -1143,53 +806,9 @@ export default function cclawPlugin(ctx) {
1143
806
  }
1144
807
  }
1145
808
 
1146
- function toText(value) {
1147
- if (typeof value === "string") return value;
1148
- try {
1149
- return JSON.stringify(value);
1150
- } catch {
1151
- return value == null ? "" : String(value);
1152
- }
1153
- }
1154
-
1155
- function truncateText(value, maxLen = 2000) {
1156
- const text = toText(value);
1157
- return text.length > maxLen ? text.slice(0, maxLen) : text;
1158
- }
1159
-
1160
- function extractToolName(payload) {
1161
- const candidates = [
1162
- payload?.tool,
1163
- payload?.toolName,
1164
- payload?.tool_name,
1165
- payload?.name,
1166
- payload?.id,
1167
- payload?.command,
1168
- payload?.tool?.name,
1169
- payload?.tool?.id,
1170
- payload?.input?.tool,
1171
- payload?.input?.toolName,
1172
- payload?.input?.tool_name,
1173
- payload?.input?.name,
1174
- payload?.input?.id,
1175
- payload?.input?.command,
1176
- payload?.input?.tool?.name,
1177
- payload?.input?.tool?.id
1178
- ];
1179
- for (const value of candidates) {
1180
- if (typeof value === "string" && value.trim()) return value.trim();
1181
- }
1182
- return "unknown";
1183
- }
1184
-
1185
809
  function normalizeToolPayload(input, output) {
1186
- if (typeof output === "undefined") {
1187
- return input ?? {};
1188
- }
1189
- return {
1190
- input: input ?? {},
1191
- output: output ?? {}
1192
- };
810
+ if (typeof output === "undefined") return input ?? {};
811
+ return { input: input ?? {}, output: output ?? {} };
1193
812
  }
1194
813
 
1195
814
  function resolveEventType(payload) {
@@ -1212,77 +831,41 @@ export default function cclawPlugin(ctx) {
1212
831
  return payload;
1213
832
  }
1214
833
 
1215
- function appendJsonLine(filePath, value) {
1216
- try {
1217
- appendFileSync(filePath, JSON.stringify(value) + "\\n", "utf8");
1218
- } catch {
1219
- // ignore
1220
- }
1221
- }
1222
-
1223
- function recordToolEvent(phase, payload) {
1224
- if (!observationEnabled()) return;
1225
- const flow = readFlowState();
1226
- const ts = new Date().toISOString();
1227
- const tool = extractToolName(payload);
1228
- const event = phase === "pre" ? "tool_start" : "tool_complete";
1229
- ensureRuntimeDirs();
1230
- appendJsonLine(observationsPath, {
1231
- ts,
1232
- event,
1233
- tool,
1234
- phase,
1235
- stage: flow.stage,
1236
- runId: flow.activeRunId,
1237
- data: truncateText(payload)
1238
- });
1239
- appendJsonLine(activityPath, {
1240
- ts,
1241
- event,
1242
- tool,
1243
- phase,
1244
- stage: flow.stage,
1245
- runId: flow.activeRunId
1246
- });
1247
- }
834
+ ensureRuntimeDirs();
1248
835
 
1249
836
  return {
1250
837
  event: async (payload) => {
1251
838
  const eventType = resolveEventType(payload);
1252
839
  const eventData = resolveEventData(payload);
1253
- if (eventType === "session.created" || eventType === "session.resumed" || eventType === "session.compacted" || eventType === "session.cleared") {
840
+ if (
841
+ eventType === "session.created" ||
842
+ eventType === "session.resumed" ||
843
+ eventType === "session.compacted" ||
844
+ eventType === "session.cleared"
845
+ ) {
1254
846
  emitBootstrap();
1255
847
  }
1256
- if (eventType === "session.updated") {
1257
- // no-op: tracked via activity log
1258
- }
1259
848
  if (eventType === "session.idle") {
1260
- if (!observationEnabled()) return;
1261
- await runHookScript("summarize-observations.sh");
1262
849
  await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
1263
850
  }
1264
851
  if (eventType === "tool.execute.before") {
1265
852
  const toolPayload = normalizeToolPayload(eventData, undefined);
1266
853
  await runHookScript("prompt-guard.sh", toolPayload);
1267
854
  await runHookScript("workflow-guard.sh", toolPayload);
1268
- recordToolEvent("pre", toolPayload);
1269
855
  }
1270
856
  if (eventType === "tool.execute.after") {
1271
857
  const toolPayload = normalizeToolPayload(eventData, undefined);
1272
858
  await runHookScript("context-monitor.sh", toolPayload);
1273
- recordToolEvent("post", toolPayload);
1274
859
  }
1275
860
  },
1276
861
  "tool.execute.before": async (input, output) => {
1277
862
  const payload = normalizeToolPayload(input, output);
1278
863
  await runHookScript("prompt-guard.sh", payload);
1279
864
  await runHookScript("workflow-guard.sh", payload);
1280
- recordToolEvent("pre", payload);
1281
865
  },
1282
866
  "tool.execute.after": async (input, output) => {
1283
867
  const payload = normalizeToolPayload(input, output);
1284
868
  await runHookScript("context-monitor.sh", payload);
1285
- recordToolEvent("post", payload);
1286
869
  },
1287
870
  "experimental.chat.system.transform": (payload) => {
1288
871
  const bootstrap = buildBootstrap();
@@ -1294,7 +877,7 @@ export default function cclawPlugin(ctx) {
1294
877
  return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
1295
878
  }
1296
879
  return payload;
1297
- },
880
+ }
1298
881
  };
1299
882
  }
1300
883
  `;
@@ -1307,7 +890,7 @@ export function hooksAgentsMdBlock() {
1307
890
 
1308
891
  Cclaw generates real hook integrations across harnesses:
1309
892
  - **Claude/Cursor/Codex:** lifecycle rehydration + PreToolUse/PostToolUse + Stop
1310
- - **OpenCode:** session lifecycle + system transform rehydration + bootstrap parity (digest/warnings/suggestions/stage learnings)
893
+ - **OpenCode:** session lifecycle + system transform rehydration + bootstrap parity (digest/warnings/knowledge snapshot)
1311
894
 
1312
895
  | Harness | Hook file | Events |
1313
896
  |---------|-----------|--------|
@@ -1319,7 +902,5 @@ Cclaw generates real hook integrations across harnesses:
1319
902
  Hook state files:
1320
903
  - \`${RUNTIME_ROOT}/state/stage-activity.jsonl\`
1321
904
  - \`${RUNTIME_ROOT}/state/checkpoint.json\`
1322
-
1323
- Disable observation: \`touch ${RUNTIME_ROOT}/.observe-disabled\`
1324
905
  `;
1325
906
  }