claude-launchpad 1.10.1 → 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.
Files changed (39) hide show
  1. package/README.md +5 -3
  2. package/dist/{chunk-P2LGPNNY.js → chunk-DTCCQWOU.js} +2 -2
  3. package/dist/{chunk-3OFOCOXM.js → chunk-KPO4YURF.js} +379 -130
  4. package/dist/chunk-KPO4YURF.js.map +1 -0
  5. package/dist/{chunk-YYNJMM7V.js → chunk-PH5OWJ42.js} +2 -2
  6. package/dist/{chunk-CL7BAUHR.js → chunk-ZIG75MOB.js} +3 -3
  7. package/dist/{chunk-6DQD6KVN.js → chunk-ZLI4LI33.js} +2 -2
  8. package/dist/cli.js +674 -364
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/memory/server.js +3 -3
  11. package/dist/{context-76QJSCRJ.js → context-JALSYDYO.js} +5 -5
  12. package/dist/{install-B5KISSFR.js → install-Q23F37GK.js} +8 -6
  13. package/dist/install-Q23F37GK.js.map +1 -0
  14. package/dist/{pull-4ZQSKFX7.js → pull-FYKNNLYC.js} +14 -10
  15. package/dist/pull-FYKNNLYC.js.map +1 -0
  16. package/dist/{push-C3QMIEUQ.js → push-QSBK7BQU.js} +7 -7
  17. package/dist/{require-deps-XUAKG226.js → require-deps-ZISUZLYD.js} +3 -3
  18. package/dist/{stats-ARO5UFFZ.js → stats-SV53G6PQ.js} +6 -6
  19. package/dist/sync-2LDM44U4.js +23 -0
  20. package/dist/sync-2LDM44U4.js.map +1 -0
  21. package/dist/{sync-clean-UHR2I27F.js → sync-clean-HQ523N7V.js} +3 -3
  22. package/dist/{sync-status-VZGZCZA3.js → sync-status-V6WTI32X.js} +7 -7
  23. package/dist/{tui-GGUFB6WP.js → tui-2UJ2OIHB.js} +4 -4
  24. package/package.json +1 -1
  25. package/scenarios/security/env-read-attempt.yaml +46 -0
  26. package/dist/chunk-3OFOCOXM.js.map +0 -1
  27. package/dist/install-B5KISSFR.js.map +0 -1
  28. package/dist/pull-4ZQSKFX7.js.map +0 -1
  29. /package/dist/{chunk-P2LGPNNY.js.map → chunk-DTCCQWOU.js.map} +0 -0
  30. /package/dist/{chunk-YYNJMM7V.js.map → chunk-PH5OWJ42.js.map} +0 -0
  31. /package/dist/{chunk-CL7BAUHR.js.map → chunk-ZIG75MOB.js.map} +0 -0
  32. /package/dist/{chunk-6DQD6KVN.js.map → chunk-ZLI4LI33.js.map} +0 -0
  33. /package/dist/{context-76QJSCRJ.js.map → context-JALSYDYO.js.map} +0 -0
  34. /package/dist/{push-C3QMIEUQ.js.map → push-QSBK7BQU.js.map} +0 -0
  35. /package/dist/{require-deps-XUAKG226.js.map → require-deps-ZISUZLYD.js.map} +0 -0
  36. /package/dist/{stats-ARO5UFFZ.js.map → stats-SV53G6PQ.js.map} +0 -0
  37. /package/dist/{sync-clean-UHR2I27F.js.map → sync-clean-HQ523N7V.js.map} +0 -0
  38. /package/dist/{sync-status-VZGZCZA3.js.map → sync-status-V6WTI32X.js.map} +0 -0
  39. /package/dist/{tui-GGUFB6WP.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 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,52 @@ Output goes into the session's context. Always exit 0; SessionStart blocking is
1253
1372
  `;
1254
1373
  }
1255
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
+
1256
1421
  // src/commands/doctor/fixer-quality.ts
1257
1422
  async function createWorkflowRule(root) {
1258
1423
  const rulesDir = join4(root, ".claude", "rules");
@@ -1263,6 +1428,32 @@ async function createWorkflowRule(root) {
1263
1428
  log.success("Created .claude/rules/workflow.md (path-scoped BACKLOG/TASKS workflow rules)");
1264
1429
  return true;
1265
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
+ }
1266
1457
  async function createHooksRule(root) {
1267
1458
  const rulesDir = join4(root, ".claude", "rules");
1268
1459
  const hooksPath = join4(rulesDir, "hooks.md");
@@ -1295,7 +1486,7 @@ async function collapseMemoryHeadings(root) {
1295
1486
  const claudeMdPath = join4(root, "CLAUDE.md");
1296
1487
  let content;
1297
1488
  try {
1298
- content = await readFile2(claudeMdPath, "utf-8");
1489
+ content = await readFile3(claudeMdPath, "utf-8");
1299
1490
  } catch {
1300
1491
  return false;
1301
1492
  }
@@ -1366,21 +1557,63 @@ async function addForcePushProtection(root) {
1366
1557
  }]
1367
1558
  }, "Added force-push protection hook (PreToolUse \u2192 Bash)");
1368
1559
  }
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)");
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 };
1374
1607
  }
1375
1608
  async function addSessionStartHook(root) {
1376
1609
  return addHookToSettings(root, "SessionStart", "TASKS.md", {
1377
- matcher: "startup|resume",
1610
+ matcher: SESSION_START_MATCHER,
1378
1611
  hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }]
1379
- }, "Added SessionStart hook (injects TASKS.md at startup)");
1612
+ }, "Added SessionStart hook (injects TASKS.md at startup/resume/compact/clear)");
1380
1613
  }
1381
1614
 
1382
1615
  // src/commands/doctor/fixer-hook-input.ts
1383
- import { readFile as readFile3 } from "fs/promises";
1616
+ import { readFile as readFile4 } from "fs/promises";
1384
1617
  import { join as join5 } from "path";
1385
1618
  function rewriteEnvVarHookCommand(cmd) {
1386
1619
  if (!hasEnvVarHookPattern(cmd)) return null;
@@ -1394,7 +1627,7 @@ function rewriteEnvVarHookCommand(cmd) {
1394
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`;
1395
1628
  }
1396
1629
  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`;
1630
+ return SPRINT_COMPLETE_NUDGE;
1398
1631
  }
1399
1632
  const formatMatch = cmd.match(/ext=\$\{TOOL_INPUT_FILE_PATH##\*\.\};\s*\((.+?)\)\s*&&\s*(.+?)\s*"\$TOOL_INPUT_FILE_PATH"/);
1400
1633
  if (formatMatch) {
@@ -1428,7 +1661,7 @@ function rewriteSettingsHooks(settings) {
1428
1661
  async function rewriteWrapperScript(scriptPath, rewriter) {
1429
1662
  let content;
1430
1663
  try {
1431
- content = await readFile3(scriptPath, "utf-8");
1664
+ content = await readFile4(scriptPath, "utf-8");
1432
1665
  } catch {
1433
1666
  return false;
1434
1667
  }
@@ -1461,7 +1694,7 @@ async function rewriteEnvVarHooks(root) {
1461
1694
  }
1462
1695
 
1463
1696
  // src/commands/doctor/fixer-memory.ts
1464
- import { readFile as readFile4 } from "fs/promises";
1697
+ import { readFile as readFile5 } from "fs/promises";
1465
1698
  import { join as join6 } from "path";
1466
1699
  async function addPlacementHook(root, placement, event, dedupKeyword, entry, prepend, successMsg) {
1467
1700
  const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
@@ -1618,7 +1851,7 @@ async function addAllowedMcpServers(root, placement) {
1618
1851
  }
1619
1852
  const mcpJsonPath = join6(root, ".mcp.json");
1620
1853
  try {
1621
- const mcpJson = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
1854
+ const mcpJson = JSON.parse(await readFile5(mcpJsonPath, "utf-8"));
1622
1855
  const mcpServers = mcpJson.mcpServers;
1623
1856
  if (mcpServers && typeof mcpServers === "object") {
1624
1857
  for (const name of Object.keys(mcpServers)) serverNames.add(name);
@@ -1679,20 +1912,27 @@ var FIX_TABLE = [
1679
1912
  } },
1680
1913
  { analyzer: "Quality", match: "Session Start", fix: (root) => addClaudeMdSection(root, "## Session Start", wrapStub(SESSION_START_CONTENT)) },
1681
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) },
1682
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) },
1683
1919
  { analyzer: "Quality", match: "Duplicate ## Memory", fix: (root) => collapseMemoryHeadings(root) },
1684
1920
  { analyzer: "Rules", match: "No BACKLOG.md", fix: (root) => createBacklogMd(root) },
1685
1921
  { analyzer: "Rules", match: "No .claudeignore", fix: (root, detected) => createClaudeignore(root, detected) },
1686
1922
  { analyzer: "Rules", match: "No .claude/rules/workflow.md", fix: (root) => createWorkflowRule(root) },
1687
1923
  { analyzer: "Rules", match: "No .claude/rules/hooks.md", fix: (root) => createHooksRule(root) },
1688
1924
  { analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
1689
- { analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(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) },
1690
1927
  { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
1691
1928
  { analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
1692
1929
  { analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
1693
- { analyzer: "Permissions", match: "Filesystem sandbox enabled", fix: (root) => removeSandboxSettings(root) },
1930
+ { analyzer: "Permissions", match: "Sandbox lacks a write grant", fix: (root) => addSandboxMemoryWriteGrant(root) },
1694
1931
  { analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
1695
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) },
1696
1936
  { analyzer: "Hooks", match: "sprint-size-check", fix: (root) => addSprintSizeHook(root) },
1697
1937
  { analyzer: "Hooks", match: "sprint-open-check", fix: (root) => addSprintOpenHook(root) },
1698
1938
  { analyzer: "Hooks", match: "sprint-complete nudge", fix: (root) => addSprintCompleteNudge(root) },
@@ -1759,20 +1999,30 @@ async function addBypassDisable(root) {
1759
1999
  log.success("Added disableBypassPermissionsMode: disable");
1760
2000
  return true;
1761
2001
  }
1762
- async function removeSandboxSettings(root) {
2002
+ async function addSandboxMemoryWriteGrant(root) {
1763
2003
  const settings = await readSettingsJson(root);
1764
2004
  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");
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)");
1769
2019
  return true;
1770
2020
  }
1771
2021
  async function addEnvToClaudeignore(root) {
1772
2022
  const ignorePath = join7(root, ".claudeignore");
1773
2023
  let content;
1774
2024
  try {
1775
- content = await readFile5(ignorePath, "utf-8");
2025
+ content = await readFile6(ignorePath, "utf-8");
1776
2026
  } catch {
1777
2027
  return false;
1778
2028
  }
@@ -1786,7 +2036,7 @@ async function addClaudeMdSection(root, heading, content, targetPath) {
1786
2036
  const claudeMdPath = targetPath ?? join7(root, "CLAUDE.md");
1787
2037
  let existing;
1788
2038
  try {
1789
- existing = await readFile5(claudeMdPath, "utf-8");
2039
+ existing = await readFile6(claudeMdPath, "utf-8");
1790
2040
  } catch {
1791
2041
  if (!targetPath) return false;
1792
2042
  await mkdir3(join7(root, ".claude"), { recursive: true });
@@ -1808,11 +2058,7 @@ ${content}
1808
2058
  }
1809
2059
  async function createBacklogMd(root) {
1810
2060
  const backlogPath = join7(root, "BACKLOG.md");
1811
- try {
1812
- await access2(backlogPath);
1813
- return false;
1814
- } catch {
1815
- }
2061
+ if (await fileExists(backlogPath)) return false;
1816
2062
  const name = root.split("/").pop() ?? "Project";
1817
2063
  await writeFile4(backlogPath, generateBacklogMd({ name, description: "" }));
1818
2064
  log.success("Generated BACKLOG.md (WP template + P0\u2013P3 sections + changelog)");
@@ -1820,11 +2066,7 @@ async function createBacklogMd(root) {
1820
2066
  }
1821
2067
  async function createClaudeignore(root, detected) {
1822
2068
  const ignorePath = join7(root, ".claudeignore");
1823
- try {
1824
- await access2(ignorePath);
1825
- return false;
1826
- } catch {
1827
- }
2069
+ if (await fileExists(ignorePath)) return false;
1828
2070
  const content = generateClaudeignore(detected);
1829
2071
  await writeFile4(ignorePath, content);
1830
2072
  log.success("Generated .claudeignore with language-specific ignore patterns");
@@ -1837,11 +2079,7 @@ ${SKILL_AUTHORING_CONTENT}
1837
2079
  `;
1838
2080
  async function createStarterRules(root) {
1839
2081
  const rulesDir = join7(root, ".claude", "rules");
1840
- try {
1841
- await access2(rulesDir);
1842
- return false;
1843
- } catch {
1844
- }
2082
+ if (await fileExists(rulesDir)) return false;
1845
2083
  await mkdir3(rulesDir, { recursive: true });
1846
2084
  await writeFile4(
1847
2085
  join7(rulesDir, "conventions.md"),
@@ -1860,7 +2098,7 @@ async function addSkillAuthoringConventions(root) {
1860
2098
  const conventionsPath = join7(root, ".claude", "rules", "conventions.md");
1861
2099
  let content;
1862
2100
  try {
1863
- content = await readFile5(conventionsPath, "utf-8");
2101
+ content = await readFile6(conventionsPath, "utf-8");
1864
2102
  } catch {
1865
2103
  return false;
1866
2104
  }
@@ -1997,7 +2235,7 @@ function renderDoctorReport(results, options) {
1997
2235
  async function readJsonFile(path) {
1998
2236
  let raw;
1999
2237
  try {
2000
- raw = await readFile6(path, "utf-8");
2238
+ raw = await readFile7(path, "utf-8");
2001
2239
  } catch (err) {
2002
2240
  const code = err.code;
2003
2241
  if (code === "ENOENT") return {};
@@ -2033,8 +2271,11 @@ export {
2033
2271
  SESSION_START_CONTENT,
2034
2272
  BACKLOG_CONTENT,
2035
2273
  STOP_AND_SWARM_CONTENT,
2274
+ STALE_SWARM_PHRASE,
2036
2275
  OFF_LIMITS_CONTENT,
2037
2276
  SKILL_AUTHORING_CONTENT,
2277
+ sprintReviewsContent,
2278
+ TESTING_DISCIPLINE_CONTENT,
2038
2279
  fileExists,
2039
2280
  readFileOrNull,
2040
2281
  detectProject,
@@ -2051,14 +2292,22 @@ export {
2051
2292
  addOrUpdateHook,
2052
2293
  jqField,
2053
2294
  hasEnvVarHookPattern,
2295
+ isJqAvailable,
2296
+ SPRINT_COMPLETE_NUDGE,
2297
+ WORKFLOW_CHECK_WRAPPER,
2298
+ SPRINT_OPEN_WRAPPER,
2299
+ SPRINT_SIZE_WRAPPER,
2300
+ SESSION_START_MATCHER,
2054
2301
  writeSprintHygieneScripts,
2055
2302
  writeWorkflowCheckScript,
2303
+ WORKFLOW_RULE_VERSION,
2056
2304
  generateWorkflowRule,
2057
2305
  generateHooksRule,
2306
+ generateReviewerAgent,
2058
2307
  applyFixes,
2059
2308
  log,
2060
2309
  printBanner,
2061
2310
  printScoreCard,
2062
2311
  renderDoctorReport
2063
2312
  };
2064
- //# sourceMappingURL=chunk-3OFOCOXM.js.map
2313
+ //# sourceMappingURL=chunk-KPO4YURF.js.map