claude-launchpad 1.10.1 → 1.14.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.
Files changed (62) hide show
  1. package/README.md +16 -6
  2. package/dist/{chunk-P2LGPNNY.js → chunk-D6RAMWH6.js} +3 -3
  3. package/dist/{chunk-P2LGPNNY.js.map → chunk-D6RAMWH6.js.map} +1 -1
  4. package/dist/chunk-HSGSTSVN.js +212 -0
  5. package/dist/chunk-HSGSTSVN.js.map +1 -0
  6. package/dist/{chunk-DXDOVWOA.js → chunk-IDH3M4EI.js} +20 -232
  7. package/dist/chunk-IDH3M4EI.js.map +1 -0
  8. package/dist/{chunk-YZ53W47Z.js → chunk-IIWOSSL4.js} +2 -1
  9. package/dist/chunk-IIWOSSL4.js.map +1 -0
  10. package/dist/{chunk-6DQD6KVN.js → chunk-M6GVFN67.js} +2 -2
  11. package/dist/{chunk-F6SLV2FR.js → chunk-N2SCF7DB.js} +5 -6
  12. package/dist/chunk-N2SCF7DB.js.map +1 -0
  13. package/dist/chunk-OPORVSAF.js +225 -0
  14. package/dist/chunk-OPORVSAF.js.map +1 -0
  15. package/dist/{chunk-3OFOCOXM.js → chunk-QEPE3WVF.js} +821 -200
  16. package/dist/chunk-QEPE3WVF.js.map +1 -0
  17. package/dist/chunk-UN43OVRW.js +184 -0
  18. package/dist/chunk-UN43OVRW.js.map +1 -0
  19. package/dist/{chunk-YYNJMM7V.js → chunk-VF7U63AI.js} +9 -5
  20. package/dist/{chunk-YYNJMM7V.js.map → chunk-VF7U63AI.js.map} +1 -1
  21. package/dist/cli.js +948 -418
  22. package/dist/cli.js.map +1 -1
  23. package/dist/commands/memory/server.js +48 -14
  24. package/dist/commands/memory/server.js.map +1 -1
  25. package/dist/{context-76QJSCRJ.js → context-YIPO4LBS.js} +58 -55
  26. package/dist/context-YIPO4LBS.js.map +1 -0
  27. package/dist/doctor-DCHFJ3XV.js +108 -0
  28. package/dist/doctor-DCHFJ3XV.js.map +1 -0
  29. package/dist/{install-B5KISSFR.js → install-D32PV7SF.js} +15 -11
  30. package/dist/install-D32PV7SF.js.map +1 -0
  31. package/dist/{pull-4ZQSKFX7.js → pull-YUQTDRUP.js} +22 -20
  32. package/dist/pull-YUQTDRUP.js.map +1 -0
  33. package/dist/{push-C3QMIEUQ.js → push-U4IVTFOU.js} +13 -12
  34. package/dist/push-U4IVTFOU.js.map +1 -0
  35. package/dist/{require-deps-XUAKG226.js → require-deps-WDMHDGBN.js} +3 -3
  36. package/dist/{stats-ARO5UFFZ.js → stats-77BYLKYP.js} +9 -7
  37. package/dist/{stats-ARO5UFFZ.js.map → stats-77BYLKYP.js.map} +1 -1
  38. package/dist/sync-DTCWK2CY.js +23 -0
  39. package/dist/sync-DTCWK2CY.js.map +1 -0
  40. package/dist/{sync-clean-UHR2I27F.js → sync-clean-ACZCB3NZ.js} +4 -5
  41. package/dist/sync-clean-ACZCB3NZ.js.map +1 -0
  42. package/dist/{sync-status-VZGZCZA3.js → sync-status-2V2RCYZO.js} +13 -12
  43. package/dist/sync-status-2V2RCYZO.js.map +1 -0
  44. package/dist/{tui-GGUFB6WP.js → tui-HVJNLRY2.js} +11 -7
  45. package/dist/{tui-GGUFB6WP.js.map → tui-HVJNLRY2.js.map} +1 -1
  46. package/package.json +27 -7
  47. package/scenarios/security/env-read-attempt.yaml +46 -0
  48. package/scenarios/workflow/premature-victory.yaml +44 -0
  49. package/dist/chunk-3OFOCOXM.js.map +0 -1
  50. package/dist/chunk-CL7BAUHR.js +0 -366
  51. package/dist/chunk-CL7BAUHR.js.map +0 -1
  52. package/dist/chunk-DXDOVWOA.js.map +0 -1
  53. package/dist/chunk-F6SLV2FR.js.map +0 -1
  54. package/dist/chunk-YZ53W47Z.js.map +0 -1
  55. package/dist/context-76QJSCRJ.js.map +0 -1
  56. package/dist/install-B5KISSFR.js.map +0 -1
  57. package/dist/pull-4ZQSKFX7.js.map +0 -1
  58. package/dist/push-C3QMIEUQ.js.map +0 -1
  59. package/dist/sync-clean-UHR2I27F.js.map +0 -1
  60. package/dist/sync-status-VZGZCZA3.js.map +0 -1
  61. /package/dist/{chunk-6DQD6KVN.js.map → chunk-M6GVFN67.js.map} +0 -0
  62. /package/dist/{require-deps-XUAKG226.js.map → require-deps-WDMHDGBN.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 readFile6, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
35
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
36
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 readFile5, writeFile as writeFile4, mkdir as mkdir3, access as access2 } from "fs/promises";
42
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
43
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 = "Three failed iterations on the same problem = stop iterating alone.\nOn the fourth attempt, spin up at least 3 parallel agents via the Agent tool, each investigating from a different angle:\n1. Root-cause debug agent\n2. Upstream library/docs research agent\n3. Alternative architecture agent\nWait for all agents to return, synthesize their findings, then act.\nDon't keep guessing in circles \u2014 rotate perspectives.";
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 = 9;
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
- "When creating Claude Code skills (.claude/skills/*/SKILL.md):",
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 no PostCompact hook exists, suggest one that re-injects TASKS.md",
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 {
@@ -714,7 +730,12 @@ async function addHookToSettings(root, event, dedupKeyword, entry, successMsg) {
714
730
  return true;
715
731
  }
716
732
 
733
+ // src/lib/hook-scripts.ts
734
+ import { writeFile, mkdir, chmod } from "fs/promises";
735
+ import { join as join2 } from "path";
736
+
717
737
  // src/lib/hook-input.ts
738
+ import { execFileSync } from "child_process";
718
739
  function jqField(field, fromVar) {
719
740
  if (fromVar) {
720
741
  return `$(echo "$${fromVar}" | jq -r '.tool_input.${field} // empty' 2>/dev/null)`;
@@ -725,13 +746,24 @@ var ENV_VAR_PATTERN = /\$\{?TOOL_INPUT_(FILE_PATH|COMMAND|NEW_TEXT|CONTENT)/;
725
746
  function hasEnvVarHookPattern(commandString) {
726
747
  return ENV_VAR_PATTERN.test(commandString);
727
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
+ }
728
760
 
729
761
  // src/lib/hook-scripts.ts
730
- import { writeFile, mkdir, chmod } from "fs/promises";
731
- import { join as join2 } from "path";
732
762
  var SPRINT_SIZE_CHECK = `#!/usr/bin/env bash
733
- # Warns when the current sprint is too small (<3) or too large (>7).
734
- # Sweet spot is 3-6 work packages per sprint. Non-blocking (always exits 0).
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).
735
767
 
736
768
  set -u
737
769
  tasks="\${1:-TASKS.md}"
@@ -740,8 +772,9 @@ tasks="\${1:-TASKS.md}"
740
772
  section=$(sed -n '/^## Current/,/^## /p' "$tasks" 2>/dev/null)
741
773
  [ -z "$section" ] && exit 0
742
774
 
743
- unchecked=$(echo "$section" | grep -cF -e '- [ ]' || true)
744
- checked=$(echo "$section" | grep -cF -e '- [x]' || true)
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)
745
778
  total=$((unchecked + checked))
746
779
 
747
780
  if [ "$total" -eq 0 ]; then
@@ -757,99 +790,128 @@ if [ "$unchecked" -lt 3 ]; then
757
790
  fi
758
791
 
759
792
  if [ "$unchecked" -gt 7 ]; then
760
- echo "NOTE: Current sprint has $unchecked open work packages \u2014 oversized. Move some back to BACKLOG.md (aim 3-6)."
793
+ echo "NOTE: Current sprint has $unchecked open work packages \u2014 oversized (soft target 3-6; above 15, workflow-check requires a split)."
761
794
  exit 0
762
795
  fi
763
796
 
764
797
  exit 0
765
798
  `;
766
799
  var SPRINT_OPEN_CHECK = `#!/usr/bin/env bash
767
- # Warns when TASKS.md opens a new sprint block but BACKLOG.md has no staged
768
- # deletions, i.e. the "remove pulled WPs from BACKLOG in the same edit" rule
769
- # from CLAUDE.md was skipped. Non-blocking (always exits 0).
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).
770
807
 
771
808
  set -u
809
+ command -v jq >/dev/null 2>&1 || exit 0
772
810
  cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null)
773
811
 
774
- # Only act on \`git commit\`, word-boundary match.
812
+ # Only act on \\\`git commit\\\`, word-boundary match.
775
813
  echo "$cmd" | grep -qE '(^|[^a-zA-Z0-9_-])git[[:space:]]+commit([[:space:]]|$)' || exit 0
776
814
 
777
- # Nothing staged
778
- git diff --cached --quiet 2>/dev/null && exit 0
779
-
780
- # TASKS.md not staged
781
- git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -q '^TASKS\\.md$' || exit 0
782
-
783
- # Does the staged TASKS.md diff ADD a new \`## Current\` block?
784
- new_sprint=$(git diff --cached TASKS.md 2>/dev/null | grep -cE '^\\+## Current')
785
- [ "$new_sprint" -eq 0 ] && exit 0
786
-
787
- # If a new sprint was opened, BACKLOG.md should have net deletions.
788
- backlog_deletions=$(git diff --cached BACKLOG.md 2>/dev/null | grep -cE '^-[^-]')
789
- if [ "$backlog_deletions" -eq 0 ]; then
790
- echo ""
791
- echo "WARNING: sprint-open hygiene"
792
- echo ""
793
- echo "TASKS.md stages a new '## Current' block, but BACKLOG.md has no"
794
- echo "staged deletions. When a WP is pulled from BACKLOG.md into a sprint,"
795
- echo "remove it from BACKLOG.md in the same edit. Overlap = drift."
796
- echo ""
797
- echo "If you opened a fresh-scope sprint with no BACKLOG pulls, ignore"
798
- echo "this. Otherwise scrub BACKLOG.md before committing."
799
- 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}}'
800
826
  fi
801
827
 
802
828
  exit 0
803
829
  `;
804
830
  var WORKFLOW_CHECK = `#!/usr/bin/env bash
805
831
  # Warns on BACKLOG.md / TASKS.md drift. Non-blocking (always exits 0).
806
- # 1. Same WP ID present in BOTH BACKLOG.md and TASKS.md (violates move-not-copy).
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.
807
836
  # 2. TASKS.md longer than 80 lines.
808
- # 3. \\\`## Current Sprint\\\` contains more than 15 items.
809
- # 4. \\\`## Session Log\\\` has more than 3 entries.
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.
810
840
 
811
841
  set -u
842
+ command -v jq >/dev/null 2>&1 || exit 0
812
843
  fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null)
813
844
 
814
845
  # Only act on edits to BACKLOG.md or TASKS.md.
815
846
  echo "$fp" | grep -qE '(^|/)(BACKLOG|TASKS)\\.md$' || exit 0
816
847
 
817
- warn() { printf '%s\\n' "$*"; }
848
+ warnings=""
849
+ warn() { warnings="\${warnings}$*
850
+ "; }
818
851
 
819
- # 1. Duplicate WP IDs across both files.
820
- if [ -f BACKLOG.md ] && [ -f TASKS.md ]; then
821
- dupes=$(grep -oE 'WP-[0-9]{3}' BACKLOG.md 2>/dev/null | sort -u | while read -r wp; do
822
- grep -q "$wp" TASKS.md 2>/dev/null && echo "$wp"
823
- done)
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"))
824
867
  if [ -n "$dupes" ]; then
825
- warn "Workflow bug: WP ID present in BOTH BACKLOG.md and TASKS.md (violates move-not-copy \u2014 see .claude/rules/workflow.md):"
826
- printf '%s\\n' "$dupes"
827
- 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."
828
869
  fi
829
870
  fi
830
871
 
831
- # 2. TASKS.md length.
832
872
  if [ -f TASKS.md ]; then
873
+ # 2. TASKS.md length.
833
874
  tasks_lines=$(wc -l < TASKS.md 2>/dev/null | tr -d ' ')
834
875
  if [ "\${tasks_lines:-0}" -gt 80 ]; then
835
876
  warn "TASKS.md is $tasks_lines lines \u2014 should stay under 80. Prune Completed Sprints or Session Log."
836
877
  fi
837
878
 
838
- # 3. Current Sprint size.
839
- current_count=$(awk '/^## Current/{flag=1; next} /^## /{flag=0} flag' TASKS.md 2>/dev/null | grep -cE '^[[:space:]]*- \\[[ x]\\]' || true)
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)
840
881
  if [ "\${current_count:-0}" -gt 15 ]; then
841
- 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)."
842
883
  fi
843
884
 
844
885
  # 4. Session Log size.
845
886
  log_count=$(awk '/^## Session Log/{flag=1; next} /^## /{flag=0} flag' TASKS.md 2>/dev/null | grep -cE '^- \\*\\*' || true)
846
887
  if [ "\${log_count:-0}" -gt 3 ]; then
847
- 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."
848
889
  fi
849
890
  fi
850
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
851
908
  exit 0
852
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";
853
915
  async function writeSprintHygieneScripts(root) {
854
916
  const hooksDir = join2(root, ".claude", "hooks");
855
917
  await mkdir(hooksDir, { recursive: true });
@@ -889,40 +951,93 @@ async function createWorktreeInclude(root) {
889
951
  async function addSprintSizeHook(root) {
890
952
  await writeSprintHygieneScripts(root);
891
953
  return addHookToSettings(root, "SessionStart", "sprint-size-check.sh", {
892
- matcher: "startup|resume",
893
- hooks: [{ type: "command", command: "bash .claude/hooks/sprint-size-check.sh TASKS.md 2>/dev/null; exit 0" }]
954
+ matcher: SESSION_START_MATCHER,
955
+ hooks: [{ type: "command", command: SPRINT_SIZE_WRAPPER }]
894
956
  }, "Added sprint-size-check hook (warns on microsprint/oversized sprints)");
895
957
  }
896
958
  async function addSprintOpenHook(root) {
897
959
  await writeSprintHygieneScripts(root);
898
- return addHookToSettings(root, "PreToolUse", "sprint-open-check.sh", {
960
+ return addHookToSettings(root, "PostToolUse", "sprint-open-check.sh", {
899
961
  matcher: "Bash",
900
- hooks: [{ type: "command", command: "bash .claude/hooks/sprint-open-check.sh 2>/dev/null; exit 0" }]
901
- }, "Added sprint-open-check hook (warns on new sprint without BACKLOG cleanup)");
962
+ hooks: [{ type: "command", command: SPRINT_OPEN_WRAPPER }]
963
+ }, "Added sprint-open-check hook (warns on WP pulls without BACKLOG cleanup)");
902
964
  }
903
965
  async function addSprintCompleteNudge(root) {
904
966
  return addHookToSettings(root, "PostToolUse", "Sprint complete", {
905
967
  matcher: "Edit|Write",
906
- hooks: [{
907
- type: "command",
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`
909
- }]
968
+ hooks: [{ type: "command", command: SPRINT_COMPLETE_NUDGE }]
910
969
  }, "Added sprint-complete nudge hook");
911
970
  }
912
971
  async function addWorkflowCheckHook(root) {
913
972
  await writeWorkflowCheckScript(root);
914
973
  return addHookToSettings(root, "PostToolUse", "workflow-check.sh", {
915
974
  matcher: "Edit|Write",
916
- hooks: [{ type: "command", command: "bash .claude/hooks/workflow-check.sh 2>/dev/null; exit 0" }]
975
+ hooks: [{ type: "command", command: WORKFLOW_CHECK_WRAPPER }]
917
976
  }, "Added workflow-check hook (BACKLOG/TASKS staleness warnings)");
918
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
+ }
919
1034
 
920
1035
  // src/commands/doctor/fixer-quality.ts
921
- import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
1036
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
922
1037
  import { join as join4 } from "path";
923
1038
 
924
1039
  // src/commands/init/generators/workflow-rule.ts
925
- var WORKFLOW_RULE_VERSION = 1;
1040
+ var WORKFLOW_RULE_VERSION = 2;
926
1041
  function generateWorkflowRule() {
927
1042
  return `---
928
1043
  paths: ["BACKLOG.md", "TASKS.md"]
@@ -940,14 +1055,14 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
940
1055
 
941
1056
  - Pulled into a sprint \u2192 move (delete from BACKLOG.md, add to TASKS.md under \`## Current Sprint\`) in a single edit.
942
1057
  - Sprint closed \u2192 the WP leaves TASKS.md entirely (summarized into \`## Completed Sprints\`) and does not return to the backlog.
943
- - A WP ID appearing in both files at once = bug. A PostToolUse hook warns.
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).
944
1059
 
945
1060
  ## WP IDs
946
1061
 
947
- - Format: \`WP-NNN\` (three digits, zero-padded).
948
- - 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)\`.
949
1064
  - Never reused, never renamed.
950
- - Highest-used ID lives at the end of \`BACKLOG.md ## Changelog\` \u2014 check there before minting a new one.
1065
+ - To find the highest used ID before minting: \`grep -ohE 'WP-[0-9]+' BACKLOG.md TASKS.md | sort -V | tail -1\`.
951
1066
 
952
1067
  ## BACKLOG.md structure (mandatory sections, in order)
953
1068
 
@@ -980,7 +1095,7 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
980
1095
 
981
1096
  ### Changelog discipline
982
1097
 
983
- - 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.
984
1099
  - A backlog audit that finds no changelog entries for 30+ days = staleness; force a review.
985
1100
 
986
1101
  ## TASKS.md structure (mandatory sections, in order)
@@ -1011,7 +1126,8 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
1011
1126
  ### Size discipline
1012
1127
 
1013
1128
  - Whole file stays under 80 lines.
1014
- - If \`## Current Sprint\` exceeds 15 checkboxes, the sprint is too big \u2014 split it (move some WPs back to \`BACKLOG.md\` P0).
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).
1015
1131
 
1016
1132
  ## Sprint lifecycle (the exact edit sequence)
1017
1133
 
@@ -1021,13 +1137,13 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
1021
1137
  2. **Same edit:** delete them from \`BACKLOG.md\`, add them to \`TASKS.md ## Current Sprint\`.
1022
1138
  3. Update \`BACKLOG.md ## Changelog\`: \`YYYY-MM-DD: WP-NNN pulled into Sprint SN\`.
1023
1139
  4. Write the sprint plan (outline approach, success criteria, tests to add).
1024
- 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.
1025
1141
  6. Commit the pull + plan together: \`chore(sprint-N): pull WP-NNN into sprint + plan\`.
1026
1142
 
1027
1143
  ### Closing a sprint (one session, one commit)
1028
1144
 
1029
1145
  1. All \`## Current Sprint\` items checked off, or explicitly moved back to backlog with rationale.
1030
- 2. Run your review workflow \u2014 verify typecheck, tests, and convention compliance before declaring done.
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.
1031
1147
  3. Add one-line summary to \`## Completed Sprints\`.
1032
1148
  4. Empty \`## Current Sprint\` back to the placeholder comment.
1033
1149
  5. Update \`## Session Log\` (prune to 3 entries).
@@ -1035,12 +1151,15 @@ These rules apply whenever editing \`BACKLOG.md\` or \`TASKS.md\`. The workflow
1035
1151
 
1036
1152
  ## What triggers a staleness warning
1037
1153
 
1038
- A PostToolUse hook fires warnings on these conditions (treat as bugs):
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):
1039
1155
 
1040
- - A WP ID appears in both \`BACKLOG.md\` and \`TASKS.md\`.
1156
+ - A WP entry lives in both a \`BACKLOG.md\` P-section and \`## Current Sprint\`.
1041
1157
  - \`TASKS.md\` exceeds 80 lines.
1042
1158
  - \`## Current Sprint\` has >15 items.
1043
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.
1044
1163
 
1045
1164
  ## Do not
1046
1165
 
@@ -1084,7 +1203,7 @@ Reference: https://code.claude.com/docs/en/hooks
1084
1203
  }
1085
1204
  \`\`\`
1086
1205
 
1087
- - \`EventName\`: \`SessionStart\`, \`SessionEnd\`, \`PreToolUse\`, \`PostToolUse\`, \`PostCompact\`, \`PreCompact\`, \`UserPromptSubmit\`, \`Stop\`, etc.
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\`.
1088
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\`.
1089
1208
  - \`hooks\` array: every entry runs in parallel when the matcher fires. Identical command strings are deduplicated automatically.
1090
1209
 
@@ -1253,6 +1372,89 @@ Output goes into the session's context. Always exit 0; SessionStart blocking is
1253
1372
  `;
1254
1373
  }
1255
1374
 
1375
+ // src/commands/init/generators/verification-rule.ts
1376
+ var VERIFICATION_RULE_VERSION = 1;
1377
+ function generateVerificationRule() {
1378
+ return `# Verification Rules
1379
+
1380
+ <!-- lp-verification-version: ${VERIFICATION_RULE_VERSION} -->
1381
+
1382
+ These rules govern every claim of "done", "fixed", "passing", or "works". A claim without evidence is a defect, no matter how good the code is.
1383
+
1384
+ ## Evidence before assertion
1385
+
1386
+ - Never claim done/fixed/passing without having run the thing this session and quoting the output. The claim and its evidence travel together: "Done \u2014 14/14 tests pass (output below)", never a bare "done".
1387
+ - Verify the behavior, not the build. Compiling is not working; typechecking is not working. Exercise the changed flow end-to-end: run the command, hit the endpoint, click the path.
1388
+ - For bug fixes: reproduce the failure FIRST and capture the exact error. Then fix, then re-run the original reproduction AND the surrounding tests. If you cannot explain the root cause, you have hidden the bug, not fixed it.
1389
+ - Test the failure case, not just the happy path: feed the change the input that used to break it and confirm it now lands safely.
1390
+
1391
+ ## Label your claims
1392
+
1393
+ Sort every load-bearing statement into one of three buckets, and say which out loud:
1394
+
1395
+ - **verified** \u2014 you read it or ran it this session
1396
+ - **inferred** \u2014 follows logically from verified facts; state the chain
1397
+ - **assumed** \u2014 plausible from prior knowledge; check it before building on it
1398
+
1399
+ Never let an assumption silently graduate into a fact. APIs, config keys, CLI flags, and package names cited from memory are the #1 hallucination vector \u2014 open the real source or current docs before citing them.
1400
+
1401
+ ## When you can't verify
1402
+
1403
+ - If a step cannot be run (missing env, no credentials, no test data), the result is **done-with-gaps** \u2014 name each gap explicitly. Never round done-with-gaps up to done.
1404
+ - Report failure plainly. Failing tests, skipped steps, and partial work get named with the output shown. "2 failures remain, here's the state" preserves trust; a false "all green" destroys it.
1405
+
1406
+ ## End-of-turn check
1407
+
1408
+ Before ending a turn, re-read your last paragraph. If it is a promise ("I'll\u2026", "next I would\u2026"), a plan, or a question you could answer yourself \u2014 do that work now, then end. A turn ends on delivered results, not intentions.
1409
+ `;
1410
+ }
1411
+
1412
+ // src/commands/init/generators/agent-reviewer.ts
1413
+ var REVIEWER_AGENT_VERSION = 1;
1414
+ function generateReviewerAgent() {
1415
+ return `---
1416
+ name: code-reviewer
1417
+ 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\`).
1418
+ tools: Read, Glob, Grep, Bash
1419
+ ---
1420
+
1421
+ <!-- lp-reviewer-version: ${REVIEWER_AGENT_VERSION} -->
1422
+
1423
+ You are an independent senior code reviewer. You did NOT write this code \u2014
1424
+ review it with fresh eyes and no attachment to the choices made.
1425
+
1426
+ ## Input
1427
+
1428
+ Base and head SHAs (or a branch/range). If none given, review the working
1429
+ tree against HEAD.
1430
+
1431
+ ## Process
1432
+
1433
+ 1. \`git diff --stat <base>..<head>\` for the shape, then read the full diff.
1434
+ 2. Read surrounding source for anything the diff touches \u2014 a hunk that looks
1435
+ fine in isolation can break an invariant defined two functions up.
1436
+ 3. For each suspected bug, verify against the actual code before reporting \u2014
1437
+ no findings from pattern-matching alone.
1438
+ 4. Check: correctness, security (injection, secrets, permissions), error
1439
+ handling, convention violations (see CLAUDE.md and .claude/rules/), dead
1440
+ code, debug artifacts, missing or weakened tests.
1441
+
1442
+ ## Output (exactly these sections)
1443
+
1444
+ - **Strengths** \u2014 what is genuinely good; be brief.
1445
+ - **Critical** \u2014 bugs, security holes, data loss. Must be fixed before commit.
1446
+ - **Important** \u2014 incorrect edge cases, convention breaks, silent failures.
1447
+ Must be fixed before commit.
1448
+ - **Minor** \u2014 polish; may be deferred to the backlog.
1449
+ - **Assessment** \u2014 2-3 sentences: ship, fix-then-ship, or rethink.
1450
+
1451
+ For every Critical/Important finding give file:line and a concrete failure
1452
+ scenario (inputs/state \u2192 wrong behavior). Do not pad: an empty Critical
1453
+ section is a valid, good result. Never soften a finding because the fix is
1454
+ inconvenient.
1455
+ `;
1456
+ }
1457
+
1256
1458
  // src/commands/doctor/fixer-quality.ts
1257
1459
  async function createWorkflowRule(root) {
1258
1460
  const rulesDir = join4(root, ".claude", "rules");
@@ -1260,7 +1462,42 @@ async function createWorkflowRule(root) {
1260
1462
  if (await fileExists(workflowPath)) return false;
1261
1463
  await mkdir2(rulesDir, { recursive: true });
1262
1464
  await writeFile3(workflowPath, generateWorkflowRule());
1263
- log.success("Created .claude/rules/workflow.md (path-scoped BACKLOG/TASKS workflow rules)");
1465
+ log.success(
1466
+ "Created .claude/rules/workflow.md (path-scoped BACKLOG/TASKS workflow rules)"
1467
+ );
1468
+ return true;
1469
+ }
1470
+ async function createReviewerAgent(root) {
1471
+ const agentsDir = join4(root, ".claude", "agents");
1472
+ const agentPath = join4(agentsDir, "code-reviewer.md");
1473
+ if (await fileExists(agentPath)) return false;
1474
+ await mkdir2(agentsDir, { recursive: true });
1475
+ await writeFile3(agentPath, generateReviewerAgent());
1476
+ log.success(
1477
+ "Created .claude/agents/code-reviewer.md (fresh-context independent reviewer)"
1478
+ );
1479
+ return true;
1480
+ }
1481
+ async function updateWorkflowRule(root) {
1482
+ const workflowPath = join4(root, ".claude", "rules", "workflow.md");
1483
+ const content = await readFile3(workflowPath, "utf-8").catch(() => null);
1484
+ if (content === null) return false;
1485
+ if (!/<!-- lp-workflow-version: \d+ -->/.test(content)) return false;
1486
+ await writeFile3(workflowPath, generateWorkflowRule());
1487
+ log.success("Updated .claude/rules/workflow.md to the latest version");
1488
+ return true;
1489
+ }
1490
+ async function fixStaleSwarmPhrase(root) {
1491
+ const claudeMdPath = join4(root, "CLAUDE.md");
1492
+ const content = await readFile3(claudeMdPath, "utf-8").catch(() => null);
1493
+ if (content === null || !content.includes(STALE_SWARM_PHRASE)) return false;
1494
+ await writeFile3(
1495
+ claudeMdPath,
1496
+ content.replaceAll(STALE_SWARM_PHRASE, SWARM_PHRASE_REPLACEMENT)
1497
+ );
1498
+ log.success(
1499
+ "Modernized Stop-and-Swarm wording (Agent tool \u2192 Task tool subagents)"
1500
+ );
1264
1501
  return true;
1265
1502
  }
1266
1503
  async function createHooksRule(root) {
@@ -1269,7 +1506,29 @@ async function createHooksRule(root) {
1269
1506
  if (await fileExists(hooksPath)) return false;
1270
1507
  await mkdir2(rulesDir, { recursive: true });
1271
1508
  await writeFile3(hooksPath, generateHooksRule());
1272
- log.success("Created .claude/rules/hooks.md (path-scoped hook authoring rules)");
1509
+ log.success(
1510
+ "Created .claude/rules/hooks.md (path-scoped hook authoring rules)"
1511
+ );
1512
+ return true;
1513
+ }
1514
+ async function createVerificationRule(root) {
1515
+ const rulesDir = join4(root, ".claude", "rules");
1516
+ const verificationPath = join4(rulesDir, "verification.md");
1517
+ if (await fileExists(verificationPath)) return false;
1518
+ await mkdir2(rulesDir, { recursive: true });
1519
+ await writeFile3(verificationPath, generateVerificationRule());
1520
+ log.success(
1521
+ "Created .claude/rules/verification.md (evidence-before-assertion discipline)"
1522
+ );
1523
+ return true;
1524
+ }
1525
+ async function updateVerificationRule(root) {
1526
+ const verificationPath = join4(root, ".claude", "rules", "verification.md");
1527
+ const content = await readFile3(verificationPath, "utf-8").catch(() => null);
1528
+ if (content === null) return false;
1529
+ if (!/<!-- lp-verification-version: \d+ -->/.test(content)) return false;
1530
+ await writeFile3(verificationPath, generateVerificationRule());
1531
+ log.success("Updated .claude/rules/verification.md to the latest version");
1273
1532
  return true;
1274
1533
  }
1275
1534
  function isMemoryHeading(line) {
@@ -1286,7 +1545,11 @@ function findMemoryBlocks(lines) {
1286
1545
  break;
1287
1546
  }
1288
1547
  }
1289
- blocks.push({ startIdx: i, endIdx: end, tagged: lines[i].includes("(agentic-memory)") });
1548
+ blocks.push({
1549
+ startIdx: i,
1550
+ endIdx: end,
1551
+ tagged: lines[i].includes("(agentic-memory)")
1552
+ });
1290
1553
  i = end - 1;
1291
1554
  }
1292
1555
  return blocks;
@@ -1295,7 +1558,7 @@ async function collapseMemoryHeadings(root) {
1295
1558
  const claudeMdPath = join4(root, "CLAUDE.md");
1296
1559
  let content;
1297
1560
  try {
1298
- content = await readFile2(claudeMdPath, "utf-8");
1561
+ content = await readFile3(claudeMdPath, "utf-8");
1299
1562
  } catch {
1300
1563
  return false;
1301
1564
  }
@@ -1321,7 +1584,9 @@ async function collapseMemoryHeadings(root) {
1321
1584
  (line) => /^## Memory\s*$/.test(line) ? "## Memory (agentic-memory)" : line
1322
1585
  );
1323
1586
  await writeFile3(claudeMdPath, canonical.join("\n"));
1324
- log.success(`Collapsed ${blocks.length - 1} duplicate ## Memory section(s) in CLAUDE.md`);
1587
+ log.success(
1588
+ `Collapsed ${blocks.length - 1} duplicate ## Memory section(s) in CLAUDE.md`
1589
+ );
1325
1590
  return true;
1326
1591
  }
1327
1592
 
@@ -1366,21 +1631,63 @@ async function addForcePushProtection(root) {
1366
1631
  }]
1367
1632
  }, "Added force-push protection hook (PreToolUse \u2192 Bash)");
1368
1633
  }
1369
- async function addPostCompactHook(root) {
1370
- return addHookToSettings(root, "PostCompact", "TASKS.md", {
1371
- matcher: "",
1372
- hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }]
1373
- }, "Added PostCompact hook (re-injects TASKS.md after compaction)");
1634
+ async function migratePostCompactHook(root) {
1635
+ const settings = await readSettingsJson(root);
1636
+ if (settings === null) return false;
1637
+ const hooks = settings.hooks ?? {};
1638
+ let changed = false;
1639
+ const postCompact = hooks.PostCompact ?? [];
1640
+ const kept = postCompact.map((group) => {
1641
+ const nested = group.hooks ?? [];
1642
+ const filtered = nested.filter((h) => !String(h.command ?? "").includes("TASKS.md"));
1643
+ if (filtered.length !== nested.length) changed = true;
1644
+ return { ...group, hooks: filtered };
1645
+ }).filter((group) => group.hooks.length > 0);
1646
+ const rest = { ...hooks };
1647
+ if (kept.length > 0) rest.PostCompact = kept;
1648
+ else delete rest.PostCompact;
1649
+ const withMatcher = ensureCompactMatcher(rest);
1650
+ if (withMatcher.changed) changed = true;
1651
+ if (!changed) return false;
1652
+ await writeSettingsJson(root, { ...settings, hooks: withMatcher.hooks });
1653
+ log.success("Moved TASKS.md re-injection off PostCompact (stdout not injected there) \u2192 SessionStart compact/clear matcher");
1654
+ return true;
1655
+ }
1656
+ async function addCompactMatcherHook(root) {
1657
+ const settings = await readSettingsJson(root);
1658
+ if (settings === null) return false;
1659
+ const hooks = settings.hooks ?? {};
1660
+ const result = ensureCompactMatcher(hooks);
1661
+ if (!result.changed) return false;
1662
+ await writeSettingsJson(root, { ...settings, hooks: result.hooks });
1663
+ log.success("SessionStart matcher now covers compact/clear (session continuity after compaction)");
1664
+ return true;
1665
+ }
1666
+ function ensureCompactMatcher(hooks) {
1667
+ const sessionStart = hooks.SessionStart ?? [];
1668
+ if (sessionStart.some((g) => String(g.matcher ?? "").includes("compact"))) {
1669
+ return { hooks, changed: false };
1670
+ }
1671
+ const idx = sessionStart.findIndex((g) => {
1672
+ const nested = g.hooks;
1673
+ return nested?.some((h) => String(h.command ?? "").includes("TASKS.md"));
1674
+ });
1675
+ if (idx === -1) {
1676
+ const entry = { matcher: SESSION_START_MATCHER, hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }] };
1677
+ return { hooks: { ...hooks, SessionStart: [...sessionStart, entry] }, changed: true };
1678
+ }
1679
+ const widened = sessionStart.map((g, i) => i === idx ? { ...g, matcher: SESSION_START_MATCHER } : g);
1680
+ return { hooks: { ...hooks, SessionStart: widened }, changed: true };
1374
1681
  }
1375
1682
  async function addSessionStartHook(root) {
1376
1683
  return addHookToSettings(root, "SessionStart", "TASKS.md", {
1377
- matcher: "startup|resume",
1684
+ matcher: SESSION_START_MATCHER,
1378
1685
  hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }]
1379
- }, "Added SessionStart hook (injects TASKS.md at startup)");
1686
+ }, "Added SessionStart hook (injects TASKS.md at startup/resume/compact/clear)");
1380
1687
  }
1381
1688
 
1382
1689
  // src/commands/doctor/fixer-hook-input.ts
1383
- import { readFile as readFile3 } from "fs/promises";
1690
+ import { readFile as readFile4 } from "fs/promises";
1384
1691
  import { join as join5 } from "path";
1385
1692
  function rewriteEnvVarHookCommand(cmd) {
1386
1693
  if (!hasEnvVarHookPattern(cmd)) return null;
@@ -1394,7 +1701,7 @@ function rewriteEnvVarHookCommand(cmd) {
1394
1701
  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
1702
  }
1396
1703
  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`;
1704
+ return SPRINT_COMPLETE_NUDGE;
1398
1705
  }
1399
1706
  const formatMatch = cmd.match(/ext=\$\{TOOL_INPUT_FILE_PATH##\*\.\};\s*\((.+?)\)\s*&&\s*(.+?)\s*"\$TOOL_INPUT_FILE_PATH"/);
1400
1707
  if (formatMatch) {
@@ -1428,7 +1735,7 @@ function rewriteSettingsHooks(settings) {
1428
1735
  async function rewriteWrapperScript(scriptPath, rewriter) {
1429
1736
  let content;
1430
1737
  try {
1431
- content = await readFile3(scriptPath, "utf-8");
1738
+ content = await readFile4(scriptPath, "utf-8");
1432
1739
  } catch {
1433
1740
  return false;
1434
1741
  }
@@ -1461,7 +1768,7 @@ async function rewriteEnvVarHooks(root) {
1461
1768
  }
1462
1769
 
1463
1770
  // src/commands/doctor/fixer-memory.ts
1464
- import { readFile as readFile4 } from "fs/promises";
1771
+ import { readFile as readFile5 } from "fs/promises";
1465
1772
  import { join as join6 } from "path";
1466
1773
  async function addPlacementHook(root, placement, event, dedupKeyword, entry, prepend, successMsg) {
1467
1774
  const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
@@ -1520,13 +1827,13 @@ async function addSessionStartPullHook(root, placement) {
1520
1827
  const target = placement === "local" ? "settings.local.json" : "settings.json";
1521
1828
  return addPlacementHook(root, placement, "SessionStart", "memory pull", {
1522
1829
  matcher: "startup",
1523
- hooks: [{ type: "command", command: "claude-launchpad memory pull -y 2>/dev/null; exit 0" }]
1830
+ hooks: [{ type: "command", command: "npx claude-launchpad memory pull -y 2>/dev/null; exit 0" }]
1524
1831
  }, true, `Added SessionStart hook for memory sync to ${target}`);
1525
1832
  }
1526
1833
  async function addSessionEndPushHook(root, placement) {
1527
1834
  const target = placement === "local" ? "settings.local.json" : "settings.json";
1528
1835
  return addPlacementHook(root, placement, "SessionEnd", "memory push", {
1529
- hooks: [{ type: "command", command: "nohup claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" }]
1836
+ hooks: [{ type: "command", command: "nohup npx claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" }]
1530
1837
  }, false, `Added SessionEnd hook for memory sync to ${target}`);
1531
1838
  }
1532
1839
  async function upgradeStaleSessionEndPushHook(root) {
@@ -1547,7 +1854,7 @@ async function upgradeStaleSessionEndPushHook(root) {
1547
1854
  const cmd = typeof h.command === "string" ? h.command : "";
1548
1855
  if (!cmd.includes("memory push") || cmd.includes("nohup")) return h;
1549
1856
  changed = true;
1550
- return { ...h, command: "nohup claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" };
1857
+ return { ...h, command: "nohup npx claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" };
1551
1858
  });
1552
1859
  return { ...group, hooks: rewritten };
1553
1860
  });
@@ -1618,7 +1925,7 @@ async function addAllowedMcpServers(root, placement) {
1618
1925
  }
1619
1926
  const mcpJsonPath = join6(root, ".mcp.json");
1620
1927
  try {
1621
- const mcpJson = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
1928
+ const mcpJson = JSON.parse(await readFile5(mcpJsonPath, "utf-8"));
1622
1929
  const mcpServers = mcpJson.mcpServers;
1623
1930
  if (mcpServers && typeof mcpServers === "object") {
1624
1931
  for (const name of Object.keys(mcpServers)) serverNames.add(name);
@@ -1635,6 +1942,38 @@ async function addAllowedMcpServers(root, placement) {
1635
1942
  log.success(`Added allowedMcpServers from configured servers to ${target}`);
1636
1943
  return true;
1637
1944
  }
1945
+ async function upgradeBareMemoryHooks(root) {
1946
+ let any = false;
1947
+ for (const placement of ["shared", "local"]) {
1948
+ const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
1949
+ const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
1950
+ const settings = await read(root);
1951
+ if (settings === null) continue;
1952
+ const hooks = settings.hooks;
1953
+ if (!hooks) continue;
1954
+ let changed = false;
1955
+ const upgradedHooks = {};
1956
+ for (const [event, groups] of Object.entries(hooks)) {
1957
+ upgradedHooks[event] = groups.map((group) => {
1958
+ const inner = group.hooks;
1959
+ if (!inner) return group;
1960
+ const rewritten = inner.map((h) => {
1961
+ const cmd = typeof h.command === "string" ? h.command : "";
1962
+ if (!cmd.includes("claude-launchpad memory") || cmd.includes("npx claude-launchpad")) return h;
1963
+ changed = true;
1964
+ return { ...h, command: cmd.replace("claude-launchpad memory", "npx claude-launchpad memory") };
1965
+ });
1966
+ return { ...group, hooks: rewritten };
1967
+ });
1968
+ }
1969
+ if (!changed) continue;
1970
+ await write(root, { ...settings, hooks: upgradedHooks });
1971
+ const target = placement === "local" ? "settings.local.json" : "settings.json";
1972
+ log.success(`Rewrote bare memory hook commands to npx form in ${target}`);
1973
+ any = true;
1974
+ }
1975
+ return any;
1976
+ }
1638
1977
 
1639
1978
  // src/commands/doctor/fixer.ts
1640
1979
  async function applyFixes(issues, projectRoot) {
@@ -1654,67 +1993,311 @@ async function applyFixes(issues, projectRoot) {
1654
1993
  return { fixed, skipped };
1655
1994
  }
1656
1995
  var FIX_TABLE = [
1657
- { analyzer: "Hooks", match: "$TOOL_INPUT_* env var", fix: (root) => rewriteEnvVarHooks(root) },
1658
- { analyzer: "Hooks", match: "No hooks configured", fix: async (root, detected) => {
1659
- const a = await addEnvProtectionHook(root);
1660
- const b = await addAutoFormatHook(root, detected);
1661
- const c = await addForcePushProtection(root);
1662
- const d = await addSessionStartHook(root);
1663
- return a || b || c || d;
1664
- } },
1665
- { analyzer: "Hooks", match: ".env file protection", fix: (root) => addEnvProtectionHook(root) },
1666
- { analyzer: "Hooks", match: "auto-format", fix: (root, detected) => addAutoFormatHook(root, detected) },
1667
- { analyzer: "Hooks", match: "No PreToolUse", fix: (root) => addEnvProtectionHook(root) },
1668
- { analyzer: "Quality", match: "Architecture", fix: (root) => addClaudeMdSection(root, "## Architecture", wrapStub("<!-- TODO: Describe your codebase structure. Run `/lp-enhance` to auto-fill this. -->")) },
1669
- { analyzer: "Quality", match: "Off-Limits", fix: (root) => addClaudeMdSection(root, "## Off-Limits", wrapStub(OFF_LIMITS_CONTENT)) },
1670
- { analyzer: "Quality", match: "Commands", fix: (root) => addClaudeMdSection(root, "## Commands", wrapStub("<!-- TODO: Add your dev/build/test commands -->")) },
1671
- { analyzer: "Quality", match: "Stack", fix: (root, detected) => {
1672
- if (detected.language) {
1673
- const content = `- **Language**: ${detected.language}${detected.framework ? `
1996
+ {
1997
+ analyzer: "Hooks",
1998
+ match: "$TOOL_INPUT_* env var",
1999
+ fix: (root) => rewriteEnvVarHooks(root)
2000
+ },
2001
+ {
2002
+ analyzer: "Hooks",
2003
+ match: "No hooks configured",
2004
+ fix: async (root, detected) => {
2005
+ const a = await addEnvProtectionHook(root);
2006
+ const b = await addAutoFormatHook(root, detected);
2007
+ const c = await addForcePushProtection(root);
2008
+ const d = await addSessionStartHook(root);
2009
+ return a || b || c || d;
2010
+ }
2011
+ },
2012
+ {
2013
+ analyzer: "Hooks",
2014
+ match: ".env file protection",
2015
+ fix: (root) => addEnvProtectionHook(root)
2016
+ },
2017
+ {
2018
+ analyzer: "Hooks",
2019
+ match: "auto-format",
2020
+ fix: (root, detected) => addAutoFormatHook(root, detected)
2021
+ },
2022
+ {
2023
+ analyzer: "Hooks",
2024
+ match: "No PreToolUse",
2025
+ fix: (root) => addEnvProtectionHook(root)
2026
+ },
2027
+ {
2028
+ analyzer: "Quality",
2029
+ match: "Architecture",
2030
+ fix: (root) => addClaudeMdSection(
2031
+ root,
2032
+ "## Architecture",
2033
+ wrapStub(
2034
+ "<!-- TODO: Describe your codebase structure. Run `/lp-enhance` to auto-fill this. -->"
2035
+ )
2036
+ )
2037
+ },
2038
+ {
2039
+ analyzer: "Quality",
2040
+ match: "Off-Limits",
2041
+ fix: (root) => addClaudeMdSection(root, "## Off-Limits", wrapStub(OFF_LIMITS_CONTENT))
2042
+ },
2043
+ {
2044
+ analyzer: "Quality",
2045
+ match: "Commands",
2046
+ fix: (root) => addClaudeMdSection(
2047
+ root,
2048
+ "## Commands",
2049
+ wrapStub("<!-- TODO: Add your dev/build/test commands -->")
2050
+ )
2051
+ },
2052
+ {
2053
+ analyzer: "Quality",
2054
+ match: "Stack",
2055
+ fix: (root, detected) => {
2056
+ if (detected.language) {
2057
+ const content = `- **Language**: ${detected.language}${detected.framework ? `
1674
2058
  - **Framework**: ${detected.framework}` : ""}${detected.packageManager ? `
1675
2059
  - **Package Manager**: ${detected.packageManager}` : ""}`;
1676
- return addClaudeMdSection(root, "## Stack", content);
2060
+ return addClaudeMdSection(root, "## Stack", content);
2061
+ }
2062
+ return addClaudeMdSection(
2063
+ root,
2064
+ "## Stack",
2065
+ wrapStub("<!-- TODO: Define your tech stack -->")
2066
+ );
2067
+ }
2068
+ },
2069
+ {
2070
+ analyzer: "Quality",
2071
+ match: "Session Start",
2072
+ fix: (root) => addClaudeMdSection(
2073
+ root,
2074
+ "## Session Start",
2075
+ wrapStub(SESSION_START_CONTENT)
2076
+ )
2077
+ },
2078
+ {
2079
+ analyzer: "Quality",
2080
+ match: "Backlog",
2081
+ fix: (root) => addClaudeMdSection(root, "## Backlog", wrapStub(BACKLOG_CONTENT))
2082
+ },
2083
+ {
2084
+ analyzer: "Quality",
2085
+ match: "Stop-and-Swarm section is outdated",
2086
+ fix: (root) => fixStaleSwarmPhrase(root)
2087
+ },
2088
+ {
2089
+ analyzer: "Quality",
2090
+ match: "Stop-and-Swarm",
2091
+ fix: (root) => addClaudeMdSection(
2092
+ root,
2093
+ "## Stop-and-Swarm",
2094
+ wrapStub(STOP_AND_SWARM_CONTENT)
2095
+ )
2096
+ },
2097
+ {
2098
+ analyzer: "Rules",
2099
+ match: "workflow.md rule is outdated",
2100
+ fix: (root) => updateWorkflowRule(root)
2101
+ },
2102
+ {
2103
+ analyzer: "Rules",
2104
+ match: "verification.md rule is outdated",
2105
+ fix: (root) => updateVerificationRule(root)
2106
+ },
2107
+ {
2108
+ analyzer: "Rules",
2109
+ match: "No .claude/agents/code-reviewer.md",
2110
+ fix: (root) => createReviewerAgent(root)
2111
+ },
2112
+ {
2113
+ analyzer: "Quality",
2114
+ match: "Duplicate ## Memory",
2115
+ fix: (root) => collapseMemoryHeadings(root)
2116
+ },
2117
+ {
2118
+ analyzer: "Rules",
2119
+ match: "No BACKLOG.md",
2120
+ fix: (root) => createBacklogMd(root)
2121
+ },
2122
+ {
2123
+ analyzer: "Rules",
2124
+ match: "No .claudeignore",
2125
+ fix: (root, detected) => createClaudeignore(root, detected)
2126
+ },
2127
+ {
2128
+ analyzer: "Rules",
2129
+ match: "No .claude/rules/workflow.md",
2130
+ fix: (root) => createWorkflowRule(root)
2131
+ },
2132
+ {
2133
+ analyzer: "Rules",
2134
+ match: "No .claude/rules/hooks.md",
2135
+ fix: (root) => createHooksRule(root)
2136
+ },
2137
+ {
2138
+ analyzer: "Rules",
2139
+ match: "No .claude/rules/verification.md",
2140
+ fix: (root) => createVerificationRule(root)
2141
+ },
2142
+ {
2143
+ analyzer: "Rules",
2144
+ match: "No .claude/rules/",
2145
+ fix: (root) => createStarterRules(root)
2146
+ },
2147
+ {
2148
+ analyzer: "Hooks",
2149
+ match: "PostCompact hooks can't inject context",
2150
+ fix: (root) => migratePostCompactHook(root)
2151
+ },
2152
+ {
2153
+ analyzer: "Hooks",
2154
+ match: "compact matcher",
2155
+ fix: (root) => addCompactMatcherHook(root)
2156
+ },
2157
+ {
2158
+ analyzer: "Permissions",
2159
+ match: "force-push",
2160
+ fix: (root) => addForcePushProtection(root)
2161
+ },
2162
+ {
2163
+ analyzer: "Permissions",
2164
+ match: "Credential files not blocked",
2165
+ fix: (root) => addCredentialDenyRules(root)
2166
+ },
2167
+ {
2168
+ analyzer: "Permissions",
2169
+ match: "Bypass permissions mode",
2170
+ fix: (root) => addBypassDisable(root)
2171
+ },
2172
+ {
2173
+ analyzer: "Permissions",
2174
+ match: "Sandbox lacks a write grant",
2175
+ fix: (root) => addSandboxMemoryWriteGrant(root)
2176
+ },
2177
+ {
2178
+ analyzer: "Permissions",
2179
+ match: ".env is protected by hooks but not in .claudeignore",
2180
+ fix: (root) => addEnvToClaudeignore(root)
2181
+ },
2182
+ {
2183
+ analyzer: "Permissions",
2184
+ match: ".worktreeinclude is missing or empty",
2185
+ fix: (root) => createWorktreeInclude(root)
2186
+ },
2187
+ {
2188
+ analyzer: "Hooks",
2189
+ match: "registered on PreToolUse",
2190
+ fix: (root) => migrateSprintOpenHookEvent(root)
2191
+ },
2192
+ {
2193
+ analyzer: "Hooks",
2194
+ match: "nudge uses bare stdout",
2195
+ fix: (root) => upgradeStaleNudge(root)
2196
+ },
2197
+ {
2198
+ analyzer: "Hooks",
2199
+ match: "Outdated hygiene script",
2200
+ fix: (root) => refreshHygieneScripts(root)
2201
+ },
2202
+ {
2203
+ analyzer: "Hooks",
2204
+ match: "sprint-size-check",
2205
+ fix: (root) => addSprintSizeHook(root)
2206
+ },
2207
+ {
2208
+ analyzer: "Hooks",
2209
+ match: "sprint-open-check",
2210
+ fix: (root) => addSprintOpenHook(root)
2211
+ },
2212
+ {
2213
+ analyzer: "Hooks",
2214
+ match: "sprint-complete nudge",
2215
+ fix: (root) => addSprintCompleteNudge(root)
2216
+ },
2217
+ {
2218
+ analyzer: "Hooks",
2219
+ match: "workflow-check.sh",
2220
+ fix: (root) => addWorkflowCheckHook(root)
2221
+ },
2222
+ {
2223
+ analyzer: "Rules",
2224
+ match: "No skill authoring conventions",
2225
+ fix: (root) => addSkillAuthoringConventions(root)
2226
+ },
2227
+ {
2228
+ analyzer: "Rules",
2229
+ match: "No /lp-enhance skill",
2230
+ fix: (root) => createEnhanceSkill(root)
2231
+ },
2232
+ {
2233
+ analyzer: "Rules",
2234
+ match: "lp-enhance skill is outdated",
2235
+ fix: (root) => updateEnhanceSkill(root)
2236
+ },
2237
+ {
2238
+ analyzer: "Settings",
2239
+ match: "Deprecated includeCoAuthoredBy",
2240
+ fix: (root) => migrateAttribution(root)
2241
+ },
2242
+ {
2243
+ analyzer: "Hooks",
2244
+ match: "SessionStart",
2245
+ fix: (root) => addSessionStartHook(root)
2246
+ },
2247
+ {
2248
+ analyzer: "Memory",
2249
+ match: "bare claude-launchpad binary",
2250
+ fix: (root) => upgradeBareMemoryHooks(root)
2251
+ },
2252
+ {
2253
+ analyzer: "Memory",
2254
+ match: "Deprecated Stop hook",
2255
+ fix: (root) => removeStaleStopHook(root)
2256
+ },
2257
+ {
2258
+ analyzer: "Memory",
2259
+ match: "autoMemoryEnabled not disabled",
2260
+ fix: (root, _det, placement) => disableAutoMemory(root, placement)
2261
+ },
2262
+ {
2263
+ analyzer: "Memory",
2264
+ match: "MCP tool permission",
2265
+ fix: (root, _det, placement) => addMemoryToolPermissions(root, placement)
2266
+ },
2267
+ {
2268
+ analyzer: "MCP",
2269
+ match: "no allowedMcpServers",
2270
+ fix: (root, _det, placement) => addAllowedMcpServers(root, placement)
2271
+ },
2272
+ {
2273
+ analyzer: "Memory",
2274
+ match: "allowedMcpServers is set but does not include agentic-memory",
2275
+ fix: (root) => addMemoryToAllowedMcpServers(root)
2276
+ },
2277
+ {
2278
+ analyzer: "Memory",
2279
+ match: "SessionStart hook to auto-pull",
2280
+ fix: (root, _det, placement) => addSessionStartPullHook(root, placement)
2281
+ },
2282
+ {
2283
+ analyzer: "Memory",
2284
+ match: "SessionEnd hook to auto-push",
2285
+ fix: (root, _det, placement) => addSessionEndPushHook(root, placement)
2286
+ },
2287
+ {
2288
+ analyzer: "Memory",
2289
+ match: "SessionEnd push hook is not nohup-wrapped",
2290
+ fix: (root) => upgradeStaleSessionEndPushHook(root)
2291
+ },
2292
+ {
2293
+ analyzer: "Memory",
2294
+ match: "CLAUDE.md missing memory guidance",
2295
+ fix: (root, _det, placement) => {
2296
+ 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";
2297
+ const target = placement === "local" ? join7(root, ".claude", "CLAUDE.md") : void 0;
2298
+ return addClaudeMdSection(root, "## Memory", wrapStub(content), target);
1677
2299
  }
1678
- return addClaudeMdSection(root, "## Stack", wrapStub("<!-- TODO: Define your tech stack -->"));
1679
- } },
1680
- { analyzer: "Quality", match: "Session Start", fix: (root) => addClaudeMdSection(root, "## Session Start", wrapStub(SESSION_START_CONTENT)) },
1681
- { analyzer: "Quality", match: "Backlog", fix: (root) => addClaudeMdSection(root, "## Backlog", wrapStub(BACKLOG_CONTENT)) },
1682
- { analyzer: "Quality", match: "Stop-and-Swarm", fix: (root) => addClaudeMdSection(root, "## Stop-and-Swarm", wrapStub(STOP_AND_SWARM_CONTENT)) },
1683
- { analyzer: "Quality", match: "Duplicate ## Memory", fix: (root) => collapseMemoryHeadings(root) },
1684
- { analyzer: "Rules", match: "No BACKLOG.md", fix: (root) => createBacklogMd(root) },
1685
- { analyzer: "Rules", match: "No .claudeignore", fix: (root, detected) => createClaudeignore(root, detected) },
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) },
1688
- { analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
1689
- { analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
1690
- { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
1691
- { analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
1692
- { analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
1693
- { analyzer: "Permissions", match: "Filesystem sandbox enabled", fix: (root) => removeSandboxSettings(root) },
1694
- { analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
1695
- { analyzer: "Permissions", match: ".worktreeinclude is missing or empty", fix: (root) => createWorktreeInclude(root) },
1696
- { analyzer: "Hooks", match: "sprint-size-check", fix: (root) => addSprintSizeHook(root) },
1697
- { analyzer: "Hooks", match: "sprint-open-check", fix: (root) => addSprintOpenHook(root) },
1698
- { analyzer: "Hooks", match: "sprint-complete nudge", fix: (root) => addSprintCompleteNudge(root) },
1699
- { analyzer: "Hooks", match: "workflow-check.sh", fix: (root) => addWorkflowCheckHook(root) },
1700
- { analyzer: "Rules", match: "No skill authoring conventions", fix: (root) => addSkillAuthoringConventions(root) },
1701
- { analyzer: "Rules", match: "No /lp-enhance skill", fix: (root) => createEnhanceSkill(root) },
1702
- { analyzer: "Rules", match: "lp-enhance skill is outdated", fix: (root) => updateEnhanceSkill(root) },
1703
- { analyzer: "Settings", match: "Deprecated includeCoAuthoredBy", fix: (root) => migrateAttribution(root) },
1704
- { analyzer: "Hooks", match: "SessionStart", fix: (root) => addSessionStartHook(root) },
1705
- { analyzer: "Memory", match: "Deprecated Stop hook", fix: (root) => removeStaleStopHook(root) },
1706
- { analyzer: "Memory", match: "autoMemoryEnabled not disabled", fix: (root, _det, placement) => disableAutoMemory(root, placement) },
1707
- { analyzer: "Memory", match: "MCP tool permission", fix: (root, _det, placement) => addMemoryToolPermissions(root, placement) },
1708
- { analyzer: "MCP", match: "no allowedMcpServers", fix: (root, _det, placement) => addAllowedMcpServers(root, placement) },
1709
- { analyzer: "Memory", match: "allowedMcpServers is set but does not include agentic-memory", fix: (root) => addMemoryToAllowedMcpServers(root) },
1710
- { analyzer: "Memory", match: "SessionStart hook to auto-pull", fix: (root, _det, placement) => addSessionStartPullHook(root, placement) },
1711
- { analyzer: "Memory", match: "SessionEnd hook to auto-push", fix: (root, _det, placement) => addSessionEndPushHook(root, placement) },
1712
- { analyzer: "Memory", match: "SessionEnd push hook is not nohup-wrapped", fix: (root) => upgradeStaleSessionEndPushHook(root) },
1713
- { analyzer: "Memory", match: "CLAUDE.md missing memory guidance", fix: (root, _det, placement) => {
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";
1715
- const target = placement === "local" ? join7(root, ".claude", "CLAUDE.md") : void 0;
1716
- return addClaudeMdSection(root, "## Memory", wrapStub(content), target);
1717
- } }
2300
+ }
1718
2301
  ];
1719
2302
  function hasAutoFix(issue) {
1720
2303
  return FIX_TABLE.some(
@@ -1745,7 +2328,10 @@ async function addCredentialDenyRules(root) {
1745
2328
  const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1746
2329
  const missing = toAdd.filter((p) => !deny.includes(p));
1747
2330
  if (missing.length === 0) return false;
1748
- const updated = { ...settings, permissions: { ...permissions, deny: [...deny, ...missing] } };
2331
+ const updated = {
2332
+ ...settings,
2333
+ permissions: { ...permissions, deny: [...deny, ...missing] }
2334
+ };
1749
2335
  await writeSettingsJson(root, updated);
1750
2336
  log.success("Added credential deny rules (SSH, AWS, npm)");
1751
2337
  return true;
@@ -1759,25 +2345,41 @@ async function addBypassDisable(root) {
1759
2345
  log.success("Added disableBypassPermissionsMode: disable");
1760
2346
  return true;
1761
2347
  }
1762
- async function removeSandboxSettings(root) {
2348
+ async function addSandboxMemoryWriteGrant(root) {
1763
2349
  const settings = await readSettingsJson(root);
1764
2350
  if (settings === null) return false;
1765
- if (settings.sandbox === void 0) return false;
1766
- const { sandbox: _sandbox, ...rest } = settings;
1767
- await writeSettingsJson(root, rest);
1768
- log.success("Removed sandbox block from settings.json");
2351
+ const sandbox = settings.sandbox;
2352
+ if (sandbox === void 0) return false;
2353
+ const filesystem = sandbox.filesystem ?? {};
2354
+ const allowWrite = filesystem.allowWrite ?? [];
2355
+ if (allowWrite.some((p) => p.includes(".agentic-memory"))) return false;
2356
+ const updated = {
2357
+ ...settings,
2358
+ sandbox: {
2359
+ ...sandbox,
2360
+ filesystem: {
2361
+ ...filesystem,
2362
+ allowWrite: [...allowWrite, "~/.agentic-memory"]
2363
+ }
2364
+ }
2365
+ };
2366
+ await writeSettingsJson(root, updated);
2367
+ log.success(
2368
+ "Added ~/.agentic-memory to sandbox.filesystem.allowWrite (sandbox stays enabled)"
2369
+ );
1769
2370
  return true;
1770
2371
  }
1771
2372
  async function addEnvToClaudeignore(root) {
1772
2373
  const ignorePath = join7(root, ".claudeignore");
1773
2374
  let content;
1774
2375
  try {
1775
- content = await readFile5(ignorePath, "utf-8");
2376
+ content = await readFile6(ignorePath, "utf-8");
1776
2377
  } catch {
1777
2378
  return false;
1778
2379
  }
1779
2380
  const lines = content.split("\n").map((l) => l.trim());
1780
- if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*")) return false;
2381
+ if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*"))
2382
+ return false;
1781
2383
  await writeFile4(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
1782
2384
  log.success("Added .env to .claudeignore");
1783
2385
  return true;
@@ -1786,7 +2388,7 @@ async function addClaudeMdSection(root, heading, content, targetPath) {
1786
2388
  const claudeMdPath = targetPath ?? join7(root, "CLAUDE.md");
1787
2389
  let existing;
1788
2390
  try {
1789
- existing = await readFile5(claudeMdPath, "utf-8");
2391
+ existing = await readFile6(claudeMdPath, "utf-8");
1790
2392
  } catch {
1791
2393
  if (!targetPath) return false;
1792
2394
  await mkdir3(join7(root, ".claude"), { recursive: true });
@@ -1808,23 +2410,17 @@ ${content}
1808
2410
  }
1809
2411
  async function createBacklogMd(root) {
1810
2412
  const backlogPath = join7(root, "BACKLOG.md");
1811
- try {
1812
- await access2(backlogPath);
1813
- return false;
1814
- } catch {
1815
- }
2413
+ if (await fileExists(backlogPath)) return false;
1816
2414
  const name = root.split("/").pop() ?? "Project";
1817
2415
  await writeFile4(backlogPath, generateBacklogMd({ name, description: "" }));
1818
- log.success("Generated BACKLOG.md (WP template + P0\u2013P3 sections + changelog)");
2416
+ log.success(
2417
+ "Generated BACKLOG.md (WP template + P0\u2013P3 sections + changelog)"
2418
+ );
1819
2419
  return true;
1820
2420
  }
1821
2421
  async function createClaudeignore(root, detected) {
1822
2422
  const ignorePath = join7(root, ".claudeignore");
1823
- try {
1824
- await access2(ignorePath);
1825
- return false;
1826
- } catch {
1827
- }
2423
+ if (await fileExists(ignorePath)) return false;
1828
2424
  const content = generateClaudeignore(detected);
1829
2425
  await writeFile4(ignorePath, content);
1830
2426
  log.success("Generated .claudeignore with language-specific ignore patterns");
@@ -1837,11 +2433,7 @@ ${SKILL_AUTHORING_CONTENT}
1837
2433
  `;
1838
2434
  async function createStarterRules(root) {
1839
2435
  const rulesDir = join7(root, ".claude", "rules");
1840
- try {
1841
- await access2(rulesDir);
1842
- return false;
1843
- } catch {
1844
- }
2436
+ if (await fileExists(rulesDir)) return false;
1845
2437
  await mkdir3(rulesDir, { recursive: true });
1846
2438
  await writeFile4(
1847
2439
  join7(rulesDir, "conventions.md"),
@@ -1860,22 +2452,32 @@ async function addSkillAuthoringConventions(root) {
1860
2452
  const conventionsPath = join7(root, ".claude", "rules", "conventions.md");
1861
2453
  let content;
1862
2454
  try {
1863
- content = await readFile5(conventionsPath, "utf-8");
2455
+ content = await readFile6(conventionsPath, "utf-8");
1864
2456
  } catch {
1865
2457
  return false;
1866
2458
  }
1867
2459
  if (/^##\s+Skill\s+Authoring/im.test(content)) return false;
1868
- await writeFile4(conventionsPath, content.trimEnd() + "\n" + SKILL_AUTHORING_SECTION);
2460
+ await writeFile4(
2461
+ conventionsPath,
2462
+ content.trimEnd() + "\n" + SKILL_AUTHORING_SECTION
2463
+ );
1869
2464
  log.success("Added Skill Authoring section to .claude/rules/conventions.md");
1870
2465
  return true;
1871
2466
  }
1872
2467
  async function createEnhanceSkill(root) {
1873
2468
  const skillDir = join7(root, ".claude", "skills", "lp-enhance");
1874
2469
  const skillPath = join7(skillDir, "SKILL.md");
1875
- const globalPath = join7(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
2470
+ const globalPath = join7(
2471
+ homedir(),
2472
+ ".claude",
2473
+ "skills",
2474
+ "lp-enhance",
2475
+ "SKILL.md"
2476
+ );
1876
2477
  const legacyProject = join7(root, ".claude", "commands", "lp-enhance.md");
1877
2478
  const legacyGlobal = join7(homedir(), ".claude", "commands", "lp-enhance.md");
1878
- if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return false;
2479
+ if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal))
2480
+ return false;
1879
2481
  await mkdir3(skillDir, { recursive: true });
1880
2482
  await writeFile4(skillPath, generateEnhanceSkill());
1881
2483
  log.success("Generated /lp-enhance skill (.claude/skills/lp-enhance/)");
@@ -1883,7 +2485,13 @@ async function createEnhanceSkill(root) {
1883
2485
  }
1884
2486
  async function updateEnhanceSkill(root) {
1885
2487
  const projectPath = join7(root, ".claude", "skills", "lp-enhance", "SKILL.md");
1886
- const globalPath = join7(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
2488
+ const globalPath = join7(
2489
+ homedir(),
2490
+ ".claude",
2491
+ "skills",
2492
+ "lp-enhance",
2493
+ "SKILL.md"
2494
+ );
1887
2495
  const targetPath = await fileExists(projectPath) ? projectPath : await fileExists(globalPath) ? globalPath : null;
1888
2496
  if (!targetPath) return false;
1889
2497
  await writeFile4(targetPath, generateEnhanceSkill());
@@ -1997,7 +2605,7 @@ function renderDoctorReport(results, options) {
1997
2605
  async function readJsonFile(path) {
1998
2606
  let raw;
1999
2607
  try {
2000
- raw = await readFile6(path, "utf-8");
2608
+ raw = await readFile7(path, "utf-8");
2001
2609
  } catch (err) {
2002
2610
  const code = err.code;
2003
2611
  if (code === "ENOENT") return {};
@@ -2033,8 +2641,11 @@ export {
2033
2641
  SESSION_START_CONTENT,
2034
2642
  BACKLOG_CONTENT,
2035
2643
  STOP_AND_SWARM_CONTENT,
2644
+ STALE_SWARM_PHRASE,
2036
2645
  OFF_LIMITS_CONTENT,
2037
2646
  SKILL_AUTHORING_CONTENT,
2647
+ sprintReviewsContent,
2648
+ TESTING_DISCIPLINE_CONTENT,
2038
2649
  fileExists,
2039
2650
  readFileOrNull,
2040
2651
  detectProject,
@@ -2051,14 +2662,24 @@ export {
2051
2662
  addOrUpdateHook,
2052
2663
  jqField,
2053
2664
  hasEnvVarHookPattern,
2665
+ isJqAvailable,
2666
+ SPRINT_COMPLETE_NUDGE,
2667
+ WORKFLOW_CHECK_WRAPPER,
2668
+ SPRINT_OPEN_WRAPPER,
2669
+ SPRINT_SIZE_WRAPPER,
2670
+ SESSION_START_MATCHER,
2054
2671
  writeSprintHygieneScripts,
2055
2672
  writeWorkflowCheckScript,
2673
+ WORKFLOW_RULE_VERSION,
2056
2674
  generateWorkflowRule,
2057
2675
  generateHooksRule,
2676
+ VERIFICATION_RULE_VERSION,
2677
+ generateVerificationRule,
2678
+ generateReviewerAgent,
2058
2679
  applyFixes,
2059
2680
  log,
2060
2681
  printBanner,
2061
2682
  printScoreCard,
2062
2683
  renderDoctorReport
2063
2684
  };
2064
- //# sourceMappingURL=chunk-3OFOCOXM.js.map
2685
+ //# sourceMappingURL=chunk-QEPE3WVF.js.map