@jenga-ai/agent 3.5.0 → 4.0.0

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 (136) hide show
  1. package/README.md +85 -78
  2. package/agents/developer.md +1 -1
  3. package/agents/scrum-master.md +20 -2
  4. package/agents/tester.md +3 -3
  5. package/hooks/on_session_end.sh +5 -5
  6. package/lib/generate-agent-context.js +2 -2
  7. package/lib/generate-copilot-hooks.js +1 -1
  8. package/lib/generate-skill-allow-list.js +79 -8
  9. package/lib/mirror.js +1 -1
  10. package/lib/postinstall-manifest.js +1 -1
  11. package/lib/skill-allow-list.json +2 -2
  12. package/mcp/help/index.js +8 -17
  13. package/mcp/help/scan.js +73 -0
  14. package/package.json +5 -1
  15. package/project/app/api/lib/resolve-project-root.js +1 -1
  16. package/project/app/api/parsers/knowledge-graph.js +100 -9
  17. package/project/app/api/routes/health.js +36 -0
  18. package/project/app/api/scripts/capture-snapshot.js +9 -6
  19. package/project/app/package.json +4 -0
  20. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  21. package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
  22. package/project/app/ui/dist/index.html +2 -2
  23. package/project/app/ui/package.json +4 -0
  24. package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
  25. package/scripts/acquire-concurrency-slot.sh +35 -5
  26. package/scripts/apply-j-prefix.sh +46 -5
  27. package/scripts/build-pages-site.sh +1 -1
  28. package/scripts/check-public-playbook-steps.sh +158 -52
  29. package/scripts/check-publicignore-match.sh +2 -2
  30. package/scripts/compute-deploy-reconcile.sh +5 -5
  31. package/scripts/delete-bare-skill-dirs.sh +329 -0
  32. package/scripts/generate-legacy-shipped-paths.js +2 -2
  33. package/scripts/idea_manager.sh +273 -3
  34. package/scripts/mark-deployed.sh +2 -2
  35. package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
  36. package/scripts/populate-knowledge-graph.js +213 -5
  37. package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
  38. package/scripts/postinstall.js +1 -1
  39. package/scripts/release-concurrency-slot.sh +34 -4
  40. package/scripts/render-ranked-list.sh +270 -0
  41. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  42. package/scripts/repoint-skill-refs.sh +539 -0
  43. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  44. package/scripts/strip-polyfill-frontmatter.py +166 -0
  45. package/scripts/todo_manager.sh +16 -1
  46. package/scripts/validate-typed-object.sh +750 -0
  47. package/scripts/verify-legacy-seed-reconcile.sh +10 -10
  48. package/scripts/verify-postinstall-reconcile.sh +7 -7
  49. package/scripts/write-context-digest.sh +1 -1
  50. package/skills/j-brainstorm/SKILL.md +3 -4
  51. package/skills/j-btw/SKILL.md +3 -4
  52. package/skills/j-clearify/SKILL.md +5 -6
  53. package/skills/j-close-story/SKILL.md +11 -12
  54. package/skills/j-close-story/scripts/check-privatized.sh +4 -4
  55. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  56. package/skills/j-commit/SKILL.md +3 -4
  57. package/skills/j-continue/SKILL.md +5 -6
  58. package/skills/j-deep-dive/SKILL.md +3 -4
  59. package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
  60. package/skills/j-distribute/SKILL.md +3 -4
  61. package/skills/j-do/SKILL.md +100 -18
  62. package/skills/j-doc/SKILL.md +3 -4
  63. package/skills/j-doc-sync/SKILL.md +4 -4
  64. package/skills/j-dooo/SKILL.md +6 -15
  65. package/skills/j-error/SKILL.md +3 -4
  66. package/skills/j-evaluate/SKILL.md +3 -4
  67. package/skills/j-examplify/SKILL.md +3 -4
  68. package/skills/j-gitignore/SKILL.md +157 -0
  69. package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
  70. package/skills/j-gitignore/scripts/_catalog.sh +105 -0
  71. package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
  72. package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
  73. package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
  74. package/skills/j-help/SKILL.md +3 -4
  75. package/skills/j-idea/SKILL.md +80 -9
  76. package/skills/j-idea/assets/idea_template.md +1 -1
  77. package/skills/j-improve/SKILL.md +4 -5
  78. package/skills/j-init/SKILL.md +23 -14
  79. package/skills/j-init/assets/scope-thresholds_template.json +5 -2
  80. package/skills/j-init/scripts/apply-scaffold-visibility.sh +9 -7
  81. package/skills/j-init/scripts/init.sh +4 -4
  82. package/skills/j-jbp/SKILL.md +3 -4
  83. package/skills/j-lgtm/SKILL.md +3 -4
  84. package/skills/j-pi-plan/SKILL.md +3 -4
  85. package/skills/j-playbook/SKILL.md +1 -1
  86. package/skills/j-proceed/SKILL.md +3 -4
  87. package/skills/j-publish/SKILL.md +4 -5
  88. package/skills/j-publish/adapters/npm-ci.md +6 -1
  89. package/skills/j-publish/adapters/npm.md +1 -1
  90. package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
  91. package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
  92. package/skills/j-publish/scripts/run_gates.sh +1 -1
  93. package/skills/j-reconcile/SKILL.md +40 -7
  94. package/skills/j-reconcile/assets/report_format.md +11 -0
  95. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +13 -13
  96. package/skills/j-reconcile-origin/SKILL.md +3 -4
  97. package/skills/j-redo/SKILL.md +4 -5
  98. package/skills/j-skillify/SKILL.md +3 -4
  99. package/skills/j-spinoff/SKILL.md +4 -5
  100. package/skills/j-status/SKILL.md +18 -4
  101. package/skills/j-todo/SKILL.md +44 -5
  102. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  103. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  104. package/skills/j-uncharted/SKILL.md +252 -13
  105. package/skills/j-uncharted/scripts/detect-dependencies.sh +80 -22
  106. package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
  107. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  108. package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
  109. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  110. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  111. package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
  112. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  113. package/skills/j-wtf/SKILL.md +4 -5
  114. package/skills/jenga/SKILL.md +106 -11
  115. package/skills/jenga/playbooks/board-hygiene.json +32 -0
  116. package/skills/jenga/playbooks/schema.json +73 -6
  117. package/skills/jenga/playbooks/understand-then-commit.json +19 -0
  118. package/skills/jenga/scripts/load-nl-catalog.js +5 -2
  119. package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
  120. package/skills/jenga/scripts/load-playbooks.sh +290 -5
  121. package/skills/jenga/scripts/match-playbook.sh +4 -4
  122. package/skills/jenga/scripts/run-playbook-step.sh +267 -1
  123. package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
  124. package/templates/SKILL_TEMPLATE.md +12 -0
  125. package/templates/permission-levels/level-1-locked.json +1 -1
  126. package/templates/permission-levels/level-2-guarded.json +1 -1
  127. package/templates/permission-levels/level-3-standard.json +1 -1
  128. package/templates/permission-levels/level-4-elevated.json +2 -2
  129. package/templates/permission-levels/level-5-unrestricted.json +2 -2
  130. package/templates/playbook-types.json +40 -6
  131. package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
  132. package/project/app/ui/dist/assets/index-CdK3Qrep.css +0 -1
  133. package/scripts/audit-twin-divergence.sh +0 -625
  134. package/scripts/generate-j-alias.sh +0 -333
  135. package/skills/j-dev-done/SKILL.md +0 -53
  136. package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/repoint-skill-refs.sh — repoint bare skills/<name>/ path references to their
3
+ # canonical skills/j-<name>/ twin (E50_S11).
4
+ #
5
+ # Purpose
6
+ # -------
7
+ # `skills/j-<name>/` was promoted to the sole canonical skill directory by the E50
8
+ # reopening of 2026-09-09; the old bare `skills/<name>/` directories are deleted later
9
+ # in the sequence (E50_S15). This script performs the ordering-safety half of that
10
+ # cutover: rewriting every functional (non-prose) reference of the form
11
+ # `skills/<bare-name>/...` to `skills/j-<bare-name>/...` WHILE both directories still
12
+ # exist. The twin is already byte-identical to its bare source, so applying this script
13
+ # is a behaviour-neutral no-op at runtime — it only changes which (already-working)
14
+ # path a reference resolves through, in preparation for the bare directory's later
15
+ # deletion.
16
+ #
17
+ # Per CLAUDE.md's "Skill Implementation Principle — Scripts Over Inline Logic", this is
18
+ # a deterministic, re-runnable, idempotent script — not inline agent judgment applied
19
+ # file-by-file.
20
+ #
21
+ # Modes
22
+ # -----
23
+ # --dry-run Report every candidate file, line number, and the before/after string.
24
+ # Nothing is written.
25
+ # --apply Perform the rewrite in place.
26
+ #
27
+ # Exactly one of --dry-run/--apply is required. Either mode always runs the
28
+ # bare-vs-twin delta check FIRST (see below) before touching or reporting on any file.
29
+ #
30
+ # Usage
31
+ # -----
32
+ # scripts/repoint-skill-refs.sh (--dry-run|--apply) [PATH...]
33
+ #
34
+ # PATH may be a file or a directory (searched recursively); it is resolved relative to
35
+ # the caller's cwd if relative, or used as-is if absolute. If no PATH is given, the
36
+ # script defaults to this repo's whole canonical source scope: skills/, agents/,
37
+ # hooks/, scripts/, lib/, and .github/agents/ — the exact set of "canonical, hand-edited
38
+ # source" locations named by CLAUDE.md's Source-of-Truth rule and this story's Context.
39
+ #
40
+ # Examples
41
+ # --------
42
+ # scripts/repoint-skill-refs.sh --dry-run skills/j-do
43
+ # scripts/repoint-skill-refs.sh --apply agents/developer.md .github/agents/developer.md
44
+ # scripts/repoint-skill-refs.sh --apply # full default canonical scope
45
+ #
46
+ # Exception list — single named source
47
+ # --------------------------------------
48
+ # REPOINT_SKILL_REFS_EXCEPTIONS below is the ONLY place this script's exception list is
49
+ # defined; every other check in this script calls is_exception() rather than
50
+ # re-listing the names. These three directories keep their bare names permanently
51
+ # (E50_S06/E50's "three permanent exceptions") and are never rewritten to a `j-` form,
52
+ # and never required to have a `j-` twin:
53
+ # - skills/jenga/ (root orchestrator command)
54
+ # - skills/jenga-permission-level/ (root orchestrator command)
55
+ # - skills/index/ (not a skill — no SKILL.md, not part of routing)
56
+ #
57
+ # Double-prefix safety
58
+ # ---------------------
59
+ # The rewrite rule's source pattern is always `skills/<bare-name>/`, where <bare-name>
60
+ # is drawn from the LIVE list of non-"j-"-prefixed directory names under skills/ (see
61
+ # the delta check below), minus the exceptions above. Because every eligible name is
62
+ # itself bare (never starts with "j-") and the pattern requires the literal trailing
63
+ # "/" delimiter, the string `skills/<bare-name>/` can never occur as a substring of
64
+ # `skills/j-<bare-name>/` or of any other twin path — right after "skills/" a twin path
65
+ # always reads "j-", never the bare name directly. Double-prefixing
66
+ # (`skills/j-j-<name>/`) is therefore structurally impossible by construction, not
67
+ # merely avoided by a runtime guard.
68
+ #
69
+ # Bare-vs-twin delta check
70
+ # --------------------------
71
+ # Computed fresh from disk on every invocation (never a hardcoded list): every
72
+ # directory directly under skills/ is classified as either a twin (name starts with
73
+ # "j-") or bare (does not). Every bare, non-excepted directory MUST have a
74
+ # `skills/j-<name>/` twin on disk; if not, that directory is UNPAIRED. The script
75
+ # prints the bare count, the twin count, and every unpaired directory's name, then —
76
+ # if any unpaired directory exists — exits non-zero (naming the directory) BEFORE
77
+ # performing or reporting any rewrite. This does not require twin_count == bare_count:
78
+ # a twin-only skill authored after the E50 reopening (e.g. skills/j-playbook/ before it
79
+ # gained a jenga/scripts/load-playbooks.sh reference, or skills/j-cloud-connect/, which
80
+ # never had a bare form at all) is expected and not flagged — only a BARE directory
81
+ # lacking its twin is an error condition.
82
+ #
83
+ # Path excludes — single named source
84
+ # --------------------------------------
85
+ # PATH_EXCLUDE_PATTERNS below is the only place file/directory exclusions are defined.
86
+ # Always excluded, regardless of what PATHs are passed on the command line:
87
+ # - .git/, node_modules/ (VCS / dependency internals)
88
+ # - .claude/, .agents/ (generated mirrors — CLAUDE.md: never edit directly)
89
+ # - docs/ (prose/docs rewrite is E50_S17's scope, not this story's)
90
+ # - .publicignore (owned by E50_S16)
91
+ # - lib/legacy-shipped-paths.json (deliberately retained bare-path list — see E50_S16)
92
+ #
93
+ # Idempotency
94
+ # -----------
95
+ # Because the rewrite eliminates every occurrence of `skills/<bare-name>/` (bare) that
96
+ # it can see, and never introduces a new one (only ever `skills/j-<bare-name>/`), a
97
+ # second --apply run over the same tree finds nothing left to change and reports zero
98
+ # files rewritten.
99
+
100
+ set -euo pipefail
101
+
102
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
103
+ # shellcheck source=lib/resolve-project-dir.sh
104
+ source "$SCRIPT_DIR/../lib/resolve-project-dir.sh"
105
+
106
+ PROJECT_DIR="$JENGA_PROJECT_DIR"
107
+ SKILLS_DIR="$PROJECT_DIR/skills"
108
+
109
+ # ---- Single named source: directory exception list ----
110
+ REPOINT_SKILL_REFS_EXCEPTIONS=(jenga jenga-permission-level index)
111
+
112
+ is_exception() {
113
+ local name="$1" ex
114
+ for ex in "${REPOINT_SKILL_REFS_EXCEPTIONS[@]}"; do
115
+ if [ "$name" = "$ex" ]; then
116
+ return 0
117
+ fi
118
+ done
119
+ return 1
120
+ }
121
+
122
+ # ---- Single named source: path excludes ----
123
+ # Each entry is a pattern matched against the path relative to $PROJECT_DIR (POSIX
124
+ # separators). A path is excluded if it equals, or is nested under, any entry.
125
+ PATH_EXCLUDE_PATTERNS=(
126
+ ".git"
127
+ "node_modules"
128
+ ".claude"
129
+ ".agents"
130
+ "docs"
131
+ ".publicignore"
132
+ "lib/legacy-shipped-paths.json"
133
+ # This script's own source. Its header comments quote real, deliberately-unrewritten
134
+ # bare-path content from other files as illustrative examples (e.g. "skills/wtf/"
135
+ # inside a sentence explaining why skills/j-wtf/SKILL.md's own self-reference is
136
+ # left alone) and one historical bug description (the original, broken
137
+ # `mkdir -p ".../skills/do"` line this script's own fix commit corrected). Running
138
+ # the sweep against itself would rewrite those intentional quotes, making them
139
+ # inaccurate descriptions of the content/history they document. Not a "the tool
140
+ # can't sweep its own kind" restriction — every other *.sh file is swept normally.
141
+ "scripts/repoint-skill-refs.sh"
142
+ )
143
+
144
+ is_path_excluded() {
145
+ local rel="$1" pattern
146
+ for pattern in "${PATH_EXCLUDE_PATTERNS[@]}"; do
147
+ if [ "$rel" = "$pattern" ] || [[ "$rel" == "$pattern"/* ]]; then
148
+ return 0
149
+ fi
150
+ done
151
+ return 1
152
+ }
153
+
154
+ # ---- Self-reference guard ----
155
+ # A file living under skills/j-<X>/... never has its OWN "skills/<X>/" (same name)
156
+ # occurrences rewritten, even though every other eligible name is still rewritten
157
+ # normally in that same file. This mirrors this story's own scope boundary
158
+ # (E50_S11_T02's title: "repoint CROSS-skill bare references inside skills/j-*/
159
+ # twins") — self-referential mentions of a twin's own bare source are out of scope
160
+ # for two concrete, evidenced reasons, not a hypothetical:
161
+ #
162
+ # 1. `scripts/generate-j-alias.sh` step 2 already rewrites self-referential
163
+ # `skills/<X>/` occurrences to `skills/j-<X>/` for ordinary body content at
164
+ # generation time — a REMAINING bare self-reference in a `skills/j-<X>/` file is
165
+ # therefore either (a) inside the generator's own OWN appended note (L294-304),
166
+ # whose bare self-mentions are inserted AFTER that rewrite pass and so were never
167
+ # caught by it, and which is accurate AS WRITTEN (describes real generation
168
+ # provenance) — see e.g. `skills/j-wtf/SKILL.md`'s "This skill is a
169
+ # literal-directory-name duplicate of `skills/wtf/`" / "generated/synced by
170
+ # ... from `skills/wtf/SKILL.md`" — or (b) deliberate, hand-written prose a later
171
+ # task substituted in place of that note, e.g. `skills/j-do/SKILL.md`'s
172
+ # "this file was previously generated from `skills/do/SKILL.md` ... `skills/do/`
173
+ # is the copy awaiting deletion by E50_S15" and `skills/j-dev-done/SKILL.md`'s
174
+ # equivalent — both intentionally keep referencing the BARE directory (as a
175
+ # concept: its history, or its pending-deletion fate), and rewriting either
176
+ # mention corrupts the sentence (turns "generated from X" into "generated from
177
+ # itself", or turns "X is the copy awaiting deletion" into a false claim about
178
+ # the twin itself).
179
+ # 2. `skills/j-init/` is the one hand-maintained (non-generator) pair predating this
180
+ # mechanism entirely — `scripts/generate-j-alias.sh` explicitly refuses to touch
181
+ # it. Its own prose ("Keep this file's instructions in lockstep with
182
+ # `skills/init/SKILL.md`") and its `scripts/apply-scaffold-visibility.sh` ("Keep
183
+ # this file's logic in lockstep with `skills/init/`'s copy") are legitimate,
184
+ # current, hand-authored statements about a live sibling relationship between two
185
+ # directories that both still exist and are both still hand-edited — rewriting
186
+ # either turns "in lockstep with skills/init/" into "in lockstep with itself".
187
+ #
188
+ # In every case found, retiring/rewriting this content (once the generator itself is
189
+ # retired/inverted, or once E50_S15 deletes the bare directories these notes describe)
190
+ # is E50_S14's scope ("Retire or Invert the Twin-Generation Scripts"), not this
191
+ # story's — E50_S11 rewrites CROSS-skill path strings only.
192
+ file_self_reference_name() {
193
+ local file="$1" rel
194
+ rel="${file#"$PROJECT_DIR"/}"
195
+ case "$rel" in
196
+ skills/j-*)
197
+ rel="${rel#skills/j-}"
198
+ echo "${rel%%/*}"
199
+ ;;
200
+ esac
201
+ }
202
+
203
+ usage() {
204
+ cat >&2 <<'EOF'
205
+ Usage: repoint-skill-refs.sh (--dry-run|--apply) [PATH...]
206
+
207
+ --dry-run Report every candidate file/line/before/after. Writes nothing.
208
+ --apply Perform the rewrite in place.
209
+
210
+ PATH may be a file or directory; repeatable. Defaults to this repo's canonical
211
+ source scope (skills/, agents/, hooks/, scripts/, lib/, .github/agents/) when
212
+ no PATH is given.
213
+ EOF
214
+ }
215
+
216
+ MODE=""
217
+ TARGET_ARGS=()
218
+
219
+ while [ "$#" -gt 0 ]; do
220
+ case "$1" in
221
+ --dry-run)
222
+ MODE="dry-run"
223
+ shift
224
+ ;;
225
+ --apply)
226
+ MODE="apply"
227
+ shift
228
+ ;;
229
+ -h|--help)
230
+ usage
231
+ exit 0
232
+ ;;
233
+ --)
234
+ shift
235
+ while [ "$#" -gt 0 ]; do
236
+ TARGET_ARGS+=("$1")
237
+ shift
238
+ done
239
+ ;;
240
+ -*)
241
+ echo "repoint-skill-refs.sh: error: unknown flag '$1'" >&2
242
+ usage
243
+ exit 1
244
+ ;;
245
+ *)
246
+ TARGET_ARGS+=("$1")
247
+ shift
248
+ ;;
249
+ esac
250
+ done
251
+
252
+ if [ -z "$MODE" ]; then
253
+ echo "repoint-skill-refs.sh: error: exactly one of --dry-run or --apply is required" >&2
254
+ usage
255
+ exit 1
256
+ fi
257
+
258
+ # ============================================================================
259
+ # 1. Live bare-vs-twin delta check — ALWAYS runs first, before any file I/O.
260
+ # ============================================================================
261
+ BARE_DIRS=()
262
+ TWIN_DIRS=()
263
+ UNPAIRED=()
264
+
265
+ if [ -d "$SKILLS_DIR" ]; then
266
+ for d in "$SKILLS_DIR"/*/; do
267
+ [ -d "$d" ] || continue
268
+ name="$(basename "$d")"
269
+ case "$name" in
270
+ j-*)
271
+ TWIN_DIRS+=("$name")
272
+ continue
273
+ ;;
274
+ esac
275
+ BARE_DIRS+=("$name")
276
+ if is_exception "$name"; then
277
+ continue
278
+ fi
279
+ if [ ! -d "$SKILLS_DIR/j-$name" ]; then
280
+ UNPAIRED+=("$name")
281
+ fi
282
+ done
283
+ fi
284
+
285
+ echo "== repoint-skill-refs.sh: bare-vs-twin delta =="
286
+ echo " bare directories (skills/<name>/, incl. exceptions): ${#BARE_DIRS[@]}"
287
+ echo " twin directories (skills/j-<name>/): ${#TWIN_DIRS[@]}"
288
+ if [ "${#UNPAIRED[@]}" -gt 0 ]; then
289
+ echo " UNPAIRED bare directories (no twin, not on the exception list):"
290
+ for u in "${UNPAIRED[@]}"; do
291
+ echo " - $u"
292
+ done
293
+ echo "repoint-skill-refs.sh: error: the directory(ies) named above have no skills/j-<name>/ twin and are not on the exception list. Disposition each one (generate the missing twin, add it to REPOINT_SKILL_REFS_EXCEPTIONS, or record another explicit decision) before running this script." >&2
294
+ exit 1
295
+ fi
296
+ echo " delta check clean — every bare directory is either excepted or paired with a twin."
297
+ echo
298
+
299
+ # ============================================================================
300
+ # 2. Determine the rewrite name set: every bare directory minus exceptions.
301
+ # ============================================================================
302
+ REWRITE_NAMES=()
303
+ for name in "${BARE_DIRS[@]}"; do
304
+ if ! is_exception "$name"; then
305
+ REWRITE_NAMES+=("$name")
306
+ fi
307
+ done
308
+
309
+ # ============================================================================
310
+ # 3. Resolve targets -> a flat file list (excluding PATH_EXCLUDE_PATTERNS and
311
+ # binary files).
312
+ # ============================================================================
313
+ if [ "${#TARGET_ARGS[@]}" -eq 0 ]; then
314
+ DEFAULT_SCOPE=(skills agents hooks scripts lib .github/agents)
315
+ for d in "${DEFAULT_SCOPE[@]}"; do
316
+ if [ -e "$PROJECT_DIR/$d" ]; then
317
+ TARGET_ARGS+=("$PROJECT_DIR/$d")
318
+ fi
319
+ done
320
+ fi
321
+
322
+ FILES=()
323
+ for target in "${TARGET_ARGS[@]}"; do
324
+ case "$target" in
325
+ /*) abs_target="$target" ;;
326
+ *) abs_target="$PWD/$target" ;;
327
+ esac
328
+ if [ ! -e "$abs_target" ]; then
329
+ echo "repoint-skill-refs.sh: error: target '$target' does not exist" >&2
330
+ exit 1
331
+ fi
332
+ if [ -f "$abs_target" ]; then
333
+ candidates=("$abs_target")
334
+ else
335
+ while IFS= read -r -d '' f; do
336
+ candidates+=("$f")
337
+ done < <(find "$abs_target" -type f -print0)
338
+ fi
339
+ for f in "${candidates[@]-}"; do
340
+ [ -n "${f:-}" ] || continue
341
+ rel="${f#"$PROJECT_DIR"/}"
342
+ if is_path_excluded "$rel"; then
343
+ continue
344
+ fi
345
+ # Skip binary files.
346
+ if ! grep -Iq . "$f" 2>/dev/null; then
347
+ continue
348
+ fi
349
+ FILES+=("$f")
350
+ done
351
+ candidates=()
352
+ done
353
+
354
+ # Deduplicate FILES (a file could be reachable via more than one target arg).
355
+ # Avoid `mapfile` (a bash 4+ builtin) — this repo must stay compatible with macOS's
356
+ # stock /bin/bash 3.2 (see PROJECT_SUMMARY.md's E47_S04 note on this exact class of
357
+ # portability defect), so dedup via a plain while-read loop instead.
358
+ if [ "${#FILES[@]}" -gt 0 ]; then
359
+ DEDUPED_FILES=()
360
+ while IFS= read -r f; do
361
+ [ -n "$f" ] || continue
362
+ DEDUPED_FILES+=("$f")
363
+ done < <(printf '%s\n' "${FILES[@]}" | sort -u)
364
+ FILES=("${DEDUPED_FILES[@]}")
365
+ fi
366
+
367
+ # ============================================================================
368
+ # 4. Scan (and, in --apply mode, rewrite) every file for every eligible name.
369
+ #
370
+ # Line-at-a-time (not a whole-file sed pass), against a per-file EFFECTIVE name list
371
+ # that excludes the file's own self-reference name (see file_self_reference_name()
372
+ # above) — so a skills/j-<X>/... file never has its own "skills/<X>/" mentions
373
+ # rewritten, while every other eligible cross-skill name is still rewritten normally
374
+ # in that same file.
375
+ #
376
+ # Boundary rule (not just a trailing slash): a match requires `skills/<name>`
377
+ # followed by either end-of-line or a character that is NOT [A-Za-z0-9_-] — i.e. not
378
+ # something that could continue the same identifier. A bare directory REFERENCE
379
+ # almost always has a trailing "/" (`skills/do/SKILL.md`), but a small, real set of
380
+ # prose/code mentions the bare name with no trailing slash at all — e.g.
381
+ # `mkdir -p "$dir/skills/do" "$dir/agents"` (found in
382
+ # scripts/verify-legacy-seed-reconcile.sh: this exact line, unrewritten, left the
383
+ # fixture creating a "do" directory while a nearby line wrote into "j-do/SKILL.md",
384
+ # breaking the rehearsal), or a comment like "Consumed by skills/do (Step 0)". A
385
+ # trailing-slash-only pattern would silently miss both. The boundary class still
386
+ # blocks a match from firing on a mere PREFIX of a longer name (`do` inside `doc`,
387
+ # `reconcile` inside `reconcile-origin`) because the very next character in those
388
+ # cases (`c`, `-`) is itself in the blocked class.
389
+ # ============================================================================
390
+ CHANGED_FILES=()
391
+ REPORT_COUNT=0
392
+ SKIPPED_COUNT=0
393
+
394
+ # Extended-regex boundary suffix shared by every match/replace in this section: end
395
+ # of line, or any character that is not part of a bare skill-directory-name token.
396
+ BOUNDARY_SUFFIX='([^A-Za-z0-9_-]|$)'
397
+
398
+ join_by_pipe() {
399
+ local IFS='|'
400
+ echo "$*"
401
+ }
402
+
403
+ for f in "${FILES[@]-}"; do
404
+ [ -n "${f:-}" ] || continue
405
+
406
+ self_name="$(file_self_reference_name "$f")"
407
+
408
+ # Effective rewrite name list for THIS file: every eligible name except its own
409
+ # self-reference name (if any).
410
+ FILE_REWRITE_NAMES=()
411
+ for name in "${REWRITE_NAMES[@]-}"; do
412
+ [ -n "${name:-}" ] || continue
413
+ if [ -n "$self_name" ] && [ "$name" = "$self_name" ]; then
414
+ continue
415
+ fi
416
+ FILE_REWRITE_NAMES+=("$name")
417
+ done
418
+
419
+ self_ref_regex=""
420
+ if [ -n "$self_name" ]; then
421
+ self_ref_regex="skills/${self_name}${BOUNDARY_SUFFIX}"
422
+ fi
423
+
424
+ # Cheap short-circuit: does this file contain any EFFECTIVE candidate at all? A
425
+ # plain substring pre-check ("skills/" anywhere) gates the more expensive
426
+ # boundary-regex grep below, since the overwhelming majority of files never
427
+ # mention "skills/" at all.
428
+ has_candidate=0
429
+ if [ "${#FILE_REWRITE_NAMES[@]}" -gt 0 ] && grep -qF -- "skills/" "$f" 2>/dev/null; then
430
+ file_rewrite_alt="$(join_by_pipe "${FILE_REWRITE_NAMES[@]}")"
431
+ if grep -Eq -- "skills/(${file_rewrite_alt})${BOUNDARY_SUFFIX}" "$f" 2>/dev/null; then
432
+ has_candidate=1
433
+ fi
434
+ fi
435
+
436
+ if [ "$has_candidate" -eq 0 ]; then
437
+ # Still report the self-reference skip, if this file has one, so the report stays
438
+ # transparent about why a visible "skills/<self_name>" mention was left alone.
439
+ if [ -n "$self_name" ] && grep -Eq -- "$self_ref_regex" "$f" 2>/dev/null; then
440
+ while IFS=: read -r lineno linetext; do
441
+ [ -n "$lineno" ] || continue
442
+ echo "$f:$lineno: SKIP (self-reference to skills/${self_name} — E50_S14 scope): $linetext"
443
+ SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
444
+ done < <(grep -En -- "$self_ref_regex" "$f" 2>/dev/null)
445
+ fi
446
+ continue
447
+ fi
448
+
449
+ file_changed=0
450
+ tmp_file=""
451
+ if [ "$MODE" = "apply" ]; then
452
+ tmp_file="$(mktemp "${f}.repoint.XXXXXX")"
453
+ # `cp -p` before truncating: mktemp's own file is created with fresh default
454
+ # permissions (typically 600), which would silently drop an executable bit (or
455
+ # any other mode/timestamp) when moved over the original. Copying the original
456
+ # onto the temp path first inherits its mode; the later truncate-and-rewrite
457
+ # (via `>`/`>>` redirection below) only replaces content, never permissions.
458
+ cp -p "$f" "$tmp_file"
459
+ : > "$tmp_file"
460
+ fi
461
+
462
+ lineno=0
463
+ while IFS= read -r linetext || [ -n "$linetext" ]; do
464
+ lineno=$((lineno + 1))
465
+ newtext="$linetext"
466
+
467
+ case "$linetext" in
468
+ *"skills/"*)
469
+ # Per-line effective name list: drop any name whose TWIN form already
470
+ # appears on this same line. Handles the "named pair" sentence shape —
471
+ # e.g. scripts/generate-j-alias.sh: "the skills/init/ <-> skills/j-init/
472
+ # pair" — where the bare mention and the twin mention are deliberately
473
+ # both present, describing the pair itself. Rewriting the bare half there
474
+ # would turn it into "skills/j-init/ <-> skills/j-init/", a duplicate that
475
+ # erases the sentence's actual point. This is a per-LINE check (not
476
+ # per-file, unlike the self-reference guard above) because a pair sentence
477
+ # like this can appear anywhere, including files with no self-reference
478
+ # name at all.
479
+ line_names=()
480
+ for name in "${FILE_REWRITE_NAMES[@]-}"; do
481
+ [ -n "${name:-}" ] || continue
482
+ case "$linetext" in
483
+ *"skills/j-${name}"*) continue ;;
484
+ esac
485
+ line_names+=("$name")
486
+ done
487
+ if [ "${#line_names[@]}" -gt 0 ]; then
488
+ line_rewrite_alt="$(join_by_pipe "${line_names[@]}")"
489
+ # sed -E (POSIX extended regex, portable across GNU and BSD sed) does the
490
+ # actual boundary-preserving substitution: \1 is the matched name, \2 is
491
+ # the boundary character (or empty at end-of-line), both re-emitted after
492
+ # the "j-" prefix so only the name itself changes.
493
+ newtext="$(printf '%s' "$linetext" | sed -E "s#skills/(${line_rewrite_alt})${BOUNDARY_SUFFIX}#skills/j-\\1\\2#g")"
494
+ fi
495
+ ;;
496
+ esac
497
+
498
+ if [ -n "$self_name" ] && [[ "$linetext" =~ $self_ref_regex ]]; then
499
+ echo "$f:$lineno: SKIP (self-reference to skills/${self_name} — E50_S14 scope): $linetext"
500
+ SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
501
+ fi
502
+
503
+ if [ "$newtext" != "$linetext" ]; then
504
+ echo "$f:$lineno: $linetext"
505
+ echo "$f:$lineno: -> $newtext"
506
+ REPORT_COUNT=$((REPORT_COUNT + 1))
507
+ file_changed=1
508
+ fi
509
+
510
+ if [ "$MODE" = "apply" ]; then
511
+ printf '%s\n' "$newtext" >> "$tmp_file"
512
+ fi
513
+ done < "$f"
514
+
515
+ if [ "$MODE" = "apply" ]; then
516
+ if [ "$file_changed" -eq 1 ]; then
517
+ mv "$tmp_file" "$f"
518
+ else
519
+ rm -f "$tmp_file"
520
+ fi
521
+ fi
522
+
523
+ if [ "$file_changed" -eq 1 ]; then
524
+ CHANGED_FILES+=("$f")
525
+ fi
526
+ done
527
+
528
+ echo
529
+ if [ "$MODE" = "dry-run" ]; then
530
+ echo "== repoint-skill-refs.sh: dry-run complete — $REPORT_COUNT line(s) would change, $SKIPPED_COUNT line(s) skipped (self-reference) =="
531
+ else
532
+ echo "== repoint-skill-refs.sh: apply complete — ${#CHANGED_FILES[@]} file(s) changed, $REPORT_COUNT line(s) rewritten, $SKIPPED_COUNT line(s) skipped (self-reference) =="
533
+ for cf in "${CHANGED_FILES[@]-}"; do
534
+ [ -n "${cf:-}" ] || continue
535
+ echo " - ${cf#"$PROJECT_DIR"/}"
536
+ done
537
+ fi
538
+
539
+ exit 0