@jenga-ai/agent 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +85 -78
  2. package/agents/developer.md +1 -1
  3. package/agents/scrum-master.md +20 -2
  4. package/agents/tester.md +3 -3
  5. package/hooks/on_session_end.sh +5 -5
  6. package/lib/generate-agent-context.js +2 -2
  7. package/lib/generate-copilot-hooks.js +1 -1
  8. package/lib/generate-skill-allow-list.js +37 -3
  9. package/lib/mirror.js +1 -1
  10. package/lib/postinstall-manifest.js +1 -1
  11. package/lib/skill-allow-list.json +2 -2
  12. package/mcp/help/index.js +8 -17
  13. package/mcp/help/scan.js +73 -0
  14. package/package.json +6 -1
  15. package/project/app/api/parsers/knowledge-graph.js +100 -9
  16. package/project/app/api/routes/health.js +36 -0
  17. package/project/app/api/scripts/capture-snapshot.js +9 -6
  18. package/project/app/ui/dist/assets/{index-CdK3Qrep.css → index-BVR_7Owg.css} +1 -1
  19. package/project/app/ui/dist/assets/index-CtU2xLQm.js +104 -0
  20. package/project/app/ui/dist/index.html +2 -2
  21. package/project/app/ui/package.json +4 -0
  22. package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
  23. package/scripts/acquire-concurrency-slot.sh +1 -1
  24. package/scripts/apply-j-prefix.sh +46 -5
  25. package/scripts/audit-twin-divergence.sh +73 -5
  26. package/scripts/build-pages-site.sh +1 -1
  27. package/scripts/check-public-playbook-steps.sh +158 -52
  28. package/scripts/check-publicignore-match.sh +2 -2
  29. package/scripts/compute-deploy-reconcile.sh +5 -5
  30. package/scripts/delete-bare-skill-dirs.sh +330 -0
  31. package/scripts/generate-legacy-shipped-paths.js +2 -2
  32. package/scripts/idea_manager.sh +258 -3
  33. package/scripts/mark-deployed.sh +2 -2
  34. package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
  35. package/scripts/populate-knowledge-graph.js +213 -5
  36. package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
  37. package/scripts/postinstall.js +1 -1
  38. package/scripts/repoint-skill-refs.sh +539 -0
  39. package/scripts/todo_manager.sh +1 -1
  40. package/scripts/verify-legacy-seed-reconcile.sh +10 -10
  41. package/scripts/verify-postinstall-reconcile.sh +7 -7
  42. package/scripts/write-context-digest.sh +1 -1
  43. package/skills/j-clearify/SKILL.md +2 -2
  44. package/skills/j-close-story/scripts/check-privatized.sh +4 -4
  45. package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
  46. package/skills/j-do/SKILL.md +101 -17
  47. package/skills/j-doc-sync/SKILL.md +1 -0
  48. package/skills/j-gitignore/SKILL.md +157 -0
  49. package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
  50. package/skills/j-gitignore/scripts/_catalog.sh +105 -0
  51. package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
  52. package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
  53. package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
  54. package/skills/j-idea/SKILL.md +78 -6
  55. package/skills/j-idea/assets/idea_template.md +1 -1
  56. package/skills/j-improve/SKILL.md +1 -1
  57. package/skills/j-init/SKILL.md +53 -14
  58. package/skills/j-init/assets/.gitignore_template +1 -2
  59. package/skills/j-init/assets/scope-thresholds_template.json +5 -2
  60. package/skills/j-init/scripts/apply-scaffold-visibility.sh +192 -0
  61. package/skills/j-init/scripts/init.sh +22 -8
  62. package/skills/j-playbook/SKILL.md +1 -1
  63. package/skills/j-publish/SKILL.md +1 -1
  64. package/skills/j-publish/adapters/npm-ci.md +6 -1
  65. package/skills/j-publish/adapters/npm.md +1 -1
  66. package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
  67. package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
  68. package/skills/j-reconcile/SKILL.md +2 -2
  69. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +11 -11
  70. package/skills/j-redo/SKILL.md +1 -1
  71. package/skills/j-skillify/assets/init-new/assets/.gitignore_template +1 -2
  72. package/skills/j-spinoff/SKILL.md +1 -1
  73. package/skills/j-status/SKILL.md +15 -0
  74. package/skills/j-todo/SKILL.md +3 -1
  75. package/skills/j-uncharted/SKILL.md +55 -8
  76. package/skills/j-uncharted/assets/NODE_QUESTION_TEMPLATE.md +69 -0
  77. package/skills/j-uncharted/scripts/detect-dependencies.sh +1 -1
  78. package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
  79. package/skills/j-uncharted/scripts/elicitation-state.sh +46 -8
  80. package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
  81. package/skills/j-wtf/SKILL.md +1 -1
  82. package/skills/jenga/SKILL.md +43 -9
  83. package/skills/jenga/playbooks/board-hygiene.json +32 -0
  84. package/skills/jenga/playbooks/schema.json +73 -6
  85. package/skills/jenga/playbooks/understand-then-commit.json +19 -0
  86. package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
  87. package/skills/jenga/scripts/load-playbooks.sh +23 -11
  88. package/skills/jenga/scripts/match-playbook.sh +1 -1
  89. package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
  90. package/templates/SKILL_TEMPLATE.md +12 -0
  91. package/templates/permission-levels/level-4-elevated.json +1 -1
  92. package/templates/permission-levels/level-5-unrestricted.json +1 -1
  93. package/templates/playbook-types.json +34 -6
  94. package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
  95. package/scripts/generate-j-alias.sh +0 -333
  96. package/skills/j-dev-done/SKILL.md +0 -53
  97. package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/delete-bare-skill-dirs.sh — delete the bare-name skill directories and
3
+ # rewrite their surviving j-<name> twins' frontmatter (E50_S15).
4
+ #
5
+ # Purpose
6
+ # -------
7
+ # Performs E50_S15's own irreversible step: for every skills/<name>/ that still has
8
+ # its original, pre-`j-` "bare" directory form alongside its canonical
9
+ # skills/j-<name>/ twin, (1) delete the bare directory outright and (2) rewrite the
10
+ # twin's SKILL.md frontmatter `name:` field from the transitional `j.j-<name>` form
11
+ # to the settled `j.<name>` form (E50_S10's contract). Both steps land together for
12
+ # every entry in one script run: renaming the twin first (while the bare directory
13
+ # still exists) would leave two directories both claiming `j.<name>`; deleting the
14
+ # bare directory first would leave no directory claiming it until the rename
15
+ # catches up. Neither intermediate state is ever produced by --apply, because this
16
+ # script performs every rewrite before it performs any delete (see step 4 below),
17
+ # and only after that first pass completes clean does it touch a filesystem entry.
18
+ #
19
+ # This script does NOT invoke `git commit`. E50_S15_T04 (the sole task allowed to
20
+ # run --apply) is responsible for staging and committing the resulting working-tree
21
+ # state as a single atomic commit immediately afterward — mirroring
22
+ # scripts/repoint-skill-refs.sh and scripts/apply-j-prefix.sh, neither of which
23
+ # commits on the caller's behalf either. Keeping VCS operations at the call site
24
+ # (not buried inside this script) keeps the script's own contract to "leave the
25
+ # working tree in the correct final state or abort with zero writes" — it does not
26
+ # also have to reason about repo-wide staging concerns (what else might already be
27
+ # staged, whether a lock file needs to be excluded, etc).
28
+ #
29
+ # Modes
30
+ # -----
31
+ # --dry-run Report the full computed delete set and, for each entry, the exact
32
+ # frontmatter line change planned. Nothing is written.
33
+ # --apply Perform the deletion + rename in place. Requires the live count to
34
+ # still match EXPECTED_DELETE_COUNT (see below) or the run aborts
35
+ # with zero filesystem writes.
36
+ #
37
+ # Exactly one of --dry-run/--apply is required. Either mode always runs the delta
38
+ # check and the count cross-check FIRST (see below) before touching or reporting on
39
+ # any file.
40
+ #
41
+ # Usage
42
+ # -----
43
+ # scripts/delete-bare-skill-dirs.sh --dry-run
44
+ # scripts/delete-bare-skill-dirs.sh --apply
45
+ #
46
+ # Delete-set computation — never hardcoded
47
+ # ------------------------------------------
48
+ # Computed fresh from disk on every invocation: every directory directly under
49
+ # skills/ that (a) is not itself "j-"-prefixed, (b) is not one of the three
50
+ # permanent exceptions (jenga, jenga-permission-level, index — E50_S06/E50), and
51
+ # (c) has a matching skills/j-<name>/ twin on disk. E50_S15_T01 already showed the
52
+ # live tree can drift from the story's authoring-time snapshot (6 orphan
53
+ # j-<name>-only directories were added after the story was written) — a hardcoded
54
+ # name list would silently miss a future drift the exact same way, so the set is
55
+ # always recomputed, never literal.
56
+ #
57
+ # Count cross-check
58
+ # ------------------
59
+ # EXPECTED_DELETE_COUNT below is the one hardcoded number in this script — not a
60
+ # name list, a single integer recorded by E50_S15_T01's live pre-flight recompute
61
+ # (2026-09-19: 40). If the live computed delete-set size does not equal this
62
+ # number, the script aborts with a non-zero exit and performs zero filesystem
63
+ # writes, in either mode. This catches the case where skills/ drifted again
64
+ # between T01's recompute and this script actually running --apply.
65
+ #
66
+ # Bare-vs-twin delta check
67
+ # --------------------------
68
+ # Before computing the delete set, every bare (non-"j-"-prefixed, non-exception)
69
+ # directory under skills/ MUST have a skills/j-<name>/ twin on disk. Any unpaired
70
+ # bare directory aborts the run (naming it) before any write — mirrors
71
+ # scripts/repoint-skill-refs.sh's identical guard.
72
+ #
73
+ # Frontmatter rewrite
74
+ # ---------------------
75
+ # For every delete-set entry <name>, skills/j-<name>/SKILL.md's frontmatter `name:`
76
+ # field is rewritten from `j.j-<name>` to `j.<name>`. A twin whose frontmatter does
77
+ # not already read exactly `j.j-<name>` is treated as an error (aborts before any
78
+ # write) rather than silently skipped or forced — the whole point of this
79
+ # cross-check is that any twin already in an unexpected state must be looked at by
80
+ # a human, not silently overwritten or silently left behind.
81
+ #
82
+ # Never touches the generated mirrors
83
+ # --------------------------------------
84
+ # This script only ever reads/writes under skills/. It has no code path that
85
+ # touches .claude/skills/ or .agents/skills/ — those are regenerated by
86
+ # /self-sync (E50_S15_T05), never hand-edited here, per CLAUDE.md's standing
87
+ # read-only-mirror rule.
88
+
89
+ set -euo pipefail
90
+
91
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
92
+ # This task's AC requires a clean default-level shellcheck run; SC1091 is an
93
+ # info-level "did not follow sourced file" finding that would otherwise fail that
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.
97
+ # shellcheck source=lib/resolve-project-dir.sh disable=SC1091
98
+ source "$SCRIPT_DIR/../lib/resolve-project-dir.sh"
99
+
100
+ PROJECT_DIR="$JENGA_PROJECT_DIR"
101
+ SKILLS_DIR="$PROJECT_DIR/skills"
102
+
103
+ # ---- Single named source: directory exception list (mirrors repoint-skill-refs.sh) ----
104
+ DELETE_BARE_SKILL_DIRS_EXCEPTIONS=(jenga jenga-permission-level index)
105
+
106
+ is_exception() {
107
+ local name="$1" ex
108
+ for ex in "${DELETE_BARE_SKILL_DIRS_EXCEPTIONS[@]}"; do
109
+ if [ "$name" = "$ex" ]; then
110
+ return 0
111
+ fi
112
+ done
113
+ return 1
114
+ }
115
+
116
+ # ---- Single named source: expected delete-set count (E50_S15_T01, 2026-09-19) ----
117
+ EXPECTED_DELETE_COUNT=40
118
+
119
+ usage() {
120
+ cat >&2 <<'EOF'
121
+ Usage: delete-bare-skill-dirs.sh (--dry-run|--apply)
122
+
123
+ --dry-run Report the computed delete set and planned frontmatter rewrites.
124
+ Writes nothing.
125
+ --apply Delete every bare directory in the delete set and rewrite its
126
+ twin's frontmatter name: line, in one pass. Aborts with zero
127
+ writes if the live delete-set count does not match the recorded
128
+ expected count.
129
+ EOF
130
+ }
131
+
132
+ MODE=""
133
+
134
+ while [ "$#" -gt 0 ]; do
135
+ case "$1" in
136
+ --dry-run)
137
+ MODE="dry-run"
138
+ shift
139
+ ;;
140
+ --apply)
141
+ MODE="apply"
142
+ shift
143
+ ;;
144
+ -h|--help)
145
+ usage
146
+ exit 0
147
+ ;;
148
+ *)
149
+ echo "delete-bare-skill-dirs.sh: error: unknown argument '$1'" >&2
150
+ usage
151
+ exit 1
152
+ ;;
153
+ esac
154
+ done
155
+
156
+ if [ -z "$MODE" ]; then
157
+ echo "delete-bare-skill-dirs.sh: error: exactly one of --dry-run or --apply is required" >&2
158
+ usage
159
+ exit 1
160
+ fi
161
+
162
+ # ============================================================================
163
+ # 1. Live bare-vs-twin delta check — ALWAYS runs first, before any file I/O.
164
+ # ============================================================================
165
+ BARE_DIRS=()
166
+ TWIN_NAMES=()
167
+ UNPAIRED=()
168
+
169
+ if [ -d "$SKILLS_DIR" ]; then
170
+ for d in "$SKILLS_DIR"/*/; do
171
+ [ -d "$d" ] || continue
172
+ name="$(basename "$d")"
173
+ case "$name" in
174
+ j-*)
175
+ TWIN_NAMES+=("${name#j-}")
176
+ continue
177
+ ;;
178
+ esac
179
+ BARE_DIRS+=("$name")
180
+ done
181
+ fi
182
+
183
+ is_twin_name() {
184
+ local target="$1" n
185
+ for n in "${TWIN_NAMES[@]-}"; do
186
+ [ -n "${n:-}" ] || continue
187
+ if [ "$n" = "$target" ]; then
188
+ return 0
189
+ fi
190
+ done
191
+ return 1
192
+ }
193
+
194
+ for name in "${BARE_DIRS[@]-}"; do
195
+ [ -n "${name:-}" ] || continue
196
+ if is_exception "$name"; then
197
+ continue
198
+ fi
199
+ if ! is_twin_name "$name"; then
200
+ UNPAIRED+=("$name")
201
+ fi
202
+ done
203
+
204
+ echo "== delete-bare-skill-dirs.sh: bare-vs-twin delta =="
205
+ echo " bare directories under skills/ (incl. exceptions): ${#BARE_DIRS[@]}"
206
+ echo " twin directories (skills/j-<name>/): $(printf '%s\n' "${TWIN_NAMES[@]-}" | grep -c . || true)"
207
+ if [ "${#UNPAIRED[@]}" -gt 0 ]; then
208
+ echo " UNPAIRED bare directories (no twin, not on the exception list):"
209
+ for u in "${UNPAIRED[@]}"; do
210
+ echo " - $u"
211
+ done
212
+ echo "delete-bare-skill-dirs.sh: error: the directory(ies) named above have no skills/j-<name>/ twin and are not on the exception list. Nothing was written. Disposition each one before running this script." >&2
213
+ exit 1
214
+ fi
215
+ echo " delta check clean — every bare directory is either excepted or paired with a twin."
216
+ echo
217
+
218
+ # ============================================================================
219
+ # 2. Compute the delete set: every bare, non-excepted directory with a twin.
220
+ # ============================================================================
221
+ DELETE_SET=()
222
+ for name in "${BARE_DIRS[@]-}"; do
223
+ [ -n "${name:-}" ] || continue
224
+ if is_exception "$name"; then
225
+ continue
226
+ fi
227
+ DELETE_SET+=("$name")
228
+ done
229
+
230
+ # Sort for deterministic, reviewable output.
231
+ if [ "${#DELETE_SET[@]}" -gt 0 ]; then
232
+ SORTED_DELETE_SET=()
233
+ while IFS= read -r n; do
234
+ [ -n "$n" ] || continue
235
+ SORTED_DELETE_SET+=("$n")
236
+ done < <(printf '%s\n' "${DELETE_SET[@]}" | sort)
237
+ DELETE_SET=("${SORTED_DELETE_SET[@]}")
238
+ fi
239
+
240
+ echo "== delete-bare-skill-dirs.sh: computed delete set =="
241
+ echo " count: ${#DELETE_SET[@]} (expected: $EXPECTED_DELETE_COUNT)"
242
+ if [ "${#DELETE_SET[@]}" -ne "$EXPECTED_DELETE_COUNT" ]; then
243
+ echo "delete-bare-skill-dirs.sh: error: live delete-set count (${#DELETE_SET[@]}) does not match the recorded expected count ($EXPECTED_DELETE_COUNT, from E50_S15_T01's 2026-09-19 pre-flight recompute). Nothing was written. skills/ has drifted since that recompute -- re-run E50_S15_T01's checks before proceeding." >&2
244
+ exit 1
245
+ fi
246
+ for n in "${DELETE_SET[@]}"; do
247
+ echo " - $n"
248
+ done
249
+ echo " count check clean."
250
+ echo
251
+
252
+ # ============================================================================
253
+ # 3. Pre-validate every twin's current frontmatter name: line before writing
254
+ # anything, in either mode -- an unexpected frontmatter state aborts the
255
+ # whole run rather than being silently skipped or forced.
256
+ # ============================================================================
257
+ PLANNED=() # "name|old_line|new_line" per entry, in DELETE_SET order
258
+
259
+ for name in "${DELETE_SET[@]}"; do
260
+ twin_skill_md="$SKILLS_DIR/j-$name/SKILL.md"
261
+ if [ ! -f "$twin_skill_md" ]; then
262
+ echo "delete-bare-skill-dirs.sh: error: expected twin file not found: $twin_skill_md. Nothing was written." >&2
263
+ exit 1
264
+ fi
265
+ current_line="$(grep -m1 -E '^name:[[:space:]]' "$twin_skill_md" || true)"
266
+ expected_old="name: j.j-${name}"
267
+ new_line="name: j.${name}"
268
+ if [ "$current_line" != "$expected_old" ]; then
269
+ echo "delete-bare-skill-dirs.sh: error: $twin_skill_md's frontmatter name: line reads '$current_line', expected exactly '$expected_old'. Nothing was written. This twin is in an unexpected state -- resolve it manually before re-running." >&2
270
+ exit 1
271
+ fi
272
+ PLANNED+=("${name}|${expected_old}|${new_line}")
273
+ done
274
+
275
+ echo "== delete-bare-skill-dirs.sh: planned frontmatter rewrites (${#PLANNED[@]}) =="
276
+ for entry in "${PLANNED[@]}"; do
277
+ IFS='|' read -r name old_line new_line <<< "$entry"
278
+ echo " skills/j-${name}/SKILL.md: '${old_line}' -> '${new_line}'"
279
+ done
280
+ echo
281
+
282
+ if [ "$MODE" = "dry-run" ]; then
283
+ echo "== delete-bare-skill-dirs.sh: dry-run complete — ${#DELETE_SET[@]} director(ies) would be deleted, ${#PLANNED[@]} frontmatter line(s) would be rewritten =="
284
+ echo " Bare directories that would be deleted:"
285
+ for name in "${DELETE_SET[@]}"; do
286
+ echo " - skills/${name}/"
287
+ done
288
+ echo " No files were written. Run with --apply to perform this (E50_S15_T04 only)."
289
+ exit 0
290
+ fi
291
+
292
+ # ============================================================================
293
+ # 4. --apply: rewrite every twin's frontmatter FIRST (full pass), then delete
294
+ # every bare directory (full pass). Doing all rewrites before any delete
295
+ # means a mid-run failure during the rewrite pass leaves every bare
296
+ # directory still present (recoverable: nothing deleted yet), and a
297
+ # mid-run failure during the delete pass leaves every twin already
298
+ # correctly renamed (recoverable: re-run recomputes a smaller, still-valid
299
+ # delete set next time, since a twin with no more `j.j-` prefix simply
300
+ # won't re-match the pre-validation above for whichever entries already
301
+ # completed). Neither pass order can leave a "two directories claim
302
+ # j.<name>" state or a "no directory claims it" state uncommitted, because
303
+ # a bare directory is never deleted before its own twin's rename has
304
+ # already succeeded.
305
+ # ============================================================================
306
+ echo "== delete-bare-skill-dirs.sh: applying frontmatter rewrites =="
307
+ for entry in "${PLANNED[@]}"; do
308
+ IFS='|' read -r name old_line new_line <<< "$entry"
309
+ twin_skill_md="$SKILLS_DIR/j-${name}/SKILL.md"
310
+ tmp_file="$(mktemp "${twin_skill_md}.rename.XXXXXX")"
311
+ cp -p "$twin_skill_md" "$tmp_file"
312
+ awk -v old="$old_line" -v new="$new_line" '
313
+ $0 == old && !done { print new; done=1; next }
314
+ { print }
315
+ ' "$twin_skill_md" > "$tmp_file"
316
+ mv "$tmp_file" "$twin_skill_md"
317
+ echo " rewrote skills/j-${name}/SKILL.md"
318
+ done
319
+ echo
320
+
321
+ echo "== delete-bare-skill-dirs.sh: deleting bare directories =="
322
+ for name in "${DELETE_SET[@]}"; do
323
+ rm -rf "${SKILLS_DIR:?}/${name:?}"
324
+ echo " deleted skills/${name}/"
325
+ done
326
+ echo
327
+
328
+ echo "== delete-bare-skill-dirs.sh: apply complete — ${#DELETE_SET[@]} director(ies) deleted, ${#PLANNED[@]} frontmatter line(s) rewritten =="
329
+ echo " Working tree now needs a single commit covering both changes (this script does not commit)."
330
+ exit 0
@@ -35,7 +35,7 @@
35
35
  * and unions it with the paths CURRENTLY on disk under this repo's own `skills/`
36
36
  * and `agents/` directories — i.e. "what this release is about to ship" folds into
37
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
38
+ * (`skills/j-publish/scripts/npm_pipeline.sh` / `npm_ci_pipeline.sh`, via the
39
39
  * `generate:legacy-paths` npm script) so every future publish keeps the list
40
40
  * current with zero network dependency and zero publish-time registry flakiness.
41
41
  *
@@ -57,7 +57,7 @@
57
57
  * "generated_at": "<ISO 8601>",
58
58
  * "package": "@jenga-ai/agent",
59
59
  * "source": "bootstrap-from-registry+incremental" | "incremental",
60
- * "paths": ["agents/developer.md", "skills/do/SKILL.md", ...]
60
+ * "paths": ["agents/developer.md", "skills/j-do/SKILL.md", ...]
61
61
  * }
62
62
  *
63
63
  * `paths` are relative to a mirror root, POSIX-separated, deduped and sorted — matching the same
@@ -3,15 +3,35 @@
3
3
  # Run from the repository root.
4
4
 
5
5
  IDEA_FILE="project/ideas.md"
6
- TEMPLATE="skills/idea/assets/idea_template.md"
6
+ TEMPLATE="skills/j-idea/assets/idea_template.md"
7
+ PLANS_DIR="project/documentation/plans"
7
8
 
8
9
  usage() {
9
10
  cat >&2 <<EOF
10
11
  Usage: $0 <subcommand> [args]
11
12
 
12
13
  Subcommands:
13
- add "<entry>" Append entry to project/ideas.md (auto-creates from template if missing)
14
- list Print all non-comment, non-blank entries (silent if file missing/empty)
14
+ add "<entry>" Append entry to project/ideas.md (auto-creates from
15
+ template if missing)
16
+ list Print all non-comment, non-blank entries (silent if
17
+ file missing/empty)
18
+ tag "<idea-text>" <TAG> Tag an existing idea line. <TAG> is either
19
+ "PROMOTED:E##_S##[_T##]" or "REJECTED". Errors if the
20
+ line is not found, is ambiguous, or is already tagged.
21
+ get-id "<idea-text>" Print the embedded board ID from a PROMOTED line
22
+ (errors if the line has no PROMOTED tag).
23
+ get-rapport "<idea-text>" Print the source-rapport path recorded on the line via
24
+ a "<!-- rapport: <path> -->" comment. Prints nothing
25
+ (exit 0) if the line has no such comment.
26
+ resolve "<idea-text>" <promoted|rejected> [board_id]
27
+ The full outcome-record mechanism: tags the line, then
28
+ either updates the idea's source rapport (decision,
29
+ date, resulting board ID, [SPIKE]-prefixed title) or,
30
+ if the idea has no source rapport, creates
31
+ project/documentation/plans/<slug>_PROMOTED.md or
32
+ _REJECTED.md with the same content. board_id is
33
+ required for "promoted" and must be omitted for
34
+ "rejected".
15
35
  EOF
16
36
  exit 1
17
37
  }
@@ -23,6 +43,129 @@ real_entries() {
23
43
  | grep -v '^\s*<!--'
24
44
  }
25
45
 
46
+ # find_idea_line <idea-text>
47
+ # Prints "LINE_NUM:CONTENT" for the single line in $IDEA_FILE matching <idea-text> literally.
48
+ # Errors (exit 1) if the file is missing, no line matches, or more than one line matches.
49
+ find_idea_line() {
50
+ local needle="$1"
51
+ local matches
52
+ if [ ! -f "$IDEA_FILE" ]; then
53
+ echo "Error: $IDEA_FILE does not exist" >&2
54
+ return 1
55
+ fi
56
+ matches=$(grep -Fn -- "$needle" "$IDEA_FILE")
57
+ if [ -z "$matches" ]; then
58
+ echo "Error: no line in $IDEA_FILE matches: $needle" >&2
59
+ return 1
60
+ fi
61
+ if [ "$(printf '%s\n' "$matches" | wc -l | tr -d ' ')" -gt 1 ]; then
62
+ echo "Error: idea text matches multiple lines (ambiguous), refine it: $needle" >&2
63
+ return 1
64
+ fi
65
+ printf '%s\n' "$matches"
66
+ }
67
+
68
+ # tag_line <line-num> <tag-text>
69
+ # Appends " <!-- <tag-text> -->" to the given line number of $IDEA_FILE, in place.
70
+ # Uses awk string concatenation (not sed substitution) so arbitrary idea text/paths
71
+ # containing &, /, or other sed-special characters are never re-interpreted.
72
+ tag_line() {
73
+ local linenum="$1"
74
+ local tagtext="$2"
75
+ local tmpfile
76
+ tmpfile="${IDEA_FILE}.tmp.$$"
77
+ awk -v n="$linenum" -v suffix=" <!-- ${tagtext} -->" \
78
+ 'NR==n { $0 = $0 suffix } { print }' "$IDEA_FILE" > "$tmpfile" && mv "$tmpfile" "$IDEA_FILE"
79
+ }
80
+
81
+ # slugify <text> — lowercase, non-alphanumeric runs collapsed to a single '-', trimmed, capped
82
+ slugify() {
83
+ printf '%s' "$1" \
84
+ | tr '[:upper:]' '[:lower:]' \
85
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \
86
+ | cut -c1-60
87
+ }
88
+
89
+ # extract_rapport_link <line-content> — prints the rapport: path, or nothing if absent
90
+ extract_rapport_link() {
91
+ printf '%s' "$1" | grep -oE '<!-- rapport: [^>]*-->' | sed -E 's/<!-- rapport: *([^ ]+) *-->/\1/'
92
+ }
93
+
94
+ # extract_promoted_id <line-content> — prints the embedded board ID, or nothing if absent
95
+ extract_promoted_id() {
96
+ printf '%s' "$1" | grep -oE '<!-- PROMOTED: [^>]*-->' | sed -E 's/<!-- PROMOTED: *([^ ]+) *-->/\1/'
97
+ }
98
+
99
+ # already_tagged <line-content> — returns 0 (true) if the line already has a terminal tag
100
+ already_tagged() {
101
+ case "$1" in
102
+ *"<!-- PROMOTED"*|*"<!-- REJECTED"*) return 0 ;;
103
+ *) return 1 ;;
104
+ esac
105
+ }
106
+
107
+ # validate_board_id <id> — loose shape check: E<digits> optionally _S<digits> optionally _T<digits>
108
+ validate_board_id() {
109
+ case "$1" in
110
+ E[0-9]*) return 0 ;;
111
+ *) return 1 ;;
112
+ esac
113
+ }
114
+
115
+ # update_rapport_outcome <rapport-path> <decision-label> <date> <board-id> <idea-text>
116
+ # Prefixes the rapport's "# Rapport: ..." title with "[SPIKE] " if not already present, then
117
+ # appends an "## Idea Outcome Record" section with the decision.
118
+ update_rapport_outcome() {
119
+ local rapport_path="$1" decision_label="$2" today="$3" board_id="$4" idea_text="$5"
120
+ local tmpfile
121
+ tmpfile="${rapport_path}.tmp.$$"
122
+
123
+ awk '
124
+ /^# Rapport:/ && $0 !~ /\[SPIKE\]/ {
125
+ sub(/^# Rapport: /, "# Rapport: [SPIKE] ")
126
+ }
127
+ { print }
128
+ ' "$rapport_path" > "$tmpfile" && mv "$tmpfile" "$rapport_path"
129
+
130
+ {
131
+ echo ""
132
+ echo "---"
133
+ echo ""
134
+ echo "## Idea Outcome Record"
135
+ echo ""
136
+ echo "**Decision:** ${decision_label}"
137
+ echo "**Date:** ${today} (UTC)"
138
+ if [ "$decision_label" = "Promoted" ]; then
139
+ echo "**Resulting Board ID:** ${board_id}"
140
+ fi
141
+ echo "**Idea:** ${idea_text}"
142
+ } >> "$rapport_path"
143
+ }
144
+
145
+ # write_fallback_outcome <idea-text> <decision-label> <date> <board-id> <file-suffix>
146
+ # Creates project/documentation/plans/<slug><file-suffix> and prints its path.
147
+ write_fallback_outcome() {
148
+ local idea_text="$1" decision_label="$2" today="$3" board_id="$4" suffix="$5"
149
+ local slug outfile
150
+ slug=$(slugify "$idea_text")
151
+ mkdir -p "$PLANS_DIR"
152
+ outfile="${PLANS_DIR}/${slug}${suffix}"
153
+ {
154
+ echo "# Idea Outcome: ${idea_text}"
155
+ echo ""
156
+ echo "**Decision:** ${decision_label}"
157
+ echo "**Date:** ${today} (UTC)"
158
+ if [ "$decision_label" = "Promoted" ]; then
159
+ echo "**Resulting Board ID:** ${board_id}"
160
+ fi
161
+ echo ""
162
+ echo "## Idea"
163
+ echo ""
164
+ echo "${idea_text}"
165
+ } > "$outfile"
166
+ printf '%s' "$outfile"
167
+ }
168
+
26
169
  case "${1:-}" in
27
170
  add)
28
171
  [ -z "${2:-}" ] && { echo "Error: add requires an entry argument" >&2; exit 1; }
@@ -41,6 +184,118 @@ case "${1:-}" in
41
184
  exit 0
42
185
  ;;
43
186
 
187
+ tag)
188
+ [ -z "${2:-}" ] && { echo "Error: tag requires an idea-text argument" >&2; exit 1; }
189
+ [ -z "${3:-}" ] && { echo "Error: tag requires a TAG argument (PROMOTED:E##_S##[_T##] or REJECTED)" >&2; exit 1; }
190
+ MATCH=$(find_idea_line "$2") || exit 1
191
+ LINENUM=$(printf '%s' "$MATCH" | cut -d: -f1)
192
+ CONTENT=$(printf '%s' "$MATCH" | cut -d: -f2-)
193
+ if already_tagged "$CONTENT"; then
194
+ echo "Error: idea line is already tagged; refusing to double-tag: $CONTENT" >&2
195
+ exit 1
196
+ fi
197
+ RAW_TAG="$3"
198
+ case "$RAW_TAG" in
199
+ PROMOTED:*)
200
+ BOARD_ID="${RAW_TAG#PROMOTED:}"
201
+ if ! validate_board_id "$BOARD_ID"; then
202
+ echo "Error: invalid board ID format in PROMOTED tag: $BOARD_ID (expected E##_S##[_T##] or E##)" >&2
203
+ exit 1
204
+ fi
205
+ TAG_TEXT="PROMOTED: $BOARD_ID"
206
+ ;;
207
+ REJECTED)
208
+ TAG_TEXT="REJECTED"
209
+ ;;
210
+ *)
211
+ echo "Error: TAG must be PROMOTED:E##_S##[_T##] or REJECTED, got: $RAW_TAG" >&2
212
+ exit 1
213
+ ;;
214
+ esac
215
+ tag_line "$LINENUM" "$TAG_TEXT"
216
+ echo "Tagged line $LINENUM: <!-- $TAG_TEXT -->"
217
+ ;;
218
+
219
+ get-id)
220
+ [ -z "${2:-}" ] && { echo "Error: get-id requires an idea-text argument" >&2; exit 1; }
221
+ MATCH=$(find_idea_line "$2") || exit 1
222
+ CONTENT=$(printf '%s' "$MATCH" | cut -d: -f2-)
223
+ ID=$(extract_promoted_id "$CONTENT")
224
+ if [ -z "$ID" ]; then
225
+ echo "Error: no PROMOTED tag found on matching line" >&2
226
+ exit 1
227
+ fi
228
+ echo "$ID"
229
+ ;;
230
+
231
+ get-rapport)
232
+ [ -z "${2:-}" ] && { echo "Error: get-rapport requires an idea-text argument" >&2; exit 1; }
233
+ MATCH=$(find_idea_line "$2") || exit 1
234
+ CONTENT=$(printf '%s' "$MATCH" | cut -d: -f2-)
235
+ extract_rapport_link "$CONTENT"
236
+ exit 0
237
+ ;;
238
+
239
+ resolve)
240
+ [ -z "${2:-}" ] && { echo "Error: resolve requires an idea-text argument" >&2; exit 1; }
241
+ [ -z "${3:-}" ] && { echo "Error: resolve requires a decision argument: promoted|rejected" >&2; exit 1; }
242
+ IDEA_TEXT="$2"
243
+ DECISION="$3"
244
+ BOARD_ID="${4:-}"
245
+
246
+ case "$DECISION" in
247
+ promoted|rejected) : ;;
248
+ *) echo "Error: decision must be 'promoted' or 'rejected', got: $DECISION" >&2; exit 1 ;;
249
+ esac
250
+
251
+ if [ "$DECISION" = "promoted" ]; then
252
+ [ -z "$BOARD_ID" ] && { echo "Error: promoted decision requires a board_id argument (E##_S##[_T##] or E##)" >&2; exit 1; }
253
+ if ! validate_board_id "$BOARD_ID"; then
254
+ echo "Error: invalid board ID format: $BOARD_ID" >&2
255
+ exit 1
256
+ fi
257
+ else
258
+ [ -n "$BOARD_ID" ] && { echo "Error: rejected decision does not take a board_id argument" >&2; exit 1; }
259
+ fi
260
+
261
+ MATCH=$(find_idea_line "$IDEA_TEXT") || exit 1
262
+ LINENUM=$(printf '%s' "$MATCH" | cut -d: -f1)
263
+ CONTENT=$(printf '%s' "$MATCH" | cut -d: -f2-)
264
+
265
+ if already_tagged "$CONTENT"; then
266
+ echo "Error: idea line is already tagged; refusing to re-resolve: $CONTENT" >&2
267
+ exit 1
268
+ fi
269
+
270
+ RAPPORT_PATH=$(extract_rapport_link "$CONTENT")
271
+
272
+ if [ "$DECISION" = "promoted" ]; then
273
+ TAG_TEXT="PROMOTED: $BOARD_ID"
274
+ DECISION_LABEL="Promoted"
275
+ FILE_SUFFIX="_PROMOTED.md"
276
+ else
277
+ TAG_TEXT="REJECTED"
278
+ DECISION_LABEL="Rejected"
279
+ FILE_SUFFIX="_REJECTED.md"
280
+ fi
281
+
282
+ TODAY=$(date -u +%Y-%m-%d)
283
+
284
+ tag_line "$LINENUM" "$TAG_TEXT"
285
+
286
+ if [ -n "$RAPPORT_PATH" ]; then
287
+ if [ ! -f "$RAPPORT_PATH" ]; then
288
+ echo "Error: source rapport not found at $RAPPORT_PATH; idea line was tagged but outcome record was not written" >&2
289
+ exit 1
290
+ fi
291
+ update_rapport_outcome "$RAPPORT_PATH" "$DECISION_LABEL" "$TODAY" "$BOARD_ID" "$IDEA_TEXT"
292
+ echo "Idea line $LINENUM tagged <!-- $TAG_TEXT -->; rapport updated: $RAPPORT_PATH"
293
+ else
294
+ OUTFILE=$(write_fallback_outcome "$IDEA_TEXT" "$DECISION_LABEL" "$TODAY" "$BOARD_ID" "$FILE_SUFFIX")
295
+ echo "Idea line $LINENUM tagged <!-- $TAG_TEXT -->; outcome record created: $OUTFILE"
296
+ fi
297
+ ;;
298
+
44
299
  *)
45
300
  usage
46
301
  ;;
@@ -13,7 +13,7 @@
13
13
  # 2. Derives each eligible ticket's matched EST-tagged commit SHAs (and,
14
14
  # for parity/logging, their unioned touched-file list) using the
15
15
  # identical anchored-grep + union technique
16
- # skills/self-sync/scripts/mark-merged.sh already implements.
16
+ # skills/j-self-sync/scripts/mark-merged.sh already implements.
17
17
  # 3. Checks ANCESTRY, not diff-membership: a ticket is "covered" by a tag
18
18
  # only when EVERY one of its matched commits is an ancestor of that
19
19
  # tag's resolved_private_sha (git merge-base --is-ancestor). A tag
@@ -183,7 +183,7 @@ REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
183
183
 
184
184
  COMPUTE_SCRIPT="$SCRIPT_DIR/compute-deploy-reconcile.sh"
185
185
  WITH_LOCK_SCRIPT="$REPO_ROOT/scripts/with-lock.sh"
186
- UPDATE_FRONTMATTER_SCRIPT="$REPO_ROOT/skills/close-story/scripts/update-task-frontmatter.sh"
186
+ UPDATE_FRONTMATTER_SCRIPT="$REPO_ROOT/skills/j-close-story/scripts/update-task-frontmatter.sh"
187
187
  MARKER_FILE="$REPO_ROOT/project/data/deploy-reconcile-marker.json"
188
188
 
189
189
  [ -f "$WITH_LOCK_SCRIPT" ] || die "expected script not found: $WITH_LOCK_SCRIPT"