Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.0] - 2026-06-11

### Added
- **Goal mode — give the crew a verifiable goal instead of a one-shot task.** Flip the Goal toggle when starting a task and the chat stays open until the goal demonstrably holds:
- **Scoping.** The lead agent asks a short round of clarifying questions (tap-to-answer chips or free text), then locks the goal into a checklist of objectively checkable criteria. Locking starts the work immediately.
- **Live goal card.** A pinned card tracks every criterion's status. You can edit, add, or remove criteria at any time — changing the checklist under an open gate re-arms verification.
- **Independent verification gate.** When the crew declares the goal ready, a dedicated anonymous verifier re-checks every criterion in an isolated turn — fresh session, no conversation history, restricted capabilities — so the crew never grades its own work. Criteria the verifier doesn't cover stay unverified and hold the gate.
- **Auto-continue loop.** A held gate sends the crew straight back to work with the failed criteria attached, up to 5 automatic attempts per run; any message from you re-arms the budget.
- **Sign-off.** When every criterion verifies, accept to close the task — or send it back with notes, which resets the checklist and re-arms the gate. The crew can also re-declare readiness after follow-up work, re-running verification.
- The new-task composer now always leads with the Partner agent; the lead picker is gone.

### Changed
- Assistant-emitted protocol markers are now ignored inside Markdown code fences and nested blocks, are bounded in size, and survive CRLF output — quoting a marker can never trigger it.

### Fixed
- Send controls stay pinned to the right when the new-task toolbar wraps on narrow windows.

## [0.7.0] - 2026-06-01

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ The same skills folder runs across every provider, so you're never locked in. Ha
| Chat | A turn-by-turn thread. One in-flight response at a time; events are an append-only `events.jsonl`. |
| Worktree | An optional isolated git checkout for a chat. Toggle it on a new task and the crew works on its own `crew/…` branch without touching your working tree. |
| Handover | A marker an agent emits to pass the turn to a teammate, with a one-line brief. |
| Goal | An optional verifiable outcome for a task. The crew scopes it into a locked criteria checklist, and an independent verifier — fresh session, no chat history — must confirm every criterion before you sign off. |

The default crew ships with a **Partner**, an **Engineer**, a **Product Lead**, and a **Designer** — each owning a role, a model, and its own skills folder.

Expand Down
57 changes: 45 additions & 12 deletions daemon/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1045,16 +1045,36 @@ func (a *App) resolveWorkdir(projectID, chatID string) (string, error) {
return strings.TrimSpace(chatWorkdir(chat, project)), nil
}

// CreateChat creates a chat under a project. useWorktree is tri-state: nil
// falls back to the project default, otherwise it's an explicit request. When
// a worktree is wanted but the workdir isn't a git repo, an explicit request
// is rejected while a default-derived one silently falls back to no worktree.
//
// chatIDOverride lets a caller pre-allocate the chat's ID — the new-task UI
// supplies one so it can preview the exact worktree branch (crew/<id8>) before
// the chat exists. Ignored unless it is a single, syntactically safe value;
// otherwise a fresh ID is minted.
// ChatCreateOptions carries the optional New Task toggles for chat creation.
// UseWorktree is tri-state: nil falls back to the project default. ID lets a
// caller pre-allocate the chat's ID — the new-task UI supplies one so it can
// preview the exact worktree branch (crew/<id8>) before the chat exists.
// GoalMode seeds the chat in Goal mode (docs/goal-0610.md): the lead agent
// scopes the goal first and the crew iterates until every criterion verifies.
type ChatCreateOptions struct {
UseWorktree *bool
BaseRef string
GoalMode bool
ID string
}

// CreateChat creates a chat under a project. Kept for existing callers; new
// option-bearing callers use CreateChatWithOptions.
func (a *App) CreateChat(projectID, title, mainAgentID string, useWorktree *bool, baseRef string, chatIDOverride ...string) (model.ChatRecord, error) {
opts := ChatCreateOptions{UseWorktree: useWorktree, BaseRef: baseRef}
if len(chatIDOverride) > 0 {
opts.ID = chatIDOverride[0]
}
return a.CreateChatWithOptions(projectID, title, mainAgentID, opts)
}

// CreateChatWithOptions creates a chat under a project. When a worktree is
// wanted but the workdir isn't a git repo, an explicit request is rejected
// while a default-derived one silently falls back to no worktree. The ID
// override is ignored unless it is a syntactically safe, unused value.
func (a *App) CreateChatWithOptions(projectID, title, mainAgentID string, opts ChatCreateOptions) (model.ChatRecord, error) {
useWorktree := opts.UseWorktree
baseRef := opts.BaseRef
project, err := a.store.GetProject(projectID)
if err != nil {
return model.ChatRecord{}, a.mapError(err)
Expand All @@ -1072,14 +1092,14 @@ func (a *App) CreateChat(projectID, title, mainAgentID string, useWorktree *bool
want = *useWorktree
}
chatID := id.New()
if len(chatIDOverride) > 0 && safeChatID(chatIDOverride[0]) {
if safeChatID(opts.ID) {
// store.SaveChat upserts by ID (and would even move the chat across
// projects), so honoring an ID that already belongs to a chat would
// silently overwrite that record and orphan any worktree it held.
// Only take the client-supplied ID when it's actually free; on a
// collision keep the freshly minted one rather than clobber.
if _, err := a.store.GetChat(chatIDOverride[0]); err != nil {
chatID = chatIDOverride[0]
if _, err := a.store.GetChat(opts.ID); err != nil {
chatID = opts.ID
}
}
var binding *model.WorktreeBinding
Expand All @@ -1096,6 +1116,15 @@ func (a *App) CreateChat(projectID, title, mainAgentID string, useWorktree *bool
}

now := time.Now().UTC()
var goal *model.GoalState
if opts.GoalMode {
goal = &model.GoalState{
Phase: model.GoalPhaseScoping,
AttemptCap: model.GoalDefaultAttemptCap,
CreatedAt: now,
UpdatedAt: now,
}
}
record := model.ChatRecord{
ID: chatID,
ProjectID: project.ID,
Expand All @@ -1105,6 +1134,7 @@ func (a *App) CreateChat(projectID, title, mainAgentID string, useWorktree *bool
ParticipantAgentIDs: []string{mainAgentID},
Status: "active",
Worktree: binding,
Goal: goal,
Stream: model.ChatStreamState{
Status: "idle",
},
Expand Down Expand Up @@ -1219,6 +1249,9 @@ func (a *App) GetChat(id string) (model.ChatRecord, error) {
}

func (a *App) UpdateChat(chat model.ChatRecord) (model.ChatRecord, error) {
a.mu.Lock()
defer a.mu.Unlock()

current, err := a.store.GetChat(chat.ID)
if err != nil {
return model.ChatRecord{}, a.mapError(err)
Expand Down
Loading