セッション情報を残す、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
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `takt /run-tasks` | Execute all pending tasks from `.takt/tasks/` once |
|
||||
| `takt /watch` | Watch `.takt/tasks/` and auto-execute tasks (resident process) |
|
||||
| `takt /add-task` | Add a new task interactively (YAML format) |
|
||||
| `takt /switch` | Switch workflow interactively |
|
||||
| `takt /clear` | Clear agent conversation sessions (reset state) |
|
||||
| `takt /refresh-builtin` | Update builtin resources from `resources/` to `~/.takt/` |
|
||||
| `takt /help` | Show help message |
|
||||
| `takt /config` | Display current configuration |
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `takt /run-tasks` | `/run` | Execute all pending tasks from `.takt/tasks/` once |
|
||||
| `takt /watch` | | Watch `.takt/tasks/` and auto-execute tasks (resident process) |
|
||||
| `takt /add-task` | `/add` | Add a new task interactively (YAML format, multiline supported) |
|
||||
| `takt /review-tasks` | `/review` | Review worktree task results (try merge, merge & cleanup, or delete) |
|
||||
| `takt /switch` | | Switch workflow interactively |
|
||||
| `takt /clear` | | Clear agent conversation sessions (reset state) |
|
||||
| `takt /refresh-builtin` | | Update builtin resources from `resources/` to `~/.takt/` |
|
||||
| `takt /help` | | Show help message |
|
||||
| `takt /config` | | Display current configuration |
|
||||
|
||||
## Architecture
|
||||
|
||||
@ -185,6 +186,18 @@ model: opus # Default model for all steps (unless overridden)
|
||||
- `improve` - Needs improvement (security concerns, quality issues)
|
||||
- `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
|
||||
|
||||
- Vitest for testing framework
|
||||
|
||||
70
README.md
70
README.md
@ -23,7 +23,7 @@ npm install -g takt
|
||||
## Quick Start
|
||||
|
||||
```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"
|
||||
|
||||
# Add a task to the queue
|
||||
@ -35,24 +35,28 @@ takt /run-tasks
|
||||
# Watch for tasks and auto-execute
|
||||
takt /watch
|
||||
|
||||
# Review worktree results (merge or delete)
|
||||
takt /review-tasks
|
||||
|
||||
# Switch workflow
|
||||
takt /switch
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `takt "task"` | Execute task with current workflow (continues session) |
|
||||
| `takt -r "task"` | Execute task, resuming previous session |
|
||||
| `takt /run-tasks` | Run all pending tasks from `.takt/tasks/` |
|
||||
| `takt /watch` | Watch `.takt/tasks/` and auto-execute tasks (stays resident) |
|
||||
| `takt /add-task` | Add a new task interactively (YAML format) |
|
||||
| `takt /switch` | Switch workflow interactively |
|
||||
| `takt /clear` | Clear agent conversation sessions |
|
||||
| `takt /refresh-builtin` | Update builtin agents/workflows to latest version |
|
||||
| `takt /config` | Display current configuration |
|
||||
| `takt /help` | Show help |
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `takt "task"` | | Execute task with current workflow (continues session) |
|
||||
| `takt -r "task"` | | Execute task, resuming previous session |
|
||||
| `takt /run-tasks` | `/run` | Run all pending tasks from `.takt/tasks/` |
|
||||
| `takt /watch` | | Watch `.takt/tasks/` and auto-execute tasks (stays resident) |
|
||||
| `takt /add-task` | `/add` | Add a new task interactively (YAML format, multiline supported) |
|
||||
| `takt /review-tasks` | `/review` | Review worktree task results (try merge, merge & cleanup, or delete) |
|
||||
| `takt /switch` | | Switch workflow interactively |
|
||||
| `takt /clear` | | Clear agent conversation sessions |
|
||||
| `takt /refresh-builtin` | | Update builtin agents/workflows to latest version |
|
||||
| `takt /config` | | Display current configuration |
|
||||
| `takt /help` | | Show help |
|
||||
|
||||
## Workflows
|
||||
|
||||
@ -183,7 +187,10 @@ Available Codex models:
|
||||
├── completed/ # Completed tasks with reports
|
||||
├── worktrees/ # Git worktrees for isolated task execution
|
||||
├── 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
|
||||
@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
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)
|
||||
- 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`
|
||||
|
||||
```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
|
||||
- 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
|
||||
|
||||
Available variables in `instruction_template`:
|
||||
@ -401,7 +440,7 @@ transitions:
|
||||
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).
|
||||
|
||||
**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.) |
|
||||
| `provider` | - | Override provider for this step (`claude` or `codex`) |
|
||||
| `model` | - | Override model for this step |
|
||||
| `permission_mode` | `default` | Permission mode: `default`, `acceptEdits`, or `bypassPermissions` |
|
||||
|
||||
## 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,
|
||||
finalizeSessionLog,
|
||||
saveSessionLog,
|
||||
updateLatestPointer,
|
||||
} from '../utils/session.js';
|
||||
import { createLogger } from '../utils/debug.js';
|
||||
import { notifySuccess, notifyError } from '../utils/notification.js';
|
||||
@ -84,6 +85,10 @@ export async function executeWorkflow(
|
||||
const workflowSessionId = generateSessionId();
|
||||
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
|
||||
const displayRef: { current: StreamDisplay | null } = { current: null };
|
||||
|
||||
@ -191,6 +196,10 @@ export async function executeWorkflow(
|
||||
status('Session', response.sessionId);
|
||||
}
|
||||
addToSessionLog(sessionLog, step.name, response);
|
||||
|
||||
// Incremental save after each step
|
||||
saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
||||
updateLatestPointer(sessionLog, workflowSessionId, projectCwd);
|
||||
});
|
||||
|
||||
engine.on('workflow:complete', (state) => {
|
||||
@ -198,6 +207,7 @@ export async function executeWorkflow(
|
||||
finalizeSessionLog(sessionLog, 'completed');
|
||||
// Save log to project root so user can find it easily
|
||||
const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
||||
updateLatestPointer(sessionLog, workflowSessionId, projectCwd);
|
||||
|
||||
const elapsed = sessionLog.endTime
|
||||
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
||||
@ -219,6 +229,7 @@ export async function executeWorkflow(
|
||||
finalizeSessionLog(sessionLog, 'aborted');
|
||||
// Save log to project root so user can find it easily
|
||||
const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
|
||||
updateLatestPointer(sessionLog, workflowSessionId, projectCwd);
|
||||
|
||||
const elapsed = sessionLog.endTime
|
||||
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
||||
|
||||
@ -86,6 +86,7 @@ export {
|
||||
|
||||
// Re-export session storage functions for backward compatibility
|
||||
export {
|
||||
writeFileAtomic,
|
||||
getInputHistoryPath,
|
||||
MAX_INPUT_HISTORY,
|
||||
loadInputHistory,
|
||||
|
||||
@ -13,7 +13,7 @@ import { getProjectConfigDir, ensureDir } from './paths.js';
|
||||
* Write file atomically using temp file + rename.
|
||||
* 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`;
|
||||
try {
|
||||
writeFileSync(tempPath, content, 'utf-8');
|
||||
|
||||
@ -48,18 +48,16 @@ function resolveWorktreePath(projectDir: string, options: WorktreeOptions): stri
|
||||
? options.worktree
|
||||
: path.resolve(projectDir, options.worktree);
|
||||
|
||||
if (!isPathSafe(projectDir, resolved)) {
|
||||
throw new Error(`Worktree path escapes project directory: ${options.worktree}`);
|
||||
}
|
||||
|
||||
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 slug = slugify(options.taskSlug);
|
||||
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
|
||||
*/
|
||||
|
||||
import { writeFileSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, copyFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
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 */
|
||||
export interface SessionLog {
|
||||
@ -112,7 +112,7 @@ export function saveSessionLog(
|
||||
const filename = `${sessionId}.json`;
|
||||
const filepath = join(logsDir, filename);
|
||||
|
||||
writeFileSync(filepath, JSON.stringify(log, null, 2), 'utf-8');
|
||||
writeFileAtomic(filepath, JSON.stringify(log, null, 2));
|
||||
return filepath;
|
||||
}
|
||||
|
||||
@ -144,6 +144,56 @@ export function loadProjectContext(projectDir: string): string {
|
||||
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 */
|
||||
export function workflowStateToSessionLog(
|
||||
state: WorkflowState,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user