@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
|
@@ -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
|