@jenga-ai/agent 3.6.0 → 4.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 (86) hide show
  1. package/README.md +32 -1
  2. package/lib/generate-skill-allow-list.js +42 -5
  3. package/lib/skill-allow-list.json +1 -1
  4. package/package.json +1 -2
  5. package/project/app/api/lib/resolve-project-root.js +1 -1
  6. package/project/app/api/scripts/capture-snapshot.js +12 -7
  7. package/project/app/package.json +4 -0
  8. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  9. package/project/app/ui/dist/assets/index-DX2pfTAW.js +104 -0
  10. package/project/app/ui/dist/index.html +2 -2
  11. package/scripts/acquire-concurrency-slot.sh +34 -4
  12. package/scripts/apply-j-prefix.sh +1 -1
  13. package/scripts/delete-bare-skill-dirs.sh +1 -2
  14. package/scripts/idea_manager.sh +16 -1
  15. package/scripts/release-concurrency-slot.sh +34 -4
  16. package/scripts/render-ranked-list.sh +270 -0
  17. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  18. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  19. package/scripts/strip-polyfill-frontmatter.py +166 -0
  20. package/scripts/todo_manager.sh +16 -1
  21. package/scripts/validate-typed-object.sh +750 -0
  22. package/scripts/verify-postinstall-reconcile.sh +33 -7
  23. package/skills/index/scripts/board_index.py +29 -1
  24. package/skills/j-brainstorm/SKILL.md +3 -4
  25. package/skills/j-btw/SKILL.md +3 -4
  26. package/skills/j-clearify/SKILL.md +3 -4
  27. package/skills/j-close-story/SKILL.md +11 -12
  28. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  29. package/skills/j-commit/SKILL.md +3 -4
  30. package/skills/j-continue/SKILL.md +5 -6
  31. package/skills/j-deep-dive/SKILL.md +3 -4
  32. package/skills/j-distribute/SKILL.md +3 -4
  33. package/skills/j-do/SKILL.md +13 -15
  34. package/skills/j-doc/SKILL.md +3 -4
  35. package/skills/j-doc/scripts/resolve_last_update.py +27 -1
  36. package/skills/j-doc-sync/SKILL.md +3 -4
  37. package/skills/j-dooo/SKILL.md +6 -15
  38. package/skills/j-error/SKILL.md +3 -4
  39. package/skills/j-evaluate/SKILL.md +3 -4
  40. package/skills/j-examplify/SKILL.md +3 -4
  41. package/skills/j-help/SKILL.md +3 -4
  42. package/skills/j-idea/SKILL.md +3 -4
  43. package/skills/j-improve/SKILL.md +3 -4
  44. package/skills/j-init/SKILL.md +20 -11
  45. package/skills/j-init/scripts/apply-scaffold-visibility.sh +8 -6
  46. package/skills/j-jbp/SKILL.md +3 -4
  47. package/skills/j-lgtm/SKILL.md +3 -4
  48. package/skills/j-pi-plan/SKILL.md +3 -4
  49. package/skills/j-proceed/SKILL.md +3 -4
  50. package/skills/j-publish/SKILL.md +3 -4
  51. package/skills/j-publish/scripts/run_gates.sh +1 -1
  52. package/skills/j-reconcile/SKILL.md +38 -5
  53. package/skills/j-reconcile/assets/report_format.md +11 -0
  54. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +2 -2
  55. package/skills/j-reconcile-origin/SKILL.md +3 -4
  56. package/skills/j-redo/SKILL.md +3 -4
  57. package/skills/j-skillify/SKILL.md +3 -4
  58. package/skills/j-spinoff/SKILL.md +3 -4
  59. package/skills/j-status/SKILL.md +4 -5
  60. package/skills/j-todo/SKILL.md +42 -5
  61. package/skills/j-todo/scripts/add_trivial_task.sh +12 -1
  62. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  63. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  64. package/skills/j-uncharted/SKILL.md +251 -12
  65. package/skills/j-uncharted/scripts/detect-dependencies.sh +79 -21
  66. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  67. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  68. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  69. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  70. package/skills/j-wtf/SKILL.md +3 -4
  71. package/skills/jenga/SKILL.md +106 -10
  72. package/skills/jenga/playbooks/schema.json +4 -4
  73. package/skills/jenga/scripts/enrich-nl-prompt.sh +225 -0
  74. package/skills/jenga/scripts/load-nl-catalog.js +6 -3
  75. package/skills/jenga/scripts/load-playbooks.sh +289 -4
  76. package/skills/jenga/scripts/match-playbook.sh +6 -5
  77. package/skills/jenga/scripts/run-playbook-step.sh +270 -3
  78. package/templates/permission-levels/level-1-locked.json +1 -1
  79. package/templates/permission-levels/level-2-guarded.json +1 -1
  80. package/templates/permission-levels/level-3-standard.json +1 -1
  81. package/templates/permission-levels/level-4-elevated.json +1 -1
  82. package/templates/permission-levels/level-5-unrestricted.json +1 -1
  83. package/templates/playbook-types.json +6 -0
  84. package/project/app/ui/dist/assets/index-BVR_7Owg.css +0 -1
  85. package/project/app/ui/dist/assets/index-CtU2xLQm.js +0 -104
  86. package/scripts/audit-twin-divergence.sh +0 -693
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Jenga AI Dashboard</title>
7
- <script type="module" crossorigin src="/assets/index-CtU2xLQm.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BVR_7Owg.css">
7
+ <script type="module" crossorigin src="/assets/index-DX2pfTAW.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BADc5mmH.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -90,7 +90,37 @@ resolve_project_dir() {
90
90
  # Absolute path to this script itself, so the lock-holding re-invocation
91
91
  # below works regardless of how this script was originally invoked (a
92
92
  # relative path, a PATH lookup, etc.) and regardless of cwd.
93
- SCRIPT_ABS_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
93
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
94
+ SCRIPT_ABS_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")"
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Resolve with-lock.sh.
98
+ #
99
+ # scripts/postinstall.js mirrors only skills/ and agents/ into a consumer's
100
+ # .claude/ and .agents/ — scripts/ (which owns with-lock.sh) is never copied
101
+ # there, so a consumer install has no <consumer>/scripts/with-lock.sh at all.
102
+ # Probing "$PROJECT_DIR/scripts/with-lock.sh" alone therefore hard-fails every
103
+ # consumer install, even though the file ships fine inside the package.
104
+ #
105
+ # The sibling probe is what fixes that: with-lock.sh always lands in the same
106
+ # directory as this script, in a monorepo checkout AND inside the installed
107
+ # npm package. The remaining two tiers mirror the three-tier shape already
108
+ # used by skills/j-uncharted/scripts/elicitation-state.sh.
109
+ # ---------------------------------------------------------------------------
110
+ resolve_with_lock() {
111
+ local candidate
112
+ for candidate in \
113
+ "$SCRIPT_DIR/with-lock.sh" \
114
+ "$PROJECT_DIR/scripts/with-lock.sh" \
115
+ "$PROJECT_DIR/node_modules/@jenga-ai/agent/scripts/with-lock.sh"
116
+ do
117
+ if [ -f "$candidate" ]; then
118
+ printf '%s\n' "$candidate"
119
+ return 0
120
+ fi
121
+ done
122
+ return 1
123
+ }
94
124
 
95
125
  # ---------------------------------------------------------------------------
96
126
  # Internal locked-mutation entrypoint. Not intended to be invoked directly —
@@ -206,13 +236,13 @@ COUNTER_FILE="$PROJECT_DIR/project/queue/concurrency-slots-${SESSION_ID}.json"
206
236
  CONFIG_FILE="$PROJECT_DIR/project/configs/scope-thresholds.json"
207
237
 
208
238
  command -v jq >/dev/null 2>&1 || die 5 "jq is required but not found on PATH"
209
- [ -x "$PROJECT_DIR/scripts/with-lock.sh" ] || [ -f "$PROJECT_DIR/scripts/with-lock.sh" ] \
210
- || die 5 "scripts/with-lock.sh not found at $PROJECT_DIR/scripts/with-lock.sh"
239
+ WITH_LOCK="$(resolve_with_lock)" \
240
+ || die 5 "with-lock.sh not found (looked in $SCRIPT_DIR, $PROJECT_DIR/scripts, and $PROJECT_DIR/node_modules/@jenga-ai/agent/scripts)"
211
241
 
212
242
  mkdir -p "$PROJECT_DIR/project/queue" 2>/dev/null || true
213
243
 
214
244
  set +e
215
- "$PROJECT_DIR/scripts/with-lock.sh" "$COUNTER_FILE" -- \
245
+ "$WITH_LOCK" "$COUNTER_FILE" -- \
216
246
  "$SCRIPT_ABS_PATH" __locked-acquire "$ROLE" "$HOLDER_ID" "$COUNTER_FILE" "$CONFIG_FILE"
217
247
  STATUS=$?
218
248
  set -e
@@ -61,7 +61,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
61
61
  # E50_S14_T02: disabled rather than left as an accepted info-level finding — this
62
62
  # script's AC requires a clean default-level shellcheck run, and the sourced file does
63
63
  # exist; shellcheck simply will not follow it without -x (same fix already applied to
64
- # scripts/audit-twin-divergence.sh and scripts/build-pages-site.sh).
64
+ # scripts/build-pages-site.sh).
65
65
  # shellcheck source=lib/resolve-project-dir.sh disable=SC1091
66
66
  source "$SCRIPT_DIR/../lib/resolve-project-dir.sh"
67
67
 
@@ -92,8 +92,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
92
92
  # This task's AC requires a clean default-level shellcheck run; SC1091 is an
93
93
  # info-level "did not follow sourced file" finding that would otherwise fail that
94
94
  # bar even though the sourced file does exist (see -x). Same fix already applied to
95
- # scripts/apply-j-prefix.sh, scripts/audit-twin-divergence.sh, and
96
- # scripts/build-pages-site.sh.
95
+ # scripts/apply-j-prefix.sh and scripts/build-pages-site.sh.
97
96
  # shellcheck source=lib/resolve-project-dir.sh disable=SC1091
98
97
  source "$SCRIPT_DIR/../lib/resolve-project-dir.sh"
99
98
 
@@ -3,9 +3,24 @@
3
3
  # Run from the repository root.
4
4
 
5
5
  IDEA_FILE="project/ideas.md"
6
- TEMPLATE="skills/j-idea/assets/idea_template.md"
7
6
  PLANS_DIR="project/documentation/plans"
8
7
 
8
+ # PACKAGE_ROOT locates the package-shipped idea template (skills/j-idea/assets/).
9
+ # This script always lives at scripts/, one level below the package root, in
10
+ # both layouts:
11
+ # - this monorepo checkout
12
+ # - a consumer install, inside node_modules/@jenga-ai/agent/
13
+ # BASH_SOURCE-derived resolution is exact in both cases (same reasoning as
14
+ # scripts/jenga-permission-level-switch.sh's PACKAGE_ROOT derivation), with a
15
+ # defensive node_modules-relative fallback matching the PKG_ROOT convention in
16
+ # skills/init/scripts/init.sh, in case BASH_SOURCE resolution is ever
17
+ # unavailable (e.g. the script is sourced rather than executed).
18
+ PACKAGE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19
+ TEMPLATE="$PACKAGE_ROOT/skills/j-idea/assets/idea_template.md"
20
+ if [ ! -f "$TEMPLATE" ]; then
21
+ TEMPLATE="node_modules/@jenga-ai/agent/skills/j-idea/assets/idea_template.md"
22
+ fi
23
+
9
24
  usage() {
10
25
  cat >&2 <<EOF
11
26
  Usage: $0 <subcommand> [args]
@@ -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()