Skip to content

feat: move and delete quests with quest mv and quest rm - #164

Merged
kixelated merged 8 commits into
mainfrom
quest/m0/helpers/move
Oct 21, 2026
Merged

kixelated merged 8 commits into
mainfrom
quest/m0/helpers/move

Conversation

@kixelated

@kixelated kixelated commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Completes quest/m0/helpers/move.md. Agents were running git rm or git mv and then fixing links with grep -r and sed -i (203 sessions since Oct 14).

Changes

  • quest mv <from> <to> moves a quest, or an epic directory and everything under it, with git mv. It then rewrites every root-absolute link to the moved paths, keeping fragments.
  • quest rm [-r] <path> deletes a quest or epic with git rm. It drops each Required, Related, or Closes entry that opens with a link to a deleted document, and removes a heading once its last entry is gone.
  • Both commands stage the change and print R from -> to, D path, or M path. On stderr they list every remaining mention of the old path (git grep, prose and files outside quest/ included) without editing it. They then run quest check and exit 1 if it fails.
  • mv refuses the root and milestones, as rm does, and a destination that already exists. It also refuses while any moved quest has a claim branch or an open PR from a branch of that name on origin or, when origin is a GitHub fork, on its parent repository, because the old branch would no longer name it.
  • rm refuses to remove the root or a milestone, and refuses an epic that holds other quests unless you pass -r.
  • Core (src/core/edit.ts): move and remove are pure edits of document text. parse now records each link's source span and each entry's last line, so edits work from the Markdown AST rather than regexes.
  • src/cli/claims.ts: the claim lookup is one ls-remote for refs/heads/quest/* plus one paginated open-PR listing. It is built so quest start can reuse it.
  • src/cli/github.ts: as the helpers epic decided, the GitHub client (auth, rate-limit retry, same-origin pagination) moves out of wait.ts now that a second caller uses it. wait behaves the same, and its tests pass unchanged.
  • The delete and audit skills now use quest rm and quest mv. The README has a new "Move and delete quests" section that matches --help.
  • rm also drops every Required link to a finished milestone (a milestone README with no ## Required left), including one this rm just emptied and links that were already stale. It repeats until nothing changes, so a milestone that only waited on another finishes in turn, and it keeps the root's own listing. It reuses finishedMilestone from src/core/rules.ts, now shared with the check rule from kixelated/quest#160.
  • The guide's Deletion section (assets/AGENTS.md) now says to use quest rm instead of grepping for the path (user-approved).
  • This PR deleted its own quest with quest rm. The command dropped the entries in the helpers README, and reported two prose links, which I fixed by hand. Merging main resolved the conflicts with kixelated/quest#160, which deleted quest/m0/milestone-links.md.

Decisions made (routine; recorded for review)

  1. When claims can't be checked fully. With no origin remote, mv warns and skips the claim check. If origin is not on GitHub, it checks only branches and warns. On GitHub it needs a token, the same as quest wait. A network failure fails the command.
  2. When -r is required. rm needs -r when it would remove more than one quest document. You can name an epic by its directory or by its README. An epic README with no children left is deleted like any other quest.
  3. What rm drops. Only list entries in Required, Related, or Closes whose opening link points at a deleted path. Bullets in Plan, mid-sentence links, and orphaned reference definitions are reported, not edited. A section is removed only once nothing but blank lines is left in it.
  4. Which links mv rewrites. Only root-absolute ones, which make up the dependency graph. Relative links in moved documents are left to quest check. If a link's destination is written differently from how it parses (escapes, entities), mv reports it instead of guessing.
  5. Check failures. A failing check after the change exits 1, and the change stays staged so you can fix it up. Example: a quest moved out of its epic.
  6. Remote. Claims are checked on origin and, when it is a GitHub fork, on its parent (branches through the API, plus open PRs). quest start plans --remote and can add it to the shared lookup.

Validation

  • just check: passed.
  • just test: passed. Vitest ran 254 tests, including 22 new ones in test/move.test.ts; the Worker suite ran 257. The lifecycle checks on both builds and the shell suites also passed.
  • New tests cover: dropping links to finished milestones with a cascade, a stale link, and the root's listing kept; moving a quest; moving an epic; deleting a quest listed under several Required and Related sections; deleting a milestone's last child drops another quest's link to it while the root still lists it; refusing an epic without -r; refusing a destination that exists; refusing to move a claimed quest (local bare remote); reporting mentions; open-PR pagination.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 10:21
`quest mv <from> <to>` moves a quest or epic with git mv and rewrites every
root-absolute link to it. `quest rm [-r] <path>` deletes one with git rm and
drops each Required, Related, or Closes entry that linked to it, with any
heading left empty. Both stage the change, report other mentions without
editing them, and run quest check on the result. mv refuses while a moved
quest has a claim branch on origin or an open PR on GitHub.

The link edits are pure functions in the core (src/core/edit.ts); the CLI
owns git. The GitHub client moves out of wait.ts into src/cli/github.ts so
the claim lookup can share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Owner Author

Outcome: quest/m0/helpers/move.md is done and this PR is open for review. just check and just test passed locally. The PR body lists the routine decisions I made unattended. Wiring finished-milestone link removal into quest rm stays with quest/m0/milestone-links.md, which is in progress in parallel. If that quest merges first, this PR must add the wiring before it merges. Suggested follow-up: change the guide's Deletion line in assets/AGENTS.md to point at quest rm.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T19:56:15.579273Z 322a3d1 New commits
🔒 Security Review ✅ Completed 2026-10-21T19:54:25.023477Z 322a3d1 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e00ba7183

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/move.ts Outdated
workTree(root);
const from = treePath(root, fromArg);
const to = treePath(root, toArg);
if (from === "quest" || from === ROOT) throw new Error("the root epic cannot move");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent moving permanent milestones

Reject milestone paths here as well as the root. With a source such as quest/m0, the command moves the entire milestone, rewrites the root entry, and can still pass quest check, even though the existing permanent() contract treats milestone READMEs as identities that outlive their contents. Guard permanent(directory ? ${from}/README.md : from) as removeQuest already does so quest mv cannot silently renumber or remove a milestone.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99a3071: mv now refuses the root and milestones through permanent(), the same guard rm uses, with a test.

(Written by Claude Opus 5.5)

Comment thread src/cli/move.ts Outdated
);
return {
changes: [`R ${from} -> ${to}`, ...modified(edited.texts)],
warnings: [...warnings, ...mentions(root, directory ? `${from}/` : from)],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Search the epic directory itself for stale mentions

When moving an epic directory, searching only for ${from}/ misses exact prose or documentation mentions such as quest/m0/drain followed by whitespace or punctuation—the same form shown in the new README command examples. Those stale mentions are neither rewritten nor reported, contrary to the command's promise to report every remaining mention; recursive removal has the same problem at line 104. Search for the directory path itself while applying a boundary that avoids matching sibling prefixes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99a3071: mentions now match the path itself, followed by a non-name character, a full stop, or the end of the line. That covers quest/m0/epic and quest/m0/epic/... but not quest/m0/epic-2 or quest/m0/epic.md. Applies to mv and rm -r, with a test.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 11:44
…594a39c02f8

# Conflicts:
#	quest/m0/helpers/move.md
#	quest/m0/landed-quests.md
#	quest/m0/milestone-links.md
quest rm now also drops every Required link to a finished milestone,
including one it just emptied and links that were already stale, and
repeats until nothing changes. The root's listing stays. It reuses the
finished-milestone predicate from the check rule (#160).

The guide's Deletion section points at quest rm instead of a grep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ebde166d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/claims.ts Outdated
if (repository === null) {
deps.warn(`${remote} is not on GitHub, so only its branches were checked for claims`);
} else {
for (const pr of await openPullRequests(new GitHub(deps.github()), repository)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Query the upstream repository for claims

When origin is the current user's fork, repository names that fork, so this API call only lists PRs opened against the fork. Another contributor's open quest/... PR against the upstream repository appears neither in this fork's remote branches nor in this PR listing, allowing quest mv to rename a quest that is actively claimed. Resolve and query the upstream repository rather than assuming origin is the PR base.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99a3071: when origin is a GitHub fork, claims also list open PRs in its parent repository, with a test. Branches are still checked on origin only. A branch pushed upstream with no PR yet is the brief window before its draft PR opens, and quest start plans --remote for that.

(Written by Claude Opus 5.5)

…aims

- mv refuses milestones as well as the root, as rm does.
- Mentions of a moved or removed epic match its directory path itself,
  not only paths under it, while skipping longer sibling names.
- In a fork's clone, claims also count open PRs in the repository the
  fork came from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99a307122b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/claims.ts
}
const wanted = new Set(branches);
const held = new Map<string, string | null>();
for (const branch of remoteBranches(root, remote)) if (wanted.has(branch)) held.set(branch, null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check upstream branches before allowing a move

When origin is a fork, this checks branch claims only in that fork. The new parent-repository lookup fixes the previously reported open-PR case, but a quest branch pushed directly to the upstream repository while its draft PR is being opened still appears in neither remoteBranches(origin) nor the open-PR results, so quest mv can rename actively claimed work. Query the resolved upstream repository's branches as well before allowing the move.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 73d51ad: when origin is a GitHub fork, claims now also include the parent's quest/* branches, listed with git/matching-refs, alongside its open PRs. This supersedes my earlier reply that left upstream branches out.

(Written by Claude Opus 5.5)

In a fork's clone, a quest branch pushed upstream while its draft PR is
opened is a claim too, so check the parent's quest/* branches through
the GitHub API alongside its open PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73d51adac0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/move.ts Outdated
const pattern = `${path.replace(/[.[\]()*+?{}|^$\\]/g, "\\$&")}\\.*([^A-Za-z0-9_.-]|$)`;
let out: string;
try {
out = execFileSync("git", ["-C", root, "grep", "-n", "-I", "-E", "-e", pattern], {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include untracked files when scanning stale mentions

When a newly created, untracked file outside quest/ mentions the moved or removed path, this scan silently omits it, so the command can report success while leaving a reference that becomes stale once that file is committed. git grep -h documents --untracked as “search in both tracked and untracked files”; add it here, preferably with --exclude-standard, so the implementation matches the documented promise to report mentions outside quest/.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0adfbf3: the mentions scan for mv and rm now runs git grep --untracked --exclude-standard, so unignored untracked files are reported and ignored ones are not, with a regression test.

(Written by Claude Opus 5.5)

main landed src/cli/github.ts with quest status. Keep one client: main's
GitHub class gains this PR's Repository, remoteUrl, githubRepository, and
the list/all REST pagination, which wait now uses too. claims.ts keeps its
API on top of it.

Also scan unignored untracked files for stale mentions in mv and rm, with
a regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0adfbf37a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/move.ts
Comment on lines +93 to +95
const inside = epic === null ? [] : docs.filter((doc) => relocate(doc.path, epic, epic) !== null);
const whole = epic !== null && (directory || inside.length > 1);
const doomed = whole ? inside : docs.filter((doc) => doc.path === target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject directories that contain no quest documents

When the target is an existing directory containing only non-quest files (for example, quest/m0/research/AGENTS.md or supporting data), inside and therefore doomed are empty, but whole is still true merely because the target is a directory. The command consequently reaches git rm -r, deletes every tracked file in that directory, reports no D entries, and can exit successfully; reject the target when doomed.length === 0 rather than treating any directory under quest/ as an epic.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 322a3d1: rm now refuses a target that holds no quest documents, before running anything, with a test on a directory holding only AGENTS.md.

(Written by Claude Opus 5.5)

Comment thread src/core/edit.ts Outdated
Comment on lines +141 to +142
function encode(path: string): string {
return path.split("/").map(encodeURIComponent).join("/");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Percent-encode parentheses in rewritten Markdown paths

When the destination contains an unmatched ( or ), such as quest/m0/a).md, encodeURIComponent leaves that character literal. The generated inline destination ](/quest/m0/a).md) is then parsed as a link to /quest/m0/a followed by text, so the command stages the rename while replacing valid references with malformed or unresolved ones. Encode parentheses explicitly when constructing Markdown destinations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 322a3d1: rewritten destinations now encode ( and ) as %28 and %29, which the link resolver decodes back, with a test.

(Written by Claude Opus 5.5)

An unmatched parenthesis in a destination path ended the rewritten inline
link early, so encode both as %28 and %29. rm -r on a directory holding
only non-quest files would delete them all; refuse it instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 322a3d1230

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/move.ts
function write(root: string, texts: Map<string, string>) {
if (texts.size === 0) return;
for (const [path, text] of texts) writeFileSync(join(root, path), text);
git(root, "add", "--", ...texts.keys());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve unrelated edits when staging rewritten files

When a document being rewritten already has unstaged or partially staged edits, this unconditional git add stages its entire working-tree version, collapsing the user's index/worktree split and potentially including unrelated work in the next commit. Refuse dirty affected paths or stage only the link-removal/rewrite patch so these commands preserve existing changes.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is worth guarding, but it is an edge case outside this round's scope, so it is recorded as a follow-up rather than fixed here: refuse when a document mv or rm would rewrite has unstaged or partially staged changes, so its staging cannot sweep in unrelated work.

(Written by Claude Opus 5.5)

Comment thread src/cli/github.ts
Comment on lines +59 to +62
export function githubRepository(url: string): Repository | null {
const match =
/^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url);
return match ? { owner: match[1], repo: match[2] } : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize credentialed GitHub remote URLs

When origin is a valid credentialed HTTPS URL such as https://user:token@github.com/owner/repo.git, this regex returns null. The claim check then treats GitHub as an unrelated forge and skips all open PRs and fork-parent branches, so quest mv can rename a quest actively claimed from another fork despite its advertised guard. Parse the URL structurally or accept standard credential-bearing GitHub URLs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recorded as a follow-up rather than fixed here: this regex predates the PR (it came from quest wait's and quest status's PR-number parsing), so parsing remote URLs structurally, including credentialed HTTPS ones, belongs in the shared client for every command at once.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Owner Author

Merge summary

Adds quest mv and quest rm, which move or delete a quest or epic with git, rewrite or drop the root-absolute links to it, report remaining mentions, and refuse while a moved quest is claimed.

Changes in this merge round:

  • Merged main and folded this PR's GitHub client into the shared src/cli/github.ts from kixelated/quest#162. There is one client: it gains Repository, remoteUrl, githubRepository, and the list/all REST pagination, which quest wait now uses too. src/cli/claims.ts keeps its API on top of it for kixelated/quest#167 to rebase onto.
  • The mentions scan includes unignored untracked files (git grep --untracked --exclude-standard).
  • rm refuses a directory that holds no quest documents.
  • Rewritten destinations encode ( and ).

Each has a regression test. just check and just test pass locally; CI is green.

Recorded as follow-ups rather than more review rounds:

  • Refuse to rewrite documents that have unstaged or partially staged changes (thread).
  • Parse GitHub remote URLs structurally, including credentialed HTTPS ones, in the shared client (thread).

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit f4f059c into main Oct 21, 2026
14 checks passed
kixelated added a commit that referenced this pull request Oct 21, 2026
Keep the deletion of quest/m0/landed-quests.md over #164's
edit, and keep both README sections. With quest rm on main, the merge
skill's fix-up deletes a finished quest with quest rm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 21, 2026
Design checkpoint decision (option 1): checks:passed fires only when no
check is pending, no GitHub Actions run on the head is unfinished, every
check the base branch names is present, and every required check passed.
checks:failed stays immediate on the first required failure.

This replaces app-bound requirement matching and required-workflow path
tracking, which are deleted. A workflows rule only disables the
everything-is-required fallback. Resolves Codex's findings r4341313633
(sequential jobs in the fallback) and r4341313643 (workflow identity).

Main brings #164's client helpers; the rules listing now uses
GitHub.all instead of a local pagination loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated deleted the quest/m0/helpers/move branch October 21, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant