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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ blocked on QUEST-42 is now available for whoever walks in next.

```bash
guild quest brief "shipped retry in commit abc1234; QUEST-43 ready to start"
guild quest clear QUEST-42 --report "done, shipped in abc1234"
guild quest fulfill QUEST-42 --report "done, shipped in abc1234"
```

Tomorrow's agent — same project, maybe a different MCP client — opens
Expand Down
24 changes: 13 additions & 11 deletions docs/generated/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ Every `guild <verb>` subcommand generated from the live cobra tree.
- [`guild quest brief`](#guild-quest-brief) — write session-end handoff for the next agent
- [`guild quest campaign`](#guild-quest-campaign) — bulk-set campaign
- [`guild quest campfire`](#guild-quest-campfire) — save working state before compaction
- [`guild quest clear`](#guild-quest-clear) — complete a quest (cascades unblock)
- [`guild quest forfeit`](#guild-quest-forfeit) — release a claim without completing
- [`guild quest fulfill`](#guild-quest-fulfill) — fulfill a quest (cascades unblock)
- [`guild quest guild`](#guild-quest-guild) — per-project summary by campaign
- [`guild quest init`](#guild-quest-init) — register this repo as a guild project
- [`guild quest journal`](#guild-quest-journal) — append a task-scoped journal note
Expand Down Expand Up @@ -811,43 +811,45 @@ Save a structured working-state checkpoint (hypothesis, tried, next, token warni
| `--token-warning` | bool | `false` | context window is running low |
| `--tried` | stringArray | `[]` | approach tried (repeatable) |

## `guild quest clear`
## `guild quest forfeit`

complete a quest (cascades unblock)
release a claim without completing

**Usage**

```
guild quest clear QUEST_ID [flags]
guild quest forfeit QUEST_ID [flags]
```

Complete a quest. Report is REQUIRED — commit hash, files, remaining issues. Cascades unblock dependent quests.
Release a claimed quest back to the queue. Use when blocked or ceding to another agent. Only acts on status=in_progress quests — refuses on done and no-ops on next.

**Flags**

| flag | type | default | description |
| --- | --- | --- | --- |
| `--json` | bool | `false` | emit structured JSON result instead of formatted text |
| `--report` | string | `—` | completion report: commit hash, files, remaining issues |
| `--note` | string | `—` | reason the claim is being released (optional) |

## `guild quest forfeit`
## `guild quest fulfill`

release a claim without completing
fulfill a quest (cascades unblock)

**Usage**

```
guild quest forfeit QUEST_ID [flags]
guild quest fulfill QUEST_ID [flags]
```

Release a claimed quest back to the queue. Use when blocked or ceding to another agent. Only acts on status=in_progress quests — refuses on done and no-ops on next.
**Aliases:** clear

Fulfill a quest. Report is REQUIRED — commit hash, files, remaining issues. Cascades unblock dependent quests.

**Flags**

| flag | type | default | description |
| --- | --- | --- | --- |
| `--json` | bool | `false` | emit structured JSON result instead of formatted text |
| `--note` | string | `—` | reason the claim is being released (optional) |
| `--report` | string | `—` | completion report: commit hash, files, remaining issues |

## `guild quest guild`

Expand Down
43 changes: 39 additions & 4 deletions docs/generated/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# guild MCP tool catalog

Every tool a registered MCP client sees. 37 tools.
Every tool a registered MCP client sees. 38 tools.

## Tools

Expand Down Expand Up @@ -31,9 +31,10 @@ Every tool a registered MCP client sees. 37 tools.
- [`quest_bounties`](#quest_bounties) — On-demand session snapshot: brief, oath, echoes, top task, and parallel candidates.
- [`quest_brief`](#quest_brief) — Session-end briefing for the next agent.
- [`quest_campfire`](#quest_campfire) — Save a structured working-state checkpoint (hypothesis, tried, next, token warning) before the context compacts.
- [`quest_clear`](#quest_clear) — Complete a quest.
- [`quest_clear`](#quest_clear) — Fulfill a quest.
- [`quest_epic`](#quest_epic) — Set the campaign name on a batch of quests.
- [`quest_forfeit`](#quest_forfeit) — Release a claimed quest back to the queue.
- [`quest_fulfill`](#quest_fulfill) — Fulfill a quest.
- [`quest_guild`](#quest_guild) — Per-project quest summary grouped by campaign: counts of next, in-progress, blocked, and done quests.
- [`quest_journal`](#quest_journal) — Task-scoped scratchpad.
- [`quest_list`](#quest_list) — All open tasks.
Expand Down Expand Up @@ -914,7 +915,7 @@ _no arguments_

## `quest_clear`

Complete a quest. Report is REQUIRED — commit hash, files, remaining issues. Cascades unblock dependent quests.
Fulfill a quest. Report is REQUIRED — commit hash, files, remaining issues. Cascades unblock dependent quests.

_no arguments_

Expand All @@ -928,7 +929,7 @@ _no arguments_
"type": "string"
},
"quest_id": {
"description": "QUEST-NNN to mark complete",
"description": "QUEST-NNN to fulfill",
"type": "string"
},
"report": {
Expand Down Expand Up @@ -1022,6 +1023,40 @@ _no arguments_

</details>

## `quest_fulfill`

Fulfill a quest. Report is REQUIRED — commit hash, files, remaining issues. Cascades unblock dependent quests.

_no arguments_

<details><summary>Raw JSON schema</summary>

```json
{
"additionalProperties": false,
"properties": {
"project": {
"type": "string"
},
"quest_id": {
"description": "QUEST-NNN to fulfill",
"type": "string"
},
"report": {
"description": "specific completion report: commit hash, files, issues — REQUIRED",
"type": "string"
}
},
"required": [
"quest_id",
"report"
],
"type": "object"
}
```

</details>

## `quest_guild`

Per-project quest summary grouped by campaign: counts of next, in-progress, blocked, and done quests.
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/quest.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func init() {
// concern, not verb concern). See docs/architecture/COMMAND_REGISTRY.md.
deps := buildCLICommandDeps()
bindRegistryVerb(questCmd, quest.AcceptCommand, deps, "quest accept")
bindRegistryVerb(questCmd, quest.ClearCommand, deps, "quest clear")
bindRegistryVerb(questCmd, quest.FulfillCommand, deps, "quest fulfill")
bindRegistryVerb(questCmd, quest.ForfeitCommand, deps, "quest forfeit")
bindRegistryVerb(questCmd, quest.JournalCommand, deps, "quest journal")
bindRegistryVerb(questCmd, quest.BriefCommand, deps, "quest brief")
Expand Down
25 changes: 11 additions & 14 deletions internal/cli/quest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,15 @@ func TestCLI_QuestPostClearRoundTrip(t *testing.T) {
t.Errorf("post stdout = %q, want contains 'posted QUEST-1: hello world'", stdout)
}

// Clear.
// Fulfill (via the `clear` cobra alias to verify QUEST-106 backward compat).
stdout, _, err = runQuest(t, []string{"quest", "clear",
"--project", "guild-cli-test",
"QUEST-1", "--report", "commit abc"})
if err != nil {
t.Fatalf("clear: %v", err)
t.Fatalf("fulfill: %v", err)
}
// QUEST-45 unified clear output — was "quest cleared: X", now matches
// MCP's "cleared X" header with an indented unblocked block.
if !strings.Contains(stdout, "cleared QUEST-1") {
t.Errorf("clear stdout = %q, want to contain 'cleared QUEST-1'", stdout)
if !strings.Contains(stdout, "fulfilled QUEST-1") {
t.Errorf("fulfill stdout = %q, want to contain 'fulfilled QUEST-1'", stdout)
}
}

Expand All @@ -94,18 +92,17 @@ func TestCLI_QuestPostCascade(t *testing.T) {
"--depends-on", "QUEST-1", "B"}); err != nil {
t.Fatalf("post B: %v", err)
}
// Clear A — expect cascade 🔓 line for QUEST-2.
stdout, _, err := runQuest(t, []string{"quest", "clear", "-p", "guild-cli-casc", "QUEST-1"})
// Fulfill A — expect cascade line for QUEST-2. Using `fulfill` as
// primary verb (QUEST-106).
stdout, _, err := runQuest(t, []string{"quest", "fulfill", "-p", "guild-cli-casc", "QUEST-1"})
if err != nil {
t.Fatalf("clear A: %v", err)
t.Fatalf("fulfill A: %v", err)
}
// QUEST-45 unified clear output — cascade list is now indented
// under "unblocked:" rather than one "unblocked: <ID>" line.
if !strings.Contains(stdout, "cleared QUEST-1") {
t.Errorf("clear stdout missing cleared: %q", stdout)
if !strings.Contains(stdout, "fulfilled QUEST-1") {
t.Errorf("fulfill stdout missing fulfilled: %q", stdout)
}
if !strings.Contains(stdout, "QUEST-2") || !strings.Contains(stdout, "unblocked") {
t.Errorf("clear stdout missing cascade: %q", stdout)
t.Errorf("fulfill stdout missing cascade: %q", stdout)
}
}

Expand Down
7 changes: 5 additions & 2 deletions internal/mcp/budget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ const (
// registered tools (descriptions + input schemas). Raised to 4500
// in QUEST-101 to accommodate the 5 previously-deferred tools
// (lore_seal, lore_catalog, quest_epic, quest_active, quest_forfeit)
// now in the always-on tier. Expected actual: ~4100-4300 tokens.
totalMaxTokens = 4500
// now in the always-on tier. Raised to 4700 in QUEST-106 when
// quest_fulfill was added alongside quest_clear as a backward-compat
// alias — net +~100 tokens for the duplicate tool. Expected actual:
// ~4400-4600 tokens.
totalMaxTokens = 4700
)

// TestDescriptionBudget enforces the token budgets against the full
Expand Down
8 changes: 4 additions & 4 deletions internal/mcp/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ You are working with a persistent agent-memory (**lore**) and task-coordination

Together they make agents as autonomous as possible. The core loop runs without a human between tasks:

quest_bounties → quest_accept → work → quest_clear(report=...) → quest_bounties → ...
quest_bounties → quest_accept → work → quest_fulfill(report=...) → quest_bounties → ...

Every adventurer is transient; the guild is eternal. Your job is to take a bounty, do it well, inscribe what's worth keeping, brief the next arrival, and end cleanly.

Expand Down Expand Up @@ -132,12 +132,12 @@ If results are current, use them — do not re-research. If empty or stale, rese

**You finished a quest:**
```
quest_clear(
quest_fulfill(
quest_id="QUEST-42",
report="Fixed the race in retry budget accounting. Commit abc1234. Tests added in budget_test.go."
)
```
Report is REQUIRED. Be specific.
Report is REQUIRED. Be specific. `quest_clear(quest_id="...", report="...")` also works as a backward-compat alias.

**You hit a wall mid-quest and the context window is warning you:**
```
Expand Down Expand Up @@ -231,7 +231,7 @@ Use these shapes when you need a concrete schema:
quest_accept(quest_id="QUEST-7")
quest_journal(quest_id="QUEST-7", text="Found race in retry budget accounting.")
quest_campfire(quest_id="QUEST-7", hypothesis="budget race", next="inspect retry state")
quest_clear(quest_id="QUEST-7", report="done in abc123; tests added")
quest_fulfill(quest_id="QUEST-7", report="done in abc123; tests added")
quest_brief(text="Retry budget shipped; next session should profile p99 latency.")
quest_bounties()
quest_list(status="next")
Expand Down
5 changes: 3 additions & 2 deletions internal/mcp/instructions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import (
// direct edits to instructions.md do. See QUEST-57 for the dynamic
// build path and its separate tests.
//
// Last updated for the lazy-load tool-discovery note (Codex + Cursor).
const wantStaticSHA = "325854527015b8efd79ab5d9b2b672119d196633fd9fad51f357150813135d48"
// Last updated for QUEST-106: quest_clear → quest_fulfill rename with
// backward-compat alias note.
const wantStaticSHA = "4508848753ecaadb59348e720797424b1452469ab98ff8353893d5507a49481a"

func TestStaticInstructions_Embedded(t *testing.T) {
if staticInstructions == "" {
Expand Down
4 changes: 4 additions & 0 deletions internal/mcp/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ func registerAlwaysOn(s *sdkmcp.Server) {
quest.AcceptCommand.BindMCP(s, mcpDeps)
quest.JournalCommand.BindMCP(s, mcpDeps)
quest.CampfireCommand.BindMCP(s, mcpDeps)
quest.FulfillCommand.BindMCP(s, mcpDeps)
// quest_clear is kept as a backward-compat MCP alias (same handler,
// different tool name) so agents trained on the pre-QUEST-106 verb
// still work. Tool discovery surfaces both; new agents prefer fulfill.
quest.ClearCommand.BindMCP(s, mcpDeps)
quest.BriefCommand.BindMCP(s, mcpDeps)
quest.SummonCommand.BindMCP(s, mcpDeps)
Expand Down
1 change: 1 addition & 0 deletions internal/mcp/smoke_full_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var toolArgSpecs = map[string][]command.ArgSpec{
"quest_clear": quest.ClearCommand.Args,
"quest_epic": quest.EpicCommand.Args,
"quest_forfeit": quest.ForfeitCommand.Args,
"quest_fulfill": quest.FulfillCommand.Args,
"quest_guild": quest.GuildCommand.Args,
"quest_journal": quest.JournalCommand.Args,
"quest_list": quest.ListCommand.Args,
Expand Down
10 changes: 9 additions & 1 deletion internal/mcp/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ var expectedTools = []struct {
{"quest_bounties"},
{"quest_brief"},
{"quest_campfire"},
{"quest_clear"},
{"quest_clear"}, // backward-compat alias for quest_fulfill (QUEST-106)
{"quest_epic"},
{"quest_forfeit"},
{"quest_fulfill"},
{"quest_guild"},
{"quest_journal"},
{"quest_list"},
Expand Down Expand Up @@ -709,6 +710,8 @@ func minArgsFor(name string) map[string]any {
return map[string]any{"quest_id": "QUEST-1"}
case "quest_clear":
return map[string]any{"quest_id": "QUEST-1", "report": "r"}
case "quest_fulfill":
return map[string]any{"quest_id": "QUEST-1", "report": "r"}
case "quest_journal":
return map[string]any{"quest_id": "QUEST-1", "text": "t"}
case "quest_brief":
Expand Down Expand Up @@ -793,6 +796,11 @@ func smokeArgsFor(name string) map[string]any {
"quest_id": "QUEST-99999", "report": "r",
"project": "testproj",
}
case "quest_fulfill":
return map[string]any{
"quest_id": "QUEST-99999", "report": "r",
"project": "testproj",
}
case "quest_brief":
return map[string]any{"text": "t", "project": "testproj"}
case "quest_bounties":
Expand Down
12 changes: 8 additions & 4 deletions internal/quest/clear.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import (
"time"
)

// Clear marks taskID as status='done' and atomically runs the cascade-
// Clear is a backward-compat alias for Fulfill. Existing callers keep
// compiling; new code should call Fulfill directly. QUEST-106 / LORE-80.
var Clear = Fulfill

// Fulfill marks taskID as status='done' and atomically runs the cascade-
// unblock pass: every quest currently in status='blocked' whose full
// depends_on list is satisfied by the post-Clear done-set flips to
// depends_on list is satisfied by the post-Fulfill done-set flips to
// status='next'.
//
// Non-negotiable correctness (QUEST-9 spec): the cascade must find
Expand All @@ -28,7 +32,7 @@ import (
// payload) and as a `[completed] <report>` note in task_notes for
// chronological history. If `report` is empty, only the event fires
// (no note).
func Clear(ctx context.Context, db *sql.DB, projectID, taskID, report string) (*ClearResult, error) {
func Fulfill(ctx context.Context, db *sql.DB, projectID, taskID, report string) (*FulfillResult, error) {
if db == nil {
return nil, fmt.Errorf("quest: clear: nil db")
}
Expand Down Expand Up @@ -114,5 +118,5 @@ func Clear(ctx context.Context, db *sql.DB, projectID, taskID, report string) (*
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("quest: clear: commit: %w", err)
}
return &ClearResult{Cleared: cleared, Unblocked: unblocked}, nil
return &FulfillResult{Cleared: cleared, Unblocked: unblocked}, nil
}
Loading
Loading