@jenga-ai/agent 3.5.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +85 -78
  2. package/agents/developer.md +1 -1
  3. package/agents/scrum-master.md +20 -2
  4. package/agents/tester.md +3 -3
  5. package/hooks/on_session_end.sh +5 -5
  6. package/lib/generate-agent-context.js +2 -2
  7. package/lib/generate-copilot-hooks.js +1 -1
  8. package/lib/generate-skill-allow-list.js +79 -8
  9. package/lib/mirror.js +1 -1
  10. package/lib/postinstall-manifest.js +1 -1
  11. package/lib/skill-allow-list.json +2 -2
  12. package/mcp/help/index.js +8 -17
  13. package/mcp/help/scan.js +73 -0
  14. package/package.json +5 -1
  15. package/project/app/api/lib/resolve-project-root.js +1 -1
  16. package/project/app/api/parsers/knowledge-graph.js +100 -9
  17. package/project/app/api/routes/health.js +36 -0
  18. package/project/app/api/scripts/capture-snapshot.js +9 -6
  19. package/project/app/package.json +4 -0
  20. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  21. package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
  22. package/project/app/ui/dist/index.html +2 -2
  23. package/project/app/ui/package.json +4 -0
  24. package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
  25. package/scripts/acquire-concurrency-slot.sh +35 -5
  26. package/scripts/apply-j-prefix.sh +46 -5
  27. package/scripts/build-pages-site.sh +1 -1
  28. package/scripts/check-public-playbook-steps.sh +158 -52
  29. package/scripts/check-publicignore-match.sh +2 -2
  30. package/scripts/compute-deploy-reconcile.sh +5 -5
  31. package/scripts/delete-bare-skill-dirs.sh +329 -0
  32. package/scripts/generate-legacy-shipped-paths.js +2 -2
  33. package/scripts/idea_manager.sh +273 -3
  34. package/scripts/mark-deployed.sh +2 -2
  35. package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
  36. package/scripts/populate-knowledge-graph.js +213 -5
  37. package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
  38. package/scripts/postinstall.js +1 -1
  39. package/scripts/release-concurrency-slot.sh +34 -4
  40. package/scripts/render-ranked-list.sh +270 -0
  41. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  42. package/scripts/repoint-skill-refs.sh +539 -0
  43. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  44. package/scripts/strip-polyfill-frontmatter.py +166 -0
  45. package/scripts/todo_manager.sh +16 -1
  46. package/scripts/validate-typed-object.sh +750 -0
  47. package/scripts/verify-legacy-seed-reconcile.sh +10 -10
  48. package/scripts/verify-postinstall-reconcile.sh +7 -7
  49. package/scripts/write-context-digest.sh +1 -1
  50. package/skills/j-brainstorm/SKILL.md +3 -4
  51. package/skills/j-btw/SKILL.md +3 -4
  52. package/skills/j-clearify/SKILL.md +5 -6
  53. package/skills/j-close-story/SKILL.md +11 -12
  54. package/skills/j-close-story/scripts/check-privatized.sh +4 -4
  55. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  56. package/skills/j-commit/SKILL.md +3 -4
  57. package/skills/j-continue/SKILL.md +5 -6
  58. package/skills/j-deep-dive/SKILL.md +3 -4
  59. package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
  60. package/skills/j-distribute/SKILL.md +3 -4
  61. package/skills/j-do/SKILL.md +100 -18
  62. package/skills/j-doc/SKILL.md +3 -4
  63. package/skills/j-doc-sync/SKILL.md +4 -4
  64. package/skills/j-dooo/SKILL.md +6 -15
  65. package/skills/j-error/SKILL.md +3 -4
  66. package/skills/j-evaluate/SKILL.md +3 -4
  67. package/skills/j-examplify/SKILL.md +3 -4
  68. package/skills/j-gitignore/SKILL.md +157 -0
  69. package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
  70. package/skills/j-gitignore/scripts/_catalog.sh +105 -0
  71. package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
  72. package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
  73. package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
  74. package/skills/j-help/SKILL.md +3 -4
  75. package/skills/j-idea/SKILL.md +80 -9
  76. package/skills/j-idea/assets/idea_template.md +1 -1
  77. package/skills/j-improve/SKILL.md +4 -5
  78. package/skills/j-init/SKILL.md +23 -14
  79. package/skills/j-init/assets/scope-thresholds_template.json +5 -2
  80. package/skills/j-init/scripts/apply-scaffold-visibility.sh +9 -7
  81. package/skills/j-init/scripts/init.sh +4 -4
  82. package/skills/j-jbp/SKILL.md +3 -4
  83. package/skills/j-lgtm/SKILL.md +3 -4
  84. package/skills/j-pi-plan/SKILL.md +3 -4
  85. package/skills/j-playbook/SKILL.md +1 -1
  86. package/skills/j-proceed/SKILL.md +3 -4
  87. package/skills/j-publish/SKILL.md +4 -5
  88. package/skills/j-publish/adapters/npm-ci.md +6 -1
  89. package/skills/j-publish/adapters/npm.md +1 -1
  90. package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
  91. package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
  92. package/skills/j-publish/scripts/run_gates.sh +1 -1
  93. package/skills/j-reconcile/SKILL.md +40 -7
  94. package/skills/j-reconcile/assets/report_format.md +11 -0
  95. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +13 -13
  96. package/skills/j-reconcile-origin/SKILL.md +3 -4
  97. package/skills/j-redo/SKILL.md +4 -5
  98. package/skills/j-skillify/SKILL.md +3 -4
  99. package/skills/j-spinoff/SKILL.md +4 -5
  100. package/skills/j-status/SKILL.md +18 -4
  101. package/skills/j-todo/SKILL.md +44 -5
  102. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  103. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  104. package/skills/j-uncharted/SKILL.md +252 -13
  105. package/skills/j-uncharted/scripts/detect-dependencies.sh +80 -22
  106. package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
  107. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  108. package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
  109. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  110. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  111. package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
  112. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  113. package/skills/j-wtf/SKILL.md +4 -5
  114. package/skills/jenga/SKILL.md +106 -11
  115. package/skills/jenga/playbooks/board-hygiene.json +32 -0
  116. package/skills/jenga/playbooks/schema.json +73 -6
  117. package/skills/jenga/playbooks/understand-then-commit.json +19 -0
  118. package/skills/jenga/scripts/load-nl-catalog.js +5 -2
  119. package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
  120. package/skills/jenga/scripts/load-playbooks.sh +290 -5
  121. package/skills/jenga/scripts/match-playbook.sh +4 -4
  122. package/skills/jenga/scripts/run-playbook-step.sh +267 -1
  123. package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
  124. package/templates/SKILL_TEMPLATE.md +12 -0
  125. package/templates/permission-levels/level-1-locked.json +1 -1
  126. package/templates/permission-levels/level-2-guarded.json +1 -1
  127. package/templates/permission-levels/level-3-standard.json +1 -1
  128. package/templates/permission-levels/level-4-elevated.json +2 -2
  129. package/templates/permission-levels/level-5-unrestricted.json +2 -2
  130. package/templates/playbook-types.json +40 -6
  131. package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
  132. package/project/app/ui/dist/assets/index-CdK3Qrep.css +0 -1
  133. package/scripts/audit-twin-divergence.sh +0 -625
  134. package/scripts/generate-j-alias.sh +0 -333
  135. package/skills/j-dev-done/SKILL.md +0 -53
  136. package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
@@ -53,7 +53,37 @@ resolve_project_dir() {
53
53
  git rev-parse --show-toplevel 2>/dev/null || pwd
54
54
  }
55
55
 
56
- SCRIPT_ABS_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
56
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
57
+ SCRIPT_ABS_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")"
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Resolve with-lock.sh.
61
+ #
62
+ # scripts/postinstall.js mirrors only skills/ and agents/ into a consumer's
63
+ # .claude/ and .agents/ — scripts/ (which owns with-lock.sh) is never copied
64
+ # there, so a consumer install has no <consumer>/scripts/with-lock.sh at all.
65
+ # Probing "$PROJECT_DIR/scripts/with-lock.sh" alone therefore hard-fails every
66
+ # consumer install, even though the file ships fine inside the package.
67
+ #
68
+ # The sibling probe is what fixes that: with-lock.sh always lands in the same
69
+ # directory as this script, in a monorepo checkout AND inside the installed
70
+ # npm package. The remaining two tiers mirror the three-tier shape already
71
+ # used by skills/j-uncharted/scripts/elicitation-state.sh.
72
+ # ---------------------------------------------------------------------------
73
+ resolve_with_lock() {
74
+ local candidate
75
+ for candidate in \
76
+ "$SCRIPT_DIR/with-lock.sh" \
77
+ "$PROJECT_DIR/scripts/with-lock.sh" \
78
+ "$PROJECT_DIR/node_modules/@jenga-ai/agent/scripts/with-lock.sh"
79
+ do
80
+ if [ -f "$candidate" ]; then
81
+ printf '%s\n' "$candidate"
82
+ return 0
83
+ fi
84
+ done
85
+ return 1
86
+ }
57
87
 
58
88
  # ---------------------------------------------------------------------------
59
89
  # Internal locked-mutation entrypoint. Not intended to be invoked directly —
@@ -113,15 +143,15 @@ PROJECT_DIR="$(resolve_project_dir)"
113
143
  COUNTER_FILE="$PROJECT_DIR/project/queue/concurrency-slots-${SESSION_ID}.json"
114
144
 
115
145
  command -v jq >/dev/null 2>&1 || die 5 "jq is required but not found on PATH"
116
- [ -f "$PROJECT_DIR/scripts/with-lock.sh" ] \
117
- || die 5 "scripts/with-lock.sh not found at $PROJECT_DIR/scripts/with-lock.sh"
146
+ WITH_LOCK="$(resolve_with_lock)" \
147
+ || die 5 "with-lock.sh not found (looked in $SCRIPT_DIR, $PROJECT_DIR/scripts, and $PROJECT_DIR/node_modules/@jenga-ai/agent/scripts)"
118
148
 
119
149
  # No counter file at all yet: nothing to release, idempotent no-op, no lock
120
150
  # needed.
121
151
  [ -f "$COUNTER_FILE" ] || exit 0
122
152
 
123
153
  set +e
124
- "$PROJECT_DIR/scripts/with-lock.sh" "$COUNTER_FILE" -- \
154
+ "$WITH_LOCK" "$COUNTER_FILE" -- \
125
155
  "$SCRIPT_ABS_PATH" __locked-release "$ROLE" "$HOLDER_ID" "$COUNTER_FILE"
126
156
  STATUS=$?
127
157
  set -e
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # scripts/render-ranked-list.sh
4
+ #
5
+ # Shared implementation of /dooo's step 2 (identify parallelisable
6
+ # stories/tasks against project/todo.md) and step 3 (render them as a
7
+ # numbered list). Extracted per E63_S01_T01 so this logic exists exactly
8
+ # once — /dooo calls this script instead of describing the scan/render as
9
+ # inline prose, and a sibling task (E63_S01_T03, not yet started) wires
10
+ # `/todo --ranked-list` onto the same script rather than writing a second,
11
+ # independent copy of the eligibility scan.
12
+ #
13
+ # ---------------------------------------------------------------------------
14
+ # THE ELIGIBILITY RULE (verbatim from /dooo's prior prose — do not alter here
15
+ # without updating the source-of-truth prose in the parent story/task board
16
+ # files first; this script is a faithful extraction, not a redesign)
17
+ # ---------------------------------------------------------------------------
18
+ # A story or task is eligible if ALL of:
19
+ # - its status is exactly `Pending`
20
+ # - it has no unresolved dependencies: every ID listed in its `depends_on`
21
+ # frontmatter field resolves to a board file whose own `status` is one of
22
+ # `Running`, `In Progress`, or `Passed`. The pre-extraction SKILL.md prose
23
+ # literally named only `Running`/`Passed`; `In Progress` is added here
24
+ # per this task's own board-file description of the rule (which paired
25
+ # `Running`/`In Progress` as the two items' in-work labels) and because no
26
+ # board file has ever actually carried `status: Running` (the schema's
27
+ # real in-work value is `In Progress` — see
28
+ # templates/SCRUM_BOARD_SCHEMA.md's Status Values table), so this is a
29
+ # faithful reproduction of the rule's real-world effect, not a behavior
30
+ # change. This three-value set otherwise still excludes `Passed with
31
+ # remarks`, `Merged`, and `Done`, same as the original prose.
32
+ # - it is directly listed in project/todo.md's real entries, OR (for tasks)
33
+ # its parent story is listed there
34
+ #
35
+ # Candidates are read from two directories: project/board/stories/*.md and
36
+ # project/board/tasks/*.md. A third directory, project/board/epics/*.md, is
37
+ # read only to resolve the status of a dependency that happens to be an
38
+ # epic ID.
39
+ #
40
+ # ---------------------------------------------------------------------------
41
+ # OUTPUT SHAPE — binding
42
+ # ---------------------------------------------------------------------------
43
+ # 1-indexed "<id> — <title>" lines, one per eligible item, stories first then
44
+ # tasks, each group in deterministic (sorted) board-file order. No trailing
45
+ # "Done" option, no prompt text — that is /dooo's own interactive framing,
46
+ # added on top of this script's output, not part of it.
47
+ #
48
+ # Fully deterministic: identical board/todo.md state produces byte-identical
49
+ # output across runs. No clock, no randomness, no filesystem-order dependence
50
+ # (directory reads are sorted under LC_ALL=C).
51
+ #
52
+ # ---------------------------------------------------------------------------
53
+ # USAGE
54
+ # ---------------------------------------------------------------------------
55
+ # scripts/render-ranked-list.sh
56
+ #
57
+ # Assumes CWD is the project root, matching scripts/board_resolver.sh and
58
+ # scripts/todo_manager.sh's existing convention. Paths may be overridden via
59
+ # environment variables (for test fixtures, or for callers rooted somewhere
60
+ # other than the CWD), in the style of validate-typed-object.sh's
61
+ # JENGA_PLAYBOOK_TYPES_FILE hook:
62
+ #
63
+ # JENGA_TODO_FILE Default: project/todo.md
64
+ # JENGA_BOARD_STORIES_DIR Default: project/board/stories
65
+ # JENGA_BOARD_TASKS_DIR Default: project/board/tasks
66
+ # JENGA_BOARD_EPICS_DIR Default: project/board/epics
67
+ # ---------------------------------------------------------------------------
68
+
69
+ set -euo pipefail
70
+ export LC_ALL=C
71
+
72
+ TODO_FILE="${JENGA_TODO_FILE:-project/todo.md}"
73
+ STORIES_DIR="${JENGA_BOARD_STORIES_DIR:-project/board/stories}"
74
+ TASKS_DIR="${JENGA_BOARD_TASKS_DIR:-project/board/tasks}"
75
+ EPICS_DIR="${JENGA_BOARD_EPICS_DIR:-project/board/epics}"
76
+
77
+ # The literal, verbatim-preserved "resolved" status set for a dependency.
78
+ RESOLVED_STATUSES="Running
79
+ In Progress
80
+ Passed"
81
+
82
+ is_resolved_status() {
83
+ local status="$1"
84
+ local s
85
+ while IFS= read -r s; do
86
+ if [ "$status" = "$s" ]; then
87
+ return 0
88
+ fi
89
+ done <<EOF
90
+ $RESOLVED_STATUSES
91
+ EOF
92
+ return 1
93
+ }
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Frontmatter helpers
97
+ # ---------------------------------------------------------------------------
98
+
99
+ # frontmatter_values <file> <field>
100
+ # Prints each value for <field> on its own line: a single line for an inline
101
+ # scalar (`field: value`), or one line per `- item` for a YAML block list
102
+ # (`field:` with nothing after the colon, followed by indented `- ` lines).
103
+ # Prints nothing if the field is absent or has an empty/None-ish value.
104
+ frontmatter_values() {
105
+ local file="$1" field="$2"
106
+ awk -v f="$field" '
107
+ BEGIN { infm = 0; found = 0 }
108
+ /^---[[:space:]]*$/ {
109
+ if (infm == 0) { infm = 1; next } else { exit }
110
+ }
111
+ infm == 1 {
112
+ if (found == 1) {
113
+ if ($0 ~ /^[[:space:]]*-[[:space:]]*/) {
114
+ line = $0
115
+ sub(/^[[:space:]]*-[[:space:]]*/, "", line)
116
+ sub(/[[:space:]]*$/, "", line)
117
+ print line
118
+ next
119
+ } else {
120
+ exit
121
+ }
122
+ }
123
+ if ($0 ~ "^" f ":[[:space:]]*$") { found = 1; next }
124
+ if ($0 ~ "^" f ":[[:space:]]") {
125
+ line = $0
126
+ sub("^" f ":[[:space:]]*", "", line)
127
+ sub(/[[:space:]]*$/, "", line)
128
+ print line
129
+ exit
130
+ }
131
+ if ($0 ~ "^" f ":$") { exit }
132
+ }
133
+ ' "$file"
134
+ }
135
+
136
+ frontmatter_scalar() {
137
+ frontmatter_values "$1" "$2" | head -n1
138
+ }
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # ID → status resolution
142
+ # ---------------------------------------------------------------------------
143
+
144
+ # id_dir <id> — picks the board directory an ID belongs to, by shape:
145
+ # 0 underscores -> epic, 1 -> story, 2 -> task.
146
+ id_dir() {
147
+ local id="$1"
148
+ local underscores
149
+ underscores=$(printf '%s' "$id" | tr -cd '_' | wc -c | tr -d ' ')
150
+ case "$underscores" in
151
+ 0) printf '%s' "$EPICS_DIR" ;;
152
+ 1) printf '%s' "$STORIES_DIR" ;;
153
+ *) printf '%s' "$TASKS_DIR" ;;
154
+ esac
155
+ }
156
+
157
+ # status_of <id> — prints the status of the board file matching <id>, or
158
+ # nothing if no such file exists (a dangling/unresolvable reference).
159
+ status_of() {
160
+ local id="$1"
161
+ local dir file
162
+ dir=$(id_dir "$id")
163
+ for file in "$dir"/"$id"_*.md; do
164
+ [ -e "$file" ] || continue
165
+ frontmatter_scalar "$file" "status"
166
+ return 0
167
+ done
168
+ return 0
169
+ }
170
+
171
+ # deps_resolved <file> — true (exit 0) if every depends_on ID resolves to a
172
+ # board file whose status is in RESOLVED_STATUSES. No depends_on, or only
173
+ # empty/None-ish entries, counts as resolved.
174
+ deps_resolved() {
175
+ local file="$1"
176
+ local dep status
177
+ while IFS= read -r dep; do
178
+ [ -z "$dep" ] && continue
179
+ case "$dep" in
180
+ [Nn]one) continue ;;
181
+ esac
182
+ status=$(status_of "$dep")
183
+ if [ -z "$status" ] || ! is_resolved_status "$status"; then
184
+ return 1
185
+ fi
186
+ done < <(frontmatter_values "$file" "depends_on")
187
+ return 0
188
+ }
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # todo.md real-entries + listing check
192
+ # ---------------------------------------------------------------------------
193
+
194
+ # Mirrors todo_manager.sh's real_entries() filter (blank lines, `#` comments,
195
+ # HTML `<!-- -->` comments stripped). Duplicated in miniature rather than
196
+ # shelled out to todo_manager.sh, since that script hardcodes its own
197
+ # project/todo.md path with no override hook and this script must also run
198
+ # against test fixtures and arbitrary project roots.
199
+ TODO_REAL_ENTRIES=""
200
+ if [ -f "$TODO_FILE" ]; then
201
+ TODO_REAL_ENTRIES=$(grep -v '^[[:space:]]*$' "$TODO_FILE" 2>/dev/null \
202
+ | grep -v '^[[:space:]]*#' \
203
+ | grep -v '^[[:space:]]*<!--' || true)
204
+ fi
205
+
206
+ # todo_lists <id> — true if <id> appears, word-bounded, in a real todo.md
207
+ # entry line.
208
+ todo_lists() {
209
+ local id="$1"
210
+ [ -z "$TODO_REAL_ENTRIES" ] && return 1
211
+ printf '%s\n' "$TODO_REAL_ENTRIES" \
212
+ | grep -Eq "(^|[^A-Za-z0-9_])${id}([^A-Za-z0-9_]|\$)"
213
+ }
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # Eligibility scan
217
+ # ---------------------------------------------------------------------------
218
+
219
+ ELIGIBLE=""
220
+
221
+ add_eligible() {
222
+ local id="$1" title="$2"
223
+ ELIGIBLE="${ELIGIBLE}${id}"$'\t'"${title}"$'\n'
224
+ }
225
+
226
+ scan_stories() {
227
+ local file id status title
228
+ for file in $(find "$STORIES_DIR" -maxdepth 1 -name '*.md' 2>/dev/null | sort); do
229
+ id=$(frontmatter_scalar "$file" "id")
230
+ [ -z "$id" ] && continue
231
+ status=$(frontmatter_scalar "$file" "status")
232
+ [ "$status" = "Pending" ] || continue
233
+ deps_resolved "$file" || continue
234
+ todo_lists "$id" || continue
235
+ title=$(frontmatter_scalar "$file" "title")
236
+ add_eligible "$id" "$title"
237
+ done
238
+ }
239
+
240
+ scan_tasks() {
241
+ local file id story_id status title
242
+ for file in $(find "$TASKS_DIR" -maxdepth 1 -name '*.md' 2>/dev/null | sort); do
243
+ id=$(frontmatter_scalar "$file" "id")
244
+ [ -z "$id" ] && continue
245
+ status=$(frontmatter_scalar "$file" "status")
246
+ [ "$status" = "Pending" ] || continue
247
+ deps_resolved "$file" || continue
248
+ story_id=$(frontmatter_scalar "$file" "story_id")
249
+ if todo_lists "$id" || { [ -n "$story_id" ] && todo_lists "$story_id"; }; then
250
+ title=$(frontmatter_scalar "$file" "title")
251
+ add_eligible "$id" "$title"
252
+ fi
253
+ done
254
+ }
255
+
256
+ scan_stories
257
+ scan_tasks
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Render
261
+ # ---------------------------------------------------------------------------
262
+
263
+ n=0
264
+ while IFS=$'\t' read -r id title; do
265
+ [ -z "$id" ] && continue
266
+ n=$((n + 1))
267
+ printf '%d. %s — %s\n' "$n" "$id" "$title"
268
+ done <<EOF
269
+ $ELIGIBLE
270
+ EOF
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ E50_S22_T03 — Repoint the remaining dead bare skills/<name>/ prose references and
4
+ fix two stale scripts/generate-j-alias.sh comments in mirror.sh.
5
+
6
+ Context: the story's Findings table counted 81 dead bare-skills/<name>/ references
7
+ across skills/j-*/SKILL.md bodies. T01 (frontmatter) and T02 (body preamble
8
+ rewrite) already resolved 79 of those 81 as a side effect of rewriting the exact
9
+ text blocks that contained them — T02's replacement text reintroduces one bare
10
+ reference per file, but explicitly in past tense ("was previously generated from
11
+ a bare `skills/<name>/SKILL.md` source; `E50_S15` deleted that directory"), which
12
+ is deliberate historical narration this task's own AC requires preserving.
13
+
14
+ Of the remaining references, only two required an actual fix:
15
+ 1. skills/j-do/SKILL.md's illustrative example "(e.g. `skills/do/SKILL.md`)" —
16
+ not historical narration, just an example that should use the current path.
17
+ 2. Two comments in skills/j-mirror-public/scripts/mirror.sh (L1363, L1523) that
18
+ still describe scripts/generate-j-alias.sh as a live generator.
19
+
20
+ Both are exact string replacements, verified present verbatim before substitution
21
+ so a mismatch fails loudly rather than silently no-op'ing.
22
+
23
+ Usage: scripts/repoint-dead-bare-path-prose.py [--dry-run]
24
+ Run from the repository root.
25
+ """
26
+ import sys
27
+
28
+ REPLACEMENTS = [
29
+ (
30
+ "skills/j-do/SKILL.md",
31
+ "(e.g. `skills/do/SKILL.md`).",
32
+ "(e.g. `skills/j-do/SKILL.md`).",
33
+ ),
34
+ (
35
+ "skills/j-mirror-public/scripts/mirror.sh",
36
+ "# skills/j-<name>/ twins are generated (scripts/generate-j-alias.sh) with",
37
+ "# skills/j-<name>/ twins were generated (scripts/generate-j-alias.sh, retired by E50_S14) with",
38
+ ),
39
+ (
40
+ "skills/j-mirror-public/scripts/mirror.sh",
41
+ 'log "orphaned-twin rewrite: frontmatter scripts/generate-j-alias.sh currently writes."',
42
+ 'log "orphaned-twin rewrite: standard hand-authored frontmatter shape (scripts/generate-j-alias.sh was retired by E50_S14)."',
43
+ ),
44
+ ]
45
+
46
+
47
+ def main():
48
+ dry_run = "--dry-run" in sys.argv
49
+ changed = []
50
+
51
+ for path, old, new in REPLACEMENTS:
52
+ with open(path, encoding="utf-8") as f:
53
+ content = f.read()
54
+ count = content.count(old)
55
+ if count == 0:
56
+ print(f"ERROR: expected text not found verbatim in {path}:", file=sys.stderr)
57
+ print(repr(old), file=sys.stderr)
58
+ sys.exit(1)
59
+ if count > 1:
60
+ print(
61
+ f"ERROR: expected text matched {count} times in {path} (expected exactly 1) — "
62
+ "refusing to guess which one:",
63
+ file=sys.stderr,
64
+ )
65
+ print(repr(old), file=sys.stderr)
66
+ sys.exit(1)
67
+ if old == new:
68
+ continue
69
+ new_content = content.replace(old, new, 1)
70
+ if not dry_run:
71
+ with open(path, "w", encoding="utf-8") as f:
72
+ f.write(new_content)
73
+ changed.append(path)
74
+
75
+ print(f"Changed {len(changed)} file(s):")
76
+ for p in changed:
77
+ print(f" {p}")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()