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.
Files changed (39) hide show
  1. package/README.md +212 -3
  2. package/ai/agents/agent-resolve-conflicts.xml +148 -3
  3. package/ai/agents/agent-review-verifier.xml +181 -3
  4. package/ai/directives/sdd/audit.directive.xml +22 -11
  5. package/ai/directives/sdd/critic-protocol.xml +3 -0
  6. package/ai/directives/sdd/critic.directive.xml +25 -3
  7. package/ai/skills/README.md +150 -0
  8. package/ai/skills/sdd-check/SKILL.md +19 -9
  9. package/ai/skills/sdd-execute/scripts/README.md +3 -0
  10. package/ai/skills/sdd-execute/scripts/_sdd-lib.sh +60 -0
  11. package/ai/skills/sdd-execute/scripts/check.sh +238 -0
  12. package/ai/skills/sdd-execute/scripts/sdd +12 -0
  13. package/ai/skills/sdd-hooks-install/SKILL.md +88 -0
  14. package/ai/skills/workspace-permission-setup/SKILL.md +193 -0
  15. package/cli/cmd/README.md +185 -0
  16. package/dist/ai/agents/agent-resolve-conflicts.xml +148 -3
  17. package/dist/ai/agents/agent-review-verifier.xml +181 -3
  18. package/dist/ai/directives/sdd/audit.directive.xml +22 -11
  19. package/dist/ai/directives/sdd/critic-protocol.xml +3 -0
  20. package/dist/ai/directives/sdd/critic.directive.xml +25 -3
  21. package/dist/ai/skills/README.md +150 -0
  22. package/dist/ai/skills/sdd-check/SKILL.md +19 -9
  23. package/dist/ai/skills/sdd-execute/scripts/README.md +3 -0
  24. package/dist/ai/skills/sdd-execute/scripts/_sdd-lib.sh +60 -0
  25. package/dist/ai/skills/sdd-execute/scripts/check.sh +238 -0
  26. package/dist/ai/skills/sdd-execute/scripts/sdd +12 -0
  27. package/dist/ai/skills/sdd-hooks-install/SKILL.md +88 -0
  28. package/dist/ai/skills/workspace-permission-setup/SKILL.md +193 -0
  29. package/dist/chunks/index-BHEP1kYv.js +164 -0
  30. package/dist/chunks/index-DqSFtXFv.js +813 -0
  31. package/dist/chunks/index-Ukr3nSKB.js +377 -0
  32. package/dist/cli/cmd/lint/checks/anchor-thin.check.d.ts +11 -0
  33. package/dist/cli/cmd/lint/lint.cmd.d.ts +1 -1
  34. package/dist/cli/cmd/lint/lint.types.d.ts +2 -0
  35. package/dist/gennady.js +4 -4
  36. package/package.json +1 -1
  37. package/dist/chunks/index-4fUNp3za.js +0 -156
  38. package/dist/chunks/index-C9UEDrqg.js +0 -754
  39. 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.