@jenga-ai/agent 1.1.0 → 1.1.1
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 +7 -3
- package/agents/developer.md +82 -2
- package/agents/scrum-master.md +140 -21
- package/agents/tester.md +90 -8
- package/hooks/on_session_end.sh +171 -20
- package/package.json +1 -1
- package/scripts/check-permission-level.sh +107 -0
- package/scripts/check-publicignore-match.sh +122 -0
- package/scripts/check-worktree-liveness.sh +193 -0
- package/scripts/generate-rapport-manifest.sh +43 -0
- package/scripts/idea_manager.sh +47 -0
- package/scripts/install-worktree-commit-guard.sh +134 -0
- package/scripts/jenga-permission-level-switch.sh +109 -0
- package/scripts/smoke-harness.sh +139 -0
- package/scripts/validate-board.sh +62 -0
- package/scripts/with-lock.sh +158 -0
- package/scripts/worktree-remove-guard.sh +204 -0
- package/skills/clearify/SKILL.md +52 -0
- package/skills/commit/SKILL.md +13 -4
- package/skills/distribute/CONFIG_SCHEMA.md +60 -2
- package/skills/do/SKILL.md +48 -11
- package/skills/doc-sync/SKILL.md +16 -0
- package/skills/doc-sync/assets/doc_targets.md +11 -0
- package/skills/idea/SKILL.md +56 -0
- package/skills/idea/assets/idea_handoff_template.md +26 -0
- package/skills/idea/assets/idea_template.md +3 -0
- package/skills/init/SKILL.md +100 -7
- package/skills/init/assets/directory_structure.txt +1 -0
- package/skills/init/assets/workflow_template.json +1 -1
- package/skills/init/scripts/apply-project-visibility.sh +176 -0
- package/skills/init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/init/scripts/init.sh +30 -1
- package/skills/jenga/SKILL.md +160 -17
- package/skills/jenga/scripts/board-scan.sh +238 -0
- package/skills/jenga/scripts/cascade-resolve.sh +297 -0
- package/skills/jenga/scripts/render-confirmation.sh +679 -0
- package/skills/jenga/scripts/render-picker.sh +439 -0
- package/skills/jenga/scripts/resolve-id.sh +367 -0
- package/skills/jenga-permission-level/SKILL.md +81 -0
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +8 -5
- package/skills/publish/assets/ci-contract.md +2 -2
- package/skills/publish/assets/ownership-matrix.md +1 -1
- package/skills/publish/scripts/finalize_changelog.sh +115 -0
- package/skills/publish/scripts/generate_release_notes.sh +475 -28
- package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
- package/skills/publish/scripts/publish_deploy.sh +38 -8
- package/skills/publish/scripts/run_gates.sh +2 -2
- package/skills/reconcile/SKILL.md +117 -5
- package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
- package/skills/spinoff/SKILL.md +12 -7
- package/skills/todo/SKILL.md +2 -0
- package/skills/uncharted/SKILL.md +711 -0
- package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
- package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
- package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
- package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
- package/skills/uncharted/scripts/detect-tests.sh +553 -0
- package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
- package/skills/uncharted/scripts/enumerate-target.sh +470 -0
- package/skills/uncharted/scripts/import-source.sh +517 -0
- package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
- package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
- package/skills/uncharted/scripts/run-engine.sh +655 -0
- package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
- package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
- package/skills/wtf/SKILL.md +20 -0
- package/templates/CHANGELOG_TEMPLATE.md +13 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +157 -10
- package/templates/permission-levels/README.md +73 -0
- package/templates/permission-levels/level-1-locked.json +71 -0
- package/templates/permission-levels/level-2-guarded.json +64 -0
- package/templates/permission-levels/level-3-standard.json +62 -0
- package/templates/permission-levels/level-4-elevated.json +60 -0
- package/templates/permission-levels/level-5-unrestricted.json +58 -0
- package/skills/convert/SKILL.md +0 -124
- package/skills/convert/convert_cli.py +0 -235
- package/skills/convert/tests/sample.csv +0 -4
- package/skills/convert/tests/sample.json +0 -5
- package/skills/convert/tests/sample.jsonl +0 -3
- package/skills/convert/tests/sample.yaml +0 -18
- package/skills/convert/tests/sample_obj.csv +0 -2
- package/skills/convert/tests/sample_obj.json +0 -9
- package/skills/mirror-public/SKILL.md +0 -237
- package/skills/mirror-public/assets/config.json +0 -5
- package/skills/mirror-public/scripts/mirror.sh +0 -374
- package/skills/self-sync/SKILL.md +0 -73
- package/skills/self-sync/scripts/run.js +0 -136
- package/skills/strategy/SKILL.md +0 -312
package/hooks/on_session_end.sh
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
# 2. Detect new problem rapports using a manifest (not a timestamp)
|
|
8
8
|
# and write trigger payloads to the scrum master queue
|
|
9
9
|
# 3. Write a status-review trigger to the scrum master queue
|
|
10
|
-
# 4.
|
|
11
|
-
# to the correct next agent queue, then
|
|
10
|
+
# 4. Process every pending file under project/queue/handoffs/ (if any),
|
|
11
|
+
# routing each one's assignment to the correct next agent queue, then
|
|
12
|
+
# deleting that individual file immediately after it is routed — so a
|
|
13
|
+
# problem with one file never blocks or loses any of the others
|
|
12
14
|
#
|
|
13
15
|
# Pipeline routing (section 4):
|
|
14
16
|
# scrum-master planning_complete → developer_triggers.jsonl
|
|
@@ -26,18 +28,27 @@ source "$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh"
|
|
|
26
28
|
|
|
27
29
|
PROJECT_DIR="$JENGA_PROJECT_DIR"
|
|
28
30
|
RAPPORT_DIR="$PROJECT_DIR/project/rapports/problems"
|
|
29
|
-
|
|
31
|
+
# NOTE: this manifest lives under project/ (not .claude/) because .claude/
|
|
32
|
+
# and .agents/ are generated build outputs, clobbered on every /distribute
|
|
33
|
+
# or /self-sync run — see CLAUDE.md. Canonical, committed state must not
|
|
34
|
+
# live inside a distribution target. Seed generated via, and kept in sync
|
|
35
|
+
# by, scripts/generate-rapport-manifest.sh (see section 2 below).
|
|
36
|
+
MANIFEST="$PROJECT_DIR/project/data/rapport_manifest.json"
|
|
30
37
|
QUEUE_DIR="$PROJECT_DIR/project/queue"
|
|
31
38
|
QUEUE_FILE="$QUEUE_DIR/scrum_triggers.jsonl"
|
|
32
39
|
DEV_QUEUE="$QUEUE_DIR/developer_triggers.jsonl"
|
|
33
40
|
TESTER_QUEUE="$QUEUE_DIR/tester_triggers.jsonl"
|
|
34
|
-
|
|
41
|
+
# Per-session handoff files (E37_S01_T01) replace the old single-slot
|
|
42
|
+
# .session_handoff.json. Every file under this directory is processed in
|
|
43
|
+
# section 4 below; see templates/SCRUM_BOARD_SCHEMA.md's handoffs/ section
|
|
44
|
+
# for the write-side filename convention.
|
|
45
|
+
HANDOFF_DIR="$QUEUE_DIR/handoffs"
|
|
35
46
|
EVENTS_LOG="$PROJECT_DIR/project/logs/events.json"
|
|
36
47
|
AGENT="${JENGA_AGENT_TYPE:-unknown}"
|
|
37
48
|
SESSION_ID="${JENGA_SESSION_ID:-}"
|
|
38
49
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
39
50
|
|
|
40
|
-
mkdir -p "$PROJECT_DIR/project/logs" "$QUEUE_DIR"
|
|
51
|
+
mkdir -p "$PROJECT_DIR/project/logs" "$QUEUE_DIR" "$HANDOFF_DIR"
|
|
41
52
|
|
|
42
53
|
# --- 1. Log sender object ---
|
|
43
54
|
|
|
@@ -58,18 +69,55 @@ SENDER=$(jq -n \
|
|
|
58
69
|
}
|
|
59
70
|
}')
|
|
60
71
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
# The read-modify-write below is wrapped in scripts/with-lock.sh (E37_S01_T03)
|
|
73
|
+
# so concurrent on_session_end.sh invocations never interleave their read and
|
|
74
|
+
# write of $EVENTS_LOG — see E37_S01_T04 and the two rapports it links
|
|
75
|
+
# (project/rapports/problems/E37_S01-concurrent-session-end-race-verification-gap.md,
|
|
76
|
+
# project/rapports/problems/E39_S03_T05-events-json-concurrent-write-corruption.md)
|
|
77
|
+
# for the concurrent-invocation data loss (events.json truncated to 0 bytes)
|
|
78
|
+
# this replaces. The temp file used for the atomic `mv` is created via
|
|
79
|
+
# `mktemp` in the SAME directory as $EVENTS_LOG (not a shared
|
|
80
|
+
# /tmp/events_tmp.json path) so it is (a) unique per invocation — no two
|
|
81
|
+
# concurrent processes can ever collide on the same temp filename — and
|
|
82
|
+
# (b) on the same filesystem as the destination, so the final `mv` stays an
|
|
83
|
+
# atomic rename rather than a cross-filesystem copy. The update logic is
|
|
84
|
+
# captured as a standalone script string (via a quoted heredoc, so jq's own
|
|
85
|
+
# `$entry` and quoting pass through untouched by this outer shell) and run
|
|
86
|
+
# through `bash -c` with $EVENTS_LOG/$SENDER passed as positional args
|
|
87
|
+
# rather than interpolated into the script text, avoiding fragile
|
|
88
|
+
# nested-quote escaping while keeping this fix self-contained in this file.
|
|
89
|
+
APPEND_EVENT_SCRIPT=$(cat <<'EOS'
|
|
90
|
+
events_log="$1"
|
|
91
|
+
sender_json="$2"
|
|
92
|
+
tmp_file=$(mktemp "$(dirname "$events_log")/events_tmp.XXXXXX") || exit 1
|
|
93
|
+
if [ -s "$events_log" ]; then
|
|
94
|
+
jq --argjson entry "$sender_json" '. += [$entry]' "$events_log" > "$tmp_file" \
|
|
95
|
+
&& mv "$tmp_file" "$events_log"
|
|
64
96
|
else
|
|
65
|
-
|
|
97
|
+
printf '[%s]' "$sender_json" > "$tmp_file" \
|
|
98
|
+
&& mv "$tmp_file" "$events_log"
|
|
66
99
|
fi
|
|
100
|
+
rc=$?
|
|
101
|
+
[ -f "$tmp_file" ] && rm -f "$tmp_file"
|
|
102
|
+
exit "$rc"
|
|
103
|
+
EOS
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
"$PROJECT_DIR/scripts/with-lock.sh" "$EVENTS_LOG" -- bash -c "$APPEND_EVENT_SCRIPT" _ "$EVENTS_LOG" "$SENDER"
|
|
67
107
|
|
|
68
108
|
# --- 2. Manifest-based rapport detection ---
|
|
69
109
|
# Use a JSON array of known filenames instead of a mtime sentinel file.
|
|
70
110
|
# This prevents silently missing rapports written before the session ends
|
|
71
111
|
# but after the sentinel was last touched.
|
|
112
|
+
#
|
|
113
|
+
# The manifest itself is committed to the repo (seeded and kept in sync via
|
|
114
|
+
# scripts/generate-rapport-manifest.sh), so on a normal checkout it already
|
|
115
|
+
# exists and reflects the rapports present at commit time. The bootstrap
|
|
116
|
+
# below is a non-destructive fallback for the genuinely unexpected case
|
|
117
|
+
# where the file is missing (e.g. project/data/ was pruned) — it does not
|
|
118
|
+
# run on a fresh clone with the manifest intact.
|
|
72
119
|
|
|
120
|
+
mkdir -p "$(dirname "$MANIFEST")"
|
|
73
121
|
if [ ! -f "$MANIFEST" ]; then
|
|
74
122
|
echo "[]" > "$MANIFEST"
|
|
75
123
|
fi
|
|
@@ -77,12 +125,28 @@ fi
|
|
|
77
125
|
if [ -d "$RAPPORT_DIR" ]; then
|
|
78
126
|
# Collect current rapport files (exclude .IGNORE.md files — already resolved)
|
|
79
127
|
# Results are stored in a bash array to avoid word-splitting on paths.
|
|
80
|
-
|
|
128
|
+
# Uses a portable `while read` loop rather than mapfile/readarray: macOS
|
|
129
|
+
# ships bash 3.2 (no mapfile support), and this hook must run there —
|
|
130
|
+
# matches the convention already established in scripts/smoke-harness.sh,
|
|
131
|
+
# skills/publish/scripts/generate_release_notes.sh, and
|
|
132
|
+
# skills/publish/scripts/finalize_changelog.sh. Fixed incidentally here
|
|
133
|
+
# because this task's acceptance criteria require the hook to actually
|
|
134
|
+
# execute end-to-end (mapfile silently failed on stock macOS bash,
|
|
135
|
+
# leaving CURRENT_FILES empty and masking real detection results).
|
|
136
|
+
CURRENT_FILES=()
|
|
137
|
+
while IFS= read -r line; do
|
|
138
|
+
[ -n "$line" ] && CURRENT_FILES+=("$line")
|
|
139
|
+
done < <(find "$RAPPORT_DIR" -name "*.md" ! -name "*.IGNORE.md" 2>/dev/null | sort)
|
|
81
140
|
|
|
82
|
-
# Identify new files not present in the manifest
|
|
141
|
+
# Identify new files not present in the manifest. The manifest stores
|
|
142
|
+
# paths relative to $PROJECT_DIR (portable across clones/worktrees), so
|
|
143
|
+
# each absolute CURRENT_FILES entry is relativized before the lookup.
|
|
144
|
+
# NEW_FILES itself stays absolute — it feeds rapport_files in the
|
|
145
|
+
# trigger below, which the scrum master reads directly.
|
|
83
146
|
NEW_FILES=()
|
|
84
147
|
for file in "${CURRENT_FILES[@]}"; do
|
|
85
|
-
|
|
148
|
+
rel="${file#"$PROJECT_DIR"/}"
|
|
149
|
+
known=$(jq --arg f "$rel" 'index($f) != null' "$MANIFEST" 2>/dev/null)
|
|
86
150
|
if [ "$known" != "true" ]; then
|
|
87
151
|
NEW_FILES+=("$file")
|
|
88
152
|
fi
|
|
@@ -109,8 +173,11 @@ if [ -d "$RAPPORT_DIR" ]; then
|
|
|
109
173
|
|
|
110
174
|
echo "$TRIGGER" >> "$QUEUE_FILE"
|
|
111
175
|
|
|
112
|
-
# Update the manifest to include all current files
|
|
113
|
-
|
|
176
|
+
# Update the manifest to include all current files. Delegates to the
|
|
177
|
+
# shared generator script (rather than re-inlining the same jq
|
|
178
|
+
# pipeline) so the runtime update and the committed seed can never
|
|
179
|
+
# drift in how they compute "current rapport files".
|
|
180
|
+
bash "$PROJECT_DIR/scripts/generate-rapport-manifest.sh" "$MANIFEST" >/dev/null
|
|
114
181
|
fi
|
|
115
182
|
fi
|
|
116
183
|
|
|
@@ -129,15 +196,97 @@ TRIGGER=$(jq -n \
|
|
|
129
196
|
|
|
130
197
|
echo "$TRIGGER" >> "$QUEUE_FILE"
|
|
131
198
|
|
|
199
|
+
# --- Helper: has this handoff's referenced task already reached a
|
|
200
|
+
# terminal board status? ---------------------------------------------------
|
|
201
|
+
# Guards against resurrecting a stale handoff file (e.g. one that was
|
|
202
|
+
# accidentally committed to git alongside unrelated work, or one left over
|
|
203
|
+
# from before this directory was ever consumed) as a fresh routing signal
|
|
204
|
+
# for work that has already been fully verified. Terminal here means "no
|
|
205
|
+
# further routing action should occur": Passed / Passed with remarks /
|
|
206
|
+
# Rejected / Done are all end states, and Blocked is included because a
|
|
207
|
+
# blocked task must not be silently re-touched by any agent per
|
|
208
|
+
# agents/developer.md's Blocked-halt contract (only a human may clear it).
|
|
209
|
+
# Deliberately fails open (returns 1 / "not terminal") when the task file
|
|
210
|
+
# can't be found or has no readable status, so a lookup miss falls back to
|
|
211
|
+
# today's behavior (route it) rather than silently dropping a handoff whose
|
|
212
|
+
# task genuinely can't be identified.
|
|
213
|
+
is_task_terminal() {
|
|
214
|
+
local task_id="$1" task_file task_status
|
|
215
|
+
task_file=$(find "$PROJECT_DIR/project/board/tasks" -maxdepth 1 -iname "${task_id}_*.md" 2>/dev/null | head -1)
|
|
216
|
+
[ -z "$task_file" ] && return 1
|
|
217
|
+
task_status=$(awk -F': ' '/^status:/ {print $2; exit}' "$task_file" 2>/dev/null)
|
|
218
|
+
case "$task_status" in
|
|
219
|
+
Passed|"Passed with remarks"|Rejected|Done|Blocked) return 0 ;;
|
|
220
|
+
*) return 1 ;;
|
|
221
|
+
esac
|
|
222
|
+
}
|
|
223
|
+
|
|
132
224
|
# --- 4. Session handoff routing ---
|
|
133
|
-
# Each agent writes
|
|
134
|
-
#
|
|
135
|
-
#
|
|
225
|
+
# Each agent writes a per-session handoff file under project/queue/handoffs/
|
|
226
|
+
# before its session ends (see E37_S01_T01). This section processes every
|
|
227
|
+
# file currently present in that directory — not just one fixed path — so
|
|
228
|
+
# concurrent sessions ending close together each get routed instead of the
|
|
229
|
+
# last writer silently clobbering the others. Every file is routed through
|
|
230
|
+
# the same per-agent logic below, then deleted individually right after
|
|
231
|
+
# routing, so a problem with one file can't block or lose any of the rest.
|
|
232
|
+
#
|
|
233
|
+
# The glob is intentionally generic (*.json, not a pattern anchored to the
|
|
234
|
+
# <agent>-<session_id>-<task_id> convention) so it also picks up older
|
|
235
|
+
# ad-hoc-named files written before that convention existed — routing below
|
|
236
|
+
# only ever reads the .agent/.status fields from file contents, never the
|
|
237
|
+
# filename, so this is safe.
|
|
238
|
+
#
|
|
239
|
+
# Before routing, a developer/tester handoff whose referenced task is
|
|
240
|
+
# already terminal on the board is treated as stale (deleted, not routed)
|
|
241
|
+
# rather than as a live signal — see is_task_terminal() above. This closes
|
|
242
|
+
# a real regression found during E37_S01_T02 testing: this directory can
|
|
243
|
+
# accumulate committed leftover files for already-shipped work (nothing
|
|
244
|
+
# consumed them before this task existed), and a bare generic glob would
|
|
245
|
+
# otherwise resurrect all of them as fresh test_assignment/story_rollup
|
|
246
|
+
# triggers the moment this hook first runs against such a directory.
|
|
247
|
+
for HANDOFF_FILE in "$HANDOFF_DIR"/*.json; do
|
|
248
|
+
# Guard against the glob matching nothing (no nullglob dependency, so
|
|
249
|
+
# this stays portable to stock macOS bash 3.2 — same rationale as the
|
|
250
|
+
# portable `while read` loop in section 2 above).
|
|
251
|
+
[ -e "$HANDOFF_FILE" ] || continue
|
|
252
|
+
|
|
253
|
+
# Atomic per-file claim (E37_S01_T04) — closes the TOCTOU window between
|
|
254
|
+
# this per-process glob snapshot and the eventual delete at the bottom of
|
|
255
|
+
# the loop. `mv` between two paths on the same filesystem is a rename(2)
|
|
256
|
+
# syscall: the kernel guarantees that when multiple concurrent processes
|
|
257
|
+
# race to rename the same source path, exactly one succeeds and every
|
|
258
|
+
# other attempt fails because the source no longer exists. No GNU-only
|
|
259
|
+
# flags are involved, so this is portable to macOS/BSD as well as Linux.
|
|
260
|
+
# Without this claim, concurrent invocations could each see the same
|
|
261
|
+
# not-yet-deleted file, read it, and route it before either deleted it —
|
|
262
|
+
# confirmed in project/rapports/problems/E37_S01-concurrent-session-end-race-verification-gap.md
|
|
263
|
+
# as duplicate (2x/2x/4x observed) trigger entries for the same handoff.
|
|
264
|
+
# If the rename fails, another concurrent process already claimed this
|
|
265
|
+
# exact file — skip it without reading, routing, or deleting anything;
|
|
266
|
+
# that other process now owns it.
|
|
267
|
+
CLAIMED_HANDOFF_FILE="${HANDOFF_FILE}.claimed.$$"
|
|
268
|
+
if ! mv "$HANDOFF_FILE" "$CLAIMED_HANDOFF_FILE" 2>/dev/null; then
|
|
269
|
+
echo "[on_session_end] handoff already claimed by a concurrent invocation — skipping $(basename "$HANDOFF_FILE")"
|
|
270
|
+
continue
|
|
271
|
+
fi
|
|
272
|
+
HANDOFF_FILE="$CLAIMED_HANDOFF_FILE"
|
|
136
273
|
|
|
137
|
-
if [ -f "$HANDOFF_FILE" ]; then
|
|
138
274
|
HANDOFF_AGENT=$(jq -r '.agent // empty' "$HANDOFF_FILE" 2>/dev/null)
|
|
139
275
|
HANDOFF_STATUS=$(jq -r '.status // empty' "$HANDOFF_FILE" 2>/dev/null)
|
|
140
276
|
|
|
277
|
+
# Staleness check — only meaningful for developer/tester handoffs, which
|
|
278
|
+
# each reference exactly one task_id. scrum-master's planning_complete
|
|
279
|
+
# handoff carries a task_ids array of just-created tasks that can't
|
|
280
|
+
# already be terminal in practice, so it is not checked here.
|
|
281
|
+
if [ "$HANDOFF_AGENT" = "developer" ] || [ "$HANDOFF_AGENT" = "tester" ]; then
|
|
282
|
+
CHECK_TASK_ID=$(jq -r '.task_id // empty' "$HANDOFF_FILE" 2>/dev/null)
|
|
283
|
+
if [ -n "$CHECK_TASK_ID" ] && is_task_terminal "$CHECK_TASK_ID"; then
|
|
284
|
+
echo "[on_session_end] stale handoff for already-terminal task $CHECK_TASK_ID — skipping routing, deleting $(basename "$HANDOFF_FILE")"
|
|
285
|
+
rm -f "$HANDOFF_FILE"
|
|
286
|
+
continue
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
|
|
141
290
|
case "$HANDOFF_AGENT" in
|
|
142
291
|
|
|
143
292
|
scrum-master)
|
|
@@ -228,9 +377,11 @@ if [ -f "$HANDOFF_FILE" ]; then
|
|
|
228
377
|
|
|
229
378
|
esac
|
|
230
379
|
|
|
231
|
-
# Consume
|
|
380
|
+
# Consume this handoff file — it is single-use. Deleted immediately after
|
|
381
|
+
# routing (not batched after the loop) so a later file's failure can never
|
|
382
|
+
# cause an earlier, already-routed file to be left unconsumed or vice versa.
|
|
232
383
|
rm -f "$HANDOFF_FILE"
|
|
233
|
-
|
|
384
|
+
done
|
|
234
385
|
|
|
235
386
|
# --- 5. Todo cleanup ---
|
|
236
387
|
# Remove project/todo.md if it is effectively empty (only blanks, # Todo, and HTML comments).
|
package/package.json
CHANGED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# check-permission-level.sh — read-only gate for a skill's declared minimum
|
|
4
|
+
# permission level (the `minimum_permission_level` SKILL.md frontmatter field,
|
|
5
|
+
# see docs/skill-authoring.md).
|
|
6
|
+
#
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# CHECK vs ENFORCE — READ THIS BEFORE WIRING THIS SCRIPT INTO A SKILL
|
|
9
|
+
# ============================================================================
|
|
10
|
+
# This script is READ-ONLY. It CHECKS the current session's permission level
|
|
11
|
+
# against a required minimum and SIGNALS the result. It does NOT:
|
|
12
|
+
# - prompt the user for confirmation
|
|
13
|
+
# - elevate the session level
|
|
14
|
+
# - write to .jenga-permission-level.json, .claude/settings.json, or
|
|
15
|
+
# .agents/settings.json under any code path
|
|
16
|
+
#
|
|
17
|
+
# The CALLING SKILL's own instructions are responsible for:
|
|
18
|
+
# 1. Detecting the `NEEDS_CONFIRMATION` signal (non-zero exit) below.
|
|
19
|
+
# 2. Explicitly asking the user to confirm elevation (no silent
|
|
20
|
+
# auto-elevation, ever).
|
|
21
|
+
# 3. On confirmation, invoking `jenga-permission-level-switch.sh <minimum-level>`
|
|
22
|
+
# (a sibling script, scripts/jenga-permission-level-switch.sh — task E33_S02_T02) to
|
|
23
|
+
# actually perform the elevation.
|
|
24
|
+
# 4. Immediately after the skill's own work completes, resetting the
|
|
25
|
+
# session back to Guarded (2) — typically via `jenga-permission-level-switch.sh 2` as
|
|
26
|
+
# the skill's last step. This reset is NOT automatic and is NOT this
|
|
27
|
+
# script's job.
|
|
28
|
+
#
|
|
29
|
+
# Usage:
|
|
30
|
+
# scripts/check-permission-level.sh <minimum-level>
|
|
31
|
+
#
|
|
32
|
+
# <minimum-level> Integer 1-5 — the value a calling skill read from its
|
|
33
|
+
# own `minimum_permission_level` frontmatter field.
|
|
34
|
+
#
|
|
35
|
+
# Exit codes:
|
|
36
|
+
# 0 Current session level already meets <minimum-level>. No output.
|
|
37
|
+
# 1 Current session level is below <minimum-level>. Prints:
|
|
38
|
+
# NEEDS_CONFIRMATION: elevate from <current> to <minimum-level>
|
|
39
|
+
# 2 Bad usage / invalid argument. Prints an error to stderr.
|
|
40
|
+
#
|
|
41
|
+
# Current level source:
|
|
42
|
+
# .jenga-permission-level.json at the repo root, field `session_level`
|
|
43
|
+
# (the format `jenga-permission-level-switch.sh`, E33_S02_T02, writes: {"session_level": <n>}).
|
|
44
|
+
# Missing file, unreadable file, malformed JSON, or a missing/non-integer
|
|
45
|
+
# `session_level` field are all treated as level 2 (Guarded), the
|
|
46
|
+
# permanent default — this script never fails merely because the level
|
|
47
|
+
# file is absent or malformed.
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
set -euo pipefail
|
|
51
|
+
|
|
52
|
+
usage() {
|
|
53
|
+
echo "Usage: $(basename "$0") <minimum-level>" >&2
|
|
54
|
+
echo " <minimum-level> must be an integer 1-5 (the skill's minimum_permission_level)." >&2
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if [ "$#" -ne 1 ]; then
|
|
58
|
+
usage
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
MIN_LEVEL="$1"
|
|
63
|
+
|
|
64
|
+
case "$MIN_LEVEL" in
|
|
65
|
+
1|2|3|4|5) ;;
|
|
66
|
+
*)
|
|
67
|
+
echo "Error: <minimum-level> must be an integer 1-5, got: '$MIN_LEVEL'" >&2
|
|
68
|
+
usage
|
|
69
|
+
exit 2
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
|
|
73
|
+
LEVEL_FILE=".jenga-permission-level.json"
|
|
74
|
+
CURRENT_LEVEL=2
|
|
75
|
+
|
|
76
|
+
if [ -f "$LEVEL_FILE" ]; then
|
|
77
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
78
|
+
PARSED=$(python3 -c "
|
|
79
|
+
import json, sys
|
|
80
|
+
try:
|
|
81
|
+
with open('$LEVEL_FILE') as f:
|
|
82
|
+
data = json.load(f)
|
|
83
|
+
level = data.get('session_level')
|
|
84
|
+
if isinstance(level, bool) or not isinstance(level, int) or not (1 <= level <= 5):
|
|
85
|
+
raise ValueError('invalid session_level')
|
|
86
|
+
print(level)
|
|
87
|
+
except Exception:
|
|
88
|
+
print(2)
|
|
89
|
+
" 2>/dev/null || echo 2)
|
|
90
|
+
CURRENT_LEVEL="$PARSED"
|
|
91
|
+
else
|
|
92
|
+
# No python3 available — fall back to a permissive grep-based extraction.
|
|
93
|
+
# Any failure to confidently parse an integer 1-5 falls back to level 2.
|
|
94
|
+
GREPPED=$(grep -o '"session_level"[[:space:]]*:[[:space:]]*[0-9]' "$LEVEL_FILE" 2>/dev/null | grep -o '[0-9]$' | head -n1 || true)
|
|
95
|
+
case "$GREPPED" in
|
|
96
|
+
1|2|3|4|5) CURRENT_LEVEL="$GREPPED" ;;
|
|
97
|
+
*) CURRENT_LEVEL=2 ;;
|
|
98
|
+
esac
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if [ "$CURRENT_LEVEL" -ge "$MIN_LEVEL" ]; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
echo "NEEDS_CONFIRMATION: elevate from $CURRENT_LEVEL to $MIN_LEVEL"
|
|
107
|
+
exit 1
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# scripts/check-publicignore-match.sh
|
|
4
|
+
#
|
|
5
|
+
# Classify one or more repo-relative paths as PUBLIC (would ship to the
|
|
6
|
+
# public mirror repo) or BLOCKED (excluded by .publicignore), using the
|
|
7
|
+
# exact same rsync --exclude-from=.publicignore matching semantics as
|
|
8
|
+
# skills/mirror-public/scripts/mirror.sh's --dry-run "ship list" computation
|
|
9
|
+
# (see the SHIP_LIST_FILE block in that script). A file classified as
|
|
10
|
+
# "would be blocked" by `/mirror-public --dry-run` is guaranteed to be
|
|
11
|
+
# classified as BLOCKED here too, and vice versa for PUBLIC.
|
|
12
|
+
#
|
|
13
|
+
# This does NOT touch the network, clone the public repo, or require
|
|
14
|
+
# /mirror-public to be configured (skills/mirror-public/assets/config.json
|
|
15
|
+
# is never read) — it only needs a .publicignore file at the repo root.
|
|
16
|
+
# Rsync's include/exclude filter evaluation does not depend on destination
|
|
17
|
+
# state (destination state only affects delete/itemize-flag details for
|
|
18
|
+
# files already present there), so probing against an empty scratch temp
|
|
19
|
+
# directory yields an identical "would ship" set to probing against
|
|
20
|
+
# mirror.sh's real public-clone destination.
|
|
21
|
+
#
|
|
22
|
+
# Callers (e.g. /doc-sync step 4, and the E36 changelog generator's
|
|
23
|
+
# .publicignore-awareness) are expected to skip invoking this script
|
|
24
|
+
# entirely when .publicignore does not exist at the repo root — that is
|
|
25
|
+
# the established no-op-when-absent precedent, and it stays owned by each
|
|
26
|
+
# caller rather than being silently absorbed here.
|
|
27
|
+
#
|
|
28
|
+
# Usage:
|
|
29
|
+
# check-publicignore-match.sh <path> [<path> ...]
|
|
30
|
+
#
|
|
31
|
+
# Paths are interpreted relative to the repo root (same convention as
|
|
32
|
+
# `git ls-files`). Output: one "STATUS<TAB>path" line per input path, in
|
|
33
|
+
# input order. STATUS is PUBLIC or BLOCKED.
|
|
34
|
+
#
|
|
35
|
+
# Note: a path that does not exist on disk is classified BLOCKED (it will
|
|
36
|
+
# not appear in rsync's transfer list regardless of .publicignore). Callers
|
|
37
|
+
# such as /doc-sync only ever pass paths already confirmed to exist as
|
|
38
|
+
# real new-in-source candidates, so this does not arise in practice — but
|
|
39
|
+
# it is not the same guarantee as ".publicignore explicitly matched it".
|
|
40
|
+
#
|
|
41
|
+
# Exit codes:
|
|
42
|
+
# 0 classification completed (regardless of individual PUBLIC/BLOCKED results)
|
|
43
|
+
# 1 usage error
|
|
44
|
+
# 2 repo root not found, .publicignore missing, or rsync unavailable
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
IFS=$'\n\t'
|
|
49
|
+
|
|
50
|
+
die() {
|
|
51
|
+
printf 'check-publicignore-match.sh: error: %s\n' "$*" >&2
|
|
52
|
+
exit 2
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if [ "$#" -eq 0 ]; then
|
|
56
|
+
printf 'usage: check-publicignore-match.sh <path> [<path> ...]\n' >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
SCRIPT_PATH="${BASH_SOURCE[0]}"
|
|
61
|
+
while [ -h "$SCRIPT_PATH" ]; do
|
|
62
|
+
LINK_TARGET="$(readlink "$SCRIPT_PATH")"
|
|
63
|
+
case "$LINK_TARGET" in
|
|
64
|
+
/*) SCRIPT_PATH="$LINK_TARGET" ;;
|
|
65
|
+
*) SCRIPT_PATH="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)/$LINK_TARGET" ;;
|
|
66
|
+
esac
|
|
67
|
+
done
|
|
68
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
|
69
|
+
|
|
70
|
+
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
71
|
+
[ -n "$REPO_ROOT" ] || die "could not locate repo root (git rev-parse failed from $SCRIPT_DIR)"
|
|
72
|
+
|
|
73
|
+
PUBLICIGNORE="$REPO_ROOT/.publicignore"
|
|
74
|
+
[ -f "$PUBLICIGNORE" ] || die "no .publicignore at $PUBLICIGNORE (caller should skip invoking this script when absent — that is the no-op case, not an error the caller should surface)"
|
|
75
|
+
|
|
76
|
+
command -v rsync >/dev/null 2>&1 || die "rsync not installed"
|
|
77
|
+
|
|
78
|
+
TMP_DEST="$(mktemp -d)"
|
|
79
|
+
SHIP_LIST_FILE="$(mktemp)"
|
|
80
|
+
cleanup() {
|
|
81
|
+
rm -rf "$TMP_DEST"
|
|
82
|
+
rm -f "$SHIP_LIST_FILE"
|
|
83
|
+
}
|
|
84
|
+
trap cleanup EXIT
|
|
85
|
+
|
|
86
|
+
# Same rsync flags + itemize/awk filter as mirror.sh's --dry-run ship-list
|
|
87
|
+
# computation (kept in lockstep with that block intentionally), targeted at
|
|
88
|
+
# an empty scratch dir instead of a real public clone.
|
|
89
|
+
rsync -a --delete \
|
|
90
|
+
--exclude-from="$PUBLICIGNORE" \
|
|
91
|
+
--exclude=".git" \
|
|
92
|
+
--dry-run --itemize-changes --out-format='%i %n' \
|
|
93
|
+
"$REPO_ROOT/" \
|
|
94
|
+
"$TMP_DEST/" \
|
|
95
|
+
| awk '{
|
|
96
|
+
flag = $1;
|
|
97
|
+
first = substr(flag, 1, 1);
|
|
98
|
+
second = substr(flag, 2, 1);
|
|
99
|
+
# Skip directories — public tree recreates them implicitly.
|
|
100
|
+
if (second == "d") next;
|
|
101
|
+
# Keep only entries that transfer or create a file/symlink/hardlink.
|
|
102
|
+
keep = 0;
|
|
103
|
+
if (first == ">" || first == "<") keep = 1; # file transfer
|
|
104
|
+
else if (first == "c" && second == "L") keep = 1; # create symlink
|
|
105
|
+
else if (first == "h") keep = 1; # hard link
|
|
106
|
+
if (!keep) next;
|
|
107
|
+
$1 = "";
|
|
108
|
+
sub(/^ /, "");
|
|
109
|
+
sub(/\/$/, "");
|
|
110
|
+
if (length($0) > 0) print $0;
|
|
111
|
+
}' \
|
|
112
|
+
| LC_ALL=C sort -u > "$SHIP_LIST_FILE"
|
|
113
|
+
|
|
114
|
+
for candidate in "$@"; do
|
|
115
|
+
# Normalize a single leading "./" if present, so callers can pass either form.
|
|
116
|
+
norm="${candidate#./}"
|
|
117
|
+
if LC_ALL=C grep -Fxq "$norm" "$SHIP_LIST_FILE"; then
|
|
118
|
+
printf 'PUBLIC\t%s\n' "$candidate"
|
|
119
|
+
else
|
|
120
|
+
printf 'BLOCKED\t%s\n' "$candidate"
|
|
121
|
+
fi
|
|
122
|
+
done
|