.takt/tasks/ に TASK-FORMAT ドキュメントを配置、dotgitignore リネーム対応

This commit is contained in:
nrslib 2026-01-29 17:06:16 +09:00
parent a4e793c070
commit 73c2d6b381
5 changed files with 129 additions and 4 deletions

View File

@ -0,0 +1,37 @@
TAKT Task File Format
=====================
Tasks placed in this directory (.takt/tasks/) will be processed by TAKT.
## YAML Format (Recommended)
# .takt/tasks/my-task.yaml
task: "Task description"
worktree: true # (optional) true | "/path/to/dir"
branch: "feat/my-feature" # (optional) branch name
workflow: "default" # (optional) workflow name
Fields:
task (required) Task description (string)
worktree (optional) true: create shared clone, "/path": clone at path
branch (optional) Branch name (auto-generated if omitted: takt/{timestamp}-{slug})
workflow (optional) Workflow name (uses current workflow if omitted)
## Markdown Format (Simple)
# .takt/tasks/my-task.md
Entire file content becomes the task description.
Supports multiline. No structured options available.
## Supported Extensions
.yaml, .yml -> YAML format (parsed and validated)
.md -> Markdown format (plain text, backward compatible)
## Commands
takt /add-task Add a task interactively
takt /run-tasks Run all pending tasks
takt /watch Watch and auto-run tasks
takt /list-tasks List task branches (merge/delete)

View File

@ -27,7 +27,7 @@ vi.mock('../prompt/index.js', () => ({
// Import after mocks are set up // Import after mocks are set up
const { needsLanguageSetup } = await import('../config/initialization.js'); const { needsLanguageSetup } = await import('../config/initialization.js');
const { getGlobalAgentsDir, getGlobalWorkflowsDir } = await import('../config/paths.js'); const { getGlobalAgentsDir, getGlobalWorkflowsDir } = await import('../config/paths.js');
const { copyLanguageResourcesToDir, getLanguageResourcesDir } = await import('../resources/index.js'); const { copyLanguageResourcesToDir, copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js');
describe('initialization', () => { describe('initialization', () => {
beforeEach(() => { beforeEach(() => {
@ -87,6 +87,43 @@ describe('initialization', () => {
}); });
}); });
describe('copyProjectResourcesToDir', () => {
const testProjectDir = join(tmpdir(), `takt-project-test-${Date.now()}`);
beforeEach(() => {
mkdirSync(testProjectDir, { recursive: true });
});
afterEach(() => {
if (existsSync(testProjectDir)) {
rmSync(testProjectDir, { recursive: true });
}
});
it('should rename dotgitignore to .gitignore during copy', () => {
const resourcesDir = getProjectResourcesDir();
if (!existsSync(join(resourcesDir, 'dotgitignore'))) {
return; // Skip if resource file doesn't exist
}
copyProjectResourcesToDir(testProjectDir);
expect(existsSync(join(testProjectDir, '.gitignore'))).toBe(true);
expect(existsSync(join(testProjectDir, 'dotgitignore'))).toBe(false);
});
it('should copy tasks/TASK-FORMAT to target directory', () => {
const resourcesDir = getProjectResourcesDir();
if (!existsSync(join(resourcesDir, 'tasks', 'TASK-FORMAT'))) {
return; // Skip if resource file doesn't exist
}
copyProjectResourcesToDir(testProjectDir);
expect(existsSync(join(testProjectDir, 'tasks', 'TASK-FORMAT'))).toBe(true);
});
});
describe('getLanguageResourcesDir', () => { describe('getLanguageResourcesDir', () => {
it('should return correct path for English', () => { it('should return correct path for English', () => {
const path = getLanguageResourcesDir('en'); const path = getLanguageResourcesDir('en');

View File

@ -6,6 +6,52 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, readdirSync } from 'node:fs'; import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { TaskRunner } from '../task/runner.js'; import { TaskRunner } from '../task/runner.js';
import { isTaskFile, parseTaskFiles } from '../task/parser.js';
describe('isTaskFile', () => {
it('should accept .yaml files', () => {
expect(isTaskFile('task.yaml')).toBe(true);
});
it('should accept .yml files', () => {
expect(isTaskFile('task.yml')).toBe(true);
});
it('should accept .md files', () => {
expect(isTaskFile('task.md')).toBe(true);
});
it('should reject extensionless files like TASK-FORMAT', () => {
expect(isTaskFile('TASK-FORMAT')).toBe(false);
});
it('should reject .txt files', () => {
expect(isTaskFile('readme.txt')).toBe(false);
});
});
describe('parseTaskFiles', () => {
const testDir = `/tmp/takt-parse-test-${Date.now()}`;
beforeEach(() => {
mkdirSync(testDir, { recursive: true });
});
afterEach(() => {
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
it('should ignore extensionless files like TASK-FORMAT', () => {
writeFileSync(join(testDir, 'TASK-FORMAT'), 'Format documentation');
writeFileSync(join(testDir, 'real-task.md'), 'Real task');
const tasks = parseTaskFiles(testDir);
expect(tasks).toHaveLength(1);
expect(tasks[0]?.name).toBe('real-task');
});
});
describe('TaskRunner', () => { describe('TaskRunner', () => {
const testDir = `/tmp/takt-task-test-${Date.now()}`; const testDir = `/tmp/takt-task-test-${Date.now()}`;

View File

@ -67,7 +67,9 @@ export function copyProjectResourcesToDir(targetDir: string): void {
if (!existsSync(resourcesDir)) { if (!existsSync(resourcesDir)) {
return; return;
} }
copyDirRecursive(resourcesDir, targetDir); copyDirRecursive(resourcesDir, targetDir, {
renameMap: { dotgitignore: '.gitignore' },
});
} }
/** /**
@ -146,6 +148,8 @@ interface CopyOptions {
overwrite?: boolean; overwrite?: boolean;
/** Collect copied file paths into this array */ /** Collect copied file paths into this array */
copiedFiles?: string[]; copiedFiles?: string[];
/** Rename files during copy (source name → dest name) */
renameMap?: Record<string, string>;
} }
/** /**
@ -154,7 +158,7 @@ interface CopyOptions {
* If true, overwrites existing files. * If true, overwrites existing files.
*/ */
function copyDirRecursive(srcDir: string, destDir: string, options: CopyOptions = {}): void { function copyDirRecursive(srcDir: string, destDir: string, options: CopyOptions = {}): void {
const { skipDirs = [], overwrite = false, copiedFiles } = options; const { skipDirs = [], overwrite = false, copiedFiles, renameMap } = options;
if (!existsSync(destDir)) { if (!existsSync(destDir)) {
mkdirSync(destDir, { recursive: true }); mkdirSync(destDir, { recursive: true });
@ -165,7 +169,8 @@ function copyDirRecursive(srcDir: string, destDir: string, options: CopyOptions
if (skipDirs.includes(entry)) continue; if (skipDirs.includes(entry)) continue;
const srcPath = join(srcDir, entry); const srcPath = join(srcDir, entry);
const destPath = join(destDir, entry); const destName = renameMap?.[entry] ?? entry;
const destPath = join(destDir, destName);
const stat = statSync(srcPath); const stat = statSync(srcPath);
if (stat.isDirectory()) { if (stat.isDirectory()) {