@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.
Files changed (91) hide show
  1. package/README.md +7 -3
  2. package/agents/developer.md +82 -2
  3. package/agents/scrum-master.md +140 -21
  4. package/agents/tester.md +90 -8
  5. package/hooks/on_session_end.sh +171 -20
  6. package/package.json +1 -1
  7. package/scripts/check-permission-level.sh +107 -0
  8. package/scripts/check-publicignore-match.sh +122 -0
  9. package/scripts/check-worktree-liveness.sh +193 -0
  10. package/scripts/generate-rapport-manifest.sh +43 -0
  11. package/scripts/idea_manager.sh +47 -0
  12. package/scripts/install-worktree-commit-guard.sh +134 -0
  13. package/scripts/jenga-permission-level-switch.sh +109 -0
  14. package/scripts/smoke-harness.sh +139 -0
  15. package/scripts/validate-board.sh +62 -0
  16. package/scripts/with-lock.sh +158 -0
  17. package/scripts/worktree-remove-guard.sh +204 -0
  18. package/skills/clearify/SKILL.md +52 -0
  19. package/skills/commit/SKILL.md +13 -4
  20. package/skills/distribute/CONFIG_SCHEMA.md +60 -2
  21. package/skills/do/SKILL.md +48 -11
  22. package/skills/doc-sync/SKILL.md +16 -0
  23. package/skills/doc-sync/assets/doc_targets.md +11 -0
  24. package/skills/idea/SKILL.md +56 -0
  25. package/skills/idea/assets/idea_handoff_template.md +26 -0
  26. package/skills/idea/assets/idea_template.md +3 -0
  27. package/skills/init/SKILL.md +100 -7
  28. package/skills/init/assets/directory_structure.txt +1 -0
  29. package/skills/init/assets/workflow_template.json +1 -1
  30. package/skills/init/scripts/apply-project-visibility.sh +176 -0
  31. package/skills/init/scripts/detect-existing-codebase.sh +166 -0
  32. package/skills/init/scripts/init.sh +30 -1
  33. package/skills/jenga/SKILL.md +160 -17
  34. package/skills/jenga/scripts/board-scan.sh +238 -0
  35. package/skills/jenga/scripts/cascade-resolve.sh +297 -0
  36. package/skills/jenga/scripts/render-confirmation.sh +679 -0
  37. package/skills/jenga/scripts/render-picker.sh +439 -0
  38. package/skills/jenga/scripts/resolve-id.sh +367 -0
  39. package/skills/jenga-permission-level/SKILL.md +81 -0
  40. package/skills/proceed/SKILL.md +1 -1
  41. package/skills/publish/SKILL.md +8 -5
  42. package/skills/publish/assets/ci-contract.md +2 -2
  43. package/skills/publish/assets/ownership-matrix.md +1 -1
  44. package/skills/publish/scripts/finalize_changelog.sh +115 -0
  45. package/skills/publish/scripts/generate_release_notes.sh +475 -28
  46. package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
  47. package/skills/publish/scripts/publish_deploy.sh +38 -8
  48. package/skills/publish/scripts/run_gates.sh +2 -2
  49. package/skills/reconcile/SKILL.md +117 -5
  50. package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
  51. package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
  52. package/skills/spinoff/SKILL.md +12 -7
  53. package/skills/todo/SKILL.md +2 -0
  54. package/skills/uncharted/SKILL.md +711 -0
  55. package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
  56. package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
  57. package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
  58. package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
  59. package/skills/uncharted/scripts/detect-tests.sh +553 -0
  60. package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
  61. package/skills/uncharted/scripts/enumerate-target.sh +470 -0
  62. package/skills/uncharted/scripts/import-source.sh +517 -0
  63. package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
  64. package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
  65. package/skills/uncharted/scripts/run-engine.sh +655 -0
  66. package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
  67. package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
  68. package/skills/wtf/SKILL.md +20 -0
  69. package/templates/CHANGELOG_TEMPLATE.md +13 -0
  70. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
  71. package/templates/SCRUM_BOARD_SCHEMA.md +157 -10
  72. package/templates/permission-levels/README.md +73 -0
  73. package/templates/permission-levels/level-1-locked.json +71 -0
  74. package/templates/permission-levels/level-2-guarded.json +64 -0
  75. package/templates/permission-levels/level-3-standard.json +62 -0
  76. package/templates/permission-levels/level-4-elevated.json +60 -0
  77. package/templates/permission-levels/level-5-unrestricted.json +58 -0
  78. package/skills/convert/SKILL.md +0 -124
  79. package/skills/convert/convert_cli.py +0 -235
  80. package/skills/convert/tests/sample.csv +0 -4
  81. package/skills/convert/tests/sample.json +0 -5
  82. package/skills/convert/tests/sample.jsonl +0 -3
  83. package/skills/convert/tests/sample.yaml +0 -18
  84. package/skills/convert/tests/sample_obj.csv +0 -2
  85. package/skills/convert/tests/sample_obj.json +0 -9
  86. package/skills/mirror-public/SKILL.md +0 -237
  87. package/skills/mirror-public/assets/config.json +0 -5
  88. package/skills/mirror-public/scripts/mirror.sh +0 -374
  89. package/skills/self-sync/SKILL.md +0 -73
  90. package/skills/self-sync/scripts/run.js +0 -136
  91. package/skills/strategy/SKILL.md +0 -312
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/check-worktree-liveness.sh — detect OS processes still rooted
3
+ # inside a given worktree path.
4
+ #
5
+ # Usage:
6
+ # scripts/check-worktree-liveness.sh <worktree-path>
7
+ #
8
+ # Why this exists: `ListAgents` reports agent lifecycle, not detached OS
9
+ # processes. A live incident (2026-08-25, see E37's Purpose) showed a
10
+ # completed agent leaving shell polling loops running with their cwd inside
11
+ # a worktree; `ListAgents` showed the agent itself as "completed" and gave
12
+ # no signal that those loops were still alive, so the worktree was removed
13
+ # out from under them. This script answers a narrower, deterministic
14
+ # question instead: "does any OS process currently have its *current
15
+ # working directory* rooted inside this path (the path itself, or any
16
+ # subdirectory of it)?" It is intended to be run as a precondition before
17
+ # `git worktree remove` (wired in the sibling task E37_S03_T02, not here —
18
+ # this task only produces the standalone check).
19
+ #
20
+ # Detection mechanism (primary): `lsof +D <path> -F pcfn`
21
+ # `+D <path>` recursively restricts lsof's report to files open within
22
+ # that directory subtree (this alone gives us "inside the given path,
23
+ # including subdirectories" — verified locally against a nested-subdir
24
+ # case). `-F pcfn` is lsof's machine-parsable output mode, emitting one
25
+ # attribute per line: `p<pid>`, `c<command>`, `f<fd-or-descriptor-type>`,
26
+ # `n<file name>`, grouped per matching open-file record. Records are then
27
+ # filtered down to those whose `f` field is literally `cwd` — this is what
28
+ # distinguishes "this process's current working directory is here" from
29
+ # "this process merely has some unrelated file open under here" (e.g. a
30
+ # log file), which `+D` alone does not distinguish.
31
+ #
32
+ # `-F pcfn` (not lsof's default human-readable columnar output) is used
33
+ # deliberately: column widths/ordering in the default output are not
34
+ # guaranteed stable across lsof versions/platforms, whereas the `-F` field
35
+ # protocol is. Verified directly against this repo's own Darwin runtime
36
+ # (lsof 4.91) — both an exact-path cwd match and a subdirectory cwd match
37
+ # are correctly reported, and process cwd is resolved through symlinks
38
+ # (macOS reports `/tmp/x` as `/private/tmp/x`), which is why the input
39
+ # path is canonicalized below before comparison.
40
+ #
41
+ # Detection mechanism (fallback, when `lsof` is not on PATH): enumerate
42
+ # `/proc/[0-9]*` (Linux only — macOS has no `/proc`) and `readlink` each
43
+ # `/proc/<pid>/cwd`, comparing against the canonicalized target path. macOS
44
+ # ships `lsof` by default, so this branch is not expected to run on this
45
+ # repo's actual runtime, but keeps the script functional on minimal Linux
46
+ # containers that may lack `lsof`. If neither mechanism is available, the
47
+ # script cannot determine liveness at all and fails closed (non-zero exit,
48
+ # clear stderr message) rather than silently reporting "clear" when it does
49
+ # not actually know.
50
+ #
51
+ # Command-line resolution for reported matches uses `ps -o command= -p
52
+ # <pid>` — confirmed to behave identically on macOS's BSD `ps` and Linux's
53
+ # GNU `ps`, unlike some other `ps` flags/keywords (e.g. `-e` listing
54
+ # semantics differ between BSD and GNU `ps`, which is why the fallback path
55
+ # above enumerates PIDs via `/proc` rather than `ps -e`).
56
+ #
57
+ # Caller-cwd self-detection hardening: if the *caller* invokes this script
58
+ # from a shell whose own cwd is already inside the target path (e.g. `cd
59
+ # <worktree> && check-worktree-liveness.sh <worktree>` — a very plausible
60
+ # state right before a worktree is removed), every subprocess the script
61
+ # forks (the `lsof` child, and the process-substitution subshell wrapping
62
+ # it below, or the `readlink` calls in the /proc fallback) would otherwise
63
+ # *inherit* that cwd, since bash processes inherit cwd from their parent by
64
+ # default. That produced a real, reproduced false positive: the script's
65
+ # own subprocess chain got reported as a "live process rooted in" the very
66
+ # path being checked, even with zero genuinely orphaned processes running.
67
+ # See project/rapports/problems/E37_S03_T01-self-detection-false-positive-remarks.md
68
+ # for the original reproduction and the two fix options it proposed.
69
+ #
70
+ # Fix: immediately after resolving $ABS_PATH (and before spawning anything),
71
+ # the script `cd`s itself to `/` — a path guaranteed to be outside every
72
+ # possible target. This moves the *script's own process* outside the target,
73
+ # so every descendant it forks from this point on inherits a cwd of `/`
74
+ # instead of the caller's original cwd, regardless of where the caller
75
+ # happened to be standing when it invoked this script. This is preferred
76
+ # over tracking and excluding individual subprocess PIDs (the script's own
77
+ # PID `$$` was already excluded as a courtesy, see below) because `cd /`
78
+ # removes the precondition for the whole bug class structurally, rather
79
+ # than requiring every current and future subprocess this script might ever
80
+ # spawn to be enumerated and excluded by PID after the fact.
81
+ #
82
+ # The script also excludes its own PID ($$) from reported matches as a
83
+ # second, defense-in-depth courtesy (now largely redundant given the `cd /`
84
+ # fix above, but harmless to keep).
85
+ #
86
+ # Exit codes:
87
+ # 0 clear — no live process found, no output
88
+ # 1 one or more live processes found — PID(s) and command(s) printed
89
+ # 2 invalid usage (wrong argument count / empty argument)
90
+ # 3 <worktree-path> does not resolve to an existing directory
91
+ # 4 unable to determine liveness on this platform (neither `lsof` nor
92
+ # /proc available) — fail closed, this is NOT the same as "clear"
93
+
94
+ set -u
95
+
96
+ SCRIPT_NAME="check-worktree-liveness.sh"
97
+
98
+ usage() {
99
+ echo "Usage: $SCRIPT_NAME <worktree-path>" >&2
100
+ exit 2
101
+ }
102
+
103
+ [ "$#" -eq 1 ] || usage
104
+ RAW_PATH="$1"
105
+ [ -n "$RAW_PATH" ] || usage
106
+
107
+ if [ ! -d "$RAW_PATH" ]; then
108
+ echo "$SCRIPT_NAME: '$RAW_PATH' is not a directory" >&2
109
+ exit 3
110
+ fi
111
+
112
+ # Canonicalize to an absolute, symlink-resolved path so comparisons against
113
+ # process cwd values (which the OS reports fully resolved — e.g. macOS
114
+ # reports /tmp as /private/tmp) match correctly regardless of how the
115
+ # caller spelled the input path.
116
+ ABS_PATH="$(cd "$RAW_PATH" 2>/dev/null && pwd -P)"
117
+ if [ -z "$ABS_PATH" ]; then
118
+ echo "$SCRIPT_NAME: unable to resolve '$RAW_PATH' to an absolute path" >&2
119
+ exit 3
120
+ fi
121
+
122
+ # Move this process (and therefore every subprocess it forks from here on)
123
+ # outside of every possible target path, so the scan below can never
124
+ # self-detect regardless of the caller's cwd at invocation time. See the
125
+ # "Caller-cwd self-detection hardening" comment in the header above. `/` is
126
+ # always present and readable on every platform this script targets; if for
127
+ # some reason it could not be `cd`'d into, fall through rather than abort —
128
+ # worst case the pre-existing $$ exclusion below is the only mitigation,
129
+ # same as before this fix.
130
+ cd / 2>/dev/null || true
131
+
132
+ SELF_PID="$$"
133
+ FOUND_ANY=0
134
+ REPORTED_PIDS=""
135
+
136
+ already_reported() {
137
+ case " $REPORTED_PIDS " in
138
+ *" $1 "*) return 0 ;;
139
+ *) return 1 ;;
140
+ esac
141
+ }
142
+
143
+ report_match() {
144
+ pid="$1"
145
+ already_reported "$pid" && return 0
146
+ REPORTED_PIDS="$REPORTED_PIDS $pid"
147
+ cmd="$(ps -o command= -p "$pid" 2>/dev/null)"
148
+ [ -n "$cmd" ] || cmd="(command unavailable — process may have exited)"
149
+ echo "$SCRIPT_NAME: live process rooted in '$ABS_PATH' — PID $pid: $cmd"
150
+ FOUND_ANY=1
151
+ }
152
+
153
+ if command -v lsof >/dev/null 2>&1; then
154
+ current_pid=""
155
+ current_fd=""
156
+ while IFS= read -r line; do
157
+ case "$line" in
158
+ p*)
159
+ current_pid="${line#p}"
160
+ current_fd=""
161
+ ;;
162
+ f*)
163
+ current_fd="${line#f}"
164
+ ;;
165
+ n*)
166
+ if [ "$current_fd" = "cwd" ] && [ -n "$current_pid" ] && [ "$current_pid" != "$SELF_PID" ]; then
167
+ report_match "$current_pid"
168
+ fi
169
+ ;;
170
+ esac
171
+ done < <(lsof +D "$ABS_PATH" -F pcfn 2>/dev/null)
172
+ else
173
+ if [ -d /proc ]; then
174
+ for pid_dir in /proc/[0-9]*; do
175
+ [ -e "$pid_dir" ] || continue
176
+ pid="${pid_dir#/proc/}"
177
+ [ "$pid" != "$SELF_PID" ] || continue
178
+ cwd_link="$(readlink "$pid_dir/cwd" 2>/dev/null)" || continue
179
+ [ -n "$cwd_link" ] || continue
180
+ case "$cwd_link" in
181
+ "$ABS_PATH"|"$ABS_PATH"/*)
182
+ report_match "$pid"
183
+ ;;
184
+ esac
185
+ done
186
+ else
187
+ echo "$SCRIPT_NAME: neither 'lsof' nor /proc is available on this system — cannot determine worktree liveness for '$ABS_PATH'" >&2
188
+ exit 4
189
+ fi
190
+ fi
191
+
192
+ [ "$FOUND_ANY" -eq 1 ] && exit 1
193
+ exit 0
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # scripts/generate-rapport-manifest.sh
3
+ #
4
+ # Regenerates the rapport manifest used by hooks/on_session_end.sh to detect
5
+ # *new* problem rapports (project/rapports/problems/*.md, excluding resolved
6
+ # *.IGNORE.md files). This script is the single source of truth for "what
7
+ # does the manifest look like right now" — it is used both to produce the
8
+ # committed seed manifest and, at runtime, by the hook itself whenever it
9
+ # rewrites the manifest after finding new rapports.
10
+ #
11
+ # Entries are stored as paths relative to the repo root (e.g.
12
+ # "project/rapports/problems/foo.md"), NOT absolute paths. The manifest is
13
+ # a committed artifact shared across every clone/worktree, each of which
14
+ # has a different absolute checkout location — absolute paths would make
15
+ # the seed match nobody's filesystem but the one it was generated on.
16
+ #
17
+ # Safe to re-run at any time: it is a pure snapshot of the current directory
18
+ # contents, never a diff, so running it twice in a row produces identical
19
+ # output.
20
+ #
21
+ # Usage: scripts/generate-rapport-manifest.sh [output_file]
22
+ # output_file defaults to project/data/rapport_manifest.json
23
+
24
+ set -euo pipefail
25
+
26
+ # shellcheck source=lib/resolve-project-dir.sh
27
+ source "$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh"
28
+
29
+ PROJECT_DIR="$JENGA_PROJECT_DIR"
30
+ RAPPORT_DIR="$PROJECT_DIR/project/rapports/problems"
31
+ OUTPUT="${1:-$PROJECT_DIR/project/data/rapport_manifest.json}"
32
+
33
+ mkdir -p "$(dirname "$OUTPUT")"
34
+
35
+ if [ -d "$RAPPORT_DIR" ]; then
36
+ find "$RAPPORT_DIR" -name "*.md" ! -name "*.IGNORE.md" 2>/dev/null \
37
+ | sed "s|^$PROJECT_DIR/||" \
38
+ | sort | jq -R . | jq -s . > "$OUTPUT"
39
+ else
40
+ echo "[]" > "$OUTPUT"
41
+ fi
42
+
43
+ echo "Wrote $(jq 'length' "$OUTPUT") rapport filename(s) to $OUTPUT"
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/idea_manager.sh — canonical owner of all project/ideas.md operations.
3
+ # Run from the repository root.
4
+
5
+ IDEA_FILE="project/ideas.md"
6
+ TEMPLATE="skills/idea/assets/idea_template.md"
7
+
8
+ usage() {
9
+ cat >&2 <<EOF
10
+ Usage: $0 <subcommand> [args]
11
+
12
+ Subcommands:
13
+ add "<entry>" Append entry to project/ideas.md (auto-creates from template if missing)
14
+ list Print all non-comment, non-blank entries (silent if file missing/empty)
15
+ EOF
16
+ exit 1
17
+ }
18
+
19
+ # Filters out blank lines, lines starting with #, and HTML comments
20
+ real_entries() {
21
+ grep -v '^\s*$' "$1" 2>/dev/null \
22
+ | grep -v '^\s*#' \
23
+ | grep -v '^\s*<!--'
24
+ }
25
+
26
+ case "${1:-}" in
27
+ add)
28
+ [ -z "${2:-}" ] && { echo "Error: add requires an entry argument" >&2; exit 1; }
29
+ if [ ! -f "$IDEA_FILE" ]; then
30
+ if [ ! -f "$TEMPLATE" ]; then
31
+ echo "Error: template not found at $TEMPLATE" >&2; exit 1
32
+ fi
33
+ cp "$TEMPLATE" "$IDEA_FILE"
34
+ fi
35
+ printf '%s\n' "$2" >> "$IDEA_FILE"
36
+ ;;
37
+
38
+ list)
39
+ [ ! -f "$IDEA_FILE" ] && exit 0
40
+ real_entries "$IDEA_FILE"
41
+ exit 0
42
+ ;;
43
+
44
+ *)
45
+ usage
46
+ ;;
47
+ esac
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/install-worktree-commit-guard.sh — install a branch-guard pre-commit
3
+ # hook into a given git worktree.
4
+ #
5
+ # Usage:
6
+ # scripts/install-worktree-commit-guard.sh <worktree-path> <branch-name>
7
+ #
8
+ # Writes a `pre-commit` hook that rejects any commit attempted while HEAD is
9
+ # on a branch other than <branch-name>. This exists because a tester once
10
+ # committed its verification of a task to `main` instead of the branch it was
11
+ # actually verifying, from inside the worktree created for that task —
12
+ # nothing mechanically prevented it (see E37's Purpose / E37_S02). Written
13
+ # rules alone have a documented track record of drifting in this project
14
+ # (E17 rollup note: two of its own stories were marked complete without the
15
+ # underlying change landing), so this is a real guard, not just a documented
16
+ # rule.
17
+ #
18
+ # IMPORTANT — why this does NOT just write to `git rev-parse --git-path
19
+ # hooks`: that path resolves to the *shared* `hooks` directory common to the
20
+ # main repository and every one of its linked worktrees (verified against
21
+ # git 2.39: `git -C <any-worktree> rev-parse --git-path hooks` returns the
22
+ # same common path for every worktree of the same repo, including the main
23
+ # one). A naive install there would mean each new worktree's guard silently
24
+ # clobbers every other worktree's guard (only the most-recently-created
25
+ # worktree's branch would ever be accepted), and every *other* worktree would
26
+ # then have its commits blocked outright, since the current branch would
27
+ # never match the last-installed expected branch. That is worse than doing
28
+ # nothing: it defeats the isolation this task exists to provide.
29
+ #
30
+ # The actual fix is git's per-worktree config file
31
+ # (`$GIT_DIR/worktrees/<name>/config.worktree`, gated behind the
32
+ # `extensions.worktreeConfig` repo extension) combined with the `core.hooksPath`
33
+ # setting, which git DOES resolve per-worktree when scoped that way. This
34
+ # script:
35
+ # 1. enables `extensions.worktreeConfig` on the repo if not already set
36
+ # (idempotent, additive, does not disturb any existing config value —
37
+ # it only enables the *possibility* of per-worktree config overrides)
38
+ # 2. resolves this worktree's own private git-dir via
39
+ # `git -C <worktree-path> rev-parse --absolute-git-dir` (this IS
40
+ # genuinely private per worktree — `.git/worktrees/<name>` for a linked
41
+ # worktree, verified distinct per worktree in the same test)
42
+ # 3. points `core.hooksPath`, set with `git config --worktree` (so it lands
43
+ # in that worktree's own config.worktree file, not the shared config),
44
+ # at a private `hooks-guard` directory under that private git-dir
45
+ # 4. writes the branch-guard `pre-commit` hook there
46
+ #
47
+ # This was manually verified end-to-end against a scratch repo with two
48
+ # worktrees: installing the guard for worktree A blocked wrong-branch commits
49
+ # in A and had zero effect on worktree B or the main repo, both of which kept
50
+ # using the shared default hooks directory untouched.
51
+ #
52
+ # This script is intended to be called automatically at the end of the
53
+ # `WorktreeCreate` hook in the canonical root `settings.json`, immediately
54
+ # after `git worktree add "$DIR" -b "$NAME"` — see that file for the wiring.
55
+ # It can also be run manually against any existing worktree, and is safe to
56
+ # re-run against the same worktree (idempotent — simply overwrites the hook).
57
+ #
58
+ # Exit codes:
59
+ # 0 hook installed successfully
60
+ # 1 invalid usage (wrong argument count)
61
+ # 2 <worktree-path> is not a valid git working tree
62
+
63
+ set -u
64
+
65
+ usage() {
66
+ echo "Usage: $0 <worktree-path> <branch-name>" >&2
67
+ exit 1
68
+ }
69
+
70
+ [ "$#" -eq 2 ] || usage
71
+ WORKTREE_PATH="$1"
72
+ BRANCH_NAME="$2"
73
+
74
+ [ -n "$WORKTREE_PATH" ] || usage
75
+ [ -n "$BRANCH_NAME" ] || usage
76
+
77
+ if ! git -C "$WORKTREE_PATH" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
78
+ echo "install-worktree-commit-guard: '$WORKTREE_PATH' is not a valid git working tree" >&2
79
+ exit 2
80
+ fi
81
+
82
+ # Enable per-worktree config overrides on the repo, if not already enabled.
83
+ # This is a one-time, repo-wide, additive flag — it does not move or change
84
+ # any existing config value, it only makes `git config --worktree` (used
85
+ # below) actually take effect instead of silently writing to a file git
86
+ # never reads.
87
+ if [ "$(git -C "$WORKTREE_PATH" config --get extensions.worktreeConfig 2>/dev/null)" != "true" ]; then
88
+ git -C "$WORKTREE_PATH" config extensions.worktreeConfig true
89
+ fi
90
+
91
+ # This worktree's own private git-dir. For a linked worktree this resolves to
92
+ # `.git/worktrees/<name>` (private to this worktree); for the main working
93
+ # tree it resolves to the common `.git` dir itself. Either way it is safe to
94
+ # scope a private hooks directory under it.
95
+ PRIVATE_GIT_DIR="$(git -C "$WORKTREE_PATH" rev-parse --absolute-git-dir)"
96
+
97
+ # A distinctly-named directory (not the default `hooks`) so this never
98
+ # collides with, or gets confused for, whatever the shared default hooks
99
+ # directory may already contain.
100
+ HOOKS_DIR="$PRIVATE_GIT_DIR/hooks-guard"
101
+ mkdir -p "$HOOKS_DIR"
102
+
103
+ # Scope the override to THIS worktree only, via its private config.worktree
104
+ # file — not the shared/common config, which would affect every worktree.
105
+ git -C "$WORKTREE_PATH" config --worktree core.hooksPath "$HOOKS_DIR"
106
+
107
+ HOOK_FILE="$HOOKS_DIR/pre-commit"
108
+
109
+ # The expected branch name is baked into the generated hook as a literal
110
+ # comparison value (it is fixed at install time — the branch this worktree
111
+ # was created for). The *actual* branch is always re-checked live, at commit
112
+ # time, via `git rev-parse --abbrev-ref HEAD`.
113
+ cat > "$HOOK_FILE" <<EOF
114
+ #!/usr/bin/env bash
115
+ # Installed by scripts/install-worktree-commit-guard.sh — do not edit by hand.
116
+ # Rejects any commit made while this worktree is not checked out on its
117
+ # assigned branch: '$BRANCH_NAME'.
118
+ set -u
119
+
120
+ expected_branch="$BRANCH_NAME"
121
+ current_branch="\$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
122
+
123
+ if [ "\$current_branch" != "\$expected_branch" ]; then
124
+ echo "pre-commit: commit blocked — this worktree is assigned to branch '\$expected_branch' but HEAD is currently on '\$current_branch'." >&2
125
+ echo "pre-commit: verification/implementation commits for this worktree must land on '\$expected_branch', never on any other branch (including main)." >&2
126
+ exit 1
127
+ fi
128
+
129
+ exit 0
130
+ EOF
131
+
132
+ chmod +x "$HOOK_FILE"
133
+
134
+ echo "install-worktree-commit-guard: installed branch guard for '$BRANCH_NAME' at $HOOK_FILE (core.hooksPath scoped to this worktree only)"
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/jenga-permission-level-switch.sh — switch the current session's
3
+ # 5-tier permission level by copying the matching template into
4
+ # .claude/settings.json and .agents/settings.json, and recording the level
5
+ # in .jenga-permission-level.json at the repo root.
6
+ #
7
+ # Usage: bash scripts/jenga-permission-level-switch.sh <n>
8
+ # <n> must be an integer 1-5.
9
+ #
10
+ # Behavior:
11
+ # - Templates live at templates/permission-levels/level-<n>-<name>.json
12
+ # and are COMPLETE settings.json documents (defaultMode, env,
13
+ # permissions, hooks). The copy is a whole-file overwrite of the
14
+ # destination, not a merge — this intentionally drops any extra
15
+ # top-level blocks (e.g. autoMode) present in the destination but
16
+ # absent from the template. autoMode is never used at any permission
17
+ # level (see PROJECT_SUMMARY.md, epic E33).
18
+ # - Both destination writes use a temp-file-then-atomic-rename pattern.
19
+ # - .jenga-permission-level.json is only written after BOTH settings.json
20
+ # copies succeed. If the .claude/ copy succeeds but the .agents/ copy
21
+ # fails, the script fails loud (no rollback of the .claude/ copy, no
22
+ # write to .jenga-permission-level.json) and instructs the user to
23
+ # re-run — re-running with the same <n> is idempotent and self-heals
24
+ # the inconsistency, since every copy is an unconditional overwrite.
25
+ # - Invalid input (missing/extra args, non-integer, out of range 1-5)
26
+ # exits non-zero and makes zero file changes.
27
+ #
28
+ # Exit codes:
29
+ # 0 Success — all three files updated consistently.
30
+ # 1 Invalid usage/argument — no files changed.
31
+ # 2 Mapped template file missing — no files changed.
32
+ # 3 Failed to copy template to .claude/settings.json — no files changed.
33
+ # 4 Failed to copy template to .agents/settings.json — .claude/settings.json
34
+ # WAS updated (inconsistent state); re-run the script to repair.
35
+ # 5 Failed to write .jenga-permission-level.json — both settings.json
36
+ # files WERE updated (inconsistent state); re-run the script to repair.
37
+
38
+ set -euo pipefail
39
+
40
+ usage() {
41
+ echo "Usage: $(basename "$0") <n> (n must be an integer 1-5)" >&2
42
+ }
43
+
44
+ if [[ $# -ne 1 ]]; then
45
+ echo "Error: expected exactly one argument, got $#." >&2
46
+ usage
47
+ exit 1
48
+ fi
49
+
50
+ LEVEL="$1"
51
+
52
+ if ! [[ "$LEVEL" =~ ^[1-5]$ ]]; then
53
+ echo "Error: permission level must be an integer between 1 and 5, got '$LEVEL'." >&2
54
+ usage
55
+ exit 1
56
+ fi
57
+
58
+ case "$LEVEL" in
59
+ 1) NAME="locked" ;;
60
+ 2) NAME="guarded" ;;
61
+ 3) NAME="standard" ;;
62
+ 4) NAME="elevated" ;;
63
+ 5) NAME="unrestricted" ;;
64
+ esac
65
+
66
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
67
+ TEMPLATE="$REPO_ROOT/templates/permission-levels/level-${LEVEL}-${NAME}.json"
68
+ CLAUDE_SETTINGS="$REPO_ROOT/.claude/settings.json"
69
+ AGENTS_SETTINGS="$REPO_ROOT/.agents/settings.json"
70
+ LEVEL_FILE="$REPO_ROOT/.jenga-permission-level.json"
71
+
72
+ if [[ ! -f "$TEMPLATE" ]]; then
73
+ echo "Error: template file not found for level $LEVEL: $TEMPLATE" >&2
74
+ exit 2
75
+ fi
76
+
77
+ # Whole-file overwrite of destination via temp-file + atomic rename.
78
+ copy_template() {
79
+ local dest="$1"
80
+ local tmp="${dest}.tmp"
81
+ mkdir -p "$(dirname "$dest")"
82
+ cp "$TEMPLATE" "$tmp" && mv "$tmp" "$dest"
83
+ }
84
+
85
+ if ! copy_template "$CLAUDE_SETTINGS"; then
86
+ echo "Error: failed to copy template to $CLAUDE_SETTINGS. No files changed." >&2
87
+ rm -f "${CLAUDE_SETTINGS}.tmp"
88
+ exit 3
89
+ fi
90
+
91
+ if ! copy_template "$AGENTS_SETTINGS"; then
92
+ echo "Error: failed to copy template to $AGENTS_SETTINGS." >&2
93
+ echo "State is now INCONSISTENT: $CLAUDE_SETTINGS was updated to level $LEVEL but $AGENTS_SETTINGS was not." >&2
94
+ echo ".jenga-permission-level.json was NOT updated. Re-run this script to repair (it is idempotent)." >&2
95
+ rm -f "${AGENTS_SETTINGS}.tmp"
96
+ exit 4
97
+ fi
98
+
99
+ # Only write the metadata file after both settings.json copies succeeded.
100
+ LEVEL_TMP="${LEVEL_FILE}.tmp"
101
+ if ! jq -n --argjson n "$LEVEL" '{"session_level": $n}' > "$LEVEL_TMP" || ! mv "$LEVEL_TMP" "$LEVEL_FILE"; then
102
+ echo "Error: failed to write $LEVEL_FILE." >&2
103
+ echo "State is now INCONSISTENT: both settings.json files were updated to level $LEVEL but .jenga-permission-level.json was not. Re-run this script to repair (it is idempotent)." >&2
104
+ rm -f "$LEVEL_TMP"
105
+ exit 5
106
+ fi
107
+
108
+ echo "Switched to level $LEVEL ($NAME)."
109
+ exit 0
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-harness.sh — Per-repository smoke test harness for inline-scoped task execution
3
+ #
4
+ # Usage:
5
+ # bash scripts/smoke-harness.sh [changed_file ...]
6
+ #
7
+ # If changed files are supplied as positional arguments, those are used directly.
8
+ # If no arguments are given, the harness infers changed files from:
9
+ # git diff --name-only HEAD
10
+ #
11
+ # Discovery algorithm (in priority order):
12
+ # 1. scripts/smoke.sh — if present, delegate and return its exit code
13
+ # 2. npm test — if package.json has a .scripts.test entry and jq is available
14
+ # 3. pytest — if pytest is in PATH and setup.py/pyproject.toml exists
15
+ # 4. go test ./... — if go.mod exists
16
+ # 5. No runner found — pass if all changed files are config/docs; fail otherwise
17
+ #
18
+ # Exit codes:
19
+ # 0 — smoke test passed (or structural-only change with no runner available)
20
+ # 1 — smoke test failed (or logic files changed with no runner available)
21
+
22
+ set -uo pipefail
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Resolve repo root
26
+ # ---------------------------------------------------------------------------
27
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Collect changed files
31
+ # ---------------------------------------------------------------------------
32
+ # Accept changed files as positional arguments; if none given, infer from git.
33
+ # Uses a portable loop rather than mapfile/readarray (bash 3.x safe for macOS).
34
+ if [[ $# -gt 0 ]]; then
35
+ CHANGED_FILES=("$@")
36
+ else
37
+ CHANGED_FILES=()
38
+ while IFS= read -r line; do
39
+ [[ -n "$line" ]] && CHANGED_FILES+=("$line")
40
+ done < <(git diff --name-only HEAD 2>/dev/null || true)
41
+ fi
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Step 1: Delegate to scripts/smoke.sh if it exists
45
+ # ---------------------------------------------------------------------------
46
+ if [[ -f "$REPO_ROOT/scripts/smoke.sh" ]]; then
47
+ echo "INFO: Found scripts/smoke.sh — delegating to project smoke test."
48
+ bash "$REPO_ROOT/scripts/smoke.sh"
49
+ exit $?
50
+ fi
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Step 2: Infer npm test
54
+ # Requires: package.json with a non-null .scripts.test field, and jq in PATH.
55
+ # ---------------------------------------------------------------------------
56
+ if [[ -f "$REPO_ROOT/package.json" ]]; then
57
+ if command -v jq > /dev/null 2>&1; then
58
+ if jq -e '.scripts.test // empty' "$REPO_ROOT/package.json" > /dev/null 2>&1; then
59
+ echo "INFO: Detected npm project with test script — running npm test."
60
+ cd "$REPO_ROOT" && npm test
61
+ exit $?
62
+ else
63
+ echo "INFO: package.json found but no .scripts.test entry — skipping npm test inference."
64
+ fi
65
+ else
66
+ echo "INFO: package.json found but jq is not available — skipping npm test inference."
67
+ fi
68
+ fi
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Step 3: Infer pytest
72
+ # Requires: pytest in PATH, and setup.py or pyproject.toml at repo root.
73
+ # ---------------------------------------------------------------------------
74
+ if command -v pytest > /dev/null 2>&1; then
75
+ if [[ -f "$REPO_ROOT/setup.py" || -f "$REPO_ROOT/pyproject.toml" ]]; then
76
+ echo "INFO: Detected Python project — running pytest."
77
+ cd "$REPO_ROOT" && pytest
78
+ exit $?
79
+ fi
80
+ fi
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Step 4: Infer go test
84
+ # Requires: go.mod at repo root.
85
+ # ---------------------------------------------------------------------------
86
+ if [[ -f "$REPO_ROOT/go.mod" ]]; then
87
+ echo "INFO: Detected Go project — running go test ./..."
88
+ cd "$REPO_ROOT" && go test ./...
89
+ exit $?
90
+ fi
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Step 5: No test runner found — classify changed files
94
+ #
95
+ # Files are considered config/documentation if they match one of:
96
+ # - Extension: .json .yaml .yml .md .txt .toml .ini .cfg
97
+ # - Path prefix: docs/ project/ templates/
98
+ # - Shell scripts in scripts/: scripts/*.sh (structural scaffolding, not logic)
99
+ # All other files are treated as logic/source files requiring a real test.
100
+ # ---------------------------------------------------------------------------
101
+ CONFIG_DOC_PATTERN='\.(json|yaml|yml|md|txt|toml|ini|cfg)$'
102
+ STRUCTURAL_SCRIPT_PATTERN='^scripts/[^/]+\.sh$'
103
+ STRUCTURAL_PATH_PATTERN='^(docs|project|templates)/'
104
+
105
+ NON_CONFIG_FILES=()
106
+ for f in "${CHANGED_FILES[@]}"; do
107
+ is_config=false
108
+
109
+ # Check extension pattern
110
+ if echo "$f" | grep -qE "$CONFIG_DOC_PATTERN"; then
111
+ is_config=true
112
+ fi
113
+
114
+ # Check structural script pattern (scripts/*.sh)
115
+ if echo "$f" | grep -qE "$STRUCTURAL_SCRIPT_PATTERN"; then
116
+ is_config=true
117
+ fi
118
+
119
+ # Check structural path prefix
120
+ if echo "$f" | grep -qE "$STRUCTURAL_PATH_PATTERN"; then
121
+ is_config=true
122
+ fi
123
+
124
+ if [[ "$is_config" == "false" ]]; then
125
+ NON_CONFIG_FILES+=("$f")
126
+ fi
127
+ done
128
+
129
+ if [[ ${#NON_CONFIG_FILES[@]} -gt 0 ]]; then
130
+ echo "ERROR: No smoke test runner found and the task touches logic/source files:" >&2
131
+ for f in "${NON_CONFIG_FILES[@]}"; do
132
+ echo " - $f" >&2
133
+ done
134
+ echo "Add a test runner (scripts/smoke.sh, npm test, pytest, or go test) before marking this task complete." >&2
135
+ exit 1
136
+ fi
137
+
138
+ echo "INFO: No test runner discoverable; all changed files are config/docs/structural. Smoke test passes."
139
+ exit 0