@neikyun/ciel 6.14.0 → 6.14.2

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.
Files changed (101) hide show
  1. package/assets/.claude/hooks/check-dispatch-gate.sh +14 -41
  2. package/assets/.claude/hooks/memory-engine.py +66 -5
  3. package/assets/.claude/hooks/pre-tool-write.sh +17 -52
  4. package/assets/.claude/hooks/session-start.sh +15 -128
  5. package/assets/.claude/hooks/stop.sh +10 -85
  6. package/assets/.claude/hooks/user-prompt-submit.sh +17 -110
  7. package/assets/.claude/rules/api-design.md +23 -0
  8. package/assets/.claude/rules/backend.md +22 -0
  9. package/assets/.claude/rules/cicd-pipeline.md +23 -0
  10. package/assets/.claude/rules/containers.md +23 -0
  11. package/assets/.claude/rules/database-design.md +22 -0
  12. package/assets/.claude/rules/environments.md +27 -0
  13. package/assets/.claude/rules/frontend.md +25 -0
  14. package/assets/.claude/rules/github.md +22 -0
  15. package/assets/.claude/rules/logging.md +23 -0
  16. package/assets/.claude/rules/monitoring.md +25 -0
  17. package/assets/.claude/rules/research.md +20 -0
  18. package/assets/.claude/settings.json +2 -58
  19. package/assets/.claude/skills/agile/SKILL.md +42 -0
  20. package/assets/.claude/skills/alerting/SKILL.md +55 -0
  21. package/assets/.claude/skills/api-design/SKILL.md +46 -0
  22. package/assets/.claude/skills/appsec/SKILL.md +43 -0
  23. package/assets/.claude/skills/architecture/SKILL.md +74 -0
  24. package/assets/.claude/skills/backend/SKILL.md +41 -0
  25. package/assets/.claude/skills/backup-recovery/SKILL.md +42 -0
  26. package/assets/.claude/skills/caching/SKILL.md +44 -0
  27. package/assets/.claude/skills/cdn/SKILL.md +42 -0
  28. package/assets/.claude/skills/chaos/SKILL.md +41 -0
  29. package/assets/.claude/skills/cicd-pipeline/SKILL.md +56 -0
  30. package/assets/.claude/skills/ciel/SKILL.md +14 -0
  31. package/assets/.claude/skills/ciel/reference.md +171 -0
  32. package/assets/.claude/skills/cloud/SKILL.md +42 -0
  33. package/assets/.claude/skills/code-quality/SKILL.md +42 -0
  34. package/assets/.claude/skills/code-review/SKILL.md +41 -0
  35. package/assets/.claude/skills/communication/SKILL.md +42 -0
  36. package/assets/.claude/skills/containers/SKILL.md +42 -0
  37. package/assets/.claude/skills/cqrs/SKILL.md +41 -0
  38. package/assets/.claude/skills/crypto/SKILL.md +46 -0
  39. package/assets/.claude/skills/data-engineering/SKILL.md +42 -0
  40. package/assets/.claude/skills/database-design/SKILL.md +46 -0
  41. package/assets/.claude/skills/ddd/SKILL.md +45 -0
  42. package/assets/.claude/skills/deployment-strategies/SKILL.md +51 -0
  43. package/assets/.claude/skills/desktop/SKILL.md +42 -0
  44. package/assets/.claude/skills/devsecops/SKILL.md +43 -0
  45. package/assets/.claude/skills/environments/SKILL.md +66 -0
  46. package/assets/.claude/skills/event-driven/SKILL.md +46 -0
  47. package/assets/.claude/skills/frontend/SKILL.md +41 -0
  48. package/assets/.claude/skills/functional/SKILL.md +42 -0
  49. package/assets/.claude/skills/github/SKILL.md +61 -0
  50. package/assets/.claude/skills/high-availability/SKILL.md +42 -0
  51. package/assets/.claude/skills/iac/SKILL.md +46 -0
  52. package/assets/.claude/skills/logging/SKILL.md +46 -0
  53. package/assets/.claude/skills/ml-engineering/SKILL.md +42 -0
  54. package/assets/.claude/skills/mobile/SKILL.md +42 -0
  55. package/assets/.claude/skills/monitoring/SKILL.md +54 -0
  56. package/assets/.claude/skills/networking/SKILL.md +42 -0
  57. package/assets/.claude/skills/nosql/SKILL.md +41 -0
  58. package/assets/.claude/skills/oop-solid/SKILL.md +42 -0
  59. package/assets/.claude/skills/performance/SKILL.md +41 -0
  60. package/assets/.claude/skills/reactive/SKILL.md +42 -0
  61. package/assets/.claude/skills/release-management/SKILL.md +51 -0
  62. package/assets/.claude/skills/research/SKILL.md +69 -0
  63. package/assets/.claude/skills/resilience/SKILL.md +41 -0
  64. package/assets/.claude/skills/serverless/SKILL.md +42 -0
  65. package/assets/.claude/skills/servers/SKILL.md +41 -0
  66. package/assets/.claude/skills/sql/SKILL.md +45 -0
  67. package/assets/.claude/skills/supply-chain/SKILL.md +41 -0
  68. package/assets/.claude/skills/system-design/SKILL.md +91 -0
  69. package/assets/.claude/skills/tech-leadership/SKILL.md +46 -0
  70. package/assets/.claude/skills/testing/SKILL.md +41 -0
  71. package/assets/.claude/skills/tracing/SKILL.md +36 -0
  72. package/assets/CLAUDE.md +31 -122
  73. package/assets/commands/{ciel-memory-bootstrap.md → ciel-memory-init.md} +3 -3
  74. package/assets/commands/ciel-memory.md +210 -0
  75. package/assets/platforms/opencode/.opencode/commands/{ciel-memory-bootstrap.md → ciel-memory-init.md} +3 -3
  76. package/assets/skills/ciel/SKILL.md +8 -97
  77. package/bin/ciel.js +1 -1
  78. package/dist/cli/check.d.ts.map +1 -1
  79. package/dist/cli/check.js +5 -11
  80. package/dist/cli/check.js.map +1 -1
  81. package/dist/cli/claude.d.ts.map +1 -1
  82. package/dist/cli/claude.js +42 -4
  83. package/dist/cli/claude.js.map +1 -1
  84. package/dist/cli/doctor.d.ts +16 -0
  85. package/dist/cli/doctor.d.ts.map +1 -0
  86. package/dist/cli/doctor.js +168 -0
  87. package/dist/cli/doctor.js.map +1 -0
  88. package/dist/cli/index.js +76 -0
  89. package/dist/cli/index.js.map +1 -1
  90. package/dist/cli/init.d.ts.map +1 -1
  91. package/dist/cli/init.js +23 -4
  92. package/dist/cli/init.js.map +1 -1
  93. package/dist/cli/memory.d.ts +18 -0
  94. package/dist/cli/memory.d.ts.map +1 -0
  95. package/dist/cli/memory.js +304 -0
  96. package/dist/cli/memory.js.map +1 -0
  97. package/dist/cli/opencode.js +1 -1
  98. package/dist/cli/opencode.js.map +1 -1
  99. package/package.json +2 -2
  100. /package/assets/{rules → .claude/rules}/security.md +0 -0
  101. /package/assets/{rules → .claude/rules}/testing.md +0 -0
@@ -1,12 +1,8 @@
1
1
  #!/bin/bash
2
- # Ciel — PreToolUse hook for Read|Bash
3
- # Trigger: PreToolUse on Read|Bash
4
- # Purpose: BLOCK inline source-code research until ciel-researcher is dispatched.
5
- # Forces the model to dispatch researcher + explorer in parallel before
6
- # reading source files or running research commands (grep, cat, curl, rg, etc.).
7
- # Always allows: config files, project docs, CI files, state files.
8
- # Escape hatch: [CIEL_GATE_BYPASS] anywhere in the tool input bypasses the gate.
9
- # Dispatch tracker: /tmp/ciel_dispatched.* (created by SubagentStart hooks)
2
+ # Ciel v9 — PreToolUse hook for Read|Bash
3
+ # Gate DISPATCH: blocks source-code Read/Bash until researcher+explorer dispatched.
4
+ # Always allows: config files, docs, CI files, .ciel/*, .claude/*
5
+ # Escape hatch: [CIEL_GATE_BYPASS] in tool input
10
6
 
11
7
  INPUT=$(cat 2>/dev/null || echo "{}")
12
8
 
@@ -16,26 +12,21 @@ TASK_DEPTH="Standard"
16
12
  if [ -n "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/.ciel/last-depth" ]; then
17
13
  TASK_DEPTH=$(cat "$PROJECT_DIR/.ciel/last-depth" 2>/dev/null || echo "Standard")
18
14
  fi
19
-
20
- # Trivial + Spike tasks always pass — no dispatch required
21
15
  [ "$TASK_DEPTH" = "Trivial" ] && exit 0
22
16
  [ "$TASK_DEPTH" = "Spike" ] && exit 0
23
17
 
24
18
  # ── Bypass check ──
25
19
  if echo "$INPUT" | grep -q '\[CIEL_GATE_BYPASS\]'; then
26
- echo "[CIEL] Dispatch gate bypassed via [CIEL_GATE_BYPASS]" >&2
20
+ echo "[CIEL] Dispatch gate bypassed" >&2
27
21
  exit 0
28
22
  fi
29
23
 
30
24
  # ── Dispatch check ──
31
- # If any Ciel agent has been dispatched this session, allow inline research.
32
- # /tmp/ciel_dispatched.* files are created by SubagentStart hooks for
33
- # ciel-researcher, ciel-explorer, and ciel-critic.
34
25
  if ls /tmp/ciel_dispatched.* >/dev/null 2>&1; then
35
26
  exit 0
36
27
  fi
37
28
 
38
- # ── Extract target ──
29
+ # ── Extract tool fields ──
39
30
  TOOL_NAME=$(echo "$INPUT" | python3 -c "
40
31
  import sys, json
41
32
  try:
@@ -65,11 +56,9 @@ except:
65
56
  print('')
66
57
  " 2>/dev/null || echo "")
67
58
 
68
- # ── Read gate ──
69
- # Always-allow list: config files the model MUST be able to read for DOCS step.
70
- # These are needed BEFORE any dispatch — CLAUDE.md, AGENTS.md, .ciel/*, .claude/*,
71
- # SKILL.md, package.json, README, Makefile, VERSION, docker-compose, CI configs.
59
+ # ── Read gate: allow config/docs, block source code ──
72
60
  if [ "$TOOL_NAME" = "Read" ] && [ -n "$FILE_PATH" ]; then
61
+ # Always-allow: .ciel, .claude, .github, project config, docs
73
62
  if echo "$FILE_PATH" | grep -qE '(^|/)\.(ciel|claude|github|opencode)(/|$|\.)'; then
74
63
  exit 0
75
64
  fi
@@ -80,41 +69,25 @@ if [ "$TOOL_NAME" = "Read" ] && [ -n "$FILE_PATH" ]; then
80
69
  exit 0
81
70
  fi
82
71
  if echo "$FILE_PATH" | grep -qE '\.(yml|yaml|toml|cfg|ini|conf|json|md|txt|css|html|xml|svg)$'; then
83
- # Config/docs files — allow (but NOT source code like .ts in JSON disguise)
84
- # The depth classification + pipeline docs phase needs broad config access
85
72
  exit 0
86
73
  fi
87
74
 
88
- # Source code file BLOCK until researcher dispatched
89
- echo "[CIEL DISPATCH GATE] BLOCKED: Read $(basename "$FILE_PATH") before researcher dispatch" >&2
90
- echo "" >&2
91
- echo " Depth: $TASK_DEPTH — no Ciel agent dispatched yet." >&2
92
- echo " Ciel pipeline requires dispatching ciel-researcher + ciel-explorer" >&2
93
- echo " in parallel BEFORE reading source code. This ensures official docs," >&2
94
- echo " anti-patterns, and codebase patterns are checked first." >&2
95
- echo "" >&2
96
- echo " To proceed without dispatch: add [CIEL_GATE_BYPASS] to the tool input." >&2
97
- echo " But prefer dispatching — it prevents the blind-pattern-copy anti-pattern." >&2
75
+ echo "[CIEL DISPATCH GATE] Blocked: Read $(basename "$FILE_PATH") before researcher dispatch" >&2
76
+ echo " Dispatch ciel-researcher + ciel-explorer in parallel first." >&2
77
+ echo " Or add [CIEL_GATE_BYPASS] to bypass." >&2
98
78
  exit 2
99
79
  fi
100
80
 
101
- # ── Bash gate ──
102
- # Allow infrastructure commands (ls, find, git status, mkdir, cd, npm, node -v, etc.)
103
- # Block research commands that read/search source files without dispatch.
81
+ # ── Bash gate: allow infra, block source-code research ──
104
82
  if [ "$TOOL_NAME" = "Bash" ] && [ -n "$BASH_CMD" ]; then
105
- # Always-allow patterns: infrastructure, package management, git meta
106
83
  if echo "$BASH_CMD" | grep -qE '^(ls |find |git (status|diff|log|branch|remote|config|stash|add |commit|push|pull|fetch|checkout|switch|restore)|mkdir |cd |npm |npx |node -[vp]|pnpm |yarn |cargo |pip |poetry |python3 -c|which |type |command -v|echo |cat .*(\.json|\.md|\.yml|\.yaml|\.toml|\.lock|VERSION|Makefile|README|\.gitignore)|gh (pr|issue|release|run|repo|workflow))'; then
107
84
  exit 0
108
85
  fi
109
86
 
110
- # Research commands that scan/read source code — BLOCK
111
87
  if echo "$BASH_CMD" | grep -qE '(grep |rg |ag |cat .*\.(kt|ts|tsx|js|jsx|py|go|rs|rb|java|php|scala|swift|cs|cpp|c|h|vue|svelte)|tail |head |curl |wget )'; then
112
- echo "[CIEL DISPATCH GATE] BLOCKED: Research Bash before researcher dispatch" >&2
113
- echo "" >&2
114
- echo " Depth: $TASK_DEPTH — no Ciel agent dispatched yet." >&2
88
+ echo "[CIEL DISPATCH GATE] Blocked: research Bash before dispatch" >&2
115
89
  echo " Command: $(echo "$BASH_CMD" | cut -c1-80)" >&2
116
- echo " Dispatch ciel-researcher + ciel-explorer first." >&2
117
- echo " Or add [CIEL_GATE_BYPASS] if this is infrastructure." >&2
90
+ echo " Dispatch ciel-researcher + ciel-explorer first, or use [CIEL_GATE_BYPASS]." >&2
118
91
  exit 2
119
92
  fi
120
93
  fi
@@ -26,6 +26,18 @@ import secrets
26
26
  from datetime import datetime, timezone
27
27
  from pathlib import Path
28
28
 
29
+ # ─── Structured Logging ──────────────────────────────────────────────────────
30
+
31
+ def _log(level, message, **context):
32
+ """Write structured JSON log line to stderr. Never touches stdout."""
33
+ entry = {
34
+ "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
35
+ "level": level,
36
+ "msg": message,
37
+ **context,
38
+ }
39
+ print(json.dumps(entry, ensure_ascii=False), file=sys.stderr)
40
+
29
41
  # ─── Constants ──────────────────────────────────────────────────────────────
30
42
 
31
43
  TOKEN_CAPS = {
@@ -494,6 +506,7 @@ def cmd_query(args):
494
506
  for mid, m, _ in selected:
495
507
  m['trigger_count'] = (m.get('trigger_count') or 0) + 1
496
508
  m['last_triggered'] = iso_now
509
+ m['last_edited'] = iso_now
497
510
 
498
511
  for _, _, line in selected:
499
512
  output_lines.append(line)
@@ -534,7 +547,7 @@ def cmd_init(args):
534
547
  # Fields whose values must remain string regardless of how they look.
535
548
  # Prevents int-coercion of numeric-looking ids (e.g. "12345") which would
536
549
  # break the index keying and JSON round-trip.
537
- _STRING_FIELDS = frozenset({'id', 'title', 'last_triggered', 'captured_at', 'file', 'source', 'captured_from'})
550
+ _STRING_FIELDS = frozenset({'id', 'title', 'last_triggered', 'captured_at', 'last_edited', 'file', 'source', 'captured_from'})
538
551
 
539
552
 
540
553
  def parse_yaml_frontmatter(text: str) -> dict:
@@ -567,7 +580,13 @@ def parse_yaml_frontmatter(text: str) -> dict:
567
580
  out[key] = []
568
581
  elif val.startswith('[') and val.endswith(']'):
569
582
  inner = val[1:-1].strip()
570
- items = [x.strip().strip('"\'') for x in inner.split(',') if x.strip()]
583
+ items = []
584
+ seen = set()
585
+ for x in inner.split(','):
586
+ x = x.strip().strip('"\'')
587
+ if x and x not in seen:
588
+ seen.add(x)
589
+ items.append(x)
571
590
  out[key] = items
572
591
  elif val.lower() == 'null' or val == '~':
573
592
  out[key] = None
@@ -591,7 +610,7 @@ def cmd_rebuild_index(args):
591
610
  cwd = resolve_cwd(args.cwd)
592
611
  base = cwd / '.ciel' / 'memory'
593
612
  if not base.exists():
594
- print(f"No memory directory at {base}", file=sys.stderr)
613
+ _log("error", "No memory directory", path=str(base))
595
614
  sys.exit(1)
596
615
 
597
616
  # Preserve trigger counts from existing index. cmd_query updates counts
@@ -607,6 +626,29 @@ def cmd_rebuild_index(args):
607
626
  except (json.JSONDecodeError, OSError):
608
627
  pass
609
628
 
629
+ # Index schema contract (version 2)
630
+ # ─────────────────────────────────────────────────────────────────
631
+ # memories: mid → frontmatter 1:1 PK — mid is unique
632
+ # by_path: pattern → [mid, ...] 1:N FK → memories
633
+ # by_symbol: symbol → [mid, ...] 1:N FK → memories
634
+ # by_intent: intent → [mid, ...] 1:N FK → memories
635
+ # by_language: lang → [mid, ...] 1:N FK → memories
636
+ #
637
+ # Guarantees (enforced at rebuild time):
638
+ # G1 — Referential integrity: every mid in any index exists in memories
639
+ # G2 — No self-duplicates: a given (key, mid) pair appears at most once
640
+ # per index list (setdefault+append is safe when source frontmatter
641
+ # has no duplicate entries — which the parser guarantees)
642
+ # G3 — Index values are always lists, never None/scalar
643
+ # G4 — No orphan index keys: empty [] keys are pruned after build
644
+ #
645
+ # Non-guarantees (by design):
646
+ # - Order within index lists is insertion order (filesystem order), not
647
+ # relevance-sorted. Callers (cmd_query) re-rank by trigger_count.
648
+ # - Duplicate mids across different keys in the same index: a memory
649
+ # with symbols [auth, oauth] appears under both keys. This is correct.
650
+ # - Index is a cache: rebuild-index reconstructs it from source .md files.
651
+ # The source of truth is always the .md frontmatter, never the index.
610
652
  idx = {
611
653
  "version": 2,
612
654
  "memories": {},
@@ -638,6 +680,10 @@ def cmd_rebuild_index(args):
638
680
  old_lt = old.get('last_triggered')
639
681
  if old_lt and not fm.get('last_triggered'):
640
682
  fm['last_triggered'] = old_lt
683
+ old_le = old.get('last_edited')
684
+ new_le = fm.get('last_edited')
685
+ if old_le and (not new_le or old_le > new_le):
686
+ fm['last_edited'] = old_le
641
687
  fm['file'] = str(mdfile.relative_to(base))
642
688
  idx['memories'][mid] = fm
643
689
  for path in fm.get('path_patterns') or []:
@@ -650,7 +696,21 @@ def cmd_rebuild_index(args):
650
696
  idx['by_language'].setdefault(lang, []).append(mid)
651
697
  parsed += 1
652
698
  except (OSError, UnicodeDecodeError) as e:
653
- print(f"Warning: skipping {mdfile}: {e}", file=sys.stderr)
699
+ _log("warn", "Skipping unparseable memory file", file=str(mdfile), error=str(e))
700
+
701
+ # Integrity checks (G1, G3)
702
+ mid_set = set(idx['memories'].keys())
703
+ for index_name in ('by_path', 'by_symbol', 'by_intent', 'by_language'):
704
+ idx_map = idx[index_name]
705
+ empty_keys = [k for k, v in idx_map.items() if not v]
706
+ for k in empty_keys:
707
+ del idx_map[k]
708
+ for key, mids in idx_map.items():
709
+ orphans = [m for m in mids if m not in mid_set]
710
+ if orphans:
711
+ _log("warn", "Dangling reference in index — repaired",
712
+ index=index_name, key=key, orphans=orphans)
713
+ idx_map[key] = [m for m in mids if m in mid_set]
654
714
 
655
715
  out = base / 'index.json'
656
716
  atomic_write_json(out, idx)
@@ -705,6 +765,7 @@ def cmd_capture(args):
705
765
  "captured_at": iso_now,
706
766
  "captured_from": args.captured_from or 'runtime',
707
767
  "source": args.source or 'manual capture',
768
+ "last_edited": iso_now,
708
769
  "trigger_count": 0,
709
770
  "last_triggered": None,
710
771
  "stale_after_days": 90,
@@ -759,7 +820,7 @@ def cmd_analyze(args):
759
820
  cwd = resolve_cwd(args.cwd)
760
821
  base = cwd / '.ciel' / 'memory'
761
822
  if not base.exists():
762
- print(f"No memory directory at {base}", file=sys.stderr)
823
+ _log("error", "No memory directory", path=str(base))
763
824
  sys.exit(1)
764
825
 
765
826
  index_file = base / 'index.json'
@@ -1,10 +1,7 @@
1
1
  #!/bin/bash
2
- # Ciel — PreToolUse hook for Write/Edit
3
- # Trigger: PreToolUse on Write|Edit
4
- # Purpose: BLOCK source code writes until dispatch gate passes (v8 — enforcement)
5
- # Test files and non-code files pass through (always allowed).
6
- # Escape hatch: [CIEL_GATE_BYPASS] anywhere in the tool input bypasses the gate.
7
- # Dispatch tracker: /tmp/ciel_dispatched.* (created by SubagentStart hooks)
2
+ # Ciel v9 — PreToolUse hook for Write/Edit
3
+ # Gate DISPATCH: blocks source code writes until researcher+explorer dispatched
4
+ # Escape hatch: [CIEL_GATE_BYPASS] in tool input bypasses the gate
8
5
 
9
6
  INPUT=$(cat 2>/dev/null || echo "{}")
10
7
 
@@ -22,87 +19,55 @@ except:
22
19
 
23
20
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-}"
24
21
 
25
- # === BYPASS CHECK ===
26
- # [CIEL_GATE_BYPASS] anywhere in the input is an intentional override
22
+ # ── Bypass check ──────────────────────────────────────────────────────────
27
23
  if echo "$INPUT" | grep -q '\[CIEL_GATE_BYPASS\]'; then
28
- echo "[CIEL] Gate bypassed via [CIEL_GATE_BYPASS] — allowing write to $(basename "$FILE_PATH")" >&2
24
+ echo "[CIEL] Gate bypassed — allowing write to $(basename "$FILE_PATH")" >&2
29
25
  exit 0
30
26
  fi
31
27
 
32
- # === FILE CLASSIFICATION ===
33
- # Source code files that require dispatch before editing
28
+ # ── File classification ───────────────────────────────────────────────────
34
29
  IS_SOURCE=0
35
30
  if echo "$FILE_PATH" | grep -qE '\.(kt|java|ts|tsx|js|jsx|py|go|rs|rb|php|cs|cpp|c|swift|scala|vue|svelte)$'; then
36
31
  IS_SOURCE=1
37
32
  fi
38
33
 
39
- # Test files always pass (test-first RED)
40
34
  IS_TEST=0
41
35
  if echo "$FILE_PATH" | grep -qE '\.(test|spec)\.|_test\.|_spec\.|/test/|/tests/|/__tests__/'; then
42
36
  IS_TEST=1
43
37
  fi
44
38
 
45
- # === DISPATCH GATE CHECK ===
46
- # /tmp/ciel_dispatched.* files created by SubagentStart hooks when Ciel agents dispatch
39
+ # ── Dispatch gate the core enforcement ──────────────────────────────────
47
40
  DISPATCHED=0
48
41
  if ls /tmp/ciel_dispatched.* >/dev/null 2>&1; then
49
42
  DISPATCHED=1
50
43
  fi
51
44
 
52
- # === DEPTH CHECK ===
53
- # Read depth classification from UserPromptSubmit hook (auto-bypass for Trivial)
54
45
  TASK_DEPTH="Standard"
55
46
  if [ -n "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/.ciel/last-depth" ]; then
56
47
  TASK_DEPTH=$(cat "$PROJECT_DIR/.ciel/last-depth" 2>/dev/null || echo "Standard")
57
48
  fi
58
49
 
59
- # === DISPATCH GATE — BLOCK (v8 enforcement) ===
60
- # Block source code edits when no Ciel agent has been dispatched.
61
- # Auto-bypass for Trivial tasks (depth classified by UserPromptSubmit hook).
62
- # Skip non-code files (docs, config, JSON, YAML, etc.) — safe to edit inline.
63
- # Skip test files — test-first RED means tests must be written before source.
64
50
  if [ "$IS_SOURCE" -eq 1 ] && [ "$IS_TEST" -eq 0 ] && [ "$DISPATCHED" -eq 0 ] && [ "$TASK_DEPTH" != "Trivial" ]; then
65
- echo "[CIEL DISPATCH GATE] BLOCKED: Write to $(basename "$FILE_PATH")" >&2
66
- echo "" >&2
67
- echo " Depth: $TASK_DEPTH no Ciel agent dispatched yet." >&2
68
- echo " Dispatch ciel-researcher + ciel-explorer in parallel (Agent tool)," >&2
69
- echo " with relevant domain skill names in the dispatch prompt." >&2
70
- echo "" >&2
71
- echo " This is actually a Trivial task? Use [CIEL_GATE_BYPASS]." >&2
51
+ echo "[CIEL DISPATCH GATE] Write blocked: $(basename "$FILE_PATH") — no researcher+explorer dispatched yet." >&2
52
+ echo " Dispatch them in parallel via Agent tool, then retry." >&2
53
+ echo " Trivial task? Use [CIEL_GATE_BYPASS] in tool input." >&2
72
54
  exit 2
73
55
  fi
74
56
 
75
- # === FILE TRACK COUNT (RELIRE GATE) ===
57
+ # ── RELIRE gate reminder (soft, after 3+ edits in session) ────────────────
76
58
  COUNT=0
77
59
  if [ -n "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/.ciel/tracked-files.json" ]; then
78
- COUNT=$(CIEL_PATH="$PROJECT_DIR/.ciel/tracked-files.json" python3 -c "
79
- import json, os
80
- try: print(len(json.load(open(os.environ['CIEL_PATH']))))
60
+ COUNT=$(python3 -c "
61
+ import json
62
+ try:
63
+ with open('$PROJECT_DIR/.ciel/tracked-files.json') as f:
64
+ print(len(json.load(f)))
81
65
  except: print(0)
82
66
  " 2>/dev/null || echo "0")
83
67
  fi
84
68
 
85
- # RELIRE gate warning (3+ files → critic required)
86
69
  if [ "${COUNT:-0}" -ge 2 ] 2>/dev/null; then
87
- echo "[CIEL RELIRE GATE] ${COUNT} file(s) edited — dispatch ciel-critic MODE=RELIRE when done." >&2
88
- fi
89
-
90
- # === FAIRE GATE REMINDER ===
91
- # Skip non-code files for faire gate
92
- if [ "$IS_SOURCE" -eq 0 ] && [ "$IS_TEST" -eq 0 ]; then
93
- exit 0
94
- fi
95
-
96
- # Check if file is critical path
97
- CRITICAL=false
98
- if echo "$FILE_PATH" | grep -qiE '(auth|Auth|security|Security|Token|Session|Password|Secret)'; then
99
- CRITICAL=true
100
- fi
101
-
102
- if $CRITICAL; then
103
- echo " [CIEL] Critical path: invoke faire-gatekeeper, stride-analyzer must have run, test-first (RED). After FAIRE: TESTER (run test suite)." >&2
104
- else
105
- echo " [CIEL] Standard path: invoke faire-gatekeeper (alternatives, idiomatic, quality, removal, test-first). After FAIRE: TESTER (run test suite)." >&2
70
+ echo "[CIEL REVIEW GATE] ${COUNT} files edited — dispatch ciel-critic MODE=RELIRE before commit." >&2
106
71
  fi
107
72
 
108
73
  exit 0
@@ -1,15 +1,13 @@
1
1
  #!/bin/bash
2
- # Ciel — SessionStart hook
3
- # Trigger: session begins or resumes
4
- # Purpose: print Ciel banner, load overlay context, set TRACE_ID for eval logging
2
+ # Ciel v9 — SessionStart hook
3
+ # Prints version banner + loads overlay context.
5
4
  # Never blocks (exit 0 always). Stdout is added to Claude's context.
6
5
 
7
6
  INPUT=$(cat 2>/dev/null || echo "{}")
8
7
  CWD=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin).get('cwd', ''))" 2>/dev/null || pwd)
9
- # python3 succeeds with an empty string when stdin JSON lacks a 'cwd' key — fall through to pwd.
10
8
  [ -z "$CWD" ] && CWD="$(pwd)"
11
9
 
12
- # Detect overlay presence
10
+ # Detect overlay
13
11
  OVERLAY=""
14
12
  for candidate in "$CWD/ciel-overlay.md" "$CWD/.claude/ciel-overlay.md"; do
15
13
  if [[ -f "$candidate" ]]; then
@@ -18,159 +16,48 @@ for candidate in "$CWD/ciel-overlay.md" "$CWD/.claude/ciel-overlay.md"; do
18
16
  fi
19
17
  done
20
18
 
21
- # ─── META-pending check ──────────────────────────────────────────────────
22
- # If previous session ended without META reflection after 3+ edits,
23
- # inject a prominent warning BEFORE resetting the tracker.
24
- META_PENDING=""
25
- if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "$CLAUDE_PROJECT_DIR/.ciel/meta-pending" ]; then
26
- META_INFO=$(python3 -c "
27
- import json
28
- try:
29
- with open('$CLAUDE_PROJECT_DIR/.ciel/meta-pending') as f:
30
- d = json.load(f)
31
- print(f\"{d.get('edits', '?')} files edited, pending since {d.get('since', '?')}\")
32
- except: print('unknown')
33
- " 2>/dev/null || echo "unknown")
34
- META_PENDING="
35
-
36
- ═══════════════════════════════════════════════════════════════
37
- [CIEL] META STILL DUE — Previous task ended without META reflection.
38
- $META_INFO
39
- Invoke Skill(meta-critiquer) to complete the 10-item reflection.
40
- Clear .ciel/meta-pending when done.
41
- DO NOT start a new task until META is complete.
42
- ═══════════════════════════════════════════════════════════════
43
- "
44
- fi
45
-
46
- # Reset session-scoped edit tracker so META/RELIRE gates don't bleed across sessions
47
- if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "$CLAUDE_PROJECT_DIR/.ciel/tracked-files.json" ]; then
48
- echo "[]" > "$CLAUDE_PROJECT_DIR/.ciel/tracked-files.json" 2>/dev/null || true
49
- fi
50
-
51
- # Generate TRACE_ID for this session (used by eval logging)
19
+ # Generate TRACE_ID
52
20
  TRACE_ID=$(date -u +%Y%m%dT%H%M%SZ)-$$
53
21
  export CIEL_TRACE_ID="$TRACE_ID"
54
22
 
55
- # Resolve Ciel version at runtime (single source of truth, no hardcoded drift).
56
- # Fallback chain: project sentinel → user sentinel → npm package → marketplace plugin → repo VERSION → unknown.
57
- # Note: $HOME/.ciel/version is "last writer wins" across npm installs from different projects —
58
- # project sentinel ($CWD/.ciel/version) is authoritative when present.
23
+ # Resolve version project sentinel > user sentinel > npm package > VERSION file
59
24
  _resolve_ciel_version() {
60
25
  local v=""
61
26
  for f in \
62
27
  "$CWD/.ciel/version" \
63
28
  "$HOME/.ciel/version" \
64
29
  "$HOME/.claude/plugins/ciel/package.json" \
65
- "$HOME/.claude/plugins/ciel/.claude-plugin/plugin.json" \
66
30
  "$(dirname "$0")/../../VERSION" \
67
31
  "$(dirname "$0")/../VERSION"; do
68
- # Skip entries that resolved against an empty $CWD/$HOME (e.g., "/.ciel/version").
69
32
  case "$f" in /.ciel/*|/.claude/*) continue ;; esac
70
33
  [ -r "$f" ] || continue
71
34
  case "$f" in
72
- *.json)
73
- # Anchor to line-start whitespace so we only match top-level "version",
74
- # not nested keys like "schema_version" or `"version"` deeper in the doc.
75
- v=$(sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$f" 2>/dev/null | head -1)
76
- ;;
77
- *)
78
- v=$(tr -d '[:space:]' <"$f" 2>/dev/null)
79
- ;;
35
+ *.json) v=$(sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$f" 2>/dev/null | head -1) ;;
36
+ *) v=$(tr -d '[:space:]' <"$f" 2>/dev/null) ;;
80
37
  esac
81
38
  [ -n "$v" ] && { echo "$v"; return; }
82
39
  done
83
40
  echo "unknown"
84
41
  }
85
42
  CIEL_VERSION="$(_resolve_ciel_version)"
86
- MSG="CIEL v${CIEL_VERSION} — Skills-first deep-reasoning active. "
87
- if [[ -n "$META_PENDING" ]]; then
88
- MSG+="$META_PENDING"
89
- fi
90
- if [[ -n "$OVERLAY" ]]; then
91
- MSG+="Overlay loaded: $OVERLAY. "
92
- else
93
- MSG+="No overlay found at $CWD/ciel-overlay.md — create one for project-specific rules. "
94
- fi
95
- MSG+="Trace ID: $TRACE_ID. Principle: Understand before generating. Verify before claiming done."
96
43
 
97
- # ─── Cued-recall: surface relevant memories ──────────────────────────────────
98
- # If a memory corpus exists, list active (non-stale) memories so the model
99
- # knows what cues are available. Full content read on-demand. See ADR-0001.
100
- MEMORY_INDEX="$CWD/.ciel/memory/index.json"
101
- if [[ -f "$MEMORY_INDEX" ]]; then
102
- MEMORY_SUMMARY=$(MEMORY_INDEX="$MEMORY_INDEX" python3 -c "
103
- import json, os
104
- try:
105
- with open(os.environ['MEMORY_INDEX']) as f:
106
- idx = json.load(f)
107
- mems = idx.get('memories', {})
108
- active = [(mid, m) for mid, m in mems.items() if not m.get('stale')]
109
- if not active:
110
- print('')
111
- else:
112
- # Sort by trigger_count desc, then by last_triggered desc
113
- active.sort(key=lambda x: (-(x[1].get('trigger_count') or 0), x[1].get('last_triggered') or ''), reverse=False)
114
- active.sort(key=lambda x: -(x[1].get('trigger_count') or 0))
115
- top = active[:10]
116
- lines = [f\" [{mid}, {m.get('trigger_count', 0)}x] {m.get('title', '?')}\" for mid, m in top]
117
- total = len(active)
118
- more = f' (+{total - len(top)} more)' if total > len(top) else ''
119
- promo = [(mid, m) for mid, m in mems.items() if (m.get('trigger_count') or 0) >= 5 and str(m.get('file', '')).startswith('episodes/')]
120
- promo_hint = ''
121
- if promo:
122
- promo.sort(key=lambda x: -(x[1].get('trigger_count') or 0))
123
- promo_names = ', '.join([m.get('title', '?') for _, m in promo[:3]])
124
- more_promo = f' +{len(promo) - 3} more' if len(promo) > 3 else ''
125
- promo_hint = '\\nConsolidation candidates (' + str(len(promo)) + ' episodes >=5 triggers): ' + promo_names + more_promo + '. Run memoire-consolidator.'
126
- print(f'Cued-recall memory active ({total} memories{more}):\\n' + '\\n'.join(lines) + promo_hint)
127
- except Exception:
128
- print('')
129
- " 2>/dev/null || echo "")
130
- if [[ -n "$MEMORY_SUMMARY" ]]; then
131
- MSG+=$'\n'"$MEMORY_SUMMARY"
132
- MSG+=$'\n'"Memories auto-inject when path/symbol/intent cues match. Read full content from .ciel/memory/{episodes,concepts,guards}/ when relevant."
133
- fi
134
- elif [[ -d "$CWD/.ciel" ]] || [[ -f "$CWD/.claude/settings.json" ]] || [[ -f "$CWD/opencode.json" ]] || [[ -f "$CWD/ciel-overlay.md" ]]; then
135
- # Only suggest bootstrap if Ciel is actually installed in this project (not
136
- # any random repo with a CLAUDE.md). Markers checked: .ciel/ dir, .claude
137
- # settings, opencode config, or an explicit Ciel overlay.
138
- MSG+=$'\n'"No cued-recall memory yet. Run /ciel-memory-bootstrap to scan project for ingestable tribal docs (lessons.md, ciel-overlay.md, .claude/rules/, etc.)."
44
+ MSG="Ciel v${CIEL_VERSION} — Trace: ${TRACE_ID}."
45
+ if [[ -n "$OVERLAY" ]]; then
46
+ MSG+=" Overlay: $OVERLAY."
139
47
  fi
140
48
 
141
- # ─── Pre-flight checklist — injected into every session ──────────────────────
142
- MSG+=$'\n'"---"
143
- MSG+=$'\n'"Checklist pre-vol (verifier avant chaque tache) :"
144
- MSG+=$'\n'" Logs : structured JSON + correlation ID par requete ?"
145
- MSG+=$'\n'" Backups : RPO/RTO definis ? Test de restore fait ?"
146
- MSG+=$'\n'" Cache : TTL + strategie d'invalidation en place ?"
147
- MSG+=$'\n'" Timeouts : explicites (connect/read/request) — pas d'infini ?"
148
- MSG+=$'\n'" Securite : auth, injection, CSRF, rate limiting verifies ?"
149
- MSG+=$'\n'" Metriques : RED (Rate, Errors, Duration) exposees par endpoint ?"
150
- MSG+=$'\n'" Deploiement : CI/CD automatise, health check, rollback possible ?"
151
- MSG+=$'\n'" Pagination : cursor-based, limite max definie ?"
152
- MSG+=$'\n'" Erreurs : structurees (code + message + details) ?"
153
- MSG+=$'\n'" Angles morts : 3 choses identifiees qui manquent dans la demande ?"
154
-
155
49
  # ─── Update check (throttled to once per 24h, never blocks) ──────────────────
156
- # Fetches GitHub VERSION non-blocking (max 2s). Any failure → silent skip.
157
50
  MANIFEST="$HOME/.ciel/manifest.json"
158
51
  LAST_CHECK="$HOME/.ciel/.last-update-check"
159
52
  if [[ -f "$MANIFEST" ]]; then
160
53
  STALE=false
161
- if [[ ! -f "$LAST_CHECK" ]]; then
162
- STALE=true
163
- elif find "$LAST_CHECK" -mmin +1440 2>/dev/null | grep -q .; then
164
- STALE=true
165
- fi
54
+ if [[ ! -f "$LAST_CHECK" ]]; then STALE=true
55
+ elif find "$LAST_CHECK" -mmin +1440 2>/dev/null | grep -q .; then STALE=true; fi
166
56
  if $STALE; then
167
- LOCAL_VER=$(grep -oE '"version":[[:space:]]*"[^"]+"' "$MANIFEST" 2>/dev/null \
168
- | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
169
- REMOTE_VER=$(curl -fsSL --max-time 2 \
170
- https://raw.githubusercontent.com/KaosKyun/Ciel/main/VERSION 2>/dev/null \
171
- | tr -d '[:space:]')
57
+ LOCAL_VER=$(grep -oE '"version":[[:space:]]*"[^"]+"' "$MANIFEST" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
58
+ REMOTE_VER=$(curl -fsSL --max-time 2 https://raw.githubusercontent.com/KaosKyun/Ciel/main/VERSION 2>/dev/null | tr -d '[:space:]')
172
59
  if [[ -n "$LOCAL_VER" && -n "$REMOTE_VER" && "$LOCAL_VER" != "$REMOTE_VER" ]]; then
173
- MSG+=" [UPDATE] Ciel v$LOCAL_VER v$REMOTE_VER available. Run /ciel-update."
60
+ MSG+=" [UPDATE] v${LOCAL_VER}→v${REMOTE_VER}. Run /ciel-update."
174
61
  fi
175
62
  mkdir -p "$HOME/.ciel" 2>/dev/null || true
176
63
  touch "$LAST_CHECK" 2>/dev/null || true