@jenga-ai/agent 3.5.0 → 3.6.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 (94) 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 +37 -3
  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 +6 -1
  15. package/project/app/api/parsers/knowledge-graph.js +100 -9
  16. package/project/app/api/routes/health.js +36 -0
  17. package/project/app/api/scripts/capture-snapshot.js +9 -6
  18. package/project/app/ui/dist/assets/{index-CdK3Qrep.css → index-BVR_7Owg.css} +1 -1
  19. package/project/app/ui/dist/assets/index-CtU2xLQm.js +104 -0
  20. package/project/app/ui/dist/index.html +2 -2
  21. package/project/app/ui/package.json +4 -0
  22. package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
  23. package/scripts/acquire-concurrency-slot.sh +1 -1
  24. package/scripts/apply-j-prefix.sh +46 -5
  25. package/scripts/audit-twin-divergence.sh +73 -5
  26. package/scripts/build-pages-site.sh +1 -1
  27. package/scripts/check-public-playbook-steps.sh +158 -52
  28. package/scripts/check-publicignore-match.sh +2 -2
  29. package/scripts/compute-deploy-reconcile.sh +5 -5
  30. package/scripts/delete-bare-skill-dirs.sh +330 -0
  31. package/scripts/generate-legacy-shipped-paths.js +2 -2
  32. package/scripts/idea_manager.sh +258 -3
  33. package/scripts/mark-deployed.sh +2 -2
  34. package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
  35. package/scripts/populate-knowledge-graph.js +213 -5
  36. package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
  37. package/scripts/postinstall.js +1 -1
  38. package/scripts/repoint-skill-refs.sh +539 -0
  39. package/scripts/todo_manager.sh +1 -1
  40. package/scripts/verify-legacy-seed-reconcile.sh +10 -10
  41. package/scripts/verify-postinstall-reconcile.sh +7 -7
  42. package/scripts/write-context-digest.sh +1 -1
  43. package/skills/j-clearify/SKILL.md +2 -2
  44. package/skills/j-close-story/scripts/check-privatized.sh +4 -4
  45. package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
  46. package/skills/j-do/SKILL.md +101 -17
  47. package/skills/j-doc-sync/SKILL.md +1 -0
  48. package/skills/j-gitignore/SKILL.md +157 -0
  49. package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
  50. package/skills/j-gitignore/scripts/_catalog.sh +105 -0
  51. package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
  52. package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
  53. package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
  54. package/skills/j-idea/SKILL.md +78 -6
  55. package/skills/j-idea/assets/idea_template.md +1 -1
  56. package/skills/j-improve/SKILL.md +1 -1
  57. package/skills/j-init/SKILL.md +3 -3
  58. package/skills/j-init/assets/scope-thresholds_template.json +5 -2
  59. package/skills/j-init/scripts/apply-scaffold-visibility.sh +1 -1
  60. package/skills/j-init/scripts/init.sh +4 -4
  61. package/skills/j-playbook/SKILL.md +1 -1
  62. package/skills/j-publish/SKILL.md +1 -1
  63. package/skills/j-publish/adapters/npm-ci.md +6 -1
  64. package/skills/j-publish/adapters/npm.md +1 -1
  65. package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
  66. package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
  67. package/skills/j-reconcile/SKILL.md +2 -2
  68. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +11 -11
  69. package/skills/j-redo/SKILL.md +1 -1
  70. package/skills/j-spinoff/SKILL.md +1 -1
  71. package/skills/j-status/SKILL.md +15 -0
  72. package/skills/j-todo/SKILL.md +3 -1
  73. package/skills/j-uncharted/SKILL.md +1 -1
  74. package/skills/j-uncharted/scripts/detect-dependencies.sh +1 -1
  75. package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
  76. package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
  77. package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
  78. package/skills/j-wtf/SKILL.md +1 -1
  79. package/skills/jenga/SKILL.md +43 -9
  80. package/skills/jenga/playbooks/board-hygiene.json +32 -0
  81. package/skills/jenga/playbooks/schema.json +73 -6
  82. package/skills/jenga/playbooks/understand-then-commit.json +19 -0
  83. package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
  84. package/skills/jenga/scripts/load-playbooks.sh +1 -1
  85. package/skills/jenga/scripts/match-playbook.sh +1 -1
  86. package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
  87. package/templates/SKILL_TEMPLATE.md +12 -0
  88. package/templates/permission-levels/level-4-elevated.json +1 -1
  89. package/templates/permission-levels/level-5-unrestricted.json +1 -1
  90. package/templates/playbook-types.json +34 -6
  91. package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
  92. package/scripts/generate-j-alias.sh +0 -333
  93. package/skills/j-dev-done/SKILL.md +0 -53
  94. 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
@@ -3,7 +3,7 @@
3
3
  # Run from the repository root.
4
4
 
5
5
  TODO_FILE="project/todo.md"
6
- TEMPLATE="skills/todo/assets/todo_template.md"
6
+ TEMPLATE="skills/j-todo/assets/todo_template.md"
7
7
 
8
8
  usage() {
9
9
  cat >&2 <<EOF
@@ -83,8 +83,8 @@ make_pkg() {
83
83
  cat > "$dir/package.json" <<'JSON'
84
84
  { "name": "@jenga-ai/agent", "version": "3.0.0", "type": "module" }
85
85
  JSON
86
- mkdir -p "$dir/skills/do" "$dir/agents"
87
- printf '# do skill (still shipped)\n' > "$dir/skills/do/SKILL.md"
86
+ mkdir -p "$dir/skills/j-do" "$dir/agents"
87
+ printf '# do skill (still shipped)\n' > "$dir/skills/j-do/SKILL.md"
88
88
  printf '# developer agent (still shipped)\n' > "$dir/agents/developer.md"
89
89
  }
90
90
 
@@ -116,7 +116,7 @@ PKG="$FIXTURE/pkg"
116
116
  make_pkg "$PKG"
117
117
  write_legacy_paths "$PKG" \
118
118
  "agents/developer.md" \
119
- "skills/do/SKILL.md" \
119
+ "skills/j-do/SKILL.md" \
120
120
  "skills/j-legacy-twin/SKILL.md" \
121
121
  "../OUTSIDE-THE-FIXTURE.txt" \
122
122
  "skills/trap/LINK.md"
@@ -146,7 +146,7 @@ assert_dir "$CONSUMER_A/.agents/skills/j-legacy-twin" \
146
146
  "directory survives because it still holds a consumer file (.agents)"
147
147
  assert_grep "seeded from known-shipped legacy paths" "$FIXTURE/a-install.log" \
148
148
  "install log attributes the cleanup to legacy-path seeding"
149
- assert_file "$CONSUMER_A/.agents/skills/do/SKILL.md" \
149
+ assert_file "$CONSUMER_A/.agents/skills/j-do/SKILL.md" \
150
150
  "currently-shipped skill untouched"
151
151
  assert_file "$CONSUMER_A/.agents/agents/developer.md" \
152
152
  "currently-shipped agent file untouched"
@@ -158,7 +158,7 @@ import json, sys
158
158
  paths = json.load(open(sys.argv[1]))["paths"]
159
159
  assert "skills/j-legacy-twin/SKILL.md" not in paths, "deleted orphan must not be in the fresh manifest"
160
160
  assert "skills/j-legacy-twin/my-notes.md" not in paths, "consumer file must never be in the manifest"
161
- assert "skills/do/SKILL.md" in paths and "agents/developer.md" in paths, "current package paths must be present"
161
+ assert "skills/j-do/SKILL.md" in paths and "agents/developer.md" in paths, "current package paths must be present"
162
162
  PYEOF
163
163
  if [ $? -eq 0 ]; then pass "fresh manifest describes only what is actually on disk post-cleanup"; else fail "fresh manifest is inaccurate"; fi
164
164
 
@@ -174,13 +174,13 @@ assert_nogrep_re "[1-9][0-9]* stale file" "$FIXTURE/b-install.log" \
174
174
  "first-ever install deletes nothing despite a non-empty legacy path list"
175
175
  assert_grep "no previous install manifest" "$FIXTURE/b-install.log" \
176
176
  "first-ever install reports plain additive-only, not a seeded outcome"
177
- assert_file "$CONSUMER_B/.agents/skills/do/SKILL.md" \
177
+ assert_file "$CONSUMER_B/.agents/skills/j-do/SKILL.md" \
178
178
  "first-ever install still copies current package files"
179
179
 
180
180
  # ═══ Scenario C — a legacy path still currently shipped is never touched ═════
181
- # (Implicitly exercised by Scenario A's "do"/"developer.md" already surviving, but
181
+ # (Implicitly exercised by Scenario A's "j-do"/"developer.md" already surviving, but
182
182
  # asserted here as its own named check for clarity.)
183
- assert_file "$CONSUMER_A/.claude/skills/do/SKILL.md" \
183
+ assert_file "$CONSUMER_A/.claude/skills/j-do/SKILL.md" \
184
184
  "a legacy-listed path that is STILL shipped survives untouched (not stale)"
185
185
 
186
186
  # ═══ Scenario D — a legacy path absent from disk is never phantom-seeded ═════
@@ -219,7 +219,7 @@ PKG_F1="$FIXTURE/pkg-f1"
219
219
  make_pkg "$PKG_F1"
220
220
  mkdir -p "$PKG_F1/skills/renamed-old"
221
221
  printf '# renamed-old — removed in the next version\n' > "$PKG_F1/skills/renamed-old/SKILL.md"
222
- write_legacy_paths "$PKG_F1" "agents/developer.md" "skills/do/SKILL.md"
222
+ write_legacy_paths "$PKG_F1" "agents/developer.md" "skills/j-do/SKILL.md"
223
223
 
224
224
  PKG_F2="$FIXTURE/pkg-f2"
225
225
  make_pkg "$PKG_F2"
@@ -228,7 +228,7 @@ import json
228
228
  p = '$PKG_F2/package.json'
229
229
  d = json.load(open(p)); d['version'] = '3.0.1'; json.dump(d, open(p, 'w'))
230
230
  "
231
- write_legacy_paths "$PKG_F2" "agents/developer.md" "skills/do/SKILL.md" "skills/renamed-old/SKILL.md"
231
+ write_legacy_paths "$PKG_F2" "agents/developer.md" "skills/j-do/SKILL.md" "skills/renamed-old/SKILL.md"
232
232
 
233
233
  CONSUMER_F="$FIXTURE/consumer-f"
234
234
  mkdir -p "$CONSUMER_F"
@@ -90,7 +90,7 @@ echo " fixture: $FIXTURE"
90
90
  # ── package v1.0.0 ───────────────────────────────────────────────────────────
91
91
  PKG1="$FIXTURE/pkg-v1"
92
92
  make_pkg "$PKG1" "1.0.0"
93
- add_skill "$PKG1" "do" "# do — stable across both versions"
93
+ add_skill "$PKG1" "j-do" "# j-do — stable across both versions"
94
94
  add_skill "$PKG1" "renamed-old" "# renamed-old — becomes renamed-new in v2"
95
95
  add_skill "$PKG1" "j-legacy-twin" "# j-legacy-twin — excluded in v2 (E50_S06 style)"
96
96
  printf '# developer agent v1\n' > "$PKG1/agents/developer.md"
@@ -98,7 +98,7 @@ printf '# developer agent v1\n' > "$PKG1/agents/developer.md"
98
98
  # ── package v2.0.0 ───────────────────────────────────────────────────────────
99
99
  PKG2="$FIXTURE/pkg-v2"
100
100
  make_pkg "$PKG2" "2.0.0"
101
- add_skill "$PKG2" "do" "# do — stable across both versions" # byte-identical
101
+ add_skill "$PKG2" "j-do" "# j-do — stable across both versions" # byte-identical
102
102
  add_skill "$PKG2" "renamed-new" "# renamed-new — replaces renamed-old"
103
103
  # j-legacy-twin intentionally absent (excluded), renamed-old intentionally absent
104
104
  printf '# developer agent v2 (changed)\n' > "$PKG2/agents/developer.md"
@@ -136,7 +136,7 @@ assert_file "$CONSUMER/.claude/.jenga-postinstall-manifest.json" \
136
136
  "manifest written to .claude on first install"
137
137
  assert_nogrep "preexisting/JUNK.md" "$CONSUMER/.agents/.jenga-postinstall-manifest.json" \
138
138
  "consumer file is NOT recorded in the manifest"
139
- assert_grep "skills/do/SKILL.md" "$CONSUMER/.agents/.jenga-postinstall-manifest.json" \
139
+ assert_grep "skills/j-do/SKILL.md" "$CONSUMER/.agents/.jenga-postinstall-manifest.json" \
140
140
  "manifest records mirrored package paths"
141
141
 
142
142
  # ═══ Scenario B — upgrade with renames / removals / exclusions ════════════════
@@ -153,7 +153,7 @@ done
153
153
 
154
154
  # Record identity of a file common to both versions, to prove it is not needlessly
155
155
  # deleted and recopied (mirror() should classify it as `skipped`).
156
- DO_BEFORE="$(ls -li "$CONSUMER/.agents/skills/do/SKILL.md" | awk '{print $1, $6, $7, $8}')"
156
+ DO_BEFORE="$(ls -li "$CONSUMER/.agents/skills/j-do/SKILL.md" | awk '{print $1, $6, $7, $8}')"
157
157
 
158
158
  run_install "$PKG2" "$CONSUMER" "$FIXTURE/install-v2.log"
159
159
 
@@ -196,7 +196,7 @@ assert_file "$CONSUMER/.claude/skills/renamed-new/SKILL.md" \
196
196
  "renamed-to skill installed (.claude)"
197
197
 
198
198
  # 5. Unchanged common file was neither deleted nor recopied.
199
- DO_AFTER="$(ls -li "$CONSUMER/.agents/skills/do/SKILL.md" | awk '{print $1, $6, $7, $8}')"
199
+ DO_AFTER="$(ls -li "$CONSUMER/.agents/skills/j-do/SKILL.md" | awk '{print $1, $6, $7, $8}')"
200
200
  if [ "$DO_BEFORE" = "$DO_AFTER" ]; then
201
201
  pass "file common to both versions untouched (same inode+mtime)"
202
202
  else
@@ -375,9 +375,9 @@ mkdir -p "$CONSUMER_M"
375
375
  run_install "$PKG1" "$CONSUMER_M" "$FIXTURE/missing-v1.log"
376
376
  run_install "$PKG2M" "$CONSUMER_M" "$FIXTURE/missing-v2.log"
377
377
 
378
- assert_file "$CONSUMER_M/.agents/skills/do/SKILL.md" \
378
+ assert_file "$CONSUMER_M/.agents/skills/j-do/SKILL.md" \
379
379
  "missing copySet entry does not wipe the mirrored subtree (.agents)"
380
- assert_file "$CONSUMER_M/.claude/skills/do/SKILL.md" \
380
+ assert_file "$CONSUMER_M/.claude/skills/j-do/SKILL.md" \
381
381
  "missing copySet entry does not wipe the mirrored subtree (.claude)"
382
382
  assert_grep "Upgrade cleanup skipped" "$FIXTURE/missing-v2.log" \
383
383
  "packaging regression reported, cleanup skipped"