-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.sh
More file actions
executable file
·1128 lines (1008 loc) · 44.8 KB
/
Copy pathrun.sh
File metadata and controls
executable file
·1128 lines (1008 loc) · 44.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# agent-config — 에이전트 인프라 원커맨드 CLI
# Usage: ./run.sh <command> [args]
#
# SSOT: skills/<스킬>/<바이너리> + skills/<스킬>/SKILL.md
# 머신별 네이티브 빌드, NixOS 환경 전용
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SM_DIR="$HOME/repos/gh/andenken"
SKILLS_DIR="$SCRIPT_DIR/skills"
ENV_FILE="$HOME/.env.local"
REPOS="$HOME/repos/gh"
THIRD_REPOS="$HOME/repos/3rd"
ARCH="$(uname -m)" # aarch64 / x86_64
# --- Helpers ---
load_env() {
if [ -f "$ENV_FILE" ]; then
set -a; source "$ENV_FILE"; set +a
else
echo "⚠ $ENV_FILE not found"
fi
}
log() { echo " $*"; }
ok() { echo " ✅ $*"; }
warn() { echo " ⚠ $*"; }
fail() { echo " ❌ $*"; }
section() { echo ""; echo "=== $* ==="; }
# Setup path: keep edited repos safe, but refresh clean managed inputs before build.
# Dirty repos are never pulled; clean repos may fast-forward unless opted out with
# AGENT_CONFIG_SETUP_NO_UPDATE=1. Use `./run.sh update` for an explicit refresh.
ensure_repo_present() {
local dir=$1 name=$2
if [ ! -d "$dir/.git" ]; then
fail "$name: expected git repo at $dir"
return 1
fi
ok "$name: present"
}
# Update path: fetch + fast-forward pull. Skips repos with dirty tree (warns,
# never fails the whole update). Only invoked from `./run.sh update`.
pull_repo_if_clean() {
local dir=$1 name=$2
if [ ! -d "$dir/.git" ]; then
fail "$name: expected git repo at $dir"
return 1
fi
log "$name: pulling..."
(
cd "$dir"
if ! git diff --quiet || ! git diff --cached --quiet; then
echo " ⚠ $name: working tree dirty, skipping pull" >&2
exit 0
fi
git fetch --all --prune --quiet
local upstream
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)
if [ -z "$upstream" ]; then
echo " ⚠ $name: no upstream, skipping pull" >&2
exit 0
fi
git pull --ff-only --quiet
)
ok "$name: up to date"
}
repo_dirty() {
local dir=$1
(cd "$dir" && { ! git diff --quiet || ! git diff --cached --quiet; })
}
refresh_repo_if_clean() {
local dir=$1 name=$2
if [ ! -d "$dir/.git" ]; then
warn "$name: expected git repo at $dir — skipping refresh"
return 0
fi
if repo_dirty "$dir"; then
warn "$name: dirty, skipping refresh (build uses local source)"
return 0
fi
(
cd "$dir"
local upstream local_head upstream_head base
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)
if [ -z "$upstream" ]; then
echo " ⚠ $name: no upstream, skipping refresh" >&2
exit 0
fi
if ! git fetch --prune --quiet; then
echo " ⚠ $name: fetch failed, using local source" >&2
exit 0
fi
local_head=$(git rev-parse HEAD)
upstream_head=$(git rev-parse "$upstream")
base=$(git merge-base HEAD "$upstream")
if [ "$local_head" = "$upstream_head" ]; then
echo " ✅ $name: up to date"
elif [ "$local_head" = "$base" ]; then
if git pull --ff-only --quiet; then
echo " ✅ $name: fast-forwarded"
else
echo " ⚠ $name: fast-forward failed, using local source" >&2
fi
else
echo " ⚠ $name: local branch diverged from $upstream, skipping refresh" >&2
fi
)
}
ensure_repo() {
local name=$1 url=$2
local dir="$REPOS/$name"
if [ -d "$dir" ]; then
ensure_repo_present "$dir" "$name"
else
log "$name: cloning..."
git clone "$url" "$dir"
ok "$name: cloned"
fi
}
ensure_repo_at() {
local base_dir=$1 name=$2 url=$3
local dir="$base_dir/$name"
mkdir -p "$base_dir"
if [ -d "$dir" ]; then
ensure_repo_present "$dir" "$name"
else
log "$name: cloning..."
git clone "$url" "$dir"
ok "$name: cloned"
fi
}
# Create symlink — remove old/broken, backup regular files
ensure_link() {
local target=$1 link=$2
if [ -L "$link" ]; then
local current
current=$(readlink "$link")
if [ "$current" = "$target" ]; then
return
fi
rm "$link"
elif [ -e "$link" ]; then
# Regular file/dir exists — back up and replace
mv "$link" "${link}.bak.$(date +%Y%m%d)"
log "$(basename "$link"): backed up existing → $(basename "${link}.bak.$(date +%Y%m%d)")"
fi
local parent
parent=$(dirname "$link")
mkdir -p "$parent"
ln -s "$target" "$link"
ok "$(basename "$link") → $target"
}
# Merge a JSON keyset fragment into a destination settings file WITHOUT owning
# the whole file. Used where another writer co-owns the same file by a disjoint
# keyset — entwurf meta-bridge (Claude settings.json) or the pi runtime
# (pi settings.json, e.g. lastChangelogVersion). We inject only OUR keys; every
# other key survives. NEVER symlink such a file: a symlink = whole-file ownership,
# and the next writer's atomic rename silently clobbers the other side (this is
# exactly why the pi runtime kept dirtying the repo through the old symlink).
#
# Co-owner is authoritative — the merge is EXISTING-WINS:
# jq '.[1] * .[0]' = fragment * existing → existing wins on overlap,
# objects merge recursively, arrays replace. The fragment only FILLS keys the
# live file lacks; it never overwrites a value the co-owner already set. So
# entwurf/runtime keys (statusLine, B-lite scalars, enabledPlugins.entwurf-
# meta-receive, extraKnownMarketplaces, lastChangelogVersion) survive untouched.
#
# Divergence guard: if the live file already carries a leaf our fragment also
# sets, but with a DIFFERENT value, we WARN (and keep the live value) so the
# operator reconciles instead of a silent clobber. To push a new agent-config
# default onto such a key, clear it from the live file first. Idempotent.
merge_settings() {
local fragment=$1 dest=$2
local existing='{}'
if [ -L "$dest" ]; then
# Legacy symlink from the old whole-file model — de-reference its content,
# then break the link so we write a real, co-ownable file.
existing=$(cat "$dest" 2>/dev/null || echo '{}')
rm "$dest"
log "$(basename "$dest"): broke legacy symlink → real file (keyset-merge model)"
elif [ -f "$dest" ]; then
existing=$(cat "$dest" 2>/dev/null || echo '{}')
fi
# Warn on any fragment leaf where the live (co-owner) value diverges.
local collisions
# NB: do not use paths(scalars): it drops false/null leaves. Also treat arrays
# as a single leaf because jq object merge replaces arrays wholesale.
collisions=$(jq -rs '
def leaf_paths:
if type == "object" then
to_entries[] | [.key] + (.value | leaf_paths)
else
[]
end;
def path_exists($p):
if ($p | length) == 0 then true
elif type == "object" and ($p[0] | type) == "string" and has($p[0]) then
.[$p[0]] | path_exists($p[1:])
elif type == "array" and ($p[0] | type) == "number" and $p[0] >= 0 and $p[0] < length then
.[$p[0]] | path_exists($p[1:])
else
false
end;
.[0] as $live | .[1] as $frag |
[ ($frag | leaf_paths) as $p
| select($live | path_exists($p))
| ($live | getpath($p)) as $lv
| ($frag | getpath($p)) as $fv
| select($lv != $fv)
| "\($p | map(tostring) | join(".")): live=\($lv|tojson) ≠ agent-config=\($fv|tojson)"
] | .[]
' <(printf '%s' "$existing") "$fragment" 2>/dev/null) || true
if [ -n "$collisions" ]; then
warn "$(basename "$dest"): live diverges from agent-config fragment — keeping live (co-owner authoritative):"
while IFS= read -r _line; do warn " · $_line"; done <<< "$collisions"
fi
local merged
merged=$(jq -s '.[1] * .[0]' <(printf '%s' "$existing") "$fragment") || {
fail "$(basename "$dest"): jq merge failed — left unchanged"
return 1
}
local parent tmp
parent=$(dirname "$dest"); mkdir -p "$parent"
tmp="${dest}.tmp.$$"
printf '%s\n' "$merged" > "$tmp" && mv "$tmp" "$dest"
ok "$(basename "$dest") ← merged $(basename "$fragment") keyset (existing-wins; co-owner keys preserved)"
}
# Build Go binary — CGO_ENABLED=0, static, stripped
go_build() {
local src_dir=$1 output=$2
(cd "$src_dir" && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "$output" .)
}
# --- CLI Repo Definitions ---
# name, git_url, go_src_subdir, build_type
# build_type: go | go-version | graalvm | go-install
declare -A CLI_REPOS=(
[denotecli]="https://github.com/junghan0611/denotecli.git"
[gitcli]="https://github.com/junghan0611/gitcli.git"
[lifetract]="https://github.com/junghan0611/lifetract.git"
[zotero-config]="https://github.com/junghan0611/zotero-config.git"
[dictcli]="https://github.com/junghan0611/dictcli.git"
)
# Third-party packages used by the harness
# pi-packages (ben-vargas/pi-claude-code-use) is intentionally disabled for now.
# Reason: pause the Claude Code-style compatibility patch path until account-risk is clearer.
declare -A THIRD_PARTY_PACKAGE_REPOS=()
# Local provider/package repos cloned as SOURCE for dev dogfooding.
# entwurf is the current Claude path in pi via ACP. agent-config clones it as
# source only — install/auth/setup belong to entwurf's own `./run.sh setup`.
# A consumer install here would weaken entwurf's own release-gate 검증면.
declare -A PACKAGE_REPOS=(
[entwurf]="https://github.com/junghan0611/entwurf.git"
)
# is_server_device drives device-specific settings/hooks selection only.
# (entwurf is NOT consumer-installed by device class anymore — always the dev
# clone in ~/repos/gh/; see setup_repos + setup_npm.)
#
# Primary signal: ~/.current-forge-profile (the forge skill's machine SSOT).
# A forge *host* machine writes its profile there (oracle / work) and is, by
# definition, a server. Client machines (thinkpad/laptop/nuc) have no such file
# and resolve their forge per-cwd → they fall through to the dev path.
# SERVER_PROFILES is the set of profiles that mean "this machine is a server".
SERVER_PROFILES="oracle work"
FORGE_PROFILE_FILE="$HOME/.current-forge-profile"
# Legacy fallback: an explicit device-name allowlist, used only when no forge
# profile is present. Public-safe defaults here; private device names go in
# ~/.config/agent-config/server-devices.txt (one per line).
SERVER_DEVICES="oracle"
SERVER_DEVICES_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/agent-config/server-devices.txt"
# Current machine's forge profile (whitespace-trimmed), empty if the file is absent.
current_forge_profile() {
[ -f "$FORGE_PROFILE_FILE" ] || return 0
tr -d '[:space:]' < "$FORGE_PROFILE_FILE"
}
server_devices_list() {
printf '%s\n' "$SERVER_DEVICES"
if [ -f "$SERVER_DEVICES_FILE" ]; then
grep -v '^[[:space:]]*#' "$SERVER_DEVICES_FILE" | sed '/^[[:space:]]*$/d'
fi
}
# True when this machine is a server.
# 1순위: ~/.current-forge-profile 이 존재하면 그 값만 보고 판정한다 (forge host = server).
# 2순위: forge profile 이 없을 때만 device-name allowlist 로 fallback.
is_server_device() {
local profile device
profile="$(current_forge_profile)"
if [ -n "$profile" ]; then
printf '%s\n' $SERVER_PROFILES | grep -Fxq "$profile"
return
fi
device="$(cat "$HOME/.current-device" 2>/dev/null || echo unknown)"
server_devices_list | grep -Fxq "$device"
}
# Go src subdirectory within each repo
declare -A CLI_GO_SRC=(
[denotecli]="denotecli"
[gitcli]="gitcli"
[lifetract]="lifetract"
[bibcli]="bibcli" # inside zotero-config
)
# --- setup:repos — Clone missing repos ---
setup_preflight() {
section "Preflight"
# Node >= 22.6 — entwurf's MCP launchers run TS via --experimental-strip-types.
local node_v
node_v="$(node --version 2>/dev/null | sed 's/^v//')"
if [ -z "$node_v" ]; then
fail "node not found in PATH"
return 1
fi
local node_major="${node_v%%.*}"
if [ "$node_major" -lt 22 ]; then
fail "node >= 22.6 required (found $node_v) — entwurf uses --experimental-strip-types"
return 1
fi
ok "node $node_v"
# pi binary on PATH
if command -v pi &>/dev/null; then
ok "pi $(pi --version 2>/dev/null | head -1 || echo 'present')"
else
warn "pi not in PATH — install pi-mono before launching sessions"
fi
# ~/.current-device — drives device-specific Claude + pi settings selection
if [ -f "$HOME/.current-device" ]; then
if is_server_device; then
ok "device: $(cat "$HOME/.current-device") (server)"
else
ok "device: $(cat "$HOME/.current-device") (developer)"
fi
else
warn "~/.current-device not set — server-mode hooks/settings won't activate"
fi
# Anthropic auth — entwurf's sync-auth alias copies from here.
if [ -f "$HOME/.pi/agent/auth.json" ] || [ -f "$HOME/.claude.json" ]; then
ok "claude auth detected"
else
warn "no claude auth — run 'claude' once or seed ~/.pi/agent/auth.json before launching sessions"
fi
return 0
}
setup_repos() {
section "Git Repositories"
for name in "${!CLI_REPOS[@]}"; do
ensure_repo "$name" "${CLI_REPOS[$name]}"
done
section "Third-Party Package Repositories"
for name in "${!THIRD_PARTY_PACKAGE_REPOS[@]}"; do
ensure_repo_at "$THIRD_REPOS" "$name" "${THIRD_PARTY_PACKAGE_REPOS[$name]}"
done
section "Provider Package Repositories"
# entwurf source clone on EVERY device (dev dogfooding). install/auth belong
# to entwurf's own ./run.sh setup, not here.
for name in "${!PACKAGE_REPOS[@]}"; do
ensure_repo "$name" "${PACKAGE_REPOS[$name]}"
done
return 0
}
# --- setup refresh — fast-forward clean build inputs before compiling ---
setup_refresh_self() {
[ "${AGENT_CONFIG_SETUP_NO_UPDATE:-}" = "1" ] && { log "setup auto-update disabled (AGENT_CONFIG_SETUP_NO_UPDATE=1)"; return 0; }
[ "${AGENT_CONFIG_SETUP_SELF_REFRESHED:-}" = "1" ] && return 0
[ -d "$SCRIPT_DIR/.git" ] || return 0
if repo_dirty "$SCRIPT_DIR"; then
warn "agent-config: dirty, skipping self-refresh"
return 0
fi
set +e
(
cd "$SCRIPT_DIR"
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)
[ -n "$upstream" ] || exit 0
git fetch --prune --quiet || exit 0
local_head=$(git rev-parse HEAD)
upstream_head=$(git rev-parse "$upstream")
base=$(git merge-base HEAD "$upstream")
if [ "$local_head" = "$upstream_head" ]; then
exit 0
elif [ "$local_head" = "$base" ]; then
git pull --ff-only --quiet || exit 0
exit 10
else
exit 0
fi
)
local rc=$?
set -e
if [ "$rc" -eq 10 ]; then
ok "agent-config: fast-forwarded; restarting setup with updated run.sh"
exec env AGENT_CONFIG_SETUP_SELF_REFRESHED=1 "$SCRIPT_DIR/run.sh" setup
fi
}
setup_refresh_managed_repos() {
if [ "${AGENT_CONFIG_SETUP_NO_UPDATE:-}" = "1" ]; then
log "managed repo refresh skipped (AGENT_CONFIG_SETUP_NO_UPDATE=1)"
return 0
fi
section "Refresh Managed Repositories"
for name in "${!CLI_REPOS[@]}"; do
refresh_repo_if_clean "$REPOS/$name" "$name"
done
for name in "${!THIRD_PARTY_PACKAGE_REPOS[@]}"; do
refresh_repo_if_clean "$THIRD_REPOS/$name" "$name"
done
for name in "${!PACKAGE_REPOS[@]}"; do
refresh_repo_if_clean "$REPOS/$name" "$name"
done
}
# --- update — pull every known repo that's clean; warn-and-skip on dirty ---
update_repos() {
section "Pulling agent-config's tracked repos"
for name in "${!CLI_REPOS[@]}"; do
pull_repo_if_clean "$REPOS/$name" "$name"
done
for name in "${!THIRD_PARTY_PACKAGE_REPOS[@]}"; do
pull_repo_if_clean "$THIRD_REPOS/$name" "$name"
done
for name in "${!PACKAGE_REPOS[@]}"; do
pull_repo_if_clean "$REPOS/$name" "$name"
done
return 0
}
# --- setup:build — Build all CLI binaries ---
setup_build() {
section "Build CLI Binaries ($ARCH)"
# Go-based CLIs
log "--- denotecli ---"
go_build "$REPOS/denotecli/denotecli" "$SKILLS_DIR/denotecli/denotecli"
ok "denotecli $(du -h "$SKILLS_DIR/denotecli/denotecli" | cut -f1)"
log "--- bibcli ---"
local BIBCLI_VERSION
BIBCLI_VERSION="$(git -C "$REPOS/zotero-config" describe --tags --always --dirty 2>/dev/null || echo dev)"
(cd "$REPOS/zotero-config/bibcli" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$BIBCLI_VERSION" -o "$SKILLS_DIR/bibcli/bibcli" .)
ok "bibcli $(du -h "$SKILLS_DIR/bibcli/bibcli" | cut -f1)"
log "--- gitcli ---"
go_build "$REPOS/gitcli/gitcli" "$SKILLS_DIR/gitcli/gitcli"
ok "gitcli $(du -h "$SKILLS_DIR/gitcli/gitcli" | cut -f1)"
log "--- lifetract ---"
go_build "$REPOS/lifetract/lifetract" "$SKILLS_DIR/lifetract/lifetract"
ok "lifetract $(du -h "$SKILLS_DIR/lifetract/lifetract" | cut -f1)"
# gog — NOT built here. The junghan0611/gogcli fork bundle is retired; gog is now
# a global upstream install managed by nixos-config
# (scripts/external-packages.sh install gog → ~/.local/bin/gog). See gogcli SKILL.md.
log "--- dictcli (GraalVM native-image + Kiwi stem) ---"
if [ -d "$REPOS/dictcli" ]; then
# Kiwi jar + 모델 다운로드 (stem용 — JVM 모드)
(cd "$REPOS/dictcli" && ./run.sh stem-setup) || true
# binary + graph.edn 세트 복사 (SSOT: dictcli 리포)
if ! (cd "$REPOS/dictcli" && ./run.sh build --output "$SKILLS_DIR/dictcli/dictcli"); then
warn "dictcli: build failed (기존 바이너리 유지)"
fi
if [ -f "$SKILLS_DIR/dictcli/dictcli" ]; then
ok "dictcli $(du -h "$SKILLS_DIR/dictcli/dictcli" | cut -f1)"
else
fail "dictcli: binary missing"
fi
else
warn "dictcli: repo not found at $REPOS/dictcli"
fi
return 0
}
# --- setup:links — Symlinks for pi, claude, codex, gemini, antigravity ---
setup_links() {
section "Pi Agent Links"
# Extensions — 폴더 심볼릭 링크면 실제 디렉토리로 교체
if [ -L "$HOME/.pi/agent/extensions" ]; then
rm "$HOME/.pi/agent/extensions"
log "extensions: removed legacy folder symlink"
fi
mkdir -p "$HOME/.pi/agent/extensions"
# pi-extensions/ 내 .ts 파일만 (semantic-memory는 스킬로 사용)
# Remove legacy semantic-memory extension link if present
[ -L "$HOME/.pi/agent/extensions/semantic-memory" ] && rm "$HOME/.pi/agent/extensions/semantic-memory"
for ext_file in "$SCRIPT_DIR"/pi-extensions/*.ts; do
[ -f "$ext_file" ] || continue
ensure_link "$ext_file" "$HOME/.pi/agent/extensions/$(basename "$ext_file")"
done
# control.ts: formerly from 3rd-party agent-stuff, now managed in agent-config/pi-extensions/
# (2026-04-13: forked with targetSessionId fallback + gcStaleSockets)
# Phase 4 migration cleanup — entwurf surface moved to entwurf.
# Old machines (Oracle etc.) may still have these from pre-migration setups.
for legacy in delegate.ts delegate-targets.json lib semantic-memory; do
if [ -e "$HOME/.pi/agent/extensions/$legacy" ] || [ -L "$HOME/.pi/agent/extensions/$legacy" ]; then
rm -rf "$HOME/.pi/agent/extensions/$legacy"
log "extensions/$legacy: removed legacy entry (now owned by entwurf / andenken)"
fi
done
if [ -e "$HOME/.pi/agent/delegate-targets.json" ] || [ -L "$HOME/.pi/agent/delegate-targets.json" ]; then
rm -f "$HOME/.pi/agent/delegate-targets.json"
log "delegate-targets.json: removed (entwurf-targets.json now owned by entwurf)"
fi
# Settings — keyset-merge, never symlink: the pi runtime co-owns settings.json
# (writes lastChangelogVersion), so a symlink lets it dirty the repo. Merge keeps
# a co-ownable real file; agent-config keys apply, runtime keys are preserved.
local PI_SETTINGS_FILE="$SCRIPT_DIR/pi/settings.json"
if is_server_device; then
PI_SETTINGS_FILE="$SCRIPT_DIR/pi/settings.server.json"
log "device=$(cat "$HOME/.current-device") → server pi settings (keyset-merge)"
fi
merge_settings "$PI_SETTINGS_FILE" "$HOME/.pi/agent/settings.json"
ensure_link "$SCRIPT_DIR/pi/keybindings.json" "$HOME/.pi/agent/keybindings.json"
# Skills (pi) — 개별 링크.
mkdir -p "$HOME/.pi/agent/skills/pi-skills"
# 기존 디렉토리 심링크가 있으면 제거 (개별 링크로 전환)
[ -L "$HOME/.pi/agent/skills/pi-skills" ] && rm "$HOME/.pi/agent/skills/pi-skills" && mkdir -p "$HOME/.pi/agent/skills/pi-skills"
# PI_SKIP_SKILLS — 일부러 비워둔다. semantic-memory는 pi 네이티브에서도 SKILL.md 스킬로 노출한다.
# andenken extension이 session_search / knowledge_search registerTool을 별도 제공하지만,
# 같은 capability를 두 surface로 부를 수 있는 것이 정책상 중립이다(SSOT는 하나, 호출 표면이 둘).
# registerTool과 스킬은 충돌하지 않고, 모든 백엔드(pi/ACP Claude/Codex/Gemini)가 동일한
# `semantic-memory` 스킬 이름을 알게 되어 surface 비대칭이 줄어든다.
local PI_SKIP_SKILLS=""
for skill_dir in "$SKILLS_DIR"/*/; do
[ -f "$skill_dir/SKILL.md" ] || continue
local sname
sname=$(basename "$skill_dir")
# 제외 목록에 있으면 건너뛰기
if echo "$PI_SKIP_SKILLS" | grep -qw "$sname"; then
[ -L "$HOME/.pi/agent/skills/pi-skills/$sname" ] && rm "$HOME/.pi/agent/skills/pi-skills/$sname"
continue
fi
ensure_link "$skill_dir" "$HOME/.pi/agent/skills/pi-skills/$sname"
done
# .bak.* 정리 — ensure_link가 만든 백업이 pi 스킬 스캔에 잡히지 않도록
for bak_dir in "$HOME/.pi/agent/skills/pi-skills"/*.bak.*; do
[ -d "$bak_dir" ] || continue
rm -rf "$bak_dir"
log "cleaned up: $(basename "$bak_dir")"
done
section "ENTWURF Claude Plugin"
# entwurf는 SDK 격리 모드(settingSources: [])라 ~/.claude/skills/를 자동 발견하지 않음.
# 그래서 agent-config는 SDK plugins:[{type:"local", path}]가 읽을 local plugin root 한 벌을 구성한다.
# 이 디렉토리(~/.pi/agent/claude-plugin/)는 agent-config의 운영 경로일 뿐, entwurf 자체 계약은 아님.
# semantic-memory 포함: entwurf Claude 세션은 andenken 네이티브 tool이 없음 → 스킬로 제공.
mkdir -p "$HOME/.pi/agent/claude-plugin/.claude-plugin"
mkdir -p "$HOME/.pi/agent/claude-plugin/skills"
ensure_link "$SCRIPT_DIR/pi/claude-plugin.json" \
"$HOME/.pi/agent/claude-plugin/.claude-plugin/plugin.json"
for skill_dir in "$SKILLS_DIR"/*/; do
[ -f "$skill_dir/SKILL.md" ] || continue
local sname
sname=$(basename "$skill_dir")
ensure_link "$skill_dir" "$HOME/.pi/agent/claude-plugin/skills/$sname"
done
# .bak.* 정리 — SDK가 백업 디렉토리를 스킬로 스캔하면 충돌
for bak_dir in "$HOME/.pi/agent/claude-plugin/skills"/*.bak.*; do
[ -d "$bak_dir" ] || continue
rm -rf "$bak_dir"
log "cleaned up: $(basename "$bak_dir")"
done
section "PATH Binaries (~/.local/bin)"
mkdir -p "$HOME/.local/bin"
# dictcli 제외 — CWD에 graph.edn 필요하므로 PATH 심링크 불가. 스킬 디렉토리에서만 실행.
for cli in denotecli bibcli gitcli lifetract; do
local src="$SKILLS_DIR/$cli/$cli"
local dst="$HOME/.local/bin/$cli"
if [ -f "$src" ]; then
ensure_link "$src" "$dst"
fi
done
# dictcli PATH 심링크가 남아있으면 제거 (심링크에 write하면 바이너리 파괴)
[ -e "$HOME/.local/bin/dictcli" ] && rm -f "$HOME/.local/bin/dictcli" && log "dictcli: removed from PATH (skill-only)"
# gog: managed globally by nixos-config (upstream go install → ~/.local/bin/gog).
# Only drop a stale symlink from the old bundled-fork setup; never touch the real
# binary that `go install` writes (-L matches symlinks only, not regular files).
[ -L "$HOME/.local/bin/gog" ] && rm -f "$HOME/.local/bin/gog" && log "gog: removed stale skill-dir symlink (now nixos-managed global)"
section "Pi Themes"
mkdir -p "$HOME/.pi/agent/themes"
for theme_file in "$SCRIPT_DIR"/pi-themes/*.json; do
[ -f "$theme_file" ] || continue
local tname
tname=$(basename "$theme_file")
ensure_link "$theme_file" "$HOME/.pi/agent/themes/$tname"
done
section "Pi Prompts (Commands)"
mkdir -p "$HOME/.pi/agent/prompts"
# Legacy cleanup — /recap renamed to /recall on 2026-05-12 (avoids Claude Code built-in shadow).
# Remove stale symlinks so other devices don't end up with broken /recap + working /recall.
[ -L "$HOME/.pi/agent/prompts/recap.md" ] && rm "$HOME/.pi/agent/prompts/recap.md" && log "prompts/recap.md (removed — renamed to recall.md)"
for cmd_file in "$SCRIPT_DIR"/commands/*.md; do
[ -f "$cmd_file" ] || continue
ensure_link "$cmd_file" "$HOME/.pi/agent/prompts/$(basename "$cmd_file")"
done
section "Pi Telegram (pi-telegram bridge config)"
# PI_ENTWURF_BOT_TOKEN이 있으면 telegram.json 자동 생성
load_env
if [ -n "${PI_ENTWURF_BOT_TOKEN:-}" ]; then
local tg_json="$HOME/.pi/agent/telegram.json"
local bot_id
bot_id=$(echo "$PI_ENTWURF_BOT_TOKEN" | cut -d: -f1)
local chat_id="${PI_TELEGRAM_CHAT_ID:-0}"
cat > "$tg_json" << TGJSON
{
"botToken": "$PI_ENTWURF_BOT_TOKEN",
"botId": $bot_id,
"allowedUserId": $chat_id
}
TGJSON
ok "telegram.json (@glg_entwurf_bot, chatId=$chat_id)"
else
log "PI_ENTWURF_BOT_TOKEN not set — skipping telegram.json"
fi
section "Home AGENTS.md / CLAUDE.md"
ensure_link "$SCRIPT_DIR/home/AGENTS.md" "$HOME/AGENTS.md"
ensure_link "$SCRIPT_DIR/home/CLAUDE.md" "$HOME/CLAUDE.md"
# Clean up legacy MITSEIN.md / ENTWURF.md symlinks — Mitsein moved to ~/sync/org/MITSEIN.md
[ -L "$HOME/MITSEIN.md" ] && rm "$HOME/MITSEIN.md" && log "MITSEIN.md (removed — moved to ~/sync/org/)"
[ -L "$HOME/ENTWURF.md" ] && rm "$HOME/ENTWURF.md" && log "ENTWURF.md (removed legacy symlink)"
section "Claude Code Config"
mkdir -p "$HOME/.claude/hooks"
# CLAUDE.md — Claude Code가 non-append 모드에서 읽는 진입점 (@AGENTS.md include)
ensure_link "$SCRIPT_DIR/home/CLAUDE.md" "$HOME/.claude/CLAUDE.md"
# settings.json 전략 (서버·워크스테이션 공통): 심링크 금지 — entwurf meta-bridge가
# 공동 소유할 수 있으므로 항상 keyset-merge(co-ownable 실제 파일). 서버는 서버용
# 프래그먼트, 워크스테이션은 기본 프래그먼트를 merge-in 하고 co-owner 키는 보존.
local DEVICE
DEVICE=$(cat "$HOME/.current-device" 2>/dev/null || echo "unknown")
if is_server_device; then
log "device=$DEVICE → server settings (keyset-merge, co-ownable real file)"
merge_settings "$SCRIPT_DIR/claude/settings.server.json" "$HOME/.claude/settings.json"
else
merge_settings "$SCRIPT_DIR/claude/settings.fragment.json" "$HOME/.claude/settings.json"
fi
ensure_link "$SCRIPT_DIR/claude/settings.local.json" "$HOME/.claude/settings.local.json"
ensure_link "$SCRIPT_DIR/claude/keybindings.json" "$HOME/.claude/keybindings.json"
ensure_link "$SCRIPT_DIR/claude/statusline.sh" "$HOME/.claude/statusline.sh"
ensure_link "$SCRIPT_DIR/claude/hooks/session-info.sh" "$HOME/.claude/hooks/session-info.sh"
# Notification hook (server settings only) — pings ntfy when an agent needs the
# operator (permission / idle). Shared by all harnesses; topic per ~/.current-forge-profile.
[ -L "$HOME/.claude/hooks/session-end-ntfy.sh" ] && rm "$HOME/.claude/hooks/session-end-ntfy.sh"
ensure_link "$SCRIPT_DIR/claude/hooks/agent-notify.sh" "$HOME/.claude/hooks/agent-notify.sh"
section "Claude Code Skills"
# ~/.claude/skills → skills/ (단일 디렉토리 링크)
ensure_link "$SKILLS_DIR" "$HOME/.claude/skills"
section "Claude Code Commands"
# 백엔드 직접 사용 모드 슬래시 자리 (~/.claude/commands/<name>.md).
# entwurf 경유 모드는 ~/.pi/agent/prompts/가 이미 처리하므로 충돌 없음 — 모드별 분리.
# plugin namespace 측(~/.pi/agent/claude-plugin/commands/)도 같은 SSOT를 가리키게 둔다.
# Codex / Gemini는 surface 비대칭(Codex: 사용자 슬래시 surface 자체 없음 / Gemini: .toml 변환 magic
# 필요)이라 박지 않는다 — North Star "thin bridge / no magic".
ensure_link "$SCRIPT_DIR/commands" "$HOME/.claude/commands"
mkdir -p "$HOME/.pi/agent/claude-plugin/commands"
# Legacy cleanup — /recap renamed to /recall on 2026-05-12 (avoids Claude Code built-in shadow).
[ -L "$HOME/.pi/agent/claude-plugin/commands/recap.md" ] && rm "$HOME/.pi/agent/claude-plugin/commands/recap.md" && log "claude-plugin/commands/recap.md (removed — renamed to recall.md)"
for cmd_file in "$SCRIPT_DIR"/commands/*.md; do
[ -f "$cmd_file" ] || continue
ensure_link "$cmd_file" "$HOME/.pi/agent/claude-plugin/commands/$(basename "$cmd_file")"
done
section "Codex Skills"
# ~/.codex/skills/ has .system/ (built-in) — individual skill links, not directory replace
mkdir -p "$HOME/.codex/skills"
for skill_dir in "$SKILLS_DIR"/*/; do
[ -f "$skill_dir/SKILL.md" ] || continue
local sname
sname=$(basename "$skill_dir")
ensure_link "$skill_dir" "$HOME/.codex/skills/$sname"
done
# Clean up old skills not in our set
for old in bd-to-br-migration pi-agent-rust; do
[ -L "$HOME/.codex/skills/$old" ] && rm "$HOME/.codex/skills/$old"
[ -d "$HOME/.codex/skills/$old" ] && rm -rf "$HOME/.codex/skills/$old"
done
ensure_link "$SCRIPT_DIR/codex/config.toml" "$HOME/.codex/config.toml"
section "Gemini CLI (legacy) Config"
ensure_link "$SCRIPT_DIR/gemini/settings.json" "$HOME/.gemini/settings.json"
section "Gemini CLI (legacy) Skills"
# ~/.gemini/skills → skills/ (단일 디렉토리 링크, Agent Skills open standard)
# Gemini CLI v0.40+ discovers SKILL.md from ~/.gemini/skills/<sname>/ — same SSOT
mkdir -p "$HOME/.gemini"
ensure_link "$SKILLS_DIR" "$HOME/.gemini/skills"
section "Antigravity CLI Settings"
# agy direct harness persistent settings (status line, permissions, model, etc.)
mkdir -p "$HOME/.gemini/antigravity-cli"
ensure_link "$SCRIPT_DIR/antigravity/settings.json" "$HOME/.gemini/antigravity-cli/settings.json"
section "Antigravity CLI Skills"
# agy direct harness global skills path
ensure_link "$SKILLS_DIR" "$HOME/.gemini/antigravity-cli/skills"
# agy MCP config는 agent-config가 아니라 entwurf의 install-agy-bridge
# adapter(봉인 7)가 소유한다: 그 adapter가 regular file을 adopt/create 하고
# bare `entwurf-bridge` stable bin을 command로 기록한다. 여기서 심링크를 걸면
# adapter가 REFUSE 하므로, agent-config는 agy mcp_config를 더 이상 링크하지
# 않는다. (위 skills 링크는 유지 — 소유 이관 대상은 mcp_config 뿐)
return 0
}
# --- setup:npm — pnpm install for extensions/skills ---
setup_npm() {
section "pnpm install"
# andenken
if [ -f "$SM_DIR/run.sh" ]; then
"$SM_DIR/run.sh" setup
ok "andenken"
else
warn "andenken: repo not found at $SM_DIR"
fi
# entwurf (self-built telegram bridge) — deprecated 2026-05-03 in favor of
# pi-telegram (badlogic). The "entwurf" name now belongs to entwurf's
# sibling-spawn surface only. Repo at ~/repos/gh/entwurf/ is preserved for
# history; not loaded as a pi package and not built here.
# pi-packages (ben-vargas) intentionally disabled for now.
log "pi-packages: disabled (skipping pi-claude-code-use install)"
# entwurf install/auth/setup는 agent-config가 소유하지 않는다. entwurf는 자기
# repo(~/repos/gh/entwurf)에서 `./run.sh setup`으로 스스로 설치·auth·검증한다
# (dev dogfooding). agent-config가 consumer로 install하면 entwurf의 release-gate
# 검증면이 약해지므로, 여기서는 소스 clone/pull(setup_repos)까지만 관여하고
# 설치면(pi install / pnpm / sync-auth / check-mcp / entwurf-targets 링크)은
# entwurf에 맡긴다. agy MCP도 install-agy-bridge adapter가 소유(위 참조).
# Stale project-local .pi/settings.json detection.
# Pre-migration leftovers (e.g. claude-agent-sdk-pi) override the global SSOT
# silently. Project-local file is gitignored, so it never gets fixed by pull.
local PROJECT_PI_SETTINGS="$SCRIPT_DIR/.pi/settings.json"
if [ -f "$PROJECT_PI_SETTINGS" ]; then
if grep -q "claude-agent-sdk-pi" "$PROJECT_PI_SETTINGS" 2>/dev/null; then
warn "stale $PROJECT_PI_SETTINGS references claude-agent-sdk-pi (pre-migration). Recommend: rm $PROJECT_PI_SETTINGS"
fi
fi
# entwurf-bridge + native async delegate validations moved to entwurf's
# own run.sh after the Entwurf Orchestration migration — their code now lives
# there, and owning validation belongs with owning code.
# pi-telegram (production Telegram bridge by pi author)
# Installed as pi package — no local clone needed
if command -v pi &>/dev/null; then
if pi list 2>/dev/null | grep -q "pi-telegram"; then
ok "pi-telegram (already installed)"
else
log "pi-telegram: installing..."
pi install git:github.com/badlogic/pi-telegram 2>/dev/null && ok "pi-telegram" || warn "pi-telegram: install failed"
fi
else
warn "pi-telegram: pi not found in PATH"
fi
# pi-extensions (grammy 등) — pnpm
local ext_dir="$SCRIPT_DIR/pi-extensions"
if [ -f "$ext_dir/package.json" ]; then
log "pi-extensions: installing..."
if (cd "$ext_dir" && pnpm install --silent --frozen-lockfile); then
ok "pi-extensions"
else
fail "pi-extensions: pnpm install failed"
fi
fi
# Skills with package.json — pnpm
for pkg_dir in "$SKILLS_DIR"/*/; do
if [ -f "$pkg_dir/package.json" ] && [ ! -d "$pkg_dir/node_modules" ]; then
local sname
sname=$(basename "$pkg_dir")
log "$sname: installing..."
if (cd "$pkg_dir" && pnpm install --silent --frozen-lockfile); then
ok "$sname"
else
fail "$sname: pnpm install failed"
fi
fi
done
return 0
}
# --- setup:git-hooks — Global commit/push safety rail ---
# nixos-config 의 home-manager `programs.git.settings.core.hooksPath` 가
# 정식 경로. 이 함수는 nixos-rebuild 전에도 즉시 활성화하고 싶을 때 사용.
# ~/.gitconfig 에 직접 써서 home-manager 설정과 충돌 없이 공존시킨다.
setup_git_hooks() {
section "Global Git Safety Hooks"
local hooks_dir="$SCRIPT_DIR/git-hooks"
if [ ! -d "$hooks_dir" ]; then
fail "git-hooks dir missing: $hooks_dir"
return 1
fi
# Verify executables
for h in pre-commit pre-push _scan.sh _delegate.sh; do
if [ ! -x "$hooks_dir/$h" ]; then
log "chmod +x $h"
chmod +x "$hooks_dir/$h"
fi
done
# Write explicitly to ~/.gitconfig.
# - `git config --global` follows XDG and may try ~/.config/git/config
# first; on NixOS home-manager that file is a read-only nix-store
# symlink → "could not lock config file ... Permission denied".
# - Pinning the target with --file (and GIT_CONFIG_GLOBAL for the
# verification read) avoids that trap and is harmless on devices
# where ~/.gitconfig is already the default location.
local gitconfig="$HOME/.gitconfig"
[ -e "$gitconfig" ] || touch "$gitconfig"
local current
current=$(GIT_CONFIG_GLOBAL="$gitconfig" git config --global --get core.hooksPath 2>/dev/null || echo "")
if [ "$current" = "$hooks_dir" ]; then
ok "core.hooksPath already set → $hooks_dir"
else
git config --file "$gitconfig" core.hooksPath "$hooks_dir"
ok "core.hooksPath → $hooks_dir (written to $gitconfig)"
[ -n "$current" ] && warn "replaced previous value: $current"
fi
# gitleaks presence check (graceful — fallback exists)
if command -v gitleaks >/dev/null 2>&1; then
ok "gitleaks $(gitleaks version 2>/dev/null | head -1)"
else
warn "gitleaks not installed — fallback secret patterns will be used."
warn " Add to nixos-config and rebuild, or: nix shell nixpkgs#gitleaks"
fi
log ""
log "Behavior:"
log " - Public repos (github.com/junghan0611/* | junghanacs/*): secrets + identity terms"
log " - Other repos: secrets only"
log " - Per-repo override: write 'strict'|'loose'|'off' to <repo>/.git-hooks-mode"
log " - Bypass (GLG only): AGENT_ALLOW_UNSAFE_COMMIT=1 git commit ..."
log ""
log "Docs: $hooks_dir/README.md"
return 0
}
# --- setup — 원커맨드: clone + build + link + pnpm ---
setup_all() {
echo "🔧 agent-config setup ($ARCH)"
echo " SSOT: $SKILLS_DIR"
setup_refresh_self
setup_preflight || return 1
setup_repos
setup_refresh_managed_repos
setup_build
setup_links
setup_npm
setup_git_hooks
section "Verification"
local total=0 pass=0
for cli in denotecli bibcli gitcli lifetract dictcli; do
local bin="$SKILLS_DIR/$cli/$cli"
if [ -f "$bin" ] && [ -x "$bin" ]; then
local arch
arch=$(file "$bin" | grep -oP 'ARM aarch64|x86-64' || echo "unknown")
ok "$cli ($arch, $(du -h "$bin" | cut -f1))"
pass=$((pass + 1))
else
fail "$cli: missing or not executable"
fi
total=$((total + 1))
done
# gog — global on PATH (nixos-config managed, upstream), not bundled here
if command -v gog >/dev/null 2>&1; then
ok "gog ($(gog --version 2>/dev/null | head -1), PATH)"
pass=$((pass + 1))
else
fail "gog: not on PATH — nixos-config: external-packages.sh install gog"
fi
total=$((total + 1))
section "Summary"
echo " Binaries: $pass/$total"
echo " Skills: $(find "$SKILLS_DIR" -name "SKILL.md" | wc -l)"
echo " Arch: $ARCH"
echo " Claude in pi (default): entwurf via ACP"
echo " Pi ext: $(readlink "$HOME/.pi/agent/extensions/semantic-memory" 2>/dev/null || echo 'not linked')"
echo " Pi skill: $(readlink "$HOME/.pi/agent/skills/pi-skills" 2>/dev/null || echo 'not linked')"
echo " Claude: $(readlink "$HOME/.claude/settings.json" 2>/dev/null || { [ -f "$HOME/.claude/settings.json" ] && echo 'merged (keyset, not linked)' || echo 'absent'; })"
echo " Codex: $(readlink "$HOME/.codex/config.toml" 2>/dev/null || echo 'config not linked') + $(ls -d "$HOME/.codex/skills"/*/SKILL.md 2>/dev/null | wc -l) skills"
echo " Gemini: legacy $(readlink "$HOME/.gemini/settings.json" 2>/dev/null || echo 'config not linked') + $(readlink "$HOME/.gemini/skills" 2>/dev/null || echo 'skills not linked')"
echo " Antigrav: $(readlink "$HOME/.gemini/antigravity-cli/settings.json" 2>/dev/null || echo 'settings not linked') + $(readlink "$HOME/.gemini/antigravity-cli/skills" 2>/dev/null || echo 'skills not linked') (mcp: entwurf-owned)"
# Sentinel (delegate matrix) moved to entwurf with the rest of the
# Entwurf Orchestration surface — run it from there when exercising the
# delegate paths.
echo ""
echo "DONE: agent-config setup complete"
}
# --- help ---
help() {
cat << 'EOF'
agent-config — 에이전트 인프라 원커맨드 CLI
Usage: ./run.sh <command> [args]
=== 설치 ===
setup 원커맨드 전체 설치 (이것만 기억하면 됨)
→ clone + build + link + pnpm 전부 수행
→ 어떤 디바이스든 이것 하나로 재현
setup:preflight|repos|build|links|pnpm 개별 단계 (디버깅용, 보통 불필요)
setup:git-hooks 글로벌 git 안전망 설치 (core.hooksPath)
→ 공개 repo에 민감 단어/시크릿 commit/push 차단
→ 정식 경로는 nixos-config rebuild. 이건 즉시 활성화용
update 추적 리포 일괄 pull (dirty면 skip) — setup은 pull 안 함
=== 테스트 ===
test 모든 테스트 (unit + integration)
test:unit 유닛 테스트 (API 불필요)
test:integration 통합 테스트 (API 필요)
test:search "q" 라이브 검색 테스트
=== 인덱싱 ===
index:sessions [--force] 세션 인덱싱 (OpenRouter 8B / 4096d)
index:md [--force] md 가든 인덱싱 (agent-facing knowledge axis)
sync:md md 증분 인덱싱
estimate:md [--full] md 비용/청크 API-0 추정
sync:md:oracle [flags] md.lance + md-manifest.json Oracle 동기화
verify [sessions|md|org] 인덱스 무결성 확인
index:org [--force] Org 인덱싱 (production disabled / R&D only)
compact [sessions|md|org] DB 조각 모음
status 인덱스 상태
=== 벤치마크 ===
bench 전체 벤치마크 (API 필요)
bench:dry 드라이런
=== 유틸 ===
chunk:org [--sample] 청킹 통계
env 환경변수 상태
EOF