セッション情報を残す、worktreeをプロジェクト管理外に移動
This commit is contained in:
parent
df22e4c33b
commit
57fd01819f
33
CLAUDE.md
33
CLAUDE.md
@ -19,16 +19,17 @@ TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Cl
|
|||||||
|
|
||||||
## CLI Slash Commands
|
## CLI Slash Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Alias | Description |
|
||||||
|---------|-------------|
|
|---------|-------|-------------|
|
||||||
| `takt /run-tasks` | Execute all pending tasks from `.takt/tasks/` once |
|
| `takt /run-tasks` | `/run` | Execute all pending tasks from `.takt/tasks/` once |
|
||||||
| `takt /watch` | Watch `.takt/tasks/` and auto-execute tasks (resident process) |
|
| `takt /watch` | | Watch `.takt/tasks/` and auto-execute tasks (resident process) |
|
||||||
| `takt /add-task` | Add a new task interactively (YAML format) |
|
| `takt /add-task` | `/add` | Add a new task interactively (YAML format, multiline supported) |
|
||||||
| `takt /switch` | Switch workflow interactively |
|
| `takt /review-tasks` | `/review` | Review worktree task results (try merge, merge & cleanup, or delete) |
|
||||||
| `takt /clear` | Clear agent conversation sessions (reset state) |
|
| `takt /switch` | | Switch workflow interactively |
|
||||||
| `takt /refresh-builtin` | Update builtin resources from `resources/` to `~/.takt/` |
|
| `takt /clear` | | Clear agent conversation sessions (reset state) |
|
||||||
| `takt /help` | Show help message |
|
| `takt /refresh-builtin` | | Update builtin resources from `resources/` to `~/.takt/` |
|
||||||
| `takt /config` | Display current configuration |
|
| `takt /help` | | Show help message |
|
||||||
|
| `takt /config` | | Display current configuration |
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@ -185,6 +186,18 @@ model: opus # Default model for all steps (unless overridden)
|
|||||||
- `improve` - Needs improvement (security concerns, quality issues)
|
- `improve` - Needs improvement (security concerns, quality issues)
|
||||||
- `always` - Unconditional transition
|
- `always` - Unconditional transition
|
||||||
|
|
||||||
|
## Worktree Execution
|
||||||
|
|
||||||
|
When tasks specify `worktree: true` or `worktree: "path"`, code runs in a git worktree (separate checkout). Key constraints:
|
||||||
|
|
||||||
|
- **Session isolation**: Claude Code sessions are stored per-cwd in `~/.claude/projects/{encoded-path}/`. Sessions from the main project cannot be resumed in a worktree. The engine skips session resume when `cwd !== projectCwd`.
|
||||||
|
- **No node_modules**: Worktrees only contain tracked files. `node_modules/` is absent.
|
||||||
|
- **Dual cwd**: `cwd` = worktree path (where agents run), `projectCwd` = project root (where `.takt/` lives). Reports, logs, and session data always write to `projectCwd`.
|
||||||
|
|
||||||
|
## Error Propagation
|
||||||
|
|
||||||
|
`ClaudeResult` (from SDK) has an `error` field. This must be propagated through `AgentResponse.error` → session log history → console output. Without this, SDK failures (exit code 1, rate limits, auth errors) appear as empty `blocked` status with no diagnostic info.
|
||||||
|
|
||||||
## Testing Notes
|
## Testing Notes
|
||||||
|
|
||||||
- Vitest for testing framework
|
- Vitest for testing framework
|
||||||
|
|||||||
70
README.md
70
README.md
@ -23,7 +23,7 @@ npm install -g takt
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run a task (will prompt for workflow selection)
|
# Run a task (will prompt for workflow selection and optional worktree creation)
|
||||||
takt "Add a login feature"
|
takt "Add a login feature"
|
||||||
|
|
||||||
# Add a task to the queue
|
# Add a task to the queue
|
||||||
@ -35,24 +35,28 @@ takt /run-tasks
|
|||||||
# Watch for tasks and auto-execute
|
# Watch for tasks and auto-execute
|
||||||
takt /watch
|
takt /watch
|
||||||
|
|
||||||
|
# Review worktree results (merge or delete)
|
||||||
|
takt /review-tasks
|
||||||
|
|
||||||
# Switch workflow
|
# Switch workflow
|
||||||
takt /switch
|
takt /switch
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Alias | Description |
|
||||||
|---------|-------------|
|
|---------|-------|-------------|
|
||||||
| `takt "task"` | Execute task with current workflow (continues session) |
|
| `takt "task"` | | Execute task with current workflow (continues session) |
|
||||||
| `takt -r "task"` | Execute task, resuming previous session |
|
| `takt -r "task"` | | Execute task, resuming previous session |
|
||||||
| `takt /run-tasks` | Run all pending tasks from `.takt/tasks/` |
|
| `takt /run-tasks` | `/run` | Run all pending tasks from `.takt/tasks/` |
|
||||||
| `takt /watch` | Watch `.takt/tasks/` and auto-execute tasks (stays resident) |
|
| `takt /watch` | | Watch `.takt/tasks/` and auto-execute tasks (stays resident) |
|
||||||
| `takt /add-task` | Add a new task interactively (YAML format) |
|
| `takt /add-task` | `/add` | Add a new task interactively (YAML format, multiline supported) |
|
||||||
| `takt /switch` | Switch workflow interactively |
|
| `takt /review-tasks` | `/review` | Review worktree task results (try merge, merge & cleanup, or delete) |
|
||||||
| `takt /clear` | Clear agent conversation sessions |
|
| `takt /switch` | | Switch workflow interactively |
|
||||||
| `takt /refresh-builtin` | Update builtin agents/workflows to latest version |
|
| `takt /clear` | | Clear agent conversation sessions |
|
||||||
| `takt /config` | Display current configuration |
|
| `takt /refresh-builtin` | | Update builtin agents/workflows to latest version |
|
||||||
| `takt /help` | Show help |
|
| `takt /config` | | Display current configuration |
|
||||||
|
| `takt /help` | | Show help |
|
||||||
|
|
||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
@ -183,7 +187,10 @@ Available Codex models:
|
|||||||
├── completed/ # Completed tasks with reports
|
├── completed/ # Completed tasks with reports
|
||||||
├── worktrees/ # Git worktrees for isolated task execution
|
├── worktrees/ # Git worktrees for isolated task execution
|
||||||
├── reports/ # Execution reports (auto-generated)
|
├── reports/ # Execution reports (auto-generated)
|
||||||
└── logs/ # Session logs
|
└── logs/ # Session logs (incremental)
|
||||||
|
├── latest.json # Pointer to current/latest session
|
||||||
|
├── previous.json # Pointer to previous session
|
||||||
|
└── {sessionId}.json # Full session log per workflow run
|
||||||
```
|
```
|
||||||
|
|
||||||
### Global Configuration
|
### Global Configuration
|
||||||
@ -224,6 +231,15 @@ takt -r "The bug occurs when the password contains special characters"
|
|||||||
|
|
||||||
The `-r` flag preserves the agent's conversation history, allowing for natural back-and-forth interaction.
|
The `-r` flag preserves the agent's conversation history, allowing for natural back-and-forth interaction.
|
||||||
|
|
||||||
|
### Interactive Workflow
|
||||||
|
|
||||||
|
When running `takt "task"`, you are prompted to:
|
||||||
|
|
||||||
|
1. **Select a workflow** - Choose from available workflows (arrow keys, ESC to cancel)
|
||||||
|
2. **Create a worktree** (optional) - Optionally run the task in an isolated git worktree
|
||||||
|
|
||||||
|
This interactive flow ensures each task runs with the right workflow and isolation level.
|
||||||
|
|
||||||
### Adding Custom Workflows
|
### Adding Custom Workflows
|
||||||
|
|
||||||
Create your own workflow by adding YAML files to `~/.takt/workflows/`:
|
Create your own workflow by adding YAML files to `~/.takt/workflows/`:
|
||||||
@ -335,6 +351,8 @@ YAML task files can specify `worktree` to run each task in an isolated git workt
|
|||||||
- `branch: "feat/xxx"` - Use specified branch (auto-generated as `takt/{timestamp}-{slug}` if omitted)
|
- `branch: "feat/xxx"` - Use specified branch (auto-generated as `takt/{timestamp}-{slug}` if omitted)
|
||||||
- Omit `worktree` - Run in current working directory (default)
|
- Omit `worktree` - Run in current working directory (default)
|
||||||
|
|
||||||
|
When a worktree task completes successfully, TAKT automatically commits all changes (`auto-commit`). Use `takt /review-tasks` to review, try-merge, or delete completed worktree branches.
|
||||||
|
|
||||||
#### Running Tasks with `/run-tasks`
|
#### Running Tasks with `/run-tasks`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -356,6 +374,27 @@ Watch mode polls `.takt/tasks/` for new task files and auto-executes them as the
|
|||||||
- Automated workflows where tasks are added by external processes
|
- Automated workflows where tasks are added by external processes
|
||||||
- Long-running development sessions where tasks are queued over time
|
- Long-running development sessions where tasks are queued over time
|
||||||
|
|
||||||
|
#### Reviewing Worktree Results with `/review-tasks`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
takt /review-tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
Lists all `takt/`-prefixed worktree branches with file change counts. For each branch you can:
|
||||||
|
- **Try merge** - Attempt merge into main (dry-run check, then actual merge)
|
||||||
|
- **Merge & cleanup** - Merge and remove the worktree
|
||||||
|
- **Delete** - Remove the worktree and branch without merging
|
||||||
|
|
||||||
|
### Session Logs
|
||||||
|
|
||||||
|
TAKT writes session logs incrementally to `.takt/logs/`. Logs are saved at workflow start, after each step, and at workflow end — so even if the process crashes mid-execution, partial logs are preserved.
|
||||||
|
|
||||||
|
- `.takt/logs/latest.json` - Pointer to the current (or most recent) session
|
||||||
|
- `.takt/logs/previous.json` - Pointer to the previous session
|
||||||
|
- `.takt/logs/{sessionId}.json` - Full session log with step history
|
||||||
|
|
||||||
|
Agents can read `previous.json` to pick up context from a prior run (e.g., when resuming with `takt "続けて"`).
|
||||||
|
|
||||||
### Workflow Variables
|
### Workflow Variables
|
||||||
|
|
||||||
Available variables in `instruction_template`:
|
Available variables in `instruction_template`:
|
||||||
@ -401,7 +440,7 @@ transitions:
|
|||||||
next_step: ABORT # End workflow with failure
|
next_step: ABORT # End workflow with failure
|
||||||
```
|
```
|
||||||
|
|
||||||
Available transition conditions: `done`, `blocked`, `approved`, `rejected`, `improve`, `always`.
|
Available transition conditions: `done`, `blocked`, `approved`, `rejected`, `improve`, `answer`, `always`.
|
||||||
Special next_step values: `COMPLETE` (success), `ABORT` (failure).
|
Special next_step values: `COMPLETE` (success), `ABORT` (failure).
|
||||||
|
|
||||||
**Step options:**
|
**Step options:**
|
||||||
@ -413,6 +452,7 @@ Special next_step values: `COMPLETE` (success), `ABORT` (failure).
|
|||||||
| `allowed_tools` | - | List of tools the agent can use (Read, Glob, Grep, Edit, Write, Bash, etc.) |
|
| `allowed_tools` | - | List of tools the agent can use (Read, Glob, Grep, Edit, Write, Bash, etc.) |
|
||||||
| `provider` | - | Override provider for this step (`claude` or `codex`) |
|
| `provider` | - | Override provider for this step (`claude` or `codex`) |
|
||||||
| `model` | - | Override model for this step |
|
| `model` | - | Override model for this step |
|
||||||
|
| `permission_mode` | `default` | Permission mode: `default`, `acceptEdits`, or `bypassPermissions` |
|
||||||
|
|
||||||
## API Usage
|
## API Usage
|
||||||
|
|
||||||
|
|||||||
174
src/__tests__/session.test.ts
Normal file
174
src/__tests__/session.test.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Tests for session log incremental writes and pointer management
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { existsSync, readFileSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import {
|
||||||
|
createSessionLog,
|
||||||
|
saveSessionLog,
|
||||||
|
updateLatestPointer,
|
||||||
|
type LatestLogPointer,
|
||||||
|
type SessionLog,
|
||||||
|
} from '../utils/session.js';
|
||||||
|
|
||||||
|
/** Create a temp project directory with .takt/logs structure */
|
||||||
|
function createTempProject(): string {
|
||||||
|
const dir = join(tmpdir(), `takt-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('saveSessionLog (atomic)', () => {
|
||||||
|
let projectDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
projectDir = createTempProject();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(projectDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create session log file with correct content', () => {
|
||||||
|
const log = createSessionLog('test task', projectDir, 'default');
|
||||||
|
const sessionId = 'test-session-001';
|
||||||
|
|
||||||
|
const filepath = saveSessionLog(log, sessionId, projectDir);
|
||||||
|
|
||||||
|
expect(existsSync(filepath)).toBe(true);
|
||||||
|
const content = JSON.parse(readFileSync(filepath, 'utf-8')) as SessionLog;
|
||||||
|
expect(content.task).toBe('test task');
|
||||||
|
expect(content.workflowName).toBe('default');
|
||||||
|
expect(content.status).toBe('running');
|
||||||
|
expect(content.iterations).toBe(0);
|
||||||
|
expect(content.history).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing log file on subsequent saves', () => {
|
||||||
|
const log = createSessionLog('test task', projectDir, 'default');
|
||||||
|
const sessionId = 'test-session-002';
|
||||||
|
|
||||||
|
saveSessionLog(log, sessionId, projectDir);
|
||||||
|
|
||||||
|
log.iterations = 3;
|
||||||
|
log.status = 'completed';
|
||||||
|
saveSessionLog(log, sessionId, projectDir);
|
||||||
|
|
||||||
|
const filepath = join(projectDir, '.takt', 'logs', `${sessionId}.json`);
|
||||||
|
const content = JSON.parse(readFileSync(filepath, 'utf-8')) as SessionLog;
|
||||||
|
expect(content.iterations).toBe(3);
|
||||||
|
expect(content.status).toBe('completed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateLatestPointer', () => {
|
||||||
|
let projectDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
projectDir = createTempProject();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(projectDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create latest.json with pointer data', () => {
|
||||||
|
const log = createSessionLog('my task', projectDir, 'default');
|
||||||
|
const sessionId = 'abc-123';
|
||||||
|
|
||||||
|
updateLatestPointer(log, sessionId, projectDir);
|
||||||
|
|
||||||
|
const latestPath = join(projectDir, '.takt', 'logs', 'latest.json');
|
||||||
|
expect(existsSync(latestPath)).toBe(true);
|
||||||
|
|
||||||
|
const pointer = JSON.parse(readFileSync(latestPath, 'utf-8')) as LatestLogPointer;
|
||||||
|
expect(pointer.sessionId).toBe('abc-123');
|
||||||
|
expect(pointer.logFile).toBe('abc-123.json');
|
||||||
|
expect(pointer.task).toBe('my task');
|
||||||
|
expect(pointer.workflowName).toBe('default');
|
||||||
|
expect(pointer.status).toBe('running');
|
||||||
|
expect(pointer.iterations).toBe(0);
|
||||||
|
expect(pointer.startTime).toBeDefined();
|
||||||
|
expect(pointer.updatedAt).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create previous.json when copyToPrevious is false', () => {
|
||||||
|
const log = createSessionLog('task', projectDir, 'wf');
|
||||||
|
updateLatestPointer(log, 'sid-1', projectDir);
|
||||||
|
|
||||||
|
const previousPath = join(projectDir, '.takt', 'logs', 'previous.json');
|
||||||
|
expect(existsSync(previousPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create previous.json when copyToPrevious is true but latest.json does not exist', () => {
|
||||||
|
const log = createSessionLog('task', projectDir, 'wf');
|
||||||
|
updateLatestPointer(log, 'sid-1', projectDir, { copyToPrevious: true });
|
||||||
|
|
||||||
|
const previousPath = join(projectDir, '.takt', 'logs', 'previous.json');
|
||||||
|
// latest.json didn't exist before this call, so previous.json should not be created
|
||||||
|
expect(existsSync(previousPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy latest.json to previous.json when copyToPrevious is true and latest exists', () => {
|
||||||
|
const log1 = createSessionLog('first task', projectDir, 'wf1');
|
||||||
|
updateLatestPointer(log1, 'sid-first', projectDir);
|
||||||
|
|
||||||
|
// Simulate a second workflow starting
|
||||||
|
const log2 = createSessionLog('second task', projectDir, 'wf2');
|
||||||
|
updateLatestPointer(log2, 'sid-second', projectDir, { copyToPrevious: true });
|
||||||
|
|
||||||
|
const logsDir = join(projectDir, '.takt', 'logs');
|
||||||
|
const latest = JSON.parse(readFileSync(join(logsDir, 'latest.json'), 'utf-8')) as LatestLogPointer;
|
||||||
|
const previous = JSON.parse(readFileSync(join(logsDir, 'previous.json'), 'utf-8')) as LatestLogPointer;
|
||||||
|
|
||||||
|
// latest should point to second session
|
||||||
|
expect(latest.sessionId).toBe('sid-second');
|
||||||
|
expect(latest.task).toBe('second task');
|
||||||
|
|
||||||
|
// previous should point to first session
|
||||||
|
expect(previous.sessionId).toBe('sid-first');
|
||||||
|
expect(previous.task).toBe('first task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update previous.json on step-complete calls (no copyToPrevious)', () => {
|
||||||
|
// Workflow 1 creates latest
|
||||||
|
const log1 = createSessionLog('first', projectDir, 'wf');
|
||||||
|
updateLatestPointer(log1, 'sid-1', projectDir);
|
||||||
|
|
||||||
|
// Workflow 2 starts → copies latest to previous
|
||||||
|
const log2 = createSessionLog('second', projectDir, 'wf');
|
||||||
|
updateLatestPointer(log2, 'sid-2', projectDir, { copyToPrevious: true });
|
||||||
|
|
||||||
|
// Step completes → updates only latest (no copyToPrevious)
|
||||||
|
log2.iterations = 1;
|
||||||
|
updateLatestPointer(log2, 'sid-2', projectDir);
|
||||||
|
|
||||||
|
const logsDir = join(projectDir, '.takt', 'logs');
|
||||||
|
const previous = JSON.parse(readFileSync(join(logsDir, 'previous.json'), 'utf-8')) as LatestLogPointer;
|
||||||
|
|
||||||
|
// previous should still point to first session
|
||||||
|
expect(previous.sessionId).toBe('sid-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update iterations and status in latest.json on subsequent calls', () => {
|
||||||
|
const log = createSessionLog('task', projectDir, 'wf');
|
||||||
|
updateLatestPointer(log, 'sid-1', projectDir, { copyToPrevious: true });
|
||||||
|
|
||||||
|
// Simulate step completion
|
||||||
|
log.iterations = 2;
|
||||||
|
updateLatestPointer(log, 'sid-1', projectDir);
|
||||||
|
|
||||||
|
// Simulate workflow completion
|
||||||
|
log.status = 'completed';
|
||||||
|
log.iterations = 3;
|
||||||
|
updateLatestPointer(log, 'sid-1', projectDir);
|
||||||
|
|
||||||
|
const latestPath = join(projectDir, '.takt', 'logs', 'latest.json');
|
||||||
|
const pointer = JSON.parse(readFileSync(latestPath, 'utf-8')) as LatestLogPointer;
|
||||||
|
expect(pointer.status).toBe('completed');
|
||||||
|
expect(pointer.iterations).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
addToSessionLog,
|
addToSessionLog,
|
||||||
finalizeSessionLog,
|
finalizeSessionLog,
|
||||||
saveSessionLog,
|
saveSessionLog,
|
||||||
|
updateLatestPointer,
|
||||||
} from '../utils/session.js';
|
} from '../utils/session.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
import { notifySuccess, notifyError } from '../utils/notification.js';
|
import { notifySuccess, notifyError } from '../utils/notification.js';
|
||||||
@ -84,6 +85,10 @@ export async function executeWorkflow(
|
|||||||
const workflowSessionId = generateSessionId();
|
const workflowSessionId = generateSessionId();
|
||||||
const sessionLog = createSessionLog(task, projectCwd, workflowConfig.name);
|
const sessionLog = createSessionLog(task, projectCwd, workflowConfig.name);
|
||||||
|
|
||||||
|
// Persist initial log + pointer at workflow start (enables crash recovery)
|
||||||
|
saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
||||||
|
updateLatestPointer(sessionLog, workflowSessionId, projectCwd, { copyToPrevious: true });
|
||||||
|
|
||||||
// Track current display for streaming
|
// Track current display for streaming
|
||||||
const displayRef: { current: StreamDisplay | null } = { current: null };
|
const displayRef: { current: StreamDisplay | null } = { current: null };
|
||||||
|
|
||||||
@ -191,6 +196,10 @@ export async function executeWorkflow(
|
|||||||
status('Session', response.sessionId);
|
status('Session', response.sessionId);
|
||||||
}
|
}
|
||||||
addToSessionLog(sessionLog, step.name, response);
|
addToSessionLog(sessionLog, step.name, response);
|
||||||
|
|
||||||
|
// Incremental save after each step
|
||||||
|
saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
||||||
|
updateLatestPointer(sessionLog, workflowSessionId, projectCwd);
|
||||||
});
|
});
|
||||||
|
|
||||||
engine.on('workflow:complete', (state) => {
|
engine.on('workflow:complete', (state) => {
|
||||||
@ -198,6 +207,7 @@ export async function executeWorkflow(
|
|||||||
finalizeSessionLog(sessionLog, 'completed');
|
finalizeSessionLog(sessionLog, 'completed');
|
||||||
// Save log to project root so user can find it easily
|
// Save log to project root so user can find it easily
|
||||||
const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
||||||
|
updateLatestPointer(sessionLog, workflowSessionId, projectCwd);
|
||||||
|
|
||||||
const elapsed = sessionLog.endTime
|
const elapsed = sessionLog.endTime
|
||||||
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
||||||
@ -219,6 +229,7 @@ export async function executeWorkflow(
|
|||||||
finalizeSessionLog(sessionLog, 'aborted');
|
finalizeSessionLog(sessionLog, 'aborted');
|
||||||
// Save log to project root so user can find it easily
|
// Save log to project root so user can find it easily
|
||||||
const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
||||||
|
updateLatestPointer(sessionLog, workflowSessionId, projectCwd);
|
||||||
|
|
||||||
const elapsed = sessionLog.endTime
|
const elapsed = sessionLog.endTime
|
||||||
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export {
|
|||||||
|
|
||||||
// Re-export session storage functions for backward compatibility
|
// Re-export session storage functions for backward compatibility
|
||||||
export {
|
export {
|
||||||
|
writeFileAtomic,
|
||||||
getInputHistoryPath,
|
getInputHistoryPath,
|
||||||
MAX_INPUT_HISTORY,
|
MAX_INPUT_HISTORY,
|
||||||
loadInputHistory,
|
loadInputHistory,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { getProjectConfigDir, ensureDir } from './paths.js';
|
|||||||
* Write file atomically using temp file + rename.
|
* Write file atomically using temp file + rename.
|
||||||
* This prevents corruption when multiple processes write simultaneously.
|
* This prevents corruption when multiple processes write simultaneously.
|
||||||
*/
|
*/
|
||||||
function writeFileAtomic(filePath: string, content: string): void {
|
export function writeFileAtomic(filePath: string, content: string): void {
|
||||||
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
||||||
try {
|
try {
|
||||||
writeFileSync(tempPath, content, 'utf-8');
|
writeFileSync(tempPath, content, 'utf-8');
|
||||||
|
|||||||
@ -48,18 +48,16 @@ function resolveWorktreePath(projectDir: string, options: WorktreeOptions): stri
|
|||||||
? options.worktree
|
? options.worktree
|
||||||
: path.resolve(projectDir, options.worktree);
|
: path.resolve(projectDir, options.worktree);
|
||||||
|
|
||||||
if (!isPathSafe(projectDir, resolved)) {
|
|
||||||
throw new Error(`Worktree path escapes project directory: ${options.worktree}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
// worktree: true → .takt/worktrees/{timestamp}-{task-slug}/
|
// worktree: true → sibling directory: ../{timestamp}-{task-slug}/
|
||||||
|
// Worktrees MUST be outside the project directory to avoid Claude Code
|
||||||
|
// detecting the parent .git directory and writing to the main project.
|
||||||
const timestamp = generateTimestamp();
|
const timestamp = generateTimestamp();
|
||||||
const slug = slugify(options.taskSlug);
|
const slug = slugify(options.taskSlug);
|
||||||
const dirName = slug ? `${timestamp}-${slug}` : timestamp;
|
const dirName = slug ? `${timestamp}-${slug}` : timestamp;
|
||||||
return path.join(projectDir, '.takt', 'worktrees', dirName);
|
return path.join(path.dirname(projectDir), dirName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
* Session management utilities
|
* Session management utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { writeFileSync, existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync, copyFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import type { AgentResponse, WorkflowState } from '../models/types.js';
|
import type { AgentResponse, WorkflowState } from '../models/types.js';
|
||||||
import { getProjectLogsDir, getGlobalLogsDir, ensureDir } from '../config/paths.js';
|
import { getProjectLogsDir, getGlobalLogsDir, ensureDir, writeFileAtomic } from '../config/paths.js';
|
||||||
|
|
||||||
/** Session log entry */
|
/** Session log entry */
|
||||||
export interface SessionLog {
|
export interface SessionLog {
|
||||||
@ -112,7 +112,7 @@ export function saveSessionLog(
|
|||||||
const filename = `${sessionId}.json`;
|
const filename = `${sessionId}.json`;
|
||||||
const filepath = join(logsDir, filename);
|
const filepath = join(logsDir, filename);
|
||||||
|
|
||||||
writeFileSync(filepath, JSON.stringify(log, null, 2), 'utf-8');
|
writeFileAtomic(filepath, JSON.stringify(log, null, 2));
|
||||||
return filepath;
|
return filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +144,56 @@ export function loadProjectContext(projectDir: string): string {
|
|||||||
return contextParts.join('\n\n---\n\n');
|
return contextParts.join('\n\n---\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pointer metadata for latest/previous log files */
|
||||||
|
export interface LatestLogPointer {
|
||||||
|
sessionId: string;
|
||||||
|
logFile: string;
|
||||||
|
task: string;
|
||||||
|
workflowName: string;
|
||||||
|
status: SessionLog['status'];
|
||||||
|
startTime: string;
|
||||||
|
updatedAt: string;
|
||||||
|
iterations: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update latest.json pointer file.
|
||||||
|
* On first call (workflow start), copies existing latest.json to previous.json.
|
||||||
|
* On subsequent calls (step complete / workflow end), only overwrites latest.json.
|
||||||
|
*/
|
||||||
|
export function updateLatestPointer(
|
||||||
|
log: SessionLog,
|
||||||
|
sessionId: string,
|
||||||
|
projectDir?: string,
|
||||||
|
options?: { copyToPrevious?: boolean }
|
||||||
|
): void {
|
||||||
|
const logsDir = projectDir
|
||||||
|
? getProjectLogsDir(projectDir)
|
||||||
|
: getGlobalLogsDir();
|
||||||
|
ensureDir(logsDir);
|
||||||
|
|
||||||
|
const latestPath = join(logsDir, 'latest.json');
|
||||||
|
const previousPath = join(logsDir, 'previous.json');
|
||||||
|
|
||||||
|
// Copy latest → previous only when explicitly requested (workflow start)
|
||||||
|
if (options?.copyToPrevious && existsSync(latestPath)) {
|
||||||
|
copyFileSync(latestPath, previousPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointer: LatestLogPointer = {
|
||||||
|
sessionId,
|
||||||
|
logFile: `${sessionId}.json`,
|
||||||
|
task: log.task,
|
||||||
|
workflowName: log.workflowName,
|
||||||
|
status: log.status,
|
||||||
|
startTime: log.startTime,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
iterations: log.iterations,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileAtomic(latestPath, JSON.stringify(pointer, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
/** Convert workflow state to session log */
|
/** Convert workflow state to session log */
|
||||||
export function workflowStateToSessionLog(
|
export function workflowStateToSessionLog(
|
||||||
state: WorkflowState,
|
state: WorkflowState,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user