claude-launchpad 1.10.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/{chunk-P2LGPNNY.js → chunk-D6RAMWH6.js} +3 -3
- package/dist/{chunk-P2LGPNNY.js.map → chunk-D6RAMWH6.js.map} +1 -1
- package/dist/chunk-HSGSTSVN.js +212 -0
- package/dist/chunk-HSGSTSVN.js.map +1 -0
- package/dist/{chunk-DXDOVWOA.js → chunk-IDH3M4EI.js} +20 -232
- package/dist/chunk-IDH3M4EI.js.map +1 -0
- package/dist/{chunk-YZ53W47Z.js → chunk-IIWOSSL4.js} +2 -1
- package/dist/chunk-IIWOSSL4.js.map +1 -0
- package/dist/{chunk-6DQD6KVN.js → chunk-M6GVFN67.js} +2 -2
- package/dist/{chunk-F6SLV2FR.js → chunk-N2SCF7DB.js} +5 -6
- package/dist/chunk-N2SCF7DB.js.map +1 -0
- package/dist/chunk-OPORVSAF.js +225 -0
- package/dist/chunk-OPORVSAF.js.map +1 -0
- package/dist/{chunk-3OFOCOXM.js → chunk-QEPE3WVF.js} +821 -200
- package/dist/chunk-QEPE3WVF.js.map +1 -0
- package/dist/chunk-UN43OVRW.js +184 -0
- package/dist/chunk-UN43OVRW.js.map +1 -0
- package/dist/{chunk-YYNJMM7V.js → chunk-VF7U63AI.js} +9 -5
- package/dist/{chunk-YYNJMM7V.js.map → chunk-VF7U63AI.js.map} +1 -1
- package/dist/cli.js +948 -418
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +48 -14
- package/dist/commands/memory/server.js.map +1 -1
- package/dist/{context-76QJSCRJ.js → context-YIPO4LBS.js} +58 -55
- package/dist/context-YIPO4LBS.js.map +1 -0
- package/dist/doctor-DCHFJ3XV.js +108 -0
- package/dist/doctor-DCHFJ3XV.js.map +1 -0
- package/dist/{install-B5KISSFR.js → install-D32PV7SF.js} +15 -11
- package/dist/install-D32PV7SF.js.map +1 -0
- package/dist/{pull-4ZQSKFX7.js → pull-YUQTDRUP.js} +22 -20
- package/dist/pull-YUQTDRUP.js.map +1 -0
- package/dist/{push-C3QMIEUQ.js → push-U4IVTFOU.js} +13 -12
- package/dist/push-U4IVTFOU.js.map +1 -0
- package/dist/{require-deps-XUAKG226.js → require-deps-WDMHDGBN.js} +3 -3
- package/dist/{stats-ARO5UFFZ.js → stats-77BYLKYP.js} +9 -7
- package/dist/{stats-ARO5UFFZ.js.map → stats-77BYLKYP.js.map} +1 -1
- package/dist/sync-DTCWK2CY.js +23 -0
- package/dist/sync-DTCWK2CY.js.map +1 -0
- package/dist/{sync-clean-UHR2I27F.js → sync-clean-ACZCB3NZ.js} +4 -5
- package/dist/sync-clean-ACZCB3NZ.js.map +1 -0
- package/dist/{sync-status-VZGZCZA3.js → sync-status-2V2RCYZO.js} +13 -12
- package/dist/sync-status-2V2RCYZO.js.map +1 -0
- package/dist/{tui-GGUFB6WP.js → tui-HVJNLRY2.js} +11 -7
- package/dist/{tui-GGUFB6WP.js.map → tui-HVJNLRY2.js.map} +1 -1
- package/package.json +27 -7
- package/scenarios/security/env-read-attempt.yaml +46 -0
- package/scenarios/workflow/premature-victory.yaml +44 -0
- package/dist/chunk-3OFOCOXM.js.map +0 -1
- package/dist/chunk-CL7BAUHR.js +0 -366
- package/dist/chunk-CL7BAUHR.js.map +0 -1
- package/dist/chunk-DXDOVWOA.js.map +0 -1
- package/dist/chunk-F6SLV2FR.js.map +0 -1
- package/dist/chunk-YZ53W47Z.js.map +0 -1
- package/dist/context-76QJSCRJ.js.map +0 -1
- package/dist/install-B5KISSFR.js.map +0 -1
- package/dist/pull-4ZQSKFX7.js.map +0 -1
- package/dist/push-C3QMIEUQ.js.map +0 -1
- package/dist/sync-clean-UHR2I27F.js.map +0 -1
- package/dist/sync-status-VZGZCZA3.js.map +0 -1
- /package/dist/{chunk-6DQD6KVN.js.map → chunk-M6GVFN67.js.map} +0 -0
- /package/dist/{require-deps-XUAKG226.js.map → require-deps-WDMHDGBN.js.map} +0 -0
|
@@ -32,23 +32,31 @@ async function readJsonOrNull(path) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/lib/settings.ts
|
|
35
|
-
import { readFile as
|
|
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,89 @@ Output goes into the session's context. Always exit 0; SessionStart blocking is
|
|
|
1253
1372
|
`;
|
|
1254
1373
|
}
|
|
1255
1374
|
|
|
1375
|
+
// src/commands/init/generators/verification-rule.ts
|
|
1376
|
+
var VERIFICATION_RULE_VERSION = 1;
|
|
1377
|
+
function generateVerificationRule() {
|
|
1378
|
+
return `# Verification Rules
|
|
1379
|
+
|
|
1380
|
+
<!-- lp-verification-version: ${VERIFICATION_RULE_VERSION} -->
|
|
1381
|
+
|
|
1382
|
+
These rules govern every claim of "done", "fixed", "passing", or "works". A claim without evidence is a defect, no matter how good the code is.
|
|
1383
|
+
|
|
1384
|
+
## Evidence before assertion
|
|
1385
|
+
|
|
1386
|
+
- Never claim done/fixed/passing without having run the thing this session and quoting the output. The claim and its evidence travel together: "Done \u2014 14/14 tests pass (output below)", never a bare "done".
|
|
1387
|
+
- Verify the behavior, not the build. Compiling is not working; typechecking is not working. Exercise the changed flow end-to-end: run the command, hit the endpoint, click the path.
|
|
1388
|
+
- For bug fixes: reproduce the failure FIRST and capture the exact error. Then fix, then re-run the original reproduction AND the surrounding tests. If you cannot explain the root cause, you have hidden the bug, not fixed it.
|
|
1389
|
+
- Test the failure case, not just the happy path: feed the change the input that used to break it and confirm it now lands safely.
|
|
1390
|
+
|
|
1391
|
+
## Label your claims
|
|
1392
|
+
|
|
1393
|
+
Sort every load-bearing statement into one of three buckets, and say which out loud:
|
|
1394
|
+
|
|
1395
|
+
- **verified** \u2014 you read it or ran it this session
|
|
1396
|
+
- **inferred** \u2014 follows logically from verified facts; state the chain
|
|
1397
|
+
- **assumed** \u2014 plausible from prior knowledge; check it before building on it
|
|
1398
|
+
|
|
1399
|
+
Never let an assumption silently graduate into a fact. APIs, config keys, CLI flags, and package names cited from memory are the #1 hallucination vector \u2014 open the real source or current docs before citing them.
|
|
1400
|
+
|
|
1401
|
+
## When you can't verify
|
|
1402
|
+
|
|
1403
|
+
- If a step cannot be run (missing env, no credentials, no test data), the result is **done-with-gaps** \u2014 name each gap explicitly. Never round done-with-gaps up to done.
|
|
1404
|
+
- Report failure plainly. Failing tests, skipped steps, and partial work get named with the output shown. "2 failures remain, here's the state" preserves trust; a false "all green" destroys it.
|
|
1405
|
+
|
|
1406
|
+
## End-of-turn check
|
|
1407
|
+
|
|
1408
|
+
Before ending a turn, re-read your last paragraph. If it is a promise ("I'll\u2026", "next I would\u2026"), a plan, or a question you could answer yourself \u2014 do that work now, then end. A turn ends on delivered results, not intentions.
|
|
1409
|
+
`;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/commands/init/generators/agent-reviewer.ts
|
|
1413
|
+
var REVIEWER_AGENT_VERSION = 1;
|
|
1414
|
+
function generateReviewerAgent() {
|
|
1415
|
+
return `---
|
|
1416
|
+
name: code-reviewer
|
|
1417
|
+
description: Independent, fresh-context review of a diff. Use PROACTIVELY before sprint-ending commits. Pass base and head SHAs (find the base with \`git log --grep 'chore(sprint-' -n 1 --format=%H\`).
|
|
1418
|
+
tools: Read, Glob, Grep, Bash
|
|
1419
|
+
---
|
|
1420
|
+
|
|
1421
|
+
<!-- lp-reviewer-version: ${REVIEWER_AGENT_VERSION} -->
|
|
1422
|
+
|
|
1423
|
+
You are an independent senior code reviewer. You did NOT write this code \u2014
|
|
1424
|
+
review it with fresh eyes and no attachment to the choices made.
|
|
1425
|
+
|
|
1426
|
+
## Input
|
|
1427
|
+
|
|
1428
|
+
Base and head SHAs (or a branch/range). If none given, review the working
|
|
1429
|
+
tree against HEAD.
|
|
1430
|
+
|
|
1431
|
+
## Process
|
|
1432
|
+
|
|
1433
|
+
1. \`git diff --stat <base>..<head>\` for the shape, then read the full diff.
|
|
1434
|
+
2. Read surrounding source for anything the diff touches \u2014 a hunk that looks
|
|
1435
|
+
fine in isolation can break an invariant defined two functions up.
|
|
1436
|
+
3. For each suspected bug, verify against the actual code before reporting \u2014
|
|
1437
|
+
no findings from pattern-matching alone.
|
|
1438
|
+
4. Check: correctness, security (injection, secrets, permissions), error
|
|
1439
|
+
handling, convention violations (see CLAUDE.md and .claude/rules/), dead
|
|
1440
|
+
code, debug artifacts, missing or weakened tests.
|
|
1441
|
+
|
|
1442
|
+
## Output (exactly these sections)
|
|
1443
|
+
|
|
1444
|
+
- **Strengths** \u2014 what is genuinely good; be brief.
|
|
1445
|
+
- **Critical** \u2014 bugs, security holes, data loss. Must be fixed before commit.
|
|
1446
|
+
- **Important** \u2014 incorrect edge cases, convention breaks, silent failures.
|
|
1447
|
+
Must be fixed before commit.
|
|
1448
|
+
- **Minor** \u2014 polish; may be deferred to the backlog.
|
|
1449
|
+
- **Assessment** \u2014 2-3 sentences: ship, fix-then-ship, or rethink.
|
|
1450
|
+
|
|
1451
|
+
For every Critical/Important finding give file:line and a concrete failure
|
|
1452
|
+
scenario (inputs/state \u2192 wrong behavior). Do not pad: an empty Critical
|
|
1453
|
+
section is a valid, good result. Never soften a finding because the fix is
|
|
1454
|
+
inconvenient.
|
|
1455
|
+
`;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1256
1458
|
// src/commands/doctor/fixer-quality.ts
|
|
1257
1459
|
async function createWorkflowRule(root) {
|
|
1258
1460
|
const rulesDir = join4(root, ".claude", "rules");
|
|
@@ -1260,7 +1462,42 @@ async function createWorkflowRule(root) {
|
|
|
1260
1462
|
if (await fileExists(workflowPath)) return false;
|
|
1261
1463
|
await mkdir2(rulesDir, { recursive: true });
|
|
1262
1464
|
await writeFile3(workflowPath, generateWorkflowRule());
|
|
1263
|
-
log.success(
|
|
1465
|
+
log.success(
|
|
1466
|
+
"Created .claude/rules/workflow.md (path-scoped BACKLOG/TASKS workflow rules)"
|
|
1467
|
+
);
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
async function createReviewerAgent(root) {
|
|
1471
|
+
const agentsDir = join4(root, ".claude", "agents");
|
|
1472
|
+
const agentPath = join4(agentsDir, "code-reviewer.md");
|
|
1473
|
+
if (await fileExists(agentPath)) return false;
|
|
1474
|
+
await mkdir2(agentsDir, { recursive: true });
|
|
1475
|
+
await writeFile3(agentPath, generateReviewerAgent());
|
|
1476
|
+
log.success(
|
|
1477
|
+
"Created .claude/agents/code-reviewer.md (fresh-context independent reviewer)"
|
|
1478
|
+
);
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
async function updateWorkflowRule(root) {
|
|
1482
|
+
const workflowPath = join4(root, ".claude", "rules", "workflow.md");
|
|
1483
|
+
const content = await readFile3(workflowPath, "utf-8").catch(() => null);
|
|
1484
|
+
if (content === null) return false;
|
|
1485
|
+
if (!/<!-- lp-workflow-version: \d+ -->/.test(content)) return false;
|
|
1486
|
+
await writeFile3(workflowPath, generateWorkflowRule());
|
|
1487
|
+
log.success("Updated .claude/rules/workflow.md to the latest version");
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
async function fixStaleSwarmPhrase(root) {
|
|
1491
|
+
const claudeMdPath = join4(root, "CLAUDE.md");
|
|
1492
|
+
const content = await readFile3(claudeMdPath, "utf-8").catch(() => null);
|
|
1493
|
+
if (content === null || !content.includes(STALE_SWARM_PHRASE)) return false;
|
|
1494
|
+
await writeFile3(
|
|
1495
|
+
claudeMdPath,
|
|
1496
|
+
content.replaceAll(STALE_SWARM_PHRASE, SWARM_PHRASE_REPLACEMENT)
|
|
1497
|
+
);
|
|
1498
|
+
log.success(
|
|
1499
|
+
"Modernized Stop-and-Swarm wording (Agent tool \u2192 Task tool subagents)"
|
|
1500
|
+
);
|
|
1264
1501
|
return true;
|
|
1265
1502
|
}
|
|
1266
1503
|
async function createHooksRule(root) {
|
|
@@ -1269,7 +1506,29 @@ async function createHooksRule(root) {
|
|
|
1269
1506
|
if (await fileExists(hooksPath)) return false;
|
|
1270
1507
|
await mkdir2(rulesDir, { recursive: true });
|
|
1271
1508
|
await writeFile3(hooksPath, generateHooksRule());
|
|
1272
|
-
log.success(
|
|
1509
|
+
log.success(
|
|
1510
|
+
"Created .claude/rules/hooks.md (path-scoped hook authoring rules)"
|
|
1511
|
+
);
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
async function createVerificationRule(root) {
|
|
1515
|
+
const rulesDir = join4(root, ".claude", "rules");
|
|
1516
|
+
const verificationPath = join4(rulesDir, "verification.md");
|
|
1517
|
+
if (await fileExists(verificationPath)) return false;
|
|
1518
|
+
await mkdir2(rulesDir, { recursive: true });
|
|
1519
|
+
await writeFile3(verificationPath, generateVerificationRule());
|
|
1520
|
+
log.success(
|
|
1521
|
+
"Created .claude/rules/verification.md (evidence-before-assertion discipline)"
|
|
1522
|
+
);
|
|
1523
|
+
return true;
|
|
1524
|
+
}
|
|
1525
|
+
async function updateVerificationRule(root) {
|
|
1526
|
+
const verificationPath = join4(root, ".claude", "rules", "verification.md");
|
|
1527
|
+
const content = await readFile3(verificationPath, "utf-8").catch(() => null);
|
|
1528
|
+
if (content === null) return false;
|
|
1529
|
+
if (!/<!-- lp-verification-version: \d+ -->/.test(content)) return false;
|
|
1530
|
+
await writeFile3(verificationPath, generateVerificationRule());
|
|
1531
|
+
log.success("Updated .claude/rules/verification.md to the latest version");
|
|
1273
1532
|
return true;
|
|
1274
1533
|
}
|
|
1275
1534
|
function isMemoryHeading(line) {
|
|
@@ -1286,7 +1545,11 @@ function findMemoryBlocks(lines) {
|
|
|
1286
1545
|
break;
|
|
1287
1546
|
}
|
|
1288
1547
|
}
|
|
1289
|
-
blocks.push({
|
|
1548
|
+
blocks.push({
|
|
1549
|
+
startIdx: i,
|
|
1550
|
+
endIdx: end,
|
|
1551
|
+
tagged: lines[i].includes("(agentic-memory)")
|
|
1552
|
+
});
|
|
1290
1553
|
i = end - 1;
|
|
1291
1554
|
}
|
|
1292
1555
|
return blocks;
|
|
@@ -1295,7 +1558,7 @@ async function collapseMemoryHeadings(root) {
|
|
|
1295
1558
|
const claudeMdPath = join4(root, "CLAUDE.md");
|
|
1296
1559
|
let content;
|
|
1297
1560
|
try {
|
|
1298
|
-
content = await
|
|
1561
|
+
content = await readFile3(claudeMdPath, "utf-8");
|
|
1299
1562
|
} catch {
|
|
1300
1563
|
return false;
|
|
1301
1564
|
}
|
|
@@ -1321,7 +1584,9 @@ async function collapseMemoryHeadings(root) {
|
|
|
1321
1584
|
(line) => /^## Memory\s*$/.test(line) ? "## Memory (agentic-memory)" : line
|
|
1322
1585
|
);
|
|
1323
1586
|
await writeFile3(claudeMdPath, canonical.join("\n"));
|
|
1324
|
-
log.success(
|
|
1587
|
+
log.success(
|
|
1588
|
+
`Collapsed ${blocks.length - 1} duplicate ## Memory section(s) in CLAUDE.md`
|
|
1589
|
+
);
|
|
1325
1590
|
return true;
|
|
1326
1591
|
}
|
|
1327
1592
|
|
|
@@ -1366,21 +1631,63 @@ async function addForcePushProtection(root) {
|
|
|
1366
1631
|
}]
|
|
1367
1632
|
}, "Added force-push protection hook (PreToolUse \u2192 Bash)");
|
|
1368
1633
|
}
|
|
1369
|
-
async function
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1634
|
+
async function migratePostCompactHook(root) {
|
|
1635
|
+
const settings = await readSettingsJson(root);
|
|
1636
|
+
if (settings === null) return false;
|
|
1637
|
+
const hooks = settings.hooks ?? {};
|
|
1638
|
+
let changed = false;
|
|
1639
|
+
const postCompact = hooks.PostCompact ?? [];
|
|
1640
|
+
const kept = postCompact.map((group) => {
|
|
1641
|
+
const nested = group.hooks ?? [];
|
|
1642
|
+
const filtered = nested.filter((h) => !String(h.command ?? "").includes("TASKS.md"));
|
|
1643
|
+
if (filtered.length !== nested.length) changed = true;
|
|
1644
|
+
return { ...group, hooks: filtered };
|
|
1645
|
+
}).filter((group) => group.hooks.length > 0);
|
|
1646
|
+
const rest = { ...hooks };
|
|
1647
|
+
if (kept.length > 0) rest.PostCompact = kept;
|
|
1648
|
+
else delete rest.PostCompact;
|
|
1649
|
+
const withMatcher = ensureCompactMatcher(rest);
|
|
1650
|
+
if (withMatcher.changed) changed = true;
|
|
1651
|
+
if (!changed) return false;
|
|
1652
|
+
await writeSettingsJson(root, { ...settings, hooks: withMatcher.hooks });
|
|
1653
|
+
log.success("Moved TASKS.md re-injection off PostCompact (stdout not injected there) \u2192 SessionStart compact/clear matcher");
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
async function addCompactMatcherHook(root) {
|
|
1657
|
+
const settings = await readSettingsJson(root);
|
|
1658
|
+
if (settings === null) return false;
|
|
1659
|
+
const hooks = settings.hooks ?? {};
|
|
1660
|
+
const result = ensureCompactMatcher(hooks);
|
|
1661
|
+
if (!result.changed) return false;
|
|
1662
|
+
await writeSettingsJson(root, { ...settings, hooks: result.hooks });
|
|
1663
|
+
log.success("SessionStart matcher now covers compact/clear (session continuity after compaction)");
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
function ensureCompactMatcher(hooks) {
|
|
1667
|
+
const sessionStart = hooks.SessionStart ?? [];
|
|
1668
|
+
if (sessionStart.some((g) => String(g.matcher ?? "").includes("compact"))) {
|
|
1669
|
+
return { hooks, changed: false };
|
|
1670
|
+
}
|
|
1671
|
+
const idx = sessionStart.findIndex((g) => {
|
|
1672
|
+
const nested = g.hooks;
|
|
1673
|
+
return nested?.some((h) => String(h.command ?? "").includes("TASKS.md"));
|
|
1674
|
+
});
|
|
1675
|
+
if (idx === -1) {
|
|
1676
|
+
const entry = { matcher: SESSION_START_MATCHER, hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }] };
|
|
1677
|
+
return { hooks: { ...hooks, SessionStart: [...sessionStart, entry] }, changed: true };
|
|
1678
|
+
}
|
|
1679
|
+
const widened = sessionStart.map((g, i) => i === idx ? { ...g, matcher: SESSION_START_MATCHER } : g);
|
|
1680
|
+
return { hooks: { ...hooks, SessionStart: widened }, changed: true };
|
|
1374
1681
|
}
|
|
1375
1682
|
async function addSessionStartHook(root) {
|
|
1376
1683
|
return addHookToSettings(root, "SessionStart", "TASKS.md", {
|
|
1377
|
-
matcher:
|
|
1684
|
+
matcher: SESSION_START_MATCHER,
|
|
1378
1685
|
hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }]
|
|
1379
|
-
}, "Added SessionStart hook (injects TASKS.md at startup)");
|
|
1686
|
+
}, "Added SessionStart hook (injects TASKS.md at startup/resume/compact/clear)");
|
|
1380
1687
|
}
|
|
1381
1688
|
|
|
1382
1689
|
// src/commands/doctor/fixer-hook-input.ts
|
|
1383
|
-
import { readFile as
|
|
1690
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1384
1691
|
import { join as join5 } from "path";
|
|
1385
1692
|
function rewriteEnvVarHookCommand(cmd) {
|
|
1386
1693
|
if (!hasEnvVarHookPattern(cmd)) return null;
|
|
@@ -1394,7 +1701,7 @@ function rewriteEnvVarHookCommand(cmd) {
|
|
|
1394
1701
|
return `cmd=${jqField("command")}; echo "$cmd" | grep -qE 'push.*--force|push.*-f' && { echo 'WARNING: Force push detected \u2014 this can destroy remote history' >&2; exit 2; }; exit 0`;
|
|
1395
1702
|
}
|
|
1396
1703
|
if (cmd.includes("Sprint complete") && cmd.includes("TASKS.md")) {
|
|
1397
|
-
return
|
|
1704
|
+
return SPRINT_COMPLETE_NUDGE;
|
|
1398
1705
|
}
|
|
1399
1706
|
const formatMatch = cmd.match(/ext=\$\{TOOL_INPUT_FILE_PATH##\*\.\};\s*\((.+?)\)\s*&&\s*(.+?)\s*"\$TOOL_INPUT_FILE_PATH"/);
|
|
1400
1707
|
if (formatMatch) {
|
|
@@ -1428,7 +1735,7 @@ function rewriteSettingsHooks(settings) {
|
|
|
1428
1735
|
async function rewriteWrapperScript(scriptPath, rewriter) {
|
|
1429
1736
|
let content;
|
|
1430
1737
|
try {
|
|
1431
|
-
content = await
|
|
1738
|
+
content = await readFile4(scriptPath, "utf-8");
|
|
1432
1739
|
} catch {
|
|
1433
1740
|
return false;
|
|
1434
1741
|
}
|
|
@@ -1461,7 +1768,7 @@ async function rewriteEnvVarHooks(root) {
|
|
|
1461
1768
|
}
|
|
1462
1769
|
|
|
1463
1770
|
// src/commands/doctor/fixer-memory.ts
|
|
1464
|
-
import { readFile as
|
|
1771
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1465
1772
|
import { join as join6 } from "path";
|
|
1466
1773
|
async function addPlacementHook(root, placement, event, dedupKeyword, entry, prepend, successMsg) {
|
|
1467
1774
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
@@ -1520,13 +1827,13 @@ async function addSessionStartPullHook(root, placement) {
|
|
|
1520
1827
|
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1521
1828
|
return addPlacementHook(root, placement, "SessionStart", "memory pull", {
|
|
1522
1829
|
matcher: "startup",
|
|
1523
|
-
hooks: [{ type: "command", command: "claude-launchpad memory pull -y 2>/dev/null; exit 0" }]
|
|
1830
|
+
hooks: [{ type: "command", command: "npx claude-launchpad memory pull -y 2>/dev/null; exit 0" }]
|
|
1524
1831
|
}, true, `Added SessionStart hook for memory sync to ${target}`);
|
|
1525
1832
|
}
|
|
1526
1833
|
async function addSessionEndPushHook(root, placement) {
|
|
1527
1834
|
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1528
1835
|
return addPlacementHook(root, placement, "SessionEnd", "memory push", {
|
|
1529
|
-
hooks: [{ type: "command", command: "nohup claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" }]
|
|
1836
|
+
hooks: [{ type: "command", command: "nohup npx claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" }]
|
|
1530
1837
|
}, false, `Added SessionEnd hook for memory sync to ${target}`);
|
|
1531
1838
|
}
|
|
1532
1839
|
async function upgradeStaleSessionEndPushHook(root) {
|
|
@@ -1547,7 +1854,7 @@ async function upgradeStaleSessionEndPushHook(root) {
|
|
|
1547
1854
|
const cmd = typeof h.command === "string" ? h.command : "";
|
|
1548
1855
|
if (!cmd.includes("memory push") || cmd.includes("nohup")) return h;
|
|
1549
1856
|
changed = true;
|
|
1550
|
-
return { ...h, command: "nohup claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" };
|
|
1857
|
+
return { ...h, command: "nohup npx claude-launchpad memory push -y </dev/null >/dev/null 2>&1 & exit 0" };
|
|
1551
1858
|
});
|
|
1552
1859
|
return { ...group, hooks: rewritten };
|
|
1553
1860
|
});
|
|
@@ -1618,7 +1925,7 @@ async function addAllowedMcpServers(root, placement) {
|
|
|
1618
1925
|
}
|
|
1619
1926
|
const mcpJsonPath = join6(root, ".mcp.json");
|
|
1620
1927
|
try {
|
|
1621
|
-
const mcpJson = JSON.parse(await
|
|
1928
|
+
const mcpJson = JSON.parse(await readFile5(mcpJsonPath, "utf-8"));
|
|
1622
1929
|
const mcpServers = mcpJson.mcpServers;
|
|
1623
1930
|
if (mcpServers && typeof mcpServers === "object") {
|
|
1624
1931
|
for (const name of Object.keys(mcpServers)) serverNames.add(name);
|
|
@@ -1635,6 +1942,38 @@ async function addAllowedMcpServers(root, placement) {
|
|
|
1635
1942
|
log.success(`Added allowedMcpServers from configured servers to ${target}`);
|
|
1636
1943
|
return true;
|
|
1637
1944
|
}
|
|
1945
|
+
async function upgradeBareMemoryHooks(root) {
|
|
1946
|
+
let any = false;
|
|
1947
|
+
for (const placement of ["shared", "local"]) {
|
|
1948
|
+
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1949
|
+
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
1950
|
+
const settings = await read(root);
|
|
1951
|
+
if (settings === null) continue;
|
|
1952
|
+
const hooks = settings.hooks;
|
|
1953
|
+
if (!hooks) continue;
|
|
1954
|
+
let changed = false;
|
|
1955
|
+
const upgradedHooks = {};
|
|
1956
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
1957
|
+
upgradedHooks[event] = groups.map((group) => {
|
|
1958
|
+
const inner = group.hooks;
|
|
1959
|
+
if (!inner) return group;
|
|
1960
|
+
const rewritten = inner.map((h) => {
|
|
1961
|
+
const cmd = typeof h.command === "string" ? h.command : "";
|
|
1962
|
+
if (!cmd.includes("claude-launchpad memory") || cmd.includes("npx claude-launchpad")) return h;
|
|
1963
|
+
changed = true;
|
|
1964
|
+
return { ...h, command: cmd.replace("claude-launchpad memory", "npx claude-launchpad memory") };
|
|
1965
|
+
});
|
|
1966
|
+
return { ...group, hooks: rewritten };
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
if (!changed) continue;
|
|
1970
|
+
await write(root, { ...settings, hooks: upgradedHooks });
|
|
1971
|
+
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1972
|
+
log.success(`Rewrote bare memory hook commands to npx form in ${target}`);
|
|
1973
|
+
any = true;
|
|
1974
|
+
}
|
|
1975
|
+
return any;
|
|
1976
|
+
}
|
|
1638
1977
|
|
|
1639
1978
|
// src/commands/doctor/fixer.ts
|
|
1640
1979
|
async function applyFixes(issues, projectRoot) {
|
|
@@ -1654,67 +1993,311 @@ async function applyFixes(issues, projectRoot) {
|
|
|
1654
1993
|
return { fixed, skipped };
|
|
1655
1994
|
}
|
|
1656
1995
|
var FIX_TABLE = [
|
|
1657
|
-
{
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1996
|
+
{
|
|
1997
|
+
analyzer: "Hooks",
|
|
1998
|
+
match: "$TOOL_INPUT_* env var",
|
|
1999
|
+
fix: (root) => rewriteEnvVarHooks(root)
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
analyzer: "Hooks",
|
|
2003
|
+
match: "No hooks configured",
|
|
2004
|
+
fix: async (root, detected) => {
|
|
2005
|
+
const a = await addEnvProtectionHook(root);
|
|
2006
|
+
const b = await addAutoFormatHook(root, detected);
|
|
2007
|
+
const c = await addForcePushProtection(root);
|
|
2008
|
+
const d = await addSessionStartHook(root);
|
|
2009
|
+
return a || b || c || d;
|
|
2010
|
+
}
|
|
2011
|
+
},
|
|
2012
|
+
{
|
|
2013
|
+
analyzer: "Hooks",
|
|
2014
|
+
match: ".env file protection",
|
|
2015
|
+
fix: (root) => addEnvProtectionHook(root)
|
|
2016
|
+
},
|
|
2017
|
+
{
|
|
2018
|
+
analyzer: "Hooks",
|
|
2019
|
+
match: "auto-format",
|
|
2020
|
+
fix: (root, detected) => addAutoFormatHook(root, detected)
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
analyzer: "Hooks",
|
|
2024
|
+
match: "No PreToolUse",
|
|
2025
|
+
fix: (root) => addEnvProtectionHook(root)
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
analyzer: "Quality",
|
|
2029
|
+
match: "Architecture",
|
|
2030
|
+
fix: (root) => addClaudeMdSection(
|
|
2031
|
+
root,
|
|
2032
|
+
"## Architecture",
|
|
2033
|
+
wrapStub(
|
|
2034
|
+
"<!-- TODO: Describe your codebase structure. Run `/lp-enhance` to auto-fill this. -->"
|
|
2035
|
+
)
|
|
2036
|
+
)
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
analyzer: "Quality",
|
|
2040
|
+
match: "Off-Limits",
|
|
2041
|
+
fix: (root) => addClaudeMdSection(root, "## Off-Limits", wrapStub(OFF_LIMITS_CONTENT))
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
analyzer: "Quality",
|
|
2045
|
+
match: "Commands",
|
|
2046
|
+
fix: (root) => addClaudeMdSection(
|
|
2047
|
+
root,
|
|
2048
|
+
"## Commands",
|
|
2049
|
+
wrapStub("<!-- TODO: Add your dev/build/test commands -->")
|
|
2050
|
+
)
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
analyzer: "Quality",
|
|
2054
|
+
match: "Stack",
|
|
2055
|
+
fix: (root, detected) => {
|
|
2056
|
+
if (detected.language) {
|
|
2057
|
+
const content = `- **Language**: ${detected.language}${detected.framework ? `
|
|
1674
2058
|
- **Framework**: ${detected.framework}` : ""}${detected.packageManager ? `
|
|
1675
2059
|
- **Package Manager**: ${detected.packageManager}` : ""}`;
|
|
1676
|
-
|
|
2060
|
+
return addClaudeMdSection(root, "## Stack", content);
|
|
2061
|
+
}
|
|
2062
|
+
return addClaudeMdSection(
|
|
2063
|
+
root,
|
|
2064
|
+
"## Stack",
|
|
2065
|
+
wrapStub("<!-- TODO: Define your tech stack -->")
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
analyzer: "Quality",
|
|
2071
|
+
match: "Session Start",
|
|
2072
|
+
fix: (root) => addClaudeMdSection(
|
|
2073
|
+
root,
|
|
2074
|
+
"## Session Start",
|
|
2075
|
+
wrapStub(SESSION_START_CONTENT)
|
|
2076
|
+
)
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
analyzer: "Quality",
|
|
2080
|
+
match: "Backlog",
|
|
2081
|
+
fix: (root) => addClaudeMdSection(root, "## Backlog", wrapStub(BACKLOG_CONTENT))
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
analyzer: "Quality",
|
|
2085
|
+
match: "Stop-and-Swarm section is outdated",
|
|
2086
|
+
fix: (root) => fixStaleSwarmPhrase(root)
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
analyzer: "Quality",
|
|
2090
|
+
match: "Stop-and-Swarm",
|
|
2091
|
+
fix: (root) => addClaudeMdSection(
|
|
2092
|
+
root,
|
|
2093
|
+
"## Stop-and-Swarm",
|
|
2094
|
+
wrapStub(STOP_AND_SWARM_CONTENT)
|
|
2095
|
+
)
|
|
2096
|
+
},
|
|
2097
|
+
{
|
|
2098
|
+
analyzer: "Rules",
|
|
2099
|
+
match: "workflow.md rule is outdated",
|
|
2100
|
+
fix: (root) => updateWorkflowRule(root)
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
analyzer: "Rules",
|
|
2104
|
+
match: "verification.md rule is outdated",
|
|
2105
|
+
fix: (root) => updateVerificationRule(root)
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
analyzer: "Rules",
|
|
2109
|
+
match: "No .claude/agents/code-reviewer.md",
|
|
2110
|
+
fix: (root) => createReviewerAgent(root)
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
analyzer: "Quality",
|
|
2114
|
+
match: "Duplicate ## Memory",
|
|
2115
|
+
fix: (root) => collapseMemoryHeadings(root)
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
analyzer: "Rules",
|
|
2119
|
+
match: "No BACKLOG.md",
|
|
2120
|
+
fix: (root) => createBacklogMd(root)
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
analyzer: "Rules",
|
|
2124
|
+
match: "No .claudeignore",
|
|
2125
|
+
fix: (root, detected) => createClaudeignore(root, detected)
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
analyzer: "Rules",
|
|
2129
|
+
match: "No .claude/rules/workflow.md",
|
|
2130
|
+
fix: (root) => createWorkflowRule(root)
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
analyzer: "Rules",
|
|
2134
|
+
match: "No .claude/rules/hooks.md",
|
|
2135
|
+
fix: (root) => createHooksRule(root)
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
analyzer: "Rules",
|
|
2139
|
+
match: "No .claude/rules/verification.md",
|
|
2140
|
+
fix: (root) => createVerificationRule(root)
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
analyzer: "Rules",
|
|
2144
|
+
match: "No .claude/rules/",
|
|
2145
|
+
fix: (root) => createStarterRules(root)
|
|
2146
|
+
},
|
|
2147
|
+
{
|
|
2148
|
+
analyzer: "Hooks",
|
|
2149
|
+
match: "PostCompact hooks can't inject context",
|
|
2150
|
+
fix: (root) => migratePostCompactHook(root)
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
analyzer: "Hooks",
|
|
2154
|
+
match: "compact matcher",
|
|
2155
|
+
fix: (root) => addCompactMatcherHook(root)
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
analyzer: "Permissions",
|
|
2159
|
+
match: "force-push",
|
|
2160
|
+
fix: (root) => addForcePushProtection(root)
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
analyzer: "Permissions",
|
|
2164
|
+
match: "Credential files not blocked",
|
|
2165
|
+
fix: (root) => addCredentialDenyRules(root)
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
analyzer: "Permissions",
|
|
2169
|
+
match: "Bypass permissions mode",
|
|
2170
|
+
fix: (root) => addBypassDisable(root)
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
analyzer: "Permissions",
|
|
2174
|
+
match: "Sandbox lacks a write grant",
|
|
2175
|
+
fix: (root) => addSandboxMemoryWriteGrant(root)
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
analyzer: "Permissions",
|
|
2179
|
+
match: ".env is protected by hooks but not in .claudeignore",
|
|
2180
|
+
fix: (root) => addEnvToClaudeignore(root)
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
analyzer: "Permissions",
|
|
2184
|
+
match: ".worktreeinclude is missing or empty",
|
|
2185
|
+
fix: (root) => createWorktreeInclude(root)
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
analyzer: "Hooks",
|
|
2189
|
+
match: "registered on PreToolUse",
|
|
2190
|
+
fix: (root) => migrateSprintOpenHookEvent(root)
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
analyzer: "Hooks",
|
|
2194
|
+
match: "nudge uses bare stdout",
|
|
2195
|
+
fix: (root) => upgradeStaleNudge(root)
|
|
2196
|
+
},
|
|
2197
|
+
{
|
|
2198
|
+
analyzer: "Hooks",
|
|
2199
|
+
match: "Outdated hygiene script",
|
|
2200
|
+
fix: (root) => refreshHygieneScripts(root)
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
analyzer: "Hooks",
|
|
2204
|
+
match: "sprint-size-check",
|
|
2205
|
+
fix: (root) => addSprintSizeHook(root)
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
analyzer: "Hooks",
|
|
2209
|
+
match: "sprint-open-check",
|
|
2210
|
+
fix: (root) => addSprintOpenHook(root)
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
analyzer: "Hooks",
|
|
2214
|
+
match: "sprint-complete nudge",
|
|
2215
|
+
fix: (root) => addSprintCompleteNudge(root)
|
|
2216
|
+
},
|
|
2217
|
+
{
|
|
2218
|
+
analyzer: "Hooks",
|
|
2219
|
+
match: "workflow-check.sh",
|
|
2220
|
+
fix: (root) => addWorkflowCheckHook(root)
|
|
2221
|
+
},
|
|
2222
|
+
{
|
|
2223
|
+
analyzer: "Rules",
|
|
2224
|
+
match: "No skill authoring conventions",
|
|
2225
|
+
fix: (root) => addSkillAuthoringConventions(root)
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
analyzer: "Rules",
|
|
2229
|
+
match: "No /lp-enhance skill",
|
|
2230
|
+
fix: (root) => createEnhanceSkill(root)
|
|
2231
|
+
},
|
|
2232
|
+
{
|
|
2233
|
+
analyzer: "Rules",
|
|
2234
|
+
match: "lp-enhance skill is outdated",
|
|
2235
|
+
fix: (root) => updateEnhanceSkill(root)
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
analyzer: "Settings",
|
|
2239
|
+
match: "Deprecated includeCoAuthoredBy",
|
|
2240
|
+
fix: (root) => migrateAttribution(root)
|
|
2241
|
+
},
|
|
2242
|
+
{
|
|
2243
|
+
analyzer: "Hooks",
|
|
2244
|
+
match: "SessionStart",
|
|
2245
|
+
fix: (root) => addSessionStartHook(root)
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
analyzer: "Memory",
|
|
2249
|
+
match: "bare claude-launchpad binary",
|
|
2250
|
+
fix: (root) => upgradeBareMemoryHooks(root)
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
analyzer: "Memory",
|
|
2254
|
+
match: "Deprecated Stop hook",
|
|
2255
|
+
fix: (root) => removeStaleStopHook(root)
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
analyzer: "Memory",
|
|
2259
|
+
match: "autoMemoryEnabled not disabled",
|
|
2260
|
+
fix: (root, _det, placement) => disableAutoMemory(root, placement)
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
analyzer: "Memory",
|
|
2264
|
+
match: "MCP tool permission",
|
|
2265
|
+
fix: (root, _det, placement) => addMemoryToolPermissions(root, placement)
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
analyzer: "MCP",
|
|
2269
|
+
match: "no allowedMcpServers",
|
|
2270
|
+
fix: (root, _det, placement) => addAllowedMcpServers(root, placement)
|
|
2271
|
+
},
|
|
2272
|
+
{
|
|
2273
|
+
analyzer: "Memory",
|
|
2274
|
+
match: "allowedMcpServers is set but does not include agentic-memory",
|
|
2275
|
+
fix: (root) => addMemoryToAllowedMcpServers(root)
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
analyzer: "Memory",
|
|
2279
|
+
match: "SessionStart hook to auto-pull",
|
|
2280
|
+
fix: (root, _det, placement) => addSessionStartPullHook(root, placement)
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
analyzer: "Memory",
|
|
2284
|
+
match: "SessionEnd hook to auto-push",
|
|
2285
|
+
fix: (root, _det, placement) => addSessionEndPushHook(root, placement)
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
analyzer: "Memory",
|
|
2289
|
+
match: "SessionEnd push hook is not nohup-wrapped",
|
|
2290
|
+
fix: (root) => upgradeStaleSessionEndPushHook(root)
|
|
2291
|
+
},
|
|
2292
|
+
{
|
|
2293
|
+
analyzer: "Memory",
|
|
2294
|
+
match: "CLAUDE.md missing memory guidance",
|
|
2295
|
+
fix: (root, _det, placement) => {
|
|
2296
|
+
const content = "Use agentic-memory to persist knowledge across sessions:\n- Memories are automatically injected at session start\n- STORE IMMEDIATELY when: a dependency strategy changes, an architecture decision is made, a convention is established, a bug pattern is discovered, or a feature is killed/added\n- Use memory_search before memory_store to check for duplicates\n- NEVER store credentials, API keys, tokens, or secrets in memories";
|
|
2297
|
+
const target = placement === "local" ? join7(root, ".claude", "CLAUDE.md") : void 0;
|
|
2298
|
+
return addClaudeMdSection(root, "## Memory", wrapStub(content), target);
|
|
1677
2299
|
}
|
|
1678
|
-
|
|
1679
|
-
} },
|
|
1680
|
-
{ analyzer: "Quality", match: "Session Start", fix: (root) => addClaudeMdSection(root, "## Session Start", wrapStub(SESSION_START_CONTENT)) },
|
|
1681
|
-
{ analyzer: "Quality", match: "Backlog", fix: (root) => addClaudeMdSection(root, "## Backlog", wrapStub(BACKLOG_CONTENT)) },
|
|
1682
|
-
{ analyzer: "Quality", match: "Stop-and-Swarm", fix: (root) => addClaudeMdSection(root, "## Stop-and-Swarm", wrapStub(STOP_AND_SWARM_CONTENT)) },
|
|
1683
|
-
{ analyzer: "Quality", match: "Duplicate ## Memory", fix: (root) => collapseMemoryHeadings(root) },
|
|
1684
|
-
{ analyzer: "Rules", match: "No BACKLOG.md", fix: (root) => createBacklogMd(root) },
|
|
1685
|
-
{ analyzer: "Rules", match: "No .claudeignore", fix: (root, detected) => createClaudeignore(root, detected) },
|
|
1686
|
-
{ analyzer: "Rules", match: "No .claude/rules/workflow.md", fix: (root) => createWorkflowRule(root) },
|
|
1687
|
-
{ analyzer: "Rules", match: "No .claude/rules/hooks.md", fix: (root) => createHooksRule(root) },
|
|
1688
|
-
{ analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
|
|
1689
|
-
{ analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
|
|
1690
|
-
{ analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
|
|
1691
|
-
{ analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
|
|
1692
|
-
{ analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
|
|
1693
|
-
{ analyzer: "Permissions", match: "Filesystem sandbox enabled", fix: (root) => removeSandboxSettings(root) },
|
|
1694
|
-
{ analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
|
|
1695
|
-
{ analyzer: "Permissions", match: ".worktreeinclude is missing or empty", fix: (root) => createWorktreeInclude(root) },
|
|
1696
|
-
{ analyzer: "Hooks", match: "sprint-size-check", fix: (root) => addSprintSizeHook(root) },
|
|
1697
|
-
{ analyzer: "Hooks", match: "sprint-open-check", fix: (root) => addSprintOpenHook(root) },
|
|
1698
|
-
{ analyzer: "Hooks", match: "sprint-complete nudge", fix: (root) => addSprintCompleteNudge(root) },
|
|
1699
|
-
{ analyzer: "Hooks", match: "workflow-check.sh", fix: (root) => addWorkflowCheckHook(root) },
|
|
1700
|
-
{ analyzer: "Rules", match: "No skill authoring conventions", fix: (root) => addSkillAuthoringConventions(root) },
|
|
1701
|
-
{ analyzer: "Rules", match: "No /lp-enhance skill", fix: (root) => createEnhanceSkill(root) },
|
|
1702
|
-
{ analyzer: "Rules", match: "lp-enhance skill is outdated", fix: (root) => updateEnhanceSkill(root) },
|
|
1703
|
-
{ analyzer: "Settings", match: "Deprecated includeCoAuthoredBy", fix: (root) => migrateAttribution(root) },
|
|
1704
|
-
{ analyzer: "Hooks", match: "SessionStart", fix: (root) => addSessionStartHook(root) },
|
|
1705
|
-
{ analyzer: "Memory", match: "Deprecated Stop hook", fix: (root) => removeStaleStopHook(root) },
|
|
1706
|
-
{ analyzer: "Memory", match: "autoMemoryEnabled not disabled", fix: (root, _det, placement) => disableAutoMemory(root, placement) },
|
|
1707
|
-
{ analyzer: "Memory", match: "MCP tool permission", fix: (root, _det, placement) => addMemoryToolPermissions(root, placement) },
|
|
1708
|
-
{ analyzer: "MCP", match: "no allowedMcpServers", fix: (root, _det, placement) => addAllowedMcpServers(root, placement) },
|
|
1709
|
-
{ analyzer: "Memory", match: "allowedMcpServers is set but does not include agentic-memory", fix: (root) => addMemoryToAllowedMcpServers(root) },
|
|
1710
|
-
{ analyzer: "Memory", match: "SessionStart hook to auto-pull", fix: (root, _det, placement) => addSessionStartPullHook(root, placement) },
|
|
1711
|
-
{ analyzer: "Memory", match: "SessionEnd hook to auto-push", fix: (root, _det, placement) => addSessionEndPushHook(root, placement) },
|
|
1712
|
-
{ analyzer: "Memory", match: "SessionEnd push hook is not nohup-wrapped", fix: (root) => upgradeStaleSessionEndPushHook(root) },
|
|
1713
|
-
{ analyzer: "Memory", match: "CLAUDE.md missing memory guidance", fix: (root, _det, placement) => {
|
|
1714
|
-
const content = "Use agentic-memory to persist knowledge across sessions:\n- Memories are automatically injected at session start\n- STORE IMMEDIATELY when: a dependency strategy changes, an architecture decision is made, a convention is established, a bug pattern is discovered, or a feature is killed/added\n- Use memory_search before memory_store to check for duplicates\n- NEVER store credentials, API keys, tokens, or secrets in memories";
|
|
1715
|
-
const target = placement === "local" ? join7(root, ".claude", "CLAUDE.md") : void 0;
|
|
1716
|
-
return addClaudeMdSection(root, "## Memory", wrapStub(content), target);
|
|
1717
|
-
} }
|
|
2300
|
+
}
|
|
1718
2301
|
];
|
|
1719
2302
|
function hasAutoFix(issue) {
|
|
1720
2303
|
return FIX_TABLE.some(
|
|
@@ -1745,7 +2328,10 @@ async function addCredentialDenyRules(root) {
|
|
|
1745
2328
|
const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
|
|
1746
2329
|
const missing = toAdd.filter((p) => !deny.includes(p));
|
|
1747
2330
|
if (missing.length === 0) return false;
|
|
1748
|
-
const updated = {
|
|
2331
|
+
const updated = {
|
|
2332
|
+
...settings,
|
|
2333
|
+
permissions: { ...permissions, deny: [...deny, ...missing] }
|
|
2334
|
+
};
|
|
1749
2335
|
await writeSettingsJson(root, updated);
|
|
1750
2336
|
log.success("Added credential deny rules (SSH, AWS, npm)");
|
|
1751
2337
|
return true;
|
|
@@ -1759,25 +2345,41 @@ async function addBypassDisable(root) {
|
|
|
1759
2345
|
log.success("Added disableBypassPermissionsMode: disable");
|
|
1760
2346
|
return true;
|
|
1761
2347
|
}
|
|
1762
|
-
async function
|
|
2348
|
+
async function addSandboxMemoryWriteGrant(root) {
|
|
1763
2349
|
const settings = await readSettingsJson(root);
|
|
1764
2350
|
if (settings === null) return false;
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2351
|
+
const sandbox = settings.sandbox;
|
|
2352
|
+
if (sandbox === void 0) return false;
|
|
2353
|
+
const filesystem = sandbox.filesystem ?? {};
|
|
2354
|
+
const allowWrite = filesystem.allowWrite ?? [];
|
|
2355
|
+
if (allowWrite.some((p) => p.includes(".agentic-memory"))) return false;
|
|
2356
|
+
const updated = {
|
|
2357
|
+
...settings,
|
|
2358
|
+
sandbox: {
|
|
2359
|
+
...sandbox,
|
|
2360
|
+
filesystem: {
|
|
2361
|
+
...filesystem,
|
|
2362
|
+
allowWrite: [...allowWrite, "~/.agentic-memory"]
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
await writeSettingsJson(root, updated);
|
|
2367
|
+
log.success(
|
|
2368
|
+
"Added ~/.agentic-memory to sandbox.filesystem.allowWrite (sandbox stays enabled)"
|
|
2369
|
+
);
|
|
1769
2370
|
return true;
|
|
1770
2371
|
}
|
|
1771
2372
|
async function addEnvToClaudeignore(root) {
|
|
1772
2373
|
const ignorePath = join7(root, ".claudeignore");
|
|
1773
2374
|
let content;
|
|
1774
2375
|
try {
|
|
1775
|
-
content = await
|
|
2376
|
+
content = await readFile6(ignorePath, "utf-8");
|
|
1776
2377
|
} catch {
|
|
1777
2378
|
return false;
|
|
1778
2379
|
}
|
|
1779
2380
|
const lines = content.split("\n").map((l) => l.trim());
|
|
1780
|
-
if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*"))
|
|
2381
|
+
if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*"))
|
|
2382
|
+
return false;
|
|
1781
2383
|
await writeFile4(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
|
|
1782
2384
|
log.success("Added .env to .claudeignore");
|
|
1783
2385
|
return true;
|
|
@@ -1786,7 +2388,7 @@ async function addClaudeMdSection(root, heading, content, targetPath) {
|
|
|
1786
2388
|
const claudeMdPath = targetPath ?? join7(root, "CLAUDE.md");
|
|
1787
2389
|
let existing;
|
|
1788
2390
|
try {
|
|
1789
|
-
existing = await
|
|
2391
|
+
existing = await readFile6(claudeMdPath, "utf-8");
|
|
1790
2392
|
} catch {
|
|
1791
2393
|
if (!targetPath) return false;
|
|
1792
2394
|
await mkdir3(join7(root, ".claude"), { recursive: true });
|
|
@@ -1808,23 +2410,17 @@ ${content}
|
|
|
1808
2410
|
}
|
|
1809
2411
|
async function createBacklogMd(root) {
|
|
1810
2412
|
const backlogPath = join7(root, "BACKLOG.md");
|
|
1811
|
-
|
|
1812
|
-
await access2(backlogPath);
|
|
1813
|
-
return false;
|
|
1814
|
-
} catch {
|
|
1815
|
-
}
|
|
2413
|
+
if (await fileExists(backlogPath)) return false;
|
|
1816
2414
|
const name = root.split("/").pop() ?? "Project";
|
|
1817
2415
|
await writeFile4(backlogPath, generateBacklogMd({ name, description: "" }));
|
|
1818
|
-
log.success(
|
|
2416
|
+
log.success(
|
|
2417
|
+
"Generated BACKLOG.md (WP template + P0\u2013P3 sections + changelog)"
|
|
2418
|
+
);
|
|
1819
2419
|
return true;
|
|
1820
2420
|
}
|
|
1821
2421
|
async function createClaudeignore(root, detected) {
|
|
1822
2422
|
const ignorePath = join7(root, ".claudeignore");
|
|
1823
|
-
|
|
1824
|
-
await access2(ignorePath);
|
|
1825
|
-
return false;
|
|
1826
|
-
} catch {
|
|
1827
|
-
}
|
|
2423
|
+
if (await fileExists(ignorePath)) return false;
|
|
1828
2424
|
const content = generateClaudeignore(detected);
|
|
1829
2425
|
await writeFile4(ignorePath, content);
|
|
1830
2426
|
log.success("Generated .claudeignore with language-specific ignore patterns");
|
|
@@ -1837,11 +2433,7 @@ ${SKILL_AUTHORING_CONTENT}
|
|
|
1837
2433
|
`;
|
|
1838
2434
|
async function createStarterRules(root) {
|
|
1839
2435
|
const rulesDir = join7(root, ".claude", "rules");
|
|
1840
|
-
|
|
1841
|
-
await access2(rulesDir);
|
|
1842
|
-
return false;
|
|
1843
|
-
} catch {
|
|
1844
|
-
}
|
|
2436
|
+
if (await fileExists(rulesDir)) return false;
|
|
1845
2437
|
await mkdir3(rulesDir, { recursive: true });
|
|
1846
2438
|
await writeFile4(
|
|
1847
2439
|
join7(rulesDir, "conventions.md"),
|
|
@@ -1860,22 +2452,32 @@ async function addSkillAuthoringConventions(root) {
|
|
|
1860
2452
|
const conventionsPath = join7(root, ".claude", "rules", "conventions.md");
|
|
1861
2453
|
let content;
|
|
1862
2454
|
try {
|
|
1863
|
-
content = await
|
|
2455
|
+
content = await readFile6(conventionsPath, "utf-8");
|
|
1864
2456
|
} catch {
|
|
1865
2457
|
return false;
|
|
1866
2458
|
}
|
|
1867
2459
|
if (/^##\s+Skill\s+Authoring/im.test(content)) return false;
|
|
1868
|
-
await writeFile4(
|
|
2460
|
+
await writeFile4(
|
|
2461
|
+
conventionsPath,
|
|
2462
|
+
content.trimEnd() + "\n" + SKILL_AUTHORING_SECTION
|
|
2463
|
+
);
|
|
1869
2464
|
log.success("Added Skill Authoring section to .claude/rules/conventions.md");
|
|
1870
2465
|
return true;
|
|
1871
2466
|
}
|
|
1872
2467
|
async function createEnhanceSkill(root) {
|
|
1873
2468
|
const skillDir = join7(root, ".claude", "skills", "lp-enhance");
|
|
1874
2469
|
const skillPath = join7(skillDir, "SKILL.md");
|
|
1875
|
-
const globalPath = join7(
|
|
2470
|
+
const globalPath = join7(
|
|
2471
|
+
homedir(),
|
|
2472
|
+
".claude",
|
|
2473
|
+
"skills",
|
|
2474
|
+
"lp-enhance",
|
|
2475
|
+
"SKILL.md"
|
|
2476
|
+
);
|
|
1876
2477
|
const legacyProject = join7(root, ".claude", "commands", "lp-enhance.md");
|
|
1877
2478
|
const legacyGlobal = join7(homedir(), ".claude", "commands", "lp-enhance.md");
|
|
1878
|
-
if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal))
|
|
2479
|
+
if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal))
|
|
2480
|
+
return false;
|
|
1879
2481
|
await mkdir3(skillDir, { recursive: true });
|
|
1880
2482
|
await writeFile4(skillPath, generateEnhanceSkill());
|
|
1881
2483
|
log.success("Generated /lp-enhance skill (.claude/skills/lp-enhance/)");
|
|
@@ -1883,7 +2485,13 @@ async function createEnhanceSkill(root) {
|
|
|
1883
2485
|
}
|
|
1884
2486
|
async function updateEnhanceSkill(root) {
|
|
1885
2487
|
const projectPath = join7(root, ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
1886
|
-
const globalPath = join7(
|
|
2488
|
+
const globalPath = join7(
|
|
2489
|
+
homedir(),
|
|
2490
|
+
".claude",
|
|
2491
|
+
"skills",
|
|
2492
|
+
"lp-enhance",
|
|
2493
|
+
"SKILL.md"
|
|
2494
|
+
);
|
|
1887
2495
|
const targetPath = await fileExists(projectPath) ? projectPath : await fileExists(globalPath) ? globalPath : null;
|
|
1888
2496
|
if (!targetPath) return false;
|
|
1889
2497
|
await writeFile4(targetPath, generateEnhanceSkill());
|
|
@@ -1997,7 +2605,7 @@ function renderDoctorReport(results, options) {
|
|
|
1997
2605
|
async function readJsonFile(path) {
|
|
1998
2606
|
let raw;
|
|
1999
2607
|
try {
|
|
2000
|
-
raw = await
|
|
2608
|
+
raw = await readFile7(path, "utf-8");
|
|
2001
2609
|
} catch (err) {
|
|
2002
2610
|
const code = err.code;
|
|
2003
2611
|
if (code === "ENOENT") return {};
|
|
@@ -2033,8 +2641,11 @@ export {
|
|
|
2033
2641
|
SESSION_START_CONTENT,
|
|
2034
2642
|
BACKLOG_CONTENT,
|
|
2035
2643
|
STOP_AND_SWARM_CONTENT,
|
|
2644
|
+
STALE_SWARM_PHRASE,
|
|
2036
2645
|
OFF_LIMITS_CONTENT,
|
|
2037
2646
|
SKILL_AUTHORING_CONTENT,
|
|
2647
|
+
sprintReviewsContent,
|
|
2648
|
+
TESTING_DISCIPLINE_CONTENT,
|
|
2038
2649
|
fileExists,
|
|
2039
2650
|
readFileOrNull,
|
|
2040
2651
|
detectProject,
|
|
@@ -2051,14 +2662,24 @@ export {
|
|
|
2051
2662
|
addOrUpdateHook,
|
|
2052
2663
|
jqField,
|
|
2053
2664
|
hasEnvVarHookPattern,
|
|
2665
|
+
isJqAvailable,
|
|
2666
|
+
SPRINT_COMPLETE_NUDGE,
|
|
2667
|
+
WORKFLOW_CHECK_WRAPPER,
|
|
2668
|
+
SPRINT_OPEN_WRAPPER,
|
|
2669
|
+
SPRINT_SIZE_WRAPPER,
|
|
2670
|
+
SESSION_START_MATCHER,
|
|
2054
2671
|
writeSprintHygieneScripts,
|
|
2055
2672
|
writeWorkflowCheckScript,
|
|
2673
|
+
WORKFLOW_RULE_VERSION,
|
|
2056
2674
|
generateWorkflowRule,
|
|
2057
2675
|
generateHooksRule,
|
|
2676
|
+
VERIFICATION_RULE_VERSION,
|
|
2677
|
+
generateVerificationRule,
|
|
2678
|
+
generateReviewerAgent,
|
|
2058
2679
|
applyFixes,
|
|
2059
2680
|
log,
|
|
2060
2681
|
printBanner,
|
|
2061
2682
|
printScoreCard,
|
|
2062
2683
|
renderDoctorReport
|
|
2063
2684
|
};
|
|
2064
|
-
//# sourceMappingURL=chunk-
|
|
2685
|
+
//# sourceMappingURL=chunk-QEPE3WVF.js.map
|