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
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,24 @@ That file sits in your quest log, a tree under `quest/`:
quest/
README.md # The roadmap
m0/
README.md # First milestone, in priority order
README.md # The first milestone
export.md # Ready
download.md # Required: export.md
```

Folders are **epics**. A README's `Required` lists its children in
priority order, beside any other dependencies. The README is the epic's
Folders are **epics**, and the quests in a folder are its children; its
README lists only what blocks it, never its children. The README is the epic's
own quest, and it becomes ready once its children have merged. The top folders
(`m0`, `m1`, ...) are **milestones** that give the work a delivery horizon.
Finishing a quest deletes its file and edits no README, so PRs that finish
sibling quests do not conflict.

When a quest is complete, its PR removes the plan and the links that depended
on it, so the quests that required it can become ready. A PR that leaves work
rewrites the plan to what remains. Milestones are never
deleted, so a PR that finishes one (its README has no `Required` left) removes
the links to that milestone too; `quest check` flags any it misses. The
deleted, so a PR that finishes one (its folder holds no other quest and its
README has no `Required` left) removes the links to that milestone too;
`quest check` flags any it misses. The
finished plan stays in Git history, beside the code that completed it.

Work is taken through a live runner lock or an open change on the forge.
Expand Down Expand Up @@ -119,7 +122,8 @@ nonzero exit means the command failed.
## Prioritize ready work

`quest unblocked` ranks ready quests by milestone priority, descending number
of distinct transitive dependents, then existing tree order. Counts include
of distinct transitive dependents, then path order, which compares numbers by
value so `m10` follows `m9`. Counts include
later milestones without moving their candidates ahead of earlier milestones.
Structural root, milestone, and epic membership does not add to the count;
real dependencies on an epic still reach its remaining work. Other prerequisites
Expand Down Expand Up @@ -284,7 +288,8 @@ just finished, but keeps the root's listing of the milestone.
Both stage the change, print one line per file (`R from -> to`, `D path`, or
`M path`), and report on stderr any other mention of the path, such as prose
or files outside `quest/`, without editing it. They run `quest check` on the
result and exit 1 if it fails, for example when a quest moved out of its epic.
result and exit 1 if it fails, for example when a quest moved into a folder
with no README.

An epic is its directory or README; removing one that holds other quests needs
`-r`. The root and milestones are never moved or removed. `mv` refuses a
Expand All @@ -295,8 +300,8 @@ Neither command touches branches or PRs.

## Merge quest lists

Concurrent PRs edit the same link lists: the base branch deletes finished
quests' lines while a branch inserts a new quest beside them. `quest init`
Concurrent PRs can edit the same link list: the base branch deletes a finished
blocker's line while a branch adds another blocker beside it. `quest init`
routes `quest/**` through a merge driver in `.gitattributes` and registers it
in the clone's git config, so a local `git merge` resolves those conflicts.

Expand Down Expand Up @@ -1649,7 +1654,7 @@ Automatic project history passes read updates since their prior issue/PR
boundaries, including timestamp ties. Explicit Refresh starts or resumes a full
history pass; successive refreshes repair cached issues deleted without a
webhook. Open draft bodies are read only to classify `## Waiting`.
[The first milestone](quest/m0/README.md) lists the quests that remain.
[The first milestone](quest/m0/) holds the quests that remain.

## Contributing

Expand Down
18 changes: 9 additions & 9 deletions assets/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ GitHub issues remain the untrusted public front door, while quests provide durab

A **quest** is a Markdown file, completed in one PR.

An **epic** is a directory that consists of multiple quests.
The `README.md` file is the quest and the `Required` section lists the children.
An **epic** is a directory of quests.
Its children are the quest files in it and the epics one level down; its `README.md` is the epic's own work and lists none of them.
It completes when its own work is done and every child has merged.

The root's entries are special epics called **milestones** (`m0`, `m1`, ...) that group work by priority horizon.
The directories under `quest/` are special epics called **milestones** (`m0`, `m1`, ...) that group work by priority horizon.
Milestones are permanent.

Quests are linked with root-absolute paths.
Expand Down Expand Up @@ -45,9 +45,8 @@ Current decisions, open questions, or implementation guidance.

## Required

- [Child quest](/quest/foo/bar.md) - the outcome, so the list reads without opening it
- [Nested epic](/quest/foo/baz/README.md) - what the whole epic delivers
- [Blocker](/quest/bar.md) - work that must finish before this can start
- [Blocker](/quest/foo/bar.md) - what it delivers that this needs, so the list reads without opening it
- [Epic blocker](/quest/baz/README.md) - an epic finishes with all of its children

## Closes

Expand All @@ -63,14 +62,15 @@ The `Goal` section is required; everything else is optional. Use these exact hea
Size the title `[XS]` to `[XL]` by the agent's effort to implement, verify, and land it, judgment included.
Much code that is mechanical stays small, such as applying one rename the same way across the tree, sized XS.
Little code that is mostly judgment is larger, such as a 30-line rewording of a skill, sized M rather than XS.
An epic carries no size until its last child is removed; its README is then the epic's remaining work.
An epic carries no size until its last child is deleted; its README is then the epic's remaining work.

`Waiting` lists what the work waits on outside the tree; each item opens with who acts, `Maintainer:` or the outside party, before a colon.
A decision item gives its options and a recommendation.
Remove an item once its condition clears, and the heading with its last item.

`Required` lists the quests that must finish before the work starts.
The list is ordered by priority, inserted at rank.
`Required` lists the quests that must finish before the work starts; an epic's never lists its own quests, which it already waits for.
A child that must precede a sibling is in that sibling's `Required`.
Nothing else orders work within a milestone: `quest unblocked` ranks it by downstream impact, then path.
When the section is empty, delete it; the quest is now unblocked.
`Required` must be acyclic.

Expand Down
6 changes: 3 additions & 3 deletions assets/skills/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ description: Audit and fix outstanding quests for disagreements, conflicts, misa

Before you begin, run `quest guide` and `quest skill plan`, and read their output completely.

The goal is a quest tree that agrees with itself and the code, ordered by priority.
The goal is a quest tree that agrees with itself and the code, with each quest in the milestone its priority calls for.
The scope is every outstanding quest; the argument (if provided) filters it to specific quests or epics.

Look for:
- Disagreements: quests whose goals or plans contradict each other, the code, or the repository's docs.
- Conflicts: quests that would change the same code or interface incompatibly, or that must land in order without a `Required` link.
- Misaligned priorities: a quest ranked or placed in a milestone ahead of work it depends on, or behind work it blocks.
- Misaligned priorities: a quest placed in a milestone ahead of work it depends on, or behind work it blocks.
- Stale plans: work already done, blockers that have cleared, or references to code that no longer exists.
- Obsolete quests: a problem that is no longer real, or a plan a later change or decision has made the wrong solution.

Split the reading across parallel sub-agents, each given a slice of the scope and returning its findings with evidence (a path, line, or commit).

Apply factual corrections and changes the user has already decided. Moving a quest between milestones or changing its rank is always the user's decision, never a factual correction. Use `/quest-plan` only for unresolved scope, priority, API, or other consequential choices; recommendations are not decisions. Once those choices are settled, apply them without another approval round.
Apply factual corrections and changes the user has already decided. Moving a quest between milestones is always the user's decision, never a factual correction. Use `/quest-plan` only for unresolved scope, priority, API, or other consequential choices; recommendations are not decisions. Once those choices are settled, apply them without another approval round.

Move and delete quests with `quest mv` and `quest rm`, which update the links to them; edit any mentions they report, run `quest check`, and follow the repository's PR workflow within the user's authorized scope. Report the changes with their evidence and any decisions still open. An explicit read-only audit request leaves the tree unchanged.
2 changes: 1 addition & 1 deletion assets/skills/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Once complete, create, update, or delete the relevant quests and epics.
Record each settled decision and its reason in the quest's Plan, so later sessions don't ask it again, and the decision prompts in the PR.
A fix quest's Plan names the PR that caused the problem when it is known, so its implementer lists it under `## Caused by` without searching again.
When the PR deletes the quest, record its decisions in the PR body's `## Decisions` section instead, since the deletion takes the Plan with it.
New work joins the milestone the user picked, at its rank; an epic groups related quests, and its README holds the work no child owns (the end-to-end test, the docs page).
New work joins the milestone the user picked, in its directory or an epic's there; an epic groups related quests, and its README holds the work no child owns (the end-to-end test, the docs page).

When done, commit, open the PR as a draft, and run `/quest-review` on it, which reviews the branch, records the `## Review` section, and marks the PR ready.
Quests planned as follow-ups of an open PR may be committed to that PR instead, whose next push is reviewed as `quest context`'s Reviews section describes.
6 changes: 3 additions & 3 deletions assets/skills/recurring.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ Before you begin, run `quest guide` and `quest skill plan`, and read their outpu
Run the plan interview for a quest that recurs: the PR that finishes one run creates the next.
The argument (if provided) is the work to repeat, or an existing quest to make recurring.

Agents working the quest never load this skill, so its Plan says, in its own words, where the next run goes, what gates it, and that it's linked from its parent's `Required`.
Agents working the quest never load this skill, so its Plan says, in its own words, where the next run goes and what gates it.
The next run keeps the Goal and recurrence, and its Plan carries forward what this run learned (baselines, results, what to check first).

Also settle in the interview:

- Where the next run goes. If milestones only grow, it's the same path in the next milestone (`m1/perf/audit.md` to `m2/perf/audit.md`), created with any missing milestone or epic. If they shift up after a release, it's wherever the next run belongs after the shift.
- What gates it. Rank doesn't delay a quest: one with no `Required` is ready at once. Gate it on another recurring quest (such as a release cut) or a condition quest. A quest can't require its own milestone, so to run at the end of mN, the gate requires mN and the next run requires the gate.
- What each run delivers, where it ranks, and whether anything ends the recurrence.
- What gates it. Placement doesn't delay a quest: one with no `Required` is ready at once. Gate it on another recurring quest (such as a release cut) or a condition quest. A quest can't require its own milestone, so to run at the end of mN, the gate requires mN and the next run requires the gate.
- What each run delivers, and whether anything ends the recurrence.

A recurring gate's Plan also says to remove its path from every `Required` before re-creating it, or its dependents never clear.
Findings that need their own work are follow-ups for `/quest-plan`.
2 changes: 1 addition & 1 deletion assets/skills/spawn.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Before you begin, run `quest guide` and read its output completely.

Your goal is to execute many quests in parallel.
The scope consists of all ready quests that are not claimed.
Run `quest unblocked --unclaimed --json` for unclaimed ready candidates in milestone priority, descending downstream impact, then tree order.
Run `quest unblocked --unclaimed --json` for unclaimed ready candidates in milestone priority, descending downstream impact, then path order.
The count is distinct transitive dependents; other prerequisites may still remain.
Use that order when dispatching.
As each agent reports, rerun `quest unblocked --unclaimed --json`, which reads the upstream tree afresh, and start the newly ready quests you would recommend, within the harness's agent limit: merges elsewhere and released claims make more work ready while the spawn runs.
Expand Down
27 changes: 5 additions & 22 deletions cloudflare/dev/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,11 @@ export function world(): World {
{ path: "flake.nix", content: "{ outputs = _: { }; }\n" },
{
path: "quest/README.md",
content: quest("Quests", "Plan the rocket app.", [
"- [M0: Beta](/quest/m0/README.md) - the first public beta",
"- [M1: Later](/quest/m1/README.md) - after the beta",
]),
content: quest("Quests", "Plan the rocket app."),
},
{
path: "quest/m0/README.md",
content: quest("Beta", "Ship the first public beta.", [
"- [Landing page](/quest/m0/landing.md) - explain the app",
"- [Passkey sign-in](/quest/m0/sign-in.md) - sign in without a password",
"- [Getting started](/quest/m0/docs.md) - a first-run guide",
"- [Crash telemetry](/quest/m0/telemetry.md) - learn about crashes",
"- [ZIP export](/quest/m0/export.md) - take your data with you",
"- [ZIP import](/quest/m0/import.md) - bring your data back",
]),
content: quest("Beta", "Ship the first public beta."),
},
{ path: "quest/m0/import.md", content: quest("[S] Import a project from a ZIP archive", "Restore it.") },
{ path: "quest/m0/landing.md", content: quest("[S] Build the landing page", "Explain the app.") },
Expand All @@ -63,12 +53,7 @@ export function world(): World {
{ path: "quest/m0/export.md", content: quest("[S] Export a project as a ZIP archive", "Take your data.") },
{
path: "quest/m1/README.md",
content: quest("Later", "Work after the beta.", [
"- [Offline mode](/quest/m1/offline.md) - keep working without a network",
"- [Themes](/quest/m1/themes.md) - custom colors",
"- [Search](/quest/m1/search.md) - find anything",
"- [Fonts](/quest/m1/fonts.md) - choose the editor font",
]),
content: quest("Later", "Work after the beta."),
},
{ path: "quest/m1/fonts.md", content: quest("[S] Choose the editor font", "Read comfortably.") },
{ path: "quest/m1/search.md", content: quest("[S] Search across projects", "Find anything.") },
Expand Down Expand Up @@ -201,13 +186,11 @@ export function world(): World {
files: [
{
path: "quest/README.md",
content: quest("Quests", "Retire the old site.", ["- [M0: Retire](/quest/m0/README.md) - retire it"]),
content: quest("Quests", "Retire the old site."),
},
{
path: "quest/m0/README.md",
content: quest("Retire", "Retire the old site.", [
"- [Clean up](/quest/m0/cleanup.md) - remove old pages",
]),
content: quest("Retire", "Retire the old site."),
},
{ path: "quest/m0/cleanup.md", content: quest("[S] Remove the old pages", "Nothing links to them.") },
],
Expand Down
53 changes: 19 additions & 34 deletions cloudflare/src/board/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
branchQuest,
children,
comparePaths,
entries,
isEpic,
parse,
permanent,
Expand Down Expand Up @@ -45,7 +44,7 @@ export type Quest = {
/** Finished work out of all work, by count and weighted by difficulty. */
export type Progress = { done: number; total: number; weight: number; totalWeight: number };

/** A milestone or epic: a directory of quests, listed in priority order. */
/** A milestone or epic: a directory of quests, listed in path order. */
export type Group = {
path: string;
href: string;
Expand Down Expand Up @@ -171,7 +170,7 @@ export function readBoard(project: Project, jobs: ReadonlyMap<string, string> =

const progress = (path: string): Progress => {
const under = (p: string) => p.startsWith(`${dir(path)}/`);
const remaining = parsed.filter((doc) => under(doc.path) && !isEpic(doc));
const remaining = parsed.filter((doc) => under(doc.path) && !isEpic(parsed, doc));
const done = finishedUnder(path);
const weigh = (titles: string[]) => titles.reduce((sum, text) => sum + WEIGHTS[title(text).size ?? "XS"], 0);
const weight = weigh(done.map((c) => c.title));
Expand All @@ -183,21 +182,17 @@ export function readBoard(project: Project, jobs: ReadonlyMap<string, string> =
};
};

// A group's items are its children in priority order, then anything under
// it that no README lists, so nothing in the tree goes unseen.
// A group's items are its children: the quests in its directory, in path order.
const seen = new Set<string>([ROOT]);
const group = (doc: Doc): Group => {
seen.add(doc.path);
const listed = children(doc).filter((path) => docs.has(path) && !seen.has(path));
const base = dir(doc.path);
const loose = parsed
.map((d) => d.path)
.filter((path) => !listed.includes(path) && !seen.has(path) && direct(base, path));
const items = [...listed, ...loose].flatMap((path): Item[] => {
const items = children(parsed, doc).flatMap((path): Item[] => {
if (seen.has(path)) return [];
seen.add(path);
const child = docs.get(path)!;
return isEpic(child) ? [{ kind: "epic", epic: group(child) }] : [{ kind: "quest", quest: quest(child) }];
return isEpic(parsed, child)
? [{ kind: "epic", epic: group(child) }]
: [{ kind: "quest", quest: quest(child) }];
});
return {
path: doc.path,
Expand All @@ -208,28 +203,26 @@ export function readBoard(project: Project, jobs: ReadonlyMap<string, string> =
};
};

const root = docs.get(ROOT);
const milestonePaths = [...(root ? children(root) : []), ...parsed.map((doc) => doc.path)].filter(
(path, i, all) => path !== ROOT && permanent(path) && docs.has(path) && all.indexOf(path) === i,
);
const milestones = milestonePaths.map((path): Milestone => {
const segment = path.split("/")[1];
return {
...group(docs.get(path)!),
number: /^[a-z](\d+)$/.exec(segment)?.[1] ?? segment,
summary: lead(contents.get(path) ?? "", "Goal"),
};
});
const milestones = parsed
.filter((doc) => doc.path !== ROOT && permanent(doc.path))
.map((doc): Milestone => {
const segment = doc.path.split("/")[1];
return {
...group(doc),
number: /^[a-z](\d+)$/.exec(segment)?.[1] ?? segment,
summary: lead(contents.get(doc.path) ?? "", "Goal"),
};
});

// Quests outside every milestone still show, in a group of their own.
const rest = parsed.filter((doc) => !seen.has(doc.path) && !isEpic(doc)).map(quest);
const rest = parsed.filter((doc) => !seen.has(doc.path) && !isEpic(parsed, doc)).map(quest);
if (rest.length > 0) {
milestones.push({
path: "quest",
href: questHref(project, ROOT),
title: "Unsorted",
number: "",
summary: "Quests that no milestone lists.",
summary: "Quests outside every milestone.",
progress: { done: 0, total: rest.length, weight: 0, totalWeight: 0 },
items: rest.map((quest) => ({ kind: "quest", quest })),
});
Expand Down Expand Up @@ -263,14 +256,6 @@ export function readBoard(project: Project, jobs: ReadonlyMap<string, string> =
};
}

/** Whether `path` sits directly in `dir`: a quest file, or an epic's README one level down. */
function direct(dir: string, path: string): boolean {
const rest = path.slice(dir.length + 1);
if (!path.startsWith(`${dir}/`)) return false;
const parts = rest.split("/");
return parts.length === 1 ? parts[0] !== "README.md" : parts.length === 2 && parts[1] === "README.md";
}

/** Only the quests with this status, dropping epics left empty. */
export function only(items: Item[], kind: Status["kind"]): Item[] {
return items.flatMap((item): Item[] => {
Expand Down
Loading
Loading