sf snapshot: uncommitted changes after 49m inactivity
This commit is contained in:
parent
89677b7e9b
commit
6fc054e7c3
200 changed files with 1171 additions and 1167 deletions
|
|
@ -121,7 +121,7 @@ Every exported function, type, class, and module-level constant opens with a JSD
|
|||
* unavailable (shared NFS, broken filesystem semantics) — the conditional UPDATE in
|
||||
* SQLite is the safety net.
|
||||
*
|
||||
* Consumer: auto-dispatch.ts when picking the next eligible unit per poll tick.
|
||||
* Consumer: autonomous dispatch.ts when picking the next eligible unit per poll tick.
|
||||
*/
|
||||
export function claimUnit(unitId: string, leaseMs: number): boolean { ... }
|
||||
```
|
||||
|
|
@ -237,7 +237,7 @@ See [`docs/plans/README.md`](docs/plans/README.md), [`docs/adr/README.md`](docs/
|
|||
|
||||
## SF Schedule
|
||||
|
||||
The SF schedule system (`/sf schedule`) stores project time-bound reminders in the repo SQLite DB (`.sf/sf.db`, `schedule_entries`) and global reminders in `~/.sf/sf.db`. Legacy `.sf/schedule.jsonl` rows are import-only compatibility input when a project has no schedule rows yet. Items surface on their due date via pull queries at launch and auto-mode boundaries — there is no background daemon.
|
||||
The SF schedule system (`/sf schedule`) stores project time-bound reminders in the repo SQLite DB (`.sf/sf.db`, `schedule_entries`) and global reminders in `~/.sf/sf.db`. Legacy `.sf/schedule.jsonl` rows are import-only compatibility input when a project has no schedule rows yet. Items surface on their due date via pull queries at launch and autonomous mode boundaries — there is no background daemon.
|
||||
|
||||
**When to use `sf schedule` vs backlog:**
|
||||
- **`sf schedule`** — time-bound items that must surface at a future date: a 2-week adoption review after shipping a feature, a 1-month audit of an architectural decision, a 30-minute reminder to run a command. Use when the *timing* matters, not just the *priority*.
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -121,7 +121,7 @@ Full documentation is in the [`docs/`](./docs/) directory:
|
|||
### User Guides
|
||||
|
||||
- **[Getting Started](./docs/user-docs/getting-started.md)** — install, first run, basic usage
|
||||
- **[Autonomous Mode](./docs/user-docs/auto-mode.md)** — autonomous execution deep-dive
|
||||
- **[Autonomous Mode](./docs/user-docs/autonomous-mode.md)** — autonomous execution deep-dive
|
||||
- **[Configuration](./docs/user-docs/configuration.md)** — all preferences, models, git, and hooks
|
||||
- **[Custom Models](./docs/user-docs/custom-models.md)** — add custom providers (Ollama, vLLM, LM Studio, proxies)
|
||||
- **[Token Optimization](./docs/user-docs/token-optimization.md)** — profiles, context compression, complexity routing
|
||||
|
|
@ -153,7 +153,7 @@ Full documentation is in the [`docs/`](./docs/) directory:
|
|||
The original SF was a collection of markdown prompts installed into `~/.claude/commands/`. It relied entirely on the LLM reading those prompts and doing the right thing. That worked surprisingly well — but it had hard limits:
|
||||
|
||||
- **No context control.** The LLM accumulated garbage over a long session. Quality degraded.
|
||||
- **No real automation.** "Auto mode" was the LLM calling itself in a loop, burning context on orchestration overhead.
|
||||
- **No real automation.** The old continuous loop was the LLM calling itself, burning context on orchestration overhead.
|
||||
- **No crash recovery.** If the session died mid-task, you started over.
|
||||
- **No observability.** No cost tracking, no progress dashboard, no stuck detection.
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ SF v2 solves all of these because it's not a prompt framework anymore — it's a
|
|||
| -------------------- | ---------------------------- | ------------------------------------------------------- |
|
||||
| Runtime | Claude Code slash commands | Standalone CLI via Pi SDK |
|
||||
| Context management | Hope the LLM doesn't fill up | Fresh session per task, programmatic |
|
||||
| Auto mode | LLM self-loop | State machine reading `.sf/` files |
|
||||
| Autonomous mode | LLM self-loop | State machine reading `.sf/` files |
|
||||
| Crash recovery | None | Lock files + session forensics |
|
||||
| Git strategy | LLM writes git commands | Worktree isolation, sequential commits, squash merge |
|
||||
| Cost tracking | None | Per-unit token/cost ledger with dashboard |
|
||||
|
|
@ -235,7 +235,7 @@ This is what makes SF different. Run it, walk away, come back to built software.
|
|||
/sf autonomous
|
||||
```
|
||||
|
||||
Autonomous mode is governed by the Unified Operation Kernel (UOK), not by the LLM or a loose file loop. UOK reads canonical project state, records each run in the DB-backed ledger, projects runtime files for query/UI/backcompat, determines the next unit of work, creates a fresh agent session, injects a focused prompt with all relevant context pre-inlined, and lets the LLM execute. When the LLM finishes, autonomous mode reconciles the UOK ledger and projections before dispatching the next unit. Legacy `/sf auto` remains accepted only for compatibility; new prompts and docs use `/sf autonomous`.
|
||||
Autonomous mode is governed by the Unified Operation Kernel (UOK), not by the LLM or a loose file loop. UOK reads canonical project state, records each run in the DB-backed ledger, projects runtime files for query/UI, determines the next unit of work, creates a fresh agent session, injects a focused prompt with all relevant context pre-inlined, and lets the LLM execute. When the LLM finishes, autonomous mode reconciles the UOK ledger and projections before dispatching the next unit. Use `/sf autonomous`; there is no separate `/sf auto` mode.
|
||||
|
||||
**What happens under the hood:**
|
||||
|
||||
|
|
@ -263,18 +263,18 @@ Autonomous mode is governed by the Unified Operation Kernel (UOK), not by the LL
|
|||
|
||||
12. **Escape hatch** — Press Escape to pause. The conversation is preserved. Interact with the agent, inspect what happened, or just `/sf autonomous` to resume from disk state.
|
||||
|
||||
### `/sf` and `/sf next` — Step Mode
|
||||
### `/sf` and `/sf next` — Assisted Mode
|
||||
|
||||
By default, `/sf` runs in **step mode**: the same UOK-governed dispatch loop as autonomous mode, but it pauses between units with a wizard showing what completed and what's next. You advance one step at a time, review the output, and continue when ready.
|
||||
By default, `/sf` runs in **assisted mode**: the same UOK-governed dispatch loop as autonomous mode, but it pauses between units with a wizard showing what completed and what's next. You advance one step at a time, review the output, and continue when ready.
|
||||
|
||||
- **No `.sf/` directory** → Start a new project. Discussion flow captures your vision, constraints, and preferences.
|
||||
- **Milestone exists, no roadmap** → Discuss or research the milestone.
|
||||
- **Roadmap exists, slices pending** → Plan the next slice, execute one task, or switch to auto.
|
||||
- **Roadmap exists, slices pending** → Plan the next slice, execute one task, or switch to autonomous mode.
|
||||
- **Mid-task** → Resume from where you left off.
|
||||
|
||||
`/sf next` is an explicit alias for step mode. You can switch from step → auto mid-session via the wizard.
|
||||
`/sf next` is an explicit alias for assisted mode. You can switch from assisted mode to autonomous mode mid-session via the wizard.
|
||||
|
||||
Step mode is the on-ramp. Auto mode is the highway.
|
||||
Assisted mode pauses after each unit. Autonomous mode continues until policy, evidence, budget, blockers, or completion stops it.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -313,7 +313,7 @@ sf
|
|||
|
||||
SF opens an interactive agent session. From there, you have two ways to work:
|
||||
|
||||
**`/sf` — step mode.** Type `/sf` and SF executes one unit of work at a time, pausing between each with a wizard showing what completed and what's next. Same UOK lifecycle and recovery model as autonomous mode, but you stay in the loop. No project yet? It starts the discussion flow. Roadmap exists? It plans or executes the next step.
|
||||
**`/sf` — assisted mode.** Type `/sf` and SF executes one unit of work at a time, pausing between each with a wizard showing what completed and what's next. Same UOK lifecycle and recovery model as autonomous mode, but you stay in the loop. No project yet? It starts the discussion flow. Roadmap exists? It plans or executes the next step.
|
||||
|
||||
**`/sf autonomous` — autonomous mode.** Type `/sf autonomous` and walk away. SF researches, plans, executes, verifies, commits, and advances through every slice until the milestone is complete. Fresh context window per task. No babysitting.
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ surface, not run control, not a permission profile, and not an output format.
|
|||
sf headless --timeout 600000 autonomous
|
||||
|
||||
# Create and execute a milestone end-to-end
|
||||
sf headless new-milestone --context spec.md --auto
|
||||
sf headless new-milestone --context spec.md --autonomous
|
||||
|
||||
# One unit at a time (cron-friendly)
|
||||
sf headless next
|
||||
|
|
@ -394,8 +394,8 @@ On first run, SF launches a branded setup wizard that walks you through LLM prov
|
|||
|
||||
| Command | What it does |
|
||||
| ----------------------- | --------------------------------------------------------------- |
|
||||
| `/sf` | Step mode — executes one unit at a time, pauses between each |
|
||||
| `/sf next` | Explicit step mode (same as bare `/sf`) |
|
||||
| `/sf` | Assisted mode — executes one unit at a time, pauses between each |
|
||||
| `/sf next` | Explicit assisted mode (same as bare `/sf`) |
|
||||
| `/sf autonomous` | Autonomous mode — researches, plans, executes, commits, repeats |
|
||||
| `/sf quick` | Execute a quick task with SF guarantees, skip planning overhead |
|
||||
| `/sf stop` | Stop autonomous mode gracefully |
|
||||
|
|
@ -496,7 +496,7 @@ The verification ladder: static checks → command execution → behavioral test
|
|||
`Ctrl+Alt+G` or `/sf status` opens a real-time overlay showing:
|
||||
|
||||
- Current milestone, slice, and task progress
|
||||
- Auto mode elapsed time and phase
|
||||
- Autonomous mode elapsed time and phase
|
||||
- Per-unit cost and token breakdown by phase, slice, and model
|
||||
- Cost projections based on completed work
|
||||
- Completed and in-progress units
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Welcome to the SF documentation. SF is a purpose-to-software compiler: it turns bounded intent into PDD contracts, researches missing context, writes failing tests or executable evidence first, implements the smallest satisfying change, and records verification. See [ADR-0000](./adr/0000-purpose-to-software-compiler.md) and [Spec-First TDD](./SPEC_FIRST_TDD.md) before changing product behavior.
|
||||
|
||||
This index covers everything from getting started to advanced configuration, autonomous-mode internals, and extending SF with the Pi SDK.
|
||||
This index covers everything from getting started to advanced configuration, autonomous mode internals, and extending SF with the Pi SDK.
|
||||
|
||||
## User Documentation
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ Guides for installing, configuring, and using SF day-to-day. Located in [`user-d
|
|||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Getting Started](./user-docs/getting-started.md) | Installation, first run, and basic usage |
|
||||
| [Autonomous Mode](./user-docs/auto-mode.md) | How autonomous execution works — the state machine, crash recovery, and steering |
|
||||
| [Autonomous Mode](./user-docs/autonomous-mode.md) | How autonomous execution works — the state machine, crash recovery, and steering |
|
||||
| [Commands Reference](./user-docs/commands.md) | All commands, keyboard shortcuts, and CLI flags |
|
||||
| [Remote Questions](./user-docs/remote-questions.md) | Discord and Slack delivery for run-control-gated questions |
|
||||
| [Configuration](./user-docs/configuration.md) | Preferences, model selection, git settings, and token profiles |
|
||||
|
|
@ -19,7 +19,7 @@ Guides for installing, configuring, and using SF day-to-day. Located in [`user-d
|
|||
| [Custom Models](./user-docs/custom-models.md) | Advanced model configuration — models.json schema, compat flags, overrides |
|
||||
| [Token Optimization](./user-docs/token-optimization.md) | Token profiles, context compression, complexity routing, and adaptive learning (v2.17) |
|
||||
| [Dynamic Model Routing](./user-docs/dynamic-model-routing.md) | Complexity-based model selection, cost tables, escalation, and budget pressure (v2.19) |
|
||||
| [Captures & Triage](./user-docs/captures-triage.md) | Fire-and-forget thought capture during auto-mode with automated triage (v2.19) |
|
||||
| [Captures & Triage](./user-docs/captures-triage.md) | Fire-and-forget thought capture during autonomous mode with automated triage (v2.19) |
|
||||
| [Workflow Visualizer](./user-docs/visualizer.md) | Interactive TUI overlay for progress, dependencies, metrics, and timeline (v2.19) |
|
||||
| [Cost Management](./user-docs/cost-management.md) | Budget ceilings, cost tracking, projections, and enforcement modes |
|
||||
| [Git Strategy](./user-docs/git-strategy.md) | Worktree isolation, branching model, and merge behavior |
|
||||
|
|
|
|||
|
|
@ -711,7 +711,7 @@ The mechanical summary quality might be insufficient for complex slices.
|
|||
24. Simplify `auto-stuck-detection.ts` — fewer unit type patterns
|
||||
25. Simplify `auto-idempotency.ts` — fewer completed-key types
|
||||
26. Review `auto-recovery.ts` — simplify recovery paths for unit types that are now fallback-only
|
||||
27. Update auto-mode documentation (`docs/auto-mode.md`)
|
||||
27. Update autonomous mode documentation (`docs/user-docs/autonomous-mode.md`)
|
||||
|
||||
## Audit Trail
|
||||
|
||||
|
|
|
|||
|
|
@ -959,9 +959,6 @@
|
|||
| scripts/preview-dashboard.ts | Web Mode | Dashboard preview server |
|
||||
| scripts/ci_monitor.cjs | Build System | CI monitoring dashboard |
|
||||
| scripts/recover-sf-1364.sh | Build System, Migration | Recovery script for issue #1364 |
|
||||
| scripts/recover-sf-1364.ps1 | Build System, Migration | Recovery script for issue #1364 (PowerShell) |
|
||||
| scripts/recover-sf-1668.sh | Build System, Migration | Recovery script for issue #1668 |
|
||||
| scripts/recover-sf-1668.ps1 | Build System, Migration | Recovery script for issue #1668 (PowerShell) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -303,12 +303,12 @@ cluster, no edits, no test runs.
|
|||
|
||||
### LOW
|
||||
|
||||
- `src/headless.ts:348-356` [autonomous → auto alias] **Status: CONFIRMED**
|
||||
The `autonomous` command aliases to `auto` in CLI parsing, but `--auto` must be passed explicitly for auto-mode chaining to activate. Running `sf headless autonomous` without `--auto` does not chain, which contradicts the command name's intent.
|
||||
Suggested fix: When the `autonomous` subcommand is parsed, implicitly set `options.auto = true`.
|
||||
- `src/headless.ts:348-356` [autonomous command chaining] **Status: CONFIRMED**
|
||||
The `autonomous` command took the wrong parser path, so autonomous chaining did not activate from the command name alone.
|
||||
Suggested fix: When the `autonomous` subcommand is parsed, implicitly set the autonomous chaining option.
|
||||
|
||||
- `src/headless.ts:1189` [auto-mode chaining visibility] **Status: CONFIRMED**
|
||||
With verbose output enabled, log lines containing "milestone X ready" could spuriously match `isMilestoneReadyText()` and trigger premature auto-mode chaining.
|
||||
- `src/headless.ts:1189` [autonomous chaining visibility] **Status: CONFIRMED**
|
||||
With verbose output enabled, log lines containing "milestone X ready" could spuriously match `isMilestoneReadyText()` and trigger premature autonomous chaining.
|
||||
Suggested fix: Restrict the detection to `message_delta` events of type `text` and exclude tool-output or log-prefixed content.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -498,6 +498,12 @@ The terminal-agent pass adds concrete machine/API patterns:
|
|||
- Amazon Q has the richest declarative agent schema and useful lifecycle/text/
|
||||
tool/state event taxonomy. Forge should adapt manifests and event taxonomy,
|
||||
not recursive delegate subprocesses or raw passthrough protocol events.
|
||||
- GitHub Copilot CLI's autopilot documentation is a useful naming cross-check:
|
||||
autopilot is the continuation behavior, `--allow-all`/`--yolo` are permission
|
||||
expansion, and `--no-ask-user` is question suppression. Forge should keep the
|
||||
same separation but use SF's own terms: run control is `manual | assisted |
|
||||
autonomous`, permission profile is `restricted | normal | trusted |
|
||||
unrestricted`, and headless/machine output is a surface/format concern.
|
||||
|
||||
## Local Spec-System Cross-Check
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,15 @@ Run control describes how far SF continues through the flow before stopping for
|
|||
- **assisted** — SF proposes and executes bounded steps, with human approval for important or uncertain actions.
|
||||
- **autonomous** — SF continues through the flow until policy, evidence, budget, or completion stops it.
|
||||
|
||||
`auto` is acceptable only as shorthand in commands, flags, and UI labels. The concept name is **autonomous**.
|
||||
`auto` is not a run-control mode. Use **autonomous** for continuous run control; use **assisted** for bounded human-guided progression.
|
||||
|
||||
> Implementation note: if product language uses "autopilot", it maps to
|
||||
> autonomous run control. It must not introduce a separate flow, protocol,
|
||||
> output format, or compatibility alias. Autopilot means the UOK-governed
|
||||
> autonomous controller keeps moving until one of its explicit stop conditions
|
||||
> fires.
|
||||
|
||||
UOK kernel records carry `runControl` as a first-class lifecycle field. Workflow phases such as planning, building, verification, and finalization are separate execution stages, not run-control modes.
|
||||
|
||||
## Permission Profile
|
||||
|
||||
|
|
@ -83,6 +91,8 @@ A permission profile describes what SF is allowed to touch when a run-control mo
|
|||
|
||||
Run control and permission profile are independent. For example, `autonomous + restricted` can keep going with narrow permissions, while `manual + trusted` still asks before each consequential step but can perform broader approved actions.
|
||||
|
||||
UOK kernel records and execution-policy decisions carry `permissionProfile` as the trust posture. Permission expansion never implies autonomous continuation.
|
||||
|
||||
## Naming Rules
|
||||
|
||||
- Say **flow** for the shared planning/execution engine.
|
||||
|
|
@ -104,6 +114,11 @@ Markdown under `.sf/` has two roles:
|
|||
|
||||
Markdown under `docs/specs/` is a human export for review, navigation, and git history. Generated docs can change; Git records that human-facing history. If SF needs its own operational history, it should store that in `.sf`/DB-backed state. Plans should record any surface, protocol, output-format, run-control, or permission-profile impact explicitly when a milestone changes integration behavior.
|
||||
|
||||
Reflection notes, capture files, and session thoughts are input material, not a
|
||||
parallel backlog. Autonomous mode may triage them, but durable outcomes must
|
||||
graduate into DB-backed requirements, decisions, knowledge, roadmap rows,
|
||||
tests, or tracked documentation.
|
||||
|
||||
## Source Placement
|
||||
|
||||
SF source placement follows the same axis model. New code should extend the owning axis instead of creating parallel trees.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Use `sf schedule` when something needs to happen at a specific future time but c
|
|||
SF has no long-running daemon. Entries are not "fired" by a timer. Instead, the schedule store is queried at specific integration points:
|
||||
|
||||
1. **On launch** — `loader.ts` calls `findDue()` and prints a banner if items are due
|
||||
2. **Auto-mode boundaries** — `sf headless query` (machine snapshot) and the TUI status overlay include due/upcoming entries in their output
|
||||
2. **Autonomous mode boundaries** — `sf headless query` (machine snapshot) and the TUI status overlay include due/upcoming entries in their output
|
||||
3. **CLI query** — `sf schedule list --due` shows items whose `due_at <= now`
|
||||
|
||||
This means: if an item is scheduled for 3 AM and you open SF at 9 AM, you will see the item as overdue. There is no fire-at-exact-time guarantee. This is an explicit trade-off — see the [pull-based ADR](../adr/0002-sf-schedule-pull-based.md) for the full decision record.
|
||||
|
|
@ -70,15 +70,15 @@ Legacy `schedule.jsonl` files are import-only compatibility inputs. Rows without
|
|||
"created_at": "2026-05-15T09:00:00.000Z",
|
||||
"snoozed_at": "2026-06-01T09:00:00.000Z", // ISO-8601 — set on each snooze
|
||||
"payload": { "message": "Review adoption metrics" }, // kind-specific
|
||||
"created_by": "user", // user | auto | system
|
||||
"auto_dispatch": false // if true + kind=reminder, surface in auto-mode dispatch
|
||||
"created_by": "user", // user | agent | system
|
||||
"autonomous_dispatch": false // if true + kind=prompt/command, consume from autonomous mode
|
||||
}
|
||||
```
|
||||
|
||||
### Legacy JSONL Line Example
|
||||
|
||||
```
|
||||
{"schemaVersion":1,"id":"01ARZ3NDEKTSV4RRFFQ69G5FAV","kind":"reminder","status":"pending","due_at":"2026-06-15T09:00:00.000Z","created_at":"2026-05-15T09:00:00.000Z","payload":{"message":"Review adoption metrics"},"created_by":"user","auto_dispatch":false}
|
||||
{"schemaVersion":1,"id":"01ARZ3NDEKTSV4RRFFQ69G5FAV","kind":"reminder","status":"pending","due_at":"2026-06-15T09:00:00.000Z","created_at":"2026-05-15T09:00:00.000Z","payload":{"message":"Review adoption metrics"},"created_by":"user","autonomous_dispatch":false}
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -95,7 +95,7 @@ Legacy `schedule.jsonl` files are import-only compatibility inputs. Rows without
|
|||
| `audit` | Audit surfaced at next planning turn | `unitId?` |
|
||||
| `command` | Shell command run by explicit `sf schedule run <id>` | `command`, `capture?` |
|
||||
|
||||
`review` and `audit` kinds are surfaced to the next autonomous planning turn (TBD: integration point in `sf_plan_slice` / `sf_plan_task` / auto-dispatch). They are stored but not auto-dispatched without a consumer.
|
||||
`review` and `audit` kinds are surfaced to the next autonomous planning turn (TBD: integration point in `sf_plan_slice` / `sf_plan_task` / autonomous dispatch). They are stored but not autonomous dispatched without a consumer.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -236,9 +236,9 @@ schedule:
|
|||
|
||||
These entries are created at milestone creation time. The `in` field is relative to `now`. The `on_complete` variant fires a duration after milestone completion.
|
||||
|
||||
### Auto-Dispatch
|
||||
### Autonomous Dispatch
|
||||
|
||||
When `auto_dispatch: true` and `kind: "reminder"`, the item is surfaced as a dispatch input in auto-mode when `due_at <= now`. This is the mechanism for time-bound autonomous reminders.
|
||||
When `autonomous_dispatch: true` and `kind: "prompt"` or `kind: "command"`, the item is consumed by autonomous mode when `due_at <= now`. This is the mechanism for time-bound autonomous repo work.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
Autonomous mode is SF's product-development execution engine for the purpose-to-software compiler. It advances only from structured state: bounded intent, PDD fields, research assumptions, tests or executable evidence, implementation, verification, and recorded outcomes. Run `/sf autonomous`, walk away, come back to built software with clean git history.
|
||||
|
||||
> Terminology: "autopilot" is user-facing shorthand for autonomous mode. It is
|
||||
> not a second mode, not a looser `auto` compatibility path, and not a permission
|
||||
> bypass. The same UOK policy, evidence, budget, blocker, and completion gates
|
||||
> decide how far it continues.
|
||||
|
||||
## How It Works
|
||||
|
||||
Autonomous mode is governed by the **Unified Operation Kernel (UOK)**. UOK reads canonical project state, records lifecycle and recovery in the DB-backed ledger, and writes runtime files as projections for query, UI, and compatibility. It determines the next unit of work, creates a fresh agent session, injects a focused prompt with all relevant context pre-inlined, and lets the LLM execute. When the LLM finishes, autonomous mode reconciles the UOK ledger and projections before dispatching the next unit. Markdown files are projections for humans when structured state exists.
|
||||
|
|
@ -79,6 +84,12 @@ No manual intervention needed for transient errors — the session pauses briefl
|
|||
|
||||
SF maintains a `KNOWLEDGE.md` file — an append-only register of project-specific rules, patterns, and lessons learned. The agent reads it at the start of every unit and appends to it when discovering recurring issues, non-obvious patterns, or rules that future sessions should follow. This gives autonomous mode cross-session memory that survives context window boundaries.
|
||||
|
||||
Reflection files and thought dumps are treated as raw observations. Autonomous
|
||||
mode should triage them before acting: keep weak hypotheses as source evidence,
|
||||
promote validated patterns into `.sf`/DB-backed knowledge or requirements, and
|
||||
turn actionable findings into tests, tasks, or docs. A thought file by itself is
|
||||
not an instruction queue.
|
||||
|
||||
### Context Pressure Monitor (v2.26)
|
||||
|
||||
When context usage reaches 70%, SF sends a wrap-up signal to the agent, nudging it to finish durable output (commit, write summaries) before the context window fills. This prevents sessions from hitting the hard context limit mid-task with no artifacts written.
|
||||
|
|
@ -267,7 +278,7 @@ Open the workflow visualizer — interactive tabs for progress, dependencies, me
|
|||
`Ctrl+Alt+G` or `/sf status` shows real-time progress:
|
||||
|
||||
- Current milestone, slice, and task
|
||||
- Auto mode elapsed time and phase
|
||||
- Autonomous mode elapsed time and phase
|
||||
- Per-unit cost and token breakdown
|
||||
- Cost projections
|
||||
- Completed and in-progress units
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/sf` | Step mode — execute one unit at a time, pause between each |
|
||||
| `/sf next` | Explicit step mode (same as `/sf`) |
|
||||
| `/sf` | Assisted mode — execute one unit at a time, pause between each |
|
||||
| `/sf next` | Explicit assisted mode (same as `/sf`) |
|
||||
| `/sf autonomous` | Autonomous product loop — research, plan, execute, commit, repeat |
|
||||
| `/sf quick` | Execute a quick task with SF guarantees (atomic commits, state tracking) without full planning overhead |
|
||||
| `/sf stop` | Stop autonomous mode gracefully |
|
||||
|
|
@ -214,7 +214,7 @@ sf headless --timeout 600000 autonomous
|
|||
sf headless dispatch plan
|
||||
|
||||
# Create a new milestone from a context file and start autonomous mode
|
||||
sf headless new-milestone --context brief.md --auto
|
||||
sf headless new-milestone --context brief.md --autonomous
|
||||
|
||||
# Create a milestone from inline text
|
||||
sf headless new-milestone --context-text "Build a REST API with auth"
|
||||
|
|
@ -232,7 +232,7 @@ echo "Build a CLI tool" | sf headless new-milestone --context -
|
|||
| `--model ID` | Override the model for the headless session |
|
||||
| `--context <file>` | Context file for `new-milestone` (use `-` for stdin) |
|
||||
| `--context-text <text>` | Inline context text for `new-milestone` |
|
||||
| `--auto` | Chain into autonomous mode after milestone creation |
|
||||
| `--autonomous` | Chain into autonomous mode after milestone creation |
|
||||
|
||||
**Exit codes:** `0` = complete, `1` = error or timeout, `2` = blocked.
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ Type `/sf` inside a session. SF executes one unit of work at a time, pausing bet
|
|||
- **Roadmap exists, slices pending** — plan the next slice or execute a task
|
||||
- **Mid-task** — resume where you left off
|
||||
|
||||
Step mode keeps you in the loop, reviewing output between each step.
|
||||
Assisted mode keeps you in the loop, reviewing output between each step.
|
||||
|
||||
### Autonomous Mode — `/sf autonomous`
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ Type `/sf autonomous` and walk away. SF researches, plans, executes, verifies, c
|
|||
/sf autonomous
|
||||
```
|
||||
|
||||
See [Autonomous Mode](./auto-mode.md) for full details.
|
||||
See [Autonomous Mode](./autonomous-mode.md) for full details.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ For more, see [Troubleshooting](./troubleshooting.md).
|
|||
|
||||
## Next Steps
|
||||
|
||||
- [Autonomous Mode](./auto-mode.md) — deep dive into autonomous execution
|
||||
- [Autonomous Mode](./autonomous-mode.md) — deep dive into autonomous execution
|
||||
- [Configuration](./configuration.md) — model selection, timeouts, budgets
|
||||
- [Commands Reference](./commands.md) — all commands and shortcuts
|
||||
- [Provider Setup](./providers.md) — detailed setup for every provider
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ These features apply only in **worktree mode**.
|
|||
|
||||
### Automatic (Autonomous Mode)
|
||||
|
||||
Auto mode creates and manages worktrees automatically:
|
||||
Autonomous mode creates and manages worktrees automatically:
|
||||
|
||||
1. When a milestone starts, a worktree is created at `.sf/worktrees/<MID>/` on branch `milestone/<MID>`
|
||||
2. Planning artifacts from `.sf/milestones/` are copied into the worktree
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ It checks:
|
|||
|
||||
## Common Issues
|
||||
|
||||
### Auto mode loops on the same unit
|
||||
### Autonomous mode loops on the same unit
|
||||
|
||||
**Symptoms:** The same unit (e.g., `research-slice` or `plan-slice`) dispatches repeatedly until hitting the dispatch limit.
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ It checks:
|
|||
|
||||
**Fix:** Run `/sf doctor` to repair state, then resume with `/sf autonomous`. If the issue persists, check that the expected artifact file exists on disk.
|
||||
|
||||
### Auto mode stops with "Loop detected"
|
||||
### Autonomous mode stops with "Loop detected"
|
||||
|
||||
**Cause:** A unit failed to produce its expected artifact twice in a row.
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ source ~/.bashrc
|
|||
|
||||
### Provider errors during autonomous mode
|
||||
|
||||
**Symptoms:** Auto mode pauses with a provider error (rate limit, server error, auth failure).
|
||||
**Symptoms:** Autonomous mode pauses with a provider error (rate limit, server error, auth failure).
|
||||
|
||||
**How SF handles it (v2.26):**
|
||||
|
||||
|
|
@ -99,13 +99,13 @@ For common provider setup issues (role errors, streaming errors, model ID mismat
|
|||
|
||||
### Budget ceiling reached
|
||||
|
||||
**Symptoms:** Auto mode pauses with "Budget ceiling reached."
|
||||
**Symptoms:** Autonomous mode pauses with "Budget ceiling reached."
|
||||
|
||||
**Fix:** Increase `budget_ceiling` in preferences, or switch to `budget` token profile to reduce per-unit cost, then resume with `/sf autonomous`.
|
||||
|
||||
### Stale lock file
|
||||
|
||||
**Symptoms:** Auto mode won't start, says another session is running.
|
||||
**Symptoms:** Autonomous mode won't start, says another session is running.
|
||||
|
||||
**Fix:** SF automatically detects stale locks — if the owning PID is dead, the lock is cleaned up and re-acquired on the next `/sf autonomous`. This includes stranded `.sf.lock/` directories left by `proper-lockfile` after crashes. If automatic recovery fails, delete `.sf/auto.lock` and the `.sf.lock/` directory manually:
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ rm -rf "$(dirname .sf)/.sf.lock"
|
|||
|
||||
### Pre-dispatch says the milestone integration branch no longer exists
|
||||
|
||||
**Symptoms:** Auto mode or `/sf doctor` reports that a milestone recorded an integration branch that no longer exists in git.
|
||||
**Symptoms:** Autonomous mode or `/sf doctor` reports that a milestone recorded an integration branch that no longer exists in git.
|
||||
|
||||
**What it means:** The milestone's `.sf/milestones/<MID>/<MID>-META.json` still points at the branch that was active when the milestone started, but that branch has since been renamed or deleted.
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ rm -rf "$(dirname .sf)/.sf.lock"
|
|||
|
||||
### Session lock stolen by `/sf` in another terminal
|
||||
|
||||
**Symptoms:** Running `/sf` (step mode) in a second terminal causes a running autonomous mode session to lose its lock.
|
||||
**Symptoms:** Running `/sf` (assisted mode) in a second terminal causes a running autonomous mode session to lose its lock.
|
||||
|
||||
**Fix:** Fixed in v2.36.0. Bare `/sf` no longer steals the session lock from a running autonomous mode session. Upgrade to the latest version.
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const TOOLS: Tool[] = [
|
|||
{
|
||||
name: "start_session",
|
||||
description:
|
||||
'Start a new SF autonomous-mode session for a project. Provide the absolute project path. Optionally provide a command to run instead of the default "/sf autonomous".',
|
||||
'Start a new SF autonomous mode session for a project. Provide the absolute project path. Optionally provide a command to run instead of the default "/sf autonomous".',
|
||||
input_schema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class MockRpcClient {
|
|||
type: "extension_ui_request",
|
||||
id: "pause-notice",
|
||||
method: "notify",
|
||||
message: "Auto-mode paused: daemon reload requested",
|
||||
message: "Autonomous mode paused: daemon reload requested",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -363,7 +363,7 @@ describe("SessionManager", () => {
|
|||
type: "extension_ui_request",
|
||||
id: "n1",
|
||||
method: "notify",
|
||||
message: "Auto-mode stopped: completed all tasks",
|
||||
message: "Autonomous mode stopped: completed all tasks",
|
||||
});
|
||||
|
||||
assert.equal(session.status, "completed");
|
||||
|
|
@ -451,7 +451,7 @@ describe("SessionManager", () => {
|
|||
type: "extension_ui_request",
|
||||
id: "n2",
|
||||
method: "notify",
|
||||
message: "Auto-mode stopped: all done",
|
||||
message: "Autonomous mode stopped: all done",
|
||||
});
|
||||
|
||||
assert.equal(session.status, "completed");
|
||||
|
|
@ -644,9 +644,9 @@ describe("SessionManager", () => {
|
|||
assert.equal(session.pendingBlocker, null);
|
||||
});
|
||||
|
||||
// ---- Terminal detection (auto-mode stopped notification) ----
|
||||
// ---- Terminal detection (autonomous mode stopped notification) ----
|
||||
|
||||
it("detects terminal from auto-mode stopped notification", async () => {
|
||||
it("detects terminal from autonomous mode stopped notification", async () => {
|
||||
const { manager } = createManager();
|
||||
|
||||
const sessionId = await manager.startSession({
|
||||
|
|
@ -658,7 +658,7 @@ describe("SessionManager", () => {
|
|||
type: "extension_ui_request",
|
||||
id: "n1",
|
||||
method: "notify",
|
||||
message: "Step-mode stopped: user requested",
|
||||
message: "Assisted mode stopped: user requested",
|
||||
});
|
||||
|
||||
assert.equal(session.status, "completed");
|
||||
|
|
@ -763,7 +763,7 @@ describe("SessionManager", () => {
|
|||
type: "extension_ui_request",
|
||||
id: "n1",
|
||||
method: "notify",
|
||||
message: "Auto-mode stopped: success",
|
||||
message: "Autonomous mode stopped: success",
|
||||
});
|
||||
|
||||
assert.ok(emittedData);
|
||||
|
|
@ -935,7 +935,7 @@ describe("SessionManager", () => {
|
|||
type: "extension_ui_request",
|
||||
id: "bn-1",
|
||||
method: "notify",
|
||||
message: "Auto-mode stopped: Blocked: waiting for approval",
|
||||
message: "Autonomous mode stopped: Blocked: waiting for approval",
|
||||
});
|
||||
|
||||
assert.equal(session.status, "blocked");
|
||||
|
|
|
|||
|
|
@ -43,10 +43,9 @@ const FIRE_AND_FORGET_METHODS = new Set([
|
|||
|
||||
const TERMINAL_PREFIXES = [
|
||||
"autonomous mode stopped",
|
||||
"auto-mode stopped",
|
||||
"autonomous mode paused",
|
||||
"auto-mode paused",
|
||||
"step-mode stopped",
|
||||
"assisted mode stopped",
|
||||
"assisted mode paused",
|
||||
];
|
||||
const RELOAD_PAUSE_TIMEOUT_MS = 5_000;
|
||||
|
||||
|
|
@ -62,7 +61,9 @@ function isBlockedNotification(event: Record<string, unknown>): boolean {
|
|||
return false;
|
||||
const message = String(event.message ?? "").toLowerCase();
|
||||
return (
|
||||
message.includes("blocked:") || message.startsWith("autonomous mode paused")
|
||||
message.includes("blocked:") ||
|
||||
message.startsWith("autonomous mode paused") ||
|
||||
message.startsWith("assisted mode paused")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -434,7 +435,7 @@ export class SessionManager extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// Terminal detection — autonomous mode/step-mode stopped
|
||||
// Terminal detection — autonomous mode/assisted mode stopped
|
||||
if (isTerminalNotification(event as Record<string, unknown>)) {
|
||||
if (isBlockedNotification(event as Record<string, unknown>)) {
|
||||
session.status = "blocked";
|
||||
|
|
|
|||
|
|
@ -1795,7 +1795,7 @@ export class AgentSession {
|
|||
} finally {
|
||||
this._sessionSwitchPending = false;
|
||||
}
|
||||
// Update cwd to current process directory — auto-mode may have chdir'd
|
||||
// Update cwd to current process directory — autonomous mode may have chdir'd
|
||||
// into a worktree since the original session was created.
|
||||
const previousCwd = this._cwd;
|
||||
this._cwd = process.cwd();
|
||||
|
|
@ -1807,7 +1807,7 @@ export class AgentSession {
|
|||
|
||||
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
||||
|
||||
// Rebuild tools when cwd changed (e.g., auto-mode entered a worktree).
|
||||
// Rebuild tools when cwd changed (e.g., autonomous mode entered a worktree).
|
||||
// Tools capture cwd at creation time for path resolution — without
|
||||
// rebuilding, write/read/edit/bash resolve relative paths against
|
||||
// the original project root instead of the worktree (#633).
|
||||
|
|
@ -1821,7 +1821,7 @@ export class AgentSession {
|
|||
// Extensions (e.g., discuss flows) may narrow the active tool list
|
||||
// via setActiveTools() during a session. Without this refresh, the
|
||||
// narrowed set persists into the next session — causing tools like
|
||||
// sf_plan_slice to be missing from auto-mode subagent sessions.
|
||||
// sf_plan_slice to be missing from autonomous mode subagent sessions.
|
||||
this._refreshToolRegistry({
|
||||
activeToolNames: this.getActiveToolNames(),
|
||||
includeAllExtensionTools: true,
|
||||
|
|
|
|||
|
|
@ -564,7 +564,7 @@ async function getOrCreateClientOnce(
|
|||
});
|
||||
|
||||
// Handle spawn failure (e.g., ENOENT when the command doesn't exist).
|
||||
// Without this, the error bubbles up and can crash auto-mode (#901).
|
||||
// Without this, the error bubbles up and can crash autonomous mode (#901).
|
||||
proc.on("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "ENOENT") {
|
||||
proc.emit("exit", 1);
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ describe("ModelRegistry — public discovery providers", () => {
|
|||
const registry = new ModelRegistry(
|
||||
AuthStorage.inMemory({}),
|
||||
undefined,
|
||||
undefined,
|
||||
new ModelDiscoveryCache(join(testDir, "ollama-cloud-cache.json")),
|
||||
);
|
||||
const results = await registry.discoverModels(["ollama-cloud"]);
|
||||
|
|
@ -173,6 +174,7 @@ describe("ModelRegistry — public discovery providers", () => {
|
|||
"ollama-cloud": { type: "api_key", key: "ollama-test" },
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
new ModelDiscoveryCache(join(testDir, "ollama-cloud-auth-cache.json")),
|
||||
);
|
||||
await registry.discoverModels(["ollama-cloud"]);
|
||||
|
|
@ -213,6 +215,7 @@ describe("ModelRegistry — public discovery providers", () => {
|
|||
zai: { type: "api_key", key: "zai-test" },
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
new ModelDiscoveryCache(join(testDir, "zai-cache.json")),
|
||||
);
|
||||
const results = await registry.discoverModels(["zai"]);
|
||||
|
|
@ -269,6 +272,7 @@ describe("ModelRegistry — public discovery providers", () => {
|
|||
const registry = new ModelRegistry(
|
||||
AuthStorage.inMemory({}),
|
||||
undefined,
|
||||
undefined,
|
||||
new ModelDiscoveryCache(join(testDir, "memory-cache.json")),
|
||||
);
|
||||
const results = await registry.discoverModelCatalogs([
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export class RetryHandler {
|
|||
if (isRateLimit || isQuotaError || isAuthError) {
|
||||
// For quota errors with a retry-after hint, wait before switching providers.
|
||||
// Only wait if the reset is very short (< 5s); otherwise falling back to
|
||||
// another provider is faster and keeps auto-mode throughput up.
|
||||
// another provider is faster and keeps autonomous mode throughput up.
|
||||
const QUOTA_WAIT_THRESHOLD_MS = 5_000;
|
||||
if (
|
||||
isQuotaError &&
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ function parseTopLevelCatalogCommands() {
|
|||
function parseHandledTopLevelCommands() {
|
||||
const handlerFiles = [
|
||||
"core.js",
|
||||
"auto.js",
|
||||
"autonomous.js",
|
||||
"parallel.js",
|
||||
"workflow.js",
|
||||
"ops.js",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# recover-sf-1364.sh — Recovery script for issue #1364 (Linux / macOS)
|
||||
#
|
||||
# For Windows use the PowerShell equivalent:
|
||||
# powershell -ExecutionPolicy Bypass -File scripts\recover-sf-1364.ps1 [-DryRun]
|
||||
#
|
||||
# CRITICAL DATA-LOSS BUG: SF versions 2.30.0–2.35.x unconditionally added
|
||||
# ".sf" to .gitignore via ensureGitignore(), causing git to report all
|
||||
# tracked .sf/ files as deleted. Fixed in v2.36.0 (PR #1367).
|
||||
|
|
|
|||
|
|
@ -1,446 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# recover-sf-1668.sh — Recovery script for issue #1668 (Linux / macOS)
|
||||
#
|
||||
# SF v2.39.x deleted the milestone branch and worktree directory when a
|
||||
# merge failed due to the repo using `master` as its default branch (not
|
||||
# `main`). The commits were never merged — they are orphaned in the git
|
||||
# object store and can be recovered via git reflog or git fsck.
|
||||
#
|
||||
# This script:
|
||||
# 1. Searches git reflog for the deleted milestone branch (fastest path)
|
||||
# 2. Falls back to git fsck --unreachable to find orphaned commits
|
||||
# 3. Ranks candidates by recency and SF commit message patterns
|
||||
# 4. Creates a recovery branch at the identified commit
|
||||
# 5. Reports what was found and how to complete the merge manually
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/recover-sf-1668.sh [--milestone <ID>] [--dry-run] [--auto]
|
||||
#
|
||||
# Options:
|
||||
# --milestone <ID> SF milestone ID (e.g. M001-g2nalq).
|
||||
# When omitted the script scans all recent orphans.
|
||||
# --dry-run Show what would be done without making any changes.
|
||||
# --auto Pick the best candidate automatically (no prompts).
|
||||
#
|
||||
# Requirements: git >= 2.23, bash >= 4.x
|
||||
#
|
||||
# Affected versions: SF.39.x
|
||||
# Fixed in: SF.40.1 (PR #1669)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Colours ──────────────────────────────────────────────────────────────────
|
||||
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
RESET='\033[0m'
|
||||
|
||||
# ─── Args ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
DRY_RUN=false
|
||||
AUTO=false
|
||||
MILESTONE_ID=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--auto) AUTO=true; shift ;;
|
||||
--milestone)
|
||||
[[ $# -lt 2 ]] && { echo "Error: --milestone requires an argument" >&2; exit 1; }
|
||||
MILESTONE_ID="$2"; shift 2 ;;
|
||||
--milestone=*)
|
||||
MILESTONE_ID="${1#--milestone=}"; shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,/^set -/p' "$0" | grep '^#' | sed 's/^# \{0,1\}//'
|
||||
exit 0 ;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
echo "Usage: $0 [--milestone <ID>] [--dry-run] [--auto]" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
info() { echo -e "${CYAN}[info]${RESET} $*"; }
|
||||
ok() { echo -e "${GREEN}[ok]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[warn]${RESET} $*"; }
|
||||
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
|
||||
section() { echo -e "\n${BOLD}$*${RESET}"; }
|
||||
dim() { echo -e "${DIM}$*${RESET}"; }
|
||||
|
||||
die() {
|
||||
error "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
run() {
|
||||
if $DRY_RUN; then
|
||||
echo -e " ${YELLOW}(dry-run)${RESET} $*"
|
||||
else
|
||||
eval "$*"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Preflight ────────────────────────────────────────────────────────────────
|
||||
|
||||
section "── Preflight ───────────────────────────────────────────────────────────"
|
||||
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
die "Not inside a git repository. Run this from your project root."
|
||||
fi
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
cd "$REPO_ROOT"
|
||||
info "Repo root: $REPO_ROOT"
|
||||
|
||||
$DRY_RUN && warn "DRY-RUN mode — no changes will be made."
|
||||
|
||||
# ─── Step 1: Confirm the milestone branch is gone ─────────────────────────────
|
||||
|
||||
section "── Step 1: Verify milestone branch is missing ───────────────────────────"
|
||||
|
||||
BRANCH_PATTERN="milestone/"
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
BRANCH_PATTERN="milestone/${MILESTONE_ID}"
|
||||
fi
|
||||
|
||||
LIVE_BRANCHES="$(git branch | grep "$BRANCH_PATTERN" 2>/dev/null | tr -d '* ' || true)"
|
||||
|
||||
if [[ -n "$LIVE_BRANCHES" ]]; then
|
||||
ok "Found live milestone branch(es):"
|
||||
echo "$LIVE_BRANCHES" | while IFS= read -r b; do echo " $b"; done
|
||||
echo ""
|
||||
warn "The branch still exists — are you sure it was lost?"
|
||||
echo " If you want to check out existing work: git checkout ${LIVE_BRANCHES%%$'\n'*}"
|
||||
echo " To merge it manually: git checkout master && git merge --squash ${LIVE_BRANCHES%%$'\n'*}"
|
||||
echo ""
|
||||
echo "Re-run with --milestone <ID> to force scanning for a specific orphaned commit."
|
||||
if [[ -z "$MILESTONE_ID" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$MILESTONE_ID" && -n "$LIVE_BRANCHES" ]]; then
|
||||
warn "Milestone branch milestone/${MILESTONE_ID} is still live — continuing scan anyway."
|
||||
elif [[ -n "$MILESTONE_ID" ]]; then
|
||||
info "Confirmed: milestone/${MILESTONE_ID} branch is gone."
|
||||
else
|
||||
info "No live milestone/ branches found — scanning for orphaned commits."
|
||||
fi
|
||||
|
||||
# ─── Step 2: Search git reflog (fastest, most reliable) ───────────────────────
|
||||
|
||||
section "── Step 2: Search git reflog for deleted branch ────────────────────────"
|
||||
|
||||
# git reflog stores branch moves and deletions in .git/logs/refs/heads/
|
||||
# It is retained for 90 days by default (gc.reflogExpire).
|
||||
REFLOG_FOUND_SHA=""
|
||||
REFLOG_FOUND_BRANCH=""
|
||||
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
REFLOG_PATH="${REPO_ROOT}/.git/logs/refs/heads/milestone/${MILESTONE_ID}"
|
||||
if [[ -f "$REFLOG_PATH" ]]; then
|
||||
# Last line of the reflog for this branch is the most recent tip
|
||||
REFLOG_FOUND_SHA="$(tail -1 "$REFLOG_PATH" | awk '{print $2}')"
|
||||
REFLOG_FOUND_BRANCH="milestone/${MILESTONE_ID}"
|
||||
ok "Reflog entry found for milestone/${MILESTONE_ID} — commit: ${REFLOG_FOUND_SHA:0:12}"
|
||||
else
|
||||
info "No reflog file at .git/logs/refs/heads/milestone/${MILESTONE_ID}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also try git reflog (in-memory index, works without the raw file)
|
||||
if [[ -z "$REFLOG_FOUND_SHA" ]]; then
|
||||
info "Scanning git reflog for milestone/ commits..."
|
||||
REFLOG_MILESTONES="$(git reflog --all --format="%H %gs" 2>/dev/null \
|
||||
| grep -E "(checkout|commit|merge).*milestone/" \
|
||||
| head -20 || true)"
|
||||
|
||||
if [[ -n "$REFLOG_MILESTONES" ]]; then
|
||||
info "Found milestone-related reflog entries:"
|
||||
echo "$REFLOG_MILESTONES" | while IFS= read -r line; do
|
||||
dim " $line"
|
||||
done
|
||||
# Extract the most recent SHA from the most relevant entry
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
MATCH="$(echo "$REFLOG_MILESTONES" | grep "milestone/${MILESTONE_ID}" | head -1 || true)"
|
||||
else
|
||||
MATCH="$(echo "$REFLOG_MILESTONES" | head -1 || true)"
|
||||
fi
|
||||
if [[ -n "$MATCH" ]]; then
|
||||
REFLOG_FOUND_SHA="$(echo "$MATCH" | awk '{print $1}')"
|
||||
REFLOG_FOUND_BRANCH="$(echo "$MATCH" | grep -oE 'milestone/[^ ]+' | head -1 || echo "milestone/unknown")"
|
||||
fi
|
||||
else
|
||||
info "No milestone/ entries in reflog."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── Step 3: Fall back to git fsck if reflog didn't find it ───────────────────
|
||||
|
||||
section "── Step 3: Scan for orphaned (unreachable) commits ───────────────────"
|
||||
|
||||
FSCK_CANDIDATES=()
|
||||
FSCK_CANDIDATE_MSGS=()
|
||||
FSCK_CANDIDATE_DATES=()
|
||||
FSCK_CANDIDATE_FILES=()
|
||||
|
||||
if [[ -z "$REFLOG_FOUND_SHA" ]]; then
|
||||
info "Running git fsck --unreachable (this may take a moment)..."
|
||||
|
||||
# Collect all unreachable commit hashes
|
||||
UNREACHABLE_COMMITS="$(git fsck --unreachable --no-reflogs 2>/dev/null \
|
||||
| grep '^unreachable commit' \
|
||||
| awk '{print $3}' || true)"
|
||||
|
||||
if [[ -z "$UNREACHABLE_COMMITS" ]]; then
|
||||
# Try without --no-reflogs as a fallback (less conservative)
|
||||
UNREACHABLE_COMMITS="$(git fsck --unreachable 2>/dev/null \
|
||||
| grep '^unreachable commit' \
|
||||
| awk '{print $3}' || true)"
|
||||
fi
|
||||
|
||||
TOTAL="$(echo "$UNREACHABLE_COMMITS" | grep -c . || true)"
|
||||
info "Found ${TOTAL} unreachable commit object(s)."
|
||||
|
||||
if [[ -z "$UNREACHABLE_COMMITS" || "$TOTAL" -eq 0 ]]; then
|
||||
error "No unreachable commits found."
|
||||
echo ""
|
||||
echo "This means one of:"
|
||||
echo " 1. git gc has already been run and the objects were pruned"
|
||||
echo " (objects are pruned after 14 days by default)"
|
||||
echo " 2. The commits were never written to the object store"
|
||||
echo " 3. The wrong repository is being scanned"
|
||||
echo ""
|
||||
echo "If git gc ran, the objects may be unrecoverable without a backup."
|
||||
echo "Try: git reflog --all | grep milestone"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Score each unreachable commit — rank by recency and SF message patterns.
|
||||
# SF milestone commits look like: "feat(M001-g2nalq): <title>"
|
||||
# Slice merges look like: "feat(M001-g2nalq/S01): <slice>"
|
||||
#
|
||||
# Performance: use a single `git log --no-walk=unsorted --stdin` call to
|
||||
# read all commit metadata in one pass instead of one `git show` per commit.
|
||||
CUTOFF="$(date -d '30 days ago' '+%s' 2>/dev/null || date -v-30d '+%s' 2>/dev/null || echo 0)"
|
||||
WEEK_AGO="$(date -d '7 days ago' '+%s' 2>/dev/null || date -v-7d '+%s' 2>/dev/null || echo 0)"
|
||||
|
||||
# Batch-read all commits: output format per commit is:
|
||||
# HASH<TAB>UNIX_TIMESTAMP<TAB>ISO_DATE<TAB>SUBJECT
|
||||
# separated by NUL so multi-line subjects don't break parsing.
|
||||
BATCH_LOG="$(echo "$UNREACHABLE_COMMITS" \
|
||||
| git log --no-walk=unsorted --stdin --format=$'%H\t%ct\t%ci\t%s' 2>/dev/null || true)"
|
||||
|
||||
while IFS=$'\t' read -r sha commit_ts commit_date_hr commit_msg; do
|
||||
[[ -z "$sha" ]] && continue
|
||||
[[ -z "$commit_ts" || "$commit_ts" -lt "$CUTOFF" ]] && continue
|
||||
|
||||
# Score: milestone pattern in subject is highest signal
|
||||
SCORE=0
|
||||
if [[ -n "$MILESTONE_ID" ]] && echo "$commit_msg" | grep -qiE "(milestone[/ ])?${MILESTONE_ID}"; then
|
||||
SCORE=$((SCORE + 100))
|
||||
fi
|
||||
if echo "$commit_msg" | grep -qE '^feat\([A-Z][0-9]+'; then
|
||||
SCORE=$((SCORE + 50))
|
||||
fi
|
||||
if echo "$commit_msg" | grep -qiE 'milestone/|complete-milestone|SF|slice'; then
|
||||
SCORE=$((SCORE + 20))
|
||||
fi
|
||||
if [[ "$commit_ts" -gt "$WEEK_AGO" ]]; then
|
||||
SCORE=$((SCORE + 10))
|
||||
fi
|
||||
|
||||
FSCK_CANDIDATES+=("$sha|$SCORE")
|
||||
FSCK_CANDIDATE_MSGS+=("$commit_msg")
|
||||
FSCK_CANDIDATE_DATES+=("$commit_date_hr")
|
||||
FSCK_CANDIDATE_FILES+=("?")
|
||||
done <<< "$BATCH_LOG"
|
||||
|
||||
if [[ ${#FSCK_CANDIDATES[@]} -eq 0 ]]; then
|
||||
error "No recent unreachable commits found within the last 30 days."
|
||||
echo ""
|
||||
echo "Objects may have been pruned by git gc, or the issue occurred more than 30 days ago."
|
||||
echo "Try: git fsck --unreachable --no-reflogs 2>/dev/null | grep commit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sort by score descending, keep top 10
|
||||
IFS=$'\n' SORTED_CANDIDATES=($(
|
||||
for i in "${!FSCK_CANDIDATES[@]}"; do
|
||||
echo "${FSCK_CANDIDATES[$i]}|$i"
|
||||
done | sort -t'|' -k2 -rn | head -10
|
||||
))
|
||||
unset IFS
|
||||
|
||||
info "Top candidates (scored by recency and SF message patterns):"
|
||||
echo ""
|
||||
NUM=1
|
||||
SORTED_IDXS=()
|
||||
for entry in "${SORTED_CANDIDATES[@]}"; do
|
||||
SHA="${entry%%|*}"
|
||||
IDX="${entry##*|}"
|
||||
SORTED_IDXS+=("$IDX")
|
||||
MSG="${FSCK_CANDIDATE_MSGS[$IDX]}"
|
||||
DATE="${FSCK_CANDIDATE_DATES[$IDX]}"
|
||||
FILES="${FSCK_CANDIDATE_FILES[$IDX]}"
|
||||
echo -e " ${BOLD}${NUM})${RESET} ${sha:0:12} ${GREEN}${MSG}${RESET}"
|
||||
echo -e " ${DIM}${DATE} — ${FILES}${RESET}"
|
||||
NUM=$((NUM + 1))
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ─── Step 4: Select the recovery commit ───────────────────────────────────────
|
||||
|
||||
section "── Step 4: Select recovery commit ──────────────────────────────────────"
|
||||
|
||||
RECOVERY_SHA=""
|
||||
RECOVERY_SOURCE=""
|
||||
|
||||
if [[ -n "$REFLOG_FOUND_SHA" ]]; then
|
||||
RECOVERY_SHA="$REFLOG_FOUND_SHA"
|
||||
RECOVERY_SOURCE="reflog (${REFLOG_FOUND_BRANCH})"
|
||||
info "Using reflog candidate: ${RECOVERY_SHA:0:12}"
|
||||
MSG="$(git show -s --format="%s %ci" "$RECOVERY_SHA" 2>/dev/null || echo "unknown")"
|
||||
dim " $MSG"
|
||||
|
||||
elif [[ ${#SORTED_IDXS[@]} -eq 1 ]] || $AUTO; then
|
||||
# Auto-select first (highest scored) candidate
|
||||
FIRST_ENTRY="${SORTED_CANDIDATES[0]}"
|
||||
FIRST_SHA="${FIRST_ENTRY%%|*}"
|
||||
FIRST_IDX="${FIRST_ENTRY##*|}"
|
||||
RECOVERY_SHA="$FIRST_SHA"
|
||||
RECOVERY_SOURCE="fsck (auto-selected)"
|
||||
info "Auto-selecting best candidate: ${RECOVERY_SHA:0:12}"
|
||||
|
||||
else
|
||||
# Prompt user to select
|
||||
echo -n "Select a candidate to recover [1-${#SORTED_CANDIDATES[@]}, or q to quit]: "
|
||||
read -r SELECTION
|
||||
|
||||
if [[ "$SELECTION" == "q" ]]; then
|
||||
info "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! [[ "$SELECTION" =~ ^[0-9]+$ ]] || \
|
||||
[[ "$SELECTION" -lt 1 ]] || \
|
||||
[[ "$SELECTION" -gt ${#SORTED_CANDIDATES[@]} ]]; then
|
||||
die "Invalid selection: $SELECTION"
|
||||
fi
|
||||
|
||||
SEL_IDX=$((SELECTION - 1))
|
||||
SEL_ENTRY="${SORTED_CANDIDATES[$SEL_IDX]}"
|
||||
RECOVERY_SHA="${SEL_ENTRY%%|*}"
|
||||
RECOVERY_SOURCE="fsck (user-selected #${SELECTION})"
|
||||
fi
|
||||
|
||||
if [[ -z "$RECOVERY_SHA" ]]; then
|
||||
die "Could not determine a recovery commit. See output above."
|
||||
fi
|
||||
|
||||
ok "Recovery commit: ${RECOVERY_SHA:0:16} (source: ${RECOVERY_SOURCE})"
|
||||
|
||||
# Show what's in this commit
|
||||
echo ""
|
||||
info "Commit details:"
|
||||
git show -s --format=" Message: %s%n Author: %an <%ae>%n Date: %ci%n Full SHA: %H" "$RECOVERY_SHA"
|
||||
echo ""
|
||||
info "Files at this commit (first 30):"
|
||||
git show --stat --format="" "$RECOVERY_SHA" 2>/dev/null | head -30
|
||||
echo ""
|
||||
|
||||
# ─── Step 5: Create recovery branch ───────────────────────────────────────────
|
||||
|
||||
section "── Step 5: Create recovery branch ──────────────────────────────────────"
|
||||
|
||||
# Determine recovery branch name
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
RECOVERY_BRANCH="recovery/1668/${MILESTONE_ID}"
|
||||
elif [[ -n "$REFLOG_FOUND_BRANCH" ]]; then
|
||||
CLEAN_NAME="${REFLOG_FOUND_BRANCH//\//-}"
|
||||
RECOVERY_BRANCH="recovery/1668/${CLEAN_NAME}"
|
||||
else
|
||||
SHORT_SHA="${RECOVERY_SHA:0:8}"
|
||||
RECOVERY_BRANCH="recovery/1668/commit-${SHORT_SHA}"
|
||||
fi
|
||||
|
||||
# Check if it already exists
|
||||
if git show-ref --verify --quiet "refs/heads/${RECOVERY_BRANCH}" 2>/dev/null; then
|
||||
warn "Branch ${RECOVERY_BRANCH} already exists."
|
||||
if ! $AUTO; then
|
||||
echo -n "Overwrite it? [y/N]: "
|
||||
read -r ANSWER
|
||||
if [[ "$ANSWER" != "y" && "$ANSWER" != "Y" ]]; then
|
||||
info "Aborted. Existing branch preserved."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
run "git branch -D \"${RECOVERY_BRANCH}\""
|
||||
fi
|
||||
|
||||
run "git branch \"${RECOVERY_BRANCH}\" \"${RECOVERY_SHA}\""
|
||||
|
||||
if ! $DRY_RUN; then
|
||||
ok "Recovery branch created: ${RECOVERY_BRANCH}"
|
||||
else
|
||||
ok "(dry-run) Would create branch: ${RECOVERY_BRANCH} → ${RECOVERY_SHA:0:12}"
|
||||
fi
|
||||
|
||||
# ─── Step 6: Verify the recovery branch ───────────────────────────────────────
|
||||
|
||||
if ! $DRY_RUN; then
|
||||
section "── Step 6: Verify recovery branch ──────────────────────────────────────"
|
||||
|
||||
FILE_LIST="$(git ls-tree -r --name-only "${RECOVERY_BRANCH}" 2>/dev/null | grep -v '^\.sf/' || true)"
|
||||
FILE_COUNT="$(echo "$FILE_LIST" | grep -c . || true)"
|
||||
|
||||
info "Files recoverable (excluding .sf/ state files): ${FILE_COUNT}"
|
||||
echo "$FILE_LIST" | head -30 | while IFS= read -r f; do echo " $f"; done
|
||||
if [[ "$FILE_COUNT" -gt 30 ]]; then
|
||||
dim " ... and $((FILE_COUNT - 30)) more"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── Summary ──────────────────────────────────────────────────────────────────
|
||||
|
||||
section "── Recovery Summary ─────────────────────────────────────────────────────"
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo -e "${YELLOW}Dry-run complete. Re-run without --dry-run to apply.${RESET}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DEFAULT_BRANCH="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' \
|
||||
|| git for-each-ref --format='%(refname:short)' 'refs/heads/main' 'refs/heads/master' 2>/dev/null | head -1 \
|
||||
|| git branch --show-current)"
|
||||
|
||||
echo -e "${GREEN}Recovery branch ready: ${BOLD}${RECOVERY_BRANCH}${RESET}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo -e " ${BOLD}1. Inspect the recovered files:${RESET}"
|
||||
echo " git checkout ${RECOVERY_BRANCH}"
|
||||
echo " ls -la"
|
||||
echo ""
|
||||
echo -e " ${BOLD}2. Verify your code is intact:${RESET}"
|
||||
echo " git log --oneline ${RECOVERY_BRANCH} | head -20"
|
||||
echo " git show --stat ${RECOVERY_BRANCH}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}3. Merge to your default branch (${DEFAULT_BRANCH}):${RESET}"
|
||||
echo " git checkout ${DEFAULT_BRANCH}"
|
||||
echo " git merge --squash ${RECOVERY_BRANCH}"
|
||||
echo " git commit -m \"feat: recover milestone from #1668\""
|
||||
echo ""
|
||||
echo -e " ${BOLD}4. Clean up after verifying:${RESET}"
|
||||
echo " git branch -D ${RECOVERY_BRANCH}"
|
||||
echo ""
|
||||
echo -e "${DIM}Note: update SF to v2.40.1+ to prevent this from recurring.${RESET}"
|
||||
echo " PR: https://github.com/singularity-forge/sf-run/pull/1669"
|
||||
echo ""
|
||||
|
|
@ -711,7 +711,7 @@ if (cliFlags.listModels !== undefined) {
|
|||
const searchPattern =
|
||||
typeof cliFlags.listModels === "string" ? cliFlags.listModels : undefined;
|
||||
// Apply allowed_providers / blocked_providers from SF preferences so the
|
||||
// listing matches what auto-mode would actually be willing to dispatch.
|
||||
// listing matches what autonomous mode would actually be willing to dispatch.
|
||||
const sfPrefs = loadEffectiveSFPreferences()?.preferences as
|
||||
| {
|
||||
allowed_providers?: string[];
|
||||
|
|
|
|||
|
|
@ -95,13 +95,13 @@ export function shouldRestartHeadlessRun(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detect genuine auto-mode termination notifications.
|
||||
* Detect genuine autonomous mode termination notifications.
|
||||
*
|
||||
* Only matches the actual stop/pause signals emitted by stopAuto()/pauseAuto():
|
||||
* "Auto-mode stopped..."
|
||||
* "Step-mode stopped..."
|
||||
* "Auto-mode paused..."
|
||||
* "Step-mode paused..."
|
||||
* "Autonomous mode stopped..."
|
||||
* "Assisted mode stopped..."
|
||||
* "Autonomous mode paused..."
|
||||
* "Assisted mode paused..."
|
||||
*
|
||||
* Does NOT match progress notifications that happen to contain words like
|
||||
* "complete" or "stopped" (e.g., "Override resolved — rewrite-docs completed",
|
||||
|
|
@ -110,10 +110,10 @@ export function shouldRestartHeadlessRun(
|
|||
* Blocked detection is separate — checked via isBlockedNotification.
|
||||
*/
|
||||
export const TERMINAL_PREFIXES = [
|
||||
"auto-mode stopped",
|
||||
"step-mode stopped",
|
||||
"auto-mode paused",
|
||||
"step-mode paused",
|
||||
"autonomous mode stopped",
|
||||
"assisted mode stopped",
|
||||
"autonomous mode paused",
|
||||
"assisted mode paused",
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -132,8 +132,8 @@ export const NEW_MILESTONE_IDLE_TIMEOUT_MS = 120_000;
|
|||
/**
|
||||
* Deadlock backstop for long-running multi-turn commands (auto, next,
|
||||
* discuss, plan). The role here is NOT idle-detection ("are we done?") —
|
||||
* those commands signal completion explicitly via "auto-mode stopped" /
|
||||
* "step-mode stopped" terminal notifications, and the agent's child-process
|
||||
* those commands signal completion explicitly via "autonomous mode stopped" /
|
||||
* "assisted mode stopped" terminal notifications, and the agent's child-process
|
||||
* exit catches crashes. The only remaining failure mode is a truly hung
|
||||
* process (deadlock, network stuck without retry, infinite reasoning loop
|
||||
* outside the LLM's awareness). 30 minutes is long enough to never misfire
|
||||
|
|
@ -171,7 +171,7 @@ function getEventMetadata(
|
|||
}
|
||||
|
||||
/**
|
||||
* Detect genuine auto-mode or step-mode termination signals. Checks structured
|
||||
* Detect genuine autonomous mode or assisted mode termination signals. Checks structured
|
||||
* metadata first, then falls back to legacy text-matching heuristics.
|
||||
*/
|
||||
export function isTerminalNotification(
|
||||
|
|
@ -196,8 +196,8 @@ export function isPauseNotification(event: Record<string, unknown>): boolean {
|
|||
// Fallback: legacy text heuristics.
|
||||
const message = String(event.message ?? "").toLowerCase();
|
||||
return (
|
||||
message.startsWith("auto-mode paused") ||
|
||||
message.startsWith("step-mode paused")
|
||||
message.startsWith("autonomous mode paused") ||
|
||||
message.startsWith("assisted mode paused")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* Output: { schemaVersion, state, next, cost }
|
||||
* schemaVersion — output contract version
|
||||
* state — deriveState() output (phase, milestones, progress, blockers)
|
||||
* next — dry-run dispatch preview (what auto-mode would do next)
|
||||
* next — dry-run dispatch preview (what autonomous mode would do next)
|
||||
* cost — aggregated parallel worker costs
|
||||
*
|
||||
* Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
|
||||
|
|
@ -27,7 +27,7 @@ const jiti = createJiti(import.meta.filename, {
|
|||
debug: false,
|
||||
});
|
||||
// Resolve extensions from the synced agent directory so headless-query
|
||||
// loads the same extension copy as interactive/auto modes (#3471).
|
||||
// loads the same extension copy as interactive/autonomous modes (#3471).
|
||||
// The synced runtime is compiled .js; source-tree fallback is .ts.
|
||||
const agentExtensionsDir = join(getSfEnv().agentDir, "extensions", "sf");
|
||||
const useAgentDir = existsSync(join(agentExtensionsDir, "state.js"));
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ export function handleExtensionUIRequest(
|
|||
// to proceed. Detect by title and pick the force option.
|
||||
const title = String(event.title ?? "");
|
||||
let selected = event.options?.[0] ?? "";
|
||||
if (title.includes("Auto-mode is running") && event.options) {
|
||||
if (title.includes("Autonomous mode is running") && event.options) {
|
||||
const forceOption = event.options.find((o) =>
|
||||
o.toLowerCase().includes("force start"),
|
||||
);
|
||||
|
|
@ -556,7 +556,7 @@ export function formatCostLine(
|
|||
/**
|
||||
* Format a periodic liveness line for headless runs.
|
||||
*
|
||||
* Purpose: make long model calls and quiet auto-mode phases observable without
|
||||
* Purpose: make long model calls and quiet autonomous mode phases observable without
|
||||
* changing machine-readable JSON output.
|
||||
*/
|
||||
export function formatHeadlessHeartbeat(ctx: HeadlessHeartbeatContext): string {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ import {
|
|||
summarizeToolArgs,
|
||||
} from "./headless-ui.js";
|
||||
import { getProjectSessionsDir } from "./project-sessions.js";
|
||||
import {
|
||||
findUnsupportedAutonomousArgs,
|
||||
formatUnsupportedAutonomousArgs,
|
||||
} from "./resources/extensions/sf/autonomous-command-args.js";
|
||||
import {
|
||||
ensureSfSymlink,
|
||||
externalSfRoot,
|
||||
|
|
@ -187,7 +191,7 @@ export interface HeadlessOptions {
|
|||
commandArgs: string[];
|
||||
context?: string; // file path or '-' for stdin
|
||||
contextText?: string; // inline text
|
||||
auto?: boolean; // chain into autonomous mode after milestone creation
|
||||
chainAutonomous?: boolean; // chain into autonomous mode after milestone creation
|
||||
verbose?: boolean; // show tool calls in output
|
||||
maxRestarts?: number; // auto-restart on crash (default 3, 0 to disable)
|
||||
supervised?: boolean; // supervised mode: forward interactive requests to orchestrator
|
||||
|
|
@ -408,8 +412,13 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
options.context = args[++i];
|
||||
} else if (arg === "--context-text" && i + 1 < args.length) {
|
||||
options.contextText = args[++i];
|
||||
} else if (arg === "--autonomous") {
|
||||
options.chainAutonomous = true;
|
||||
} else if (arg === "--auto") {
|
||||
options.auto = true;
|
||||
process.stderr.write(
|
||||
"[headless] Error: --auto was removed. Use --autonomous to chain into autonomous mode.\n",
|
||||
);
|
||||
process.exit(1);
|
||||
} else if (arg === "--verbose") {
|
||||
options.verbose = true;
|
||||
} else if (arg === "--max-restarts" && i + 1 < args.length) {
|
||||
|
|
@ -457,7 +466,7 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
} else if (!commandSeen) {
|
||||
if (arg === "autonomous") {
|
||||
options.command = "autonomous";
|
||||
options.auto = true; // autonomous subcommand implies --auto
|
||||
options.chainAutonomous = true;
|
||||
} else {
|
||||
options.command = arg;
|
||||
}
|
||||
|
|
@ -565,6 +574,37 @@ async function runHeadlessOnce(
|
|||
const headlessRunId = `headless-${new Date(startTime).toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
||||
const requestedCommand = options.command;
|
||||
const requestedCommandArgs = [...options.commandArgs];
|
||||
if (options.command === "autonomous") {
|
||||
const unsupportedArgs = findUnsupportedAutonomousArgs(options.commandArgs);
|
||||
if (unsupportedArgs.length > 0) {
|
||||
process.stderr.write(
|
||||
`[headless] ${formatUnsupportedAutonomousArgs(unsupportedArgs)}\n`,
|
||||
);
|
||||
if (options.outputFormat === "json") {
|
||||
const result: HeadlessJsonResult = {
|
||||
schemaVersion: 1,
|
||||
status: "error",
|
||||
exitCode: EXIT_ERROR,
|
||||
duration: Date.now() - startTime,
|
||||
cost: {
|
||||
total: 0,
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
},
|
||||
toolCalls: 0,
|
||||
events: 0,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
}
|
||||
return {
|
||||
exitCode: EXIT_ERROR,
|
||||
interrupted: false,
|
||||
timedOut: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (options.command === "help") {
|
||||
const { printSubcommandHelp } = await import("./help-text.js");
|
||||
printSubcommandHelp("headless", process.env.SF_VERSION || "0.0.0");
|
||||
|
|
@ -579,7 +619,7 @@ async function runHeadlessOnce(
|
|||
);
|
||||
}
|
||||
options.command = "new-milestone";
|
||||
options.auto = true;
|
||||
options.chainAutonomous = true;
|
||||
options.contextText = buildAutoBootstrapContext(process.cwd());
|
||||
}
|
||||
}
|
||||
|
|
@ -591,11 +631,11 @@ async function runHeadlessOnce(
|
|||
options.timeout = 600_000; // 10 minutes
|
||||
}
|
||||
|
||||
// auto-mode sessions are long-running (minutes to hours) with their own internal
|
||||
// autonomous mode sessions are long-running (minutes to hours) with their own internal
|
||||
// per-unit timeout via auto-supervisor. Disable the overall timeout unless the
|
||||
// user explicitly set --timeout.
|
||||
const isAutoMode = options.command === "autonomous";
|
||||
const wasRequestedAutoMode = requestedCommand === "autonomous";
|
||||
const isAutonomousCommand = options.command === "autonomous";
|
||||
const wasRequestedAutonomousCommand = requestedCommand === "autonomous";
|
||||
// discuss and plan are multi-turn: they involve multiple question rounds,
|
||||
// codebase scanning, and artifact writing before the workflow completes (#3547).
|
||||
const isMultiTurnCommand =
|
||||
|
|
@ -612,7 +652,7 @@ async function runHeadlessOnce(
|
|||
options.supervised = false;
|
||||
}
|
||||
|
||||
if (isAutoMode && options.timeout === 300_000) {
|
||||
if (isAutonomousCommand && options.timeout === 300_000) {
|
||||
options.timeout = 0;
|
||||
}
|
||||
|
||||
|
|
@ -834,7 +874,7 @@ async function runHeadlessOnce(
|
|||
let blocked = false;
|
||||
let completed = false;
|
||||
let exitCode = 0;
|
||||
let milestoneReady = false; // tracks "Milestone X ready." for auto-chaining
|
||||
let milestoneReady = false; // tracks "Milestone X ready." for autonomous chaining
|
||||
let timedOut = false; // true only when the overall timeout timer fires
|
||||
// Rolling buffer for milestone-ready detection across split streaming deltas.
|
||||
// Capped at 200 chars — long enough to bridge any realistic delta boundary.
|
||||
|
|
@ -878,7 +918,7 @@ async function runHeadlessOnce(
|
|||
let inThinkingBlock = false;
|
||||
|
||||
// ─── Structured trace state ───────────────────────────────────────────────
|
||||
// Lazy-init: traces only created for auto-mode and new-milestone+auto.
|
||||
// Lazy-init: traces only created for autonomous mode and new-milestone+autonomous.
|
||||
// Uses maybeStartTrace() — not called upfront so we pay zero cost when disabled.
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
|
@ -890,7 +930,7 @@ async function runHeadlessOnce(
|
|||
// Tracks pending tool_execution_start for which we haven't seen toolName yet
|
||||
const pendingToolSpans = new Map<string, ReturnType<typeof startToolSpan>>();
|
||||
|
||||
/** Lazily initialize trace when entering auto-mode. Idempotent. */
|
||||
/** Lazily initialize trace when entering autonomous mode. Idempotent. */
|
||||
function maybeStartTrace(sessionId?: string): void {
|
||||
if (traceActive) return;
|
||||
if (!isTraceEnabled()) return;
|
||||
|
|
@ -1194,7 +1234,7 @@ async function runHeadlessOnce(
|
|||
// - new-milestone: bounded creative task; 120s buffer for LLM thinking
|
||||
// between bootstrap steps. (NEW_MILESTONE_IDLE_TIMEOUT_MS)
|
||||
// - Multi-turn (auto, next, discuss, plan): NOT a completion detector —
|
||||
// those signal done via "auto-mode stopped" terminal notifications,
|
||||
// those signal done via "autonomous mode stopped" terminal notifications,
|
||||
// and child-process exit catches crashes. The idle timer here is a
|
||||
// deadlock BACKSTOP only: 30 minutes, long enough to never misfire on
|
||||
// legitimate LLM reasoning, short enough to recover from a real hang.
|
||||
|
|
@ -1230,7 +1270,7 @@ async function runHeadlessOnce(
|
|||
// Precompute supervised response timeout
|
||||
const responseTimeout = options.responseTimeout ?? 30_000;
|
||||
|
||||
// Overall timeout (disabled when options.timeout === 0, e.g. auto-mode)
|
||||
// Overall timeout (disabled when options.timeout === 0, e.g. autonomous mode)
|
||||
const timeoutTimer =
|
||||
options.timeout > 0
|
||||
? setTimeout(() => {
|
||||
|
|
@ -1292,8 +1332,8 @@ async function runHeadlessOnce(
|
|||
if (toolCallId && isInteractiveHeadlessTool(toolName)) {
|
||||
interactiveToolCallIds.add(toolCallId);
|
||||
}
|
||||
// Lazy-start trace on first real tool call in auto-mode
|
||||
if (!traceActive && isAutoMode) {
|
||||
// Lazy-start trace on first real tool call in autonomous mode
|
||||
if (!traceActive && isAutonomousCommand) {
|
||||
maybeStartTrace(lastSessionId);
|
||||
}
|
||||
// Start a tool span if tracing is active
|
||||
|
|
@ -1423,7 +1463,11 @@ async function runHeadlessOnce(
|
|||
// output and verbose log lines that could spuriously match the pattern).
|
||||
// Accumulate a rolling 200-char buffer so patterns split across two
|
||||
// consecutive deltas are still detected.
|
||||
if (isNewMilestone && options.auto && ame?.type === "text_delta") {
|
||||
if (
|
||||
isNewMilestone &&
|
||||
options.chainAutonomous &&
|
||||
ame?.type === "text_delta"
|
||||
) {
|
||||
const deltaText = String(ame?.delta ?? ame?.text ?? "");
|
||||
if (deltaText) {
|
||||
milestoneDetectionBuffer = (
|
||||
|
|
@ -1565,7 +1609,7 @@ async function runHeadlessOnce(
|
|||
|
||||
// Handle execution_complete (v2 structured completion)
|
||||
// Skip for multi-turn commands (auto, next) — their completion is detected via
|
||||
// isTerminalNotification("Auto-mode stopped..."/"Step-mode stopped..."), not per-turn events
|
||||
// isTerminalNotification("Autonomous mode stopped..."/"Assisted mode stopped..."), not per-turn events
|
||||
if (
|
||||
eventObj.type === "execution_complete" &&
|
||||
!completed &&
|
||||
|
|
@ -1593,7 +1637,7 @@ async function runHeadlessOnce(
|
|||
blocked = true;
|
||||
}
|
||||
|
||||
// Detect "Milestone X ready." for auto-mode chaining
|
||||
// Detect "Milestone X ready." for autonomous mode chaining
|
||||
if (isMilestoneReadyNotification(eventObj)) {
|
||||
milestoneReady = true;
|
||||
}
|
||||
|
|
@ -1607,8 +1651,8 @@ async function runHeadlessOnce(
|
|||
const message = String(eventObj.message ?? "");
|
||||
observeHeadlessNotification(message);
|
||||
if (
|
||||
message.includes("Auto-mode resumed") ||
|
||||
message.includes("Step-mode resumed") ||
|
||||
message.includes("Autonomous mode resumed") ||
|
||||
message.includes("Assisted mode resumed") ||
|
||||
(message.includes("[unit]") && message.includes("starting"))
|
||||
) {
|
||||
providerAutoResumePending = false;
|
||||
|
|
@ -1868,11 +1912,11 @@ async function runHeadlessOnce(
|
|||
await completionPromise;
|
||||
}
|
||||
|
||||
// Autonomous-mode chaining: if --auto and milestone creation succeeded,
|
||||
// Autonomous mode chaining: if --autonomous and milestone creation succeeded,
|
||||
// send the canonical autonomous command.
|
||||
if (
|
||||
isNewMilestone &&
|
||||
options.auto &&
|
||||
options.chainAutonomous &&
|
||||
milestoneReady &&
|
||||
!blocked &&
|
||||
exitCode === EXIT_SUCCESS
|
||||
|
|
@ -1883,13 +1927,13 @@ async function runHeadlessOnce(
|
|||
);
|
||||
}
|
||||
|
||||
// Reset completion state for the auto-mode phase.
|
||||
// Disable the overall timeout — auto-mode has its own internal supervisor.
|
||||
// Reset completion state for the autonomous mode phase.
|
||||
// Disable the overall timeout — autonomous mode has its own internal supervisor.
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||
completed = false;
|
||||
milestoneReady = false;
|
||||
blocked = false;
|
||||
const autoCompletionPromise = new Promise<void>((resolve) => {
|
||||
const autonomousCompletionPromise = new Promise<void>((resolve) => {
|
||||
resolveCompletion = resolve;
|
||||
});
|
||||
|
||||
|
|
@ -1904,7 +1948,7 @@ async function runHeadlessOnce(
|
|||
}
|
||||
|
||||
if (exitCode === EXIT_SUCCESS || exitCode === EXIT_BLOCKED) {
|
||||
await autoCompletionPromise;
|
||||
await autonomousCompletionPromise;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1925,7 +1969,7 @@ async function runHeadlessOnce(
|
|||
await client.stop();
|
||||
|
||||
const solverEvalRecord =
|
||||
(isAutoMode || wasRequestedAutoMode) && timedOut
|
||||
(isAutonomousCommand || wasRequestedAutonomousCommand) && timedOut
|
||||
? await runHeadlessTimeoutSolverEval(process.cwd())
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ const SUBCOMMAND_HELP: Record<string, string> = {
|
|||
"new-milestone flags:",
|
||||
" --context <path> Path to spec/PRD file (use '-' for stdin)",
|
||||
" --context-text <txt> Inline specification text",
|
||||
" --auto Start autonomous mode after milestone creation",
|
||||
" --autonomous Start autonomous mode after milestone creation",
|
||||
" --verbose Show tool calls in progress output",
|
||||
"",
|
||||
"Output formats:",
|
||||
|
|
@ -246,7 +246,7 @@ const SUBCOMMAND_HELP: Record<string, string> = {
|
|||
" sf headless --resume abc123 autonomous Resume a prior session",
|
||||
" sf headless new-milestone --context spec.md Create milestone from file",
|
||||
" cat spec.md | sf headless new-milestone --context - From stdin",
|
||||
" sf headless new-milestone --context spec.md --auto Create + auto-execute",
|
||||
" sf headless new-milestone --context spec.md --autonomous Create + run autonomously",
|
||||
" sf headless --supervised autonomous Supervised orchestrator mode",
|
||||
" sf headless --answers answers.json autonomous With pre-supplied answers",
|
||||
" sf headless --events agent_end,extension_ui_request autonomous Filtered event stream",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ if (
|
|||
try {
|
||||
const now = Date.now();
|
||||
let passiveDueCount = 0;
|
||||
let projectAutoDispatchDueCount = 0;
|
||||
let projectAutonomousDispatchDueCount = 0;
|
||||
const schedulePaths = [
|
||||
{ path: join(process.cwd(), ".sf", "schedule.jsonl"), scope: "project" },
|
||||
{
|
||||
|
|
@ -149,10 +149,10 @@ if (
|
|||
) {
|
||||
if (
|
||||
scope === "project" &&
|
||||
entry.auto_dispatch === true &&
|
||||
entry.autonomous_dispatch === true &&
|
||||
(entry.kind === "command" || entry.kind === "prompt")
|
||||
) {
|
||||
projectAutoDispatchDueCount++;
|
||||
projectAutonomousDispatchDueCount++;
|
||||
} else {
|
||||
passiveDueCount++;
|
||||
}
|
||||
|
|
@ -164,9 +164,9 @@ if (
|
|||
`[forge] ${passiveDueCount} passive scheduled item${passiveDueCount === 1 ? "" : "s"} due now. Manage: /sf schedule list\n`,
|
||||
);
|
||||
}
|
||||
if (projectAutoDispatchDueCount > 0) {
|
||||
if (projectAutonomousDispatchDueCount > 0) {
|
||||
process.stderr.write(
|
||||
`[forge] ${projectAutoDispatchDueCount} scheduled auto-dispatch item${projectAutoDispatchDueCount === 1 ? "" : "s"} due now; autonomous mode will consume project entries.\n`,
|
||||
`[forge] ${projectAutonomousDispatchDueCount} scheduled autonomous dispatch item${projectAutonomousDispatchDueCount === 1 ? "" : "s"} due now; autonomous mode will consume project entries.\n`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ export function cleanupAll() {
|
|||
}
|
||||
/**
|
||||
* Kill all alive, non-persistent bg processes.
|
||||
* Called between auto-mode units to prevent orphaned servers from
|
||||
* Called between autonomous mode units to prevent orphaned servers from
|
||||
* keeping ports bound across task boundaries (#1209).
|
||||
*/
|
||||
export function killSessionProcesses() {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export default function ollama(pi) {
|
|||
} else {
|
||||
await registerOllamaTools(pi);
|
||||
}
|
||||
// In headless/auto mode, await the probe so the fallback resolver can
|
||||
// In headless/autonomous mode, await the probe so the fallback resolver can
|
||||
// see Ollama before the first LLM call (#3531 race condition).
|
||||
// In interactive mode, keep it async for fast startup.
|
||||
if (!ctx.hasUI) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Remote Notifications — one-way alert delivery to configured channels.
|
||||
*
|
||||
* Sends informational messages to Slack/Discord/Telegram without expecting
|
||||
* a reply. Used for auto-mode events like secrets-required pauses where
|
||||
* a reply. Used for autonomous mode events like secrets-required pauses where
|
||||
* the user needs to be notified but should NOT send sensitive data back
|
||||
* through the channel.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* SF Activity Log — Save raw chat sessions to .sf/activity/
|
||||
*
|
||||
* Before each context wipe in auto-mode, dumps the full session
|
||||
* Before each context wipe in autonomous mode, dumps the full session
|
||||
* as JSONL. No formatting, no truncation, no information loss.
|
||||
* These are debug artifacts — only read when summaries aren't enough.
|
||||
*
|
||||
|
|
@ -30,7 +30,7 @@ import { isAuditEnvelopeEnabled } from "./uok/audit-toggle.js";
|
|||
const activityLogState = new Map();
|
||||
/**
|
||||
* Clear accumulated activity log state (#611).
|
||||
* Call when auto-mode stops to prevent unbounded memory growth
|
||||
* Call when autonomous mode stops to prevent unbounded memory growth
|
||||
* from lastSnapshotKeyByUnit maps accumulating across units.
|
||||
*/
|
||||
export function clearActivityLogState() {
|
||||
|
|
@ -154,7 +154,7 @@ export function saveActivityLog(ctx, basePath, unitType, unitId) {
|
|||
}
|
||||
return filePath;
|
||||
} catch (e) {
|
||||
// Don't let logging failures break auto-mode
|
||||
// Don't let logging failures break autonomous mode
|
||||
void e;
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// SF Auto-mode — Artifact Path Resolution
|
||||
// SF Autonomous mode — Artifact Path Resolution
|
||||
//
|
||||
// resolveExpectedArtifactPath and diagnoseExpectedArtifact moved here from
|
||||
// auto-recovery.ts (Phase 5 dead-code cleanup). The artifact verification
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Budget alert level tracking and enforcement for auto-mode.
|
||||
* Budget alert level tracking and enforcement for autonomous mode.
|
||||
* Pure functions — no module state or side effects.
|
||||
*/
|
||||
export function getBudgetAlertLevel(budgetPct) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Auto-mode Dashboard — progress widget rendering, elapsed time formatting,
|
||||
* Autonomous mode Dashboard — progress widget rendering, elapsed time formatting,
|
||||
* unit description helpers, and slice progress caching.
|
||||
*
|
||||
* Pure functions that accept specific parameters — no module-level globals
|
||||
|
|
@ -243,7 +243,7 @@ export function describeNextUnit(state) {
|
|||
}
|
||||
}
|
||||
// ─── Elapsed Time Formatting ──────────────────────────────────────────────────
|
||||
/** Format elapsed time since auto-mode started */
|
||||
/** Format elapsed time since autonomous mode started */
|
||||
export function formatAutoElapsed(autoStartTime) {
|
||||
if (!autoStartTime || autoStartTime <= 0 || !Number.isFinite(autoStartTime))
|
||||
return "";
|
||||
|
|
@ -401,7 +401,7 @@ function getLastCommit(basePath) {
|
|||
}
|
||||
// ─── Footer Factory ───────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Footer factory used by auto-mode.
|
||||
* Footer factory used by autonomous mode.
|
||||
* Keep footer minimal but preserve extension status context from setStatus().
|
||||
*/
|
||||
function sanitizeFooterStatus(text) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|||
);
|
||||
return;
|
||||
}
|
||||
// When require_slice_discussion is enabled, pause auto-mode before
|
||||
// When require_slice_discussion is enabled, pause autonomous mode before
|
||||
// each new slice so the user can discuss requirements first (#789).
|
||||
const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
|
||||
const requireDiscussion =
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
|
|||
import {
|
||||
buildScheduledPrompt,
|
||||
executeProjectScheduleCommand,
|
||||
isAutoDispatchScheduleEntry,
|
||||
isAutonomousDispatchScheduleEntry,
|
||||
markProjectScheduleDone,
|
||||
} from "./schedule/schedule-auto-dispatch.js";
|
||||
} from "./schedule/schedule-autonomous-dispatch.js";
|
||||
import { createScheduleStore } from "./schedule/schedule-store.js";
|
||||
import {
|
||||
getMilestone,
|
||||
|
|
@ -259,7 +259,7 @@ function findMissingSummaries(basePath, mid) {
|
|||
const MAX_REWRITE_ATTEMPTS = 3;
|
||||
// ─── Disk-persisted rewrite attempt counter ──────────────────────────────────
|
||||
// The counter must survive session restarts (crash recovery, pause/resume,
|
||||
// step-mode). Storing it on the in-memory session object caused the circuit
|
||||
// assisted mode). Storing it on the in-memory session object caused the circuit
|
||||
// breaker to never trip — see https://github.com/singularity-forge/sf-run/issues/2203
|
||||
function rewriteCountPath(basePath) {
|
||||
return join(sfRoot(basePath), "runtime", "rewrite-count.json");
|
||||
|
|
@ -518,15 +518,17 @@ export async function enhanceUnitRankingWithMemory(units, baseScores = {}) {
|
|||
// ─── Rules ────────────────────────────────────────────────────────────────
|
||||
export const DISPATCH_RULES = [
|
||||
{
|
||||
name: "schedule auto-dispatch",
|
||||
name: "schedule autonomous dispatch",
|
||||
match: async ({ basePath }) => {
|
||||
try {
|
||||
const store = createScheduleStore(basePath);
|
||||
const due = store.findDue("project", new Date());
|
||||
const autoDispatch = due.filter(isAutoDispatchScheduleEntry);
|
||||
if (autoDispatch.length === 0) return null;
|
||||
const autonomousDispatch = due.filter(
|
||||
isAutonomousDispatchScheduleEntry,
|
||||
);
|
||||
if (autonomousDispatch.length === 0) return null;
|
||||
|
||||
const entry = autoDispatch[0];
|
||||
const entry = autonomousDispatch[0];
|
||||
if (entry.kind === "command") {
|
||||
const result = executeProjectScheduleCommand(basePath, entry);
|
||||
if (result.ok) {
|
||||
|
|
@ -1745,6 +1747,8 @@ function emitDispatchEnvelope(ctx, action) {
|
|||
action: envelopeAction,
|
||||
unitType,
|
||||
unitId,
|
||||
runControl: ctx.runControl,
|
||||
permissionProfile: ctx.permissionProfile,
|
||||
reasonCode,
|
||||
summary,
|
||||
evidence: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Model selection and dynamic routing for auto-mode unit dispatch.
|
||||
* Model selection and dynamic routing for autonomous mode unit dispatch.
|
||||
* Handles complexity-based routing, model resolution across providers,
|
||||
* and fallback chains.
|
||||
*/
|
||||
|
|
@ -91,7 +91,7 @@ function restoreToolBaseline(pi) {
|
|||
const key = pi;
|
||||
const baseline = TOOL_BASELINE.get(key);
|
||||
if (baseline === undefined) {
|
||||
// First call: capture the canonical pre-dispatch tool set. At auto-mode
|
||||
// First call: capture the canonical pre-dispatch tool set. At autonomous mode
|
||||
// start the active set has not yet been narrowed for any provider.
|
||||
// Guarded against test fakes that omit getActiveTools — record an empty
|
||||
// baseline so subsequent calls don't keep re-probing.
|
||||
|
|
@ -201,7 +201,7 @@ function matchesBareModelId(candidateId, requestedId) {
|
|||
}
|
||||
/**
|
||||
* Resolve preferred model configuration for a unit type from preferences or dynamic routing.
|
||||
* Returns undefined if no explicit config and auto-mode is disabled or flat-rate provider detected.
|
||||
* Returns undefined if no explicit config and autonomous mode is disabled or flat-rate provider detected.
|
||||
*/
|
||||
export function resolvePreferredModelConfig(
|
||||
unitType,
|
||||
|
|
@ -265,11 +265,11 @@ export async function selectAndApplyModel(
|
|||
autoModeStartModel,
|
||||
retryContext,
|
||||
/** When false (interactive/guided-flow), skip dynamic routing and use the session model.
|
||||
* Dynamic routing only applies in auto-mode where cost optimization is expected. (#3962) */
|
||||
* Dynamic routing only applies in autonomous mode where cost optimization is expected. (#3962) */
|
||||
isAutoMode = true,
|
||||
/** Explicit /sf model pin captured at bootstrap for long-running auto loops. */
|
||||
sessionModelOverride,
|
||||
/** Thinking level captured at auto-mode start and re-applied after model swaps. */
|
||||
/** Thinking level captured at autonomous mode start and re-applied after model swaps. */
|
||||
autoModeStartThinkingLevel,
|
||||
) {
|
||||
// ── Restore active-tool baseline before policy evaluation (#4959, #4681, #4850) ──
|
||||
|
|
@ -347,7 +347,7 @@ export async function selectAndApplyModel(
|
|||
const modelPolicyTurnId = `${unitType}:${unitId}`;
|
||||
let policyAllowedModelKeys = null;
|
||||
// ─── Dynamic Model Routing ─────────────────────────────────────────
|
||||
// Dynamic routing (complexity-based downgrading) only applies in auto-mode.
|
||||
// Dynamic routing (complexity-based downgrading) only applies in autonomous mode.
|
||||
// Interactive/guided-flow dispatches use the user's session model directly,
|
||||
// respecting their /model selection without silent downgrades (#3962).
|
||||
const routingConfig = resolveDynamicRoutingConfig();
|
||||
|
|
@ -756,7 +756,7 @@ export async function selectAndApplyModel(
|
|||
}
|
||||
} else if (autoModeStartModel) {
|
||||
// No model preference for this unit type — re-apply the model captured
|
||||
// at auto-mode start to prevent bleed from shared global settings.json (#650).
|
||||
// at autonomous mode start to prevent bleed from shared global settings.json (#650).
|
||||
const availableModels = filterModelsByProviderModelAllow(
|
||||
ctx.modelRegistry
|
||||
.getAvailable()
|
||||
|
|
|
|||
|
|
@ -842,7 +842,7 @@ export async function postUnitPreVerification(pctx, opts) {
|
|||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
// Non-conflict failures (dirty main, rev-walk error, etc.) can
|
||||
// leave the checkout in an unexpected state. Stop auto-mode so
|
||||
// leave the checkout in an unexpected state. Stop autonomous mode so
|
||||
// the next slice doesn't dispatch on top of it.
|
||||
const { stopAuto } = await import("./auto.js");
|
||||
await stopAuto(ctx, undefined, `slice-merge-error on ${sid}`);
|
||||
|
|
@ -1144,7 +1144,7 @@ export async function postUnitPreVerification(pctx, opts) {
|
|||
unitId: s.currentUnit.id,
|
||||
});
|
||||
ctx.ui.notify(
|
||||
`${s.currentUnit.type} ${s.currentUnit.id} is waiting for your input — pausing auto-mode instead of retrying the missing artifact.`,
|
||||
`${s.currentUnit.type} ${s.currentUnit.id} is waiting for your input — pausing autonomous mode instead of retrying the missing artifact.`,
|
||||
"info",
|
||||
);
|
||||
s.lastToolInvocationError = null;
|
||||
|
|
@ -1218,14 +1218,14 @@ export async function postUnitPreVerification(pctx, opts) {
|
|||
}
|
||||
// #2883/#3595: If the artifact is missing because the tool invocation
|
||||
// failed (malformed JSON) or was skipped (queued user message), retrying
|
||||
// will produce the same failure. Pause auto-mode instead of looping.
|
||||
// will produce the same failure. Pause autonomous mode instead of looping.
|
||||
if (s.lastToolInvocationError) {
|
||||
const isUserSkip = /queued user message/i.test(
|
||||
s.lastToolInvocationError,
|
||||
);
|
||||
const errMsg = isUserSkip
|
||||
? `Tool skipped for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Queued user message interrupted the turn — pausing auto-mode.`
|
||||
: `Tool invocation failed for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Structured argument generation failed — pausing auto-mode.`;
|
||||
? `Tool skipped for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Queued user message interrupted the turn — pausing autonomous mode.`
|
||||
: `Tool invocation failed for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Structured argument generation failed — pausing autonomous mode.`;
|
||||
debugLog("postUnit", {
|
||||
phase: "tool-invocation-error-pause",
|
||||
unitType: s.currentUnit.type,
|
||||
|
|
@ -1252,7 +1252,7 @@ export async function postUnitPreVerification(pctx, opts) {
|
|||
// the stub SUMMARY has no recovery value (milestone is terminal),
|
||||
// it does not update DB status (so deriveState never advances),
|
||||
// and it fools stopAuto's presence check into merging a milestone
|
||||
// that was never legitimately completed. Pause auto-mode with a
|
||||
// that was never legitimately completed. Pause autonomous mode with a
|
||||
// clear single failure signal and preserve the worktree branch.
|
||||
if (s.currentUnit.type === "complete-milestone") {
|
||||
debugLog("postUnit", {
|
||||
|
|
@ -1331,7 +1331,7 @@ export async function postUnitPreVerification(pctx, opts) {
|
|||
*
|
||||
* Returns:
|
||||
* - "continue" — proceed to sidecar drain / normal dispatch
|
||||
* - "step-wizard" — step mode, show wizard instead
|
||||
* - "step-wizard" — assisted mode, show wizard instead
|
||||
* - "stopped" — stopAuto was called
|
||||
*/
|
||||
export async function postUnitPostVerification(pctx) {
|
||||
|
|
@ -1587,7 +1587,7 @@ export async function postUnitPostVerification(pctx) {
|
|||
const stopCapture = pending.find((c) => STOP_PATTERN.test(c.text.trim()));
|
||||
if (stopCapture) {
|
||||
ctx.ui.notify(
|
||||
`Stop directive detected in pending capture ${stopCapture.id}: "${stopCapture.text}" — pausing auto-mode.`,
|
||||
`Stop directive detected in pending capture ${stopCapture.id}: "${stopCapture.text}" — pausing autonomous mode.`,
|
||||
"warning",
|
||||
);
|
||||
debugLog("postUnit", { phase: "fast-stop", captureId: stopCapture.id });
|
||||
|
|
@ -1749,7 +1749,7 @@ export async function postUnitPostVerification(pctx) {
|
|||
durationMs: result.durationMs,
|
||||
});
|
||||
} catch (preExecError) {
|
||||
// Fail-closed: if runPreExecutionChecks throws, pause auto-mode instead of silently continuing
|
||||
// Fail-closed: if runPreExecutionChecks throws, pause autonomous mode instead of silently continuing
|
||||
const errorMessage =
|
||||
preExecError instanceof Error
|
||||
? preExecError.message
|
||||
|
|
@ -1913,8 +1913,8 @@ export async function postUnitPostVerification(pctx) {
|
|||
debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
|
||||
}
|
||||
}
|
||||
// Step mode → show wizard instead of dispatch.
|
||||
// Without this notify(), /sf in step mode finishes a unit and silently
|
||||
// Assisted mode → show wizard instead of dispatch.
|
||||
// Without this notify(), /sf in assisted mode finishes a unit and silently
|
||||
// exits the loop, leaving the user with no hint to /clear and /sf again.
|
||||
if (s.stepMode) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Auto-mode Prompt Builders — construct dispatch prompts for each unit type.
|
||||
* Autonomous mode Prompt Builders — construct dispatch prompts for each unit type.
|
||||
*
|
||||
* Pure async functions that load templates and inline file content. No module-level
|
||||
* state, no globals — every dependency is passed as a parameter or imported as a
|
||||
|
|
@ -1751,7 +1751,7 @@ export async function buildExecuteTaskPrompt(
|
|||
})();
|
||||
// SF ADR-011 P2: when the feature is enabled, teach the executor that it can
|
||||
// surface non-obvious choices via the `escalation` field on sf_task_complete
|
||||
// rather than silently picking. Auto-mode auto-accepts the recommendation
|
||||
// rather than silently picking. Autonomous mode auto-accepts the recommendation
|
||||
// (see phases.escalation_auto_accept), so this is low-cost overhead — but
|
||||
// it produces an audit trail and a hard constraint for downstream tasks.
|
||||
// When the feature is off, the field is silently dropped, so we omit the
|
||||
|
|
@ -1774,7 +1774,7 @@ export async function buildExecuteTaskPrompt(
|
|||
"}",
|
||||
"```",
|
||||
"",
|
||||
"Provide 2–4 options with concrete tradeoffs. The recommendation must reference one of the option ids. Auto-mode accepts your recommendation, persists the choice + rationale as a memory, and carries it forward as a hard constraint for downstream tasks. The operator can review the audit trail later via `/sf escalate list --all`; the executed work itself can't be retroactively undone, so document your reasoning thoroughly. Set `continueWithDefault: false` only when the choice is severe enough that the loop should pause for human review even in auto-mode (rare).",
|
||||
"Provide 2–4 options with concrete tradeoffs. The recommendation must reference one of the option ids. Autonomous mode accepts your recommendation, persists the choice + rationale as a memory, and carries it forward as a hard constraint for downstream tasks. The operator can review the audit trail later via `/sf escalate list --all`; the executed work itself can't be retroactively undone, so document your reasoning thoroughly. Set `continueWithDefault: false` only when the choice is severe enough that the loop should pause for human review even in autonomous mode (rare).",
|
||||
].join("\n")
|
||||
: "";
|
||||
// Apply knowledge injection for this task context
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Auto-mode Recovery — artifact resolution, verification, blocker placeholders,
|
||||
* Autonomous mode Recovery — artifact resolution, verification, blocker placeholders,
|
||||
* skip artifacts, merge state reconciliation,
|
||||
* self-heal runtime records, and loop remediation steps.
|
||||
*
|
||||
|
|
@ -468,13 +468,13 @@ export function writeBlockerPlaceholder(unitType, unitId, base, reason) {
|
|||
const dir = dirname(absPath);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
const content = [
|
||||
`# BLOCKER — auto-mode recovery failed`,
|
||||
`# BLOCKER — autonomous mode recovery failed`,
|
||||
``,
|
||||
`Unit \`${unitType}\` for \`${unitId}\` failed to produce this artifact after idle recovery exhausted all retries.`,
|
||||
``,
|
||||
`**Reason**: ${reason}`,
|
||||
``,
|
||||
`This placeholder was written by auto-mode so the pipeline can advance.`,
|
||||
`This placeholder was written by autonomous mode so the pipeline can advance.`,
|
||||
`Review and replace this file before relying on downstream artifacts.`,
|
||||
].join("\n");
|
||||
writeFileSync(absPath, content, "utf-8");
|
||||
|
|
@ -662,7 +662,7 @@ export function reconcileMergeState(basePath, ctx) {
|
|||
// Code conflicts present — fail safe and preserve any manual resolution
|
||||
// work instead of discarding it with merge --abort/reset --hard.
|
||||
ctx.ui.notify(
|
||||
"Detected leftover merge state with unresolved code conflicts. Auto-mode will pause without modifying the worktree so manual conflict resolution is preserved.",
|
||||
"Detected leftover merge state with unresolved code conflicts. Autonomous mode will pause without modifying the worktree so manual conflict resolution is preserved.",
|
||||
"error",
|
||||
);
|
||||
return "blocked";
|
||||
|
|
@ -682,7 +682,7 @@ export function buildLoopRemediationSteps(unitType, unitId, base) {
|
|||
if (!mid || !sid || !tid) break;
|
||||
return [
|
||||
` 1. Run \`sf undo-task ${tid}\` to reset the task state`,
|
||||
` 2. Resume auto-mode — it will re-execute the task`,
|
||||
` 2. Resume autonomous mode — it will re-execute the task`,
|
||||
` 3. If the task keeps failing, run \`sf recover\` to rebuild DB state from disk`,
|
||||
].join("\n");
|
||||
}
|
||||
|
|
@ -696,14 +696,14 @@ export function buildLoopRemediationSteps(unitType, unitId, base) {
|
|||
return [
|
||||
` 1. Write ${artifactRel} manually (or with the LLM in interactive mode)`,
|
||||
` 2. Run \`sf recover\` to rebuild DB state from disk`,
|
||||
` 3. Resume auto-mode`,
|
||||
` 3. Resume autonomous mode`,
|
||||
].join("\n");
|
||||
}
|
||||
case "complete-slice": {
|
||||
if (!mid || !sid) break;
|
||||
return [
|
||||
` 1. Run \`sf reset-slice ${sid}\` to reset the slice and all its tasks`,
|
||||
` 2. Resume auto-mode — it will re-execute incomplete tasks and re-complete the slice`,
|
||||
` 2. Resume autonomous mode — it will re-execute incomplete tasks and re-complete the slice`,
|
||||
` 3. If the slice keeps failing, run \`sf recover\` to rebuild DB state from disk`,
|
||||
].join("\n");
|
||||
}
|
||||
|
|
@ -713,7 +713,7 @@ export function buildLoopRemediationSteps(unitType, unitId, base) {
|
|||
return [
|
||||
` 1. Write ${artifactRel} with verdict: pass`,
|
||||
` 2. Run \`sf recover\` to rebuild DB state from disk`,
|
||||
` 3. Resume auto-mode`,
|
||||
` 3. Resume autonomous mode`,
|
||||
].join("\n");
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Diagnostic budget guard for unusually long auto-mode units.
|
||||
* Diagnostic budget guard for unusually long autonomous mode units.
|
||||
*
|
||||
* This is intentionally not a blind tool-count kill switch. It gives the agent
|
||||
* explicit turns to explain whether the unit is legitimately large, stuck, or
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// SF auto-mode runtime state
|
||||
// SF autonomous mode runtime state
|
||||
import { AutoSession } from "./auto/session.js";
|
||||
import {
|
||||
isDeterministicPolicyError,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Auto-mode bootstrap — fresh-start initialization path.
|
||||
* Autonomous mode bootstrap — fresh-start initialization path.
|
||||
*
|
||||
* Git/state bootstrap, crash lock detection, debug init, worktree recovery,
|
||||
* guided flow gate, session init, worktree lifecycle, DB lifecycle,
|
||||
|
|
@ -109,7 +109,7 @@ import {
|
|||
import { emitWorktreeOrphaned } from "./worktree-telemetry.js";
|
||||
|
||||
/**
|
||||
* Bootstrap a fresh auto-mode session. Handles everything from git init
|
||||
* Bootstrap a fresh autonomous mode session. Handles everything from git init
|
||||
* through secrets collection, returning when ready for the first
|
||||
* dispatchNextUnit call.
|
||||
*
|
||||
|
|
@ -148,7 +148,7 @@ export async function openProjectDbIfPresent(basePath) {
|
|||
* After a milestone completes, the teardown step (merge branch → main,
|
||||
* delete branch, remove worktree) runs as a post-completion engine step.
|
||||
* If the session ends between completion and teardown, the branch and
|
||||
* worktree are orphaned — the DB says "complete" so auto-mode won't
|
||||
* worktree are orphaned — the DB says "complete" so autonomous mode won't
|
||||
* re-enter the milestone, and the teardown is never retried.
|
||||
*
|
||||
* This audit runs on every fresh bootstrap to catch that gap:
|
||||
|
|
@ -197,7 +197,7 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
|
|||
const milestone = getMilestone(milestoneId);
|
||||
if (!milestone) continue;
|
||||
// #4762 — in-progress milestone branch with unmerged commits ahead of
|
||||
// main. This is the pre-completion orphan case: auto-mode exited without
|
||||
// main. This is the pre-completion orphan case: autonomous mode exited without
|
||||
// completing the milestone (pause, stop, crash, merge error, blocker) and
|
||||
// work is stranded on the branch or in the worktree. Data safety first:
|
||||
// we never delete or touch; we just surface a warning so the user knows
|
||||
|
|
@ -364,7 +364,7 @@ export async function bootstrapAutoSession(
|
|||
// Exception (#4122): when the session provider is a custom provider declared
|
||||
// in ~/.sf/agent/models.json (Ollama, vLLM, OpenAI-compatible proxy, etc.),
|
||||
// PREFERENCES.md is skipped entirely. PREFERENCES.md cannot reference custom
|
||||
// providers, so honoring it would silently reroute auto-mode to a built-in
|
||||
// providers, so honoring it would silently reroute autonomous mode to a built-in
|
||||
// provider the user is not logged into and surface as "Not logged in · Please
|
||||
// run /login" before pausing and resetting to claude-code/claude-sonnet-4-6.
|
||||
const manualSessionOverride = getSessionModelOverride(
|
||||
|
|
@ -606,7 +606,7 @@ export async function bootstrapAutoSession(
|
|||
// Survivor branch exists but milestone still needs discussion (#1726):
|
||||
// The worktree/branch was created but the milestone only has CONTEXT-DRAFT.md.
|
||||
// Route to the interactive discussion handler instead of falling through to
|
||||
// auto-mode, which would immediately stop with "needs discussion".
|
||||
// autonomous mode, which would immediately stop with "needs discussion".
|
||||
if (decideSurvivorAction(hasSurvivorBranch, state.phase) === "discuss") {
|
||||
const { showWorkflowEntry } = await import("./guided-flow.js");
|
||||
await showWorkflowEntry(ctx, pi, base, { step: requestedStepMode });
|
||||
|
|
@ -661,7 +661,7 @@ export async function bootstrapAutoSession(
|
|||
);
|
||||
return releaseLockAndReturn();
|
||||
}
|
||||
// Auto mode: autonomously map the codebase and create milestones
|
||||
// Autonomous mode: map the codebase and create milestones
|
||||
// without waiting for user answers. Uses discuss-headless prompt.
|
||||
ctx.ui.notify(
|
||||
"No milestones found. Bootstrapping from repo docs and source inventory.",
|
||||
|
|
@ -678,7 +678,7 @@ export async function bootstrapAutoSession(
|
|||
const bootstrapContext = buildAutoBootstrapContext(base);
|
||||
const nextId = bootstrapNewMilestone(base);
|
||||
await dispatchNewMilestoneDiscuss(ctx, pi, base, nextId, {
|
||||
auto: true,
|
||||
autonomousBootstrap: true,
|
||||
preamble: injectTodoContext(base, bootstrapContext),
|
||||
});
|
||||
invalidateAllCaches();
|
||||
|
|
@ -689,7 +689,7 @@ export async function bootstrapAutoSession(
|
|||
"warning",
|
||||
);
|
||||
await dispatchNewMilestoneDiscuss(ctx, pi, base, nextId, {
|
||||
auto: true,
|
||||
autonomousBootstrap: true,
|
||||
preamble: injectTodoContext(
|
||||
base,
|
||||
[
|
||||
|
|
@ -699,7 +699,7 @@ export async function bootstrapAutoSession(
|
|||
bootstrapContext,
|
||||
"Start the roadmap planning session now: build project knowledge, run the planning meeting, and persist artifacts.",
|
||||
"Do not stop after reflection. At minimum write CONTEXT-DRAFT with evidence and open questions.",
|
||||
"If confidence is high enough, write CONTEXT and call sf_plan_milestone so auto-mode can continue.",
|
||||
"If confidence is high enough, write CONTEXT and call sf_plan_milestone so autonomous mode can continue.",
|
||||
].join("\n"),
|
||||
),
|
||||
});
|
||||
|
|
@ -732,7 +732,7 @@ export async function bootstrapAutoSession(
|
|||
"warning",
|
||||
);
|
||||
await dispatchNewMilestoneDiscuss(ctx, pi, base, repairId, {
|
||||
auto: true,
|
||||
autonomousBootstrap: true,
|
||||
preamble: injectTodoContext(
|
||||
base,
|
||||
[
|
||||
|
|
@ -742,7 +742,7 @@ export async function bootstrapAutoSession(
|
|||
bootstrapContext,
|
||||
"Reuse this milestone ID. Do not create a new milestone for the same bootstrap work.",
|
||||
"Run the roadmap planning session now and persist CONTEXT or CONTEXT-DRAFT at minimum.",
|
||||
"If confidence is high enough, write CONTEXT and call sf_plan_milestone so auto-mode can continue.",
|
||||
"If confidence is high enough, write CONTEXT and call sf_plan_milestone so autonomous mode can continue.",
|
||||
].join("\n"),
|
||||
),
|
||||
});
|
||||
|
|
@ -816,7 +816,7 @@ export async function bootstrapAutoSession(
|
|||
await import("./guided-flow.js");
|
||||
const bootstrapContext = buildAutoBootstrapContext(base);
|
||||
await dispatchNewMilestoneDiscuss(ctx, pi, base, mid, {
|
||||
auto: true,
|
||||
autonomousBootstrap: true,
|
||||
preamble: injectTodoContext(
|
||||
base,
|
||||
[
|
||||
|
|
@ -1008,12 +1008,12 @@ export async function bootstrapAutoSession(
|
|||
}
|
||||
// Gate: abort bootstrap if the DB file exists but the provider is
|
||||
// still unavailable after both open attempts above. Without this,
|
||||
// auto-mode starts but every sf_task_complete / sf_slice_complete
|
||||
// autonomous mode starts but every sf_task_complete / sf_slice_complete
|
||||
// call returns "db_unavailable", triggering artifact-retry which
|
||||
// re-dispatches the same task — producing an infinite loop (#2419).
|
||||
if (existsSync(sfDbPath) && !isDbAvailable()) {
|
||||
ctx.ui.notify(
|
||||
"SQLite database exists but failed to open. Auto-mode cannot proceed without a working database provider. " +
|
||||
"SQLite database exists but failed to open. Autonomous mode cannot proceed without a working database provider. " +
|
||||
"Check for corrupt sf.db or missing native SQLite bindings.",
|
||||
"error",
|
||||
);
|
||||
|
|
@ -1070,7 +1070,7 @@ export async function bootstrapAutoSession(
|
|||
// Hide sf-health during AUTO — sf-progress is the single source of truth
|
||||
// for last-commit / cost / health signal while auto is running.
|
||||
safeSetWidget(ctx, "sf-health", undefined);
|
||||
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
||||
const modeLabel = s.stepMode ? "Assisted mode" : "Autonomous mode";
|
||||
const pendingCount = (state.registry ?? []).filter(
|
||||
(m) => m.status !== "complete" && m.status !== "parked",
|
||||
).length;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Auto-mode Supervisor — signal handling and working-tree activity detection.
|
||||
* Autonomous mode Supervisor — signal handling and working-tree activity detection.
|
||||
*
|
||||
* Pure functions — no module-level globals or AutoContext dependency.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Timeout recovery logic for auto-mode units.
|
||||
* Timeout recovery logic for autonomous mode units.
|
||||
* Handles idle and hard timeout recovery with escalation, steering messages,
|
||||
* and blocker placeholder generation.
|
||||
*/
|
||||
|
|
@ -132,7 +132,7 @@ export async function recoverTimedOutUnit(
|
|||
recovery: status,
|
||||
});
|
||||
ctx.ui.notify(
|
||||
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode. (attempt ${attemptNumber})`,
|
||||
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing autonomous mode. (attempt ${attemptNumber})`,
|
||||
"info",
|
||||
);
|
||||
unitRecoveryCount.delete(recoveryKey);
|
||||
|
|
@ -263,7 +263,7 @@ export async function recoverTimedOutUnit(
|
|||
"If you are truly blocked, write the file with a BLOCKER section explaining why.",
|
||||
]
|
||||
: [
|
||||
`**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — stay in auto-mode.**`,
|
||||
`**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — stay in autonomous mode.**`,
|
||||
`You are still executing ${unitType} ${unitId}.`,
|
||||
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
|
||||
`Expected durable output: ${status.expected}.`,
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ export function startUnitSupervision(sctx) {
|
|||
},
|
||||
);
|
||||
ctx.ui.notify(
|
||||
`Unit ${unitType} ${unitId} made no meaningful progress for ${supervisor.idle_timeout_minutes}min. Pausing auto-mode.`,
|
||||
`Unit ${unitType} ${unitId} made no meaningful progress for ${supervisor.idle_timeout_minutes}min. Pausing autonomous mode.`,
|
||||
"warning",
|
||||
);
|
||||
await pauseAuto(ctx, pi);
|
||||
|
|
@ -480,7 +480,7 @@ export function startUnitSupervision(sctx) {
|
|||
);
|
||||
if (recovery === "recovered") return;
|
||||
ctx.ui.notify(
|
||||
`Unit ${unitType} ${unitId} exceeded ${supervisor.hard_timeout_minutes}min hard timeout. Pausing auto-mode.`,
|
||||
`Unit ${unitType} ${unitId} exceeded ${supervisor.hard_timeout_minutes}min hard timeout. Pausing autonomous mode.`,
|
||||
"warning",
|
||||
);
|
||||
await pauseAuto(ctx, pi);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* In-flight tool call tracking for auto-mode idle detection.
|
||||
* In-flight tool call tracking for autonomous mode idle detection.
|
||||
* Tracks which tool calls are currently executing so the idle watchdog
|
||||
* can distinguish "waiting for tool completion" from "truly idle".
|
||||
*/
|
||||
|
|
@ -132,7 +132,7 @@ export const DETERMINISTIC_POLICY_ERROR_STRINGS = [
|
|||
/**
|
||||
* Returns true if the error message indicates a deterministic policy gate
|
||||
* blocked the tool call before execution. Retrying the same unit without
|
||||
* changing behavior will hit the same gate, so auto-mode should write a
|
||||
* changing behavior will hit the same gate, so autonomous mode should write a
|
||||
* blocker placeholder instead of re-dispatching (#4973).
|
||||
*/
|
||||
export function isDeterministicPolicyError(errorMsg) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Post-unit verification gate for auto-mode.
|
||||
* Post-unit verification gate for autonomous mode.
|
||||
*
|
||||
* Runs typecheck/lint/test checks, captures runtime errors, performs
|
||||
* dependency audits, handles auto-fix retry logic, and writes
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* SF Auto-Worktree -- lifecycle management for auto-mode worktrees.
|
||||
* SF Auto-Worktree -- lifecycle management for autonomous mode worktrees.
|
||||
*
|
||||
* Auto-mode creates worktrees with `milestone/<MID>` branches (distinct from
|
||||
* Autonomous mode creates worktrees with `milestone/<MID>` branches (distinct from
|
||||
* manual `/worktree` which uses `worktree/<name>` branches). This module
|
||||
* manages create, enter, detect, and teardown for auto-mode worktrees.
|
||||
* manages create, enter, detect, and teardown for autonomous mode worktrees.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
|
@ -972,7 +972,7 @@ export function autoWorktreeBranch(milestoneId) {
|
|||
* Forward-merge plan checkbox state from the project root into a freshly
|
||||
* re-attached worktree (#778).
|
||||
*
|
||||
* When auto-mode stops via crash (not graceful stop), the milestone branch
|
||||
* When autonomous mode stops via crash (not graceful stop), the milestone branch
|
||||
* HEAD may be behind the filesystem state at the project root because
|
||||
* syncStateToProjectRoot() runs after every task completion but the final
|
||||
* git commit may not have happened before the crash. On restart the worktree
|
||||
|
|
@ -1088,7 +1088,7 @@ export function createAutoWorktree(basePath, milestoneId) {
|
|||
);
|
||||
}
|
||||
const branch = autoWorktreeBranch(milestoneId);
|
||||
// Check if the milestone branch already exists — it survives auto-mode
|
||||
// Check if the milestone branch already exists — it survives autonomous mode
|
||||
// stop/pause and contains committed work from prior sessions. If it exists,
|
||||
// re-attach the worktree to it WITHOUT resetting. Only create a fresh branch
|
||||
// from the integration branch when no prior work exists.
|
||||
|
|
@ -1120,7 +1120,7 @@ export function createAutoWorktree(basePath, milestoneId) {
|
|||
// Copy .sf/ planning artifacts from the source repo into the new worktree.
|
||||
// Worktrees are fresh git checkouts — untracked files don't carry over.
|
||||
// Planning artifacts may be untracked if the project's .gitignore had a
|
||||
// blanket .sf/ rule (pre-v2.14.0). Without this copy, auto-mode loops
|
||||
// blanket .sf/ rule (pre-v2.14.0). Without this copy, autonomous mode loops
|
||||
// on plan-slice because the plan file doesn't exist in the worktree.
|
||||
//
|
||||
// IMPORTANT: Skip when re-attaching to an existing branch (#759).
|
||||
|
|
@ -1134,7 +1134,7 @@ export function createAutoWorktree(basePath, milestoneId) {
|
|||
// Re-attaching to an existing branch: forward-merge any plan checkpoint
|
||||
// state from the project root into the worktree (#778).
|
||||
//
|
||||
// If auto-mode stopped via crash, the milestone branch HEAD may lag behind
|
||||
// If autonomous mode stopped via crash, the milestone branch HEAD may lag behind
|
||||
// the project root filesystem because syncStateToProjectRoot() ran after
|
||||
// task completion but the auto-commit never fired. On restart the worktree
|
||||
// is re-created from the branch HEAD (which has [ ] for the crashed task),
|
||||
|
|
@ -1169,7 +1169,7 @@ export function createAutoWorktree(basePath, milestoneId) {
|
|||
* Copies milestones/, DECISIONS.md, REQUIREMENTS.md, PROJECT.md, QUEUE.md,
|
||||
* STATE.md, KNOWLEDGE.md, and OVERRIDES.md.
|
||||
* Skips runtime files (auto.lock, metrics.json, etc.) and the worktrees/ dir.
|
||||
* Best-effort — failures are non-fatal since auto-mode can recreate artifacts.
|
||||
* Best-effort — failures are non-fatal since autonomous mode can recreate artifacts.
|
||||
*/
|
||||
function copyPlanningArtifacts(srcBase, wtPath) {
|
||||
const srcSf = join(srcBase, ".sf");
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export {
|
|||
} from "./auto/session.js";
|
||||
|
||||
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
|
||||
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
|
||||
// ALL mutable autonomous mode state lives in the AutoSession class (auto/session.ts).
|
||||
// This file must NOT declare module-level `let` or `var` variables for state.
|
||||
// The single `s` instance below is the only mutable module-level binding.
|
||||
//
|
||||
|
|
@ -309,18 +309,18 @@ export function shouldUseWorktreeIsolation() {
|
|||
/** Track dynamic routing decision for the current unit (for metrics) */
|
||||
/** Queue of quick-task captures awaiting dispatch after triage resolution */
|
||||
/**
|
||||
* Model captured at auto-mode start. Used to prevent model bleed between
|
||||
* Model captured at autonomous mode start. Used to prevent model bleed between
|
||||
* concurrent SF instances sharing the same global settings.json (#650).
|
||||
* When preferences don't specify a model for a unit type, this ensures
|
||||
* the session's original model is re-applied instead of reading from
|
||||
* the shared global settings (which another instance may have overwritten).
|
||||
*/
|
||||
/** Track current milestone to detect transitions */
|
||||
/** Model the user had selected before auto-mode started */
|
||||
/** Model the user had selected before autonomous mode started */
|
||||
/** Progress-aware timeout supervision */
|
||||
/** Context-pressure continue-here monitor — fires once when context usage >= 70% */
|
||||
/** Prompt character measurement for token savings analysis (R051). */
|
||||
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
||||
/** SIGTERM handler registered while autonomous mode is active — cleared on stop/pause. */
|
||||
/**
|
||||
* Tool calls currently being executed — prevents false idle detection during long-running tools.
|
||||
* Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
|
||||
|
|
@ -424,7 +424,7 @@ export function setActiveRunDir(runDir) {
|
|||
s.activeRunDir = runDir;
|
||||
}
|
||||
/**
|
||||
* Return the model captured at auto-mode start for this session.
|
||||
* Return the model captured at autonomous mode start for this session.
|
||||
* Used by error-recovery to fall back to the session's own model
|
||||
* instead of reading (potentially stale) preferences from disk (#1065).
|
||||
*/
|
||||
|
|
@ -526,7 +526,7 @@ function normalizeTaskCompleteFailure(errorMsg) {
|
|||
/**
|
||||
* Record a tool invocation error on the current session (#2883).
|
||||
* Called from tool_execution_end when a SF tool fails with isError.
|
||||
* Malformed/truncated JSON errors still pause auto-mode. sf_task_complete
|
||||
* Malformed/truncated JSON errors still pause autonomous mode. sf_task_complete
|
||||
* execution errors are tracked separately so the same task can retry in-flow.
|
||||
*/
|
||||
export function recordToolInvocationError(toolName, errorMsg) {
|
||||
|
|
@ -550,7 +550,7 @@ export function getOldestInFlightToolAgeMs() {
|
|||
/**
|
||||
* Return the base path to use for the auto.lock file.
|
||||
* Always uses the original project root (not the worktree) so that
|
||||
* a second terminal can discover and stop a running auto-mode session.
|
||||
* a second terminal can discover and stop a running autonomous mode session.
|
||||
*
|
||||
* Delegates to AutoSession.lockBasePath — the single source of truth.
|
||||
*/
|
||||
|
|
@ -558,7 +558,7 @@ function lockBase() {
|
|||
return s.lockBasePath;
|
||||
}
|
||||
/**
|
||||
* Attempt to stop a running auto-mode session from a different process.
|
||||
* Attempt to stop a running autonomous mode session from a different process.
|
||||
* Reads the lock file at the project root, checks if the PID is alive,
|
||||
* and sends SIGTERM to gracefully stop it.
|
||||
*
|
||||
|
|
@ -578,7 +578,7 @@ export function stopAutoRemote(projectRoot) {
|
|||
clearLock(projectRoot);
|
||||
return { found: false };
|
||||
}
|
||||
// Send SIGTERM — the auto-mode process has a handler that clears the lock and exits
|
||||
// Send SIGTERM — the autonomous mode process has a handler that clears the lock and exits
|
||||
try {
|
||||
process.kill(lock.pid, "SIGTERM");
|
||||
return { found: true, pid: lock.pid };
|
||||
|
|
@ -587,7 +587,7 @@ export function stopAutoRemote(projectRoot) {
|
|||
}
|
||||
}
|
||||
/**
|
||||
* Check if a remote auto-mode session is running (from a different process).
|
||||
* Check if a remote autonomous mode session is running (from a different process).
|
||||
* Reads the crash lock, checks PID liveness, and returns session details.
|
||||
* Used by the guard in commands.ts to prevent bare /sf, /sf next, and
|
||||
* /sf autonomous from stealing the session lock.
|
||||
|
|
@ -596,7 +596,7 @@ export function checkRemoteAutoSession(projectRoot) {
|
|||
const lock = readCrashLock(projectRoot);
|
||||
if (!lock) return { running: false };
|
||||
// Our own PID is not a "remote" session — it is a stale lock left by this
|
||||
// process (e.g. after step-mode exit without full cleanup). (#2730)
|
||||
// process (e.g. after assisted mode exit without full cleanup). (#2730)
|
||||
if (lock.pid === process.pid) return { running: false };
|
||||
if (!isLockProcessAlive(lock)) {
|
||||
// Stale lock from a dead process — not a live remote session
|
||||
|
|
@ -720,7 +720,7 @@ function cleanupAfterLoopExit(ctx) {
|
|||
);
|
||||
}
|
||||
// A transient provider-error pause intentionally leaves the paused badge
|
||||
// visible so the user still has a resumable auto-mode signal on screen.
|
||||
// visible so the user still has a resumable autonomous mode signal on screen.
|
||||
if (!s.paused) {
|
||||
ctx.ui.setStatus("sf-auto", undefined);
|
||||
safeSetWidget(ctx, "sf-progress", undefined);
|
||||
|
|
@ -947,12 +947,16 @@ export async function stopAuto(ctx, pi, reason) {
|
|||
if (ledger && ledger.units.length > 0) {
|
||||
const totals = getProjectTotals(ledger.units);
|
||||
ctx?.ui.notify(
|
||||
`Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
|
||||
`Autonomous mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
|
||||
"info",
|
||||
stopMeta,
|
||||
);
|
||||
} else {
|
||||
ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info", stopMeta);
|
||||
ctx?.ui.notify(
|
||||
`Autonomous mode stopped${reasonSuffix}.`,
|
||||
"info",
|
||||
stopMeta,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog("stop-cleanup-ledger", {
|
||||
|
|
@ -964,7 +968,7 @@ export async function stopAuto(ctx, pi, reason) {
|
|||
clearCmuxSidebar(loadedPreferences);
|
||||
logCmuxEvent(
|
||||
loadedPreferences,
|
||||
`Auto-mode stopped${reasonSuffix || ""}.`,
|
||||
`Autonomous mode stopped${reasonSuffix || ""}.`,
|
||||
reason?.startsWith("Blocked:") ? "warning" : "info",
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
@ -1115,9 +1119,9 @@ export async function stopAuto(ctx, pi, reason) {
|
|||
}
|
||||
}
|
||||
/**
|
||||
* Pause auto-mode without destroying state. Context is preserved.
|
||||
* Pause autonomous mode without destroying state. Context is preserved.
|
||||
* The user can interact with the agent, then `/sf autonomous` resumes
|
||||
* from disk state. Called when the user presses Escape during auto-mode.
|
||||
* from disk state. Called when the user presses Escape during autonomous mode.
|
||||
*/
|
||||
export async function pauseAuto(ctx, _pi, _errorContext) {
|
||||
if (!s.active) return;
|
||||
|
|
@ -1417,8 +1421,8 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|||
const pid = freshStartAssessment.lock?.pid;
|
||||
ctx.ui.notify(
|
||||
pid
|
||||
? `Another auto-mode session (PID ${pid}) appears to be running.\nStop it with \`kill ${pid}\` before starting a new session.`
|
||||
: "Another auto-mode session appears to be running.",
|
||||
? `Another autonomous mode session (PID ${pid}) appears to be running.\nStop it with \`kill ${pid}\` before starting a new session.`
|
||||
: "Another autonomous mode session appears to be running.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
|
|
@ -1612,7 +1616,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|||
s.cmdCtx = ctx;
|
||||
s.basePath = base;
|
||||
// Ensure the workflow-logger audit log is pinned to the project root
|
||||
// even when auto-mode is entered via a path that bypasses the
|
||||
// even when autonomous mode is entered via a path that bypasses the
|
||||
// bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
|
||||
// (e.g. /clear resume, hot-reload).
|
||||
setLogBasePath(base);
|
||||
|
|
@ -1643,7 +1647,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|||
ctx.ui.setStatus("sf-auto", s.stepMode ? "next" : "auto");
|
||||
ctx.ui.setFooter(hideFooter);
|
||||
ctx.ui.notify(
|
||||
s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.",
|
||||
s.stepMode ? "Assisted mode resumed." : "Autonomous mode resumed.",
|
||||
"info",
|
||||
);
|
||||
restoreHookState(s.basePath);
|
||||
|
|
@ -1716,7 +1720,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|||
writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown");
|
||||
logCmuxEvent(
|
||||
loadEffectiveSFPreferences()?.preferences,
|
||||
s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.",
|
||||
s.stepMode ? "Assisted mode resumed." : "Autonomous mode resumed.",
|
||||
"progress",
|
||||
);
|
||||
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
||||
|
|
@ -1755,7 +1759,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|||
await deriveState(s.basePath),
|
||||
);
|
||||
} catch (err) {
|
||||
// Best-effort only — sidebar sync must never block auto-mode startup
|
||||
// Best-effort only — sidebar sync must never block autonomous mode startup
|
||||
logWarning(
|
||||
"engine",
|
||||
`cmux sync failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
|
|
@ -1764,7 +1768,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|||
}
|
||||
logCmuxEvent(
|
||||
loadEffectiveSFPreferences()?.preferences,
|
||||
requestedStepMode ? "Step-mode started." : "Auto-mode started.",
|
||||
requestedStepMode ? "Assisted mode started." : "Autonomous mode started.",
|
||||
"progress",
|
||||
);
|
||||
// Dispatch the first unit
|
||||
|
|
@ -1925,7 +1929,7 @@ export async function dispatchHookUnit(
|
|||
s.unitTimeoutHandle = null;
|
||||
if (!s.active) return;
|
||||
ctx.ui.notify(
|
||||
`Hook ${hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing auto-mode.`,
|
||||
`Hook ${hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing autonomous mode.`,
|
||||
"warning",
|
||||
);
|
||||
resetHookState();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* auto/loop.ts — Main auto-mode execution loop.
|
||||
* auto/loop.ts — Main autonomous mode execution loop.
|
||||
*
|
||||
* Iterates: derive → dispatch → guards → runUnit → finalize → repeat.
|
||||
* Exits when s.active becomes false or a terminal condition is reached.
|
||||
|
|
@ -40,7 +40,7 @@ import { MAX_LOOP_ITERATIONS } from "./types.js";
|
|||
|
||||
// ── Stuck detection persistence (#3704) ──────────────────────────────────
|
||||
// Persist stuck detection state to disk so it survives session restarts.
|
||||
// Without this, restarting auto-mode resets all counters, allowing the
|
||||
// Without this, restarting autonomous mode resets all counters, allowing the
|
||||
// same blocked unit to burn a full retry budget each session.
|
||||
function stuckStatePath(basePath) {
|
||||
return join(sfRoot(basePath), "runtime", "stuck-state.json");
|
||||
|
|
@ -344,7 +344,7 @@ async function runExitSolverEval(ctx, s, deps, iteration) {
|
|||
}
|
||||
}
|
||||
/**
|
||||
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
||||
* Main autonomous mode execution loop. Iterates: derive → dispatch → guards →
|
||||
* runUnit → finalize → repeat. Exits when s.active becomes false or a
|
||||
* terminal condition is reached.
|
||||
*
|
||||
|
|
@ -426,7 +426,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|||
if (mem.pressured) {
|
||||
logWarning(
|
||||
"dispatch",
|
||||
`Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping auto-mode to prevent OOM kill`,
|
||||
`Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping autonomous mode to prevent OOM kill`,
|
||||
);
|
||||
await deps.stopAuto(
|
||||
ctx,
|
||||
|
|
@ -800,7 +800,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|||
// ── P4-A: Doctor issues → reassess escalation ─────────────────────
|
||||
// If the health gate detects issues that mention slice IDs (state
|
||||
// inconsistencies that reassessment can fix), queue reassess instead
|
||||
// of pausing auto-mode. This runs separately from the gate inside
|
||||
// of pausing autonomous mode. This runs separately from the gate inside
|
||||
// runPreDispatch so we can intercept *before* the break path.
|
||||
try {
|
||||
const healthCheck = await deps.preDispatchHealthGate(s.basePath);
|
||||
|
|
@ -1000,7 +1000,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|||
// candidate model is denied (cross-provider disabled + flat-rate
|
||||
// baseline + tool-policy denial), retrying the same unit produces the
|
||||
// same denial — burning the consecutive-error budget toward a 3-strike
|
||||
// hard stop and corrupting auto-mode state. Pause for user attention
|
||||
// hard stop and corrupting autonomous mode state. Pause for user attention
|
||||
// instead, with the per-model deny reasons surfaced from the typed error.
|
||||
if (loopErr instanceof ModelPolicyDispatchBlockedError) {
|
||||
debugLog("autoLoop", {
|
||||
|
|
@ -1011,7 +1011,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|||
reasons: loopErr.reasons,
|
||||
});
|
||||
ctx.ui.notify(
|
||||
`Auto-mode paused: model-policy denied dispatch for ${loopErr.unitType}/${loopErr.unitId}. ${msg}`,
|
||||
`Autonomous mode paused: model-policy denied dispatch for ${loopErr.unitType}/${loopErr.unitId}. ${msg}`,
|
||||
"error",
|
||||
);
|
||||
deps.emitJournalEvent({
|
||||
|
|
@ -1051,7 +1051,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|||
error: msg,
|
||||
});
|
||||
ctx.ui.notify(
|
||||
`Auto-mode stopped: infrastructure error ${infraCode} — ${msg}`,
|
||||
`Autonomous mode stopped: infrastructure error ${infraCode} — ${msg}`,
|
||||
"error",
|
||||
);
|
||||
await deps.stopAuto(
|
||||
|
|
@ -1097,7 +1097,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|||
});
|
||||
if (consecutiveCooldowns > MAX_COOLDOWN_RETRIES) {
|
||||
ctx.ui.notify(
|
||||
`Auto-mode stopped: ${consecutiveCooldowns} consecutive credential cooldowns — rate limit or quota may be persistently exhausted.`,
|
||||
`Autonomous mode stopped: ${consecutiveCooldowns} consecutive credential cooldowns — rate limit or quota may be persistently exhausted.`,
|
||||
"error",
|
||||
);
|
||||
await deps.stopAuto(
|
||||
|
|
@ -1137,7 +1137,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|||
.map((m, i) => ` ${i + 1}. ${m}`)
|
||||
.join("\n");
|
||||
ctx.ui.notify(
|
||||
`Auto-mode stopped: ${consecutiveErrors} consecutive iteration failures:\n${errorHistory}`,
|
||||
`Autonomous mode stopped: ${consecutiveErrors} consecutive iteration failures:\n${errorHistory}`,
|
||||
"error",
|
||||
);
|
||||
await deps.stopAuto(
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ async function generateMilestoneReport(s, ctx, milestoneId) {
|
|||
}
|
||||
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
||||
/**
|
||||
* If a unit is in-flight, close it out, then stop auto-mode.
|
||||
* If a unit is in-flight, close it out, then stop autonomous mode.
|
||||
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
||||
*/
|
||||
async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
||||
|
|
@ -1053,6 +1053,8 @@ export async function runDispatch(ic, preData, loopState) {
|
|||
state,
|
||||
prefs,
|
||||
session: s,
|
||||
runControl: deps.uokRunControl,
|
||||
permissionProfile: deps.uokPermissionProfile,
|
||||
});
|
||||
if (dispatchResult.action === "stop") {
|
||||
deps.emitJournalEvent({
|
||||
|
|
@ -1359,7 +1361,7 @@ export async function runGuards(ic, mid, unitType, unitId, sliceId) {
|
|||
"stop-directive",
|
||||
basename(s.originalBasePath || s.basePath),
|
||||
);
|
||||
// Pause first — ensures auto-mode stops even if later steps fail
|
||||
// Pause first — ensures autonomous mode stops even if later steps fail
|
||||
await deps.pauseAuto(ctx, pi);
|
||||
// For backtrack captures, write the backtrack trigger after pausing
|
||||
if (isBacktrack) {
|
||||
|
|
@ -1457,7 +1459,7 @@ export async function runGuards(ic, mid, unitType, unitId, sliceId) {
|
|||
ctx.ui.notify(msg, "error");
|
||||
deps.sendDesktopNotification(
|
||||
"SF",
|
||||
"Production mutation guard paused auto-mode",
|
||||
"Production mutation guard paused autonomous mode",
|
||||
"warning",
|
||||
"safety",
|
||||
basename(s.originalBasePath || s.basePath),
|
||||
|
|
@ -1483,7 +1485,7 @@ export async function runGuards(ic, mid, unitType, unitId, sliceId) {
|
|||
const budgetCeiling = prefs?.budget_ceiling;
|
||||
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
||||
const currentLedger = deps.getLedger();
|
||||
// In parallel worker mode, only count cost from the current auto-mode session
|
||||
// In parallel worker mode, only count cost from the current autonomous mode session
|
||||
// to avoid hitting the ceiling due to historical project-wide spend (#2184).
|
||||
let costUnits = currentLedger?.units;
|
||||
if (
|
||||
|
|
@ -2092,7 +2094,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|||
getRequiredWorkflowToolsForAutoUnit(unitType),
|
||||
{
|
||||
projectRoot: s.basePath,
|
||||
surface: "auto-mode",
|
||||
surface: "autonomous mode",
|
||||
unitType,
|
||||
authMode: s.currentUnitModel?.provider
|
||||
? ctx.modelRegistry.getProviderAuthMode(s.currentUnitModel.provider)
|
||||
|
|
@ -2462,7 +2464,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|||
}
|
||||
// Unit hard timeout (30min+): pause without auto-resume — stuck agent
|
||||
ctx.ui.notify(
|
||||
`Unit timed out for ${unitType} ${unitId} (supervision may have failed). Pausing auto-mode.`,
|
||||
`Unit timed out for ${unitType} ${unitId} (supervision may have failed). Pausing autonomous mode.`,
|
||||
"warning",
|
||||
);
|
||||
debugLog("autoLoop", {
|
||||
|
|
@ -2499,7 +2501,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|||
currentUnitResult.errorContext,
|
||||
);
|
||||
ctx.ui.notify(
|
||||
`Session creation failed for ${unitType} ${unitId}: ${currentUnitResult.errorContext?.message ?? "unknown"}. Stopping auto-mode.`,
|
||||
`Session creation failed for ${unitType} ${unitId}: ${currentUnitResult.errorContext?.message ?? "unknown"}. Stopping autonomous mode.`,
|
||||
"warning",
|
||||
);
|
||||
await deps.stopAuto(
|
||||
|
|
@ -2848,7 +2850,7 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|||
});
|
||||
if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
|
||||
ctx.ui.notify(
|
||||
`postUnitPreVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping auto-mode to prevent budget waste`,
|
||||
`postUnitPreVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping autonomous mode to prevent budget waste`,
|
||||
"error",
|
||||
);
|
||||
await deps.stopAuto(
|
||||
|
|
@ -2909,7 +2911,7 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|||
}
|
||||
if (pauseAfterUatDispatch) {
|
||||
ctx.ui.notify(
|
||||
"UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
|
||||
"UAT requires human execution. Autonomous mode will pause after this unit writes the result file.",
|
||||
"info",
|
||||
);
|
||||
await deps.pauseAuto(ctx, pi);
|
||||
|
|
@ -3062,7 +3064,7 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|||
});
|
||||
if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
|
||||
ctx.ui.notify(
|
||||
`postUnitPostVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping auto-mode to prevent budget waste`,
|
||||
`postUnitPostVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping autonomous mode to prevent budget waste`,
|
||||
"error",
|
||||
);
|
||||
await deps.stopAuto(
|
||||
|
|
@ -3087,7 +3089,7 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|||
return { action: "break", reason: "post-verification-stopped" };
|
||||
}
|
||||
if (postResult === "step-wizard") {
|
||||
// Step mode — exit the loop (caller handles wizard)
|
||||
// Assisted mode — exit the loop (caller handles wizard)
|
||||
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
||||
return { action: "break", reason: "step-wizard" };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* AutoSession — encapsulates all mutable auto-mode state into a single instance.
|
||||
* AutoSession — encapsulates all mutable autonomous mode state into a single instance.
|
||||
*
|
||||
* Replaces ~40 module-level variables scattered across auto.ts with typed
|
||||
* properties on a class instance. Benefits:
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
* - grep `s.` shows every state access
|
||||
* - Constructable for testing
|
||||
*
|
||||
* MAINTENANCE RULE: All new mutable auto-mode state MUST be added here as a
|
||||
* MAINTENANCE RULE: All new mutable autonomous mode state MUST be added here as a
|
||||
* class property, not as a module-level variable in auto.ts. If the state
|
||||
* needs clearing on stop, add it to reset(). Tests in
|
||||
* auto-session-encapsulation.test.ts enforce that auto.ts has no module-level
|
||||
|
|
@ -41,7 +41,7 @@ export class AutoSession {
|
|||
stepMode = false;
|
||||
/**
|
||||
* When false, the agent is forbidden from calling ask_user_questions.
|
||||
* Step mode sets this true; `/sf autonomous` sets it false.
|
||||
* Assisted mode sets this true; `/sf autonomous` sets it false.
|
||||
*/
|
||||
canAskUser = true;
|
||||
verbose = false;
|
||||
|
|
@ -113,7 +113,7 @@ export class AutoSession {
|
|||
* Last sf_task_complete execution error for the current turn.
|
||||
* Unlike malformed tool invocation errors, these are normal tool execution
|
||||
* failures (for example a transient SUMMARY.md write failure) and should be
|
||||
* retried in-flow instead of pausing auto-mode.
|
||||
* retried in-flow instead of pausing autonomous mode.
|
||||
*/
|
||||
lastTaskCompleteFailure = null;
|
||||
/** Per-unit task completion failures to surface in the next execute-task prompt. */
|
||||
|
|
|
|||
|
|
@ -36,5 +36,5 @@ export const BUDGET_THRESHOLDS = [
|
|||
},
|
||||
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
||||
];
|
||||
/** Max consecutive finalize timeouts before hard-stopping auto-mode. */
|
||||
/** Max consecutive finalize timeouts before hard-stopping autonomous mode. */
|
||||
export const MAX_FINALIZE_TIMEOUTS = 3;
|
||||
|
|
|
|||
12
src/resources/extensions/sf/autonomous-command-args.d.ts
vendored
Normal file
12
src/resources/extensions/sf/autonomous-command-args.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* autonomous-command-args.d.ts — Type declarations for autonomous command validation.
|
||||
*
|
||||
* Purpose: let TypeScript command surfaces import the JS extension helper
|
||||
* without weakening root compile safety.
|
||||
*
|
||||
* Consumer: src/headless.ts and tests that validate removed shorthand flags.
|
||||
*/
|
||||
|
||||
export function findUnsupportedAutonomousArgs(args: string[]): string[];
|
||||
|
||||
export function formatUnsupportedAutonomousArgs(args: string[]): string;
|
||||
53
src/resources/extensions/sf/autonomous-command-args.js
Normal file
53
src/resources/extensions/sf/autonomous-command-args.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* autonomous-command-args.js — validates `/sf autonomous` command arguments.
|
||||
*
|
||||
* Purpose: keep autonomous run control strict and explainable by accepting only
|
||||
* the small documented argument set; invented knobs fail as unsupported input.
|
||||
*
|
||||
* Consumer: headless.ts before machine-surface startup and the interactive
|
||||
* autonomous command handler before dispatching the run loop.
|
||||
*/
|
||||
|
||||
const VALUE_FLAGS = new Set(["--yolo", "-y"]);
|
||||
const SWITCH_FLAGS = new Set(["--verbose", "--debug"]);
|
||||
const MILESTONE_TARGET_RE = /^M\d+(?:-[a-z0-9]{6})?$/i;
|
||||
|
||||
/**
|
||||
* Return autonomous arguments that are not part of the supported command grammar.
|
||||
*
|
||||
* Purpose: reject stale or invented knobs before they can be confused with run
|
||||
* control, permission profiles, or output formats.
|
||||
*
|
||||
* Consumer: headless machine-surface validation and `/sf autonomous` routing.
|
||||
*/
|
||||
export function findUnsupportedAutonomousArgs(args) {
|
||||
const unsupported = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = String(args[i] ?? "").trim();
|
||||
if (!arg) continue;
|
||||
if (VALUE_FLAGS.has(arg)) {
|
||||
const value = String(args[i + 1] ?? "").trim();
|
||||
if (!value || value.startsWith("-")) {
|
||||
unsupported.push(arg);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (SWITCH_FLAGS.has(arg) || MILESTONE_TARGET_RE.test(arg)) continue;
|
||||
unsupported.push(arg);
|
||||
}
|
||||
return unsupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format unsupported autonomous arguments for human and machine-surface stderr.
|
||||
*
|
||||
* Purpose: give callers a direct correction path without giving unsupported
|
||||
* arguments product meaning.
|
||||
*
|
||||
* Consumer: headless.ts and the autonomous command handler.
|
||||
*/
|
||||
export function formatUnsupportedAutonomousArgs(args) {
|
||||
return `Unsupported /sf autonomous argument(s): ${args.join(", ")}. Supported arguments: --verbose, --debug, --yolo <file>, and optional M### milestone target.`;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
// capacity reasons.
|
||||
//
|
||||
// Lives at `.sf/runtime/blocked-models.json` so the block survives /sf autonomous
|
||||
// restarts. Auto-mode model selection skips blocked entries; agent-end
|
||||
// restarts. Autonomous mode model selection skips blocked entries; agent-end
|
||||
// recovery adds entries when a runtime rejection is classified as
|
||||
// `unsupported-model`. See issue #4513.
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
ctx.ui.notify(
|
||||
`Auto-mode error after empty-content abort: ${message}. Stopping auto-mode.`,
|
||||
`Autonomous mode error after empty-content abort: ${message}. Stopping autonomous mode.`,
|
||||
"error",
|
||||
);
|
||||
try {
|
||||
|
|
@ -192,7 +192,7 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|||
const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
|
||||
const currentRoute = getCurrentRouteFromMessage(lastMsg, ctx);
|
||||
const dash = getAutoDashboardData();
|
||||
// SF owns provider-route recovery in auto-mode. Quota/rate-limit/server/
|
||||
// SF owns provider-route recovery in autonomous mode. Quota/rate-limit/server/
|
||||
// stream/connection failures must leave the failed provider/model route
|
||||
// immediately instead of sleeping or waiting for same-model retry loops.
|
||||
// Cap rate-limit backoff for CLI-style providers (openai-codex, google-gemini-cli)
|
||||
|
|
@ -343,7 +343,7 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
ctx.ui.notify(
|
||||
`Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`,
|
||||
`Autonomous mode error in agent_end handler: ${message}. Stopping autonomous mode.`,
|
||||
"error",
|
||||
);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { logWarning } from "../workflow-logger.js";
|
|||
* (`isAutoActive() && !isCanAskUser()`) the call is blocked with a structured
|
||||
* rejection message the agent can read and act on (escalate to Tier 1/2).
|
||||
*
|
||||
* In auto/step mode (`canAskUser=true`) all calls pass through.
|
||||
* In manual/assisted mode (`canAskUser=true`) all calls pass through.
|
||||
*
|
||||
* @param questionPayload - Raw tool-call input; used only for diagnostic logging.
|
||||
* @returns `{ allow: true }` to permit the call, or `{ allow: false, reason }` to block.
|
||||
|
|
|
|||
|
|
@ -1741,7 +1741,7 @@ export function registerDbTools(pi) {
|
|||
}),
|
||||
continueWithDefault: Type.Boolean({
|
||||
description:
|
||||
"When true, loop continues (artifact logged for later review). When false, auto-mode pauses until the user resolves via /sf escalate resolve.",
|
||||
"When true, loop continues (artifact logged for later review). When false, autonomous mode pauses until the user resolves via /sf escalate resolve.",
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
|
@ -2020,7 +2020,7 @@ export function registerDbTools(pi) {
|
|||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Skipped slice ${params.sliceId} (${params.milestoneId}). Reason: ${params.reason ?? "User-directed skip"}. Auto-mode will advance past this slice.`,
|
||||
text: `Skipped slice ${params.sliceId} (${params.milestoneId}). Reason: ${params.reason ?? "User-directed skip"}. Autonomous mode will advance past this slice.`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
|
|
@ -2046,10 +2046,10 @@ export function registerDbTools(pi) {
|
|||
name: "sf_skip_slice",
|
||||
label: "Skip Slice",
|
||||
description:
|
||||
"Mark a slice as skipped so auto-mode advances past it without executing. " +
|
||||
"Mark a slice as skipped so autonomous mode advances past it without executing. " +
|
||||
"The slice data is preserved for reference. The state machine treats skipped slices like completed ones for dependency satisfaction.",
|
||||
promptSnippet:
|
||||
"Skip a SF slice (mark as skipped, auto-mode will advance past it)",
|
||||
"Skip a SF slice (mark as skipped, autonomous mode will advance past it)",
|
||||
promptGuidelines: [
|
||||
"Use sf_skip_slice when a slice should be bypassed — descoped, superseded, or no longer relevant.",
|
||||
"Cannot skip a slice that is already complete.",
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ export function registerJournalTools(pi) {
|
|||
name: "sf_journal_query",
|
||||
label: "Query Journal",
|
||||
description:
|
||||
"Query the structured event journal for auto-mode iterations. " +
|
||||
"Query the structured event journal for autonomous mode iterations. " +
|
||||
"Returns matching journal entries filtered by flow ID, unit ID, rule name, event type, or time range.",
|
||||
promptSnippet:
|
||||
"Query the SF event journal with filters (flowId, unitId, rule, eventType, time range, limit)",
|
||||
promptGuidelines: [
|
||||
"Filter by flowId to trace all events from a single auto-mode iteration.",
|
||||
"Filter by flowId to trace all events from a single autonomous mode iteration.",
|
||||
"Filter by unitId to reconstruct the causal chain for a specific milestone/slice/task.",
|
||||
"Use limit to control context size — default is 100 entries.",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export async function resumeAutoAfterProviderDelay(
|
|||
if (!snapshot.paused) return "not-paused";
|
||||
if (!snapshot.basePath) {
|
||||
ctx.ui.notify(
|
||||
"Provider error recovery delay elapsed, but no paused auto-mode base path was available. Leaving auto-mode paused.",
|
||||
"Provider error recovery delay elapsed, but no paused autonomous mode base path was available. Leaving autonomous mode paused.",
|
||||
"warning",
|
||||
);
|
||||
return "missing-base";
|
||||
|
|
@ -32,7 +32,7 @@ export async function resumeAutoAfterProviderDelay(
|
|||
: (deps.getCommandContext?.() ?? null);
|
||||
if (!commandCtx || typeof commandCtx.newSession !== "function") {
|
||||
ctx.ui.notify(
|
||||
"Provider error recovery delay elapsed, but no command context with newSession was available. Leaving auto-mode paused.",
|
||||
"Provider error recovery delay elapsed, but no command context with newSession was available. Leaving autonomous mode paused.",
|
||||
"warning",
|
||||
);
|
||||
return "missing-command-context";
|
||||
|
|
|
|||
|
|
@ -427,8 +427,8 @@ export function registerHooks(pi, ecosystemHandlers = []) {
|
|||
}
|
||||
});
|
||||
pi.on("session_before_compact", async () => {
|
||||
// Only cancel compaction while auto-mode is actively running.
|
||||
// Paused auto-mode should allow compaction — the user may be doing
|
||||
// Only cancel compaction while autonomous mode is actively running.
|
||||
// Paused autonomous mode should allow compaction — the user may be doing
|
||||
// interactive work (#3165).
|
||||
if (isAutoActive()) {
|
||||
return { cancel: true };
|
||||
|
|
@ -611,7 +611,7 @@ export function registerHooks(pi, ecosystemHandlers = []) {
|
|||
}
|
||||
if (!isToolCallEventType("write", event)) return;
|
||||
// ── Worktree isolation: block writes outside the worktree and main .sf/ ──
|
||||
// Only enforced in auto-mode — interactive sessions skip this check.
|
||||
// Only enforced in autonomous mode — interactive sessions skip this check.
|
||||
// When SF_WORKTREE is set, process.cwd() is the worktree directory.
|
||||
// The agent should only write inside the worktree OR inside the main repo's .sf/.
|
||||
if (isAutoActive() && process.env.SF_WORKTREE) {
|
||||
|
|
@ -648,7 +648,7 @@ export function registerHooks(pi, ecosystemHandlers = []) {
|
|||
if (!isAutoActive()) return;
|
||||
safetyRecordToolCall(event.toolCallId, event.toolName, event.input);
|
||||
const policyDash = getAutoDashboardData();
|
||||
const policyProfile = isQueuePhaseActive() ? "plan" : "build";
|
||||
const policyProfile = isQueuePhaseActive() ? "restricted" : "normal";
|
||||
if (policyDash.basePath) {
|
||||
emitJournalEvent(
|
||||
policyDash.basePath,
|
||||
|
|
@ -857,7 +857,7 @@ export function registerHooks(pi, ecosystemHandlers = []) {
|
|||
applyCompletionNudgeTemperature(payload);
|
||||
// ── Observation Masking ─────────────────────────────────────────────
|
||||
// Replace old tool results with placeholders to reduce context bloat.
|
||||
// Only active during auto-mode when context_management.observation_masking is enabled.
|
||||
// Only active during autonomous mode when context_management.observation_masking is enabled.
|
||||
if (isAutoActive()) {
|
||||
try {
|
||||
const { loadEffectiveSFPreferences } = await import(
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ function warnDeprecatedAgentInstructions() {
|
|||
* stronger language that forbids `ask_user_questions` entirely and instructs
|
||||
* the agent to exit with a structured blocker message instead.
|
||||
*
|
||||
* @param canAskUser - true in auto/step mode; false in autonomous mode.
|
||||
* @param canAskUser - true in manual/assisted mode; false in autonomous mode.
|
||||
*/
|
||||
export function buildEscalationPolicyBlock(canAskUser) {
|
||||
const tier3 = canAskUser
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Tool-call loop guard.
|
||||
*
|
||||
* Detects when a model calls the same tool with identical arguments
|
||||
* repeatedly within a single agent turn. Works in both auto-mode and
|
||||
* repeatedly within a single agent turn. Works in both autonomous mode and
|
||||
* interactive sessions by hooking into the `tool_call` event, which
|
||||
* fires before execution and can block the call.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -231,9 +231,9 @@ export function loadActionableCaptures(basePath, currentMilestoneId) {
|
|||
);
|
||||
}
|
||||
/**
|
||||
* Load unexecuted stop captures — user directives to halt auto-mode.
|
||||
* Load unexecuted stop captures — user directives to halt autonomous mode.
|
||||
* These are checked in the pre-dispatch guard pipeline (runGuards) to
|
||||
* pause auto-mode before the next unit is dispatched.
|
||||
* pause autonomous mode before the next unit is dispatched.
|
||||
*/
|
||||
export function loadStopCaptures(basePath) {
|
||||
return loadAllCaptures(basePath).filter(
|
||||
|
|
@ -245,7 +245,7 @@ export function loadStopCaptures(basePath) {
|
|||
}
|
||||
/**
|
||||
* Load unexecuted backtrack captures specifically — captures directing
|
||||
* auto-mode to abandon current milestone and return to a previous one.
|
||||
* autonomous mode to abandon current milestone and return to a previous one.
|
||||
*/
|
||||
export function loadBacktrackCaptures(basePath) {
|
||||
return loadAllCaptures(basePath).filter(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { workflowTemplateCommandDefinitions } from "./workflow-templates.js";
|
|||
|
||||
const TOP_LEVEL_SUBCOMMANDS = [
|
||||
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
||||
{ cmd: "next", desc: "Explicit step mode (same as /sf)" },
|
||||
{ cmd: "next", desc: "Assisted mode — execute one unit, then pause" },
|
||||
{
|
||||
cmd: "autonomous",
|
||||
desc: "Autonomous mode — research, plan, execute, commit, repeat",
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export async function handleEscalate(args, ctx) {
|
|||
if (!art) continue;
|
||||
count++;
|
||||
const isAutoResolved =
|
||||
art.respondedAt && art.userRationale?.startsWith("auto-mode:");
|
||||
art.respondedAt && art.userRationale?.startsWith("autonomous mode:");
|
||||
const status =
|
||||
task.escalation_pending === 1
|
||||
? "PENDING"
|
||||
|
|
@ -138,7 +138,7 @@ export async function handleEscalate(args, ctx) {
|
|||
}
|
||||
out.push(`\nRationale for recommendation: ${art.recommendationRationale}`);
|
||||
if (art.respondedAt) {
|
||||
const isAutoResolved = art.userRationale?.startsWith("auto-mode:");
|
||||
const isAutoResolved = art.userRationale?.startsWith("autonomous mode:");
|
||||
const verb = isAutoResolved ? "Auto-accepted" : "Resolved";
|
||||
out.push(
|
||||
`\n${verb} ${art.respondedAt} → choice="${art.userChoice}"${art.userRationale ? ` (rationale: ${art.userRationale})` : ""}`,
|
||||
|
|
|
|||
|
|
@ -235,14 +235,14 @@ export async function handleExtractLearnings(args, ctx, pi) {
|
|||
}
|
||||
/**
|
||||
* Canonical structured-extraction instructions, shared by the manual
|
||||
* `/sf extract-learnings` path and the auto-mode complete-milestone turn.
|
||||
* `/sf extract-learnings` path and the autonomous mode complete-milestone turn.
|
||||
*/
|
||||
export function buildExtractionStepsBlock(ctx) {
|
||||
return `## Structured Learnings Extraction
|
||||
|
||||
Perform the following steps IN ORDER. Each step is mandatory unless explicitly
|
||||
marked optional. These instructions are the single source of truth shared by
|
||||
\`/sf extract-learnings\` and the auto-mode milestone-completion turn.
|
||||
\`/sf extract-learnings\` and the autonomous mode milestone-completion turn.
|
||||
|
||||
### Step 1 — Classify findings into four categories
|
||||
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ export async function handleSteer(change, ctx, pi) {
|
|||
const sid = state.activeSlice?.id ?? "none";
|
||||
const tid = state.activeTask?.id ?? "none";
|
||||
const appliedAt = `${mid}/${sid}/${tid}`;
|
||||
// Resolve the correct target path: only route to a worktree when auto-mode
|
||||
// Resolve the correct target path: only route to a worktree when autonomous mode
|
||||
// is actively running there (in-process or remote). A worktree directory may
|
||||
// exist from a previous session without being the active runtime path —
|
||||
// writing there without a live session would silently drop the override.
|
||||
|
|
@ -565,7 +565,7 @@ Examples:
|
|||
);
|
||||
if (!success) {
|
||||
ctx.ui.notify(
|
||||
"Failed to dispatch hook. Auto-mode may have been cancelled.",
|
||||
"Failed to dispatch hook. Autonomous mode may have been cancelled.",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ async function handleLogsList(basePath, ctx) {
|
|||
const debugLogs = listDebugLogs(basePath);
|
||||
if (activities.length === 0 && debugLogs.length === 0) {
|
||||
ctx.ui.notify(
|
||||
"No logs found.\n\nActivity logs are created during auto-mode.\nDebug logs require SF_DEBUG=1.",
|
||||
"No logs found.\n\nActivity logs are created during autonomous mode.\nDebug logs require SF_DEBUG=1.",
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
|
|
@ -463,7 +463,7 @@ async function handleLogsTail(basePath, ctx, count) {
|
|||
const activities = listActivityLogs(basePath);
|
||||
if (activities.length === 0) {
|
||||
ctx.ui.notify(
|
||||
"No activity logs found. Logs are created during auto-mode.",
|
||||
"No activity logs found. Logs are created during autonomous mode.",
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
|
|
@ -529,7 +529,7 @@ async function handleLogsCurrent(basePath, ctx) {
|
|||
const lockData = readSessionLockData(basePath);
|
||||
if (!lockData) {
|
||||
ctx.ui.notify(
|
||||
"No active auto-mode session.\n\nauto.lock not found — auto-mode is not running.",
|
||||
"No active autonomous mode session.\n\nauto.lock not found — autonomous mode is not running.",
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ export async function handleSkip(unitArg, ctx, basePath) {
|
|||
mkDir(pathJoin(basePath, ".sf"), { recursive: true });
|
||||
writeFile(completedKeysFile, JSON.stringify(keys), "utf-8");
|
||||
ctx.ui.notify(
|
||||
`Skipped: ${skipKey}. Will not be dispatched in auto-mode.`,
|
||||
`Skipped: ${skipKey}. Will not be dispatched in autonomous mode.`,
|
||||
"success",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import {
|
||||
executeProjectScheduleCommand,
|
||||
markProjectScheduleDone,
|
||||
} from "./schedule/schedule-auto-dispatch.js";
|
||||
} from "./schedule/schedule-autonomous-dispatch.js";
|
||||
import { createScheduleStore } from "./schedule/schedule-store.js";
|
||||
import { ALL_SCHEDULE_KINDS, isValidKind } from "./schedule/schedule-types.js";
|
||||
import { generateULID } from "./schedule/schedule-ulid.js";
|
||||
|
|
@ -167,7 +167,7 @@ async function addItem(args, ctx) {
|
|||
let kind = "reminder";
|
||||
let scope = "project";
|
||||
let dueAt = null;
|
||||
let autoDispatch = false;
|
||||
let autonomousDispatch = false;
|
||||
let capture = null;
|
||||
const titleParts = [];
|
||||
|
||||
|
|
@ -205,10 +205,17 @@ async function addItem(args, ctx) {
|
|||
dueAt = new Date(parsed).toISOString();
|
||||
continue;
|
||||
}
|
||||
if (p === "--auto-dispatch" || p === "--auto") {
|
||||
autoDispatch = true;
|
||||
if (p === "--autonomous-dispatch") {
|
||||
autonomousDispatch = true;
|
||||
continue;
|
||||
}
|
||||
if (p === "--auto-dispatch" || p === "--auto") {
|
||||
ctx.ui.notify(
|
||||
"Unsupported schedule argument: use --autonomous-dispatch.",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (p === "--capture") {
|
||||
capture = parts[++i];
|
||||
continue;
|
||||
|
|
@ -258,7 +265,7 @@ async function addItem(args, ctx) {
|
|||
created_at: new Date().toISOString(),
|
||||
payload: _payloadForKind(kind, title, capture),
|
||||
created_by: "user",
|
||||
...(autoDispatch ? { auto_dispatch: true } : {}),
|
||||
...(autonomousDispatch ? { autonomous_dispatch: true } : {}),
|
||||
};
|
||||
store.appendEntry(scope, entry);
|
||||
ctx.ui.notify(`Scheduled: ${entry.id}\nDue: ${entry.due_at}`, "success");
|
||||
|
|
@ -488,7 +495,7 @@ async function runItem(args, ctx) {
|
|||
status: entry.status,
|
||||
cwd: _basePath(),
|
||||
command,
|
||||
auto_dispatch: entry.auto_dispatch === true,
|
||||
autonomous_dispatch: entry.autonomous_dispatch === true,
|
||||
would_execute: typeof command === "string" && command.length > 0,
|
||||
},
|
||||
null,
|
||||
|
|
@ -560,7 +567,7 @@ export async function handleSchedule(args, ctx) {
|
|||
case "":
|
||||
ctx.ui.notify(
|
||||
"Usage: /sf schedule add|list|done|cancel|snooze|run\n" +
|
||||
" add --in \u003cduration\u003e [--kind \u003ckind\u003e] [--scope \u003cscope\u003e] [--auto-dispatch] \u003ctitle-or-command\u003e\n" +
|
||||
" add --in \u003cduration\u003e [--kind \u003ckind\u003e] [--scope \u003cscope\u003e] [--autonomous-dispatch] \u003ctitle-or-command\u003e\n" +
|
||||
" list [--due] [--all] [--json] [--scope \u003cscope\u003e]\n" +
|
||||
" done \u003cid\u003e\n" +
|
||||
" cancel \u003cid\u003e\n" +
|
||||
|
|
|
|||
|
|
@ -149,7 +149,9 @@ function generatePRContent(basePath, milestoneId, milestoneTitle) {
|
|||
sections.push("- [ ] `chore` — Build, CI, or tooling changes\n");
|
||||
// AI disclosure
|
||||
sections.push("---\n");
|
||||
sections.push("*This PR was prepared with AI assistance (SF auto-mode).*");
|
||||
sections.push(
|
||||
"*This PR was prepared with AI assistance (SF autonomous mode).*",
|
||||
);
|
||||
return { title, body: sections.join("\n") };
|
||||
}
|
||||
export async function handleShip(args, ctx, _pi) {
|
||||
|
|
|
|||
|
|
@ -185,12 +185,12 @@ export async function handleStart(args, ctx, pi) {
|
|||
ctx.ui.notify(listTemplates(), "info");
|
||||
return;
|
||||
}
|
||||
// ─── Auto-mode conflict guard ──────────────────────────────────────────
|
||||
// ─── Autonomous mode conflict guard ──────────────────────────────────────────
|
||||
// Workflow templates dispatch their own messages and switch git branches,
|
||||
// which would conflict with an active auto-mode dispatch loop.
|
||||
// which would conflict with an active autonomous mode dispatch loop.
|
||||
if (isAutoActive()) {
|
||||
ctx.ui.notify(
|
||||
"Cannot start a workflow template while auto-mode is running.\n" +
|
||||
"Cannot start a workflow template while autonomous mode is running.\n" +
|
||||
"Run /sf pause first, then /sf start.",
|
||||
"warning",
|
||||
);
|
||||
|
|
@ -198,7 +198,7 @@ export async function handleStart(args, ctx, pi) {
|
|||
}
|
||||
if (isAutoPaused()) {
|
||||
ctx.ui.notify(
|
||||
"Auto-mode is paused. Starting a workflow template will run independently.\n" +
|
||||
"Autonomous mode is paused. Starting a workflow template will run independently.\n" +
|
||||
"The paused autonomous session can be resumed later with /sf autonomous.",
|
||||
"info",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const SF_COMMAND_DESCRIPTION =
|
|||
*/
|
||||
export const TOP_LEVEL_SUBCOMMANDS = [
|
||||
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
||||
{ cmd: "next", desc: "Explicit step mode (same as /sf)" },
|
||||
{ cmd: "next", desc: "Assisted mode — execute one unit, then pause" },
|
||||
{
|
||||
cmd: "autonomous",
|
||||
desc: "Autonomous mode — continuous loop, never asks user (self-resolves or stops with blocker)",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { SFNoProjectError } from "./context.js";
|
||||
import { handleAutoCommand } from "./handlers/auto.js";
|
||||
import { handleAutonomousCommand } from "./handlers/autonomous.js";
|
||||
import { handleCoreCommand } from "./handlers/core.js";
|
||||
import { handleOpsCommand } from "./handlers/ops.js";
|
||||
import { handleParallelCommand } from "./handlers/parallel.js";
|
||||
|
|
@ -8,7 +8,7 @@ export async function handleSFCommand(args, ctx, pi) {
|
|||
const trimmed = (typeof args === "string" ? args : "").trim();
|
||||
const handlers = [
|
||||
() => handleCoreCommand(trimmed, ctx, pi),
|
||||
() => handleAutoCommand(trimmed, ctx, pi),
|
||||
() => handleAutonomousCommand(trimmed, ctx, pi),
|
||||
() => handleParallelCommand(trimmed, ctx, pi),
|
||||
() => handleWorkflowCommand(trimmed, ctx, pi),
|
||||
() => handleOpsCommand(trimmed, ctx, pi),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import {
|
|||
stopAuto,
|
||||
stopAutoRemote,
|
||||
} from "../../auto.js";
|
||||
import {
|
||||
findUnsupportedAutonomousArgs,
|
||||
formatUnsupportedAutonomousArgs,
|
||||
} from "../../autonomous-command-args.js";
|
||||
import { handleRate } from "../../commands-rate.js";
|
||||
import { enableDebug } from "../../debug-logger.js";
|
||||
import { findMilestoneIds } from "../../milestone-id-utils.js";
|
||||
|
|
@ -46,12 +50,6 @@ export function parseMilestoneTarget(input) {
|
|||
const rest = input.replace(match[0], "").replace(/\s+/g, " ").trim();
|
||||
return { milestoneId: match[1], rest };
|
||||
}
|
||||
function hasRemovedFullFlag(input) {
|
||||
return input
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.some((token) => token === "full" || token === "--full");
|
||||
}
|
||||
/**
|
||||
* Dispatch entry point for the autonomous command family.
|
||||
*
|
||||
|
|
@ -70,7 +68,7 @@ function hasRemovedFullFlag(input) {
|
|||
* dispatches via `launchAuto` (which routes between machine-surface and detached
|
||||
* spawn paths).
|
||||
*/
|
||||
export async function handleAutoCommand(trimmed, ctx, pi) {
|
||||
export async function handleAutonomousCommand(trimmed, ctx, pi) {
|
||||
const isAutonomousVerb =
|
||||
trimmed === "autonomous" || trimmed.startsWith("autonomous ");
|
||||
/**
|
||||
|
|
@ -119,17 +117,18 @@ export async function handleAutoCommand(trimmed, ctx, pi) {
|
|||
return true;
|
||||
}
|
||||
if (isAutonomousVerb) {
|
||||
const { yoloSeedFile, rest: afterYolo } = parseYoloFlag(trimmed);
|
||||
const autonomousArgsText = trimmed.replace(/^autonomous\b/, "").trim();
|
||||
const { yoloSeedFile, rest: afterYolo } = parseYoloFlag(autonomousArgsText);
|
||||
const { milestoneId, rest: afterMilestone } =
|
||||
parseMilestoneTarget(afterYolo);
|
||||
const verboseMode = afterMilestone.includes("--verbose");
|
||||
const debugMode = afterMilestone.includes("--debug");
|
||||
const canAskUser = false;
|
||||
if (hasRemovedFullFlag(afterMilestone)) {
|
||||
ctx.ui.notify(
|
||||
"`/sf autonomous full` was removed. Use `/sf autonomous`; autonomous run control already continues through eligible milestones until policy, evidence, budget, blockers, or completion stops it.",
|
||||
"warning",
|
||||
);
|
||||
const unsupportedArgs = findUnsupportedAutonomousArgs(
|
||||
afterMilestone.split(/\s+/).filter(Boolean),
|
||||
);
|
||||
if (unsupportedArgs.length > 0) {
|
||||
ctx.ui.notify(formatUnsupportedAutonomousArgs(unsupportedArgs), "error");
|
||||
return true;
|
||||
}
|
||||
if (debugMode) enableDebug(projectRoot());
|
||||
|
|
@ -25,7 +25,7 @@ export function showHelp(ctx, args = "") {
|
|||
"SF — Singularity Forge\n",
|
||||
"QUICK START",
|
||||
" /sf start <tpl> Start a workflow template",
|
||||
" /sf Run next unit (same as /sf next)",
|
||||
" /sf Run one assisted unit (same as /sf next)",
|
||||
" /sf autonomous Run all queued product units continuously",
|
||||
" /sf pause Pause autonomous mode",
|
||||
" /sf stop Stop autonomous mode gracefully",
|
||||
|
|
@ -52,15 +52,15 @@ export function showHelp(ctx, args = "") {
|
|||
" /sf prefs Manage preferences",
|
||||
" /sf doctor Diagnose and repair .sf/ state",
|
||||
"",
|
||||
"Use /sf help full for the complete command reference.",
|
||||
"Use /sf help all for the complete command reference.",
|
||||
];
|
||||
const fullLines = [
|
||||
const allLines = [
|
||||
"SF — Singularity Forge\n",
|
||||
"WORKFLOW",
|
||||
" /sf start <tpl> Start a workflow template (bugfix, spike, feature, hotfix, etc.)",
|
||||
" /sf templates List available workflow templates [info <name>]",
|
||||
" /sf Run next unit in step mode (same as /sf next)",
|
||||
" /sf next Execute next task, then pause [--dry-run] [--verbose]",
|
||||
" /sf Run one assisted unit (same as /sf next)",
|
||||
" /sf next Assisted mode: execute next task, then pause [--dry-run] [--verbose]",
|
||||
" /sf autonomous Run all queued product units continuously [--verbose]",
|
||||
" /sf stop Stop autonomous mode gracefully",
|
||||
" /sf pause Pause autonomous mode (preserves state, /sf autonomous to resume)",
|
||||
|
|
@ -121,8 +121,8 @@ export function showHelp(ctx, args = "") {
|
|||
" /sf inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
|
||||
" /sf update Update SF to the latest version via npm",
|
||||
];
|
||||
const full = ["full", "--full", "all"].includes(args.trim().toLowerCase());
|
||||
ctx.ui.notify((full ? fullLines : summaryLines).join("\n"), "info");
|
||||
const showAll = args.trim().toLowerCase() === "all";
|
||||
ctx.ui.notify((showAll ? allLines : summaryLines).join("\n"), "info");
|
||||
}
|
||||
export async function handleStatus(ctx) {
|
||||
const basePath = projectRoot();
|
||||
|
|
@ -301,9 +301,7 @@ async function selectModelByProvider(title, models, ctx, currentModel) {
|
|||
return optionToModel.get(modelChoice);
|
||||
}
|
||||
async function resolveRequestedModel(query, ctx) {
|
||||
const { resolveModelId } = await import(
|
||||
"../../autonomous model-selection.js"
|
||||
);
|
||||
const { resolveModelId } = await import("../../auto-model-selection.js");
|
||||
const models = ctx.modelRegistry.getAvailable();
|
||||
const exact = resolveModelId(query, models, ctx.model?.provider);
|
||||
if (exact) return exact;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Observation masking for SF auto-mode sessions.
|
||||
* Observation masking for SF autonomous mode sessions.
|
||||
*
|
||||
* Replaces tool result content older than N turns with a placeholder.
|
||||
* Reduces context bloat between compactions with zero LLM overhead.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* SF Crash Recovery
|
||||
*
|
||||
* Detects interrupted auto-mode sessions via a lock file.
|
||||
* Detects interrupted autonomous mode sessions via a lock file.
|
||||
* Written on auto-start, updated on each unit dispatch, deleted on clean stop.
|
||||
* If the lock file exists on next startup, the previous session crashed.
|
||||
*
|
||||
|
|
@ -19,7 +19,7 @@ import { effectiveLockFile } from "./session-lock.js";
|
|||
function lockPath(basePath) {
|
||||
return join(sfRoot(basePath), effectiveLockFile());
|
||||
}
|
||||
/** Write or update the lock file with current auto-mode state. */
|
||||
/** Write or update the lock file with current autonomous mode state. */
|
||||
export function writeLock(basePath, unitType, unitId, sessionFile) {
|
||||
try {
|
||||
const data = {
|
||||
|
|
@ -83,7 +83,7 @@ export function isLockProcessAlive(lock) {
|
|||
/** Format crash info for display or injection into a prompt. */
|
||||
export function formatCrashInfo(lock) {
|
||||
const lines = [
|
||||
`Previous auto-mode session was interrupted.`,
|
||||
`Previous autonomous mode session was interrupted.`,
|
||||
` Was executing: ${lock.unitType} (${lock.unitId})`,
|
||||
` Started at: ${lock.unitStartedAt}`,
|
||||
` PID: ${lock.pid}`,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* SF Dashboard Overlay
|
||||
*
|
||||
* Full-screen overlay showing auto-mode progress: milestone/slice/task
|
||||
* Full-screen overlay showing autonomous mode progress: milestone/slice/task
|
||||
* breakdown, current unit, completed units, timing, and activity log.
|
||||
* Toggled with Ctrl+Alt+G (⌃⌥G on macOS), Ctrl+Shift+G fallback,
|
||||
* or opened from /sf status.
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ export async function saveDecisionToDb(fields, basePath) {
|
|||
// #2661: When a decision defers a slice, update the slice status in the DB
|
||||
// so the dispatcher skips it. Without this, STATE.md and DECISIONS.md are
|
||||
// in split-brain: the decision says "deferred" but the state still says
|
||||
// "active", causing auto-mode to keep dispatching the deferred work.
|
||||
// "active", causing autonomous mode to keep dispatching the deferred work.
|
||||
try {
|
||||
const sliceRef = extractDeferredSliceRef(fields);
|
||||
if (sliceRef) {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export function debugPeak(counter, value) {
|
|||
}
|
||||
}
|
||||
/**
|
||||
* Write the debug summary and disable logging. Call this when auto-mode stops.
|
||||
* Write the debug summary and disable logging. Call this when autonomous mode stops.
|
||||
* Returns the log file path for user notification.
|
||||
*/
|
||||
export function writeDebugSummary() {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Implements WorkflowEngine by delegating to existing SF state derivation
|
||||
* and dispatch logic. This is the "dev" engine — it wraps the current SF
|
||||
* auto-mode behavior behind the engine-polymorphic interface.
|
||||
* autonomous mode behavior behind the engine-polymorphic interface.
|
||||
*/
|
||||
import { resolveDispatch } from "./auto-dispatch.js";
|
||||
import { loadEffectiveSFPreferences } from "./preferences.js";
|
||||
|
|
@ -42,7 +42,7 @@ export function bridgeDispatchAction(da) {
|
|||
}
|
||||
// ─── DevWorkflowEngine ───────────────────────────────────────────────────
|
||||
/**
|
||||
* DevWorkflowEngine wraps current SF auto-mode behavior behind the engine interface.
|
||||
* DevWorkflowEngine wraps current SF autonomous mode behavior behind the engine interface.
|
||||
* Implements WorkflowEngine by delegating to existing state derivation and dispatch logic.
|
||||
*/
|
||||
export class DevWorkflowEngine {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|||
- `commit_type`: string — override the conventional commit type prefix. Must be one of: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style`. Default: inferred from diff content.
|
||||
- `main_branch`: string — the primary branch name for new git repos (e.g., `"main"`, `"master"`, `"trunk"`). Also used by `getMainBranch()` as the preferred branch when auto-detection is ambiguous. Default: `"main"`.
|
||||
- `merge_strategy`: `"squash"` or `"merge"` — controls how worktree branches are merged back. `"squash"` combines all commits into one; `"merge"` preserves individual commits. Default: `"squash"`.
|
||||
- `isolation`: `"worktree"`, `"branch"`, or `"none"` — controls autonomous mode git isolation strategy. `"worktree"` creates a milestone worktree for isolated work; `"branch"` works directly in the project root but creates a milestone branch (useful for submodule-heavy repos); `"none"` works directly on the current branch with no worktree or milestone branch (ideal for step-mode with hot reloads). Default: `"worktree"`.
|
||||
- `isolation`: `"worktree"`, `"branch"`, or `"none"` — controls autonomous mode git isolation strategy. `"worktree"` creates a milestone worktree for isolated work; `"branch"` works directly in the project root but creates a milestone branch (useful for submodule-heavy repos); `"none"` works directly on the current branch with no worktree or milestone branch (ideal for assisted mode with hot reloads). Default: `"worktree"`.
|
||||
- `manage_gitignore`: boolean — when `false`, SF will not touch `.gitignore` at all. Useful when your project has a strictly managed `.gitignore` and you don't want SF adding entries. Default: `true`.
|
||||
- `worktree_post_create`: string — script to run after a worktree is created (both autonomous mode and manual `/worktree`). Receives `SOURCE_DIR` and `WORKTREE_DIR` as environment variables. Can be absolute or relative to project root. Runs with 30-second timeout. Failure is non-fatal (logged as warning). Default: none.
|
||||
- `auto_pr`: boolean — automatically create a GitHub pull request after a milestone branch is merged. Requires `gh` CLI to be installed. Default: `false`.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* SF Doctor — Proactive Healing Layer
|
||||
*
|
||||
* Three mechanisms for automatic health monitoring during auto-mode:
|
||||
* Three mechanisms for automatic health monitoring during autonomous mode:
|
||||
*
|
||||
* 1. Pre-dispatch health gate: lightweight check before each unit dispatch.
|
||||
* Returns blocking issues that should pause auto-mode rather than
|
||||
* Returns blocking issues that should pause autonomous mode rather than
|
||||
* dispatching into a broken state.
|
||||
*
|
||||
* 2. Health score tracking: tracks issue counts over time to detect
|
||||
|
|
@ -40,7 +40,7 @@ import {
|
|||
} from "./snapshot-safety.js";
|
||||
import { deriveState } from "./state.js";
|
||||
|
||||
/** In-memory health history for the current auto-mode session. */
|
||||
/** In-memory health history for the current autonomous mode session. */
|
||||
let healthHistory = [];
|
||||
/** Count of consecutive units with unresolved errors. */
|
||||
let consecutiveErrorUnits = 0;
|
||||
|
|
@ -48,11 +48,11 @@ let consecutiveErrorUnits = 0;
|
|||
let healthUnitIndex = 0;
|
||||
/** Previous progress level for state transition detection. */
|
||||
let previousProgressLevel = "green";
|
||||
/** Callback for state transition notifications. Set by auto-mode. */
|
||||
/** Callback for state transition notifications. Set by autonomous mode. */
|
||||
let onLevelChange = null;
|
||||
/**
|
||||
* Register a callback for progress level transitions (green→yellow, yellow→red, etc.).
|
||||
* Called once when auto-mode starts. Pass null to unregister.
|
||||
* Called once when autonomous mode starts. Pass null to unregister.
|
||||
*/
|
||||
export function setLevelChangeCallback(cb) {
|
||||
onLevelChange = cb;
|
||||
|
|
@ -161,7 +161,7 @@ export function getLatestHealthFixes() {
|
|||
return [];
|
||||
}
|
||||
/**
|
||||
* Reset health tracking state. Called on auto-mode start/stop.
|
||||
* Reset health tracking state. Called on autonomous mode start/stop.
|
||||
*/
|
||||
export function resetHealthTracking() {
|
||||
healthHistory = [];
|
||||
|
|
@ -424,13 +424,13 @@ export function checkHealEscalation(errors, unresolvedIssues) {
|
|||
};
|
||||
}
|
||||
/**
|
||||
* Reset escalation state. Called on auto-mode start/stop.
|
||||
* Reset escalation state. Called on autonomous mode start/stop.
|
||||
*/
|
||||
export function resetEscalation() {
|
||||
escalationTriggered = false;
|
||||
}
|
||||
/**
|
||||
* Format a health summary for display in the auto-mode dashboard.
|
||||
* Format a health summary for display in the autonomous mode dashboard.
|
||||
* Human-readable with full words, not abbreviations.
|
||||
*/
|
||||
export function formatHealthSummary() {
|
||||
|
|
@ -478,7 +478,7 @@ export function formatHealthSummary() {
|
|||
return parts.join(" · ");
|
||||
}
|
||||
/**
|
||||
* Reset all proactive healing state. Called on auto-mode start/stop.
|
||||
* Reset all proactive healing state. Called on autonomous mode start/stop.
|
||||
*/
|
||||
export function resetProactiveHealing() {
|
||||
resetHealthTracking();
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export async function checkRuntimeHealth(
|
|||
code: "stranded_lock_directory",
|
||||
scope: "project",
|
||||
unitId: "project",
|
||||
message: `Stranded lock directory "${lockDir}" exists but no live process holds the session lock. This blocks new auto-mode sessions from starting.`,
|
||||
message: `Stranded lock directory "${lockDir}" exists but no live process holds the session lock. This blocks new autonomous mode sessions from starting.`,
|
||||
file: lockDir,
|
||||
fixable: true,
|
||||
});
|
||||
|
|
@ -275,7 +275,7 @@ export async function checkRuntimeHealth(
|
|||
state.cycleCounts &&
|
||||
typeof state.cycleCounts === "object" &&
|
||||
Object.keys(state.cycleCounts).length > 0;
|
||||
// Only flag if there are actual cycle counts AND no auto-mode is running
|
||||
// Only flag if there are actual cycle counts AND no autonomous mode is running
|
||||
if (hasCycleCounts) {
|
||||
const lock = readCrashLock(basePath);
|
||||
const autoRunning = lock ? isLockProcessAlive(lock) : false;
|
||||
|
|
|
|||
|
|
@ -1067,7 +1067,7 @@ async function updateStateFile(basePath, fixesApplied) {
|
|||
* Rebuild STATE.md from current disk state.
|
||||
*
|
||||
* Invalidates state cache, re-derives from milestone/slice/task directories,
|
||||
* and rewrites STATE.md. Called from auto-mode post-hooks and doctor recovery paths.
|
||||
* and rewrites STATE.md. Called from autonomous mode post-hooks and doctor recovery paths.
|
||||
*/
|
||||
export async function rebuildState(basePath) {
|
||||
invalidateAllCaches();
|
||||
|
|
|
|||
|
|
@ -312,9 +312,9 @@ export function resolveEscalation(
|
|||
};
|
||||
}
|
||||
if (art.respondedAt) {
|
||||
const wasAuto = art.userRationale?.startsWith("auto-mode:");
|
||||
const wasAuto = art.userRationale?.startsWith("autonomous mode:");
|
||||
const detail = wasAuto
|
||||
? ` (auto-accepted in auto-mode → choice="${art.userChoice}"; the carry-forward was already injected into the downstream task, so this can't be retroactively changed via /sf escalate resolve. Capture the corrective decision as \`/sf memory note "..."\` so future tasks pick it up.)`
|
||||
? ` (auto-accepted in autonomous mode → choice="${art.userChoice}"; the carry-forward was already injected into the downstream task, so this can't be retroactively changed via /sf escalate resolve. Capture the corrective decision as \`/sf memory note "..."\` so future tasks pick it up.)`
|
||||
: ` (resolved by user → choice="${art.userChoice}").`;
|
||||
return {
|
||||
status: "already-resolved",
|
||||
|
|
@ -352,7 +352,7 @@ export function resolveEscalation(
|
|||
traceId: `escalation:${milestoneId}:${sliceId}:${taskId}`,
|
||||
category: "gate",
|
||||
type:
|
||||
source === "auto-mode"
|
||||
source === "autonomous mode"
|
||||
? "escalation-auto-accepted"
|
||||
: "escalation-user-responded",
|
||||
payload: {
|
||||
|
|
|
|||
|
|
@ -11,19 +11,20 @@
|
|||
*/
|
||||
|
||||
import { shouldBlockQueueExecutionInSnapshot } from "./bootstrap/write-gate.js";
|
||||
import { resolvePermissionProfile } from "./operating-model.js";
|
||||
import { classifyCommand } from "./safety/destructive-guard.js";
|
||||
|
||||
export const EXECUTION_POLICY_PROFILES = {
|
||||
plan: {
|
||||
id: "plan",
|
||||
restricted: {
|
||||
id: "restricted",
|
||||
permissionProfile: "restricted",
|
||||
filesystem: "read-mostly",
|
||||
network: "read-only",
|
||||
git: "read-only",
|
||||
mutation: "planning-artifacts-only",
|
||||
},
|
||||
build: {
|
||||
id: "build",
|
||||
normal: {
|
||||
id: "normal",
|
||||
permissionProfile: "normal",
|
||||
filesystem: "workspace-write",
|
||||
network: "allowed",
|
||||
|
|
@ -57,7 +58,8 @@ export const EXECUTION_POLICY_PROFILES = {
|
|||
* Consumer: tool-call classifiers and future typed headless events.
|
||||
*/
|
||||
export function resolveExecutionPolicyProfile(profileId) {
|
||||
return EXECUTION_POLICY_PROFILES[profileId] ?? EXECUTION_POLICY_PROFILES.plan;
|
||||
const id = resolvePermissionProfile(profileId);
|
||||
return EXECUTION_POLICY_PROFILES[id] ?? EXECUTION_POLICY_PROFILES.restricted;
|
||||
}
|
||||
|
||||
function classifyBashRisk(command) {
|
||||
|
|
@ -89,7 +91,7 @@ function classifyBashRisk(command) {
|
|||
*/
|
||||
export function classifyExecutionPolicyCall(profileId, toolName, input = "") {
|
||||
const profile = resolveExecutionPolicyProfile(profileId);
|
||||
if (profile.id === "plan") {
|
||||
if (profile.id === "restricted") {
|
||||
const queueDecision = shouldBlockQueueExecutionInSnapshot(
|
||||
{
|
||||
verifiedDepthMilestones: [],
|
||||
|
|
@ -154,7 +156,7 @@ export function extractExecutionPolicyInput(toolName, input) {
|
|||
* the comparison-survey direction from Codex/Crush without changing runtime
|
||||
* permissions in this slice.
|
||||
*
|
||||
* Consumer: SF auto-mode tool_call hook.
|
||||
* Consumer: SF autonomous mode tool_call hook.
|
||||
*/
|
||||
export function buildExecutionPolicyJournalEntry(args) {
|
||||
const input = extractExecutionPolicyInput(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export function registerExitCommand(pi, deps = {}) {
|
|||
pi.registerCommand("exit", {
|
||||
description: "Exit SF gracefully",
|
||||
handler: async (_args, ctx) => {
|
||||
// Stop auto-mode first so locks and activity state are cleaned up before shutdown.
|
||||
// Stop autonomous mode first so locks and activity state are cleaned up before shutdown.
|
||||
// Wrapped in try/catch: if sf-run was updated on disk mid-session, the dynamic
|
||||
// import may resolve a new auto-worktree.js whose static imports reference
|
||||
// exports absent from the process-cached native-git-bridge.js (ESM cache is
|
||||
|
|
@ -16,7 +16,7 @@ export function registerExitCommand(pi, deps = {}) {
|
|||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
ctx.ui?.notify?.(
|
||||
`Auto-mode cleanup skipped (module version mismatch): ${msg}`,
|
||||
`Autonomous mode cleanup skipped (module version mismatch): ${msg}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* SF Forensics — Post-mortem investigation of auto-mode failures
|
||||
* SF Forensics — Post-mortem investigation of autonomous mode failures
|
||||
*
|
||||
* Programmatically scans activity logs, metrics, crash locks, and doctor
|
||||
* diagnostics for anomalies, then hands a structured report to the LLM
|
||||
|
|
@ -130,7 +130,7 @@ async function writeForensicsDedupPref(ctx, enabled) {
|
|||
export async function handleForensics(args, ctx, pi) {
|
||||
if (isAutoActive()) {
|
||||
ctx.ui.notify(
|
||||
"Cannot run forensics while auto-mode is active. Stop auto-mode first.",
|
||||
"Cannot run forensics while autonomous mode is active. Stop autonomous mode first.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
|
|
@ -146,7 +146,7 @@ export async function handleForensics(args, ctx, pi) {
|
|||
problemDescription =
|
||||
(await ctx.ui.input(
|
||||
"Describe what went wrong:",
|
||||
"e.g. auto-mode got stuck on task T03",
|
||||
"e.g. autonomous mode got stuck on task T03",
|
||||
)) ?? "";
|
||||
}
|
||||
if (!problemDescription?.trim()) {
|
||||
|
|
@ -397,7 +397,7 @@ const MAX_JOURNAL_RECENT_EVENTS = 20;
|
|||
/**
|
||||
* Intelligently scan journal files for forensic summary.
|
||||
*
|
||||
* Journal files can be huge (thousands of JSONL entries over weeks of auto-mode).
|
||||
* Journal files can be huge (thousands of JSONL entries over weeks of autonomous mode).
|
||||
* Instead of loading all entries into memory:
|
||||
* - Only fully parse the most recent N daily files (event counts, flow tracking)
|
||||
* - Line-count older files for approximate totals (no JSON parsing)
|
||||
|
|
@ -772,7 +772,7 @@ function detectJournalAnomalies(journal, anomalies) {
|
|||
type: "journal-stuck",
|
||||
severity: stuckCount >= 3 ? "error" : "warning",
|
||||
summary: `Journal recorded ${stuckCount} stuck-detected event(s)`,
|
||||
details: `The auto-mode loop detected it was stuck ${stuckCount} time(s). Check journal events for flow IDs and causal chains to trace the root cause.`,
|
||||
details: `The autonomous mode loop detected it was stuck ${stuckCount} time(s). Check journal events for flow IDs and causal chains to trace the root cause.`,
|
||||
});
|
||||
}
|
||||
// Detect guard-block events (dispatch was blocked by a guard)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* git-self-heal.ts — Automated git state recovery utilities.
|
||||
*
|
||||
* Four synchronous functions for recovering from broken git state
|
||||
* during auto-mode operations. Uses only `git reset --hard HEAD` —
|
||||
* during autonomous mode operations. Uses only `git reset --hard HEAD` —
|
||||
* never `git clean` (which would delete untracked .sf/ dirs).
|
||||
*
|
||||
* Observability: Each function returns structured results describing
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ export function readIntegrationBranch(basePath, milestoneId) {
|
|||
/**
|
||||
* Persist the integration branch for a milestone.
|
||||
*
|
||||
* Called when auto-mode starts on a milestone. Records the branch the user
|
||||
* Called when autonomous mode starts on a milestone. Records the branch the user
|
||||
* was on at that point, so the milestone worktree merges back to the correct
|
||||
* branch. Idempotent when the branch matches; updates the record when the
|
||||
* user starts from a different branch.
|
||||
|
|
@ -214,7 +214,7 @@ export function writeIntegrationBranch(basePath, milestoneId, branch) {
|
|||
// Validate
|
||||
if (!VALID_BRANCH_NAME.test(branch)) return;
|
||||
// Skip if already recorded with the same branch (idempotent across restarts).
|
||||
// If recorded with a different branch, update it — the user started auto-mode
|
||||
// If recorded with a different branch, update it — the user started autonomous mode
|
||||
// from a new branch and expects slices to merge back there (#300).
|
||||
const existingBranch = readIntegrationBranch(basePath, milestoneId);
|
||||
if (existingBranch === branch) return;
|
||||
|
|
@ -622,7 +622,7 @@ export class GitServiceImpl {
|
|||
) {
|
||||
return this.prefs.main_branch;
|
||||
}
|
||||
// Check milestone integration branch — recorded when auto-mode starts
|
||||
// Check milestone integration branch — recorded when autonomous mode starts
|
||||
if (this._milestoneId) {
|
||||
const resolved = resolveMilestoneIntegrationBranch(
|
||||
this.basePath,
|
||||
|
|
@ -634,10 +634,10 @@ export class GitServiceImpl {
|
|||
}
|
||||
const wtName = detectWorktreeName(this.basePath);
|
||||
if (wtName) {
|
||||
// Auto-mode worktrees use milestone/<MID> branches (wtName = milestone ID)
|
||||
// Autonomous mode worktrees use milestone/<MID> branches (wtName = milestone ID)
|
||||
const _milestoneBranch = `milestone/${wtName}`;
|
||||
const currentBranch = nativeGetCurrentBranch(this.basePath);
|
||||
// If we're on a milestone/<MID> branch, use it (auto-mode case)
|
||||
// If we're on a milestone/<MID> branch, use it (autonomous mode case)
|
||||
if (currentBranch.startsWith("milestone/")) {
|
||||
return currentBranch;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
* SF Queue Management — showQueue, reorder, add, and context builder.
|
||||
*
|
||||
* Self-contained queue UI extracted from guided-flow.ts.
|
||||
* Safe to run while auto-mode is executing — only writes to future milestone
|
||||
* directories (which auto-mode won't touch until it reaches them).
|
||||
* Safe to run while autonomous mode is executing — only writes to future milestone
|
||||
* directories (which autonomous mode won't touch until it reaches them).
|
||||
*/
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { showNextAction } from "../shared/tui.js";
|
||||
|
|
@ -27,15 +27,15 @@ import { deriveState } from "./state.js";
|
|||
/**
|
||||
* Queue future milestones via conversational intake.
|
||||
*
|
||||
* Safe to run while auto-mode is executing — only writes to future milestone
|
||||
* directories (which auto-mode won't touch until it reaches them) and appends
|
||||
* Safe to run while autonomous mode is executing — only writes to future milestone
|
||||
* directories (which autonomous mode won't touch until it reaches them) and appends
|
||||
* to project.md / queue.md.
|
||||
*
|
||||
* The flow:
|
||||
* 1. Build context about all existing milestones (complete, active, pending)
|
||||
* 2. Dispatch the queue prompt — LLM discusses with the user, assesses scope
|
||||
* 3. LLM writes CONTEXT.md files for new milestones (no roadmaps — JIT)
|
||||
* 4. Auto-mode picks them up naturally when it advances past current work
|
||||
* 4. Autonomous mode picks them up naturally when it advances past current work
|
||||
*
|
||||
* Root durable artifacts use uppercase names like PROJECT.md and QUEUE.md.
|
||||
*/
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue