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.
- package/.claude/commands/tlc/build.md +75 -5
- package/.claude/commands/tlc/discuss.md +174 -123
- package/.claude/commands/tlc/e2e-verify.md +1 -1
- package/.claude/commands/tlc/plan.md +77 -2
- package/.claude/commands/tlc/recall.md +59 -87
- package/.claude/commands/tlc/remember.md +76 -71
- package/.claude/commands/tlc/review.md +76 -21
- package/.claude/commands/tlc/tlc.md +204 -473
- package/.claude/hooks/tlc-capture-exchange.sh +50 -21
- package/.claude/hooks/tlc-session-init.sh +30 -0
- package/CLAUDE.md +6 -5
- package/bin/init.js +12 -3
- package/package.json +4 -1
- package/scripts/dev-link.sh +29 -0
- package/scripts/test-package.sh +54 -0
- package/scripts/version-sync.js +42 -0
- package/scripts/version-sync.test.js +100 -0
- package/server/lib/capture/classifier.js +71 -0
- package/server/lib/capture/classifier.test.js +71 -0
- package/server/lib/capture/claude-capture.js +140 -0
- package/server/lib/capture/claude-capture.test.js +152 -0
- package/server/lib/capture/codex-capture.js +79 -0
- package/server/lib/capture/codex-capture.test.js +161 -0
- package/server/lib/capture/codex-event-parser.js +76 -0
- package/server/lib/capture/codex-event-parser.test.js +83 -0
- package/server/lib/capture/ensure-ready.js +56 -0
- package/server/lib/capture/ensure-ready.test.js +135 -0
- package/server/lib/capture/envelope.js +77 -0
- package/server/lib/capture/envelope.test.js +169 -0
- package/server/lib/capture/extractor.js +51 -0
- package/server/lib/capture/extractor.test.js +92 -0
- package/server/lib/capture/generic-capture.js +96 -0
- package/server/lib/capture/generic-capture.test.js +171 -0
- package/server/lib/capture/index.js +117 -0
- package/server/lib/capture/index.test.js +263 -0
- package/server/lib/capture/redactor.js +68 -0
- package/server/lib/capture/redactor.test.js +93 -0
- package/server/lib/capture/spool-processor.js +155 -0
- package/server/lib/capture/spool-processor.test.js +278 -0
- package/server/lib/health-check.js +255 -0
- package/server/lib/health-check.test.js +243 -0
- package/server/lib/model-router.js +11 -2
- package/server/lib/model-router.test.js +27 -1
- package/server/lib/orchestration/cli-dispatch.js +200 -0
- package/server/lib/orchestration/cli-dispatch.test.js +242 -0
- package/server/lib/orchestration/codex-orchestrator.js +185 -0
- package/server/lib/orchestration/codex-orchestrator.test.js +221 -0
- package/server/lib/orchestration/dep-linker.js +61 -0
- package/server/lib/orchestration/dep-linker.test.js +174 -0
- package/server/lib/orchestration/prompt-builder.js +118 -0
- package/server/lib/orchestration/prompt-builder.test.js +200 -0
- package/server/lib/orchestration/standalone-compat.js +39 -0
- package/server/lib/orchestration/standalone-compat.test.js +144 -0
- package/server/lib/orchestration/worktree-manager.js +43 -0
- package/server/lib/orchestration/worktree-manager.test.js +50 -0
- package/server/lib/router-config.js +18 -3
- package/server/lib/router-config.test.js +57 -1
- package/server/lib/routing/index.js +34 -0
- package/server/lib/routing/index.test.js +33 -0
- package/server/lib/routing-command.js +11 -2
- package/server/lib/routing-command.test.js +39 -1
- package/server/lib/routing-preamble.integration.test.js +319 -0
- package/server/lib/routing-preamble.js +34 -11
- package/server/lib/routing-preamble.test.js +11 -0
- package/server/lib/task-router-config.js +35 -14
- 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
|
-
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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. **
|
|
8
|
-
2. **
|
|
9
|
-
3. **
|
|
10
|
-
4. **No
|
|
11
|
-
5. **
|
|
7
|
+
1. **Never commit to main.** Always create `phase/{N}` branch. PR back to main when done.
|
|
8
|
+
2. **Tests before code.** Always. Red → Green → 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
|
-
|
|
221
|
-
|
|
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
|
|
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.
|
|
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
|
+
});
|