claude-launchpad 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-5KQ2JDZN.js → chunk-3OFOCOXM.js} +348 -40
  3. package/dist/chunk-3OFOCOXM.js.map +1 -0
  4. package/dist/{chunk-OWOW5KFX.js → chunk-6DQD6KVN.js} +2 -2
  5. package/dist/{chunk-42Q2MAQB.js → chunk-CL7BAUHR.js} +3 -3
  6. package/dist/{chunk-WVQBG4YR.js → chunk-P2LGPNNY.js} +2 -2
  7. package/dist/{chunk-ZIEONJBY.js → chunk-YYNJMM7V.js} +2 -2
  8. package/dist/cli.js +48 -19
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/memory/server.js +3 -3
  11. package/dist/{context-QLQLJOR2.js → context-76QJSCRJ.js} +5 -5
  12. package/dist/{install-5XZFLN3C.js → install-B5KISSFR.js} +5 -5
  13. package/dist/{pull-46YFKQ6S.js → pull-4ZQSKFX7.js} +7 -7
  14. package/dist/{push-FMAHNK4U.js → push-C3QMIEUQ.js} +7 -7
  15. package/dist/{require-deps-H4SHQWD2.js → require-deps-XUAKG226.js} +3 -3
  16. package/dist/{stats-YGK6PZ3A.js → stats-ARO5UFFZ.js} +6 -6
  17. package/dist/{sync-clean-PCR3QCZK.js → sync-clean-UHR2I27F.js} +3 -3
  18. package/dist/{sync-status-KZSPPHPY.js → sync-status-VZGZCZA3.js} +7 -7
  19. package/dist/{tui-XXYVOGJL.js → tui-GGUFB6WP.js} +4 -4
  20. package/package.json +1 -1
  21. package/dist/chunk-5KQ2JDZN.js.map +0 -1
  22. /package/dist/{chunk-OWOW5KFX.js.map → chunk-6DQD6KVN.js.map} +0 -0
  23. /package/dist/{chunk-42Q2MAQB.js.map → chunk-CL7BAUHR.js.map} +0 -0
  24. /package/dist/{chunk-WVQBG4YR.js.map → chunk-P2LGPNNY.js.map} +0 -0
  25. /package/dist/{chunk-ZIEONJBY.js.map → chunk-YYNJMM7V.js.map} +0 -0
  26. /package/dist/{context-QLQLJOR2.js.map → context-76QJSCRJ.js.map} +0 -0
  27. /package/dist/{install-5XZFLN3C.js.map → install-B5KISSFR.js.map} +0 -0
  28. /package/dist/{pull-46YFKQ6S.js.map → pull-4ZQSKFX7.js.map} +0 -0
  29. /package/dist/{push-FMAHNK4U.js.map → push-C3QMIEUQ.js.map} +0 -0
  30. /package/dist/{require-deps-H4SHQWD2.js.map → require-deps-XUAKG226.js.map} +0 -0
  31. /package/dist/{stats-YGK6PZ3A.js.map → stats-ARO5UFFZ.js.map} +0 -0
  32. /package/dist/{sync-clean-PCR3QCZK.js.map → sync-clean-UHR2I27F.js.map} +0 -0
  33. /package/dist/{sync-status-KZSPPHPY.js.map → sync-status-VZGZCZA3.js.map} +0 -0
  34. /package/dist/{tui-XXYVOGJL.js.map → tui-GGUFB6WP.js.map} +0 -0
package/README.md CHANGED
@@ -70,7 +70,7 @@ The three-file split keeps each concern where it belongs:
70
70
  | `TASKS.md` | What we're doing now | Current sprint, session log (empty between sprints) |
71
71
  | `BACKLOG.md` | What we're doing later | WP-NNN template, 7 mandatory fields, P0/P1/P2/P3 sections |
72
72
 
73
- Init generates all three plus `.claude/rules/workflow.md` (path-scoped auto-loads only when editing BACKLOG/TASKS) and a `workflow-check.sh` hook that warns on drift: duplicate WP IDs across files, TASKS.md > 80 lines, `## Current Sprint` > 15 items, `## Session Log` > 3 entries.
73
+ Init generates all three plus `.claude/rules/workflow.md`, a path-scoped rule file Claude auto-loads only when editing BACKLOG.md or TASKS.md. It also installs a `workflow-check.sh` hook that warns on drift: duplicate WP IDs across files, TASKS.md > 80 lines, Current Sprint > 15 items, Session Log > 3 entries.
74
74
 
75
75
  Doctor flags MEDIUM when workflow.md is missing, LOW when the hook is missing, and MEDIUM on duplicate `## Memory` headings in CLAUDE.md. `--fix` installs or repairs any of them without clobbering existing user content. See the [workflow docs](https://mboss37.github.io/claude-launchpad/docs/workflow) for the full lifecycle.
76
76
 
@@ -32,15 +32,15 @@ async function readJsonOrNull(path) {
32
32
  }
33
33
 
34
34
  // src/lib/settings.ts
35
- import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
36
- import { join as join7 } from "path";
35
+ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
36
+ import { join as join8 } from "path";
37
37
 
38
38
  // src/lib/output.ts
39
39
  import chalk from "chalk";
40
40
 
41
41
  // src/commands/doctor/fixer.ts
42
- import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir3, access as access2 } from "fs/promises";
43
- import { join as join6 } from "path";
42
+ import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3, access as access2 } from "fs/promises";
43
+ import { join as join7 } from "path";
44
44
  import { homedir } from "os";
45
45
 
46
46
  // src/lib/sections.ts
@@ -714,6 +714,18 @@ async function addHookToSettings(root, event, dedupKeyword, entry, successMsg) {
714
714
  return true;
715
715
  }
716
716
 
717
+ // src/lib/hook-input.ts
718
+ function jqField(field, fromVar) {
719
+ if (fromVar) {
720
+ return `$(echo "$${fromVar}" | jq -r '.tool_input.${field} // empty' 2>/dev/null)`;
721
+ }
722
+ return `$(jq -r '.tool_input.${field} // empty' 2>/dev/null)`;
723
+ }
724
+ var ENV_VAR_PATTERN = /\$\{?TOOL_INPUT_(FILE_PATH|COMMAND|NEW_TEXT|CONTENT)/;
725
+ function hasEnvVarHookPattern(commandString) {
726
+ return ENV_VAR_PATTERN.test(commandString);
727
+ }
728
+
717
729
  // src/lib/hook-scripts.ts
718
730
  import { writeFile, mkdir, chmod } from "fs/promises";
719
731
  import { join as join2 } from "path";
@@ -757,7 +769,7 @@ var SPRINT_OPEN_CHECK = `#!/usr/bin/env bash
757
769
  # from CLAUDE.md was skipped. Non-blocking (always exits 0).
758
770
 
759
771
  set -u
760
- cmd="\${TOOL_INPUT_COMMAND:-}"
772
+ cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null)
761
773
 
762
774
  # Only act on \`git commit\`, word-boundary match.
763
775
  echo "$cmd" | grep -qE '(^|[^a-zA-Z0-9_-])git[[:space:]]+commit([[:space:]]|$)' || exit 0
@@ -797,7 +809,7 @@ var WORKFLOW_CHECK = `#!/usr/bin/env bash
797
809
  # 4. \\\`## Session Log\\\` has more than 3 entries.
798
810
 
799
811
  set -u
800
- fp="\${TOOL_INPUT_FILE_PATH:-}"
812
+ fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null)
801
813
 
802
814
  # Only act on edits to BACKLOG.md or TASKS.md.
803
815
  echo "$fp" | grep -qE '(^|/)(BACKLOG|TASKS)\\.md$' || exit 0
@@ -893,7 +905,7 @@ async function addSprintCompleteNudge(root) {
893
905
  matcher: "Edit|Write",
894
906
  hooks: [{
895
907
  type: "command",
896
- command: `echo "$TOOL_INPUT_FILE_PATH" | grep -q TASKS.md || exit 0; section=$(sed -n '/^## Current/,/^## /p' TASKS.md 2>/dev/null); [ -z "$section" ] && exit 0; unchecked=$(echo "$section" | grep -cF '- [ ]' || true); checked=$(echo "$section" | grep -cF '- [x]' || true); [ "$unchecked" -eq 0 ] && [ "$checked" -gt 0 ] && echo 'Sprint complete \u2014 all current tasks done. Consider a quick quality check before committing: scan for dead code, debug artifacts, TODO hacks, and convention violations. Run tests if available. Skip if trivial.'; exit 0`
908
+ command: `fp=${jqField("file_path")}; echo "$fp" | grep -q TASKS.md || exit 0; section=$(sed -n '/^## Current/,/^## /p' TASKS.md 2>/dev/null); [ -z "$section" ] && exit 0; unchecked=$(echo "$section" | grep -cF '- [ ]' || true); checked=$(echo "$section" | grep -cF '- [x]' || true); [ "$unchecked" -eq 0 ] && [ "$checked" -gt 0 ] && echo 'Sprint complete \u2014 all current tasks done. Consider a quick quality check before committing: scan for dead code, debug artifacts, TODO hacks, and convention violations. Run tests if available. Skip if trivial.'; exit 0`
897
909
  }]
898
910
  }, "Added sprint-complete nudge hook");
899
911
  }
@@ -1040,6 +1052,207 @@ A PostToolUse hook fires warnings on these conditions (treat as bugs):
1040
1052
  `;
1041
1053
  }
1042
1054
 
1055
+ // src/commands/init/generators/hooks-rule.ts
1056
+ var HOOKS_RULE_VERSION = 1;
1057
+ function generateHooksRule() {
1058
+ return `---
1059
+ paths: [".claude/settings.json", ".claude/settings.local.json"]
1060
+ ---
1061
+
1062
+ # Claude Code Hook Authoring Rules
1063
+
1064
+ <!-- lp-hooks-version: ${HOOKS_RULE_VERSION} -->
1065
+
1066
+ Applies to every hook entry in \`.claude/settings.json\` and \`.claude/settings.local.json\`. Hooks are the project's automated safety net. Getting the API wrong silently disables the protection without any error. **A broken hook is worse than no hook because it gives false confidence.**
1067
+
1068
+ Reference: https://code.claude.com/docs/en/hooks
1069
+
1070
+ ## Anatomy of a hook entry
1071
+
1072
+ \`\`\`json
1073
+ {
1074
+ "hooks": {
1075
+ "<EventName>": [
1076
+ {
1077
+ "matcher": "<ToolMatcher>",
1078
+ "hooks": [
1079
+ { "type": "command", "command": "<shell-string>" }
1080
+ ]
1081
+ }
1082
+ ]
1083
+ }
1084
+ }
1085
+ \`\`\`
1086
+
1087
+ - \`EventName\`: \`SessionStart\`, \`SessionEnd\`, \`PreToolUse\`, \`PostToolUse\`, \`PostCompact\`, \`PreCompact\`, \`UserPromptSubmit\`, \`Stop\`, etc.
1088
+ - \`matcher\`: a regex-style string matching tool names (e.g. \`Bash\`, \`Read|Write|Edit\`). Empty string matches all tools for the event. For SessionStart use \`startup\`, \`resume\`, \`clear\`, or \`compact\`.
1089
+ - \`hooks\` array: every entry runs in parallel when the matcher fires. Identical command strings are deduplicated automatically.
1090
+
1091
+ ## Input \u2014 JSON on stdin, NOT env vars
1092
+
1093
+ The hook receives a JSON payload on stdin. **There are no \`TOOL_INPUT_*\` environment variables in current Claude Code.** A hook that does \`cmd="$TOOL_INPUT_COMMAND"\` reads an empty string and silently no-ops. This is the single most common authoring bug.
1094
+
1095
+ Canonical extraction:
1096
+
1097
+ \`\`\`bash
1098
+ fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null) # PreToolUse Read|Write|Edit
1099
+ cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null) # PreToolUse Bash
1100
+ new=$(jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null) # PostToolUse Edit|Write
1101
+ \`\`\`
1102
+
1103
+ Top-level keys you can read: \`session_id\`, \`transcript_path\`, \`cwd\`, \`permission_mode\`, \`hook_event_name\`, \`tool_name\`, \`tool_input\`, \`tool_use_id\`. \`PostToolUse\` adds \`tool_response\`.
1104
+
1105
+ \`jq\` is a project-level requirement. Install via \`brew install jq\` (macOS) or your distro's package manager.
1106
+
1107
+ ## Exit codes \u2014 2 blocks, 1 does not
1108
+
1109
+ | Exit code | Effect |
1110
+ |---|---|
1111
+ | \`0\` | Allow the action. Hook stdout becomes context (SessionStart) or is informational (PostToolUse). |
1112
+ | \`2\` | Block the action. Hook stderr is fed back to Claude as the error reason. |
1113
+ | any other non-zero (\`1\`, \`127\`, \u2026) | Treated as a non-blocking hook error. **The action proceeds anyway.** |
1114
+
1115
+ \`exit 1\` does NOT block. If the hook is meant to enforce a policy, it must end in \`exit 2\`.
1116
+
1117
+ Block reasons go to **stderr** (\`echo '...' >&2\`), not stdout. Stdout is ignored when exit code is 2 unless the hook returns the JSON envelope (see "Richer control" below).
1118
+
1119
+ ## Output \u2014 stderr for blocks, stdout for warnings
1120
+
1121
+ \`\`\`bash
1122
+ # Blocking: stderr + exit 2
1123
+ echo 'BLOCKED: <reason shown to Claude>' >&2
1124
+ exit 2
1125
+
1126
+ # Informational warning that does not block: stdout + exit 0
1127
+ echo 'WARNING: <message printed to user>'
1128
+ exit 0
1129
+ \`\`\`
1130
+
1131
+ Richer control (only use when the simple form is insufficient):
1132
+
1133
+ \`\`\`bash
1134
+ jq -n '{
1135
+ hookSpecificOutput: {
1136
+ hookEventName: "PreToolUse",
1137
+ permissionDecision: "deny",
1138
+ permissionDecisionReason: "<reason>"
1139
+ }
1140
+ }'
1141
+ exit 0
1142
+ \`\`\`
1143
+
1144
+ ## Multi-hook same matcher \u2014 combine into ONE entry
1145
+
1146
+ If two top-level entries share the same matcher (e.g. two \`PreToolUse\` entries both with \`matcher: "Bash"\`), behavior is undefined: in practice the second entry can fail to fire. **Always combine commands for the same matcher into a single entry's \`hooks\` array.**
1147
+
1148
+ Wrong:
1149
+
1150
+ \`\`\`jsonc
1151
+ "PreToolUse": [
1152
+ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "<destructive-guard>" }] },
1153
+ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "<sprint-open-check>" }] }
1154
+ ]
1155
+ \`\`\`
1156
+
1157
+ Right:
1158
+
1159
+ \`\`\`jsonc
1160
+ "PreToolUse": [
1161
+ {
1162
+ "matcher": "Bash",
1163
+ "hooks": [
1164
+ { "type": "command", "command": "<destructive-guard>" },
1165
+ { "type": "command", "command": "<sprint-open-check>" }
1166
+ ]
1167
+ }
1168
+ ]
1169
+ \`\`\`
1170
+
1171
+ ## Hot-reload \u2014 there is none
1172
+
1173
+ Edits to \`.claude/settings.json\` or \`.claude/settings.local.json\` only take effect after a Claude Code session restart. Mid-session edits parse fine and persist to disk, but the running session keeps the hooks it loaded at start.
1174
+
1175
+ When fixing or adding a hook:
1176
+
1177
+ 1. Edit settings.json
1178
+ 2. Test the hook command in isolation (see "Testing" below)
1179
+ 3. Restart Claude Code
1180
+ 4. Verify the hook fires by triggering the conditions it watches
1181
+
1182
+ Skipping step 3 means the new hook does not exist yet, no matter what the file says.
1183
+
1184
+ ## Testing a hook command in isolation
1185
+
1186
+ Before installing or restarting, verify the command works by piping a fake JSON payload to it:
1187
+
1188
+ \`\`\`bash
1189
+ HOOK=$(jq -r '.hooks.PreToolUse[<index>].hooks[<index>].command' .claude/settings.json)
1190
+ echo '{"tool_input":{"command":"git push --force"}}' | bash -c "$HOOK"
1191
+ echo "exit=$?"
1192
+ \`\`\`
1193
+
1194
+ Expected for a correctly-blocking hook: stderr contains the BLOCKED reason, \`exit=2\`. If you see \`exit=0\` here, the hook will not block in production either.
1195
+
1196
+ For PostToolUse hooks that read file paths or content, build the JSON to mirror the tool you're matching:
1197
+
1198
+ \`\`\`bash
1199
+ echo '{"tool_input":{"file_path":"docs/architecture.md","new_string":"..."}}' | bash -c "$HOOK"
1200
+ \`\`\`
1201
+
1202
+ ## Canonical templates
1203
+
1204
+ ### PreToolUse Bash gate (block on regex match)
1205
+
1206
+ \`\`\`bash
1207
+ cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null); echo "$cmd" | grep -qE '<your-pattern>' || exit 0; echo 'BLOCKED: <reason>' >&2; exit 2
1208
+ \`\`\`
1209
+
1210
+ **Do not anchor the pattern with \`^[[:space:]]*\`** when you want to catch chained commands. The hook receives the entire shell command string verbatim, so an anchored pattern silently misses commands like \`git status && git push\`. Use \`(^|[^[:alnum:]])<token>([[:space:]]|$)\` to match the token at start-of-line OR after a non-alphanumeric separator.
1211
+
1212
+ ### PreToolUse Read|Write|Edit gate (block on file-path match)
1213
+
1214
+ \`\`\`bash
1215
+ fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); echo "$fp" | grep -qE '<your-pattern>' || exit 0; echo 'BLOCKED: <reason>' >&2; exit 2
1216
+ \`\`\`
1217
+
1218
+ ### PostToolUse informational warner (never blocks; just prints)
1219
+
1220
+ \`\`\`bash
1221
+ fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); echo "$fp" | grep -qE '<your-pattern>' || exit 0; echo '<warning text>'; exit 0
1222
+ \`\`\`
1223
+
1224
+ PostToolUse blocking (exit 2) is supported but rare. By the time PostToolUse fires, the file write has already happened. Almost always you want exit 0 with a stdout message.
1225
+
1226
+ ### SessionStart context injection
1227
+
1228
+ \`\`\`bash
1229
+ cat <some-file> 2>/dev/null; exit 0
1230
+ \`\`\`
1231
+
1232
+ Output goes into the session's context. Always exit 0; SessionStart blocking is not a thing.
1233
+
1234
+ ## Adding a new hook \u2014 checklist
1235
+
1236
+ - [ ] Identify the event and matcher you actually need (\`PreToolUse\` for blocking before, \`PostToolUse\` for after-the-fact warnings).
1237
+ - [ ] Read the input via stdin JSON with \`jq\`, **not env vars**.
1238
+ - [ ] If it blocks: send reason to stderr, end with \`exit 2\`. If it warns: stdout, end with \`exit 0\`.
1239
+ - [ ] If a hook for the same matcher already exists, add your command to its existing \`hooks\` array \u2014 do NOT create a second entry with the same matcher.
1240
+ - [ ] Test the command in isolation by piping a fake JSON payload (see "Testing" above) \u2014 verify the exit code matches your intent.
1241
+ - [ ] Restart Claude Code.
1242
+ - [ ] Verify the hook fires under the conditions it watches and does not fire under benign conditions.
1243
+
1244
+ ## Do not
1245
+
1246
+ - Don't reference \`$TOOL_INPUT_COMMAND\`, \`$TOOL_INPUT_FILE_PATH\`, \`$TOOL_INPUT_NEW_TEXT\`, or any other \`TOOL_INPUT_*\` env var. They do not exist; the hook silently no-ops.
1247
+ - Don't \`exit 1\` to block. It is silently non-blocking. Use \`exit 2\`.
1248
+ - Don't echo the block reason to stdout when exiting 2. Stdout is ignored in that case; only stderr reaches Claude.
1249
+ - Don't create a second top-level entry with a matcher that already has an entry. Combine commands in the existing entry's \`hooks\` array.
1250
+ - Don't expect mid-session settings.json edits to take effect. Restart is required.
1251
+ - Don't ship a hook without isolation-testing the command first. A silent no-op is worse than no hook.
1252
+ - Don't put long debug logging into a production hook. If you need diagnostics during development, write to \`/tmp/<name>.log\` and remove the line before commit.
1253
+ `;
1254
+ }
1255
+
1043
1256
  // src/commands/doctor/fixer-quality.ts
1044
1257
  async function createWorkflowRule(root) {
1045
1258
  const rulesDir = join4(root, ".claude", "rules");
@@ -1050,6 +1263,15 @@ async function createWorkflowRule(root) {
1050
1263
  log.success("Created .claude/rules/workflow.md (path-scoped BACKLOG/TASKS workflow rules)");
1051
1264
  return true;
1052
1265
  }
1266
+ async function createHooksRule(root) {
1267
+ const rulesDir = join4(root, ".claude", "rules");
1268
+ const hooksPath = join4(rulesDir, "hooks.md");
1269
+ if (await fileExists(hooksPath)) return false;
1270
+ await mkdir2(rulesDir, { recursive: true });
1271
+ await writeFile3(hooksPath, generateHooksRule());
1272
+ log.success("Created .claude/rules/hooks.md (path-scoped hook authoring rules)");
1273
+ return true;
1274
+ }
1053
1275
  function isMemoryHeading(line) {
1054
1276
  return /^## Memory( \(agentic-memory\))?\s*$/.test(line);
1055
1277
  }
@@ -1118,7 +1340,7 @@ async function addEnvProtectionHook(root) {
1118
1340
  matcher: "Read|Write|Edit",
1119
1341
  hooks: [{
1120
1342
  type: "command",
1121
- command: `echo "$TOOL_INPUT_FILE_PATH" | grep -qE '\\.(env|env\\..*)$' && ! echo "$TOOL_INPUT_FILE_PATH" | grep -q '.env.example' && echo 'BLOCKED: .env files contain secrets' && exit 1; exit 0`
1343
+ command: `fp=${jqField("file_path")}; echo "$fp" | grep -qE '\\.(env|env\\..*)$' && ! echo "$fp" | grep -q '.env.example' && { echo 'BLOCKED: .env files contain secrets' >&2; exit 2; }; exit 0`
1122
1344
  }]
1123
1345
  }, "Added .env file protection hook (PreToolUse)");
1124
1346
  }
@@ -1131,7 +1353,7 @@ async function addAutoFormatHook(root, detected) {
1131
1353
  matcher: "Write|Edit",
1132
1354
  hooks: [{
1133
1355
  type: "command",
1134
- command: `ext=\${TOOL_INPUT_FILE_PATH##*.}; (${extChecks}) && ${config.command} "$TOOL_INPUT_FILE_PATH" 2>/dev/null; exit 0`
1356
+ command: `fp=${jqField("file_path")}; ext="\${fp##*.}"; (${extChecks}) && ${config.command} "$fp" 2>/dev/null; exit 0`
1135
1357
  }]
1136
1358
  }, `Added auto-format hook (PostToolUse \u2192 ${config.command})`);
1137
1359
  }
@@ -1140,7 +1362,7 @@ async function addForcePushProtection(root) {
1140
1362
  matcher: "Bash",
1141
1363
  hooks: [{
1142
1364
  type: "command",
1143
- command: `echo "$TOOL_INPUT_COMMAND" | grep -qE 'push.*--force|push.*-f' && echo 'WARNING: Force push detected \u2014 this can destroy remote history' && exit 1; exit 0`
1365
+ command: `cmd=${jqField("command")}; echo "$cmd" | grep -qE 'push.*--force|push.*-f' && { echo 'WARNING: Force push detected \u2014 this can destroy remote history' >&2; exit 2; }; exit 0`
1144
1366
  }]
1145
1367
  }, "Added force-push protection hook (PreToolUse \u2192 Bash)");
1146
1368
  }
@@ -1157,9 +1379,90 @@ async function addSessionStartHook(root) {
1157
1379
  }, "Added SessionStart hook (injects TASKS.md at startup)");
1158
1380
  }
1159
1381
 
1160
- // src/commands/doctor/fixer-memory.ts
1382
+ // src/commands/doctor/fixer-hook-input.ts
1161
1383
  import { readFile as readFile3 } from "fs/promises";
1162
1384
  import { join as join5 } from "path";
1385
+ function rewriteEnvVarHookCommand(cmd) {
1386
+ if (!hasEnvVarHookPattern(cmd)) return null;
1387
+ if (cmd.includes("BLOCKED: .env files contain secrets")) {
1388
+ return `fp=${jqField("file_path")}; echo "$fp" | grep -qE '\\.(env|env\\..*)$' && ! echo "$fp" | grep -q '.env.example' && { echo 'BLOCKED: .env files contain secrets' >&2; exit 2; }; exit 0`;
1389
+ }
1390
+ if (cmd.includes("BLOCKED: Destructive command detected")) {
1391
+ return `cmd=${jqField("command")}; echo "$cmd" | grep -qE 'rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE|push.*--force|push.*-f' && { echo 'BLOCKED: Destructive command detected' >&2; exit 2; }; exit 0`;
1392
+ }
1393
+ if (cmd.includes("WARNING: Force push detected") || cmd.includes("Force push detected")) {
1394
+ return `cmd=${jqField("command")}; echo "$cmd" | grep -qE 'push.*--force|push.*-f' && { echo 'WARNING: Force push detected \u2014 this can destroy remote history' >&2; exit 2; }; exit 0`;
1395
+ }
1396
+ if (cmd.includes("Sprint complete") && cmd.includes("TASKS.md")) {
1397
+ return `fp=${jqField("file_path")}; echo "$fp" | grep -q TASKS.md || exit 0; section=$(sed -n '/^## Current/,/^## /p' TASKS.md 2>/dev/null); [ -z "$section" ] && exit 0; unchecked=$(echo "$section" | grep -cF '- [ ]' || true); checked=$(echo "$section" | grep -cF '- [x]' || true); [ "$unchecked" -eq 0 ] && [ "$checked" -gt 0 ] && echo 'Sprint complete \u2014 all current tasks done. Consider a quick quality check before committing: scan for dead code, debug artifacts, TODO hacks, and convention violations. Run tests if available. Skip if trivial.'; exit 0`;
1398
+ }
1399
+ const formatMatch = cmd.match(/ext=\$\{TOOL_INPUT_FILE_PATH##\*\.\};\s*\((.+?)\)\s*&&\s*(.+?)\s*"\$TOOL_INPUT_FILE_PATH"/);
1400
+ if (formatMatch) {
1401
+ const extChecks = formatMatch[1].trim();
1402
+ const formatter = formatMatch[2].trim();
1403
+ return `fp=${jqField("file_path")}; ext="\${fp##*.}"; (${extChecks}) && ${formatter} "$fp" 2>/dev/null; exit 0`;
1404
+ }
1405
+ return null;
1406
+ }
1407
+ function rewriteSettingsHooks(settings) {
1408
+ const hooks = settings.hooks;
1409
+ if (!hooks) return { settings, changed: false };
1410
+ let changed = false;
1411
+ const next = {};
1412
+ for (const [event, groups] of Object.entries(hooks)) {
1413
+ next[event] = groups.map((group) => ({
1414
+ ...group,
1415
+ hooks: group.hooks.map((hook) => {
1416
+ if (hook.type !== "command") return hook;
1417
+ if (!hasEnvVarHookPattern(hook.command)) return hook;
1418
+ const rewritten = rewriteEnvVarHookCommand(hook.command);
1419
+ if (rewritten === null) return hook;
1420
+ changed = true;
1421
+ return { ...hook, command: rewritten };
1422
+ })
1423
+ }));
1424
+ }
1425
+ if (!changed) return { settings, changed: false };
1426
+ return { settings: { ...settings, hooks: next }, changed: true };
1427
+ }
1428
+ async function rewriteWrapperScript(scriptPath, rewriter) {
1429
+ let content;
1430
+ try {
1431
+ content = await readFile3(scriptPath, "utf-8");
1432
+ } catch {
1433
+ return false;
1434
+ }
1435
+ if (!hasEnvVarHookPattern(content)) return false;
1436
+ await rewriter();
1437
+ return true;
1438
+ }
1439
+ async function rewriteEnvVarHooks(root) {
1440
+ let didFix = false;
1441
+ const settings = await readSettingsJson(root);
1442
+ if (settings !== null) {
1443
+ const outcome = rewriteSettingsHooks(settings);
1444
+ if (outcome.changed) {
1445
+ await writeSettingsJson(root, outcome.settings);
1446
+ log.success("Rewrote inert $TOOL_INPUT_* hooks in settings.json to canonical jq+stdin form");
1447
+ didFix = true;
1448
+ }
1449
+ }
1450
+ const workflowCheckPath = join5(root, ".claude", "hooks", "workflow-check.sh");
1451
+ if (await rewriteWrapperScript(workflowCheckPath, () => writeWorkflowCheckScript(root))) {
1452
+ log.success("Rewrote .claude/hooks/workflow-check.sh to canonical jq+stdin form");
1453
+ didFix = true;
1454
+ }
1455
+ const sprintOpenPath = join5(root, ".claude", "hooks", "sprint-open-check.sh");
1456
+ if (await rewriteWrapperScript(sprintOpenPath, () => writeSprintHygieneScripts(root))) {
1457
+ log.success("Rewrote .claude/hooks/sprint-open-check.sh to canonical jq+stdin form");
1458
+ didFix = true;
1459
+ }
1460
+ return didFix;
1461
+ }
1462
+
1463
+ // src/commands/doctor/fixer-memory.ts
1464
+ import { readFile as readFile4 } from "fs/promises";
1465
+ import { join as join6 } from "path";
1163
1466
  async function addPlacementHook(root, placement, event, dedupKeyword, entry, prepend, successMsg) {
1164
1467
  const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
1165
1468
  const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
@@ -1313,9 +1616,9 @@ async function addAllowedMcpServers(root, placement) {
1313
1616
  if (settingsServers && typeof settingsServers === "object") {
1314
1617
  for (const name of Object.keys(settingsServers)) serverNames.add(name);
1315
1618
  }
1316
- const mcpJsonPath = join5(root, ".mcp.json");
1619
+ const mcpJsonPath = join6(root, ".mcp.json");
1317
1620
  try {
1318
- const mcpJson = JSON.parse(await readFile3(mcpJsonPath, "utf-8"));
1621
+ const mcpJson = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
1319
1622
  const mcpServers = mcpJson.mcpServers;
1320
1623
  if (mcpServers && typeof mcpServers === "object") {
1321
1624
  for (const name of Object.keys(mcpServers)) serverNames.add(name);
@@ -1351,6 +1654,7 @@ async function applyFixes(issues, projectRoot) {
1351
1654
  return { fixed, skipped };
1352
1655
  }
1353
1656
  var FIX_TABLE = [
1657
+ { analyzer: "Hooks", match: "$TOOL_INPUT_* env var", fix: (root) => rewriteEnvVarHooks(root) },
1354
1658
  { analyzer: "Hooks", match: "No hooks configured", fix: async (root, detected) => {
1355
1659
  const a = await addEnvProtectionHook(root);
1356
1660
  const b = await addAutoFormatHook(root, detected);
@@ -1380,6 +1684,7 @@ var FIX_TABLE = [
1380
1684
  { analyzer: "Rules", match: "No BACKLOG.md", fix: (root) => createBacklogMd(root) },
1381
1685
  { analyzer: "Rules", match: "No .claudeignore", fix: (root, detected) => createClaudeignore(root, detected) },
1382
1686
  { analyzer: "Rules", match: "No .claude/rules/workflow.md", fix: (root) => createWorkflowRule(root) },
1687
+ { analyzer: "Rules", match: "No .claude/rules/hooks.md", fix: (root) => createHooksRule(root) },
1383
1688
  { analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
1384
1689
  { analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
1385
1690
  { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
@@ -1407,7 +1712,7 @@ var FIX_TABLE = [
1407
1712
  { analyzer: "Memory", match: "SessionEnd push hook is not nohup-wrapped", fix: (root) => upgradeStaleSessionEndPushHook(root) },
1408
1713
  { analyzer: "Memory", match: "CLAUDE.md missing memory guidance", fix: (root, _det, placement) => {
1409
1714
  const content = "Use agentic-memory to persist knowledge across sessions:\n- Memories are automatically injected at session start\n- STORE IMMEDIATELY when: a dependency strategy changes, an architecture decision is made, a convention is established, a bug pattern is discovered, or a feature is killed/added\n- Use memory_search before memory_store to check for duplicates\n- NEVER store credentials, API keys, tokens, or secrets in memories";
1410
- const target = placement === "local" ? join6(root, ".claude", "CLAUDE.md") : void 0;
1715
+ const target = placement === "local" ? join7(root, ".claude", "CLAUDE.md") : void 0;
1411
1716
  return addClaudeMdSection(root, "## Memory", wrapStub(content), target);
1412
1717
  } }
1413
1718
  ];
@@ -1464,10 +1769,10 @@ async function removeSandboxSettings(root) {
1464
1769
  return true;
1465
1770
  }
1466
1771
  async function addEnvToClaudeignore(root) {
1467
- const ignorePath = join6(root, ".claudeignore");
1772
+ const ignorePath = join7(root, ".claudeignore");
1468
1773
  let content;
1469
1774
  try {
1470
- content = await readFile4(ignorePath, "utf-8");
1775
+ content = await readFile5(ignorePath, "utf-8");
1471
1776
  } catch {
1472
1777
  return false;
1473
1778
  }
@@ -1478,13 +1783,13 @@ async function addEnvToClaudeignore(root) {
1478
1783
  return true;
1479
1784
  }
1480
1785
  async function addClaudeMdSection(root, heading, content, targetPath) {
1481
- const claudeMdPath = targetPath ?? join6(root, "CLAUDE.md");
1786
+ const claudeMdPath = targetPath ?? join7(root, "CLAUDE.md");
1482
1787
  let existing;
1483
1788
  try {
1484
- existing = await readFile4(claudeMdPath, "utf-8");
1789
+ existing = await readFile5(claudeMdPath, "utf-8");
1485
1790
  } catch {
1486
1791
  if (!targetPath) return false;
1487
- await mkdir3(join6(root, ".claude"), { recursive: true });
1792
+ await mkdir3(join7(root, ".claude"), { recursive: true });
1488
1793
  existing = "# Local Claude Config\n";
1489
1794
  }
1490
1795
  if (existing.includes(heading)) return false;
@@ -1502,7 +1807,7 @@ ${content}
1502
1807
  return true;
1503
1808
  }
1504
1809
  async function createBacklogMd(root) {
1505
- const backlogPath = join6(root, "BACKLOG.md");
1810
+ const backlogPath = join7(root, "BACKLOG.md");
1506
1811
  try {
1507
1812
  await access2(backlogPath);
1508
1813
  return false;
@@ -1514,7 +1819,7 @@ async function createBacklogMd(root) {
1514
1819
  return true;
1515
1820
  }
1516
1821
  async function createClaudeignore(root, detected) {
1517
- const ignorePath = join6(root, ".claudeignore");
1822
+ const ignorePath = join7(root, ".claudeignore");
1518
1823
  try {
1519
1824
  await access2(ignorePath);
1520
1825
  return false;
@@ -1531,7 +1836,7 @@ var SKILL_AUTHORING_SECTION = `
1531
1836
  ${SKILL_AUTHORING_CONTENT}
1532
1837
  `;
1533
1838
  async function createStarterRules(root) {
1534
- const rulesDir = join6(root, ".claude", "rules");
1839
+ const rulesDir = join7(root, ".claude", "rules");
1535
1840
  try {
1536
1841
  await access2(rulesDir);
1537
1842
  return false;
@@ -1539,7 +1844,7 @@ async function createStarterRules(root) {
1539
1844
  }
1540
1845
  await mkdir3(rulesDir, { recursive: true });
1541
1846
  await writeFile4(
1542
- join6(rulesDir, "conventions.md"),
1847
+ join7(rulesDir, "conventions.md"),
1543
1848
  `# Project Conventions
1544
1849
 
1545
1850
  - Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
@@ -1552,10 +1857,10 @@ ${SKILL_AUTHORING_SECTION}`
1552
1857
  return true;
1553
1858
  }
1554
1859
  async function addSkillAuthoringConventions(root) {
1555
- const conventionsPath = join6(root, ".claude", "rules", "conventions.md");
1860
+ const conventionsPath = join7(root, ".claude", "rules", "conventions.md");
1556
1861
  let content;
1557
1862
  try {
1558
- content = await readFile4(conventionsPath, "utf-8");
1863
+ content = await readFile5(conventionsPath, "utf-8");
1559
1864
  } catch {
1560
1865
  return false;
1561
1866
  }
@@ -1565,11 +1870,11 @@ async function addSkillAuthoringConventions(root) {
1565
1870
  return true;
1566
1871
  }
1567
1872
  async function createEnhanceSkill(root) {
1568
- const skillDir = join6(root, ".claude", "skills", "lp-enhance");
1569
- const skillPath = join6(skillDir, "SKILL.md");
1570
- const globalPath = join6(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
1571
- const legacyProject = join6(root, ".claude", "commands", "lp-enhance.md");
1572
- const legacyGlobal = join6(homedir(), ".claude", "commands", "lp-enhance.md");
1873
+ const skillDir = join7(root, ".claude", "skills", "lp-enhance");
1874
+ const skillPath = join7(skillDir, "SKILL.md");
1875
+ const globalPath = join7(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
1876
+ const legacyProject = join7(root, ".claude", "commands", "lp-enhance.md");
1877
+ const legacyGlobal = join7(homedir(), ".claude", "commands", "lp-enhance.md");
1573
1878
  if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return false;
1574
1879
  await mkdir3(skillDir, { recursive: true });
1575
1880
  await writeFile4(skillPath, generateEnhanceSkill());
@@ -1577,8 +1882,8 @@ async function createEnhanceSkill(root) {
1577
1882
  return true;
1578
1883
  }
1579
1884
  async function updateEnhanceSkill(root) {
1580
- const projectPath = join6(root, ".claude", "skills", "lp-enhance", "SKILL.md");
1581
- const globalPath = join6(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
1885
+ const projectPath = join7(root, ".claude", "skills", "lp-enhance", "SKILL.md");
1886
+ const globalPath = join7(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
1582
1887
  const targetPath = await fileExists(projectPath) ? projectPath : await fileExists(globalPath) ? globalPath : null;
1583
1888
  if (!targetPath) return false;
1584
1889
  await writeFile4(targetPath, generateEnhanceSkill());
@@ -1692,7 +1997,7 @@ function renderDoctorReport(results, options) {
1692
1997
  async function readJsonFile(path) {
1693
1998
  let raw;
1694
1999
  try {
1695
- raw = await readFile5(path, "utf-8");
2000
+ raw = await readFile6(path, "utf-8");
1696
2001
  } catch (err) {
1697
2002
  const code = err.code;
1698
2003
  if (code === "ENOENT") return {};
@@ -1707,20 +2012,20 @@ async function readJsonFile(path) {
1707
2012
  }
1708
2013
  }
1709
2014
  async function readSettingsJson(root) {
1710
- return readJsonFile(join7(root, ".claude", "settings.json"));
2015
+ return readJsonFile(join8(root, ".claude", "settings.json"));
1711
2016
  }
1712
2017
  async function writeSettingsJson(root, settings) {
1713
- const dir = join7(root, ".claude");
2018
+ const dir = join8(root, ".claude");
1714
2019
  await mkdir4(dir, { recursive: true });
1715
- await writeFile5(join7(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
2020
+ await writeFile5(join8(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
1716
2021
  }
1717
2022
  async function readSettingsLocalJson(root) {
1718
- return readJsonFile(join7(root, ".claude", "settings.local.json"));
2023
+ return readJsonFile(join8(root, ".claude", "settings.local.json"));
1719
2024
  }
1720
2025
  async function writeSettingsLocalJson(root, settings) {
1721
- const dir = join7(root, ".claude");
2026
+ const dir = join8(root, ".claude");
1722
2027
  await mkdir4(dir, { recursive: true });
1723
- await writeFile5(join7(dir, "settings.local.json"), JSON.stringify(settings, null, 2) + "\n");
2028
+ await writeFile5(join8(dir, "settings.local.json"), JSON.stringify(settings, null, 2) + "\n");
1724
2029
  }
1725
2030
 
1726
2031
  export {
@@ -1744,13 +2049,16 @@ export {
1744
2049
  getMemoryPlacement,
1745
2050
  LP_STUB_OPEN,
1746
2051
  addOrUpdateHook,
2052
+ jqField,
2053
+ hasEnvVarHookPattern,
1747
2054
  writeSprintHygieneScripts,
1748
2055
  writeWorkflowCheckScript,
1749
2056
  generateWorkflowRule,
2057
+ generateHooksRule,
1750
2058
  applyFixes,
1751
2059
  log,
1752
2060
  printBanner,
1753
2061
  printScoreCard,
1754
2062
  renderDoctorReport
1755
2063
  };
1756
- //# sourceMappingURL=chunk-5KQ2JDZN.js.map
2064
+ //# sourceMappingURL=chunk-3OFOCOXM.js.map