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.
- package/README.md +1 -1
- package/dist/{chunk-5KQ2JDZN.js → chunk-3OFOCOXM.js} +348 -40
- package/dist/chunk-3OFOCOXM.js.map +1 -0
- package/dist/{chunk-OWOW5KFX.js → chunk-6DQD6KVN.js} +2 -2
- package/dist/{chunk-42Q2MAQB.js → chunk-CL7BAUHR.js} +3 -3
- package/dist/{chunk-WVQBG4YR.js → chunk-P2LGPNNY.js} +2 -2
- package/dist/{chunk-ZIEONJBY.js → chunk-YYNJMM7V.js} +2 -2
- package/dist/cli.js +48 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +3 -3
- package/dist/{context-QLQLJOR2.js → context-76QJSCRJ.js} +5 -5
- package/dist/{install-5XZFLN3C.js → install-B5KISSFR.js} +5 -5
- package/dist/{pull-46YFKQ6S.js → pull-4ZQSKFX7.js} +7 -7
- package/dist/{push-FMAHNK4U.js → push-C3QMIEUQ.js} +7 -7
- package/dist/{require-deps-H4SHQWD2.js → require-deps-XUAKG226.js} +3 -3
- package/dist/{stats-YGK6PZ3A.js → stats-ARO5UFFZ.js} +6 -6
- package/dist/{sync-clean-PCR3QCZK.js → sync-clean-UHR2I27F.js} +3 -3
- package/dist/{sync-status-KZSPPHPY.js → sync-status-VZGZCZA3.js} +7 -7
- package/dist/{tui-XXYVOGJL.js → tui-GGUFB6WP.js} +4 -4
- package/package.json +1 -1
- package/dist/chunk-5KQ2JDZN.js.map +0 -1
- /package/dist/{chunk-OWOW5KFX.js.map → chunk-6DQD6KVN.js.map} +0 -0
- /package/dist/{chunk-42Q2MAQB.js.map → chunk-CL7BAUHR.js.map} +0 -0
- /package/dist/{chunk-WVQBG4YR.js.map → chunk-P2LGPNNY.js.map} +0 -0
- /package/dist/{chunk-ZIEONJBY.js.map → chunk-YYNJMM7V.js.map} +0 -0
- /package/dist/{context-QLQLJOR2.js.map → context-76QJSCRJ.js.map} +0 -0
- /package/dist/{install-5XZFLN3C.js.map → install-B5KISSFR.js.map} +0 -0
- /package/dist/{pull-46YFKQ6S.js.map → pull-4ZQSKFX7.js.map} +0 -0
- /package/dist/{push-FMAHNK4U.js.map → push-C3QMIEUQ.js.map} +0 -0
- /package/dist/{require-deps-H4SHQWD2.js.map → require-deps-XUAKG226.js.map} +0 -0
- /package/dist/{stats-YGK6PZ3A.js.map → stats-ARO5UFFZ.js.map} +0 -0
- /package/dist/{sync-clean-PCR3QCZK.js.map → sync-clean-UHR2I27F.js.map} +0 -0
- /package/dist/{sync-status-KZSPPHPY.js.map → sync-status-VZGZCZA3.js.map} +0 -0
- /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
|
|
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
|
|
36
|
-
import { join as
|
|
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
|
|
43
|
-
import { join as
|
|
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
|
|
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
|
|
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 "$
|
|
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 "$
|
|
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
|
|
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 "$
|
|
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-
|
|
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 =
|
|
1619
|
+
const mcpJsonPath = join6(root, ".mcp.json");
|
|
1317
1620
|
try {
|
|
1318
|
-
const mcpJson = JSON.parse(await
|
|
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" ?
|
|
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 =
|
|
1772
|
+
const ignorePath = join7(root, ".claudeignore");
|
|
1468
1773
|
let content;
|
|
1469
1774
|
try {
|
|
1470
|
-
content = await
|
|
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 ??
|
|
1786
|
+
const claudeMdPath = targetPath ?? join7(root, "CLAUDE.md");
|
|
1482
1787
|
let existing;
|
|
1483
1788
|
try {
|
|
1484
|
-
existing = await
|
|
1789
|
+
existing = await readFile5(claudeMdPath, "utf-8");
|
|
1485
1790
|
} catch {
|
|
1486
1791
|
if (!targetPath) return false;
|
|
1487
|
-
await mkdir3(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
1860
|
+
const conventionsPath = join7(root, ".claude", "rules", "conventions.md");
|
|
1556
1861
|
let content;
|
|
1557
1862
|
try {
|
|
1558
|
-
content = await
|
|
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 =
|
|
1569
|
-
const skillPath =
|
|
1570
|
-
const globalPath =
|
|
1571
|
-
const legacyProject =
|
|
1572
|
-
const legacyGlobal =
|
|
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 =
|
|
1581
|
-
const globalPath =
|
|
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
|
|
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(
|
|
2015
|
+
return readJsonFile(join8(root, ".claude", "settings.json"));
|
|
1711
2016
|
}
|
|
1712
2017
|
async function writeSettingsJson(root, settings) {
|
|
1713
|
-
const dir =
|
|
2018
|
+
const dir = join8(root, ".claude");
|
|
1714
2019
|
await mkdir4(dir, { recursive: true });
|
|
1715
|
-
await writeFile5(
|
|
2020
|
+
await writeFile5(join8(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
1716
2021
|
}
|
|
1717
2022
|
async function readSettingsLocalJson(root) {
|
|
1718
|
-
return readJsonFile(
|
|
2023
|
+
return readJsonFile(join8(root, ".claude", "settings.local.json"));
|
|
1719
2024
|
}
|
|
1720
2025
|
async function writeSettingsLocalJson(root, settings) {
|
|
1721
|
-
const dir =
|
|
2026
|
+
const dir = join8(root, ".claude");
|
|
1722
2027
|
await mkdir4(dir, { recursive: true });
|
|
1723
|
-
await writeFile5(
|
|
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-
|
|
2064
|
+
//# sourceMappingURL=chunk-3OFOCOXM.js.map
|