Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/alcode-catchup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@paleo/alcode": minor
---

The `catchup` protocol replaces `read`: it loads the ticket's history and returns a synthesis, and a following `resume` continues the ticket with that history in context. `reserve-side-ticket` now also counts the side tickets archived under `.plans/_archives/`.
5 changes: 5 additions & 0 deletions .changeset/plans-share-archive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@paleo/plans-share": minor
---

Added `archive <ticket-id | path>` and `auto-archive`, which move ticket directories to `.plans/_archives/`, and the `sync --auto-archive` option. Auto-archiving moves the ticket directories and no-ticket alcode session files untouched for `PLANS_SHARE_ARCHIVE_DAYS` days (default 7).
2 changes: 1 addition & 1 deletion alignfirst-developer-tests/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ RUN cd /opt/alignfirst-developer-tests/fixtures/template && \
RUN npx -y skills add https://github.com/paleo/alignfirst --global --yes \
--agent universal --agent claude-code \
--skill alignfirst --skill al --skill alplan --skill alspec \
--skill aldescription --skill alreview --skill alread \
--skill aldescription --skill alreview --skill alcatchup \
--skill almerge --skill alignfirst-setup-guide \
--skill alignfirst-developer-openclaw-playbook

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export async function waitForCompletionReport(

/**
* Wait for the investigation findings — the agent relaying, in the thread, the result of a
* `read`-protocol delegation (A03). Judged per message like the ack/completion waits: tolerant of
* no-protocol delegation (A03). Judged per message like the ack/completion waits: tolerant of
* phrasing and language, and it judges the earlier launch/ack lines false rather than needing a regex
* to exclude them.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ function parseWorktreeRequest(
}

const CODING_PROTOCOL_RE =
/^Run the _(spec|AAD|plan|description|read|review|merge)_ protocol from the \*alignfirst\* skill\./;
/^Run the _(spec|AAD|plan|description|catchup|review|merge)_ protocol from the \*alignfirst\* skill\./;

// The edit each coding result stands behind, applied to the fixture's
// `home-page.mjs` with `sed`. A run reports "changes committed on the ticket
Expand Down
2 changes: 1 addition & 1 deletion alignfirst-developer-tests/workspace/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ When a channel or DM message names a project or a ticket and you are not already

Don't investigate the **code** yourself. Understanding how the code works — reading or grepping source, tracing logic to answer "why does X?" / "should we Y?" — is alcode's job. Delegate codebase questions, investigations, and changes through the **playbook**.

Repo and workflow **metadata** is fair game directly: `git` (status, log, branch, diff, fetch), `gh` (PR/issue state), `ls`, the workspace tooling, `DEVELOPERS.md`, the `.plans/` listing. A status request on a ticket ("where does ABC-123 stand?") is ticket work — handle it through the **playbook**: combine that metadata with the ticket's spec/summary history (via alcode `read`), never by reading the source.
Repo and workflow **metadata** is fair game directly: `git` (status, log, branch, diff, fetch), `gh` (PR/issue state), `ls`, the workspace tooling, `DEVELOPERS.md`, the `.plans/` listing. A status request on a ticket ("where does ABC-123 stand?") is ticket work — handle it through the **playbook**: combine that metadata with the ticket's spec/summary history (via alcode `catchup`), never by reading the source.

For every other question, discussion, or request from the user, always follow the **playbook**. The playbook is your guide for everything.

Expand Down
9 changes: 5 additions & 4 deletions alignfirst-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ AlignFirst enables AI agents to write the code you would write. It's distributed
## Installation

```bash
npx skills add https://github.com/paleo/alignfirst --global --skill alignfirst --skill al --skill alplan --skill alspec --skill aldescription --skill alreview --skill alread --skill almerge
npx skills add https://github.com/paleo/alignfirst --global --skill alignfirst --skill al --skill alplan --skill alspec --skill aldescription --skill alreview --skill alcatchup --skill almerge
```

> **Note:** We recommend installing these skills globally.
Expand Down Expand Up @@ -117,12 +117,13 @@ Resolve conflicts after a merge or rebase:

The agent investigates both sides of each conflict, resolves them, and writes a `.plans/AB-123/A4-merge.summary.md` file documenting any tricky resolutions.

### Read Task Context
### Catch Up

Load into context all specs and summaries of the current ticket:
Load the history of the current ticket, then continue with your instructions or get a synthesis:

```markdown
/alread
/alcatchup
/alcatchup then start an AAD: …
```

## Rationale
Expand Down
13 changes: 7 additions & 6 deletions docs/alignfirst-developer/alignfirst-developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ deployment, use the
2. **Operating-instructions playbook** — the
[`alignfirst-developer-openclaw-playbook`](../../skills/alignfirst-developer-openclaw-playbook/)
skill. `SKILL.md` routes thread sessions to `working-session.md` and channel/DM sessions to
`channel-handling.md`. Its references own working sessions, channel handling, project workspace
setup, project lifecycle, and the `message` tool per surface. Project discovery comes from `alproject --guide`; the delegation
procedure comes from `alcode --openclaw-guide` only when delegation starts.
`channel-handling.md`. Its references own working sessions, channel handling, the `runbooks/`
directory for project workspace setup and project lifecycle, and the `message` tool per surface.
Project discovery comes from `alproject --guide`; the delegation procedure comes from
`alcode --openclaw-guide` only when delegation starts.
3. **Regression-test harness** —
[`alignfirst-developer-tests/`](../../alignfirst-developer-tests/). This standalone Dockerised
consumer drives the workspace through synthetic Discord and Slack channels and judges the result.
Expand All @@ -31,8 +32,8 @@ user message
→ workspace AGENTS.md (auto-loaded) layer 1
→ alignfirst-developer-openclaw-playbook/SKILL.md (read first) layer 2 ← procedural dispatcher
→ references/working-session.md | channel-handling.md layer 2
→ references/project-lifecycle.md (create/remove) layer 2
→ references/project-workspace-setup.md (if the thread gets its workspace) layer 2
→ references/runbooks/project-lifecycle.md (create/onboard/remove) layer 2
→ references/runbooks/project-workspace-setup.md (if the thread gets its workspace) layer 2
→ run `alcode --openclaw-guide` (delegation manual, read last), then delegate via alcode
```

Expand All @@ -42,7 +43,7 @@ Layer 1 is the only thing OpenClaw injects automatically; everything in layer 2

A channel/DM session runs `alproject list --json` before routing a message that may refer to a project. It resolves filesystem-present projects only, then records the known project paths, ticket, one-line task, and the full text of a detailed request. It opens a thread and ends the turn. Resource URLs, multi-project requests, and requests that may need no project can leave values for the working session to resolve. Duplicate names and missing project paths stay unresolved until the user selects a usable canonical path. The channel session never sets up a workspace, delegates to `alcode`, inspects a codebase, or reports a status — the thread session does all of that, whatever the user asked for and however explicit their green light was.

The cost is one round-trip: a thread session activates on the user's next message in that thread, so the starter ends by bringing the user back. It asks only for a value the channel can establish is required; otherwise it states that the next message launches the working session. Project creation is the exception to the path requirement: the lifecycle procedure establishes the new canonical path. The gain is that everything substantive runs in a session whose plain text auto-streams to the right surface. The previous contract had the channel session finish the setup in-turn, which forced every post through `message`+`threadId` and made a leak to the channel root the standard failure (`alignfirst-developer-tests/artifacts/2026-07-15T10-31-39-655Z/`).
The cost is one round-trip: a thread session activates on the user's next message in that thread, so the starter ends by bringing the user back. It asks only for a value the channel can establish is required; otherwise it states that the next message launches the working session. Project creation and repository onboarding are the exceptions to the path requirement: the lifecycle procedure establishes the new canonical path. The gain is that everything substantive runs in a session whose plain text auto-streams to the right surface. The previous contract had the channel session finish the setup in-turn, which forced every post through `message`+`threadId` and made a leak to the channel root the standard failure (`alignfirst-developer-tests/artifacts/2026-07-15T10-31-39-655Z/`).

## Reading order for maintainers

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"docmap": "docmap",
"workspace": "node scripts/workspace.mjs",
"plans:setup": "plans-share setup --folder alignfirst",
"plans:sync": "plans-share sync",
"plans:sync": "plans-share sync --auto-archive",
"lint": "biome check",
"lint:fix": "biome check --write",
"build": "npm run build --workspaces --if-present",
Expand Down
18 changes: 10 additions & 8 deletions packages/alcode/src/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export const PROTOCOLS = ["spec", "plan", "aad", "description", "read", "review", "merge"] as const;
export const PROTOCOLS = [
"spec",
"plan",
"aad",
"description",
"catchup",
"review",
"merge",
] as const;

export type Protocol = (typeof PROTOCOLS)[number];

Expand All @@ -7,6 +15,7 @@ export const PROTOCOL_LABELS: Record<string, string> = {
aad: "AAD",
plan: "plan",
description: "description",
catchup: "catchup",
review: "review",
merge: "merge",
};
Expand All @@ -20,16 +29,9 @@ export interface PromptInput {
export function buildPrompt(input: PromptInput): string {
const { protocol, ticket, message } = input;
if (!protocol) return message ?? "";
if (protocol === "read") return buildReadPrompt(ticket, message);
return buildProtocolPrompt(PROTOCOL_LABELS[protocol], ticket, message);
}

function buildReadPrompt(ticket?: string, message?: string): string {
const ticketPart = ticket ? ` for ticket ${ticket}` : "";
const messagePart = message ? `\n\n${message}` : "";
return `Use the *alignfirst* skill to determine the TASK_DIR${ticketPart}. Then read every \`*spec.md\` and \`*summary.md\` file in the TASK_DIR.${messagePart}`;
}

function buildProtocolPrompt(label: string, ticket?: string, message?: string): string {
const ticketPart = ticket ? ` Ticket ID = ${ticket}.` : "";
const messagePart = message ? `\n\n${message}` : "";
Expand Down
18 changes: 13 additions & 5 deletions packages/alcode/src/session-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,10 @@ export function listSessionRecords(cwd: string): SessionRecord[] {
// concurrent reservation took the id, so the loop moves on to the next one.
export function reserveSideTicket(cwd: string): string {
const plansDir = join(cwd, ".plans");
let highest = 0;
for (const entry of readEntries(plansDir)) {
const match = entry.isDirectory() ? entry.name.match(/^side-(\d+)$/) : null;
if (match) highest = Math.max(highest, Number(match[1]));
}
const highest = Math.max(
highestSideTicket(plansDir),
highestSideTicket(join(plansDir, "_archives")),
);
for (let n = highest + 1; ; ++n) {
const ticket = `side-${n}`;
try {
Expand All @@ -234,6 +233,15 @@ export function reserveSideTicket(cwd: string): string {
}
}

function highestSideTicket(dir: string): number {
let highest = 0;
for (const entry of readEntries(dir)) {
const match = entry.isDirectory() ? entry.name.match(/^side-(\d+)$/) : null;
if (match) highest = Math.max(highest, Number(match[1]));
}
return highest;
}

function readEntries(dir: string): Dirent[] {
try {
return readdirSync(dir, { withFileTypes: true });
Expand Down
8 changes: 4 additions & 4 deletions packages/alcode/templates/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ alcode reserve-side-ticket

| Option | Description |
|--------|-------------|
| `--protocol <p>` | One of `spec`, `plan`, `aad`, `description`, `read`, `review`, `merge`. Optional. |
| `--protocol <p>` | One of `spec`, `plan`, `aad`, `description`, `catchup`, `review`, `merge`. Optional. |
| `--ticket <id>` | Ticket ID. `new --protocol` requires it, or `--no-ticket`. |
| `--no-ticket` | Work without a ticket: `alcode` reserves the next side ticket `side-N` and passes it to the agent. `new` only, with a protocol. The reserved id is in the session file's path and `ticket:` frontmatter; pass it as `--ticket side-N` in later runs. |
| `--message "..."` | Message to send, written in English. `-m` is the short form. Required for `spec`, `aad`, and when no `--protocol`. |
Expand Down Expand Up @@ -83,14 +83,14 @@ Stop AAD now. Start a spec instead (alignfirst).
Two fresh sessions: one reviews, one fixes.

1. **Review** — `alcode new --protocol review --ticket AB-123`. The agent reviews the current branch against the base branch and writes a review file; its path is in the run's result. The base defaults to the repository's default branch; override it via `--message "Base branch: \`develop\`"`.
2. **Fix** (optional, always in a fresh session — never in the review session) — `alcode new --protocol aad --ticket AB-123 --message "Here is a code review: \`.plans/AB-123/B1-review.md\`. What should we fix?"`. Point the message at wherever the review lives: the review file, or the PR/MR whose comments carry it. The agent proposes fixes; decide together what to fix, as in any AAD session, then it implements and writes a summary file.
2. **Fix** (optional, always in a fresh session — never in the review session) — `alcode new --protocol aad --ticket AB-123 --message "Here is a code review: \`.plans/AB-123/B1-review.md\`. What should we fix?"`. Point the message at wherever the review lives: the review file, or the PR/MR whose comments carry it. The agent proposes fixes; decide together what to fix, as in any AAD session. Keep it simple and avoid overengineering. When the agent asks about scope, welcome expansion that cleans things up and refuse expansion that adds complexity; simplicity wins. The agent then implements and writes a summary file.

Skip the fix step when the review is informational.

## Other protocols

- **description** — `alcode new --protocol description --ticket AB-123`. Writes a PR/MR description for committed work. No discussion.
- **read** — `alcode new --protocol read --ticket AB-123 [--message "..."]`. Loads the ticket's spec and summary files into context; with a message, answers it against that context.
- **catchup** — `alcode new --protocol catchup --ticket AB-123 [--message "..."]`. Loads the ticket's history from its requests, specs, and summaries and returns a synthesis. To continue the ticket with that history in context, run `alcode resume <sessionId> --protocol aad --message "..."` (or `--protocol spec`) in the same session. Runs are sequential, so the one-protocol-at-a-time rule still holds.
- **review** — see the review workflow above.
- **merge** — `alcode new --protocol merge --ticket AB-123`. Resolves conflicts and summarizes tricky resolutions. Pass the incoming branch via `--message` to start the merge.

Expand All @@ -105,7 +105,7 @@ alcode resume <sessionId> --message \
3 - Yes, it should be optional."
```

**Technical questions** — architecture, patterns, existing behavior, anything answerable by reading the code. Never escalate these to the user. Push the agent to investigate: *"Explore the codebase to find out, and give me your opinion."*, *"Do not rush. Take the time to fully understand the situation first."*, *"What would be the most elegant way to do it?"*, *"Check if a similar pattern is already implemented elsewhere in the codebase."*
**Technical questions** — architecture, patterns, existing behavior, anything answerable by reading the code. Never escalate these to the user. Push the agent to investigate: *"Explore the codebase to find out, and give me your opinion."*, *"Do not rush. Take the time to fully understand the situation first."*, *"What would be the elegant, proper, simple yet robust solution?"*, *"Check if a similar pattern is already implemented elsewhere in the codebase."*

**Functional or UX questions** — product behavior, user-facing decisions, business rules. These need human judgement: escalate to your user, then relay the answer.

Expand Down
2 changes: 1 addition & 1 deletion packages/alcode/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ describe("parseAlcodeArgs", () => {
describe("validateSessionArgs", () => {
it("rejects an unknown protocol", () => {
expect(validate(["new", "--protocol", "bogus", "--ticket", "1"])).toBe(
"Error: --protocol must be one of: spec, plan, aad, description, read, review, merge.",
"Error: --protocol must be one of: spec, plan, aad, description, catchup, review, merge.",
);
});

Expand Down
14 changes: 3 additions & 11 deletions packages/alcode/test/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,9 @@ describe("buildPrompt", () => {
);
});

it("builds the special read prompt with a ticket", () => {
expect(buildPrompt({ protocol: "read", ticket: "29" })).toBe(
"Use the *alignfirst* skill to determine the TASK_DIR for ticket 29. " +
"Then read every `*spec.md` and `*summary.md` file in the TASK_DIR.",
);
});

it("appends the message to a read prompt", () => {
expect(buildPrompt({ protocol: "read", ticket: "7", message: "What changed?" })).toBe(
"Use the *alignfirst* skill to determine the TASK_DIR for ticket 7. " +
"Then read every `*spec.md` and `*summary.md` file in the TASK_DIR.\n\nWhat changed?",
it("builds the catchup protocol prompt", () => {
expect(buildPrompt({ protocol: "catchup", ticket: "29", message: "What changed?" })).toBe(
"Run the _catchup_ protocol from the *alignfirst* skill. Ticket ID = 29.\n\nWhat changed?",
);
});
});
8 changes: 8 additions & 0 deletions packages/alcode/test/session-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ describe("reserveSideTicket", () => {
expect(reserveSideTicket(dir)).toBe("side-4");
});

it("takes one above the highest archived side ticket", () => {
mkdirSync(join(dir, ".plans", "side-1"));
mkdirSync(join(dir, ".plans", "_archives", "side-5"), { recursive: true });
mkdirSync(join(dir, ".plans", "_archives", "side-5-2"));
expect(reserveSideTicket(dir)).toBe("side-6");
expect(existsSync(join(dir, ".plans", "side-6"))).toBe(true);
});

it("skips a candidate whose creation loses to an existing entry", () => {
mkdirSync(join(dir, ".plans", "side-1"));
writeFileSync(join(dir, ".plans", "side-2"), ""); // not a directory: invisible to the scan
Expand Down
23 changes: 21 additions & 2 deletions packages/plans-share/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Add the npm scripts, with the project folder baked in:
```json
{
"plans:setup": "plans-share setup --folder project-a",
"plans:sync": "plans-share sync"
"plans:sync": "plans-share sync --auto-archive"
}
```

Expand All @@ -55,6 +55,21 @@ npm run plans:sync

A project may keep `.plans` as a plain local directory. `sync` then reports local plans mode and exits successfully.

Archive one ticket immediately by id or path:

```sh
npx --no plans-share archive 250
npx --no plans-share archive .plans/250
```

Archive stale entries without synchronizing:

```sh
npx --no plans-share auto-archive
```

Pass `--auto-archive` to `sync` to archive stale entries after pulling and before committing. The recommended `plans:sync` script above enables it.

To verify that `.plans` is usable and report its mode:

```sh
Expand All @@ -72,7 +87,11 @@ Pass `--no` to keep npx off the registry. The bin is `plans-share`, while the pa

## Archiving

To keep `.plans` small, move finished tickets to `_archives/` inside the project folder — anyone, anytime:
Automatic archiving moves stale ticket directories and stale no-ticket session files from `.plans/_alcode/` into `.plans/_archives/`. A ticket's age is the newest modification time among its files. `PLANS_SHARE_ARCHIVE_DAYS` sets the threshold in days and defaults to `7`.

Existing names gain a numeric suffix, such as `250-2` or `20260101-101010-2.md`.

Manual moves remain valid:

```sh
mv .plans/250 .plans/_archives/
Expand Down
Loading