repo-nexus 0.8.7 → 0.8.8

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 (33) hide show
  1. package/README.md +16 -5
  2. package/lib/ai.sh +227 -78
  3. package/lib/commands/init.sh +1 -4
  4. package/lib/commands/plugin.sh +1 -9
  5. package/lib/commands/status.sh +29 -19
  6. package/lib/commands/sync.sh +1 -4
  7. package/lib/core.sh +2 -2
  8. package/lib/hooks.sh +1 -4
  9. package/lib/plugins.sh +36 -7
  10. package/lib/repos.sh +0 -5
  11. package/lib/sdk.sh +110 -31
  12. package/lib/workspace.sh +4 -0
  13. package/lib/yaml.sh +11 -50
  14. package/package.json +1 -1
  15. package/rnex +1 -0
  16. package/toolkit/plugins/claude/hooks/after_sync.sh +5 -0
  17. package/toolkit/plugins/claude/hooks/before_plugin_disable.sh +6 -0
  18. package/toolkit/plugins/codex/hooks/after_sync.sh +5 -0
  19. package/toolkit/plugins/codex/hooks/before_plugin_disable.sh +6 -0
  20. package/toolkit/plugins/copilot/hooks/after_sync.sh +5 -0
  21. package/toolkit/plugins/copilot/hooks/before_plugin_disable.sh +6 -0
  22. package/toolkit/plugins/cursor/hooks/after_sync.sh +5 -0
  23. package/toolkit/plugins/cursor/hooks/before_plugin_disable.sh +6 -0
  24. package/toolkit/plugins/gemini/hooks/after_sync.sh +5 -0
  25. package/toolkit/plugins/gemini/hooks/before_plugin_disable.sh +6 -0
  26. package/toolkit/plugins/karpathy-llm/README.md +3 -3
  27. package/toolkit/plugins/karpathy-llm/commands/ingest.sh +23 -290
  28. package/toolkit/plugins/karpathy-llm/commands/lint.sh +9 -30
  29. package/toolkit/plugins/karpathy-llm/hooks/_common.sh +330 -2
  30. package/toolkit/plugins/karpathy-llm/hooks/after_sync.sh +11 -245
  31. package/toolkit/plugins/karpathy-llm/hooks/before_plugin_disable.sh +5 -0
  32. package/toolkit/plugins/windsurf/hooks/after_sync.sh +5 -0
  33. package/toolkit/plugins/windsurf/hooks/before_plugin_disable.sh +6 -0
package/README.md CHANGED
@@ -49,7 +49,7 @@ A **Virtual Meta-Repo** companion for multi-repo workspaces and shared AI contex
49
49
 
50
50
  | What It Is | What It Isn't |
51
51
  | :--- | :--- |
52
- | **A lightweight companion utility** (~25 KB POSIX script). | **NOT a replacement for your workspace or tools.** It doesn't replace VS Code, Cursor, JetBrains, or your terminal. |
52
+ | **A lightweight companion utility** (modular pure POSIX `/bin/sh` CLI). | **NOT a replacement for your workspace or tools.** It doesn't replace VS Code, Cursor, JetBrains, or your terminal. |
53
53
  | **A Virtual Meta-Repo orchestrator** managing member clones in `./repos/`. | **NOT a build tool or monorepo orchestrator.** It doesn't replace tools like Nx, Turborepo, Cargo, or Gradle. |
54
54
  | **Zero Git friction.** Repositories remain normal, autonomous Git repos. | **NOT Git submodules or subtrees.** No `.gitmodules` files, no detached HEADs, no commit coordination lock-in. |
55
55
  | **Strictly encapsulated.** Internal rnex assets live exclusively in `.rnex/`. | **NOT invasive.** Member repositories remain clean; workspace-related context is isolated to `repos/<name>/.rnex/`. |
@@ -134,10 +134,21 @@ All declared member repositories are cloned and wired up automatically!
134
134
 
135
135
  ## Project Structure
136
136
 
137
- ```
137
+ ```text
138
138
  repo-nexus/
139
139
  ├── package.json # npm package manifest (single version source of truth)
140
- ├── rnex # CLI executable (POSIX shell)
140
+ ├── rnex # CLI entrypoint (pure POSIX /bin/sh)
141
+ ├── lib/ # Modular POSIX shell libraries
142
+ │ ├── core.sh # Terminal colors, logging, path & version helpers
143
+ │ ├── yaml.sh # Two-level YAML parser & config mutators
144
+ │ ├── workspace.sh # Workspace root discovery & auto-sync engine
145
+ │ ├── repos.sh # Member repo path resolution & on-demand .rnex/
146
+ │ ├── plugins.sh # Plugin discovery, template & command engine
147
+ │ ├── sdk.sh # Plugin Hook SDK & User Skill generator
148
+ │ ├── ai.sh # AI routing instructions, .gitignore & .code-workspace
149
+ │ ├── hooks.sh # Declarative Git hooks & lifecycle event dispatcher
150
+ │ ├── commands.sh # CLI subcommand module loader
151
+ │ └── commands/ # Subcommand implementations (init, sync, repo, etc.)
141
152
  ├── AGENTS.md # Routing-only universal AI instructions
142
153
  ├── CHANGELOG.md # Release history and migration notes
143
154
  ├── CONTRIBUTION.md # POSIX standards, testing setup, and plugin contract
@@ -158,9 +169,9 @@ repo-nexus/
158
169
  ├── tests/
159
170
  │ ├── test_cli.sh # Main test runner
160
171
  │ ├── test_helper.sh # Shared test harness and utilities
161
- │ └── suites/ # Modular test suites
172
+ │ └── suites/ # Modular test suites (01–07)
162
173
  ├── toolkit/ # Built-in plugins and core agentic skills
163
- │ ├── plugins/ # Scoped workspace plugins
174
+ │ ├── plugins/ # Scoped workspace & AI provider plugins
164
175
  │ └── skills/ # Core workspace agentic skills
165
176
  └── README.md
166
177
  ```
package/lib/ai.sh CHANGED
@@ -6,16 +6,14 @@
6
6
 
7
7
  # ---- Gitignore & Search Tool Ignore Reconciliation -------------------------
8
8
 
9
- # Reconcile workspace root .gitignore and .ignore:
10
- # - Ensures repos/ is ignored in git to prevent accidental commits of member repos
11
- # - Ensures .local.rnex.yaml is ignored
12
- # - Ensures .ignore un-ignores repos/* so ripgrep and AI tools can index member files
13
- reconcile_gitignore() {
14
- _dir="${1:-$NEXUS_DIR}"
15
- _gi="$_dir/.gitignore"
16
-
17
- if [ ! -f "$_gi" ]; then
18
- cat > "$_gi" <<GI_EOF
9
+ GITIGNORE_MARKER_START="# REPO-NEXUS:START"
10
+ GITIGNORE_MARKER_END="# REPO-NEXUS:END"
11
+
12
+ # Emit the delimited Repo Nexus .gitignore section synced with git.ignore configuration.
13
+ _build_rnex_gitignore_section() {
14
+ cat <<GI_SECTION_EOF
15
+ $GITIGNORE_MARKER_START
16
+ # Managed by Repo Nexus (rnex) — synced with git.ignore in workspace configuration
19
17
  # Member repos (managed by rnex, ignored at workspace level)
20
18
  repos/*
21
19
  !repos/.gitkeep
@@ -29,7 +27,52 @@ rnex*/
29
27
  # Local machine-specific configuration
30
28
  $RNEX_LOCAL_CONFIG_FILE
31
29
  .rnex.local.yaml
32
- GI_EOF
30
+ GI_SECTION_EOF
31
+
32
+ _custom_ignores="$(yaml_get_git_ignore 2>/dev/null || true)"
33
+ if [ -n "$_custom_ignores" ]; then
34
+ _added_custom_header=0
35
+ _seen_custom=""
36
+ printf '%s\n' "$_custom_ignores" | while IFS= read -r _ci || [ -n "$_ci" ]; do
37
+ [ -n "$_ci" ] || continue
38
+ case "$_ci" in
39
+ "repos/"|"repos/*"|"!repos/.gitkeep"|"**/.rnex*/"|"**/rnex*/"|".rnex*/"|"rnex*/"|"$RNEX_LOCAL_CONFIG_FILE"|".rnex.local.yaml")
40
+ continue
41
+ ;;
42
+ esac
43
+ case "
44
+ $_seen_custom
45
+ " in
46
+ *"
47
+ $_ci
48
+ "*) continue ;;
49
+ esac
50
+ _seen_custom="${_seen_custom:+$_seen_custom
51
+ }$_ci"
52
+ if [ "$_added_custom_header" -eq 0 ]; then
53
+ printf '\n# Custom entries from git.ignore in workspace configuration\n'
54
+ _added_custom_header=1
55
+ fi
56
+ printf '%s\n' "$_ci"
57
+ done
58
+ fi
59
+
60
+ printf '%s\n' "$GITIGNORE_MARKER_END"
61
+ }
62
+
63
+ # Reconcile workspace root .gitignore and .ignore:
64
+ # - Maintains a delimited (# REPO-NEXUS:START ... # REPO-NEXUS:END) section in .gitignore
65
+ # - Synchronizes the section with git.ignore from rnex.yaml and .local.rnex.yaml
66
+ # - Preserves user-authored .gitignore rules outside the managed section
67
+ # - Ensures .ignore un-ignores repos/* so ripgrep and AI tools can index member files
68
+ reconcile_gitignore() {
69
+ _dir="${1:-$NEXUS_DIR}"
70
+ _gi="$_dir/.gitignore"
71
+ _sec_tmp="$(mktemp)"
72
+ _build_rnex_gitignore_section > "$_sec_tmp"
73
+
74
+ if [ ! -f "$_gi" ]; then
75
+ mv "$_sec_tmp" "$_gi"
33
76
  log_ok "Created .gitignore (ignoring repos/, recursive .rnex directories, and $RNEX_LOCAL_CONFIG_FILE)"
34
77
  else
35
78
  # 1. Strip any legacy lint_trigger_counter entries if present
@@ -39,72 +82,192 @@ GI_EOF
39
82
  log_ok "Removed legacy lint trigger counter entries from .gitignore"
40
83
  fi
41
84
 
42
- # 2. Strip any legacy un-slashed .rnex or rnex rules that might match files
43
- if grep -qE '^[ ]*#?[ ]*Repo Nexus internal state' "$_gi" 2>/dev/null || grep -qE '^(\.rnex|rnex)$' "$_gi" 2>/dev/null; then
85
+ # 2. If delimited section exists, replace it in-place with the newly synced section
86
+ if grep -qFx -- "$GITIGNORE_MARKER_START" "$_gi" 2>/dev/null; then
44
87
  _tmp="$(mktemp)"
45
- grep -vE '(^[ ]*#?[ ]*Repo Nexus internal state|^(\.rnex|rnex)$)' "$_gi" > "$_tmp" && mv "$_tmp" "$_gi"
46
- fi
47
-
48
- # 3. Ensure repos/* is ignored
49
- if ! grep -q "^repos" "$_gi" 2>/dev/null; then
50
- printf '\n# Member repos (managed by rnex)\nrepos/*\n!repos/.gitkeep\n' >> "$_gi"
51
- log_dim "Added repos/* to .gitignore"
52
- fi
88
+ awk -v s="$GITIGNORE_MARKER_START" -v e="$GITIGNORE_MARKER_END" -v src="$_sec_tmp" '
89
+ $0 == s {
90
+ in_b = 1
91
+ while ((getline line < src) > 0) print line
92
+ close(src)
93
+ next
94
+ }
95
+ $0 == e {
96
+ in_b = 0
97
+ next
98
+ }
99
+ !in_b { print }
100
+ ' "$_gi" > "$_tmp" && mv "$_tmp" "$_gi"
101
+ rm -f "$_sec_tmp"
102
+ else
103
+ # 3. Upgrade legacy un-delimited .gitignore or append managed section to existing user .gitignore
104
+ _user_tmp="$(mktemp)"
105
+ awk -v local_cfg="$RNEX_LOCAL_CONFIG_FILE" '
106
+ /^[ ]*#[ ]*Member repos \(managed by rnex/ { next }
107
+ /^[ ]*#[ ]*Recursive Repo Nexus directories/ { next }
108
+ /^[ ]*#?[ ]*Repo Nexus internal state/ { next }
109
+ /^[ ]*#[ ]*Local machine-specific configuration/ { next }
110
+ /^[ ]*#[ ]*Custom entries from git\.ignore in workspace configuration/ { next }
111
+ $0 == "repos/*" || $0 == "!repos/.gitkeep" { next }
112
+ $0 == "**/.rnex*/" || $0 == "**/rnex*/" || $0 == ".rnex*/" || $0 == "rnex*/" { next }
113
+ $0 == ".rnex" || $0 == "rnex" { next }
114
+ $0 == local_cfg || $0 == ".rnex.local.yaml" { next }
115
+ { lines[++n] = $0 }
116
+ END {
117
+ start = 1
118
+ while (start <= n && lines[start] ~ /^[[:space:]]*$/) start++
119
+ end = n
120
+ while (end >= start && lines[end] ~ /^[[:space:]]*$/) end--
121
+ for (i = start; i <= end; i++) print lines[i]
122
+ }
123
+ ' "$_gi" > "$_user_tmp"
53
124
 
54
- # 4. Ensure recursive directory-only rnex rules are present
55
- _added_rnex_rules=0
56
- for _pat in "**/.rnex*/" "**/rnex*/" ".rnex*/" "rnex*/"; do
57
- if ! grep -qFx -- "$_pat" "$_gi" 2>/dev/null; then
58
- if [ "$_added_rnex_rules" -eq 0 ]; then
59
- printf '\n# Recursive Repo Nexus directories (trailing slashes prevent ignoring rnex binary or rnex.yaml)\n' >> "$_gi"
60
- _added_rnex_rules=1
61
- fi
62
- printf '%s\n' "$_pat" >> "$_gi"
125
+ _tmp="$(mktemp)"
126
+ if [ -s "$_user_tmp" ]; then
127
+ cat "$_user_tmp" > "$_tmp"
128
+ printf '\n' >> "$_tmp"
63
129
  fi
64
- done
65
-
66
- # 5. Ensure local config files are ignored
67
- if ! grep -qF "$RNEX_LOCAL_CONFIG_FILE" "$_gi" 2>/dev/null; then
68
- printf '\n# Local machine-specific configuration\n%s\n' "$RNEX_LOCAL_CONFIG_FILE" >> "$_gi"
69
- log_dim "Added $RNEX_LOCAL_CONFIG_FILE to .gitignore"
70
- fi
71
- if ! grep -qF ".rnex.local.yaml" "$_gi" 2>/dev/null; then
72
- printf '.rnex.local.yaml\n' >> "$_gi"
130
+ cat "$_sec_tmp" >> "$_tmp"
131
+ mv "$_tmp" "$_gi"
132
+ rm -f "$_user_tmp" "$_sec_tmp"
73
133
  fi
74
134
  fi
75
135
 
76
- # 6. Append any custom file and directory patterns from git.ignore
77
- _custom_ignores="$(yaml_get_git_ignore 2>/dev/null || true)"
78
- if [ -n "$_custom_ignores" ]; then
79
- _added_custom_header=0
80
- _old_ifs="$IFS"
81
- IFS='
82
- '
83
- for _ci in $_custom_ignores; do
84
- IFS="$_old_ifs"
85
- [ -n "$_ci" ] || continue
86
- if [ "$_ci" = "repos/" ] && grep -qFx -- "repos/*" "$_gi" 2>/dev/null; then
87
- continue
88
- fi
89
- if ! grep -qFx -- "$_ci" "$_gi" 2>/dev/null; then
90
- if [ "$_added_custom_header" -eq 0 ]; then
91
- printf '\n# Custom entries from git.ignore in workspace configuration\n' >> "$_gi"
92
- _added_custom_header=1
93
- fi
94
- printf '%s\n' "$_ci" >> "$_gi"
95
- fi
96
- done
97
- IFS="$_old_ifs"
98
- fi
99
-
100
136
  # Universal search tool ignore file (ripgrep, Claude Code, VS Code, Zed)
101
137
  ensure_provider_ignore "$_dir/.ignore" \
102
138
  "Member repos (un-ignored for ripgrep, Claude Code, VS Code, and search tools)" \
103
139
  "Created .ignore (un-ignoring member repos for search tools)"
104
140
  }
105
141
 
142
+ # Clean managed Repo Nexus ignore block from .gitignore, removing file if empty
143
+ clean_rnex_gitignore() {
144
+ _dir="${1:-$NEXUS_DIR}"
145
+ _gi="$_dir/.gitignore"
146
+ [ -f "$_gi" ] || return 0
147
+ if grep -qFx -- "${GITIGNORE_MARKER_START:-# REPO-NEXUS:START}" "$_gi" 2>/dev/null; then
148
+ _user_content="$(awk -v s="${GITIGNORE_MARKER_START:-# REPO-NEXUS:START}" -v e="${GITIGNORE_MARKER_END:-# REPO-NEXUS:END}" '
149
+ $0 == s { in_b = 1; next }
150
+ $0 == e { in_b = 0; next }
151
+ !in_b { print }
152
+ ' "$_gi" | sed -e '/^[[:space:]]*$/d' -e '/^[[:space:]]*#/d')"
153
+ if [ -z "$_user_content" ]; then
154
+ rm -f "$_gi"
155
+ [ "${RNEX_QUIET:-0}" -eq 0 ] && [ "${_sync_quiet:-0}" -eq 0 ] && log_ok "Removed .gitignore"
156
+ else
157
+ _tmp="$(mktemp)"
158
+ awk -v s="${GITIGNORE_MARKER_START:-# REPO-NEXUS:START}" -v e="${GITIGNORE_MARKER_END:-# REPO-NEXUS:END}" '
159
+ $0 == s { in_b = 1; next }
160
+ $0 == e { in_b = 0; next }
161
+ !in_b { print }
162
+ ' "$_gi" > "$_tmp" && mv "$_tmp" "$_gi"
163
+ [ "${RNEX_QUIET:-0}" -eq 0 ] && [ "${_sync_quiet:-0}" -eq 0 ] && log_ok "Removed Repo Nexus rules from .gitignore"
164
+ fi
165
+ fi
166
+ }
167
+
106
168
  # ---- AI Instructions Compilation (.rnex/ai-instructions.md) ----------------
107
169
 
170
+ # Resolve the primary instruction file for an AI provider plugin
171
+ plugin_get_instruction_file() {
172
+ _pif_name="$1"
173
+ _pif_pdir="${2:-}"
174
+ [ -n "$_pif_pdir" ] || _pif_pdir="$(find_plugin_dir "$_pif_name" 2>/dev/null || true)"
175
+ _pif_file=""
176
+ if [ -n "$_pif_pdir" ]; then
177
+ _pif_file="$(plugin_get_field "$_pif_pdir" "instruction_file" 2>/dev/null || true)"
178
+ fi
179
+ if [ -z "$_pif_file" ]; then
180
+ case "$_pif_name" in
181
+ claude) _pif_file="CLAUDE.md" ;;
182
+ gemini) _pif_file="GEMINI.md" ;;
183
+ cursor) _pif_file=".cursorrules" ;;
184
+ windsurf) _pif_file=".windsurfrules" ;;
185
+ codex) _pif_file="CODEX.md" ;;
186
+ copilot) _pif_file=".github/copilot-instructions.md" ;;
187
+ agents|agent) _pif_file="AGENTS.md" ;;
188
+ esac
189
+ fi
190
+ printf '%s' "$_pif_file"
191
+ }
192
+
193
+ # Enumerate primary instruction files for currently enabled AI provider plugins
194
+ get_active_provider_instruction_files() {
195
+ _gap_dir="${1:-$NEXUS_DIR}"
196
+ _gap_files=""
197
+ for _p in $(yaml_list_plugins 2>/dev/null || true); do
198
+ [ -n "$_p" ] || continue
199
+ yaml_plugin_enabled "$_p" 2>/dev/null || continue
200
+ _ifile="$(plugin_get_instruction_file "$_p" 2>/dev/null || true)"
201
+ if [ -n "$_ifile" ]; then
202
+ case " $_gap_files " in
203
+ *" $_ifile "*) ;;
204
+ *) _gap_files="${_gap_files:+$_gap_files }$_ifile" ;;
205
+ esac
206
+ fi
207
+ done
208
+ printf '%s' "$_gap_files"
209
+ }
210
+
211
+ # Check if an instruction file contains custom user instructions outside the rnex block
212
+ file_has_user_content() {
213
+ _fhuc_file="$1"
214
+ [ -f "$_fhuc_file" ] || return 1
215
+ _content=""
216
+ if grep -qF "${AI_MARKER_START:-<!-- REPO-NEXUS:START -->}" "$_fhuc_file" 2>/dev/null; then
217
+ _content="$(awk -v s="${AI_MARKER_START:-<!-- REPO-NEXUS:START -->}" -v e="${AI_MARKER_END:-<!-- REPO-NEXUS:END -->}" '
218
+ $0 == s { in_b = 1; next }
219
+ $0 == e { in_b = 0; next }
220
+ !in_b { print }
221
+ ' "$_fhuc_file" | sed -e '/^[[:space:]]*$/d')"
222
+ else
223
+ _content="$(grep -v "CRITICAL: You MUST read and strictly follow all instructions in \`.rnex/ai-instructions.md\`" "$_fhuc_file" 2>/dev/null | grep -v "Multi-Repo AI Workspace Context" | sed -e '/^[[:space:]]*$/d')"
224
+ fi
225
+ [ -n "$_content" ]
226
+ }
227
+
228
+ # Reconcile AI provider instruction files:
229
+ # - When provider plugins are enabled (e.g. Gemini, Claude), creates/updates their specific instruction files (GEMINI.md, CLAUDE.md)
230
+ # - Only creates AGENTS.md if it is the main instructions file for an enabled AI provider or no provider plugins are enabled
231
+ # - Cleans up instruction files that have no user content when their provider is disabled or inactive
232
+ reconcile_ai_instructions_files() {
233
+ _raif_dir="${1:-$NEXUS_DIR}"
234
+ _active_files="$(get_active_provider_instruction_files "$_raif_dir" 2>/dev/null || true)"
235
+
236
+ if [ -n "$_active_files" ]; then
237
+ # Active AI providers are configured with their own instruction files
238
+ for _af in $_active_files; do
239
+ merge_rnex_instructions "$_raif_dir/$_af"
240
+ done
241
+
242
+ # If AGENTS.md is not an active provider instruction file, clean rnex instructions from it
243
+ # (deleting it if solely rnex-managed, or stripping the rnex block if user content exists)
244
+ case " $_active_files " in
245
+ *" AGENTS.md "*) ;;
246
+ *)
247
+ if [ -f "$_raif_dir/AGENTS.md" ]; then
248
+ clean_rnex_instructions "$_raif_dir/AGENTS.md"
249
+ fi
250
+ ;;
251
+ esac
252
+ else
253
+ # No provider plugins enabled: AGENTS.md serves as the default main instructions file
254
+ merge_rnex_instructions "$_raif_dir/AGENTS.md"
255
+ fi
256
+
257
+ # For any existing instruction files on disk that are NOT currently active:
258
+ # Clean rnex instructions (removing file if empty outside block, or stripping rnex block)
259
+ for _if in CLAUDE.md GEMINI.md CODEX.md .cursorrules .windsurfrules .github/copilot-instructions.md; do
260
+ case " $_active_files " in
261
+ *" $_if "*) ;;
262
+ *)
263
+ if [ -f "$_raif_dir/$_if" ]; then
264
+ clean_rnex_instructions "$_raif_dir/$_if"
265
+ fi
266
+ ;;
267
+ esac
268
+ done
269
+ }
270
+
108
271
  # Compile the unified .rnex/ai-instructions.md file consumed by AI coding agents.
109
272
  # Aggregates sample template instructions and active plugin instruction sections.
110
273
  build_ai_instructions() {
@@ -299,21 +462,7 @@ get_active_provider_skill_dirs() {
299
462
  [ -n "$_p" ] || continue
300
463
  yaml_plugin_enabled "$_p" 2>/dev/null || continue
301
464
  if provider_option_enabled "$_p" "skills" 2>/dev/null; then
302
- _pdir="$(find_plugin_dir "$_p" 2>/dev/null || true)"
303
- _sdir=""
304
- if [ -n "$_pdir" ]; then
305
- _sdir="$(plugin_get_field "$_pdir" "skill_dir" 2>/dev/null || true)"
306
- fi
307
- if [ -z "$_sdir" ]; then
308
- case "$_p" in
309
- copilot) _sdir=".github/skills" ;;
310
- cursor) _sdir=".cursor/rules" ;;
311
- claude) _sdir=".claude/skills" ;;
312
- windsurf) _sdir=".windsurf/rules" ;;
313
- gemini) _sdir=".agents/skills" ;;
314
- codex) _sdir=".codex/skills" ;;
315
- esac
316
- fi
465
+ _sdir="$(plugin_get_skill_dir "$_p")"
317
466
  if [ -n "$_sdir" ]; then
318
467
  case " $_dirs " in
319
468
  *" $_sdir "*) ;;
@@ -194,10 +194,7 @@ YAML_EOF
194
194
  WORKSPACE_LOCAL_YAML="$_init_dir/$RNEX_LOCAL_CONFIG_FILE"
195
195
 
196
196
  # ---- 2. Merge routing instructions into AI instructions files ------------
197
- merge_rnex_instructions "$_init_dir/AGENTS.md"
198
- for _if in CLAUDE.md GEMINI.md CODEX.md .cursorrules .windsurfrules .github/copilot-instructions.md; do
199
- [ -f "$_init_dir/$_if" ] && merge_rnex_instructions "$_init_dir/$_if"
200
- done
197
+ reconcile_ai_instructions_files "$_init_dir"
201
198
 
202
199
  # ---- 3. Create repos directory with .gitkeep -----------------------------
203
200
  _repos_dir="$_init_dir/repos"
@@ -82,15 +82,7 @@ cmd_plugin_info() {
82
82
  [ -n "$_home" ] && printf '%bHomepage:%b %s\n' "$_BOLD" "$_NC" "$_home"
83
83
  printf '%bLocation:%b %s\n' "$_BOLD" "$_NC" "$_pdir"
84
84
 
85
- _skill_target="$(plugin_get_field "$_pdir" "skill_dir" 2>/dev/null || true)"
86
- [ -z "$_skill_target" ] && case "$_name" in
87
- copilot) _skill_target=".github/skills/" ;;
88
- claude) _skill_target=".claude/skills/" ;;
89
- gemini) _skill_target=".agents/skills/" ;;
90
- cursor) _skill_target=".cursor/rules/" ;;
91
- windsurf) _skill_target=".windsurf/rules/" ;;
92
- codex) _skill_target=".codex/skills/" ;;
93
- esac
85
+ _skill_target="$(plugin_get_skill_dir "$_name" "$_pdir")"
94
86
  case "$_skill_target" in
95
87
  */|"") ;;
96
88
  *) _skill_target="${_skill_target}/" ;;
@@ -4,21 +4,7 @@
4
4
  # lib/commands/status.sh — Workspace health and status diagnostics
5
5
  # ============================================================================
6
6
 
7
- # Query active plugins for any non-manual lint triggers (e.g. git hook triggers).
8
- _status_active_plugin_trigger() {
9
- for _p in $(yaml_list_plugins 2>/dev/null || true); do
10
- yaml_plugin_enabled "$_p" 2>/dev/null || continue
11
- _trig="$(yaml_plugin_get "$_p" "lint_trigger" 2>/dev/null || true)"
12
- [ -z "$_trig" ] && _trig="$(yaml_plugin_get "$_p" "trigger" 2>/dev/null || true)"
13
- if [ -n "$_trig" ] && [ "$_trig" != "manual" ]; then
14
- printf '%s' "$_trig"
15
- return 0
16
- fi
17
- done
18
- return 0
19
- }
20
-
21
- # Check if a directory contains any rnex-* user skills or skill symlinks.
7
+ # Check if a directory contains any rnex-* user skill pointers or skill bundles.
22
8
  _status_has_rnex_skills() {
23
9
  [ -d "$1" ] && { ls "$1"/rnex-*.md >/dev/null 2>&1 || ls -d "$1"/rnex-* >/dev/null 2>&1; }
24
10
  }
@@ -80,15 +66,31 @@ cmd_status() {
80
66
  fi
81
67
 
82
68
  _gi="$NEXUS_DIR/.gitignore"
83
- _gi_repos=0; _gi_local=0; _gi_has_counter=0; _gi_has_rnex=0
69
+ _gi_repos=0; _gi_local=0; _gi_has_counter=0; _gi_has_rnex=0; _gi_has_markers=0; _gi_custom_ok=1
84
70
  if [ -f "$_gi" ]; then
85
71
  grep -q '^repos' "$_gi" 2>/dev/null && _gi_repos=1
86
72
  grep -qF "$RNEX_LOCAL_CONFIG_FILE" "$_gi" 2>/dev/null && _gi_local=1
87
73
  grep -q "lint_trigger_counter" "$_gi" 2>/dev/null && _gi_has_counter=1
88
74
  grep -q '\*\*/\.rnex\*/' "$_gi" 2>/dev/null && _gi_has_rnex=1
75
+ if grep -qFx -- "${GITIGNORE_MARKER_START:-# REPO-NEXUS:START}" "$_gi" 2>/dev/null && \
76
+ grep -qFx -- "${GITIGNORE_MARKER_END:-# REPO-NEXUS:END}" "$_gi" 2>/dev/null; then
77
+ _gi_has_markers=1
78
+ _gi_exp_tmp="$(mktemp)"
79
+ _gi_act_tmp="$(mktemp)"
80
+ _build_rnex_gitignore_section > "$_gi_exp_tmp"
81
+ awk -v s="${GITIGNORE_MARKER_START:-# REPO-NEXUS:START}" -v e="${GITIGNORE_MARKER_END:-# REPO-NEXUS:END}" '
82
+ $0 == s { in_b = 1 }
83
+ in_b { print }
84
+ $0 == e { in_b = 0; exit }
85
+ ' "$_gi" > "$_gi_act_tmp"
86
+ if ! cmp -s "$_gi_exp_tmp" "$_gi_act_tmp"; then
87
+ _gi_custom_ok=0
88
+ fi
89
+ rm -f "$_gi_exp_tmp" "$_gi_act_tmp"
90
+ fi
89
91
  fi
90
92
 
91
- if [ "$_gi_repos" -eq 1 ] && [ "$_gi_local" -eq 1 ] && [ "$_gi_has_counter" -eq 0 ] && [ "$_gi_has_rnex" -eq 1 ]; then
93
+ if [ "$_gi_repos" -eq 1 ] && [ "$_gi_local" -eq 1 ] && [ "$_gi_has_counter" -eq 0 ] && [ "$_gi_has_rnex" -eq 1 ] && [ "$_gi_has_markers" -eq 1 ] && [ "$_gi_custom_ok" -eq 1 ]; then
92
94
  printf ' %b✓%b Git Ignore: repos/, %s, and **/.rnex*/ ignored in .gitignore\n' "$_G" "$_NC" "$RNEX_LOCAL_CONFIG_FILE"
93
95
  else
94
96
  printf ' %b!%b Git Ignore: recommend running "rnex sync" to update .gitignore (repos/, local configs)\n' "$_Y" "$_NC"
@@ -126,10 +128,11 @@ cmd_status() {
126
128
  else
127
129
  printf ' %b✗%b %s %b(compiled AI instructions — MISSING; run "rnex sync" to create)%b\n' "$_R" "$_NC" ".rnex/ai-instructions.md" "$_R" "$_NC"
128
130
  fi
131
+
132
+ _has_any_instructions=0
129
133
  if [ -f "$NEXUS_DIR/AGENTS.md" ]; then
130
134
  printf ' %b✓%b %s %b(standard instructions directive)%b\n' "$_G" "$_NC" "AGENTS.md" "$_DIM" "$_NC"
131
- else
132
- printf ' %b✗%b %s %b(standard instructions directive — MISSING; run "rnex sync" to create)%b\n' "$_R" "$_NC" "AGENTS.md" "$_R" "$_NC"
135
+ _has_any_instructions=1
133
136
  fi
134
137
  for _pair in \
135
138
  "CLAUDE.md:Claude" \
@@ -142,8 +145,15 @@ cmd_status() {
142
145
  _ilbl="${_pair#*:}"
143
146
  if [ -f "$NEXUS_DIR/$_ifile" ]; then
144
147
  printf ' %b✓%b %s %b(%s instructions)%b\n' "$_G" "$_NC" "$_ifile" "$_DIM" "$_ilbl" "$_NC"
148
+ _has_any_instructions=1
145
149
  fi
146
150
  done
151
+ if [ "$_has_any_instructions" -eq 0 ]; then
152
+ _active_ifiles=""
153
+ command -v get_active_provider_instruction_files >/dev/null 2>&1 && _active_ifiles="$(get_active_provider_instruction_files "$NEXUS_DIR" 2>/dev/null || true)"
154
+ _target_ifile="${_active_ifiles:-AGENTS.md}"
155
+ printf ' %b✗%b %s %b(instructions directive — MISSING; run "rnex sync" to create)%b\n' "$_R" "$_NC" "$_target_ifile" "$_R" "$_NC"
156
+ fi
147
157
  if [ -n "$CODE_WORKSPACE_FILE" ]; then
148
158
  if [ -f "$NEXUS_DIR/$CODE_WORKSPACE_FILE" ]; then
149
159
  printf ' %b✓%b %s %b(VS Code / Cursor workspace)%b\n' "$_G" "$_NC" "$CODE_WORKSPACE_FILE" "$_DIM" "$_NC"
@@ -84,10 +84,7 @@ cmd_sync() {
84
84
 
85
85
  # 1.2. Reconcile universal workspace instructions in AGENTS.md (permanent main context file)
86
86
  # and all existing permanent provider instructions files
87
- merge_rnex_instructions "$NEXUS_DIR/AGENTS.md"
88
- for _if in CLAUDE.md GEMINI.md CODEX.md .cursorrules .windsurfrules .github/copilot-instructions.md; do
89
- [ -f "$NEXUS_DIR/$_if" ] && merge_rnex_instructions "$NEXUS_DIR/$_if"
90
- done
87
+ reconcile_ai_instructions_files "$NEXUS_DIR"
91
88
 
92
89
  # 1.3. Generate auto-sync script (hardcoded harness)
93
90
  _write_auto_sync_script "$NEXUS_DIR"
package/lib/core.sh CHANGED
@@ -99,7 +99,7 @@ normalize_rel_path() {
99
99
  # ---- Version Resolution (package.json is single source of truth) ----------
100
100
 
101
101
  # Dynamically locate the nearest package.json and extract its "version" field.
102
- # Falls back to 0.5.1 if package.json cannot be read.
102
+ # Falls back to 0.8.7 if package.json cannot be read.
103
103
  resolve_version() {
104
104
  _sdir="$(dirname "$SCRIPT_LOCATION")"
105
105
  for _cand in "$SCRIPT_LOCATION/package.json" "$_sdir/package.json" "$_sdir/../package.json"; do
@@ -111,7 +111,7 @@ resolve_version() {
111
111
  fi
112
112
  fi
113
113
  done
114
- printf '0.5.1'
114
+ printf '0.8.7'
115
115
  }
116
116
 
117
117
  RNEX_VERSION="$(resolve_version)"
package/lib/hooks.sh CHANGED
@@ -306,10 +306,7 @@ install_git_hooks() {
306
306
  esac
307
307
  [ -d "$_git_dir" ] || return 0
308
308
  _hooks_dir="$_git_dir/hooks"
309
- mkdir -p "$_hooks_dir"
310
- for _h in $RNEX_MANAGED_GIT_HOOKS; do
311
- _create_hook_script "$_hooks_dir/$_h" "$_h"
312
- done
309
+ _install_managed_hook_scripts "$_hooks_dir"
313
310
  # Clean up legacy .rnex/hooks if core.hooksPath was previously set to it
314
311
  _hp="$(git -C "$_ws_dir" config --get core.hooksPath 2>/dev/null || true)"
315
312
  if [ "$_hp" = ".rnex/hooks" ] || [ "$_hp" = "$_ws_dir/.rnex/hooks" ]; then
package/lib/plugins.sh CHANGED
@@ -72,6 +72,28 @@ plugin_get_field() {
72
72
  ' "$_pdir/plugin.yaml"
73
73
  }
74
74
 
75
+ # Resolve target user skill directory for a provider plugin from plugin.yaml (with fallback).
76
+ plugin_get_skill_dir() {
77
+ _pgs_name="$1"
78
+ _pgs_pdir="${2:-}"
79
+ [ -n "$_pgs_pdir" ] || _pgs_pdir="$(find_plugin_dir "$_pgs_name" 2>/dev/null || true)"
80
+ _pgs_dir=""
81
+ if [ -n "$_pgs_pdir" ]; then
82
+ _pgs_dir="$(plugin_get_field "$_pgs_pdir" "skill_dir" 2>/dev/null || true)"
83
+ fi
84
+ if [ -z "$_pgs_dir" ]; then
85
+ case "$_pgs_name" in
86
+ copilot) _pgs_dir=".github/skills" ;;
87
+ cursor) _pgs_dir=".cursor/rules" ;;
88
+ claude) _pgs_dir=".claude/skills" ;;
89
+ windsurf) _pgs_dir=".windsurf/rules" ;;
90
+ gemini) _pgs_dir=".agents/skills" ;;
91
+ codex) _pgs_dir=".codex/skills" ;;
92
+ esac
93
+ fi
94
+ printf '%s' "$_pgs_dir"
95
+ }
96
+
75
97
  # Parse declared template source-to-destination mappings from plugin.yaml.
76
98
  plugin_list_templates() {
77
99
  _pdir="$1"
@@ -108,6 +130,9 @@ plugin_resolve_placeholders() {
108
130
  _p="$1"; _pd="$2"; _str="$3"
109
131
  _res="$(printf '%s' "$_str" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"")"
110
132
 
133
+ # Normalize legacy static prefixes into placeholders prior to expansion
134
+ _res="$(printf '%s' "$_res" | sed -e 's|^\.\{0,1\}\/\{0,1\}wiki/|{wiki_dir}/|' -e 's|^\.\{0,1\}\/\{0,1\}raw/|{raw_dir}/|')"
135
+
111
136
  if [ -f "$_pd/plugin.yaml" ]; then
112
137
  _keys="$(awk '
113
138
  /^config_defaults:[ ]*$/ || /^config_defaults:[ ]*#/ { in_c=1; next }
@@ -140,22 +165,23 @@ plugin_resolve_placeholders() {
140
165
  _v="$(normalize_rel_path "$_v")"
141
166
  ;;
142
167
  esac
143
- _res="$(printf '%s' "$_res" | sed -e "s|{$_k}|$_v|g")"
168
+ [ -n "$_v" ] && _res="$(printf '%s' "$_res" | sed -e "s|{$_k}|$_v|g")"
144
169
  done
145
170
  fi
146
171
 
147
- # Fallback resolution for core wiki/raw placeholders and legacy prefixes
172
+ # Fallback resolution for core wiki/raw placeholders
148
173
  _w_val="$(yaml_plugin_get "$_p" "wiki_dir" 2>/dev/null || true)"
149
174
  [ -n "$_w_val" ] || _w_val="wiki"
150
175
  _w_val="$(normalize_rel_path "$_w_val")"
176
+ [ -n "$_w_val" ] || _w_val="wiki"
151
177
 
152
178
  # {raw_dir} (legacy alias) resolves to the first explicit directory source; when no
153
179
  # directory source exists it stays unresolved so callers can skip the destination.
154
180
  _r_val="$(plugin_primary_source_dir "$_p" 2>/dev/null || true)"
155
181
 
156
- _res="$(printf '%s' "$_res" | sed -e "s|{wiki_dir}|${_w_val}|g" -e "s|^\.\{0,1\}\/\{0,1\}wiki/|${_w_val}/|")"
182
+ _res="$(printf '%s' "$_res" | sed -e "s|{wiki_dir}|${_w_val}|g")"
157
183
  if [ -n "$_r_val" ]; then
158
- _res="$(printf '%s' "$_res" | sed -e "s|{raw_dir}|${_r_val}|g" -e "s|^\.\{0,1\}\/\{0,1\}raw/|${_r_val}/|")"
184
+ _res="$(printf '%s' "$_res" | sed -e "s|{raw_dir}|${_r_val}|g")"
159
185
  fi
160
186
 
161
187
  _res="$(printf '%s' "$_res" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##')"
@@ -168,7 +194,8 @@ plugin_instantiate_template() {
168
194
 
169
195
  _w_val="$(yaml_plugin_get "$_p" "wiki_dir" 2>/dev/null || true)"
170
196
  [ -z "$_w_val" ] && _w_val="wiki"
171
- _w_val="$(printf '%s' "$_w_val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^/##' -e 's#/$##')"
197
+ _w_val="$(normalize_rel_path "$_w_val")"
198
+ [ -z "$_w_val" ] && _w_val="wiki"
172
199
 
173
200
  _r_val="$(plugin_primary_source_dir "$_p" 2>/dev/null || true)"
174
201
  [ -z "$_r_val" ] && _r_val="docs"
@@ -214,8 +241,10 @@ sync_plugin_bundle() {
214
241
  # Clean up obsolete leaked root ./raw and ./wiki directories if non-default paths are configured
215
242
  _clean_leaked_plugin_dirs() {
216
243
  _cld_root="${1:-$NEXUS_DIR}"
217
- _cld_raw="$2"
218
- _cld_wiki="$3"
244
+ _cld_raw="$(normalize_rel_path "$2")"
245
+ _cld_wiki="$(normalize_rel_path "$3")"
246
+ [ -n "$_cld_raw" ] || _cld_raw="docs"
247
+ [ -n "$_cld_wiki" ] || _cld_wiki="wiki"
219
248
 
220
249
  if [ "$_cld_raw" != "raw" ] && [ -d "$_cld_root/raw" ] && \
221
250
  ! plugin_source_dirs "karpathy-llm" 2>/dev/null | grep -qx 'raw'; then
package/lib/repos.sh CHANGED
@@ -63,11 +63,6 @@ repo_readonly() {
63
63
  yaml_repo_readonly "$1"
64
64
  }
65
65
 
66
- # Backwards-compatibility alias for checking if member .rnex exists
67
- repo_rnex_dir_enabled() {
68
- repo_has_rnex_dir "$1"
69
- }
70
-
71
66
  # Cleanly tear down .rnex in a member repository.
72
67
  # Removes auto-generated plugins and boilerplate README, pruning empty directories
73
68
  # while preserving any user-customized files.