alignfirst 0.1.1 → 0.2.1

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,56 +1,10 @@
1
1
  # alignfirst
2
2
 
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
-
5
- ## Installation
6
-
7
- Install it globally:
8
-
9
- ```sh
10
- npm install -g alignfirst
11
- ```
12
-
13
- Or run the current version without installing it:
14
-
15
- ```sh
16
- npx alignfirst
17
- ```
18
-
19
- ## Setup with your agent
20
-
21
- Temporarily install the setup-guide skill:
22
-
23
- ```sh
24
- npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide
25
- ```
26
-
27
- Then ask your agent:
28
-
29
- ```text
30
- Use your alignfirst-setup-guide skill. Set up AlignFirst in this project. Ask before installing anything globally.
31
- ```
32
-
33
- The guide installs the selected components and configures the repository. Remove the setup-guide skill when setup is complete.
34
-
35
- ## Commands
36
-
37
- - `guide` — Print an AlignFirst protocol.
38
- - `ticket` — Resolve a ticket directory, load its history, or get its next file.
39
- - `sync` — Synchronize shared plans.
40
- - `plans` — Set up, check and archive plans.
41
- - `docmap` — Browse project documentation.
42
- - `conventions` — Print the effective project conventions.
43
- - `context` — Print the conventions, the documentation map when `docs/` exists, and the protocol aliases.
44
- - `config` — Report the effective project configuration.
45
- - `doctor` — Diagnose an AlignFirst setup.
46
-
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.
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 plans repository.
48
4
 
49
5
  ## Agent skills
50
6
 
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
-
53
- Install them globally:
7
+ Nine Agent Skill stubs expose the CLI as commands in Claude Code, Codex, GitHub Copilot, and Cursor. Install them globally:
54
8
 
55
9
  ```sh
56
10
  npx skills add https://github.com/paleo/alignfirst --global \
@@ -65,7 +19,13 @@ Install the `alignfirst` skill when a project's `AGENTS.md` does not run `npx al
65
19
  npx skills add https://github.com/paleo/alignfirst --global --skill alignfirst
66
20
  ```
67
21
 
68
- Start a new agent session to load the skills.
22
+ The skills run the CLI through `npx`, so installing it is optional.
23
+
24
+ ### Update the agent skills
25
+
26
+ ```sh
27
+ npx skills update --global
28
+ ```
69
29
 
70
30
  ## Workflows
71
31
 
@@ -87,17 +47,54 @@ To implement a plan, start a fresh agent context and ask it to execute the plan
87
47
 
88
48
  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`.
89
49
 
90
- ## Catchup output
50
+ ## Global CLI
51
+
52
+ To type `alignfirst` instead of `npx alignfirst`, install the CLI globally:
53
+
54
+ ```sh
55
+ npm install -g alignfirst
56
+ ```
57
+
58
+ ### Update the global CLI
91
59
 
92
- `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, with a notice telling the agent to read the relevant files and skip the plans.
60
+ ```sh
61
+ npm update -g alignfirst
62
+ ```
93
63
 
94
- ## Updates
64
+ ## Set up a project (with your agent)
65
+
66
+ Temporarily install the setup-guide skill:
95
67
 
96
68
  ```sh
97
- npm install -g alignfirst@latest
98
- npx skills update --global --yes
69
+ npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide
70
+ ```
71
+
72
+ Then ask your agent:
73
+
74
+ ```text
75
+ Use your alignfirst-setup-guide skill. What AlignFirst tooling could we add in this project?
99
76
  ```
100
77
 
78
+ The guide installs the selected components and configures the repository. You can remove the setup-guide skill once setup is complete.
79
+
80
+ ## CLI commands
81
+
82
+ - `guide` — Print an AlignFirst protocol.
83
+ - `ticket` — Resolve a ticket directory, load its history, or get its next file.
84
+ - `sync` — Synchronize shared plans.
85
+ - `plans` — Set up, check and archive plans.
86
+ - `docmap` — Browse project documentation.
87
+ - `conventions` — Print the effective project conventions.
88
+ - `context` — Print the conventions, the documentation map when `docs/` exists, and the protocol aliases.
89
+ - `config` — Report the effective project configuration.
90
+ - `doctor` — Diagnose an AlignFirst setup.
91
+
92
+ 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.
93
+
94
+ ### Catchup output
95
+
96
+ `alignfirst ticket [<id>] --catchup` prints the ticket's Markdown files, plans excluded.
97
+
101
98
  ## Upgrade from v1, v2, or v3
102
99
 
103
100
  Install the setup-guide skill and ask your agent to run its upgrade route:
@@ -110,7 +107,7 @@ npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guid
110
107
  Use your alignfirst-setup-guide skill. Upgrade AlignFirst in this project.
111
108
  ```
112
109
 
113
- _Note: the setup-guide skill can be removed safely after it's done._
110
+ You can remove the setup-guide skill once the upgrade is complete.
114
111
 
115
112
  ## License
116
113
 
@@ -128,9 +128,10 @@ function renderCoreGuide(ctx, placeholders) {
128
128
  }
129
129
  function buildGuidePlaceholders(ctx, protocol) {
130
130
  const pattern = ctx.projectConfig?.config.ticketIdPattern;
131
- const detection = pattern === undefined ? undefined : detectTicketFromBranch(ctx.cwd, pattern);
131
+ const template = ctx.projectConfig?.config.git?.branchNameTemplate;
132
+ const detection = detectTicketFromBranch(ctx.cwd, pattern, template);
132
133
  return {
133
- ticketCommand: detection?.kind === "detected" ? "{{CMD}} ticket" : "{{CMD}} ticket <id>",
134
+ ticketCommand: detection.kind === "detected" ? "{{CMD}} ticket" : "{{CMD}} ticket <id>",
134
135
  ticketDetection: renderTicketDetection(pattern, detection),
135
136
  plansState: renderPlansState(ctx),
136
137
  commitRule: renderCommitRule(ctx),
@@ -138,11 +139,11 @@ function buildGuidePlaceholders(ctx, protocol) {
138
139
  };
139
140
  }
140
141
  function renderTicketDetection(pattern, detection) {
142
+ if (detection.kind === "detected")
143
+ return `Current ticket: \`${detection.id}\` (from branch \`${detection.branch}\`). The id argument of \`{{CMD}} ticket\` is optional and defaults to it; pass an id only when the user names another ticket.`;
141
144
  if (pattern === undefined)
142
145
  return "Ask the user for the ticket ID when it is not given.";
143
- if (detection?.kind === "detected")
144
- return `Current ticket: \`${detection.id}\` (from branch \`${detection.branch}\`). The id argument of \`{{CMD}} ticket\` is optional and defaults to it; pass an id only when the user names another ticket.`;
145
- if (detection?.kind === "noMatch")
146
+ if (detection.kind === "noMatch")
146
147
  return `No ticket id on branch \`${detection.branch}\`. Ask the user for the id.`;
147
148
  return "No ticket id: detached HEAD. Ask the user for the id.";
148
149
  }
@@ -5,7 +5,7 @@ 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, deduceTicketFromExisting, nextFilePosition, peekSideTicket, reserveSideTicket, resolveTicketDir, validateTicketId, } from "../plans/ticket.js";
8
+ import { detectTicketFromBranch, 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]
@@ -70,10 +70,11 @@ function parseTicketArgs(ctx, args, usage) {
70
70
  if (values.catchup &&
71
71
  (values.side || values.next !== undefined || values.json || values["dry-run"]))
72
72
  throw new CliError(`--catchup cannot be combined with --side, --next, --json, or --dry-run.\n\n${usage}`);
73
+ const next = values.next === undefined ? undefined : resolveNextRequest(normalized.requests, usage);
73
74
  const resolution = resolveTicketId(ctx, positionals[0], values);
74
75
  return {
75
76
  ...resolution,
76
- next: values.next === undefined ? undefined : resolveNextRequest(normalized.requests, usage),
77
+ next,
77
78
  newCycle: values["new-cycle"],
78
79
  json: values.json,
79
80
  dryRun: values["dry-run"],
@@ -125,16 +126,23 @@ function validateNextFilename(filename) {
125
126
  function resolveTicketId(ctx, positional, flags) {
126
127
  const pattern = ctx.projectConfig?.config.ticketIdPattern;
127
128
  if (positional !== undefined) {
128
- validateTicketId(positional, pattern);
129
+ validateTicketId(positional);
129
130
  return { id: positional };
130
131
  }
131
132
  if (flags.side)
132
133
  return { id: flags["dry-run"] ? peekSideTicket(ctx.cwd) : reserveSideTicket(ctx.cwd) };
134
+ const template = ctx.projectConfig?.config.git?.branchNameTemplate;
135
+ const detection = detectTicketFromBranch(ctx.cwd, pattern, template);
136
+ if (detection.kind === "detected") {
137
+ validateTicketId(detection.id);
138
+ return detection;
139
+ }
133
140
  if (pattern === undefined)
134
141
  return deduceTicketFromExisting(ctx.cwd, { sideAllowed: !flags.catchup });
135
- const deduced = deduceTicketFromBranch(ctx.cwd, pattern);
136
- validateTicketId(deduced.id, pattern);
137
- return deduced;
142
+ if (detection.kind === "noBranch")
143
+ throw new CliError("Cannot deduce a ticket id from a detached HEAD.");
144
+ const templateDetail = template?.includes("{TICKET_ID}") ? ` and template "${template}"` : "";
145
+ throw new CliError(`Cannot deduce a ticket id from branch "${detection.branch}" with pattern "${pattern}"${templateDetail}.`);
138
146
  }
139
147
  function resolveTicket(ctx, options) {
140
148
  if (options.side && !options.dryRun)
@@ -18,8 +18,8 @@ export function renderConventions(ctx) {
18
18
  function renderTicketIds(ctx) {
19
19
  const pattern = ctx.projectConfig?.config.ticketIdPattern;
20
20
  if (pattern === undefined)
21
- return "Ticket IDs: no configured format; ask the user for the ID. Without an external ticket, use the next `side-N`.";
22
- return `Ticket IDs: \`${pattern}\`; infer a matching ID from the branch. Without an external ticket, use the next \`side-N\`.`;
21
+ return "Ticket IDs: no configured format; ask the user for the ID. Without a ticket, or when the user asks for a side ticket, use the next `side-N`.";
22
+ return `Ticket IDs: \`${pattern}\`; infer a matching ID from the branch. Without a ticket, or when the user asks for a side ticket, use the next \`side-N\`.`;
23
23
  }
24
24
  function renderBranchNames(ctx) {
25
25
  const template = ctx.projectConfig?.config.git?.branchNameTemplate;
@@ -50,10 +50,10 @@ function renderPlans(ctx) {
50
50
  try {
51
51
  const mode = resolvePlansMode(ctx.cwd, ctx.form);
52
52
  const folder = ctx.projectConfig?.config.plans?.folder;
53
- const sharedFolder = folder === undefined ? "" : ` (shared folder \`${folder}\`)`;
53
+ const sharedFolder = folder === undefined ? "" : ` (shared folder \`${folder}\`, a separate git repository)`;
54
54
  const base = mode.kind === "shared"
55
- ? `Plans: use \`.plans\`${sharedFolder}; keep it out of product commits, and run \`${ctx.form} sync\` after changes.`
56
- : "Plans: use `.plans`; keep it out of product commits.";
55
+ ? `Plans: use \`.plans\`${sharedFolder}; run \`${ctx.form} sync\` after changes.`
56
+ : "Plans: use `.plans`.";
57
57
  const archival = ctx.projectConfig?.config.plans?.autoArchive === true
58
58
  ? " Automatic archival is enabled."
59
59
  : "";
@@ -37,11 +37,10 @@ export interface NextFilePosition {
37
37
  export declare function nextFilePosition(entries: readonly string[], newCycle: boolean): NextFilePosition;
38
38
  export declare function listEntries(dir: string): TicketEntry[];
39
39
  export declare function isPathSafeTicketId(id: string): boolean;
40
- export declare function validateTicketId(id: string, pattern?: string): void;
41
- export declare function detectTicketFromBranch(cwd: string, pattern: string): TicketDetection;
40
+ export declare function validateTicketId(id: string): void;
41
+ export declare function detectTicketFromBranch(cwd: string, pattern?: string, template?: string): TicketDetection;
42
42
  export interface DeduceFromExistingOptions {
43
43
  sideAllowed: boolean;
44
44
  }
45
45
  /** Without a ticket id pattern, the branch can still name an existing ticket directory. */
46
46
  export declare function deduceTicketFromExisting(cwd: string, { sideAllowed }: DeduceFromExistingOptions): DeducedTicket;
47
- export declare function deduceTicketFromBranch(cwd: string, pattern: string): DeducedTicket;
@@ -7,9 +7,10 @@ import { gitOutputOrUndefined } from "../git.js";
7
7
  import { archivesDir, isTicketName, plansDir } from "./layout.js";
8
8
  const FILE_PREFIX = /^([A-Z])(\d+)-/;
9
9
  const SIDE_TICKET = /^side-(\d+)$/;
10
+ const BRANCH_SEPARATORS = "[/_.-]";
11
+ const SIDE_TICKET_BRANCH = new RegExp(`^side-\\d+(?=$|${BRANCH_SEPARATORS})`);
10
12
  const PATH_SAFE_TICKET = /^[A-Za-z0-9._-]+$/;
11
13
  const ENTRY_ORDER = new Intl.Collator("en", { numeric: true });
12
- const BRANCH_SEPARATORS = "[/_.-]";
13
14
  const LISTED_TICKETS = 10;
14
15
  export function resolveTicketDir(cwd, id, { dryRun }) {
15
16
  const dir = join(plansDir(cwd), id);
@@ -104,22 +105,49 @@ function statOrLink(path) {
104
105
  export function isPathSafeTicketId(id) {
105
106
  return id !== "." && !id.includes("..") && PATH_SAFE_TICKET.test(id);
106
107
  }
107
- export function validateTicketId(id, pattern) {
108
+ export function validateTicketId(id) {
108
109
  if (!isPathSafeTicketId(id) || !isTicketName(id))
109
110
  throw new CliError(`Invalid ticket id: ${id}`);
110
- if (pattern !== undefined && !new RegExp(pattern).test(id) && !SIDE_TICKET.test(id))
111
- throw new CliError(`Ticket id "${id}" does not match ticketIdPattern "${pattern}".`);
112
111
  }
113
- export function detectTicketFromBranch(cwd, pattern) {
112
+ export function detectTicketFromBranch(cwd, pattern, template) {
114
113
  const branch = currentBranch(cwd);
115
114
  if (branch === undefined)
116
115
  return { kind: "noBranch" };
117
- const unanchored = pattern.replace(/^\^/, "").replace(/\$$/, "");
118
- const match = new RegExp(unanchored).exec(branch);
116
+ const sideTicket = SIDE_TICKET_BRANCH.exec(branch);
117
+ if (sideTicket)
118
+ return { kind: "detected", id: sideTicket[0], branch };
119
+ if (pattern === undefined)
120
+ return { kind: "noMatch", branch };
121
+ if (template?.includes("{TICKET_ID}")) {
122
+ const match = branchPatternFromTemplate(template, unanchorPattern(pattern)).exec(branch);
123
+ const id = match?.[1];
124
+ if (id === undefined || !isPathSafeTicketId(id) || !isTicketName(id))
125
+ return { kind: "noMatch", branch };
126
+ return { kind: "detected", id, branch };
127
+ }
128
+ const match = new RegExp(unanchorPattern(pattern)).exec(branch);
119
129
  if (!match)
120
130
  return { kind: "noMatch", branch };
121
131
  return { kind: "detected", id: match[0], branch };
122
132
  }
133
+ function unanchorPattern(pattern) {
134
+ return pattern.replace(/^\^/, "").replace(/\$$/, "");
135
+ }
136
+ function branchPatternFromTemplate(template, ticketPattern) {
137
+ const placeholder = /\{[^{}]+\}/g;
138
+ let source = "";
139
+ let cursor = 0;
140
+ for (const match of template.matchAll(placeholder)) {
141
+ source += escapeRegexLiteral(template.slice(cursor, match.index));
142
+ source += match[0] === "{TICKET_ID}" ? `((?:${ticketPattern})|side-\\d+)` : ".+";
143
+ cursor = match.index + match[0].length;
144
+ }
145
+ source += escapeRegexLiteral(template.slice(cursor));
146
+ return new RegExp(`^${source}$`);
147
+ }
148
+ function escapeRegexLiteral(value) {
149
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
150
+ }
123
151
  function currentBranch(cwd) {
124
152
  const branch = gitOutputOrUndefined(cwd, "branch", "--show-current");
125
153
  return branch === undefined || branch === "" ? undefined : branch;
@@ -157,7 +185,7 @@ function newestModification(dir) {
157
185
  return new Date(Math.max(...entries.map((entry) => entry.modifiedAt.getTime())));
158
186
  }
159
187
  function branchNamesTicket(branch, id) {
160
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
188
+ const escaped = escapeRegexLiteral(id);
161
189
  return new RegExp(`(^|${BRANCH_SEPARATORS})${escaped}($|${BRANCH_SEPARATORS})`).test(branch);
162
190
  }
163
191
  function renderMissingTicketId(tickets, sideAllowed) {
@@ -175,11 +203,3 @@ function renderMissingTicketId(tickets, sideAllowed) {
175
203
  lines.push(` … ${rest} more`);
176
204
  return `No ticket id given. Existing tickets:\n${lines.join("\n")}\n${hint}`;
177
205
  }
178
- export function deduceTicketFromBranch(cwd, pattern) {
179
- const result = detectTicketFromBranch(cwd, pattern);
180
- if (result.kind === "noBranch")
181
- throw new CliError("Cannot deduce a ticket id from a detached HEAD.");
182
- if (result.kind === "noMatch")
183
- throw new CliError(`Cannot deduce a ticket id from branch "${result.branch}" with pattern "${pattern}".`);
184
- return result;
185
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alignfirst",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "license": "CC0-1.0",
5
5
  "author": "Thomas MUR",
6
6
  "description": "The AlignFirst CLI: protocols, plans and docs in one command.",
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Ticket Directory
4
4
 
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.
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 a side ticket: work without a ticket.
6
6
 
7
7
  {{TICKET_DETECTION}}
8
8
 
@@ -10,7 +10,7 @@ TICKET_DIR holds a ticket's work files and includes a trailing slash. TICKET_ID
10
10
 
11
11
  {{PLANS_STATE}}
12
12
 
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.
13
+ When the user says there is no ticket or asks for a side 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
15
  ## Work Files
16
16
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Prerequisites
4
4
 
5
- Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
6
6
 
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Prerequisites
4
4
 
5
- Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
6
6
 
7
7
  ## Steps
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Prerequisites
4
4
 
5
- Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
6
6
 
7
7
  ---
8
8
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  You need:
8
8
 
9
- - the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` when there is no external ticket)
9
+ - the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` for a side ticket, when there is no ticket)
10
10
  - a **spec file** in the TICKET_DIR
11
11
 
12
12
  Identify TICKET_DIR and the spec file before starting the protocol. If either is missing, STOP AND ASK THE USER.
@@ -4,7 +4,7 @@
4
4
 
5
5
  You need:
6
6
 
7
- - the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` when there is no external ticket)
7
+ - the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` for a side ticket, when there is no ticket)
8
8
  - {{BASE_BRANCH_RULE}}
9
9
 
10
10
  Identify TICKET_DIR and the base branch before starting the protocol.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Prerequisites
4
4
 
5
- Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no external ticket).
5
+ Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
6
6
 
7
7
  ## Phases
8
8