@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,50 @@
1
+ # skills/j-gitignore/assets/jenga-paths.txt
2
+ #
3
+ # The canonical catalog of Jenga-owned paths a consuming project may end up
4
+ # with on disk. Consumed by audit-gitignore.sh, repair-gitignore.sh and
5
+ # untrack-jenga-files.sh — all three read this one file, so the path surface
6
+ # is defined in exactly one place.
7
+ #
8
+ # Format: <tier>TAB<path>TAB<description>
9
+ # Blank lines and lines starting with '#' are ignored.
10
+ #
11
+ # Trailing '/' marks a directory entry and is preserved verbatim into
12
+ # .gitignore (git treats 'foo/' as directory-only, which is what we want).
13
+ #
14
+ # ─── Tiers ───────────────────────────────────────────────────────────────────
15
+ #
16
+ # scaffold Entirely Jenga-owned. Regenerated wholesale by `npm postinstall`
17
+ # or `/distribute`, so nothing of yours is ever lost by ignoring
18
+ # them. Safe default.
19
+ #
20
+ # hybrid A Jenga-managed block inside a file that ALSO holds your own
21
+ # content. lib/generate-agent-context.js and
22
+ # lib/generate-copilot-instructions.js both replace only their
23
+ # managed block and preserve everything outside it. Ignoring these
24
+ # therefore also hides YOUR prose from the repo. Opt-in, never
25
+ # applied without the user saying so.
26
+ #
27
+ # board The project/ working tree (scrum board, todo.md, queue/,
28
+ # rapports/, logs/). Governed by jenga.config.json's
29
+ # `project_files_visibility`. Teams that share a board want this
30
+ # tracked; solo users usually do not. Opt-in.
31
+ #
32
+ # optional Defensive entries for paths no current Jenga version generates.
33
+ # Costs nothing to ignore, guards against a future or third-party
34
+ # agent surface appearing. Opt-in.
35
+ #
36
+ # ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ scaffold .claude/ Claude Code skill/agent mirror — rewritten by every postinstall and /distribute run (scripts/postinstall.js targetRoots). Contains .jenga-postinstall-manifest.json.
39
+ scaffold .agents/ Copilot/Codex skill/agent mirror — the second of postinstall.js's two targetRoots. Contains .jenga-postinstall-manifest.json.
40
+ scaffold .jenga/ `jenga` CLI state directory. lib/commands/init.js already appends this entry itself; listed here so the audit reports it consistently.
41
+ scaffold .jenga-version Installed-version stamp written at consumer root by scripts/postinstall.js.
42
+ scaffold jenga.config.json Jenga project config (project_files_visibility, scaffold_visibility). SEE THE WARNING IN SKILL.md — ignoring this means a fresh clone loses your visibility settings.
43
+ scaffold .github/hooks/jenga.json Copilot CLI hook wiring. "Entirely Jenga-owned output" per scripts/postinstall.js — no user content, unlike copilot-instructions.md.
44
+ scaffold J-CLAUDE.md Collision-case copy written only when you already had your own CLAUDE.md. A J- file can only ever have been created by Jenga.
45
+ scaffold J-AGENTS.md Collision-case copy written only when you already had your own AGENTS.md. Jenga-created by definition.
46
+ hybrid CLAUDE.md Jenga writes a managed block here and preserves content outside it. May contain your own instructions.
47
+ hybrid AGENTS.md Same managed-block treatment as CLAUDE.md, for Codex and generic AGENTS.md consumers.
48
+ hybrid .github/copilot-instructions.md Managed-block bootstrap. postinstall.js explicitly notes this one is NOT entirely Jenga-owned.
49
+ board project/ The whole Jenga working tree. Mirrors what apply-project-visibility.sh's JENGA_WORKING_PATHS appends at init time.
50
+ optional .copilot/ Not generated by any current Jenga version (grepped across lib/, scripts/, skills/). Defensive only.
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env bash
2
+ # -----------------------------------------------------------------------------
3
+ # skills/j-gitignore/scripts/_catalog.sh
4
+ #
5
+ # Shared helpers for the j-gitignore scripts. Sourced, never run directly:
6
+ #
7
+ # source "$(dirname "${BASH_SOURCE[0]}")/_catalog.sh"
8
+ #
9
+ # Provides:
10
+ # CATALOG_FILE absolute path to assets/jenga-paths.txt
11
+ # MANAGED_BEGIN/_END the managed-block markers
12
+ # load_catalog <tiers> populates CATALOG_PATHS/_TIERS/_DESCS arrays
13
+ # validate_tiers <csv> rejects an unknown tier name
14
+ # resolve_root <dir> cd's to a project root, verifying it is a git repo
15
+ #
16
+ # The leading underscore marks this as internal to the skill — it is not an
17
+ # entry point and takes no arguments of its own.
18
+ # -----------------------------------------------------------------------------
19
+
20
+ # Deliberately no `set -e` — callers handle their own errors, matching the
21
+ # convention in skills/j-init/scripts/apply-scaffold-visibility.sh.
22
+ set -uo pipefail
23
+
24
+ _CATALOG_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ CATALOG_FILE="${_CATALOG_LIB_DIR}/../assets/jenga-paths.txt"
26
+
27
+ MANAGED_BEGIN="# >>> jenga:gitignore >>>"
28
+ MANAGED_END="# <<< jenga:gitignore <<<"
29
+
30
+ VALID_TIERS="scaffold hybrid board optional"
31
+
32
+ # Populated by load_catalog.
33
+ CATALOG_PATHS=()
34
+ CATALOG_TIERS=()
35
+ CATALOG_DESCS=()
36
+
37
+ validate_tiers() {
38
+ local csv="$1" tier valid found
39
+ [ -n "$csv" ] || { echo "ERROR: --tiers was given an empty value." >&2; return 2; }
40
+ # Split on commas into an array FIRST, then restore IFS. Setting IFS=',' for
41
+ # the duration of both loops would stop the space-separated $VALID_TIERS from
42
+ # splitting, making every tier look invalid.
43
+ local -a requested=()
44
+ local old_ifs="$IFS"
45
+ IFS=','; read -r -a requested <<< "$csv"; IFS="$old_ifs"
46
+ for tier in "${requested[@]}"; do
47
+ found=0
48
+ for valid in $VALID_TIERS; do
49
+ [ "$tier" = "$valid" ] && found=1 && break
50
+ done
51
+ if [ "$found" -eq 0 ]; then
52
+ echo "ERROR: Unknown tier '${tier}'. Valid tiers: ${VALID_TIERS// /, }" >&2
53
+ return 2
54
+ fi
55
+ done
56
+ return 0
57
+ }
58
+
59
+ # load_catalog <comma-separated-tiers>
60
+ # Reads CATALOG_FILE, keeping only rows whose tier is in the requested set.
61
+ load_catalog() {
62
+ local want="$1"
63
+ CATALOG_PATHS=(); CATALOG_TIERS=(); CATALOG_DESCS=()
64
+
65
+ if [ ! -f "$CATALOG_FILE" ]; then
66
+ echo "ERROR: Path catalog not found: $CATALOG_FILE" >&2
67
+ return 1
68
+ fi
69
+
70
+ local tier path desc
71
+ while IFS=$'\t' read -r tier path desc || [ -n "${tier:-}" ]; do
72
+ [ -z "${tier// /}" ] && continue
73
+ case "$tier" in \#*) continue ;; esac
74
+ [ -n "${path:-}" ] || continue
75
+ case ",${want}," in
76
+ *",${tier},"*)
77
+ CATALOG_PATHS+=("$path")
78
+ CATALOG_TIERS+=("$tier")
79
+ CATALOG_DESCS+=("${desc:-}")
80
+ ;;
81
+ esac
82
+ done < "$CATALOG_FILE"
83
+
84
+ if [ "${#CATALOG_PATHS[@]}" -eq 0 ]; then
85
+ echo "ERROR: No catalog entries matched tiers '${want}'." >&2
86
+ return 1
87
+ fi
88
+ return 0
89
+ }
90
+
91
+ # resolve_root <dir> — cd into it and confirm it is the root of a git work tree.
92
+ resolve_root() {
93
+ local root="$1"
94
+ if [ ! -d "$root" ]; then
95
+ echo "ERROR: Project root does not exist: $root" >&2
96
+ return 1
97
+ fi
98
+ cd "$root" || { echo "ERROR: Cannot enter project root: $root" >&2; return 1; }
99
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
100
+ echo "ERROR: Not a git repository: $root" >&2
101
+ echo " This skill repairs a project's tracked/ignored state, so it needs git." >&2
102
+ return 1
103
+ fi
104
+ return 0
105
+ }
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env bash
2
+ # -----------------------------------------------------------------------------
3
+ # skills/j-gitignore/scripts/audit-gitignore.sh
4
+ #
5
+ # Read-only. Reports the Jenga-related git state of an already-scaffolded
6
+ # project. Changes nothing — this is the "check" half of the skill, safe to run
7
+ # anywhere, any number of times.
8
+ #
9
+ # Reports three independent things per catalog path:
10
+ #
11
+ # entry Is the exact entry literally present in .gitignore?
12
+ # ignored Does git actually ignore the path (`git check-ignore`)? This can
13
+ # be true via a broader rule even when `entry` is no — and is
14
+ # ALWAYS reported as "no" for a tracked path, because .gitignore
15
+ # has no effect on a file git already tracks. That divergence is
16
+ # the single most common cause of "I ignored it and it still keeps
17
+ # getting committed".
18
+ # tracked Does git currently track anything under the path?
19
+ #
20
+ # Plus the stray-heredoc-terminator check: a bare `EOF` line left behind by the
21
+ # botched `cat > .gitignore <<EOF` in pre-fix /init scaffolds (fixed for NEW
22
+ # projects by E31_S07_T02/T03 and E42_S06_T01, which replaced the heredoc with
23
+ # a `cp` from assets/.gitignore_template — forward-only, so already-scaffolded
24
+ # projects still carry the damage).
25
+ #
26
+ # Invoked via `bash`, not executed directly — ships without the executable bit,
27
+ # same convention as skills/j-cloud-connect/scripts/install-rclone.sh.
28
+ #
29
+ # Usage:
30
+ # audit-gitignore.sh [project_root] [--tiers scaffold,hybrid,board,optional]
31
+ #
32
+ # Exit codes:
33
+ # 0 Audit ran; nothing to fix
34
+ # 1 Bad usage or missing prerequisite (not a git repo, catalog missing)
35
+ # 2 Invalid tier name
36
+ # 10 Audit ran; findings exist (stray terminators, missing entries, or
37
+ # tracked paths). Not an error — this is the signal to run the repair.
38
+ # -----------------------------------------------------------------------------
39
+
40
+ set -uo pipefail
41
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_catalog.sh"
42
+
43
+ PROJECT_ROOT=""
44
+ TIERS="scaffold"
45
+
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --tiers) TIERS="${2:-}"; shift 2 ;;
49
+ --tiers=*) TIERS="${1#*=}"; shift ;;
50
+ -h|--help)
51
+ echo "Usage: $(basename "$0") [project_root] [--tiers scaffold,hybrid,board,optional]"
52
+ exit 0 ;;
53
+ -*)
54
+ echo "ERROR: Unknown option: $1" >&2
55
+ echo "Usage: $(basename "$0") [project_root] [--tiers <csv>]" >&2
56
+ exit 1 ;;
57
+ *)
58
+ if [ -n "$PROJECT_ROOT" ]; then
59
+ echo "ERROR: More than one project root given: '$PROJECT_ROOT' and '$1'" >&2
60
+ exit 1
61
+ fi
62
+ PROJECT_ROOT="$1"; shift ;;
63
+ esac
64
+ done
65
+
66
+ PROJECT_ROOT="${PROJECT_ROOT:-$PWD}"
67
+ validate_tiers "$TIERS" || exit 2
68
+ resolve_root "$PROJECT_ROOT" || exit 1
69
+ load_catalog "$TIERS" || exit 1
70
+
71
+ GITIGNORE=".gitignore"
72
+ FINDINGS=0
73
+
74
+ echo "═══════════════════════════════════════════════════════════════════════"
75
+ echo " Jenga gitignore audit — $(pwd)"
76
+ echo " Tiers: ${TIERS}"
77
+ echo "═══════════════════════════════════════════════════════════════════════"
78
+ echo
79
+
80
+ # ─── 1. Stray heredoc terminators ───────────────────────────────────────────
81
+ echo "── 1. Stray heredoc terminators ───────────────────────────────────────"
82
+ STRAY_COUNT=0
83
+ if [ ! -f "$GITIGNORE" ]; then
84
+ echo " No .gitignore at this root — nothing to check."
85
+ else
86
+ # A line that is exactly EOF (tolerating trailing whitespace). Reported with
87
+ # line numbers so a human can eyeball it before anything is removed.
88
+ while IFS= read -r hit; do
89
+ [ -z "$hit" ] && continue
90
+ echo " ✗ line ${hit%%:*}: stray '${hit#*:}' terminator"
91
+ STRAY_COUNT=$((STRAY_COUNT + 1))
92
+ done < <(grep -nE '^[[:space:]]*EOF[[:space:]]*$' "$GITIGNORE" 2>/dev/null)
93
+
94
+ if [ "$STRAY_COUNT" -eq 0 ]; then
95
+ echo " ✓ Clean — no stray terminator lines."
96
+ else
97
+ echo
98
+ echo " → ${STRAY_COUNT} stray line(s). repair-gitignore.sh removes these."
99
+ FINDINGS=$((FINDINGS + STRAY_COUNT))
100
+ fi
101
+ fi
102
+ echo
103
+
104
+ # ─── 2. Managed block ────────────────────────────────────────────────────────
105
+ echo "── 2. Managed block ───────────────────────────────────────────────────"
106
+ if [ -f "$GITIGNORE" ] && grep -qxF -- "$MANAGED_BEGIN" "$GITIGNORE"; then
107
+ echo " ✓ Present — this project has been through repair-gitignore.sh before."
108
+ else
109
+ echo " · Absent — entries (if any) are loose, e.g. appended by /init's"
110
+ echo " apply-project-visibility.sh / apply-scaffold-visibility.sh."
111
+ fi
112
+ echo
113
+
114
+ # ─── 3. Per-path state ───────────────────────────────────────────────────────
115
+ echo "── 3. Per-path state ──────────────────────────────────────────────────"
116
+ printf " %-34s %-7s %-8s %-8s %s\n" "PATH" "ENTRY" "IGNORED" "TRACKED" "TIER"
117
+ printf " %-34s %-7s %-8s %-8s %s\n" "----" "-----" "-------" "-------" "----"
118
+
119
+ MISSING_ENTRIES=0
120
+ TRACKED_PATHS=()
121
+
122
+ for i in "${!CATALOG_PATHS[@]}"; do
123
+ path="${CATALOG_PATHS[$i]}"
124
+ tier="${CATALOG_TIERS[$i]}"
125
+
126
+ entry="no"
127
+ if [ -f "$GITIGNORE" ] && grep -qxF -- "$path" "$GITIGNORE"; then
128
+ entry="yes"
129
+ fi
130
+
131
+ # check-ignore works on paths that do not exist on disk, so a not-yet-created
132
+ # scaffold dir is still evaluated against the rules.
133
+ ignored="no"
134
+ if git check-ignore -q -- "$path" 2>/dev/null; then
135
+ ignored="yes"
136
+ fi
137
+
138
+ tracked="no"
139
+ if [ -n "$(git ls-files -- "$path" 2>/dev/null | head -n 1)" ]; then
140
+ tracked="yes"
141
+ TRACKED_PATHS+=("$path")
142
+ fi
143
+
144
+ flag=""
145
+ if [ "$entry" = "no" ]; then flag=" ← no entry"; MISSING_ENTRIES=$((MISSING_ENTRIES + 1)); fi
146
+ if [ "$tracked" = "yes" ]; then flag="${flag} ← TRACKED"; fi
147
+
148
+ printf " %-34s %-7s %-8s %-8s %s%s\n" "$path" "$entry" "$ignored" "$tracked" "$tier" "$flag"
149
+ done
150
+ echo
151
+
152
+ # ─── 4. Origin state ─────────────────────────────────────────────────────────
153
+ echo "── 4. Origin ──────────────────────────────────────────────────────────"
154
+ BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
155
+ UPSTREAM="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)"
156
+
157
+ if [ -z "$UPSTREAM" ]; then
158
+ echo " · Branch '${BRANCH}' has no upstream — nothing has been pushed from it."
159
+ echo " Untracking here only ever affects local history."
160
+ else
161
+ echo " Upstream: ${UPSTREAM}"
162
+ ON_ORIGIN=0
163
+ for path in "${TRACKED_PATHS[@]:-}"; do
164
+ [ -z "$path" ] && continue
165
+ if [ -n "$(git ls-tree -r --name-only "$UPSTREAM" -- "$path" 2>/dev/null | head -n 1)" ]; then
166
+ echo " ✗ ${path} — present on ${UPSTREAM}"
167
+ ON_ORIGIN=$((ON_ORIGIN + 1))
168
+ fi
169
+ done
170
+ if [ "$ON_ORIGIN" -eq 0 ]; then
171
+ echo " ✓ No catalog path is present on ${UPSTREAM}."
172
+ else
173
+ echo
174
+ echo " → ${ON_ORIGIN} path(s) live on the remote. untrack-jenga-files.sh"
175
+ echo " --commit --push removes them from the branch TIP. It does NOT"
176
+ echo " rewrite history — earlier commits still contain them."
177
+ fi
178
+ fi
179
+ echo
180
+
181
+ # ─── Summary ─────────────────────────────────────────────────────────────────
182
+ FINDINGS=$((FINDINGS + MISSING_ENTRIES + ${#TRACKED_PATHS[@]}))
183
+ echo "═══════════════════════════════════════════════════════════════════════"
184
+ echo " Stray terminators : ${STRAY_COUNT}"
185
+ echo " Missing entries : ${MISSING_ENTRIES}"
186
+ echo " Tracked paths : ${#TRACKED_PATHS[@]}"
187
+ echo "═══════════════════════════════════════════════════════════════════════"
188
+
189
+ if [ "$FINDINGS" -gt 0 ]; then
190
+ echo "AUDIT_RESULT=findings"
191
+ exit 10
192
+ fi
193
+ echo "AUDIT_RESULT=clean"
194
+ exit 0
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env bash
2
+ # -----------------------------------------------------------------------------
3
+ # skills/j-gitignore/scripts/repair-gitignore.sh
4
+ #
5
+ # Repairs an already-scaffolded project's .gitignore. Two jobs, in order:
6
+ #
7
+ # 1. Strips stray heredoc terminator lines (a bare `EOF`) left behind by the
8
+ # botched `cat > .gitignore <<EOF` in pre-fix /init scaffolds. The upstream
9
+ # fix (E31_S07_T02/T03, E42_S06_T01) repaired the TEMPLATE, which only helps
10
+ # projects scaffolded afterwards; this repairs one already on disk.
11
+ #
12
+ # 2. Adds (mode `ignored`) or removes (mode `visible`) the Jenga path entries,
13
+ # inside a managed block so the operation is exactly reversible.
14
+ #
15
+ # The managed block also ABSORBS loose duplicates: /init's
16
+ # apply-project-visibility.sh and apply-scaffold-visibility.sh append bare lines
17
+ # with no markers, so a previously-initialised project has loose `project/`,
18
+ # `.claude/` and `.agents/` entries. Any loose line exactly matching a selected
19
+ # catalog path is pulled out of the file and re-emitted inside the block, rather
20
+ # than left to sit alongside it as a duplicate.
21
+ #
22
+ # Everything outside the managed block is preserved byte for byte. The file is
23
+ # written atomically (temp + mv), so an interrupted run cannot truncate it.
24
+ #
25
+ # This does NOT untrack anything. .gitignore has no effect on a file git already
26
+ # tracks — that is untrack-jenga-files.sh's job, and it must run after this.
27
+ #
28
+ # Invoked via `bash`, not executed directly.
29
+ #
30
+ # Usage:
31
+ # repair-gitignore.sh <ignored|visible> [project_root]
32
+ # [--tiers scaffold,hybrid,board,optional]
33
+ # [--dry-run] [--keep-eof]
34
+ #
35
+ # Options:
36
+ # --tiers <csv> Which catalog tiers to act on. Default: scaffold
37
+ # --dry-run Print a unified diff of the intended change; write nothing.
38
+ # --keep-eof Leave stray `EOF` lines alone. Use only if this project
39
+ # genuinely means to ignore a file literally named EOF.
40
+ #
41
+ # Exit codes:
42
+ # 0 Success (including "already correct — nothing to do")
43
+ # 1 Bad usage or missing prerequisite
44
+ # 2 Invalid mode or tier
45
+ # 3 Filesystem write failure
46
+ # -----------------------------------------------------------------------------
47
+
48
+ set -uo pipefail
49
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_catalog.sh"
50
+
51
+ info() { echo "[gitignore-repair] $*"; }
52
+ err() { echo "[gitignore-repair] ERROR: $*" >&2; }
53
+
54
+ MODE=""
55
+ PROJECT_ROOT=""
56
+ TIERS="scaffold"
57
+ DRY_RUN=0
58
+ KEEP_EOF=0
59
+
60
+ while [ $# -gt 0 ]; do
61
+ case "$1" in
62
+ --tiers) TIERS="${2:-}"; shift 2 ;;
63
+ --tiers=*) TIERS="${1#*=}"; shift ;;
64
+ --dry-run) DRY_RUN=1; shift ;;
65
+ --keep-eof) KEEP_EOF=1; shift ;;
66
+ -h|--help)
67
+ echo "Usage: $(basename "$0") <ignored|visible> [project_root] [--tiers <csv>] [--dry-run] [--keep-eof]"
68
+ exit 0 ;;
69
+ -*) err "Unknown option: $1"; exit 1 ;;
70
+ *)
71
+ if [ -z "$MODE" ]; then MODE="$1"
72
+ elif [ -z "$PROJECT_ROOT" ]; then PROJECT_ROOT="$1"
73
+ else err "Unexpected argument: $1"; exit 1
74
+ fi
75
+ shift ;;
76
+ esac
77
+ done
78
+
79
+ case "$MODE" in
80
+ ignored|visible) ;;
81
+ "") err "Missing mode. Usage: $(basename "$0") <ignored|visible> [project_root]"; exit 1 ;;
82
+ *) err "Invalid mode '${MODE}'. Allowed: ignored, visible"; exit 2 ;;
83
+ esac
84
+
85
+ PROJECT_ROOT="${PROJECT_ROOT:-$PWD}"
86
+ validate_tiers "$TIERS" || exit 2
87
+ resolve_root "$PROJECT_ROOT" || exit 1
88
+ load_catalog "$TIERS" || exit 1
89
+
90
+ GITIGNORE=".gitignore"
91
+ TMP="${GITIGNORE}.jenga-repair.$$"
92
+ cleanup() { rm -f "$TMP"; }
93
+ trap cleanup EXIT
94
+
95
+ # Fast membership test for "is this line one of our selected catalog paths".
96
+ is_catalog_path() {
97
+ local line="$1" p
98
+ for p in "${CATALOG_PATHS[@]}"; do
99
+ [ "$line" = "$p" ] && return 0
100
+ done
101
+ return 1
102
+ }
103
+
104
+ # ─── Read the existing file ─────────────────────────────────────────────────
105
+ ORIGINAL=()
106
+ if [ -f "$GITIGNORE" ]; then
107
+ while IFS= read -r line || [ -n "$line" ]; do
108
+ ORIGINAL+=("$line")
109
+ done < "$GITIGNORE"
110
+ elif [ "$MODE" = "visible" ]; then
111
+ info "No .gitignore at this root and mode is 'visible' — nothing to do."
112
+ exit 0
113
+ else
114
+ info "No .gitignore at this root — creating one."
115
+ fi
116
+
117
+ # ─── Filter: drop stray terminators, the old managed block, loose dupes ─────
118
+ FILTERED=()
119
+ in_block=0
120
+ removed_eof=0
121
+ removed_loose=0
122
+ had_block=0
123
+
124
+ for line in "${ORIGINAL[@]+"${ORIGINAL[@]}"}"; do
125
+ if [ "$in_block" -eq 1 ]; then
126
+ [ "$line" = "$MANAGED_END" ] && in_block=0
127
+ continue
128
+ fi
129
+ if [ "$line" = "$MANAGED_BEGIN" ]; then
130
+ in_block=1; had_block=1; continue
131
+ fi
132
+ if [ "$KEEP_EOF" -eq 0 ] && [[ "$line" =~ ^[[:space:]]*EOF[[:space:]]*$ ]]; then
133
+ removed_eof=$((removed_eof + 1)); continue
134
+ fi
135
+ if is_catalog_path "$line"; then
136
+ removed_loose=$((removed_loose + 1)); continue
137
+ fi
138
+ FILTERED+=("$line")
139
+ done
140
+
141
+ if [ "$in_block" -eq 1 ]; then
142
+ err "Unterminated managed block — found '${MANAGED_BEGIN}' with no matching end marker."
143
+ err "Refusing to write. Fix ${GITIGNORE} by hand, then re-run."
144
+ exit 3
145
+ fi
146
+
147
+ # Trim trailing blank lines so repeated runs cannot accumulate whitespace.
148
+ # Indexed from the end by arithmetic, NOT via ${arr[-1]}: macOS still ships
149
+ # bash 3.2, which has no negative array subscripts — there the subscript
150
+ # silently evaluates empty, the loop condition stays true, and nothing is ever
151
+ # unset, so the script spins forever.
152
+ _last=$(( ${#FILTERED[@]} - 1 ))
153
+ while [ "$_last" -ge 0 ] && [ -z "${FILTERED[$_last]// /}" ]; do
154
+ unset "FILTERED[$_last]"
155
+ _last=$(( _last - 1 ))
156
+ done
157
+
158
+ # ─── Build the new file ─────────────────────────────────────────────────────
159
+ : > "$TMP" || { err "Cannot write temp file: $TMP"; exit 3; }
160
+
161
+ for line in "${FILTERED[@]+"${FILTERED[@]}"}"; do
162
+ printf '%s\n' "$line" >> "$TMP" || { err "Write failed: $TMP"; exit 3; }
163
+ done
164
+
165
+ if [ "$MODE" = "ignored" ]; then
166
+ [ "${#FILTERED[@]}" -gt 0 ] && printf '\n' >> "$TMP"
167
+ {
168
+ printf '%s\n' "$MANAGED_BEGIN"
169
+ printf '%s\n' "# Managed by j.gitignore — regenerate with:"
170
+ printf '%s\n' "# bash <skill>/scripts/repair-gitignore.sh ignored . --tiers ${TIERS}"
171
+ printf '%s\n' "# Remove with the same script in 'visible' mode. Edits inside this block"
172
+ printf '%s\n' "# are overwritten; put your own patterns outside it."
173
+ for i in "${!CATALOG_PATHS[@]}"; do
174
+ printf '%s\n' "${CATALOG_PATHS[$i]}"
175
+ done
176
+ printf '%s\n' "$MANAGED_END"
177
+ } >> "$TMP" || { err "Write failed: $TMP"; exit 3; }
178
+ fi
179
+
180
+ # ─── Report / apply ─────────────────────────────────────────────────────────
181
+ if [ -f "$GITIGNORE" ] && cmp -s "$GITIGNORE" "$TMP"; then
182
+ info "Already correct — ${GITIGNORE} needs no change."
183
+ exit 0
184
+ fi
185
+
186
+ echo
187
+ echo "── Proposed change to ${GITIGNORE} ────────────────────────────────────"
188
+ if [ -f "$GITIGNORE" ]; then
189
+ diff -u "$GITIGNORE" "$TMP" | sed 's/^/ /' || true
190
+ else
191
+ sed 's/^/ + /' "$TMP"
192
+ fi
193
+ echo
194
+
195
+ info "Stray 'EOF' terminators removed : ${removed_eof}"
196
+ info "Loose duplicate entries folded in: ${removed_loose}"
197
+ info "Pre-existing managed block : $([ "$had_block" -eq 1 ] && echo replaced || echo none)"
198
+ info "Mode : ${MODE} (tiers: ${TIERS})"
199
+
200
+ if [ "$DRY_RUN" -eq 1 ]; then
201
+ info "--dry-run — nothing written."
202
+ exit 0
203
+ fi
204
+
205
+ if ! mv "$TMP" "$GITIGNORE"; then
206
+ err "Failed to move ${TMP} into place."
207
+ exit 3
208
+ fi
209
+ trap - EXIT
210
+
211
+ # Never report success without re-verifying the result on disk.
212
+ if [ "$MODE" = "ignored" ]; then
213
+ for p in "${CATALOG_PATHS[@]}"; do
214
+ if ! grep -qxF -- "$p" "$GITIGNORE"; then
215
+ err "Post-write verification failed: '${p}' is not in ${GITIGNORE}."
216
+ exit 3
217
+ fi
218
+ done
219
+ fi
220
+ if [ "$KEEP_EOF" -eq 0 ] && grep -qE '^[[:space:]]*EOF[[:space:]]*$' "$GITIGNORE"; then
221
+ err "Post-write verification failed: a stray EOF line survived."
222
+ exit 3
223
+ fi
224
+
225
+ info "Wrote ${GITIGNORE} (verified)."
226
+ exit 0