@jenga-ai/agent 2.0.0 → 3.1.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 (177) hide show
  1. package/README.md +82 -243
  2. package/agents/developer.md +5 -5
  3. package/agents/scrum-master.md +23 -23
  4. package/agents/tester.md +5 -5
  5. package/bin/jenga.js +10 -0
  6. package/hooks/copilot_session_end.sh +7 -3
  7. package/hooks/prompt_router_helper.js +17 -5
  8. package/lib/commands/doctor.js +351 -0
  9. package/lib/commands/init.js +16 -0
  10. package/lib/generate-copilot-hooks.js +116 -0
  11. package/lib/generate-skill-allow-list.js +9 -3
  12. package/lib/legacy-shipped-paths.json +336 -0
  13. package/lib/postinstall-manifest.js +469 -0
  14. package/lib/skill-allow-list.json +2 -3
  15. package/package.json +16 -25
  16. package/scripts/apply-j-prefix.sh +25 -12
  17. package/scripts/generate-j-alias.sh +333 -0
  18. package/scripts/generate-legacy-shipped-paths.js +248 -0
  19. package/scripts/postinstall.js +205 -2
  20. package/scripts/verify-legacy-seed-reconcile.sh +254 -0
  21. package/scripts/verify-postinstall-reconcile.sh +392 -0
  22. package/skills/{brainstorm → j-brainstorm}/SKILL.md +9 -2
  23. package/skills/{btw → j-btw}/SKILL.md +9 -2
  24. package/skills/{clearify → j-clearify}/SKILL.md +9 -2
  25. package/skills/{close-story → j-close-story}/SKILL.md +17 -10
  26. package/skills/{close-story → j-close-story}/scripts/check-privatized.sh +2 -2
  27. package/skills/{close-story → j-close-story}/scripts/check-story-closeable.sh +1 -1
  28. package/skills/{close-story → j-close-story}/scripts/extract-task-diff-stats.sh +1 -1
  29. package/skills/{commit → j-commit}/SKILL.md +9 -2
  30. package/skills/j-continue/SKILL.md +36 -0
  31. package/skills/{deep-dive → j-deep-dive}/SKILL.md +9 -8
  32. package/skills/{dev-done → j-dev-done}/SKILL.md +11 -4
  33. package/skills/{dev-done → j-dev-done}/scripts/classify-commit-outcome.sh +4 -4
  34. package/skills/{distribute → j-distribute}/SKILL.md +17 -10
  35. package/skills/{distribute → j-distribute}/scripts/distribute-changes.sh +1 -1
  36. package/skills/{do → j-do}/SKILL.md +12 -5
  37. package/skills/{doc → j-doc}/README.md +5 -5
  38. package/skills/{doc → j-doc}/SKILL.md +15 -8
  39. package/skills/{doc → j-doc}/authoring-notes.md +1 -1
  40. package/skills/{doc-sync → j-doc-sync}/SKILL.md +9 -2
  41. package/skills/{dooo → j-dooo}/SKILL.md +9 -2
  42. package/skills/j-error/SKILL.md +36 -0
  43. package/skills/{evaluate → j-evaluate}/SKILL.md +9 -2
  44. package/skills/j-examplify/SKILL.md +49 -0
  45. package/skills/{help → j-help}/SKILL.md +9 -2
  46. package/skills/{idea → j-idea}/SKILL.md +10 -3
  47. package/skills/{idea → j-idea}/assets/idea_handoff_template.md +1 -1
  48. package/skills/{improve → j-improve}/SKILL.md +9 -2
  49. package/skills/j-init/SKILL.md +2 -2
  50. package/skills/j-jbp/SKILL.md +32 -0
  51. package/skills/j-lgtm/SKILL.md +28 -0
  52. package/skills/{pi-plan → j-pi-plan}/SKILL.md +10 -3
  53. package/skills/{proceed → j-proceed}/SKILL.md +9 -2
  54. package/skills/{publish → j-publish}/SKILL.md +47 -40
  55. package/skills/{publish → j-publish}/adapters/droplet.md +1 -1
  56. package/skills/{publish → j-publish}/adapters/mobile-ios.md +3 -3
  57. package/skills/{publish → j-publish}/adapters/npm-ci.md +3 -3
  58. package/skills/{publish → j-publish}/adapters/npm.md +8 -8
  59. package/skills/{publish → j-publish}/assets/ci-contract.md +2 -2
  60. package/skills/{publish → j-publish}/schemas/publish.schema.json +1 -1
  61. package/skills/{publish → j-publish}/scripts/npm_stage_inspect.sh +34 -1
  62. package/skills/{publish → j-publish}/scripts/npm_stage_pipeline.sh +9 -4
  63. package/skills/{publish → j-publish}/scripts/publish_deploy.sh +4 -4
  64. package/skills/{publish → j-publish}/scripts/validate_npm_stage_env.sh +1 -1
  65. package/skills/{publish → j-publish}/wizards/droplet.md +1 -1
  66. package/skills/{publish → j-publish}/wizards/mobile-ios.md +1 -1
  67. package/skills/{publish → j-publish}/wizards/npm-ci.md +1 -1
  68. package/skills/{publish → j-publish}/wizards/npm.md +1 -1
  69. package/skills/{reconcile → j-reconcile}/SKILL.md +12 -5
  70. package/skills/{reconcile → j-reconcile}/scripts/detect-unlinked-code.sh +2 -2
  71. package/skills/{reconcile → j-reconcile}/scripts/resolve-reconcile-scope.sh +3 -3
  72. package/skills/{reconcile-origin → j-reconcile-origin}/SKILL.md +13 -6
  73. package/skills/{redo → j-redo}/SKILL.md +9 -2
  74. package/skills/{skillify → j-skillify}/SKILL.md +10 -3
  75. package/skills/{spinoff → j-spinoff}/SKILL.md +9 -2
  76. package/skills/{status → j-status}/SKILL.md +9 -2
  77. package/skills/{todo → j-todo}/SKILL.md +10 -3
  78. package/skills/{todo → j-todo}/assets/todo_handoff_template.md +1 -1
  79. package/skills/{todo → j-todo}/scripts/add_trivial_task.sh +3 -3
  80. package/skills/{todo → j-todo}/scripts/update_story_tasks.py +2 -2
  81. package/skills/{uncharted → j-uncharted}/SKILL.md +35 -28
  82. package/skills/{uncharted → j-uncharted}/assets/UNDERSTANDING_DOC_TEMPLATE.md +2 -2
  83. package/skills/{uncharted → j-uncharted}/scripts/detect-dependencies.sh +1 -1
  84. package/skills/{uncharted → j-uncharted}/scripts/detect-tests.sh +1 -1
  85. package/skills/{uncharted → j-uncharted}/scripts/directory-triage.sh +3 -3
  86. package/skills/{uncharted → j-uncharted}/scripts/elicitation-state.sh +3 -3
  87. package/skills/{uncharted → j-uncharted}/scripts/enumerate-target.sh +1 -1
  88. package/skills/{uncharted → j-uncharted}/scripts/import-source.sh +1 -1
  89. package/skills/{uncharted → j-uncharted}/scripts/inspect-provenance.sh +1 -1
  90. package/skills/{uncharted → j-uncharted}/scripts/resolve-segment-target.sh +5 -5
  91. package/skills/{uncharted → j-uncharted}/scripts/run-engine.sh +1 -1
  92. package/skills/{uncharted → j-uncharted}/scripts/validate-proposed-items.sh +2 -2
  93. package/skills/{uncharted → j-uncharted}/scripts/write-backfilled-epics.sh +1 -1
  94. package/skills/j-wtf/SKILL.md +27 -0
  95. package/skills/jenga/SKILL.md +1 -1
  96. package/skills/jenga-permission-level/SKILL.md +1 -1
  97. package/templates/SCRUM_BOARD_SCHEMA.md +1 -1
  98. package/templates/agent-context.md.tpl +10 -10
  99. package/templates/copilot-instructions.md.tpl +89 -19
  100. package/skills/continue/SKILL.md +0 -29
  101. package/skills/error/SKILL.md +0 -29
  102. package/skills/examplify/SKILL.md +0 -42
  103. package/skills/init/SKILL.md +0 -155
  104. package/skills/init/assets/scope-thresholds_template.json +0 -7
  105. package/skills/init/assets/strategy_stub_template.md +0 -38
  106. package/skills/init/assets/workflow_template.json +0 -30
  107. package/skills/init/scripts/apply-project-visibility.sh +0 -176
  108. package/skills/init/scripts/detect-existing-codebase.sh +0 -166
  109. package/skills/init/scripts/init.sh +0 -116
  110. package/skills/jbp/SKILL.md +0 -25
  111. package/skills/lgtm/SKILL.md +0 -21
  112. package/skills/skillify/assets/init-new/assets/.gitignore_template +0 -15
  113. package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +0 -13
  114. package/skills/skillify/assets/init-new/assets/directory_structure.txt +0 -14
  115. package/skills/skillify/assets/init-new/assets/test-config_template.json +0 -4
  116. package/skills/wtf/SKILL.md +0 -20
  117. /package/skills/{close-story → j-close-story}/scripts/compute-scope-divergence.sh +0 -0
  118. /package/skills/{close-story → j-close-story}/scripts/extract-diff-stats.sh +0 -0
  119. /package/skills/{close-story → j-close-story}/scripts/update-task-frontmatter.sh +0 -0
  120. /package/skills/{commit → j-commit}/assets/user_instructions_template.md +0 -0
  121. /package/skills/{distribute → j-distribute}/CONFIG_SCHEMA.md +0 -0
  122. /package/skills/{distribute → j-distribute}/scripts/check-version.sh +0 -0
  123. /package/skills/{distribute → j-distribute}/scripts/commit-version-bump.sh +0 -0
  124. /package/skills/{do → j-do}/assets/intent-vs-diff-prompt.md +0 -0
  125. /package/skills/{do → j-do}/assets/sender_template.json +0 -0
  126. /package/skills/{doc → j-doc}/assets/path-objectives.yaml +0 -0
  127. /package/skills/{doc → j-doc}/scripts/resolve_last_update.py +0 -0
  128. /package/skills/{doc-sync → j-doc-sync}/assets/default_excludes.txt +0 -0
  129. /package/skills/{doc-sync → j-doc-sync}/assets/doc_targets.md +0 -0
  130. /package/skills/{evaluate → j-evaluate}/assets/evaluation_invokation_template.yml +0 -0
  131. /package/skills/{evaluate → j-evaluate}/assets/evaluation_rapport_template.md +0 -0
  132. /package/skills/{idea → j-idea}/assets/idea_template.md +0 -0
  133. /package/skills/{pi-plan → j-pi-plan}/assets/epic.json +0 -0
  134. /package/skills/{pi-plan → j-pi-plan}/assets/story_template.md +0 -0
  135. /package/skills/{publish → j-publish}/assets/ExportOptions.plist.template +0 -0
  136. /package/skills/{publish → j-publish}/assets/ownership-matrix.md +0 -0
  137. /package/skills/{publish → j-publish}/assets/publish.example.json +0 -0
  138. /package/skills/{publish → j-publish}/assets/publish.example.npm-ci.json +0 -0
  139. /package/skills/{publish → j-publish}/assets/publish.example.npm.json +0 -0
  140. /package/skills/{publish → j-publish}/assets/secrets-guide.md +0 -0
  141. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-minimal.json +0 -0
  142. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-empty-secrets.json +0 -0
  143. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-workflow-path.json +0 -0
  144. /package/skills/{publish → j-publish}/scripts/check_target_config.sh +0 -0
  145. /package/skills/{publish → j-publish}/scripts/droplet_pipeline.sh +0 -0
  146. /package/skills/{publish → j-publish}/scripts/finalize_changelog.sh +0 -0
  147. /package/skills/{publish → j-publish}/scripts/generate_release_notes.sh +0 -0
  148. /package/skills/{publish → j-publish}/scripts/ios_pipeline.sh +0 -0
  149. /package/skills/{publish → j-publish}/scripts/npm_ci_pipeline.sh +0 -0
  150. /package/skills/{publish → j-publish}/scripts/npm_pipeline.sh +0 -0
  151. /package/skills/{publish → j-publish}/scripts/publish_common.sh +0 -0
  152. /package/skills/{publish → j-publish}/scripts/reconcile_tags.sh +0 -0
  153. /package/skills/{publish → j-publish}/scripts/run_gates.sh +0 -0
  154. /package/skills/{publish → j-publish}/scripts/setup_wizard.sh +0 -0
  155. /package/skills/{publish → j-publish}/scripts/show_history.sh +0 -0
  156. /package/skills/{publish → j-publish}/scripts/suggest_semver_bump.sh +0 -0
  157. /package/skills/{publish → j-publish}/scripts/validate_config.sh +0 -0
  158. /package/skills/{publish → j-publish}/scripts/validate_droplet_env.sh +0 -0
  159. /package/skills/{publish → j-publish}/scripts/validate_ios_env.sh +0 -0
  160. /package/skills/{publish → j-publish}/scripts/validate_npm_ci_env.sh +0 -0
  161. /package/skills/{publish → j-publish}/scripts/validate_npm_env.sh +0 -0
  162. /package/skills/{publish → j-publish}/scripts/write_ledger_entry.sh +0 -0
  163. /package/skills/{reconcile → j-reconcile}/assets/report_format.md +0 -0
  164. /package/skills/{reconcile-origin → j-reconcile-origin}/scripts/reconcile-origin.sh +0 -0
  165. /package/skills/{skillify → j-skillify}/assets/init-new/SKILL.md +0 -0
  166. /package/skills/{init → j-skillify/assets/init-new}/assets/.gitignore_template +0 -0
  167. /package/skills/{init → j-skillify/assets/init-new}/assets/PROJECT_SUMMARY_template.md +0 -0
  168. /package/skills/{init → j-skillify/assets/init-new}/assets/directory_structure.txt +0 -0
  169. /package/skills/{init → j-skillify/assets/init-new}/assets/test-config_template.json +0 -0
  170. /package/skills/{skillify → j-skillify}/assets/init-new/assets/workflow_template.json +0 -0
  171. /package/skills/{skillify → j-skillify}/assets/init-new/scripts/init.sh +0 -0
  172. /package/skills/{skillify → j-skillify}/assets/init-old/SKILL.md +0 -0
  173. /package/skills/{status → j-status}/assets/output_format.md +0 -0
  174. /package/skills/{todo → j-todo}/assets/todo_template.md +0 -0
  175. /package/skills/{uncharted → j-uncharted}/assets/SEGMENT_PROPOSAL_TEMPLATE.md +0 -0
  176. /package/skills/{uncharted → j-uncharted}/scripts/apply-subsystem-cap.sh +0 -0
  177. /package/skills/{uncharted → j-uncharted}/scripts/discover-subsystems.sh +0 -0
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/generate-j-alias.sh — generate/sync a j-<name> polyfill alias skill directory
3
+ #
4
+ # Mechanizes the hand-maintained pattern that produced skills/j-init/ (E50_S04): a
5
+ # literal-directory-name duplicate of skills/<name>/ that gives a guaranteed-unshadowed
6
+ # way to reach a skill even when a host tool's own built-in command of the same name
7
+ # would otherwise shadow the bare /<name> form. Per CLAUDE.md's Skill Implementation
8
+ # Principle, this must not be repeated by hand across every skill — this script performs
9
+ # both the initial generation and any later resync-after-source-change as a single
10
+ # deterministic command (E50_S05_T01).
11
+ #
12
+ # For the given <skill-name>, this script:
13
+ # 1. Copies the full skills/<skill-name>/ tree into skills/j-<skill-name>/, overwriting
14
+ # the destination on re-run (the sync case).
15
+ # 2. Rewrites every self-referential "skills/<skill-name>/" path reference inside the
16
+ # copied SKILL.md and any copied scripts to "skills/j-<skill-name>/" — including the
17
+ # .claude/skills/<skill-name>/ and .agents/skills/<skill-name>/ mirrored-install
18
+ # fallback paths, which fall out of the same literal substring replace.
19
+ # 3. Rewrites the copied SKILL.md's frontmatter: name: j.<skill-name> -> name:
20
+ # j.j-<skill-name>; description: reframed as a polyfill alias; keywords: gets
21
+ # j-<skill-name> and polyfill appended (existing keywords preserved, list created
22
+ # if absent); examples: gets a "j-<skill-name>" example appended if the list exists.
23
+ # 4. Inserts a short, programmatically generated lockstep/duplicate-alias note near the
24
+ # top of the body.
25
+ #
26
+ # Idempotent: the target directory is always fully rebuilt from the source on every run
27
+ # (never incrementally patched), so two runs against an unchanged source produce a
28
+ # byte-identical result, and a source change is picked up in full on the next run.
29
+ #
30
+ # Exclusions:
31
+ # - Only operates on directories containing a SKILL.md (skips e.g. skills/index/,
32
+ # which has no SKILL.md and is not part of skill routing) — enforced as a hard error,
33
+ # not a silent no-op, so a typo'd or non-skill argument fails clearly.
34
+ # - Refuses to touch the skills/init/ <-> skills/j-init/ pair — that pair already
35
+ # exists, is hand-maintained, and is explicitly out of scope for this story.
36
+ # - Refuses to touch skills/jenga/ or skills/jenga-permission-level/ (and their would-be
37
+ # j-jenga/j-jenga-permission-level twins) — hard error, non-zero exit (E50_S06_T01).
38
+ # These are root orchestrator commands; their invocation surface must stay exactly
39
+ # /jenga (j.jenga) and /jenga-permission-level (j.jenga-permission-level), never a
40
+ # doubled j-jenga alias. A prior generic run of this generator produced exactly that
41
+ # unreachable doubled twin by mistake — see docs/skill-authoring.md's "j-<name>
42
+ # directory twins" Exclusions paragraph.
43
+ #
44
+ # This is a single-skill generator only. Running it across every skill in the repo is a
45
+ # separate task (E50_S05_T02) — deliberately not built here.
46
+ #
47
+ # Usage:
48
+ # scripts/generate-j-alias.sh <skill-name>
49
+ #
50
+ # Examples:
51
+ # scripts/generate-j-alias.sh status # generates/syncs skills/j-status/
52
+ # scripts/generate-j-alias.sh close-story # generates/syncs skills/j-close-story/
53
+
54
+ set -euo pipefail
55
+
56
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
57
+ # shellcheck source=lib/resolve-project-dir.sh
58
+ source "$SCRIPT_DIR/../lib/resolve-project-dir.sh"
59
+
60
+ PROJECT_DIR="$JENGA_PROJECT_DIR"
61
+ SKILLS_DIR="$PROJECT_DIR/skills"
62
+
63
+ usage() {
64
+ echo "Usage: $(basename "$0") <skill-name>" >&2
65
+ echo " Generates/syncs skills/j-<skill-name>/ as a polyfill alias of skills/<skill-name>/." >&2
66
+ }
67
+
68
+ if [ "$#" -ne 1 ]; then
69
+ usage
70
+ exit 1
71
+ fi
72
+
73
+ case "$1" in
74
+ -h|--help)
75
+ usage
76
+ exit 0
77
+ ;;
78
+ esac
79
+
80
+ SKILL_NAME="$1"
81
+
82
+ # Security guard (E50_S05_T01 rework — see
83
+ # project/rapports/problems/E50_S05_T01-generator-path-traversal.md): validate
84
+ # the skill-name argument BEFORE any path is constructed from it, let alone
85
+ # before anything destructive runs. An unsanitized argument containing '/' or
86
+ # '..' segments previously let SRC_DIR/TARGET_DIR (built by plain string
87
+ # concatenation below) resolve outside $SKILLS_DIR, and the destructive
88
+ # rmtree-then-copytree step ran before the one existing safety check
89
+ # (frontmatter name match) ever fired. This allowlist alone closes that vector
90
+ # — it forbids '/' and '.' entirely — and runs first, ahead of every other
91
+ # check including the init/j-init special-case below.
92
+ if [[ ! "$SKILL_NAME" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
93
+ echo "generate-j-alias.sh: error: '$SKILL_NAME' is not a valid skill name — must match ^[a-z0-9][a-z0-9_-]*\$ (lowercase letters, digits, '-', '_' only, starting with a letter or digit). Refusing to construct a path from it." >&2
94
+ exit 1
95
+ fi
96
+
97
+ if [[ "$SKILL_NAME" == "init" || "$SKILL_NAME" == "j-init" ]]; then
98
+ echo "generate-j-alias.sh: refusing to touch '$SKILL_NAME' — the skills/init/ <-> skills/j-init/ pair already exists and is hand-maintained (out of scope for this generator, E50_S05). No-op." >&2
99
+ exit 0
100
+ fi
101
+
102
+ # Hard exclusion (E50_S06_T01): jenga and jenga-permission-level are root orchestrator
103
+ # commands whose invocation surface must stay exactly /jenga (j.jenga) and
104
+ # /jenga-permission-level (j.jenga-permission-level) — never a doubled j-jenga alias.
105
+ # Unlike the init/j-init case above (a silent no-op, exit 0, because that pair is
106
+ # legitimately hand-maintained and pre-dates this generator), this is a hard error with a
107
+ # non-zero exit: a j-jenga/j-jenga-permission-level twin should never be generated at all,
108
+ # so a future accidental invocation must fail loudly rather than silently succeed as a
109
+ # no-op. See docs/skill-authoring.md's "j-<name> directory twins" Exclusions paragraph.
110
+ if [[ "$SKILL_NAME" == "jenga" || "$SKILL_NAME" == "j-jenga" || "$SKILL_NAME" == "jenga-permission-level" || "$SKILL_NAME" == "j-jenga-permission-level" ]]; then
111
+ echo "generate-j-alias.sh: error: refusing to generate a j-<name> twin for '$SKILL_NAME' — jenga and jenga-permission-level are root orchestrator commands and are hard-excluded from this generator (E50_S06_T01). Their invocation surface must stay exactly /jenga (j.jenga) and /jenga-permission-level (j.jenga-permission-level), never a doubled j-jenga alias. This is not a no-op — it is an intentional hard failure." >&2
112
+ exit 1
113
+ fi
114
+
115
+ SRC_DIR="$SKILLS_DIR/$SKILL_NAME"
116
+ SRC_SKILL_MD="$SRC_DIR/SKILL.md"
117
+
118
+ if [ ! -f "$SRC_SKILL_MD" ]; then
119
+ echo "generate-j-alias.sh: error: '$SRC_DIR' does not contain a SKILL.md — not a skill directory, refusing to generate skills/j-$SKILL_NAME/." >&2
120
+ exit 1
121
+ fi
122
+
123
+ TARGET_DIR="$SKILLS_DIR/j-$SKILL_NAME"
124
+
125
+ # Defense in depth (belt and suspenders on top of the allowlist above):
126
+ # resolve SRC_DIR/TARGET_DIR to absolute canonical paths and hard-fail unless
127
+ # each is confined to a direct child of $SKILLS_DIR. This protects against the
128
+ # allowlist regex being weakened in a future edit, or $SKILLS_DIR itself
129
+ # containing a symlink that could redirect an otherwise-validated path outside
130
+ # skills/. Both checks below run before the python heredoc, i.e. strictly
131
+ # before any rmtree/copytree call.
132
+ REAL_SKILLS_DIR="$(realpath "$SKILLS_DIR")"
133
+ REAL_SRC_DIR="$(realpath "$SRC_DIR")"
134
+ if [ "$(dirname "$REAL_SRC_DIR")" != "$REAL_SKILLS_DIR" ]; then
135
+ echo "generate-j-alias.sh: error: resolved source path '$REAL_SRC_DIR' escapes '$REAL_SKILLS_DIR' — refusing to proceed." >&2
136
+ exit 1
137
+ fi
138
+
139
+ # TARGET_DIR may not exist yet (first-run generation case), and this
140
+ # platform's realpath has no -m/--canonicalize-missing option, so it can't be
141
+ # realpath'd directly when absent. Its parent is always $SKILLS_DIR by
142
+ # construction (SKILL_NAME is a single path component, and the allowlist above
143
+ # already forbids '/' and '..' in it), so check the literal parent/basename
144
+ # shape here, and additionally realpath the target itself when it already
145
+ # exists (the resync case) to catch a symlink planted at the target that
146
+ # points outside skills/.
147
+ TARGET_BASENAME="$(basename "$TARGET_DIR")"
148
+ if [[ "$TARGET_BASENAME" == "." || "$TARGET_BASENAME" == ".." || "$TARGET_BASENAME" == */* ]]; then
149
+ echo "generate-j-alias.sh: error: computed target basename '$TARGET_BASENAME' is unsafe — refusing to proceed." >&2
150
+ exit 1
151
+ fi
152
+ if [ "$(dirname "$TARGET_DIR")" != "$SKILLS_DIR" ]; then
153
+ echo "generate-j-alias.sh: error: computed target path '$TARGET_DIR' does not resolve directly under '$SKILLS_DIR' — refusing to proceed." >&2
154
+ exit 1
155
+ fi
156
+ if [ -e "$TARGET_DIR" ] || [ -L "$TARGET_DIR" ]; then
157
+ REAL_TARGET_DIR="$(realpath "$TARGET_DIR")"
158
+ if [ "$(dirname "$REAL_TARGET_DIR")" != "$REAL_SKILLS_DIR" ]; then
159
+ echo "generate-j-alias.sh: error: resolved target path '$REAL_TARGET_DIR' escapes '$REAL_SKILLS_DIR' — refusing to proceed." >&2
160
+ exit 1
161
+ fi
162
+ fi
163
+
164
+ export SKILL_NAME SRC_DIR TARGET_DIR
165
+
166
+ python3 - <<'PY'
167
+ import os
168
+ import re
169
+ import shutil
170
+ import sys
171
+
172
+ skill_name = os.environ["SKILL_NAME"]
173
+ src_dir = os.environ["SRC_DIR"]
174
+ target_dir = os.environ["TARGET_DIR"]
175
+
176
+ old_ref = f"skills/{skill_name}/"
177
+ new_ref = f"skills/j-{skill_name}/"
178
+
179
+ # ---- 1. Copy the full tree, overwriting the destination (the sync case) ----
180
+ if os.path.lexists(target_dir):
181
+ shutil.rmtree(target_dir)
182
+ shutil.copytree(src_dir, target_dir, symlinks=True)
183
+
184
+ # ---- 2. Rewrite self-referential paths in every copied file ----
185
+ for dirpath, _dirnames, filenames in os.walk(target_dir):
186
+ for fn in filenames:
187
+ fpath = os.path.join(dirpath, fn)
188
+ try:
189
+ with open(fpath, "r", encoding="utf-8") as fh:
190
+ content = fh.read()
191
+ except (UnicodeDecodeError, OSError):
192
+ continue # binary or unreadable — leave untouched
193
+ if old_ref in content:
194
+ with open(fpath, "w", encoding="utf-8") as fh:
195
+ fh.write(content.replace(old_ref, new_ref))
196
+
197
+ # ---- 3. Rewrite SKILL.md frontmatter + insert the lockstep note ----
198
+ target_skill_md = os.path.join(target_dir, "SKILL.md")
199
+ with open(target_skill_md, "r", encoding="utf-8") as fh:
200
+ lines = fh.readlines()
201
+
202
+ if not lines or lines[0].strip() != "---":
203
+ sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no opening '---' frontmatter marker")
204
+
205
+ close_idx = None
206
+ for idx in range(1, len(lines)):
207
+ if lines[idx].strip() == "---":
208
+ close_idx = idx
209
+ break
210
+ if close_idx is None:
211
+ sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no closing '---' frontmatter marker")
212
+
213
+ # Parse the frontmatter into an ordered list of top-level fields. This is the same
214
+ # informal line-oriented approach scripts/apply-j-prefix.sh already uses elsewhere in
215
+ # this repo for SKILL.md frontmatter — not a full YAML parser, consistent with how
216
+ # frontmatter is already handled here.
217
+ key_re = re.compile(r'^([A-Za-z_][\w-]*):(.*)$')
218
+ fields = []
219
+ i = 1
220
+ while i < close_idx:
221
+ line = lines[i]
222
+ m = key_re.match(line)
223
+ if not m:
224
+ i += 1
225
+ continue
226
+ block = [line]
227
+ j = i + 1
228
+ if m.group(2).strip() == "":
229
+ # possible nested block (list or mapping) — consume indented continuation lines
230
+ while j < close_idx and lines[j].startswith(" "):
231
+ block.append(lines[j])
232
+ j += 1
233
+ fields.append({"key": m.group(1), "lines": block})
234
+ i = j
235
+
236
+
237
+ def find_field(key):
238
+ for f in fields:
239
+ if f["key"] == key:
240
+ return f
241
+ return None
242
+
243
+
244
+ name_field = find_field("name")
245
+ desc_field = find_field("description")
246
+
247
+ if name_field is None or len(name_field["lines"]) != 1:
248
+ sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no single-line 'name:' field")
249
+ if desc_field is None or len(desc_field["lines"]) != 1:
250
+ sys.exit(f"generate-j-alias.sh: error: {target_skill_md} has no single-line 'description:' field")
251
+
252
+ current_name = key_re.match(name_field["lines"][0]).group(2).strip().strip('"\'')
253
+ # Accepts the current "j.<name>" form, the bare "<name>" form, and the legacy
254
+ # "j:<name>" form (pre-E50_S07_T01) so a twin can still be regenerated from a
255
+ # not-yet-migrated source instead of hard-aborting. Whatever the source carries,
256
+ # the twin is always emitted on the current "j." separator.
257
+ if current_name not in (f"j.{skill_name}", f"j:{skill_name}", skill_name):
258
+ sys.exit(
259
+ f"generate-j-alias.sh: error: frontmatter name '{current_name}' in {target_skill_md} "
260
+ f"does not match expected 'j.{skill_name}' (or bare '{skill_name}') — refusing to guess, aborting."
261
+ )
262
+ name_field["lines"][0] = f"name: j.j-{skill_name}\n"
263
+
264
+ original_desc = key_re.match(desc_field["lines"][0]).group(2).strip().strip('"\'').rstrip(".")
265
+ new_desc = (
266
+ f"Polyfill alias of the {skill_name} skill under a collision-safe directory name. "
267
+ f"Identical behavior to /{skill_name} — {original_desc}. "
268
+ f"Use when the bare /{skill_name} form is shadowed by another tool's own built-in "
269
+ f"command of the same name."
270
+ )
271
+ desc_field["lines"][0] = f"description: {new_desc}\n"
272
+
273
+ keywords_field = find_field("keywords")
274
+ if keywords_field is not None:
275
+ keywords_field["lines"].append(f" - j-{skill_name}\n")
276
+ keywords_field["lines"].append(" - polyfill\n")
277
+ else:
278
+ fields.append({
279
+ "key": "keywords",
280
+ "lines": ["keywords:\n", f" - j-{skill_name}\n", " - polyfill\n"],
281
+ })
282
+
283
+ examples_field = find_field("examples")
284
+ if examples_field is not None:
285
+ examples_field["lines"].append(f' - "j-{skill_name}"\n')
286
+ # if absent: per spec, do not fabricate an examples: list where none existed
287
+
288
+ new_frontmatter_lines = []
289
+ for f in fields:
290
+ new_frontmatter_lines.extend(f["lines"])
291
+
292
+ body_lines = lines[close_idx + 1:]
293
+
294
+ note_text = (
295
+ f"This skill is a literal-directory-name duplicate of `skills/{skill_name}/`. It exists so "
296
+ f"that `/j-{skill_name}` (and `j.j-{skill_name}`) give a guaranteed-unshadowed way to reach "
297
+ f"the same flow as `/{skill_name}`, even if a host tool's own built-in command of the same "
298
+ f"name would otherwise shadow or override the bare `/{skill_name}` alias (Claude Code's "
299
+ f"native skill resolution is a literal-string, directory-name-based match — see "
300
+ f"`docs/skill-authoring.md`'s \"Invocation Convention\").\n\n"
301
+ f"This file is generated/synced by `scripts/generate-j-alias.sh {skill_name}` from "
302
+ f"`skills/{skill_name}/SKILL.md` — do not hand-edit it; re-run the generator instead to "
303
+ f"pick up source changes."
304
+ )
305
+ note_block = []
306
+ for para in note_text.split("\n\n"):
307
+ note_block.append(para + "\n")
308
+ note_block.append("\n")
309
+
310
+ heading_idx = None
311
+ for idx, bl in enumerate(body_lines):
312
+ if bl.startswith("# "):
313
+ heading_idx = idx
314
+ break
315
+
316
+ if heading_idx is not None:
317
+ insert_at = heading_idx + 1
318
+ if insert_at < len(body_lines) and body_lines[insert_at].strip() == "":
319
+ insert_at += 1
320
+ prefix = body_lines[:insert_at]
321
+ if prefix and prefix[-1].strip() != "":
322
+ prefix = prefix + ["\n"]
323
+ new_body_lines = prefix + note_block + body_lines[insert_at:]
324
+ else:
325
+ new_body_lines = note_block + body_lines
326
+
327
+ new_lines = [lines[0]] + new_frontmatter_lines + [lines[close_idx]] + new_body_lines
328
+
329
+ with open(target_skill_md, "w", encoding="utf-8") as fh:
330
+ fh.writelines(new_lines)
331
+
332
+ print(f"generate-j-alias.sh: generated/synced skills/j-{skill_name}/ from skills/{skill_name}/")
333
+ PY
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/generate-legacy-shipped-paths.js — legacy shipped-path list generator (E26_S08_T03)
4
+ *
5
+ * Why this exists
6
+ * ────────────────
7
+ * `lib/postinstall-manifest.js`'s delete reconciliation (E26_S08_T01) is purely forward-looking:
8
+ * a consumer already installed before any manifest existed can never have their pre-existing
9
+ * orphans cleaned up, because the first manifest a fixed version ever writes for them records
10
+ * only what THAT run mirrored. This script produces the static list this package ships so the
11
+ * FIRST manifest a consumer ever gets can instead be *seeded* with paths known to have shipped in
12
+ * some real prior published version — see `seedFromLegacyPaths` in `lib/postinstall-manifest.js`
13
+ * and `scripts/postinstall.js`'s `no-prior-manifest` branch for how the seed is consumed.
14
+ *
15
+ * Why not git tags
16
+ * ─────────────────
17
+ * The task's design note allows deriving the list "from git tags" as an alternative to
18
+ * publish-time derivation. Checked and rejected for this repo specifically: this repo's local
19
+ * tags (`v0.0.1`, `v55.0.1`, `last-self-sync`) do not correspond to the real npm publish history
20
+ * at all — `npm view @jenga-ai/agent versions --json` shows the real, disconnected sequence
21
+ * (1.0.0 through 3.0.0 as of this writing). Reconstructing shipped paths from local git tags in
22
+ * this repo would silently produce a list bearing no relation to what was actually published.
23
+ *
24
+ * Two modes
25
+ * ─────────
26
+ * --bootstrap Fetches EVERY version `npm view <package> versions --json` currently lists from
27
+ * the real registry, `npm pack`s each one into a throwaway temp dir, and unions
28
+ * the `skills/`+`agents/` entries inside every tarball. Network-dependent. This is
29
+ * how the real historical shipped-path set is captured — including versions whose
30
+ * git history is not reliably reconstructable locally (verified true here). Meant
31
+ * to be run manually / rarely — a one-off backfill, or an occasional resync — NOT
32
+ * wired into the automatic per-publish flow (see incremental mode below for that).
33
+ *
34
+ * (default) Incremental, no network: reads whatever is already at the output path (if any)
35
+ * and unions it with the paths CURRENTLY on disk under this repo's own `skills/`
36
+ * and `agents/` directories — i.e. "what this release is about to ship" folds into
37
+ * the running cumulative record. This is the mode wired into the publish pipeline
38
+ * (`skills/publish/scripts/npm_pipeline.sh` / `npm_ci_pipeline.sh`, via the
39
+ * `generate:legacy-paths` npm script) so every future publish keeps the list
40
+ * current with zero network dependency and zero publish-time registry flakiness.
41
+ *
42
+ * IMPORTANT — this mode's first-ever run is NOT a substitute for --bootstrap: if
43
+ * the output file doesn't exist yet, incremental mode unions an EMPTY existing set
44
+ * with whatever's on disk in THIS repo's tree right now. That happens to currently
45
+ * equal the real historical union (this repo's working tree is a superset of every
46
+ * published version's file list, as of the 2026-09-07 verification below) — but
47
+ * that is a coincidence of this repo's current state, not a guarantee the mode
48
+ * itself provides. The very first generation of the shipped artifact MUST use
49
+ * --bootstrap so the baseline is verified against the real registry, not assumed.
50
+ *
51
+ * Output
52
+ * ──────
53
+ * `lib/legacy-shipped-paths.json` (ships automatically — `lib/` is already in package.json's
54
+ * `files` allow-list, no change needed there):
55
+ *
56
+ * {
57
+ * "generated_at": "<ISO 8601>",
58
+ * "package": "@jenga-ai/agent",
59
+ * "source": "bootstrap-from-registry+incremental" | "incremental",
60
+ * "paths": ["agents/developer.md", "skills/do/SKILL.md", ...]
61
+ * }
62
+ *
63
+ * `paths` are relative to a mirror root, POSIX-separated, deduped and sorted — matching the same
64
+ * shape convention `lib/postinstall-manifest.js`'s own manifest uses, for consistency.
65
+ *
66
+ * The generation step is regenerated automatically as part of the publish flow (incremental mode),
67
+ * per this task's AC — it is NOT hand-maintained, so it cannot silently go stale across releases.
68
+ *
69
+ * ESM, Node built-ins only — matches lib/postinstall-manifest.js and lib/mirror.js. `npm` itself is
70
+ * shelled out to (via `execFileSync`) only in `--bootstrap` mode.
71
+ */
72
+
73
+ import fs from 'node:fs';
74
+ import os from 'node:os';
75
+ import path from 'node:path';
76
+ import { execFileSync } from 'node:child_process';
77
+ import { fileURLToPath } from 'node:url';
78
+
79
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
80
+ const REPO_ROOT = path.join(__dirname, '..');
81
+
82
+ export const DEFAULT_OUTPUT_PATH = path.join(REPO_ROOT, 'lib', 'legacy-shipped-paths.json');
83
+ export const DEFAULT_PACKAGE_NAME = '@jenga-ai/agent';
84
+
85
+ /** Discovery-bound directories mirrored into a consumer's .agents/ and .claude/ (see docs/distribution.md §1). */
86
+ const COPY_SET = ['skills', 'agents'];
87
+
88
+ // ── walk a real directory tree ──────────────────────────────────────────────
89
+
90
+ function walkDir(base, dir, out) {
91
+ let entries;
92
+ try {
93
+ entries = fs.readdirSync(dir, { withFileTypes: true });
94
+ } catch (_) {
95
+ return;
96
+ }
97
+ for (const entry of entries) {
98
+ const abs = path.join(dir, entry.name);
99
+ if (entry.isDirectory()) {
100
+ walkDir(base, abs, out);
101
+ } else if (entry.isFile()) {
102
+ out.push(path.relative(base, abs).split(path.sep).join('/'));
103
+ }
104
+ // symlinks intentionally ignored — matches lib/mirror.js's own walk behavior.
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Relative POSIX paths this repo's CURRENT `skills/` + `agents/` trees would ship, i.e. exactly
110
+ * what a fresh install of the version about to be published would mirror.
111
+ *
112
+ * @param {string} repoRoot
113
+ * @returns {string[]} sorted, deduped
114
+ */
115
+ export function currentShippedPaths(repoRoot = REPO_ROOT) {
116
+ const out = [];
117
+ for (const entry of COPY_SET) {
118
+ const dir = path.join(repoRoot, entry);
119
+ if (fs.existsSync(dir)) walkDir(repoRoot, dir, out);
120
+ }
121
+ return [...new Set(out)].sort();
122
+ }
123
+
124
+ // ── read existing output (if any) ───────────────────────────────────────────
125
+
126
+ function readExistingPaths(outputPath) {
127
+ try {
128
+ const parsed = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
129
+ return Array.isArray(parsed.paths) ? parsed.paths.filter((p) => typeof p === 'string') : [];
130
+ } catch (_) {
131
+ return []; // absent, unreadable, or corrupt — start from an empty cumulative set
132
+ }
133
+ }
134
+
135
+ // ── bootstrap from the real npm registry ────────────────────────────────────
136
+
137
+ /**
138
+ * Fetch every currently-listed published version of `packageName`, `npm pack` each into a
139
+ * throwaway temp dir, and union the `skills/`+`agents/` entries found inside every tarball.
140
+ * Network-dependent — intended for manual/rare use (a one-off backfill or occasional resync),
141
+ * never called by the automatic per-publish (incremental) path.
142
+ *
143
+ * @param {string} packageName
144
+ * @returns {string[]} sorted, deduped relative POSIX paths
145
+ */
146
+ export function bootstrapFromRegistry(packageName = DEFAULT_PACKAGE_NAME) {
147
+ const versionsRaw = execFileSync('npm', ['view', packageName, 'versions', '--json'], {
148
+ encoding: 'utf8',
149
+ });
150
+ const versions = JSON.parse(versionsRaw);
151
+ const all = new Set();
152
+
153
+ for (const version of versions) {
154
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'jenga-legacy-bootstrap-'));
155
+ try {
156
+ execFileSync('npm', ['pack', `${packageName}@${version}`, '--silent'], { cwd: tmp, stdio: 'ignore' });
157
+ const tarball = fs.readdirSync(tmp).find((f) => f.endsWith('.tgz'));
158
+ if (!tarball) continue;
159
+ const listing = execFileSync('tar', ['-tzf', path.join(tmp, tarball)], { encoding: 'utf8' });
160
+ for (const line of listing.split('\n')) {
161
+ const m = line.match(/^package\/(skills|agents)\/(.+)$/);
162
+ if (m && !line.endsWith('/')) all.add(`${m[1]}/${m[2]}`);
163
+ }
164
+ } finally {
165
+ fs.rmSync(tmp, { recursive: true, force: true });
166
+ }
167
+ }
168
+
169
+ return [...all].sort();
170
+ }
171
+
172
+ // ── generate ─────────────────────────────────────────────────────────────────
173
+
174
+ /**
175
+ * @param {object} [opts]
176
+ * @param {string} [opts.outputPath] Default: lib/legacy-shipped-paths.json
177
+ * @param {string} [opts.packageName] Default: @jenga-ai/agent
178
+ * @param {boolean} [opts.bootstrap] Default: false (incremental, no network)
179
+ * @param {string} [opts.repoRoot] Default: this repo's own root
180
+ * @returns {{written: boolean, path: string, count: number, source: string}}
181
+ */
182
+ export function generate({
183
+ outputPath = DEFAULT_OUTPUT_PATH,
184
+ packageName = DEFAULT_PACKAGE_NAME,
185
+ bootstrap = false,
186
+ repoRoot = REPO_ROOT,
187
+ } = {}) {
188
+ const existing = readExistingPaths(outputPath);
189
+ let paths;
190
+ let source;
191
+
192
+ if (bootstrap) {
193
+ const registryPaths = bootstrapFromRegistry(packageName);
194
+ paths = [...new Set([...registryPaths, ...existing])].sort();
195
+ source = 'bootstrap-from-registry+incremental';
196
+ } else {
197
+ const current = currentShippedPaths(repoRoot);
198
+ paths = [...new Set([...existing, ...current])].sort();
199
+ source = 'incremental';
200
+ }
201
+
202
+ const artifact = {
203
+ generated_at: new Date().toISOString(),
204
+ package: packageName,
205
+ source,
206
+ paths,
207
+ };
208
+
209
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
210
+ fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
211
+
212
+ return { written: true, path: outputPath, count: paths.length, source };
213
+ }
214
+
215
+ /**
216
+ * Read the shipped legacy-paths artifact's `paths` array. Fail-toward-doing-nothing: any read or
217
+ * parse failure returns `[]` rather than throwing — a missing/corrupt legacy-paths file must
218
+ * never abort or degrade an unattended `npm install`, mirroring `readManifest`'s own posture in
219
+ * `lib/postinstall-manifest.js`.
220
+ *
221
+ * @param {string} artifactPath
222
+ * @returns {string[]}
223
+ */
224
+ export function readLegacyShippedPaths(artifactPath = DEFAULT_OUTPUT_PATH) {
225
+ try {
226
+ const parsed = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
227
+ if (!Array.isArray(parsed.paths)) return [];
228
+ return parsed.paths.filter((p) => typeof p === 'string' && p.length > 0);
229
+ } catch (_) {
230
+ return [];
231
+ }
232
+ }
233
+
234
+ // ── CLI guard ────────────────────────────────────────────────────────────────
235
+ // node scripts/generate-legacy-shipped-paths.js [--bootstrap] [--package <name>] [outputPath]
236
+
237
+ const invokedPath = process.argv[1] ? fs.realpathSync(process.argv[1]) : null;
238
+ if (invokedPath === fileURLToPath(import.meta.url)) {
239
+ const argv = process.argv.slice(2);
240
+ const bootstrap = argv.includes('--bootstrap');
241
+ const pkgFlagIndex = argv.indexOf('--package');
242
+ const packageName = pkgFlagIndex !== -1 ? argv[pkgFlagIndex + 1] : DEFAULT_PACKAGE_NAME;
243
+ const positional = argv.filter((a, i) => a !== '--bootstrap' && i !== pkgFlagIndex && i !== pkgFlagIndex + 1 && !a.startsWith('--'));
244
+ const outputPath = positional[0] || DEFAULT_OUTPUT_PATH;
245
+
246
+ const result = generate({ outputPath, packageName, bootstrap });
247
+ console.log(`✓ ${result.path} (${result.count} paths, ${result.source})`);
248
+ }