@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.
- package/assets/.claude/hooks/check-dispatch-gate.sh +14 -41
- package/assets/.claude/hooks/memory-engine.py +66 -5
- package/assets/.claude/hooks/pre-tool-write.sh +17 -52
- package/assets/.claude/hooks/session-start.sh +15 -128
- package/assets/.claude/hooks/stop.sh +10 -85
- package/assets/.claude/hooks/user-prompt-submit.sh +17 -110
- package/assets/.claude/rules/api-design.md +23 -0
- package/assets/.claude/rules/backend.md +22 -0
- package/assets/.claude/rules/cicd-pipeline.md +23 -0
- package/assets/.claude/rules/containers.md +23 -0
- package/assets/.claude/rules/database-design.md +22 -0
- package/assets/.claude/rules/environments.md +27 -0
- package/assets/.claude/rules/frontend.md +25 -0
- package/assets/.claude/rules/github.md +22 -0
- package/assets/.claude/rules/logging.md +23 -0
- package/assets/.claude/rules/monitoring.md +25 -0
- package/assets/.claude/rules/research.md +20 -0
- package/assets/.claude/settings.json +2 -58
- package/assets/.claude/skills/agile/SKILL.md +42 -0
- package/assets/.claude/skills/alerting/SKILL.md +55 -0
- package/assets/.claude/skills/api-design/SKILL.md +46 -0
- package/assets/.claude/skills/appsec/SKILL.md +43 -0
- package/assets/.claude/skills/architecture/SKILL.md +74 -0
- package/assets/.claude/skills/backend/SKILL.md +41 -0
- package/assets/.claude/skills/backup-recovery/SKILL.md +42 -0
- package/assets/.claude/skills/caching/SKILL.md +44 -0
- package/assets/.claude/skills/cdn/SKILL.md +42 -0
- package/assets/.claude/skills/chaos/SKILL.md +41 -0
- package/assets/.claude/skills/cicd-pipeline/SKILL.md +56 -0
- package/assets/.claude/skills/ciel/SKILL.md +14 -0
- package/assets/.claude/skills/ciel/reference.md +171 -0
- package/assets/.claude/skills/cloud/SKILL.md +42 -0
- package/assets/.claude/skills/code-quality/SKILL.md +42 -0
- package/assets/.claude/skills/code-review/SKILL.md +41 -0
- package/assets/.claude/skills/communication/SKILL.md +42 -0
- package/assets/.claude/skills/containers/SKILL.md +42 -0
- package/assets/.claude/skills/cqrs/SKILL.md +41 -0
- package/assets/.claude/skills/crypto/SKILL.md +46 -0
- package/assets/.claude/skills/data-engineering/SKILL.md +42 -0
- package/assets/.claude/skills/database-design/SKILL.md +46 -0
- package/assets/.claude/skills/ddd/SKILL.md +45 -0
- package/assets/.claude/skills/deployment-strategies/SKILL.md +51 -0
- package/assets/.claude/skills/desktop/SKILL.md +42 -0
- package/assets/.claude/skills/devsecops/SKILL.md +43 -0
- package/assets/.claude/skills/environments/SKILL.md +66 -0
- package/assets/.claude/skills/event-driven/SKILL.md +46 -0
- package/assets/.claude/skills/frontend/SKILL.md +41 -0
- package/assets/.claude/skills/functional/SKILL.md +42 -0
- package/assets/.claude/skills/github/SKILL.md +61 -0
- package/assets/.claude/skills/high-availability/SKILL.md +42 -0
- package/assets/.claude/skills/iac/SKILL.md +46 -0
- package/assets/.claude/skills/logging/SKILL.md +46 -0
- package/assets/.claude/skills/ml-engineering/SKILL.md +42 -0
- package/assets/.claude/skills/mobile/SKILL.md +42 -0
- package/assets/.claude/skills/monitoring/SKILL.md +54 -0
- package/assets/.claude/skills/networking/SKILL.md +42 -0
- package/assets/.claude/skills/nosql/SKILL.md +41 -0
- package/assets/.claude/skills/oop-solid/SKILL.md +42 -0
- package/assets/.claude/skills/performance/SKILL.md +41 -0
- package/assets/.claude/skills/reactive/SKILL.md +42 -0
- package/assets/.claude/skills/release-management/SKILL.md +51 -0
- package/assets/.claude/skills/research/SKILL.md +69 -0
- package/assets/.claude/skills/resilience/SKILL.md +41 -0
- package/assets/.claude/skills/serverless/SKILL.md +42 -0
- package/assets/.claude/skills/servers/SKILL.md +41 -0
- package/assets/.claude/skills/sql/SKILL.md +45 -0
- package/assets/.claude/skills/supply-chain/SKILL.md +41 -0
- package/assets/.claude/skills/system-design/SKILL.md +91 -0
- package/assets/.claude/skills/tech-leadership/SKILL.md +46 -0
- package/assets/.claude/skills/testing/SKILL.md +41 -0
- package/assets/.claude/skills/tracing/SKILL.md +36 -0
- package/assets/CLAUDE.md +31 -122
- package/assets/commands/{ciel-memory-bootstrap.md → ciel-memory-init.md} +3 -3
- package/assets/commands/ciel-memory.md +210 -0
- package/assets/platforms/opencode/.opencode/commands/{ciel-memory-bootstrap.md → ciel-memory-init.md} +3 -3
- package/assets/skills/ciel/SKILL.md +8 -97
- package/bin/ciel.js +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +5 -11
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/claude.d.ts.map +1 -1
- package/dist/cli/claude.js +42 -4
- package/dist/cli/claude.js.map +1 -1
- package/dist/cli/doctor.d.ts +16 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +168 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.js +76 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +23 -4
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/memory.d.ts +18 -0
- package/dist/cli/memory.d.ts.map +1 -0
- package/dist/cli/memory.js +304 -0
- package/dist/cli/memory.js.map +1 -0
- package/dist/cli/opencode.js +1 -1
- package/dist/cli/opencode.js.map +1 -1
- package/package.json +2 -2
- /package/assets/{rules → .claude/rules}/security.md +0 -0
- /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
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
89
|
-
echo "
|
|
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]
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
4
|
-
#
|
|
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
|
-
#
|
|
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
|
|
24
|
+
echo "[CIEL] Gate bypassed — allowing write to $(basename "$FILE_PATH")" >&2
|
|
29
25
|
exit 0
|
|
30
26
|
fi
|
|
31
27
|
|
|
32
|
-
#
|
|
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
|
-
#
|
|
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]
|
|
66
|
-
echo "" >&2
|
|
67
|
-
echo "
|
|
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
|
-
#
|
|
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=$(
|
|
79
|
-
import json
|
|
80
|
-
try:
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|