gennady 0.8.2 → 0.8.4
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 +212 -3
- package/ai/agents/agent-resolve-conflicts.xml +148 -3
- package/ai/agents/agent-review-verifier.xml +181 -3
- package/ai/directives/sdd/audit.directive.xml +22 -11
- package/ai/directives/sdd/critic-protocol.xml +3 -0
- package/ai/directives/sdd/critic.directive.xml +25 -3
- package/ai/skills/README.md +150 -0
- package/ai/skills/sdd-check/SKILL.md +19 -9
- package/ai/skills/sdd-execute/scripts/README.md +3 -0
- package/ai/skills/sdd-execute/scripts/_sdd-lib.sh +60 -0
- package/ai/skills/sdd-execute/scripts/check.sh +238 -0
- package/ai/skills/sdd-execute/scripts/sdd +12 -0
- package/ai/skills/sdd-hooks-install/SKILL.md +88 -0
- package/ai/skills/workspace-permission-setup/SKILL.md +193 -0
- package/cli/cmd/README.md +185 -0
- package/dist/ai/agents/agent-resolve-conflicts.xml +148 -3
- package/dist/ai/agents/agent-review-verifier.xml +181 -3
- package/dist/ai/directives/sdd/audit.directive.xml +22 -11
- package/dist/ai/directives/sdd/critic-protocol.xml +3 -0
- package/dist/ai/directives/sdd/critic.directive.xml +25 -3
- package/dist/ai/skills/README.md +150 -0
- package/dist/ai/skills/sdd-check/SKILL.md +19 -9
- package/dist/ai/skills/sdd-execute/scripts/README.md +3 -0
- package/dist/ai/skills/sdd-execute/scripts/_sdd-lib.sh +60 -0
- package/dist/ai/skills/sdd-execute/scripts/check.sh +238 -0
- package/dist/ai/skills/sdd-execute/scripts/sdd +12 -0
- package/dist/ai/skills/sdd-hooks-install/SKILL.md +88 -0
- package/dist/ai/skills/workspace-permission-setup/SKILL.md +193 -0
- package/dist/chunks/index-BHEP1kYv.js +164 -0
- package/dist/chunks/index-DqSFtXFv.js +813 -0
- package/dist/chunks/index-Ukr3nSKB.js +377 -0
- package/dist/cli/cmd/lint/checks/anchor-thin.check.d.ts +11 -0
- package/dist/cli/cmd/lint/lint.cmd.d.ts +1 -1
- package/dist/cli/cmd/lint/lint.types.d.ts +2 -0
- package/dist/gennady.js +4 -4
- package/package.json +1 -1
- package/dist/chunks/index-4fUNp3za.js +0 -156
- package/dist/chunks/index-C9UEDrqg.js +0 -754
- package/dist/chunks/index-jOIhIHdS.js +0 -369
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @file: Shared SDD artifact parsers — sourced by scan.sh / check.sh, never executed directly.
|
|
2
|
+
# @consumers: check.sh (canonical), scan.sh (migration pending).
|
|
3
|
+
# @contract: pure functions of file contents; no stdout side effects beyond the echoed result.
|
|
4
|
+
#
|
|
5
|
+
# Why this lib exists:
|
|
6
|
+
# - check.sh and scan.sh both parse ticket Meta.Status, Task-ID, tracker rows, file headers.
|
|
7
|
+
# - One implementation here = no drift between the two SDD tree tools (the whole point of
|
|
8
|
+
# `sdd check`: a single source of mechanical truth that both sdd-check and sdd-audit consume).
|
|
9
|
+
#
|
|
10
|
+
# macOS bash 3.2 compatible: grep -E / sed -E / awk only. No grep -P, no GNU sed -i.
|
|
11
|
+
|
|
12
|
+
# Extract Meta.Status flag from a ticket. Echoes: DONE | TODO | IN_PROGRESS | BLOCKED | UNKNOWN
|
|
13
|
+
sdd_lib_status() {
|
|
14
|
+
local f="$1" line flag
|
|
15
|
+
line=$(head -60 "$f" 2>/dev/null | grep -m1 -E '^\s*-?\s*\*?\*?Status:\*?\*?\s*\[.\]' || true)
|
|
16
|
+
[[ -z "$line" ]] && { echo "UNKNOWN"; return; }
|
|
17
|
+
flag=$(echo "$line" | sed -nE 's/.*\[(.)\].*/\1/p')
|
|
18
|
+
case "$flag" in
|
|
19
|
+
x|X) echo "DONE" ;;
|
|
20
|
+
' ') echo "TODO" ;;
|
|
21
|
+
'~') echo "IN_PROGRESS" ;;
|
|
22
|
+
'!') echo "BLOCKED" ;;
|
|
23
|
+
*) echo "UNKNOWN" ;;
|
|
24
|
+
esac
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Extract Task-ID (TSK-NN) from a ticket Meta. Echoes the ID or empty string.
|
|
28
|
+
sdd_lib_task_id() {
|
|
29
|
+
local f="$1"
|
|
30
|
+
head -30 "$f" 2>/dev/null \
|
|
31
|
+
| grep -m1 -oE 'Task-ID:\*?\*?\s*TSK-[0-9]+' \
|
|
32
|
+
| grep -oE 'TSK-[0-9]+' || true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Map a tracker-row status cell (`[x]` DONE etc.) to canonical token for ONE Task-ID.
|
|
36
|
+
# Args: <tracker-file> <TSK-NN>. Echoes DONE|TODO|IN_PROGRESS|BLOCKED|UNKNOWN (UNKNOWN if no row).
|
|
37
|
+
sdd_lib_tracker_status() {
|
|
38
|
+
local tr="$1" id="$2" row flag
|
|
39
|
+
# Row form: | [TSK-NN](...) | ... | `[x]` DONE | ... | (also bare TSK-NN)
|
|
40
|
+
row=$(grep -m1 -E "\|[[:space:]]*\[?${id}[]\(]" "$tr" 2>/dev/null || true)
|
|
41
|
+
[[ -z "$row" ]] && { echo "UNKNOWN"; return; }
|
|
42
|
+
flag=$(echo "$row" | sed -nE 's/.*`?\[(.)\]`?[[:space:]]+(DONE|TODO|IN_PROGRESS|BLOCKED).*/\1/p')
|
|
43
|
+
case "$flag" in
|
|
44
|
+
x|X) echo "DONE" ;;
|
|
45
|
+
' ') echo "TODO" ;;
|
|
46
|
+
'~') echo "IN_PROGRESS" ;;
|
|
47
|
+
'!') echo "BLOCKED" ;;
|
|
48
|
+
*) echo "UNKNOWN" ;;
|
|
49
|
+
esac
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Header-trio presence for a source file. Echoes three space-separated flags: <file> <consumers> <tasks>
|
|
53
|
+
# Each flag is 1 (marker present) or 0 (absent). `@tasks: N/A` counts as present.
|
|
54
|
+
sdd_lib_header_flags() {
|
|
55
|
+
local f="$1" hf=0 hc=0 ht=0
|
|
56
|
+
head -20 "$f" 2>/dev/null | grep -qE '@file:' && hf=1
|
|
57
|
+
head -20 "$f" 2>/dev/null | grep -qE '@consumers:' && hc=1
|
|
58
|
+
head -20 "$f" 2>/dev/null | grep -qE '@tasks:' && ht=1
|
|
59
|
+
echo "$hf $hc $ht"
|
|
60
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @file: Deterministic SDD mechanical checks shared by sdd-check (whole tree) and sdd-audit (scoped).
|
|
3
|
+
# @consumers: sdd-check skill (whole-tree preflight); sdd-audit directive STEP_2_5 (scoped pre-pass).
|
|
4
|
+
# @contract: AX_BASH_NO_SILENT_EMPTY. Single source of mechanical truth — neither skill re-implements
|
|
5
|
+
# header presence, Task-ID integrity, or tracker sync. Pure function of files on disk.
|
|
6
|
+
#
|
|
7
|
+
# Three modes:
|
|
8
|
+
# check.sh [project-root] — whole tree: TASKID + TRACKER_SYNC (all tickets) + HEADERS (all marker-bearing src)
|
|
9
|
+
# check.sh --task <TSK-NN> [root] — one ticket: TASKID (collision/orphan-for-its-refs) + TRACKER_SYNC for that id
|
|
10
|
+
# check.sh --files <f1> [f2 ...] — header-trio presence for an explicit file list (audit passes its git-diff scope)
|
|
11
|
+
#
|
|
12
|
+
# Output sections (TSV, machine-readable, stable):
|
|
13
|
+
# [HEADERS] — file \t has_file \t has_consumers \t has_tasks \t verdict(OK|PARTIAL|NONE)
|
|
14
|
+
# [TASKID] — kind(orphan|collision) \t id \t detail
|
|
15
|
+
# [TRACKER_SYNC] — task_id \t ticket_status \t tracker_status \t match(YES|NO|NO_ROW)
|
|
16
|
+
# [SUMMARY] — key=value totals + findings count
|
|
17
|
+
#
|
|
18
|
+
# Exit codes:
|
|
19
|
+
# 0 — all checks clean (zero findings)
|
|
20
|
+
# 3 — one or more findings (desync / orphan / collision / partial-or-missing header)
|
|
21
|
+
# 2 — structural failure (bad root / not an SDD project)
|
|
22
|
+
# 4 — bad invocation
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
PROG="check"
|
|
27
|
+
VERSION="1"
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
# shellcheck source=_sdd-lib.sh
|
|
30
|
+
. "$SCRIPT_DIR/_sdd-lib.sh"
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Argument parsing → MODE
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
MODE="tree"
|
|
37
|
+
TASK_ID=""
|
|
38
|
+
FILES=()
|
|
39
|
+
ROOT="."
|
|
40
|
+
|
|
41
|
+
case "${1:-}" in
|
|
42
|
+
--task)
|
|
43
|
+
MODE="task"
|
|
44
|
+
TASK_ID="${2:-}"
|
|
45
|
+
ROOT="${3:-.}"
|
|
46
|
+
if [[ -z "$TASK_ID" || ! "$TASK_ID" =~ ^TSK-[0-9]+$ ]]; then
|
|
47
|
+
cat <<EOF
|
|
48
|
+
[$PROG] BAD_INVOCATION
|
|
49
|
+
expected: $PROG --task TSK-NN [project-root]
|
|
50
|
+
got: $PROG --task '${TASK_ID:-}' ...
|
|
51
|
+
Required action: pass a Task-ID of the form TSK-<number>.
|
|
52
|
+
EOF
|
|
53
|
+
exit 4
|
|
54
|
+
fi
|
|
55
|
+
;;
|
|
56
|
+
--files)
|
|
57
|
+
MODE="files"
|
|
58
|
+
shift 2>/dev/null || true
|
|
59
|
+
FILES=("$@")
|
|
60
|
+
if [[ ${#FILES[@]} -eq 0 ]]; then
|
|
61
|
+
cat <<EOF
|
|
62
|
+
[$PROG] BAD_INVOCATION
|
|
63
|
+
expected: $PROG --files <file1> [file2 ...]
|
|
64
|
+
got: $PROG --files (no files)
|
|
65
|
+
Required action: pass at least one source file to check headers on.
|
|
66
|
+
EOF
|
|
67
|
+
exit 4
|
|
68
|
+
fi
|
|
69
|
+
;;
|
|
70
|
+
--*)
|
|
71
|
+
cat <<EOF
|
|
72
|
+
[$PROG] BAD_INVOCATION
|
|
73
|
+
unknown flag: $1
|
|
74
|
+
expected: $PROG [project-root] | $PROG --task TSK-NN [root] | $PROG --files <files...>
|
|
75
|
+
EOF
|
|
76
|
+
exit 4
|
|
77
|
+
;;
|
|
78
|
+
*)
|
|
79
|
+
MODE="tree"
|
|
80
|
+
ROOT="${1:-.}"
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
FINDINGS=0
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Mode: --files → HEADERS only
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
emit_header_row() {
|
|
91
|
+
local f="$1" flags hf hc ht verdict
|
|
92
|
+
flags=$(sdd_lib_header_flags "$f")
|
|
93
|
+
hf=$(echo "$flags" | cut -d' ' -f1)
|
|
94
|
+
hc=$(echo "$flags" | cut -d' ' -f2)
|
|
95
|
+
ht=$(echo "$flags" | cut -d' ' -f3)
|
|
96
|
+
if [[ "$hf" -eq 1 && "$ht" -eq 1 ]]; then
|
|
97
|
+
verdict="OK" # @consumers is MINOR; @file + @tasks are the required pair
|
|
98
|
+
elif [[ "$hf" -eq 0 && "$hc" -eq 0 && "$ht" -eq 0 ]]; then
|
|
99
|
+
verdict="NONE"
|
|
100
|
+
else
|
|
101
|
+
verdict="PARTIAL"; FINDINGS=$((FINDINGS+1))
|
|
102
|
+
fi
|
|
103
|
+
printf '%s\t%d\t%d\t%d\t%s\n' "$f" "$hf" "$hc" "$ht" "$verdict"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if [[ "$MODE" == "files" ]]; then
|
|
107
|
+
printf '# sdd check v%s (mode=files)\n' "$VERSION"
|
|
108
|
+
printf '\n[HEADERS]\n# file\thas_file\thas_consumers\thas_tasks\tverdict\n'
|
|
109
|
+
for f in "${FILES[@]}"; do
|
|
110
|
+
if [[ ! -f "$f" ]]; then
|
|
111
|
+
printf '%s\t-\t-\t-\tMISSING_FILE\n' "$f"; FINDINGS=$((FINDINGS+1)); continue
|
|
112
|
+
fi
|
|
113
|
+
emit_header_row "$f"
|
|
114
|
+
done
|
|
115
|
+
printf '\n[SUMMARY]\nmode=files\nfiles_checked=%d\nfindings=%d\n' "${#FILES[@]}" "$FINDINGS"
|
|
116
|
+
[[ "$FINDINGS" -gt 0 ]] && exit 3 || exit 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# tree / task modes need an SDD root
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
if [[ ! -d "$ROOT" ]]; then
|
|
124
|
+
echo "[$PROG] BAD_ROOT: $ROOT is not a directory"; exit 2
|
|
125
|
+
fi
|
|
126
|
+
ROOT_ABS="$(cd "$ROOT" && pwd)"
|
|
127
|
+
if [[ ! -d "$ROOT_ABS/tasks" && ! -d "$ROOT_ABS/specs" ]]; then
|
|
128
|
+
cat <<EOF
|
|
129
|
+
[$PROG] NOT_AN_SDD_PROJECT
|
|
130
|
+
root: $ROOT_ABS
|
|
131
|
+
reason: neither tasks/ nor specs/ found
|
|
132
|
+
Required action: run from an SDD project root, or pass it explicitly.
|
|
133
|
+
EOF
|
|
134
|
+
exit 2
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
printf '# sdd check v%s (mode=%s%s)\n' "$VERSION" "$MODE" "$([[ "$MODE" == task ]] && echo " $TASK_ID")"
|
|
138
|
+
printf 'ROOT=%s\n' "$ROOT_ABS"
|
|
139
|
+
|
|
140
|
+
TASK_FILES=$(find -L "$ROOT_ABS/tasks" -name '*.task-*.md' -type f 2>/dev/null | sort || true)
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# [TASKID] — collisions (global) + orphan @tasks references
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
printf '\n[TASKID]\n# kind\tid\tdetail\n'
|
|
147
|
+
|
|
148
|
+
# Build id → files map to detect collisions (two tickets declaring same Task-ID).
|
|
149
|
+
COLLISION_TMP="$(mktemp -t sdd-check-ids.XXXXXX)"
|
|
150
|
+
trap 'rm -f "$COLLISION_TMP"' EXIT
|
|
151
|
+
while IFS= read -r f; do
|
|
152
|
+
[[ -z "$f" ]] && continue
|
|
153
|
+
id=$(sdd_lib_task_id "$f")
|
|
154
|
+
[[ -z "$id" ]] && continue
|
|
155
|
+
printf '%s\t%s\n' "$id" "${f#$ROOT_ABS/}" >> "$COLLISION_TMP"
|
|
156
|
+
done <<< "$TASK_FILES"
|
|
157
|
+
|
|
158
|
+
# Collisions: ids appearing on >1 ticket. In task mode, restrict to TASK_ID.
|
|
159
|
+
while IFS= read -r id; do
|
|
160
|
+
[[ -z "$id" ]] && continue
|
|
161
|
+
[[ "$MODE" == "task" && "$id" != "$TASK_ID" ]] && continue
|
|
162
|
+
files=$(awk -F'\t' -v k="$id" '$1==k {print $2}' "$COLLISION_TMP" | paste -sd',' -)
|
|
163
|
+
printf 'collision\t%s\t%s\n' "$id" "$files"
|
|
164
|
+
FINDINGS=$((FINDINGS+1))
|
|
165
|
+
done < <(cut -f1 "$COLLISION_TMP" | sort | uniq -d)
|
|
166
|
+
|
|
167
|
+
# Orphans: @tasks: TSK-NN in source with no matching ticket file.
|
|
168
|
+
# Whole-tree mode only (task mode trusts its own ticket exists).
|
|
169
|
+
if [[ "$MODE" == "tree" ]]; then
|
|
170
|
+
known_ids=$(cut -f1 "$COLLISION_TMP" | sort -u)
|
|
171
|
+
# Collect @tasks references from source files (exclude heavy dirs).
|
|
172
|
+
refs=$(grep -rhoE '@tasks:[^@]*' "$ROOT_ABS" \
|
|
173
|
+
--include='*.ts' --include='*.js' --include='*.sh' --include='*.go' \
|
|
174
|
+
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist \
|
|
175
|
+
--exclude-dir=worktrees --exclude-dir=.claude 2>/dev/null \
|
|
176
|
+
| grep -oE 'TSK-[0-9]+' | sort -u || true)
|
|
177
|
+
while IFS= read -r rid; do
|
|
178
|
+
[[ -z "$rid" ]] && continue
|
|
179
|
+
if ! echo "$known_ids" | grep -qx "$rid"; then
|
|
180
|
+
printf 'orphan\t%s\t@tasks reference with no tasks/**/*.task-*.md\n' "$rid"
|
|
181
|
+
FINDINGS=$((FINDINGS+1))
|
|
182
|
+
fi
|
|
183
|
+
done <<< "$refs"
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# [TRACKER_SYNC] — ticket Meta.Status vs tracker-row status
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
printf '\n[TRACKER_SYNC]\n# task_id\tticket_status\ttracker_status\tmatch\n'
|
|
191
|
+
|
|
192
|
+
sync_one() {
|
|
193
|
+
local f="$1" id ticket_status scope tracker tracker_status match
|
|
194
|
+
id=$(sdd_lib_task_id "$f")
|
|
195
|
+
[[ -z "$id" ]] && return
|
|
196
|
+
ticket_status=$(sdd_lib_status "$f")
|
|
197
|
+
# scope = first path segment under tasks/
|
|
198
|
+
scope=$(echo "${f#$ROOT_ABS/tasks/}" | awk -F/ '{print $1}')
|
|
199
|
+
tracker="$ROOT_ABS/tasks/$scope/README.md"
|
|
200
|
+
if [[ ! -f "$tracker" ]]; then
|
|
201
|
+
printf '%s\t%s\t-\tNO_ROW\n' "$id" "$ticket_status"; FINDINGS=$((FINDINGS+1)); return
|
|
202
|
+
fi
|
|
203
|
+
tracker_status=$(sdd_lib_tracker_status "$tracker" "$id")
|
|
204
|
+
if [[ "$ticket_status" == "UNKNOWN" ]]; then
|
|
205
|
+
# Old-template ticket lacking a parseable Meta **Status:** — cannot compare.
|
|
206
|
+
# Not a desync finding (mirrors scan.sh WARN, not ERROR); surface as UNPARSEABLE.
|
|
207
|
+
match="UNPARSEABLE"
|
|
208
|
+
elif [[ "$tracker_status" == "UNKNOWN" ]]; then
|
|
209
|
+
match="NO_ROW"; FINDINGS=$((FINDINGS+1))
|
|
210
|
+
elif [[ "$tracker_status" == "$ticket_status" ]]; then
|
|
211
|
+
match="YES"
|
|
212
|
+
else
|
|
213
|
+
match="NO"; FINDINGS=$((FINDINGS+1))
|
|
214
|
+
fi
|
|
215
|
+
printf '%s\t%s\t%s\t%s\n' "$id" "$ticket_status" "$tracker_status" "$match"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
while IFS= read -r f; do
|
|
219
|
+
[[ -z "$f" ]] && continue
|
|
220
|
+
if [[ "$MODE" == "task" ]]; then
|
|
221
|
+
[[ "$(sdd_lib_task_id "$f")" == "$TASK_ID" ]] || continue
|
|
222
|
+
fi
|
|
223
|
+
sync_one "$f"
|
|
224
|
+
done <<< "$TASK_FILES"
|
|
225
|
+
|
|
226
|
+
# [HEADERS] is intentionally NOT run in tree mode: "which files must carry @tasks"
|
|
227
|
+
# is a policy (task-generated vs hand-authored), not a mechanical fact. Header presence
|
|
228
|
+
# is meaningful only against a known in-scope file set — provided by audit via --files.
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# [SUMMARY]
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
printf '\n[SUMMARY]\nmode=%s\n' "$MODE"
|
|
235
|
+
[[ "$MODE" == "task" ]] && printf 'task=%s\n' "$TASK_ID"
|
|
236
|
+
printf 'findings=%d\n' "$FINDINGS"
|
|
237
|
+
|
|
238
|
+
[[ "$FINDINGS" -gt 0 ]] && exit 3 || exit 0
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
# sdd verify <file>... — comprehensive gate: typecheck + gennady lint + forbidden_grep
|
|
10
10
|
# sdd check-blockers <ticket-file> — scan Execution Log for unresolved BLOCKER entries
|
|
11
11
|
# sdd scan [project-root] — emit comprehensive project snapshot (single rich call)
|
|
12
|
+
# sdd check [root|--task TSK-NN|--files f...] — deterministic mechanical checks (task-id, tracker-sync, headers)
|
|
12
13
|
# sdd help — print this help
|
|
13
14
|
#
|
|
14
15
|
# Why this wrapper exists:
|
|
@@ -44,6 +45,9 @@ case "$SUBCMD" in
|
|
|
44
45
|
scan)
|
|
45
46
|
exec "$SCRIPT_DIR/scan.sh" "$@"
|
|
46
47
|
;;
|
|
48
|
+
check)
|
|
49
|
+
exec "$SCRIPT_DIR/check.sh" "$@"
|
|
50
|
+
;;
|
|
47
51
|
help|--help|-h)
|
|
48
52
|
cat <<'EOF'
|
|
49
53
|
sdd — SDD command dispatcher
|
|
@@ -80,6 +84,14 @@ SUBCOMMANDS
|
|
|
80
84
|
unparseable Meta.Status, broken spec links) are surfaced
|
|
81
85
|
in [WARNINGS]. Defaults to current directory.
|
|
82
86
|
|
|
87
|
+
check [root] Deterministic mechanical checks — the single source of
|
|
88
|
+
check --task TSK-NN [root] mechanical truth shared by sdd-check (whole tree) and
|
|
89
|
+
check --files <f...> sdd-audit (scoped). Emits [TASKID] (collisions, orphan
|
|
90
|
+
@tasks refs), [TRACKER_SYNC] (ticket Meta.Status vs tracker
|
|
91
|
+
row), and — in --files mode — [HEADERS] (@file/@consumers/
|
|
92
|
+
@tasks presence on a caller-supplied in-scope file list).
|
|
93
|
+
Exit 0 clean, 3 findings, 2 structural, 4 bad invocation.
|
|
94
|
+
|
|
83
95
|
check-blockers <ticket> Scan ticket Execution Log for unresolved BLOCKER entries.
|
|
84
96
|
A BLOCKER is considered RESOLVED if a later Round contains
|
|
85
97
|
a "✅ RESOLVED" entry referencing it. Returns list of
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sdd-hooks-install
|
|
3
|
+
description: Install Claude Code hooks for live SDD subagent progress streaming. Adds PreToolUse / PostToolUse / SubagentStop entries to project's .claude/settings.json (merging with existing config), ensures .claude/sdd-progress.ndjson is gitignored, prints the operator's tail command. Use once per project before running /sdd-execute or /sdd-execute-batch when live progress visibility is wanted. Idempotent — safe to re-run.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<SDDHooksInstaller role="config-bootstrapper">
|
|
7
|
+
You install hooks into the CURRENT project to enable live SDD progress tailing. You do NOT install global hooks (those would fire on every project). Idempotent — re-running detects existing entries and skips duplicates.
|
|
8
|
+
|
|
9
|
+
<Protocol>
|
|
10
|
+
1. **Verify project context:**
|
|
11
|
+
- cwd has `.git/` OR a `.claude/` directory OR an `.ai/` directory → proceed.
|
|
12
|
+
- Otherwise → halt: "Not a project root. Run from project's working directory."
|
|
13
|
+
|
|
14
|
+
2. **Ensure `.claude/` exists:**
|
|
15
|
+
- `mkdir -p .claude`
|
|
16
|
+
|
|
17
|
+
3. **Patch `.claude/settings.json`:**
|
|
18
|
+
- File missing → write a fresh one with the hooks block below.
|
|
19
|
+
- File exists → parse JSON. Merge: under `.hooks`, add `PreToolUse`, `PostToolUse`, `SubagentStop` entries. If a matcher with same `command` already exists, skip. Preserve existing hooks.
|
|
20
|
+
- Write back with 2-space indent.
|
|
21
|
+
|
|
22
|
+
Hooks block to add:
|
|
23
|
+
```jsonc
|
|
24
|
+
{
|
|
25
|
+
"hooks": {
|
|
26
|
+
"PreToolUse": [
|
|
27
|
+
{
|
|
28
|
+
"matcher": "*",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "jq -nc --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" --arg tool \"$CLAUDE_HOOK_TOOL_NAME\" --arg agent \"${CLAUDE_HOOK_PARENT_TOOL_USE_ID:-}\" '{ts:$ts, kind:\"pre\", tool:$tool, agent:$agent}' >> .claude/sdd-progress.ndjson"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"PostToolUse": [
|
|
38
|
+
{
|
|
39
|
+
"matcher": "*",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "jq -nc --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" --arg tool \"$CLAUDE_HOOK_TOOL_NAME\" --arg agent \"${CLAUDE_HOOK_PARENT_TOOL_USE_ID:-}\" '{ts:$ts, kind:\"post\", tool:$tool, agent:$agent}' >> .claude/sdd-progress.ndjson"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"SubagentStop": [
|
|
49
|
+
{
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "jq -nc --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" --arg sess \"${CLAUDE_HOOK_SESSION_ID:-}\" '{ts:$ts, kind:\"subagent_stop\", session:$sess}' >> .claude/sdd-progress.ndjson"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
4. **Patch `.gitignore`:**
|
|
63
|
+
- Append `.claude/sdd-progress.ndjson` if not already listed.
|
|
64
|
+
- Create `.gitignore` with that single line if missing.
|
|
65
|
+
|
|
66
|
+
5. **Verify `jq` is available** (Bash `command -v jq`). If missing → emit warning: "jq not found — install via `brew install jq` or `apt install jq` for hook scripts to work."
|
|
67
|
+
|
|
68
|
+
6. **Print to operator** (final message):
|
|
69
|
+
```
|
|
70
|
+
✅ SDD hooks installed in <project-root>/.claude/settings.json
|
|
71
|
+
✅ .gitignore updated to exclude sdd-progress.ndjson
|
|
72
|
+
|
|
73
|
+
To watch live progress in a second terminal:
|
|
74
|
+
tail -f .claude/sdd-progress.ndjson | jq -r '"\(.ts) | \(.kind) | \(.tool // .session) | agent=\(.agent // "-")"'
|
|
75
|
+
|
|
76
|
+
Now run /sdd-execute <TSK-NN> or /sdd-execute-batch in this terminal.
|
|
77
|
+
|
|
78
|
+
To uninstall: edit .claude/settings.json and remove the SDD entries.
|
|
79
|
+
```
|
|
80
|
+
</Protocol>
|
|
81
|
+
|
|
82
|
+
<HardForbidden>
|
|
83
|
+
- Modifying global `~/.claude/settings.json` (this is per-project setup).
|
|
84
|
+
- Overwriting existing settings.json — must merge, preserving operator's other hooks.
|
|
85
|
+
- Adding the file outside .gitignore (would commit operator activity logs).
|
|
86
|
+
- Running the install if jq is unavailable without the warning (hooks would silently fail).
|
|
87
|
+
</HardForbidden>
|
|
88
|
+
</SDDHooksInstaller>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workspace-permission-setup
|
|
3
|
+
description: Configure .claude/settings.json so the agent works autonomously inside the current repository while being auto-denied access to anything outside it (home directory, system paths, network, secrets). Use when the user wants to enable AI-first development with minimal permission prompts, scope the agent to a workspace, set up safe autonomy for a project, stop the agent from leaving the repo, reduce repetitive approval requests, or harden permissions for a codebase.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Workspace Permission Setup
|
|
7
|
+
|
|
8
|
+
Set up `.claude/settings.json` so the agent operates fully autonomously **inside** the current repository, and is **auto-denied** (not prompted) for anything **outside** it. Goal: enable AI-first development without sacrificing safety.
|
|
9
|
+
|
|
10
|
+
## Core principle
|
|
11
|
+
|
|
12
|
+
- **Wide allow inside repo** — file edits, project tooling, git, common shell utilities.
|
|
13
|
+
- **Hard deny outside repo** — `~/`, `/etc`, `/var`, `.ssh`, `.aws`, network commands, `WebFetch`/`WebSearch`, secrets.
|
|
14
|
+
- `deny` always wins over `allow` — the agent gets a refusal, not a prompt, so the user is not interrupted.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
Always run all six steps in order. Never write to disk before Step 5 user approval.
|
|
19
|
+
|
|
20
|
+
### Step 1 — Detect project context
|
|
21
|
+
|
|
22
|
+
Determine repo root and stack so allow-list is tailored, not generic.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git rev-parse --show-toplevel 2>/dev/null || pwd
|
|
26
|
+
ls -la package.json pyproject.toml Cargo.toml go.mod Gemfile pom.xml build.gradle Makefile Dockerfile docker-compose.yml mise.toml .tool-versions 2>/dev/null
|
|
27
|
+
ls -la .claude/ 2>/dev/null
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Map detection → allow additions:
|
|
31
|
+
|
|
32
|
+
| Detected | Add to allow |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `package.json` | `Bash(npm:*)`, `Bash(npx:*)`, `Bash(pnpm:*)`, `Bash(yarn:*)`, `Bash(node:*)` |
|
|
35
|
+
| `pyproject.toml` / `requirements.txt` | `Bash(python:*)`, `Bash(python3:*)`, `Bash(pip:*)`, `Bash(uv:*)`, `Bash(poetry:*)`, `Bash(pytest:*)` |
|
|
36
|
+
| `Cargo.toml` | `Bash(cargo:*)`, `Bash(rustc:*)` |
|
|
37
|
+
| `go.mod` | `Bash(go:*)`, `Bash(gofmt:*)` |
|
|
38
|
+
| `Gemfile` | `Bash(bundle:*)`, `Bash(rake:*)`, `Bash(rspec:*)` |
|
|
39
|
+
| `Makefile` | `Bash(make:*)` |
|
|
40
|
+
| `Dockerfile` / `docker-compose.yml` | (do **not** auto-allow `docker` — ask in Step 4) |
|
|
41
|
+
| `mise.toml` / `.tool-versions` | `Bash(mise:*)` |
|
|
42
|
+
|
|
43
|
+
Also read existing `.claude/settings.json` and `.claude/settings.local.json`. **Preserve** other sections — only the `permissions` block is replaced.
|
|
44
|
+
|
|
45
|
+
### Step 2 — Harvest from transcript history
|
|
46
|
+
|
|
47
|
+
Invoke the `fewer-permission-prompts` skill via the Skill tool. It scans recent transcripts and extracts safe read-only commands the user has been approving repeatedly. Capture its proposal but **do not** let it write — we will merge into our config in Step 3.
|
|
48
|
+
|
|
49
|
+
If the skill is not available or returns nothing useful, skip — Steps 1 and 3 give a working baseline.
|
|
50
|
+
|
|
51
|
+
### Step 3 — Build the proposed config
|
|
52
|
+
|
|
53
|
+
Three sources merged into one block:
|
|
54
|
+
|
|
55
|
+
1. **Universal scoping** (deny + baseline allow — always present).
|
|
56
|
+
2. **Project-specific allow** (from Step 1).
|
|
57
|
+
3. **Transcript-derived allow** (from Step 2).
|
|
58
|
+
|
|
59
|
+
#### Universal scoping deny — always include
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
"deny": [
|
|
63
|
+
"WebFetch",
|
|
64
|
+
"WebSearch",
|
|
65
|
+
|
|
66
|
+
"Read(~/**)",
|
|
67
|
+
"Edit(~/**)",
|
|
68
|
+
"Write(~/**)",
|
|
69
|
+
"Read(/etc/**)",
|
|
70
|
+
"Read(/var/**)",
|
|
71
|
+
"Read(/private/**)",
|
|
72
|
+
"Read(/tmp/**)",
|
|
73
|
+
|
|
74
|
+
"Read(./.env*)",
|
|
75
|
+
"Read(./**/.env*)",
|
|
76
|
+
"Read(./.git/config)",
|
|
77
|
+
|
|
78
|
+
"Bash(curl:*)",
|
|
79
|
+
"Bash(wget:*)",
|
|
80
|
+
"Bash(ssh:*)",
|
|
81
|
+
"Bash(scp:*)",
|
|
82
|
+
"Bash(rsync:*)",
|
|
83
|
+
"Bash(nc:*)",
|
|
84
|
+
"Bash(cd ..*)",
|
|
85
|
+
"Bash(cd /*)",
|
|
86
|
+
"Bash(cd ~*)",
|
|
87
|
+
"Bash(rm -rf:*)",
|
|
88
|
+
"Bash(sudo:*)"
|
|
89
|
+
]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If sensitive directories exist on the machine (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config`, `~/Library`), add explicit absolute-path denies — `~` patterns may not always expand, so be belt-and-suspenders:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
"Read(/Users/<user>/.ssh/**)",
|
|
96
|
+
"Read(/Users/<user>/.aws/**)",
|
|
97
|
+
"Read(/Users/<user>/.gnupg/**)",
|
|
98
|
+
"Read(/Users/<user>/.config/**)",
|
|
99
|
+
"Read(/Users/<user>/Library/**)"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use `$HOME` or detect actual user from `whoami` / `echo $HOME`.
|
|
103
|
+
|
|
104
|
+
#### Universal allow baseline — always include
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
"allow": [
|
|
108
|
+
"Read",
|
|
109
|
+
"Grep",
|
|
110
|
+
"Glob",
|
|
111
|
+
"Edit",
|
|
112
|
+
"Write",
|
|
113
|
+
"Bash(git:*)",
|
|
114
|
+
"Bash(ls:*)",
|
|
115
|
+
"Bash(cat:*)",
|
|
116
|
+
"Bash(head:*)",
|
|
117
|
+
"Bash(tail:*)",
|
|
118
|
+
"Bash(grep:*)",
|
|
119
|
+
"Bash(rg:*)",
|
|
120
|
+
"Bash(find . *)",
|
|
121
|
+
"Bash(sed:*)",
|
|
122
|
+
"Bash(awk:*)",
|
|
123
|
+
"Bash(jq:*)",
|
|
124
|
+
"Bash(diff:*)",
|
|
125
|
+
"Bash(wc:*)",
|
|
126
|
+
"Bash(echo:*)",
|
|
127
|
+
"Bash(pwd)",
|
|
128
|
+
"Bash(mkdir:*)",
|
|
129
|
+
"Bash(touch:*)",
|
|
130
|
+
"Bash(mv:*)",
|
|
131
|
+
"Bash(cp:*)"
|
|
132
|
+
]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Default mode
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
"defaultMode": "acceptEdits"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
File edits inside the repo no longer prompt.
|
|
142
|
+
|
|
143
|
+
### Step 4 — Interactive review with the user
|
|
144
|
+
|
|
145
|
+
Present the **full proposed JSON** to the user, then ask 3–5 targeted questions tied to what was detected. Do not write yet.
|
|
146
|
+
|
|
147
|
+
Standard questions (skip those that don't apply):
|
|
148
|
+
|
|
149
|
+
1. **`git push`** — allow autonomously, or keep asking? Default recommendation: **ask** (irreversible, affects remote).
|
|
150
|
+
2. **Package install** (`npm install`, `pip install`, `cargo add`, etc.) — allow autonomously, or ask? Default recommendation: **ask** (mutates lockfile, pulls supply chain).
|
|
151
|
+
3. **`docker:*`** (if Dockerfile detected) — allow, ask, or deny?
|
|
152
|
+
4. **`WebFetch`** — currently denied. Need it for fetching external docs? Default recommendation: **keep denied**, agent should ask explicitly when needed.
|
|
153
|
+
5. **Anything project-specific** — extra CLIs (`terraform`, `kubectl`, `aws`, `gcloud`, custom scripts in `./bin/`)?
|
|
154
|
+
|
|
155
|
+
Apply answers to the config and show the final version once more.
|
|
156
|
+
|
|
157
|
+
### Step 5 — Write the config (only after explicit approval)
|
|
158
|
+
|
|
159
|
+
Wait for unambiguous user approval ("yes", "ok, write", "go").
|
|
160
|
+
|
|
161
|
+
Then merge with any existing `.claude/settings.json`:
|
|
162
|
+
|
|
163
|
+
- **Preserve** all top-level keys other than `permissions` (e.g., `env`, `hooks`, `model`, `statusLine`).
|
|
164
|
+
- **Replace** the `permissions` block with the new merged result.
|
|
165
|
+
- Use Edit (if file exists) or Write (if not) — never shell redirection.
|
|
166
|
+
|
|
167
|
+
**Do not touch `.claude/settings.local.json`** — that is the user's personal, gitignored layer.
|
|
168
|
+
|
|
169
|
+
### Step 6 — Verify and explain
|
|
170
|
+
|
|
171
|
+
Show the final file content and explain in 4–5 lines:
|
|
172
|
+
|
|
173
|
+
1. Default mode is `acceptEdits` → file edits in the repo run without prompting.
|
|
174
|
+
2. Off-workspace paths (`~`, `/etc`, `.ssh`, etc.) are now **auto-denied** — the agent will not interrupt to ask.
|
|
175
|
+
3. `WebFetch`/`WebSearch` are denied → agent won't try to leave the repo for info unless user explicitly relaxes the rule.
|
|
176
|
+
4. Suggest one quick smoke test: ask the agent to read `~/.ssh/known_hosts`, confirm it gets denied without prompting.
|
|
177
|
+
5. Mention `Shift+Tab` switches modes if user wants to temporarily relax for one task.
|
|
178
|
+
|
|
179
|
+
## Honest limits
|
|
180
|
+
|
|
181
|
+
Bash cannot be perfectly sandboxed via deny patterns alone — an agent can use `bash -c "..."`, `eval`, command substitution, environment manipulation. The patterns block the obvious, common escape paths (`curl`, `cd ..`, `sudo`), which is enough for ~95% of real-world risk. For genuine sandbox guarantees, mention these as follow-ups (do not implement here):
|
|
182
|
+
|
|
183
|
+
- Run Claude Code inside a **devcontainer** (https://code.claude.com/docs/en/devcontainer).
|
|
184
|
+
- Add a **PreToolUse hook** that parses every Bash command and rejects cwd escapes.
|
|
185
|
+
|
|
186
|
+
## Anti-patterns — never do these
|
|
187
|
+
|
|
188
|
+
- Do not overwrite `.claude/settings.json` — always merge other sections.
|
|
189
|
+
- Do not write before explicit user approval in Step 5.
|
|
190
|
+
- Do not allow-list `Bash(curl:*)`, `Bash(wget:*)`, broad `Bash(*)`, or `Bash(sudo:*)`.
|
|
191
|
+
- Do not put deny rules in `.claude/settings.local.json` — security policy belongs in the committed `settings.json` so the whole team inherits it.
|
|
192
|
+
- Do not delete the user's existing `settings.local.json` allow rules — they may have been hand-tuned for personal workflows.
|
|
193
|
+
- Do not skip Step 4 questions — the difference between "agent can `git push` at 3am" and "agent asks first" matters and is project-specific.
|