From 479a1fb355138347de07c72ba19804e7e6f87cc5 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Mon, 5 Oct 2026 00:06:59 -0400 Subject: [PATCH 1/2] Load the Factory skills from the worktree as thirdshift- (#391) The Factory skills stop being a Claude plugin. Each is renamed thirdshift- in skills/, and names the others in plain prose. thirdshift writes them out once per Command and links each into the worktree's .claude/skills/, kept out of git by a /.claude/skills/thirdshift-* entry in .git/info/exclude, added once. No session gets --plugin-dir, and each prompt's first line is /thirdshift-. Restores resolving-merge-conflicts, dropped from skills/ by a skills update though the conflict Repair prompts still invoke it. Co-Authored-By: Claude Opus 5.5 --- README.md | 12 +- .../0001-rust-binary-with-embedded-skills.md | 2 +- prompts/README.md | 8 +- prompts/architecture-review.md | 6 +- prompts/conflict-repair.md | 2 +- prompts/continuation-pr.md | 6 +- prompts/continuation.md | 6 +- prompts/foreign-conflict-repair.md | 2 +- prompts/fresh.md | 6 +- prompts/review-repair.md | 4 +- prompts/spec-review.md | 8 +- site/index.html | 12 +- site/prompts/index.html | 176 +++++++++--------- .../SKILL.md | 2 +- .../agents/openai.yaml | 0 .../DEEPENING.md | 0 .../DESIGN-IT-TWICE.md | 0 .../SKILL.md | 2 +- .../agents/openai.yaml | 0 .../SKILL.md | 6 +- .../agents/openai.yaml | 0 .../REPORT.md | 6 +- .../SKILL.md | 12 +- .../agents/openai.yaml | 0 skills/{pr => thirdshift-pr}/CREDITS.md | 0 skills/{pr => thirdshift-pr}/SKILL.md | 2 +- .../{pr => thirdshift-pr}/agents/openai.yaml | 0 .../SKILL.md | 14 ++ .../agents/openai.yaml | 3 + skills/{tdd => thirdshift-tdd}/SKILL.md | 6 +- .../agents/openai.yaml | 0 skills/{tdd => thirdshift-tdd}/mocking.md | 0 skills/{tdd => thirdshift-tdd}/tests.md | 0 .../{to-spec => thirdshift-to-spec}/SKILL.md | 2 +- .../agents/openai.yaml | 0 .../SKILL.md | 2 +- .../agents/openai.yaml | 0 src/architect.rs | 2 +- src/command.rs | 10 +- src/main.rs | 2 +- src/plugin.rs | 49 ----- src/progress.rs | 4 +- src/prompt.rs | 34 ++-- src/prompts_page.rs | 33 ++-- src/run.rs | 4 +- src/session.rs | 74 +++----- src/skills.rs | 113 +++++++++++ tests/architect_run.rs | 14 +- tests/background_work.rs | 1 + tests/continuation.rs | 2 +- tests/fakes/claude.rs | 89 ++++++--- tests/fresh_run.rs | 99 ++++++---- tests/merge_run.rs | 6 +- tests/mergeable.rs | 5 +- tests/numbered_branches.rs | 2 +- tests/packaging.rs | 18 +- tests/pickup_run.rs | 2 +- tests/progress_lines.rs | 4 +- tests/spec_run.rs | 8 +- tests/support/mod.rs | 31 +++ 60 files changed, 540 insertions(+), 373 deletions(-) rename skills/{code-review => thirdshift-code-review}/SKILL.md (99%) rename skills/{code-review => thirdshift-code-review}/agents/openai.yaml (100%) rename skills/{codebase-design => thirdshift-codebase-design}/DEEPENING.md (100%) rename skills/{codebase-design => thirdshift-codebase-design}/DESIGN-IT-TWICE.md (100%) rename skills/{codebase-design => thirdshift-codebase-design}/SKILL.md (99%) rename skills/{codebase-design => thirdshift-codebase-design}/agents/openai.yaml (100%) rename skills/{implement => thirdshift-implement}/SKILL.md (66%) rename skills/{implement => thirdshift-implement}/agents/openai.yaml (100%) rename skills/{improve-codebase-architecture => thirdshift-improve-codebase-architecture}/REPORT.md (93%) rename skills/{improve-codebase-architecture => thirdshift-improve-codebase-architecture}/SKILL.md (89%) rename skills/{improve-codebase-architecture => thirdshift-improve-codebase-architecture}/agents/openai.yaml (100%) rename skills/{pr => thirdshift-pr}/CREDITS.md (100%) rename skills/{pr => thirdshift-pr}/SKILL.md (99%) rename skills/{pr => thirdshift-pr}/agents/openai.yaml (100%) create mode 100644 skills/thirdshift-resolving-merge-conflicts/SKILL.md create mode 100644 skills/thirdshift-resolving-merge-conflicts/agents/openai.yaml rename skills/{tdd => thirdshift-tdd}/SKILL.md (94%) rename skills/{tdd => thirdshift-tdd}/agents/openai.yaml (100%) rename skills/{tdd => thirdshift-tdd}/mocking.md (100%) rename skills/{tdd => thirdshift-tdd}/tests.md (100%) rename skills/{to-spec => thirdshift-to-spec}/SKILL.md (99%) rename skills/{to-spec => thirdshift-to-spec}/agents/openai.yaml (100%) rename skills/{to-tickets => thirdshift-to-tickets}/SKILL.md (99%) rename skills/{to-tickets => thirdshift-to-tickets}/agents/openai.yaml (100%) delete mode 100644 src/plugin.rs create mode 100644 src/skills.rs diff --git a/README.md b/README.md index d7fc5da02..125011842 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ From a clone of the issue's repository, `thirdshift `: Any failed check stops the Run before any work happens. 2. Picks the **Issue branch** (`issue-`, or `issue--branch-` once earlier ones are finished) and the **Base branch**, normally the branch you have checked out. Then it makes the [Claim](#the-claim): it labels the issue `in-progress`, in place of `ready-for-agent` if it has that. 3. Creates a git worktree next to your clone, named `-`, so your own checkout is never touched. -4. Runs a headless Claude Code session in that worktree with the **Factory skills** in [`skills/`](skills/) loaded, started with one of the **Session prompts** in [`prompts/`](prompts/). The agent implements the issue, reviews its work against the Base branch, addresses the **Standards findings** and **Spec findings** it agrees with, and opens a ready-for-review pull request that lists every **Unaddressed finding** with a reason and closes the issue. +4. Runs a headless Claude Code session in that worktree with the **Factory skills** in [`skills/`](skills/) linked into its `.claude/skills/` as `thirdshift-` and kept out of git by a `.git/info/exclude` entry ([ADR-0012](docs/adr/0012-factory-skills-linked-into-the-worktree-codex-unsandboxed.md)), started with one of the **Session prompts** in [`prompts/`](prompts/). The agent implements the issue, reviews its work against the Base branch, addresses the **Standards findings** and **Spec findings** it agrees with, and opens a ready-for-review pull request that lists every **Unaddressed finding** with a reason and closes the issue. 5. Takes over deterministically: pushes the Issue branch, skipping your repo's git hooks since the session runs the tests and CI gates the pull request, checks through `gh` that the pull request exists, is open and targets the Base branch, and marks it ready for review (`gh pr ready`) if the agent left it as a draft. 6. Keeps the pull request mergeable and green: merges the Base branch in and watches CI, starting a **Repair** session for a merge conflict or failing checks, at most 5 per Run. A failed check that also failed, under the same name, on the Base branch commit the head last merged in is an **Inherited failure**, not the branch's to fix: the CI-fix Repair is given only the branch's own failures, with the Inherited failures listed as not to fix. If every failed check is an Inherited failure, no Repair starts: the Run merges the Base branch again if it has moved since, and otherwise ends as a [Failed run](#failed-runs) that says to fix the Base branch first, unless it was given `base-fix` or the [User config](#user-config) sets `base.fix`, when it starts a [Base fix](#base-fix) first. A check that is pending, passed or missing on that Base branch commit is the branch's own, and thirdshift never triggers or waits for the Base branch's CI. If a CI-fix Repair leaves the head unchanged, with no commit of its own and no Base branch move to merge, thirdshift does a **Check re-run**, once for that head: it asks GitHub to re-run the branch's own failed checks (`gh run rerun --failed`, once per workflow run), says so on stderr as `re-running the failed checks on : [, …]`, and watches CI on that head again, from the new attempt on. A check that failed for a reason that does not repeat, such as a flaky test or a runner fault, then passes, and the Run goes on as for any green head. If CI is red again, the Run ends as a Failed run, with no second Repair or re-run for that head. It ends the same way, with nothing re-run, if one of those checks can't be re-run (a commit status, or a check run that is not a GitHub Actions job), since the head could not go green, or if GitHub refuses the re-run, which stderr shows. What the Repair concluded is never read, Inherited failures are never re-run on their own account, and a Check re-run is no Repair, so it counts against no budget ([ADR-0010](docs/adr/0010-one-check-re-run-before-a-declined-ci-fix.md)). If the Base branch moves while CI runs, it merges it again and goes round, at most 5 times per Run. In a Merge run, each round also takes in **Foreign commits** first: see [Foreign commits in a Merge run](#foreign-commits-in-a-merge-run). 7. In a **Merge run** (`thirdshift merge `), does the **Self-merge**: once the pull request is open, ready for review, mergeable and green, thirdshift merges it into the Base branch with a merge commit, on exactly the head commit whose CI it watched (`gh pr merge --merge --match-head-commit `). It never uses GitHub's auto-merge ([ADR-0004](docs/adr/0004-self-merge-by-thirdshift-not-github-auto-merge.md)). A merge that fails goes back round step 6, within the same budgets, and is tried again on the new green head. If that round finds nothing to fix, the refusal is a **policy refusal**, such as merge commits being disallowed or a review being required. After the merge, thirdshift deletes the Issue branch on `origin`, and closes the issue if it is still open, with the comment `Closed by #, merged into by a thirdshift Merge run.` GitHub closes it on its own only for a merge into the repository's default branch, and then thirdshift leaves it alone. Once the issue is closed, either way, thirdshift takes `in-progress` off it, [removing the Claim](#when-the-claim-ends). -8. Cleans up: removes the worktree, the local Issue branch and the temporary plugin directory, whether the Run succeeded or not. The one exception is a **Failed run** whose work could not be pushed: see below. +8. Cleans up: removes the worktree, the local Issue branch and the temporary directory the Factory skills were written out to, whether the Run succeeded or not. The one exception is a **Failed run** whose work could not be pushed: see below. ### The Claim @@ -108,7 +108,7 @@ Sessions run headless in Claude Code's auto mode (`claude -p --permission-mode a ### What sessions leave behind -Cleanup removes only thirdshift's own worktree, local Issue branch and temporary plugin directory. Anything else an agent does as your user stays. For example, one Run downloaded a JDK to `~/.local/jdk/` because the server had no Java, installed Playwright in `/tmp/pw`, and left a pull request body draft in `/tmp/`. This is by design, but worth knowing: +Cleanup removes only thirdshift's own worktree, local Issue branch and the temporary directory it wrote the Factory skills out to. The `.git/info/exclude` entry that keeps the linked skills out of git, `/.claude/skills/thirdshift-*`, is added once and left in place, as every worktree of the repository shares the file. Anything else an agent does as your user stays. For example, one Run downloaded a JDK to `~/.local/jdk/` because the server had no Java, installed Playwright in `/tmp/pw`, and left a pull request body draft in `/tmp/`. This is by design, but worth knowing: - **Install the target repository's toolchains up front** (language runtimes, package managers, test tools), so the agents don't download their own on every Run. - **For UI work, install the headless browser dependencies**, e.g. `libgbm1` on Ubuntu server, which headless Chromium needs. Without it, agents can't take screenshots to check a UI change and fall back to reading the code. @@ -359,7 +359,7 @@ With `quiet_skips = true` in the `[activity]` section of the [User config](#user A Merge run merges only code an agent wrote or reviewed. If someone else pushes to the Issue branch during the Run, their **Foreign commits** are reviewed before they can be merged: 1. Each round of step 6 starts by fetching the Issue branch from `origin`. Any new commits there are merged into the Run's branch, by fast-forward or a merge commit, never a rebase, each logged on stderr as `merging new commit from origin/`. Foreign commits that arrive while CI runs on a green head send the Run round again rather than on to the merge. A merge that conflicts with the Run's own work gets a conflict Repair. -2. A **review Repair** then runs `/thirdshift:code-review` with the head thirdshift last knew as the Run's own as the fixed point. The agent fixes the findings it agrees with, adds the rest to the pull request body's "Unaddressed findings" section marked as coming from the Foreign commits, and pushes. +2. A **review Repair** then runs `/thirdshift-code-review` with the head thirdshift last knew as the Run's own as the fixed point. The agent fixes the findings it agrees with, adds the rest to the pull request body's "Unaddressed findings" section marked as coming from the Foreign commits, and pushes. 3. The round goes on as usual: the Base branch is merged in, CI watched, and the merge tried once the head is green. Review Repairs are logged as `repair-`, numbered with the other Repairs, count against the cap of 5 Repairs, and get a Resume like any session. A round that picks up Foreign commits counts against the same 5, like a Base branch move. A Run that spends either budget fails. @@ -512,7 +512,7 @@ An Architect run: 1. Makes the checks a Run makes that don't need an issue, before creating anything: `origin` is a GitHub repository, git has a `user.name` and `user.email`, HEAD is not detached, and the Base branch exists on `origin` with your local copy not ahead of it. With `base `, a detached HEAD is fine, since the checkout no longer picks the Base branch, and the other two checks are made on ``: `base branch does not exist on origin; push it first`, or `local is commit(s) ahead of origin/; push them first`. There is no **Origin match**, since there is no Issue URL: the repository is the one `origin` names. If another Architect run on the repository, or a [Pickup run](#pickup-runs), is still running, it is [skipped](#one-at-a-time) here, and if not, it is skipped when an earlier [Architect plan is still open](#one-architect-plan-at-a-time), and if not, when an [Architect idea waits for triage](#an-architect-idea-waits-for-triage), and if not, when the repository has a [Ready issue](#a-ready-issue-goes-first). With `launch.pull = true` in the [User config](#user-config), it then fast-forwards your checkout of the Base branch, as a Run does, and so only when the Base branch is the branch checked out: with `base ` from a clone on another branch, your checkout is left alone. 2. Creates a git worktree next to your clone, named `-architect`, detached at the head of the Base branch on `origin`, with no **Issue branch**. Your checkout, its uncommitted changes and its untracked files are never touched or scanned. 3. Runs the **Architecture review** there: a headless Claude Code session with the **Factory skills** loaded, started with the [Architecture review prompt](prompts/architecture-review.md). It looks for deepening opportunities, skips any an open issue already covers, and takes the top recommendation. If it is Strong, the review publishes it as the plan, labelled `needs-triage`: a **Spec** with **Tickets**, or a single Ticket when one session is enough. It may edit files in the worktree to check an idea, but commits and pushes nothing. It ends its final message with one line naming the plan: `Architecture review plan: `. thirdshift reads only that line. With [no Strong candidate](#no-strong-candidate) the line names another issue, which thirdshift labels an **Architect idea**, and steps 5 to 7 are skipped. -4. Removes the worktree and the temporary plugin directory once the session ends, whatever the outcome. +4. Removes the worktree once the session ends, whatever the outcome. The temporary directory the Factory skills were written out to goes as the Architect run ends. 5. Checks the plan: the issue is in this repository, is open, was created after the Architect run started, and carries no other label that says it is not agent work (`ready-for-human`, `needs-info` or `wontfix`). 6. Marks the plan ready, in one request: `needs-triage` is swapped for `ready-for-agent`, the plan is labelled `architect-plan`, the mark a later Architect run finds an open **Architect plan** by, and its other labels are kept. thirdshift adds that label, never the agent, and first creates it if the repository has none. A Spec's Tickets are left as the review labelled them, without `architect-plan`. 7. Dispatches the plan, unless given `--plan-only`, exactly as `thirdshift ` would from the same clone on the Architect run's Base branch: a [Spec run](#spec-runs) when the plan has sub-issues, a Run otherwise. Its Base branch is the Architect run's, so with `base ` it is ``, not the branch checked out. `merge` and `no-merge` apply to the Spec PR or the Run's pull request, `parallel ` to the Spec run, and `base-fix` and `no-base-fix` to whether that run may start a [Base fix](#base-fix), as if given to that command, and the [User config](#user-config) sets what they leave unsaid: `merge.always`, `spec.parallel`, `base.fix`, `launch.pull` and `logs.dir`. The Architecture review itself never watches CI, so a Base fix can only happen in the dispatched run. It makes the [Claim](#the-claim) as that command would too: once its checks pass, the plan's `ready-for-agent` is swapped for `in-progress`, and the plan keeps `architect-plan`. The one difference is that it sends no [Run notification](#run-notifications) of its own, whatever `email.always` says: the Architect run sends [the one](#one-run-notification). @@ -848,4 +848,4 @@ The tag starts the release workflow, generated by [`dist`](https://github.com/ax thirdshift is released under the [MIT License](LICENSE). -The Factory skills in `skills/` are adapted from Matt Pocock's [mattpocock/skills](https://github.com/mattpocock/skills), changed to run headless with no human in the loop. They are used under the MIT License; its copyright notice is kept in [`skills/LICENSE`](skills/LICENSE) and is embedded in the binary and written out with the skills on every Run. The `pr` skill also reproduces part of Dex Horthy's `show-me` skill; see [`skills/pr/CREDITS.md`](skills/pr/CREDITS.md). +The Factory skills in `skills/` are adapted from Matt Pocock's [mattpocock/skills](https://github.com/mattpocock/skills), changed to run headless with no human in the loop. They are used under the MIT License; its copyright notice is kept in [`skills/LICENSE`](skills/LICENSE) and is embedded in the binary and written out with the skills on every Run. The `thirdshift-pr` skill also reproduces part of Dex Horthy's `show-me` skill; see [`skills/thirdshift-pr/CREDITS.md`](skills/thirdshift-pr/CREDITS.md). diff --git a/docs/adr/0001-rust-binary-with-embedded-skills.md b/docs/adr/0001-rust-binary-with-embedded-skills.md index 5bcdb159d..20568bca0 100644 --- a/docs/adr/0001-rust-binary-with-embedded-skills.md +++ b/docs/adr/0001-rust-binary-with-embedded-skills.md @@ -5,4 +5,4 @@ The factory is a Rust binary that compiles `skills/` into itself (`include_dir!` ## Consequences - Editing a skill has no effect until the binary is rebuilt and reinstalled (`cargo install --path .`). -- The skills are loaded as a plugin, so they are namespaced (`/thirdshift:implement`), and their references to each other must use that namespace. +- ~~The skills are loaded as a plugin, so they are namespaced (`/thirdshift:implement`), and their references to each other must use that namespace.~~ Replaced by [ADR 0012](0012-factory-skills-linked-into-the-worktree-codex-unsandboxed.md): the skills are linked into each worktree, named `thirdshift-`, and name each other in plain prose. diff --git a/prompts/README.md b/prompts/README.md index 67f969a22..b1012d114 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -4,16 +4,16 @@ Every Session prompt thirdshift can start or resume an agent session with, one file each, with placeholders such as `` where each Run fills in its own values. The [Prompts and skills page](https://thirdshift.app/prompts/) shows the same prompts. -Every session runs `claude` headless in the Run's worktree, in auto permission mode, with the Factory skills loaded as a plugin from a temporary directory: +Every session runs `claude` headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's `.claude/skills/`, where Claude Code finds them, and kept out of git: ```sh -claude -p --permission-mode auto --plugin-dir --output-format stream-json --verbose +claude -p --permission-mode auto --output-format stream-json --verbose ``` A [Resume](resume.md) continues the session that ended: ```sh -claude -p --permission-mode auto --plugin-dir --output-format stream-json --verbose --resume +claude -p --permission-mode auto --output-format stream-json --verbose --resume ``` | Prompt | When it is sent | File | @@ -30,4 +30,4 @@ claude -p --permission-mode auto --plugin-dir --output-format strea | CI-fix Repair, with Inherited failures | Starts a Repair session when CI fails on the pull request's head commit and some of the failed checks, but not all, are Inherited failures: they also failed on the Base branch commit the head last merged in, so they are listed apart as not to fix. | [ci-fix-repair-inherited.md](ci-fix-repair-inherited.md) | | Resume | Continues any session, once, that ended its turn with background work still running, which was killed with it. | [resume.md](resume.md) | -The Factory skills the prompts name are in [`skills/`](../skills/), as the plugin writes them out. +The Factory skills the prompts name are in [`skills/`](../skills/), as thirdshift writes them out. diff --git a/prompts/architecture-review.md b/prompts/architecture-review.md index 331775f7c..b3f32c7fa 100644 --- a/prompts/architecture-review.md +++ b/prompts/architecture-review.md @@ -5,15 +5,15 @@ Starts the Architecture review, the session an Architect run opens with, in a worktree at the head of the Base branch. The line naming the focus is left out when the command gives none. ``` -/thirdshift:improve-codebase-architecture +/thirdshift-improve-codebase-architecture This is an Architecture review of the base branch : this worktree is checked out at its head on origin, on no branch. Focus the review on: -Find the deepening opportunities with /thirdshift:improve-codebase-architecture, using /thirdshift:codebase-design for the vocabulary. Skip any that an open issue already covers, and take the top recommendation. +Find the deepening opportunities with the `thirdshift-improve-codebase-architecture` skill, using the `thirdshift-codebase-design` skill for the vocabulary. Skip any that an open issue already covers, and take the top recommendation. -If it is Strong, settle its design yourself and publish it as the plan with /thirdshift:to-spec and /thirdshift:to-tickets: a Spec with Tickets, or a single Ticket when one session is enough. Label the plan's top issue, the Spec or the single Ticket, `needs-triage`, not `ready-for-agent`: thirdshift marks it ready once you are done. +If it is Strong, settle its design yourself and publish it as the plan with the `thirdshift-to-spec` and `thirdshift-to-tickets` skills: a Spec with Tickets, or a single Ticket when one session is enough. Label the plan's top issue, the Spec or the single Ticket, `needs-triage`, not `ready-for-agent`: thirdshift marks it ready once you are done. If it is not Strong, file it as one issue labelled `needs-triage`, unless an open issue already covers it. diff --git a/prompts/conflict-repair.md b/prompts/conflict-repair.md index 15b538b28..150c3506c 100644 --- a/prompts/conflict-repair.md +++ b/prompts/conflict-repair.md @@ -5,7 +5,7 @@ Starts a Repair session when merging the Base branch into the Issue branch leaves conflicts. ``` -/thirdshift:resolving-merge-conflicts +/thirdshift-resolving-merge-conflicts A merge of origin/ into is in progress in this worktree and has conflicts. implements ; its pull request is . diff --git a/prompts/continuation-pr.md b/prompts/continuation-pr.md index 20b831ab9..33e37eec6 100644 --- a/prompts/continuation-pr.md +++ b/prompts/continuation-pr.md @@ -5,17 +5,17 @@ Starts the implement session when the Run is a Continuation of an Issue branch whose pull request is open. ``` -/thirdshift:implement +/thirdshift-implement You are continuing work on branch , which already has commits (see git log ..HEAD). Build on them; don't start over. -The base branch is . Review with /thirdshift:code-review using as the fixed point. +The base branch is . Review with the `thirdshift-code-review` skill using as the fixed point. Address the Standards and Spec findings you agree with. Push branch . -Update PR using /thirdshift:pr, rewriting its body to cover the whole branch, marked ready for review. +Update PR using the `thirdshift-pr` skill, rewriting its body to cover the whole branch, marked ready for review. In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason. diff --git a/prompts/continuation.md b/prompts/continuation.md index fca01aa64..8bde6ee19 100644 --- a/prompts/continuation.md +++ b/prompts/continuation.md @@ -5,17 +5,17 @@ Starts the implement session when the Run is a Continuation of an Issue branch that has no pull request. ``` -/thirdshift:implement +/thirdshift-implement You are continuing work on branch , which already has commits (see git log ..HEAD). Build on them; don't start over. -The base branch is . Review with /thirdshift:code-review using as the fixed point. +The base branch is . Review with the `thirdshift-code-review` skill using as the fixed point. Address the Standards and Spec findings you agree with. Push branch . -Create a pull request against using /thirdshift:pr, marked ready for review. +Create a pull request against using the `thirdshift-pr` skill, marked ready for review. In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason. diff --git a/prompts/foreign-conflict-repair.md b/prompts/foreign-conflict-repair.md index b58bc2808..99a90c54a 100644 --- a/prompts/foreign-conflict-repair.md +++ b/prompts/foreign-conflict-repair.md @@ -5,7 +5,7 @@ In a Merge run, starts a Repair session when merging Foreign commits from the Issue branch on origin into the local one leaves conflicts. ``` -/thirdshift:resolving-merge-conflicts +/thirdshift-resolving-merge-conflicts A merge of origin/ into is in progress in this worktree and has conflicts. implements ; its pull request is . diff --git a/prompts/fresh.md b/prompts/fresh.md index 788113188..b21fce162 100644 --- a/prompts/fresh.md +++ b/prompts/fresh.md @@ -5,10 +5,10 @@ Starts the implement session, which goes on to review and open the pull request, when the Run starts a new Issue branch. ``` -/thirdshift:implement -The base branch is . Review with /thirdshift:code-review using as the fixed point. +/thirdshift-implement +The base branch is . Review with the `thirdshift-code-review` skill using as the fixed point. Address the Standards and Spec findings you agree with. -Push branch and create a pull request against using /thirdshift:pr, marked ready for review. +Push branch and create a pull request against using the `thirdshift-pr` skill, marked ready for review. In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason. Include "Closes #" in the PR body. You run headless: nobody is watching, and ending your turn ends the session. Run tests and other long commands in the foreground, raising the Bash timeout if needed. If a command is moved to the background, wait for that task by its own task id or output file, never by process names or patterns (`pgrep`, `ps | grep`, and the like): other sessions on this machine run the same commands. Never end your turn while a background task you depend on is still running: ending the turn kills it. Before ending your turn, stop every background task you no longer need with the `TaskStop` tool, by its task id: a task still running when your turn ends is taken as work you were waiting on. diff --git a/prompts/review-repair.md b/prompts/review-repair.md index 76f9b48f1..3e230acd8 100644 --- a/prompts/review-repair.md +++ b/prompts/review-repair.md @@ -5,11 +5,11 @@ In a Merge run, starts a Repair session once Foreign commits are merged into the Issue branch, to review them from the head the Run last knew as its own before they can be merged. ``` -/thirdshift:code-review +/thirdshift-code-review Someone else pushed commits to while it was being worked on, and they have been merged into in this worktree. implements ; its pull request is . They are merged into the base branch only once you have reviewed them. -Review with /thirdshift:code-review using as the fixed point: 's head before their commits were merged in. +Review with the `thirdshift-code-review` skill using as the fixed point: 's head before their commits were merged in. Address the Standards and Spec findings you agree with. diff --git a/prompts/spec-review.md b/prompts/spec-review.md index 0ac7de4dd..a22dd00aa 100644 --- a/prompts/spec-review.md +++ b/prompts/spec-review.md @@ -5,17 +5,17 @@ In a Spec run, starts the Spec review once every Ticket has landed on the Spec branch, before the Spec PR, a draft until then, is marked ready. ``` -/thirdshift:code-review , with the Spec as the spec +/thirdshift-code-review , with the Spec as the spec Every Ticket of the Spec has landed on branch , its Spec branch (see git log ..HEAD). Review the Spec as a whole, including how the Tickets' work fits together. -Review with /thirdshift:code-review using as the fixed point and as the spec. +Review with the `thirdshift-code-review` skill using as the fixed point and as the spec. -Address the Standards and Spec findings you agree with, using /thirdshift:tdd where it fits, and commit. +Address the Standards and Spec findings you agree with, using the `thirdshift-tdd` skill where it fits, and commit. Push branch . Do not rebase or force-push. -Update PR using /thirdshift:pr, rewriting its body to cover the whole Spec. Leave out its Tickets checklist, or keep it between its markers as it is: thirdshift puts it back. Leave the PR a draft: thirdshift marks it ready once you are done. +Update PR using the `thirdshift-pr` skill, rewriting its body to cover the whole Spec. Leave out its Tickets checklist, or keep it between its markers as it is: thirdshift puts it back. Leave the PR a draft: thirdshift marks it ready once you are done. In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason. diff --git a/site/index.html b/site/index.html index 802452371..9b51ff998 100644 --- a/site/index.html +++ b/site/index.html @@ -76,7 +76,7 @@

Your issues go to press overnight.

  1. /grill-with-docs
  2. /to-spec
  3. /to-tickets

The human half of the workflow: shaping work into issues the factory can take on, by grilling the idea with /grill-with-docs, writing the spec with /to-spec and cutting it into tickets with /to-tickets, and reviewing the pull requests it delivers.

Third shift

Agents, with headless copies of the same skills

-
  1. /implement
  2. /code-review
  3. /pr
+
  1. /thirdshift-implement
  2. /thirdshift-code-review
  3. /thirdshift-pr

thirdshift takes one Issue URL per Run. Its Factory skills, adapted to run with no human in the loop, implement the issue with /implement, review the work with /code-review and open a pull request that is ready for review with /pr.

Read every prompt and skill the agents get →

@@ -196,7 +196,7 @@

1 · C · Prepare

2 · M · Implement

A headless Claude Code session, with the Factory skills loaded, implements the issue in the worktree, test-first where it can, and commits.

- +
  1. thirdshift: logging the session to /home/jacob/.thirdshift/logs/JacobStephens2-thirdshift-issue-24-20260925T134143Z-implement.jsonl
  2. thirdshift: implement: session started
  3. @@ -216,9 +216,9 @@

    2 · M · Implement<

    3 · Y · Review

    The same session reviews its work against the Base branch, fixes the Standards findings and Spec findings it agrees with, and lists the rest as Unaddressed findings in the pull request body. Then it opens the pull request, ready for review.

    - +
      -
    1. thirdshift: implement: skill thirdshift:code-review
    2. +
    3. thirdshift: implement: skill thirdshift-code-review
    4. thirdshift: implement: $ git rev-parse main && git log main..HEAD --oneline && git diff main...HEAD --stat; ls .agents/skills…
    5. thirdshift: implement: Agent Standards review
    6. thirdshift: implement: $ git diff main...HEAD && cat CODING_STANDARDS.md && ls .agents/skills/codebase-design/ && cat .agents…
    7. @@ -233,7 +233,7 @@

      3 · Y · Review

      thirdshift: implement: $ python3 - <<'EOF'
    8. thirdshift: implement: SubagentHandback
    9. thirdshift: implement: commit and push
    10. -
    11. thirdshift: implement: skill thirdshift:pr
    12. +
    13. thirdshift: implement: skill thirdshift-pr
    14. thirdshift: implement: $ cat > /tmp/pr24.md <<'EOF'
    15. thirdshift: implement: session ended after 3m 40s: 5 turns, $1.06 at API prices
    @@ -241,7 +241,7 @@

    3 · Y · Review

    4 · K · Finish

    thirdshift pushes, checks the pull request, merges the Base branch in and watches CI. A merge conflict or failing CI starts a Repair, at most 5. A Resume can happen in any session and isn't a Repair. The Run ends once the pull request is open, ready, mergeable and passing CI. This Run needed no Repair.

    - +
    1. thirdshift: pushing issue-24
    2. thirdshift: checking the PR
    3. diff --git a/site/prompts/index.html b/site/prompts/index.html index a058eede1..7607c4f82 100644 --- a/site/prompts/index.html +++ b/site/prompts/index.html @@ -34,10 +34,10 @@

      Prompts and skills

      The command line

      How each session starts

      -

      Every session runs claude headless in the Run's worktree, in auto permission mode, with the Factory skills loaded as a plugin from a temporary directory. The prompt is one of the templates below.

      -
      claude -p --permission-mode auto --plugin-dir <plugin dir> --output-format stream-json --verbose <prompt>
      +

      Every session runs claude headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's .claude/skills/, where Claude Code finds them, and kept out of git. The prompt is one of the templates below.

      +
      claude -p --permission-mode auto --output-format stream-json --verbose <prompt>

      A Resume continues the session that ended:

      -
      claude -p --permission-mode auto --plugin-dir <plugin dir> --output-format stream-json --verbose --resume <session id> <prompt>
      +
      claude -p --permission-mode auto --output-format stream-json --verbose --resume <session id> <prompt>
      @@ -49,10 +49,10 @@

      Prompts and skills

      Fresh

      Starts the implement session, which goes on to review and open the pull request, when the Run starts a new Issue branch.

      Sent by 2 · M · Implement

      -
      /thirdshift:implement <Issue URL>
      -The base branch is <base>. Review with /thirdshift:code-review using <base> as the fixed point.
      +        
      /thirdshift-implement <Issue URL>
      +The base branch is <base>. Review with the `thirdshift-code-review` skill using <base> as the fixed point.
       Address the Standards and Spec findings you agree with.
      -Push branch <branch> and create a pull request against <base> using /thirdshift:pr, marked ready for review.
      +Push branch <branch> and create a pull request against <base> using the `thirdshift-pr` skill, marked ready for review.
       In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason.
       Include "Closes #<n>" in the PR body.
       You run headless: nobody is watching, and ending your turn ends the session. Run tests and other long commands in the foreground, raising the Bash timeout if needed. If a command is moved to the background, wait for that task by its own task id or output file, never by process names or patterns (`pgrep`, `ps | grep`, and the like): other sessions on this machine run the same commands. Never end your turn while a background task you depend on is still running: ending the turn kills it. Before ending your turn, stop every background task you no longer need with the `TaskStop` tool, by its task id: a task still running when your turn ends is taken as work you were waiting on.
      @@ -62,17 +62,17 @@ 

      Fresh

      Continuation, with no pull request

      Starts the implement session when the Run is a Continuation of an Issue branch that has no pull request.

      Sent by 2 · M · Implement

      -
      /thirdshift:implement <Issue URL>
      +        
      /thirdshift-implement <Issue URL>
       
       You are continuing work on branch <branch>, which already has commits (see git log <base>..HEAD). Build on them; don't start over.
       
      -The base branch is <base>. Review with /thirdshift:code-review using <base> as the fixed point.
      +The base branch is <base>. Review with the `thirdshift-code-review` skill using <base> as the fixed point.
       
       Address the Standards and Spec findings you agree with.
       
       Push branch <branch>.
       
      -Create a pull request against <base> using /thirdshift:pr, marked ready for review.
      +Create a pull request against <base> using the `thirdshift-pr` skill, marked ready for review.
       
       In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason.
       
      @@ -85,17 +85,17 @@ 

      Continuation, with no pull request

      Continuation, with an open pull request

      Starts the implement session when the Run is a Continuation of an Issue branch whose pull request is open.

      Sent by 2 · M · Implement

      -
      /thirdshift:implement <Issue URL>
      +        
      /thirdshift-implement <Issue URL>
       
       You are continuing work on branch <branch>, which already has commits (see git log <base>..HEAD). Build on them; don't start over.
       
      -The base branch is <base>. Review with /thirdshift:code-review using <base> as the fixed point.
      +The base branch is <base>. Review with the `thirdshift-code-review` skill using <base> as the fixed point.
       
       Address the Standards and Spec findings you agree with.
       
       Push branch <branch>.
       
      -Update PR <pull request URL> using /thirdshift:pr, rewriting its body to cover the whole branch, marked ready for review.
      +Update PR <pull request URL> using the `thirdshift-pr` skill, rewriting its body to cover the whole branch, marked ready for review.
       
       In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason.
       
      @@ -108,17 +108,17 @@ 

      Continuation, with an open pull requestSpec review

      In a Spec run, starts the Spec review once every Ticket has landed on the Spec branch, before the Spec PR, a draft until then, is marked ready.

      Sent by 3 · Y · Review

      -
      /thirdshift:code-review <base>, with the Spec <Spec URL> as the spec
      +        
      /thirdshift-code-review <base>, with the Spec <Spec URL> as the spec
       
       Every Ticket of the Spec <Spec URL> has landed on branch <Spec branch>, its Spec branch (see git log <base>..HEAD). Review the Spec as a whole, including how the Tickets' work fits together.
       
      -Review with /thirdshift:code-review using <base> as the fixed point and <Spec URL> as the spec.
      +Review with the `thirdshift-code-review` skill using <base> as the fixed point and <Spec URL> as the spec.
       
      -Address the Standards and Spec findings you agree with, using /thirdshift:tdd where it fits, and commit.
      +Address the Standards and Spec findings you agree with, using the `thirdshift-tdd` skill where it fits, and commit.
       
       Push branch <Spec branch>. Do not rebase or force-push.
       
      -Update PR <pull request URL> using /thirdshift:pr, rewriting its body to cover the whole Spec. Leave out its Tickets checklist, or keep it between its markers as it is: thirdshift puts it back. Leave the PR a draft: thirdshift marks it ready once you are done.
      +Update PR <pull request URL> using the `thirdshift-pr` skill, rewriting its body to cover the whole Spec. Leave out its Tickets checklist, or keep it between its markers as it is: thirdshift puts it back. Leave the PR a draft: thirdshift marks it ready once you are done.
       
       In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason.
       
      @@ -131,15 +131,15 @@ 

      Spec review

      Architecture review

      Starts the Architecture review, the session an Architect run opens with, in a worktree at the head of the Base branch. The line naming the focus is left out when the command gives none.

      Sent by an Architect run, before any unit

      -
      /thirdshift:improve-codebase-architecture
      +        
      /thirdshift-improve-codebase-architecture
       
       This is an Architecture review of the base branch <base>: this worktree is checked out at its head on origin, on no branch.
       
       Focus the review on: <focus>
       
      -Find the deepening opportunities with /thirdshift:improve-codebase-architecture, using /thirdshift:codebase-design for the vocabulary. Skip any that an open issue already covers, and take the top recommendation.
      +Find the deepening opportunities with the `thirdshift-improve-codebase-architecture` skill, using the `thirdshift-codebase-design` skill for the vocabulary. Skip any that an open issue already covers, and take the top recommendation.
       
      -If it is Strong, settle its design yourself and publish it as the plan with /thirdshift:to-spec and /thirdshift:to-tickets: a Spec with Tickets, or a single Ticket when one session is enough. Label the plan's top issue, the Spec or the single Ticket, `needs-triage`, not `ready-for-agent`: thirdshift marks it ready once you are done.
      +If it is Strong, settle its design yourself and publish it as the plan with the `thirdshift-to-spec` and `thirdshift-to-tickets` skills: a Spec with Tickets, or a single Ticket when one session is enough. Label the plan's top issue, the Spec or the single Ticket, `needs-triage`, not `ready-for-agent`: thirdshift marks it ready once you are done.
       
       If it is not Strong, file it as one issue labelled `needs-triage`, unless an open issue already covers it.
       
      @@ -157,7 +157,7 @@ 

      Architecture review

      Conflict Repair

      Starts a Repair session when merging the Base branch into the Issue branch leaves conflicts.

      Sent by 4 · K · Finish

      -
      /thirdshift:resolving-merge-conflicts
      +        
      /thirdshift-resolving-merge-conflicts
       
       A merge of origin/<base> into <branch> is in progress in this worktree and has conflicts.
       <branch> implements <Issue URL>; its pull request is <pull request URL>.
      @@ -171,7 +171,7 @@ 

      Conflict Repair

      Conflict Repair, on Foreign commits

      In a Merge run, starts a Repair session when merging Foreign commits from the Issue branch on origin into the local one leaves conflicts.

      Sent by 4 · K · Finish

      -
      /thirdshift:resolving-merge-conflicts
      +        
      /thirdshift-resolving-merge-conflicts
       
       A merge of origin/<branch> into <branch> is in progress in this worktree and has conflicts.
       <branch> implements <Issue URL>; its pull request is <pull request URL>.
      @@ -185,11 +185,11 @@ 

      Conflict Repair, on Foreign commit

      Review Repair

      In a Merge run, starts a Repair session once Foreign commits are merged into the Issue branch, to review them from the head the Run last knew as its own before they can be merged.

      Sent by 4 · K · Finish

      -
      /thirdshift:code-review <own head>
      +        
      /thirdshift-code-review <own head>
       
       Someone else pushed commits to <branch> while it was being worked on, and they have been merged into <branch> in this worktree. <branch> implements <Issue URL>; its pull request is <pull request URL>. They are merged into the base branch only once you have reviewed them.
       
      -Review with /thirdshift:code-review using <own head> as the fixed point: <branch>'s head before their commits were merged in.
      +Review with the `thirdshift-code-review` skill using <own head> as the fixed point: <branch>'s head before their commits were merged in.
       
       Address the Standards and Spec findings you agree with.
       
      @@ -254,14 +254,14 @@ 

      Resume

      Factory skills

      The skills, as the agent reads them

      -

      Every Factory skill and supporting file, raw, exactly as the plugin writes it out.

      +

      Every Factory skill and supporting file, raw, exactly as thirdshift writes it out.

      -
      -

      code-review

      +
      +

      thirdshift-code-review

      Used by 3 · Y · Review

      -

      code-review/SKILL.md

      +

      thirdshift-code-review/SKILL.md

      ---
      -name: code-review
      +name: thirdshift-code-review
       description: "Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\"."
       ---
       
      @@ -348,18 +348,18 @@ 

      code-review/SKILL.md

      Reporting them separately stops one axis from masking the other.
      -

      code-review/agents/openai.yaml

      +

      thirdshift-code-review/agents/openai.yaml

      interface:
         display_name: "Code Review"
         short_description: "Review a diff on standards and spec"
       
      -
      -

      codebase-design

      +
      +

      thirdshift-codebase-design

      Used by the Architecture review, in an Architect run

      -

      codebase-design/SKILL.md

      +

      thirdshift-codebase-design/SKILL.md

      ---
      -name: codebase-design
      +name: thirdshift-codebase-design
       description: Shared vocabulary for designing deep modules. Use when designing or improving a module's interface, finding deepening opportunities, deciding where a seam goes, making code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.
       ---
       
      @@ -473,7 +473,7 @@ 

      codebase-design/SKILL.md

      -

      codebase-design/DEEPENING.md

      +

      thirdshift-codebase-design/DEEPENING.md

      # Deepening
       
       How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [SKILL.md](SKILL.md): **module**, **interface**, **seam**, **adapter**.
      @@ -512,7 +512,7 @@ 

      codebase-design/DEEPENING.md

      -

      codebase-design/DESIGN-IT-TWICE.md

      +

      thirdshift-codebase-design/DESIGN-IT-TWICE.md

      # Design It Twice
       
       To explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout): your first idea is unlikely to be the best.
      @@ -558,33 +558,33 @@ 

      codebase-design/DESIGN-IT After comparing, decide: which design is strongest and why. If elements from different designs would combine well, take a hybrid. Be opinionated: the choice is yours to make, and nobody picks from a menu. Record the decision and its reason, with the designs you turned down and why.

      -

      codebase-design/agents/openai.yaml

      +

      thirdshift-codebase-design/agents/openai.yaml

      interface:
         display_name: "Codebase Design"
         short_description: "Vocabulary for deep-module design"
       
      -
      -

      implement

      +
      +

      thirdshift-implement

      Used by 2 · M · Implement

      -

      implement/SKILL.md

      +

      thirdshift-implement/SKILL.md

      ---
      -name: implement
      +name: thirdshift-implement
       description: "Implement a piece of work based on a spec or set of tickets."
       disable-model-invocation: false
       ---
       
       Implement the work described by the user in the spec or tickets.
       
      -Use /thirdshift:tdd where possible, at pre-agreed seams.
      +Use the `thirdshift-tdd` skill where possible, at pre-agreed seams.
       
       Run typechecking regularly, single test files regularly, and the full test suite once at the end.
       
      -Once done, use /thirdshift:code-review to review the work.
      +Once done, use the `thirdshift-code-review` skill to review the work.
       
       Commit your work to the current branch.
       
      -

      implement/agents/openai.yaml

      +

      thirdshift-implement/agents/openai.yaml

      interface:
         display_name: "Implement"
         short_description: "Build work from a spec or tickets"
      @@ -592,12 +592,12 @@ 

      implement/agents/openai.yaml

      -
      -

      improve-codebase-architecture

      +
      +

      thirdshift-improve-codebase-architecture

      Used by the Architecture review, in an Architect run

      -

      improve-codebase-architecture/SKILL.md

      +

      thirdshift-improve-codebase-architecture/SKILL.md

      ---
      -name: improve-codebase-architecture
      +name: thirdshift-improve-codebase-architecture
       description: "Scan a codebase for deepening opportunities, take the top recommendation, and publish it to the issue tracker: as a plan when it is Strong, as an idea otherwise. Only for an Architecture review."
       disable-model-invocation: false
       ---
      @@ -612,7 +612,7 @@ 

      improve-codebase-arch This command is _informed_ by the project's domain model and built on a shared design vocabulary: -- Call the Skill tool with "thirdshift:codebase-design" for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use these terms exactly in every suggestion, and don't drift into "component," "service," "API," or "boundary." +- Use the `thirdshift-codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use these terms exactly in every suggestion, and don't drift into "component," "service," "API," or "boundary." - The domain language in `CONTEXT.md` gives names to good seams; ADRs in `docs/adr/` record decisions this command should not re-litigate. The issue tracker and triage label vocabulary should have been provided to you. If `docs/agents/issue-tracker.md` is missing, fall back to the `gh` CLI. If `docs/agents/triage-labels.md` is missing, use the label names as written here, creating a label the tracker lacks (`gh label create`). @@ -669,7 +669,7 @@

      improve-codebase-arch If the scan surfaced no candidate at all, file nothing, and say so plainly in place of the final line of step 6: there is no issue for it to name. -**Use CONTEXT.md vocabulary for the domain, and the `/thirdshift:codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module," not "the FooBarHandler," and not "the Order service." +**Use CONTEXT.md vocabulary for the domain, and the `thirdshift-codebase-design` skill's vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module," not "the FooBarHandler," and not "the Order service." **ADR conflicts**: if a candidate contradicts an existing ADR, only keep it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the report (e.g. a warning callout: _"contradicts ADR-0007, but worth reopening because…"_). Don't raise every theoretical refactor an ADR forbids. @@ -690,7 +690,7 @@

      improve-codebase-arch - **Naming a deepened module after a concept not in `CONTEXT.md`?** Record the term and its definition as work for a Ticket. - **Sharpening a fuzzy term as you decide?** Record the sharper definition as work for a Ticket. - **Reopening an ADR, or making a decision worth one?** Record the new or changed ADR as work for a Ticket. A decision is worth an ADR only when it is hard to reverse, surprising without context, and the result of a real trade-off. -- **Want to explore alternative interfaces for the deepened module?** Call the Skill tool with "thirdshift:codebase-design" and use its design-it-twice parallel sub-agent pattern. +- **Want to explore alternative interfaces for the deepened module?** Use the `thirdshift-codebase-design` skill and its design-it-twice parallel sub-agent pattern. ### 4. Publish the plan @@ -698,8 +698,8 @@

      improve-codebase-arch Decide the plan's size. If one session, a single fresh context window, is enough for the whole change, the plan is a single **Ticket**. Otherwise it is a **Spec** with **Tickets**. Don't force a small refactor into a Spec. -- **A Spec with Tickets**: call the Skill tool with "thirdshift:to-spec" to publish the Spec, then with "thirdshift:to-tickets" to publish its Tickets as the Spec's sub-issues. -- **A single Ticket**: call the Skill tool with "thirdshift:to-tickets" to publish one standalone Ticket, with no parent. +- **A Spec with Tickets**: use the `thirdshift-to-spec` skill to publish the Spec, then the `thirdshift-to-tickets` skill to publish its Tickets as the Spec's sub-issues. +- **A single Ticket**: use the `thirdshift-to-tickets` skill to publish one standalone Ticket, with no parent. The plan's top issue, the Spec or the single Ticket, is labelled `needs-triage` and nothing else. thirdshift marks it ready once the session has ended and the plan passes its checks; you never do. Tickets under a Spec are labelled `ready-for-agent`. @@ -728,7 +728,7 @@

      improve-codebase-arch thirdshift reads only that line, so everything else you want read belongs in the issue.

      -

      improve-codebase-architecture/REPORT.md

      +

      thirdshift-improve-codebase-architecture/REPORT.md

      # Report Format
       
       The Architecture review's report is Markdown, written into the issue it publishes: the plan's top issue, or the idea issue. GitHub renders Mermaid in a fenced `mermaid` block, so the diagrams live where the issue is read. Write no file, and open nothing.
      @@ -737,7 +737,7 @@ 

      improve-codebase-arc ## Candidate card -The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `/thirdshift:codebase-design` skill) without ceremony. +The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `thirdshift-codebase-design` skill) without ceremony. ```md **Strength**: Strong · **Dependencies**: in-process @@ -824,7 +824,7 @@

      improve-codebase-arc ## Tone -Plain English, concise, but the architectural nouns and verbs come straight from the `/thirdshift:codebase-design` skill. Concision is not an excuse to drift. +Plain English, concise, but the architectural nouns and verbs come straight from the `thirdshift-codebase-design` skill. Concision is not an excuse to drift. **Use exactly:** module, interface, implementation, depth, deep, shallow, seam, adapter, leverage, locality. @@ -839,9 +839,9 @@

      improve-codebase-arc **Wins bullets** name the gain in glossary terms: *"locality: bugs concentrate in one module"*, *"leverage: one interface, N call sites"*, *"interface shrinks; implementation absorbs the wrappers"*. Don't write *"easier to maintain"* or *"cleaner code"*, because those terms aren't in the glossary and don't earn their place. -No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `/thirdshift:codebase-design` glossary, reach for one that is before inventing a new one. +No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `thirdshift-codebase-design` skill's glossary, reach for one that is before inventing a new one.

      -

      improve-codebase-architecture/agents/openai.yaml

      +

      thirdshift-improve-codebase-architecture/agents/openai.yaml

      interface:
         display_name: "Improve Codebase Architecture"
         short_description: "Find and publish the top architecture improvement"
      @@ -849,12 +849,12 @@ 

      improve-cod allow_implicit_invocation: true

      -
      -

      pr

      +
      +

      thirdshift-pr

      Used by 3 · Y · Review

      -

      pr/SKILL.md

      +

      thirdshift-pr/SKILL.md

      ---
      -name: pr
      +name: thirdshift-pr
       description: "Use when writing a PR body."
       metadata:
         credits:
      @@ -1022,23 +1022,23 @@ 

      pr/SKILL.md

      The blast radius is the potential impact or scope of the changes introduced by this PR. Consider all possibilities. Examples are layout shift, breakages for consumers, mobile responsiveness, etc.
      -

      pr/CREDITS.md

      +

      thirdshift-pr/CREDITS.md

      # Credits
       
       "The shape of the change" section is [Dex Horthy](https://github.com/dexhorthy)'s [`show-me`](https://github.com/humanlayer/humanlayer) skill, reproduced almost word for word, aimed at a diff instead of a live conversation. `pr` does not depend on `show-me` as a skill (it isn't part of this repo, and a hard dependency would break standalone installs), so the content is copied in rather than pointed at; this file is the attribution a dependency would otherwise have carried.
       
      -

      pr/agents/openai.yaml

      +

      thirdshift-pr/agents/openai.yaml

      interface:
         display_name: "PR"
         short_description: "Write a PR body that's fast to review"
       
      -
      -

      resolving-merge-conflicts

      +
      +

      thirdshift-resolving-merge-conflicts

      Used by 4 · K · Finish, in a Repair

      -

      resolving-merge-conflicts/SKILL.md

      +

      thirdshift-resolving-merge-conflicts/SKILL.md

      ---
      -name: resolving-merge-conflicts
      +name: thirdshift-resolving-merge-conflicts
       description: "Use when you need to resolve an in-progress git merge/rebase conflict."
       ---
       
      @@ -1051,18 +1051,18 @@ 

      resolving-merge-conflicts 4. Discover the project's **automated checks** and run them, typically typecheck, then tests, then format. Fix anything the merge broke. 5. **Finish the merge/rebase.** Stage everything, commit, and push. If rebasing, continue the rebase process until all commits are rebased.

      -

      resolving-merge-conflicts/agents/openai.yaml

      +

      thirdshift-resolving-merge-conflicts/agents/openai.yaml

      interface:
         display_name: "Resolving Merge Conflicts"
         short_description: "Resolve merge and rebase conflicts"
       
      -
      -

      tdd

      +
      +

      thirdshift-tdd

      Used by 2 · M · Implement

      -

      tdd/SKILL.md

      +

      thirdshift-tdd/SKILL.md

      ---
      -name: tdd
      +name: thirdshift-tdd
       description: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
       ---
       
      @@ -1086,7 +1086,7 @@ 

      tdd/SKILL.md

      Decide: "What's the public interface, and which seams should we test?" -When the shape of that interface is itself in question (how deep the module is, where the seam belongs, what the interface should expose), call the Skill tool with "codebase-design" for the vocabulary. It is the shared source of the module, interface, depth, seam, adapter, leverage and locality terms, and it is a reference to consult, not a session to run. +When the shape of that interface is itself in question (how deep the module is, where the seam belongs, what the interface should expose), use the `thirdshift-codebase-design` skill for the vocabulary. It is the shared source of the module, interface, depth, seam, adapter, leverage and locality terms, and it is a reference to consult, not a session to run. ## Anti-patterns @@ -1098,14 +1098,14 @@

      tdd/SKILL.md

      - **Red before green.** Write the failing test first, then only enough code to pass it. Don't anticipate future tests or add speculative features. - **One slice at a time.** One seam, one test, one minimal implementation per cycle. -- **Refactoring is not part of the loop.** It belongs to the review stage (see the `code-review` skill), not the red → green implementation cycle. +- **Refactoring is not part of the loop.** It belongs to the review stage (see the `thirdshift-code-review` skill), not the red → green implementation cycle.
      -

      tdd/agents/openai.yaml

      +

      thirdshift-tdd/agents/openai.yaml

      interface:
         display_name: "TDD"
         short_description: "Test-driven red-green-refactor"
       
      -

      tdd/mocking.md

      +

      thirdshift-tdd/mocking.md

      # When to Mock
       
       Mock at **system boundaries** only:
      @@ -1166,7 +1166,7 @@ 

      tdd/mocking.md

      - Easier to see which endpoints a test exercises - Type safety per endpoint
      -

      tdd/tests.md

      +

      thirdshift-tdd/tests.md

      # Good and Bad Tests
       
       ## Good Tests
      @@ -1246,12 +1246,12 @@ 

      tdd/tests.md

      ```
      -
      -

      to-spec

      +
      +

      thirdshift-to-spec

      Used by the Architecture review, in an Architect run

      -

      to-spec/SKILL.md

      +

      thirdshift-to-spec/SKILL.md

      ---
      -name: to-spec
      +name: thirdshift-to-spec
       description: "Turn the current session into a spec and publish it to the project issue tracker: no interview, just synthesis of what you've already settled. Only for an Architecture review."
       disable-model-invocation: false
       ---
      @@ -1328,7 +1328,7 @@ 

      to-spec/SKILL.md

      </spec-template>
      -

      to-spec/agents/openai.yaml

      +

      thirdshift-to-spec/agents/openai.yaml

      interface:
         display_name: "To Spec"
         short_description: "Turn a session into a spec"
      @@ -1336,12 +1336,12 @@ 

      to-spec/agents/openai.yaml allow_implicit_invocation: true

      -
      -

      to-tickets

      +
      +

      thirdshift-to-tickets

      Used by the Architecture review, in an Architect run

      -

      to-tickets/SKILL.md

      +

      thirdshift-to-tickets/SKILL.md

      ---
      -name: to-tickets
      +name: thirdshift-to-tickets
       description: Break a plan, spec, or the current session into a set of tracer-bullet tickets, each declaring its blocking edges, published to the issue tracker with native sub-issue and blocking links. Only for an Architecture review.
       disable-model-invocation: false
       ---
      @@ -1441,7 +1441,7 @@ 

      to-tickets/SKILL.md

      Avoid specific file paths or code snippets: they go stale fast. Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it and note briefly that it came from a prototype. Trim to the decision-rich parts, not a working demo, just the important bits.
      -

      to-tickets/agents/openai.yaml

      +

      thirdshift-to-tickets/agents/openai.yaml

      interface:
         display_name: "To Tickets"
         short_description: "Split a plan into tracer-bullet tickets"
      @@ -1455,7 +1455,7 @@ 

      to-tickets/agents/openai.yaml<

      Licence and credits

      Whose skills these are

      -

      The Factory skills are adapted from Matt Pocock's skills, under this licence. The pr skill credits Dex Horthy's show-me skill in its CREDITS.md.

      +

      The Factory skills are adapted from Matt Pocock's skills, under this licence. The thirdshift-pr skill credits Dex Horthy's show-me skill in its CREDITS.md.

      LICENSE

      MIT License
       
      diff --git a/skills/code-review/SKILL.md b/skills/thirdshift-code-review/SKILL.md
      similarity index 99%
      rename from skills/code-review/SKILL.md
      rename to skills/thirdshift-code-review/SKILL.md
      index d701b736e..e2f2e8ce2 100644
      --- a/skills/code-review/SKILL.md
      +++ b/skills/thirdshift-code-review/SKILL.md
      @@ -1,5 +1,5 @@
       ---
      -name: code-review
      +name: thirdshift-code-review
       description: "Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\"."
       ---
       
      diff --git a/skills/code-review/agents/openai.yaml b/skills/thirdshift-code-review/agents/openai.yaml
      similarity index 100%
      rename from skills/code-review/agents/openai.yaml
      rename to skills/thirdshift-code-review/agents/openai.yaml
      diff --git a/skills/codebase-design/DEEPENING.md b/skills/thirdshift-codebase-design/DEEPENING.md
      similarity index 100%
      rename from skills/codebase-design/DEEPENING.md
      rename to skills/thirdshift-codebase-design/DEEPENING.md
      diff --git a/skills/codebase-design/DESIGN-IT-TWICE.md b/skills/thirdshift-codebase-design/DESIGN-IT-TWICE.md
      similarity index 100%
      rename from skills/codebase-design/DESIGN-IT-TWICE.md
      rename to skills/thirdshift-codebase-design/DESIGN-IT-TWICE.md
      diff --git a/skills/codebase-design/SKILL.md b/skills/thirdshift-codebase-design/SKILL.md
      similarity index 99%
      rename from skills/codebase-design/SKILL.md
      rename to skills/thirdshift-codebase-design/SKILL.md
      index 37e47d19e..c0daf2e9f 100644
      --- a/skills/codebase-design/SKILL.md
      +++ b/skills/thirdshift-codebase-design/SKILL.md
      @@ -1,5 +1,5 @@
       ---
      -name: codebase-design
      +name: thirdshift-codebase-design
       description: Shared vocabulary for designing deep modules. Use when designing or improving a module's interface, finding deepening opportunities, deciding where a seam goes, making code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.
       ---
       
      diff --git a/skills/codebase-design/agents/openai.yaml b/skills/thirdshift-codebase-design/agents/openai.yaml
      similarity index 100%
      rename from skills/codebase-design/agents/openai.yaml
      rename to skills/thirdshift-codebase-design/agents/openai.yaml
      diff --git a/skills/implement/SKILL.md b/skills/thirdshift-implement/SKILL.md
      similarity index 66%
      rename from skills/implement/SKILL.md
      rename to skills/thirdshift-implement/SKILL.md
      index 9c5da65a7..adfdbfbf3 100644
      --- a/skills/implement/SKILL.md
      +++ b/skills/thirdshift-implement/SKILL.md
      @@ -1,15 +1,15 @@
       ---
      -name: implement
      +name: thirdshift-implement
       description: "Implement a piece of work based on a spec or set of tickets."
       disable-model-invocation: false
       ---
       
       Implement the work described by the user in the spec or tickets.
       
      -Use /thirdshift:tdd where possible, at pre-agreed seams.
      +Use the `thirdshift-tdd` skill where possible, at pre-agreed seams.
       
       Run typechecking regularly, single test files regularly, and the full test suite once at the end.
       
      -Once done, use /thirdshift:code-review to review the work.
      +Once done, use the `thirdshift-code-review` skill to review the work.
       
       Commit your work to the current branch.
      diff --git a/skills/implement/agents/openai.yaml b/skills/thirdshift-implement/agents/openai.yaml
      similarity index 100%
      rename from skills/implement/agents/openai.yaml
      rename to skills/thirdshift-implement/agents/openai.yaml
      diff --git a/skills/improve-codebase-architecture/REPORT.md b/skills/thirdshift-improve-codebase-architecture/REPORT.md
      similarity index 93%
      rename from skills/improve-codebase-architecture/REPORT.md
      rename to skills/thirdshift-improve-codebase-architecture/REPORT.md
      index 44224c032..6d967aa67 100644
      --- a/skills/improve-codebase-architecture/REPORT.md
      +++ b/skills/thirdshift-improve-codebase-architecture/REPORT.md
      @@ -6,7 +6,7 @@ The report is one candidate's card: the top recommendation's. The candidates tha
       
       ## Candidate card
       
      -The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `/thirdshift:codebase-design` skill) without ceremony.
      +The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `thirdshift-codebase-design` skill) without ceremony.
       
       ```md
       **Strength**: Strong · **Dependencies**: in-process
      @@ -93,7 +93,7 @@ flowchart LR
       
       ## Tone
       
      -Plain English, concise, but the architectural nouns and verbs come straight from the `/thirdshift:codebase-design` skill. Concision is not an excuse to drift.
      +Plain English, concise, but the architectural nouns and verbs come straight from the `thirdshift-codebase-design` skill. Concision is not an excuse to drift.
       
       **Use exactly:** module, interface, implementation, depth, deep, shallow, seam, adapter, leverage, locality.
       
      @@ -108,4 +108,4 @@ Plain English, concise, but the architectural nouns and verbs come straight from
       
       **Wins bullets** name the gain in glossary terms: *"locality: bugs concentrate in one module"*, *"leverage: one interface, N call sites"*, *"interface shrinks; implementation absorbs the wrappers"*. Don't write *"easier to maintain"* or *"cleaner code"*, because those terms aren't in the glossary and don't earn their place.
       
      -No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `/thirdshift:codebase-design` glossary, reach for one that is before inventing a new one.
      +No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `thirdshift-codebase-design` skill's glossary, reach for one that is before inventing a new one.
      diff --git a/skills/improve-codebase-architecture/SKILL.md b/skills/thirdshift-improve-codebase-architecture/SKILL.md
      similarity index 89%
      rename from skills/improve-codebase-architecture/SKILL.md
      rename to skills/thirdshift-improve-codebase-architecture/SKILL.md
      index 571918d39..af6fb068c 100644
      --- a/skills/improve-codebase-architecture/SKILL.md
      +++ b/skills/thirdshift-improve-codebase-architecture/SKILL.md
      @@ -1,5 +1,5 @@
       ---
      -name: improve-codebase-architecture
      +name: thirdshift-improve-codebase-architecture
       description: "Scan a codebase for deepening opportunities, take the top recommendation, and publish it to the issue tracker: as a plan when it is Strong, as an idea otherwise. Only for an Architecture review."
       disable-model-invocation: false
       ---
      @@ -14,7 +14,7 @@ It changes nothing in the repository. Never commit, push, create a branch or ope
       
       This command is _informed_ by the project's domain model and built on a shared design vocabulary:
       
      -- Call the Skill tool with "thirdshift:codebase-design" for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use these terms exactly in every suggestion, and don't drift into "component," "service," "API," or "boundary."
      +- Use the `thirdshift-codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use these terms exactly in every suggestion, and don't drift into "component," "service," "API," or "boundary."
       - The domain language in `CONTEXT.md` gives names to good seams; ADRs in `docs/adr/` record decisions this command should not re-litigate.
       
       The issue tracker and triage label vocabulary should have been provided to you. If `docs/agents/issue-tracker.md` is missing, fall back to the `gh` CLI. If `docs/agents/triage-labels.md` is missing, use the label names as written here, creating a label the tracker lacks (`gh label create`).
      @@ -71,7 +71,7 @@ Every other candidate is dropped. Don't file it, and don't list it in the plan o
       
       If the scan surfaced no candidate at all, file nothing, and say so plainly in place of the final line of step 6: there is no issue for it to name.
       
      -**Use CONTEXT.md vocabulary for the domain, and the `/thirdshift:codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module," not "the FooBarHandler," and not "the Order service."
      +**Use CONTEXT.md vocabulary for the domain, and the `thirdshift-codebase-design` skill's vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module," not "the FooBarHandler," and not "the Order service."
       
       **ADR conflicts**: if a candidate contradicts an existing ADR, only keep it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the report (e.g. a warning callout: _"contradicts ADR-0007, but worth reopening because…"_). Don't raise every theoretical refactor an ADR forbids.
       
      @@ -92,7 +92,7 @@ Side effects are recorded, never written:
       - **Naming a deepened module after a concept not in `CONTEXT.md`?** Record the term and its definition as work for a Ticket.
       - **Sharpening a fuzzy term as you decide?** Record the sharper definition as work for a Ticket.
       - **Reopening an ADR, or making a decision worth one?** Record the new or changed ADR as work for a Ticket. A decision is worth an ADR only when it is hard to reverse, surprising without context, and the result of a real trade-off.
      -- **Want to explore alternative interfaces for the deepened module?** Call the Skill tool with "thirdshift:codebase-design" and use its design-it-twice parallel sub-agent pattern.
      +- **Want to explore alternative interfaces for the deepened module?** Use the `thirdshift-codebase-design` skill and its design-it-twice parallel sub-agent pattern.
       
       ### 4. Publish the plan
       
      @@ -100,8 +100,8 @@ Only for a `Strong` top recommendation whose design is settled.
       
       Decide the plan's size. If one session, a single fresh context window, is enough for the whole change, the plan is a single **Ticket**. Otherwise it is a **Spec** with **Tickets**. Don't force a small refactor into a Spec.
       
      -- **A Spec with Tickets**: call the Skill tool with "thirdshift:to-spec" to publish the Spec, then with "thirdshift:to-tickets" to publish its Tickets as the Spec's sub-issues.
      -- **A single Ticket**: call the Skill tool with "thirdshift:to-tickets" to publish one standalone Ticket, with no parent.
      +- **A Spec with Tickets**: use the `thirdshift-to-spec` skill to publish the Spec, then the `thirdshift-to-tickets` skill to publish its Tickets as the Spec's sub-issues.
      +- **A single Ticket**: use the `thirdshift-to-tickets` skill to publish one standalone Ticket, with no parent.
       
       The plan's top issue, the Spec or the single Ticket, is labelled `needs-triage` and nothing else. thirdshift marks it ready once the session has ended and the plan passes its checks; you never do. Tickets under a Spec are labelled `ready-for-agent`.
       
      diff --git a/skills/improve-codebase-architecture/agents/openai.yaml b/skills/thirdshift-improve-codebase-architecture/agents/openai.yaml
      similarity index 100%
      rename from skills/improve-codebase-architecture/agents/openai.yaml
      rename to skills/thirdshift-improve-codebase-architecture/agents/openai.yaml
      diff --git a/skills/pr/CREDITS.md b/skills/thirdshift-pr/CREDITS.md
      similarity index 100%
      rename from skills/pr/CREDITS.md
      rename to skills/thirdshift-pr/CREDITS.md
      diff --git a/skills/pr/SKILL.md b/skills/thirdshift-pr/SKILL.md
      similarity index 99%
      rename from skills/pr/SKILL.md
      rename to skills/thirdshift-pr/SKILL.md
      index 8d71b376a..381e587eb 100644
      --- a/skills/pr/SKILL.md
      +++ b/skills/thirdshift-pr/SKILL.md
      @@ -1,5 +1,5 @@
       ---
      -name: pr
      +name: thirdshift-pr
       description: "Use when writing a PR body."
       metadata:
         credits:
      diff --git a/skills/pr/agents/openai.yaml b/skills/thirdshift-pr/agents/openai.yaml
      similarity index 100%
      rename from skills/pr/agents/openai.yaml
      rename to skills/thirdshift-pr/agents/openai.yaml
      diff --git a/skills/thirdshift-resolving-merge-conflicts/SKILL.md b/skills/thirdshift-resolving-merge-conflicts/SKILL.md
      new file mode 100644
      index 000000000..14a23d097
      --- /dev/null
      +++ b/skills/thirdshift-resolving-merge-conflicts/SKILL.md
      @@ -0,0 +1,14 @@
      +---
      +name: thirdshift-resolving-merge-conflicts
      +description: "Use when you need to resolve an in-progress git merge/rebase conflict."
      +---
      +
      +1. **See the current state** of the merge/rebase. Check git history, and the conflicting files.
      +
      +2. **Find the primary sources** for each conflict. Understand deeply why each change was made, and what the original intent was. Read the commit messages, check the PRs, check original issues/tickets.
      +
      +3. **Resolve each hunk.** Preserve both intents where possible. Where incompatible, pick the one matching the merge's stated goal and note the trade-off. Do **not** invent new behaviour. Always resolve; never `--abort`.
      +
      +4. Discover the project's **automated checks** and run them, typically typecheck, then tests, then format. Fix anything the merge broke.
      +
      +5. **Finish the merge/rebase.** Stage everything, commit, and push. If rebasing, continue the rebase process until all commits are rebased.
      \ No newline at end of file
      diff --git a/skills/thirdshift-resolving-merge-conflicts/agents/openai.yaml b/skills/thirdshift-resolving-merge-conflicts/agents/openai.yaml
      new file mode 100644
      index 000000000..331ffb9d3
      --- /dev/null
      +++ b/skills/thirdshift-resolving-merge-conflicts/agents/openai.yaml
      @@ -0,0 +1,3 @@
      +interface:
      +  display_name: "Resolving Merge Conflicts"
      +  short_description: "Resolve merge and rebase conflicts"
      diff --git a/skills/tdd/SKILL.md b/skills/thirdshift-tdd/SKILL.md
      similarity index 94%
      rename from skills/tdd/SKILL.md
      rename to skills/thirdshift-tdd/SKILL.md
      index df8b44d13..e5cc55c2f 100644
      --- a/skills/tdd/SKILL.md
      +++ b/skills/thirdshift-tdd/SKILL.md
      @@ -1,5 +1,5 @@
       ---
      -name: tdd
      +name: thirdshift-tdd
       description: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
       ---
       
      @@ -23,7 +23,7 @@ A **seam** is the public boundary you test at: the interface where you observe b
       
       Decide: "What's the public interface, and which seams should we test?"
       
      -When the shape of that interface is itself in question (how deep the module is, where the seam belongs, what the interface should expose), call the Skill tool with "codebase-design" for the vocabulary. It is the shared source of the module, interface, depth, seam, adapter, leverage and locality terms, and it is a reference to consult, not a session to run.
      +When the shape of that interface is itself in question (how deep the module is, where the seam belongs, what the interface should expose), use the `thirdshift-codebase-design` skill for the vocabulary. It is the shared source of the module, interface, depth, seam, adapter, leverage and locality terms, and it is a reference to consult, not a session to run.
       
       ## Anti-patterns
       
      @@ -35,4 +35,4 @@ When the shape of that interface is itself in question (how deep the module is,
       
       - **Red before green.** Write the failing test first, then only enough code to pass it. Don't anticipate future tests or add speculative features.
       - **One slice at a time.** One seam, one test, one minimal implementation per cycle.
      -- **Refactoring is not part of the loop.** It belongs to the review stage (see the `code-review` skill), not the red → green implementation cycle.
      +- **Refactoring is not part of the loop.** It belongs to the review stage (see the `thirdshift-code-review` skill), not the red → green implementation cycle.
      diff --git a/skills/tdd/agents/openai.yaml b/skills/thirdshift-tdd/agents/openai.yaml
      similarity index 100%
      rename from skills/tdd/agents/openai.yaml
      rename to skills/thirdshift-tdd/agents/openai.yaml
      diff --git a/skills/tdd/mocking.md b/skills/thirdshift-tdd/mocking.md
      similarity index 100%
      rename from skills/tdd/mocking.md
      rename to skills/thirdshift-tdd/mocking.md
      diff --git a/skills/tdd/tests.md b/skills/thirdshift-tdd/tests.md
      similarity index 100%
      rename from skills/tdd/tests.md
      rename to skills/thirdshift-tdd/tests.md
      diff --git a/skills/to-spec/SKILL.md b/skills/thirdshift-to-spec/SKILL.md
      similarity index 99%
      rename from skills/to-spec/SKILL.md
      rename to skills/thirdshift-to-spec/SKILL.md
      index 3fc5ec829..56dc42ed1 100644
      --- a/skills/to-spec/SKILL.md
      +++ b/skills/thirdshift-to-spec/SKILL.md
      @@ -1,5 +1,5 @@
       ---
      -name: to-spec
      +name: thirdshift-to-spec
       description: "Turn the current session into a spec and publish it to the project issue tracker: no interview, just synthesis of what you've already settled. Only for an Architecture review."
       disable-model-invocation: false
       ---
      diff --git a/skills/to-spec/agents/openai.yaml b/skills/thirdshift-to-spec/agents/openai.yaml
      similarity index 100%
      rename from skills/to-spec/agents/openai.yaml
      rename to skills/thirdshift-to-spec/agents/openai.yaml
      diff --git a/skills/to-tickets/SKILL.md b/skills/thirdshift-to-tickets/SKILL.md
      similarity index 99%
      rename from skills/to-tickets/SKILL.md
      rename to skills/thirdshift-to-tickets/SKILL.md
      index a15acbe9e..2ed473f33 100644
      --- a/skills/to-tickets/SKILL.md
      +++ b/skills/thirdshift-to-tickets/SKILL.md
      @@ -1,5 +1,5 @@
       ---
      -name: to-tickets
      +name: thirdshift-to-tickets
       description: Break a plan, spec, or the current session into a set of tracer-bullet tickets, each declaring its blocking edges, published to the issue tracker with native sub-issue and blocking links. Only for an Architecture review.
       disable-model-invocation: false
       ---
      diff --git a/skills/to-tickets/agents/openai.yaml b/skills/thirdshift-to-tickets/agents/openai.yaml
      similarity index 100%
      rename from skills/to-tickets/agents/openai.yaml
      rename to skills/thirdshift-to-tickets/agents/openai.yaml
      diff --git a/src/architect.rs b/src/architect.rs
      index b679b3db1..f5ed92e2a 100644
      --- a/src/architect.rs
      +++ b/src/architect.rs
      @@ -198,7 +198,7 @@ impl fmt::Display for Reviewed {
       /// idea. With `launch_pull`, the Launch directory's checkout of the Base
       /// branch, if that is the branch checked out, is first brought up to date
       /// with origin. The review's
      -/// worktree and the plugin directory are gone when this returns. A failure
      +/// worktree is gone when this returns. A failure
       /// after the plan is published leaves its labels as the review left them.
       ///
       /// Once the preflight checks pass, and before anything else, the Architect
      diff --git a/src/command.rs b/src/command.rs
      index 95c97e5c4..9f1380682 100644
      --- a/src/command.rs
      +++ b/src/command.rs
      @@ -6,8 +6,9 @@
       //! A command [`start`]s: it begins its record in the logs, loads the User
       //! config, offering Setup, installs the interrupt handler and checks its Run
       //! notification, in that order. It does its work, then [`Started::finish`]es
      -//! with its [`Ending`]: the Activity log's end line, the terminal display and
      -//! the Run notification, in that order, none of the first and last for a
      +//! with its [`Ending`]: the Factory skills it wrote out for its sessions
      +//! removed, then the Activity log's end line, the terminal display and the
      +//! Run notification, in that order, only the display of those three for a
       //! skipped pass.
       
       use std::process::ExitCode;
      @@ -23,6 +24,7 @@ use crate::pickup;
       use crate::progress;
       use crate::run::Ended;
       use crate::run_ending;
      +use crate::skills;
       
       /// How a command that did factory work, or was skipped before any, ended.
       pub enum Ending {
      @@ -114,12 +116,14 @@ impl Started {
               }
           }
       
      -    /// Finish the command that ended as `ending`: write the Activity log's
      +    /// Finish the command that ended as `ending`: remove the Factory skills
      +    /// it wrote out for its sessions, if it did, write the Activity log's
           /// end line, show how it ended, then send its Run notification, if it
           /// asked for one. A skipped pass only shows how it ended: its skip is
           /// recorded where it was decided, and it sends no notification. Returns
           /// its exit code.
           pub fn finish(self, ending: Ending) -> ExitCode {
      +        skills::remove();
               let account = match run_ending::read(&ending) {
                   Ok(account) => account,
                   Err(skip) => return run_ending::show_skip(skip),
      diff --git a/src/main.rs b/src/main.rs
      index f45022f69..507cd1b97 100644
      --- a/src/main.rs
      +++ b/src/main.rs
      @@ -22,7 +22,6 @@ mod logs;
       mod notification;
       mod pass;
       mod pickup;
      -mod plugin;
       mod poll;
       mod progress;
       mod prompt;
      @@ -34,6 +33,7 @@ mod resend_key;
       mod run;
       mod run_ending;
       mod session;
      +mod skills;
       mod spec_run;
       mod update;
       mod worktree;
      diff --git a/src/plugin.rs b/src/plugin.rs
      deleted file mode 100644
      index b08544508..000000000
      --- a/src/plugin.rs
      +++ /dev/null
      @@ -1,49 +0,0 @@
      -//! The Factory skills, embedded at compile time and written out as the
      -//! `thirdshift` plugin (ADR-0001).
      -
      -use std::fs;
      -use std::path::Path;
      -
      -use anyhow::{Context, Result};
      -use include_dir::{Dir, include_dir};
      -use tempfile::TempDir;
      -
      -/// The Factory skills, as the plugin writes them out and the Prompts and skills
      -/// page shows them.
      -pub static SKILLS: Dir = include_dir!("$CARGO_MANIFEST_DIR/skills");
      -
      -/// A temp directory holding the Factory skills and a generated plugin
      -/// manifest, removed when dropped.
      -pub struct Plugin {
      -    dir: TempDir,
      -}
      -
      -impl Plugin {
      -    pub fn write() -> Result {
      -        let dir = tempfile::Builder::new()
      -            .prefix("thirdshift-plugin-")
      -            .tempdir()?;
      -        let skills = dir.path().join("skills");
      -        fs::create_dir_all(&skills)?;
      -        SKILLS
      -            .extract(&skills)
      -            .context("could not write the Factory skills")?;
      -
      -        let manifest_dir = dir.path().join(".claude-plugin");
      -        fs::create_dir_all(&manifest_dir)?;
      -        let manifest = serde_json::json!({
      -            "name": "thirdshift",
      -            "version": env!("CARGO_PKG_VERSION"),
      -            "description": "Factory skills for thirdshift's headless sessions",
      -        });
      -        fs::write(
      -            manifest_dir.join("plugin.json"),
      -            serde_json::to_string_pretty(&manifest)?,
      -        )?;
      -        Ok(Plugin { dir })
      -    }
      -
      -    pub fn path(&self) -> &Path {
      -        self.dir.path()
      -    }
      -}
      diff --git a/src/progress.rs b/src/progress.rs
      index b055a9701..8433d0fa2 100644
      --- a/src/progress.rs
      +++ b/src/progress.rs
      @@ -354,9 +354,9 @@ mod tests {
               assert_eq!(
                   line_for(tool_use(
                       "Skill",
      -                json!({ "skill": "thirdshift:tdd", "args": "x" })
      +                json!({ "skill": "thirdshift-tdd", "args": "x" })
                   )),
      -            Some("skill thirdshift:tdd".to_string())
      +            Some("skill thirdshift-tdd".to_string())
               );
           }
       
      diff --git a/src/prompt.rs b/src/prompt.rs
      index db382ea13..493e86e09 100644
      --- a/src/prompt.rs
      +++ b/src/prompt.rs
      @@ -23,10 +23,10 @@ const HEADLESS: &str = "You run headless: nobody is watching, and ending your tu
       /// The fresh prompt, for a run that starts a new Issue branch.
       pub fn fresh(issue: &IssueUrl, base: &str, branch: &str) -> String {
           format!(
      -        "/thirdshift:implement {url}\n\
      -         The base branch is {base}. Review with /thirdshift:code-review using {base} as the fixed point.\n\
      +        "/thirdshift-implement {url}\n\
      +         The base branch is {base}. Review with the `thirdshift-code-review` skill using {base} as the fixed point.\n\
                Address the Standards and Spec findings you agree with.\n\
      -         Push branch {branch} and create a pull request against {base} using /thirdshift:pr, marked ready for review.\n\
      +         Push branch {branch} and create a pull request against {base} using the `thirdshift-pr` skill, marked ready for review.\n\
                In the PR body, add an \"Unaddressed findings\" section listing each skipped finding under Standards or Spec, with at least a one-line reason.\n\
                Include \"Closes #{number}\" in the PR body.\n\
                {HEADLESS}",
      @@ -40,18 +40,18 @@ pub fn fresh(issue: &IssueUrl, base: &str, branch: &str) -> String {
       pub fn continuation(issue: &IssueUrl, base: &str, branch: &str, pr_url: Option<&str>) -> String {
           let pr = match pr_url {
               Some(url) => format!(
      -            "Update PR {url} using /thirdshift:pr, rewriting its body to cover the whole branch, marked ready for review."
      +            "Update PR {url} using the `thirdshift-pr` skill, rewriting its body to cover the whole branch, marked ready for review."
               ),
               None => format!(
      -            "Create a pull request against {base} using /thirdshift:pr, marked ready for review."
      +            "Create a pull request against {base} using the `thirdshift-pr` skill, marked ready for review."
               ),
           };
           format!(
      -        "/thirdshift:implement {url}\n\
      +        "/thirdshift-implement {url}\n\
                \n\
                You are continuing work on branch {branch}, which already has commits (see git log {base}..HEAD). Build on them; don't start over.\n\
                \n\
      -         The base branch is {base}. Review with /thirdshift:code-review using {base} as the fixed point.\n\
      +         The base branch is {base}. Review with the `thirdshift-code-review` skill using {base} as the fixed point.\n\
                \n\
                Address the Standards and Spec findings you agree with.\n\
                \n\
      @@ -73,17 +73,17 @@ pub fn continuation(issue: &IssueUrl, base: &str, branch: &str, pr_url: Option<&
       /// Ticket has landed on it, with the draft Spec PR `pr_url` into `base`.
       pub fn spec_review(spec: &IssueUrl, base: &str, branch: &str, pr_url: &str) -> String {
           format!(
      -        "/thirdshift:code-review {base}, with the Spec {url} as the spec\n\
      +        "/thirdshift-code-review {base}, with the Spec {url} as the spec\n\
                \n\
                Every Ticket of the Spec {url} has landed on branch {branch}, its Spec branch (see git log {base}..HEAD). Review the Spec as a whole, including how the Tickets' work fits together.\n\
                \n\
      -         Review with /thirdshift:code-review using {base} as the fixed point and {url} as the spec.\n\
      +         Review with the `thirdshift-code-review` skill using {base} as the fixed point and {url} as the spec.\n\
                \n\
      -         Address the Standards and Spec findings you agree with, using /thirdshift:tdd where it fits, and commit.\n\
      +         Address the Standards and Spec findings you agree with, using the `thirdshift-tdd` skill where it fits, and commit.\n\
                \n\
                Push branch {branch}. Do not rebase or force-push.\n\
                \n\
      -         Update PR {pr_url} using /thirdshift:pr, rewriting its body to cover the whole Spec. Leave out its Tickets checklist, or keep it between its markers as it is: thirdshift puts it back. Leave the PR a draft: thirdshift marks it ready once you are done.\n\
      +         Update PR {pr_url} using the `thirdshift-pr` skill, rewriting its body to cover the whole Spec. Leave out its Tickets checklist, or keep it between its markers as it is: thirdshift puts it back. Leave the PR a draft: thirdshift marks it ready once you are done.\n\
                \n\
                In the PR body, add an \"Unaddressed findings\" section listing each skipped finding under Standards or Spec, with at least a one-line reason.\n\
                \n\
      @@ -113,14 +113,14 @@ pub fn architecture_review(base: &str, focus: Option<&str>) -> String {
               None => String::new(),
           };
           format!(
      -        "/thirdshift:improve-codebase-architecture\n\
      +        "/thirdshift-improve-codebase-architecture\n\
                \n\
                This is an Architecture review of the base branch {base}: this worktree is checked out at its head on origin, on no branch.\n\
                \n\
                {focus}\
      -         Find the deepening opportunities with /thirdshift:improve-codebase-architecture, using /thirdshift:codebase-design for the vocabulary. Skip any that an open issue already covers, and take the top recommendation.\n\
      +         Find the deepening opportunities with the `thirdshift-improve-codebase-architecture` skill, using the `thirdshift-codebase-design` skill for the vocabulary. Skip any that an open issue already covers, and take the top recommendation.\n\
                \n\
      -         If it is Strong, settle its design yourself and publish it as the plan with /thirdshift:to-spec and /thirdshift:to-tickets: a Spec with Tickets, or a single Ticket when one session is enough. Label the plan's top issue, the Spec or the single Ticket, `{NEEDS_TRIAGE}`, not `{READY_FOR_AGENT}`: thirdshift marks it ready once you are done.\n\
      +         If it is Strong, settle its design yourself and publish it as the plan with the `thirdshift-to-spec` and `thirdshift-to-tickets` skills: a Spec with Tickets, or a single Ticket when one session is enough. Label the plan's top issue, the Spec or the single Ticket, `{NEEDS_TRIAGE}`, not `{READY_FOR_AGENT}`: thirdshift marks it ready once you are done.\n\
                \n\
                If it is not Strong, file it as one issue labelled `{NEEDS_TRIAGE}`, unless an open issue already covers it.\n\
                \n\
      @@ -140,7 +140,7 @@ pub fn architecture_review(base: &str, focus: Option<&str>) -> String {
       /// branch with Foreign commits on it.
       pub fn conflict_repair(issue: &IssueUrl, merging: &str, branch: &str, pr_url: &str) -> String {
           format!(
      -        "/thirdshift:resolving-merge-conflicts\n\
      +        "/thirdshift-resolving-merge-conflicts\n\
                \n\
                A merge of origin/{merging} into {branch} is in progress in this worktree and has conflicts.\n\
                {branch} implements {url}; its pull request is {pr_url}.\n\
      @@ -192,11 +192,11 @@ pub fn ci_fix_repair(
       /// the Issue branch on top of `own_head`, the head it last knew as its own.
       pub fn review_repair(issue: &IssueUrl, branch: &str, pr_url: &str, own_head: &str) -> String {
           format!(
      -        "/thirdshift:code-review {own_head}\n\
      +        "/thirdshift-code-review {own_head}\n\
                \n\
                Someone else pushed commits to {branch} while it was being worked on, and they have been merged into {branch} in this worktree. {branch} implements {url}; its pull request is {pr_url}. They are merged into the base branch only once you have reviewed them.\n\
                \n\
      -         Review with /thirdshift:code-review using {own_head} as the fixed point: {branch}'s head before their commits were merged in.\n\
      +         Review with the `thirdshift-code-review` skill using {own_head} as the fixed point: {branch}'s head before their commits were merged in.\n\
                \n\
                Address the Standards and Spec findings you agree with.\n\
                \n\
      diff --git a/src/prompts_page.rs b/src/prompts_page.rs
      index 6ebedce31..6f082cb86 100644
      --- a/src/prompts_page.rs
      +++ b/src/prompts_page.rs
      @@ -14,9 +14,9 @@ use include_dir::{Dir, File};
       use crate::ci::FailedChecks;
       use crate::github::{Check, CheckState};
       use crate::issue::IssueUrl;
      -use crate::plugin::SKILLS;
       use crate::prompt;
       use crate::session::claude_args;
      +use crate::skills::SKILLS;
       
       use Sender::{ArchitectRun, Units};
       
      @@ -55,10 +55,9 @@ const CHECK_URL: &str = "";
       const INHERITED_CHECK: &str = "";
       const BACKGROUND_WORK: &str = "";
       const FOCUS: &str = "";
      -const PLUGIN_DIR: &str = "";
       const SESSION_ID: &str = "";
       const PROMPT: &str = "";
      -const PLACEHOLDERS: [&str; 16] = [
      +const PLACEHOLDERS: [&str; 15] = [
           ISSUE_URL,
           SPEC_URL,
           NUMBER,
      @@ -72,7 +71,6 @@ const PLACEHOLDERS: [&str; 16] = [
           INHERITED_CHECK,
           BACKGROUND_WORK,
           FOCUS,
      -    PLUGIN_DIR,
           SESSION_ID,
           PROMPT,
       ];
      @@ -266,7 +264,7 @@ fn readme(prompts: &[Prompt]) -> String {
       
       Every Session prompt thirdshift can start or resume an agent session with, one file each, with placeholders such as `{BRANCH}` where each Run fills in its own values. The [Prompts and skills page](https://thirdshift.app/prompts/) shows the same prompts.
       
      -Every session runs `claude` headless in the Run's worktree, in auto permission mode, with the Factory skills loaded as a plugin from a temporary directory:
      +Every session runs `claude` headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's `.claude/skills/`, where Claude Code finds them, and kept out of git:
       
       ```sh
       {new}
      @@ -295,7 +293,7 @@ A [Resume]({resume_file}) continues the session that ended:
               );
           }
           readme.push_str(
      -        "\nThe Factory skills the prompts name are in [`skills/`](../skills/), as the plugin writes them out.\n",
      +        "\nThe Factory skills the prompts name are in [`skills/`](../skills/), as thirdshift writes them out.\n",
           );
           readme
       }
      @@ -322,10 +320,15 @@ fn fence(text: &str) -> String {
       /// when, if not always; or the Architecture review, which is no unit of a Run.
       fn skill_used_by(skill: &str) -> String {
           match skill {
      -        "implement" | "tdd" => unit_links(&[IMPLEMENT]),
      -        "code-review" | "pr" => unit_links(&[REVIEW]),
      -        "resolving-merge-conflicts" => format!("{}, in a Repair", unit_links(&[FINISH])),
      -        "improve-codebase-architecture" | "to-spec" | "to-tickets" | "codebase-design" => {
      +        "thirdshift-implement" | "thirdshift-tdd" => unit_links(&[IMPLEMENT]),
      +        "thirdshift-code-review" | "thirdshift-pr" => unit_links(&[REVIEW]),
      +        "thirdshift-resolving-merge-conflicts" => {
      +            format!("{}, in a Repair", unit_links(&[FINISH]))
      +        }
      +        "thirdshift-improve-codebase-architecture"
      +        | "thirdshift-to-spec"
      +        | "thirdshift-to-tickets"
      +        | "thirdshift-codebase-design" => {
                   "the Architecture review, in an Architect run".to_string()
               }
               _ => panic!("the Factory skill {skill} has no user: add it to skill_used_by"),
      @@ -346,9 +349,7 @@ fn render() -> String {
       /// The `claude` command line a session starts with, or, given a session id,
       /// resumes with, with the page's placeholders.
       fn command(resume: Option<&str>) -> String {
      -    let args = claude_args(OsStr::new(PLUGIN_DIR), resume, PROMPT);
      -    let args: Vec<_> = args.iter().map(|arg| arg.to_string_lossy()).collect();
      -    format!("claude {}", args.join(" "))
      +    format!("claude {}", claude_args(resume, PROMPT).join(" "))
       }
       
       fn command_line(html: &mut String) {
      @@ -357,7 +358,7 @@ fn command_line(html: &mut String) {
               r##"

      The command line

      How each session starts

      -

      Every session runs claude headless in the Run's worktree, in auto permission mode, with the Factory skills loaded as a plugin from a temporary directory. The prompt is one of the templates below.

      +

      Every session runs claude headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's .claude/skills/, where Claude Code finds them, and kept out of git. The prompt is one of the templates below.

      {new}

      A Resume continues the session that ended:

      {resume}
      @@ -403,7 +404,7 @@ fn skills_section(html: &mut String) { r##"

      Factory skills

      The skills, as the agent reads them

      -

      Every Factory skill and supporting file, raw, exactly as the plugin writes it out.

      +

      Every Factory skill and supporting file, raw, exactly as thirdshift writes it out.

      "##, ); @@ -441,7 +442,7 @@ fn licence_section(html: &mut String) { r##"

      Licence and credits

      Whose skills these are

      -

      The Factory skills are adapted from Matt Pocock's skills, under this licence. The pr skill credits Dex Horthy's show-me skill in its CREDITS.md.

      +

      The Factory skills are adapted from Matt Pocock's skills, under this licence. The thirdshift-pr skill credits Dex Horthy's show-me skill in its CREDITS.md.

      "##, ); let mut files: Vec<&File> = SKILLS.files().collect(); diff --git a/src/run.rs b/src/run.rs index 73bca72f6..a2729e277 100644 --- a/src/run.rs +++ b/src/run.rs @@ -124,8 +124,8 @@ pub fn run_to_end(issue: &IssueUrl, asks: &Asks, started_by: StartedBy) -> Ended /// Take `issue` to a ready PR, or in a Merge run a merged one. Any failure /// after the worktree exists, including a merge that fails, goes through the -/// Failed run path. The worktree, the local -/// Issue branch and the plugin directory are gone when this returns, except +/// Failed run path. The worktree and the local +/// Issue branch are gone when this returns, except /// that a Failed run whose work did not reach origin keeps the worktree and /// branch. With `asks.launch_pull`, the Launch directory's checkout of the /// Base branch, if that is the branch checked out, is first brought up to diff --git a/src/session.rs b/src/session.rs index da5abe292..97cdf5480 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,7 +1,6 @@ //! Headless Claude Code sessions and their logs. use std::cell::RefCell; -use std::ffi::OsStr; use std::fs::{self, File}; use std::io::{BufRead, BufReader, Read, Write}; use std::os::unix::process::CommandExt; @@ -15,9 +14,9 @@ use anyhow::{Context, Result, anyhow, bail}; use crate::interrupt; use crate::issue::{IssueUrl, Repo}; use crate::logs; -use crate::plugin::Plugin; use crate::progress::{self, Progress}; use crate::prompt; +use crate::skills; /// Where a Run's or an Architect run's Session logs go: in `sessions/` under /// the root of its repository's logs, under the User config's `logs.dir` or @@ -57,12 +56,11 @@ impl Logs { } /// Where a Run's or an Architect run's sessions run: in `worktree`, with the -/// Factory skills plugin loaded, each logged in its `logs`. It lasts for the -/// steps given to [`Sessions::within`], and so does the plugin. +/// Factory skills linked into it, each logged in its `logs`. It lasts for the +/// steps given to [`Sessions::within`]. pub struct Sessions<'a> { logs: &'a Logs, worktree: &'a Path, - plugin: Plugin, /// The log of the session started last, a Resume included. last_log: RefCell>, /// How each session ended whose last ending, its Resume's if it got one, @@ -73,26 +71,24 @@ pub struct Sessions<'a> { } impl<'a> Sessions<'a> { - /// Write the Factory skills plugin, then take `steps`, which run their - /// sessions through the `Sessions` they are given. Returns what `steps` - /// came to, with the most recent session's log, if a session created it. - /// If a session's last ending, its Resume's if it got one, left - /// background work to be killed and `steps` then fail, the failure names - /// that work ahead of its own cause: the session may have stopped short - /// of its job. The plugin directory is gone when this returns. + /// Link the Factory skills into `worktree`, then take `steps`, which run + /// their sessions through the `Sessions` they are given. Returns what + /// `steps` came to, with the most recent session's log, if a session + /// created it. If a session's last ending, its Resume's if it got one, + /// left background work to be killed and `steps` then fail, the failure + /// names that work ahead of its own cause: the session may have stopped + /// short of its job. pub fn within( logs: &'a Logs, worktree: &'a Path, steps: impl FnOnce(&Self) -> Result, ) -> (Result, Option) { - let plugin = match Plugin::write() { - Ok(plugin) => plugin, - Err(error) => return (Err(error), None), - }; + if let Err(error) = skills::link_into(worktree) { + return (Err(error), None); + } let sessions = Sessions { logs, worktree, - plugin, last_log: RefCell::default(), endings_with_killed_work: RefCell::default(), }; @@ -153,14 +149,7 @@ impl<'a> Sessions<'a> { let log = self.logs.path(kind); progress::step(format_args!("logging the session to {}", log.display())); *self.last_log.borrow_mut() = Some(log.clone()); - run( - kind, - self.worktree, - self.plugin.path(), - resume, - prompt, - &log, - ) + run(kind, self.worktree, resume, prompt, &log) } } @@ -176,8 +165,8 @@ fn ending_with(killed: &[&str]) -> String { } } -/// Run `claude` headless in auto mode in `worktree`, with the Factory skills -/// plugin at `plugin_dir` loaded, streaming its output to `log` and condensing +/// Run `claude` headless in auto mode in `worktree`, where it finds the +/// Factory skills, streaming its output to `log` and condensing /// it to progress lines on stderr, each labelled `kind`. With `resume`, the /// session with that id continues, given `prompt`. Returns what the stream /// showed once `claude` has exited cleanly. An interrupt stops the session and @@ -185,7 +174,6 @@ fn ending_with(killed: &[&str]) -> String { fn run( kind: &str, worktree: &Path, - plugin_dir: &Path, resume: Option<&str>, prompt: &str, log: &Path, @@ -197,7 +185,7 @@ fn run( File::create(log).with_context(|| format!("could not create {}", log.display()))?; let started = Instant::now(); let mut child = Command::new("claude") - .args(claude_args(plugin_dir.as_os_str(), resume, prompt)) + .args(claude_args(resume, prompt)) .current_dir(worktree) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -262,23 +250,21 @@ fn run( } /// The arguments every session runs `claude` with: headless in auto mode, -/// with the Factory skills plugin at `plugin_dir` loaded, streaming JSON. With -/// `resume`, the session with that id continues. The prompt comes last. -pub fn claude_args<'a>( - plugin_dir: &'a OsStr, - resume: Option<&'a str>, - prompt: &'a str, -) -> Vec<&'a OsStr> { - let mut args: Vec<&OsStr> = ["-p", "--permission-mode", "auto", "--plugin-dir"] - .into_iter() - .map(OsStr::new) - .collect(); - args.push(plugin_dir); - args.extend(["--output-format", "stream-json", "--verbose"].map(OsStr::new)); +/// streaming JSON. With `resume`, the session with that id continues. The +/// prompt comes last. +pub fn claude_args<'a>(resume: Option<&'a str>, prompt: &'a str) -> Vec<&'a str> { + let mut args = vec![ + "-p", + "--permission-mode", + "auto", + "--output-format", + "stream-json", + "--verbose", + ]; if let Some(session_id) = resume { - args.extend(["--resume", session_id].map(OsStr::new)); + args.extend(["--resume", session_id]); } - args.push(OsStr::new(prompt)); + args.push(prompt); args } diff --git a/src/skills.rs b/src/skills.rs new file mode 100644 index 000000000..a9acccd89 --- /dev/null +++ b/src/skills.rs @@ -0,0 +1,113 @@ +//! The Factory skills, embedded at compile time (ADR-0001), written out once +//! per Command and linked into each worktree its sessions run in, where +//! Claude Code finds project skills (ADR-0012). + +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, PoisonError}; + +use anyhow::{Context, Result}; +use include_dir::{Dir, include_dir}; +use tempfile::TempDir; + +use crate::git::Git; + +/// The Factory skills, as they are written out and the Prompts and skills +/// page shows them, each in its `thirdshift-` directory. +pub static SKILLS: Dir = include_dir!("$CARGO_MANIFEST_DIR/skills"); + +/// Where in a worktree Claude Code finds its project skills. +const PROJECT_SKILLS: &str = ".claude/skills"; + +/// The `.git/info/exclude` pattern that keeps the linked skills out of git. +const EXCLUDE: &str = "/.claude/skills/thirdshift-*"; + +/// The temp directory the Command wrote the Factory skills out to, once it +/// has. +static WRITTEN: Mutex> = Mutex::new(None); + +/// Link every Factory skill into `worktree`'s `.claude/skills/`, writing them +/// out first if the Command has not yet, and make sure the repository's +/// `.git/info/exclude` keeps them out of git. The links are left in place: +/// they go with the worktree. +pub fn link_into(worktree: &Path) -> Result<()> { + let written = written()?; + exclude(&Git::new(worktree))?; + let project_skills = worktree.join(PROJECT_SKILLS); + fs::create_dir_all(&project_skills) + .with_context(|| format!("could not create {}", project_skills.display()))?; + for skill in SKILLS.dirs() { + let name = skill.path(); + let link = project_skills.join(name); + if fs::symlink_metadata(&link).is_ok() { + fs::remove_file(&link) + .with_context(|| format!("could not replace {}", link.display()))?; + } + symlink(written.join(name), &link) + .with_context(|| format!("could not link {}", link.display()))?; + } + Ok(()) +} + +/// Remove the Factory skills the Command wrote out, if it did, once its +/// sessions are over. +pub fn remove() { + drop( + WRITTEN + .lock() + .unwrap_or_else(PoisonError::into_inner) + .take(), + ); +} + +/// Where the Command wrote the Factory skills out, writing them now if it +/// has not yet. +fn written() -> Result { + let mut written = WRITTEN.lock().unwrap_or_else(PoisonError::into_inner); + if let Some(dir) = &*written { + return Ok(dir.path().to_owned()); + } + let dir = tempfile::Builder::new() + .prefix("thirdshift-skills-") + .tempdir()?; + SKILLS + .extract(dir.path()) + .context("could not write the Factory skills")?; + let path = dir.path().to_owned(); + *written = Some(dir); + Ok(path) +} + +/// Add [`EXCLUDE`] to the `.git/info/exclude` of the repository `git` works +/// in, unless it is there already. The file is shared by every worktree, so +/// the entry is left there, under a lock other Runs from the same Launch +/// directory take too. +fn exclude(git: &Git) -> Result<()> { + let _lock = git.lock("thirdshift-exclude.lock")?; + let info = git.common_dir()?.join("info"); + let path = info.join("exclude"); + let existing = match fs::read_to_string(&path) { + Ok(existing) => existing, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(error) => { + return Err(error).with_context(|| format!("could not read {}", path.display())); + } + }; + if existing.lines().any(|line| line == EXCLUDE) { + return Ok(()); + } + fs::create_dir_all(&info).with_context(|| format!("could not create {}", info.display()))?; + let separator = if existing.is_empty() || existing.ends_with('\n') { + "" + } else { + "\n" + }; + OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .and_then(|mut file| writeln!(file, "{separator}{EXCLUDE}")) + .with_context(|| format!("could not add the Factory skills to {}", path.display())) +} diff --git a/tests/architect_run.rs b/tests/architect_run.rs index e4670fc70..86231c3b1 100644 --- a/tests/architect_run.rs +++ b/tests/architect_run.rs @@ -335,10 +335,9 @@ fn the_session_prompt_names_the_factory_skills_the_base_branch_and_the_final_lin assert_eq!(result.code, Some(0), "stderr: {}", result.stderr); let prompt = scenario.first_prompt(); for expected in [ - "/thirdshift:improve-codebase-architecture", - "/thirdshift:codebase-design", - "/thirdshift:to-spec", - "/thirdshift:to-tickets", + "/thirdshift-improve-codebase-architecture\n", + "the `thirdshift-codebase-design` skill", + "the `thirdshift-to-spec` and `thirdshift-to-tickets` skills", "the base branch develop", "\nArchitecture review plan: <", "\nArchitecture review idea: <", @@ -816,7 +815,7 @@ fn a_single_ticket_plan_starts_a_run_on_it_and_ends_as_that_run_does() { assert_eq!(calls.len(), 2, "sessions: {calls:?}"); let prompt = calls[1]["prompt"].as_str().unwrap(); assert!( - prompt.starts_with(&format!("/thirdshift:implement {PLAN_URL}\n")), + prompt.starts_with(&format!("/thirdshift-implement {PLAN_URL}\n")), "{prompt}" ); assert_eq!(calls[1]["branch"], "issue-8"); @@ -1084,6 +1083,9 @@ fn base_fix_reaches_each_tickets_run_of_the_dispatched_spec_run() { for file in ["issue-9.txt", "issue-10.txt", "ci-fix.txt"] { assert!(scenario.origin_file("issue-8", file).is_some(), "{file}"); } + // The Architecture review, each Ticket's Run, the Base fix and the Spec + // review. + scenario.assert_every_session_found_the_factory_skills(); } #[test] @@ -1825,7 +1827,7 @@ fn a_second_architect_run_is_skipped_while_the_firsts_dispatched_run_or_spec_run assert_ended_with_pr(&first, &pr_from(&scenario, "issue-8"), "ready for review"); let reviews = scenario.claude_calls().into_iter().filter(|call| { let prompt = call["prompt"].as_str().unwrap(); - prompt.contains("/thirdshift:improve-codebase-architecture") + prompt.contains("/thirdshift-improve-codebase-architecture") }); assert_eq!(reviews.count(), 1, "a second review was started"); } diff --git a/tests/background_work.rs b/tests/background_work.rs index 2bc9c6467..770e3c06a 100644 --- a/tests/background_work.rs +++ b/tests/background_work.rs @@ -51,6 +51,7 @@ fn a_session_whose_background_work_was_killed_is_resumed_once_and_the_run_goes_o "argv: {argv:?}" ); assert_eq!(calls[1]["cwd"], calls[0]["cwd"]); + scenario.assert_every_session_found_the_factory_skills(); let prompt = calls[1]["prompt"].as_str().unwrap(); assert!( prompt.contains("was killed when your turn ended") && prompt.contains("in the foreground"), diff --git a/tests/continuation.rs b/tests/continuation.rs index 1941fcd2d..0fee77730 100644 --- a/tests/continuation.rs +++ b/tests/continuation.rs @@ -80,7 +80,7 @@ fn the_open_prs_base_is_the_base_branch_whatever_is_checked_out() { let prompt = scenario.first_prompt(); assert!( prompt.contains("(see git log main..HEAD)") - && prompt.contains("The base branch is main. Review with /thirdshift:code-review using main as the fixed point.") + && prompt.contains("The base branch is main. Review with the `thirdshift-code-review` skill using main as the fixed point.") && !prompt.contains("develop"), "prompt: {prompt}" ); diff --git a/tests/fakes/claude.rs b/tests/fakes/claude.rs index 13b9696ba..684741aa5 100644 --- a/tests/fakes/claude.rs +++ b/tests/fakes/claude.rs @@ -2,8 +2,10 @@ //! //! Each call appends a record to the JSON list in $FAKE_CLAUDE_RECORD: its //! argv, prompt, working directory, the branch checked out there, whether a -//! merge is in progress there, and a snapshot of the --plugin-dir contents -//! (the directory is gone by the time a test looks). +//! merge is in progress there, a snapshot of the `thirdshift-*` skills in its +//! `.claude/skills/`, through their links, and of the files written out +//! beside the skills those links point to (the links' targets are gone by +//! the time a test looks). //! //! It then runs a per-test bash script in the working directory, emits a //! couple of stream-json lines, and exits with the script's exit code. A @@ -42,25 +44,72 @@ use std::process::{Command, ExitStatus}; use crate::json::{Array, Bool, Json, Null, object, string}; -/// Every file under `plugin_dir`, by its path relative to it, with its -/// contents. -fn plugin_snapshot(plugin_dir: &Path) -> Json { - let mut files = object([]); - let mut dirs = vec![plugin_dir.to_owned()]; +/// Where Claude Code finds a worktree's project skills. +const SKILLS: &str = ".claude/skills"; + +/// Every file under `dir`, by its path relative to `root`, with its +/// contents, into `files`. Like Python's os.walk, which the fake was first +/// written with, it doesn't follow a symlink to a directory below `dir`. +fn snapshot_into(files: &mut Json, root: &Path, dir: &Path) { + let mut dirs = vec![dir.to_owned()]; while let Some(dir) = dirs.pop() { for entry in fs::read_dir(&dir).unwrap() { let path = entry.unwrap().path(); - // Like Python's os.walk, which the fake was first written with, - // this doesn't follow a symlink to a directory. if fs::symlink_metadata(&path).unwrap().is_dir() { dirs.push(path); } else if !path.is_dir() { - let relative = path.strip_prefix(plugin_dir).unwrap(); + let relative = path.strip_prefix(root).unwrap(); let contents = String::from_utf8_lossy(&fs::read(&path).unwrap()).into_owned(); files.set(relative.to_str().unwrap(), string(contents)); } } } +} + +/// The `thirdshift-*` entries of the working directory's `.claude/skills/`. +fn linked_skills() -> Vec { + let Ok(entries) = fs::read_dir(SKILLS) else { + return Vec::new(); + }; + entries + .map(|entry| entry.unwrap().path()) + .filter(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("thirdshift-")) + }) + .collect() +} + +/// Every file of the `thirdshift-*` skills in the working directory's +/// `.claude/skills/`, through their links, by its path relative to +/// `.claude/skills/`, with its contents. +fn skills_snapshot() -> Json { + let mut files = object([]); + for skill in linked_skills() { + snapshot_into(&mut files, Path::new(SKILLS), &skill); + } + files +} + +/// The files written out beside the skills the `thirdshift-*` links in the +/// working directory's `.claude/skills/` point to, such as their licence, by +/// name, with their contents. +fn beside_skills_snapshot() -> Json { + let mut files = object([]); + let written = linked_skills() + .first() + .and_then(|link| fs::read_link(link).ok()) + .and_then(|target| target.parent().map(Path::to_owned)); + if let Some(written) = written { + for entry in fs::read_dir(&written).unwrap() { + let path = entry.unwrap().path(); + if path.is_file() { + let name = path.file_name().unwrap().to_str().unwrap(); + files.set(name, string(fs::read_to_string(&path).unwrap())); + } + } + } files } @@ -122,10 +171,6 @@ fn exit_code(status: ExitStatus) -> i32 { } pub fn main(argv: Vec) { - let plugin_dir = argv - .iter() - .position(|arg| arg == "--plugin-dir") - .map(|at| PathBuf::from(&argv[at + 1])); let (branch, _) = git_here(&["branch", "--show-current"]); let (_, merging) = git_here(&["rev-parse", "-q", "--verify", "MERGE_HEAD"]); let text_mode = !argv.iter().any(|arg| arg == "--output-format"); @@ -150,20 +195,8 @@ pub fn main(argv: Vec) { ("cwd", string(cwd)), ("branch", string(branch)), ("merging", Bool(merging)), - ( - "plugin_dir", - plugin_dir - .as_ref() - .map(|dir| string(dir.to_str().unwrap())) - .unwrap_or(Null), - ), - ( - "plugin_files", - plugin_dir - .as_deref() - .map(plugin_snapshot) - .unwrap_or(object([])), - ), + ("skill_files", skills_snapshot()), + ("beside_skills", beside_skills_snapshot()), ("stdin", stdin), ]), ); diff --git a/tests/fresh_run.rs b/tests/fresh_run.rs index 9d64fb97f..65660c1f0 100644 --- a/tests/fresh_run.rs +++ b/tests/fresh_run.rs @@ -68,7 +68,7 @@ fn runs_claude_headless_in_auto_mode_in_a_sibling_worktree_on_the_issue_branch() "argv: {argv:?}" ); assert!(argv.contains(&"--verbose"), "argv: {argv:?}"); - assert!(argv.contains(&"--plugin-dir"), "argv: {argv:?}"); + assert!(!argv.contains(&"--plugin-dir"), "argv: {argv:?}"); assert_eq!( call["cwd"], scenario.path("work/widgets-issue-7").to_str().unwrap() @@ -85,10 +85,10 @@ fn gives_the_agent_the_fresh_prompt() { assert_eq!( scenario.claude_calls()[0]["prompt"], - "/thirdshift:implement https://github.com/acme/widgets/issues/7\n\ - The base branch is main. Review with /thirdshift:code-review using main as the fixed point.\n\ + "/thirdshift-implement https://github.com/acme/widgets/issues/7\n\ + The base branch is main. Review with the `thirdshift-code-review` skill using main as the fixed point.\n\ Address the Standards and Spec findings you agree with.\n\ - Push branch issue-7 and create a pull request against main using /thirdshift:pr, marked ready for review.\n\ + Push branch issue-7 and create a pull request against main using the `thirdshift-pr` skill, marked ready for review.\n\ In the PR body, add an \"Unaddressed findings\" section listing each skipped finding under Standards or Spec, with at least a one-line reason.\n\ Include \"Closes #7\" in the PR body.\n\ You run headless: nobody is watching, and ending your turn ends the session. Run tests and other long commands in the foreground, raising the Bash timeout if needed. If a command is moved to the background, wait for that task by its own task id or output file, never by process names or patterns (`pgrep`, `ps | grep`, and the like): other sessions on this machine run the same commands. Never end your turn while a background task you depend on is still running: ending the turn kills it. Before ending your turn, stop every background task you no longer need with the `TaskStop` tool, by its task id: a task still running when your turn ends is taken as work you were waiting on.\n" @@ -96,46 +96,23 @@ fn gives_the_agent_the_fresh_prompt() { } #[test] -fn loads_every_factory_skill_as_the_thirdshift_plugin() { +fn finds_every_factory_skill_in_the_worktree_as_thirdshift_skill() { let scenario = Scenario::new(); scenario.agent_does(AGENT_COMMITS_AND_OPENS_PR); scenario.run(&[&scenario.issue_url(7)]); - let files = &scenario.claude_calls()[0]["plugin_files"]; - let manifest: serde_json::Value = serde_json::from_str( - files[".claude-plugin/plugin.json"] - .as_str() - .expect("no plugin manifest"), - ) - .unwrap(); - assert_eq!(manifest["name"], "thirdshift"); - for skill in [ - "implement", - "code-review", - "pr", - "tdd", - "resolving-merge-conflicts", - "improve-codebase-architecture", - "to-spec", - "to-tickets", - "codebase-design", - ] { - let skill_md = files[format!("skills/{skill}/SKILL.md")].as_str(); - assert!( - skill_md.is_some_and(|text| text.contains(&format!("name: {skill}"))), - "skill {skill} missing from the plugin" - ); - } + scenario.assert_every_session_found_the_factory_skills(); + let files = &scenario.claude_calls()[0]["skill_files"]; for supporting in [ - "skills/tdd/tests.md", - "skills/improve-codebase-architecture/REPORT.md", - "skills/codebase-design/DEEPENING.md", - "skills/codebase-design/DESIGN-IT-TWICE.md", + "thirdshift-tdd/tests.md", + "thirdshift-improve-codebase-architecture/REPORT.md", + "thirdshift-codebase-design/DEEPENING.md", + "thirdshift-codebase-design/DESIGN-IT-TWICE.md", ] { assert!( files[supporting].is_string(), - "the supporting file {supporting} is missing from the plugin" + "the supporting file {supporting} is missing from the worktree's .claude/skills/" ); } } @@ -148,7 +125,7 @@ fn ships_the_mattpocock_skills_mit_notice_with_the_factory_skills() { scenario.run(&[&scenario.issue_url(7)]); let calls = scenario.claude_calls(); - let license = calls[0]["plugin_files"]["skills/LICENSE"].as_str(); + let license = calls[0]["beside_skills"]["LICENSE"].as_str(); assert!( license .is_some_and(|text| text.contains("MIT License") @@ -157,6 +134,56 @@ fn ships_the_mattpocock_skills_mit_notice_with_the_factory_skills() { ); } +#[test] +fn the_linked_skills_are_kept_out_of_git() { + let scenario = Scenario::new(); + let status = scenario.path("status.txt"); + scenario.agent_does(&format!( + "git status --porcelain --untracked-files=all > '{}'\n\ + echo feature > feature.txt\n\ + git add -A\n\ + git commit -q -m 'Add everything'\n\ + gh pr create --base main --head issue-7 --title 'Add feature' --body 'Closes #7'\n", + status.display() + )); + + let result = scenario.run(&[&scenario.issue_url(7)]); + + assert_eq!(result.code, Some(0), "stderr: {}", result.stderr); + assert_eq!(std::fs::read_to_string(&status).unwrap(), ""); + let committed = scenario.origin_git(&["ls-tree", "-r", "--name-only", "issue-7"]); + assert!(committed.contains("feature.txt"), "committed: {committed}"); + assert!(!committed.contains(".claude"), "committed: {committed}"); +} + +#[test] +fn a_second_run_adds_no_second_exclude_entry() { + let scenario = Scenario::new(); + scenario.issue_is(8, "OPEN"); + scenario.agent_does_for(7, AGENT_COMMITS_AND_OPENS_PR); + scenario.agent_does_for( + 8, + "echo other > other.txt\ngit add other.txt\ngit commit -q -m 'Add other'\n\ + gh pr create --base main --head issue-8 --title 'Add other' --body 'Closes #8'\n", + ); + + let first = scenario.run(&[&scenario.issue_url(7)]); + let second = scenario.run(&[&scenario.issue_url(8)]); + + assert_eq!(first.code, Some(0), "stderr: {}", first.stderr); + assert_eq!(second.code, Some(0), "stderr: {}", second.stderr); + let exclude = std::fs::read_to_string(scenario.launch_dir().join(".git/info/exclude")).unwrap(); + let entries: Vec<_> = exclude + .lines() + .filter(|line| line.contains("thirdshift-")) + .collect(); + assert_eq!( + entries, + ["/.claude/skills/thirdshift-*"], + "exclude: {exclude}" + ); +} + #[test] fn logs_the_session_stream_under_the_home_directory() { let scenario = Scenario::new(); diff --git a/tests/merge_run.rs b/tests/merge_run.rs index 6f359c22c..e8b6a6bc6 100644 --- a/tests/merge_run.rs +++ b/tests/merge_run.rs @@ -322,12 +322,12 @@ fn assert_review_repair_from(prompt: &serde_json::Value, fixed_point: &str) { let prompt = prompt.as_str().unwrap(); let fixed_point = fixed_point.trim(); assert!( - prompt.starts_with("/thirdshift:code-review"), + prompt.starts_with("/thirdshift-code-review"), "prompt: {prompt}" ); assert!( prompt.contains(&format!( - "Review with /thirdshift:code-review using {fixed_point} as the fixed point" + "Review with the `thirdshift-code-review` skill using {fixed_point} as the fixed point" )), "prompt: {prompt}" ); @@ -404,7 +404,7 @@ fn a_foreign_commit_that_conflicts_with_local_work_gets_a_conflict_repair_then_a assert_eq!(calls[2]["merging"], true); let conflict = calls[2]["prompt"].as_str().unwrap(); assert!( - conflict.starts_with("/thirdshift:resolving-merge-conflicts") + conflict.starts_with("/thirdshift-resolving-merge-conflicts") && conflict.contains("A merge of origin/issue-7 into issue-7 is in progress"), "prompt: {conflict}" ); diff --git a/tests/mergeable.rs b/tests/mergeable.rs index 5377b992e..2e1c5453f 100644 --- a/tests/mergeable.rs +++ b/tests/mergeable.rs @@ -107,7 +107,7 @@ fn hands_a_conflicting_merge_to_a_conflict_repair_and_pushes_its_resolution() { let repair = &calls[1]; assert_eq!( repair["prompt"], - "/thirdshift:resolving-merge-conflicts\n\ + "/thirdshift-resolving-merge-conflicts\n\ \n\ A merge of origin/main into issue-7 is in progress in this worktree and has conflicts.\n\ issue-7 implements https://github.com/acme/widgets/issues/7; its pull request is https://github.com/acme/widgets/pull/1.\n\ @@ -123,7 +123,8 @@ fn hands_a_conflicting_merge_to_a_conflict_repair_and_pushes_its_resolution() { let argv = call["argv"].as_array().unwrap().clone(); argv[..argv.len() - 1].to_vec() }; - assert_eq!(flags(repair), flags(&calls[0]), "same plugin and flags"); + assert_eq!(flags(repair), flags(&calls[0]), "same flags"); + scenario.assert_every_session_found_the_factory_skills(); assert_merged_main_into_issue_7(&scenario); assert_eq!( scenario.origin_git(&["show", "issue-7:feature.txt"]), diff --git a/tests/numbered_branches.rs b/tests/numbered_branches.rs index a776b8b28..dcdd64f26 100644 --- a/tests/numbered_branches.rs +++ b/tests/numbered_branches.rs @@ -30,7 +30,7 @@ fn the_next_numbered_branch_gets_the_fresh_prompt() { let prompt = scenario.first_prompt(); assert!( prompt.contains( - "\nPush branch issue-7-branch-2 and create a pull request against main using /thirdshift:pr, marked ready for review.\n" + "\nPush branch issue-7-branch-2 and create a pull request against main using the `thirdshift-pr` skill, marked ready for review.\n" ) && !prompt.contains("You are continuing work"), "prompt: {prompt}" ); diff --git a/tests/packaging.rs b/tests/packaging.rs index 318656704..1b9315e63 100644 --- a/tests/packaging.rs +++ b/tests/packaging.rs @@ -75,15 +75,15 @@ fn package_carries_the_factory_skills_with_their_license_and_credits() { let list = package_list(); for expected in [ "skills/LICENSE", - "skills/pr/CREDITS.md", - "skills/implement/SKILL.md", - "skills/improve-codebase-architecture/SKILL.md", - "skills/improve-codebase-architecture/REPORT.md", - "skills/to-spec/SKILL.md", - "skills/to-tickets/SKILL.md", - "skills/codebase-design/SKILL.md", - "skills/codebase-design/DEEPENING.md", - "skills/codebase-design/DESIGN-IT-TWICE.md", + "skills/thirdshift-pr/CREDITS.md", + "skills/thirdshift-implement/SKILL.md", + "skills/thirdshift-improve-codebase-architecture/SKILL.md", + "skills/thirdshift-improve-codebase-architecture/REPORT.md", + "skills/thirdshift-to-spec/SKILL.md", + "skills/thirdshift-to-tickets/SKILL.md", + "skills/thirdshift-codebase-design/SKILL.md", + "skills/thirdshift-codebase-design/DEEPENING.md", + "skills/thirdshift-codebase-design/DESIGN-IT-TWICE.md", "src/main.rs", "README.md", "LICENSE", diff --git a/tests/pickup_run.rs b/tests/pickup_run.rs index 2e2ea7f2d..1320fc729 100644 --- a/tests/pickup_run.rs +++ b/tests/pickup_run.rs @@ -100,7 +100,7 @@ fn the_lower_numbered_of_two_ready_issues_is_run_and_the_other_left_untouched() let prompt = calls[0]["prompt"].as_str().unwrap(); assert!( prompt.starts_with(&format!( - "/thirdshift:implement {}\n", + "/thirdshift-implement {}\n", scenario.issue_url(7) )), "{prompt}" diff --git a/tests/progress_lines.rs b/tests/progress_lines.rs index b499dd560..05879c5f1 100644 --- a/tests/progress_lines.rs +++ b/tests/progress_lines.rs @@ -43,7 +43,7 @@ fn prints_one_line_per_notable_session_event() { "{}{}{}{}{AGENT_COMMITS_AND_OPENS_PR}", emits(tool_use( "Skill", - json!({ "skill": "thirdshift:implement" }) + json!({ "skill": "thirdshift-implement" }) )), emits(tool_use( "Read", @@ -65,7 +65,7 @@ fn prints_one_line_per_notable_session_event() { let lines = stderr_lines(&result.stderr); for expected in [ "thirdshift: implement: session started", - "thirdshift: implement: skill thirdshift:implement", + "thirdshift: implement: skill thirdshift-implement", "thirdshift: implement: Read src/lib.rs", "thirdshift: implement: commit", "thirdshift: implement: push", diff --git a/tests/spec_run.rs b/tests/spec_run.rs index ee50017ee..330376e53 100644 --- a/tests/spec_run.rs +++ b/tests/spec_run.rs @@ -509,7 +509,7 @@ fn a_base_branch_that_moved_into_a_conflict_gets_a_conflict_repair_and_the_spec_ assert_eq!(result.code, Some(0), "stderr: {}", result.stderr); let prompts = spec_prompts(&scenario); assert_eq!(prompts.len(), 2, "{prompts:?}"); - assert_contains(&prompts[1], "/thirdshift:resolving-merge-conflicts"); + assert_contains(&prompts[1], "/thirdshift-resolving-merge-conflicts"); assert_contains(&prompts[1], "A merge of origin/main into issue-20"); assert_contains(&prompts[1], &spec_url(&scenario)); let spec = spec_pr(&scenario); @@ -970,11 +970,11 @@ fn once_the_last_ticket_lands_the_spec_review_reviews_the_spec_branch_against_th assert_eq!(review["branch"], "issue-20"); let prompt = review["prompt"].as_str().unwrap(); for part in [ - "/thirdshift:code-review main, with the Spec https://github.com/acme/widgets/issues/20 as the spec\n", + "/thirdshift-code-review main, with the Spec https://github.com/acme/widgets/issues/20 as the spec\n", &spec_url(&scenario), "using main as the fixed point", - "/thirdshift:tdd", - "Update PR https://github.com/acme/widgets/pull/2 using /thirdshift:pr", + "the `thirdshift-tdd` skill", + "Update PR https://github.com/acme/widgets/pull/2 using the `thirdshift-pr` skill", "\"Unaddressed findings\"", "Include \"Closes #20\"", "You run headless", diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 44df8a240..d3afd916b 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -47,6 +47,19 @@ use tempfile::TempDir; pub const OWNER: &str = "acme"; pub const REPO: &str = "widgets"; +/// Every Factory skill, by the name a session finds it under. +pub const FACTORY_SKILLS: [&str; 9] = [ + "thirdshift-implement", + "thirdshift-code-review", + "thirdshift-pr", + "thirdshift-tdd", + "thirdshift-resolving-merge-conflicts", + "thirdshift-improve-codebase-architecture", + "thirdshift-to-spec", + "thirdshift-to-tickets", + "thirdshift-codebase-design", +]; + /// How long a test waits for a Run to reach a point, such as starting the /// agent or showing a prompt, before it gives the Run up as hung. A Run that /// exits without reaching the point fails its test at once, so only a hung @@ -788,6 +801,24 @@ test -f {root}/{COPY_REPLACED} } } + /// Assert every `claude` call found every Factory skill, by its + /// `thirdshift-` name, in its worktree's `.claude/skills/`. + pub fn assert_every_session_found_the_factory_skills(&self) { + let calls = self.claude_calls(); + assert!(!calls.is_empty(), "no session ran"); + for call in calls { + for skill in FACTORY_SKILLS { + let skill_md = call["skill_files"][format!("{skill}/SKILL.md")].as_str(); + assert!( + skill_md.is_some_and(|text| text.contains(&format!("name: {skill}\n"))), + "the session in {} with the prompt {} found no {skill}", + call["cwd"], + call["prompt"] + ); + } + } + } + /// The prompt of the first `claude` call. pub fn first_prompt(&self) -> String { self.claude_calls()[0]["prompt"] From ed9a63e1befabf74b459e9ec99faa891238fe365 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Mon, 5 Oct 2026 00:12:24 -0400 Subject: [PATCH 2/2] Review clean-ups: clearer names for writing out and removing the Factory skills (#391) Co-Authored-By: Claude Opus 5.5 --- src/command.rs | 2 +- src/skills.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/command.rs b/src/command.rs index 9f1380682..2179a678a 100644 --- a/src/command.rs +++ b/src/command.rs @@ -123,7 +123,7 @@ impl Started { /// recorded where it was decided, and it sends no notification. Returns /// its exit code. pub fn finish(self, ending: Ending) -> ExitCode { - skills::remove(); + skills::remove_written(); let account = match run_ending::read(&ending) { Ok(account) => account, Err(skip) => return run_ending::show_skip(skip), diff --git a/src/skills.rs b/src/skills.rs index a9acccd89..3043887ec 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -3,7 +3,7 @@ //! Claude Code finds project skills (ADR-0012). use std::fs::{self, OpenOptions}; -use std::io::Write; +use std::io::{ErrorKind, Write}; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; use std::sync::{Mutex, PoisonError}; @@ -33,7 +33,7 @@ static WRITTEN: Mutex> = Mutex::new(None); /// `.git/info/exclude` keeps them out of git. The links are left in place: /// they go with the worktree. pub fn link_into(worktree: &Path) -> Result<()> { - let written = written()?; + let written = written_out()?; exclude(&Git::new(worktree))?; let project_skills = worktree.join(PROJECT_SKILLS); fs::create_dir_all(&project_skills) @@ -53,7 +53,7 @@ pub fn link_into(worktree: &Path) -> Result<()> { /// Remove the Factory skills the Command wrote out, if it did, once its /// sessions are over. -pub fn remove() { +pub fn remove_written() { drop( WRITTEN .lock() @@ -64,7 +64,7 @@ pub fn remove() { /// Where the Command wrote the Factory skills out, writing them now if it /// has not yet. -fn written() -> Result { +fn written_out() -> Result { let mut written = WRITTEN.lock().unwrap_or_else(PoisonError::into_inner); if let Some(dir) = &*written { return Ok(dir.path().to_owned()); @@ -90,7 +90,7 @@ fn exclude(git: &Git) -> Result<()> { let path = info.join("exclude"); let existing = match fs::read_to_string(&path) { Ok(existing) => existing, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(error) if error.kind() == ErrorKind::NotFound => String::new(), Err(error) => { return Err(error).with_context(|| format!("could not read {}", path.display())); }