alignfirst 0.1.0-beta.5 → 0.1.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # alignfirst
2
2
 
3
- The AlignFirst CLI provides collaborative software-development workflows, task files, shared plans, project conventions, documentation discovery, and setup diagnostics.
3
+ 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 repository.
4
4
 
5
5
  ## Installation
6
6
 
@@ -40,26 +40,32 @@ The guide installs the selected components and configures the repository. Remove
40
40
  - `plans` — Set up, check and archive plans.
41
41
  - `docmap` — Browse project documentation.
42
42
  - `conventions` — Print the effective project conventions.
43
- - `context` — Print the conventions and the documentation map.
43
+ - `context` — Print the conventions, the documentation map when `docs/` exists, and the protocol aliases.
44
44
  - `config` — Report the effective project configuration.
45
45
  - `doctor` — Diagnose an AlignFirst setup.
46
46
 
47
- Run `alignfirst --help` for command usage or `alignfirst guide` to choose a protocol. `alignfirst guide <protocol>` prints the selected protocol followed by shared conventions. Add `--protocol-only` when those conventions are already in context.
47
+ Run `alignfirst --help` for command usage or `alignfirst guide` to choose a protocol. `alignfirst guide <protocol>` prints the selected protocol followed by the ticket directory and work file rules. Add `--protocol-only` when those rules are already in context.
48
48
 
49
49
  ## Agent skills
50
50
 
51
- Ten optional Agent Skill stubs expose the CLI to GitHub Copilot, Cursor, Claude Code, and Codex. Protocol skills reuse guides already in context and load missing guides through `npx -y alignfirst guide`. Catchup skills load ticket history through `npx -y alignfirst ticket --catchup`.
51
+ Nine optional Agent Skill stubs expose the CLI as commands in Claude Code, Codex, GitHub Copilot, and Cursor. Protocol skills reuse guides already in context and load missing guides through `npx -y alignfirst guide`. Catchup skills load ticket history through `npx -y alignfirst ticket --catchup`.
52
52
 
53
53
  Install them globally:
54
54
 
55
55
  ```sh
56
56
  npx skills add https://github.com/paleo/alignfirst --global \
57
- --skill alignfirst --skill al --skill alplan --skill alspec \
58
- --skill aldescription --skill alreview --skill alcatchup --skill almerge \
57
+ --skill al --skill alplan --skill alspec --skill aldescription \
58
+ --skill alreview --skill alcatchup --skill almerge \
59
59
  --skill alcatchupaad --skill alcatchupspec
60
60
  ```
61
61
 
62
- Restart the agent after installation. Claude Code, GitHub Copilot, and Cursor expose skills with `/`; Codex uses `$`.
62
+ Install the `alignfirst` skill when a project's `AGENTS.md` does not run `npx alignfirst context`; it lets the agent recognize a protocol named in prose:
63
+
64
+ ```sh
65
+ npx skills add https://github.com/paleo/alignfirst --global --skill alignfirst
66
+ ```
67
+
68
+ Start a new agent session to load the skills.
63
69
 
64
70
  ## Workflows
65
71
 
@@ -73,9 +79,9 @@ These examples use the `/` form. Replace it with `$` in Codex.
73
79
  | Description | `/aldescription` | Summarize the work and propose a commit message. |
74
80
  | Review | `/alreview` | Review the current branch against its base. |
75
81
  | Merge | `/almerge` | Resolve merge or rebase conflicts. |
76
- | Catch up | `/alcatchup` | Load the current task history and continue. |
77
- | Catch up and align | `/alcatchupaad <request>` | Load history, then start AAD. |
78
- | Catch up and specify | `/alcatchupspec <request>` | Load history, then start specification. |
82
+ | Catch up | `/alcatchup` | Load the current ticket history and continue. |
83
+ | Catch up and AAD | `/alcatchupaad <request>` | Load ticket history, then start AAD. |
84
+ | Catch up and spec | `/alcatchupspec <request>` | Load ticket history, then start specification. |
79
85
 
80
86
  To implement a plan, start a fresh agent context and ask it to execute the plan file.
81
87
 
@@ -1,3 +1,6 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { renderCommandForm } from "../command-form.js";
1
4
  import { renderConventions } from "../conventions.js";
2
5
  import { parseBareCommandArgs } from "../parse-args.js";
3
6
  import { runDocmap } from "./docmap.js";
@@ -5,7 +8,16 @@ export function runContext(ctx, args) {
5
8
  const usage = `Usage: ${ctx.form} context\n`;
6
9
  if (parseBareCommandArgs(ctx, args, usage))
7
10
  return 0;
8
- ctx.stdout.write(renderConventions(ctx));
9
- ctx.stdout.write("\n");
11
+ ctx.stdout.write(`# Project Conventions\n\n${renderConventions(ctx)}`);
12
+ const code = existsSync(join(ctx.cwd, "docs")) ? writeDocmapSection(ctx) : 0;
13
+ ctx.stdout.write(`\n${renderProtocolsSection(ctx)}`);
14
+ return code;
15
+ }
16
+ function writeDocmapSection(ctx) {
17
+ ctx.stdout.write("\n# Docmap Usage\n\n");
10
18
  return runDocmap(ctx, []);
11
19
  }
20
+ function renderProtocolsSection(ctx) {
21
+ const template = readFileSync(new URL("../../templates/context/protocols.md", import.meta.url), "utf-8");
22
+ return renderCommandForm(template, ctx.form);
23
+ }
@@ -107,7 +107,7 @@ function renderGuide(ctx, options) {
107
107
  const [title, ...sections] = protocol.split("\n\n");
108
108
  return [
109
109
  title,
110
- "This guide includes the selected protocol and shared conventions. Read both before starting.",
110
+ "This guide includes the selected protocol, followed by the ticket directory and work file rules. Read both before starting.",
111
111
  ...sections,
112
112
  core,
113
113
  ].join("\n\n");
@@ -124,7 +124,7 @@ function renderCoreGuide(ctx, placeholders) {
124
124
  const core = applyPlaceholders(readGuideTemplate("core.md"), placeholders);
125
125
  if (ctx.projectConfig !== undefined)
126
126
  return core;
127
- return `${core.trimEnd()}\n\n## Project conventions\n\n${renderConventions(ctx).trimEnd()}`;
127
+ return `${core.trimEnd()}\n\n## Project Conventions\n\n${renderConventions(ctx).trimEnd()}`;
128
128
  }
129
129
  function buildGuidePlaceholders(ctx, protocol) {
130
130
  const pattern = ctx.projectConfig?.config.ticketIdPattern;
@@ -5,6 +5,7 @@ import { CliError } from "../cli-error.js";
5
5
  import { assertMainWorktreeRoot } from "../git.js";
6
6
  import { parseBareCommandArgs, parseCommandArgs } from "../parse-args.js";
7
7
  import { archiveEntry, archiveThresholdDays, autoArchive } from "../plans/archive.js";
8
+ import { isTicketName } from "../plans/layout.js";
8
9
  import { linkPlans } from "../plans/link.js";
9
10
  import { resolvePlansMode } from "../plans/mode.js";
10
11
  import { findStoppedRebase, renderStoppedRebase } from "../plans/rebase.js";
@@ -156,9 +157,10 @@ function resolveArchiveTarget(ctx, args, usage) {
156
157
  const stats = statSync(target, { throwIfNoEntry: false });
157
158
  if (!stats?.isDirectory() || realpathSync(dirname(target)) !== realpathSync(plansDir))
158
159
  throw new CliError(`${argument} must be an existing directory directly under .plans.`);
159
- if (basename(target).startsWith("_"))
160
+ const name = basename(target);
161
+ if (!isTicketName(name))
160
162
  throw new CliError(`${argument}: names starting with _ are not tickets.`);
161
- return target;
163
+ return join(plansDir, name);
162
164
  }
163
165
  function isPathArgument(argument) {
164
166
  return argument.includes("/") || argument.includes("\\");
@@ -5,22 +5,22 @@ import { formatLocalTimestamp, formatSize } from "../format.js";
5
5
  import { parseCommandArgs } from "../parse-args.js";
6
6
  import { renderCatchup } from "../plans/catchup.js";
7
7
  import { assertPlansGate } from "../plans/layout.js";
8
- import { deduceTicketFromBranch, nextFilePosition, peekSideTicket, reserveSideTicket, resolveTicketDir, validateTicketId, } from "../plans/ticket.js";
8
+ import { deduceTicketFromBranch, deduceTicketFromExisting, nextFilePosition, peekSideTicket, reserveSideTicket, resolveTicketDir, validateTicketId, } from "../plans/ticket.js";
9
9
  const USAGE = `Usage:
10
10
  {{FORM}} ticket [<id>] [--next [<filename>]] [--new-cycle] [--json] [--dry-run]
11
11
  {{FORM}} ticket --side [--next [<filename>]] [--new-cycle] [--json] [--dry-run]
12
- {{FORM}} ticket [<id> | --side] --catchup
12
+ {{FORM}} ticket [<id>] --catchup
13
13
 
14
14
  --catchup prints the ticket's Markdown files, plans excluded, summaries included.
15
15
  Files over 64 KiB are listed without content. Above 30 KiB of output, only the entry
16
16
  list is printed.
17
17
  `;
18
18
  export function runTicket(ctx, args) {
19
- assertPlansGate(ctx.cwd, ctx.form);
20
19
  const usage = renderUsage(ctx);
21
20
  const parsed = parseTicketArgs(ctx, args, usage);
22
21
  if (parsed === undefined)
23
22
  return 0;
23
+ assertPlansGate(ctx.cwd, ctx.form);
24
24
  const result = resolveTicket(ctx, parsed);
25
25
  if (parsed.catchup) {
26
26
  ctx.stdout.write(renderCatchup(ctx.cwd, result, renderReport(ctx, parsed, result)));
@@ -65,11 +65,12 @@ function parseTicketArgs(ctx, args, usage) {
65
65
  throw new CliError(`A ticket id cannot be combined with --side.\n\n${usage}`);
66
66
  if (values["new-cycle"] && values.next === undefined)
67
67
  throw new CliError(`--new-cycle requires --next.\n\n${usage}`);
68
- if (values.catchup && (values.next !== undefined || values.json || values["dry-run"]))
69
- throw new CliError(`--catchup cannot be combined with --next, --json, or --dry-run.\n\n${usage}`);
68
+ if (values.catchup &&
69
+ (values.side || values.next !== undefined || values.json || values["dry-run"]))
70
+ throw new CliError(`--catchup cannot be combined with --side, --next, --json, or --dry-run.\n\n${usage}`);
70
71
  if (normalized.filename !== undefined)
71
72
  validateNextFilename(normalized.filename);
72
- const resolution = resolveTicketId(ctx, positionals[0], values.side, values["dry-run"]);
73
+ const resolution = resolveTicketId(ctx, positionals[0], values);
73
74
  return {
74
75
  ...resolution,
75
76
  next: values.next === undefined ? undefined : (normalized.filename ?? true),
@@ -110,16 +111,16 @@ function validateNextFilename(filename) {
110
111
  throw new CliError("--next must be a non-empty single path segment.");
111
112
  }
112
113
  }
113
- function resolveTicketId(ctx, positional, side, dryRun) {
114
+ function resolveTicketId(ctx, positional, flags) {
114
115
  const pattern = ctx.projectConfig?.config.ticketIdPattern;
115
116
  if (positional !== undefined) {
116
117
  validateTicketId(positional, pattern);
117
118
  return { id: positional };
118
119
  }
119
- if (side)
120
- return { id: dryRun ? peekSideTicket(ctx.cwd) : reserveSideTicket(ctx.cwd) };
120
+ if (flags.side)
121
+ return { id: flags["dry-run"] ? peekSideTicket(ctx.cwd) : reserveSideTicket(ctx.cwd) };
121
122
  if (pattern === undefined)
122
- throw new CliError("No ticket id given. Pass it.");
123
+ return deduceTicketFromExisting(ctx.cwd, { sideAllowed: !flags.catchup });
123
124
  const deduced = deduceTicketFromBranch(ctx.cwd, pattern);
124
125
  validateTicketId(deduced.id, pattern);
125
126
  return deduced;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, lstatSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { renderDefaultBranchLine, resolveDefaultBranch } from "./default-branch.js";
4
+ import { errorMessage } from "./errors.js";
4
5
  import { gitOutputOrUndefined, gitSucceeds } from "./git.js";
5
6
  import { resolvePlansMode } from "./plans/mode.js";
6
7
  export function renderConventions(ctx) {
@@ -59,8 +60,7 @@ function renderPlans(ctx) {
59
60
  return `${base}${archival}`;
60
61
  }
61
62
  catch (error) {
62
- const message = error instanceof Error ? error.message : String(error);
63
- return `Plans: ${message.split("\n", 1)[0]}`;
63
+ return `Plans: ${errorMessage(error).split("\n", 1)[0]}`;
64
64
  }
65
65
  }
66
66
  function plansEntryExists(cwd) {
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
3
  import { CliError } from "../cli-error.js";
4
+ import { errorMessage } from "../errors.js";
4
5
  import { formatLocalTimestamp, formatSize } from "../format.js";
5
6
  const MAX_FILE_BYTES = 64 * 1024;
6
7
  const MAX_OUTPUT_BYTES = 30 * 1024;
@@ -49,7 +50,6 @@ function readBody(cwd, file) {
49
50
  return readFileSync(join(cwd, file.path), "utf-8");
50
51
  }
51
52
  catch (error) {
52
- const detail = error instanceof Error ? error.message : String(error);
53
- throw new CliError(`Cannot load catchup file ${file.path}: ${detail}`);
53
+ throw new CliError(`Cannot load catchup file ${file.path}: ${errorMessage(error)}`);
54
54
  }
55
55
  }
@@ -1,9 +1,11 @@
1
+ import { type Stats } from "node:fs";
1
2
  import { CliError } from "../cli-error.js";
2
3
  export declare const PLANS_DIR = ".plans";
3
4
  export declare const ARCHIVES_DIR = "_archives";
4
5
  export declare function isTicketName(name: string): boolean;
5
6
  export declare function plansDir(cwd: string): string;
6
7
  export declare function archivesDir(cwd: string): string;
7
- export declare function assertPlansGate(cwd: string, form: string): void;
8
+ /** Returns the lstat of `.plans`, so callers can tell a symlink from a directory. */
9
+ export declare function assertPlansGate(cwd: string, form: string): Stats;
8
10
  export declare function missingPlansMessage(form: string): string;
9
11
  export declare function missingPlansError(form: string): CliError;
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, lstatSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { CliError } from "../cli-error.js";
4
4
  export const PLANS_DIR = ".plans";
@@ -12,10 +12,17 @@ export function plansDir(cwd) {
12
12
  export function archivesDir(cwd) {
13
13
  return join(plansDir(cwd), ARCHIVES_DIR);
14
14
  }
15
+ /** Returns the lstat of `.plans`, so callers can tell a symlink from a directory. */
15
16
  export function assertPlansGate(cwd, form) {
16
- if (existsSync(plansDir(cwd)))
17
- return;
18
- throw missingPlansError(form);
17
+ const path = plansDir(cwd);
18
+ const stats = lstatSync(path, { throwIfNoEntry: false });
19
+ if (!stats)
20
+ throw missingPlansError(form);
21
+ if (stats.isSymbolicLink() && !existsSync(path))
22
+ throw new CliError(`The .plans symlink is broken. Re-run ${form} plans setup with the clone location.`);
23
+ if (!statSync(path).isDirectory())
24
+ throw new CliError(`.plans is not a directory. Remove it, then run ${form} plans setup (see the project documentation).`);
25
+ return stats;
19
26
  }
20
27
  export function missingPlansMessage(form) {
21
28
  return `No .plans/ directory in the current directory.\nLocal plans: mkdir .plans && echo .plans >> .gitignore\nTeam plans: ${form} plans setup <clone-dir>`;
@@ -1,17 +1,11 @@
1
- import { existsSync, lstatSync, realpathSync, statSync } from "node:fs";
1
+ import { realpathSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { CliError } from "../cli-error.js";
4
4
  import { gitOutput } from "../git.js";
5
- import { missingPlansError } from "./layout.js";
5
+ import { assertPlansGate } from "./layout.js";
6
6
  export function resolvePlansMode(cwd, form) {
7
7
  const plansPath = join(cwd, ".plans");
8
- const stats = lstatSync(plansPath, { throwIfNoEntry: false });
9
- if (!stats)
10
- throw missingPlansError(form);
11
- if (stats.isSymbolicLink() && !existsSync(plansPath))
12
- throw new CliError(`The .plans symlink is broken. Re-run ${form} plans setup with the clone location.`);
13
- if (!statSync(plansPath).isDirectory())
14
- throw new CliError(`.plans is not a directory. Remove it, then run ${form} plans setup (see the project documentation).`);
8
+ const stats = assertPlansGate(cwd, form);
15
9
  if (plansRepositoryId(plansPath, stats.isSymbolicLink(), form) === repositoryId(cwd))
16
10
  return { kind: "local" };
17
11
  return { kind: "shared", repoToplevel: gitOutput(plansPath, "rev-parse", "--show-toplevel") };
@@ -39,4 +39,9 @@ export declare function listEntries(dir: string): TicketEntry[];
39
39
  export declare function isPathSafeTicketId(id: string): boolean;
40
40
  export declare function validateTicketId(id: string, pattern?: string): void;
41
41
  export declare function detectTicketFromBranch(cwd: string, pattern: string): TicketDetection;
42
+ export interface DeduceFromExistingOptions {
43
+ sideAllowed: boolean;
44
+ }
45
+ /** Without a ticket id pattern, the branch can still name an existing ticket directory. */
46
+ export declare function deduceTicketFromExisting(cwd: string, { sideAllowed }: DeduceFromExistingOptions): DeducedTicket;
42
47
  export declare function deduceTicketFromBranch(cwd: string, pattern: string): DeducedTicket;
@@ -2,12 +2,15 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, statSync, }
2
2
  import { basename, join } from "node:path";
3
3
  import { CliError } from "../cli-error.js";
4
4
  import { isNodeError } from "../errors.js";
5
+ import { formatLocalTimestamp } from "../format.js";
5
6
  import { gitOutputOrUndefined } from "../git.js";
6
- import { archivesDir, plansDir } from "./layout.js";
7
+ import { archivesDir, isTicketName, plansDir } from "./layout.js";
7
8
  const FILE_PREFIX = /^([A-Z])(\d+)-/;
8
9
  const SIDE_TICKET = /^side-(\d+)$/;
9
10
  const PATH_SAFE_TICKET = /^[A-Za-z0-9._-]+$/;
10
11
  const ENTRY_ORDER = new Intl.Collator("en", { numeric: true });
12
+ const BRANCH_SEPARATORS = "[/_.-]";
13
+ const LISTED_TICKETS = 10;
11
14
  export function resolveTicketDir(cwd, id, { dryRun }) {
12
15
  const dir = join(plansDir(cwd), id);
13
16
  if (existsSync(dir))
@@ -102,14 +105,14 @@ export function isPathSafeTicketId(id) {
102
105
  return id !== "." && !id.includes("..") && PATH_SAFE_TICKET.test(id);
103
106
  }
104
107
  export function validateTicketId(id, pattern) {
105
- if (!isPathSafeTicketId(id))
108
+ if (!isPathSafeTicketId(id) || !isTicketName(id))
106
109
  throw new CliError(`Invalid ticket id: ${id}`);
107
110
  if (pattern !== undefined && !new RegExp(pattern).test(id) && !SIDE_TICKET.test(id))
108
111
  throw new CliError(`Ticket id "${id}" does not match ticketIdPattern "${pattern}".`);
109
112
  }
110
113
  export function detectTicketFromBranch(cwd, pattern) {
111
- const branch = gitOutputOrUndefined(cwd, "branch", "--show-current");
112
- if (branch === undefined || branch === "")
114
+ const branch = currentBranch(cwd);
115
+ if (branch === undefined)
113
116
  return { kind: "noBranch" };
114
117
  const unanchored = pattern.replace(/^\^/, "").replace(/\$$/, "");
115
118
  const match = new RegExp(unanchored).exec(branch);
@@ -117,6 +120,61 @@ export function detectTicketFromBranch(cwd, pattern) {
117
120
  return { kind: "noMatch", branch };
118
121
  return { kind: "detected", id: match[0], branch };
119
122
  }
123
+ function currentBranch(cwd) {
124
+ const branch = gitOutputOrUndefined(cwd, "branch", "--show-current");
125
+ return branch === undefined || branch === "" ? undefined : branch;
126
+ }
127
+ /** Without a ticket id pattern, the branch can still name an existing ticket directory. */
128
+ export function deduceTicketFromExisting(cwd, { sideAllowed }) {
129
+ const tickets = listTickets(cwd);
130
+ const branch = currentBranch(cwd);
131
+ if (branch !== undefined) {
132
+ const matches = tickets.filter((ticket) => branchNamesTicket(branch, ticket.id));
133
+ if (matches.length === 1)
134
+ return { id: matches[0].id, branch };
135
+ }
136
+ throw new CliError(renderMissingTicketId(tickets, sideAllowed));
137
+ }
138
+ function listTickets(cwd) {
139
+ return [
140
+ ...ticketDirectories(plansDir(cwd), false),
141
+ ...ticketDirectories(archivesDir(cwd), true),
142
+ ].toSorted((left, right) => right.modifiedAt.getTime() - left.modifiedAt.getTime());
143
+ }
144
+ function ticketDirectories(dir, archived) {
145
+ return readEntries(dir)
146
+ .filter((entry) => entry.isDirectory() && isTicketName(entry.name))
147
+ .map((entry) => ({
148
+ id: entry.name,
149
+ archived,
150
+ modifiedAt: newestModification(join(dir, entry.name)),
151
+ }));
152
+ }
153
+ function newestModification(dir) {
154
+ const entries = listEntries(dir);
155
+ if (entries.length === 0)
156
+ return statSync(dir).mtime;
157
+ return new Date(Math.max(...entries.map((entry) => entry.modifiedAt.getTime())));
158
+ }
159
+ function branchNamesTicket(branch, id) {
160
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ return new RegExp(`(^|${BRANCH_SEPARATORS})${escaped}($|${BRANCH_SEPARATORS})`).test(branch);
162
+ }
163
+ function renderMissingTicketId(tickets, sideAllowed) {
164
+ const hint = sideAllowed
165
+ ? "Pass a ticket id, or --side for a new side ticket."
166
+ : "Pass a ticket id.";
167
+ if (tickets.length === 0)
168
+ return `No ticket id given. ${hint}`;
169
+ const listed = tickets.slice(0, LISTED_TICKETS);
170
+ const width = Math.max(...listed.map((ticket) => ticket.id.length));
171
+ const lines = listed.map((ticket) => ` ${ticket.id.padEnd(width)} ${formatLocalTimestamp(ticket.modifiedAt)}` +
172
+ (ticket.archived ? " (archived)" : ""));
173
+ const rest = tickets.length - listed.length;
174
+ if (rest > 0)
175
+ lines.push(` … ${rest} more`);
176
+ return `No ticket id given. Existing tickets:\n${lines.join("\n")}\n${hint}`;
177
+ }
120
178
  export function deduceTicketFromBranch(cwd, pattern) {
121
179
  const result = detectTicketFromBranch(cwd, pattern);
122
180
  if (result.kind === "noBranch")
@@ -75,6 +75,26 @@ function assertValidPattern(pattern, label) {
75
75
  catch (error) {
76
76
  throw invalidConfig(label, `ticketIdPattern is not a valid regular expression: ${errorMessage(error)}`);
77
77
  }
78
+ if (hasInnerAnchor(pattern))
79
+ throw invalidConfig(label, "ticketIdPattern must be one anchored expression; group alternatives: ^(ABC|XYZ)-\\d+$");
80
+ }
81
+ /** Branch detection unanchors the pattern by trimming its ends, so inner anchors never match. */
82
+ function hasInnerAnchor(pattern) {
83
+ let inClass = false;
84
+ for (let i = 0; i < pattern.length; ++i) {
85
+ const char = pattern[i];
86
+ if (char === "\\")
87
+ ++i;
88
+ else if (inClass)
89
+ inClass = char !== "]";
90
+ else if (char === "[")
91
+ inClass = true;
92
+ else if (char === "^" && i !== 0)
93
+ return true;
94
+ else if (char === "$" && i !== pattern.length - 1)
95
+ return true;
96
+ }
97
+ return false;
78
98
  }
79
99
  function assertValidPortRange(range, label) {
80
100
  if (range.first > range.last)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alignfirst",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.7",
4
4
  "license": "CC0-1.0",
5
5
  "author": "Thomas MUR",
6
6
  "description": "The AlignFirst CLI: protocols, plans and docs in one command.",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@paleo/docmap": "~0.10.0-beta.0",
37
+ "@paleo/docmap": "~0.10.0-beta.1",
38
38
  "arktype": "^2.2.3",
39
39
  "semver": "^7.8.5"
40
40
  },
@@ -0,0 +1,5 @@
1
+ # AlignFirst Protocols
2
+
3
+ `{{CMD}} guide <protocol>` prints a collaborative protocol to follow when the user names AlignFirst or a protocol alias. A guide already in context is not reloaded. Each named guide includes the ticket directory and work file rules; `--protocol-only` omits them when they are already in context.
4
+
5
+ Aliases: `alspec` → `spec`, `alplan` → `plan`, `al` or `AAD` → `aad`, `almerge` → `merge`, `alreview` → `review`, `aldescription` → `description`. `alcatchup` stands for `{{CMD}} ticket --catchup` followed by the user's instructions; `alcatchupaad` and `alcatchupspec` run it before the `aad` or `spec` protocol.
@@ -1,6 +1,6 @@
1
- # Shared Conventions
1
+ # Ticket Directory and Work Files
2
2
 
3
- ## Ticket directory
3
+ ## Ticket Directory
4
4
 
5
5
  TICKET_DIR holds a ticket's work files and includes a trailing slash. TICKET_ID is the external ticket ID or `side-N` for work without an external ticket.
6
6
 
@@ -12,7 +12,7 @@ TICKET_DIR holds a ticket's work files and includes a trailing slash. TICKET_ID
12
12
 
13
13
  When the user says there is no external ticket, run `{{CMD}} ticket --side` and use the returned TICKET_ID in subsequent ticket commands. Reuse an existing `side-N` directory when the user refers to earlier work. Omit the ticket ID from commit messages.
14
14
 
15
- ## Work files
15
+ ## Work Files
16
16
 
17
17
  Files use `{CYCLE_LETTER}{FILE_NUMBER}-{FILE_TYPE}.md`. FILE_PREFIX combines the cycle letter and the file number within that cycle. FILE_NAME includes the prefix and extension.
18
18
 
@@ -36,17 +36,21 @@ Use Spec-Plan-Execute when:
36
36
 
37
37
  The `alcatchup` alias loads this context, then follows the user's instructions or returns a short synthesis. `alcatchupaad` and `alcatchupspec` load it, then start AAD or a specification.
38
38
 
39
- ## Description
39
+ ## Standalone Utilities
40
40
 
41
- A standalone utility (`aldescription` alias). It reads specs and summaries that have been generated for a ticket and produces a concise description of what was implemented. Typically used to generate a PR/MR description once the work is done.
41
+ Each utility runs on its own, outside the workflows.
42
42
 
43
- ## Code Review
43
+ ### Description
44
44
 
45
- A standalone utility (`alreview` alias). It compares the current branch to a base branch (defaults to the repo's default branch) and runs parallel reviewers, each with its own perspective: intent, correctness, change safety, code quality. Ecosystem modules (strict TypeScript, JavaScript, Python) sharpen the language-specific checks. The findings are merged into a concise review report.
45
+ `aldescription` alias. Reads the specs and summaries of a ticket and writes a concise description of the implemented work, typically for a PR or MR.
46
46
 
47
- ## Merge
47
+ ### Review
48
48
 
49
- A standalone utility (`almerge` alias). After a merge or rebase, the agent investigates both sides, resolves the conflicts (with a special case for lock files), and writes a brief summary of the resolutions.
49
+ `alreview` alias. Compares the current branch to a base branch (the repository's default branch unless another is given) and runs parallel reviewers, each with its own perspective: intent, correctness, change safety, code quality. Ecosystem modules (strict TypeScript, JavaScript, Python) sharpen the language-specific checks. The findings are merged into a concise review report.
50
+
51
+ ### Merge
52
+
53
+ `almerge` alias. Merges an incoming branch, or resolves the conflicts of a merge or rebase in progress. The agent investigates both sides, resolves each conflict (with a special case for lock files), and writes a brief summary of the resolutions.
50
54
 
51
55
  ## Typical Lifecycle of a Ticket
52
56
 
@@ -1,6 +1,6 @@
1
1
  # Align-and-Do Protocol (AAD)
2
2
 
3
- ## Pre-requisites
3
+ ## Prerequisites
4
4
 
5
5
  Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
6
6
 
@@ -1,6 +1,6 @@
1
- # How to Write a Description for Implemented Work
1
+ # Description Protocol
2
2
 
3
- ## Pre-requisites
3
+ ## Prerequisites
4
4
 
5
5
  Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
6
6
 
@@ -1,6 +1,6 @@
1
- # How to Resolve Merge Conflicts
1
+ # Merge Protocol
2
2
 
3
- ## Pre-requisites
3
+ ## Prerequisites
4
4
 
5
5
  Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
6
6
 
@@ -8,7 +8,7 @@ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory c
8
8
 
9
9
  This protocol applies when a merge or rebase has produced conflicts, or when the user provides an incoming branch to merge. Follow the steps below.
10
10
 
11
- ## 1. Check git status
11
+ ## 1. Check Git Status
12
12
 
13
13
  Run `git status` to check for conflicts.
14
14
 
@@ -29,7 +29,7 @@ Resolve the conflicts properly — preserve both intents whenever possible. Do n
29
29
  1. Accept all the changes from the incoming branch.
30
30
  2. After all other conflicts are resolved, run the proper install command so the package manager re-applies the current branch's dependency changes.
31
31
 
32
- ## 4. Finalize the merge
32
+ ## 4. Finalize the Merge
33
33
 
34
34
  Finalize the merge using git's default commit message (e.g. `git commit --no-edit`). Do not write your own commit message — git has already prepared the proper merge message.
35
35
 
@@ -44,11 +44,11 @@ Example:
44
44
  ```markdown
45
45
  # Merge Summary - [very short title]
46
46
 
47
- ## Notable resolutions
47
+ ## Notable Resolutions
48
48
 
49
49
  - `path/to/file.ts`: [what made it tricky and which choice was made, in one or two sentences]
50
50
 
51
- ## Lock file
51
+ ## Lock File
52
52
 
53
53
  [Only if there was a lock file conflict: which lock file, which install command was run]
54
54
  ```
@@ -1,8 +1,8 @@
1
- # How to Write Implementation Plans
1
+ # Planning Protocol
2
2
 
3
- ## Pre-requisites
3
+ ## Prerequisites
4
4
 
5
- ### Determine TICKET_DIR and the spec file
5
+ ### Determine TICKET_DIR and the Spec File
6
6
 
7
7
  You need:
8
8
 
@@ -17,14 +17,14 @@ Before starting, **read the spec file** and understand it entirely.
17
17
 
18
18
  In order to generate implementation plans, you MUST follow this process:
19
19
 
20
- 1. **Investigation Phase**: Explore the codebase, understand the current implementation, and identify the problem
21
- 2. **Analysis Phase**: Determine the plan structure (single or multiple plans) and identify relevant documentation and skills
22
- 3. **Designing Phase - Implementation Plan(s)**: Design plan(s) based on the analysis - If you discover issues or missing design decisions, STOP AND ASK THE USER
23
- 4. **Designing Phase - Main Plan**: If multiple plans, design a main plan to coordinate them
24
- 5. **Writing Phase**: Write the plan file(s)
25
- 6. **Review Phase**: Critically review and improve the plan(s)
20
+ 1. **Investigation**: Explore the codebase, understand the current implementation, and identify the problem
21
+ 2. **Analysis**: Determine the plan structure (single or multiple plans) and identify relevant documentation and skills
22
+ 3. **Plan Design**: Design plan(s) based on the analysis - If you discover issues or missing design decisions, STOP AND ASK THE USER
23
+ 4. **Main Plan Design**: If multiple plans, design a main plan to coordinate them
24
+ 5. **Writing**: Write the plan file(s)
25
+ 6. **Review**: Critically review and improve the plan(s)
26
26
 
27
- ## Phase 1. Investigation Phase
27
+ ## Phase 1. Investigation
28
28
 
29
29
  Check your context for available **documentation** and **skills**. Read every document and skill relevant to any aspect of the task — this is not optional. For each skill, also **read its relevant references**.
30
30
 
@@ -34,7 +34,7 @@ Use the SPEC text as a starting point, but do not trust it blindly. Verify the c
34
34
 
35
35
  For each operation in the spec, search for existing functions that do a similar job.
36
36
 
37
- ## Phase 2. Analysis Phase
37
+ ## Phase 2. Analysis
38
38
 
39
39
  Based on your investigation, determine the plan structure:
40
40
 
@@ -55,7 +55,7 @@ Identify which **documentation** and **skills** are relevant for the work. Omit
55
55
  - List the documentation and skills that the implementing agent should read and follow. Always exclude `alignfirst` from skills.
56
56
  - For complex skills with reference files, identify specific files that should be loaded
57
57
 
58
- ## Phase 3. Designing Phase - Implementation Plan Structure
58
+ ## Phase 3. Plan Design
59
59
 
60
60
  Design an implementation plan based on the SPEC. Include all useful information from the spec. If the spec is already detailed enough, you can extract and reuse parts of it. Add implementation details, file paths, and a breakdown into steps that weren't in the spec.
61
61
 
@@ -132,7 +132,7 @@ Do not trust this plan blindly. Be sure you understand the codebase and the plan
132
132
  **IMPORTANT**: Do NOT use external search tools (Context7, web search, documentation fetching) during implementation unless explicitly allowed in this plan. All context should be provided in this plan or discoverable in the codebase.
133
133
  ```
134
134
 
135
- ## Phase 4. Designing Phase - Main Plan
135
+ ## Phase 4. Main Plan Design
136
136
 
137
137
  **Create a main plan only when multiple plans are created. Skip this section entirely if not applicable.**
138
138
 
@@ -205,7 +205,7 @@ Note:
205
205
 
206
206
  - Replace "{PLAN_FILE_PATH}" with the complete plan file path, such as `.plans/123/A2-main-plan.md`. Its handover path is `.plans/123/A2-main-plan.summary.md`.
207
207
 
208
- ## Phase 5. Writing Phase
208
+ ## Phase 5. Writing
209
209
 
210
210
  Write the plan file(s) according to the determined structure:
211
211
 
@@ -236,7 +236,7 @@ Continue the current cycle. Immediately before writing each file, run `{{TICKET_
236
236
 
237
237
  _Important Note: There will be lint errors in the markdown files you write. Ignore them. NEVER FIX LINT ERRORS (FORMATTING ISSUES) IN THE PLANS._
238
238
 
239
- ## Phase 6. Review Phase
239
+ ## Phase 6. Review
240
240
 
241
241
  When you think the plan(s) are complete, read them again with a critical eye and edit them to improve them.
242
242
 
@@ -1,6 +1,6 @@
1
- # How to Write a Code Review Report
1
+ # Review Protocol
2
2
 
3
- ## Pre-requisites
3
+ ## Prerequisites
4
4
 
5
5
  You need:
6
6
 
@@ -37,7 +37,7 @@ Before starting, run `{{TICKET_CMD}} --next review.md --new-cycle` to start a ne
37
37
 
38
38
  ## Phase 2. Perspective Reviews
39
39
 
40
- **Small diff** (roughly under 100 changed lines, outside generated files and lockfiles): skip the subagents. Execute the perspectives yourself, sequentially — intent, correctness, safety, quality — reading the same files. The rest of the protocol is unchanged.
40
+ **Small diff** (roughly under 100 changed lines, outside generated files and lockfiles): skip the subagents. Execute the perspectives yourself, sequentially — intent, correctness, change safety, code quality — reading the same files. The rest of the protocol is unchanged.
41
41
 
42
42
  Otherwise, launch four reviewer subagents in parallel:
43
43
 
@@ -45,8 +45,8 @@ Otherwise, launch four reviewer subagents in parallel:
45
45
  | --- | --- | --- |
46
46
  | Intent | `intent` | none |
47
47
  | Correctness | `correctness` | from Phase 1 |
48
- | Change safety | `safety` | from Phase 1 |
49
- | Quality | `quality` | from Phase 1 |
48
+ | Change Safety | `safety` | from Phase 1 |
49
+ | Code Quality | `quality` | from Phase 1 |
50
50
 
51
51
  Each subagent prompt must contain:
52
52
 
@@ -1,6 +1,6 @@
1
- # How to Write a Technical Specification
1
+ # Specification Protocol
2
2
 
3
- ## Pre-requisites
3
+ ## Prerequisites
4
4
 
5
5
  Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
6
6
 
@@ -8,13 +8,13 @@ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory c
8
8
 
9
9
  When the user asks you for a SPEC (technical specification), you MUST follow this process:
10
10
 
11
- 1. **Investigation Phase**: Research the codebase to understand the current implementation and identify the problem
12
- 2. **Discussion Phase**: Collaborate with the user to explore the problem space and potential solutions BEFORE writing the specification file
13
- 3. **Specification Phase**: Only after user approval, write the final specification file
11
+ 1. **Investigation**: Research the codebase to understand the current implementation and identify the problem
12
+ 2. **Discussion**: Collaborate with the user to explore the problem space and potential solutions BEFORE writing the specification file
13
+ 3. **Specification**: Only after user approval, write the final specification file
14
14
 
15
15
  The discussion phase is MANDATORY. Remember that you are a newcomer to this project while the user has extensive experience with the codebase and will be happy to help guide you.
16
16
 
17
- ## Phase 1. Investigation Phase
17
+ ## Phase 1. Investigation
18
18
 
19
19
  Check your context for available **documentation** and **skills**. Read every document and skill relevant to any aspect of the task — this is not optional. For each skill, also **read its relevant references**.
20
20
 
@@ -22,7 +22,7 @@ Investigate the codebase yourself, find the relevant source code, think carefull
22
22
 
23
23
  Always seek a clean break solution by default. Never consider backward compatibility unless explicitly requested.
24
24
 
25
- ## Phase 2. Discussion Phase
25
+ ## Phase 2. Discussion
26
26
 
27
27
  Engage in a thorough collaborative discussion covering:
28
28
 
@@ -42,7 +42,7 @@ You should ask questions freely to ensure you fully understand:
42
42
 
43
43
  Do not use your question tool. Always ask in plain text. Your questions will be the opportunity for a real discussion.
44
44
 
45
- ## Phase 3. Specification Phase
45
+ ## Phase 3. Specification
46
46
 
47
47
  After the user approves your proposal, run `{{TICKET_CMD}} --next spec.md --new-cycle` to start a new cycle. Append FILE_NAME to TICKET_DIR, then immediately write the specification at that path. Do not overwrite an existing file.
48
48
 
@@ -1,8 +1,8 @@
1
1
  # AlignFirst Guide
2
2
 
3
- Follow the requested protocol if its guide is already in context. Otherwise, load it with the command below. Each named guide includes its protocol and shared conventions; add `--protocol-only` when those conventions are already in context.
3
+ Follow the requested protocol if its guide is already in context. Otherwise, load it with the command below. Each named guide includes its protocol and the ticket directory and work file rules; add `--protocol-only` when those rules are already in context.
4
4
 
5
- ## Choose a protocol
5
+ ## Choose a Protocol
6
6
 
7
7
  Use spec → plan → execution for most tasks, especially when the design is uncertain. Use AAD for small changes or follow-up work. Execute a written plan in a fresh agent session.
8
8