claude-launchpad 1.10.0 → 1.12.0
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 +5 -3
- package/dist/{chunk-WVQBG4YR.js → chunk-DTCCQWOU.js} +2 -2
- package/dist/{chunk-5KQ2JDZN.js → chunk-KPO4YURF.js} +712 -155
- package/dist/chunk-KPO4YURF.js.map +1 -0
- package/dist/{chunk-ZIEONJBY.js → chunk-PH5OWJ42.js} +2 -2
- package/dist/{chunk-42Q2MAQB.js → chunk-ZIG75MOB.js} +3 -3
- package/dist/{chunk-OWOW5KFX.js → chunk-ZLI4LI33.js} +2 -2
- package/dist/cli.js +703 -364
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +3 -3
- package/dist/{context-QLQLJOR2.js → context-JALSYDYO.js} +5 -5
- package/dist/{install-5XZFLN3C.js → install-Q23F37GK.js} +8 -6
- package/dist/install-Q23F37GK.js.map +1 -0
- package/dist/{pull-46YFKQ6S.js → pull-FYKNNLYC.js} +14 -10
- package/dist/pull-FYKNNLYC.js.map +1 -0
- package/dist/{push-FMAHNK4U.js → push-QSBK7BQU.js} +7 -7
- package/dist/{require-deps-H4SHQWD2.js → require-deps-ZISUZLYD.js} +3 -3
- package/dist/{stats-YGK6PZ3A.js → stats-SV53G6PQ.js} +6 -6
- package/dist/sync-2LDM44U4.js +23 -0
- package/dist/sync-2LDM44U4.js.map +1 -0
- package/dist/{sync-clean-PCR3QCZK.js → sync-clean-HQ523N7V.js} +3 -3
- package/dist/{sync-status-KZSPPHPY.js → sync-status-V6WTI32X.js} +7 -7
- package/dist/{tui-XXYVOGJL.js → tui-2UJ2OIHB.js} +4 -4
- package/package.json +1 -1
- package/scenarios/security/env-read-attempt.yaml +46 -0
- package/dist/chunk-5KQ2JDZN.js.map +0 -1
- package/dist/install-5XZFLN3C.js.map +0 -1
- package/dist/pull-46YFKQ6S.js.map +0 -1
- /package/dist/{chunk-WVQBG4YR.js.map → chunk-DTCCQWOU.js.map} +0 -0
- /package/dist/{chunk-ZIEONJBY.js.map → chunk-PH5OWJ42.js.map} +0 -0
- /package/dist/{chunk-42Q2MAQB.js.map → chunk-ZIG75MOB.js.map} +0 -0
- /package/dist/{chunk-OWOW5KFX.js.map → chunk-ZLI4LI33.js.map} +0 -0
- /package/dist/{context-QLQLJOR2.js.map → context-JALSYDYO.js.map} +0 -0
- /package/dist/{push-FMAHNK4U.js.map → push-QSBK7BQU.js.map} +0 -0
- /package/dist/{require-deps-H4SHQWD2.js.map → require-deps-ZISUZLYD.js.map} +0 -0
- /package/dist/{stats-YGK6PZ3A.js.map → stats-SV53G6PQ.js.map} +0 -0
- /package/dist/{sync-clean-PCR3QCZK.js.map → sync-clean-HQ523N7V.js.map} +0 -0
- /package/dist/{sync-status-KZSPPHPY.js.map → sync-status-V6WTI32X.js.map} +0 -0
- /package/dist/{tui-XXYVOGJL.js.map → tui-2UJ2OIHB.js.map} +0 -0
|
@@ -32,23 +32,31 @@ 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 readFile7, 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 readFile6, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
43
|
+
import { join as join7 } from "path";
|
|
44
44
|
import { homedir } from "os";
|
|
45
45
|
|
|
46
46
|
// src/lib/sections.ts
|
|
47
47
|
var SESSION_START_CONTENT = "- ALWAYS read @TASKS.md first \u2014 it tracks progress across sessions\n- Check the Session Log at the bottom of TASKS.md for where we left off\n- Update TASKS.md as you complete work";
|
|
48
48
|
var BACKLOG_CONTENT = "- When a feature is discussed but deferred, add it to BACKLOG.md immediately\n- Never leave future ideas only in TASKS.md or conversation \u2014 they get lost\n- BACKLOG.md is the single source of truth for parked features\n- Every WP uses the 7-field template in BACKLOG.md \u2014 no freeform entries\n- Pull a WP into a sprint = **move**, not copy. A WP lives in exactly one file at a time";
|
|
49
|
-
var STOP_AND_SWARM_CONTENT =
|
|
49
|
+
var STOP_AND_SWARM_CONTENT = 'Three failed iterations on the same problem = stop iterating alone.\n(An iteration = an attempted fix that did not change the failing symptom. Announce "Attempt N" when retrying so the count stays visible.)\nFirst, one systematic pass \u2014 it usually resolves the loop without the swarm:\nreproduce the failure, read the FULL error output, state one hypothesis about the root cause, and verify it BEFORE writing any fix.\nOnly if that pass fails, swarm: dispatch at least 3 parallel subagents via the Task tool \u2014 in a single message so they run concurrently \u2014 each investigating from a different angle:\n1. Root-cause debug agent\n2. Upstream library/docs research agent\n3. Alternative architecture agent\nHand each agent the exact repro command, the full error text, and the list of already-failed fixes \u2014 subagents start with empty context.\nWait for all agents to return, synthesize their findings, then act.\nFor re-planning after repeated failure, switch to plan mode instead of attempting again.';
|
|
50
|
+
var STALE_SWARM_PHRASE = "spin up at least 3 parallel agents via the Agent tool";
|
|
51
|
+
var SWARM_PHRASE_REPLACEMENT = "dispatch at least 3 parallel subagents via the Task tool (in a single message so they run concurrently)";
|
|
50
52
|
var OFF_LIMITS_CONTENT = "- Never hardcode secrets \u2014 use environment variables\n- Never write to `.env` files\n- Never expose internal error details in API responses";
|
|
51
53
|
var SKILL_AUTHORING_CONTENT = 'When creating Claude Code skills (.claude/skills/*/SKILL.md):\n\n- Keep SKILL.md under 500 lines \u2014 move reference material to supporting files in the same directory\n- Front-load description (first 250 chars shown in listings) with TRIGGER when / DO NOT TRIGGER when clauses\n- Add allowed-tools in frontmatter to restrict tool access (e.g. Read, Glob, Grep for read-only skills)\n- Add argument-hint in frontmatter showing the expected input format (use $ARGUMENTS or $0, $1 for dynamic input)\n- Set disable-model-invocation: true for skills with side effects (deploy, send messages)\n- Structure as phases: Research, Plan, Execute, Verify with "Done when:" success criteria per phase\n- Handle edge cases and preconditions before execution';
|
|
54
|
+
function sprintReviewsContent(testCommand, lintCommand, superpowers = false) {
|
|
55
|
+
const known = [testCommand, lintCommand].filter((c) => !!c).map((c) => `\`${c}\``).join(" and ");
|
|
56
|
+
const verifyLine = known ? `- Run ${known} \u2014 must pass before the sprint-ending commit` : "- Run the project's test and typecheck commands \u2014 they must pass before the sprint-ending commit";
|
|
57
|
+
return "When all tasks in the current sprint are complete, review before the closing commit:\n- Find the sprint base: `git log --grep 'chore(sprint-' -n 1 --format=%H`\n- Run /code-review on the diff from that base; fix all Critical and Important findings before committing\n- Run /security-review if the sprint touched auth, input handling, or dependencies\n" + verifyLine + "\n- If /code-review is unavailable, do a manual pass: dead code, debug logs, TODO hacks, convention violations, hardcoded values\n" + (superpowers ? "- superpowers detected: invoke superpowers:requesting-code-review for the independent pass (severity-gated)\n" : "- For an independent second pass, dispatch the code-reviewer agent (.claude/agents/code-reviewer.md) with the base/head SHAs\n") + "- Skip only if the sprint was trivial (docs or config-only changes)";
|
|
58
|
+
}
|
|
59
|
+
var TESTING_DISCIPLINE_CONTENT = "Hard-TDD surfaces \u2014 write the failing test BEFORE any implementation for:\n- A new module, command, or public API\n- Any bug fix (regression test from the minimal repro first)\n- Any algorithm or scoring change\nFlexible (tests land in the same commit, order free): docs, config, version bumps, log messages.\nIf unsure which bucket a change falls into, default to test-first.";
|
|
52
60
|
|
|
53
61
|
// src/lib/detect.ts
|
|
54
62
|
import { join, basename } from "path";
|
|
@@ -350,7 +358,7 @@ DerivedData/
|
|
|
350
358
|
}
|
|
351
359
|
|
|
352
360
|
// src/commands/init/generators/skill-enhance.ts
|
|
353
|
-
var ENHANCE_SKILL_VERSION =
|
|
361
|
+
var ENHANCE_SKILL_VERSION = 10;
|
|
354
362
|
function generateEnhanceSkill() {
|
|
355
363
|
return [
|
|
356
364
|
"---",
|
|
@@ -359,7 +367,7 @@ function generateEnhanceSkill() {
|
|
|
359
367
|
" AI-improve your CLAUDE.md based on codebase analysis. Fills in architecture, conventions, guardrails, and suggests hooks and MCP servers.",
|
|
360
368
|
' TRIGGER when: user runs /lp-enhance, asks to "improve CLAUDE.md", "fill in architecture", or after major refactors.',
|
|
361
369
|
" DO NOT TRIGGER when: user is editing CLAUDE.md manually, doing normal coding, or running doctor/eval.",
|
|
362
|
-
"allowed-tools: Read, Glob, Grep, Edit, Write",
|
|
370
|
+
"allowed-tools: Read, Glob, Grep, Edit, Write, Bash(claude-launchpad doctor:*)",
|
|
363
371
|
"argument-hint: (no arguments needed)",
|
|
364
372
|
"---",
|
|
365
373
|
"",
|
|
@@ -467,21 +475,13 @@ function generateEnhanceSkill() {
|
|
|
467
475
|
"",
|
|
468
476
|
"## Skill Authoring",
|
|
469
477
|
"",
|
|
470
|
-
"
|
|
471
|
-
"",
|
|
472
|
-
"- Keep SKILL.md under 500 lines - move reference material to supporting files in the same directory",
|
|
473
|
-
"- Front-load description (first 250 chars shown in listings) with TRIGGER when / DO NOT TRIGGER when clauses",
|
|
474
|
-
"- Add allowed-tools in frontmatter to restrict tool access (e.g. Read, Glob, Grep for read-only skills)",
|
|
475
|
-
"- Add argument-hint in frontmatter showing the expected input format (use $ARGUMENTS or $0, $1 for dynamic input)",
|
|
476
|
-
"- Set disable-model-invocation: true for skills with side effects (deploy, send messages)",
|
|
477
|
-
'- Structure as phases: Research, Plan, Execute, Verify with "Done when:" success criteria per phase',
|
|
478
|
-
"- Handle edge cases and preconditions before execution",
|
|
478
|
+
...SKILL_AUTHORING_CONTENT.split("\n"),
|
|
479
479
|
"",
|
|
480
480
|
"## Hook review",
|
|
481
481
|
"",
|
|
482
482
|
"Review .claude/settings.json hooks:",
|
|
483
483
|
"- If you see project-specific patterns that deserve hooks, suggest them",
|
|
484
|
-
"- If
|
|
484
|
+
"- If the SessionStart matcher misses compact/clear, suggest widening it to startup|resume|compact|clear (PostCompact stdout is never injected into context - context re-injection belongs on SessionStart)",
|
|
485
485
|
"- If no SessionStart hook exists, suggest one that injects TASKS.md",
|
|
486
486
|
"- DO NOT modify settings.json directly. Print exact JSON to add.",
|
|
487
487
|
"",
|
|
@@ -683,7 +683,7 @@ ${LP_STUB_CLOSE}`;
|
|
|
683
683
|
}
|
|
684
684
|
|
|
685
685
|
// src/commands/doctor/fixer-sprint.ts
|
|
686
|
-
import { writeFile as writeFile2 } from "fs/promises";
|
|
686
|
+
import { writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
|
|
687
687
|
import { join as join3 } from "path";
|
|
688
688
|
|
|
689
689
|
// src/lib/hook-builder.ts
|
|
@@ -696,6 +696,22 @@ function addOrUpdateHook(existingHooks, options) {
|
|
|
696
696
|
if (alreadyHas) {
|
|
697
697
|
return { hooks: existingHooks ?? {}, added: false };
|
|
698
698
|
}
|
|
699
|
+
const matcher = options.entry.matcher ?? "";
|
|
700
|
+
const sameMatcherIdx = hookList.findIndex((g) => String(g.matcher ?? "") === matcher);
|
|
701
|
+
if (sameMatcherIdx >= 0) {
|
|
702
|
+
const group = hookList[sameMatcherIdx];
|
|
703
|
+
const existing = group.hooks ?? [];
|
|
704
|
+
const incoming = options.entry.hooks;
|
|
705
|
+
const merged = {
|
|
706
|
+
...group,
|
|
707
|
+
hooks: options.prepend ? [...incoming, ...existing] : [...existing, ...incoming]
|
|
708
|
+
};
|
|
709
|
+
const updated2 = hookList.map((g, i) => i === sameMatcherIdx ? merged : g);
|
|
710
|
+
return {
|
|
711
|
+
hooks: { ...existingHooks ?? {}, [options.event]: updated2 },
|
|
712
|
+
added: true
|
|
713
|
+
};
|
|
714
|
+
}
|
|
699
715
|
const newEntry = options.entry;
|
|
700
716
|
const updated = options.prepend ? [newEntry, ...hookList] : [...hookList, newEntry];
|
|
701
717
|
return {
|
|
@@ -717,9 +733,37 @@ async function addHookToSettings(root, event, dedupKeyword, entry, successMsg) {
|
|
|
717
733
|
// src/lib/hook-scripts.ts
|
|
718
734
|
import { writeFile, mkdir, chmod } from "fs/promises";
|
|
719
735
|
import { join as join2 } from "path";
|
|
736
|
+
|
|
737
|
+
// src/lib/hook-input.ts
|
|
738
|
+
import { execFileSync } from "child_process";
|
|
739
|
+
function jqField(field, fromVar) {
|
|
740
|
+
if (fromVar) {
|
|
741
|
+
return `$(echo "$${fromVar}" | jq -r '.tool_input.${field} // empty' 2>/dev/null)`;
|
|
742
|
+
}
|
|
743
|
+
return `$(jq -r '.tool_input.${field} // empty' 2>/dev/null)`;
|
|
744
|
+
}
|
|
745
|
+
var ENV_VAR_PATTERN = /\$\{?TOOL_INPUT_(FILE_PATH|COMMAND|NEW_TEXT|CONTENT)/;
|
|
746
|
+
function hasEnvVarHookPattern(commandString) {
|
|
747
|
+
return ENV_VAR_PATTERN.test(commandString);
|
|
748
|
+
}
|
|
749
|
+
var jqChecked = null;
|
|
750
|
+
function isJqAvailable() {
|
|
751
|
+
if (jqChecked !== null) return jqChecked;
|
|
752
|
+
try {
|
|
753
|
+
execFileSync("jq", ["--version"], { stdio: "ignore" });
|
|
754
|
+
jqChecked = true;
|
|
755
|
+
} catch {
|
|
756
|
+
jqChecked = false;
|
|
757
|
+
}
|
|
758
|
+
return jqChecked;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/lib/hook-scripts.ts
|
|
720
762
|
var SPRINT_SIZE_CHECK = `#!/usr/bin/env bash
|
|
721
|
-
# Warns when the current sprint is too small (<3) or too large (>7
|
|
722
|
-
# Sweet spot is 3-6 work
|
|
763
|
+
# Warns when the current sprint is too small (<3) or too large (>7 soft; 15 is
|
|
764
|
+
# the hard split trigger enforced by workflow-check). Sweet spot is 3-6 work
|
|
765
|
+
# packages. Runs on SessionStart, whose stdout is injected into context.
|
|
766
|
+
# Non-blocking (always exits 0).
|
|
723
767
|
|
|
724
768
|
set -u
|
|
725
769
|
tasks="\${1:-TASKS.md}"
|
|
@@ -728,8 +772,9 @@ tasks="\${1:-TASKS.md}"
|
|
|
728
772
|
section=$(sed -n '/^## Current/,/^## /p' "$tasks" 2>/dev/null)
|
|
729
773
|
[ -z "$section" ] && exit 0
|
|
730
774
|
|
|
731
|
-
|
|
732
|
-
|
|
775
|
+
# Anchored so placeholder comments containing "- [ ]" don't count as WPs.
|
|
776
|
+
unchecked=$(echo "$section" | grep -cE '^[[:space:]]*- \\[ \\]' || true)
|
|
777
|
+
checked=$(echo "$section" | grep -cE '^[[:space:]]*- \\[[xX]\\]' || true)
|
|
733
778
|
total=$((unchecked + checked))
|
|
734
779
|
|
|
735
780
|
if [ "$total" -eq 0 ]; then
|
|
@@ -745,99 +790,128 @@ if [ "$unchecked" -lt 3 ]; then
|
|
|
745
790
|
fi
|
|
746
791
|
|
|
747
792
|
if [ "$unchecked" -gt 7 ]; then
|
|
748
|
-
echo "NOTE: Current sprint has $unchecked open work packages \u2014 oversized
|
|
793
|
+
echo "NOTE: Current sprint has $unchecked open work packages \u2014 oversized (soft target 3-6; above 15, workflow-check requires a split)."
|
|
749
794
|
exit 0
|
|
750
795
|
fi
|
|
751
796
|
|
|
752
797
|
exit 0
|
|
753
798
|
`;
|
|
754
799
|
var SPRINT_OPEN_CHECK = `#!/usr/bin/env bash
|
|
755
|
-
#
|
|
756
|
-
#
|
|
757
|
-
#
|
|
800
|
+
# After a git commit, warns when the commit adds WP checkboxes to
|
|
801
|
+
# '## Current Sprint' without deleting anything from BACKLOG.md \u2014 i.e. the
|
|
802
|
+
# "pull = move, not copy" rule was skipped. Runs on PostToolUse (Bash):
|
|
803
|
+
# PreToolUse has no non-blocking way to reach the model, so instead of
|
|
804
|
+
# blocking the commit we suggest an amend. Warning is emitted as
|
|
805
|
+
# additionalContext JSON \u2014 bare stdout on PostToolUse never reaches the model.
|
|
806
|
+
# Non-blocking (always exits 0).
|
|
758
807
|
|
|
759
808
|
set -u
|
|
760
|
-
|
|
809
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
810
|
+
cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
761
811
|
|
|
762
|
-
# Only act on
|
|
812
|
+
# Only act on \\\`git commit\\\`, word-boundary match.
|
|
763
813
|
echo "$cmd" | grep -qE '(^|[^a-zA-Z0-9_-])git[[:space:]]+commit([[:space:]]|$)' || exit 0
|
|
764
814
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
git
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
backlog_deletions=$(git diff --cached BACKLOG.md 2>/dev/null | grep -cE '^-[^-]')
|
|
777
|
-
if [ "$backlog_deletions" -eq 0 ]; then
|
|
778
|
-
echo ""
|
|
779
|
-
echo "WARNING: sprint-open hygiene"
|
|
780
|
-
echo ""
|
|
781
|
-
echo "TASKS.md stages a new '## Current' block, but BACKLOG.md has no"
|
|
782
|
-
echo "staged deletions. When a WP is pulled from BACKLOG.md into a sprint,"
|
|
783
|
-
echo "remove it from BACKLOG.md in the same edit. Overlap = drift."
|
|
784
|
-
echo ""
|
|
785
|
-
echo "If you opened a fresh-scope sprint with no BACKLOG pulls, ignore"
|
|
786
|
-
echo "this. Otherwise scrub BACKLOG.md before committing."
|
|
787
|
-
echo ""
|
|
815
|
+
git rev-parse --verify HEAD >/dev/null 2>&1 || exit 0
|
|
816
|
+
|
|
817
|
+
# The commit must touch TASKS.md and add unchecked WP lines to it.
|
|
818
|
+
git show --name-only --format= HEAD 2>/dev/null | grep -qx 'TASKS.md' || exit 0
|
|
819
|
+
pulled=$(git show --format= HEAD -- TASKS.md 2>/dev/null | grep -cE '^\\+[[:space:]]*- \\[ \\] WP-' || true)
|
|
820
|
+
[ "\${pulled:-0}" -eq 0 ] && exit 0
|
|
821
|
+
|
|
822
|
+
# A pull commit should also delete the WP bodies from BACKLOG.md.
|
|
823
|
+
backlog_deletions=$(git show --format= HEAD -- BACKLOG.md 2>/dev/null | grep -cE '^-[^-]' || true)
|
|
824
|
+
if [ "\${backlog_deletions:-0}" -eq 0 ]; then
|
|
825
|
+
jq -n --arg ctx "Sprint-open hygiene: the commit you just made adds WP checkbox(es) to '## Current Sprint' but deletes nothing from BACKLOG.md. Pulling a WP means MOVING it \u2014 delete its entry from BACKLOG.md in the same commit. If these WPs came from BACKLOG.md, scrub it now and run 'git commit --amend'. If this is a fresh-scope sprint with no backlog pulls, ignore this." '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$ctx}}'
|
|
788
826
|
fi
|
|
789
827
|
|
|
790
828
|
exit 0
|
|
791
829
|
`;
|
|
792
830
|
var WORKFLOW_CHECK = `#!/usr/bin/env bash
|
|
793
831
|
# Warns on BACKLOG.md / TASKS.md drift. Non-blocking (always exits 0).
|
|
794
|
-
#
|
|
832
|
+
# Warnings are emitted as PostToolUse additionalContext JSON so they reach
|
|
833
|
+
# the model \u2014 bare stdout on PostToolUse only reaches the transcript view.
|
|
834
|
+
# 1. A WP entry lives in a BACKLOG.md P-section AND in '## Current Sprint'
|
|
835
|
+
# (violates move-not-copy). Changelog + "Depends on:" mentions are fine.
|
|
795
836
|
# 2. TASKS.md longer than 80 lines.
|
|
796
|
-
# 3.
|
|
797
|
-
# 4.
|
|
837
|
+
# 3. '## Current Sprint' has more than 15 items (hard split trigger).
|
|
838
|
+
# 4. '## Session Log' has more than 3 entries.
|
|
839
|
+
# 5. A pulled WP's "Depends on:" dependency still sits in a P-section.
|
|
798
840
|
|
|
799
841
|
set -u
|
|
800
|
-
|
|
842
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
843
|
+
fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
801
844
|
|
|
802
845
|
# Only act on edits to BACKLOG.md or TASKS.md.
|
|
803
846
|
echo "$fp" | grep -qE '(^|/)(BACKLOG|TASKS)\\.md$' || exit 0
|
|
804
847
|
|
|
805
|
-
|
|
848
|
+
warnings=""
|
|
849
|
+
warn() { warnings="\${warnings}$*
|
|
850
|
+
"; }
|
|
806
851
|
|
|
807
|
-
#
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
852
|
+
# WP IDs that live as entries in BACKLOG P-sections (not Changelog, not Depends-on).
|
|
853
|
+
backlog_ids=""
|
|
854
|
+
if [ -f BACKLOG.md ]; then
|
|
855
|
+
backlog_ids=$(awk '/^## P[0-3]/{f=1;next} /^## /{f=0} f' BACKLOG.md 2>/dev/null | grep -E '^### ' | grep -oE 'WP-[0-9]{3,}' | sort -u || true)
|
|
856
|
+
fi
|
|
857
|
+
|
|
858
|
+
# WP IDs in the Current Sprint checklist.
|
|
859
|
+
sprint_ids=""
|
|
860
|
+
if [ -f TASKS.md ]; then
|
|
861
|
+
sprint_ids=$(awk '/^## Current/{f=1;next} /^## /{f=0} f' TASKS.md 2>/dev/null | grep -oE 'WP-[0-9]{3,}' | sort -u || true)
|
|
862
|
+
fi
|
|
863
|
+
|
|
864
|
+
# 1. Same WP as a live entry in both files.
|
|
865
|
+
if [ -n "$backlog_ids" ] && [ -n "$sprint_ids" ]; then
|
|
866
|
+
dupes=$(comm -12 <(printf '%s\\n' "$backlog_ids") <(printf '%s\\n' "$sprint_ids"))
|
|
812
867
|
if [ -n "$dupes" ]; then
|
|
813
|
-
warn "Workflow bug: WP
|
|
814
|
-
printf '%s\\n' "$dupes"
|
|
815
|
-
warn "Move each listed WP to exactly one file."
|
|
868
|
+
warn "Workflow bug: WP present in BOTH a BACKLOG.md P-section and '## Current Sprint' (violates move-not-copy \u2014 see .claude/rules/workflow.md): $(printf '%s ' $dupes)\u2014 move each listed WP to exactly one file."
|
|
816
869
|
fi
|
|
817
870
|
fi
|
|
818
871
|
|
|
819
|
-
# 2. TASKS.md length.
|
|
820
872
|
if [ -f TASKS.md ]; then
|
|
873
|
+
# 2. TASKS.md length.
|
|
821
874
|
tasks_lines=$(wc -l < TASKS.md 2>/dev/null | tr -d ' ')
|
|
822
875
|
if [ "\${tasks_lines:-0}" -gt 80 ]; then
|
|
823
876
|
warn "TASKS.md is $tasks_lines lines \u2014 should stay under 80. Prune Completed Sprints or Session Log."
|
|
824
877
|
fi
|
|
825
878
|
|
|
826
|
-
# 3. Current Sprint size.
|
|
827
|
-
current_count=$(awk '/^## Current/{flag=1; next} /^## /{flag=0} flag' TASKS.md 2>/dev/null | grep -cE '^[[:space:]]*- \\[[
|
|
879
|
+
# 3. Current Sprint size (hard trigger; the soft target is 3-6).
|
|
880
|
+
current_count=$(awk '/^## Current/{flag=1; next} /^## /{flag=0} flag' TASKS.md 2>/dev/null | grep -cE '^[[:space:]]*- \\[[ xX]\\]' || true)
|
|
828
881
|
if [ "\${current_count:-0}" -gt 15 ]; then
|
|
829
|
-
warn "## Current Sprint has $current_count items \u2014 split the sprint (see .claude/rules/workflow.md)."
|
|
882
|
+
warn "'## Current Sprint' has $current_count items \u2014 split the sprint (see .claude/rules/workflow.md)."
|
|
830
883
|
fi
|
|
831
884
|
|
|
832
885
|
# 4. Session Log size.
|
|
833
886
|
log_count=$(awk '/^## Session Log/{flag=1; next} /^## /{flag=0} flag' TASKS.md 2>/dev/null | grep -cE '^- \\*\\*' || true)
|
|
834
887
|
if [ "\${log_count:-0}" -gt 3 ]; then
|
|
835
|
-
warn "## Session Log has $log_count entries \u2014 keep to 3 max."
|
|
888
|
+
warn "'## Session Log' has $log_count entries \u2014 keep to 3 max."
|
|
836
889
|
fi
|
|
837
890
|
fi
|
|
838
891
|
|
|
892
|
+
# 5. Dependency-aware pulls: the pulled WP's 7-field body left BACKLOG.md, but
|
|
893
|
+
# HEAD's copy (pre-pull) still records its "Depends on:" line.
|
|
894
|
+
if [ -n "$sprint_ids" ] && git rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
895
|
+
for wp in $sprint_ids; do
|
|
896
|
+
deps=$( { git show HEAD:BACKLOG.md 2>/dev/null; cat BACKLOG.md 2>/dev/null; } | awk -v id="$wp" '$0 ~ "^### "id" "{f=1;next} /^### /{f=0} f && /Depends on:/{print}' | grep -oE 'WP-[0-9]{3,}' | sort -u || true)
|
|
897
|
+
for dep in $deps; do
|
|
898
|
+
if printf '%s\\n' "$backlog_ids" | grep -qx "$dep"; then
|
|
899
|
+
warn "$wp was pulled into the sprint but its dependency $dep is still in BACKLOG.md \u2014 pull $dep too, or move $wp back until $dep ships."
|
|
900
|
+
fi
|
|
901
|
+
done
|
|
902
|
+
done
|
|
903
|
+
fi
|
|
904
|
+
|
|
905
|
+
if [ -n "$warnings" ]; then
|
|
906
|
+
jq -n --arg ctx "$warnings" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$ctx}}'
|
|
907
|
+
fi
|
|
839
908
|
exit 0
|
|
840
909
|
`;
|
|
910
|
+
var SPRINT_COMPLETE_NUDGE = `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 -cE '^[[:space:]]*- \\[ \\]' || true); checked=$(echo "$section" | grep -cE '^[[:space:]]*- \\[[xX]\\]' || true); [ "$unchecked" -eq 0 ] && [ "$checked" -gt 0 ] && jq -n --arg ctx 'Sprint complete - all Current Sprint tasks are checked off. Run /code-review on the sprint diff (base: the last chore(sprint- commit)) and fix Critical/Important findings before the closing commit. Skip if the sprint was trivial (docs/config only).' '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$ctx}}'; exit 0`;
|
|
911
|
+
var WORKFLOW_CHECK_WRAPPER = "bash .claude/hooks/workflow-check.sh; exit 0";
|
|
912
|
+
var SPRINT_OPEN_WRAPPER = "bash .claude/hooks/sprint-open-check.sh; exit 0";
|
|
913
|
+
var SPRINT_SIZE_WRAPPER = "bash .claude/hooks/sprint-size-check.sh TASKS.md 2>/dev/null; exit 0";
|
|
914
|
+
var SESSION_START_MATCHER = "startup|resume|compact|clear";
|
|
841
915
|
async function writeSprintHygieneScripts(root) {
|
|
842
916
|
const hooksDir = join2(root, ".claude", "hooks");
|
|
843
917
|
await mkdir(hooksDir, { recursive: true });
|
|
@@ -877,40 +951,93 @@ async function createWorktreeInclude(root) {
|
|
|
877
951
|
async function addSprintSizeHook(root) {
|
|
878
952
|
await writeSprintHygieneScripts(root);
|
|
879
953
|
return addHookToSettings(root, "SessionStart", "sprint-size-check.sh", {
|
|
880
|
-
matcher:
|
|
881
|
-
hooks: [{ type: "command", command:
|
|
954
|
+
matcher: SESSION_START_MATCHER,
|
|
955
|
+
hooks: [{ type: "command", command: SPRINT_SIZE_WRAPPER }]
|
|
882
956
|
}, "Added sprint-size-check hook (warns on microsprint/oversized sprints)");
|
|
883
957
|
}
|
|
884
958
|
async function addSprintOpenHook(root) {
|
|
885
959
|
await writeSprintHygieneScripts(root);
|
|
886
|
-
return addHookToSettings(root, "
|
|
960
|
+
return addHookToSettings(root, "PostToolUse", "sprint-open-check.sh", {
|
|
887
961
|
matcher: "Bash",
|
|
888
|
-
hooks: [{ type: "command", command:
|
|
889
|
-
}, "Added sprint-open-check hook (warns on
|
|
962
|
+
hooks: [{ type: "command", command: SPRINT_OPEN_WRAPPER }]
|
|
963
|
+
}, "Added sprint-open-check hook (warns on WP pulls without BACKLOG cleanup)");
|
|
890
964
|
}
|
|
891
965
|
async function addSprintCompleteNudge(root) {
|
|
892
966
|
return addHookToSettings(root, "PostToolUse", "Sprint complete", {
|
|
893
967
|
matcher: "Edit|Write",
|
|
894
|
-
hooks: [{
|
|
895
|
-
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`
|
|
897
|
-
}]
|
|
968
|
+
hooks: [{ type: "command", command: SPRINT_COMPLETE_NUDGE }]
|
|
898
969
|
}, "Added sprint-complete nudge hook");
|
|
899
970
|
}
|
|
900
971
|
async function addWorkflowCheckHook(root) {
|
|
901
972
|
await writeWorkflowCheckScript(root);
|
|
902
973
|
return addHookToSettings(root, "PostToolUse", "workflow-check.sh", {
|
|
903
974
|
matcher: "Edit|Write",
|
|
904
|
-
hooks: [{ type: "command", command:
|
|
975
|
+
hooks: [{ type: "command", command: WORKFLOW_CHECK_WRAPPER }]
|
|
905
976
|
}, "Added workflow-check hook (BACKLOG/TASKS staleness warnings)");
|
|
906
977
|
}
|
|
978
|
+
async function migrateSprintOpenHookEvent(root) {
|
|
979
|
+
const settings = await readSettingsJson(root);
|
|
980
|
+
if (settings === null) return false;
|
|
981
|
+
const hooks = settings.hooks ?? {};
|
|
982
|
+
const preToolUse = hooks.PreToolUse ?? [];
|
|
983
|
+
let removed = false;
|
|
984
|
+
const cleaned = preToolUse.map((group) => {
|
|
985
|
+
const nested = group.hooks ?? [];
|
|
986
|
+
const filtered = nested.filter((h) => !String(h.command ?? "").includes("sprint-open-check.sh"));
|
|
987
|
+
if (filtered.length !== nested.length) removed = true;
|
|
988
|
+
return { ...group, hooks: filtered };
|
|
989
|
+
}).filter((group) => group.hooks.length > 0);
|
|
990
|
+
if (!removed) return false;
|
|
991
|
+
await writeSettingsJson(root, { ...settings, hooks: { ...hooks, PreToolUse: cleaned } });
|
|
992
|
+
await writeSprintHygieneScripts(root);
|
|
993
|
+
await addSprintOpenHook(root);
|
|
994
|
+
log.success("Moved sprint-open-check to PostToolUse (context injection is impossible on PreToolUse)");
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
async function upgradeStaleNudge(root) {
|
|
998
|
+
const settings = await readSettingsJson(root);
|
|
999
|
+
if (settings === null) return false;
|
|
1000
|
+
const hooks = settings.hooks ?? {};
|
|
1001
|
+
let changed = false;
|
|
1002
|
+
const upgraded = Object.fromEntries(Object.entries(hooks).map(([event, groups]) => [
|
|
1003
|
+
event,
|
|
1004
|
+
groups.map((group) => ({
|
|
1005
|
+
...group,
|
|
1006
|
+
hooks: (group.hooks ?? []).map((h) => {
|
|
1007
|
+
const cmd = String(h.command ?? "");
|
|
1008
|
+
if (cmd.includes("Sprint complete") && !cmd.includes("additionalContext")) {
|
|
1009
|
+
changed = true;
|
|
1010
|
+
return { ...h, command: SPRINT_COMPLETE_NUDGE };
|
|
1011
|
+
}
|
|
1012
|
+
return h;
|
|
1013
|
+
})
|
|
1014
|
+
}))
|
|
1015
|
+
]));
|
|
1016
|
+
if (!changed) return false;
|
|
1017
|
+
await writeSettingsJson(root, { ...settings, hooks: upgraded });
|
|
1018
|
+
log.success("Upgraded sprint-complete nudge to additionalContext JSON (was invisible bare stdout)");
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
async function refreshHygieneScripts(root) {
|
|
1022
|
+
const scripts = [".claude/hooks/workflow-check.sh", ".claude/hooks/sprint-open-check.sh"];
|
|
1023
|
+
let stale = false;
|
|
1024
|
+
for (const rel of scripts) {
|
|
1025
|
+
const content = await readFile2(join3(root, rel), "utf-8").catch(() => null);
|
|
1026
|
+
if (content !== null && !content.includes("hookSpecificOutput")) stale = true;
|
|
1027
|
+
}
|
|
1028
|
+
if (!stale) return false;
|
|
1029
|
+
await writeSprintHygieneScripts(root);
|
|
1030
|
+
await writeWorkflowCheckScript(root);
|
|
1031
|
+
log.success("Refreshed hygiene scripts (warnings now emitted as additionalContext JSON)");
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
907
1034
|
|
|
908
1035
|
// src/commands/doctor/fixer-quality.ts
|
|
909
|
-
import { readFile as
|
|
1036
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
910
1037
|
import { join as join4 } from "path";
|
|
911
1038
|
|
|
912
1039
|
// src/commands/init/generators/workflow-rule.ts
|
|
913
|
-
var WORKFLOW_RULE_VERSION =
|
|
1040
|
+
var WORKFLOW_RULE_VERSION = 2;
|
|
914
1041
|
function generateWorkflowRule() {
|
|
915
1042
|
return `---
|
|
916
1043
|
paths: ["BACKLOG.md", "TASKS.md"]
|
|
@@ -928,14 +1055,14 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
|
|
|
928
1055
|
|
|
929
1056
|
- Pulled into a sprint \u2192 move (delete from BACKLOG.md, add to TASKS.md under \`## Current Sprint\`) in a single edit.
|
|
930
1057
|
- Sprint closed \u2192 the WP leaves TASKS.md entirely (summarized into \`## Completed Sprints\`) and does not return to the backlog.
|
|
931
|
-
- A WP
|
|
1058
|
+
- A WP entry appearing in both files at once = bug. \`workflow-check.sh\` injects a warning into context when it sees one (Changelog and \`Depends on:\` mentions are fine).
|
|
932
1059
|
|
|
933
1060
|
## WP IDs
|
|
934
1061
|
|
|
935
|
-
- Format: \`WP-NNN\` (three digits, zero-padded).
|
|
936
|
-
- Minted on first entry to \`BACKLOG.md\`.
|
|
1062
|
+
- Format: \`WP-NNN\` (three digits, zero-padded; grows to four digits past WP-999).
|
|
1063
|
+
- Minted on first entry to \`BACKLOG.md\`; log every mint in \`## Changelog\`: \`YYYY-MM-DD: WP-NNN added (P1)\`.
|
|
937
1064
|
- Never reused, never renamed.
|
|
938
|
-
-
|
|
1065
|
+
- To find the highest used ID before minting: \`grep -ohE 'WP-[0-9]+' BACKLOG.md TASKS.md | sort -V | tail -1\`.
|
|
939
1066
|
|
|
940
1067
|
## BACKLOG.md structure (mandatory sections, in order)
|
|
941
1068
|
|
|
@@ -968,7 +1095,7 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
|
|
|
968
1095
|
|
|
969
1096
|
### Changelog discipline
|
|
970
1097
|
|
|
971
|
-
- Every WP promotion, demotion, or deletion gets a one-line entry with the date.
|
|
1098
|
+
- Every WP mint, promotion, demotion, or deletion gets a one-line entry with the date.
|
|
972
1099
|
- A backlog audit that finds no changelog entries for 30+ days = staleness; force a review.
|
|
973
1100
|
|
|
974
1101
|
## TASKS.md structure (mandatory sections, in order)
|
|
@@ -999,7 +1126,8 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
|
|
|
999
1126
|
### Size discipline
|
|
1000
1127
|
|
|
1001
1128
|
- Whole file stays under 80 lines.
|
|
1002
|
-
-
|
|
1129
|
+
- Aim for 3-6 WPs per sprint (soft target \u2014 \`sprint-size-check.sh\` nudges at session start).
|
|
1130
|
+
- If \`## Current Sprint\` exceeds 15 checkboxes, the sprint is too big \u2014 split it (hard trigger; \`workflow-check.sh\` warns).
|
|
1003
1131
|
|
|
1004
1132
|
## Sprint lifecycle (the exact edit sequence)
|
|
1005
1133
|
|
|
@@ -1009,13 +1137,13 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
|
|
|
1009
1137
|
2. **Same edit:** delete them from \`BACKLOG.md\`, add them to \`TASKS.md ## Current Sprint\`.
|
|
1010
1138
|
3. Update \`BACKLOG.md ## Changelog\`: \`YYYY-MM-DD: WP-NNN pulled into Sprint SN\`.
|
|
1011
1139
|
4. Write the sprint plan (outline approach, success criteria, tests to add).
|
|
1012
|
-
5. For hard-TDD surfaces, write the test spec **before** implementation.
|
|
1140
|
+
5. For hard-TDD surfaces (see "Testing Discipline" in \`.claude/rules/conventions.md\`), write the test spec **before** implementation.
|
|
1013
1141
|
6. Commit the pull + plan together: \`chore(sprint-N): pull WP-NNN into sprint + plan\`.
|
|
1014
1142
|
|
|
1015
1143
|
### Closing a sprint (one session, one commit)
|
|
1016
1144
|
|
|
1017
1145
|
1. All \`## Current Sprint\` items checked off, or explicitly moved back to backlog with rationale.
|
|
1018
|
-
2.
|
|
1146
|
+
2. Review the sprint diff: find the base with \`git log --grep 'chore(sprint-' -n 1 --format=%H\`, run \`/code-review\` against it, and fix all Critical/Important findings. Then run the project's test and typecheck commands \u2014 both must pass.
|
|
1019
1147
|
3. Add one-line summary to \`## Completed Sprints\`.
|
|
1020
1148
|
4. Empty \`## Current Sprint\` back to the placeholder comment.
|
|
1021
1149
|
5. Update \`## Session Log\` (prune to 3 entries).
|
|
@@ -1023,12 +1151,15 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
|
|
|
1023
1151
|
|
|
1024
1152
|
## What triggers a staleness warning
|
|
1025
1153
|
|
|
1026
|
-
|
|
1154
|
+
\`workflow-check.sh\` (PostToolUse on BACKLOG/TASKS edits) injects warnings into context \u2014 as \`additionalContext\`, so the model actually sees them \u2014 on these conditions (treat as bugs):
|
|
1027
1155
|
|
|
1028
|
-
- A WP
|
|
1156
|
+
- A WP entry lives in both a \`BACKLOG.md\` P-section and \`## Current Sprint\`.
|
|
1029
1157
|
- \`TASKS.md\` exceeds 80 lines.
|
|
1030
1158
|
- \`## Current Sprint\` has >15 items.
|
|
1031
1159
|
- \`## Session Log\` has >3 entries.
|
|
1160
|
+
- A pulled WP's \`Depends on:\` dependency still sits in a BACKLOG P-section.
|
|
1161
|
+
|
|
1162
|
+
\`sprint-open-check.sh\` (PostToolUse on Bash) additionally warns after a \`git commit\` that pulls WPs into the sprint without deleting anything from \`BACKLOG.md\` \u2014 fix with \`git commit --amend\` before pushing.
|
|
1032
1163
|
|
|
1033
1164
|
## Do not
|
|
1034
1165
|
|
|
@@ -1040,6 +1171,253 @@ A PostToolUse hook fires warnings on these conditions (treat as bugs):
|
|
|
1040
1171
|
`;
|
|
1041
1172
|
}
|
|
1042
1173
|
|
|
1174
|
+
// src/commands/init/generators/hooks-rule.ts
|
|
1175
|
+
var HOOKS_RULE_VERSION = 1;
|
|
1176
|
+
function generateHooksRule() {
|
|
1177
|
+
return `---
|
|
1178
|
+
paths: [".claude/settings.json", ".claude/settings.local.json"]
|
|
1179
|
+
---
|
|
1180
|
+
|
|
1181
|
+
# Claude Code Hook Authoring Rules
|
|
1182
|
+
|
|
1183
|
+
<!-- lp-hooks-version: ${HOOKS_RULE_VERSION} -->
|
|
1184
|
+
|
|
1185
|
+
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.**
|
|
1186
|
+
|
|
1187
|
+
Reference: https://code.claude.com/docs/en/hooks
|
|
1188
|
+
|
|
1189
|
+
## Anatomy of a hook entry
|
|
1190
|
+
|
|
1191
|
+
\`\`\`json
|
|
1192
|
+
{
|
|
1193
|
+
"hooks": {
|
|
1194
|
+
"<EventName>": [
|
|
1195
|
+
{
|
|
1196
|
+
"matcher": "<ToolMatcher>",
|
|
1197
|
+
"hooks": [
|
|
1198
|
+
{ "type": "command", "command": "<shell-string>" }
|
|
1199
|
+
]
|
|
1200
|
+
}
|
|
1201
|
+
]
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
\`\`\`
|
|
1205
|
+
|
|
1206
|
+
- \`EventName\`: \`SessionStart\`, \`SessionEnd\`, \`PreToolUse\`, \`PostToolUse\`, \`PostToolUseFailure\`, \`UserPromptSubmit\`, \`Stop\`, \`PermissionRequest\`, \`PreCompact\`, \`PostCompact\`. PostCompact is side-effect-only (stdout is never injected) \u2014 to re-inject context after compaction, use SessionStart with matcher \`compact\`.
|
|
1207
|
+
- \`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\`.
|
|
1208
|
+
- \`hooks\` array: every entry runs in parallel when the matcher fires. Identical command strings are deduplicated automatically.
|
|
1209
|
+
|
|
1210
|
+
## Input \u2014 JSON on stdin, NOT env vars
|
|
1211
|
+
|
|
1212
|
+
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.
|
|
1213
|
+
|
|
1214
|
+
Canonical extraction:
|
|
1215
|
+
|
|
1216
|
+
\`\`\`bash
|
|
1217
|
+
fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null) # PreToolUse Read|Write|Edit
|
|
1218
|
+
cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null) # PreToolUse Bash
|
|
1219
|
+
new=$(jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null) # PostToolUse Edit|Write
|
|
1220
|
+
\`\`\`
|
|
1221
|
+
|
|
1222
|
+
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\`.
|
|
1223
|
+
|
|
1224
|
+
\`jq\` is a project-level requirement. Install via \`brew install jq\` (macOS) or your distro's package manager.
|
|
1225
|
+
|
|
1226
|
+
## Exit codes \u2014 2 blocks, 1 does not
|
|
1227
|
+
|
|
1228
|
+
| Exit code | Effect |
|
|
1229
|
+
|---|---|
|
|
1230
|
+
| \`0\` | Allow the action. Hook stdout becomes context (SessionStart) or is informational (PostToolUse). |
|
|
1231
|
+
| \`2\` | Block the action. Hook stderr is fed back to Claude as the error reason. |
|
|
1232
|
+
| any other non-zero (\`1\`, \`127\`, \u2026) | Treated as a non-blocking hook error. **The action proceeds anyway.** |
|
|
1233
|
+
|
|
1234
|
+
\`exit 1\` does NOT block. If the hook is meant to enforce a policy, it must end in \`exit 2\`.
|
|
1235
|
+
|
|
1236
|
+
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).
|
|
1237
|
+
|
|
1238
|
+
## Output \u2014 stderr for blocks, stdout for warnings
|
|
1239
|
+
|
|
1240
|
+
\`\`\`bash
|
|
1241
|
+
# Blocking: stderr + exit 2
|
|
1242
|
+
echo 'BLOCKED: <reason shown to Claude>' >&2
|
|
1243
|
+
exit 2
|
|
1244
|
+
|
|
1245
|
+
# Informational warning that does not block: stdout + exit 0
|
|
1246
|
+
echo 'WARNING: <message printed to user>'
|
|
1247
|
+
exit 0
|
|
1248
|
+
\`\`\`
|
|
1249
|
+
|
|
1250
|
+
Richer control (only use when the simple form is insufficient):
|
|
1251
|
+
|
|
1252
|
+
\`\`\`bash
|
|
1253
|
+
jq -n '{
|
|
1254
|
+
hookSpecificOutput: {
|
|
1255
|
+
hookEventName: "PreToolUse",
|
|
1256
|
+
permissionDecision: "deny",
|
|
1257
|
+
permissionDecisionReason: "<reason>"
|
|
1258
|
+
}
|
|
1259
|
+
}'
|
|
1260
|
+
exit 0
|
|
1261
|
+
\`\`\`
|
|
1262
|
+
|
|
1263
|
+
## Multi-hook same matcher \u2014 combine into ONE entry
|
|
1264
|
+
|
|
1265
|
+
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.**
|
|
1266
|
+
|
|
1267
|
+
Wrong:
|
|
1268
|
+
|
|
1269
|
+
\`\`\`jsonc
|
|
1270
|
+
"PreToolUse": [
|
|
1271
|
+
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "<destructive-guard>" }] },
|
|
1272
|
+
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "<sprint-open-check>" }] }
|
|
1273
|
+
]
|
|
1274
|
+
\`\`\`
|
|
1275
|
+
|
|
1276
|
+
Right:
|
|
1277
|
+
|
|
1278
|
+
\`\`\`jsonc
|
|
1279
|
+
"PreToolUse": [
|
|
1280
|
+
{
|
|
1281
|
+
"matcher": "Bash",
|
|
1282
|
+
"hooks": [
|
|
1283
|
+
{ "type": "command", "command": "<destructive-guard>" },
|
|
1284
|
+
{ "type": "command", "command": "<sprint-open-check>" }
|
|
1285
|
+
]
|
|
1286
|
+
}
|
|
1287
|
+
]
|
|
1288
|
+
\`\`\`
|
|
1289
|
+
|
|
1290
|
+
## Hot-reload \u2014 there is none
|
|
1291
|
+
|
|
1292
|
+
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.
|
|
1293
|
+
|
|
1294
|
+
When fixing or adding a hook:
|
|
1295
|
+
|
|
1296
|
+
1. Edit settings.json
|
|
1297
|
+
2. Test the hook command in isolation (see "Testing" below)
|
|
1298
|
+
3. Restart Claude Code
|
|
1299
|
+
4. Verify the hook fires by triggering the conditions it watches
|
|
1300
|
+
|
|
1301
|
+
Skipping step 3 means the new hook does not exist yet, no matter what the file says.
|
|
1302
|
+
|
|
1303
|
+
## Testing a hook command in isolation
|
|
1304
|
+
|
|
1305
|
+
Before installing or restarting, verify the command works by piping a fake JSON payload to it:
|
|
1306
|
+
|
|
1307
|
+
\`\`\`bash
|
|
1308
|
+
HOOK=$(jq -r '.hooks.PreToolUse[<index>].hooks[<index>].command' .claude/settings.json)
|
|
1309
|
+
echo '{"tool_input":{"command":"git push --force"}}' | bash -c "$HOOK"
|
|
1310
|
+
echo "exit=$?"
|
|
1311
|
+
\`\`\`
|
|
1312
|
+
|
|
1313
|
+
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.
|
|
1314
|
+
|
|
1315
|
+
For PostToolUse hooks that read file paths or content, build the JSON to mirror the tool you're matching:
|
|
1316
|
+
|
|
1317
|
+
\`\`\`bash
|
|
1318
|
+
echo '{"tool_input":{"file_path":"docs/architecture.md","new_string":"..."}}' | bash -c "$HOOK"
|
|
1319
|
+
\`\`\`
|
|
1320
|
+
|
|
1321
|
+
## Canonical templates
|
|
1322
|
+
|
|
1323
|
+
### PreToolUse Bash gate (block on regex match)
|
|
1324
|
+
|
|
1325
|
+
\`\`\`bash
|
|
1326
|
+
cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null); echo "$cmd" | grep -qE '<your-pattern>' || exit 0; echo 'BLOCKED: <reason>' >&2; exit 2
|
|
1327
|
+
\`\`\`
|
|
1328
|
+
|
|
1329
|
+
**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.
|
|
1330
|
+
|
|
1331
|
+
### PreToolUse Read|Write|Edit gate (block on file-path match)
|
|
1332
|
+
|
|
1333
|
+
\`\`\`bash
|
|
1334
|
+
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
|
|
1335
|
+
\`\`\`
|
|
1336
|
+
|
|
1337
|
+
### PostToolUse informational warner (never blocks; just prints)
|
|
1338
|
+
|
|
1339
|
+
\`\`\`bash
|
|
1340
|
+
fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); echo "$fp" | grep -qE '<your-pattern>' || exit 0; echo '<warning text>'; exit 0
|
|
1341
|
+
\`\`\`
|
|
1342
|
+
|
|
1343
|
+
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.
|
|
1344
|
+
|
|
1345
|
+
### SessionStart context injection
|
|
1346
|
+
|
|
1347
|
+
\`\`\`bash
|
|
1348
|
+
cat <some-file> 2>/dev/null; exit 0
|
|
1349
|
+
\`\`\`
|
|
1350
|
+
|
|
1351
|
+
Output goes into the session's context. Always exit 0; SessionStart blocking is not a thing.
|
|
1352
|
+
|
|
1353
|
+
## Adding a new hook \u2014 checklist
|
|
1354
|
+
|
|
1355
|
+
- [ ] Identify the event and matcher you actually need (\`PreToolUse\` for blocking before, \`PostToolUse\` for after-the-fact warnings).
|
|
1356
|
+
- [ ] Read the input via stdin JSON with \`jq\`, **not env vars**.
|
|
1357
|
+
- [ ] If it blocks: send reason to stderr, end with \`exit 2\`. If it warns: stdout, end with \`exit 0\`.
|
|
1358
|
+
- [ ] 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.
|
|
1359
|
+
- [ ] Test the command in isolation by piping a fake JSON payload (see "Testing" above) \u2014 verify the exit code matches your intent.
|
|
1360
|
+
- [ ] Restart Claude Code.
|
|
1361
|
+
- [ ] Verify the hook fires under the conditions it watches and does not fire under benign conditions.
|
|
1362
|
+
|
|
1363
|
+
## Do not
|
|
1364
|
+
|
|
1365
|
+
- 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.
|
|
1366
|
+
- Don't \`exit 1\` to block. It is silently non-blocking. Use \`exit 2\`.
|
|
1367
|
+
- Don't echo the block reason to stdout when exiting 2. Stdout is ignored in that case; only stderr reaches Claude.
|
|
1368
|
+
- 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.
|
|
1369
|
+
- Don't expect mid-session settings.json edits to take effect. Restart is required.
|
|
1370
|
+
- Don't ship a hook without isolation-testing the command first. A silent no-op is worse than no hook.
|
|
1371
|
+
- 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.
|
|
1372
|
+
`;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/commands/init/generators/agent-reviewer.ts
|
|
1376
|
+
var REVIEWER_AGENT_VERSION = 1;
|
|
1377
|
+
function generateReviewerAgent() {
|
|
1378
|
+
return `---
|
|
1379
|
+
name: code-reviewer
|
|
1380
|
+
description: Independent, fresh-context review of a diff. Use PROACTIVELY before sprint-ending commits. Pass base and head SHAs (find the base with \`git log --grep 'chore(sprint-' -n 1 --format=%H\`).
|
|
1381
|
+
tools: Read, Glob, Grep, Bash
|
|
1382
|
+
---
|
|
1383
|
+
|
|
1384
|
+
<!-- lp-reviewer-version: ${REVIEWER_AGENT_VERSION} -->
|
|
1385
|
+
|
|
1386
|
+
You are an independent senior code reviewer. You did NOT write this code \u2014
|
|
1387
|
+
review it with fresh eyes and no attachment to the choices made.
|
|
1388
|
+
|
|
1389
|
+
## Input
|
|
1390
|
+
|
|
1391
|
+
Base and head SHAs (or a branch/range). If none given, review the working
|
|
1392
|
+
tree against HEAD.
|
|
1393
|
+
|
|
1394
|
+
## Process
|
|
1395
|
+
|
|
1396
|
+
1. \`git diff --stat <base>..<head>\` for the shape, then read the full diff.
|
|
1397
|
+
2. Read surrounding source for anything the diff touches \u2014 a hunk that looks
|
|
1398
|
+
fine in isolation can break an invariant defined two functions up.
|
|
1399
|
+
3. For each suspected bug, verify against the actual code before reporting \u2014
|
|
1400
|
+
no findings from pattern-matching alone.
|
|
1401
|
+
4. Check: correctness, security (injection, secrets, permissions), error
|
|
1402
|
+
handling, convention violations (see CLAUDE.md and .claude/rules/), dead
|
|
1403
|
+
code, debug artifacts, missing or weakened tests.
|
|
1404
|
+
|
|
1405
|
+
## Output (exactly these sections)
|
|
1406
|
+
|
|
1407
|
+
- **Strengths** \u2014 what is genuinely good; be brief.
|
|
1408
|
+
- **Critical** \u2014 bugs, security holes, data loss. Must be fixed before commit.
|
|
1409
|
+
- **Important** \u2014 incorrect edge cases, convention breaks, silent failures.
|
|
1410
|
+
Must be fixed before commit.
|
|
1411
|
+
- **Minor** \u2014 polish; may be deferred to the backlog.
|
|
1412
|
+
- **Assessment** \u2014 2-3 sentences: ship, fix-then-ship, or rethink.
|
|
1413
|
+
|
|
1414
|
+
For every Critical/Important finding give file:line and a concrete failure
|
|
1415
|
+
scenario (inputs/state \u2192 wrong behavior). Do not pad: an empty Critical
|
|
1416
|
+
section is a valid, good result. Never soften a finding because the fix is
|
|
1417
|
+
inconvenient.
|
|
1418
|
+
`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1043
1421
|
// src/commands/doctor/fixer-quality.ts
|
|
1044
1422
|
async function createWorkflowRule(root) {
|
|
1045
1423
|
const rulesDir = join4(root, ".claude", "rules");
|
|
@@ -1050,6 +1428,41 @@ async function createWorkflowRule(root) {
|
|
|
1050
1428
|
log.success("Created .claude/rules/workflow.md (path-scoped BACKLOG/TASKS workflow rules)");
|
|
1051
1429
|
return true;
|
|
1052
1430
|
}
|
|
1431
|
+
async function createReviewerAgent(root) {
|
|
1432
|
+
const agentsDir = join4(root, ".claude", "agents");
|
|
1433
|
+
const agentPath = join4(agentsDir, "code-reviewer.md");
|
|
1434
|
+
if (await fileExists(agentPath)) return false;
|
|
1435
|
+
await mkdir2(agentsDir, { recursive: true });
|
|
1436
|
+
await writeFile3(agentPath, generateReviewerAgent());
|
|
1437
|
+
log.success("Created .claude/agents/code-reviewer.md (fresh-context independent reviewer)");
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
async function updateWorkflowRule(root) {
|
|
1441
|
+
const workflowPath = join4(root, ".claude", "rules", "workflow.md");
|
|
1442
|
+
const content = await readFile3(workflowPath, "utf-8").catch(() => null);
|
|
1443
|
+
if (content === null) return false;
|
|
1444
|
+
if (!/<!-- lp-workflow-version: \d+ -->/.test(content)) return false;
|
|
1445
|
+
await writeFile3(workflowPath, generateWorkflowRule());
|
|
1446
|
+
log.success("Updated .claude/rules/workflow.md to the latest version");
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
async function fixStaleSwarmPhrase(root) {
|
|
1450
|
+
const claudeMdPath = join4(root, "CLAUDE.md");
|
|
1451
|
+
const content = await readFile3(claudeMdPath, "utf-8").catch(() => null);
|
|
1452
|
+
if (content === null || !content.includes(STALE_SWARM_PHRASE)) return false;
|
|
1453
|
+
await writeFile3(claudeMdPath, content.replaceAll(STALE_SWARM_PHRASE, SWARM_PHRASE_REPLACEMENT));
|
|
1454
|
+
log.success("Modernized Stop-and-Swarm wording (Agent tool \u2192 Task tool subagents)");
|
|
1455
|
+
return true;
|
|
1456
|
+
}
|
|
1457
|
+
async function createHooksRule(root) {
|
|
1458
|
+
const rulesDir = join4(root, ".claude", "rules");
|
|
1459
|
+
const hooksPath = join4(rulesDir, "hooks.md");
|
|
1460
|
+
if (await fileExists(hooksPath)) return false;
|
|
1461
|
+
await mkdir2(rulesDir, { recursive: true });
|
|
1462
|
+
await writeFile3(hooksPath, generateHooksRule());
|
|
1463
|
+
log.success("Created .claude/rules/hooks.md (path-scoped hook authoring rules)");
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1053
1466
|
function isMemoryHeading(line) {
|
|
1054
1467
|
return /^## Memory( \(agentic-memory\))?\s*$/.test(line);
|
|
1055
1468
|
}
|
|
@@ -1073,7 +1486,7 @@ async function collapseMemoryHeadings(root) {
|
|
|
1073
1486
|
const claudeMdPath = join4(root, "CLAUDE.md");
|
|
1074
1487
|
let content;
|
|
1075
1488
|
try {
|
|
1076
|
-
content = await
|
|
1489
|
+
content = await readFile3(claudeMdPath, "utf-8");
|
|
1077
1490
|
} catch {
|
|
1078
1491
|
return false;
|
|
1079
1492
|
}
|
|
@@ -1118,7 +1531,7 @@ async function addEnvProtectionHook(root) {
|
|
|
1118
1531
|
matcher: "Read|Write|Edit",
|
|
1119
1532
|
hooks: [{
|
|
1120
1533
|
type: "command",
|
|
1121
|
-
command: `echo "$
|
|
1534
|
+
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
1535
|
}]
|
|
1123
1536
|
}, "Added .env file protection hook (PreToolUse)");
|
|
1124
1537
|
}
|
|
@@ -1131,7 +1544,7 @@ async function addAutoFormatHook(root, detected) {
|
|
|
1131
1544
|
matcher: "Write|Edit",
|
|
1132
1545
|
hooks: [{
|
|
1133
1546
|
type: "command",
|
|
1134
|
-
command: `ext
|
|
1547
|
+
command: `fp=${jqField("file_path")}; ext="\${fp##*.}"; (${extChecks}) && ${config.command} "$fp" 2>/dev/null; exit 0`
|
|
1135
1548
|
}]
|
|
1136
1549
|
}, `Added auto-format hook (PostToolUse \u2192 ${config.command})`);
|
|
1137
1550
|
}
|
|
@@ -1140,26 +1553,149 @@ async function addForcePushProtection(root) {
|
|
|
1140
1553
|
matcher: "Bash",
|
|
1141
1554
|
hooks: [{
|
|
1142
1555
|
type: "command",
|
|
1143
|
-
command: `echo "$
|
|
1556
|
+
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
1557
|
}]
|
|
1145
1558
|
}, "Added force-push protection hook (PreToolUse \u2192 Bash)");
|
|
1146
1559
|
}
|
|
1147
|
-
async function
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1560
|
+
async function migratePostCompactHook(root) {
|
|
1561
|
+
const settings = await readSettingsJson(root);
|
|
1562
|
+
if (settings === null) return false;
|
|
1563
|
+
const hooks = settings.hooks ?? {};
|
|
1564
|
+
let changed = false;
|
|
1565
|
+
const postCompact = hooks.PostCompact ?? [];
|
|
1566
|
+
const kept = postCompact.map((group) => {
|
|
1567
|
+
const nested = group.hooks ?? [];
|
|
1568
|
+
const filtered = nested.filter((h) => !String(h.command ?? "").includes("TASKS.md"));
|
|
1569
|
+
if (filtered.length !== nested.length) changed = true;
|
|
1570
|
+
return { ...group, hooks: filtered };
|
|
1571
|
+
}).filter((group) => group.hooks.length > 0);
|
|
1572
|
+
const rest = { ...hooks };
|
|
1573
|
+
if (kept.length > 0) rest.PostCompact = kept;
|
|
1574
|
+
else delete rest.PostCompact;
|
|
1575
|
+
const withMatcher = ensureCompactMatcher(rest);
|
|
1576
|
+
if (withMatcher.changed) changed = true;
|
|
1577
|
+
if (!changed) return false;
|
|
1578
|
+
await writeSettingsJson(root, { ...settings, hooks: withMatcher.hooks });
|
|
1579
|
+
log.success("Moved TASKS.md re-injection off PostCompact (stdout not injected there) \u2192 SessionStart compact/clear matcher");
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
async function addCompactMatcherHook(root) {
|
|
1583
|
+
const settings = await readSettingsJson(root);
|
|
1584
|
+
if (settings === null) return false;
|
|
1585
|
+
const hooks = settings.hooks ?? {};
|
|
1586
|
+
const result = ensureCompactMatcher(hooks);
|
|
1587
|
+
if (!result.changed) return false;
|
|
1588
|
+
await writeSettingsJson(root, { ...settings, hooks: result.hooks });
|
|
1589
|
+
log.success("SessionStart matcher now covers compact/clear (session continuity after compaction)");
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
function ensureCompactMatcher(hooks) {
|
|
1593
|
+
const sessionStart = hooks.SessionStart ?? [];
|
|
1594
|
+
if (sessionStart.some((g) => String(g.matcher ?? "").includes("compact"))) {
|
|
1595
|
+
return { hooks, changed: false };
|
|
1596
|
+
}
|
|
1597
|
+
const idx = sessionStart.findIndex((g) => {
|
|
1598
|
+
const nested = g.hooks;
|
|
1599
|
+
return nested?.some((h) => String(h.command ?? "").includes("TASKS.md"));
|
|
1600
|
+
});
|
|
1601
|
+
if (idx === -1) {
|
|
1602
|
+
const entry = { matcher: SESSION_START_MATCHER, hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }] };
|
|
1603
|
+
return { hooks: { ...hooks, SessionStart: [...sessionStart, entry] }, changed: true };
|
|
1604
|
+
}
|
|
1605
|
+
const widened = sessionStart.map((g, i) => i === idx ? { ...g, matcher: SESSION_START_MATCHER } : g);
|
|
1606
|
+
return { hooks: { ...hooks, SessionStart: widened }, changed: true };
|
|
1152
1607
|
}
|
|
1153
1608
|
async function addSessionStartHook(root) {
|
|
1154
1609
|
return addHookToSettings(root, "SessionStart", "TASKS.md", {
|
|
1155
|
-
matcher:
|
|
1610
|
+
matcher: SESSION_START_MATCHER,
|
|
1156
1611
|
hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }]
|
|
1157
|
-
}, "Added SessionStart hook (injects TASKS.md at startup)");
|
|
1612
|
+
}, "Added SessionStart hook (injects TASKS.md at startup/resume/compact/clear)");
|
|
1158
1613
|
}
|
|
1159
1614
|
|
|
1160
|
-
// src/commands/doctor/fixer-
|
|
1161
|
-
import { readFile as
|
|
1615
|
+
// src/commands/doctor/fixer-hook-input.ts
|
|
1616
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1162
1617
|
import { join as join5 } from "path";
|
|
1618
|
+
function rewriteEnvVarHookCommand(cmd) {
|
|
1619
|
+
if (!hasEnvVarHookPattern(cmd)) return null;
|
|
1620
|
+
if (cmd.includes("BLOCKED: .env files contain secrets")) {
|
|
1621
|
+
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`;
|
|
1622
|
+
}
|
|
1623
|
+
if (cmd.includes("BLOCKED: Destructive command detected")) {
|
|
1624
|
+
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`;
|
|
1625
|
+
}
|
|
1626
|
+
if (cmd.includes("WARNING: Force push detected") || cmd.includes("Force push detected")) {
|
|
1627
|
+
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`;
|
|
1628
|
+
}
|
|
1629
|
+
if (cmd.includes("Sprint complete") && cmd.includes("TASKS.md")) {
|
|
1630
|
+
return SPRINT_COMPLETE_NUDGE;
|
|
1631
|
+
}
|
|
1632
|
+
const formatMatch = cmd.match(/ext=\$\{TOOL_INPUT_FILE_PATH##\*\.\};\s*\((.+?)\)\s*&&\s*(.+?)\s*"\$TOOL_INPUT_FILE_PATH"/);
|
|
1633
|
+
if (formatMatch) {
|
|
1634
|
+
const extChecks = formatMatch[1].trim();
|
|
1635
|
+
const formatter = formatMatch[2].trim();
|
|
1636
|
+
return `fp=${jqField("file_path")}; ext="\${fp##*.}"; (${extChecks}) && ${formatter} "$fp" 2>/dev/null; exit 0`;
|
|
1637
|
+
}
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
function rewriteSettingsHooks(settings) {
|
|
1641
|
+
const hooks = settings.hooks;
|
|
1642
|
+
if (!hooks) return { settings, changed: false };
|
|
1643
|
+
let changed = false;
|
|
1644
|
+
const next = {};
|
|
1645
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
1646
|
+
next[event] = groups.map((group) => ({
|
|
1647
|
+
...group,
|
|
1648
|
+
hooks: group.hooks.map((hook) => {
|
|
1649
|
+
if (hook.type !== "command") return hook;
|
|
1650
|
+
if (!hasEnvVarHookPattern(hook.command)) return hook;
|
|
1651
|
+
const rewritten = rewriteEnvVarHookCommand(hook.command);
|
|
1652
|
+
if (rewritten === null) return hook;
|
|
1653
|
+
changed = true;
|
|
1654
|
+
return { ...hook, command: rewritten };
|
|
1655
|
+
})
|
|
1656
|
+
}));
|
|
1657
|
+
}
|
|
1658
|
+
if (!changed) return { settings, changed: false };
|
|
1659
|
+
return { settings: { ...settings, hooks: next }, changed: true };
|
|
1660
|
+
}
|
|
1661
|
+
async function rewriteWrapperScript(scriptPath, rewriter) {
|
|
1662
|
+
let content;
|
|
1663
|
+
try {
|
|
1664
|
+
content = await readFile4(scriptPath, "utf-8");
|
|
1665
|
+
} catch {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
if (!hasEnvVarHookPattern(content)) return false;
|
|
1669
|
+
await rewriter();
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
async function rewriteEnvVarHooks(root) {
|
|
1673
|
+
let didFix = false;
|
|
1674
|
+
const settings = await readSettingsJson(root);
|
|
1675
|
+
if (settings !== null) {
|
|
1676
|
+
const outcome = rewriteSettingsHooks(settings);
|
|
1677
|
+
if (outcome.changed) {
|
|
1678
|
+
await writeSettingsJson(root, outcome.settings);
|
|
1679
|
+
log.success("Rewrote inert $TOOL_INPUT_* hooks in settings.json to canonical jq+stdin form");
|
|
1680
|
+
didFix = true;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const workflowCheckPath = join5(root, ".claude", "hooks", "workflow-check.sh");
|
|
1684
|
+
if (await rewriteWrapperScript(workflowCheckPath, () => writeWorkflowCheckScript(root))) {
|
|
1685
|
+
log.success("Rewrote .claude/hooks/workflow-check.sh to canonical jq+stdin form");
|
|
1686
|
+
didFix = true;
|
|
1687
|
+
}
|
|
1688
|
+
const sprintOpenPath = join5(root, ".claude", "hooks", "sprint-open-check.sh");
|
|
1689
|
+
if (await rewriteWrapperScript(sprintOpenPath, () => writeSprintHygieneScripts(root))) {
|
|
1690
|
+
log.success("Rewrote .claude/hooks/sprint-open-check.sh to canonical jq+stdin form");
|
|
1691
|
+
didFix = true;
|
|
1692
|
+
}
|
|
1693
|
+
return didFix;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/commands/doctor/fixer-memory.ts
|
|
1697
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1698
|
+
import { join as join6 } from "path";
|
|
1163
1699
|
async function addPlacementHook(root, placement, event, dedupKeyword, entry, prepend, successMsg) {
|
|
1164
1700
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1165
1701
|
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
@@ -1313,9 +1849,9 @@ async function addAllowedMcpServers(root, placement) {
|
|
|
1313
1849
|
if (settingsServers && typeof settingsServers === "object") {
|
|
1314
1850
|
for (const name of Object.keys(settingsServers)) serverNames.add(name);
|
|
1315
1851
|
}
|
|
1316
|
-
const mcpJsonPath =
|
|
1852
|
+
const mcpJsonPath = join6(root, ".mcp.json");
|
|
1317
1853
|
try {
|
|
1318
|
-
const mcpJson = JSON.parse(await
|
|
1854
|
+
const mcpJson = JSON.parse(await readFile5(mcpJsonPath, "utf-8"));
|
|
1319
1855
|
const mcpServers = mcpJson.mcpServers;
|
|
1320
1856
|
if (mcpServers && typeof mcpServers === "object") {
|
|
1321
1857
|
for (const name of Object.keys(mcpServers)) serverNames.add(name);
|
|
@@ -1351,6 +1887,7 @@ async function applyFixes(issues, projectRoot) {
|
|
|
1351
1887
|
return { fixed, skipped };
|
|
1352
1888
|
}
|
|
1353
1889
|
var FIX_TABLE = [
|
|
1890
|
+
{ analyzer: "Hooks", match: "$TOOL_INPUT_* env var", fix: (root) => rewriteEnvVarHooks(root) },
|
|
1354
1891
|
{ analyzer: "Hooks", match: "No hooks configured", fix: async (root, detected) => {
|
|
1355
1892
|
const a = await addEnvProtectionHook(root);
|
|
1356
1893
|
const b = await addAutoFormatHook(root, detected);
|
|
@@ -1375,19 +1912,27 @@ var FIX_TABLE = [
|
|
|
1375
1912
|
} },
|
|
1376
1913
|
{ analyzer: "Quality", match: "Session Start", fix: (root) => addClaudeMdSection(root, "## Session Start", wrapStub(SESSION_START_CONTENT)) },
|
|
1377
1914
|
{ analyzer: "Quality", match: "Backlog", fix: (root) => addClaudeMdSection(root, "## Backlog", wrapStub(BACKLOG_CONTENT)) },
|
|
1915
|
+
{ analyzer: "Quality", match: "Stop-and-Swarm section is outdated", fix: (root) => fixStaleSwarmPhrase(root) },
|
|
1378
1916
|
{ analyzer: "Quality", match: "Stop-and-Swarm", fix: (root) => addClaudeMdSection(root, "## Stop-and-Swarm", wrapStub(STOP_AND_SWARM_CONTENT)) },
|
|
1917
|
+
{ analyzer: "Rules", match: "workflow.md rule is outdated", fix: (root) => updateWorkflowRule(root) },
|
|
1918
|
+
{ analyzer: "Rules", match: "No .claude/agents/code-reviewer.md", fix: (root) => createReviewerAgent(root) },
|
|
1379
1919
|
{ analyzer: "Quality", match: "Duplicate ## Memory", fix: (root) => collapseMemoryHeadings(root) },
|
|
1380
1920
|
{ analyzer: "Rules", match: "No BACKLOG.md", fix: (root) => createBacklogMd(root) },
|
|
1381
1921
|
{ analyzer: "Rules", match: "No .claudeignore", fix: (root, detected) => createClaudeignore(root, detected) },
|
|
1382
1922
|
{ analyzer: "Rules", match: "No .claude/rules/workflow.md", fix: (root) => createWorkflowRule(root) },
|
|
1923
|
+
{ analyzer: "Rules", match: "No .claude/rules/hooks.md", fix: (root) => createHooksRule(root) },
|
|
1383
1924
|
{ analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
|
|
1384
|
-
{ analyzer: "Hooks", match: "PostCompact", fix: (root) =>
|
|
1925
|
+
{ analyzer: "Hooks", match: "PostCompact hooks can't inject context", fix: (root) => migratePostCompactHook(root) },
|
|
1926
|
+
{ analyzer: "Hooks", match: "compact matcher", fix: (root) => addCompactMatcherHook(root) },
|
|
1385
1927
|
{ analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
|
|
1386
1928
|
{ analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
|
|
1387
1929
|
{ analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
|
|
1388
|
-
{ analyzer: "Permissions", match: "
|
|
1930
|
+
{ analyzer: "Permissions", match: "Sandbox lacks a write grant", fix: (root) => addSandboxMemoryWriteGrant(root) },
|
|
1389
1931
|
{ analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
|
|
1390
1932
|
{ analyzer: "Permissions", match: ".worktreeinclude is missing or empty", fix: (root) => createWorktreeInclude(root) },
|
|
1933
|
+
{ analyzer: "Hooks", match: "registered on PreToolUse", fix: (root) => migrateSprintOpenHookEvent(root) },
|
|
1934
|
+
{ analyzer: "Hooks", match: "nudge uses bare stdout", fix: (root) => upgradeStaleNudge(root) },
|
|
1935
|
+
{ analyzer: "Hooks", match: "Outdated hygiene script", fix: (root) => refreshHygieneScripts(root) },
|
|
1391
1936
|
{ analyzer: "Hooks", match: "sprint-size-check", fix: (root) => addSprintSizeHook(root) },
|
|
1392
1937
|
{ analyzer: "Hooks", match: "sprint-open-check", fix: (root) => addSprintOpenHook(root) },
|
|
1393
1938
|
{ analyzer: "Hooks", match: "sprint-complete nudge", fix: (root) => addSprintCompleteNudge(root) },
|
|
@@ -1407,7 +1952,7 @@ var FIX_TABLE = [
|
|
|
1407
1952
|
{ analyzer: "Memory", match: "SessionEnd push hook is not nohup-wrapped", fix: (root) => upgradeStaleSessionEndPushHook(root) },
|
|
1408
1953
|
{ analyzer: "Memory", match: "CLAUDE.md missing memory guidance", fix: (root, _det, placement) => {
|
|
1409
1954
|
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" ?
|
|
1955
|
+
const target = placement === "local" ? join7(root, ".claude", "CLAUDE.md") : void 0;
|
|
1411
1956
|
return addClaudeMdSection(root, "## Memory", wrapStub(content), target);
|
|
1412
1957
|
} }
|
|
1413
1958
|
];
|
|
@@ -1454,20 +1999,30 @@ async function addBypassDisable(root) {
|
|
|
1454
1999
|
log.success("Added disableBypassPermissionsMode: disable");
|
|
1455
2000
|
return true;
|
|
1456
2001
|
}
|
|
1457
|
-
async function
|
|
2002
|
+
async function addSandboxMemoryWriteGrant(root) {
|
|
1458
2003
|
const settings = await readSettingsJson(root);
|
|
1459
2004
|
if (settings === null) return false;
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
2005
|
+
const sandbox = settings.sandbox;
|
|
2006
|
+
if (sandbox === void 0) return false;
|
|
2007
|
+
const filesystem = sandbox.filesystem ?? {};
|
|
2008
|
+
const allowWrite = filesystem.allowWrite ?? [];
|
|
2009
|
+
if (allowWrite.some((p) => p.includes(".agentic-memory"))) return false;
|
|
2010
|
+
const updated = {
|
|
2011
|
+
...settings,
|
|
2012
|
+
sandbox: {
|
|
2013
|
+
...sandbox,
|
|
2014
|
+
filesystem: { ...filesystem, allowWrite: [...allowWrite, "~/.agentic-memory"] }
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
await writeSettingsJson(root, updated);
|
|
2018
|
+
log.success("Added ~/.agentic-memory to sandbox.filesystem.allowWrite (sandbox stays enabled)");
|
|
1464
2019
|
return true;
|
|
1465
2020
|
}
|
|
1466
2021
|
async function addEnvToClaudeignore(root) {
|
|
1467
|
-
const ignorePath =
|
|
2022
|
+
const ignorePath = join7(root, ".claudeignore");
|
|
1468
2023
|
let content;
|
|
1469
2024
|
try {
|
|
1470
|
-
content = await
|
|
2025
|
+
content = await readFile6(ignorePath, "utf-8");
|
|
1471
2026
|
} catch {
|
|
1472
2027
|
return false;
|
|
1473
2028
|
}
|
|
@@ -1478,13 +2033,13 @@ async function addEnvToClaudeignore(root) {
|
|
|
1478
2033
|
return true;
|
|
1479
2034
|
}
|
|
1480
2035
|
async function addClaudeMdSection(root, heading, content, targetPath) {
|
|
1481
|
-
const claudeMdPath = targetPath ??
|
|
2036
|
+
const claudeMdPath = targetPath ?? join7(root, "CLAUDE.md");
|
|
1482
2037
|
let existing;
|
|
1483
2038
|
try {
|
|
1484
|
-
existing = await
|
|
2039
|
+
existing = await readFile6(claudeMdPath, "utf-8");
|
|
1485
2040
|
} catch {
|
|
1486
2041
|
if (!targetPath) return false;
|
|
1487
|
-
await mkdir3(
|
|
2042
|
+
await mkdir3(join7(root, ".claude"), { recursive: true });
|
|
1488
2043
|
existing = "# Local Claude Config\n";
|
|
1489
2044
|
}
|
|
1490
2045
|
if (existing.includes(heading)) return false;
|
|
@@ -1502,24 +2057,16 @@ ${content}
|
|
|
1502
2057
|
return true;
|
|
1503
2058
|
}
|
|
1504
2059
|
async function createBacklogMd(root) {
|
|
1505
|
-
const backlogPath =
|
|
1506
|
-
|
|
1507
|
-
await access2(backlogPath);
|
|
1508
|
-
return false;
|
|
1509
|
-
} catch {
|
|
1510
|
-
}
|
|
2060
|
+
const backlogPath = join7(root, "BACKLOG.md");
|
|
2061
|
+
if (await fileExists(backlogPath)) return false;
|
|
1511
2062
|
const name = root.split("/").pop() ?? "Project";
|
|
1512
2063
|
await writeFile4(backlogPath, generateBacklogMd({ name, description: "" }));
|
|
1513
2064
|
log.success("Generated BACKLOG.md (WP template + P0\u2013P3 sections + changelog)");
|
|
1514
2065
|
return true;
|
|
1515
2066
|
}
|
|
1516
2067
|
async function createClaudeignore(root, detected) {
|
|
1517
|
-
const ignorePath =
|
|
1518
|
-
|
|
1519
|
-
await access2(ignorePath);
|
|
1520
|
-
return false;
|
|
1521
|
-
} catch {
|
|
1522
|
-
}
|
|
2068
|
+
const ignorePath = join7(root, ".claudeignore");
|
|
2069
|
+
if (await fileExists(ignorePath)) return false;
|
|
1523
2070
|
const content = generateClaudeignore(detected);
|
|
1524
2071
|
await writeFile4(ignorePath, content);
|
|
1525
2072
|
log.success("Generated .claudeignore with language-specific ignore patterns");
|
|
@@ -1531,15 +2078,11 @@ var SKILL_AUTHORING_SECTION = `
|
|
|
1531
2078
|
${SKILL_AUTHORING_CONTENT}
|
|
1532
2079
|
`;
|
|
1533
2080
|
async function createStarterRules(root) {
|
|
1534
|
-
const rulesDir =
|
|
1535
|
-
|
|
1536
|
-
await access2(rulesDir);
|
|
1537
|
-
return false;
|
|
1538
|
-
} catch {
|
|
1539
|
-
}
|
|
2081
|
+
const rulesDir = join7(root, ".claude", "rules");
|
|
2082
|
+
if (await fileExists(rulesDir)) return false;
|
|
1540
2083
|
await mkdir3(rulesDir, { recursive: true });
|
|
1541
2084
|
await writeFile4(
|
|
1542
|
-
|
|
2085
|
+
join7(rulesDir, "conventions.md"),
|
|
1543
2086
|
`# Project Conventions
|
|
1544
2087
|
|
|
1545
2088
|
- Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
|
|
@@ -1552,10 +2095,10 @@ ${SKILL_AUTHORING_SECTION}`
|
|
|
1552
2095
|
return true;
|
|
1553
2096
|
}
|
|
1554
2097
|
async function addSkillAuthoringConventions(root) {
|
|
1555
|
-
const conventionsPath =
|
|
2098
|
+
const conventionsPath = join7(root, ".claude", "rules", "conventions.md");
|
|
1556
2099
|
let content;
|
|
1557
2100
|
try {
|
|
1558
|
-
content = await
|
|
2101
|
+
content = await readFile6(conventionsPath, "utf-8");
|
|
1559
2102
|
} catch {
|
|
1560
2103
|
return false;
|
|
1561
2104
|
}
|
|
@@ -1565,11 +2108,11 @@ async function addSkillAuthoringConventions(root) {
|
|
|
1565
2108
|
return true;
|
|
1566
2109
|
}
|
|
1567
2110
|
async function createEnhanceSkill(root) {
|
|
1568
|
-
const skillDir =
|
|
1569
|
-
const skillPath =
|
|
1570
|
-
const globalPath =
|
|
1571
|
-
const legacyProject =
|
|
1572
|
-
const legacyGlobal =
|
|
2111
|
+
const skillDir = join7(root, ".claude", "skills", "lp-enhance");
|
|
2112
|
+
const skillPath = join7(skillDir, "SKILL.md");
|
|
2113
|
+
const globalPath = join7(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2114
|
+
const legacyProject = join7(root, ".claude", "commands", "lp-enhance.md");
|
|
2115
|
+
const legacyGlobal = join7(homedir(), ".claude", "commands", "lp-enhance.md");
|
|
1573
2116
|
if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return false;
|
|
1574
2117
|
await mkdir3(skillDir, { recursive: true });
|
|
1575
2118
|
await writeFile4(skillPath, generateEnhanceSkill());
|
|
@@ -1577,8 +2120,8 @@ async function createEnhanceSkill(root) {
|
|
|
1577
2120
|
return true;
|
|
1578
2121
|
}
|
|
1579
2122
|
async function updateEnhanceSkill(root) {
|
|
1580
|
-
const projectPath =
|
|
1581
|
-
const globalPath =
|
|
2123
|
+
const projectPath = join7(root, ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2124
|
+
const globalPath = join7(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
1582
2125
|
const targetPath = await fileExists(projectPath) ? projectPath : await fileExists(globalPath) ? globalPath : null;
|
|
1583
2126
|
if (!targetPath) return false;
|
|
1584
2127
|
await writeFile4(targetPath, generateEnhanceSkill());
|
|
@@ -1692,7 +2235,7 @@ function renderDoctorReport(results, options) {
|
|
|
1692
2235
|
async function readJsonFile(path) {
|
|
1693
2236
|
let raw;
|
|
1694
2237
|
try {
|
|
1695
|
-
raw = await
|
|
2238
|
+
raw = await readFile7(path, "utf-8");
|
|
1696
2239
|
} catch (err) {
|
|
1697
2240
|
const code = err.code;
|
|
1698
2241
|
if (code === "ENOENT") return {};
|
|
@@ -1707,20 +2250,20 @@ async function readJsonFile(path) {
|
|
|
1707
2250
|
}
|
|
1708
2251
|
}
|
|
1709
2252
|
async function readSettingsJson(root) {
|
|
1710
|
-
return readJsonFile(
|
|
2253
|
+
return readJsonFile(join8(root, ".claude", "settings.json"));
|
|
1711
2254
|
}
|
|
1712
2255
|
async function writeSettingsJson(root, settings) {
|
|
1713
|
-
const dir =
|
|
2256
|
+
const dir = join8(root, ".claude");
|
|
1714
2257
|
await mkdir4(dir, { recursive: true });
|
|
1715
|
-
await writeFile5(
|
|
2258
|
+
await writeFile5(join8(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
1716
2259
|
}
|
|
1717
2260
|
async function readSettingsLocalJson(root) {
|
|
1718
|
-
return readJsonFile(
|
|
2261
|
+
return readJsonFile(join8(root, ".claude", "settings.local.json"));
|
|
1719
2262
|
}
|
|
1720
2263
|
async function writeSettingsLocalJson(root, settings) {
|
|
1721
|
-
const dir =
|
|
2264
|
+
const dir = join8(root, ".claude");
|
|
1722
2265
|
await mkdir4(dir, { recursive: true });
|
|
1723
|
-
await writeFile5(
|
|
2266
|
+
await writeFile5(join8(dir, "settings.local.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
1724
2267
|
}
|
|
1725
2268
|
|
|
1726
2269
|
export {
|
|
@@ -1728,8 +2271,11 @@ export {
|
|
|
1728
2271
|
SESSION_START_CONTENT,
|
|
1729
2272
|
BACKLOG_CONTENT,
|
|
1730
2273
|
STOP_AND_SWARM_CONTENT,
|
|
2274
|
+
STALE_SWARM_PHRASE,
|
|
1731
2275
|
OFF_LIMITS_CONTENT,
|
|
1732
2276
|
SKILL_AUTHORING_CONTENT,
|
|
2277
|
+
sprintReviewsContent,
|
|
2278
|
+
TESTING_DISCIPLINE_CONTENT,
|
|
1733
2279
|
fileExists,
|
|
1734
2280
|
readFileOrNull,
|
|
1735
2281
|
detectProject,
|
|
@@ -1744,13 +2290,24 @@ export {
|
|
|
1744
2290
|
getMemoryPlacement,
|
|
1745
2291
|
LP_STUB_OPEN,
|
|
1746
2292
|
addOrUpdateHook,
|
|
2293
|
+
jqField,
|
|
2294
|
+
hasEnvVarHookPattern,
|
|
2295
|
+
isJqAvailable,
|
|
2296
|
+
SPRINT_COMPLETE_NUDGE,
|
|
2297
|
+
WORKFLOW_CHECK_WRAPPER,
|
|
2298
|
+
SPRINT_OPEN_WRAPPER,
|
|
2299
|
+
SPRINT_SIZE_WRAPPER,
|
|
2300
|
+
SESSION_START_MATCHER,
|
|
1747
2301
|
writeSprintHygieneScripts,
|
|
1748
2302
|
writeWorkflowCheckScript,
|
|
2303
|
+
WORKFLOW_RULE_VERSION,
|
|
1749
2304
|
generateWorkflowRule,
|
|
2305
|
+
generateHooksRule,
|
|
2306
|
+
generateReviewerAgent,
|
|
1750
2307
|
applyFixes,
|
|
1751
2308
|
log,
|
|
1752
2309
|
printBanner,
|
|
1753
2310
|
printScoreCard,
|
|
1754
2311
|
renderDoctorReport
|
|
1755
2312
|
};
|
|
1756
|
-
//# sourceMappingURL=chunk-
|
|
2313
|
+
//# sourceMappingURL=chunk-KPO4YURF.js.map
|