@torus-engineering/tas-kit 1.13.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tas/_platform/claude-code/settings.json +58 -46
- package/.tas/_platform/hooks/code-quality.js +127 -127
- package/.tas/_platform/hooks/session-end.js +111 -111
- package/.tas/agents/architect.md +53 -53
- package/.tas/agents/aws-reviewer.md +71 -71
- package/.tas/agents/build-resolver.md +89 -59
- package/.tas/agents/code-explorer.md +63 -63
- package/.tas/agents/csharp-reviewer.md +62 -62
- package/.tas/agents/database-reviewer.md +73 -73
- package/.tas/agents/doc-updater.md +68 -66
- package/.tas/agents/python-reviewer.md +67 -67
- package/.tas/agents/security-reviewer.md +79 -79
- package/.tas/agents/software-engineer.md +53 -0
- package/.tas/agents/typescript-reviewer.md +65 -65
- package/.tas/commands/ado-create.md +33 -28
- package/.tas/commands/ado-delete.md +26 -22
- package/.tas/commands/ado-get.md +24 -20
- package/.tas/commands/ado-status.md +22 -18
- package/.tas/commands/ado-update.md +31 -27
- package/.tas/commands/tas-adr.md +37 -33
- package/.tas/commands/tas-apitest-plan.md +177 -173
- package/.tas/commands/tas-apitest.md +147 -143
- package/.tas/commands/tas-brainstorm.md +23 -19
- package/.tas/commands/tas-brd.md +50 -0
- package/.tas/commands/tas-bug.md +127 -113
- package/.tas/commands/tas-checklist.md +180 -0
- package/.tas/commands/tas-debug.md +103 -0
- package/.tas/commands/tas-design.md +41 -37
- package/.tas/commands/tas-dev.md +225 -125
- package/.tas/commands/tas-e2e-mobile.md +146 -155
- package/.tas/commands/tas-e2e-web.md +150 -163
- package/.tas/commands/tas-e2e.md +289 -102
- package/.tas/commands/tas-feature.md +181 -47
- package/.tas/commands/tas-fix.md +72 -51
- package/.tas/commands/tas-functest-mobile.md +138 -144
- package/.tas/commands/tas-functest-web.md +176 -192
- package/.tas/commands/tas-functest.md +225 -76
- package/.tas/commands/tas-init.md +22 -17
- package/.tas/commands/tas-master-plan.md +300 -0
- package/.tas/commands/tas-orchestrate.md +159 -0
- package/.tas/commands/tas-plan.md +152 -117
- package/.tas/commands/tas-prd.md +57 -37
- package/.tas/commands/tas-review-pr.md +174 -0
- package/.tas/commands/tas-review.md +115 -113
- package/.tas/commands/tas-sad.md +47 -43
- package/.tas/commands/tas-security.md +91 -87
- package/.tas/commands/tas-spec.md +54 -50
- package/.tas/commands/tas-status.md +25 -16
- package/.tas/project-status-example.yaml +3 -1
- package/.tas/rules/ado-integration.md +67 -65
- package/.tas/rules/common/api-design.md +517 -517
- package/.tas/rules/common/build-debug-loop.md +233 -0
- package/.tas/rules/common/code-review.md +4 -0
- package/.tas/rules/common/feature-done.md +42 -0
- package/.tas/rules/common/post-implementation-review.md +4 -0
- package/.tas/rules/common/project-status.md +33 -16
- package/.tas/rules/common/sad-impact.md +81 -0
- package/.tas/rules/common/tdd.md +104 -89
- package/.tas/rules/csharp/api-testing.md +2 -2
- package/.tas/rules/csharp/torus-core-framework.md +128 -0
- package/.tas/tas-example.yaml +9 -32
- package/.tas/templates/AGENTS.md +13 -0
- package/.tas/templates/API-Test-Spec.md +5 -4
- package/.tas/templates/BRD.md +133 -0
- package/.tas/templates/Bug.md +15 -0
- package/.tas/templates/E2E-Execution-Report.md +8 -8
- package/.tas/templates/E2E-Mobile-Spec.md +6 -8
- package/.tas/templates/E2E-Report.md +2 -2
- package/.tas/templates/E2E-Scenario.md +22 -22
- package/.tas/templates/E2E-Test-Spec.md +274 -0
- package/.tas/templates/E2E-Web-Spec.md +4 -4
- package/.tas/templates/Feature-Technical-Part.md +69 -0
- package/.tas/templates/Feature-Technical-Stack.md +74 -0
- package/.tas/templates/Feature-Technical.md +329 -0
- package/.tas/templates/Feature.md +50 -26
- package/.tas/templates/Func-Test-Script.md +29 -56
- package/.tas/templates/Func-Test-Spec.md +144 -142
- package/.tas/templates/PRD.md +173 -142
- package/.tas/templates/TestChecklist.md +96 -0
- package/.tas/templates/torus-dotnet-bootstrap.md +223 -0
- package/.tas/tools/tas-ado-readme.md +24 -27
- package/.tas/tools/tas-ado.py +328 -25
- package/.tas/tools/tas-github.py +339 -0
- package/README.md +142 -57
- package/bin/cli.js +90 -90
- package/lib/adapters/antigravity.js +131 -131
- package/lib/adapters/claude-code.js +71 -35
- package/lib/adapters/codex.js +157 -157
- package/lib/adapters/cursor.js +80 -80
- package/lib/adapters/index.js +20 -20
- package/lib/adapters/utils.js +81 -81
- package/lib/deleted-files.json +7 -0
- package/lib/install.js +546 -543
- package/package.json +2 -2
- package/.tas/README.md +0 -334
- package/.tas/commands/tas-epic.md +0 -35
- package/.tas/commands/tas-story.md +0 -91
- package/.tas/rules/common/story-done.md +0 -30
- package/.tas/templates/Epic.md +0 -46
- package/.tas/templates/Story.md +0 -90
|
@@ -1,46 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
{
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"puppeteer": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["-y", "@anthropic-ai/mcp-server-puppeteer"],
|
|
6
|
+
"description": "Browser automation for build-verify loop (screenshots, console scan, API check)"
|
|
7
|
+
},
|
|
8
|
+
"playwright": {
|
|
9
|
+
"command": "npx",
|
|
10
|
+
"args": ["-y", "@playwright/mcp"],
|
|
11
|
+
"description": "Alternative browser automation — prefer over puppeteer if already installed"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"permissions": {
|
|
15
|
+
"allow": [
|
|
16
|
+
"Read:*",
|
|
17
|
+
"Write:*",
|
|
18
|
+
"Edit:*",
|
|
19
|
+
"Bash:git:*",
|
|
20
|
+
"Bash:python:*",
|
|
21
|
+
"Bash:python3:*",
|
|
22
|
+
"Bash:dotnet:*",
|
|
23
|
+
"Bash:npm:*",
|
|
24
|
+
"Bash:node:*",
|
|
25
|
+
"Bash:az:*"
|
|
26
|
+
],
|
|
27
|
+
"deny": [
|
|
28
|
+
"Bash:sudo:*",
|
|
29
|
+
"Read:**/.env",
|
|
30
|
+
"Read:**/*.env"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"hooks": {
|
|
34
|
+
"PostToolUse": [
|
|
35
|
+
{
|
|
36
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node .claude/hooks/code-quality.js",
|
|
41
|
+
"description": "Auto format source files: prettier (TS/JS), ruff/black (Python), dotnet format (C#)"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"Stop": [
|
|
47
|
+
{
|
|
48
|
+
"hooks": [
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "node .claude/hooks/session-end.js",
|
|
52
|
+
"description": "Run test suite, check project-status.yaml updated, remind next steps"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* PostToolUse Hook: Auto format/lint source files after Write/Edit/MultiEdit
|
|
4
|
-
*
|
|
5
|
-
* Detects file extension and runs the appropriate formatter using project-local tools:
|
|
6
|
-
* .ts/.tsx/.js/.jsx → prettier (via npx/yarn)
|
|
7
|
-
* .py → ruff format (preferred) or black
|
|
8
|
-
* .cs → dotnet format --include <file>
|
|
9
|
-
*
|
|
10
|
-
* Uses project-local binaries only — never installs packages globally.
|
|
11
|
-
* Silently skips if the tool is not installed in the project.
|
|
12
|
-
*
|
|
13
|
-
* Hook input (stdin): JSON with tool_input.file_path
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const { execSync } = require('child_process');
|
|
21
|
-
|
|
22
|
-
// --- Read hook input ---
|
|
23
|
-
let input;
|
|
24
|
-
try {
|
|
25
|
-
const raw = fs.readFileSync(0, 'utf8');
|
|
26
|
-
input = JSON.parse(raw);
|
|
27
|
-
} catch {
|
|
28
|
-
process.exit(0);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const filePath = (input.tool_input || {}).file_path || '';
|
|
32
|
-
if (!filePath) process.exit(0);
|
|
33
|
-
|
|
34
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
35
|
-
const fileName = path.basename(filePath);
|
|
36
|
-
|
|
37
|
-
// Skip non-source files (markdown, yaml, json config, etc.)
|
|
38
|
-
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.cs']);
|
|
39
|
-
if (!SOURCE_EXTS.has(ext)) process.exit(0);
|
|
40
|
-
|
|
41
|
-
// --- Helper: run command, return true on success ---
|
|
42
|
-
function run(cmd, cwd) {
|
|
43
|
-
try {
|
|
44
|
-
execSync(cmd, { cwd, stdio: 'pipe' });
|
|
45
|
-
return true;
|
|
46
|
-
} catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// --- Helper: find repo root (where package.json / pyproject.toml / *.sln lives) ---
|
|
52
|
-
function findRoot(startDir, markers) {
|
|
53
|
-
let dir = path.resolve(startDir);
|
|
54
|
-
for (let i = 0; i < 10; i++) {
|
|
55
|
-
if (markers.some(m => fs.existsSync(path.join(dir, m)))) return dir;
|
|
56
|
-
const parent = path.dirname(dir);
|
|
57
|
-
if (parent === dir) break;
|
|
58
|
-
dir = parent;
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ─── TypeScript / JavaScript ─────────────────────────────────────────────────
|
|
64
|
-
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
65
|
-
const root = findRoot(path.dirname(filePath), ['package.json']);
|
|
66
|
-
if (!root) process.exit(0);
|
|
67
|
-
|
|
68
|
-
const pkg = path.join(root, 'package.json');
|
|
69
|
-
let hasScript = false;
|
|
70
|
-
try {
|
|
71
|
-
const pkgJson = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
72
|
-
hasScript = !!(pkgJson.scripts && pkgJson.scripts.format);
|
|
73
|
-
} catch { /* ignore */ }
|
|
74
|
-
|
|
75
|
-
// Prefer project format script → npx prettier → yarn prettier
|
|
76
|
-
const formatted =
|
|
77
|
-
(hasScript && run(`npm run format -- --write "${filePath}"`, root)) ||
|
|
78
|
-
run(`npx --no-install prettier --write "${filePath}"`, root) ||
|
|
79
|
-
run(`yarn prettier --write "${filePath}"`, root);
|
|
80
|
-
|
|
81
|
-
if (formatted) {
|
|
82
|
-
console.log(`[Format] ${fileName}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ─── Python ──────────────────────────────────────────────────────────────────
|
|
87
|
-
else if (ext === '.py') {
|
|
88
|
-
const root = findRoot(path.dirname(filePath), ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt']);
|
|
89
|
-
const cwd = root || path.dirname(filePath);
|
|
90
|
-
|
|
91
|
-
const formatted =
|
|
92
|
-
run(`ruff format "${filePath}"`, cwd) ||
|
|
93
|
-
run(`python -m ruff format "${filePath}"`, cwd) ||
|
|
94
|
-
run(`black "${filePath}"`, cwd) ||
|
|
95
|
-
run(`python -m black "${filePath}"`, cwd);
|
|
96
|
-
|
|
97
|
-
if (formatted) {
|
|
98
|
-
console.log(`[Format] ${fileName}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── C# ──────────────────────────────────────────────────────────────────────
|
|
103
|
-
else if (ext === '.cs') {
|
|
104
|
-
// Find nearest .csproj
|
|
105
|
-
function findCsproj(startDir) {
|
|
106
|
-
let dir = path.resolve(startDir);
|
|
107
|
-
for (let i = 0; i < 8; i++) {
|
|
108
|
-
const files = fs.readdirSync(dir).filter(f => f.endsWith('.csproj'));
|
|
109
|
-
if (files.length > 0) return { cwd: dir, proj: path.join(dir, files[0]) };
|
|
110
|
-
const parent = path.dirname(dir);
|
|
111
|
-
if (parent === dir) break;
|
|
112
|
-
dir = parent;
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const result = findCsproj(path.dirname(filePath));
|
|
118
|
-
if (!result) process.exit(0);
|
|
119
|
-
|
|
120
|
-
const formatted = run(
|
|
121
|
-
`dotnet format "${result.proj}" --include "${filePath}" --no-restore`,
|
|
122
|
-
result.cwd
|
|
123
|
-
);
|
|
124
|
-
if (formatted) {
|
|
125
|
-
console.log(`[Format] ${fileName}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook: Auto format/lint source files after Write/Edit/MultiEdit
|
|
4
|
+
*
|
|
5
|
+
* Detects file extension and runs the appropriate formatter using project-local tools:
|
|
6
|
+
* .ts/.tsx/.js/.jsx → prettier (via npx/yarn)
|
|
7
|
+
* .py → ruff format (preferred) or black
|
|
8
|
+
* .cs → dotnet format --include <file>
|
|
9
|
+
*
|
|
10
|
+
* Uses project-local binaries only — never installs packages globally.
|
|
11
|
+
* Silently skips if the tool is not installed in the project.
|
|
12
|
+
*
|
|
13
|
+
* Hook input (stdin): JSON with tool_input.file_path
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
// --- Read hook input ---
|
|
23
|
+
let input;
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
26
|
+
input = JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const filePath = (input.tool_input || {}).file_path || '';
|
|
32
|
+
if (!filePath) process.exit(0);
|
|
33
|
+
|
|
34
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
35
|
+
const fileName = path.basename(filePath);
|
|
36
|
+
|
|
37
|
+
// Skip non-source files (markdown, yaml, json config, etc.)
|
|
38
|
+
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.cs']);
|
|
39
|
+
if (!SOURCE_EXTS.has(ext)) process.exit(0);
|
|
40
|
+
|
|
41
|
+
// --- Helper: run command, return true on success ---
|
|
42
|
+
function run(cmd, cwd) {
|
|
43
|
+
try {
|
|
44
|
+
execSync(cmd, { cwd, stdio: 'pipe' });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Helper: find repo root (where package.json / pyproject.toml / *.sln lives) ---
|
|
52
|
+
function findRoot(startDir, markers) {
|
|
53
|
+
let dir = path.resolve(startDir);
|
|
54
|
+
for (let i = 0; i < 10; i++) {
|
|
55
|
+
if (markers.some(m => fs.existsSync(path.join(dir, m)))) return dir;
|
|
56
|
+
const parent = path.dirname(dir);
|
|
57
|
+
if (parent === dir) break;
|
|
58
|
+
dir = parent;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── TypeScript / JavaScript ─────────────────────────────────────────────────
|
|
64
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
65
|
+
const root = findRoot(path.dirname(filePath), ['package.json']);
|
|
66
|
+
if (!root) process.exit(0);
|
|
67
|
+
|
|
68
|
+
const pkg = path.join(root, 'package.json');
|
|
69
|
+
let hasScript = false;
|
|
70
|
+
try {
|
|
71
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
72
|
+
hasScript = !!(pkgJson.scripts && pkgJson.scripts.format);
|
|
73
|
+
} catch { /* ignore */ }
|
|
74
|
+
|
|
75
|
+
// Prefer project format script → npx prettier → yarn prettier
|
|
76
|
+
const formatted =
|
|
77
|
+
(hasScript && run(`npm run format -- --write "${filePath}"`, root)) ||
|
|
78
|
+
run(`npx --no-install prettier --write "${filePath}"`, root) ||
|
|
79
|
+
run(`yarn prettier --write "${filePath}"`, root);
|
|
80
|
+
|
|
81
|
+
if (formatted) {
|
|
82
|
+
console.log(`[Format] ${fileName}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Python ──────────────────────────────────────────────────────────────────
|
|
87
|
+
else if (ext === '.py') {
|
|
88
|
+
const root = findRoot(path.dirname(filePath), ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt']);
|
|
89
|
+
const cwd = root || path.dirname(filePath);
|
|
90
|
+
|
|
91
|
+
const formatted =
|
|
92
|
+
run(`ruff format "${filePath}"`, cwd) ||
|
|
93
|
+
run(`python -m ruff format "${filePath}"`, cwd) ||
|
|
94
|
+
run(`black "${filePath}"`, cwd) ||
|
|
95
|
+
run(`python -m black "${filePath}"`, cwd);
|
|
96
|
+
|
|
97
|
+
if (formatted) {
|
|
98
|
+
console.log(`[Format] ${fileName}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── C# ──────────────────────────────────────────────────────────────────────
|
|
103
|
+
else if (ext === '.cs') {
|
|
104
|
+
// Find nearest .csproj
|
|
105
|
+
function findCsproj(startDir) {
|
|
106
|
+
let dir = path.resolve(startDir);
|
|
107
|
+
for (let i = 0; i < 8; i++) {
|
|
108
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.csproj'));
|
|
109
|
+
if (files.length > 0) return { cwd: dir, proj: path.join(dir, files[0]) };
|
|
110
|
+
const parent = path.dirname(dir);
|
|
111
|
+
if (parent === dir) break;
|
|
112
|
+
dir = parent;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = findCsproj(path.dirname(filePath));
|
|
118
|
+
if (!result) process.exit(0);
|
|
119
|
+
|
|
120
|
+
const formatted = run(
|
|
121
|
+
`dotnet format "${result.proj}" --include "${filePath}" --no-restore`,
|
|
122
|
+
result.cwd
|
|
123
|
+
);
|
|
124
|
+
if (formatted) {
|
|
125
|
+
console.log(`[Format] ${fileName}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Stop Hook: End-of-session verification
|
|
4
|
-
*
|
|
5
|
-
* Runs when Claude Code session ends. Performs:
|
|
6
|
-
* 1. Auto-detects test runner and runs the test suite
|
|
7
|
-
* 2. Checks if project-status.yaml was updated today
|
|
8
|
-
* 3. Prints a summary of pass/fail checks
|
|
9
|
-
*
|
|
10
|
-
* Designed to be non-blocking — all failures are reported as warnings,
|
|
11
|
-
* not hard errors, so the session can still end cleanly.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
'use strict';
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const { execSync } = require('child_process');
|
|
19
|
-
|
|
20
|
-
const checks = [];
|
|
21
|
-
|
|
22
|
-
// --- Helper: run command, return { ok, output } ---
|
|
23
|
-
function run(cmd, cwd) {
|
|
24
|
-
try {
|
|
25
|
-
const output = execSync(cmd, { cwd, stdio: 'pipe', timeout: 60_000 }).toString();
|
|
26
|
-
return { ok: true, output };
|
|
27
|
-
} catch (err) {
|
|
28
|
-
return { ok: false, output: (err.stderr || err.stdout || err.message || '').toString() };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// --- Find repo root (cwd of the session) ---
|
|
33
|
-
const cwd = process.cwd();
|
|
34
|
-
|
|
35
|
-
// ─── 1. Run test suite ────────────────────────────────────────────────────────
|
|
36
|
-
let testRan = false;
|
|
37
|
-
|
|
38
|
-
// Node.js / npm
|
|
39
|
-
if (fs.existsSync(path.join(cwd, 'package.json'))) {
|
|
40
|
-
let hasTestScript = false;
|
|
41
|
-
try {
|
|
42
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
43
|
-
hasTestScript = !!(pkg.scripts && pkg.scripts.test &&
|
|
44
|
-
pkg.scripts.test !== 'echo "Error: no test specified" && exit 1');
|
|
45
|
-
} catch { /* ignore */ }
|
|
46
|
-
|
|
47
|
-
if (hasTestScript) {
|
|
48
|
-
const result = run('npm test', cwd);
|
|
49
|
-
checks.push(result.ok
|
|
50
|
-
? '✓ Tests passed (npm test)'
|
|
51
|
-
: '✗ Tests FAILED (npm test) — fix before committing'
|
|
52
|
-
);
|
|
53
|
-
testRan = true;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// .NET
|
|
58
|
-
if (!testRan) {
|
|
59
|
-
function findDotnetFiles(startDir, maxDepth = 3) {
|
|
60
|
-
const results = [];
|
|
61
|
-
function walk(dir, depth) {
|
|
62
|
-
if (depth > maxDepth) return;
|
|
63
|
-
try {
|
|
64
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
-
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
66
|
-
walk(path.join(dir, entry.name), depth + 1);
|
|
67
|
-
} else if (entry.name.endsWith('.csproj') || entry.name.endsWith('.sln')) {
|
|
68
|
-
results.push(entry.name);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
} catch { /* ignore */ }
|
|
72
|
-
}
|
|
73
|
-
walk(startDir, 0);
|
|
74
|
-
return results;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const csprojFiles = findDotnetFiles(cwd);
|
|
78
|
-
|
|
79
|
-
if (csprojFiles.length > 0) {
|
|
80
|
-
const result = run('dotnet test --no-build --logger "console;verbosity=minimal" 2>&1', cwd);
|
|
81
|
-
checks.push(result.ok
|
|
82
|
-
? '✓ Tests passed (dotnet test)'
|
|
83
|
-
: '✗ Tests FAILED (dotnet test) — fix before committing'
|
|
84
|
-
);
|
|
85
|
-
testRan = true;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Python (pytest)
|
|
90
|
-
if (!testRan) {
|
|
91
|
-
const hasPytest =
|
|
92
|
-
fs.existsSync(path.join(cwd, 'pytest.ini')) ||
|
|
93
|
-
fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
|
|
94
|
-
fs.existsSync(path.join(cwd, 'setup.cfg'));
|
|
95
|
-
|
|
96
|
-
if (hasPytest) {
|
|
97
|
-
const result = run('python -m pytest -q --tb=no 2>&1', cwd);
|
|
98
|
-
checks.push(result.ok
|
|
99
|
-
? '✓ Tests passed (pytest)'
|
|
100
|
-
: '✗ Tests FAILED (pytest) — fix before committing'
|
|
101
|
-
);
|
|
102
|
-
testRan = true;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─── Output ──────────────────────────────────────────────────────────────────
|
|
107
|
-
if (checks.length > 0) {
|
|
108
|
-
console.log('\n[TAS] Session end checks:');
|
|
109
|
-
checks.forEach(c => console.log(' ' + c));
|
|
110
|
-
console.log('');
|
|
111
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop Hook: End-of-session verification
|
|
4
|
+
*
|
|
5
|
+
* Runs when Claude Code session ends. Performs:
|
|
6
|
+
* 1. Auto-detects test runner and runs the test suite
|
|
7
|
+
* 2. Checks if project-status.yaml was updated today
|
|
8
|
+
* 3. Prints a summary of pass/fail checks
|
|
9
|
+
*
|
|
10
|
+
* Designed to be non-blocking — all failures are reported as warnings,
|
|
11
|
+
* not hard errors, so the session can still end cleanly.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { execSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
const checks = [];
|
|
21
|
+
|
|
22
|
+
// --- Helper: run command, return { ok, output } ---
|
|
23
|
+
function run(cmd, cwd) {
|
|
24
|
+
try {
|
|
25
|
+
const output = execSync(cmd, { cwd, stdio: 'pipe', timeout: 60_000 }).toString();
|
|
26
|
+
return { ok: true, output };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { ok: false, output: (err.stderr || err.stdout || err.message || '').toString() };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- Find repo root (cwd of the session) ---
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
|
|
35
|
+
// ─── 1. Run test suite ────────────────────────────────────────────────────────
|
|
36
|
+
let testRan = false;
|
|
37
|
+
|
|
38
|
+
// Node.js / npm
|
|
39
|
+
if (fs.existsSync(path.join(cwd, 'package.json'))) {
|
|
40
|
+
let hasTestScript = false;
|
|
41
|
+
try {
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
43
|
+
hasTestScript = !!(pkg.scripts && pkg.scripts.test &&
|
|
44
|
+
pkg.scripts.test !== 'echo "Error: no test specified" && exit 1');
|
|
45
|
+
} catch { /* ignore */ }
|
|
46
|
+
|
|
47
|
+
if (hasTestScript) {
|
|
48
|
+
const result = run('npm test', cwd);
|
|
49
|
+
checks.push(result.ok
|
|
50
|
+
? '✓ Tests passed (npm test)'
|
|
51
|
+
: '✗ Tests FAILED (npm test) — fix before committing'
|
|
52
|
+
);
|
|
53
|
+
testRan = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// .NET
|
|
58
|
+
if (!testRan) {
|
|
59
|
+
function findDotnetFiles(startDir, maxDepth = 3) {
|
|
60
|
+
const results = [];
|
|
61
|
+
function walk(dir, depth) {
|
|
62
|
+
if (depth > maxDepth) return;
|
|
63
|
+
try {
|
|
64
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
66
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
67
|
+
} else if (entry.name.endsWith('.csproj') || entry.name.endsWith('.sln')) {
|
|
68
|
+
results.push(entry.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
walk(startDir, 0);
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const csprojFiles = findDotnetFiles(cwd);
|
|
78
|
+
|
|
79
|
+
if (csprojFiles.length > 0) {
|
|
80
|
+
const result = run('dotnet test --no-build --logger "console;verbosity=minimal" 2>&1', cwd);
|
|
81
|
+
checks.push(result.ok
|
|
82
|
+
? '✓ Tests passed (dotnet test)'
|
|
83
|
+
: '✗ Tests FAILED (dotnet test) — fix before committing'
|
|
84
|
+
);
|
|
85
|
+
testRan = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Python (pytest)
|
|
90
|
+
if (!testRan) {
|
|
91
|
+
const hasPytest =
|
|
92
|
+
fs.existsSync(path.join(cwd, 'pytest.ini')) ||
|
|
93
|
+
fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
|
|
94
|
+
fs.existsSync(path.join(cwd, 'setup.cfg'));
|
|
95
|
+
|
|
96
|
+
if (hasPytest) {
|
|
97
|
+
const result = run('python -m pytest -q --tb=no 2>&1', cwd);
|
|
98
|
+
checks.push(result.ok
|
|
99
|
+
? '✓ Tests passed (pytest)'
|
|
100
|
+
: '✗ Tests FAILED (pytest) — fix before committing'
|
|
101
|
+
);
|
|
102
|
+
testRan = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Output ──────────────────────────────────────────────────────────────────
|
|
107
|
+
if (checks.length > 0) {
|
|
108
|
+
console.log('\n[TAS] Session end checks:');
|
|
109
|
+
checks.forEach(c => console.log(' ' + c));
|
|
110
|
+
console.log('');
|
|
111
|
+
}
|