# Dynamic Autonomy in System Prompt

## Problem

The system prompt hardcodes the autonomy rule in two places:

1. **Line 1** of `system_prompt.txt` — "do not stop to ask for confirmation or clarification"
2. **Line 100** (`# Communication`) — "If the task is ambiguous, use `think` to reason through… pick the most likely intent"

This is correct for non-interactive and reviewer modes but wrong for REPL and `Session.ask()`, where the user is present and can answer questions.

## Design: interaction policy, not CLI flag

This is an **interaction policy** problem. The policy is either `"autonomous"` or `"interactive"`, and it applies wherever the system prompt is built — both the CLI path (`main()`) and the library path (`Session`).

### Policy mapping

| Entry point | Policy |
|-------------|--------|
| CLI one-shot (`not args.repl`) | `autonomous` |
| CLI reviewer loop | `autonomous` |
| CLI REPL (`args.repl`) | `interactive` |
| `Session.run()` | `autonomous` |
| `Session.ask()` | `interactive` |
| Custom `system_prompt` | No substitution (user controls the text) |
| `provider="command"` | No substitution (uses `_COMMAND_PROVIDER_SYSTEM_PROMPT`) |

### The Session caching problem

`Session._setup()` calls `build_system_prompt()` once and caches the result in `self._system_content`. But `run()` needs autonomous policy and `ask()` needs interactive. Two options:

**Option 1 — Build two cached variants.** `_setup()` calls `build_system_prompt()` twice (or once + a string replace) and stores `_system_content_autonomous` and `_system_content_interactive`. `run()` and `ask()` pick the right one.

**Option 2 — Defer substitution to per-call.** `_setup()` builds the prompt with placeholders still in it. `_system_with_memory()` (already a per-call method) also substitutes the policy placeholders. This keeps one cached base and does the cheap `.replace()` per call.

**Recommendation: Option 2.** The `.replace()` is trivial and `_system_with_memory()` already exists as the per-call hook.

## Implementation

### Step 1: Placeholders in `system_prompt.txt`

Replace the two autonomy-sensitive passages with exact placeholders using `str.replace()`, not `.format()`.

**Line 1** — replace:
```
You solve tasks autonomously using the tools provided. Keep going until the task is fully complete — do not stop to ask for confirmation or clarification. Never ask "should I continue?" — just continue. If the task is ambiguous, use `think` to reason through the possible interpretations against the codebase context, pick the most likely intent, and briefly state your choice before acting.
```

with:
```
You are Swival, a coding agent with a friendly, human-like personality. {{AUTONOMY_DIRECTIVE}}
```

**Line 100** (`# Communication`) — replace:
```
- If the task is ambiguous, use `think` to reason through the possible interpretations against the codebase context, pick the most likely intent, and briefly state your choice before acting.
```

with:
```
{{AMBIGUITY_DIRECTIVE}}
```

### Step 2: Define the directive variants in `agent.py`

```python
from typing import Literal

_InteractionPolicy = Literal["autonomous", "interactive"]

_AUTONOMY_DIRECTIVES: dict[_InteractionPolicy, str] = {
    "autonomous": (
        "You solve tasks autonomously using the tools provided. "
        "Keep going until the task is fully complete — do not stop to ask for "
        "confirmation or clarification. Never ask \"should I continue?\" — just "
        "continue. If the task is ambiguous, use `think` to reason through the "
        "possible interpretations against the codebase context, pick the most "
        "likely intent, and briefly state your choice before acting."
    ),
    "interactive": (
        "You solve tasks using the tools provided. Keep going until the task is "
        "fully complete. If a request is genuinely ambiguous and you cannot "
        "determine the intent from codebase context, briefly ask the user to "
        "clarify before acting. For straightforward tasks, act without asking. "
        "Never ask \"should I continue?\" mid-task — just continue."
    ),
}

_AMBIGUITY_DIRECTIVES: dict[_InteractionPolicy, str] = {
    "autonomous": (
        "- If the task is ambiguous, use `think` to reason through the possible "
        "interpretations against the codebase context, pick the most likely intent, "
        "and briefly state your choice before acting."
    ),
    "interactive": (
        "- If the task is genuinely ambiguous, ask the user a brief clarifying "
        "question. For minor ambiguities, pick the most likely intent and state "
        "your choice."
    ),
}
```

The type alias is prefixed with `_` — it's internal. No export to the public API unless a future change adds policy selection to `Session.__init__()`.

### Step 3: Add a substitution helper

```python
def _apply_interaction_policy(
    system_content: str,
    policy: _InteractionPolicy,
) -> str:
    """Replace autonomy placeholders with policy-specific directives."""
    return (
        system_content
        .replace("{{AUTONOMY_DIRECTIVE}}", _AUTONOMY_DIRECTIVES[policy])
        .replace("{{AMBIGUITY_DIRECTIVE}}", _AMBIGUITY_DIRECTIVES[policy])
    )
```

### Step 4: `build_system_prompt()` — leave placeholders unsubstituted

No new parameter needed. When the default `system_prompt.txt` is loaded, the placeholders stay in the cached text. The custom-prompt and command-provider paths never contain placeholders, so `_apply_interaction_policy` is a no-op on them (the `.replace()` finds nothing to replace).

### Step 5: CLI path — substitute in `main()`

After `build_system_prompt()` returns, before creating the messages list:

```python
# swival/agent.py, after line 3773
policy: _InteractionPolicy = "interactive" if args.repl else "autonomous"
if system_content is not None:
    system_content = _apply_interaction_policy(system_content, policy)
```

### Step 6: Session path — refactor `_system_with_memory()` to single-return

The current `_system_with_memory()` has three early returns (`session.py:332-338`):

```python
def _system_with_memory(self, question, report=None):
    if self._system_content is None:
        return None                          # early return 1
    if not self.memory:
        return self._system_content          # early return 2
    if self.system_prompt:
        return self._system_content          # early return 3
    ...
    if memory_text:
        return self._system_content + "\n\n" + memory_text
    return self._system_content
```

If policy substitution is added "at the end", early returns 2 and 3 skip it. Refactor to build a result variable and return once:

```python
def _system_with_memory(
    self,
    question: str,
    report: "ReportCollector | None" = None,
    policy: _InteractionPolicy = "autonomous",
) -> str | None:
    """Return system content with memory and interaction policy applied."""
    if self._system_content is None:
        return None

    result = self._system_content

    # Inject memory (skipped for custom prompts and when memory is disabled)
    if self.memory and not self.system_prompt:
        from .agent import load_memory

        memory_text = load_memory(
            self.base_dir,
            verbose=self.verbose,
            memory_full=self.memory_full,
            user_query=question,
            report=report,
        )
        if memory_text:
            result = result + "\n\n" + memory_text

    # Substitute interaction-policy placeholders
    result = _apply_interaction_policy(result, policy)

    return result
```

Callers:
- `run()` passes `policy="autonomous"` (or omits, since that's the default)
- `ask()` passes `policy="interactive"`

```python
# session.py — run()
system_content = self._system_with_memory(question, collector)

# session.py — ask()
system_content = self._system_with_memory(question, policy="interactive")
```

`ask()` only calls `_system_with_memory` on the first question (when `_conv_state is None`). The policy is baked into the system message at that point — correct, since the policy shouldn't change mid-conversation.

## Placeholder collision edge case

`_apply_interaction_policy()` runs `.replace()` on the full assembled prompt, which includes user instructions (CLAUDE.md, AGENTS.md) and memory text. If any of those contain the literal strings `{{AUTONOMY_DIRECTIVE}}` or `{{AMBIGUITY_DIRECTIVE}}`, they would get substituted unintentionally. The double-brace + SCREAMING_SNAKE sentinel style makes accidental collision very unlikely, but it's a real edge case worth documenting in a code comment on `_apply_interaction_policy()`.

## Testing

All tests assert on rendered prompt text, not model behavior.

### Test 1: Autonomous mode renders correctly
Call `build_system_prompt(...)` with default file, then `_apply_interaction_policy(result, "autonomous")`. Assert the output contains "do not stop to ask for confirmation" and "pick the most likely intent".

### Test 2: Interactive mode renders correctly
Same setup, `_apply_interaction_policy(result, "interactive")`. Assert the output contains "ask the user to clarify" and does NOT contain "do not stop to ask for confirmation".

### Test 3: No placeholders remain
For both policies, assert `"{{AUTONOMY_DIRECTIVE}}"` and `"{{AMBIGUITY_DIRECTIVE}}"` are not in the output.

### Test 4: Custom system_prompt is untouched
Call `build_system_prompt(system_prompt="My custom prompt", ...)`, apply both policies. Assert the result equals "My custom prompt" plus the date suffix — no placeholder substitution changes anything.

### Test 5: Command provider prompt is untouched
Call `build_system_prompt(provider="command", ...)`, apply both policies. Assert the result starts with the `_COMMAND_PROVIDER_SYSTEM_PROMPT` text, contains no placeholders, and contains neither the autonomous phrases ("do not stop to ask for confirmation") nor the interactive phrases ("ask the user to clarify"). This proves the command-provider path is a true no-op for both policies.

### Test 6: Session.run() produces autonomous prompt
Create a `Session(...)` with monkeypatched `call_llm`. Patch `swival.agent.run_agent_loop` (the import target, since `session.py:422` imports it inside the method body) to capture the `messages` argument. Call `session.run("test")`. Extract `messages[0]["content"]` (the system message) and assert it contains "do not stop to ask for confirmation".

### Test 7: Session.ask() produces interactive prompt
Same setup. Patch `swival.agent.run_agent_loop` to capture `messages`. Call `session.ask("test")`. Extract `messages[0]["content"]` and assert it contains "ask the user to clarify" and does NOT contain "do not stop to ask for confirmation".

## Files to change

| File | Change |
|------|--------|
| `swival/system_prompt.txt` | Replace 2 passages with `{{AUTONOMY_DIRECTIVE}}` and `{{AMBIGUITY_DIRECTIVE}}` |
| `swival/agent.py` | Add `_InteractionPolicy` type alias, directive dicts, `_apply_interaction_policy()`. Substitute after `build_system_prompt()` in `main()` |
| `swival/session.py` | Refactor `_system_with_memory()` to single-return, add `policy` param. Pass `"interactive"` from `ask()` |
| `tests/test_system_prompt.py` (new) | 7 tests as described above |
