continuous-improvement 3.12.3 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +45 -1
  3. package/LICENSE +21 -21
  4. package/QUICKSTART.md +1 -1
  5. package/README.md +29 -8
  6. package/bin/analyze.sh +161 -161
  7. package/bin/backfill.mjs +19 -19
  8. package/bin/check-doc-runtime-claims.mjs +0 -0
  9. package/bin/check-docs-substrings.mjs +0 -0
  10. package/bin/check-everything-mirror.mjs +0 -0
  11. package/bin/check-routing-targets.mjs +0 -0
  12. package/bin/check-scripts-citation-drift.mjs +0 -0
  13. package/bin/check-skill-count-prose.mjs +168 -0
  14. package/bin/check-skill-count.mjs +32 -1
  15. package/bin/check-skill-law-tag.mjs +0 -0
  16. package/bin/check-skill-mirror.mjs +0 -0
  17. package/bin/check-skill-tiers.mjs +0 -0
  18. package/bin/check-test-imports-only.mjs +1 -1
  19. package/bin/check-third-party-shape.mjs +0 -0
  20. package/bin/check-tool-count.mjs +0 -0
  21. package/bin/companion-preference-status.mjs +0 -0
  22. package/bin/gateguard-clear.mjs +0 -0
  23. package/bin/generate-plugin-manifests.mjs +2 -1
  24. package/bin/harvest-friction.mjs +0 -0
  25. package/bin/hook-stats.mjs +21 -21
  26. package/bin/install.mjs +120 -2
  27. package/bin/lint-transcript.mjs +15 -3
  28. package/bin/mcp-server.mjs +66 -8
  29. package/bin/observe.mjs +0 -0
  30. package/bin/plan-pack.mjs +77 -0
  31. package/bin/pre-commit-block-strays.sh +0 -0
  32. package/bin/refresh-third-party.mjs +0 -0
  33. package/bin/unified-cli.mjs +55 -410
  34. package/commands/continuous-improvement.md +115 -115
  35. package/commands/dashboard.md +56 -56
  36. package/commands/model-forward.md +13 -0
  37. package/commands/production-readiness-review.md +53 -0
  38. package/commands/roast.md +34 -0
  39. package/commands/seven-laws.md +16 -16
  40. package/commands/ship.md +57 -0
  41. package/commands/workspace-surface-audit.md +77 -77
  42. package/hooks/companion-preference.mjs +31 -19
  43. package/hooks/gateguard.mjs +38 -16
  44. package/hooks/goal-drift-stop.mjs +0 -0
  45. package/hooks/hook-pack.mjs +110 -0
  46. package/hooks/observe.sh +0 -0
  47. package/hooks/recall-briefing.mjs +0 -0
  48. package/hooks/route-prompt.mjs +0 -0
  49. package/hooks/session.sh +106 -106
  50. package/hooks/three-section-close.mjs +0 -0
  51. package/hooks/workflow-distill.mjs +145 -0
  52. package/instinct-packs/go.json +58 -58
  53. package/instinct-packs/meta.json +16 -16
  54. package/instinct-packs/python.json +58 -58
  55. package/instinct-packs/react.json +58 -58
  56. package/lib/cli-anything.mjs +0 -0
  57. package/lib/gateguard-state.mjs +62 -13
  58. package/lib/goal-drift-gate.mjs +0 -0
  59. package/lib/goal-state.mjs +8 -4
  60. package/lib/hook-pack-gate.mjs +65 -0
  61. package/lib/install-targets.mjs +121 -0
  62. package/lib/observe-event.mjs +0 -0
  63. package/lib/plan-review-packet.mjs +96 -0
  64. package/lib/plugin-metadata.mjs +30 -4
  65. package/lib/pm-marketplace.mjs +0 -0
  66. package/lib/recall-briefing.mjs +0 -0
  67. package/lib/recall-index.mjs +0 -0
  68. package/lib/resolve-home-dir.mjs +0 -0
  69. package/lib/skill-distill.mjs +141 -0
  70. package/lib/skill-tiers.mjs +0 -0
  71. package/lib/version-check.mjs +115 -0
  72. package/llms.txt +2 -2
  73. package/package.json +6 -4
  74. package/plugins/beginner.json +2 -2
  75. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
  76. package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
  77. package/plugins/continuous-improvement/LICENSE +21 -21
  78. package/plugins/continuous-improvement/README.md +1 -1
  79. package/plugins/continuous-improvement/bin/backfill.mjs +19 -19
  80. package/plugins/continuous-improvement/bin/gateguard-clear.mjs +0 -0
  81. package/plugins/continuous-improvement/bin/mcp-server.mjs +66 -8
  82. package/plugins/continuous-improvement/bin/observe.mjs +0 -0
  83. package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -115
  84. package/plugins/continuous-improvement/commands/dashboard.md +56 -56
  85. package/plugins/continuous-improvement/commands/model-forward.md +13 -0
  86. package/plugins/continuous-improvement/commands/production-readiness-review.md +53 -0
  87. package/plugins/continuous-improvement/commands/roast.md +34 -0
  88. package/plugins/continuous-improvement/commands/seven-laws.md +16 -16
  89. package/plugins/continuous-improvement/commands/ship.md +57 -0
  90. package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -77
  91. package/plugins/continuous-improvement/hooks/companion-preference.mjs +31 -19
  92. package/plugins/continuous-improvement/hooks/gateguard.mjs +38 -16
  93. package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +0 -0
  94. package/plugins/continuous-improvement/hooks/hook-pack.mjs +110 -0
  95. package/plugins/continuous-improvement/hooks/hooks.json +16 -1
  96. package/plugins/continuous-improvement/hooks/observe.sh +0 -0
  97. package/plugins/continuous-improvement/hooks/recall-briefing.mjs +0 -0
  98. package/plugins/continuous-improvement/hooks/route-prompt.mjs +0 -0
  99. package/plugins/continuous-improvement/hooks/session.sh +106 -106
  100. package/plugins/continuous-improvement/hooks/three-section-close.mjs +0 -0
  101. package/plugins/continuous-improvement/hooks/workflow-distill.mjs +145 -0
  102. package/plugins/continuous-improvement/instinct-packs/go.json +58 -58
  103. package/plugins/continuous-improvement/instinct-packs/meta.json +16 -16
  104. package/plugins/continuous-improvement/instinct-packs/python.json +58 -58
  105. package/plugins/continuous-improvement/instinct-packs/react.json +58 -58
  106. package/plugins/continuous-improvement/lib/gateguard-state.mjs +62 -13
  107. package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +0 -0
  108. package/plugins/continuous-improvement/lib/goal-state.mjs +8 -4
  109. package/plugins/continuous-improvement/lib/hook-pack-gate.mjs +65 -0
  110. package/plugins/continuous-improvement/lib/observe-event.mjs +0 -0
  111. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +30 -4
  112. package/plugins/continuous-improvement/lib/recall-briefing.mjs +0 -0
  113. package/plugins/continuous-improvement/lib/recall-index.mjs +0 -0
  114. package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -0
  115. package/plugins/continuous-improvement/lib/skill-distill.mjs +141 -0
  116. package/plugins/continuous-improvement/skills/README.md +2 -1
  117. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +4 -4
  118. package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +1 -1
  119. package/plugins/continuous-improvement/skills/handoff/SKILL.md +0 -1
  120. package/plugins/continuous-improvement/skills/model-forward/SKILL.md +44 -0
  121. package/plugins/continuous-improvement/skills/roast/SKILL.md +108 -0
  122. package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +12 -32
  123. package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -411
  124. package/plugins/expert.json +5 -1
  125. package/skills/README.md +5 -3
  126. package/skills/gateguard.md +4 -4
  127. package/skills/goal-monitor.md +1 -1
  128. package/skills/handoff.md +0 -1
  129. package/skills/model-forward.md +44 -0
  130. package/skills/roast.md +108 -0
  131. package/skills/strategic-compact.md +12 -32
  132. package/skills/tdd-workflow.md +411 -411
  133. package/lib/compound-engineering.mjs +0 -831
  134. package/lib/pm-skills.mjs +0 -1274
  135. package/lib/unified-plugin.mjs +0 -924
  136. package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +0 -108
  137. package/skills/para-memory-files.md +0 -108
@@ -1,58 +1,58 @@
1
- [
2
- {
3
- "id": "react-check-existing-components",
4
- "trigger": "when creating a new React component",
5
- "body": "Search the codebase for existing components that solve the same problem before creating new ones. Check shared/, components/, and ui/ directories.",
6
- "confidence": 0.65,
7
- "domain": "workflow"
8
- },
9
- {
10
- "id": "react-prefer-server-components",
11
- "trigger": "when creating components in Next.js App Router",
12
- "body": "Default to Server Components. Only add 'use client' when the component needs useState, useEffect, event handlers, or browser APIs.",
13
- "confidence": 0.7,
14
- "domain": "patterns"
15
- },
16
- {
17
- "id": "react-key-prop-lists",
18
- "trigger": "when rendering lists with .map()",
19
- "body": "Always use a stable, unique key prop. Never use array index as key unless the list is static and never reordered.",
20
- "confidence": 0.7,
21
- "domain": "patterns"
22
- },
23
- {
24
- "id": "react-effect-cleanup",
25
- "trigger": "when writing useEffect with subscriptions or timers",
26
- "body": "Always return a cleanup function from useEffect when setting up subscriptions, event listeners, or timers to prevent memory leaks.",
27
- "confidence": 0.7,
28
- "domain": "patterns"
29
- },
30
- {
31
- "id": "react-memo-expensive",
32
- "trigger": "when a component re-renders with expensive calculations",
33
- "body": "Use useMemo for expensive computations and React.memo for components that receive the same props frequently. Don't memo everything — only what's measurably slow.",
34
- "confidence": 0.6,
35
- "domain": "patterns"
36
- },
37
- {
38
- "id": "react-form-validation",
39
- "trigger": "when building forms",
40
- "body": "Check if react-hook-form or zod is already in the project before building custom form validation. Prefer library solutions over hand-rolled validation.",
41
- "confidence": 0.65,
42
- "domain": "tooling"
43
- },
44
- {
45
- "id": "react-error-boundary",
46
- "trigger": "when adding a new page or route",
47
- "body": "Ensure error boundaries exist around new pages/routes. In Next.js App Router, add error.tsx. In other React apps, wrap with ErrorBoundary component.",
48
- "confidence": 0.6,
49
- "domain": "patterns"
50
- },
51
- {
52
- "id": "react-test-user-behavior",
53
- "trigger": "when writing React component tests",
54
- "body": "Test user behavior, not implementation details. Use @testing-library/react. Query by role, label, or text — not by class name or test ID.",
55
- "confidence": 0.65,
56
- "domain": "testing"
57
- }
58
- ]
1
+ [
2
+ {
3
+ "id": "react-check-existing-components",
4
+ "trigger": "when creating a new React component",
5
+ "body": "Search the codebase for existing components that solve the same problem before creating new ones. Check shared/, components/, and ui/ directories.",
6
+ "confidence": 0.65,
7
+ "domain": "workflow"
8
+ },
9
+ {
10
+ "id": "react-prefer-server-components",
11
+ "trigger": "when creating components in Next.js App Router",
12
+ "body": "Default to Server Components. Only add 'use client' when the component needs useState, useEffect, event handlers, or browser APIs.",
13
+ "confidence": 0.7,
14
+ "domain": "patterns"
15
+ },
16
+ {
17
+ "id": "react-key-prop-lists",
18
+ "trigger": "when rendering lists with .map()",
19
+ "body": "Always use a stable, unique key prop. Never use array index as key unless the list is static and never reordered.",
20
+ "confidence": 0.7,
21
+ "domain": "patterns"
22
+ },
23
+ {
24
+ "id": "react-effect-cleanup",
25
+ "trigger": "when writing useEffect with subscriptions or timers",
26
+ "body": "Always return a cleanup function from useEffect when setting up subscriptions, event listeners, or timers to prevent memory leaks.",
27
+ "confidence": 0.7,
28
+ "domain": "patterns"
29
+ },
30
+ {
31
+ "id": "react-memo-expensive",
32
+ "trigger": "when a component re-renders with expensive calculations",
33
+ "body": "Use useMemo for expensive computations and React.memo for components that receive the same props frequently. Don't memo everything — only what's measurably slow.",
34
+ "confidence": 0.6,
35
+ "domain": "patterns"
36
+ },
37
+ {
38
+ "id": "react-form-validation",
39
+ "trigger": "when building forms",
40
+ "body": "Check if react-hook-form or zod is already in the project before building custom form validation. Prefer library solutions over hand-rolled validation.",
41
+ "confidence": 0.65,
42
+ "domain": "tooling"
43
+ },
44
+ {
45
+ "id": "react-error-boundary",
46
+ "trigger": "when adding a new page or route",
47
+ "body": "Ensure error boundaries exist around new pages/routes. In Next.js App Router, add error.tsx. In other React apps, wrap with ErrorBoundary component.",
48
+ "confidence": 0.6,
49
+ "domain": "patterns"
50
+ },
51
+ {
52
+ "id": "react-test-user-behavior",
53
+ "trigger": "when writing React component tests",
54
+ "body": "Test user behavior, not implementation details. Use @testing-library/react. Query by role, label, or text — not by class name or test ID.",
55
+ "confidence": 0.65,
56
+ "domain": "testing"
57
+ }
58
+ ]
File without changes
@@ -2,20 +2,25 @@
2
2
  * Gateguard per-session state.
3
3
  *
4
4
  * State file: <sessionDir>/gateguard-session.json. sessionDir resolves to
5
- * GATEGUARD_SESSION_DIR (env override, used by tests) or
6
- * ~/.claude/instincts/<projectHash>/ in production.
5
+ * GATEGUARD_SESSION_DIR (env override, used by tests), or in production to
6
+ * ~/.claude/instincts/<projectHash>/sessions/<sessionId>/ when the hook passes
7
+ * the stdin session_id, falling back to ~/.claude/instincts/<projectHash>/ when
8
+ * no session id is available. Per-session scoping is what stops the clearance
9
+ * cap from bleeding across concurrent same-day sessions on a multi-Claude host.
7
10
  *
8
- * V1 limitations (documented for honesty, not mitigated in code):
11
+ * Limitations and guarantees:
9
12
  * - Honor system: clearance is granted whenever the agent sets
10
13
  * `_gateguard_facts_presented: true` in tool_input or has a prior per-file
11
14
  * marker. The hook cannot verify that real investigation occurred.
12
15
  * - State-file deletion: rm'ing the state file resets every gate in the
13
- * session. Defensible because the session itself is the trust boundary;
14
- * the cap below limits cumulative damage.
16
+ * session. Defensible because the session is the trust boundary — and with
17
+ * per-session scoping that boundary is now real, not just asserted.
18
+ * - Self-heal: loadState treats a file older than STATE_TTL_MS (or one with an
19
+ * unparseable created_at) as empty, so a stale gate never needs a manual rm.
15
20
  * - Concurrency: two parallel hook invocations can race the read+write.
16
21
  * Acceptable trade-off vs OS-specific atomic-rename complexity on Windows.
17
- * - Cap: MAX_CLEARED_FILES caps the number of distinct files a single
18
- * session can clear, bounding stuck-loop / rogue-agent damage.
22
+ * - Cap: MAX_CLEARED_FILES caps the distinct files ONE session can clear,
23
+ * bounding stuck-loop / rogue-agent damage without affecting other sessions.
19
24
  */
20
25
  import { createHash } from "node:crypto";
21
26
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -23,14 +28,36 @@ import { execFileSync } from "node:child_process";
23
28
  import { homedir } from "node:os";
24
29
  import { join } from "node:path";
25
30
  export const MAX_CLEARED_FILES = 50;
26
- export function resolveSessionDir() {
31
+ // A clearance file whose created_at is older than this self-heals to empty on
32
+ // the next load, so a stale gate never needs a manual rm. Long enough not to
33
+ // reset an active session mid-work; the real cross-session fix is the
34
+ // per-session dir in resolveSessionDir below.
35
+ export const STATE_TTL_MS = 12 * 60 * 60 * 1000; // 12h
36
+ // The root that holds every project's gateguard + instinct state. The MCP
37
+ // clearance route uses this to bound a caller-supplied state_path to this tree
38
+ // so a hostile argument can't turn the clear into an arbitrary-write primitive.
39
+ export function resolveInstinctsRoot() {
40
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
41
+ return join(home, ".claude", "instincts");
42
+ }
43
+ export function resolveSessionDir(sessionId) {
27
44
  const fromEnv = process.env.GATEGUARD_SESSION_DIR;
28
45
  if (fromEnv)
29
46
  return fromEnv;
30
- const home = process.env.HOME || process.env.USERPROFILE || homedir();
31
47
  const projectRoot = resolveProjectRoot();
32
48
  const projectHash = createHash("sha256").update(canonicalizeProjectRoot(projectRoot)).digest("hex").slice(0, 12);
33
- return join(home, ".claude", "instincts", projectHash);
49
+ const base = join(resolveInstinctsRoot(), projectHash);
50
+ const scoped = sanitizeSessionId(sessionId);
51
+ return scoped ? join(base, "sessions", scoped) : base;
52
+ }
53
+ // A session id flows into a directory name. Strip anything outside a safe
54
+ // alphabet so a hostile or malformed id can't traverse (`../`) or break the
55
+ // path, and cap the length. Returns "" for an absent/empty id, which the caller
56
+ // treats as "no session" — the legacy unscoped dir, preserving back-compat.
57
+ function sanitizeSessionId(sessionId) {
58
+ if (!sessionId)
59
+ return "";
60
+ return sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
34
61
  }
35
62
  function resolveProjectRoot() {
36
63
  const fromEnv = process.env.CLAUDE_PROJECT_DIR;
@@ -49,21 +76,43 @@ function resolveProjectRoot() {
49
76
  }
50
77
  return "global";
51
78
  }
79
+ function freshState() {
80
+ return { created_at: new Date().toISOString(), cleared_files: {} };
81
+ }
82
+ // A clearance file self-heals to empty once it ages out of STATE_TTL_MS, so a
83
+ // stale gate never needs a manual rm. An unparseable created_at fails closed
84
+ // (treated as expired) rather than living forever as a never-resetting gate.
85
+ function isExpired(createdAt) {
86
+ const created = Date.parse(createdAt);
87
+ if (Number.isNaN(created))
88
+ return true;
89
+ return Date.now() - created > STATE_TTL_MS;
90
+ }
52
91
  export function loadState(sessionDir) {
53
92
  const path = join(sessionDir, "gateguard-session.json");
54
93
  if (!existsSync(path)) {
55
- return { created_at: new Date().toISOString(), cleared_files: {} };
94
+ return freshState();
56
95
  }
57
96
  try {
58
97
  const raw = readFileSync(path, "utf8");
59
98
  const parsed = JSON.parse(raw);
99
+ // Absent created_at is treated as expired (fail closed), like an unparseable
100
+ // one — never a never-resetting gate. On expiry we persist the new epoch
101
+ // immediately so the cap re-applies from zero on the very next load;
102
+ // otherwise every load before the next clear would independently reset and a
103
+ // TTL-crossing session could clear well past MAX_CLEARED_FILES.
104
+ if (!parsed.created_at || isExpired(parsed.created_at)) {
105
+ const reset = freshState();
106
+ saveState(sessionDir, reset);
107
+ return reset;
108
+ }
60
109
  return {
61
- created_at: parsed.created_at ?? new Date().toISOString(),
110
+ created_at: parsed.created_at,
62
111
  cleared_files: parsed.cleared_files ?? {},
63
112
  };
64
113
  }
65
114
  catch {
66
- return { created_at: new Date().toISOString(), cleared_files: {} };
115
+ return freshState();
67
116
  }
68
117
  }
69
118
  export function saveState(sessionDir, state) {
File without changes
@@ -20,10 +20,14 @@ const STOPWORDS = new Set([
20
20
  "task", "goal", "plan", "work", "working", "build", "building", "code",
21
21
  ]);
22
22
  // 4 suits Latin/Cyrillic/Thai prose (Thai combining marks are now kept; see
23
- // extractKeywordsFromProse regex), but it silently drops most Korean words
24
- // (typically 2 chars). A script-aware floor for Korean is a logged follow-up
25
- // (see CLAUDE.md Deferred).
23
+ // extractKeywordsFromProse regex). Korean Hangul words are typically 2-3 chars
24
+ // but represent full concepts, so they get a script-aware floor of 2.
26
25
  const KEYWORD_MIN_LENGTH = 4;
26
+ function keywordMinLengthFor(word) {
27
+ if (/\p{Script=Hangul}/u.test(word))
28
+ return 2;
29
+ return KEYWORD_MIN_LENGTH;
30
+ }
27
31
  const KEYWORD_CAP = 20;
28
32
  const DEFAULT_WINDOW = 30;
29
33
  const DEFAULT_THRESHOLD = 0.3;
@@ -61,7 +65,7 @@ export function extractKeywordsFromProse(prose) {
61
65
  const out = [];
62
66
  for (const raw of prose.toLowerCase().split(/[^\p{L}\p{N}\p{M}]+/u)) {
63
67
  const word = raw.trim();
64
- if (word.length < KEYWORD_MIN_LENGTH)
68
+ if (word.length < keywordMinLengthFor(word))
65
69
  continue;
66
70
  if (/^\p{N}+$/u.test(word))
67
71
  continue;
@@ -0,0 +1,65 @@
1
+ // hook-pack-gate.mts — Pure decision core for the enforcing-hook pack (v1).
2
+ //
3
+ // No I/O. Given a raw Bash command string, the resolved current branch, the
4
+ // staged-file count, and the operator's mode, decide allow / warn / block.
5
+ // The hook (src/hooks/hook-pack.mts) wires stdin + git subprocess calls around
6
+ // these functions; keeping them pure lets the unit tests cover every branch
7
+ // without spawning anything. Lives in lib/ so tests can import it (the
8
+ // test-imports-only invariant forbids importing from hooks/).
9
+ //
10
+ // Fail-open by construction: callers pass safe defaults (null branch, 0 staged)
11
+ // on any failure, and an unrecognized mode resolves to warn.
12
+ export const COMMIT_FILE_LIMIT = 15;
13
+ const ALLOW = { action: "allow", reason: "" };
14
+ export function parseMode(raw) {
15
+ const v = (raw ?? "warn").trim().toLowerCase();
16
+ return v === "block" || v === "off" ? v : "warn";
17
+ }
18
+ // Match `git push` / `git commit` at the start of the command or immediately
19
+ // after a shell separator (newline, ;, |, &). This deliberately does NOT match
20
+ // a mention like `echo git push`, where git is an argument, not the command.
21
+ export function classifyCommand(command) {
22
+ if (/(?:^|[\n;|&])\s*git\s+push\b/.test(command))
23
+ return "push";
24
+ if (/(?:^|[\n;|&])\s*git\s+commit\b/.test(command))
25
+ return "commit";
26
+ return "other";
27
+ }
28
+ export function isProtectedBranch(branch) {
29
+ return branch === "main" || branch === "master" || /^release\//.test(branch);
30
+ }
31
+ // Extract the explicit destination branch of a `git push`, or null when none is
32
+ // given (the caller should then fall back to the current branch). Handles
33
+ // `git push <remote> <branch>`, `git push -u <remote> <branch>`, and refspecs
34
+ // like `HEAD:main` (destination after the colon).
35
+ function parsePushTarget(command) {
36
+ const tokens = command.split(/\s+/).filter(Boolean);
37
+ const pushIdx = tokens.findIndex((t, i) => t === "push" && tokens[i - 1] === "git");
38
+ if (pushIdx === -1)
39
+ return null;
40
+ const operands = tokens.slice(pushIdx + 1).filter((t) => !t.startsWith("-"));
41
+ // operands[0] is the remote; operands[1] is the refspec. Fewer than two means
42
+ // no explicit branch (e.g. `git push` or `git push origin`).
43
+ if (operands.length < 2)
44
+ return null;
45
+ const refspec = operands[1];
46
+ return refspec.includes(":") ? (refspec.split(":").pop() ?? null) : refspec;
47
+ }
48
+ export function evaluatePush(command, currentBranch) {
49
+ if (/(?:^|\s)--dry-run\b/.test(command))
50
+ return { gated: false, branch: null };
51
+ const branch = parsePushTarget(command) ?? currentBranch;
52
+ if (!branch)
53
+ return { gated: false, branch: null };
54
+ return { gated: isProtectedBranch(branch), branch };
55
+ }
56
+ export function evaluateCommitSize(command, stagedCount) {
57
+ if (/(?:^|\s)--amend\b/.test(command))
58
+ return { gated: false };
59
+ return { gated: stagedCount > COMMIT_FILE_LIMIT };
60
+ }
61
+ export function decide(mode, gated, reason) {
62
+ if (!gated || mode === "off")
63
+ return ALLOW;
64
+ return { action: mode === "block" ? "block" : "warn", reason };
65
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Multi-platform install targets for the 7-Laws skill.
3
+ *
4
+ * Pure planning layer — no filesystem access. The installer
5
+ * (src/bin/install.mts) feeds in the SKILL.md text plus a reader callback and
6
+ * executes the returned write plan against the project root. Keeping this
7
+ * pure lets the per-target contracts be unit-tested without temp dirs.
8
+ *
9
+ * v1 contract (docs/plans/2026-06-11-multi-platform-installer.md):
10
+ * - Non-Claude targets receive the skill text only; hooks, the MCP server,
11
+ * and instinct learning stay Claude Code-only and every plan says so.
12
+ * - Shared files (GEMINI.md, AGENTS.md, .rules, CONVENTIONS.md,
13
+ * copilot-instructions.md) are merged through a managed block so existing
14
+ * user content is never clobbered and reinstalls are idempotent.
15
+ * - Own-file targets (cursor, windsurf) are written whole.
16
+ */
17
+ export const MANAGED_BEGIN = "<!-- continuous-improvement:begin (managed block — reinstall via `npx continuous-improvement install --target <name>`; edits inside are overwritten) -->";
18
+ export const MANAGED_END = "<!-- continuous-improvement:end -->";
19
+ const TARGET_SPECS = {
20
+ gemini: { label: "Gemini CLI", mode: "managed", relPath: "GEMINI.md" },
21
+ codex: { label: "Codex CLI (agents.md standard)", mode: "managed", relPath: "AGENTS.md" },
22
+ cursor: { label: "Cursor", mode: "whole", relPath: ".cursor/rules/continuous-improvement.mdc" },
23
+ windsurf: { label: "Windsurf", mode: "whole", relPath: ".windsurf/rules/continuous-improvement.md" },
24
+ zed: { label: "Zed", mode: "managed", relPath: ".rules" },
25
+ aider: { label: "Aider", mode: "managed", relPath: "CONVENTIONS.md" },
26
+ copilot: { label: "GitHub Copilot", mode: "managed", relPath: ".github/copilot-instructions.md" },
27
+ };
28
+ export const TARGET_IDS = ["claude", ...Object.keys(TARGET_SPECS)];
29
+ export function resolveTargets(csv) {
30
+ const targets = [];
31
+ const unknown = [];
32
+ for (const raw of csv.split(",")) {
33
+ const name = raw.trim().toLowerCase();
34
+ if (name === "")
35
+ continue;
36
+ if (!TARGET_IDS.includes(name)) {
37
+ if (!unknown.includes(name))
38
+ unknown.push(name);
39
+ continue;
40
+ }
41
+ if (!targets.includes(name))
42
+ targets.push(name);
43
+ }
44
+ return { targets, unknown };
45
+ }
46
+ export function stripFrontmatter(skillMd) {
47
+ if (!skillMd.startsWith("---\n") && !skillMd.startsWith("---\r\n"))
48
+ return skillMd;
49
+ const close = skillMd.indexOf("\n---", 3);
50
+ if (close === -1)
51
+ return skillMd;
52
+ const afterClose = skillMd.indexOf("\n", close + 1);
53
+ if (afterClose === -1)
54
+ return "";
55
+ return skillMd.slice(afterClose + 1).replace(/^\s*\n/, "");
56
+ }
57
+ export function mergeManagedBlock(existing, block) {
58
+ if (existing === null || existing.trim() === "") {
59
+ return `${block}\n`;
60
+ }
61
+ const beginAt = existing.indexOf(MANAGED_BEGIN);
62
+ const endAt = existing.indexOf(MANAGED_END);
63
+ if (beginAt !== -1 && endAt !== -1 && endAt > beginAt) {
64
+ const before = existing.slice(0, beginAt);
65
+ const after = existing.slice(endAt + MANAGED_END.length);
66
+ return `${before}${block}${after}`;
67
+ }
68
+ return `${existing.trimEnd()}\n\n${block}\n`;
69
+ }
70
+ function renderManagedBlock(skillMd) {
71
+ return `${MANAGED_BEGIN}\n${stripFrontmatter(skillMd).trim()}\n${MANAGED_END}`;
72
+ }
73
+ function renderCursorRule(skillMd) {
74
+ return [
75
+ "---",
76
+ "description: 7 Laws of AI Agent Discipline — continuous-improvement framework",
77
+ "alwaysApply: true",
78
+ "---",
79
+ "",
80
+ stripFrontmatter(skillMd).trim(),
81
+ "",
82
+ ].join("\n");
83
+ }
84
+ const AIDER_CONF_PATH = ".aider.conf.yml";
85
+ const AIDER_MINIMAL_CONF = [
86
+ "# Written by `npx continuous-improvement install --target aider`.",
87
+ "# Loads the 7 Laws conventions into every Aider session.",
88
+ "read: [CONVENTIONS.md]",
89
+ "",
90
+ ].join("\n");
91
+ export function planTargetWrites(targetId, skillMd, readExisting) {
92
+ if (targetId === "claude") {
93
+ throw new Error("claude is handled by the standard installer flow, not planTargetWrites");
94
+ }
95
+ const spec = TARGET_SPECS[targetId];
96
+ if (!spec) {
97
+ throw new Error(`Unknown install target: ${targetId}. Valid: ${TARGET_IDS.join(", ")}`);
98
+ }
99
+ const writes = [];
100
+ const notes = [
101
+ `${spec.label} gets the 7-Laws skill text only — hooks, the MCP server, and instinct learning are Claude Code-only.`,
102
+ ];
103
+ if (spec.mode === "whole") {
104
+ const content = targetId === "cursor" ? renderCursorRule(skillMd) : `${stripFrontmatter(skillMd).trim()}\n`;
105
+ writes.push({ relPath: spec.relPath, content });
106
+ }
107
+ else {
108
+ const block = renderManagedBlock(skillMd);
109
+ writes.push({ relPath: spec.relPath, content: mergeManagedBlock(readExisting(spec.relPath), block) });
110
+ }
111
+ if (targetId === "aider") {
112
+ const existingConf = readExisting(AIDER_CONF_PATH);
113
+ if (existingConf === null) {
114
+ writes.push({ relPath: AIDER_CONF_PATH, content: AIDER_MINIMAL_CONF });
115
+ }
116
+ else {
117
+ notes.push(`${AIDER_CONF_PATH} already exists — add CONVENTIONS.md to its \`read:\` list yourself (e.g. \`read: [CONVENTIONS.md]\`); the installer never rewrites an existing conf.`);
118
+ }
119
+ }
120
+ return { notes, writes };
121
+ }
File without changes
@@ -0,0 +1,96 @@
1
+ /**
2
+ * plan-review-packet: turn a plan doc into a commentable review packet.
3
+ *
4
+ * Pure markdown transform — no IO. Splits a `docs/plans/*.md` plan into
5
+ * anchored sections, extracts a TLDR, and renders a packet a reviewer can
6
+ * comment on inline (one `> Comment (R#):` slot per section).
7
+ */
8
+ const TLDR_HEADINGS = new Set(["goal", "summary", "overview", "tldr"]);
9
+ const HEADING_RE = /^(#{1,6})\s+(.*\S)\s*$/;
10
+ function normalize(markdown) {
11
+ return markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
12
+ }
13
+ /** Split a plan into reviewable level-2/3 sections, each with a stable [R#] anchor. */
14
+ export function splitSections(markdown) {
15
+ const lines = normalize(markdown).split("\n");
16
+ const sections = [];
17
+ let current = null;
18
+ let counter = 0;
19
+ const flush = () => {
20
+ if (!current)
21
+ return;
22
+ counter += 1;
23
+ sections.push({
24
+ heading: current.heading,
25
+ level: current.level,
26
+ anchor: `R${counter}`,
27
+ body: current.body.join("\n").trim(),
28
+ });
29
+ current = null;
30
+ };
31
+ for (const line of lines) {
32
+ const match = HEADING_RE.exec(line);
33
+ const level = match ? match[1].length : 0;
34
+ if (level === 1) {
35
+ flush();
36
+ }
37
+ else if (level === 2 || level === 3) {
38
+ flush();
39
+ current = { heading: match[2].trim(), level, body: [] };
40
+ }
41
+ else if (current) {
42
+ current.body.push(line);
43
+ }
44
+ }
45
+ flush();
46
+ return sections;
47
+ }
48
+ /** First non-empty, non-heading paragraph of a block of markdown. */
49
+ function firstParagraph(text) {
50
+ for (const block of text.split(/\n\s*\n/)) {
51
+ const kept = block
52
+ .split("\n")
53
+ .map((l) => l.trim())
54
+ .filter((l) => l.length > 0 && !/^#{1,6}(\s|$)/.test(l));
55
+ if (kept.length > 0)
56
+ return kept.join("\n");
57
+ }
58
+ return "";
59
+ }
60
+ /** Extract a one-glance TLDR: a Goal/Summary/Overview section, else the first paragraph. */
61
+ export function extractTldr(markdown) {
62
+ const goal = splitSections(markdown).find((s) => TLDR_HEADINGS.has(s.heading.trim().toLowerCase()));
63
+ if (goal)
64
+ return firstParagraph(goal.body);
65
+ return firstParagraph(normalize(markdown));
66
+ }
67
+ function extractTitle(markdown) {
68
+ for (const line of normalize(markdown).split("\n")) {
69
+ const match = /^#\s+(.*\S)\s*$/.exec(line);
70
+ if (match)
71
+ return match[1].trim();
72
+ }
73
+ return "Plan Review";
74
+ }
75
+ /** Render a commentable review packet from a plan doc. */
76
+ export function buildReviewPacket(markdown, options = {}) {
77
+ const title = extractTitle(markdown);
78
+ const tldr = extractTldr(markdown);
79
+ const sections = splitSections(markdown);
80
+ const out = [`# Review packet: ${title}`, ""];
81
+ if (options.source)
82
+ out.push(`Source: ${options.source}`);
83
+ out.push(`TLDR: ${tldr || "(none — skim the sections below)"}`, "");
84
+ out.push("> How to comment: reply under each `> Comment (R#):` line. The `[R#]` anchors are stable references you can cite back.", "", "---", "");
85
+ if (sections.length === 0) {
86
+ out.push("_No reviewable sections found in this plan._", "");
87
+ return out.join("\n");
88
+ }
89
+ for (const section of sections) {
90
+ out.push(`${"#".repeat(section.level)} ${section.heading} [${section.anchor}]`, "");
91
+ if (section.body)
92
+ out.push(section.body, "");
93
+ out.push(`> Comment (${section.anchor}):`, "");
94
+ }
95
+ return out.join("\n");
96
+ }
@@ -26,7 +26,7 @@ const KEYWORDS = [
26
26
  "transcript-linter",
27
27
  ];
28
28
  const CLAUDE_PLUGIN_CATEGORY = "productivity";
29
- const SHARED_PLUGIN_DESCRIPTION = "Makes Claude Code reason harder, recall past corrections, and learn from every session so its competence compounds run over run. The Mulahazah engine turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 25 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.";
29
+ const SHARED_PLUGIN_DESCRIPTION = "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and through the Mulahazah engine turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 26 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.";
30
30
  // Four vendored upstream companions registered alongside the CI plugin.
31
31
  // Each entry points at a pinned-SHA snapshot under third-party/<name>/.
32
32
  // See third-party/MANIFEST.md for refresh recipes and per-snapshot
@@ -143,6 +143,10 @@ const BEGINNER_TOOL_ENTRIES = [
143
143
  type: "string",
144
144
  description: "A single file path to clear (alternative to file_paths)",
145
145
  },
146
+ state_path: {
147
+ type: "string",
148
+ description: "State-file path from the GateGuard block reason (the session-scoped gateguard-session.json). Pass it verbatim so the clearance lands in the session the hook is reading; it must resolve inside ~/.claude/instincts/. Omitting it falls back to the unscoped canonical dir — safe only in legacy (no session id) contexts; when the hook is session-scoped, omitting it writes to the wrong dir and the retry will still block.",
149
+ },
146
150
  },
147
151
  required: [],
148
152
  },
@@ -373,10 +377,16 @@ const EXPERT_TOOL_ENTRIES = [
373
377
  required: ["id"],
374
378
  },
375
379
  },
380
+ {
381
+ name: "ci_distill_from_workflow",
382
+ description: "Draft a reusable instinct from the most recent completed-and-verified native Workflow run in this project's observation feed. A Workflow script is an authored recipe, so a single run whose output passed verification is enough — unlike ci_distill_candidates, which needs a pattern repeated across sessions. Writes a DRAFT to drafts/ (a skeleton you edit); it changes no behavior until promoted with ci_distill_promote. Returns a message when no verified workflow run is found.",
383
+ manifestWhat: "Draft a reusable instinct from a verified Workflow run",
384
+ inputSchema: { type: "object", properties: {}, required: [] },
385
+ },
376
386
  ];
377
387
  const MODE_METADATA = {
378
388
  beginner: {
379
- description: "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles four grounding skills (gateguard, para-memory-files, tdd-workflow, verification-loop) so research, memory, tests, and verification happen by default — every edit starts from facts, not guesses.",
389
+ description: "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles three grounding skills (gateguard, tdd-workflow, verification-loop) so research, memory, tests, and verification happen by default — every edit starts from facts, not guesses.",
380
390
  hooks: ["PreToolUse", "PostToolUse", "UserPromptSubmit"],
381
391
  hookDescription: "Silently captures every tool call as observations and routes prompts to the matching skill via the route table. Lightweight and non-blocking.",
382
392
  },
@@ -457,6 +467,11 @@ export function getPluginHooksConfig() {
457
467
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/companion-preference.mjs\"",
458
468
  timeout: 5,
459
469
  };
470
+ const hookPackCommand = {
471
+ type: "command",
472
+ command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-pack.mjs\"",
473
+ timeout: 5,
474
+ };
460
475
  const observeCommand = {
461
476
  type: "command",
462
477
  command: "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/observe.sh\"",
@@ -477,6 +492,11 @@ export function getPluginHooksConfig() {
477
492
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/goal-drift-stop.mjs\"",
478
493
  timeout: 5,
479
494
  };
495
+ const workflowDistillCommand = {
496
+ type: "command",
497
+ command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
498
+ timeout: 5,
499
+ };
480
500
  const routePromptCommand = {
481
501
  type: "command",
482
502
  command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
@@ -488,7 +508,7 @@ export function getPluginHooksConfig() {
488
508
  timeout: 5,
489
509
  };
490
510
  return {
491
- description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
511
+ description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
492
512
  hooks: {
493
513
  // gateguard runs FIRST on PreToolUse so its block decision short-circuits
494
514
  // before companion-preference sees the call. companion-preference runs
@@ -501,12 +521,18 @@ export function getPluginHooksConfig() {
501
521
  // through with no output.
502
522
  PreToolUse: [
503
523
  { hooks: [gateguardCommand, companionPreferenceCommand] },
524
+ // hook-pack gates Bash `git push` to protected branches and oversized
525
+ // `git commit`s. The "Bash" matcher scopes it off the hot path for all
526
+ // non-Bash tools, so the two-subprocess note above still holds for
527
+ // Edit/Read/etc. Warn-default (CLAUDE_CI_HOOKPACK_GATE) — never blocks
528
+ // until the operator opts in.
529
+ { matcher: "Bash", hooks: [hookPackCommand] },
504
530
  ],
505
531
  PostToolUse: [{ hooks: [observeCommand] }],
506
532
  UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
507
533
  SessionStart: [{ hooks: [sessionCommand] }],
508
534
  SessionEnd: [{ hooks: [sessionCommand] }],
509
- Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand] }],
535
+ Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand] }],
510
536
  },
511
537
  };
512
538
  }
File without changes
File without changes
File without changes
File without changes