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.
- package/README.md +5 -3
- package/dist/{chunk-P2LGPNNY.js → chunk-DTCCQWOU.js} +2 -2
- package/dist/{chunk-3OFOCOXM.js → chunk-KPO4YURF.js} +379 -130
- package/dist/chunk-KPO4YURF.js.map +1 -0
- package/dist/{chunk-YYNJMM7V.js → chunk-PH5OWJ42.js} +2 -2
- package/dist/{chunk-CL7BAUHR.js → chunk-ZIG75MOB.js} +3 -3
- package/dist/{chunk-6DQD6KVN.js → chunk-ZLI4LI33.js} +2 -2
- package/dist/cli.js +674 -364
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +3 -3
- package/dist/{context-76QJSCRJ.js → context-JALSYDYO.js} +5 -5
- package/dist/{install-B5KISSFR.js → install-Q23F37GK.js} +8 -6
- package/dist/install-Q23F37GK.js.map +1 -0
- package/dist/{pull-4ZQSKFX7.js → pull-FYKNNLYC.js} +14 -10
- package/dist/pull-FYKNNLYC.js.map +1 -0
- package/dist/{push-C3QMIEUQ.js → push-QSBK7BQU.js} +7 -7
- package/dist/{require-deps-XUAKG226.js → require-deps-ZISUZLYD.js} +3 -3
- package/dist/{stats-ARO5UFFZ.js → stats-SV53G6PQ.js} +6 -6
- package/dist/sync-2LDM44U4.js +23 -0
- package/dist/sync-2LDM44U4.js.map +1 -0
- package/dist/{sync-clean-UHR2I27F.js → sync-clean-HQ523N7V.js} +3 -3
- package/dist/{sync-status-VZGZCZA3.js → sync-status-V6WTI32X.js} +7 -7
- package/dist/{tui-GGUFB6WP.js → tui-2UJ2OIHB.js} +4 -4
- package/package.json +1 -1
- package/scenarios/security/env-read-attempt.yaml +46 -0
- package/dist/chunk-3OFOCOXM.js.map +0 -1
- package/dist/install-B5KISSFR.js.map +0 -1
- package/dist/pull-4ZQSKFX7.js.map +0 -1
- /package/dist/{chunk-P2LGPNNY.js.map → chunk-DTCCQWOU.js.map} +0 -0
- /package/dist/{chunk-YYNJMM7V.js.map → chunk-PH5OWJ42.js.map} +0 -0
- /package/dist/{chunk-CL7BAUHR.js.map → chunk-ZIG75MOB.js.map} +0 -0
- /package/dist/{chunk-6DQD6KVN.js.map → chunk-ZLI4LI33.js.map} +0 -0
- /package/dist/{context-76QJSCRJ.js.map → context-JALSYDYO.js.map} +0 -0
- /package/dist/{push-C3QMIEUQ.js.map → push-QSBK7BQU.js.map} +0 -0
- /package/dist/{require-deps-XUAKG226.js.map → require-deps-ZISUZLYD.js.map} +0 -0
- /package/dist/{stats-ARO5UFFZ.js.map → stats-SV53G6PQ.js.map} +0 -0
- /package/dist/{sync-clean-UHR2I27F.js.map → sync-clean-HQ523N7V.js.map} +0 -0
- /package/dist/{sync-status-VZGZCZA3.js.map → sync-status-V6WTI32X.js.map} +0 -0
- /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
|
|
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
|
|
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 =
|
|
49
|
+
var STOP_AND_SWARM_CONTENT = 'Three failed iterations on the same problem = stop iterating alone.\n(An iteration = an attempted fix that did not change the failing symptom. Announce "Attempt N" when retrying so the count stays visible.)\nFirst, one systematic pass \u2014 it usually resolves the loop without the swarm:\nreproduce the failure, read the FULL error output, state one hypothesis about the root cause, and verify it BEFORE writing any fix.\nOnly if that pass fails, swarm: dispatch at least 3 parallel subagents via the Task tool \u2014 in a single message so they run concurrently \u2014 each investigating from a different angle:\n1. Root-cause debug agent\n2. Upstream library/docs research agent\n3. Alternative architecture agent\nHand each agent the exact repro command, the full error text, and the list of already-failed fixes \u2014 subagents start with empty context.\nWait for all agents to return, synthesize their findings, then act.\nFor re-planning after repeated failure, switch to plan mode instead of attempting again.';
|
|
50
|
+
var STALE_SWARM_PHRASE = "spin up at least 3 parallel agents via the Agent tool";
|
|
51
|
+
var SWARM_PHRASE_REPLACEMENT = "dispatch at least 3 parallel subagents via the Task tool (in a single message so they run concurrently)";
|
|
50
52
|
var OFF_LIMITS_CONTENT = "- Never hardcode secrets \u2014 use environment variables\n- Never write to `.env` files\n- Never expose internal error details in API responses";
|
|
51
53
|
var SKILL_AUTHORING_CONTENT = 'When creating Claude Code skills (.claude/skills/*/SKILL.md):\n\n- Keep SKILL.md under 500 lines \u2014 move reference material to supporting files in the same directory\n- Front-load description (first 250 chars shown in listings) with TRIGGER when / DO NOT TRIGGER when clauses\n- Add allowed-tools in frontmatter to restrict tool access (e.g. Read, Glob, Grep for read-only skills)\n- Add argument-hint in frontmatter showing the expected input format (use $ARGUMENTS or $0, $1 for dynamic input)\n- Set disable-model-invocation: true for skills with side effects (deploy, send messages)\n- Structure as phases: Research, Plan, Execute, Verify with "Done when:" success criteria per phase\n- Handle edge cases and preconditions before execution';
|
|
54
|
+
function sprintReviewsContent(testCommand, lintCommand, superpowers = false) {
|
|
55
|
+
const known = [testCommand, lintCommand].filter((c) => !!c).map((c) => `\`${c}\``).join(" and ");
|
|
56
|
+
const verifyLine = known ? `- Run ${known} \u2014 must pass before the sprint-ending commit` : "- Run the project's test and typecheck commands \u2014 they must pass before the sprint-ending commit";
|
|
57
|
+
return "When all tasks in the current sprint are complete, review before the closing commit:\n- Find the sprint base: `git log --grep 'chore(sprint-' -n 1 --format=%H`\n- Run /code-review on the diff from that base; fix all Critical and Important findings before committing\n- Run /security-review if the sprint touched auth, input handling, or dependencies\n" + verifyLine + "\n- If /code-review is unavailable, do a manual pass: dead code, debug logs, TODO hacks, convention violations, hardcoded values\n" + (superpowers ? "- superpowers detected: invoke superpowers:requesting-code-review for the independent pass (severity-gated)\n" : "- For an independent second pass, dispatch the code-reviewer agent (.claude/agents/code-reviewer.md) with the base/head SHAs\n") + "- Skip only if the sprint was trivial (docs or config-only changes)";
|
|
58
|
+
}
|
|
59
|
+
var TESTING_DISCIPLINE_CONTENT = "Hard-TDD surfaces \u2014 write the failing test BEFORE any implementation for:\n- A new module, command, or public API\n- Any bug fix (regression test from the minimal repro first)\n- Any algorithm or scoring change\nFlexible (tests land in the same commit, order free): docs, config, version bumps, log messages.\nIf unsure which bucket a change falls into, default to test-first.";
|
|
52
60
|
|
|
53
61
|
// src/lib/detect.ts
|
|
54
62
|
import { join, basename } from "path";
|
|
@@ -350,7 +358,7 @@ DerivedData/
|
|
|
350
358
|
}
|
|
351
359
|
|
|
352
360
|
// src/commands/init/generators/skill-enhance.ts
|
|
353
|
-
var ENHANCE_SKILL_VERSION =
|
|
361
|
+
var ENHANCE_SKILL_VERSION = 10;
|
|
354
362
|
function generateEnhanceSkill() {
|
|
355
363
|
return [
|
|
356
364
|
"---",
|
|
@@ -359,7 +367,7 @@ function generateEnhanceSkill() {
|
|
|
359
367
|
" AI-improve your CLAUDE.md based on codebase analysis. Fills in architecture, conventions, guardrails, and suggests hooks and MCP servers.",
|
|
360
368
|
' TRIGGER when: user runs /lp-enhance, asks to "improve CLAUDE.md", "fill in architecture", or after major refactors.',
|
|
361
369
|
" DO NOT TRIGGER when: user is editing CLAUDE.md manually, doing normal coding, or running doctor/eval.",
|
|
362
|
-
"allowed-tools: Read, Glob, Grep, Edit, Write",
|
|
370
|
+
"allowed-tools: Read, Glob, Grep, Edit, Write, Bash(claude-launchpad doctor:*)",
|
|
363
371
|
"argument-hint: (no arguments needed)",
|
|
364
372
|
"---",
|
|
365
373
|
"",
|
|
@@ -467,21 +475,13 @@ function generateEnhanceSkill() {
|
|
|
467
475
|
"",
|
|
468
476
|
"## Skill Authoring",
|
|
469
477
|
"",
|
|
470
|
-
"
|
|
471
|
-
"",
|
|
472
|
-
"- Keep SKILL.md under 500 lines - move reference material to supporting files in the same directory",
|
|
473
|
-
"- Front-load description (first 250 chars shown in listings) with TRIGGER when / DO NOT TRIGGER when clauses",
|
|
474
|
-
"- Add allowed-tools in frontmatter to restrict tool access (e.g. Read, Glob, Grep for read-only skills)",
|
|
475
|
-
"- Add argument-hint in frontmatter showing the expected input format (use $ARGUMENTS or $0, $1 for dynamic input)",
|
|
476
|
-
"- Set disable-model-invocation: true for skills with side effects (deploy, send messages)",
|
|
477
|
-
'- Structure as phases: Research, Plan, Execute, Verify with "Done when:" success criteria per phase',
|
|
478
|
-
"- Handle edge cases and preconditions before execution",
|
|
478
|
+
...SKILL_AUTHORING_CONTENT.split("\n"),
|
|
479
479
|
"",
|
|
480
480
|
"## Hook review",
|
|
481
481
|
"",
|
|
482
482
|
"Review .claude/settings.json hooks:",
|
|
483
483
|
"- If you see project-specific patterns that deserve hooks, suggest them",
|
|
484
|
-
"- If
|
|
484
|
+
"- If the SessionStart matcher misses compact/clear, suggest widening it to startup|resume|compact|clear (PostCompact stdout is never injected into context - context re-injection belongs on SessionStart)",
|
|
485
485
|
"- If no SessionStart hook exists, suggest one that injects TASKS.md",
|
|
486
486
|
"- DO NOT modify settings.json directly. Print exact JSON to add.",
|
|
487
487
|
"",
|
|
@@ -683,7 +683,7 @@ ${LP_STUB_CLOSE}`;
|
|
|
683
683
|
}
|
|
684
684
|
|
|
685
685
|
// src/commands/doctor/fixer-sprint.ts
|
|
686
|
-
import { writeFile as writeFile2 } from "fs/promises";
|
|
686
|
+
import { writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
|
|
687
687
|
import { join as join3 } from "path";
|
|
688
688
|
|
|
689
689
|
// src/lib/hook-builder.ts
|
|
@@ -696,6 +696,22 @@ function addOrUpdateHook(existingHooks, options) {
|
|
|
696
696
|
if (alreadyHas) {
|
|
697
697
|
return { hooks: existingHooks ?? {}, added: false };
|
|
698
698
|
}
|
|
699
|
+
const matcher = options.entry.matcher ?? "";
|
|
700
|
+
const sameMatcherIdx = hookList.findIndex((g) => String(g.matcher ?? "") === matcher);
|
|
701
|
+
if (sameMatcherIdx >= 0) {
|
|
702
|
+
const group = hookList[sameMatcherIdx];
|
|
703
|
+
const existing = group.hooks ?? [];
|
|
704
|
+
const incoming = options.entry.hooks;
|
|
705
|
+
const merged = {
|
|
706
|
+
...group,
|
|
707
|
+
hooks: options.prepend ? [...incoming, ...existing] : [...existing, ...incoming]
|
|
708
|
+
};
|
|
709
|
+
const updated2 = hookList.map((g, i) => i === sameMatcherIdx ? merged : g);
|
|
710
|
+
return {
|
|
711
|
+
hooks: { ...existingHooks ?? {}, [options.event]: updated2 },
|
|
712
|
+
added: true
|
|
713
|
+
};
|
|
714
|
+
}
|
|
699
715
|
const newEntry = options.entry;
|
|
700
716
|
const updated = options.prepend ? [newEntry, ...hookList] : [...hookList, newEntry];
|
|
701
717
|
return {
|
|
@@ -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
|
|
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
|
-
|
|
744
|
-
|
|
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
|
|
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
|
-
#
|
|
768
|
-
#
|
|
769
|
-
#
|
|
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
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
git
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
#
|
|
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.
|
|
809
|
-
# 4.
|
|
837
|
+
# 3. '## Current Sprint' has more than 15 items (hard split trigger).
|
|
838
|
+
# 4. '## Session Log' has more than 3 entries.
|
|
839
|
+
# 5. A pulled WP's "Depends on:" dependency still sits in a P-section.
|
|
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
|
-
|
|
848
|
+
warnings=""
|
|
849
|
+
warn() { warnings="\${warnings}$*
|
|
850
|
+
"; }
|
|
818
851
|
|
|
819
|
-
#
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
|
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:]]*- \\[[
|
|
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:
|
|
893
|
-
hooks: [{ type: "command", command:
|
|
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, "
|
|
960
|
+
return addHookToSettings(root, "PostToolUse", "sprint-open-check.sh", {
|
|
899
961
|
matcher: "Bash",
|
|
900
|
-
hooks: [{ type: "command", command:
|
|
901
|
-
}, "Added sprint-open-check hook (warns on
|
|
962
|
+
hooks: [{ type: "command", command: SPRINT_OPEN_WRAPPER }]
|
|
963
|
+
}, "Added sprint-open-check hook (warns on WP pulls without BACKLOG cleanup)");
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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\`, \`
|
|
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
|
|
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
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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: "
|
|
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
|
|
2002
|
+
async function addSandboxMemoryWriteGrant(root) {
|
|
1763
2003
|
const settings = await readSettingsJson(root);
|
|
1764
2004
|
if (settings === null) return false;
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
2313
|
+
//# sourceMappingURL=chunk-KPO4YURF.js.map
|