truecourse 0.5.1 → 0.5.2

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 CHANGED
@@ -90,12 +90,26 @@ truecourse rules llm --disable # Disable LLM rules
90
90
 
91
91
  ### Git Hooks
92
92
 
93
- TrueCourse can install a pre-commit hook that blocks commits with critical violations:
93
+ TrueCourse can install a pre-commit hook that blocks commits introducing new violations at or above a configured severity:
94
94
 
95
95
  ```bash
96
96
  truecourse hooks install # Install pre-commit hook
97
97
  truecourse hooks uninstall # Remove pre-commit hook
98
- truecourse hooks status # Show hook installation status
98
+ truecourse hooks status # Show hook status + config
99
+ ```
100
+
101
+ On every commit the hook runs `truecourse analyze --diff` against the repo's last full analysis and blocks if any newly-introduced violation matches the configured block severities. **Commits will take as long as a full diff analysis** — on large repos that can be tens of seconds per commit. `truecourse hooks install` warns you and requires confirmation before writing the hook.
102
+
103
+ **First-time setup:** run `truecourse analyze` once to establish a baseline. Without it the hook can't diff.
104
+
105
+ **Bypass:** `git commit --no-verify` (standard git).
106
+
107
+ **Configuration** — `hooks install` seeds `<repo>/.truecourse/hooks.yaml` with starter defaults; commit the file so your team shares one policy. The hook reads only from this file — if you delete it, the hook warns and passes every commit (no hidden code-level defaults). Current shape:
108
+
109
+ ```yaml
110
+ pre-commit:
111
+ block-on: [critical, high] # severities. Valid: info|low|medium|high|critical
112
+ llm: false # run LLM rules on every commit (tokens per commit)
99
113
  ```
100
114
 
101
115
  ### Telemetry
@@ -131,13 +145,14 @@ TrueCourse ships with **1,200+ deterministic rules** and **100 LLM rules** acros
131
145
 
132
146
  TrueCourse includes [Claude Code skills](https://docs.anthropic.com/en/docs/claude-code/skills) for conversational analysis from within Claude Code.
133
147
 
134
- Run `truecourse add` to install skills to `.claude/skills/truecourse/` in your project.
148
+ The first `truecourse analyze` (or `truecourse add`) in a fresh repo asks whether to install skills into `.claude/skills/truecourse/`. Re-runs skip the prompt if skills are already present. Pass `--install-skills` / `--no-skills` to bypass the prompt explicitly.
135
149
 
136
150
  | Skill | What it does |
137
151
  |---|---|
138
152
  | `/truecourse-analyze` | Runs analysis or diff check, summarizes results |
139
153
  | `/truecourse-list` | Shows full violation details |
140
154
  | `/truecourse-fix` | Lists fixable violations, applies changes |
155
+ | `/truecourse-hooks` | Installs, configures, or removes the pre-commit hook |
141
156
 
142
157
  ## Language Support
143
158
 
@@ -172,6 +187,10 @@ pnpm build # Build all packages
172
187
 
173
188
  TrueCourse collects anonymous usage data (event type, language, file count range, OS). No source code, file paths, or violation details are collected. Opt out with `truecourse telemetry disable` or `TRUECOURSE_TELEMETRY=0`.
174
189
 
190
+ ## Contact
191
+
192
+ Questions, feedback, or security reports: **Mushegh Gevorgyan** — [mushegh@truecourse.dev](mailto:mushegh@truecourse.dev).
193
+
175
194
  ## License
176
195
 
177
196
  MIT
package/cli.mjs CHANGED
@@ -130434,6 +130434,11 @@ var jsYaml = {
130434
130434
  };
130435
130435
 
130436
130436
  // tools/cli/src/commands/hooks.ts
130437
+ init_dist4();
130438
+ init_paths();
130439
+ init_registry();
130440
+ init_analysis_store();
130441
+ init_diff_in_process();
130437
130442
  init_helpers();
130438
130443
  var HOOK_IDENTIFIER = "# TrueCourse pre-commit hook";
130439
130444
  var HOOK_SCRIPT = `#!/bin/sh
@@ -130441,29 +130446,35 @@ ${HOOK_IDENTIFIER}
130441
130446
  # Installed by: truecourse hooks install
130442
130447
  # Bypass with: git commit --no-verify
130443
130448
 
130444
- exec truecourse hooks run
130449
+ exec npx -y truecourse hooks run
130450
+ `;
130451
+ var SEVERITIES2 = ["info", "low", "medium", "high", "critical"];
130452
+ var HOOKS_YAML_TEMPLATE = `# TrueCourse pre-commit hook config.
130453
+ # Commit this file \u2014 it's the team-shared policy for what blocks a commit.
130454
+ # Check the live values with \`truecourse hooks status\`.
130455
+ pre-commit:
130456
+ # Severities that block a commit when the diff surfaces NEW violations
130457
+ # at that level. Valid: info, low, medium, high, critical.
130458
+ block-on:
130459
+ - critical
130460
+ - high
130461
+
130462
+ # Run LLM-powered rules on every commit? Off by default (no tokens per
130463
+ # commit). Set to true for deeper semantic checks at the commit gate \u2014
130464
+ # each commit will then cost tokens.
130465
+ llm: false
130445
130466
  `;
130446
- var DEFAULT_BLOCK_ON = [
130447
- "security/deterministic/hardcoded-secret",
130448
- { severity: "critical" }
130449
- ];
130450
- var DEFAULT_TIMEOUT_MS = 3e4;
130451
130467
  function findGitDir(from) {
130452
130468
  let dir = from;
130453
130469
  while (true) {
130454
130470
  const gitPath = path21.join(dir, ".git");
130455
130471
  if (fs16.existsSync(gitPath)) {
130456
130472
  const stat = fs16.statSync(gitPath);
130457
- if (stat.isDirectory()) {
130458
- return gitPath;
130459
- }
130473
+ if (stat.isDirectory()) return gitPath;
130460
130474
  if (stat.isFile()) {
130461
130475
  const content = fs16.readFileSync(gitPath, "utf-8").trim();
130462
130476
  const match2 = content.match(/^gitdir:\s*(.+)$/);
130463
- if (match2) {
130464
- const resolved = path21.resolve(dir, match2[1]);
130465
- return resolved;
130466
- }
130477
+ if (match2) return path21.resolve(dir, match2[1]);
130467
130478
  }
130468
130479
  }
130469
130480
  const parent = path21.dirname(dir);
@@ -130474,46 +130485,68 @@ function findGitDir(from) {
130474
130485
  function findProjectRoot(from) {
130475
130486
  let dir = from;
130476
130487
  while (true) {
130477
- if (fs16.existsSync(path21.join(dir, ".git"))) {
130478
- return dir;
130479
- }
130488
+ if (fs16.existsSync(path21.join(dir, ".git"))) return dir;
130480
130489
  const parent = path21.dirname(dir);
130481
130490
  if (parent === dir) return null;
130482
130491
  dir = parent;
130483
130492
  }
130484
130493
  }
130485
- function parseTimeout(value) {
130486
- const match2 = value.match(/^(\d+)\s*(s|ms|m)?$/);
130487
- if (!match2) return DEFAULT_TIMEOUT_MS;
130488
- const num = parseInt(match2[1], 10);
130489
- const unit = match2[2] || "s";
130490
- if (unit === "ms") return num;
130491
- if (unit === "m") return num * 6e4;
130492
- return num * 1e3;
130493
- }
130494
130494
  function loadConfig(projectRoot) {
130495
130495
  const configPath = path21.join(projectRoot, ".truecourse", "hooks.yaml");
130496
- if (!fs16.existsSync(configPath)) return {};
130496
+ if (!fs16.existsSync(configPath)) return null;
130497
+ let parsed;
130497
130498
  try {
130498
130499
  const raw = fs16.readFileSync(configPath, "utf-8");
130499
- return jsYaml.load(raw) || {};
130500
- } catch {
130501
- return {};
130500
+ parsed = jsYaml.load(raw) || {};
130501
+ } catch (err) {
130502
+ console.error(`Error parsing ${configPath}: ${err.message}`);
130503
+ process.exit(1);
130502
130504
  }
130505
+ const preCommit = parsed["pre-commit"] ?? {};
130506
+ const rawBlockOn = preCommit["block-on"];
130507
+ if (!Array.isArray(rawBlockOn)) {
130508
+ console.error(
130509
+ `Invalid ${configPath}: \`pre-commit.block-on\` must be an array of severity names.`
130510
+ );
130511
+ console.error(` Valid severities: ${SEVERITIES2.join(", ")}`);
130512
+ process.exit(1);
130513
+ }
130514
+ const invalid = rawBlockOn.filter(
130515
+ (s) => typeof s !== "string" || !SEVERITIES2.includes(s)
130516
+ );
130517
+ if (invalid.length > 0) {
130518
+ console.error(
130519
+ `Invalid ${configPath}: unknown value(s) in \`pre-commit.block-on\`: ${invalid.map((v) => JSON.stringify(v)).join(", ")}`
130520
+ );
130521
+ console.error(` Valid severities: ${SEVERITIES2.join(", ")}`);
130522
+ process.exit(1);
130523
+ }
130524
+ return {
130525
+ blockOn: rawBlockOn,
130526
+ llm: preCommit.llm === true,
130527
+ configPath
130528
+ };
130503
130529
  }
130504
- function getBlockRules(config2) {
130505
- return config2["pre-commit"]?.["block-on"] ?? DEFAULT_BLOCK_ON;
130506
- }
130507
- function getTimeoutMs(config2) {
130508
- const raw = config2["pre-commit"]?.timeout;
130509
- return raw ? parseTimeout(raw) : DEFAULT_TIMEOUT_MS;
130510
- }
130511
- function runHooksInstall() {
130530
+ var INSTALL_WARNING = "The pre-commit hook runs `truecourse analyze --diff` on every commit.\nCommits will take as long as a full diff analysis of this repo \u2014\non large repos that can be tens of seconds per commit.";
130531
+ async function runHooksInstall() {
130512
130532
  const gitDir = findGitDir(process.cwd());
130513
130533
  if (!gitDir) {
130514
130534
  console.error("Error: Not a git repository.");
130515
130535
  process.exit(1);
130516
130536
  }
130537
+ if (isInteractive()) {
130538
+ O2.warn(INSTALL_WARNING);
130539
+ const proceed = await ot2({
130540
+ message: "Install the pre-commit hook?",
130541
+ initialValue: false
130542
+ });
130543
+ if (q(proceed) || !proceed) {
130544
+ pt("Install cancelled.");
130545
+ process.exit(0);
130546
+ }
130547
+ } else {
130548
+ console.log(INSTALL_WARNING);
130549
+ }
130517
130550
  const hooksDir = path21.join(gitDir, "hooks");
130518
130551
  fs16.mkdirSync(hooksDir, { recursive: true });
130519
130552
  const hookPath = path21.join(hooksDir, "pre-commit");
@@ -130531,6 +130564,16 @@ function runHooksInstall() {
130531
130564
  fs16.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 493 });
130532
130565
  console.log("TrueCourse pre-commit hook installed.");
130533
130566
  console.log(` ${hookPath}`);
130567
+ const projectRoot = findProjectRoot(process.cwd());
130568
+ if (projectRoot) {
130569
+ const cfgDir = path21.join(projectRoot, ".truecourse");
130570
+ const cfgPath = path21.join(cfgDir, "hooks.yaml");
130571
+ if (!fs16.existsSync(cfgPath)) {
130572
+ fs16.mkdirSync(cfgDir, { recursive: true });
130573
+ fs16.writeFileSync(cfgPath, HOOKS_YAML_TEMPLATE);
130574
+ console.log(` ${cfgPath} (starter config \u2014 edit to customize, commit to share with the team)`);
130575
+ }
130576
+ }
130534
130577
  }
130535
130578
  function runHooksUninstall() {
130536
130579
  const gitDir = findGitDir(process.cwd());
@@ -130568,125 +130611,97 @@ function runHooksStatus() {
130568
130611
  }
130569
130612
  const projectRoot = findProjectRoot(process.cwd());
130570
130613
  if (projectRoot) {
130571
- const configPath = path21.join(projectRoot, ".truecourse", "hooks.yaml");
130572
- if (fs16.existsSync(configPath)) {
130573
- console.log(`
130574
- Config: ${configPath}`);
130575
- const config2 = loadConfig(projectRoot);
130576
- const blockRules = getBlockRules(config2);
130577
- console.log("Block on:");
130578
- for (const rule of blockRules) {
130579
- if (typeof rule === "string") {
130580
- console.log(` - ${rule}`);
130581
- } else {
130582
- console.log(` - severity: ${rule.severity}`);
130583
- }
130584
- }
130585
- const timeoutMs = getTimeoutMs(config2);
130586
- console.log(`Timeout: ${timeoutMs / 1e3}s`);
130614
+ const cfg = loadConfig(projectRoot);
130615
+ if (!cfg) {
130616
+ console.log(
130617
+ "\nNo `.truecourse/hooks.yaml` \u2014 hook has no policy. Run `truecourse hooks install` to generate one."
130618
+ );
130587
130619
  } else {
130588
- console.log("\nNo config file. Using defaults (block on: security/deterministic/hardcoded-secret, severity: critical).");
130589
- }
130590
- }
130591
- }
130592
- function shouldBlock(violation, blockRules) {
130593
- for (const rule of blockRules) {
130594
- if (typeof rule === "string") {
130595
- if (violation.ruleKey === rule || violation.ruleKey.endsWith(`/${rule}`)) {
130596
- return true;
130597
- }
130598
- } else if (rule.severity) {
130599
- if (violation.severity.toLowerCase() === rule.severity.toLowerCase()) {
130600
- return true;
130601
- }
130620
+ console.log(`
130621
+ Config: ${cfg.configPath}`);
130622
+ console.log(`Block on severities: ${cfg.blockOn.join(", ")}`);
130623
+ console.log(`LLM rules on commit: ${cfg.llm ? "enabled (tokens per commit)" : "disabled"}`);
130602
130624
  }
130603
130625
  }
130604
- return false;
130605
130626
  }
130606
130627
  async function runHooksRun() {
130607
- const startTime = Date.now();
130608
130628
  process.stdout.write("TrueCourse pre-commit check...");
130609
130629
  const projectRoot = findProjectRoot(process.cwd());
130610
130630
  if (!projectRoot) {
130611
130631
  console.log(" skipped (not a git repository)");
130612
130632
  process.exit(0);
130613
130633
  }
130614
- const config2 = loadConfig(projectRoot);
130615
- const blockRules = getBlockRules(config2);
130616
- const timeoutMs = getTimeoutMs(config2);
130617
- let stagedFiles;
130634
+ let hasStaged = false;
130618
130635
  try {
130619
130636
  const output = execSync5("git diff --cached --name-only --diff-filter=ACM", {
130620
130637
  encoding: "utf-8",
130621
130638
  cwd: projectRoot
130622
130639
  }).trim();
130623
- stagedFiles = output ? output.split("\n") : [];
130640
+ hasStaged = output.length > 0;
130624
130641
  } catch {
130625
130642
  console.log(" skipped (git error)");
130626
130643
  process.exit(0);
130627
130644
  }
130628
- if (stagedFiles.length === 0) {
130645
+ if (!hasStaged) {
130629
130646
  console.log(" \u2714 passed (no staged files)");
130630
130647
  process.exit(0);
130631
130648
  }
130632
- let parseCode2;
130633
- let detectLanguage2;
130634
- let checkCodeRules2;
130635
- let CODE_RULES2;
130636
- try {
130637
- const analyzer = await Promise.resolve().then(() => (init_dist6(), dist_exports2));
130638
- parseCode2 = analyzer.parseCode;
130639
- detectLanguage2 = analyzer.detectLanguage;
130640
- checkCodeRules2 = analyzer.checkCodeRules;
130641
- CODE_RULES2 = analyzer.CODE_RULES;
130642
- await analyzer.initParsers();
130643
- } catch {
130644
- console.log(" skipped (analyzer not available)");
130649
+ const cfg = loadConfig(projectRoot);
130650
+ if (!cfg) {
130651
+ console.log(" skipped");
130652
+ console.error(
130653
+ "No `.truecourse/hooks.yaml` found. The pre-commit hook has no policy to\nenforce \u2014 run `truecourse hooks install` to generate one."
130654
+ );
130645
130655
  process.exit(0);
130646
130656
  }
130647
- const supportedFiles = stagedFiles.filter((f) => detectLanguage2(f) !== null);
130648
- if (supportedFiles.length === 0) {
130649
- console.log(" \u2714 passed");
130650
- process.exit(0);
130657
+ const repoDir = resolveRepoDir(process.cwd());
130658
+ const project = repoDir ? getProjectByPath(repoDir) ?? registerProject(repoDir) : null;
130659
+ if (!project || !readLatest(project.path)) {
130660
+ console.log("");
130661
+ console.error(
130662
+ "No baseline analysis yet. Run `truecourse analyze` once in this repo before\nthe pre-commit hook can block new violations. Or bypass this commit with\n`git commit --no-verify`."
130663
+ );
130664
+ process.exit(1);
130651
130665
  }
130652
- const allViolations = [];
130653
- for (const filePath of supportedFiles) {
130654
- if (Date.now() - startTime > timeoutMs) {
130655
- console.log("\n Warning: timeout reached, skipping remaining files.");
130656
- break;
130657
- }
130658
- const language = detectLanguage2(filePath);
130659
- if (!language) continue;
130660
- let content;
130661
- try {
130662
- content = execSync5(`git show ":${filePath}"`, {
130663
- encoding: "utf-8",
130664
- cwd: projectRoot,
130665
- maxBuffer: 5 * 1024 * 1024
130666
- // 5MB
130667
- });
130668
- } catch {
130669
- continue;
130670
- }
130671
- try {
130672
- const tree = parseCode2(content, language);
130673
- const violations = checkCodeRules2(tree, filePath, content, CODE_RULES2, language);
130674
- allViolations.push(...violations);
130675
- } catch {
130676
- continue;
130666
+ const abortController = new AbortController();
130667
+ const onSigint = () => abortController.abort();
130668
+ process.on("SIGINT", onSigint);
130669
+ process.stdout.write(" running analysis...");
130670
+ let newViolations;
130671
+ try {
130672
+ const { diff } = await diffInProcess(project, {
130673
+ signal: abortController.signal,
130674
+ enableLlmRulesOverride: cfg.llm,
130675
+ // Pre-approved: the user opted into LLM-in-hook by setting llm: true
130676
+ // in hooks.yaml, so we don't re-prompt for the token cost estimate.
130677
+ onLlmEstimate: async () => true
130678
+ });
130679
+ newViolations = diff.newViolations;
130680
+ } catch (err) {
130681
+ console.log("");
130682
+ if (err instanceof DOMException && err.name === "AbortError") {
130683
+ console.error("Pre-commit check cancelled.");
130684
+ process.exit(130);
130677
130685
  }
130686
+ console.error(`Pre-commit check failed: ${err instanceof Error ? err.message : String(err)}`);
130687
+ process.exit(1);
130688
+ } finally {
130689
+ process.removeListener("SIGINT", onSigint);
130678
130690
  }
130679
- const blocking = allViolations.filter((v) => shouldBlock(v, blockRules));
130691
+ const blockSet = new Set(cfg.blockOn);
130692
+ const blocking = newViolations.filter((v) => blockSet.has(v.severity.toLowerCase()));
130680
130693
  if (blocking.length === 0) {
130681
- console.log(" \u2714 passed");
130694
+ console.log(` \u2714 passed (${newViolations.length} new violations, none at or above ${cfg.blockOn.join("/")})`);
130682
130695
  process.exit(0);
130683
130696
  }
130684
130697
  console.log("\n");
130685
130698
  for (const v of blocking) {
130686
130699
  const icon = severityIcon(v.severity);
130687
130700
  const color = severityColor(v.severity);
130701
+ const location = v.filePath ? `${v.filePath}${v.lineStart ? `:${v.lineStart}` : ""}` : "(no file)";
130688
130702
  console.log(` ${color(`${icon} BLOCKED`)}: ${v.title}`);
130689
- console.log(` ${v.filePath}:${v.lineStart} \u2014 ${v.content}`);
130703
+ console.log(` ${location} \u2014 ${v.content}`);
130704
+ if (v.fixPrompt) console.log(` Fix: ${v.fixPrompt}`);
130690
130705
  console.log("");
130691
130706
  }
130692
130707
  console.log("Commit blocked. Fix the issue or bypass with --no-verify.");
@@ -130695,7 +130710,7 @@ async function runHooksRun() {
130695
130710
 
130696
130711
  // tools/cli/src/index.ts
130697
130712
  var program2 = new Command();
130698
- program2.name("truecourse").version("0.5.1").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130713
+ program2.name("truecourse").version("0.5.2").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130699
130714
  var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").option("--reconfigure", "Re-prompt for console vs background service mode").option("--service", "Run as a background service (skips mode prompt)").option("--console", "Run in this terminal (skips mode prompt)").action(async (options) => {
130700
130715
  if (options.service && options.console) {
130701
130716
  console.error("error: --service and --console are mutually exclusive");
@@ -130774,8 +130789,8 @@ telemetryCmd.command("status").description("Show current telemetry status").acti
130774
130789
  }
130775
130790
  });
130776
130791
  var hooksCmd = program2.command("hooks").description("Manage git hooks");
130777
- hooksCmd.command("install").description("Install pre-commit hook").action(() => {
130778
- runHooksInstall();
130792
+ hooksCmd.command("install").description("Install pre-commit hook").action(async () => {
130793
+ await runHooksInstall();
130779
130794
  });
130780
130795
  hooksCmd.command("uninstall").description("Remove pre-commit hook").action(() => {
130781
130796
  runHooksUninstall();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecourse",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Visualize your codebase architecture as an interactive graph",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,10 @@
21
21
  "node-windows": "^1.0.0-beta.8"
22
22
  },
23
23
  "license": "MIT",
24
+ "author": {
25
+ "name": "Mushegh Gevorgyan",
26
+ "email": "mushegh@truecourse.dev"
27
+ },
24
28
  "repository": {
25
29
  "type": "git",
26
30
  "url": "https://github.com/truecourse-ai/truecourse"
@@ -18,7 +18,7 @@ Run architecture analysis on the current repository using TrueCourse.
18
18
  - **Full analysis** stashes any uncommitted changes, analyzes the clean working tree, then unstashes. The user's uncommitted work is preserved.
19
19
  - **Diff check** analyzes the full working tree (including uncommitted changes — it does NOT stash) and compares the result against the last full analysis baseline. The report lists violations newly introduced and violations resolved since that baseline. Prefer diff for in-progress work where the user is iterating on changes.
20
20
  - **Always invoke via `npx -y`** — without `-y`, npx will hang on the "Ok to proceed?" prompt whenever the user hasn't cached the latest `truecourse` version (which happens every time we publish a new release).
21
- - **LLM rules cost real money.** Never pass `--llm` without first relaying the cost estimate to the user and getting approval. See the LLM flow below.
21
+ - **LLM rules cost tokens.** Never pass `--llm` without first relaying the token estimate to the user and getting approval. See the LLM flow below.
22
22
 
23
23
  ## Instructions
24
24
 
@@ -30,7 +30,7 @@ Ask the user whether they want a **full analysis** or a **diff check**. If they
30
30
 
31
31
  ### 2. Decide on LLM rules
32
32
 
33
- LLM rules add higher-value insights but cost money per run. Ask the user one question: **"Run LLM-powered rules this time?"** If the user is unsure, offer to run deterministic-only first (free, fast) and add LLM later.
33
+ LLM rules add higher-value insights but cost tokens per run. Ask the user one question: **"Run LLM-powered rules this time?"** If the user is unsure, offer to run deterministic-only first (no tokens, fast) and add LLM later.
34
34
 
35
35
  - If **user approves LLM**: append `--llm` to the command.
36
36
  - If **user declines LLM or wants a free run**: append `--no-llm`.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: truecourse-hooks
3
+ description: Install, configure, or remove the TrueCourse pre-commit hook
4
+ user_invocable: true
5
+ triggers:
6
+ - install the pre-commit hook
7
+ - set up truecourse hooks
8
+ - enable truecourse hook
9
+ - check hook status
10
+ - remove pre-commit hook
11
+ - change what the hook blocks
12
+ - edit hook config
13
+ ---
14
+
15
+ # TrueCourse Hooks
16
+
17
+ Install, configure, and manage the pre-commit hook that blocks new violations before they land in git.
18
+
19
+ ## Important
20
+
21
+ - **Always invoke via `npx -y`** — without `-y`, npx will hang on the "Ok to proceed?" prompt whenever the user hasn't cached the latest `truecourse` version.
22
+ - **The hook makes commits slower.** Every commit runs `truecourse analyze --diff`. On large repos that can be tens of seconds per commit. Make sure the user knows before you install.
23
+ - **Baseline required.** The hook needs a `truecourse analyze` to have run at least once in the repo — otherwise every commit is blocked with "run analyze first". If the user hasn't, suggest running `/truecourse-analyze` first (or `npx -y truecourse analyze`).
24
+ - **`hooks.yaml` is the single source of truth.** Installation creates `<repo>/.truecourse/hooks.yaml` with defaults; edit it to change policy. The file is meant to be committed so the whole team shares one hook config.
25
+
26
+ ## Instructions
27
+
28
+ ### 1. Figure out what the user wants
29
+
30
+ - "install", "set up", "enable" → **Install flow**
31
+ - "status", "is the hook active", "what does it block" → **Status flow**
32
+ - "uninstall", "remove", "disable" → **Uninstall flow**
33
+ - "change what blocks", "make it stricter/looser", "add/remove severities", "enable LLM" → **Configure flow**
34
+
35
+ ### 2. Install flow
36
+
37
+ 1. Tell the user the tradeoff upfront: commits will be slower; this repo needs a `truecourse analyze` baseline; policy lives in `.truecourse/hooks.yaml` which they should commit.
38
+ 2. Run:
39
+ ```
40
+ npx -y truecourse hooks install
41
+ ```
42
+ 3. Relay the output. Two files get created:
43
+ - `.git/hooks/pre-commit` (the script git invokes)
44
+ - `.truecourse/hooks.yaml` (starter policy, blocks `critical` and `high` by default, LLM off)
45
+ 4. If the user hasn't run a full analysis in this repo, suggest `/truecourse-analyze` — without it, every commit will be blocked with "no baseline" until they do.
46
+
47
+ ### 3. Status flow
48
+
49
+ Run:
50
+ ```
51
+ npx -y truecourse hooks status
52
+ ```
53
+ Relay the output. It reports whether the hook is installed, the config path, the block severities, and whether LLM is on.
54
+
55
+ ### 4. Uninstall flow
56
+
57
+ Run:
58
+ ```
59
+ npx -y truecourse hooks uninstall
60
+ ```
61
+ Only removes the git hook script. `hooks.yaml` is preserved (it's team policy, not install state).
62
+
63
+ ### 5. Configure flow
64
+
65
+ The config lives at `<repo>/.truecourse/hooks.yaml`. Use the Read and Edit tools — do not shell out through `truecourse` for edits.
66
+
67
+ Schema:
68
+ ```yaml
69
+ pre-commit:
70
+ block-on: [critical, high] # valid: info, low, medium, high, critical
71
+ llm: false # true = LLM rules on every commit (tokens per commit)
72
+ ```
73
+
74
+ Common edits the user might ask for:
75
+ - **Stricter** ("block medium too"): `block-on: [critical, high, medium]`
76
+ - **Permissive** ("only block criticals"): `block-on: [critical]`
77
+ - **Enable LLM** ("run full checks on commit"): set `llm: true`. Warn the user this spends tokens on every commit — confirm before flipping it.
78
+
79
+ After editing, run `npx -y truecourse hooks status` so they can verify the parsed values match their intent.
80
+
81
+ ### 6. When the user hits a blocked commit
82
+
83
+ If a user comes to you saying "my commit got blocked" or similar:
84
+ - The hook's stdout already listed the blocking violations (file, line, title, severity).
85
+ - Offer to run `/truecourse-fix` to apply fix suggestions to those violations.
86
+ - If they want to ship anyway, remind them of `git commit --no-verify` (standard git bypass).