@jenga-ai/agent 3.5.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.
- package/README.md +85 -78
- package/agents/developer.md +1 -1
- package/agents/scrum-master.md +20 -2
- package/agents/tester.md +3 -3
- package/hooks/on_session_end.sh +5 -5
- package/lib/generate-agent-context.js +2 -2
- package/lib/generate-copilot-hooks.js +1 -1
- package/lib/generate-skill-allow-list.js +37 -3
- package/lib/mirror.js +1 -1
- package/lib/postinstall-manifest.js +1 -1
- package/lib/skill-allow-list.json +2 -2
- package/mcp/help/index.js +8 -17
- package/mcp/help/scan.js +73 -0
- package/package.json +6 -1
- package/project/app/api/parsers/knowledge-graph.js +100 -9
- package/project/app/api/routes/health.js +36 -0
- package/project/app/api/scripts/capture-snapshot.js +9 -6
- package/project/app/ui/dist/assets/{index-CdK3Qrep.css → index-BVR_7Owg.css} +1 -1
- package/project/app/ui/dist/assets/index-CtU2xLQm.js +104 -0
- package/project/app/ui/dist/index.html +2 -2
- package/project/app/ui/package.json +4 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
- package/scripts/acquire-concurrency-slot.sh +1 -1
- package/scripts/apply-j-prefix.sh +46 -5
- package/scripts/audit-twin-divergence.sh +73 -5
- package/scripts/build-pages-site.sh +1 -1
- package/scripts/check-public-playbook-steps.sh +158 -52
- package/scripts/check-publicignore-match.sh +2 -2
- package/scripts/compute-deploy-reconcile.sh +5 -5
- package/scripts/delete-bare-skill-dirs.sh +330 -0
- package/scripts/generate-legacy-shipped-paths.js +2 -2
- package/scripts/idea_manager.sh +258 -3
- package/scripts/mark-deployed.sh +2 -2
- package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
- package/scripts/populate-knowledge-graph.js +213 -5
- package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
- package/scripts/postinstall.js +1 -1
- package/scripts/repoint-skill-refs.sh +539 -0
- package/scripts/todo_manager.sh +1 -1
- package/scripts/verify-legacy-seed-reconcile.sh +10 -10
- package/scripts/verify-postinstall-reconcile.sh +7 -7
- package/scripts/write-context-digest.sh +1 -1
- package/skills/j-clearify/SKILL.md +2 -2
- package/skills/j-close-story/scripts/check-privatized.sh +4 -4
- package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
- package/skills/j-do/SKILL.md +101 -17
- package/skills/j-doc-sync/SKILL.md +1 -0
- package/skills/j-gitignore/SKILL.md +157 -0
- package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
- package/skills/j-gitignore/scripts/_catalog.sh +105 -0
- package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
- package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
- package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
- package/skills/j-idea/SKILL.md +78 -6
- package/skills/j-idea/assets/idea_template.md +1 -1
- package/skills/j-improve/SKILL.md +1 -1
- package/skills/j-init/SKILL.md +3 -3
- package/skills/j-init/assets/scope-thresholds_template.json +5 -2
- package/skills/j-init/scripts/apply-scaffold-visibility.sh +1 -1
- package/skills/j-init/scripts/init.sh +4 -4
- package/skills/j-playbook/SKILL.md +1 -1
- package/skills/j-publish/SKILL.md +1 -1
- package/skills/j-publish/adapters/npm-ci.md +6 -1
- package/skills/j-publish/adapters/npm.md +1 -1
- package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
- package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
- package/skills/j-reconcile/SKILL.md +2 -2
- package/skills/j-reconcile/scripts/detect-unlinked-code.sh +11 -11
- package/skills/j-redo/SKILL.md +1 -1
- package/skills/j-spinoff/SKILL.md +1 -1
- package/skills/j-status/SKILL.md +15 -0
- package/skills/j-todo/SKILL.md +3 -1
- package/skills/j-uncharted/SKILL.md +1 -1
- package/skills/j-uncharted/scripts/detect-dependencies.sh +1 -1
- package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
- package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
- package/skills/j-wtf/SKILL.md +1 -1
- package/skills/jenga/SKILL.md +43 -9
- package/skills/jenga/playbooks/board-hygiene.json +32 -0
- package/skills/jenga/playbooks/schema.json +73 -6
- package/skills/jenga/playbooks/understand-then-commit.json +19 -0
- package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
- package/skills/jenga/scripts/load-playbooks.sh +1 -1
- package/skills/jenga/scripts/match-playbook.sh +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
- package/templates/SKILL_TEMPLATE.md +12 -0
- package/templates/permission-levels/level-4-elevated.json +1 -1
- package/templates/permission-levels/level-5-unrestricted.json +1 -1
- package/templates/playbook-types.json +34 -6
- package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
- package/scripts/generate-j-alias.sh +0 -333
- package/skills/j-dev-done/SKILL.md +0 -53
- 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
|