@torus-engineering/tas-kit 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.tas/README.md +334 -334
  2. package/{.claude → .tas/_platform/claude-code}/settings.json +0 -12
  3. package/{.claude → .tas/_platform}/hooks/code-quality.js +1 -1
  4. package/{.claude → .tas/_platform}/hooks/session-end.js +20 -25
  5. package/{.claude → .tas}/commands/ado-create.md +5 -4
  6. package/{.claude → .tas}/commands/ado-delete.md +5 -4
  7. package/{.claude → .tas}/commands/ado-update.md +5 -4
  8. package/{.claude → .tas}/commands/tas-adr.md +3 -3
  9. package/{.claude → .tas}/commands/tas-apitest-plan.md +2 -2
  10. package/{.claude → .tas}/commands/tas-apitest.md +4 -4
  11. package/{.claude → .tas}/commands/tas-bug.md +6 -6
  12. package/{.claude → .tas}/commands/tas-design.md +3 -3
  13. package/{.claude → .tas}/commands/tas-dev.md +11 -14
  14. package/{.claude → .tas}/commands/tas-epic.md +3 -3
  15. package/{.claude → .tas}/commands/tas-feature.md +4 -4
  16. package/{.claude → .tas}/commands/tas-fix.md +5 -5
  17. package/{.claude → .tas}/commands/tas-init.md +1 -1
  18. package/{.claude → .tas}/commands/tas-plan.md +198 -198
  19. package/{.claude → .tas}/commands/tas-prd.md +3 -3
  20. package/{.claude → .tas}/commands/tas-review.md +17 -15
  21. package/{.claude → .tas}/commands/tas-sad.md +3 -3
  22. package/{.claude → .tas}/commands/tas-security.md +4 -4
  23. package/{.claude → .tas}/commands/tas-story.md +3 -3
  24. package/.tas/platforms.json +5 -0
  25. package/.tas/project-status-example.yaml +17 -17
  26. package/{.claude/skills/ado-integration/SKILL.md → .tas/rules/ado-integration.md} +5 -15
  27. package/{.claude/skills/api-design/SKILL.md → .tas/rules/common/api-design.md} +517 -530
  28. package/{.claude → .tas}/rules/common/code-review.md +30 -6
  29. package/{.claude/rules/common/post-review-agent.md → .tas/rules/common/post-implementation-review.md} +51 -49
  30. package/{.claude → .tas}/rules/common/project-status.md +80 -80
  31. package/{.claude → .tas}/rules/common/stack-detection.md +29 -29
  32. package/.tas/{checklists → rules/common}/story-done.md +12 -5
  33. package/{.claude/skills/tas-tdd/SKILL.md → .tas/rules/common/tdd.md} +4 -38
  34. package/{.claude → .tas}/rules/common/testing.md +3 -8
  35. package/{.claude → .tas}/rules/common/token-logging.md +36 -27
  36. package/{.claude → .tas}/rules/csharp/api-testing.md +171 -171
  37. package/{.claude → .tas}/rules/csharp/coding-style.md +0 -2
  38. package/{.claude → .tas}/rules/csharp/security.md +10 -0
  39. package/{.claude → .tas}/rules/python/coding-style.md +0 -2
  40. package/{.claude → .tas}/rules/typescript/coding-style.md +0 -2
  41. package/.tas/rules/typescript/patterns.md +142 -0
  42. package/.tas/rules/typescript/security.md +88 -0
  43. package/{.claude → .tas}/rules/typescript/testing.md +0 -4
  44. package/{.claude → .tas}/rules/web/coding-style.md +0 -2
  45. package/.tas/tas-example.yaml +125 -126
  46. package/.tas/templates/ADR.md +47 -47
  47. package/.tas/templates/Bug.md +67 -67
  48. package/.tas/templates/Design-Spec.md +36 -36
  49. package/.tas/templates/Epic.md +46 -46
  50. package/.tas/templates/Feature.md +1 -1
  51. package/.tas/templates/Security-Report.md +27 -27
  52. package/.tas/tools/tas-ado-readme.md +169 -169
  53. package/.tas/tools/tas-ado.py +621 -621
  54. package/README.md +334 -334
  55. package/bin/cli.js +91 -73
  56. package/lib/adapters/antigravity.js +131 -0
  57. package/lib/adapters/claude-code.js +35 -0
  58. package/lib/adapters/codex.js +157 -0
  59. package/lib/adapters/cursor.js +80 -0
  60. package/lib/adapters/index.js +20 -0
  61. package/lib/adapters/utils.js +81 -0
  62. package/lib/deleted-files.json +99 -0
  63. package/lib/install.js +543 -327
  64. package/package.json +5 -4
  65. package/.claude/agents/code-reviewer.md +0 -41
  66. package/.claude/agents/e2e-runner.md +0 -61
  67. package/.claude/agents/planner.md +0 -82
  68. package/.claude/agents/tdd-guide.md +0 -84
  69. package/.claude/commands/tas-verify.md +0 -51
  70. package/.claude/rules/typescript/patterns.md +0 -62
  71. package/.claude/rules/typescript/security.md +0 -28
  72. package/.claude/settings.local.json +0 -38
  73. package/.claude/skills/ai-regression-testing/SKILL.md +0 -364
  74. package/.claude/skills/architecture-decision-records/SKILL.md +0 -184
  75. package/.claude/skills/benchmark/SKILL.md +0 -98
  76. package/.claude/skills/browser-qa/SKILL.md +0 -92
  77. package/.claude/skills/canary-watch/SKILL.md +0 -104
  78. package/.claude/skills/js-backend-patterns/SKILL.md +0 -603
  79. package/.claude/skills/tas-conventions/SKILL.md +0 -65
  80. package/.claude/skills/tas-implementation-complete/SKILL.md +0 -100
  81. package/.claude/skills/token-logger/SKILL.md +0 -19
  82. package/.tas/checklists/code-review.md +0 -29
  83. package/.tas/checklists/security.md +0 -21
  84. /package/{.claude → .tas}/agents/architect.md +0 -0
  85. /package/{.claude → .tas}/agents/aws-reviewer.md +0 -0
  86. /package/{.claude → .tas}/agents/build-resolver.md +0 -0
  87. /package/{.claude → .tas}/agents/code-explorer.md +0 -0
  88. /package/{.claude → .tas}/agents/csharp-reviewer.md +0 -0
  89. /package/{.claude → .tas}/agents/database-reviewer.md +0 -0
  90. /package/{.claude → .tas}/agents/doc-updater.md +0 -0
  91. /package/{.claude → .tas}/agents/python-reviewer.md +0 -0
  92. /package/{.claude → .tas}/agents/security-reviewer.md +0 -0
  93. /package/{.claude → .tas}/agents/typescript-reviewer.md +0 -0
  94. /package/{.claude → .tas}/commands/ado-get.md +0 -0
  95. /package/{.claude → .tas}/commands/ado-status.md +0 -0
  96. /package/{.claude → .tas}/commands/tas-brainstorm.md +0 -0
  97. /package/{.claude → .tas}/commands/tas-e2e-mobile.md +0 -0
  98. /package/{.claude → .tas}/commands/tas-e2e-web.md +0 -0
  99. /package/{.claude → .tas}/commands/tas-e2e.md +0 -0
  100. /package/{.claude → .tas}/commands/tas-functest-mobile.md +0 -0
  101. /package/{.claude → .tas}/commands/tas-functest-web.md +0 -0
  102. /package/{.claude → .tas}/commands/tas-functest.md +0 -0
  103. /package/{.claude → .tas}/commands/tas-spec.md +0 -0
  104. /package/{.claude → .tas}/commands/tas-status.md +0 -0
  105. /package/{.claude → .tas}/rules/.gitkeep +0 -0
  106. /package/{.claude → .tas}/rules/common/hooks.md +0 -0
  107. /package/{.claude → .tas}/rules/common/patterns.md +0 -0
  108. /package/{.claude → .tas}/rules/common/security.md +0 -0
  109. /package/{.claude → .tas}/rules/csharp/hooks.md +0 -0
  110. /package/{.claude → .tas}/rules/csharp/patterns.md +0 -0
  111. /package/{.claude → .tas}/rules/csharp/testing.md +0 -0
  112. /package/{.claude → .tas}/rules/python/hooks.md +0 -0
  113. /package/{.claude → .tas}/rules/python/patterns.md +0 -0
  114. /package/{.claude → .tas}/rules/python/security.md +0 -0
  115. /package/{.claude → .tas}/rules/python/testing.md +0 -0
  116. /package/{.claude → .tas}/rules/typescript/hooks.md +0 -0
  117. /package/{.claude → .tas}/rules/web/design-quality.md +0 -0
  118. /package/{.claude → .tas}/rules/web/hooks.md +0 -0
  119. /package/{.claude → .tas}/rules/web/patterns.md +0 -0
  120. /package/{.claude → .tas}/rules/web/performance.md +0 -0
  121. /package/{.claude → .tas}/rules/web/security.md +0 -0
  122. /package/{.claude → .tas}/rules/web/testing.md +0 -0
  123. /package/{CLAUDE-Example.md → .tas/templates/AGENTS.md} +0 -0
package/bin/cli.js CHANGED
@@ -1,73 +1,91 @@
1
- #!/usr/bin/env node
2
- import { install, update } from '../lib/install.js';
3
-
4
- const args = process.argv.slice(2);
5
- const command = args[0];
6
-
7
- function printHelp() {
8
- console.log(`
9
- tas-kit — Torus Agentic SDLC Kit installer
10
-
11
- Usage:
12
- npx @torus-engineering/tas-kit <command> [options]
13
-
14
- Commands:
15
- install Copy TAS Kit files into a project (first-time setup)
16
- update Overwrite .claude/ and .tas/ with the latest kit version
17
- (preserves CLAUDE.md, tas.yaml, .env.example)
18
-
19
- Options:
20
- --directory <path> Target directory (default: current working directory)
21
- --yes, -y Skip confirmation prompts (uses native hook by default)
22
- --security-hook <mode> Pre-commit hook mode: husky | native | none
23
- Default: interactive prompt on install,
24
- no-op on update unless specified.
25
- --help, -h Show this help message
26
-
27
- Examples:
28
- npx @torus-engineering/tas-kit install
29
- npx @torus-engineering/tas-kit install --directory /path/to/my-project
30
- npx @torus-engineering/tas-kit install --yes --security-hook=husky
31
- npx @torus-engineering/tas-kit install --security-hook=none
32
- npx @torus-engineering/tas-kit update
33
- npx @torus-engineering/tas-kit update --yes --security-hook=native
34
- `.trim());
35
- }
36
-
37
- if (!command || command === '--help' || command === '-h') {
38
- printHelp();
39
- process.exit(0);
40
- }
41
-
42
- if (command !== 'install' && command !== 'update') {
43
- console.error(`Unknown command: "${command}"\n`);
44
- printHelp();
45
- process.exit(1);
46
- }
47
-
48
- const opts = { directory: process.cwd(), yes: false, securityHook: null };
49
-
50
- for (let i = 1; i < args.length; i++) {
51
- const a = args[i];
52
- if ((a === '--directory' || a === '-d') && args[i + 1]) {
53
- opts.directory = args[++i];
54
- } else if (a === '--yes' || a === '-y') {
55
- opts.yes = true;
56
- } else if (a.startsWith('--security-hook=')) {
57
- opts.securityHook = a.slice('--security-hook='.length).toLowerCase();
58
- } else if (a === '--security-hook' && args[i + 1]) {
59
- opts.securityHook = args[++i].toLowerCase();
60
- }
61
- }
62
-
63
- if (opts.securityHook && !['husky', 'native', 'none'].includes(opts.securityHook)) {
64
- console.error(`Invalid --security-hook value: "${opts.securityHook}"`);
65
- console.error(`Expected one of: husky, native, none`);
66
- process.exit(1);
67
- }
68
-
69
- const runner = command === 'update' ? update : install;
70
- runner(opts).catch((err) => {
71
- console.error(`\n${command === 'update' ? 'Update' : 'Install'} failed:`, err.message);
72
- process.exit(1);
73
- });
1
+ #!/usr/bin/env node
2
+ import { install, update } from '../lib/install.js';
3
+
4
+ const args = process.argv.slice(2);
5
+ const command = args[0];
6
+
7
+ function printHelp() {
8
+ console.log(`
9
+ tas-kit — Turbo Agentic SDLC Kit installer
10
+
11
+ Usage:
12
+ npx @torus-engineering/tas-kit <command> [options]
13
+
14
+ Commands:
15
+ install Copy TAS Kit files into a project (first-time setup)
16
+ update Overwrite .tas/ and platform directories with the latest kit version
17
+ (preserves CLAUDE.md, tas.yaml, .env.example)
18
+
19
+ Options:
20
+ --directory <path> Target directory (default: current working directory)
21
+ --yes, -y Skip confirmation prompts
22
+ --platform <id,...> Platform(s) to install: claude-code, cursor, codex, antigravity
23
+ Comma-separated for multiple (default: interactive prompt)
24
+ --security-hook <mode> Pre-commit hook mode: husky | native | none
25
+ --help, -h Show this help message
26
+
27
+ Platforms:
28
+ claude-code Claude Code CLI (default)
29
+ cursor Cursor IDE
30
+ codex OpenAI Codex CLI
31
+ antigravity Google Antigravity IDE
32
+
33
+ Examples:
34
+ npx @torus-engineering/tas-kit install
35
+ npx @torus-engineering/tas-kit install --platform claude-code,cursor
36
+ npx @torus-engineering/tas-kit install --platform codex --yes
37
+ npx @torus-engineering/tas-kit install --yes --security-hook=husky
38
+ npx @torus-engineering/tas-kit update
39
+ npx @torus-engineering/tas-kit update --platform cursor
40
+ `.trim());
41
+ }
42
+
43
+ if (!command || command === '--help' || command === '-h') {
44
+ printHelp();
45
+ process.exit(0);
46
+ }
47
+
48
+ if (command !== 'install' && command !== 'update') {
49
+ console.error(`Unknown command: "${command}"\n`);
50
+ printHelp();
51
+ process.exit(1);
52
+ }
53
+
54
+ const VALID_PLATFORMS = ['claude-code', 'cursor', 'codex', 'antigravity'];
55
+ const opts = { directory: process.cwd(), yes: false, securityHook: null, platforms: [] };
56
+
57
+ for (let i = 1; i < args.length; i++) {
58
+ const a = args[i];
59
+ if ((a === '--directory' || a === '-d') && args[i + 1]) {
60
+ opts.directory = args[++i];
61
+ } else if (a === '--yes' || a === '-y') {
62
+ opts.yes = true;
63
+ } else if (a.startsWith('--security-hook=')) {
64
+ opts.securityHook = a.slice('--security-hook='.length).toLowerCase();
65
+ } else if (a === '--security-hook' && args[i + 1]) {
66
+ opts.securityHook = args[++i].toLowerCase();
67
+ } else if (a.startsWith('--platform=')) {
68
+ opts.platforms = a.slice('--platform='.length).split(',').map(s => s.trim().toLowerCase());
69
+ } else if (a === '--platform' && args[i + 1]) {
70
+ opts.platforms = args[++i].split(',').map(s => s.trim().toLowerCase());
71
+ }
72
+ }
73
+
74
+ if (opts.securityHook && !['husky', 'native', 'none'].includes(opts.securityHook)) {
75
+ console.error(`Invalid --security-hook value: "${opts.securityHook}"`);
76
+ console.error(`Expected one of: husky, native, none`);
77
+ process.exit(1);
78
+ }
79
+
80
+ const invalidPlatforms = opts.platforms.filter(p => !VALID_PLATFORMS.includes(p));
81
+ if (invalidPlatforms.length > 0) {
82
+ console.error(`Invalid platform(s): ${invalidPlatforms.join(', ')}`);
83
+ console.error(`Valid platforms: ${VALID_PLATFORMS.join(', ')}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ const runner = command === 'update' ? update : install;
88
+ runner(opts).catch((err) => {
89
+ console.error(`\n${command === 'update' ? 'Update' : 'Install'} failed:`, err.message);
90
+ process.exit(1);
91
+ });
@@ -0,0 +1,131 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ exists, listFilesRecursive,
5
+ parseFrontmatter, extractDescription, extractCommandName, extractH1, yamlValue,
6
+ } from './utils.js';
7
+
8
+ export const PLATFORM_ID = 'antigravity';
9
+ export const PLATFORM_LABEL = 'Antigravity';
10
+
11
+ export async function install({ tasDir, target }) {
12
+ const agentsDir = path.join(target, '.agents');
13
+ await fs.mkdir(agentsDir, { recursive: true });
14
+ await fs.mkdir(path.join(agentsDir, 'rules'), { recursive: true });
15
+ await fs.mkdir(path.join(agentsDir, 'skills'), { recursive: true });
16
+
17
+ // commands → .agents/<name>.md (Antigravity workflows)
18
+ const commandsDir = path.join(tasDir, 'commands');
19
+ if (await exists(commandsDir)) {
20
+ const files = await listFilesRecursive(commandsDir, '.md');
21
+ for (const file of files) {
22
+ await commandToWorkflow(file, agentsDir);
23
+ }
24
+ console.log(' [ok] .agents/ (workflows from commands)');
25
+ }
26
+
27
+ // agents → .agents/skills/<name>/SKILL.md
28
+ const agentsSrc = path.join(tasDir, 'agents');
29
+ if (await exists(agentsSrc)) {
30
+ const files = await listFilesRecursive(agentsSrc, '.md');
31
+ for (const file of files) {
32
+ await agentToSkill(file, path.join(agentsDir, 'skills'));
33
+ }
34
+ console.log(' [ok] .agents/skills/ (agents)');
35
+ }
36
+
37
+ // kit skills → .agents/skills/ (copy as-is, same open standard)
38
+ const skillsSrc = path.join(tasDir, 'skills');
39
+ if (await exists(skillsSrc)) {
40
+ const skillDirs = await fs.readdir(skillsSrc, { withFileTypes: true });
41
+ for (const d of skillDirs.filter(e => e.isDirectory())) {
42
+ await fs.cp(
43
+ path.join(skillsSrc, d.name),
44
+ path.join(agentsDir, 'skills', d.name),
45
+ { recursive: true }
46
+ );
47
+ }
48
+ console.log(' [ok] .agents/skills/ (skills)');
49
+ }
50
+
51
+ // rules → .agents/rules/<stack>-<topic>.md
52
+ const rulesDir = path.join(tasDir, 'rules');
53
+ if (await exists(rulesDir)) {
54
+ await convertRules(rulesDir, path.join(agentsDir, 'rules'));
55
+ console.log(' [ok] .agents/rules/ (from .tas/rules/)');
56
+ }
57
+
58
+ // platform index (not root AGENTS.md — that's the template/system prompt)
59
+ await generateReadme(agentsDir);
60
+ console.log(' [ok] .agents/README.md');
61
+ }
62
+
63
+ async function commandToWorkflow(file, agentsDir) {
64
+ const content = await fs.readFile(file, 'utf8');
65
+ const name = extractCommandName(content) || path.basename(file, '.md');
66
+ // Antigravity workflows: same .md format, stored in .agents/<name>.md
67
+ await fs.writeFile(path.join(agentsDir, `${name}.md`), content);
68
+ }
69
+
70
+ async function agentToSkill(file, skillsDir) {
71
+ const content = await fs.readFile(file, 'utf8');
72
+ const { frontmatter, body } = parseFrontmatter(content);
73
+ const name = frontmatter.name || path.basename(file, '.md');
74
+ const description = frontmatter.description || extractDescription(body, name);
75
+ const skillDir = path.join(skillsDir, name);
76
+ await fs.mkdir(skillDir, { recursive: true });
77
+ // Antigravity SKILL.md: same open standard, drop allowed-tools (platform manages tool access)
78
+ const skillMd = `---\nname: ${yamlValue(name)}\ndescription: ${yamlValue(description)}\n---\n\n${body}`;
79
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
80
+ }
81
+
82
+ async function convertRules(rulesDir, outDir) {
83
+ const entries = await fs.readdir(rulesDir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ if (entry.isDirectory()) {
86
+ const stack = entry.name;
87
+ const files = await listFilesRecursive(path.join(rulesDir, stack), '.md');
88
+ for (const f of files) {
89
+ const topic = path.basename(f, '.md');
90
+ await fs.copyFile(f, path.join(outDir, `${stack}-${topic}.md`));
91
+ }
92
+ } else if (entry.name.endsWith('.md')) {
93
+ await fs.copyFile(path.join(rulesDir, entry.name), path.join(outDir, entry.name));
94
+ }
95
+ }
96
+ }
97
+
98
+ async function generateReadme(agentsDir) {
99
+ const workflowFiles = (await fs.readdir(agentsDir, { withFileTypes: true }))
100
+ .filter(e => e.isFile() && e.name.endsWith('.md'))
101
+ .map(e => `- \`/${e.name.replace('.md', '')}\``);
102
+
103
+ const skillDirs = await fs.readdir(path.join(agentsDir, 'skills'), { withFileTypes: true });
104
+ const skillLines = [];
105
+ for (const d of skillDirs.filter(e => e.isDirectory())) {
106
+ const skillFile = path.join(agentsDir, 'skills', d.name, 'SKILL.md');
107
+ if (!(await exists(skillFile))) continue;
108
+ const content = await fs.readFile(skillFile, 'utf8');
109
+ const { frontmatter } = parseFrontmatter(content);
110
+ if (frontmatter.name) {
111
+ skillLines.push(`- **${frontmatter.name}**: ${frontmatter.description || ''}`);
112
+ }
113
+ }
114
+
115
+ const readme = [
116
+ `# TAS Kit — Antigravity`,
117
+ ``,
118
+ `## Workflows`,
119
+ ``,
120
+ `Invoke via \`/workflow-name\` in chat:`,
121
+ ``,
122
+ ...workflowFiles,
123
+ ``,
124
+ `## Skills`,
125
+ ``,
126
+ ...skillLines,
127
+ ``,
128
+ ].join('\n');
129
+
130
+ await fs.writeFile(path.join(agentsDir, 'README.md'), readme);
131
+ }
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { copyDir, exists } from './utils.js';
4
+
5
+ export const PLATFORM_ID = 'claude-code';
6
+ export const PLATFORM_LABEL = 'Claude Code';
7
+
8
+ export async function install({ tasDir, target }) {
9
+ const claudeDir = path.join(target, '.claude');
10
+ await fs.mkdir(claudeDir, { recursive: true });
11
+
12
+ await copyDir(path.join(tasDir, 'commands'), path.join(claudeDir, 'commands'));
13
+ console.log(' [ok] .claude/commands/');
14
+
15
+ await copyDir(path.join(tasDir, 'agents'), path.join(claudeDir, 'agents'));
16
+ console.log(' [ok] .claude/agents/');
17
+
18
+ const skillsSrc = path.join(tasDir, 'skills');
19
+ if (await exists(skillsSrc)) {
20
+ await copyDir(skillsSrc, path.join(claudeDir, 'skills'));
21
+ console.log(' [ok] .claude/skills/');
22
+ }
23
+
24
+ const hooksSrc = path.join(tasDir, '_platform', 'hooks');
25
+ if (await exists(hooksSrc)) {
26
+ await copyDir(hooksSrc, path.join(claudeDir, 'hooks'));
27
+ console.log(' [ok] .claude/hooks/');
28
+ }
29
+
30
+ const settingsSrc = path.join(tasDir, '_platform', 'claude-code', 'settings.json');
31
+ if (await exists(settingsSrc)) {
32
+ await fs.copyFile(settingsSrc, path.join(claudeDir, 'settings.json'));
33
+ console.log(' [ok] .claude/settings.json');
34
+ }
35
+ }
@@ -0,0 +1,157 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ exists, listFilesRecursive,
5
+ parseFrontmatter, extractDescription, extractCommandName, extractH1, yamlValue,
6
+ } from './utils.js';
7
+
8
+ export const PLATFORM_ID = 'codex';
9
+ export const PLATFORM_LABEL = 'Codex';
10
+
11
+ export async function install({ tasDir, target }) {
12
+ const codexDir = path.join(target, '.codex');
13
+ const skillsOut = path.join(codexDir, 'skills');
14
+ await fs.mkdir(skillsOut, { recursive: true });
15
+
16
+ // commands → skills
17
+ const commandsDir = path.join(tasDir, 'commands');
18
+ if (await exists(commandsDir)) {
19
+ const files = await listFilesRecursive(commandsDir, '.md');
20
+ for (const file of files) {
21
+ await commandToSkill(file, skillsOut);
22
+ }
23
+ console.log(' [ok] .codex/skills/ (commands)');
24
+ }
25
+
26
+ // agents → skills
27
+ const agentsDir = path.join(tasDir, 'agents');
28
+ if (await exists(agentsDir)) {
29
+ const files = await listFilesRecursive(agentsDir, '.md');
30
+ for (const file of files) {
31
+ await agentToSkill(file, skillsOut);
32
+ }
33
+ console.log(' [ok] .codex/skills/ (agents)');
34
+ }
35
+
36
+ // skills → skills (copy as-is, same format)
37
+ const skillsSrc = path.join(tasDir, 'skills');
38
+ if (await exists(skillsSrc)) {
39
+ const skillDirs = await fs.readdir(skillsSrc, { withFileTypes: true });
40
+ for (const d of skillDirs.filter(e => e.isDirectory())) {
41
+ const src = path.join(skillsSrc, d.name);
42
+ const dest = path.join(skillsOut, d.name);
43
+ await fs.cp(src, dest, { recursive: true });
44
+ }
45
+ console.log(' [ok] .codex/skills/ (skills)');
46
+ }
47
+
48
+ // rules → skills with references/
49
+ const rulesDir = path.join(tasDir, 'rules');
50
+ if (await exists(rulesDir)) {
51
+ await rulesToSkills(rulesDir, skillsOut);
52
+ console.log(' [ok] .codex/skills/ (rules)');
53
+ }
54
+
55
+ // platform index (not root AGENTS.md — that's the template/system prompt)
56
+ await generateReadme(skillsOut, codexDir);
57
+ console.log(' [ok] .codex/README.md');
58
+ }
59
+
60
+ async function commandToSkill(file, skillsOut) {
61
+ const content = await fs.readFile(file, 'utf8');
62
+ const name = extractCommandName(content) || path.basename(file, '.md');
63
+ const description = extractDescription(content, name);
64
+ const skillDir = path.join(skillsOut, name);
65
+ await fs.mkdir(skillDir, { recursive: true });
66
+ const skillMd = `---\nname: ${yamlValue(name)}\ndescription: ${yamlValue(description)}\n---\n\n${content}`;
67
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
68
+ }
69
+
70
+ async function agentToSkill(file, skillsOut) {
71
+ const content = await fs.readFile(file, 'utf8');
72
+ const { frontmatter, body } = parseFrontmatter(content);
73
+ const name = frontmatter.name || path.basename(file, '.md');
74
+ const description = frontmatter.description || extractDescription(body, name);
75
+ const skillDir = path.join(skillsOut, name);
76
+ await fs.mkdir(skillDir, { recursive: true });
77
+ const skillMd = `---\nname: ${yamlValue(name)}\ndescription: ${yamlValue(description)}\n---\n\n${body}`;
78
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
79
+ }
80
+
81
+ async function rulesToSkills(rulesDir, skillsOut) {
82
+ const stacks = await fs.readdir(rulesDir, { withFileTypes: true });
83
+ for (const entry of stacks) {
84
+ if (!entry.isDirectory() && !entry.name.endsWith('.md')) continue;
85
+
86
+ if (entry.isDirectory()) {
87
+ const stack = entry.name;
88
+ const stackDir = path.join(rulesDir, stack);
89
+ const files = await listFilesRecursive(stackDir, '.md');
90
+ if (files.length === 0) continue;
91
+
92
+ const skillName = `${stack}-rules`;
93
+ const skillDir = path.join(skillsOut, skillName);
94
+ const refsDir = path.join(skillDir, 'references');
95
+ await fs.mkdir(refsDir, { recursive: true });
96
+
97
+ const refList = [];
98
+ for (const f of files) {
99
+ const dest = path.join(refsDir, path.basename(f));
100
+ await fs.copyFile(f, dest);
101
+ refList.push(`- ${path.basename(f, '.md')}`);
102
+ }
103
+
104
+ const skillMd = [
105
+ `---`,
106
+ `name: ${yamlValue(skillName)}`,
107
+ `description: ${yamlValue(`${stack} coding standards, patterns and security rules`)}`,
108
+ `---`,
109
+ ``,
110
+ `# ${stack} Rules`,
111
+ ``,
112
+ `References (loaded on demand):`,
113
+ ...refList,
114
+ ].join('\n') + '\n';
115
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
116
+ } else {
117
+ // top-level .md file (e.g. ado-integration.md)
118
+ const file = path.join(rulesDir, entry.name);
119
+ const content = await fs.readFile(file, 'utf8');
120
+ const name = path.basename(entry.name, '.md') + '-rules';
121
+ const skillDir = path.join(skillsOut, name);
122
+ const refsDir = path.join(skillDir, 'references');
123
+ await fs.mkdir(refsDir, { recursive: true });
124
+ await fs.copyFile(file, path.join(refsDir, entry.name));
125
+ const title = extractH1(content) || name;
126
+ const skillMd = `---\nname: ${yamlValue(name)}\ndescription: ${yamlValue(title)}\n---\n\n# ${title}\n\nSee references/${entry.name}\n`;
127
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
128
+ }
129
+ }
130
+ }
131
+
132
+ async function generateReadme(skillsOut, codexDir) {
133
+ const skillDirs = await fs.readdir(skillsOut, { withFileTypes: true });
134
+ const skillLines = [];
135
+ for (const d of skillDirs.filter(e => e.isDirectory())) {
136
+ const skillFile = path.join(skillsOut, d.name, 'SKILL.md');
137
+ if (!(await exists(skillFile))) continue;
138
+ const content = await fs.readFile(skillFile, 'utf8');
139
+ const { frontmatter } = parseFrontmatter(content);
140
+ if (frontmatter.name) {
141
+ skillLines.push(`- **${frontmatter.name}**: ${frontmatter.description || ''}`);
142
+ }
143
+ }
144
+
145
+ const readme = [
146
+ `# TAS Kit — Codex`,
147
+ ``,
148
+ `## Available Skills`,
149
+ ``,
150
+ `Use \`/skills\` to list and invoke skills:`,
151
+ ``,
152
+ ...skillLines,
153
+ ``,
154
+ ].join('\n');
155
+
156
+ await fs.writeFile(path.join(codexDir, 'README.md'), readme);
157
+ }
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { copyDir, exists, listFilesRecursive, extractH1, yamlValue } from './utils.js';
4
+
5
+ export const PLATFORM_ID = 'cursor';
6
+ export const PLATFORM_LABEL = 'Cursor';
7
+
8
+ const HOOKS_JSON = {
9
+ postToolUse: [
10
+ {
11
+ matcher: '^(Write|Edit|MultiEdit)$',
12
+ hooks: [
13
+ {
14
+ type: 'command',
15
+ command: 'node .tas/_platform/hooks/code-quality.js',
16
+ description: 'Auto format source files: prettier (TS/JS), ruff/black (Python), dotnet format (C#)',
17
+ },
18
+ ],
19
+ },
20
+ ],
21
+ agentLoopEnd: [
22
+ {
23
+ hooks: [
24
+ {
25
+ type: 'command',
26
+ command: 'node .tas/_platform/hooks/session-end.js',
27
+ description: 'Run test suite, check project-status.yaml updated, remind next steps',
28
+ },
29
+ ],
30
+ },
31
+ ],
32
+ };
33
+
34
+ export async function install({ tasDir, target }) {
35
+ const cursorDir = path.join(target, '.cursor');
36
+ await fs.mkdir(cursorDir, { recursive: true });
37
+
38
+ // commands — same format as Claude Code
39
+ await copyDir(path.join(tasDir, 'commands'), path.join(cursorDir, 'commands'));
40
+ console.log(' [ok] .cursor/commands/');
41
+
42
+ // agents — identical frontmatter format
43
+ await copyDir(path.join(tasDir, 'agents'), path.join(cursorDir, 'agents'));
44
+ console.log(' [ok] .cursor/agents/');
45
+
46
+ // skills — same open standard
47
+ const skillsSrc = path.join(tasDir, 'skills');
48
+ if (await exists(skillsSrc)) {
49
+ await copyDir(skillsSrc, path.join(cursorDir, 'skills'));
50
+ console.log(' [ok] .cursor/skills/');
51
+ }
52
+
53
+ // rules → .mdc files with frontmatter
54
+ const rulesDir = path.join(tasDir, 'rules');
55
+ const rulesOut = path.join(cursorDir, 'rules');
56
+ await fs.mkdir(rulesOut, { recursive: true });
57
+ await convertRulesToMdc(rulesDir, rulesOut);
58
+ console.log(' [ok] .cursor/rules/ (converted from .tas/rules/)');
59
+
60
+ // hooks.json
61
+ await fs.writeFile(
62
+ path.join(cursorDir, 'hooks.json'),
63
+ JSON.stringify(HOOKS_JSON, null, 2) + '\n'
64
+ );
65
+ console.log(' [ok] .cursor/hooks.json');
66
+ }
67
+
68
+ async function convertRulesToMdc(rulesDir, outDir) {
69
+ if (!(await exists(rulesDir))) return;
70
+ const files = await listFilesRecursive(rulesDir, '.md');
71
+ for (const file of files) {
72
+ const rel = path.relative(rulesDir, file);
73
+ // flatten: common/code-review.md → common-code-review.mdc
74
+ const flatName = rel.replace(/[\\/]/g, '-').replace(/\.md$/, '.mdc');
75
+ const content = await fs.readFile(file, 'utf8');
76
+ const title = extractH1(content) || path.basename(file, '.md');
77
+ const mdc = `---\ndescription: ${yamlValue(title)}\nalwaysApply: false\n---\n\n${content}`;
78
+ await fs.writeFile(path.join(outDir, flatName), mdc);
79
+ }
80
+ }
@@ -0,0 +1,20 @@
1
+ export { PLATFORM_ID as CLAUDE_CODE, install as installClaudeCode } from './claude-code.js';
2
+ export { PLATFORM_ID as CURSOR, install as installCursor } from './cursor.js';
3
+ export { PLATFORM_ID as CODEX, install as installCodex } from './codex.js';
4
+ export { PLATFORM_ID as ANTIGRAVITY, install as installAntigravity } from './antigravity.js';
5
+
6
+ import * as claudeCode from './claude-code.js';
7
+ import * as cursor from './cursor.js';
8
+ import * as codex from './codex.js';
9
+ import * as antigravity from './antigravity.js';
10
+
11
+ export const PLATFORMS = [
12
+ { id: claudeCode.PLATFORM_ID, label: claudeCode.PLATFORM_LABEL, install: claudeCode.install },
13
+ { id: cursor.PLATFORM_ID, label: cursor.PLATFORM_LABEL, install: cursor.install },
14
+ { id: codex.PLATFORM_ID, label: codex.PLATFORM_LABEL, install: codex.install },
15
+ { id: antigravity.PLATFORM_ID, label: antigravity.PLATFORM_LABEL, install: antigravity.install },
16
+ ];
17
+
18
+ export function getPlatform(id) {
19
+ return PLATFORMS.find(p => p.id === id);
20
+ }
@@ -0,0 +1,81 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export async function exists(p) {
5
+ return fs.access(p).then(() => true).catch(() => false);
6
+ }
7
+
8
+ export async function copyDir(src, dest) {
9
+ await fs.cp(src, dest, { recursive: true });
10
+ }
11
+
12
+ export async function listFilesRecursive(dir, ext = '') {
13
+ const results = [];
14
+ async function walk(current) {
15
+ const entries = await fs.readdir(current, { withFileTypes: true });
16
+ for (const e of entries) {
17
+ const full = path.join(current, e.name);
18
+ if (e.isDirectory()) await walk(full);
19
+ else if (!ext || e.name.endsWith(ext)) results.push(full);
20
+ }
21
+ }
22
+ await walk(dir);
23
+ return results;
24
+ }
25
+
26
+ export function parseFrontmatter(content) {
27
+ if (!content.startsWith('---')) return { frontmatter: {}, body: content };
28
+ const end = content.indexOf('\n---', 3);
29
+ if (end === -1) return { frontmatter: {}, body: content };
30
+ const yaml = content.slice(4, end);
31
+ const body = content.slice(end + 4).trimStart();
32
+ const frontmatter = {};
33
+ for (const line of yaml.split('\n')) {
34
+ const colon = line.indexOf(':');
35
+ if (colon > 0) {
36
+ const key = line.slice(0, colon).trim();
37
+ let val = line.slice(colon + 1).trim();
38
+ // handle multiline: only single-line values here
39
+ val = val.replace(/^["']|["']$/g, '');
40
+ frontmatter[key] = val;
41
+ }
42
+ }
43
+ return { frontmatter, body };
44
+ }
45
+
46
+ // Extract description: first non-empty, non-heading line after the H1
47
+ export function extractDescription(content, fallback = '') {
48
+ const lines = content.split('\n');
49
+ let pastH1 = false;
50
+ for (const line of lines) {
51
+ const trimmed = line.trim();
52
+ if (!pastH1) {
53
+ if (trimmed.startsWith('# ')) pastH1 = true;
54
+ continue;
55
+ }
56
+ if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('|') && !trimmed.startsWith('-')) {
57
+ return trimmed.replace(/^Role:\s*/i, '').replace(/\*\*/g, '').slice(0, 150);
58
+ }
59
+ }
60
+ return fallback;
61
+ }
62
+
63
+ // Extract H1 title from markdown
64
+ export function extractH1(content) {
65
+ const match = content.match(/^#\s+(.+)$/m);
66
+ return match ? match[1].trim() : '';
67
+ }
68
+
69
+ // Extract command name from first line: "# /tas-dev $ARGUMENTS" → "tas-dev"
70
+ export function extractCommandName(content) {
71
+ const match = content.match(/^#\s+\/(\S+)/);
72
+ return match ? match[1] : '';
73
+ }
74
+
75
+ // Escape a string for use in YAML frontmatter value (single-line)
76
+ export function yamlValue(str) {
77
+ if (/[:#\[\]{},|>&*!'"\\]/.test(str) || str.includes('\n')) {
78
+ return `"${str.replace(/"/g, '\\"')}"`;
79
+ }
80
+ return str;
81
+ }