tlc-claude-code 2.4.2 → 2.4.4

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 (66) hide show
  1. package/.claude/commands/tlc/build.md +75 -5
  2. package/.claude/commands/tlc/discuss.md +174 -123
  3. package/.claude/commands/tlc/e2e-verify.md +1 -1
  4. package/.claude/commands/tlc/plan.md +77 -2
  5. package/.claude/commands/tlc/recall.md +59 -87
  6. package/.claude/commands/tlc/remember.md +76 -71
  7. package/.claude/commands/tlc/review.md +76 -21
  8. package/.claude/commands/tlc/tlc.md +204 -473
  9. package/.claude/hooks/tlc-capture-exchange.sh +50 -21
  10. package/.claude/hooks/tlc-session-init.sh +30 -0
  11. package/CLAUDE.md +6 -5
  12. package/bin/init.js +12 -3
  13. package/package.json +4 -1
  14. package/scripts/dev-link.sh +29 -0
  15. package/scripts/test-package.sh +54 -0
  16. package/scripts/version-sync.js +42 -0
  17. package/scripts/version-sync.test.js +100 -0
  18. package/server/lib/capture/classifier.js +71 -0
  19. package/server/lib/capture/classifier.test.js +71 -0
  20. package/server/lib/capture/claude-capture.js +140 -0
  21. package/server/lib/capture/claude-capture.test.js +152 -0
  22. package/server/lib/capture/codex-capture.js +79 -0
  23. package/server/lib/capture/codex-capture.test.js +161 -0
  24. package/server/lib/capture/codex-event-parser.js +76 -0
  25. package/server/lib/capture/codex-event-parser.test.js +83 -0
  26. package/server/lib/capture/ensure-ready.js +56 -0
  27. package/server/lib/capture/ensure-ready.test.js +135 -0
  28. package/server/lib/capture/envelope.js +77 -0
  29. package/server/lib/capture/envelope.test.js +169 -0
  30. package/server/lib/capture/extractor.js +51 -0
  31. package/server/lib/capture/extractor.test.js +92 -0
  32. package/server/lib/capture/generic-capture.js +96 -0
  33. package/server/lib/capture/generic-capture.test.js +171 -0
  34. package/server/lib/capture/index.js +117 -0
  35. package/server/lib/capture/index.test.js +263 -0
  36. package/server/lib/capture/redactor.js +68 -0
  37. package/server/lib/capture/redactor.test.js +93 -0
  38. package/server/lib/capture/spool-processor.js +155 -0
  39. package/server/lib/capture/spool-processor.test.js +278 -0
  40. package/server/lib/health-check.js +255 -0
  41. package/server/lib/health-check.test.js +243 -0
  42. package/server/lib/model-router.js +11 -2
  43. package/server/lib/model-router.test.js +27 -1
  44. package/server/lib/orchestration/cli-dispatch.js +200 -0
  45. package/server/lib/orchestration/cli-dispatch.test.js +242 -0
  46. package/server/lib/orchestration/codex-orchestrator.js +185 -0
  47. package/server/lib/orchestration/codex-orchestrator.test.js +221 -0
  48. package/server/lib/orchestration/dep-linker.js +61 -0
  49. package/server/lib/orchestration/dep-linker.test.js +174 -0
  50. package/server/lib/orchestration/prompt-builder.js +118 -0
  51. package/server/lib/orchestration/prompt-builder.test.js +200 -0
  52. package/server/lib/orchestration/standalone-compat.js +39 -0
  53. package/server/lib/orchestration/standalone-compat.test.js +144 -0
  54. package/server/lib/orchestration/worktree-manager.js +43 -0
  55. package/server/lib/orchestration/worktree-manager.test.js +50 -0
  56. package/server/lib/router-config.js +18 -3
  57. package/server/lib/router-config.test.js +57 -1
  58. package/server/lib/routing/index.js +34 -0
  59. package/server/lib/routing/index.test.js +33 -0
  60. package/server/lib/routing-command.js +11 -2
  61. package/server/lib/routing-command.test.js +39 -1
  62. package/server/lib/routing-preamble.integration.test.js +319 -0
  63. package/server/lib/routing-preamble.js +34 -11
  64. package/server/lib/routing-preamble.test.js +11 -0
  65. package/server/lib/task-router-config.js +35 -14
  66. package/server/lib/task-router-config.test.js +77 -13
@@ -15,35 +15,64 @@ INPUT=$(cat)
15
15
  # Quick exit if no input
16
16
  [ -z "$INPUT" ] && exit 0
17
17
 
18
- # Use the capture-bridge Node.js module for reliable processing
19
18
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
20
- BRIDGE_SCRIPT="$PROJECT_DIR/server/lib/capture-bridge.js"
19
+ CAPTURE_MOD=""
20
+ if node -e "require('tlc-claude-code/server/lib/capture/claude-capture')" 2>/dev/null; then
21
+ CAPTURE_MOD="tlc-claude-code/server/lib/capture/claude-capture"
22
+ elif [ -f "$PROJECT_DIR/server/lib/capture/claude-capture.js" ]; then
23
+ CAPTURE_MOD="$PROJECT_DIR/server/lib/capture/claude-capture"
24
+ fi
25
+
26
+ if [ -n "$CAPTURE_MOD" ]; then
27
+ # Resolve spool processor for same-session drain
28
+ SPOOL_MOD=""
29
+ if node -e "require('tlc-claude-code/server/lib/capture/spool-processor')" 2>/dev/null; then
30
+ SPOOL_MOD="tlc-claude-code/server/lib/capture/spool-processor"
31
+ elif [ -f "$PROJECT_DIR/server/lib/capture/spool-processor.js" ]; then
32
+ SPOOL_MOD="$PROJECT_DIR/server/lib/capture/spool-processor"
33
+ fi
21
34
 
22
- if [ -f "$BRIDGE_SCRIPT" ]; then
23
35
  echo "$INPUT" | node -e "
24
- const bridge = require('$BRIDGE_SCRIPT');
36
+ const { captureClaudeExchange } = require('$CAPTURE_MOD');
25
37
  let input = '';
26
38
  process.stdin.on('data', d => input += d);
27
39
  process.stdin.on('end', async () => {
28
- const parsed = bridge.parseStopHookInput(input);
29
- if (!parsed || !parsed.assistantMessage) process.exit(0);
30
-
31
- const userMessage = parsed.transcriptPath
32
- ? bridge.extractLastUserMessage(parsed.transcriptPath)
33
- : null;
34
-
35
- await bridge.captureExchange({
36
- cwd: parsed.cwd || '$PROJECT_DIR',
37
- assistantMessage: parsed.assistantMessage,
38
- userMessage,
39
- sessionId: parsed.sessionId,
40
- });
41
-
42
- const path = require('path');
43
- const spoolDir = path.join(parsed.cwd || '$PROJECT_DIR', '.tlc', 'memory');
44
- await bridge.drainSpool(spoolDir);
40
+ captureClaudeExchange({ hookInput: input, projectDir: '$PROJECT_DIR' });
41
+ // Drain spool so memories are available same-session
42
+ try {
43
+ const { processSpool } = require('$SPOOL_MOD');
44
+ await processSpool('$PROJECT_DIR');
45
+ } catch {}
45
46
  });
46
47
  " 2>/dev/null
48
+ else
49
+ BRIDGE_SCRIPT="$PROJECT_DIR/server/lib/capture-bridge.js"
50
+ if [ -f "$BRIDGE_SCRIPT" ]; then
51
+ echo "$INPUT" | node -e "
52
+ const bridge = require('$BRIDGE_SCRIPT');
53
+ let input = '';
54
+ process.stdin.on('data', d => input += d);
55
+ process.stdin.on('end', async () => {
56
+ const parsed = bridge.parseStopHookInput(input);
57
+ if (!parsed || !parsed.assistantMessage) process.exit(0);
58
+
59
+ const userMessage = parsed.transcriptPath
60
+ ? bridge.extractLastUserMessage(parsed.transcriptPath)
61
+ : null;
62
+
63
+ await bridge.captureExchange({
64
+ cwd: parsed.cwd || '$PROJECT_DIR',
65
+ assistantMessage: parsed.assistantMessage,
66
+ userMessage,
67
+ sessionId: parsed.sessionId,
68
+ });
69
+
70
+ const path = require('path');
71
+ const spoolDir = path.join(parsed.cwd || '$PROJECT_DIR', '.tlc', 'memory');
72
+ await bridge.drainSpool(spoolDir);
73
+ });
74
+ " 2>/dev/null
75
+ fi
47
76
  fi
48
77
 
49
78
  # Always exit 0 - never block Claude
@@ -43,6 +43,23 @@ else
43
43
  done
44
44
  fi
45
45
 
46
+ # ─── Memory System Init ─────────────────────────────
47
+ mkdir -p "$PROJECT_DIR/.tlc/memory/team/decisions" \
48
+ "$PROJECT_DIR/.tlc/memory/team/gotchas" \
49
+ "$PROJECT_DIR/.tlc/memory/.local/sessions"
50
+
51
+ # ─── Spool Drain ─────────────────────────────────────
52
+ # Try to drain any pending spool entries from previous sessions
53
+ SPOOL_DRAIN=""
54
+ if node -e "require('tlc-claude-code/server/lib/capture/spool-processor')" 2>/dev/null; then
55
+ SPOOL_DRAIN="require('tlc-claude-code/server/lib/capture/spool-processor').processSpool('$PROJECT_DIR')"
56
+ elif [ -f "$PROJECT_DIR/server/lib/capture/spool-processor.js" ]; then
57
+ SPOOL_DRAIN="require('$PROJECT_DIR/server/lib/capture/spool-processor').processSpool('$PROJECT_DIR')"
58
+ fi
59
+ if [ -n "$SPOOL_DRAIN" ]; then
60
+ node -e "$SPOOL_DRAIN" 2>/dev/null
61
+ fi
62
+
46
63
  # ─── LLM Router: Probe Providers ─────────────────────────
47
64
  #
48
65
  # Writes .tlc/.router-state.json with provider availability.
@@ -132,3 +149,16 @@ else
132
149
  echo "LLM Router: ${COUNT} providers available (cached). State at .tlc/.router-state.json."
133
150
  fi
134
151
  fi
152
+
153
+ # ─── Health Check (runs AFTER router probe so provider state is fresh) ───
154
+ HEALTH_MOD=""
155
+ if node -e "require('tlc-claude-code/server/lib/health-check')" 2>/dev/null; then
156
+ HEALTH_MOD="tlc-claude-code/server/lib/health-check"
157
+ elif [ -f "$PROJECT_DIR/server/lib/health-check.js" ]; then
158
+ HEALTH_MOD="$PROJECT_DIR/server/lib/health-check"
159
+ fi
160
+ if [ -n "$HEALTH_MOD" ]; then
161
+ node -e "require('$HEALTH_MOD').runHealthChecks('$PROJECT_DIR').then(r => r.warnings.forEach(w => console.error(w)))" 2>/dev/null
162
+ fi
163
+
164
+ exit 0
package/CLAUDE.md CHANGED
@@ -4,11 +4,12 @@
4
4
 
5
5
  ## Rules (Enforced by hooks — violations are blocked)
6
6
 
7
- 1. **Tests before code.** Always. Red Green Refactor. Use `/tlc:build`.
8
- 2. **Plans go in files.** Use `/tlc:plan`writes to `.planning/phases/`. Never plan in chat.
9
- 3. **No direct implementation.** User says "build X" run `/tlc:progress` then `/tlc:build`.
10
- 4. **No Co-Authored-By in commits.** The user is the author. Claude is a tool.
11
- 5. **Ask before `git push`.** Never push without explicit approval.
7
+ 1. **Never commit to main.** Always create `phase/{N}` branch. PR back to main when done.
8
+ 2. **Tests before code.** Always. RedGreen Refactor. Use `/tlc:build`.
9
+ 3. **Plans go in files.** Use `/tlc:plan` writes to `.planning/phases/`. Never plan in chat.
10
+ 4. **No direct implementation.** User says "build X" run `/tlc:progress` then `/tlc:build`.
11
+ 5. **No Co-Authored-By in commits.** The user is the author. Claude is a tool.
12
+ 6. **Ask before `git push`.** Never push without explicit approval.
12
13
 
13
14
  ## Command Dispatch
14
15
 
package/bin/init.js CHANGED
@@ -9,6 +9,7 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
 
11
11
  const projectDir = process.cwd();
12
+ const TLC_GITIGNORE_BLOCK = '.tlc/*\n!.tlc/memory/\n.tlc/memory/.local/\n';
12
13
 
13
14
  // Windows batch file content
14
15
  const batContent = `@echo off
@@ -217,8 +218,16 @@ if (fs.existsSync(gitignorePath)) {
217
218
  gitignore += '\n# TLC dev server (local only)\ntlc-start.*\n';
218
219
  updated = true;
219
220
  }
220
- if (!gitignore.includes('.tlc/')) {
221
- gitignore += '.tlc/\n';
221
+ // Remove legacy ".tlc/" ignore that blocks team memory in upgraded repos
222
+ if (gitignore.includes('.tlc/') && !gitignore.includes('.tlc/*')) {
223
+ gitignore = gitignore.replace(/^\.tlc\/\s*$/gm, '');
224
+ updated = true;
225
+ }
226
+ if (!gitignore.includes('.tlc/*') || !gitignore.includes('!.tlc/memory/') || !gitignore.includes('.tlc/memory/.local/')) {
227
+ if (!gitignore.endsWith('\n')) {
228
+ gitignore += '\n';
229
+ }
230
+ gitignore += TLC_GITIGNORE_BLOCK;
222
231
  updated = true;
223
232
  }
224
233
  if (updated) {
@@ -226,7 +235,7 @@ if (fs.existsSync(gitignorePath)) {
226
235
  console.log('[TLC] Updated .gitignore');
227
236
  }
228
237
  } else {
229
- fs.writeFileSync(gitignorePath, '# TLC dev server (local only)\ntlc-start.*\n.tlc/\n');
238
+ fs.writeFileSync(gitignorePath, '# TLC dev server (local only)\ntlc-start.*\n' + TLC_GITIGNORE_BLOCK);
230
239
  console.log('[TLC] Created .gitignore');
231
240
  }
232
241
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc-claude-code": "./bin/install.js",
@@ -34,11 +34,14 @@
34
34
  "postinstall": "node bin/postinstall.js",
35
35
  "build": "cd dashboard && npm run build",
36
36
  "build:web": "cd dashboard-web && npm install && npm run build",
37
+ "preversion": "node scripts/version-sync.js && git diff --quiet || (echo 'Working tree not clean' && exit 1)",
38
+ "version": "node scripts/version-sync.js && git add .tlc.json",
37
39
  "prepublishOnly": "npm run build && npm run build:web",
38
40
  "docs": "node scripts/docs-update.js",
39
41
  "docs:check": "node scripts/docs-update.js --check",
40
42
  "docs:screenshots": "node scripts/generate-screenshots.js",
41
43
  "docs:capture": "node scripts/capture-screenshots.js",
44
+ "test:version-sync": "cd server && npx vitest run --dir .. scripts/version-sync.test.js",
42
45
  "test:e2e": "npx playwright test",
43
46
  "test:e2e:ui": "npx playwright test --ui"
44
47
  },
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+
3
+ set -u
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
7
+
8
+ cd "${REPO_ROOT}" || {
9
+ echo "Error: failed to change to repo root: ${REPO_ROOT}" >&2
10
+ exit 1
11
+ }
12
+
13
+ if ! npm link; then
14
+ echo "Error: npm link failed" >&2
15
+ exit 1
16
+ fi
17
+
18
+ LINK_PATH="$(command -v tlc-claude-code || true)"
19
+
20
+ if [ -z "${LINK_PATH}" ]; then
21
+ echo "Error: tlc-claude-code was not found on PATH after npm link" >&2
22
+ exit 1
23
+ fi
24
+
25
+ echo "Local link created successfully."
26
+ echo "Use it from another project with:"
27
+ echo " npm link tlc-claude-code"
28
+ echo "Resolved binary:"
29
+ echo " ${LINK_PATH}"
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+
3
+ set -u
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
7
+ TMP_DIR=""
8
+ TARBALL_PATH=""
9
+ NPM_CACHE_DIR=""
10
+
11
+ cleanup() {
12
+ if [ -n "${TMP_DIR}" ] && [ -d "${TMP_DIR}" ]; then
13
+ rm -rf "${TMP_DIR}"
14
+ fi
15
+
16
+ if [ -n "${TARBALL_PATH}" ] && [ -f "${TARBALL_PATH}" ]; then
17
+ rm -f "${TARBALL_PATH}"
18
+ fi
19
+ }
20
+
21
+ fail() {
22
+ echo "Error: $1" >&2
23
+ exit 1
24
+ }
25
+
26
+ trap cleanup EXIT
27
+
28
+ TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t tlc-test-package)" || fail "failed to create temporary directory"
29
+ NPM_CACHE_DIR="${TMP_DIR}/npm-cache"
30
+ mkdir -p "${NPM_CACHE_DIR}" || fail "failed to create npm cache directory"
31
+
32
+ cd "${REPO_ROOT}" || fail "failed to change to repo root: ${REPO_ROOT}"
33
+
34
+ PACK_OUTPUT="$(npm_config_cache="${NPM_CACHE_DIR}" npm pack --json 2>&1)" || fail "npm pack failed: ${PACK_OUTPUT}"
35
+ TARBALL_NAME="$(printf '%s\n' "${PACK_OUTPUT}" | node -e "let input=''; process.stdin.on('data', (chunk) => input += chunk); process.stdin.on('end', () => { const start = input.indexOf('['); if (start === -1) { process.exit(1); } const data = JSON.parse(input.slice(start)); if (!Array.isArray(data) || !data[0] || !data[0].filename) { process.exit(1); } process.stdout.write(data[0].filename); });")" || fail "failed to parse npm pack output"
36
+
37
+ [ -n "${TARBALL_NAME}" ] || fail "npm pack did not return a tarball name"
38
+
39
+ TARBALL_PATH="${REPO_ROOT}/${TARBALL_NAME}"
40
+ [ -f "${TARBALL_PATH}" ] || fail "tarball was not created: ${TARBALL_PATH}"
41
+
42
+ cd "${TMP_DIR}" || fail "failed to change to temp directory: ${TMP_DIR}"
43
+
44
+ npm_config_cache="${NPM_CACHE_DIR}" npm install "${TARBALL_PATH}" >/dev/null 2>&1 || fail "npm install failed for ${TARBALL_NAME}"
45
+
46
+ PACKAGE_FILE="${TMP_DIR}/node_modules/tlc-claude-code/server/lib/routing-preamble.js"
47
+ [ -f "${PACKAGE_FILE}" ] || fail "installed package is missing server/lib/routing-preamble.js"
48
+
49
+ OUTPUT="$(node -e "const {generatePreamble} = require('tlc-claude-code/server/lib/routing-preamble'); const p = generatePreamble('build'); console.log(p.substring(0,20))" 2>&1)" || fail "routing preamble validation failed: ${OUTPUT}"
50
+
51
+ [ -n "${OUTPUT}" ] || fail "routing preamble validation produced no output"
52
+
53
+ echo "Package test passed."
54
+ exit 0
@@ -0,0 +1,42 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function syncVersion(rootDir = path.resolve(__dirname, '..')) {
5
+ const packageJsonPath = path.join(rootDir, 'package.json');
6
+ const tlcConfigPath = path.join(rootDir, '.tlc.json');
7
+
8
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
9
+
10
+ if (typeof packageJson.version !== 'string' || packageJson.version.trim() === '') {
11
+ throw new Error('package.json is missing a valid version field');
12
+ }
13
+
14
+ let tlcConfig = {};
15
+
16
+ if (fs.existsSync(tlcConfigPath)) {
17
+ tlcConfig = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf8'));
18
+ }
19
+
20
+ tlcConfig.version = packageJson.version;
21
+ tlcConfig.tlcVersion = packageJson.version;
22
+
23
+ fs.writeFileSync(tlcConfigPath, `${JSON.stringify(tlcConfig, null, 2)}\n`);
24
+ }
25
+
26
+ function main() {
27
+ try {
28
+ syncVersion();
29
+ process.exit(0);
30
+ } catch (error) {
31
+ console.error(error instanceof Error ? error.message : String(error));
32
+ process.exit(1);
33
+ }
34
+ }
35
+
36
+ if (require.main === module) {
37
+ main();
38
+ }
39
+
40
+ module.exports = {
41
+ syncVersion,
42
+ };
@@ -0,0 +1,100 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+
7
+ import versionSyncModule from './version-sync.js';
8
+
9
+ const { syncVersion } = versionSyncModule;
10
+
11
+ const tempDirs = [];
12
+
13
+ function createTempProject(packageVersion, tlcConfig) {
14
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'version-sync-'));
15
+ tempDirs.push(tempDir);
16
+
17
+ fs.writeFileSync(
18
+ path.join(tempDir, 'package.json'),
19
+ `${JSON.stringify({ version: packageVersion }, null, 2)}\n`
20
+ );
21
+
22
+ if (tlcConfig !== undefined) {
23
+ fs.writeFileSync(
24
+ path.join(tempDir, '.tlc.json'),
25
+ `${JSON.stringify(tlcConfig, null, 2)}\n`
26
+ );
27
+ }
28
+
29
+ return tempDir;
30
+ }
31
+
32
+ function readJson(filePath) {
33
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
34
+ }
35
+
36
+ afterEach(() => {
37
+ while (tempDirs.length > 0) {
38
+ fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
39
+ }
40
+ });
41
+
42
+ describe('syncVersion', () => {
43
+ it('reads version from package.json correctly', () => {
44
+ const tempDir = createTempProject('1.2.3', { existing: true });
45
+
46
+ syncVersion(tempDir);
47
+
48
+ const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
49
+ expect(tlcConfig.version).toBe('1.2.3');
50
+ });
51
+
52
+ it('writes to .tlc.json version field', () => {
53
+ const tempDir = createTempProject('2.3.4', {});
54
+
55
+ syncVersion(tempDir);
56
+
57
+ const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
58
+ expect(tlcConfig.version).toBe('2.3.4');
59
+ });
60
+
61
+ it('writes to .tlc.json tlcVersion field', () => {
62
+ const tempDir = createTempProject('3.4.5', {});
63
+
64
+ syncVersion(tempDir);
65
+
66
+ const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
67
+ expect(tlcConfig.tlcVersion).toBe('3.4.5');
68
+ });
69
+
70
+ it('handles missing .tlc.json gracefully', () => {
71
+ const tempDir = createTempProject('4.5.6');
72
+
73
+ expect(() => syncVersion(tempDir)).not.toThrow();
74
+
75
+ const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
76
+ expect(tlcConfig).toEqual({
77
+ version: '4.5.6',
78
+ tlcVersion: '4.5.6',
79
+ });
80
+ });
81
+
82
+ it('does not overwrite other .tlc.json fields', () => {
83
+ const tempDir = createTempProject('5.6.7', {
84
+ project: 'TLC',
85
+ nested: { keep: true },
86
+ version: 'old',
87
+ tlcVersion: 'old',
88
+ });
89
+
90
+ syncVersion(tempDir);
91
+
92
+ const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
93
+ expect(tlcConfig).toEqual({
94
+ project: 'TLC',
95
+ nested: { keep: true },
96
+ version: '5.6.7',
97
+ tlcVersion: '5.6.7',
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,71 @@
1
+ const PATTERN_GROUPS = [
2
+ {
3
+ type: 'gotcha',
4
+ scope: 'team',
5
+ patterns: [
6
+ { regex: /\bdon't do\b/i, confidence: 0.85 },
7
+ { regex: /\bwatch out\b/i, confidence: 0.85 },
8
+ { regex: /\bbreaks when\b/i, confidence: 0.9 },
9
+ { regex: /\bcareful with\b/i, confidence: 0.8 },
10
+ { regex: /\bnever use\b/i, confidence: 0.9 },
11
+ { regex: /\bgotcha\b/i, confidence: 0.75 },
12
+ { regex: /\bdon't use\b/i, confidence: 0.9 },
13
+ ],
14
+ },
15
+ {
16
+ type: 'preference',
17
+ scope: 'personal',
18
+ patterns: [
19
+ { regex: /\bi prefer\b/i, confidence: 0.8 },
20
+ { regex: /\bi like\b/i, confidence: 0.7 },
21
+ { regex: /\bfor me\b/i, confidence: 0.65 },
22
+ { regex: /\bmy workflow\b/i, confidence: 0.75 },
23
+ ],
24
+ },
25
+ {
26
+ type: 'decision',
27
+ scope: 'team',
28
+ patterns: [
29
+ { regex: /\bwe decided\b/i, confidence: 0.9 },
30
+ { regex: /\bchose\b/i, confidence: 0.75 },
31
+ { regex: /\bgoing with\b/i, confidence: 0.8 },
32
+ { regex: /\bthe approach is\b/i, confidence: 0.8 },
33
+ { regex: /\busing\b.+\bfor\b/i, confidence: 0.7 },
34
+ { regex: /\bsplit into\b/i, confidence: 0.75 },
35
+ { regex: /\bcreated\b/i, confidence: 0.7 },
36
+ { regex: /\bimplemented\b/i, confidence: 0.7 },
37
+ ],
38
+ },
39
+ ];
40
+
41
+ function classify(statement) {
42
+ const text = typeof statement === 'string' ? statement.trim() : '';
43
+
44
+ if (!text) {
45
+ return { type: 'session', scope: 'personal', confidence: 0.1 };
46
+ }
47
+
48
+ let bestMatch = null;
49
+
50
+ for (const group of PATTERN_GROUPS) {
51
+ for (const pattern of group.patterns) {
52
+ if (!pattern.regex.test(text)) {
53
+ continue;
54
+ }
55
+
56
+ if (!bestMatch || pattern.confidence > bestMatch.confidence) {
57
+ bestMatch = {
58
+ type: group.type,
59
+ scope: group.scope,
60
+ confidence: pattern.confidence,
61
+ };
62
+ }
63
+ }
64
+ }
65
+
66
+ return bestMatch || { type: 'session', scope: 'personal', confidence: 0.1 };
67
+ }
68
+
69
+ module.exports = {
70
+ classify,
71
+ };
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ const { classify } = require('./classifier.js');
4
+
5
+ describe('capture/classifier', () => {
6
+ it('classifies explicit decision statements as decision/team', () => {
7
+ expect(classify('We decided to use Postgres')).toEqual({
8
+ type: 'decision',
9
+ scope: 'team',
10
+ confidence: expect.any(Number),
11
+ });
12
+ });
13
+
14
+ it('classifies gotcha statements as gotcha/team', () => {
15
+ const result = classify("Don't use synchronous fs calls");
16
+
17
+ expect(result.type).toBe('gotcha');
18
+ expect(result.scope).toBe('team');
19
+ expect(result.confidence).toBeGreaterThan(0.1);
20
+ });
21
+
22
+ it('classifies preference statements as preference/personal', () => {
23
+ const result = classify('I prefer dark theme');
24
+
25
+ expect(result).toEqual({
26
+ type: 'preference',
27
+ scope: 'personal',
28
+ confidence: expect.any(Number),
29
+ });
30
+ });
31
+
32
+ it('returns session/personal with low confidence for unknown text', () => {
33
+ expect(classify('Hello there')).toEqual({
34
+ type: 'session',
35
+ scope: 'personal',
36
+ confidence: 0.1,
37
+ });
38
+ });
39
+
40
+ it('treats code-change language as a team decision', () => {
41
+ const result = classify('Created 3 modules');
42
+
43
+ expect(result.type).toBe('decision');
44
+ expect(result.scope).toBe('team');
45
+ });
46
+
47
+ it('classifies using X for Y phrasing as a decision', () => {
48
+ const result = classify('Using Redis for rate limiting');
49
+
50
+ expect(result.type).toBe('decision');
51
+ expect(result.scope).toBe('team');
52
+ });
53
+
54
+ it('classifies watch out phrasing as a gotcha', () => {
55
+ const result = classify('Watch out for stale closures in async loops');
56
+
57
+ expect(result.type).toBe('gotcha');
58
+ expect(result.scope).toBe('team');
59
+ });
60
+
61
+ it('classifies workflow phrasing as a personal preference', () => {
62
+ const result = classify('My workflow is to review diffs before running tests');
63
+
64
+ expect(result.type).toBe('preference');
65
+ expect(result.scope).toBe('personal');
66
+ });
67
+
68
+ it('keeps decision confidence above the default fallback confidence', () => {
69
+ expect(classify('Implemented request tracing').confidence).toBeGreaterThan(0.1);
70
+ });
71
+ });