diff --git a/CHANGELOG.md b/CHANGELOG.md index c715764..41229b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.4.0] - 2026-02-04 + +### Added + +- Externalized prompt system: all internal prompts moved to versioned, translatable files (`src/shared/prompts/en/`, `src/shared/prompts/ja/`) +- i18n label system: UI labels extracted to separate YAML files (`labels_en.yaml`, `labels_ja.yaml`) with `src/shared/i18n/` module +- Prompt preview functionality (`src/features/prompt/preview.ts`) +- Phase system injection into agents for improved workflow phase awareness +- Enhanced debug capabilities with new debug log viewer (`tools/debug-log-viewer.html`) +- Comprehensive test coverage: + - i18n system tests (`i18n.test.ts`) + - Prompt system tests (`prompts.test.ts`) + - Session management tests (`session.test.ts`) + - Worktree integration tests (`it-worktree-delete.test.ts`, `it-worktree-sessions.test.ts`) + +### Changed + +- **BREAKING:** Internal terminology renamed: `WorkflowStep` → `WorkflowMovement`, `StepExecutor` → `MovementExecutor`, `ParallelSubStepRawSchema` → `ParallelSubMovementRawSchema`, `WorkflowStepRawSchema` → `WorkflowMovementRawSchema` +- **BREAKING:** Removed unnecessary backward compatibility code +- **BREAKING:** Disabled interactive prompt override feature +- Workflow resource directory renamed: `resources/global/*/workflows/` → `resources/global/*/pieces/` +- Prompts restructured for better readability and maintainability +- Removed unnecessary task requirement summarization from conversation flow +- Suppressed unnecessary report output during workflow execution + +### Fixed + +- `takt worktree` bug fix for worktree operations + +### Internal + +- Extracted prompt management into `src/shared/prompts/index.ts` with language-aware file loading +- Created `src/shared/i18n/index.ts` for centralized label management +- Enhanced `tools/jsonl-viewer.html` with additional features +- Major refactoring across 162 files (~5,800 insertions, ~2,900 deletions) + ## [0.3.9] - 2026-02-03 ### Added diff --git a/README.md b/README.md index 58fed8b..f3e0a33 100644 --- a/README.md +++ b/README.md @@ -266,9 +266,9 @@ TAKT uses YAML-based workflow definitions and rule-based routing. Builtin workfl ```yaml name: default max_iterations: 10 -initial_step: plan +initial_movement: plan -steps: +movements: - name: plan agent: ../agents/default/planner.md model: opus @@ -303,9 +303,9 @@ steps: Review the implementation from architecture and code quality perspectives. ``` -### Agentless Steps +### Agentless Movements -The `agent` field is optional. When omitted, the step executes using only the `instruction_template` without a system prompt. This is useful for simple tasks that don't require agent behavior customization. +The `agent` field is optional. When omitted, the movement executes using only the `instruction_template` without a system prompt. This is useful for simple tasks that don't require agent behavior customization. ```yaml - name: summarize @@ -328,9 +328,9 @@ You can also write an inline system prompt as the `agent` value (if the specifie Review code quality. ``` -### Parallel Steps +### Parallel Movements -Execute sub-steps in parallel within a step and evaluate with aggregate conditions: +Execute sub-movements in parallel within a movement and evaluate with aggregate conditions: ```yaml - name: reviewers @@ -356,15 +356,15 @@ Execute sub-steps in parallel within a step and evaluate with aggregate conditio next: fix ``` -- `all("X")`: true if ALL sub-steps matched condition X -- `any("X")`: true if ANY sub-step matched condition X -- Sub-step `rules` define possible outcomes, but `next` is optional (parent controls transition) +- `all("X")`: true if ALL sub-movements matched condition X +- `any("X")`: true if ANY sub-movement matched condition X +- Sub-movement `rules` define possible outcomes, but `next` is optional (parent controls transition) ### Rule Condition Types | Type | Syntax | Description | |------|--------|-------------| -| Tag-based | `"condition text"` | Agent outputs `[STEP:N]` tag, matched by index | +| Tag-based | `"condition text"` | Agent outputs `[MOVEMENTNAME:N]` tag, matched by index | | AI judge | `ai("condition text")` | AI evaluates condition against agent output | | Aggregate | `all("X")` / `any("X")` | Aggregates parallel sub-step matched conditions | @@ -579,9 +579,9 @@ takt eject default name: my-workflow description: Custom workflow max_iterations: 5 -initial_step: analyze +initial_movement: analyze -steps: +movements: - name: analyze agent: ~/.takt/agents/my-agents/analyzer.md edit: false @@ -629,7 +629,7 @@ Variables available in `instruction_template`: | `{task}` | Original user request (auto-injected if not in template) | | `{iteration}` | Workflow-wide turn count (total steps executed) | | `{max_iterations}` | Maximum iteration count | -| `{step_iteration}` | Per-step iteration count (times this step has been executed) | +| `{movement_iteration}` | Per-movement iteration count (times this movement has been executed) | | `{previous_response}` | Output from previous step (auto-injected if not in template) | | `{user_inputs}` | Additional user inputs during workflow (auto-injected if not in template) | | `{report_dir}` | Report directory path (e.g., `.takt/reports/20250126-143052-task-summary`) | @@ -637,7 +637,7 @@ Variables available in `instruction_template`: ### Workflow Design -Elements needed for each workflow step: +Elements needed for each workflow movement: **1. Agent** - Markdown file containing system prompt: @@ -646,7 +646,7 @@ agent: ../agents/default/coder.md # Path to agent prompt file agent_name: coder # Display name (optional) ``` -**2. Rules** - Define routing from step to next step. The instruction builder auto-injects status output rules, so agents know which tags to output: +**2. Rules** - Define routing from movement to next movement. The instruction builder auto-injects status output rules, so agents know which tags to output: ```yaml rules: @@ -658,15 +658,15 @@ rules: Special `next` values: `COMPLETE` (success), `ABORT` (failure) -**3. Step Options:** +**3. Movement Options:** | Option | Default | Description | |--------|---------|-------------| -| `edit` | - | Whether step can edit project files (`true`/`false`) | -| `pass_previous_response` | `true` | Pass previous step output to `{previous_response}` | +| `edit` | - | Whether movement can edit project files (`true`/`false`) | +| `pass_previous_response` | `true` | Pass previous movement output to `{previous_response}` | | `allowed_tools` | - | List of tools 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 | +| `provider` | - | Override provider for this movement (`claude` or `codex`) | +| `model` | - | Override model for this movement | | `permission_mode` | - | Permission mode: `readonly`, `edit`, `full` (provider-independent) | | `report` | - | Auto-generated report file settings (name, format) | diff --git a/src/core/workflow/engine/ParallelRunner.ts b/src/core/workflow/engine/ParallelRunner.ts index 4b833d7..46e6512 100644 --- a/src/core/workflow/engine/ParallelRunner.ts +++ b/src/core/workflow/engine/ParallelRunner.ts @@ -55,7 +55,10 @@ export class ParallelRunner { maxIterations: number, updateAgentSession: (agent: string, sessionId: string | undefined) => void, ): Promise<{ response: AgentResponse; instruction: string }> { - const subMovements = step.parallel!; + if (!step.parallel) { + throw new Error(`Movement "${step.name}" has no parallel sub-movements`); + } + const subMovements = step.parallel; const movementIteration = incrementMovementIteration(state, step.name); log.debug('Running parallel movement', { movement: step.name, diff --git a/src/core/workflow/engine/parallel-logger.ts b/src/core/workflow/engine/parallel-logger.ts index d99cd2c..48ae69a 100644 --- a/src/core/workflow/engine/parallel-logger.ts +++ b/src/core/workflow/engine/parallel-logger.ts @@ -50,7 +50,7 @@ export class ParallelLogger { * Format: `\x1b[COLORm[name]\x1b[0m` + padding spaces */ buildPrefix(name: string, index: number): string { - const color = COLORS[index % COLORS.length]!; + const color = COLORS[index % COLORS.length]; const padding = ' '.repeat(this.maxNameLength - name.length); return `${color}[${name}]${RESET}${padding} `; } @@ -99,7 +99,8 @@ export class ParallelLogger { const parts = combined.split('\n'); // Last part is incomplete (no trailing newline) — keep in buffer - this.lineBuffers.set(name, parts.pop()!); + const remainder = parts.pop() ?? ''; + this.lineBuffers.set(name, remainder); // Output all complete lines for (const line of parts) { diff --git a/src/core/workflow/evaluation/AggregateEvaluator.ts b/src/core/workflow/evaluation/AggregateEvaluator.ts index b6a9458..506fd06 100644 --- a/src/core/workflow/evaluation/AggregateEvaluator.ts +++ b/src/core/workflow/evaluation/AggregateEvaluator.ts @@ -38,7 +38,8 @@ export class AggregateEvaluator { if (!this.step.rules || !this.step.parallel || this.step.parallel.length === 0) return -1; for (let i = 0; i < this.step.rules.length; i++) { - const rule = this.step.rules[i]!; + const rule = this.step.rules[i]; + if (!rule) continue; if (!rule.isAggregateCondition || !rule.aggregateType || !rule.aggregateConditionText) { continue; } diff --git a/src/core/workflow/evaluation/RuleEvaluator.ts b/src/core/workflow/evaluation/RuleEvaluator.ts index 0b85fb8..b905d32 100644 --- a/src/core/workflow/evaluation/RuleEvaluator.ts +++ b/src/core/workflow/evaluation/RuleEvaluator.ts @@ -115,7 +115,8 @@ export class RuleEvaluator { const aiConditions: { index: number; text: string }[] = []; for (let i = 0; i < this.step.rules.length; i++) { - const rule = this.step.rules[i]!; + const rule = this.step.rules[i]; + if (!rule) continue; if (rule.interactiveOnly && this.ctx.interactive !== true) { continue; } @@ -135,7 +136,8 @@ export class RuleEvaluator { const judgeResult = await this.ctx.callAiJudge(agentOutput, judgeConditions, { cwd: this.ctx.cwd }); if (judgeResult >= 0 && judgeResult < aiConditions.length) { - const matched = aiConditions[judgeResult]!; + const matched = aiConditions[judgeResult]; + if (!matched) return -1; log.debug('AI judge matched condition', { movement: this.step.name, judgeResult, @@ -172,7 +174,7 @@ export class RuleEvaluator { log.debug('AI judge (fallback) matched condition', { movement: this.step.name, ruleIndex: judgeResult, - condition: conditions[judgeResult]!.text, + condition: conditions[judgeResult]?.text, }); return judgeResult; } diff --git a/src/core/workflow/instruction/InstructionBuilder.ts b/src/core/workflow/instruction/InstructionBuilder.ts index 841e00f..60aa10f 100644 --- a/src/core/workflow/instruction/InstructionBuilder.ts +++ b/src/core/workflow/instruction/InstructionBuilder.ts @@ -49,8 +49,8 @@ export class InstructionBuilder { const hasReport = !!(this.step.report && this.context.reportDir); let reportInfo = ''; let phaseNote = ''; - if (hasReport) { - reportInfo = renderReportContext(this.step.report!, this.context.reportDir!); + if (hasReport && this.step.report && this.context.reportDir) { + reportInfo = renderReportContext(this.step.report, this.context.reportDir); phaseNote = language === 'ja' ? '**注意:** これはPhase 1(本来の作業)です。作業完了後、Phase 2で自動的にレポートを生成します。' : '**Note:** This is Phase 1 (main work). After you complete your work, Phase 2 will automatically generate the report based on your findings.'; @@ -72,8 +72,8 @@ export class InstructionBuilder { this.context.previousOutput && !hasPreviousResponsePlaceholder ); - const previousResponse = hasPreviousResponse - ? escapeTemplateChars(this.context.previousOutput!.content) + const previousResponse = hasPreviousResponse && this.context.previousOutput + ? escapeTemplateChars(this.context.previousOutput.content) : ''; // User Inputs diff --git a/src/features/tasks/add/index.ts b/src/features/tasks/add/index.ts index 93d85d7..cb4afbf 100644 --- a/src/features/tasks/add/index.ts +++ b/src/features/tasks/add/index.ts @@ -11,7 +11,7 @@ import { stringify as stringifyYaml } from 'yaml'; import { promptInput, confirm } from '../../../shared/prompt/index.js'; import { success, info } from '../../../shared/ui/index.js'; import { summarizeTaskName, type TaskFileData } from '../../../infra/task/index.js'; -import { loadGlobalConfig, getWorkflowDescription } from '../../../infra/config/index.js'; +import { getWorkflowDescription } from '../../../infra/config/index.js'; import { determineWorkflow } from '../execute/selectAndExecute.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/index.js'; diff --git a/src/features/workflowSelection/index.ts b/src/features/workflowSelection/index.ts index cf38def..17c2774 100644 --- a/src/features/workflowSelection/index.ts +++ b/src/features/workflowSelection/index.ts @@ -106,7 +106,7 @@ export function buildCategoryWorkflowOptions( if (!categoryItem || categoryItem.type !== 'category') return null; return categoryItem.workflows.map((qualifiedName) => { - const displayName = qualifiedName.split('/').pop()!; + const displayName = qualifiedName.split('/').pop() ?? qualifiedName; const isCurrent = qualifiedName === currentWorkflow; const label = isCurrent ? `${displayName} (current)` : displayName; return { label, value: qualifiedName }; diff --git a/src/infra/config/loaders/agentLoader.ts b/src/infra/config/loaders/agentLoader.ts index 6db604d..90ead67 100644 --- a/src/infra/config/loaders/agentLoader.ts +++ b/src/infra/config/loaders/agentLoader.ts @@ -77,7 +77,8 @@ export function loadAgentPrompt(agent: CustomAgentConfig): string { } if (agent.promptFile) { - const isValid = getAllowedAgentBases().some((base) => isPathSafe(base, agent.promptFile!)); + const promptFile = agent.promptFile; + const isValid = getAllowedAgentBases().some((base) => isPathSafe(base, promptFile)); if (!isValid) { throw new Error(`Agent prompt file path is not allowed: ${agent.promptFile}`); } diff --git a/src/infra/config/loaders/workflowParser.ts b/src/infra/config/loaders/workflowParser.ts index ae0f13a..1644566 100644 --- a/src/infra/config/loaders/workflowParser.ts +++ b/src/infra/config/loaders/workflowParser.ts @@ -109,7 +109,7 @@ function parseAggregateConditions(argsText: string): string[] { let match: RegExpExecArray | null; while ((match = regex.exec(argsText)) !== null) { - conditions.push(match[1]!); + if (match[1]) conditions.push(match[1]); } if (conditions.length === 0) { @@ -146,7 +146,9 @@ function normalizeRule(r: { const aggMatch = r.condition.match(AGGREGATE_CONDITION_REGEX); if (aggMatch?.[1] && aggMatch[2]) { const conditions = parseAggregateConditions(aggMatch[2]); - const aggregateConditionText = conditions.length === 1 ? conditions[0]! : conditions; + // parseAggregateConditions guarantees conditions.length >= 1 + const aggregateConditionText: string | string[] = + conditions.length === 1 ? (conditions[0] as string) : conditions; return { condition: r.condition, next, diff --git a/src/shared/prompt/select.ts b/src/shared/prompt/select.ts index 29685d0..182fe85 100644 --- a/src/shared/prompt/select.ts +++ b/src/shared/prompt/select.ts @@ -33,7 +33,8 @@ export function renderMenu( const lines: string[] = []; for (let i = 0; i < options.length; i++) { - const opt = options[i]!; + const opt = options[i]; + if (!opt) continue; const isSelected = i === selectedIndex; const cursor = isSelected ? chalk.cyan('❯') : ' '; const truncatedLabel = truncateText(opt.label, maxWidth - labelPrefix);