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/alignfirst-git-failure-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"alignfirst": patch
---

A failing git command now reports git's own message instead of pointing at output that was never printed, and names the subcommand that failed rather than a leading global option.
6 changes: 6 additions & 0 deletions .changeset/work-files-vocabulary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"alignfirst": patch
"@paleo/alproject": patch
---

Renamed the container concept in every message and document to "work files": the conventions line now starts with `Work files:`, `sync` reports "Work files synchronized", `doctor` shows a "Work files" section, and the team repository is called the work-files repository. The `.plans` directory, the `plans` command and the `plans` config key keep their names.
27 changes: 23 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
outputs:
published: ${{ steps.changesets.outputs.published }}
published-packages: ${{ steps.changesets.outputs.published-packages }}
finished-at: ${{ steps.finished.outputs.at }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -72,12 +73,30 @@ jobs:
uses: changesets/action@8488615a623b1b9c987934bb89eae8af6a946ac1 # v2.1.1
with:
publish-script: npm run ci:publish
- id: finished
run: echo "at=$(date +%s)" >> "$GITHUB_OUTPUT"

verify:
needs: publish
if: needs.publish.outputs.published == 'true'
runs-on: ubuntu-latest
# The `verify` environment carries a 15-minute wait timer: the registry serves a
# stale packument for several minutes after a publish, and a job held by a timer
# occupies no runner. The step below enforces the same delay when the timer is
# missing -- a fork, or a re-created environment -- and costs nothing when it is not.
environment: verify
steps:
- name: Wait out the registry CDN
shell: bash
env:
PUBLISHED_AT: ${{ needs.publish.outputs.finished-at }}
run: |
published_at=${PUBLISHED_AT:-$(date +%s)}
remaining=$(( published_at + 900 - $(date +%s) ))
if [ "$remaining" -gt 0 ]; then
echo "Environment wait timer absent or short; sleeping ${remaining}s"
sleep "$remaining"
fi
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 26
Expand All @@ -91,11 +110,11 @@ jobs:
cd "$(mktemp -d)"
npm init -y > /dev/null
# --omit=peer: auto-installed peers drag unrelated trees into the audit
for i in $(seq 1 10); do
for i in 1 2; do
if npm install --omit=peer $pkgs; then break; fi
if [ "$i" = 10 ]; then echo "Install failed after 10 attempts"; exit 1; fi
echo "Registry not ready, retrying in 30s"
sleep 30
if [ "$i" = 2 ]; then echo "Install failed after 2 attempts"; exit 1; fi
echo "Registry not ready, retrying in 60s"
sleep 60
done
npm audit signatures
alignfirst_version=$(node -e 'console.log(JSON.parse(process.env.PUBLISHED).find((p) => p.name === "alignfirst")?.version ?? "")')
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project conventions and documentation

Run `npx -y alignfirst context` from the repository root, _before_ reading any other file. It prints the project conventions (ticket IDs, branch names, commit format, plans folder), the index of documentation, and the AlignFirst protocols.
Run `npx -y alignfirst context` from the repository root, _before_ reading any other file. It prints the project conventions (ticket IDs, branch names, commit format, work files), the index of documentation, and the AlignFirst protocols.

## Tooling

Expand All @@ -28,7 +28,7 @@ This repository is on *GitHub*.

## Packages

- `alignfirst` — the AlignFirst CLI: protocols, plans and docs
- `alignfirst` — the AlignFirst CLI: protocols, work files and docs
- `@paleo/alcode` — coding agent wrapper for the AlignFirst Developer
- `@paleo/alproject` — project inventory and port allocation for the AlignFirst Developer host
- `@paleo/docmap` — lightweight documentation system for AI agents and humans
Expand Down
4 changes: 2 additions & 2 deletions DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ The tooling runs through the `alignfirst` CLI built in this workspace, so run `n
| `npm run lint` / `npm run lint:fix` | Check / fix with Biome |
| `npm run docmap` | Browse the project documentation |
| `npm run workspace -- <command>` | Manage worktree workspaces (`--guide` for the procedures) |
| `npm run plans:sync` | Publish and retrieve the task plans (`.plans`) |
| `npm run plans:sync` | Publish and retrieve the work files (`.plans`) |

In the main worktree, `.plans` is a symlink to the `alignfirst/` folder in a team plans repository clone. The folder is shared with the team and never committed here.
In the main worktree, `.plans` is a symlink to the `alignfirst/` folder in a clone of the work-files repository. The folder is shared with the team and never committed here.
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ npm run docmap

- `.local-wt/` — per-worktree. Dev-server logs, setup log.
- `.local/` — symlinked across worktrees. Shared gitignored files (workspace registry, dev-server registry, personal notes).
- `.plans/` — symlinked across worktrees. AlignFirst task plans.
- `.plans/` — symlinked across worktrees. AlignFirst work files.
2 changes: 1 addition & 1 deletion alignfirst-developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Ask the agent to create an AlignFirst Developer. The setup skill collects deploy
one Slack or Discord overlay and one Claude Code or Codex overlay, and produces role-specific
installation, security, operation, and recovery runbooks.

Managed projects receive the full preparation contract: the AlignFirst bootstrap line, optional team plans,
Managed projects receive the full preparation contract: the AlignFirst bootstrap line, an optional work-files repository,
docmap, isolated workspaces, and a project-specific `DEVELOPERS.md`.

## Maintain the Product
Expand Down
2 changes: 1 addition & 1 deletion docs/creating-a-pull-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ When the user asks to create a PR:
1. **Pre-flight checks.** Run `npm run lint:fix`, `npm run build`, and `npm test`. All must pass before proceeding.
2. **Ensure a changeset exists** when the branch touches `packages/`. Check for at least one changeset file in `.changeset/` (excluding `README.md`); if none exists, create one following [writing-a-changeset.md](writing-a-changeset.md), and include it in the commit.
3. **Commit and push.** Commit any uncommitted changes, including files modified by `lint:fix`. Then push the branch.
4. **Generate the PR description.** Use `alignfirst` to find the plan directory. If a description file (`*-description.md`) already exists **and** is the last file in the directory (no further work was done after it), use its content directly. Otherwise, run `npx -y alignfirst guide description` and follow it. Use the description body as the PR description and the suggested commit message as the PR title.
4. **Generate the PR description.** Use `alignfirst` to find the ticket directory. If a description file (`*-description.md`) already exists **and** is the last file in the directory (no further work was done after it), use its content directly. Otherwise, run `npx -y alignfirst guide description` and follow it. Use the description body as the PR description and the suggested commit message as the PR title.
5. **Create the PR** with `gh pr create`. The title follows the commit convention — conventional commits without the ticket ID, e.g. `feat: add new feature`:

```sh
Expand Down
2 changes: 1 addition & 1 deletion docs/proposals/project-overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The only footprint left in the repository is the `.plans` symlink, registered in

`ALIGNFIRST_OVERLAYS` names the directory holding the overlays. Each overlay is `<ALIGNFIRST_OVERLAYS>/<name>/_project/`. The underscore keeps it out of the ticket listing, like `_archives`.

The recommended value is the team plans clone. A project's overlay then sits next to its tickets, is versioned, shared with the team, and travels with `alignfirst sync`. The AlignFirst Developer template set the variable in `environment.d/common.conf` to `~/projects/<plans-clone>`. Any other directory works.
The recommended value is the work-files repository clone. A project's overlay then sits next to its tickets, is versioned, shared with the team, and travels with `alignfirst sync`. The AlignFirst Developer template set the variable in `environment.d/common.conf` to `~/projects/<plans-clone>`. Any other directory works.

An overlay holds any of: `.alignfirst.json`, `AGENTS.md`, `DEVELOPERS.md`, `docs/`.

Expand Down
16 changes: 15 additions & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Packages publish from GitHub Actions through npm trusted publishing (OIDC). Ther
2. `.github/workflows/release.yml` runs on the push. Its `version` job creates or updates the **release: version packages** PR, which applies the pending changesets to the manifests and changelogs.
3. Merging that PR pushes the bumped versions to `main`. The `check` job now finds versions absent from the registry and enables the `publish` job.
4. `publish` is bound to the `release` environment, so it waits for one approval. After approval it builds, tests, strips the `scripts` field from the workspace manifests, and runs `changeset publish`. npm attaches a provenance attestation to each tarball. The action then pushes git tags and creates the GitHub releases.
5. `verify` installs the freshly published versions in an empty directory, runs `npm audit signatures`, then asserts that each version carries a provenance attestation.
5. `verify` waits 15 minutes, then installs the freshly published versions in an empty directory, runs `npm audit signatures`, and asserts that each version carries a provenance attestation. The wait is the `verify` environment's timer: npm's CDN keeps serving a stale packument for several minutes after a publish, and a job held by a wait timer occupies no runner.

A push that publishes nothing — a feature merge, a docs-only merge — leaves `check` reporting no pending version, so no approval is ever requested.

Expand Down Expand Up @@ -80,6 +80,20 @@ Done on 2026-08-22. Requires the package owner's npm account and repository admi

3. Enable **Allow GitHub Actions to create and approve pull requests** in Settings → Actions → General → Workflow permissions. The `version` job needs it to open the Version Packages PR with the default `GITHUB_TOKEN`.

## The `verify` environment

Created on 2026-09-14. Its only purpose is the wait timer, so it carries no reviewer and no branch policy:

```bash
gh api -X PUT repos/paleo/alignfirst/environments/verify -F wait_timer=15
```

Before it existed, `verify` ran the moment `publish` finished and failed on every release: the
registry answered `ETARGET` for the versions just published, for more than five minutes each time.
The job's retry loop never once outlasted the stale packument. Recreate the environment with the
command above if it is ever deleted — the job's first step then waits out the remainder itself, so a
missing timer costs runner minutes rather than a failed release.

## Owner steps for the AlignFirst CLI

The first **release: version packages** PR bumps `alignfirst` to `0.1.0`. Do not let its publish job
Expand Down
2 changes: 1 addition & 1 deletion docs/writing-a-changeset.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Write the file directly. `npm run changeset` is the interactive equivalent, for
git status --short # include uncommitted files
```

2. **Gather context from the plan directory.** Run `alignfirst ticket <id>` to find the plan
2. **Gather context from the ticket directory.** Run `alignfirst ticket <id>` to find the ticket
directory. Read the summary files (`*-summary.md`) and spec files to write a meaningful
description.

Expand Down
8 changes: 4 additions & 4 deletions packages/alignfirst/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# alignfirst

The AlignFirst CLI provides collaborative software-development workflows, work files, and documentation discovery. Work files are organized by ticket and kept either git-ignored in the project or synchronized through a team plans repository.
The AlignFirst CLI provides collaborative software-development workflows, work files, and documentation discovery. Work files are organized by ticket and kept either git-ignored in the project or synchronized through a work-files repository.

## Agent skills

Expand Down Expand Up @@ -45,7 +45,7 @@ These examples use the `/` form. Replace it with `$` in Codex.

To implement a plan, start a fresh agent context and ask it to execute the plan file.

AlignFirst stores specifications, plans, and summaries in `.plans/<ticket-id>/`. It normally derives the ticket ID from the request or branch and asks when none is available. Files use a cycle letter and sequence number, such as `A1-spec.md` and `A2-plan.md`.
AlignFirst stores the work files of a ticket, such as specifications, plans and summaries, in `.plans/<ticket-id>/`. It normally derives the ticket ID from the request or branch and asks when none is available. Files use a cycle letter and sequence number, such as `A1-spec.md` and `A2-plan.md`.

## Global CLI

Expand Down Expand Up @@ -81,8 +81,8 @@ The guide installs the selected components and configures the repository. You ca

- `guide` — Print an AlignFirst protocol.
- `ticket` — Resolve a ticket directory, load its history, or get its next file.
- `sync` — Synchronize shared plans.
- `plans` — Set up, check and archive plans.
- `sync` — Synchronize the work files with the work-files repository.
- `plans` — Link `.plans` to the work-files repository, check the link, archive tickets.
- `docmap` — Browse project documentation.
- `conventions` — Print the effective project conventions.
- `context` — Print the conventions, the documentation map when `docs/` exists, and the protocol aliases.
Expand Down
2 changes: 1 addition & 1 deletion packages/alignfirst/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.3.0",
"license": "CC0-1.0",
"author": "Thomas MUR",
"description": "The AlignFirst CLI: protocols, plans and docs in one command.",
"description": "The AlignFirst CLI: protocols, work files and docs in one command.",
"keywords": [
"alignfirst",
"cli",
Expand Down
5 changes: 3 additions & 2 deletions packages/alignfirst/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export async function main(options?: MainOptions): Promise<number> {
ctx.projectConfig = resolveProjectConfig(ctx.cwd);
checkCliRange(ctx.projectConfig?.config, ctx.version, [command, ...args]);
}
return dispatch(ctx, command, args);
const code = await dispatch(ctx, command, args);
return code;
} catch (error) {
if (!(error instanceof CliError)) throw error;
ctx.stderr.write(`${error.message}\n`);
Expand All @@ -68,7 +69,7 @@ function readPackageVersion(): string {
}

function renderHelp(ctx: CommandContext): string {
return `alignfirst — protocols, plans and docs in one command.
return `alignfirst — protocols, work files and docs in one command.

Usage:
${ctx.form} guide [<protocol>]
Expand Down
2 changes: 1 addition & 1 deletion packages/alignfirst/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function runDoctor(ctx: CommandContext, args: string[]): number {
return inspectConfig(ctx, resolved);
});
writeSection(ctx, "Git", () => inspectGit(ctx, resolved));
writeSection(ctx, "Plans", () => inspectPlans(ctx));
writeSection(ctx, "Work files", () => inspectPlans(ctx));
writeSection(ctx, "Docmap", () => inspectDocmap(ctx));
writeSection(ctx, "Skills", () => inspectSkills(ctx));
return 0;
Expand Down
19 changes: 8 additions & 11 deletions packages/alignfirst/src/commands/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ function runSetup(ctx: CommandContext, args: string[]): number {

function createPlansDirectory(cloneDir: string, folder: string): string {
if (folder.length === 0 || folder === "." || folder === ".." || /[\\/]/u.test(folder)) {
throw new CliError(`Plans folder "${folder}" must be a single path segment.`);
throw new CliError(`Project folder "${folder}" must be a single path segment.`);
}
if (RESERVED_PLANS_FOLDERS.has(folder.toLowerCase())) {
throw new CliError(`Plans folder "${folder}" is reserved.`);
throw new CliError(`Project folder "${folder}" is reserved.`);
}
const cloneRoot = realpathSync(cloneDir);
const projectDir = join(cloneRoot, folder);
Expand All @@ -72,7 +72,7 @@ function createPlansDirectory(cloneDir: string, folder: string): string {
relativeTarget.startsWith(`..${sep}`) ||
isAbsolute(relativeTarget)
) {
throw new CliError(`Plans folder "${folder}" must resolve inside ${cloneRoot}.`);
throw new CliError(`Project folder "${folder}" must resolve inside ${cloneRoot}.`);
}
return projectDir;
}
Expand Down Expand Up @@ -117,15 +117,15 @@ function parseSetupArgs(
function checkClone(ctx: CommandContext, cloneDir: string): void {
if (!existsSync(cloneDir))
throw new CliError(
`${cloneDir} does not exist. Clone the team plans repository there first (see the instruction file).`,
`${cloneDir} does not exist. Clone the work-files repository there first (see the instruction file).`,
);
if (!existsSync(join(cloneDir, ".git")))
throw new CliError(
`${cloneDir} is not a git repository. Point ${ctx.form} plans setup at a clone of the team plans repository.`,
`${cloneDir} is not a git repository. Point ${ctx.form} plans setup at a clone of the work-files repository.`,
);
if (realpathSync(cloneDir) === realpathSync(ctx.cwd))
throw new CliError(
`${cloneDir} is the product repository itself. Point ${ctx.form} plans setup at a clone of the team plans repository.`,
`${cloneDir} is the product repository itself. Point ${ctx.form} plans setup at a clone of the work-files repository.`,
);
}

Expand All @@ -137,11 +137,8 @@ function runCheck(ctx: CommandContext, args: string[]): number {
const stopped = findStoppedRebase(mode.repoToplevel);
if (stopped !== undefined) throw new CliError(renderStoppedRebase(stopped, ctx.form));
}
if (mode.kind === "shared") ctx.stdout.write(".plans is linked to the team plans repository.\n");
else
ctx.stdout.write(
".plans is a local directory (local plans mode): synchronization is disabled.\n",
);
if (mode.kind === "shared") ctx.stdout.write(".plans is linked to the work-files repository.\n");
else ctx.stdout.write(".plans is a local directory (local mode): synchronization is disabled.\n");
return 0;
}

Expand Down
Loading