# TAKT Architecture Knowledge

## Core Structure

WorkflowEngine is a state machine. It manages step transitions via EventEmitter.

```
CLI → WorkflowEngine → Runner (4 types) → RuleEvaluator → next step
```

| Runner | Purpose | When to Use |
|--------|---------|-------------|
| StepExecutor | Standard 3-phase execution | Default |
| ParallelRunner | Concurrent sub-steps | parallel block |
| ArpeggioRunner | Data-driven batch processing | arpeggio block |
| TeamLeaderRunner | Task decomposition → parallel sub-agents | team_leader block |

Runners are mutually exclusive. Do not specify multiple runner types on a single step.

### 3-Phase Execution Model

Normal steps execute in up to 3 phases. Sessions persist across phases.

| Phase | Purpose | Tools | Condition |
|-------|---------|-------|-----------|
| Phase 1 | Main work | Step's allowed_tools | Always |
| Phase 2 | Report output | Write only | When output_contracts defined |
| Phase 3 | Status judgment | None (judgment only) | When tag-based rules exist |

## Rule Evaluation

RuleEvaluator evaluates every rule in YAML order and adopts the first matching rule. Semantic labels are selected once during Phase 3; `when(...)` and aggregate conditions are evaluated deterministically in that same ordered loop. If no rule matches, the workflow aborts with `rule_no_match`.

| Priority | Method | Target |
|----------|--------|--------|
| YAML order | condition | first true rule |

### Condition Syntax

| Syntax | Parsing | Regex |
|--------|---------|-------|
| `when(...)` | Deterministic workflow-state predicate | `isWhenConditionExpression` |
| `all("...")` / `any("...")` | Aggregate condition | `AGGREGATE_CONDITION_REGEX` |
| Plain semantic label | Matches the single Phase 3 selection | — |

Semantic and aggregate conditions can be combined with `&& when(...)`. The condition parser and RuleEvaluator must be updated together for new syntax.

## Provider Integration

Abstracted through the Provider interface. SDK-specific details are encapsulated within each provider.

```
Provider.setup(AgentSetup) → ProviderAgent
ProviderAgent.call(prompt, options) → AgentResponse
```


### Model Resolution

Provider and model resolve independently per field. Higher takes precedence.

1. CLI / environment explicit override
2. Matching promotion (normal agent steps only; parallel sub-steps disallow `promotion` at the schema level)
3. Step / parallel sub-step direct provider / model
4. workflow_call override
5. provider_routing (steps → tags → personas)
6. persona_providers (deprecated)
7. Auto routing
8. Workflow → project config.yaml → global config.yaml → provider default

## Auxiliary Entry Contracts

In TAKT, workflow runtime is not the only user-visible contract entry. Preview, doctor, workflow summary, validation, and report paths are also contract entries. Auxiliary entries that display or validate config values, providers, models, tools, permissions, or output contracts should use the same normalized input, resolver, and override order as runtime.


## Runtime Asset Consumption Boundaries

TAKT runtime assets get their meaning from the entry point that consumes them, not only from their location or name. The same string can be an asset reference, session identifier, display name, or directly supplied body, and each is a separate contract.


### Reference Names and Identity Names

Strings such as `persona`, `session_key`, and `name` mean different things depending on whether they are reference names or identity names. A reference name causes the corresponding resolver to load an asset. An identity name is a key for sessions, logs, state, or display, and a same-named file is not used unless that entry reads it. When adding a new asset, trace the loader that reads it and the call site that consumes it.

## Facet Assembly

The faceted-prompting module is independent from TAKT core.

```
compose(facets, options) → ComposedPrompt { systemPrompt, userMessage }
```


### 3-Layer Facet Resolution Priority

Project `.takt/` → User `~/.takt/` → Builtin `builtins/{lang}/`

Same-named facets are overridden by higher-priority layers. Customize builtins by overriding in upper layers.

## Test Layers and Execution Gates

TAKT classifies unit, light integration, heavy integration, and E2E tests by the boundaries they actually cross, not by filename or duration. A test that starts a real child process is still heavy integration rather than E2E when it calls a local fake CLI from an internal client instead of entering through a user-facing command.

| Layer | Boundary | Standard Gate |
|-------|----------|---------------|
| Unit | Individual function or class; direct dependencies are test doubles, with no real process, Git, filesystem, or workflow engine | `npm test` |
| Light integration | Real filesystem, bounded storage, or multiple production components, without resource-heavy process or engine execution | `npm run test:it` |
| Heavy integration | Real child process, Git, complete WorkflowEngine or TeamLeader execution, or a measured resource-heavy case requiring serial execution | `npm run test:it:heavy` |
| E2E | Runs the application from a user-facing entry point such as the CLI and observes user-visible results | Provider-specific E2E gate |

### Development Execution Order

| State | Execution |
|-------|-----------|
| During implementation | Repeat the unit gate |
| After implementation | Run the light integration gate after the unit gate |
| Added or changed an integration test | Run the `releaseVerificationWiring.test.ts` classification contract by itself |
| Added or changed a heavy integration test | Run the changed file yourself as a target instead of waiting for the full heavy suite |
| Pull request or release | Run the complete light and heavy integration suites |

Heavy integration runners use one worker to avoid process, Git, and synchronous I/O contention. Full local execution is serial, while pull-request CI splits heavy parallel integration across six isolated runners and isolates each serial group on its own runner. `npm test -- <test-file>` routes a classified target to the corresponding runner. The owner of a new or changed heavy integration test must leave this targeted run as completion evidence and must not delegate its first execution to the pull-request-wide heavy gate. `npm run check:release` runs unit, light integration, heavy integration, and E2E in order.

### Mock Provider

`--provider mock` returns deterministic responses. Scenario queues compose multi-turn tests.

```typescript
// Avoid: Calling real API in tests
const response = await callClaude(prompt)

// Example: Set up scenario with mock provider
setMockScenario([
  { persona: 'coder', status: 'done', content: '[STEP:1]\nDone.' },
  { persona: 'reviewer', status: 'done', content: '[STEP:1]\napproved' },
])
```

### Test Isolation

Shared test setup assigns an isolated configuration root to `TAKT_CONFIG_DIR` for each test environment. Except for tests of configuration-directory selection itself, tests create required configuration inside the assigned root rather than replacing the process-wide `TAKT_CONFIG_DIR`. Replacing a process-wide environment variable can leak state into parallel or subsequent tests.


## Platform Priority

TAKT treats Windows as a secondary platform.

## Error Propagation

Provider errors propagate through: `AgentResponse.error` → session log → console output.


## Session Management

Agent sessions are stored per-cwd and per-provider. Session resume is skipped during worktree/clone execution.

When a normal Phase 1 response merely omits `sessionId`, that alone is not a reason to discard the existing session. Paths that are allowed to continue the existing resume context should preserve the old sessionId.

However, when a retry or fallback explicitly runs as a new session and succeeds, a missing `sessionId` must not continue using the old resumed session. The storage layer must be told that the new run produced no sessionId, so the old session is cleared or isolated.

The Report Phase is Phase 2 and reads Phase 1 outputs. Its execution contract is readonly and tool-free. Report retry/fallback must preserve `permissionMode: readonly`, empty tool permission, and provider capability overrides such as turn limits.


## Asynchronous Work and Process Boundaries

Work that must complete after the calling CLI exits cannot be owned only by an unresolved Promise in the CLI process. Before the CLI returns, ownership must transfer to a worker or external service that can survive independently of the CLI process.

| Boundary | Fact to Verify |
|----------|----------------|
| Worker launch target | Resolves from both the distributed build and every supported source or development execution mode |
| Child-process start | Spawn success is observed separately from module loading, work completion, and exit status |
| Parent CLI exit | Required artifacts appear after parent exit, or failure remains observable |
| Polling | A time or attempt bound and the result at that bound are defined |
| Marker or artifact publication | Transient absence or read races during replacement follow the publication contract through bounded retry or explicit failure |

A launch API returning without an error does not prove that the worker loaded its target module or completed its work. Use an artifact or persisted failure whose lifetime does not depend on the parent process. An exit status is independent evidence only when an independent worker or supervisor that survives the parent records it durably.


## Termination-Path Completeness

For features that create temporary files or external resources, verify that they are released not only on normal completion but at every terminal: failure, cancellation, and forced termination. `process.exit()` and forced termination (repeated SIGINT, an abort handler that exits immediately) do not run `finally` blocks. Cleanup that relies on `finally` is bypassed on any path that calls `process.exit` inside it and on forced-termination paths. For each entry point that creates resources, build the list of terminals (normal, failure, cancellation, forced termination) and enumerate the terminals where cleanup does not run.
