From 37a7d9999ed7ed8e3546dc8af79f094c71dec438 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 06:58:34 +0200 Subject: [PATCH 1/8] fix: keep git output inside the CLI streams, delay the verify job --- .changeset/alignfirst-git-failure-messages.md | 5 +++ .github/workflows/release.yml | 11 +++--- docs/releasing.md | 10 ++++-- packages/alignfirst/src/commands/sync.ts | 14 ++++---- packages/alignfirst/src/context.ts | 9 +++-- packages/alignfirst/src/git.ts | 36 +++++++++++-------- packages/alignfirst/src/plans/conflicts.ts | 5 ++- packages/alignfirst/src/plans/rebase.ts | 16 ++++----- packages/alignfirst/test/helpers.ts | 5 ++- 9 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 .changeset/alignfirst-git-failure-messages.md diff --git a/.changeset/alignfirst-git-failure-messages.md b/.changeset/alignfirst-git-failure-messages.md new file mode 100644 index 00000000..a3a2ab5b --- /dev/null +++ b/.changeset/alignfirst-git-failure-messages.md @@ -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. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75371934..0321d6c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,6 +77,9 @@ jobs: needs: publish if: needs.publish.outputs.published == 'true' runs-on: ubuntu-latest + # The `verify` environment carries a wait timer: the registry serves a stale + # packument for several minutes after a publish. No runner during the wait. + environment: verify steps: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -91,11 +94,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 ?? "")') diff --git a/docs/releasing.md b/docs/releasing.md index 1df6fe97..8cab90f1 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -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. @@ -78,7 +78,13 @@ Done on 2026-08-22. Requires the package owner's npm account and repository admi gh api -X POST repos/paleo/alignfirst/environments/release/deployment-branch-policies -f name=main ``` -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`. +3. Create the `verify` environment. 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 + ``` + +4. 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`. ## Owner steps for the AlignFirst CLI diff --git a/packages/alignfirst/src/commands/sync.ts b/packages/alignfirst/src/commands/sync.ts index 1347fec9..43c2cc48 100644 --- a/packages/alignfirst/src/commands/sync.ts +++ b/packages/alignfirst/src/commands/sync.ts @@ -29,24 +29,24 @@ export function runSync(ctx: CommandContext, args: string[]): number { } const repoDir = mode.repoToplevel; assertNoStoppedRebase(repoDir, ctx.form); - git(repoDir, "add", "-A"); - if (hasStagedChanges(repoDir)) git(repoDir, "commit", "--quiet", "-m", "sync"); + git(ctx, repoDir, "add", "-A"); + if (hasStagedChanges(repoDir)) git(ctx, repoDir, "commit", "--quiet", "-m", "sync"); if (hasUpstream(repoDir)) { try { - git(repoDir, "pull", "--rebase"); + git(ctx, repoDir, "pull", "--rebase"); } catch { if (findStoppedRebase(repoDir) === undefined) throw new CliError("git pull failed. See the git output above."); - resolveStoppedRebase(repoDir, ctx.stdout); + resolveStoppedRebase(ctx, repoDir); } } if (thresholdDays !== undefined && autoArchive(plansDir, thresholdDays, ctx.stdout)) { - git(repoDir, "add", "-A"); - if (hasStagedChanges(repoDir)) git(repoDir, "commit", "--quiet", "-m", "sync"); + git(ctx, repoDir, "add", "-A"); + if (hasStagedChanges(repoDir)) git(ctx, repoDir, "commit", "--quiet", "-m", "sync"); } if (hasCommitsToSend(repoDir)) { try { - git(repoDir, "push", "--quiet", "-u", "origin", "HEAD"); + git(ctx, repoDir, "push", "--quiet", "-u", "origin", "HEAD"); } catch { throw new CliError( `git push failed. See the git output above. Another synchronization may have landed first: run ${ctx.form} sync again.`, diff --git a/packages/alignfirst/src/context.ts b/packages/alignfirst/src/context.ts index a0c59576..85f5cd82 100644 --- a/packages/alignfirst/src/context.ts +++ b/packages/alignfirst/src/context.ts @@ -4,12 +4,15 @@ export interface Output { write(text: string): void; } -export interface CommandContext { +export interface Streams { + stdout: Output; + stderr: Output; +} + +export interface CommandContext extends Streams { cwd: string; env: NodeJS.ProcessEnv; home: string; - stdout: Output; - stderr: Output; form: string; version: string; projectConfig?: ResolvedProjectConfig; diff --git a/packages/alignfirst/src/git.ts b/packages/alignfirst/src/git.ts index 5d79a159..2e0b778f 100644 --- a/packages/alignfirst/src/git.ts +++ b/packages/alignfirst/src/git.ts @@ -1,19 +1,23 @@ -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import { realpathSync } from "node:fs"; import { resolve } from "node:path"; import { CliError } from "./cli-error.js"; +import type { Streams } from "./context.js"; -export function git(dir: string, ...args: string[]): void { - try { - execFileSync("git", ["-C", dir, ...args], { stdio: "inherit" }); - } catch { - throw gitFailure(args); - } +export function git(streams: Streams, dir: string, ...args: string[]): void { + const result = spawnSync("git", ["-C", dir, ...args], { encoding: "utf-8" }); + if (result.error !== undefined) throw gitFailure(args, result.error.message); + streams.stdout.write(result.stdout); + streams.stderr.write(result.stderr); + if (result.status !== 0) throw gitFailure(args); } -function gitFailure(args: string[]): CliError { - return new CliError(`git ${args[0]} failed. See the git output above.`); +function gitFailure(args: string[], detail?: string): CliError { + const output = detail?.trim(); + if (output === undefined || output === "") + return new CliError(`git ${args[0]} failed. See the git output above.`); + return new CliError(`git ${args[0]} failed:\n${output}`); } export function assertMainWorktreeRoot(cwd: string): void { @@ -33,18 +37,20 @@ export function gitOutput(dir: string, ...args: string[]): string { } export function gitOutputRaw(dir: string, ...args: string[]): string { - let output: Buffer; - try { - output = execFileSync("git", ["-C", dir, ...args]); - } catch { - throw gitFailure(args); - } + const output = gitBuffer(dir, ...args); const text = output.toString("utf8"); if (!Buffer.from(text).equals(output)) throw new CliError("Cannot read non-UTF-8 Git output safely. Resolve the rebase manually."); return text; } +export function gitBuffer(dir: string, ...args: string[]): Buffer { + const result = spawnSync("git", ["-C", dir, ...args]); + if (result.error !== undefined) throw gitFailure(args, result.error.message); + if (result.status !== 0) throw gitFailure(args, result.stderr.toString("utf8")); + return result.stdout; +} + export function gitOutputOrUndefined(dir: string, ...args: string[]): string | undefined { try { return execFileSync("git", ["-C", dir, ...args], { diff --git a/packages/alignfirst/src/plans/conflicts.ts b/packages/alignfirst/src/plans/conflicts.ts index ebbe5905..dfb57872 100644 --- a/packages/alignfirst/src/plans/conflicts.ts +++ b/packages/alignfirst/src/plans/conflicts.ts @@ -1,4 +1,3 @@ -import { execFileSync } from "node:child_process"; import { chmodSync, lstatSync, @@ -12,7 +11,7 @@ import { basename, dirname, extname, join, relative, resolve } from "node:path"; import { CliError } from "../cli-error.js"; import type { Output } from "../context.js"; -import { gitOutput, gitOutputRaw } from "../git.js"; +import { gitBuffer, gitOutput, gitOutputRaw } from "../git.js"; import { nextFilePosition } from "./ticket.js"; interface Resolution { @@ -248,7 +247,7 @@ function assertNoLaterEdits(repoDir: string, renames: ReadonlyMap= MAX_REBASE_STEPS) throw new CliError(`Could not finish the stopped rebase in ${repoDir}.`); - resolveConflictedPaths(repoDir, stdout); - git(repoDir, "add", "-A"); - continueRebase(repoDir); + resolveConflictedPaths(repoDir, streams.stdout); + git(streams, repoDir, "add", "-A"); + continueRebase(streams, repoDir); } } -function continueRebase(repoDir: string): void { +function continueRebase(streams: Streams, repoDir: string): void { const args = ["-c", "core.editor=true", "rebase", "--continue"]; try { - git(repoDir, ...args); + git(streams, repoDir, ...args); } catch (error) { const stopped = findStoppedRebase(repoDir); if (stopped?.conflictedFiles.length) return; if (stopped && gitSucceeds(repoDir, "diff", "--cached", "--quiet")) { - git(repoDir, "rebase", "--skip"); + git(streams, repoDir, "rebase", "--skip"); return; } throw error; diff --git a/packages/alignfirst/test/helpers.ts b/packages/alignfirst/test/helpers.ts index 6f29e013..3617c5cc 100644 --- a/packages/alignfirst/test/helpers.ts +++ b/packages/alignfirst/test/helpers.ts @@ -63,7 +63,10 @@ export function configureGit(dir: string): void { } export function git(dir: string, ...args: string[]): string { - return execFileSync("git", ["-C", dir, ...args], { encoding: "utf-8" }).trim(); + return execFileSync("git", ["-C", dir, ...args], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); } function readPackageVersion(): string { From 88e1d217dc31614ed620eafc2b2a1829205930c9 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 07:06:19 +0200 Subject: [PATCH 2/8] fix(setup-guide): omit the cli range from .alignfirst.json by default --- skills/alignfirst-setup-guide/SKILL.md | 2 +- .../references/alignfirst-skills-setup.md | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/skills/alignfirst-setup-guide/SKILL.md b/skills/alignfirst-setup-guide/SKILL.md index 3de2d348..4f76c637 100644 --- a/skills/alignfirst-setup-guide/SKILL.md +++ b/skills/alignfirst-setup-guide/SKILL.md @@ -6,7 +6,7 @@ description: >- license: CC0 1.0 metadata: author: Paleo - version: "0.37.2" + version: "0.37.3" repository: https://github.com/paleo/alignfirst --- diff --git a/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md b/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md index faf2140f..b4cd0284 100644 --- a/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md @@ -100,15 +100,11 @@ expression; alternatives go inside a group, as in `^(ABC|XYZ)-\d+$`. Detect the `git ls-remote --symref origin HEAD`; use the sole remote when `origin` is absent, and ask the user when several non-`origin` remotes exist. -Write `.alignfirst.json` with the agreed fields. When the project declares no `alignfirst` dependency, -set `cli` to its supported version range. Without this field, each machine runs whichever version it -fetched. The version guard reports a mismatch and gives the exact -`npx -y alignfirst@""` command to run: +Write `.alignfirst.json` with the agreed fields: ```json { "schemaVersion": 1, - "cli": "", "ticketIdPattern": "^\\d+$", "plans": { "folder": "acme-web", "autoArchive": true }, "portRange": { "first": 8100, "last": 8299 }, @@ -121,8 +117,9 @@ fetched. The version guard reports a mismatch and gives the exact } ``` -Keep only applicable optional fields. Replace any hand-written AlignFirst or docmap section in -`AGENTS.md` or `CLAUDE.md` with the following section. Place it before every other section whenever +Keep only applicable optional fields. Omit `cli` unless the user asks to pin the CLI version. It takes a semver range; the version guard then rejects a mismatching CLI and prints the matching `npx -y alignfirst@""` command. + +Replace any hand-written AlignFirst or docmap section in `AGENTS.md` or `CLAUDE.md` with the following section. Place it before every other section whenever possible: ```markdown From 20eff8bffc85eb1f788b668b81f9b4feee078389 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 07:29:18 +0200 Subject: [PATCH 3/8] docs: say work files instead of plans for the .plans container` --- .changeset/work-files-vocabulary.md | 6 ++++++ AGENTS.md | 4 ++-- DEVELOPERS.md | 4 ++-- .../projects-fixture/template/DEVELOPERS.md | 2 +- alignfirst-developer.md | 2 +- docs/creating-a-pull-request.md | 2 +- docs/proposals/project-overlays.md | 2 +- docs/writing-a-changeset.md | 2 +- packages/alignfirst/README.md | 8 ++++---- packages/alignfirst/package.json | 2 +- packages/alignfirst/src/cli.ts | 2 +- packages/alignfirst/src/commands/doctor.ts | 2 +- packages/alignfirst/src/commands/plans.ts | 19 ++++++++---------- packages/alignfirst/src/commands/sync.ts | 6 +++--- packages/alignfirst/src/conventions.ts | 8 ++++---- packages/alignfirst/src/plans/conflicts.ts | 4 ++-- packages/alignfirst/src/plans/layout.ts | 2 +- packages/alignfirst/src/plans/link.ts | 2 +- packages/alignfirst/src/plans/rebase.ts | 2 +- .../guide/code-review/reviewer-common.md | 2 +- packages/alignfirst/test/conventions.test.ts | 8 ++++---- packages/alignfirst/test/doctor.test.ts | 4 ++-- packages/alignfirst/test/plans.test.ts | 10 +++++----- packages/alproject/src/render.ts | 2 +- packages/workspace/src/workspace.ts | 2 +- .../references/runbooks/project-lifecycle.md | 6 +++--- skills/alignfirst-setup-guide/SKILL.md | 14 ++++++------- .../base/AGENTS.md | 4 ++-- .../base/DEVELOPERS.md | 4 ++-- .../docs/installations/02-admin-repository.md | 8 ++++---- .../base/docs/operations/add-project.md | 6 +++--- .../references/alignfirst-developer.md | 20 +++++++++---------- .../references/alignfirst-skills-setup.md | 6 +++--- .../references/alignfirst-upgrade-from-v2.md | 2 +- .../references/alignfirst-upgrade-from-v3.md | 18 ++++++++--------- .../references/alignfirst-upgrade.md | 2 +- .../references/docmap-setup.md | 2 +- .../references/plans-setup.md | 16 +++++++-------- .../references/workspace-setup.md | 6 +++--- 39 files changed, 113 insertions(+), 110 deletions(-) create mode 100644 .changeset/work-files-vocabulary.md diff --git a/.changeset/work-files-vocabulary.md b/.changeset/work-files-vocabulary.md new file mode 100644 index 00000000..a8232cc1 --- /dev/null +++ b/.changeset/work-files-vocabulary.md @@ -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. diff --git a/AGENTS.md b/AGENTS.md index db1b4a49..1cee040d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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 diff --git a/DEVELOPERS.md b/DEVELOPERS.md index a0c96be4..eb3b6176 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -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 -- ` | 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. diff --git a/alignfirst-developer-tests/projects-fixture/template/DEVELOPERS.md b/alignfirst-developer-tests/projects-fixture/template/DEVELOPERS.md index 71734a07..57d0170f 100644 --- a/alignfirst-developer-tests/projects-fixture/template/DEVELOPERS.md +++ b/alignfirst-developer-tests/projects-fixture/template/DEVELOPERS.md @@ -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. diff --git a/alignfirst-developer.md b/alignfirst-developer.md index 261245cc..3ac9b925 100644 --- a/alignfirst-developer.md +++ b/alignfirst-developer.md @@ -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 diff --git a/docs/creating-a-pull-request.md b/docs/creating-a-pull-request.md index 8843b71c..c7c76c6d 100644 --- a/docs/creating-a-pull-request.md +++ b/docs/creating-a-pull-request.md @@ -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 diff --git a/docs/proposals/project-overlays.md b/docs/proposals/project-overlays.md index 84540b96..0a13609a 100644 --- a/docs/proposals/project-overlays.md +++ b/docs/proposals/project-overlays.md @@ -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 `//_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/`. 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/`. Any other directory works. An overlay holds any of: `.alignfirst.json`, `AGENTS.md`, `DEVELOPERS.md`, `docs/`. diff --git a/docs/writing-a-changeset.md b/docs/writing-a-changeset.md index 794c09b8..4ff9b16c 100644 --- a/docs/writing-a-changeset.md +++ b/docs/writing-a-changeset.md @@ -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 ` to find the plan +2. **Gather context from the ticket directory.** Run `alignfirst ticket ` to find the ticket directory. Read the summary files (`*-summary.md`) and spec files to write a meaningful description. diff --git a/packages/alignfirst/README.md b/packages/alignfirst/README.md index 95147050..5cd59984 100644 --- a/packages/alignfirst/README.md +++ b/packages/alignfirst/README.md @@ -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 @@ -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//`. 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//`. 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 @@ -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. diff --git a/packages/alignfirst/package.json b/packages/alignfirst/package.json index 1b9b42e8..55684561 100644 --- a/packages/alignfirst/package.json +++ b/packages/alignfirst/package.json @@ -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", diff --git a/packages/alignfirst/src/cli.ts b/packages/alignfirst/src/cli.ts index f5c20714..c1bbff60 100644 --- a/packages/alignfirst/src/cli.ts +++ b/packages/alignfirst/src/cli.ts @@ -68,7 +68,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 [] diff --git a/packages/alignfirst/src/commands/doctor.ts b/packages/alignfirst/src/commands/doctor.ts index c38da056..feacc610 100644 --- a/packages/alignfirst/src/commands/doctor.ts +++ b/packages/alignfirst/src/commands/doctor.ts @@ -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; diff --git a/packages/alignfirst/src/commands/plans.ts b/packages/alignfirst/src/commands/plans.ts index e9e819f5..6b976778 100644 --- a/packages/alignfirst/src/commands/plans.ts +++ b/packages/alignfirst/src/commands/plans.ts @@ -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); @@ -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; } @@ -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.`, ); } @@ -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; } diff --git a/packages/alignfirst/src/commands/sync.ts b/packages/alignfirst/src/commands/sync.ts index 43c2cc48..5fa5fd96 100644 --- a/packages/alignfirst/src/commands/sync.ts +++ b/packages/alignfirst/src/commands/sync.ts @@ -24,7 +24,7 @@ export function runSync(ctx: CommandContext, args: string[]): number { const plansDir = join(ctx.cwd, ".plans"); if (mode.kind === "local") { if (thresholdDays !== undefined) autoArchive(plansDir, thresholdDays, ctx.stdout); - ctx.stdout.write("(local plans mode, nothing to sync)\n"); + ctx.stdout.write("(local mode, nothing to sync)\n"); return 0; } const repoDir = mode.repoToplevel; @@ -52,9 +52,9 @@ export function runSync(ctx: CommandContext, args: string[]): number { `git push failed. See the git output above. Another synchronization may have landed first: run ${ctx.form} sync again.`, ); } - ctx.stdout.write("Plans synchronized: local changes sent.\n"); + ctx.stdout.write("Work files synchronized: local changes sent.\n"); } else { - ctx.stdout.write("Plans synchronized: nothing to send.\n"); + ctx.stdout.write("Work files synchronized: nothing to send.\n"); } return 0; } diff --git a/packages/alignfirst/src/conventions.ts b/packages/alignfirst/src/conventions.ts index 43996760..a0eff8f2 100644 --- a/packages/alignfirst/src/conventions.ts +++ b/packages/alignfirst/src/conventions.ts @@ -65,18 +65,18 @@ function renderPlans(ctx: CommandContext): string | undefined { const mode = resolvePlansMode(ctx.cwd, ctx.form); const folder = ctx.projectConfig?.config.plans?.folder; const sharedFolder = - folder === undefined ? "" : ` (shared folder \`${folder}\`, a separate git repository)`; + folder === undefined ? "" : ` (folder \`${folder}\` in the work-files repository)`; const base = mode.kind === "shared" - ? `Plans: use \`.plans\`${sharedFolder}; run \`${ctx.form} sync\` after changes.` - : "Plans: use `.plans`."; + ? `Work files: use \`.plans\`${sharedFolder}; run \`${ctx.form} sync\` after changes.` + : "Work files: use `.plans`."; const archival = ctx.projectConfig?.config.plans?.autoArchive === true ? " Automatic archival is enabled." : ""; return `${base}${archival}`; } catch (error) { - return `Plans: ${errorMessage(error).split("\n", 1)[0]}`; + return `Work files: ${errorMessage(error).split("\n", 1)[0]}`; } } diff --git a/packages/alignfirst/src/plans/conflicts.ts b/packages/alignfirst/src/plans/conflicts.ts index dfb57872..e5cec2cc 100644 --- a/packages/alignfirst/src/plans/conflicts.ts +++ b/packages/alignfirst/src/plans/conflicts.ts @@ -74,7 +74,7 @@ function readConflicts(repoDir: string): Map> { const header = entry.slice(0, tab).split(" "); const stage = Number(header[2]); if (tab === -1 || ![1, 2, 3].includes(stage)) - throw new CliError("Could not read the conflicted plans index."); + throw new CliError("Could not read the git index of the work-files repository."); const path = entry.slice(tab + 1); const stages = result.get(path) ?? new Set(); stages.add(stage); @@ -262,7 +262,7 @@ function updateReferences( const to = relative(dirname(source), after); replacements.set(from, to); replacements.set(`./${from}`, `./${to}`); - // A shared plans clone stores the project's .plans directory under its configured folder. + // A work-files repository clone stores the project's .plans directory under its configured folder. replacements.set(before.replace(/^[^/]+\//, ".plans/"), after.replace(/^[^/]+\//, ".plans/")); } if (replacements.size === 0) return original; diff --git a/packages/alignfirst/src/plans/layout.ts b/packages/alignfirst/src/plans/layout.ts index e69b8b80..ba1fffb5 100644 --- a/packages/alignfirst/src/plans/layout.ts +++ b/packages/alignfirst/src/plans/layout.ts @@ -35,7 +35,7 @@ export function assertPlansGate(cwd: string, form: string): Stats { } export function missingPlansMessage(form: string): string { - return `No .plans/ directory in the current directory.\nLocal plans: mkdir .plans && echo .plans >> .gitignore\nTeam plans: ${form} plans setup `; + return `No .plans/ directory in the current directory.\nLocal work files: mkdir .plans && echo .plans >> .gitignore\nTeam work files: ${form} plans setup `; } export function missingPlansError(form: string): CliError { diff --git a/packages/alignfirst/src/plans/link.ts b/packages/alignfirst/src/plans/link.ts index f5118d08..7936b613 100644 --- a/packages/alignfirst/src/plans/link.ts +++ b/packages/alignfirst/src/plans/link.ts @@ -17,7 +17,7 @@ export function linkPlans(ctx: CommandContext, targetDir: string): void { const stats = lstatSync(plansPath, { throwIfNoEntry: false }); if (stats?.isSymbolicLink()) { if (existsSync(plansPath) && realpathSync(plansPath) === realpathSync(targetDir)) { - ctx.stdout.write(".plans already links to the plans repository.\n"); + ctx.stdout.write(".plans already links to the work-files repository.\n"); return; } rmSync(plansPath); diff --git a/packages/alignfirst/src/plans/rebase.ts b/packages/alignfirst/src/plans/rebase.ts index f670ef26..64413eb7 100644 --- a/packages/alignfirst/src/plans/rebase.ts +++ b/packages/alignfirst/src/plans/rebase.ts @@ -54,7 +54,7 @@ function continueRebase(streams: Streams, repoDir: string): void { export function renderStoppedRebase(stopped: StoppedRebase, form: string): string { const files = stopped.conflictedFiles.map((path) => ` ${path}`); return [ - `Plans synchronization stopped on a conflict in ${stopped.repoDir}:`, + `Work-files synchronization stopped on a conflict in ${stopped.repoDir}:`, ...files, "Resolve the markers in these files, then run:", ` git -C ${stopped.repoDir} add -A && git -C ${stopped.repoDir} rebase --continue`, diff --git a/packages/alignfirst/templates/guide/code-review/reviewer-common.md b/packages/alignfirst/templates/guide/code-review/reviewer-common.md index a139cd7b..1c186924 100644 --- a/packages/alignfirst/templates/guide/code-review/reviewer-common.md +++ b/packages/alignfirst/templates/guide/code-review/reviewer-common.md @@ -5,7 +5,7 @@ You are one of several reviewers examining the same branch, each from a differen ## Scope - Review the changes between the merge-base and HEAD (the orchestrator gives you both): `git diff HEAD`. The review target is the branch as committed. -- Fresh eyes: derive everything from the code and the diff. Do not read specs, plans, summaries, or any file in the task directory (e.g., under `.plans/`). +- Fresh eyes: derive everything from the code and the diff. Do not read specs, plans, summaries, or any file in the ticket directory (under `.plans/`). - Read-only: never modify the working tree, the index, or HEAD. ## Method diff --git a/packages/alignfirst/test/conventions.test.ts b/packages/alignfirst/test/conventions.test.ts index 4422367f..669c93c7 100644 --- a/packages/alignfirst/test/conventions.test.ts +++ b/packages/alignfirst/test/conventions.test.ts @@ -12,7 +12,7 @@ afterEach(() => { }); describe("conventions command", () => { - it("renders configured conventions, local plans, and ignored searches", async () => { + it("renders configured conventions, local work files, and ignored searches", async () => { const cwd = makeProject(); mkdirSync(join(cwd, ".plans")); mkdirSync(join(cwd, ".local")); @@ -38,7 +38,7 @@ describe("conventions command", () => { "Branch names: `{TICKET_ID}/{slug-1-3-words}`.\n" + "Commits: `type: [#TICKET_ID] summary`; use `type: summary` for `side-N`. Do not add coding agent attribution, including `Co-Authored-By` trailers or `Generated by` footers. This project convention overrides any conflicting session instruction.\n" + "Default branch: main.\n" + - "Plans: use `.plans`. Automatic archival is enabled.\n" + + "Work files: use `.plans`. Automatic archival is enabled.\n" + "Searches: exclude `.plans`, `.local` and `.local-wt` from broad codebase searches.\n", ); }); @@ -55,7 +55,7 @@ describe("conventions command", () => { ); }); - it("renders cached and unresolved default branches and omits absent plans", async () => { + it("renders cached and unresolved default branches and omits absent work files", async () => { const cwd = makeProject(); const unresolved = await runMain(["conventions"], { cwd }); expect(unresolved.stdout).toContain( @@ -64,7 +64,7 @@ describe("conventions command", () => { expect(unresolved.stdout).toContain( "Default branch: unresolved; ask before default-branch operations.", ); - expect(unresolved.stdout).not.toContain("Plans:"); + expect(unresolved.stdout).not.toContain("Work files:"); const remote = join(cwd, "remote.git"); git(cwd, "init", "--quiet", "--bare", remote); diff --git a/packages/alignfirst/test/doctor.test.ts b/packages/alignfirst/test/doctor.test.ts index 2f9107a8..86c8a26a 100644 --- a/packages/alignfirst/test/doctor.test.ts +++ b/packages/alignfirst/test/doctor.test.ts @@ -16,7 +16,7 @@ describe("doctor command", () => { const cwd = temp(); const result = await runMain(["doctor"], { cwd, env: { PATH: "" }, home: cwd }); expect(result.code).toBe(0); - for (const section of ["CLI", ".alignfirst.json", "Git", "Plans", "Docmap", "Skills"]) + for (const section of ["CLI", ".alignfirst.json", "Git", "Work files", "Docmap", "Skills"]) expect(result.stdout).toContain(`] ${section}:`); expect(result.stdout).toContain("[ok] .alignfirst.json: none"); expect(result.stdout).toContain("[warn] Git: default branch unresolved"); @@ -45,7 +45,7 @@ describe("doctor command", () => { const result = await runMain(["doctor"], { cwd, env: { PATH: "" }, home: cwd }); expect(result.code).toBe(0); expect(result.stdout).toContain("[error] .alignfirst.json: Invalid"); - expect(result.stdout).toContain("] Plans:"); + expect(result.stdout).toContain("] Work files:"); }); it("reports the configured default branch and skill generation", async () => { diff --git a/packages/alignfirst/test/plans.test.ts b/packages/alignfirst/test/plans.test.ts index 08745a76..178bac76 100644 --- a/packages/alignfirst/test/plans.test.ts +++ b/packages/alignfirst/test/plans.test.ts @@ -24,12 +24,12 @@ afterEach(() => { }); describe("plans commands", () => { - it("reports local plans mode", async () => { + it("reports local mode", async () => { const fixture = makeFixture(); mkdirSync(join(fixture.product, ".plans")); const result = await runMain(["plans", "check"], { cwd: fixture.product }); expect(result).toMatchObject({ code: 0, stderr: "" }); - expect(result.stdout).toContain("local plans mode"); + expect(result.stdout).toContain("local mode"); }); it("sets up the plans link with the configured folder", async () => { @@ -93,7 +93,7 @@ describe("plans commands", () => { cwd: fixture.product, }); expect(result.code).toBe(1); - expect(result.stderr).toContain(`Plans folder "${folder}" is reserved.`); + expect(result.stderr).toContain(`Project folder "${folder}" is reserved.`); } expect(readFileSync(localPlan, "utf8")).toBe("spec\n"); @@ -244,7 +244,7 @@ describe("plans commands", () => { const conflict = await runMain(["sync"], { cwd: fixture.product }); expect(conflict.code).toBe(1); - expect(conflict.stderr).toContain("Plans synchronization stopped on a conflict"); + expect(conflict.stderr).toContain("Work-files synchronization stopped on a conflict"); expect((await runMain(["plans", "check"], { cwd: fixture.product })).code).toBe(1); expect( ( @@ -254,7 +254,7 @@ describe("plans commands", () => { home: fixture.root, }) ).stdout, - ).toContain("[error] Plans: rebase stopped on a conflict in"); + ).toContain("[error] Work files: rebase stopped on a conflict in"); writeFileSync(plan, "resolved\n"); git(fixture.clone, "add", "-A"); diff --git a/packages/alproject/src/render.ts b/packages/alproject/src/render.ts index c80d2d5e..ac119b47 100644 --- a/packages/alproject/src/render.ts +++ b/packages/alproject/src/render.ts @@ -99,7 +99,7 @@ export function renderProjectStatus(details: ProjectDetails): string { ` Directory: ${renderOutputValue(details.directory)}`, ` Remote host: ${renderNullableValue(details.remoteHost)}`, ` Port range: ${renderProjectRange(details.portRange ?? undefined, details.portRangeCode ?? undefined)}`, - ` Plans folder: ${renderNullableValue(details.plansFolder)}`, + ` Work-files folder: ${renderNullableValue(details.plansFolder)}`, ` Ticket id pattern: ${renderNullableValue(details.ticketIdPattern)}`, ` Workspaces: ${renderValues(details.workspaces)}`, " Worktrees:", diff --git a/packages/workspace/src/workspace.ts b/packages/workspace/src/workspace.ts index abd86cc6..8846ed12 100644 --- a/packages/workspace/src/workspace.ts +++ b/packages/workspace/src/workspace.ts @@ -1269,7 +1269,7 @@ export function linkSharedDirectories( for (const dirName of dirs) { const mainDir = join(ctx.mainWorktree, dirName); if (!existsSync(mainDir)) { - // A dead symlink (e.g. `.plans` pointing at a moved clone of the plans repository) must be repaired by the + // A dead symlink (e.g. `.plans` pointing at a moved clone of the work-files repository) must be repaired by the // user, not shadowed by a fresh directory. if (lstatSync(mainDir, { throwIfNoEntry: false })?.isSymbolicLink()) { throw new WorkspaceError( diff --git a/skills/alignfirst-developer-openclaw-playbook/references/runbooks/project-lifecycle.md b/skills/alignfirst-developer-openclaw-playbook/references/runbooks/project-lifecycle.md index a0480957..e5906510 100644 --- a/skills/alignfirst-developer-openclaw-playbook/references/runbooks/project-lifecycle.md +++ b/skills/alignfirst-developer-openclaw-playbook/references/runbooks/project-lifecycle.md @@ -50,7 +50,7 @@ The contract is the one the `alignfirst-setup-guide` lists under "Prepare a Proj End the turn on a message that explains the procedure: a branch created in the main worktree, preparation commits by the coding agent, a pull request the user must merge, and work waiting for that merge before the original request resumes. -Ask the user to approve this procedure and whether `.plans` must be shared through a team plans repository. If yes, ask for the repository URL. If no, `.plans` stays a plain directory. Wait for explicit approval. +Ask the user to approve this procedure and whether `.plans` must be shared through a work-files repository. If yes, ask for the repository URL. If no, `.plans` stays a plain directory. Wait for explicit approval. ### Step 4 — Prepare the project on a branch @@ -58,7 +58,7 @@ On approval: 1. Create `.plans/` in the main worktree and run `alignfirst sync`. Run `alignfirst ticket --side` from PROJECT_PATH, write `.plans/{TICKET_ID}/A1-request.md` with the recorded request, then run `alignfirst sync`. 2. Create `{TICKET_ID}/alignfirst-setup` in the main worktree. This setup branch is the second main-worktree exception, next to new-project bootstrap. -3. Run `alcode --openclaw-guide`. From PROJECT_PATH, delegate the preparation to alcode without a protocol: use the `alignfirst-setup-guide` skill and prepare the repository for an AlignFirst Developer, with the user's team plans decision and repository URL. It must run `alproject doctor --root ~/projects` after writing `.alignfirst.json` and before workspace setup, stopping on an unhealthy inventory. Instruct alcode to commit and push the branch. The setup guide's rule against pushing addresses a human's laptop session, not this procedure. +3. Run `alcode --openclaw-guide`. From PROJECT_PATH, delegate the preparation to alcode without a protocol: use the `alignfirst-setup-guide` skill and prepare the repository for an AlignFirst Developer, with the user's work-files repository decision and its URL. It must run `alproject doctor --root ~/projects` after writing `.alignfirst.json` and before workspace setup, stopping on an unhealthy inventory. Instruct alcode to commit and push the branch. The setup guide's rule against pushing addresses a human's laptop session, not this procedure. 4. Have alcode create a ready pull request, not a draft. 5. End the turn on the PR link and state that work resumes once the PR is merged. @@ -68,7 +68,7 @@ When the user reports the merge, or you observe it while checking the PR: 1. In the main worktree, switch back to the default branch, pull, and delete the local setup branch. 2. Install dependencies and build. -3. When the user chose the team plans repository, clone it under `~/projects` when no clone exists there (the projects guide names the repository), then run `alignfirst plans setup ~/projects/` from PROJECT_PATH. Otherwise, run `mkdir .plans`. +3. When the user chose the work-files repository, clone it under `~/projects` when no clone exists there (the projects guide names the repository), then run `alignfirst plans setup ~/projects/` from PROJECT_PATH. Otherwise, run `mkdir .plans`. 4. Run `alproject doctor --root ~/projects`. Stop when the inventory is unhealthy. 5. Run the project's `workspace setup` on the main worktree. Add `--profile remote` when the deployment sets `REMOTE_DEV_DOMAIN`. 6. Continue with the normal working-session flow for the original request through `project-workspace-setup.md`. diff --git a/skills/alignfirst-setup-guide/SKILL.md b/skills/alignfirst-setup-guide/SKILL.md index 4f76c637..e9cfc66c 100644 --- a/skills/alignfirst-setup-guide/SKILL.md +++ b/skills/alignfirst-setup-guide/SKILL.md @@ -26,8 +26,8 @@ Cursor, or `$alspec` in Codex. The optional `alignfirst` skill lets the agent re named in prose; a project whose instruction file starts with the canonical `alignfirst context` section provides this itself. -`alignfirst-setup-guide` and `alignfirst-developer-openclaw-playbook` are separate skills. A team -plans repository is an optional CLI mode configured through `alignfirst plans setup`. +`alignfirst-setup-guide` and `alignfirst-developer-openclaw-playbook` are separate skills. A +work-files repository is an optional CLI mode configured through `alignfirst plans setup`. An AlignFirst Developer host also installs `@paleo/alcode`, the companion CLI for coding-agent delegation and project discovery. @@ -40,7 +40,7 @@ upgrade only what they requested. - **AlignFirst CLI and skills**: [alignfirst-skills-setup.md](references/alignfirst-skills-setup.md). For an existing v1, v2, or v3 installation, start with [alignfirst-upgrade.md](references/alignfirst-upgrade.md). -- **Team plans repository**: [plans-setup.md](references/plans-setup.md). +- **Work-files repository**: [plans-setup.md](references/plans-setup.md). - **docmap**: [docmap-setup.md](references/docmap-setup.md). - **workspace**: [workspace-setup.md](references/workspace-setup.md). @@ -52,13 +52,13 @@ When the user asks what the project could adopt, inspect the repository and pres choices: - **AlignFirst** installs the CLI and the command skills for collaborative specification, planning, - implementation, merge, review, description, and catch-up workflows. A team plans repository is an + implementation, merge, review, description, and catch-up workflows. A work-files repository is an optional sub-choice. - **docmap** makes the repository's `docs/` tree discoverable to agents and humans. It is available through the AlignFirst CLI or as the standalone `@paleo/docmap` package. - **workspace** creates isolated git-worktree development environments. -Determine whether a team plans repository exists before offering that option. Let the user choose +Determine whether a work-files repository exists before offering that option. Let the user choose any subset. ## AlignFirst Developer @@ -80,7 +80,7 @@ Inspect the repository before changing it. A prepared project has all of these: `.alignfirst.json` is required for an AlignFirst Developer project and optional otherwise. 2. A clean `alproject doctor --root ` result after writing `.alignfirst.json` and before workspace setup. Stop preparation when the inventory is unhealthy. -3. The team plans repository through `alignfirst plans setup` when the team has one. +3. The work-files repository through `alignfirst plans setup` when the team has one. 4. docmap, including project scripts or CLI instructions. When the repository has no `docs/` directory, bootstrap its documentation through [docmap-bootstrapping.md](references/docmap-bootstrapping.md) as part of the preparation. @@ -115,7 +115,7 @@ Detect existing footprints before proposing changes: - workspace: a `workspace` script or `@paleo/workspace`. - AlignFirst: `.alignfirst.json`, `.plans/`, a bootstrap section running `alignfirst context` or `npx alignfirst context`, an AlignFirst instruction section, or a canonical skill installation. -- team plans: a `.plans` symlink or `plans.folder` in `.alignfirst.json`. +- work-files repository: a `.plans` symlink or `plans.folder` in `.alignfirst.json`. - AlignFirst Developer preparation: the complete seven-part contract above. Require a clean working tree immediately before project mutations. Read-only discovery and diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md index 49010758..5139c789 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md @@ -31,9 +31,9 @@ _Commit message convention:_ Conventional Commits with a very short subject, e.g _Default branch:_ `main`. -### Team Plans Repository +### Work-Files Repository -In the main worktree, `.plans` is a symlink into a clone of the team plans repository (folder `{{ADMIN_REPOSITORY_NAME}}/`). Plans are shared with the team through that repository and are never committed in this one. +In the main worktree, `.plans` is a symlink into a clone of the work-files repository (folder `{{ADMIN_REPOSITORY_NAME}}/`). The work files are shared with the team through that repository and never committed in this one. After every change in `.plans/`, run `alignfirst sync`. diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md index fad209eb..3b0ef11d 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md @@ -9,7 +9,7 @@ This repository holds the configuration of `{{SERVER_HOST}}`: runbooks under `do - `scripts/workspace/` — the portless workspace wrapper. - `.reports/` — one journal per operator task, committed. -- `.plans/` — task plans. Symlinked across worktrees, and into a clone of the team plans repository so plans are shared with the team. Run `alignfirst sync` after changing anything under it. +- `.plans/` — work files. Symlinked across worktrees, and into a clone of the work-files repository so the team shares them. Run `alignfirst sync` after changing anything under it. - `.local/`, `.local-wt/` — shared notes and per-worktree state, gitignored. @@ -32,5 +32,5 @@ Run `npm run workspace -- --guide` for the procedures. | `npm run workspace -- ` | Manage worktree workspaces (`--guide` for the procedures) | | `npm run validate` | docmap check and a syntax check of the wrapper | -| `alignfirst sync` | Publish and retrieve the task plans (`.plans`) | +| `alignfirst sync` | Publish and retrieve the work files (`.plans`) | diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md index 026f89a5..0934b9b9 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md @@ -45,11 +45,11 @@ npm install ``` -## Team plans repository +## Work-files repository -`.plans` is a symlink into `~/projects/{{PLANS_CLONE_NAME}}/{{ADMIN_REPOSITORY_NAME}}`. Clone the team plans repository once with the operator's credentials. +`.plans` is a symlink into `~/projects/{{PLANS_CLONE_NAME}}/{{ADMIN_REPOSITORY_NAME}}`. Clone the work-files repository once with the operator's credentials. -> **User action required.** Enable the deploy key on the plans repository too, with write access: `alignfirst sync` pushes. A key enabled read-only clones fine and fails on the first push with `This deploy key does not have write access`. +> **User action required.** Enable the deploy key on the work-files repository too, with write access: `alignfirst sync` pushes. A key enabled read-only clones fine and fails on the first push with `This deploy key does not have write access`. ```sh mkdir -p ~/projects @@ -78,5 +78,5 @@ Continue with [03-toolchain.md](03-toolchain.md). Delete the deploy key on the git host, then repeat [Deploy key](#deploy-key); the alias keeps pointing at the regenerated file. -Then enable it again on the plans repository ([Team plans repository](#team-plans-repository)). +Then enable it again on the work-files repository ([Work-files repository](#work-files-repository)). diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/operations/add-project.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/operations/add-project.md index 32917338..01b96f4d 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/operations/add-project.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/operations/add-project.md @@ -25,9 +25,9 @@ It installs the CLI prerequisite and skills, writes `.alignfirst.json`, and conf workspace, and `DEVELOPERS.md`. -## Team plans +## Work-files repository -The service account keeps its plans clone beside the projects. Clone it with the service account's +The service account keeps its work-files clone beside the projects. Clone it with the service account's credentials when it is missing: ```sh @@ -38,7 +38,7 @@ fi ' ``` -From the new project root, link the plans folder configured in `.alignfirst.json`: +From the new project root, link the work-files folder configured in `.alignfirst.json`: ```sh sudo -H -u {{SERVICE_USER}} bash -lc ' diff --git a/skills/alignfirst-setup-guide/references/alignfirst-developer.md b/skills/alignfirst-setup-guide/references/alignfirst-developer.md index d54e055f..95142ca4 100644 --- a/skills/alignfirst-setup-guide/references/alignfirst-developer.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-developer.md @@ -7,7 +7,7 @@ An AlignFirst Developer is a dedicated Linux service account that receives work Three roles, named as the runbooks name them: - **Support** — a coding-agent session on a laptop. Edits the admin repository, never executes on the server. -- **Operator** — a coding-agent session in the admin account `{{SERVER_ADMIN_USER}}` (sudo) on `{{SERVER_HOST}}`. Edits and executes. Holds the admin repository at `~{{SERVER_ADMIN_USER}}/{{ADMIN_REPOSITORY_NAME}}` and, with team plans, the plans clone under `~/projects`. Root steps are the operator's, through `sudo`. +- **Operator** — a coding-agent session in the admin account `{{SERVER_ADMIN_USER}}` (sudo) on `{{SERVER_HOST}}`. Edits and executes. Holds the admin repository at `~{{SERVER_ADMIN_USER}}/{{ADMIN_REPOSITORY_NAME}}` and, with a work-files repository, its clone under `~/projects`. Root steps are the operator's, through `sudo`. - **Service account** — `{{SERVICE_USER}}`, no sudo, no inbound SSH, reached with `sudo -i -u {{SERVICE_USER}} -- ` (or `sudo -H -u {{SERVICE_USER}} bash -lc '…'` when the command defines a variable). Runs OpenClaw on fixed system Node, and runs the coding agent, `alignfirst`, `alcode`, rootless podman and managed projects in fnm developer shells. The service account never reads the admin repository. It works from a snapshot at `~{{SERVICE_USER}}/seed/`, an `rsync` of `infra/openclaw/` with `.env` included, refreshed by the root-owned maintenance wrapper before every protected change. The wrapper contains the service account, unlocks only named scopes, runs one command as that account, and restores hardening through an exit trap. From there: @@ -19,7 +19,7 @@ The service account never reads the admin repository. It works from a snapshot a - `~/.npm-system-global/` — protected OpenClaw, coding-agent and admin CLI packages. - `~/.config/environment.d/` — the non-secret variables `systemd --user` injects into the gateway and `~/.bash_profile` sources for login shells. - The gateway unit, written by `openclaw gateway install`, enabled under lingering. -- `~/projects` — the managed projects, their `.alignfirst-projects.json` marker and, with team plans, the service account's own clone of the plans repository (a repository, never a project). +- `~/projects` — the managed projects, their `.alignfirst-projects.json` marker and, with a work-files repository, the service account's own clone of it (a repository, never a project). Both accounts install the same selected coding agent. The admin account uses it as the operator with the project-local `sysadmin` skill; the service account uses it through `alcode`. @@ -31,7 +31,7 @@ The human performs every interactive authentication and secret entry. Credential - **Surface**: `slack` or `discord`. - **Coding agent**: `claude-code` or `codex`. -- **Team plans repository**: yes or no. Yes when the team has one (see [plans-setup.md](plans-setup.md)). +- **Work-files repository**: yes or no. Yes when the team has one (see [plans-setup.md](plans-setup.md)). - **Dev-server gateway**: yes or no, default yes. Skipping is not recommended: without the gateway there are no remote dev URLs, and `workspace setup --profile remote` is unusable in the managed projects. Choose the model provider and model separately; the template favors no provider. @@ -44,7 +44,7 @@ The agent **runtime** is fixed: every AlignFirst Developer uses OpenClaw's embed | Token | Supplied by | Used by | | --- | --- | --- | -| `{{ADMIN_REPOSITORY_NAME}}` | Operator | `package.json`, root files, `~/{{ADMIN_REPOSITORY_NAME}}` in the runbooks, the team plans folder | +| `{{ADMIN_REPOSITORY_NAME}}` | Operator | `package.json`, root files, `~/{{ADMIN_REPOSITORY_NAME}}` in the runbooks, the work-files folder | | `{{ADMIN_REPOSITORY_URL}}` | Operator or git host | `02` (deploy key) | | `{{SERVER_HOST}}` | Server administrator | hostname (`01`), deploy-key alias (`02`), overview, workspace files | | `{{SERVER_ADMIN_USER}}` | Server administrator | admin account (`01`), operator commands, hardening ownership | @@ -56,8 +56,8 @@ The agent **runtime** is fixed: every AlignFirst Developer uses OpenClaw's embed | `{{TEAM_NAME}}` | Operator | README, `IDENTITY.md`, `SOUL.md`, `USER.md` | | `{{TEAM_MEMBERS}}` | Operator | `USER.md` | | `{{PORT_RANGE_FIRST}}`, `{{PORT_RANGE_LAST}}` | Operator (suggested 28000–28599) | `.alignfirst-projects.json`, overview, workspace `AGENTS.md`, `09` | -| `{{PLANS_REPOSITORY_URL}}` | Operator, team plans only | `02`, `add-project.md` | -| `{{PLANS_CLONE_NAME}}` | Operator, team plans only | `common.conf`, `02`, `add-project.md` | +| `{{PLANS_REPOSITORY_URL}}` | Operator, work-files repository only | `02`, `add-project.md` | +| `{{PLANS_CLONE_NAME}}` | Operator, work-files repository only | `common.conf`, `02`, `add-project.md` | | `{{PLANS_CLONE_NOTE}}` | Derived | projects marker | | `{{SLACK_OWNER_ID}}`, `{{SLACK_CHANNEL_ID}}` | Slack administrator | `.env.example` (Slack overlay) | | `{{DISCORD_OWNER_ID}}`, `{{DISCORD_GUILD_ID}}`, `{{DISCORD_CHANNEL_ID}}` | Discord administrator | `.env.example` (Discord overlay) | @@ -75,7 +75,7 @@ The channel IDs are known before the bot exists (the channel, the server and the The last two rows exist only when the gateway option is on. `{{PORT_RANGE_REGEX}}` is a regex matching exactly the integers `PORT_RANGE_FIRST..PORT_RANGE_LAST`: one digit class per position when the range allows it (`28000..28599` → `28[0-5][0-9]{2}`), otherwise an alternation of such classes (`6500..7700` → `6[5-9][0-9]{2}|7[0-6][0-9]{2}|7700`). `{{DEV_DOMAIN_REGEX}}` is `DEV_DOMAIN` with every `.` escaped as `\.`. -With team plans, collect `{{PLANS_REPOSITORY_URL}}` and `{{PLANS_CLONE_NAME}}` at render time. Both the operator and the service account clone it under `~/projects`; each account supplies its own credentials. `{{PLANS_CLONE_NOTE}}` is then a space followed by `The plans clone at ~/projects/{{PLANS_CLONE_NAME}} is a repository, not a project.`; without team plans it is empty. +With a work-files repository, collect `{{PLANS_REPOSITORY_URL}}` and `{{PLANS_CLONE_NAME}}` at render time. Both the operator and the service account clone it under `~/projects`; each account supplies its own credentials. `{{PLANS_CLONE_NOTE}}` is then a space followed by `The work-files clone at ~/projects/{{PLANS_CLONE_NAME}} is a repository, not a project.`; without one it is empty. ## Assemble the Admin Repository @@ -95,7 +95,7 @@ On the operator's machine, from the installed skill directory: grep -rlE "$re" . | while read -r f; do awk -v re="$re" '$0 ~ re { skip = !skip; next } !skip' "$f" | cat -s > "$f.tmp" && cat "$f.tmp" > "$f" && rm "$f.tmp"; done ``` -6. Team plans on: delete the `TEAM_PLANS_SECTION` marker lines. The guarded block in `.alignfirst.json` writes `plans.folder` as `{{ADMIN_REPOSITORY_NAME}}`. Off: delete each block, including that field. +6. Work-files repository on: delete the `TEAM_PLANS_SECTION` marker lines. The guarded block in `.alignfirst.json` writes `plans.folder` as `{{ADMIN_REPOSITORY_NAME}}`. Off: delete each block, including that field. 7. Replace every `{{TOKEN}}`, after all overlays are present and the derived tokens are computed. `sed` handles single-line values; the member list needs the editor or a Node one-liner. Dotfiles (`.env.example`, `.alignfirst.json`, `.alignfirst-projects.json`) are part of the sweep. 8. `npm install`. 9. Install `sysadmin` project-locally, so the clone carries it: `npx -y skills add https://github.com/paleo/skills --yes --agent --skill sysadmin **User action required.**`. Execution order: 1. `01-server-setup.md` — **human administrator**, on the fresh server: admin account, SSH key-only, firewall, Node, podman. Ends with the coding agent installed and logged in for the admin account, then a session of that agent in the clone takes over as the **operator**. -2. `02-admin-repository.md` — operator: deploy key (human registers it), clone, plans clone and `alignfirst plans setup` when enabled, `workspace setup`. +2. `02-admin-repository.md` — operator: deploy key (human registers it), clone, work-files clone and `alignfirst plans setup` when enabled, `workspace setup`. 3. `03-toolchain.md` — service account created, npm prefix, the CLIs, the coding agent, git access (human: key registration or device code). 4. `05-openclaw-dependencies.md` — OS packages for the tools, git-host CLIs and their authentication (human), Chromium and its AppArmor user-namespace profile. 5. `07-channel.md`, platform part — **channel administrator** creates the app and collects the tokens and IDs for `.env`. diff --git a/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md b/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md index b4cd0284..251d2356 100644 --- a/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md @@ -87,7 +87,7 @@ _Default branch:_ `{DETECTED_DEFAULT_BRANCH}` _Ticket ID format:_ `{DETECTED_TICKET_FORMAT}` ``` -Omit any convention that repository evidence cannot establish. When the project uses a team plans +Omit any convention that repository evidence cannot establish. When the project uses a work-files repository, add: After every change in `.plans/`, run `npx alignfirst sync`. Add `--skill alignfirst` to the skills command above, since no bootstrap section describes the protocols. @@ -125,7 +125,7 @@ possible: ```markdown ## 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. ``` ### Local installation @@ -136,7 +136,7 @@ Add the exact current `alignfirst` version as a dev dependency with the project The CLI brings `@paleo/docmap`, `arktype` and `semver` into the project's dependency graph. A repository with such gates must allow the transitive `@paleo/docmap` too, since the CLI tracks its releases closely. Where `arktype` is an optional peer of an existing dependency, expect the lockfile to record it as one. -Continue with [plans-setup.md](plans-setup.md) when the team has a plans repository. Finish with: +Continue with [plans-setup.md](plans-setup.md) when the team has a work-files repository. Finish with: ```sh npx alignfirst config diff --git a/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md index 78e062a0..da463840 100644 --- a/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md @@ -28,7 +28,7 @@ Remove only known AlignFirst command files from existing project directories: These paths are legacy cleanup targets, not current installation locations. -## Migrate Plans and Instructions +## Migrate Work Files and Instructions 1. If only `_plans/` exists, rename it to `.plans/`. If both exist, move `_plans/` to `.plans/_plans-archives/`. Remove `.plans/.gitkeep`. diff --git a/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v3.md b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v3.md index 62ba08bc..70ec582c 100644 --- a/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v3.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v3.md @@ -1,7 +1,7 @@ # Upgrade from AlignFirst v3 -Replace the full-content v3 skills and plans package with the AlignFirst CLI and v4 stub skills. The -migration leaves no plans compatibility package or npm-script wrappers. +Replace the full-content v3 skills and `plans-share` package with the AlignFirst CLI and v4 stub skills. The +migration leaves no `plans-share` compatibility package or npm-script wrappers. ## Install the CLI @@ -19,7 +19,7 @@ An AlignFirst Developer host installs the CLI globally and replaces the retired npm install -g alignfirst @paleo/alcode @paleo/alproject ``` -## Inventory the Plans Contract +## Inventory the `plans-share` Contract Before removing anything, search the whole repository, excluding dependencies and generated output, for: @@ -30,11 +30,11 @@ for: - setup, sync, check, archive, and auto-archive calls. Include documentation, CI, package scripts, shell scripts, hooks, deployment files, and automation. -Record the plans folder and whether synchronization uses `--auto-archive`. +Record the work-files folder and whether synchronization uses `--auto-archive`. -Recover the plans folder from the static `--folder` value in the old setup script or another setup +Recover the work-files folder from the static `--folder` value in the old setup script or another setup call. If that value is absent or dynamic and `.plans` is a symlink, resolve its target and use the -target directory's basename after verifying that its parent is the plans repository clone. Use an +target directory's basename after verifying that its parent is the work-files repository clone. Use an existing `plans.folder` when it agrees. Ask the user when these sources are missing or conflict. Keep `.plans` unchanged; an existing symlink remains valid. @@ -55,7 +55,7 @@ npm pkg delete scripts.plans:setup scripts.plans:sync Detect the ticket pattern from the repository's branch and ticket conventions. Issue-number tickets use `^\d+$`; Jira-like keys use `^[A-Z]+-\d+$`; omit the field when there is no convention. -Write `.alignfirst.json` by hand. Preserve the recovered plans folder. When the old synchronization +Write `.alignfirst.json` by hand. Preserve the recovered work-files folder. When the old synchronization path used `--auto-archive`, preserve that behavior with `plans.autoArchive: true`: ```json @@ -67,7 +67,7 @@ path used `--auto-archive`, preserve that behavior with `plans.autoArchive: true } ``` -Keep only applicable plans fields. `plans.autoArchive` works in local mode without `plans.folder`. +Keep only applicable `plans` fields. `plans.autoArchive` works in local mode without `plans.folder`. Add `portRange` when the workspace wrapper declares a port scheme. Update the README guidance and install the stubs. @@ -118,7 +118,7 @@ npx -y skills update --global --yes ``` Use `--project` instead of `--global` for a project-local installation. Finish by checking the -effective project and the plans workflow: +effective project and the work-files link: ```sh npx alignfirst config diff --git a/skills/alignfirst-setup-guide/references/alignfirst-upgrade.md b/skills/alignfirst-setup-guide/references/alignfirst-upgrade.md index fbac1bc4..2322f708 100644 --- a/skills/alignfirst-setup-guide/references/alignfirst-upgrade.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-upgrade.md @@ -33,7 +33,7 @@ generated output. - v1: follow [alignfirst-upgrade-from-v1.md](alignfirst-upgrade-from-v1.md). - v2: follow [alignfirst-upgrade-from-v2.md](alignfirst-upgrade-from-v2.md). - v3: follow [alignfirst-upgrade-from-v3.md](alignfirst-upgrade-from-v3.md). -- v4: keep the current skills. When legacy plans artifacts remain, apply the cleanup, command sweep, +- v4: keep the current skills. When legacy `plans-share` artifacts remain, apply the cleanup, command sweep, and verification sections of the v3 upgrade. - No detected installation: use [alignfirst-skills-setup.md](alignfirst-skills-setup.md). diff --git a/skills/alignfirst-setup-guide/references/docmap-setup.md b/skills/alignfirst-setup-guide/references/docmap-setup.md index 978b9e14..b68e0fd5 100644 --- a/skills/alignfirst-setup-guide/references/docmap-setup.md +++ b/skills/alignfirst-setup-guide/references/docmap-setup.md @@ -15,7 +15,7 @@ Use this form when the project already requires the AlignFirst CLI. It adds no p ```markdown ## 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. ``` 4. Read the authoring guide with `npx alignfirst docmap --guide`. diff --git a/skills/alignfirst-setup-guide/references/plans-setup.md b/skills/alignfirst-setup-guide/references/plans-setup.md index d82f06ef..b6e99ac8 100644 --- a/skills/alignfirst-setup-guide/references/plans-setup.md +++ b/skills/alignfirst-setup-guide/references/plans-setup.md @@ -1,10 +1,10 @@ -# Team Plans Repository Setup +# Work-Files Repository Setup -Share `.plans/` through a dedicated team repository. Solo users keep `.plans/` as a local directory. +Share `.plans/` through a dedicated team repository, the work-files repository. Solo users keep `.plans/` as a local directory. ## How It Works -The team hosts one private, multi-project plans repository: +The team hosts one private, multi-project work-files repository: ```text myteam-plans/ @@ -21,7 +21,7 @@ choose, typically beside the code repositories. In a configured project, `.plans the folder named by `plans.folder` in `.alignfirst.json`. Linked worktrees continue through the main worktree's symlink. -A contributor without access to the plans repository uses a plain `.plans` directory. The CLI +A contributor without access to the work-files repository uses a plain `.plans` directory. The CLI accepts both modes. Run `npx alignfirst plans check` to report the current mode. `npx alignfirst sync` publishes changes. Set `plans.autoArchive` to `true` in `.alignfirst.json` to archive @@ -50,11 +50,11 @@ With the `alignfirst context` bootstrap section, the CLI delivers the sync instr > After every change in `.plans/`, run `npx alignfirst sync`. For a project prepared for an AlignFirst Developer, the `.plans/` entry in `DEVELOPERS.md` also -names the shared repository and the sync command. +names the work-files repository and the sync command. ## Configure Each Machine -Clone the plans repository with the developer's own credentials. From the project root, link it: +Clone the work-files repository with the developer's own credentials. From the project root, link it: ```sh git clone ../myteam-plans @@ -86,7 +86,7 @@ preSetup: ({ isMainWorktree, currentWorktree }) => { ``` Keep the `isMainWorktree` gate. `preSetup` runs before the kernel creates shared-directory symlinks, -so a fresh linked worktree has no `.plans` yet. The check accepts a usable plans symlink and a local +so a fresh linked worktree has no `.plans` yet. The check accepts a usable `.plans` symlink and a local directory. Document these new-machine steps in `README.md` before workspace setup: @@ -102,6 +102,6 @@ For a public repository, make local mode the default and avoid naming a private ```sh npm install -mkdir .plans # or run npx alignfirst plans setup with the team plans clone +mkdir .plans # or run npx alignfirst plans setup with the work-files clone npm run workspace -- setup ``` diff --git a/skills/alignfirst-setup-guide/references/workspace-setup.md b/skills/alignfirst-setup-guide/references/workspace-setup.md index ef51a219..e7e3e48c 100644 --- a/skills/alignfirst-setup-guide/references/workspace-setup.md +++ b/skills/alignfirst-setup-guide/references/workspace-setup.md @@ -38,7 +38,7 @@ Apply in order. The per-project decisions live in the [checklist](#checklist); t For each gitignored directory, decide: **shared** across worktrees, or **isolated** per worktree? -- **Shared** directories are symlinked to the main worktree — things that should be the same everywhere: personal notes, task plans. +- **Shared** directories are symlinked to the main worktree — things that should be the same everywhere: personal notes, work files. - **Per-worktree** directories are created fresh in each worktree — things that must differ: databases, caches, logs, Docker volumes. | Directory | Kind | Contents | @@ -53,7 +53,7 @@ Suggest `.local/` by default, even when the repo has no such directory yet: a gi `runtimeDir` (`.local-wt/` above) stays per-worktree, but the kernel symlinks its `workspace-registry/` sub-directory to the main worktree's, so every worktree reads one registry. -The main worktree's `.plans` may itself be a symlink — into a clone of a team plans repository (see [plans-setup.md](plans-setup.md)); the symlink chain resolves on its own. +The main worktree's `.plans` may itself be a symlink — into a clone of the work-files repository (see [plans-setup.md](plans-setup.md)); the symlink chain resolves on its own. ### Contiguous port scheme @@ -125,7 +125,7 @@ Builds a `WorkspaceConfig` and calls `runWorkspace`. Key fields: - `ports` — optional group: `base` (first port of the main worktree's block), `maxWorkspaces` (main included, required), `perWorkspace` (defaults to `names.length`; required with `compute`), and exactly one of `names` (consecutive ports from `firstPort`) or `compute({ index, firstPort })` (full control; computed ports must stay within the block). Omit the whole group for [portless mode](#portless-mode). See [The workspace registry](#the-workspace-registry). - `sharedDirs` (symlinked from main), `runtimeDir` (per-worktree; holds logs and the registry). - `gitignoredFiles: Array<{ path, source, patch?, optional? }>` — one entry per gitignored file (see above). `source` (required) is `{ kind: "mainWorktree", fallback? }`, `{ kind: "committed", path }`, or `{ kind: "content", content }`. Functional `content(ctx)` and `patch(content, ctx)` receive `{ name, ports, mainWorktree, currentWorktree, isMainWorktree }`; omit `patch` to copy verbatim. -- `preSetup({ name, isMainWorktree, currentWorktree, mainWorktree, force, profile?, log })` — optional; runs **before** `gitignoredFiles` are copied. Use it for work outside file-source resolution, such as checking the plans clone with `npx alignfirst plans check`, creating directories, or configuring git hooks. **MUST be idempotent**; on a linked-worktree setup it MUST NOT mutate the main worktree. Omit the hook only when it has no remaining work. `profile` is set only during `setup --profile `: check the profile's external requirements here (an environment variable, a reachable host) to fail before any file is written. +- `preSetup({ name, isMainWorktree, currentWorktree, mainWorktree, force, profile?, log })` — optional; runs **before** `gitignoredFiles` are copied. Use it for work outside file-source resolution, such as checking the work-files link with `npx alignfirst plans check`, creating directories, or configuring git hooks. **MUST be idempotent**; on a linked-worktree setup it MUST NOT mutate the main worktree. Omit the hook only when it has no remaining work. `profile` is set only during `setup --profile `: check the profile's external requirements here (an environment variable, a reachable host) to fail before any file is written. - `setupProfiles: { : { description, apply } }` — optional; enables `setup --profile `. The kernel checks the name and lists each `description` (one line) in `--help` and `--guide`. `apply({ name, ports, currentWorktree, mainWorktree, isMainWorktree, log })` runs on the **main worktree only**, after `gitignoredFiles` are seeded, and rewrites the ignored files for that environment. The profile rewrites the ignored main files once; linked worktrees inherit them through `mainWorktree` sources, so patchers stay profile-agnostic. Check every computed change before the first write, leave unrelated files untouched, and **MUST be idempotent** — reapplying the same profile produces the same files. - `finalizeWorkspace(ctx)` — the detached background step: infrastructure startup, DB readiness wait, install / build, migrations, seed. `ctx` carries `name`, `ports`, `branch`, `currentWorktree`, `mainWorktree`, `isMainWorktree`, `force`, and `progress(label)`. **MUST be idempotent** — `workspace setup` is the documented retry path and re-runs it; idempotency also covers a name reused after an orphan (force-remove the stale container named after the workspace before `up`). **Run `npm install` first**, so any later failure still leaves usable `node_modules/` for the retry to import `@paleo/workspace`. May `return { purgeData }` — an opaque blob persisted on the registry entry and handed to `purgeInfrastructure`; use it **only** for teardown identifiers you can't re-derive at purge time (deterministic container / volume names come from `name` + paths, so they don't go here). - `purgeInfrastructure(ctx)` — optional destructive teardown (typically `docker compose down -v`). Runs on `workspace remove`, `prune`, and orphan removal. **MUST be idempotent and cwd-independent**: `ctx.worktree` may be gone (orphan), so branch on its presence and tear down *by name* in that case — derive names from `ctx.name` / `ctx.worktree` / `ctx.mainWorktree`, and read `ctx.purgeData` for non-derivable ids. Swallow errors. From aa6969db750e79ab43cb5685093ab6742f343237 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 08:25:42 +0200 Subject: [PATCH 4/8] chore: skill version --- skills/alignfirst-setup-guide/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/alignfirst-setup-guide/SKILL.md b/skills/alignfirst-setup-guide/SKILL.md index e9cfc66c..8ab92e6d 100644 --- a/skills/alignfirst-setup-guide/SKILL.md +++ b/skills/alignfirst-setup-guide/SKILL.md @@ -6,7 +6,7 @@ description: >- license: CC0 1.0 metadata: author: Paleo - version: "0.37.3" + version: "0.38.0" repository: https://github.com/paleo/alignfirst --- From 4c28a4ed7560b4fe5fceb550497f4f38c1db4db2 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 08:43:26 +0200 Subject: [PATCH 5/8] fix: create the verify environment, name the failing git subcommand --- .changeset/alignfirst-git-failure-messages.md | 2 +- .github/workflows/release.yml | 20 +++++++++- docs/releasing.md | 18 ++++++--- packages/alignfirst/src/git.ts | 12 +++++- packages/alignfirst/src/plans/conflicts.ts | 6 +-- packages/alignfirst/src/plans/rebase.ts | 2 +- packages/alignfirst/test/git.test.ts | 40 +++++++++++++++++++ 7 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 packages/alignfirst/test/git.test.ts diff --git a/.changeset/alignfirst-git-failure-messages.md b/.changeset/alignfirst-git-failure-messages.md index a3a2ab5b..e9916736 100644 --- a/.changeset/alignfirst-git-failure-messages.md +++ b/.changeset/alignfirst-git-failure-messages.md @@ -2,4 +2,4 @@ "alignfirst": patch --- -A failing git command now reports git's own message instead of pointing at output that was never printed. +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. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0321d6c3..5bd73016 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: @@ -72,15 +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 wait timer: the registry serves a stale - # packument for several minutes after a publish. No runner during the wait. + # 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 diff --git a/docs/releasing.md b/docs/releasing.md index 8cab90f1..9e39fd58 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -78,13 +78,21 @@ Done on 2026-08-22. Requires the package owner's npm account and repository admi gh api -X POST repos/paleo/alignfirst/environments/release/deployment-branch-policies -f name=main ``` -3. Create the `verify` environment. Its only purpose is the wait timer, so it carries no reviewer and no branch policy: +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`. - ```bash - gh api -X PUT repos/paleo/alignfirst/environments/verify -F wait_timer=15 - ``` +## 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 +``` -4. 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`. +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 diff --git a/packages/alignfirst/src/git.ts b/packages/alignfirst/src/git.ts index 2e0b778f..d7ac4b69 100644 --- a/packages/alignfirst/src/git.ts +++ b/packages/alignfirst/src/git.ts @@ -15,9 +15,17 @@ export function git(streams: Streams, dir: string, ...args: string[]): void { function gitFailure(args: string[], detail?: string): CliError { const output = detail?.trim(); + const subcommand = gitSubcommand(args); + const label = subcommand === undefined ? "git command" : `git ${subcommand}`; if (output === undefined || output === "") - return new CliError(`git ${args[0]} failed. See the git output above.`); - return new CliError(`git ${args[0]} failed:\n${output}`); + return new CliError(`${label} failed. See the git output above.`); + return new CliError(`${label} failed:\n${output}`); +} + +function gitSubcommand(args: string[]): string | undefined { + let index = 0; + while (args[index] === "-c") index += 2; + return args[index]; } export function assertMainWorktreeRoot(cwd: string): void { diff --git a/packages/alignfirst/src/plans/conflicts.ts b/packages/alignfirst/src/plans/conflicts.ts index e5cec2cc..c92006a3 100644 --- a/packages/alignfirst/src/plans/conflicts.ts +++ b/packages/alignfirst/src/plans/conflicts.ts @@ -10,7 +10,7 @@ import { import { basename, dirname, extname, join, relative, resolve } from "node:path"; import { CliError } from "../cli-error.js"; -import type { Output } from "../context.js"; +import type { Streams } from "../context.js"; import { gitBuffer, gitOutput, gitOutputRaw } from "../git.js"; import { nextFilePosition } from "./ticket.js"; @@ -30,7 +30,7 @@ interface Blob { mode: string; } -export function resolveConflictedPaths(repoDir: string, stdout: Output): void { +export function resolveConflictedPaths(streams: Streams, repoDir: string): void { const conflicts = readConflicts(repoDir); const remote = readTree(repoDir, "HEAD"); const local = readTree(repoDir, "REBASE_HEAD"); @@ -58,7 +58,7 @@ export function resolveConflictedPaths(repoDir: string, stdout: Output): void { applyResolution(repoDir, conflicts, resolution); for (const path of conflicts.keys()) { const renamed = resolution.renames.get(path); - stdout.write( + streams.stdout.write( renamed === undefined ? `Resolved ${path}: preserved committed contents at the surviving paths.\n` : `Resolved ${path}: kept the published version; saved the local version as ${renamed}.\n`, diff --git a/packages/alignfirst/src/plans/rebase.ts b/packages/alignfirst/src/plans/rebase.ts index 64413eb7..8d2c59d2 100644 --- a/packages/alignfirst/src/plans/rebase.ts +++ b/packages/alignfirst/src/plans/rebase.ts @@ -30,7 +30,7 @@ export function resolveStoppedRebase(streams: Streams, repoDir: string): void { for (let step = 0; findStoppedRebase(repoDir) !== undefined; ++step) { if (step >= MAX_REBASE_STEPS) throw new CliError(`Could not finish the stopped rebase in ${repoDir}.`); - resolveConflictedPaths(repoDir, streams.stdout); + resolveConflictedPaths(streams, repoDir); git(streams, repoDir, "add", "-A"); continueRebase(streams, repoDir); } diff --git a/packages/alignfirst/test/git.test.ts b/packages/alignfirst/test/git.test.ts new file mode 100644 index 00000000..a134e08c --- /dev/null +++ b/packages/alignfirst/test/git.test.ts @@ -0,0 +1,40 @@ +import { rmSync } from "node:fs"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { git as runGit } from "../src/git.js"; +import { configureGit, git, makeSink, makeTempDir, runMain } from "./helpers.js"; + +const dirs: string[] = []; + +afterEach(() => { + for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +describe("git failures", () => { + it("carries git's own message when nothing was printed", async () => { + const dir = makeFixture(); + const result = await runMain(["plans", "setup", "clone", "--folder", "product"], { cwd: dir }); + expect(result.code).toBe(1); + expect(result.stderr).toContain("git rev-parse failed:"); + expect(result.stderr).toContain("not a git repository"); + }); + + it("writes git output to the streams and names the subcommand past global options", () => { + const dir = makeFixture(); + git(dir, "init", "--quiet"); + const stdout = makeSink(); + const stderr = makeSink(); + expect(() => + runGit({ stdout, stderr }, dir, "-c", "core.editor=true", "rebase", "--continue"), + ).toThrow("git rebase failed. See the git output above."); + expect(stderr.text()).toContain("no rebase in progress"); + }); +}); + +function makeFixture(): string { + const dir = makeTempDir("alignfirst-git-"); + dirs.push(dir); + configureGit(dir); + return dir; +} From a6ba3c90041be919635bdac5edaafdac519ab43e Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 09:52:40 +0200 Subject: [PATCH 6/8] fix: stream git command output --- packages/alignfirst/src/cli.ts | 3 ++- packages/alignfirst/src/commands/sync.ts | 16 ++++++++-------- packages/alignfirst/src/git.ts | 20 +++++++++++++------- packages/alignfirst/src/plans/rebase.ts | 12 ++++++------ packages/alignfirst/test/git.test.ts | 23 +++++++++++++++++++---- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/alignfirst/src/cli.ts b/packages/alignfirst/src/cli.ts index c1bbff60..99708e6b 100644 --- a/packages/alignfirst/src/cli.ts +++ b/packages/alignfirst/src/cli.ts @@ -51,7 +51,8 @@ export async function main(options?: MainOptions): Promise { 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`); diff --git a/packages/alignfirst/src/commands/sync.ts b/packages/alignfirst/src/commands/sync.ts index 5fa5fd96..cb212a70 100644 --- a/packages/alignfirst/src/commands/sync.ts +++ b/packages/alignfirst/src/commands/sync.ts @@ -9,7 +9,7 @@ import { archiveThresholdDays, autoArchive } from "../plans/archive.js"; import { resolvePlansMode } from "../plans/mode.js"; import { findStoppedRebase, renderStoppedRebase, resolveStoppedRebase } from "../plans/rebase.js"; -export function runSync(ctx: CommandContext, args: string[]): number { +export async function runSync(ctx: CommandContext, args: string[]): Promise { const usage = `Usage: ${ctx.form} sync [--auto-archive | --no-auto-archive]\n`; const options = parseSyncArgs(ctx, args, usage); if (options === undefined) return 0; @@ -29,24 +29,24 @@ export function runSync(ctx: CommandContext, args: string[]): number { } const repoDir = mode.repoToplevel; assertNoStoppedRebase(repoDir, ctx.form); - git(ctx, repoDir, "add", "-A"); - if (hasStagedChanges(repoDir)) git(ctx, repoDir, "commit", "--quiet", "-m", "sync"); + await git(ctx, repoDir, "add", "-A"); + if (hasStagedChanges(repoDir)) await git(ctx, repoDir, "commit", "--quiet", "-m", "sync"); if (hasUpstream(repoDir)) { try { - git(ctx, repoDir, "pull", "--rebase"); + await git(ctx, repoDir, "pull", "--rebase"); } catch { if (findStoppedRebase(repoDir) === undefined) throw new CliError("git pull failed. See the git output above."); - resolveStoppedRebase(ctx, repoDir); + await resolveStoppedRebase(ctx, repoDir); } } if (thresholdDays !== undefined && autoArchive(plansDir, thresholdDays, ctx.stdout)) { - git(ctx, repoDir, "add", "-A"); - if (hasStagedChanges(repoDir)) git(ctx, repoDir, "commit", "--quiet", "-m", "sync"); + await git(ctx, repoDir, "add", "-A"); + if (hasStagedChanges(repoDir)) await git(ctx, repoDir, "commit", "--quiet", "-m", "sync"); } if (hasCommitsToSend(repoDir)) { try { - git(ctx, repoDir, "push", "--quiet", "-u", "origin", "HEAD"); + await git(ctx, repoDir, "push", "--quiet", "-u", "origin", "HEAD"); } catch { throw new CliError( `git push failed. See the git output above. Another synchronization may have landed first: run ${ctx.form} sync again.`, diff --git a/packages/alignfirst/src/git.ts b/packages/alignfirst/src/git.ts index d7ac4b69..09229d14 100644 --- a/packages/alignfirst/src/git.ts +++ b/packages/alignfirst/src/git.ts @@ -1,16 +1,22 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFileSync, spawn, spawnSync } from "node:child_process"; import { realpathSync } from "node:fs"; import { resolve } from "node:path"; import { CliError } from "./cli-error.js"; import type { Streams } from "./context.js"; -export function git(streams: Streams, dir: string, ...args: string[]): void { - const result = spawnSync("git", ["-C", dir, ...args], { encoding: "utf-8" }); - if (result.error !== undefined) throw gitFailure(args, result.error.message); - streams.stdout.write(result.stdout); - streams.stderr.write(result.stderr); - if (result.status !== 0) throw gitFailure(args); +export function git(streams: Streams, dir: string, ...args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn("git", ["-C", dir, ...args], { + stdio: ["inherit", "pipe", "pipe"], + }); + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (text: string) => streams.stdout.write(text)); + child.stderr.on("data", (text: string) => streams.stderr.write(text)); + child.once("error", (error) => reject(gitFailure(args, error.message))); + child.once("close", (code) => (code === 0 ? resolve() : reject(gitFailure(args)))); + }); } function gitFailure(args: string[], detail?: string): CliError { diff --git a/packages/alignfirst/src/plans/rebase.ts b/packages/alignfirst/src/plans/rebase.ts index 8d2c59d2..93d19877 100644 --- a/packages/alignfirst/src/plans/rebase.ts +++ b/packages/alignfirst/src/plans/rebase.ts @@ -26,25 +26,25 @@ export function findStoppedRebase(repoDir: string): StoppedRebase | undefined { }; } -export function resolveStoppedRebase(streams: Streams, repoDir: string): void { +export async function resolveStoppedRebase(streams: Streams, repoDir: string): Promise { for (let step = 0; findStoppedRebase(repoDir) !== undefined; ++step) { if (step >= MAX_REBASE_STEPS) throw new CliError(`Could not finish the stopped rebase in ${repoDir}.`); resolveConflictedPaths(streams, repoDir); - git(streams, repoDir, "add", "-A"); - continueRebase(streams, repoDir); + await git(streams, repoDir, "add", "-A"); + await continueRebase(streams, repoDir); } } -function continueRebase(streams: Streams, repoDir: string): void { +async function continueRebase(streams: Streams, repoDir: string): Promise { const args = ["-c", "core.editor=true", "rebase", "--continue"]; try { - git(streams, repoDir, ...args); + await git(streams, repoDir, ...args); } catch (error) { const stopped = findStoppedRebase(repoDir); if (stopped?.conflictedFiles.length) return; if (stopped && gitSucceeds(repoDir, "diff", "--cached", "--quiet")) { - git(streams, repoDir, "rebase", "--skip"); + await git(streams, repoDir, "rebase", "--skip"); return; } throw error; diff --git a/packages/alignfirst/test/git.test.ts b/packages/alignfirst/test/git.test.ts index a134e08c..857d1dbb 100644 --- a/packages/alignfirst/test/git.test.ts +++ b/packages/alignfirst/test/git.test.ts @@ -1,4 +1,5 @@ -import { rmSync } from "node:fs"; +import { rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -20,16 +21,30 @@ describe("git failures", () => { expect(result.stderr).toContain("not a git repository"); }); - it("writes git output to the streams and names the subcommand past global options", () => { + it("writes git output to the streams and names the subcommand past global options", async () => { const dir = makeFixture(); git(dir, "init", "--quiet"); const stdout = makeSink(); const stderr = makeSink(); - expect(() => + await expect( runGit({ stdout, stderr }, dir, "-c", "core.editor=true", "rebase", "--continue"), - ).toThrow("git rebase failed. See the git output above."); + ).rejects.toThrow("git rebase failed. See the git output above."); expect(stderr.text()).toContain("no rebase in progress"); }); + + it("streams output beyond the synchronous child-process buffer limit", async () => { + const dir = makeFixture(); + git(dir, "init", "--quiet"); + const contents = "x".repeat(2 * 1024 * 1024); + writeFileSync(join(dir, "large.txt"), contents); + git(dir, "add", "large.txt"); + git(dir, "commit", "--quiet", "-m", "large output"); + const stdout = makeSink(); + const stderr = makeSink(); + await runGit({ stdout, stderr }, dir, "show", "HEAD:large.txt"); + expect(stdout.text()).toBe(contents); + expect(stderr.text()).toBe(""); + }); }); function makeFixture(): string { From 88737b004327e2858d075af4f88274f7f11757a6 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 09:53:13 +0200 Subject: [PATCH 7/8] fix(setup-guide): apply the alignfirst context bootstrap to the developer template --- .../base/.alignfirst.json | 16 +++++++++---- .../base/AGENTS.md | 24 +++---------------- .../base/DEVELOPERS.md | 14 ++++------- .../base/README.md | 3 ++- .../docs/installations/02-admin-repository.md | 2 +- .../base/package.json | 4 ++-- .../base/scripts/workspace/workspace.mjs | 2 +- .../references/alignfirst-developer.md | 2 +- 8 files changed, 27 insertions(+), 40 deletions(-) diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/.alignfirst.json b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/.alignfirst.json index 6cafa32e..7a283aaa 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/.alignfirst.json +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/.alignfirst.json @@ -1,7 +1,15 @@ { "schemaVersion": 1, - // TEAM_PLANS_SECTION - "plans": { "folder": "{{ADMIN_REPOSITORY_NAME}}" }, - // TEAM_PLANS_SECTION - "ticketIdPattern": "^\\d+$" + "ticketIdPattern": "^\\d+$", + "plans": { + // TEAM_PLANS_SECTION + "folder": "{{ADMIN_REPOSITORY_NAME}}", + // TEAM_PLANS_SECTION + "autoArchive": true + }, + "git": { + "defaultBranch": "main", + "commit": { "style": "conventionalCommit" }, + "agentCoauthoring": false + } } diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md index 5139c789..d347f146 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/AGENTS.md @@ -2,7 +2,9 @@ This repository documents and operates `{{SERVER_HOST}}`, the server that runs **{{DEVELOPER_NAME}}**, an AlignFirst Developer. Every configuration step is a runbook under `docs/installations/`, so the server can be rebuilt from scratch. -Always ignore the `.plans`, `.local` and `.local-wt` directories when searching the codebase. +## 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, work files), the index of documentation, and the AlignFirst protocols. ## Sysadmin workflow @@ -18,26 +20,6 @@ Repository specifics: - Runbooks match `docs/installations/01-server-setup.md`: one short line of prose, then a fenced code block. - `.reports/` is committed. -## Docmap - Seek Documentation - -*Before* any investigation or code exploration, run `alignfirst docmap`, then read the relevant documentation. Mandatory for every task. - -Always read `docs/overview.md`. - -## AlignFirst - Commit Message and Default Branch - -_Commit message convention:_ Conventional Commits with a very short subject, e.g. `docs: tighten 04 seed section`. No body unless the change needs one. Do not mention the ticket ID. - -_Default branch:_ `main`. - - -### Work-Files Repository - -In the main worktree, `.plans` is a symlink into a clone of the work-files repository (folder `{{ADMIN_REPOSITORY_NAME}}/`). The work files are shared with the team through that repository and never committed in this one. - -After every change in `.plans/`, run `alignfirst sync`. - - ## Workspaces A **workspace** is a git worktree (with its branch) plus its own dev setup: symlinked shared directories and seeded config files. Workspaces are isolated, so you can work on several branches in parallel. This repository has no dev server, so the system runs portless: nothing to start, no `dev` script. diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md index 3b0ef11d..4f2218cc 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/DEVELOPERS.md @@ -4,12 +4,12 @@ This repository holds the configuration of `{{SERVER_HOST}}`: runbooks under `do ## Layout -- `docs/` — runbooks and notes, listed by `alignfirst docmap`. +- `docs/` — runbooks and notes, listed by `npx -y alignfirst docmap`. - `infra/openclaw/` — `seed.sh` and its modules, `environment.d/`, `bin/`, `projects/`, `workspace/`, `coding-agent/`. `.env` is gitignored. - `scripts/workspace/` — the portless workspace wrapper. - `.reports/` — one journal per operator task, committed. -- `.plans/` — work files. Symlinked across worktrees, and into a clone of the work-files repository so the team shares them. Run `alignfirst sync` after changing anything under it. +- `.plans/` — work files. Symlinked across worktrees, and into a clone of the work-files repository so the team shares them. Run `npx -y alignfirst sync` after changing anything under it. - `.local/`, `.local-wt/` — shared notes and per-worktree state, gitignored. @@ -19,18 +19,14 @@ A **workspace** is a git worktree (with its branch) plus its own dev setup: syml Run `npm run workspace -- --guide` for the procedures. -## Conventions - -- _Commit messages_: Conventional Commits, very short subject, no ticket ID. -- _Default branch_: `main`. - ## Everyday commands | Command | Purpose | | --- | --- | -| `alignfirst docmap` | Browse the documentation; read `docs/overview.md` first | +| `npx -y alignfirst context` | Conventions, documentation index and protocols; read it first | +| `npx -y alignfirst docmap` | Browse the documentation | | `npm run workspace -- ` | Manage worktree workspaces (`--guide` for the procedures) | | `npm run validate` | docmap check and a syntax check of the wrapper | -| `alignfirst sync` | Publish and retrieve the work files (`.plans`) | +| `npx -y alignfirst sync` | Publish and retrieve the work files (`.plans`) | diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/README.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/README.md index be150528..7f459282 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/README.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/README.md @@ -27,11 +27,12 @@ In the admin account: npm install -g alignfirst npm install # TEAM_PLANS_SECTION +git clone {{PLANS_REPOSITORY_URL}} alignfirst plans setup # TEAM_PLANS_SECTION mkdir -p .plans .local npm run workspace -- setup -alignfirst docmap +alignfirst context ``` Optional upstream reference for investigations (host-only, gitignored): diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md index 0934b9b9..a5130e39 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/docs/installations/02-admin-repository.md @@ -68,7 +68,7 @@ alignfirst plans check cd ~/{{ADMIN_REPOSITORY_NAME}} mkdir -p .plans .local npm run workspace -- setup -alignfirst docmap +alignfirst context ``` Continue with [03-toolchain.md](03-toolchain.md). diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/package.json b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/package.json index 01410483..6c86b1c5 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/package.json +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/package.json @@ -4,9 +4,9 @@ "type": "module", "engines": { "node": ">=24.16.0" }, "scripts": { - "docmap": "alignfirst docmap", + "docmap": "npx -y alignfirst docmap", "workspace": "node scripts/workspace/workspace.mjs", - "validate": "alignfirst docmap --check && node --check scripts/workspace/workspace.mjs" + "validate": "npx -y alignfirst docmap --check && node --check scripts/workspace/workspace.mjs" }, "devDependencies": { "@paleo/workspace": "~0.32.0" diff --git a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/scripts/workspace/workspace.mjs b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/scripts/workspace/workspace.mjs index a7087526..03a19049 100644 --- a/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/scripts/workspace/workspace.mjs +++ b/skills/alignfirst-setup-guide/assets/alignfirst-developer-template/base/scripts/workspace/workspace.mjs @@ -13,7 +13,7 @@ await runWorkspace({ // TEAM_PLANS_SECTION preSetup: ({ isMainWorktree, currentWorktree }) => { if (!isMainWorktree) return; - execFileSync("alignfirst", ["plans", "check"], { + execFileSync("npx", ["alignfirst", "plans", "check"], { cwd: currentWorktree, stdio: "inherit", }); diff --git a/skills/alignfirst-setup-guide/references/alignfirst-developer.md b/skills/alignfirst-setup-guide/references/alignfirst-developer.md index 95142ca4..e81d5665 100644 --- a/skills/alignfirst-setup-guide/references/alignfirst-developer.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-developer.md @@ -56,7 +56,7 @@ The agent **runtime** is fixed: every AlignFirst Developer uses OpenClaw's embed | `{{TEAM_NAME}}` | Operator | README, `IDENTITY.md`, `SOUL.md`, `USER.md` | | `{{TEAM_MEMBERS}}` | Operator | `USER.md` | | `{{PORT_RANGE_FIRST}}`, `{{PORT_RANGE_LAST}}` | Operator (suggested 28000–28599) | `.alignfirst-projects.json`, overview, workspace `AGENTS.md`, `09` | -| `{{PLANS_REPOSITORY_URL}}` | Operator, work-files repository only | `02`, `add-project.md` | +| `{{PLANS_REPOSITORY_URL}}` | Operator, work-files repository only | README, `02`, `add-project.md` | | `{{PLANS_CLONE_NAME}}` | Operator, work-files repository only | `common.conf`, `02`, `add-project.md` | | `{{PLANS_CLONE_NOTE}}` | Derived | projects marker | | `{{SLACK_OWNER_ID}}`, `{{SLACK_CHANNEL_ID}}` | Slack administrator | `.env.example` (Slack overlay) | From fdc345bc0cbd9eb496d94e1fae2802892dc5a0d6 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 14 Sep 2026 09:55:18 +0200 Subject: [PATCH 8/8] chore: skill version --- skills/alignfirst-developer-openclaw-playbook/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/alignfirst-developer-openclaw-playbook/SKILL.md b/skills/alignfirst-developer-openclaw-playbook/SKILL.md index 905e0942..9a43c0eb 100644 --- a/skills/alignfirst-developer-openclaw-playbook/SKILL.md +++ b/skills/alignfirst-developer-openclaw-playbook/SKILL.md @@ -4,7 +4,7 @@ description: "Operating-instructions dispatcher for an AlignFirst Developer runn license: CC0 1.0 metadata: author: Paleo - version: "0.37.0" + version: "0.38.0" repository: https://github.com/paleo/alignfirst ---