alignfirst 0.1.0-beta.2 → 0.1.0-beta.4

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
@@ -35,7 +35,7 @@ The guide installs the selected components and configures the repository. Remove
35
35
  ## Commands
36
36
 
37
37
  - `guide` — Print an AlignFirst protocol.
38
- - `ticket` — Resolve a ticket directory and its next file.
38
+ - `ticket` — Resolve a ticket directory, load its history, or get its next file.
39
39
  - `sync` — Synchronize shared plans.
40
40
  - `plans` — Set up, check and archive plans.
41
41
  - `docmap` — Browse project documentation.
@@ -44,18 +44,19 @@ The guide installs the selected components and configures the repository. Remove
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` for the collaboration guide.
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.
48
48
 
49
49
  ## Agent skills
50
50
 
51
- Eight optional Agent Skill stubs expose the CLI to GitHub Copilot, Cursor, Claude Code, and Codex. The skills contain no protocols; they invoke `npx alignfirst guide`.
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`.
52
52
 
53
53
  Install them globally:
54
54
 
55
55
  ```sh
56
56
  npx skills add https://github.com/paleo/alignfirst --global \
57
57
  --skill alignfirst --skill al --skill alplan --skill alspec \
58
- --skill aldescription --skill alreview --skill alcatchup --skill almerge
58
+ --skill aldescription --skill alreview --skill alcatchup --skill almerge \
59
+ --skill alcatchupaad --skill alcatchupspec
59
60
  ```
60
61
 
61
62
  Restart the agent after installation. Claude Code, GitHub Copilot, and Cursor expose skills with `/`; Codex uses `$`.
@@ -73,11 +74,17 @@ These examples use the `/` form. Replace it with `$` in Codex.
73
74
  | Review | `/alreview` | Review the current branch against its base. |
74
75
  | Merge | `/almerge` | Resolve merge or rebase conflicts. |
75
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. |
76
79
 
77
80
  To implement a plan, start a fresh agent context and ask it to execute the plan file.
78
81
 
79
82
  AlignFirst stores specifications, plans, and summaries in `.plans/<ticket-id>/`. It normally derives the ticket ID from the request or branch and asks when none is available. Files use a cycle letter and sequence number, such as `A1-spec.md` and `A2-plan.md`.
80
83
 
84
+ ## Catchup output
85
+
86
+ `alignfirst ticket [<id>] --catchup` prints the ticket's Markdown files, plans excluded, each in a `<file>` block carrying its path and modification time. When the history exceeds the output budget, only the entry list is printed so the agent can read the relevant files.
87
+
81
88
  ## Updates
82
89
 
83
90
  ```sh
package/dist/cli.js CHANGED
@@ -59,7 +59,7 @@ function renderHelp(ctx) {
59
59
 
60
60
  Usage:
61
61
  ${ctx.form} guide [<protocol>]
62
- ${ctx.form} ticket [<id>]
62
+ ${ctx.form} ticket [<id>] [--catchup]
63
63
  ${ctx.form} sync [--auto-archive | --no-auto-archive]
64
64
  ${ctx.form} plans <command>
65
65
  ${ctx.form} docmap [<arguments>]
@@ -11,7 +11,7 @@ import { resolvePlansMode } from "../plans/mode.js";
11
11
  import { detectTicketFromBranch } from "../plans/ticket.js";
12
12
  import { PROTOCOLS } from "../protocols.js";
13
13
  const TICKET_CMD_PLACEHOLDER = "{{TICKET_CMD}}";
14
- const TICKET_CONTEXT_PLACEHOLDER = "{{TICKET_CONTEXT}}";
14
+ const TICKET_DETECTION_PLACEHOLDER = "{{TICKET_DETECTION}}";
15
15
  const PLANS_STATE_PLACEHOLDER = "{{PLANS_STATE}}";
16
16
  const COMMIT_RULE_PLACEHOLDER = "{{COMMIT_RULE}}";
17
17
  const BASE_BRANCH_RULE_PLACEHOLDER = "{{BASE_BRANCH_RULE}}";
@@ -102,9 +102,15 @@ function renderGuide(ctx, options) {
102
102
  return applyPlaceholders(readProtocolTemplate(options.protocol), placeholders);
103
103
  const core = renderCoreGuide(ctx, placeholders);
104
104
  if (options.protocol === undefined)
105
- return core;
105
+ return `${readGuideTemplate("selection.md")}\n\n${core}`;
106
106
  const protocol = applyPlaceholders(readProtocolTemplate(options.protocol), placeholders);
107
- return `${core.trimEnd()}\n\n${protocol.trimEnd()}`;
107
+ const [title, ...sections] = protocol.split("\n\n");
108
+ return [
109
+ title,
110
+ "This guide includes the selected protocol and shared conventions. Read both before starting.",
111
+ ...sections,
112
+ core,
113
+ ].join("\n\n");
108
114
  }
109
115
  function renderReviewerGuide(perspective, modules) {
110
116
  const templates = [
@@ -125,13 +131,13 @@ function buildGuidePlaceholders(ctx, protocol) {
125
131
  const detection = pattern === undefined ? undefined : detectTicketFromBranch(ctx.cwd, pattern);
126
132
  return {
127
133
  ticketCommand: detection?.kind === "detected" ? "{{CMD}} ticket" : "{{CMD}} ticket <id>",
128
- ticketContext: renderTicketContext(pattern, detection),
134
+ ticketDetection: renderTicketDetection(pattern, detection),
129
135
  plansState: renderPlansState(ctx),
130
136
  commitRule: renderCommitRule(ctx),
131
137
  baseBranchRule: renderBaseBranchRule(ctx, protocol),
132
138
  };
133
139
  }
134
- function renderTicketContext(pattern, detection) {
140
+ function renderTicketDetection(pattern, detection) {
135
141
  if (pattern === undefined)
136
142
  return "Ask the user for the ticket ID when it is not given.";
137
143
  if (detection?.kind === "detected")
@@ -146,7 +152,7 @@ function renderPlansState(ctx) {
146
152
  return `\`\`\`text\n${missingPlansMessage(ctx.form)}\n\`\`\``;
147
153
  try {
148
154
  return resolvePlansMode(ctx.cwd, ctx.form).kind === "shared"
149
- ? "After every change in TASK_DIR, run `{{CMD}} sync`."
155
+ ? "After every change in TICKET_DIR, run `{{CMD}} sync`."
150
156
  : "";
151
157
  }
152
158
  catch {
@@ -156,7 +162,7 @@ function renderPlansState(ctx) {
156
162
  function renderCommitRule(ctx) {
157
163
  const commit = ctx.projectConfig?.config.git?.commit;
158
164
  if (commit === undefined)
159
- return "(follow the convention you are aware of, or default to `<type>: [<ticket_id>] very short description`)";
165
+ return "(follow the convention you are aware of, or default to `<type>: [TICKET_ID] very short description`)";
160
166
  const { subject, side } = commitSubject(commit);
161
167
  const rule = side === undefined ? subject : `${subject}; ${side}`;
162
168
  return `(project convention: ${rule})`;
@@ -177,7 +183,7 @@ function applyPlaceholders(template, values) {
177
183
  return template
178
184
  .replaceAll(`${PLANS_STATE_PLACEHOLDER}\n\n`, () => values.plansState === "" ? "" : `${values.plansState}\n\n`)
179
185
  .replaceAll(TICKET_CMD_PLACEHOLDER, () => values.ticketCommand)
180
- .replaceAll(TICKET_CONTEXT_PLACEHOLDER, () => values.ticketContext)
186
+ .replaceAll(TICKET_DETECTION_PLACEHOLDER, () => values.ticketDetection)
181
187
  .replaceAll(PLANS_STATE_PLACEHOLDER, () => values.plansState)
182
188
  .replaceAll(COMMIT_RULE_PLACEHOLDER, () => values.commitRule)
183
189
  .replaceAll(BASE_BRANCH_RULE_PLACEHOLDER, () => values.baseBranchRule);
@@ -1,12 +1,19 @@
1
1
  import { join, relative } from "node:path";
2
2
  import { parseArgs } from "node:util";
3
3
  import { CliError } from "../cli-error.js";
4
+ import { formatLocalTimestamp, formatSize } from "../format.js";
4
5
  import { parseCommandArgs } from "../parse-args.js";
6
+ import { renderCatchup } from "../plans/catchup.js";
5
7
  import { assertPlansGate } from "../plans/layout.js";
6
- import { deduceTicketFromBranch, nextFileName, peekSideTicket, reserveSideTicket, resolveTicketDir, validateTicketId, } from "../plans/ticket.js";
8
+ import { deduceTicketFromBranch, nextFilePosition, peekSideTicket, reserveSideTicket, resolveTicketDir, validateTicketId, } from "../plans/ticket.js";
7
9
  const USAGE = `Usage:
8
- {{FORM}} ticket [<id>] [--next <filename>] [--new-cycle] [--json] [--dry-run]
9
- {{FORM}} ticket --side [--json] [--dry-run]
10
+ {{FORM}} ticket [<id>] [--next [<filename>]] [--new-cycle] [--json] [--dry-run]
11
+ {{FORM}} ticket --side [--next [<filename>]] [--new-cycle] [--json] [--dry-run]
12
+ {{FORM}} ticket [<id> | --side] --catchup
13
+
14
+ --catchup prints the ticket's Markdown files, plans excluded, summaries included.
15
+ Files over 64 KiB are listed without content. Above 30 KiB of output, only the entry
16
+ list is printed.
10
17
  `;
11
18
  export function runTicket(ctx, args) {
12
19
  assertPlansGate(ctx.cwd, ctx.form);
@@ -15,27 +22,34 @@ export function runTicket(ctx, args) {
15
22
  if (parsed === undefined)
16
23
  return 0;
17
24
  const result = resolveTicket(ctx, parsed);
18
- const next = parsed.next === undefined
19
- ? undefined
20
- : nextFileName(result.entries, parsed.next, parsed.newCycle);
25
+ if (parsed.catchup) {
26
+ ctx.stdout.write(renderCatchup(ctx.cwd, result, renderReport(ctx, parsed, result)));
27
+ return 0;
28
+ }
29
+ if (parsed.next !== undefined) {
30
+ writeNextReport(ctx, parsed, result, parsed.next);
31
+ return 0;
32
+ }
21
33
  if (parsed.json)
22
- ctx.stdout.write(`${JSON.stringify(jsonReport(ctx, parsed, result, next), undefined, 2)}\n`);
34
+ ctx.stdout.write(`${JSON.stringify(jsonReport(ctx, parsed, result), undefined, 2)}\n`);
23
35
  else
24
- ctx.stdout.write(renderReport(ctx, parsed, result, next));
36
+ ctx.stdout.write(renderReport(ctx, parsed, result));
25
37
  return 0;
26
38
  }
27
39
  function renderUsage(ctx) {
28
40
  return USAGE.replaceAll("{{FORM}}", ctx.form);
29
41
  }
30
42
  function parseTicketArgs(ctx, args, usage) {
43
+ const normalized = normalizeNextArgs(args);
31
44
  const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
32
- args,
45
+ args: normalized.args,
33
46
  options: {
34
- next: { type: "string" },
47
+ next: { type: "boolean" },
35
48
  "new-cycle": { type: "boolean", default: false },
36
49
  json: { type: "boolean", default: false },
37
50
  "dry-run": { type: "boolean", default: false },
38
51
  side: { type: "boolean", default: false },
52
+ catchup: { type: "boolean", default: false },
39
53
  help: { type: "boolean", short: "h", default: false },
40
54
  },
41
55
  strict: true,
@@ -51,18 +65,46 @@ function parseTicketArgs(ctx, args, usage) {
51
65
  throw new CliError(`A ticket id cannot be combined with --side.\n\n${usage}`);
52
66
  if (values["new-cycle"] && values.next === undefined)
53
67
  throw new CliError(`--new-cycle requires --next.\n\n${usage}`);
54
- if (values.next !== undefined)
55
- validateNextFilename(values.next);
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}`);
70
+ if (normalized.filename !== undefined)
71
+ validateNextFilename(normalized.filename);
56
72
  const resolution = resolveTicketId(ctx, positionals[0], values.side, values["dry-run"]);
57
73
  return {
58
74
  ...resolution,
59
- next: values.next,
75
+ next: values.next === undefined ? undefined : (normalized.filename ?? true),
60
76
  newCycle: values["new-cycle"],
61
77
  json: values.json,
62
78
  dryRun: values["dry-run"],
63
79
  side: values.side,
80
+ catchup: values.catchup,
64
81
  };
65
82
  }
83
+ function normalizeNextArgs(args) {
84
+ const normalized = { args: [] };
85
+ for (let index = 0; index < args.length; ++index) {
86
+ const arg = args[index];
87
+ if (arg === "--") {
88
+ normalized.args.push(...args.slice(index));
89
+ break;
90
+ }
91
+ if (arg.startsWith("--next=")) {
92
+ normalized.filename = arg.slice("--next=".length);
93
+ normalized.args.push("--next");
94
+ continue;
95
+ }
96
+ normalized.args.push(arg);
97
+ if (arg !== "--next")
98
+ continue;
99
+ const following = args[index + 1];
100
+ delete normalized.filename;
101
+ if (following !== undefined && !following.startsWith("-")) {
102
+ normalized.filename = following;
103
+ ++index;
104
+ }
105
+ }
106
+ return normalized;
107
+ }
66
108
  function validateNextFilename(filename) {
67
109
  if (filename.length === 0 || filename === "." || filename === ".." || /[\\/]/u.test(filename)) {
68
110
  throw new CliError("--next must be a non-empty single path segment.");
@@ -92,33 +134,55 @@ function resolveTicket(ctx, options) {
92
134
  };
93
135
  return resolveTicketDir(ctx.cwd, options.id, { dryRun: options.dryRun });
94
136
  }
95
- function jsonReport(ctx, options, result, next) {
137
+ function writeNextReport(ctx, options, result, filename) {
138
+ const names = result.entries.map((entry) => entry.name);
139
+ const { cycleLetter, fileNumber } = nextFilePosition(names, options.newCycle);
140
+ const prefix = `${cycleLetter}${fileNumber}`;
141
+ const report = {
142
+ TICKET_DIR: `${relative(ctx.cwd, result.dir)}/`,
143
+ CYCLE_LETTER: cycleLetter,
144
+ FILE_NUMBER: fileNumber,
145
+ ...(filename === true ? { FILE_PREFIX: prefix } : { FILE_NAME: `${prefix}-${filename}` }),
146
+ };
147
+ if (options.json) {
148
+ ctx.stdout.write(`${JSON.stringify(report, undefined, 2)}\n`);
149
+ return;
150
+ }
151
+ const lines = Object.entries(report).map(([name, value]) => `- ${name}: \`${value}\``);
152
+ ctx.stdout.write(`${lines.join("\n")}\n`);
153
+ }
154
+ function jsonReport(ctx, options, result) {
96
155
  return {
97
- id: result.id,
98
- dir: relative(ctx.cwd, result.dir),
156
+ TICKET_ID: result.id,
157
+ TICKET_DIR: `${relative(ctx.cwd, result.dir)}/`,
99
158
  state: result.state,
100
159
  ...(options.branch === undefined ? {} : { branch: options.branch }),
101
- entries: result.entries,
102
- ...(next === undefined ? {} : { next: relative(ctx.cwd, join(result.dir, next)) }),
160
+ entries: result.entries.map((entry) => ({
161
+ ...entry,
162
+ modifiedAt: entry.modifiedAt.toISOString(),
163
+ })),
103
164
  };
104
165
  }
105
- function renderReport(ctx, options, result, next) {
166
+ function renderReport(ctx, options, result) {
106
167
  const reservation = options.side && options.dryRun ? " (would be reserved)" : "";
107
168
  const deduction = options.branch === undefined ? "" : ` (deduced from branch ${options.branch})`;
108
169
  const directoryState = renderDirectoryState(result.state, options.dryRun);
109
170
  const directory = `${relative(ctx.cwd, result.dir)}/`;
110
171
  const lines = [
111
- `Ticket ${result.id}${reservation}${deduction}`,
112
- `Directory: ${directory}${directoryState}`,
172
+ `- TICKET_ID: \`${result.id}\`${reservation}${deduction}`,
173
+ `- TICKET_DIR: \`${directory}\`${directoryState}`,
113
174
  ];
114
175
  if (result.entries.length === 0)
115
176
  lines.push("Entries: (none)");
116
177
  else
117
- lines.push("Entries:", ...result.entries.map((entry) => ` ${entry}`));
118
- if (next !== undefined)
119
- lines.push(`Next file: ${relative(ctx.cwd, join(result.dir, next))}`);
178
+ lines.push("Entries:", ...result.entries.map((entry) => ` ${renderEntry(entry)}`));
120
179
  return `${lines.join("\n")}\n`;
121
180
  }
181
+ function renderEntry(entry) {
182
+ if (entry.size === undefined)
183
+ return entry.name;
184
+ return `${entry.name} (${formatSize(entry.size)}, ${formatLocalTimestamp(entry.modifiedAt)})`;
185
+ }
122
186
  function renderDirectoryState(state, dryRun) {
123
187
  if (state === "existing")
124
188
  return "";
@@ -38,9 +38,9 @@ function renderCommits(ctx) {
38
38
  }
39
39
  export function commitSubject(commit) {
40
40
  if (commit.ticketReference === "bracketed")
41
- return { subject: "`type: [ticketId] summary`", side: "`type: summary` for `side-N`" };
41
+ return { subject: "`type: [TICKET_ID] summary`", side: "`type: summary` for `side-N`" };
42
42
  if (commit.ticketReference === "bracketedHash")
43
- return { subject: "`type: [#ticketId] summary`", side: "`type: summary` for `side-N`" };
43
+ return { subject: "`type: [#TICKET_ID] summary`", side: "`type: summary` for `side-N`" };
44
44
  return { subject: "`type: summary`" };
45
45
  }
46
46
  function renderPlans(ctx) {
@@ -0,0 +1,2 @@
1
+ export declare function formatSize(bytes: number): string;
2
+ export declare function formatLocalTimestamp(date: Date): string;
package/dist/format.js ADDED
@@ -0,0 +1,23 @@
1
+ const KIB = 1024;
2
+ const MIB = KIB * KIB;
3
+ export function formatSize(bytes) {
4
+ if (bytes < KIB)
5
+ return `${bytes} B`;
6
+ if (bytes < MIB)
7
+ return `${trimDecimal(bytes / KIB)} KiB`;
8
+ return `${trimDecimal(bytes / MIB)} MiB`;
9
+ }
10
+ function trimDecimal(value) {
11
+ return value.toFixed(1).replace(/\.0$/, "");
12
+ }
13
+ export function formatLocalTimestamp(date) {
14
+ const offsetMinutes = -date.getTimezoneOffset();
15
+ const sign = offsetMinutes < 0 ? "-" : "+";
16
+ const offset = Math.abs(offsetMinutes);
17
+ const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
18
+ const time = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
19
+ return `${day}T${time}${sign}${pad(Math.floor(offset / 60))}:${pad(offset % 60)}`;
20
+ }
21
+ function pad(value) {
22
+ return String(value).padStart(2, "0");
23
+ }
@@ -0,0 +1,2 @@
1
+ import type { ResolvedTicketDir } from "./ticket.js";
2
+ export declare function renderCatchup(cwd: string, ticket: ResolvedTicketDir, report: string): string;
@@ -0,0 +1,55 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { CliError } from "../cli-error.js";
4
+ import { formatLocalTimestamp, formatSize } from "../format.js";
5
+ const MAX_FILE_BYTES = 64 * 1024;
6
+ const MAX_OUTPUT_BYTES = 30 * 1024;
7
+ const PLAN_FILE = /^[A-Z]\d+-(?:main-plan|plan-.*)\.md$/;
8
+ const OMISSION_NOTICE = `Content omitted: over the ${formatSize(MAX_FILE_BYTES)} limit.`;
9
+ const TOO_LARGE_NOTICE = `History too large to print (over the ${formatSize(MAX_OUTPUT_BYTES)} budget). Read the relevant files among the entries above rather than all of them.`;
10
+ export function renderCatchup(cwd, ticket, report) {
11
+ const files = ticket.entries.flatMap((entry) => catchupFile(cwd, ticket.dir, entry));
12
+ if (files.length === 0)
13
+ return `${report}\nNo Markdown files to load.\n`;
14
+ const outputBytes = files.reduce((total, file) => total + sectionBytes(file), Buffer.byteLength(report) + 1);
15
+ if (outputBytes > MAX_OUTPUT_BYTES)
16
+ return `${report}\n${TOO_LARGE_NOTICE}\n`;
17
+ return `${report}\n${files.map((file) => renderSection(cwd, file)).join("")}`;
18
+ }
19
+ function catchupFile(cwd, dir, entry) {
20
+ if (entry.size === undefined || !isHistoryFile(entry.name))
21
+ return [];
22
+ return [
23
+ {
24
+ path: relative(cwd, join(dir, entry.name)),
25
+ size: entry.size,
26
+ modified: formatLocalTimestamp(entry.modifiedAt),
27
+ },
28
+ ];
29
+ }
30
+ function isHistoryFile(name) {
31
+ return name.endsWith(".md") && (name.endsWith(".summary.md") || !PLAN_FILE.test(name));
32
+ }
33
+ function sectionBytes(file) {
34
+ const body = isOversized(file) ? Buffer.byteLength(OMISSION_NOTICE) : file.size;
35
+ return Buffer.byteLength(wrapSection(file, "")) + body;
36
+ }
37
+ function isOversized(file) {
38
+ return file.size > MAX_FILE_BYTES;
39
+ }
40
+ function wrapSection(file, body) {
41
+ return `<file path="${file.path}" modified="${file.modified}">\n${body}\n</file>\n\n`;
42
+ }
43
+ function renderSection(cwd, file) {
44
+ const body = isOversized(file) ? OMISSION_NOTICE : readBody(cwd, file);
45
+ return wrapSection(file, body.replace(/\n+$/, ""));
46
+ }
47
+ function readBody(cwd, file) {
48
+ try {
49
+ return readFileSync(join(cwd, file.path), "utf-8");
50
+ }
51
+ catch (error) {
52
+ const detail = error instanceof Error ? error.message : String(error);
53
+ throw new CliError(`Cannot load catchup file ${file.path}: ${detail}`);
54
+ }
55
+ }
@@ -2,7 +2,13 @@ export interface ResolvedTicketDir {
2
2
  id: string;
3
3
  dir: string;
4
4
  state: "existing" | "created" | "restored";
5
- entries: string[];
5
+ entries: TicketEntry[];
6
+ }
7
+ /** `name` ends with a slash for a directory, which has no `size`. */
8
+ export interface TicketEntry {
9
+ name: string;
10
+ size?: number;
11
+ modifiedAt: Date;
6
12
  }
7
13
  export interface ResolveTicketOptions {
8
14
  dryRun: boolean;
@@ -24,8 +30,12 @@ export type TicketDetection = {
24
30
  export declare function resolveTicketDir(cwd: string, id: string, { dryRun }: ResolveTicketOptions): ResolvedTicketDir;
25
31
  export declare function reserveSideTicket(cwd: string): string;
26
32
  export declare function peekSideTicket(cwd: string): string;
27
- export declare function nextFileName(entries: readonly string[], filename: string, newCycle: boolean): string;
28
- export declare function listEntries(dir: string): string[];
33
+ export interface NextFilePosition {
34
+ cycleLetter: string;
35
+ fileNumber: number;
36
+ }
37
+ export declare function nextFilePosition(entries: readonly string[], newCycle: boolean): NextFilePosition;
38
+ export declare function listEntries(dir: string): TicketEntry[];
29
39
  export declare function isPathSafeTicketId(id: string): boolean;
30
40
  export declare function validateTicketId(id: string, pattern?: string): void;
31
41
  export declare function detectTicketFromBranch(cwd: string, pattern: string): TicketDetection;
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, readdirSync, renameSync, statSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, statSync, } from "node:fs";
2
+ import { basename, join } from "node:path";
3
3
  import { CliError } from "../cli-error.js";
4
4
  import { isNodeError } from "../errors.js";
5
5
  import { gitOutputOrUndefined } from "../git.js";
@@ -7,6 +7,7 @@ import { archivesDir, plansDir } from "./layout.js";
7
7
  const FILE_PREFIX = /^([A-Z])(\d+)-/;
8
8
  const SIDE_TICKET = /^side-(\d+)$/;
9
9
  const PATH_SAFE_TICKET = /^[A-Za-z0-9._-]+$/;
10
+ const ENTRY_ORDER = new Intl.Collator("en", { numeric: true });
10
11
  export function resolveTicketDir(cwd, id, { dryRun }) {
11
12
  const dir = join(plansDir(cwd), id);
12
13
  if (existsSync(dir))
@@ -63,25 +64,39 @@ function readEntries(dir) {
63
64
  return [];
64
65
  }
65
66
  }
66
- export function nextFileName(entries, filename, newCycle) {
67
+ export function nextFilePosition(entries, newCycle) {
67
68
  const prefixes = entries.flatMap((entry) => {
68
69
  const match = FILE_PREFIX.exec(entry);
69
70
  return match ? [{ cycle: match[1], number: Number(match[2]) }] : [];
70
71
  });
71
72
  if (prefixes.length === 0)
72
- return `A1-${filename}`;
73
+ return { cycleLetter: "A", fileNumber: 1 };
73
74
  const highestCycle = prefixes.reduce((highest, prefix) => (prefix.cycle > highest ? prefix.cycle : highest), "A");
74
75
  if (newCycle)
75
- return `${String.fromCharCode(highestCycle.charCodeAt(0) + 1)}1-${filename}`;
76
+ return { cycleLetter: String.fromCharCode(highestCycle.charCodeAt(0) + 1), fileNumber: 1 };
76
77
  const highestNumber = Math.max(...prefixes.filter(({ cycle }) => cycle === highestCycle).map(({ number }) => number));
77
- return `${highestCycle}${highestNumber + 1}-${filename}`;
78
+ return { cycleLetter: highestCycle, fileNumber: highestNumber + 1 };
78
79
  }
79
80
  export function listEntries(dir) {
80
81
  if (!existsSync(dir) || !statSync(dir).isDirectory())
81
82
  return [];
82
- return readdirSync(dir, { withFileTypes: true })
83
- .map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`)
84
- .toSorted();
83
+ return readdirSync(dir)
84
+ .map((name) => describeEntry(join(dir, name)))
85
+ .toSorted((left, right) => ENTRY_ORDER.compare(left.name, right.name));
86
+ }
87
+ function describeEntry(path) {
88
+ const stat = statOrLink(path);
89
+ if (stat.isDirectory())
90
+ return { name: `${basename(path)}/`, modifiedAt: stat.mtime };
91
+ return { name: basename(path), size: stat.size, modifiedAt: stat.mtime };
92
+ }
93
+ function statOrLink(path) {
94
+ try {
95
+ return statSync(path);
96
+ }
97
+ catch {
98
+ return lstatSync(path);
99
+ }
85
100
  }
86
101
  export function isPathSafeTicketId(id) {
87
102
  return id !== "." && !id.includes("..") && PATH_SAFE_TICKET.test(id);
@@ -1,2 +1,2 @@
1
- export declare const PROTOCOLS: readonly ["spec", "plan", "aad", "catchup", "merge", "review", "description"];
1
+ export declare const PROTOCOLS: readonly ["spec", "plan", "aad", "merge", "review", "description"];
2
2
  export type Protocol = (typeof PROTOCOLS)[number];
package/dist/protocols.js CHANGED
@@ -1,9 +1 @@
1
- export const PROTOCOLS = [
2
- "spec",
3
- "plan",
4
- "aad",
5
- "catchup",
6
- "merge",
7
- "review",
8
- "description",
9
- ];
1
+ export const PROTOCOLS = ["spec", "plan", "aad", "merge", "review", "description"];
package/dist/skills.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const STUB_SKILLS: readonly ["alignfirst", "alspec", "alplan", "al", "alcatchup", "almerge", "alreview", "aldescription"];
1
+ export declare const STUB_SKILLS: readonly ["alignfirst", "alspec", "alplan", "al", "alcatchup", "alcatchupaad", "alcatchupspec", "almerge", "alreview", "aldescription"];
2
2
  export declare const SKILL_ROOTS: readonly [".agents/skills", ".claude/skills", ".codex/skills"];
3
3
  export interface InstalledSkill {
4
4
  root: string;
package/dist/skills.js CHANGED
@@ -6,6 +6,8 @@ export const STUB_SKILLS = [
6
6
  "alplan",
7
7
  "al",
8
8
  "alcatchup",
9
+ "alcatchupaad",
10
+ "alcatchupspec",
9
11
  "almerge",
10
12
  "alreview",
11
13
  "aldescription",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alignfirst",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "license": "CC0-1.0",
5
5
  "author": "Thomas MUR",
6
6
  "description": "The AlignFirst CLI: protocols, plans and docs in one command.",
@@ -1,54 +1,27 @@
1
- # AlignFirst Guide
1
+ # Shared Conventions
2
2
 
3
- An agent that does not know which protocol to use runs `{{CMD}} guide overview`.
3
+ ## Ticket directory
4
4
 
5
- ## Protocols
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
 
7
- - **Technical Specification** (_spec_, or _alspec_): `{{CMD}} guide spec --protocol-only`
8
- - **Implementation Plans** (_plan_, or _alplan_): `{{CMD}} guide plan --protocol-only`
9
- - **Align-and-Do Protocol** (_AAD_): `{{CMD}} guide aad --protocol-only`
10
- - **Catch Up** (_catchup_, or _alcatchup_): `{{CMD}} guide catchup --protocol-only`
11
- - **Merge** (_merge_, or _almerge_): `{{CMD}} guide merge --protocol-only`
12
- - **Code Review** (_alreview_): `{{CMD}} guide review --protocol-only`
13
- - **Description** (_aldescription_): `{{CMD}} guide description --protocol-only`
7
+ {{TICKET_DETECTION}}
14
8
 
15
- ## TASK_DIR Location
16
-
17
- TASK_DIR holds the work files of a ticket. `{{TICKET_CMD}}` prints it and lists its entries; it creates a missing directory and restores an archived one.
18
-
19
- {{TICKET_CONTEXT}}
9
+ `{{TICKET_CMD}}` prints TICKET_DIR and its entries, creates a missing directory, and restores an archived one. Run it once to load the ticket directory context, unless `{{CMD}} ticket --catchup` already did.
20
10
 
21
11
  {{PLANS_STATE}}
22
12
 
23
- **Work without a ticket:** when the user says there is no ticket, run `{{CMD}} ticket --side`. Reuse an existing `side-N` directory when the user refers to that earlier work. Omit the ticket ID from commit messages.
24
-
25
- ## File Naming Convention
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.
26
14
 
27
- Format: `{CYCLE_LETTER}{FILE_NUMBER}-{FILE_TYPE}.md`
15
+ ## Work files
28
16
 
29
- **Common file types:**
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.
30
18
 
31
- - `spec` - technical specification
32
- - `plan` - implementation plan
33
- - `AAD.summary` - AAD summary document
34
- - `description` - PR/MR description
35
- - `review` - code review report
36
- - `merge.summary` - merge conflicts resolution summary
19
+ Immediately before creating each file, run `{{TICKET_CMD}} --next <filename>` with the extension included. It returns TICKET_DIR, CYCLE_LETTER, FILE_NUMBER, and FILE_NAME. Append FILE_NAME to TICKET_DIR to get the file path, preserving the leading dot and existing slash.
37
20
 
38
- **Example structure:**
21
+ With no filename, `{{TICKET_CMD}} --next` returns FILE_PREFIX instead of FILE_NAME. Add `--new-cycle` to either form when the protocol or user calls for a new cycle.
39
22
 
40
- ```text
41
- A1-spec.md
42
- A2-plan.md
43
- A3-AAD.summary.md
44
- B1-spec.md
45
- ```
23
+ Neither form reserves a number. Write the file before requesting another filename. With `--json`, the same information uses the same field names.
46
24
 
47
- ## Notes
25
+ Common file types are `spec`, `plan`, `AAD.summary`, `description`, `review`, and `merge.summary`. Use another type when needed.
48
26
 
49
- - **TICKET_ID** is a unique identifier for the task, often an issue or ticket number.
50
- - `{{TICKET_CMD}} --next <filename>` prints the path of the next file in the current cycle, the extension included (`--next spec.md` giving `A2-spec.md`).
51
- - `--new-cycle` starts a new cycle.
52
- - The protocol or the user decides whether to continue the current cycle or start a new one.
53
- - Cycle letters and file numbers are internal. Never discuss them with the user.
54
- - New file types are welcome.
27
+ Cycle letters and file numbers are internal. Never discuss them with the user.
@@ -8,8 +8,8 @@ AlignFirst is a set of collaborative protocols for working with a user on coding
8
8
 
9
9
  The standard workflow for most tasks. It produces formal artifacts at each stage:
10
10
 
11
- 1. **Spec** (`/alspec`): Investigate the codebase, discuss with the user, then write a technical specification. There are usually several back-and-forths before the spec is written.
12
- 2. **Plan** (`/alplan`): Read the spec, investigate further, then write implementation plan(s). The plan is a self-contained prompt for the implementing agent.
11
+ 1. **Spec** (`alspec` alias): Investigate the codebase, discuss with the user, then write a technical specification. There are usually several back-and-forths before the spec is written.
12
+ 2. **Plan** (`alplan` alias): Read the spec, investigate further, then write implementation plan(s). The plan is a self-contained prompt for the implementing agent.
13
13
  3. **Execute**: A new session implements the plan and writes a handover document summarizing the changes.
14
14
 
15
15
  After execution, additional rounds of AAD (see below) can address follow-up fixes or adjustments.
@@ -18,7 +18,7 @@ After execution, additional rounds of AAD (see below) can address follow-up fixe
18
18
 
19
19
  For smaller tasks that don't justify a formal spec and plan. Everything happens in one session: investigate, discuss, implement, summarize.
20
20
 
21
- Use AAD (`/al`) when:
21
+ Use AAD (`al` alias) when:
22
22
 
23
23
  - The task is small or well-understood
24
24
  - It's a follow-up change after a plan has already been executed
@@ -32,19 +32,21 @@ Use Spec-Plan-Execute when:
32
32
 
33
33
  ## Catch Up
34
34
 
35
- A standalone utility (`/alcatchup`). It loads the ticket's history from its requests, specs, and summaries. It then follows the user's instructions or returns a short synthesis. To continue directly into a light workflow, use `/alcatchup then start an AAD: …`.
35
+ `{{CMD}} ticket --catchup` loads the ticket's history: its requests, specs, reviews and summaries, without the plans. When the history is too large, the command lists the files instead and the agent reads the relevant ones.
36
+
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.
36
38
 
37
39
  ## Description
38
40
 
39
- A standalone utility (`/aldescription`). 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
+ 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.
40
42
 
41
43
  ## Code Review
42
44
 
43
- A standalone utility (`/alreview`). 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
+ 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.
44
46
 
45
47
  ## Merge
46
48
 
47
- A standalone utility (`/almerge`). 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
+ 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.
48
50
 
49
51
  ## Typical Lifecycle of a Ticket
50
52
 
@@ -2,12 +2,7 @@
2
2
 
3
3
  ## Pre-requisites
4
4
 
5
- You need:
6
-
7
- - the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
8
- - the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{TICKET_CMD}} --next AAD.summary.md` prints the file to create
9
-
10
- Identify and state these values before starting the protocol.
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
11
6
 
12
7
  ---
13
8
 
@@ -42,9 +37,7 @@ Do not use your question tool. Always ask in plain text. Your questions will be
42
37
 
43
38
  ## 3. Act
44
39
 
45
- When you and the user agree, create the summary file in TASK_DIR, then start implementing.
46
-
47
- Compose the filename using the current CYCLE_LETTER and the bumped FILE_NUMBER, then append `-AAD.summary.md`. For example, if the last file is `E5-plan-something.md`, create `E6-AAD.summary.md`. Do not overwrite an existing file.
40
+ When you and the user agree, run `{{TICKET_CMD}} --next AAD.summary.md` to continue the current cycle. Append FILE_NAME to TICKET_DIR, then immediately create the summary file at that path. Do not overwrite an existing file. Start implementing after creating it.
48
41
 
49
42
  Maintain the file as a **live report** while you work.
50
43
 
@@ -2,18 +2,13 @@
2
2
 
3
3
  ## Pre-requisites
4
4
 
5
- You need:
6
-
7
- - the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
8
- - the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{TICKET_CMD}} --next description.md --new-cycle` prints the file to create
9
-
10
- Identify and state these values before starting the protocol.
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
11
6
 
12
7
  ## Steps
13
8
 
14
- 1. Find the current ticket plan directory.
15
- 2. Create your description as a new file `{CYCLE_LETTER}1-description.md` with the next cycle letter, containing just the header — this reserves the filename.
16
- 3. If a previous `*description.md` file exists in the TASK_DIR, find the latest one. Only read `*spec.md` and `*summary.md` files that come *after* it — earlier work is already covered.
9
+ 1. Run `{{TICKET_CMD}} --next description.md --new-cycle` to start a new cycle.
10
+ 2. Append FILE_NAME to TICKET_DIR, then immediately create the description at that path with just the header. Creating the file reserves the filename.
11
+ 3. If a previous `*description.md` file exists in the TICKET_DIR, find the latest one. Only read `*spec.md` and `*summary.md` files that come *after* it — earlier work is already covered.
17
12
  Otherwise, read all `*spec.md` and `*summary.md` files.
18
13
  4. Write the commit message and description into your file.
19
14
 
@@ -2,12 +2,7 @@
2
2
 
3
3
  ## Pre-requisites
4
4
 
5
- You need:
6
-
7
- - the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
8
- - the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{TICKET_CMD}} --next merge.summary.md` prints the file to create
9
-
10
- Identify and state these values before starting the protocol.
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
11
6
 
12
7
  ---
13
8
 
@@ -25,7 +20,7 @@ Take the time to understand how things work in the incoming branch and in the cu
25
20
 
26
21
  ## 3. Resolve
27
22
 
28
- Create the summary file now: a new file `{CYCLE_LETTER}{FILE_NUMBER}-merge.summary.md` in the TASK_DIR. Log each notable resolution in it as you resolve (see step 5 for the expected content).
23
+ Run `{{TICKET_CMD}} --next merge.summary.md` to continue the current cycle. Append FILE_NAME to TICKET_DIR, then immediately create the summary at that path. Log each notable resolution in it as you resolve (see step 5 for the expected content).
29
24
 
30
25
  Resolve the conflicts properly — preserve both intents whenever possible. Do not blindly accept one side.
31
26
 
@@ -2,15 +2,14 @@
2
2
 
3
3
  ## Pre-requisites
4
4
 
5
- ### Determine TASK_DIR, CYCLE_LETTER, and FILE_NUMBER
5
+ ### Determine TICKET_DIR and the spec file
6
6
 
7
7
  You need:
8
8
 
9
- - the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
10
- - the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{TICKET_CMD}} --next plan.md` or `{{TICKET_CMD}} --next main-plan.md` prints the file to create
11
- - a **spec file** in the TASK_DIR
9
+ - the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` when there is no external ticket)
10
+ - a **spec file** in the TICKET_DIR
12
11
 
13
- Identify and state these values before starting the protocol. If any of these pieces of information is missing, STOP AND ASK THE USER.
12
+ Identify TICKET_DIR and the spec file before starting the protocol. If either is missing, STOP AND ASK THE USER.
14
13
 
15
14
  ## Phases
16
15
 
@@ -113,13 +112,13 @@ Example:
113
112
  _For all plans (single or specialized)_, add a final step named "Write a Handover Document" with this content:
114
113
 
115
114
  ```markdown
116
- Write a **handover document**. This document must contain the list of all files you updated. Also, summarize the changes made in a very concise way. Add only relevant information that will help your teammates understand what's new. Do not mention obvious information. It's not a course or a tutorial, if there is nothing to explain, then do not explain. Write this handover document in `{PLAN_FILE_PATH}.summary.md`. Ignore lint errors (formatting issues) in this file. At the end, give the path of this handover file to the user.
115
+ Write a **handover document**. This document must contain the list of all files you updated. Also, summarize the changes made in a very concise way. Add only relevant information that will help your teammates understand what's new. Do not mention obvious information. It's not a course or a tutorial, if there is nothing to explain, then do not explain. Create its path by replacing the final `.md` in `{PLAN_FILE_PATH}` with `.summary.md`, then write the handover there. Ignore lint errors (formatting issues) in this file. At the end, give the path of this handover file to the user.
117
116
  ```
118
117
 
119
118
  Note:
120
119
 
121
120
  - This is a regular step, it should be numbered like the other steps. For example, if your plan has 5 steps, this becomes step 6.
122
- - Replace "{PLAN_FILE_PATH}" with the actual plan file path without extension (e.g., for plan `.plans/123/A2-plan-backend.md`, use `.plans/123/A2-plan-backend`, resulting in `.plans/123/A2-plan-backend.summary.md`).
121
+ - Replace "{PLAN_FILE_PATH}" with the complete plan file path, such as `.plans/123/A2-plan-backend.md`. Its handover path is `.plans/123/A2-plan-backend.summary.md`.
123
122
 
124
123
  ### 3.5 Common Footer for All Plans
125
124
 
@@ -193,7 +192,7 @@ Write a **main plan handover document**. This document should:
193
192
  - State "Completed" if the plan was executed successfully
194
193
  - Detail any issues encountered during execution
195
194
 
196
- Keep this handover very short. Do not combine or repeat the content of individual handovers. Write this document in `{PLAN_FILE_PATH}.summary.md`. Ignore lint errors (formatting issues) in this file.
195
+ Keep this handover very short. Do not combine or repeat the content of individual handovers. Create its path by replacing the final `.md` in `{PLAN_FILE_PATH}` with `.summary.md`, then write the handover there. Ignore lint errors (formatting issues) in this file.
197
196
 
198
197
  ---
199
198
 
@@ -204,34 +203,33 @@ Do not trust this plan blindly. Be sure you understand the codebase and all spec
204
203
 
205
204
  Note:
206
205
 
207
- - Replace "{PLAN_FILE_PATH}" with the actual plan file path without extension (e.g., for plan `.plans/123/A2-main-plan.md`, use `.plans/123/A2-main-plan`, resulting in `.plans/123/A2-main-plan.summary.md`)
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`.
208
207
 
209
208
  ## Phase 5. Writing Phase
210
209
 
211
210
  Write the plan file(s) according to the determined structure:
212
211
 
213
- Use `{{TICKET_CMD}} --next plan.md`, `{{TICKET_CMD}} --next main-plan.md`, or `{{TICKET_CMD}} --next plan-<descriptor>.md` to get the next number. Run one command per file.
212
+ Continue the current cycle. Immediately before writing each file, run `{{TICKET_CMD}} --next plan.md`, `{{TICKET_CMD}} --next main-plan.md`, or `{{TICKET_CMD}} --next plan-<descriptor>.md`. Append FILE_NAME to TICKET_DIR to get its path. Write that file before requesting the next one.
214
213
 
215
214
  **Single Plan**:
216
215
 
217
- - **Single plan**: `{TASK_DIR}/{CYCLE_LETTER}{FILE_NUMBER}-plan.md`
216
+ - **Single plan**: `{TICKET_DIR}{CYCLE_LETTER}{FILE_NUMBER}-plan.md`
218
217
  - Example: `.plans/123/A2-plan.md`
219
218
  - Handover: `.plans/123/A2-plan.summary.md`
220
219
  - No main plan needed
221
220
 
222
221
  **Multiple Plans**:
223
222
 
224
- - **Main plan**: `{TASK_DIR}/{CYCLE_LETTER}{FILE_NUMBER}-main-plan.md`
223
+ - **Main plan**: `{TICKET_DIR}{CYCLE_LETTER}{FILE_NUMBER}-main-plan.md`
225
224
  - Example: `.plans/123/A2-main-plan.md`
226
225
  - Handover: `.plans/123/A2-main-plan.summary.md` (written after all specialized plans complete)
227
- - **Specialized plans**: `{TASK_DIR}/{CYCLE_LETTER}{FILE_NUMBER}-plan-{DESCRIPTOR}.md`
226
+ - **Specialized plans**: `{TICKET_DIR}{CYCLE_LETTER}{FILE_NUMBER}-plan-{DESCRIPTOR}.md`
228
227
  - Use a descriptive name as `{DESCRIPTOR}` (e.g., work scope, stack area)
229
228
  - Example: `.plans/123/A3-plan-api.md`, `.plans/123/A4-plan-ui.md`
230
229
  - Handovers: `.plans/123/A3-plan-api.summary.md`, etc.
231
230
 
232
231
  **Important**:
233
232
 
234
- - Increment FILE_NUMBER for each plan file
235
233
  - Use lowercase, hyphenated descriptors for plan names (work scope descriptor)
236
234
  - When multiple plans are created, the main plan should be written first and have the lowest FILE_NUMBER
237
235
  - Be careful never to overwrite an existing file
@@ -4,19 +4,18 @@
4
4
 
5
5
  You need:
6
6
 
7
- - the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
8
- - the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{TICKET_CMD}} --next review.md --new-cycle` prints the file to create
7
+ - the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` when there is no external ticket)
9
8
  - {{BASE_BRANCH_RULE}}
10
9
 
11
- Identify and state these values before starting the protocol.
10
+ Identify TICKET_DIR and the base branch before starting the protocol.
12
11
 
13
12
  ## Overview
14
13
 
15
14
  We need a code review for this branch, compared to the base branch. A code review, above all, guarantees that the codebase stays healthy.
16
15
 
17
- You are the orchestrator: you scope the work, run one reviewer subagent per perspective, then merge their findings into a single report. Reviewers work with fresh eyes — they derive intent from the code and the diff. Neither you nor the reviewers read specs, plans, summaries, or any file content in TASK_DIR.
16
+ You are the orchestrator: you scope the work, run one reviewer subagent per perspective, then merge their findings into a single report. Reviewers work with fresh eyes — they derive intent from the code and the diff. Neither you nor the reviewers read specs, plans, summaries, or any file content in TICKET_DIR.
18
17
 
19
- Before starting, create your report as a new file `{CYCLE_LETTER}1-review.md` in the TASK_DIR, containing just the header — this reserves the filename. Write the report into it at the end.
18
+ Before starting, run `{{TICKET_CMD}} --next review.md --new-cycle` to start a new cycle. Append FILE_NAME to TICKET_DIR, then immediately create the report at that path with just the header. Creating the file reserves the filename. Write the report into it at the end.
20
19
 
21
20
  ## Phase 1. Scoping
22
21
 
@@ -2,12 +2,7 @@
2
2
 
3
3
  ## Pre-requisites
4
4
 
5
- You need:
6
-
7
- - the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
8
- - the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{TICKET_CMD}} --next spec.md --new-cycle` prints the file to create
9
-
10
- Identify and state these values before starting the protocol.
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
11
6
 
12
7
  ## Phases
13
8
 
@@ -49,7 +44,7 @@ Do not use your question tool. Always ask in plain text. Your questions will be
49
44
 
50
45
  ## Phase 3. Specification Phase
51
46
 
52
- After the user approves your proposal, write the specification in a markdown file in TASK_DIR. Compose the filename with the current CYCLE_LETTER and the next FILE_NUMBER, e.g. `A1-spec.md`. Do not overwrite an existing file.
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.
53
48
 
54
49
  - After the title, include a suggested commit message {{COMMIT_RULE}}. The shorter the better. Then list the required documentation and skills. List each doc file individually — never a folder. Always exclude `alignfirst` from skills. Omit any field with nothing to list. Example:
55
50
 
@@ -0,0 +1,19 @@
1
+ # AlignFirst Guide
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.
4
+
5
+ ## Choose a protocol
6
+
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
+
9
+ | Protocol | Purpose | Command |
10
+ | --- | --- | --- |
11
+ | Specification (`alspec` alias) | Investigate, discuss, and write a technical specification. | `{{CMD}} guide spec` |
12
+ | Planning (`alplan` alias) | Turn a specification into implementation plans. | `{{CMD}} guide plan` |
13
+ | Align-and-Do (`AAD`, `al` aliases) | Investigate, agree, implement, and summarize a small change. | `{{CMD}} guide aad` |
14
+ | Merge (`almerge` alias) | Merge an incoming branch and resolve conflicts. | `{{CMD}} guide merge` |
15
+ | Review (`alreview` alias) | Review committed branch changes against a base branch. | `{{CMD}} guide review` |
16
+ | Description (`aldescription` alias) | Write a concise description of implemented work. | `{{CMD}} guide description` |
17
+ | Catch up (`alcatchup`, `alcatchupaad`, `alcatchupspec` aliases) | Load the ticket history, then continue, start AAD, or start a specification. | `{{CMD}} ticket --catchup` |
18
+
19
+ For more detail on workflows and the ticket lifecycle, read `{{CMD}} guide overview`.
@@ -1,13 +0,0 @@
1
- # How to Catch Up With a Task
2
-
3
- ## Pre-requisites
4
-
5
- You need the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket).
6
-
7
- ## Steps
8
-
9
- 1. Determine TASK_DIR.
10
- 2. Read every `*request.md`, `*spec.md` and `*summary.md` file in TASK_DIR, in filename order.
11
- 3. When the user's message carries instructions, follow them with this context loaded, and write no synthesis. Otherwise reply with a short synthesis: what was requested, what was decided, what was done, what remains or is open.
12
-
13
- This protocol writes no file.