quest

package
v0.3.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package quest is the quest file format: the load-bearing types plus parsing and schema validation of the quest document. A quest is JSON — the single source of truth — embedded in a self-contained HTML file inside a <script type="application/json" id="quest"> block. The terminal reads the JSON and renders a detail pane (render.go); a build step turns the same JSON into the browser HTML (build.go). Two renderers, one parsed model.

Stage 1 parses, validates, renders, and displays gates; it does not execute them (the gate runner is Stage 2). Status is human-owned and never set by an executing agent.

Index

Constants

View Source
const (
	BlockHeading = "heading"
	BlockText    = "text"
	BlockList    = "list"
	BlockCode    = "code"
	BlockRich    = "rich"
)

Block type discriminators for the body[] array.

View Source
const BeforePR = "pr"

BeforePR is the only non-empty gate position in Stage 1: a barrier the loop will not cross until the gate passes, holding PR creation.

View Source
const HomeEnv = "QUESTMASTER_HOME"

HomeEnv overrides the questmaster home directory (the parent of quests/). Defaults to ~/.questmaster. Authored quest docs live here, deliberately separate from the ephemeral session-state root (~/.questmaster-state).

View Source
const IDPrefix = "quest-"
View Source
const UnsortedProject = "Unsorted"

UnsortedProject is the trailing section label for quests with no project (e.g. quests authored before project stamping, or outside any git repo).

Variables

This section is empty.

Functions

func Approve

func Approve(q *Quest) error

Approve posts a quest to the board (active), from any state.

func AuthoringClause

func AuthoringClause() string

AuthoringClause is the briefing for a master or standalone session: how to create and write a conformant quest through qm, where quests live, that gates must be real checkable criteria, to run the validator, and that the author cannot post (approve) or close (mark done) a quest. Injected as persistent identity so any master/standalone is quest-authoring-aware.

func Build

func Build(q *Quest) ([]byte, error)

Build renders a quest to a self-contained, browser-openable HTML document. It is the sibling of the terminal renderer: the same body-block dispatch, HTML output. The canonical JSON is written verbatim into the <script type="application/json" id="quest"> block (the source of truth, machine-read back by Parse), the docs-style <meta> frontmatter is emitted so a future quest index can read the built files, and a collapsible Source panel pretty-prints the same JSON for in-browser audit. Runs on save and on `quest open`.

func ExtractJSON

func ExtractJSON(raw []byte) ([]byte, error)

ExtractJSON pulls the canonical quest JSON out of a quest HTML file. It does not unmarshal — see Parse. Returns an error if the quest script block is absent.

Build always emits the canonical block last, after the body — where a raw rich-html block could carry its own id="quest" script and shadow the real source of truth. So we take the LAST match, which is the guaranteed-canonical block, rather than the first.

func Home

func Home() string

Home resolves the questmaster home directory: $QUESTMASTER_HOME, else ~/.questmaster. Returns "" only when neither the override nor $HOME is set.

func MarkDone

func MarkDone(q *Quest) error

MarkDone turns a quest in (done), from any state.

func Marshal

func Marshal(q *Quest) ([]byte, error)

Marshal renders a Quest as canonical, human-readable JSON (two-space indent, HTML left unescaped so authored rich content stays legible). This is the editor buffer and the Source-panel text. For embedding in the <script> block, build.go neutralizes the closing-tag sequence.

func NewID added in v0.3.16

func NewID(timestamp int64) string

NewID formats the base ID for an auto-generated quest.

func NewIDWithSuffix added in v0.3.16

func NewIDWithSuffix(timestamp int64, suffix int) string

NewIDWithSuffix formats a collision-retry ID for an auto-generated quest.

func QuestsDir

func QuestsDir() string

QuestsDir is <home>/quests, the store root. Quests are never written into a repo and never committed.

func RenderDetail

func RenderDetail(q *Quest, runtime Runtime, width int) string

RenderDetail returns the read-only quest detail pane (no interactive focus). Pure and deterministic — golden-testable.

func RenderDetailFocused

func RenderDetailFocused(q *Quest, runtime Runtime, width int, focus DetailFocus) string

RenderDetailFocused renders the detail pane with an optional interactive focus on one row (a toggle gate or a related entry). The board passes an active focus when the pane has focus; qm quest view passes none. Layout: header (id + status), title, meta line, attached/adventurers line (from runtime), objective, definition of done, related, then the body.

func RenderDetailLines

func RenderDetailLines(q *Quest, runtime Runtime, width int, focus DetailFocus) ([]string, int)

RenderDetailLines is RenderDetailFocused split into lines plus the index of the focused interactive row's line (-1 when the pane is not focused). The board uses the index to paint a full-width selection background on that line, matching the list; the renderer itself draws no focus marker.

func RenderListRow

func RenderListRow(q *Quest, runtime Runtime, width int, mode ListTagMode) string

RenderListRow returns one quest-log list line: id, goal, and the right-column marker selected by mode. Fits width on a single line.

func RenderTrackerLine

func RenderTrackerLine(q *Quest, width int) string

RenderTrackerLine returns the tracker's per-session quest line: "⚑ id · goal", no status. Fits width.

func SetStatus

func SetStatus(q *Quest, to Status) error

SetStatus moves a quest to any valid human-owned state.

func Validate

func Validate(q *Quest) error

Validate enforces the quest schema (quest-format.md). It is the refuse-malformed gate: a quest that fails Validate must not be saved, and the error is fed back to the author (the refuse-and-re-engage loop). Errors are single-line and specific so an authoring session can self-correct. Malformed JSON is caught earlier, by Parse.

func Withdraw

func Withdraw(q *Quest) error

Withdraw sends a quest back to draft (wip), from any state.

func WorkingClause

func WorkingClause(q *Quest) string

WorkingClause is the per-session briefing for a session attached to a quest: the goal, the gates as the definition of done, the plan, and the working rules. qm seeds it as the opening prompt at spawn and injects it on attach. It hands the agent the parsed quest and the one hard rule that keeps status human-owned: you work to the gates, you cannot mark the quest done, and you can re-read the current quest at any time.

Types

type Adventurer added in v0.3.21

type Adventurer struct {
	ID    string
	Agent string
	// State is the hook-observed primary-pane state: working | done | blocked
	// | starting | unknown.
	State string
	// Since is when the current state began (WorkingSince for working,
	// otherwise the last state transition).
	Since time.Time
	// Loop is the session's armed loop marker, when present.
	Loop *LoopRuntime
}

Adventurer is one attached session's live activity, derived from the session scan at render time (never stored on the quest).

type Block

type Block struct {
	Type string `json:"type"`
	ID   string `json:"id,omitempty"`

	// heading
	Level int `json:"level,omitempty"`

	// heading, text, code
	Text string `json:"text,omitempty"`

	// list
	Ordered bool     `json:"ordered,omitempty"`
	Items   []string `json:"items,omitempty"`

	// code
	Lang string `json:"lang,omitempty"`

	// rich
	Format   string `json:"format,omitempty"`
	Fallback string `json:"fallback,omitempty"`
	Content  string `json:"content,omitempty"`
}

Block is one ordered body block. Its meaning is discriminated by Type; the fields a Type uses are documented per kind. Array order is document order, so structure is preserved for free. A single flat shape keeps the model simple and lets both renderers dispatch on Type. Unknown types are not an error — they degrade to Fallback at render time (forward compatibility).

  • heading: Level, Text
  • text: Text
  • list: Ordered, Items
  • code: Lang, Text
  • rich: Format, Fallback, Content

ID is an optional stable handle on any block, for future referencing.

type DetailFocus

type DetailFocus struct {
	Active bool
	Kind   DetailTargetKind
	Index  int
}

DetailFocus describes which interactive row the detail pane has focused. Active is false for read-only renders (qm quest view); the board sets it when the pane has focus. Shared by the board (navigation) and the renderer (highlight) so they agree on what is selected.

type DetailTarget

type DetailTarget struct {
	Kind  DetailTargetKind
	Index int
}

DetailTarget is one interactive row: a toggle gate or a related entry, addressed by its index into q.Gates / q.Related.

func DetailTargets

func DetailTargets(q *Quest) []DetailTarget

DetailTargets enumerates the interactive rows in display order: toggle gates (in gate order) then related entries. Auto gates are not interactive — their state is observed, not authored — so they are skipped.

type DetailTargetKind

type DetailTargetKind int

DetailTargetKind identifies an interactive row in the detail pane.

const (
	// TargetGate is a toggle gate (the only flippable gate kind).
	TargetGate DetailTargetKind = iota
	// TargetRelated is a related entry (openable in T12).
	TargetRelated
)

type FileStore

type FileStore struct {
	// contains filtered or unexported fields
}

FileStore is the on-disk quest store rooted at a quests directory (under the questmaster home — never a repo path). Files are self-contained, browser- openable HTML carrying the canonical JSON in a <script id="quest"> block.

func DefaultStore

func DefaultStore() *FileStore

DefaultStore returns a FileStore rooted at the resolved QuestsDir.

func NewStore

func NewStore(dir string) *FileStore

NewStore returns a FileStore rooted at dir. The directory is created lazily on Save.

func (*FileStore) Delete added in v0.3.20

func (s *FileStore) Delete(id string) error

Delete removes a quest file by id. The id must be a safe single path component (same guard as Load/Save). A missing quest is reported as not-found rather than surfacing the raw os error.

func (*FileStore) Dir

func (s *FileStore) Dir() string

Dir returns the store's root directory.

func (*FileStore) Exists

func (s *FileStore) Exists(id string) bool

Exists reports whether a quest file is present for id.

func (*FileStore) List

func (s *FileStore) List() ([]Quest, error)

List returns every parseable quest in the store, sorted by id. Files that fail to parse are skipped so one malformed quest never blanks the board.

func (*FileStore) Load

func (s *FileStore) Load(id string) (*Quest, error)

Load reads and parses a quest file by id. It does not re-validate the schema (the write path is the gate); callers wanting integrity Validate the result.

func (*FileStore) Path

func (s *FileStore) Path(id string) string

Path returns the absolute file path for a quest id, always under the store directory. An id that is not a safe single path component yields a path that Load/Save reject.

func (*FileStore) Save

func (s *FileStore) Save(q *Quest) error

Save validates the quest and, only if it conforms, rebuilds the HTML body from the canonical JSON (Build) and writes it to <dir>/<id>.html atomically (tmp + rename). A malformed quest is refused and the validation error returned, to be fed back to the author (refuse-and-re-engage). Quests are never written into a repo and never committed.

type Gate

type Gate struct {
	Name   string   `json:"name"`
	Type   GateType `json:"type"`
	Check  string   `json:"check,omitempty"`
	Before string   `json:"before,omitempty"`
	// Checked is the human-authored met-state of a toggle gate ([x] vs [ ]).
	// It is authored, so it lives in the JSON. Auto gates never carry it —
	// their results are observed, not authored, and live in the runtime sidecar.
	Checked bool `json:"checked,omitempty"`
}

Gate is a single done-criterion. Check is required iff Type == auto and must be empty for toggle. Before names the transition the gate guards ("" guards done; "pr" is a barrier before PR creation). The check grammar is authored and displayed on the quest, then executed by qm.

type GateType

type GateType string

GateType is the kind of a single done-criterion.

const (
	// GateAuto is a runnable check measured externally by the harness.
	GateAuto GateType = "auto"
	// GateToggle is a human checkbox; the honest home for anything unverifiable.
	GateToggle GateType = "toggle"
)

type ListTagMode added in v0.3.20

type ListTagMode int

ListTagMode selects the right-column marker for a list row.

const (
	// TagStatus shows the colour-coded status glyph (◆/○/●, ⚔ when attached).
	// Used by `quest ls`, which has no tabs to convey status.
	TagStatus ListTagMode = iota
	// TagAttached shows only the ⚔ on-it marker (blank otherwise). Used by the
	// board, where the selected status tab already conveys status.
	TagAttached
)

type LoopRuntime added in v0.3.13

type LoopRuntime struct {
	SessionID   string
	Iterations  int
	LastVerdict string
	// Phase is what the armed loop is doing right now: waiting | checking |
	// paused. Empty on markers written by older binaries.
	Phase string
}

LoopRuntime is the render-time view of an armed quest loop marker.

func (LoopRuntime) Label added in v0.3.13

func (l LoopRuntime) Label() string

Label returns the compact loop-mode indicator used by tracker and board.

type ProjectGroup added in v0.3.20

type ProjectGroup struct {
	Project string
	Quests  []Quest
}

ProjectGroup is one project section of the quest log: a project name (or UnsortedProject) and its quests, ordered by status then id.

func GroupByProject added in v0.3.20

func GroupByProject(quests []Quest) []ProjectGroup

GroupByProject sorts quests into project sections — alphabetical, with the project-less "Unsorted" section last — and within each section by status (active → wip → done) then id. The board and `quest ls` share it so both surfaces present the log identically.

type Quest

type Quest struct {
	ID      string `json:"id"`
	Title   string `json:"title"`
	Status  Status `json:"status"`
	Summary string `json:"summary"`
	Date    string `json:"date,omitempty"`
	// Agent is accepted when parsing legacy quests, but canonical save/build
	// output drops it. Display uses the attached session runtime instead.
	Agent   string        `json:"agent,omitempty"`
	Project string        `json:"project,omitempty"`
	Related []RelatedLink `json:"related,omitempty"`

	Gates []Gate  `json:"gates,omitempty"`
	Body  []Block `json:"body,omitempty"`
}

Quest is the parsed canonical JSON of a quest: docs-style frontmatter, the gates that are the definition of done, and the ordered body blocks. It is the single source of truth — the HTML body is generated from it and never parsed back.

func Parse

func Parse(raw []byte) (*Quest, error)

Parse reads the canonical JSON from a quest HTML file and unmarshals it into a Quest. The generated HTML body is never parsed back; only the JSON block is read. Parse does not validate the schema (Validate is the separate gate).

func ParseJSON

func ParseJSON(data []byte) (*Quest, error)

ParseJSON unmarshals bare canonical quest JSON into a Quest. Used by the edit flow, which re-reads the JSON a human edited in $EDITOR.

func Scaffold

func Scaffold(id, title, summary, date string) *Quest

Scaffold returns a minimal, schema-valid wip quest for `qm quest new`. It is born wip — only the Questmaster approves it to active — and carries placeholder content plus a safe toggle reminding the author to define the real gates via `qm quest edit`. title and summary fall back to safe placeholders so the scaffold always validates.

func (*Quest) Goal

func (q *Quest) Goal() string

Goal is the one-line objective. It is the summary; falls back to the title. Used by the working-clause brief.

type RelatedLink struct {
	Type  string `json:"type,omitempty"`
	Title string `json:"title"`
	URL   string `json:"url,omitempty"`
}

RelatedLink is a related ticket / PR / doc: a typed, titled, optionally linked reference. The HTML build renders it as an anchor; the terminal shows the title (and, later, the app can open the URL). For backward compatibility a bare JSON string decodes into a RelatedLink with just its Title set (see UnmarshalJSON in parse.go).

func (*RelatedLink) UnmarshalJSON

func (r *RelatedLink) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts either the structured object form ({"type","title","url"}) or a bare string (which becomes the Title), so older quests authored with plain string related entries still parse.

type Runtime

type Runtime struct {
	// Sessions are the session IDs currently attached to (on) the quest.
	Sessions []string
	// Adventurers is the per-session live activity for the attached sessions, in
	// Sessions order. Populated by the shared runtime scan; renderers fall
	// back to the bare Sessions line when it is empty.
	Adventurers []Adventurer
	// Agent is derived from the attached session's primary agent at render
	// time. Authored quest JSON does not decide the displayed agent.
	Agent string
	// Gates overlays observed auto-gate results (gate name → "pass"/"fail"/
	// "error") from the runtime sidecar. Empty until a check has run. Toggle
	// gates ignore this — their state is authored in the JSON.
	Gates map[string]string
	// GatesAt overlays each observed auto-gate result's run time, so the
	// renderer can show how fresh a verdict is.
	GatesAt map[string]time.Time
	// ObservedAt is when this runtime was derived. Durations and verdict ages
	// are computed against it, keeping the renderer pure (no global clock).
	ObservedAt time.Time
	// Loop is present when a visible foreground `qm quest loop` is armed for
	// one of the attached sessions.
	Loop *LoopRuntime
}

Runtime is the derived, render-time state of a quest — which sessions are on it — gathered from the session scan and injected at render time. It is never stored on the quest itself (one fact, one home: the file holds authored content + status; attachment lives on session state).

func (Runtime) Attached

func (r Runtime) Attached() bool

Attached reports whether any session is on the quest.

type Status

type Status string

Status is the authored, human-owned lifecycle state of a quest. A quest is born wip, approved to active by the Questmaster, and marked done once its gates hold. The agent never sets this.

const (
	// StatusWIP is a draft awaiting the Questmaster's review.
	StatusWIP Status = "wip"
	// StatusActive is posted to the board and attachable to a session.
	StatusActive Status = "active"
	// StatusDone is a turned-in quest the Questmaster has accepted.
	StatusDone Status = "done"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL