@jenga-ai/agent 1.3.0 → 3.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 (175) hide show
  1. package/README.md +93 -256
  2. package/agents/developer.md +9 -8
  3. package/agents/scrum-master.md +57 -23
  4. package/agents/tester.md +51 -5
  5. package/hooks/on_session_end.sh +13 -1
  6. package/lib/generate-agent-context.js +18 -1
  7. package/lib/generate-copilot-instructions.js +18 -1
  8. package/lib/generate-skill-allow-list.js +197 -0
  9. package/lib/skill-allow-list.json +42 -0
  10. package/package.json +17 -13
  11. package/scripts/apply-j-prefix.sh +243 -0
  12. package/scripts/consume-context-digest.sh +103 -0
  13. package/scripts/generate-j-alias.sh +333 -0
  14. package/scripts/postinstall.js +25 -0
  15. package/scripts/sweep-stale-context-digests.sh +132 -0
  16. package/scripts/validate-board.sh +5 -0
  17. package/scripts/write-context-digest.sh +230 -0
  18. package/skills/{brainstorm → j-brainstorm}/SKILL.md +9 -2
  19. package/skills/{btw → j-btw}/SKILL.md +9 -2
  20. package/skills/{clearify → j-clearify}/SKILL.md +9 -2
  21. package/skills/{close-story → j-close-story}/SKILL.md +92 -13
  22. package/skills/j-close-story/scripts/check-privatized.sh +345 -0
  23. package/skills/{close-story → j-close-story}/scripts/check-story-closeable.sh +1 -1
  24. package/skills/{close-story → j-close-story}/scripts/extract-task-diff-stats.sh +1 -1
  25. package/skills/{commit → j-commit}/SKILL.md +9 -2
  26. package/skills/j-continue/SKILL.md +36 -0
  27. package/skills/{deep-dive → j-deep-dive}/SKILL.md +9 -8
  28. package/skills/{dev-done → j-dev-done}/SKILL.md +11 -4
  29. package/skills/{dev-done → j-dev-done}/scripts/classify-commit-outcome.sh +4 -4
  30. package/skills/{distribute → j-distribute}/SKILL.md +17 -10
  31. package/skills/{distribute → j-distribute}/scripts/distribute-changes.sh +1 -1
  32. package/skills/{do → j-do}/SKILL.md +111 -14
  33. package/skills/j-doc/README.md +155 -0
  34. package/skills/{doc → j-doc}/SKILL.md +55 -18
  35. package/skills/j-doc/authoring-notes.md +72 -0
  36. package/skills/j-doc/scripts/resolve_last_update.py +149 -0
  37. package/skills/{doc-sync → j-doc-sync}/SKILL.md +9 -2
  38. package/skills/{dooo → j-dooo}/SKILL.md +9 -2
  39. package/skills/j-error/SKILL.md +36 -0
  40. package/skills/{evaluate → j-evaluate}/SKILL.md +9 -2
  41. package/skills/j-examplify/SKILL.md +49 -0
  42. package/skills/{help → j-help}/SKILL.md +9 -2
  43. package/skills/{idea → j-idea}/SKILL.md +10 -3
  44. package/skills/{idea → j-idea}/assets/idea_handoff_template.md +1 -1
  45. package/skills/{improve → j-improve}/SKILL.md +9 -2
  46. package/skills/{init → j-init}/SKILL.md +21 -8
  47. package/skills/j-init/assets/scope-thresholds_template.json +7 -0
  48. package/skills/j-jbp/SKILL.md +32 -0
  49. package/skills/j-lgtm/SKILL.md +28 -0
  50. package/skills/{pi-plan → j-pi-plan}/SKILL.md +10 -3
  51. package/skills/{proceed → j-proceed}/SKILL.md +9 -2
  52. package/skills/{publish → j-publish}/SKILL.md +47 -40
  53. package/skills/{publish → j-publish}/adapters/droplet.md +1 -1
  54. package/skills/{publish → j-publish}/adapters/mobile-ios.md +3 -3
  55. package/skills/{publish → j-publish}/adapters/npm-ci.md +29 -7
  56. package/skills/{publish → j-publish}/adapters/npm.md +8 -8
  57. package/skills/{publish → j-publish}/assets/ci-contract.md +2 -2
  58. package/skills/{publish → j-publish}/schemas/publish.schema.json +1 -1
  59. package/skills/{publish → j-publish}/scripts/npm_ci_pipeline.sh +21 -1
  60. package/skills/{publish → j-publish}/scripts/npm_stage_inspect.sh +34 -1
  61. package/skills/{publish → j-publish}/scripts/npm_stage_pipeline.sh +9 -4
  62. package/skills/{publish → j-publish}/scripts/publish_deploy.sh +4 -4
  63. package/skills/{publish → j-publish}/scripts/validate_npm_stage_env.sh +1 -1
  64. package/skills/{publish → j-publish}/wizards/droplet.md +1 -1
  65. package/skills/{publish → j-publish}/wizards/mobile-ios.md +1 -1
  66. package/skills/{publish → j-publish}/wizards/npm-ci.md +1 -1
  67. package/skills/{publish → j-publish}/wizards/npm.md +1 -1
  68. package/skills/{reconcile → j-reconcile}/SKILL.md +12 -5
  69. package/skills/{reconcile → j-reconcile}/scripts/detect-unlinked-code.sh +2 -2
  70. package/skills/{reconcile → j-reconcile}/scripts/resolve-reconcile-scope.sh +3 -3
  71. package/skills/{reconcile-origin → j-reconcile-origin}/SKILL.md +13 -6
  72. package/skills/{redo → j-redo}/SKILL.md +9 -2
  73. package/skills/{skillify → j-skillify}/SKILL.md +10 -3
  74. package/skills/{spinoff → j-spinoff}/SKILL.md +9 -2
  75. package/skills/{status → j-status}/SKILL.md +9 -2
  76. package/skills/j-todo/SKILL.md +92 -0
  77. package/skills/{todo → j-todo}/assets/todo_handoff_template.md +1 -1
  78. package/skills/j-todo/scripts/add_trivial_task.sh +216 -0
  79. package/skills/j-todo/scripts/update_story_tasks.py +87 -0
  80. package/skills/{uncharted → j-uncharted}/SKILL.md +35 -28
  81. package/skills/{uncharted → j-uncharted}/assets/UNDERSTANDING_DOC_TEMPLATE.md +2 -2
  82. package/skills/{uncharted → j-uncharted}/scripts/detect-dependencies.sh +1 -1
  83. package/skills/{uncharted → j-uncharted}/scripts/detect-tests.sh +1 -1
  84. package/skills/{uncharted → j-uncharted}/scripts/directory-triage.sh +3 -3
  85. package/skills/{uncharted → j-uncharted}/scripts/elicitation-state.sh +3 -3
  86. package/skills/{uncharted → j-uncharted}/scripts/enumerate-target.sh +1 -1
  87. package/skills/{uncharted → j-uncharted}/scripts/import-source.sh +1 -1
  88. package/skills/{uncharted → j-uncharted}/scripts/inspect-provenance.sh +1 -1
  89. package/skills/{uncharted → j-uncharted}/scripts/resolve-segment-target.sh +5 -5
  90. package/skills/{uncharted → j-uncharted}/scripts/run-engine.sh +1 -1
  91. package/skills/{uncharted → j-uncharted}/scripts/validate-proposed-items.sh +2 -2
  92. package/skills/{uncharted → j-uncharted}/scripts/write-backfilled-epics.sh +1 -1
  93. package/skills/j-wtf/SKILL.md +27 -0
  94. package/skills/jenga/SKILL.md +1 -1
  95. package/skills/jenga/scripts/render-confirmation.sh +55 -18
  96. package/skills/jenga-permission-level/SKILL.md +1 -1
  97. package/templates/SCRUM_BOARD_SCHEMA.md +33 -2
  98. package/templates/agent-context.md.tpl +32 -9
  99. package/templates/copilot-instructions.md.tpl +66 -11
  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/assets/scope-thresholds_template.json +0 -7
  104. package/skills/jbp/SKILL.md +0 -25
  105. package/skills/lgtm/SKILL.md +0 -21
  106. package/skills/todo/SKILL.md +0 -48
  107. package/skills/wtf/SKILL.md +0 -20
  108. /package/skills/{close-story → j-close-story}/scripts/compute-scope-divergence.sh +0 -0
  109. /package/skills/{close-story → j-close-story}/scripts/extract-diff-stats.sh +0 -0
  110. /package/skills/{close-story → j-close-story}/scripts/update-task-frontmatter.sh +0 -0
  111. /package/skills/{commit → j-commit}/assets/user_instructions_template.md +0 -0
  112. /package/skills/{distribute → j-distribute}/CONFIG_SCHEMA.md +0 -0
  113. /package/skills/{distribute → j-distribute}/scripts/check-version.sh +0 -0
  114. /package/skills/{distribute → j-distribute}/scripts/commit-version-bump.sh +0 -0
  115. /package/skills/{do → j-do}/assets/intent-vs-diff-prompt.md +0 -0
  116. /package/skills/{do → j-do}/assets/sender_template.json +0 -0
  117. /package/skills/{doc → j-doc}/assets/path-objectives.yaml +0 -0
  118. /package/skills/{doc-sync → j-doc-sync}/assets/default_excludes.txt +0 -0
  119. /package/skills/{doc-sync → j-doc-sync}/assets/doc_targets.md +0 -0
  120. /package/skills/{evaluate → j-evaluate}/assets/evaluation_invokation_template.yml +0 -0
  121. /package/skills/{evaluate → j-evaluate}/assets/evaluation_rapport_template.md +0 -0
  122. /package/skills/{idea → j-idea}/assets/idea_template.md +0 -0
  123. /package/skills/{init → j-init}/assets/.gitignore_template +0 -0
  124. /package/skills/{init → j-init}/assets/PROJECT_SUMMARY_template.md +0 -0
  125. /package/skills/{init → j-init}/assets/directory_structure.txt +0 -0
  126. /package/skills/{init → j-init}/assets/strategy_stub_template.md +0 -0
  127. /package/skills/{init → j-init}/assets/test-config_template.json +0 -0
  128. /package/skills/{init → j-init}/assets/workflow_template.json +0 -0
  129. /package/skills/{init → j-init}/scripts/apply-project-visibility.sh +0 -0
  130. /package/skills/{init → j-init}/scripts/detect-existing-codebase.sh +0 -0
  131. /package/skills/{init → j-init}/scripts/init.sh +0 -0
  132. /package/skills/{pi-plan → j-pi-plan}/assets/epic.json +0 -0
  133. /package/skills/{pi-plan → j-pi-plan}/assets/story_template.md +0 -0
  134. /package/skills/{publish → j-publish}/assets/ExportOptions.plist.template +0 -0
  135. /package/skills/{publish → j-publish}/assets/ownership-matrix.md +0 -0
  136. /package/skills/{publish → j-publish}/assets/publish.example.json +0 -0
  137. /package/skills/{publish → j-publish}/assets/publish.example.npm-ci.json +0 -0
  138. /package/skills/{publish → j-publish}/assets/publish.example.npm.json +0 -0
  139. /package/skills/{publish → j-publish}/assets/secrets-guide.md +0 -0
  140. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-minimal.json +0 -0
  141. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-empty-secrets.json +0 -0
  142. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-workflow-path.json +0 -0
  143. /package/skills/{publish → j-publish}/scripts/check_target_config.sh +0 -0
  144. /package/skills/{publish → j-publish}/scripts/droplet_pipeline.sh +0 -0
  145. /package/skills/{publish → j-publish}/scripts/finalize_changelog.sh +0 -0
  146. /package/skills/{publish → j-publish}/scripts/generate_release_notes.sh +0 -0
  147. /package/skills/{publish → j-publish}/scripts/ios_pipeline.sh +0 -0
  148. /package/skills/{publish → j-publish}/scripts/npm_pipeline.sh +0 -0
  149. /package/skills/{publish → j-publish}/scripts/publish_common.sh +0 -0
  150. /package/skills/{publish → j-publish}/scripts/reconcile_tags.sh +0 -0
  151. /package/skills/{publish → j-publish}/scripts/run_gates.sh +0 -0
  152. /package/skills/{publish → j-publish}/scripts/setup_wizard.sh +0 -0
  153. /package/skills/{publish → j-publish}/scripts/show_history.sh +0 -0
  154. /package/skills/{publish → j-publish}/scripts/suggest_semver_bump.sh +0 -0
  155. /package/skills/{publish → j-publish}/scripts/validate_config.sh +0 -0
  156. /package/skills/{publish → j-publish}/scripts/validate_droplet_env.sh +0 -0
  157. /package/skills/{publish → j-publish}/scripts/validate_ios_env.sh +0 -0
  158. /package/skills/{publish → j-publish}/scripts/validate_npm_ci_env.sh +0 -0
  159. /package/skills/{publish → j-publish}/scripts/validate_npm_env.sh +0 -0
  160. /package/skills/{publish → j-publish}/scripts/write_ledger_entry.sh +0 -0
  161. /package/skills/{reconcile → j-reconcile}/assets/report_format.md +0 -0
  162. /package/skills/{reconcile-origin → j-reconcile-origin}/scripts/reconcile-origin.sh +0 -0
  163. /package/skills/{skillify → j-skillify}/assets/init-new/SKILL.md +0 -0
  164. /package/skills/{skillify → j-skillify}/assets/init-new/assets/.gitignore_template +0 -0
  165. /package/skills/{skillify → j-skillify}/assets/init-new/assets/PROJECT_SUMMARY_template.md +0 -0
  166. /package/skills/{skillify → j-skillify}/assets/init-new/assets/directory_structure.txt +0 -0
  167. /package/skills/{skillify → j-skillify}/assets/init-new/assets/test-config_template.json +0 -0
  168. /package/skills/{skillify → j-skillify}/assets/init-new/assets/workflow_template.json +0 -0
  169. /package/skills/{skillify → j-skillify}/assets/init-new/scripts/init.sh +0 -0
  170. /package/skills/{skillify → j-skillify}/assets/init-old/SKILL.md +0 -0
  171. /package/skills/{status → j-status}/assets/output_format.md +0 -0
  172. /package/skills/{todo → j-todo}/assets/todo_template.md +0 -0
  173. /package/skills/{uncharted → j-uncharted}/assets/SEGMENT_PROPOSAL_TEMPLATE.md +0 -0
  174. /package/skills/{uncharted → j-uncharted}/scripts/apply-subsystem-cap.sh +0 -0
  175. /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
@@ -33,6 +33,7 @@ import { fileURLToPath } from 'node:url';
33
33
 
34
34
  import { mirror } from '../lib/mirror.js';
35
35
  import { generateCopilotInstructions } from '../lib/generate-copilot-instructions.js';
36
+ import { generateSkillAllowList } from '../lib/generate-skill-allow-list.js';
36
37
 
37
38
  // ESM equivalent of __dirname
38
39
  const __filename = fileURLToPath(import.meta.url);
@@ -154,6 +155,30 @@ function main() {
154
155
  totalSkipped += result.skipped.length;
155
156
  }
156
157
 
158
+ // Regenerate the canonical skill allow-list (E50_S02) from the freshly mirrored
159
+ // `.agents/skills` BEFORE bootstrapping .github/copilot-instructions.md below — so a consumer
160
+ // install ships a correct, current allow-list from the very first `npm install`, not
161
+ // only after a later `/self-sync`. The output is written into this installed package's own
162
+ // `lib/` (not the consumer root — `lib/` is never duplicated into the consumer per this file's
163
+ // header comment), matching generateSkillAllowList's own default output location and the path
164
+ // the MCP router / native routing enforcement (E50_S02_T03/T04) read from at runtime.
165
+ //
166
+ // Ordering matters (E50_S02_T04): generateCopilotInstructions() below now renders an
167
+ // {{ALLOWED_SKILL_IDS}} block by reading this same lib/skill-allow-list.json path. Running
168
+ // this regeneration first means that render reflects the freshly-mirrored `.agents/skills`
169
+ // scan from this very postinstall run, not the stale artifact committed at publish time.
170
+ // Wrapped in a try/catch-and-warn pattern so a generation failure degrades gracefully
171
+ // (console warning) rather than failing the whole postinstall.
172
+ try {
173
+ const result = generateSkillAllowList(
174
+ path.join(consumerRoot, '.agents', 'skills'),
175
+ path.join(packageRoot, 'lib', 'skill-allow-list.json')
176
+ );
177
+ console.log(` ✓ lib/skill-allow-list.json regenerated (${result.skill_count} skills)`);
178
+ } catch (e) {
179
+ console.log(` ⚠ Could not regenerate lib/skill-allow-list.json — ${e.message}`);
180
+ }
181
+
157
182
  // Bootstrap .github/copilot-instructions.md unconditionally, right after the mirror step —
158
183
  // no interactive prompts, since postinstall runs unattended during `npm install`. Without
159
184
  // this, a consumer whose very first action is a Copilot slash command (before ever running
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # scripts/sweep-stale-context-digests.sh
4
+ #
5
+ # Staleness-cleanup backstop for project/queue/context/ digest files
6
+ # (E49_S01_T02). The PRIMARY cleanup path is scripts/consume-context-digest.sh,
7
+ # called by the receiving subagent once it has read its digest. This script
8
+ # exists for the cases that leaves uncovered:
9
+ # - a digest whose intended receiving session never starts (e.g. the
10
+ # dispatch it was written for was abandoned or superseded)
11
+ # - a receiver that reads the raw file directly instead of going through
12
+ # consume-context-digest.sh and forgets to delete it
13
+ #
14
+ # Invoked from hooks/on_session_end.sh on every session end (any agent), so
15
+ # it runs far more often than any single digest's realistic lifetime — a
16
+ # digest is meant to be consumed by the very next session dispatched for its
17
+ # task, not to sit for a full day. Age-based (mtime), not consumption-based:
18
+ # unlike project/queue/handoffs/ (routed and deleted deterministically, same
19
+ # hook invocation, by on_session_end.sh section 4), a digest's consumer is a
20
+ # LATER session that hasn't necessarily started yet when an unrelated
21
+ # session's SessionEnd hook fires — so this sweep must not delete a digest
22
+ # just because some other session ended in the meantime. The default
23
+ # threshold (24h) is deliberately generous for that reason.
24
+ #
25
+ # Usage:
26
+ # sweep-stale-context-digests.sh [--context-dir <dir>] [--max-age-seconds <n>]
27
+ #
28
+ # Options:
29
+ # --context-dir <dir> override project/queue/context (default:
30
+ # resolved via JENGA_PROJECT_DIR / git toplevel)
31
+ # --max-age-seconds <n> override the default 86400s (24h) threshold
32
+ # (env: CONTEXT_DIGEST_STALE_SECONDS)
33
+ # -h, --help
34
+ #
35
+ # Output (stdout): one line per file removed: "removed: <path> (age <n>s)"
36
+ #
37
+ # Exit codes:
38
+ # 0 ran successfully (including the common case of nothing to remove)
39
+ # 1 usage error
40
+ # ---------------------------------------------------------------------------
41
+
42
+ set -u
43
+
44
+ SELF="$(basename "$0")"
45
+
46
+ die() {
47
+ local code="$1"; shift
48
+ printf '%s: error: %s\n' "$SELF" "$*" >&2
49
+ exit "$code"
50
+ }
51
+
52
+ usage() {
53
+ sed -n '/^# Usage:/,/^# Exit codes:/p' "$0" | sed 's/^# \{0,1\}//' >&2
54
+ exit 1
55
+ }
56
+
57
+ resolve_project_dir() {
58
+ if [ -n "${JENGA_PROJECT_DIR:-}" ]; then
59
+ printf '%s\n' "$JENGA_PROJECT_DIR"
60
+ return 0
61
+ fi
62
+ if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
63
+ printf '%s\n' "$CLAUDE_PROJECT_DIR"
64
+ return 0
65
+ fi
66
+ git rev-parse --show-toplevel 2>/dev/null || pwd
67
+ }
68
+
69
+ CONTEXT_DIR=""
70
+ MAX_AGE_SECONDS="${CONTEXT_DIGEST_STALE_SECONDS:-86400}"
71
+
72
+ while [ "$#" -gt 0 ]; do
73
+ case "$1" in
74
+ --context-dir)
75
+ [ "$#" -ge 2 ] || die 1 "--context-dir requires a value"
76
+ CONTEXT_DIR="$2"; shift 2 ;;
77
+ --context-dir=*)
78
+ CONTEXT_DIR="${1#--context-dir=}"; shift ;;
79
+ --max-age-seconds)
80
+ [ "$#" -ge 2 ] || die 1 "--max-age-seconds requires a value"
81
+ MAX_AGE_SECONDS="$2"; shift 2 ;;
82
+ --max-age-seconds=*)
83
+ MAX_AGE_SECONDS="${1#--max-age-seconds=}"; shift ;;
84
+ -h|--help)
85
+ usage ;;
86
+ *)
87
+ die 1 "unknown argument: $1" ;;
88
+ esac
89
+ done
90
+
91
+ case "$MAX_AGE_SECONDS" in
92
+ ''|*[!0-9]*) die 1 "--max-age-seconds must be a non-negative integer, got '$MAX_AGE_SECONDS'" ;;
93
+ esac
94
+
95
+ if [ -z "$CONTEXT_DIR" ]; then
96
+ PROJECT_DIR="$(resolve_project_dir)"
97
+ CONTEXT_DIR="$PROJECT_DIR/project/queue/context"
98
+ fi
99
+
100
+ [ -d "$CONTEXT_DIR" ] || exit 0
101
+
102
+ # Portable mtime lookup: GNU stat (Linux) uses -c, BSD stat (macOS) uses -f —
103
+ # same fallback pattern as scripts/with-lock.sh's lock_mtime_epoch().
104
+ file_mtime_epoch() {
105
+ stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null
106
+ }
107
+
108
+ now_epoch() {
109
+ date +%s
110
+ }
111
+
112
+ NOW="$(now_epoch)"
113
+
114
+ for FILE in "$CONTEXT_DIR"/*.json; do
115
+ [ -e "$FILE" ] || continue
116
+ # Never sweep a file mid-claim by consume-context-digest.sh — that script
117
+ # renames its target to a .claimed.$$ sibling before reading it, and this
118
+ # glob (*.json) doesn't match that suffix, so in-flight claims are already
119
+ # excluded structurally. This comment documents that rather than adding a
120
+ # redundant runtime check.
121
+
122
+ MTIME="$(file_mtime_epoch "$FILE")" || continue
123
+ AGE=$(( NOW - MTIME ))
124
+
125
+ if [ "$AGE" -ge "$MAX_AGE_SECONDS" ]; then
126
+ if rm -f "$FILE" 2>/dev/null; then
127
+ echo "removed: $FILE (age ${AGE}s)"
128
+ fi
129
+ fi
130
+ done
131
+
132
+ exit 0
@@ -25,6 +25,11 @@ STATUS_VALUES = {
25
25
  "Blocked",
26
26
  "Backlog",
27
27
  "Done",
28
+ "Merged",
29
+ "Publicized",
30
+ "Privatized",
31
+ "Deployed to Stage",
32
+ "Deployed to Prod",
28
33
  }
29
34
 
30
35
  # E39 tiered item-level caution/escalation. crucial_level is OPTIONAL — absence means no