cclaw-cli 0.5.3 → 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,9 +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
 
592
+ RUN_SYNC_NOTE="Run metadata sync removed; active artifacts stay in ${RUNTIME_ROOT}/artifacts until cclaw archive."
593
+
681
594
  # --- Escape for JSON ---
682
595
  ${ESCAPE_FN}
683
- MSG=$(escape_json "Cclaw: session ending (stage=$STAGE, run=$ACTIVE_RUN). $CHECKPOINT_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.")
684
597
 
685
598
  # --- Output harness-specific JSON ---
686
599
  case "$HARNESS" in
@@ -704,8 +617,7 @@ esac
704
617
  `;
705
618
  }
706
619
  // ---------------------------------------------------------------------------
707
- // hooks.json generators NOW use observe.ts versions with PreToolUse/PostToolUse
708
- // These are kept as fallbacks for when observation is disabled.
620
+ // hooks.json generators are defined in observe.ts (shared across harnesses).
709
621
  // ---------------------------------------------------------------------------
710
622
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
711
623
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
@@ -713,151 +625,70 @@ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
713
625
  // ---------------------------------------------------------------------------
714
626
  // OpenCode plugin — JS module
715
627
  // ---------------------------------------------------------------------------
716
- export function opencodePluginJs(options = {}) {
717
- const globalLearningsEnabledLiteral = options.globalLearningsEnabled === true ? "true" : "false";
718
- const globalLearningsPathLiteral = JSON.stringify(options.globalLearningsPath?.trim() ?? "");
628
+ export function opencodePluginJs(_options = {}) {
719
629
  return `// cclaw OpenCode plugin — generated by cclaw sync
720
- import {
721
- appendFileSync,
722
- closeSync,
723
- existsSync,
724
- fstatSync,
725
- mkdirSync,
726
- openSync,
727
- readFileSync,
728
- readSync
729
- } from "node:fs";
730
- import { homedir } from "node:os";
731
- import { isAbsolute, join } from "node:path";
630
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
631
+ import { join } from "node:path";
732
632
 
733
633
  export default function cclawPlugin(ctx) {
734
634
  const root = ctx.directory || process.cwd();
735
635
  const runtimeDir = join(root, "${RUNTIME_ROOT}");
736
- const stateDir = join(root, "${RUNTIME_ROOT}/state");
737
- const observationsPath = join(root, "${RUNTIME_ROOT}/observations.jsonl");
738
- const learningsPath = join(root, "${RUNTIME_ROOT}/learnings.jsonl");
739
- const checkpointPath = join(root, "${RUNTIME_ROOT}/state/checkpoint.json");
740
- const activityPath = join(root, "${RUNTIME_ROOT}/state/stage-activity.jsonl");
741
- const suggestionMemoryPath = join(root, "${RUNTIME_ROOT}/state/suggestion-memory.json");
742
- const contextWarningsPath = join(root, "${RUNTIME_ROOT}/state/context-warnings.jsonl");
743
- const contextModePath = join(root, "${RUNTIME_ROOT}/state/context-mode.json");
744
- const contextsDir = join(root, "${RUNTIME_ROOT}/contexts");
745
- const sessionDigestPath = join(root, "${RUNTIME_ROOT}/state/session-digest.md");
746
- const observeDisabledPath = join(root, "${RUNTIME_ROOT}/.observe-disabled");
747
- const globalLearningsEnabled = ${globalLearningsEnabledLiteral};
748
- 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");
749
646
 
750
- function readFlowState() {
647
+ function ensureRuntimeDirs() {
751
648
  try {
752
- const raw = readFileSync(join(root, "${RUNTIME_ROOT}/state/flow-state.json"), "utf8");
753
- const state = JSON.parse(raw);
754
- return {
755
- stage: state.currentStage || "none",
756
- completed: (state.completedStages || []).length,
757
- activeRunId: state.activeRunId || "none",
758
- };
649
+ mkdirSync(runtimeDir, { recursive: true });
759
650
  } catch {
760
- return { stage: "none", completed: 0, activeRunId: "none" };
651
+ // ignore
761
652
  }
762
- }
763
-
764
- function readMetaSkill() {
765
653
  try {
766
- return readFileSync(join(root, "${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"), "utf8");
654
+ mkdirSync(stateDir, { recursive: true });
767
655
  } catch {
768
- return "";
656
+ // ignore
769
657
  }
770
658
  }
771
659
 
772
- function readTailText(filePath, maxBytes = 65536) {
773
- let fd;
660
+ function readFlowState() {
774
661
  try {
775
- fd = openSync(filePath, "r");
776
- const size = fstatSync(fd).size;
777
- if (!Number.isFinite(size) || size <= 0) return "";
778
- const bytesToRead = Math.min(size, maxBytes);
779
- const buffer = Buffer.allocUnsafe(bytesToRead);
780
- readSync(fd, buffer, 0, bytesToRead, size - bytesToRead);
781
- 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
+ };
782
669
  } catch {
783
- return "";
784
- } finally {
785
- if (fd !== undefined) {
786
- try {
787
- closeSync(fd);
788
- } catch {
789
- // ignore
790
- }
791
- }
792
- }
793
- }
794
-
795
- function readTailLines(filePath, maxLines, maxBytes = 65536) {
796
- const raw = readTailText(filePath, maxBytes).trim();
797
- if (!raw) return [];
798
- return raw.split(/\\r?\\n/).slice(-maxLines);
799
- }
800
-
801
- function resolveGlobalLearningsPath(rawPath) {
802
- if (!rawPath || typeof rawPath !== "string" || rawPath.trim().length === 0) {
803
- return join(homedir(), ".cclaw-global-learnings.jsonl");
804
- }
805
- const trimmed = rawPath.trim();
806
- if (trimmed.startsWith("~/")) {
807
- return join(homedir(), trimmed.slice(2));
808
- }
809
- if (isAbsolute(trimmed)) {
810
- return trimmed;
811
- }
812
- return join(root, trimmed);
813
- }
814
-
815
- function learningFiles() {
816
- const files = [learningsPath];
817
- if (globalLearningsEnabled) {
818
- const globalPath = resolveGlobalLearningsPath(globalLearningsPathRaw);
819
- if (globalPath !== learningsPath) {
820
- files.push(globalPath);
821
- }
822
- }
823
- return files;
824
- }
825
-
826
- function readLearningLines(maxLines, maxBytes) {
827
- const combined = [];
828
- for (const filePath of learningFiles()) {
829
- combined.push(...readTailLines(filePath, maxLines, maxBytes));
670
+ return { stage: "none", completed: 0, activeRunId: "none" };
830
671
  }
831
- return combined;
832
672
  }
833
673
 
834
- function readTopLearnings() {
674
+ function readFileText(filePath) {
835
675
  try {
836
- const lines = readLearningLines(30, 65536);
837
- if (lines.length === 0) return [];
838
- const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
839
- const deduped = new Map();
840
- for (const e of entries) {
841
- const key = e.key + ":" + e.type;
842
- if (!deduped.has(key) || e.ts > deduped.get(key).ts) deduped.set(key, e);
843
- }
844
- const now = Date.now();
845
- return [...deduped.values()]
846
- .map(e => {
847
- const months = e.source === "user-stated" ? 0 : Math.floor((now - new Date(e.ts).getTime()) / (30 * 86400000));
848
- const eff = Math.max(0, e.confidence - months);
849
- return { ...e, effective_confidence: eff };
850
- })
851
- .sort((a, b) => b.effective_confidence - a.effective_confidence)
852
- .slice(0, 3);
676
+ return readFileSync(filePath, "utf8");
853
677
  } catch {
854
- return [];
678
+ return "";
855
679
  }
856
680
  }
857
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
+
858
688
  function readCheckpointSummary() {
859
689
  try {
860
- const raw = readFileSync(checkpointPath, "utf8");
690
+ const raw = readFileText(checkpointPath);
691
+ if (!raw) return "";
861
692
  const cp = JSON.parse(raw);
862
693
  return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
863
694
  } catch {
@@ -868,13 +699,12 @@ export default function cclawPlugin(ctx) {
868
699
  function readContextMode() {
869
700
  let mode = "default";
870
701
  try {
871
- const raw = readFileSync(contextModePath, "utf8");
872
- const parsed = JSON.parse(raw);
702
+ const parsed = JSON.parse(readFileText(contextModePath));
873
703
  if (parsed && typeof parsed.activeMode === "string" && parsed.activeMode.trim().length > 0) {
874
704
  mode = parsed.activeMode.trim();
875
705
  }
876
706
  } catch {
877
- // use default mode
707
+ // keep default
878
708
  }
879
709
  const guidePath = join(contextsDir, mode + ".md");
880
710
  const guide = existsSync(guidePath) ? "${RUNTIME_ROOT}/contexts/" + mode + ".md" : "";
@@ -883,40 +713,32 @@ export default function cclawPlugin(ctx) {
883
713
 
884
714
  function readRecentActivity() {
885
715
  try {
886
- const lines = readTailLines(activityPath, 5, 32768);
716
+ const lines = readTailLines(activityPath, 5);
887
717
  if (lines.length === 0) return [];
888
- return lines.map((line) => {
889
- try {
890
- return JSON.parse(line);
891
- } catch {
892
- return null;
893
- }
894
- }).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"})\`);
895
728
  } catch {
896
729
  return [];
897
730
  }
898
731
  }
899
732
 
900
- function readSessionDigest() {
901
- try {
902
- const digest = readFileSync(sessionDigestPath, "utf8").trim();
903
- return digest;
904
- } catch {
905
- return "";
906
- }
907
- }
908
-
909
733
  function readLatestContextWarning() {
910
734
  try {
911
- const line = readTailLines(contextWarningsPath, 1, 8192)[0];
735
+ const line = readTailLines(contextWarningsPath, 1)[0];
912
736
  if (!line) return "";
913
737
  try {
914
738
  const parsed = JSON.parse(line);
915
- if (parsed && typeof parsed.note === "string") {
916
- return parsed.note;
917
- }
739
+ if (parsed && typeof parsed.note === "string") return parsed.note;
918
740
  } catch {
919
- // non-json line fallback
741
+ // non-json fallback
920
742
  }
921
743
  return line;
922
744
  } catch {
@@ -924,95 +746,42 @@ export default function cclawPlugin(ctx) {
924
746
  }
925
747
  }
926
748
 
927
- function stageSuggestionFor(stage) {
928
- const map = {
929
- brainstorm: "Suggestion: list 2-3 alternatives and ask a single focused clarifying question before direction lock.",
930
- scope: "Suggestion: lock explicit in-scope/out-of-scope boundaries and choose one scope mode.",
931
- design: "Suggestion: map failure modes per new codepath and confirm architecture boundaries before moving forward.",
932
- spec: "Suggestion: ensure every acceptance criterion is measurable and mapped to a concrete test.",
933
- plan: "Suggestion: group tasks into dependency waves and keep WAIT_FOR_CONFIRM pending until approval.",
934
- tdd: "Suggestion: execute RED → GREEN → REFACTOR for each selected slice and capture evidence per cycle.",
935
- review: "Suggestion: run Layer 1 before Layer 2 and reconcile findings into 07-review-army.json.",
936
- ship: "Suggestion: verify preflight + rollback plan before selecting exactly one finalization mode."
937
- };
938
- return map[stage] || "";
939
- }
940
-
941
- function readStageSuggestion(stage) {
942
- let memory = { enabled: true, mutedStages: [] };
943
- try {
944
- const parsed = JSON.parse(readFileSync(suggestionMemoryPath, "utf8"));
945
- if (parsed && typeof parsed === "object") {
946
- memory = {
947
- enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : true,
948
- mutedStages: Array.isArray(parsed.mutedStages)
949
- ? parsed.mutedStages.filter((item) => typeof item === "string")
950
- : []
951
- };
952
- }
953
- } catch {
954
- // use defaults
955
- }
956
- if (!memory.enabled || memory.mutedStages.includes(stage)) {
957
- return "";
958
- }
959
- return stageSuggestionFor(stage);
960
- }
961
-
962
- function readStageLearnings(stage) {
963
- try {
964
- const lines = readLearningLines(60, 131072);
965
- const entries = lines
966
- .map((line) => {
967
- try {
968
- return JSON.parse(line);
969
- } catch {
970
- return null;
971
- }
972
- })
973
- .filter(Boolean);
974
- if (entries.length === 0) return [];
975
- return entries
976
- .filter((entry) => typeof entry.key === "string" && entry.key.toLowerCase().includes(stage.toLowerCase()))
977
- .sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0))
978
- .slice(0, 2)
979
- .map((entry) => "- [stage] " + entry.key + ": " + entry.insight);
980
- } catch {
981
- return [];
982
- }
749
+ function readKnowledgeSnapshot() {
750
+ return readTailLines(knowledgePath, 30);
983
751
  }
984
752
 
985
753
  function buildBootstrap() {
986
- const { stage, completed, activeRunId } = readFlowState();
987
- const meta = readMetaSkill();
988
- const learnings = readTopLearnings();
989
- const checkpoint = readCheckpointSummary();
990
- const activity = readRecentActivity();
991
- const digest = readSessionDigest();
992
- 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
+ ];
993
758
  const contextMode = readContextMode();
994
- const stageSuggestion = readStageSuggestion(stage);
995
- const stageLearnings = readStageLearnings(stage);
996
- const parts = [\`cclaw loaded. Flow: stage=\${stage} (\${completed}/8 completed, run=\${activeRunId}). Active run artifacts: ${RUNTIME_ROOT}/runs/\${activeRunId}/artifacts/\`];
997
759
  parts.push(
998
760
  contextMode.guide
999
761
  ? \`Context mode: \${contextMode.mode} (guide: \${contextMode.guide})\`
1000
762
  : \`Context mode: \${contextMode.mode}\`
1001
763
  );
764
+
765
+ const checkpoint = readCheckpointSummary();
1002
766
  if (checkpoint) parts.push(checkpoint);
767
+
768
+ const digest = readFileText(sessionDigestPath).trim();
1003
769
  if (digest) parts.push("Last session:", digest);
770
+
771
+ const activity = readRecentActivity();
1004
772
  if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
773
+
774
+ const warning = readLatestContextWarning();
1005
775
  if (warning) parts.push("Latest context warning:", warning);
1006
- if (stageSuggestion) {
1007
- parts.push(stageSuggestion, "To disable suggestions persistently set .cclaw/state/suggestion-memory.json -> enabled=false.");
1008
- }
1009
- if (learnings.length > 0) {
1010
- parts.push("Top learnings:");
1011
- for (const l of learnings) parts.push(\`- \${l.key} (conf \${l.effective_confidence}): \${l.insight}\`);
1012
- }
1013
- if (stageLearnings.length > 0) {
1014
- parts.push(\`Stage learnings (\${stage}):\`, ...stageLearnings);
1015
- }
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();
1016
785
  if (meta) parts.push("", meta);
1017
786
  return parts.join("\\n");
1018
787
  }
@@ -1021,23 +790,6 @@ export default function cclawPlugin(ctx) {
1021
790
  console.log(buildBootstrap());
1022
791
  }
1023
792
 
1024
- function observationEnabled() {
1025
- return !existsSync(observeDisabledPath);
1026
- }
1027
-
1028
- function ensureRuntimeDirs() {
1029
- try {
1030
- mkdirSync(runtimeDir, { recursive: true });
1031
- } catch {
1032
- // ignore
1033
- }
1034
- try {
1035
- mkdirSync(stateDir, { recursive: true });
1036
- } catch {
1037
- // ignore
1038
- }
1039
- }
1040
-
1041
793
  async function runHookScript(scriptFileName, payload = {}) {
1042
794
  const { spawnSync } = await import("node:child_process");
1043
795
  const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
@@ -1054,53 +806,9 @@ export default function cclawPlugin(ctx) {
1054
806
  }
1055
807
  }
1056
808
 
1057
- function toText(value) {
1058
- if (typeof value === "string") return value;
1059
- try {
1060
- return JSON.stringify(value);
1061
- } catch {
1062
- return value == null ? "" : String(value);
1063
- }
1064
- }
1065
-
1066
- function truncateText(value, maxLen = 2000) {
1067
- const text = toText(value);
1068
- return text.length > maxLen ? text.slice(0, maxLen) : text;
1069
- }
1070
-
1071
- function extractToolName(payload) {
1072
- const candidates = [
1073
- payload?.tool,
1074
- payload?.toolName,
1075
- payload?.tool_name,
1076
- payload?.name,
1077
- payload?.id,
1078
- payload?.command,
1079
- payload?.tool?.name,
1080
- payload?.tool?.id,
1081
- payload?.input?.tool,
1082
- payload?.input?.toolName,
1083
- payload?.input?.tool_name,
1084
- payload?.input?.name,
1085
- payload?.input?.id,
1086
- payload?.input?.command,
1087
- payload?.input?.tool?.name,
1088
- payload?.input?.tool?.id
1089
- ];
1090
- for (const value of candidates) {
1091
- if (typeof value === "string" && value.trim()) return value.trim();
1092
- }
1093
- return "unknown";
1094
- }
1095
-
1096
809
  function normalizeToolPayload(input, output) {
1097
- if (typeof output === "undefined") {
1098
- return input ?? {};
1099
- }
1100
- return {
1101
- input: input ?? {},
1102
- output: output ?? {}
1103
- };
810
+ if (typeof output === "undefined") return input ?? {};
811
+ return { input: input ?? {}, output: output ?? {} };
1104
812
  }
1105
813
 
1106
814
  function resolveEventType(payload) {
@@ -1123,77 +831,41 @@ export default function cclawPlugin(ctx) {
1123
831
  return payload;
1124
832
  }
1125
833
 
1126
- function appendJsonLine(filePath, value) {
1127
- try {
1128
- appendFileSync(filePath, JSON.stringify(value) + "\\n", "utf8");
1129
- } catch {
1130
- // ignore
1131
- }
1132
- }
1133
-
1134
- function recordToolEvent(phase, payload) {
1135
- if (!observationEnabled()) return;
1136
- const flow = readFlowState();
1137
- const ts = new Date().toISOString();
1138
- const tool = extractToolName(payload);
1139
- const event = phase === "pre" ? "tool_start" : "tool_complete";
1140
- ensureRuntimeDirs();
1141
- appendJsonLine(observationsPath, {
1142
- ts,
1143
- event,
1144
- tool,
1145
- phase,
1146
- stage: flow.stage,
1147
- runId: flow.activeRunId,
1148
- data: truncateText(payload)
1149
- });
1150
- appendJsonLine(activityPath, {
1151
- ts,
1152
- event,
1153
- tool,
1154
- phase,
1155
- stage: flow.stage,
1156
- runId: flow.activeRunId
1157
- });
1158
- }
834
+ ensureRuntimeDirs();
1159
835
 
1160
836
  return {
1161
837
  event: async (payload) => {
1162
838
  const eventType = resolveEventType(payload);
1163
839
  const eventData = resolveEventData(payload);
1164
- 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
+ ) {
1165
846
  emitBootstrap();
1166
847
  }
1167
- if (eventType === "session.updated") {
1168
- // no-op: tracked via activity log
1169
- }
1170
848
  if (eventType === "session.idle") {
1171
- if (!observationEnabled()) return;
1172
- await runHookScript("summarize-observations.sh");
1173
849
  await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
1174
850
  }
1175
851
  if (eventType === "tool.execute.before") {
1176
852
  const toolPayload = normalizeToolPayload(eventData, undefined);
1177
853
  await runHookScript("prompt-guard.sh", toolPayload);
1178
854
  await runHookScript("workflow-guard.sh", toolPayload);
1179
- recordToolEvent("pre", toolPayload);
1180
855
  }
1181
856
  if (eventType === "tool.execute.after") {
1182
857
  const toolPayload = normalizeToolPayload(eventData, undefined);
1183
858
  await runHookScript("context-monitor.sh", toolPayload);
1184
- recordToolEvent("post", toolPayload);
1185
859
  }
1186
860
  },
1187
861
  "tool.execute.before": async (input, output) => {
1188
862
  const payload = normalizeToolPayload(input, output);
1189
863
  await runHookScript("prompt-guard.sh", payload);
1190
864
  await runHookScript("workflow-guard.sh", payload);
1191
- recordToolEvent("pre", payload);
1192
865
  },
1193
866
  "tool.execute.after": async (input, output) => {
1194
867
  const payload = normalizeToolPayload(input, output);
1195
868
  await runHookScript("context-monitor.sh", payload);
1196
- recordToolEvent("post", payload);
1197
869
  },
1198
870
  "experimental.chat.system.transform": (payload) => {
1199
871
  const bootstrap = buildBootstrap();
@@ -1205,7 +877,7 @@ export default function cclawPlugin(ctx) {
1205
877
  return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
1206
878
  }
1207
879
  return payload;
1208
- },
880
+ }
1209
881
  };
1210
882
  }
1211
883
  `;
@@ -1218,7 +890,7 @@ export function hooksAgentsMdBlock() {
1218
890
 
1219
891
  Cclaw generates real hook integrations across harnesses:
1220
892
  - **Claude/Cursor/Codex:** lifecycle rehydration + PreToolUse/PostToolUse + Stop
1221
- - **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)
1222
894
 
1223
895
  | Harness | Hook file | Events |
1224
896
  |---------|-----------|--------|
@@ -1230,7 +902,5 @@ Cclaw generates real hook integrations across harnesses:
1230
902
  Hook state files:
1231
903
  - \`${RUNTIME_ROOT}/state/stage-activity.jsonl\`
1232
904
  - \`${RUNTIME_ROOT}/state/checkpoint.json\`
1233
-
1234
- Disable observation: \`touch ${RUNTIME_ROOT}/.observe-disabled\`
1235
905
  `;
1236
906
  }