@chief-clancy/plan 0.1.0 → 0.2.0

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
@@ -8,7 +8,7 @@
8
8
  npx @chief-clancy/plan
9
9
  ```
10
10
 
11
- Fetch backlog tickets from your board, explore the codebase, and generate structured implementation plans posted as comments for human review. Works with board credentials no full pipeline required.
11
+ Generate structured implementation plans for your codebase. Plan from board tickets (posted as ticket comments) or fully offline from local Clancy briefs (read from `.clancy/briefs/`, with plans saved to `.clancy/plans/`). No full pipeline required.
12
12
 
13
13
  ## What it does
14
14
 
@@ -25,25 +25,62 @@ The `/clancy:plan` slash command explores your codebase, runs a feasibility scan
25
25
  ## How it works
26
26
 
27
27
  1. **Install:** `npx @chief-clancy/plan` — choose global or local
28
- 2. **Configure board:** `/clancy:board-setup` — connect to your project board
29
- 3. **Run:** `/clancy:plan PROJ-123` plan a specific ticket
30
-
31
- Plans are posted as comments on the ticket for human review.
28
+ 2. **Pick an input source:**
29
+ - **Board tickets** — run `/clancy:board-setup` to configure credentials, then `/clancy:plan PROJ-123`. Plans are posted as comments on the ticket.
30
+ - **Local briefs** — point at a Clancy brief file with `/clancy:plan --from .clancy/briefs/<brief>.md`. Plans are saved to `.clancy/plans/<slug>-<row>.md`.
31
+ 3. **Review and revise:** add a `## Feedback` section to the plan (board comment or local file), then re-run `/clancy:plan` to revise.
32
32
 
33
33
  ## Input modes
34
34
 
35
- | Mode | Example | Board needed? |
36
- | --------------- | ------------------------------------------- | --------------------------- |
37
- | Specific ticket | `/clancy:plan PROJ-123`, `/clancy:plan #42` | Yes (`/clancy:board-setup`) |
38
- | Batch | `/clancy:plan 3` | Yes (`/clancy:board-setup`) |
39
- | Queue (default) | `/clancy:plan` | Yes (`/clancy:board-setup`) |
35
+ | Mode | Example | Board needed? |
36
+ | --------------- | ----------------------------------------------------- | --------------------------- |
37
+ | Local brief | `/clancy:plan --from .clancy/briefs/add-dark-mode.md` | No |
38
+ | Specific ticket | `/clancy:plan PROJ-123`, `/clancy:plan #42` | Yes (`/clancy:board-setup`) |
39
+ | Batch | `/clancy:plan 3` | Yes (`/clancy:board-setup`) |
40
+ | Queue (default) | `/clancy:plan` | Yes (`/clancy:board-setup`) |
41
+
42
+ The local brief path can be any Clancy-format markdown file — `--from` is not tied to `@chief-clancy/brief`. You can hand-write a brief (just include a `## Problem Statement` or `## Ticket Decomposition` heading) and point at it from anywhere on disk. Briefs generated by `@chief-clancy/brief` happen to use a `YYYY-MM-DD-slug.md` naming convention, but the date prefix is optional — Step 3a strips it when present and uses the full filename otherwise.
40
43
 
41
44
  ## Flags
42
45
 
43
- | Flag | Description |
44
- | --------- | ------------------------------------ |
45
- | `--afk` | Auto-confirm all prompts |
46
- | `--fresh` | Discard existing plan and start over |
46
+ | Flag | Description |
47
+ | ------------------- | ----------------------------------------------------------------------- |
48
+ | `--from <path> [N]` | Plan from a local Clancy brief file. Optional row number targets a row. |
49
+ | `--afk` | Auto-confirm all prompts. With `--from`, plans every unplanned row. |
50
+ | `--fresh` | Discard the existing plan and start over. |
51
+ | `--list` | Show inventory of existing local plans in `.clancy/plans/` and stop. |
52
+
53
+ ## Local planning workflow
54
+
55
+ The `--from` flag lets you go from idea to implementation plan without ever touching a board:
56
+
57
+ ```bash
58
+ # 1. Generate a brief (uses @chief-clancy/brief)
59
+ /clancy:brief "Add dark mode support"
60
+ # Saved to .clancy/briefs/2026-04-08-add-dark-mode.md
61
+
62
+ # 2. Plan the first decomposition row
63
+ /clancy:plan --from .clancy/briefs/2026-04-08-add-dark-mode.md
64
+ # Saved to .clancy/plans/add-dark-mode-1.md
65
+
66
+ # 3. Plan a specific row
67
+ /clancy:plan --from .clancy/briefs/2026-04-08-add-dark-mode.md 3
68
+ # Saved to .clancy/plans/add-dark-mode-3.md
69
+
70
+ # 4. Or plan every unplanned row in one pass
71
+ /clancy:plan --afk --from .clancy/briefs/2026-04-08-add-dark-mode.md
72
+
73
+ # 5. Review what's been planned
74
+ /clancy:plan --list
75
+
76
+ # 6. Revise — edit the plan file and add a `## Feedback` section
77
+ /clancy:plan --from .clancy/briefs/2026-04-08-add-dark-mode.md 3
78
+ # Detects feedback, regenerates with a `### Changes From Previous Plan` section
79
+ ```
80
+
81
+ Plans are tracked in the brief file itself via a `<!-- planned:1,2,3 -->` marker, so re-running advances to the next unplanned row automatically.
82
+
83
+ To approve and implement plans you'll need the full pipeline (`npx chief-clancy`).
47
84
 
48
85
  ## Board ticket mode
49
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chief-clancy/plan",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Implementation planner for Claude Code — decompose briefs into actionable plans",
6
6
  "author": "Alex Clapperton",
@@ -37,3 +37,36 @@ describe('commands directory structure', () => {
37
37
  expect(issues).toEqual([]);
38
38
  });
39
39
  });
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // plan.md content assertions
43
+ // ---------------------------------------------------------------------------
44
+
45
+ describe('plan command', () => {
46
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
47
+
48
+ it('documents --from flag', () => {
49
+ expect(content).toContain('--from');
50
+ });
51
+
52
+ it('shows --from example', () => {
53
+ expect(content).toContain('/clancy:plan --from');
54
+ });
55
+
56
+ it('notes --from cannot combine with ticket key', () => {
57
+ expect(content).toContain('Cannot be combined with a ticket key');
58
+ });
59
+
60
+ it('documents row selection with bare integer', () => {
61
+ expect(content).toContain('row number to target');
62
+ });
63
+
64
+ it('documents --list flag', () => {
65
+ expect(content).toContain('--list');
66
+ expect(content).toContain('inventory');
67
+ });
68
+
69
+ it('shows --list example', () => {
70
+ expect(content).toContain('/clancy:plan --list');
71
+ });
72
+ });
@@ -1,22 +1,26 @@
1
1
  # /clancy:plan
2
2
 
3
- Fetch backlog tickets from the board, explore the codebase, and generate structured implementation plans. Plans are posted as comments on the ticket for human review.
3
+ Fetch backlog tickets from the board, explore the codebase, and generate structured implementation plans. Plans are posted as comments on the ticket for human review. With `--from`, plans from local brief files are saved to `.clancy/plans/` instead, with an optional board comment when credentials are available.
4
4
 
5
5
  Accepts optional arguments:
6
6
 
7
+ - **From brief:** `/clancy:plan --from .clancy/briefs/slug.md` — plan from a local brief file. Cannot be combined with a ticket key or batch number. Optionally pass a row number to target a specific decomposition row (e.g. `/clancy:plan --from brief.md 3`). Without a row number, plans the first unplanned row.
7
8
  - **Batch mode:** `/clancy:plan 3` — plan up to 3 tickets from the queue
8
9
  - **Specific ticket:** `/clancy:plan PROJ-123`, `/clancy:plan #42`, `/clancy:plan ENG-42` — plan a single ticket by key
9
10
  - **Fresh start:** `--fresh` — discard any existing plan and start over
10
11
  - **Skip confirmations:** `--afk` — auto-confirm all prompts (for automation)
12
+ - **List plans:** `--list` — show inventory of existing local plans in `.clancy/plans/` and stop
11
13
 
12
14
  Examples:
13
15
 
16
+ - `/clancy:plan --from .clancy/briefs/add-dark-mode.md` — plan from a local brief (any Clancy-format markdown file path works)
14
17
  - `/clancy:plan` — plan 1 ticket from queue
15
18
  - `/clancy:plan 3` — plan 3 tickets from queue
16
19
  - `/clancy:plan PROJ-123` — plan a specific Jira/Linear ticket
17
20
  - `/clancy:plan #42` — plan a specific GitHub issue
18
21
  - `/clancy:plan --fresh PROJ-123` — discard existing plan and start over
22
+ - `/clancy:plan --list` — show inventory of all local plans
19
23
 
20
24
  @.claude/clancy/workflows/plan.md
21
25
 
22
- Follow the plan workflow above. For each ticket: run the feasibility scan, explore the codebase, generate the plan, and post it as a comment. Do not implement anything — planning only.
26
+ Follow the plan workflow above. For each ticket or brief: run the feasibility scan, explore the codebase, and generate the plan. In board mode, post it as a comment. With `--from`, save it to `.clancy/plans/`. Do not implement anything — planning only.
@@ -2,12 +2,18 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- Fetch backlog tickets from the board, explore the codebase, and generate structured implementation plans. Plans are posted as comments on the ticket for human review. Does not implement anything — planning only. In **terminal mode**, use `/clancy:approve-plan` to promote plans. In **standalone mode** or **standalone+board mode**, install the full pipeline (`npx chief-clancy`) to promote plans.
5
+ Fetch backlog tickets from the board, explore the codebase, and generate structured implementation plans. In board mode, plans are posted as comments on the ticket for human review. With `--from`, plans from local brief files are saved to `.clancy/plans/`, with an optional board comment when credentials are available. Does not implement anything — planning only. In **terminal mode**, use `/clancy:approve-plan` to promote plans. In **standalone mode** or **standalone+board mode**, install the full pipeline (`npx chief-clancy`) to promote plans.
6
6
 
7
7
  ---
8
8
 
9
9
  ## Step 1 — Preflight checks
10
10
 
11
+ ### 0. Short-circuit on `--list`
12
+
13
+ If the `--list` flag is present in the arguments, skip the rest of Step 1 entirely (no installation detection, no `git fetch`, no docs check, no branch freshness prompt) and jump straight to Step 8 (Plan inventory). The inventory is filesystem-only and never needs board credentials, network access, or a clean working tree.
14
+
15
+ `--list` always wins over other flags: if `--list` is combined with `--from`, `--fresh`, a ticket key, or a batch number, the inventory is displayed and the other flags are ignored for this run.
16
+
11
17
  ### 1. Detect installation context
12
18
 
13
19
  Check for `.clancy/.env`:
@@ -74,6 +80,11 @@ If the user declines, stop. If they confirm, continue without docs context.
74
80
 
75
81
  Parse the arguments passed to the command:
76
82
 
83
+ - **`--from {path} [N]`** — From local brief file mode. Read a Clancy brief file and plan from its content. A bare integer after the path selects row N from the decomposition table (e.g. `--from brief.md 3` selects row 3). Without a number, defaults to the first unplanned row. Cannot be combined with a ticket reference (`Cannot use both a ticket reference and --from. Use one or the other.`). Cannot be combined with a batch number (`Cannot use batch mode with --from. Use --from with a brief file path.`). Validate the file:
84
+ - File does not exist: `File not found: {path}` Stop.
85
+ - File is empty: `File is empty: {path}` Stop.
86
+ - File > 50KB: Warn `Large file ({size}KB). Clancy will use the first ~50KB for context.` Truncate internally, continue.
87
+ - File is not a Clancy brief: must contain `## Problem Statement` or `## Ticket Decomposition`. If neither found: `File does not appear to be a Clancy brief. Use /clancy:brief to generate one, or /clancy:brief --from {path} to brief from a raw file.` Stop.
77
88
  - **No argument:** plan 1 ticket from the queue
78
89
  - **Numeric argument** (e.g. `/clancy:plan 3`): plan up to N tickets from the queue, cap at 10
79
90
  - **Specific ticket key:** plan a single ticket by key, with per-platform validation:
@@ -86,8 +97,13 @@ Parse the arguments passed to the command:
86
97
  [2] Plan 10 tickets (max batch)
87
98
  ```
88
99
  - **`--fresh`:** discard any existing plan and start over from scratch. This is NOT re-plan with feedback — it ignores existing plans entirely.
100
+ - **`--list`:** display the plan inventory (all files in `.clancy/plans/`) and stop. No plan is generated, no board API calls are made.
89
101
  - Arguments can appear in any order (e.g. `/clancy:plan --fresh PROJ-123` or `/clancy:plan PROJ-123 --fresh`)
90
102
 
103
+ ### --list flag handling
104
+
105
+ If `--list` is present, jump to Step 8 (Plan inventory) and stop — the short-circuit at the top of Step 1 also handles this case for runs that never reached Step 2. The flag works in any installation mode (standalone, standalone+board, terminal) — `--list` is filesystem-only and never touches the board. When combined with `--from`, `--fresh`, a ticket key, or a batch number, `--list` wins and the other arguments are ignored.
106
+
91
107
  If N > 10: `Maximum batch size is 10. Planning 10 tickets.`
92
108
 
93
109
  If N >= 5: display a confirmation (skip in AFK mode — `--afk` flag or `CLANCY_MODE=afk`):
@@ -98,12 +114,19 @@ Planning {N} tickets — each requires codebase exploration. Continue? [Y/n]
98
114
 
99
115
  ### Standalone board-ticket guard
100
116
 
117
+ Skip this guard entirely if `--list` was passed — the inventory step is filesystem-only and runs in any installation mode (the Step 1 short-circuit normally handles this, but state it here too so the guard never blocks `--list`).
118
+
119
+ **`--from` mode** bypasses the standalone board-ticket guard entirely — no board credentials are needed for local brief planning. `--list` bypasses the guard for the same reason: the inventory reads only `.clancy/plans/`. The guard evaluates the resolved input mode (ticket/batch/no-arg), not flags like `--afk`.
120
+
101
121
  If running in **standalone mode** (Step 1 detected no `.clancy/.env`) and the resolved input mode is **board ticket**, **batch mode**, or **no argument** (which defaults to queue fetch):
102
122
 
103
123
  ```
104
124
  Board credentials not found. To plan from board tickets:
105
125
  /clancy:board-setup — configure board credentials (standalone)
106
126
  npx chief-clancy — install the full pipeline
127
+
128
+ To plan from a local brief file:
129
+ /clancy:plan --from .clancy/briefs/{slug}.md
107
130
  ```
108
131
 
109
132
  Stop.
@@ -112,6 +135,105 @@ In **standalone+board mode**, board ticket and batch modes proceed normally —
112
135
 
113
136
  ---
114
137
 
138
+ ## Step 3a — Gather from local brief (--from mode only)
139
+
140
+ If `--from {path}` was set in Step 2, run this step **instead of** Steps 3, 3b, and 3c (all board-mode ticket fetching, existing plan detection via comments, and feedback loop). Skip all of them entirely — Step 3a handles its own existing plan check below.
141
+
142
+ ### Read and parse the brief file
143
+
144
+ Read the file at `{path}`. Extract the following sections:
145
+
146
+ - **Source** field (`**Source:**` line) — the source value (e.g. `[#50] Redesign settings page`, `"Add dark mode"`, `docs/rfcs/auth-rework.md`). Used for the plan header and display identifier.
147
+ - `## Problem Statement` — used as the ticket description equivalent for plan context. Optional — if missing, use the brief's overall content as context instead.
148
+ - `## Goals` — used alongside Problem Statement for plan context. Optional.
149
+ - `## Ticket Decomposition` — the decomposition table. Parse the table rows for row-level planning.
150
+
151
+ ### Parse decomposition table rows
152
+
153
+ Parse the `## Ticket Decomposition` table to extract plannable rows. A valid row must have at minimum a row number (column 1) and a title (column 2). Rows are 1-indexed, corresponding to the order of data rows in the decomposition table (excluding header and separator rows).
154
+
155
+ **Missing table:** If `## Ticket Decomposition` is missing or has no data rows, treat the entire brief as a single planning unit (row 1). Warn: `No decomposition table found in {path}. Planning the brief as a single item.`
156
+
157
+ **Malformed rows:** Skip malformed rows with a warning: `Skipping malformed row {line}`. If ALL rows are malformed, treat as missing table (single planning unit).
158
+
159
+ ### Row selection
160
+
161
+ Check for an existing `<!-- planned:1,2,3 -->` marker comment in the brief file. If no marker exists, no rows have been planned.
162
+
163
+ **Row validation:** If `--from path N` was specified, validate N:
164
+
165
+ - N must be a positive integer. If N <= 0 or not an integer: `Row number must be a positive integer.` Stop.
166
+ - N must exist in the decomposition table. If N > total rows: `Row {N} not found. The brief has {max} decomposition rows.` Stop.
167
+
168
+ When `--from` is present, a bare integer is always interpreted as a row number, never as a batch count.
169
+
170
+ **Row targeting:**
171
+
172
+ - If `--from path N` was specified in Step 2, select row N specifically. If row N is already in the planned marker, do not stop here — defer the "already planned" decision to the later "Existing local plan check," after the plan slug/path is known and the workflow can inspect the existing plan file for `## Feedback` and apply `--fresh` rules.
173
+ - Without a number, select the first unplanned row — the first row whose number is NOT in the planned set.
174
+ - If no number was specified and all rows are planned: `All decomposition rows have been planned. Use --fresh to re-plan a specific row.` Stop.
175
+
176
+ **`--fresh` with row selection:** When `--fresh` is used to re-plan a specific row, the row's existing plan file is overwritten. The planned marker is not modified — the row was already in the marker and stays there (no remove-and-re-add cycle needed).
177
+
178
+ **`--fresh` + `--afk`:** Re-plans all rows from scratch. Deletes all existing plan files for this brief, clears the planned marker entirely, then plans all rows sequentially.
179
+
180
+ **Multi-row mode:** `--from` with `--afk` plans all unplanned rows sequentially — loop through each unplanned row, running Steps 4-5a for each one. Without `--afk`, plan exactly one row (first unplanned, or the specified row) then stop.
181
+
182
+ ### Update planned marker
183
+
184
+ After planning each row, update the `<!-- planned:N -->` marker in the brief file:
185
+
186
+ - If no marker exists, append `<!-- planned:{row} -->` to the file. If the last `---` line in the file is a brief footer (trailing `---`), insert before it. Otherwise append at EOF.
187
+ - If a marker exists, update it: `<!-- planned:1,2,3 -->` → `<!-- planned:1,2,3,4 -->`.
188
+
189
+ **Concurrency note:** The planned marker is file-based and not concurrency-safe. Running multiple `--from` commands against the same brief simultaneously may produce duplicate plans.
190
+
191
+ Pass the selected row's context (title, description, size, dependencies) along with the brief's Problem Statement, Goals, and Source to Step 4 as the ticket context.
192
+
193
+ ### --from mode Step 4 adaptations
194
+
195
+ When running in `--from` mode, Steps 4a-4f have these adaptations:
196
+
197
+ - **Display identifier:** Use the slug (derived from brief filename) wherever board mode uses `{KEY}`. Progress display shows `[{slug}#{row}] {row title}` instead of `[{KEY}] {Title}`.
198
+ - **Step 4a (feasibility scan):** Run the scan normally (the brief content replaces the ticket description). If infeasible, skip — but do NOT post a "Clancy skipped" comment to any board (no board ticket). Use the canonical Step 6 log format, with `{slug}` in place of `{KEY}` for the identifier.
199
+ - **Step 4b (QA return detection):** Skip entirely in `--from` mode — there is no implementation history to check.
200
+ - **Step 4c-4e:** Run normally — codebase context, Figma, and exploration work the same regardless of input source.
201
+ - **Step 4f (generate plan):** Use the local plan header format (Source/Brief fields) instead of the board header format (Ticket field). See Step 5a for the header template.
202
+
203
+ ### Existing local plan check
204
+
205
+ Before planning each row, check for an existing plan file at `.clancy/plans/{slug}-{row-number}.md` where the slug is derived from the brief filename:
206
+
207
+ **Slug generation:** strip the `YYYY-MM-DD-` date prefix (pattern: 4 digits, dash, 2 digits, dash, 2 digits, dash) if present, then strip the `.md` extension. If no date prefix matches, use the full filename minus extension.
208
+
209
+ If an existing plan file is found, scan it for a `## Feedback` section. Match `## Feedback` as a heading at the start of a line (not inside code fences or backtick spans). The feedback section is appended by the user to request revisions.
210
+
211
+ | Condition | Behaviour |
212
+ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
213
+ | No existing plan | Proceed to Step 4 |
214
+ | Existing plan + `--fresh` | Delete and overwrite the existing plan file. `--fresh` takes precedence over feedback — any `## Feedback` section is discarded. Proceed to Step 4. |
215
+ | Existing plan + `## Feedback` section | Revise: read existing plan + feedback, generate revised plan with `### Changes From Previous Plan` section |
216
+ | Existing plan + no feedback + no `--fresh` | Stop: `Already planned. Add a ## Feedback section to revise, or use --fresh to start over.` |
217
+
218
+ ### Read feedback for revision
219
+
220
+ When revising from feedback (auto-detected from a real `## Feedback` heading in the local plan file), read the entire `## Feedback` section content — from that heading until the next real `##` heading at the start of a line, outside fenced code blocks and inline backtick spans, or EOF. If multiple `## Feedback` sections exist (the user added more after a previous revision), concatenate all sections in order.
221
+
222
+ Pass this feedback to the plan generation step (Step 4f) as additional context, alongside the brief's Problem Statement, Goals, and the row context. The `## Feedback` section is the user's revision request — typically natural language describing what to change, what was missing, or what went wrong.
223
+
224
+ **Revision procedure:** When revising, the planner SHOULD:
225
+
226
+ - Skip Step 4a (feasibility scan) — the previous plan already passed feasibility
227
+ - Skip Step 4b (QA return detection) — N/A in `--from` mode
228
+ - Re-run Step 4c-4e (codebase context, Figma, exploration) only if the feedback explicitly references new files, components, or areas not covered by the previous plan. Otherwise reuse the existing exploration.
229
+ - Run Step 4f to regenerate the plan, addressing the feedback while preserving acceptance criteria and affected files that the feedback does not touch
230
+
231
+ **Feedback lifecycle:** The revised plan file overwrites the existing plan file completely. The `## Feedback` section is NOT carried forward into the new file — it is consumed by the revision and the audit trail lives in the `### Changes From Previous Plan` section (which quotes or summarises the feedback that was addressed).
232
+
233
+ **Row selection with feedback:** When selecting rows to plan in `--afk` multi-row mode, also include rows whose plan files contain a `## Feedback` section. The selection set is: (unplanned rows) ∪ (rows with feedback). For default row selection (no row N, no `--afk`), the first row needing attention is: first row with feedback if any, otherwise first unplanned row.
234
+
235
+ ---
236
+
115
237
  ## Step 3 — Fetch backlog tickets
116
238
 
117
239
  Detect board from `.clancy/.env` and fetch tickets from the **planning queue** (different from the implementation queue used by `/clancy:implement`).
@@ -733,9 +855,48 @@ _Generated by [Clancy](https://github.com/Pushedskydiver/chief-clancy). To reque
733
855
 
734
856
  ---
735
857
 
736
- ## Step 5 — Post plan as comment
858
+ ## Step 5 — Save / post plan
859
+
860
+ ### 5a. Local plan output (--from mode)
861
+
862
+ If planning from a local brief (`--from`), save the plan to a local file instead of posting to the board.
863
+
864
+ **Output path:** `.clancy/plans/{slug}-{row-number}.md`
865
+
866
+ Create `.clancy/plans/` directory if it does not exist.
867
+
868
+ The slug is the same one computed in Step 3a (brief filename minus date prefix and extension). The row number is the 1-indexed decomposition row being planned.
869
+
870
+ **Local plan header:** The plan uses the same `## Clancy Implementation Plan` template from Step 4f, but with local-specific header fields:
871
+
872
+ ```markdown
873
+ ## Clancy Implementation Plan
874
+
875
+ **Source:** {brief source field value}
876
+ **Brief:** {brief filename}
877
+ **Row:** #{row number} — {row title}
878
+ **Planned:** {YYYY-MM-DD}
879
+ ```
880
+
881
+ Replace the `**Ticket:** [{KEY}] {Title}` line from the board template with `**Source:**` and `**Brief:**` lines.
882
+
883
+ **Revision header (when revising from feedback):** If this is a revision (existing plan had `## Feedback`), insert `### Changes From Previous Plan` immediately after the local header block (after `**Planned:**`) and before `### Summary`. Same structure as the board template's revision section.
884
+
885
+ **Local plan footer:** Replace the board-specific footer with:
886
+
887
+ ```
888
+ _Generated by [Clancy](https://github.com/Pushedskydiver/chief-clancy). To request changes: add a ## Feedback section to this file, then re-run `/clancy:plan --from {path}` to revise. To start over: `/clancy:plan --fresh --from {path}`. To approve: install the full pipeline — npx chief-clancy._
889
+ ```
890
+
891
+ **Re-planning:** If `--fresh` was used, the existing plan file is overwritten (same slug + row number = same filename).
737
892
 
738
- **Guard:** Only run Step 5 when board credentials are available (terminal mode or standalone+board mode). In standalone mode (no `.clancy/.env`), skip this step entirely the plan is still generated and printed to stdout in Step 4.
893
+ **Board comment offer:** If board credentials ARE available (terminal mode or standalone+board mode), after saving the local file, offer to also post the plan as a comment on the source ticket (if the brief's Source field contains a ticket key). This is optional the local file is the primary output.
894
+
895
+ After saving, skip to Step 6 (log). Do not run Step 5b (board posting) for `--from` plans unless the user opts in above.
896
+
897
+ ### 5b. Post plan as comment (board mode)
898
+
899
+ **Guard:** Only run Step 5b when board credentials are available (terminal mode or standalone+board mode). In standalone mode (no `.clancy/.env`), skip this step entirely — the plan is still generated and printed to stdout in Step 4.
739
900
 
740
901
  ### Jira — POST comment
741
902
 
@@ -851,6 +1012,8 @@ curl -s \
851
1012
 
852
1013
  For each planned ticket, append to `.clancy/progress.txt` using the appropriate variant:
853
1014
 
1015
+ ### Board mode log entries
1016
+
854
1017
  | Outcome | Log entry |
855
1018
  | ------------------------------- | ------------------------------------------------------ |
856
1019
  | Normal | `YYYY-MM-DD HH:MM \| {KEY} \| PLAN \| {S/M/L}` |
@@ -858,12 +1021,28 @@ For each planned ticket, append to `.clancy/progress.txt` using the appropriate
858
1021
  | Comment post failed | `YYYY-MM-DD HH:MM \| {KEY} \| POST_FAILED \| {reason}` |
859
1022
  | Skipped (infeasible) | `YYYY-MM-DD HH:MM \| {KEY} \| SKIPPED \| {reason}` |
860
1023
 
1024
+ ### --from mode log entries
1025
+
1026
+ Use the slug as the identifier instead of a ticket key:
1027
+
1028
+ | Outcome | Log entry |
1029
+ | ------------------------------- | -------------------------------------------------------------- |
1030
+ | Normal | `YYYY-MM-DD HH:MM \| {slug}#{row} \| LOCAL_PLAN \| {S/M/L}` |
1031
+ | Revised (re-plan with feedback) | `YYYY-MM-DD HH:MM \| {slug}#{row} \| LOCAL_REVISED \| {S/M/L}` |
1032
+ | Skipped (infeasible) | `YYYY-MM-DD HH:MM \| {slug}#{row} \| SKIPPED \| {reason}` |
1033
+
1034
+ ### --list mode
1035
+
1036
+ `--list` display is not logged to `.clancy/progress.txt` — the inventory is read-only and does not change project state.
1037
+
861
1038
  ---
862
1039
 
863
1040
  ## Step 7 — Summary
864
1041
 
865
1042
  After all tickets are processed, display:
866
1043
 
1044
+ ### Board mode summary
1045
+
867
1046
  ```
868
1047
  Planned {N} ticket(s):
869
1048
 
@@ -879,6 +1058,82 @@ Plans written to your board. After review:
879
1058
  "Let me dust this for prints..."
880
1059
  ```
881
1060
 
1061
+ ### --from mode summary
1062
+
1063
+ Single row:
1064
+
1065
+ ```
1066
+ 🚨 Clancy — Plan
1067
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1068
+
1069
+ ✅ Saved to .clancy/plans/{slug}-{row-number}.md
1070
+
1071
+ To approve: install the full pipeline — npx chief-clancy
1072
+
1073
+ "Let me dust this for prints..."
1074
+ ```
1075
+
1076
+ Multi-row (`--afk`):
1077
+
1078
+ ```
1079
+ 🚨 Clancy — Plan
1080
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1081
+
1082
+ ✅ Row 1: {title} — Saved to .clancy/plans/{slug}-1.md
1083
+ ✅ Row 2: {title} — Saved to .clancy/plans/{slug}-2.md
1084
+ ⏭️ Row 3: {title} — already planned
1085
+
1086
+ To approve: install the full pipeline — npx chief-clancy
1087
+
1088
+ "Let me dust this for prints..."
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ ## Step 8 — Plan inventory (`--list`)
1094
+
1095
+ If `--list` was passed (detected at the top of Step 1), display the local plan inventory and stop. This step is filesystem-only — no API calls, no board access, no `.clancy/.env` required.
1096
+
1097
+ Scan `.clancy/plans/` for all `.md` files. For each file, parse the local plan header (the block at the top of the file written by Step 5a) and capture these fields:
1098
+
1099
+ - **Plan ID** — the plan filename minus the `.md` extension (e.g. `add-dark-mode-2`). This is `{slug}-{row}` as written by Step 5a, where `{slug}` is the brief slug from Step 3a and `{row}` is the decomposition row number. Always present (it is the filename).
1100
+ - **Brief** — value of the `**Brief:**` line. The brief filename the plan was generated from. Display the literal `?` if the line is absent or empty after the colon.
1101
+ - **Row** — value of the `**Row:**` line (e.g. `#2 — Add toggle component`). Display `?` if absent or empty.
1102
+ - **Source** — value of the `**Source:**` line (the brief's Source field). Display `?` if absent or empty.
1103
+ - **Planned** — value of the `**Planned:**` line (the YYYY-MM-DD planned date). Display `?` if absent or unparseable as a date.
1104
+ - **Status** — for now this is always `Planned`. Reserved for `/clancy:approve-plan` (a future PR) which will write a sibling `.approved` marker file (e.g. `.clancy/plans/add-dark-mode-2.approved`). When that marker exists for a given plan, Status becomes `Approved`. The column is included in the listing today so the format will not change when approval markers ship.
1105
+
1106
+ A field is considered missing if the line is absent or its value is empty after the colon. Plans missing all expected fields are still listed (with `?` placeholders) so the user can find and clean them up.
1107
+
1108
+ **Sort:** by `**Planned**` date, newest first. Tie-break on same date by Plan ID, alphabetical ascending. Files with a missing or unparseable date sort last (after all dated plans), and tie-break among themselves by Plan ID alphabetical ascending. The sort must be deterministic across runs.
1109
+
1110
+ Display:
1111
+
1112
+ ```
1113
+ Clancy — Plans
1114
+ ================================================================
1115
+
1116
+ [1] add-dark-mode-2 2026-04-08 Planned Row #2 — Add toggle component Brief: 2026-04-01-add-dark-mode.md Source: #50
1117
+ [2] add-dark-mode-1 2026-04-07 Planned Row #1 — Wire theme context Brief: 2026-04-01-add-dark-mode.md Source: #50
1118
+ [3] customer-portal-3 2026-04-05 Planned Row #3 — Billing page Brief: 2026-03-28-customer-portal.md Source: PROJ-200
1119
+
1120
+ 3 local plan(s).
1121
+
1122
+ To revise: add a `## Feedback` section to the plan file, then re-run /clancy:plan --from <brief>.
1123
+ To start over: /clancy:plan --fresh --from <brief>.
1124
+ To approve: install the full pipeline — npx chief-clancy.
1125
+ ```
1126
+
1127
+ The first column in the listing is the Plan ID (filename without `.md`), not the brief slug.
1128
+
1129
+ If `.clancy/plans/` does not exist or contains no `.md` files:
1130
+
1131
+ ```
1132
+ No plans found. Run /clancy:plan --from .clancy/briefs/<brief>.md to create one.
1133
+ ```
1134
+
1135
+ Stop after display. The `--list` step never logs to `.clancy/progress.txt` and never modifies any file — it is purely a read-only inventory view of the local plans directory.
1136
+
882
1137
  ---
883
1138
 
884
1139
  ## Notes
@@ -891,3 +1146,6 @@ Plans written to your board. After review:
891
1146
  - All board API calls are best-effort — if a comment fails to post, print the plan to stdout as fallback
892
1147
  - When exploring the codebase, use Glob and Read for small tickets, parallel Explore subagents for larger ones
893
1148
  - The `## Clancy Implementation Plan` marker in comments is used by both `/clancy:plan` (to detect existing plans) and `/clancy:approve-plan` (to find the plan to promote)
1149
+ - `--from` mode is fully offline — no board credentials needed. Plans saved to `.clancy/plans/` as the source of truth
1150
+ - `--from` requires a Clancy brief (structured format with `## Problem Statement` or `## Ticket Decomposition`). For raw files, use `/clancy:brief --from {path}` first to generate a brief
1151
+ - `--list` is filesystem-only and short-circuits at the top of Step 1 — no installation context, network, board, or git checks run before the inventory displays
@@ -120,6 +120,56 @@ describe('board-setup workflow', () => {
120
120
  // plan.md content assertions
121
121
  // ---------------------------------------------------------------------------
122
122
 
123
+ // ---------------------------------------------------------------------------
124
+ // --from flag: parsing and validation
125
+ // ---------------------------------------------------------------------------
126
+
127
+ describe('--from flag parsing', () => {
128
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
129
+
130
+ it('documents --from input mode in Step 2', () => {
131
+ expect(content).toContain('--from');
132
+ expect(content).toContain('local brief file');
133
+ });
134
+
135
+ it('cannot combine --from with ticket key', () => {
136
+ expect(content).toContain('Cannot use both a ticket reference and --from');
137
+ });
138
+
139
+ it('cannot combine --from with batch mode', () => {
140
+ expect(content).toContain('Cannot use batch mode with --from');
141
+ });
142
+
143
+ it('validates file exists', () => {
144
+ expect(content).toContain('File not found');
145
+ });
146
+
147
+ it('validates file is not empty', () => {
148
+ expect(content).toContain('File is empty');
149
+ });
150
+
151
+ it('warns on large files (>50KB)', () => {
152
+ expect(content).toContain('50KB');
153
+ });
154
+ });
155
+
156
+ describe('--from brief validation', () => {
157
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
158
+
159
+ it('validates file is a Clancy brief', () => {
160
+ expect(content).toContain('does not appear to be a Clancy brief');
161
+ });
162
+
163
+ it('checks for Problem Statement or Ticket Decomposition', () => {
164
+ expect(content).toContain('## Problem Statement');
165
+ expect(content).toContain('## Ticket Decomposition');
166
+ });
167
+
168
+ it('points to /clancy:brief --from for raw files', () => {
169
+ expect(content).toContain('/clancy:brief --from');
170
+ });
171
+ });
172
+
123
173
  describe('three-state mode detection', () => {
124
174
  const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
125
175
 
@@ -150,6 +200,29 @@ describe('three-state mode detection', () => {
150
200
  );
151
201
  });
152
202
 
203
+ it('--from mode gathers from local brief file', () => {
204
+ expect(content).toContain('Step 3a');
205
+ expect(content).toContain('Gather from local brief');
206
+ });
207
+
208
+ it('--from parses Source field from brief', () => {
209
+ expect(content).toContain('**Source:**');
210
+ });
211
+
212
+ it('--from extracts Problem Statement and Goals', () => {
213
+ expect(content).toContain('## Problem Statement');
214
+ expect(content).toContain('## Goals');
215
+ });
216
+
217
+ it('--from reads Ticket Decomposition for context', () => {
218
+ expect(content).toContain('## Ticket Decomposition');
219
+ });
220
+
221
+ it('--from mode bypasses standalone board-ticket guard', () => {
222
+ expect(content).toContain('--from');
223
+ expect(content).toContain('bypasses the standalone board-ticket guard');
224
+ });
225
+
153
226
  it('standalone guard mentions /clancy:board-setup', () => {
154
227
  expect(content).toContain('Standalone board-ticket guard');
155
228
  expect(content).toContain('Board credentials not found');
@@ -166,3 +239,404 @@ describe('three-state mode detection', () => {
166
239
  expect(content).toContain('npx chief-clancy');
167
240
  });
168
241
  });
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Local plan output (--from mode)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ describe('local plan output', () => {
248
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
249
+
250
+ it('saves plans to .clancy/plans/ directory', () => {
251
+ expect(content).toContain('.clancy/plans/');
252
+ });
253
+
254
+ it('creates .clancy/plans/ directory if needed', () => {
255
+ expect(content).toContain('Create `.clancy/plans/` directory');
256
+ });
257
+
258
+ it('generates slug from brief filename', () => {
259
+ expect(content).toContain('YYYY-MM-DD-');
260
+ expect(content).toContain('date prefix');
261
+ });
262
+
263
+ it('checks for existing local plan by filename', () => {
264
+ expect(content).toContain('.clancy/plans/{slug}-{row-number}.md');
265
+ });
266
+
267
+ it('--fresh overwrites existing local plan', () => {
268
+ expect(content).toContain('--fresh');
269
+ expect(content).toContain('overwrite');
270
+ });
271
+
272
+ it('stops if plan exists without --fresh', () => {
273
+ expect(content).toContain('Already planned');
274
+ });
275
+
276
+ it('local plan header includes Source and Brief fields', () => {
277
+ expect(content).toContain('**Source:**');
278
+ expect(content).toContain('**Brief:**');
279
+ });
280
+
281
+ it('offers to post as comment when board credentials available', () => {
282
+ expect(content).toContain('board credentials ARE available');
283
+ });
284
+ });
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // --from mode Step 4 adaptations
288
+ // ---------------------------------------------------------------------------
289
+
290
+ describe('--from Step 4 adaptations', () => {
291
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
292
+
293
+ it('documents Step 4 adaptations for --from mode', () => {
294
+ expect(content).toContain('--from mode Step 4 adaptations');
295
+ });
296
+
297
+ it('uses slug as display identifier instead of ticket key', () => {
298
+ expect(content).toContain('Use the slug');
299
+ expect(content).toContain('wherever board mode uses `{KEY}`');
300
+ });
301
+
302
+ it('skips board comment posting for infeasible tickets', () => {
303
+ expect(content).toContain(
304
+ 'do NOT post a "Clancy skipped" comment to any board',
305
+ );
306
+ });
307
+
308
+ it('skips QA return detection in --from mode', () => {
309
+ expect(content).toContain('Skip entirely in `--from` mode');
310
+ });
311
+ });
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // --from mode Step 6 log entries
315
+ // ---------------------------------------------------------------------------
316
+
317
+ describe('--from Step 6 log entries', () => {
318
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
319
+
320
+ it('defines --from mode log format with slug', () => {
321
+ expect(content).toContain('LOCAL_PLAN');
322
+ expect(content).toContain('{slug}');
323
+ });
324
+ });
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // Summary update for --from mode
328
+ // ---------------------------------------------------------------------------
329
+
330
+ describe('--from summary output', () => {
331
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
332
+
333
+ it('shows local file path instead of Comment posted', () => {
334
+ expect(content).toContain('Saved to .clancy/plans/');
335
+ });
336
+
337
+ it('mentions --from in standalone guard hint', () => {
338
+ expect(content).toContain('/clancy:plan --from');
339
+ });
340
+ });
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Row selection + multi-row planning (--from mode)
344
+ // ---------------------------------------------------------------------------
345
+
346
+ describe('decomposition table parsing', () => {
347
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
348
+
349
+ it('parses decomposition table rows', () => {
350
+ expect(content).toContain('row number (column 1)');
351
+ expect(content).toContain('title (column 2)');
352
+ });
353
+
354
+ it('validates rows have minimum required fields', () => {
355
+ expect(content).toContain('valid row must have');
356
+ });
357
+
358
+ it('skips malformed rows with warning', () => {
359
+ expect(content).toContain('Skipping malformed row');
360
+ });
361
+
362
+ it('falls back to single planning unit when table is missing', () => {
363
+ expect(content).toContain('Planning the brief as a single item');
364
+ });
365
+
366
+ it('falls back to single unit when all rows are malformed', () => {
367
+ expect(content).toContain('ALL rows are malformed');
368
+ });
369
+
370
+ it('rows are 1-indexed from data rows', () => {
371
+ expect(content).toContain('1-indexed');
372
+ expect(content).toContain('excluding header and separator');
373
+ });
374
+ });
375
+
376
+ describe('planned marker tracking', () => {
377
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
378
+
379
+ it('tracks planned rows via HTML comment marker', () => {
380
+ expect(content).toContain('<!-- planned:');
381
+ });
382
+
383
+ it('updates marker after planning a row', () => {
384
+ expect(content).toContain('<!-- planned:1,2,3 -->');
385
+ expect(content).toContain('<!-- planned:1,2,3,4 -->');
386
+ });
387
+
388
+ it('places marker before trailing --- or at EOF', () => {
389
+ expect(content).toContain('trailing `---`');
390
+ });
391
+
392
+ it('stops when all rows are planned', () => {
393
+ expect(content).toContain('All decomposition rows have been planned');
394
+ });
395
+
396
+ it('notes concurrency limitation', () => {
397
+ expect(content).toContain('not concurrency-safe');
398
+ });
399
+ });
400
+
401
+ describe('row targeting with --from path N', () => {
402
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
403
+
404
+ it('bare integer after --from selects a specific row', () => {
405
+ expect(content).toContain('selects row N');
406
+ });
407
+
408
+ it('defaults to first unplanned row without a number', () => {
409
+ expect(content).toContain('first unplanned row');
410
+ });
411
+
412
+ it('errors if targeted row is already planned without --fresh', () => {
413
+ expect(content).toContain('already planned');
414
+ });
415
+
416
+ it('validates row number is a positive integer', () => {
417
+ expect(content).toContain('Row number must be a positive integer');
418
+ });
419
+
420
+ it('validates row number exists in decomposition table', () => {
421
+ expect(content).toContain('Row {N} not found');
422
+ expect(content).toContain('decomposition rows');
423
+ });
424
+
425
+ it('bare integer with --from is always a row number, not batch', () => {
426
+ expect(content).toContain(
427
+ 'bare integer is always interpreted as a row number',
428
+ );
429
+ });
430
+ });
431
+
432
+ describe('--afk multi-row and single-row default', () => {
433
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
434
+
435
+ it('--afk plans all unplanned rows in sequence', () => {
436
+ expect(content).toContain('all unplanned rows');
437
+ expect(content).toContain('sequentially');
438
+ });
439
+
440
+ it('without --afk plans exactly one row', () => {
441
+ expect(content).toContain('exactly one row');
442
+ });
443
+
444
+ it('--fresh with specific row overwrites plan file', () => {
445
+ expect(content).toContain('marker is not modified');
446
+ });
447
+
448
+ it('--fresh + --afk re-plans all rows from scratch', () => {
449
+ expect(content).toContain('clears the planned marker entirely');
450
+ });
451
+ });
452
+
453
+ describe('row-aware plan filename', () => {
454
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
455
+
456
+ it('plan filename includes row number', () => {
457
+ expect(content).toContain('{slug}-{row-number}.md');
458
+ });
459
+
460
+ it('plan header includes Row field', () => {
461
+ expect(content).toContain('**Row:**');
462
+ });
463
+ });
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // Local plan feedback loop (--from mode)
467
+ // ---------------------------------------------------------------------------
468
+
469
+ describe('local plan feedback loop', () => {
470
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
471
+
472
+ it('detects ## Feedback section in existing plan files', () => {
473
+ expect(content).toContain('## Feedback');
474
+ expect(content).toContain('local plan file');
475
+ });
476
+
477
+ it('revises plan when feedback found', () => {
478
+ expect(content).toContain('Revise:');
479
+ expect(content).toContain('read existing plan + feedback');
480
+ });
481
+
482
+ it('stops on existing plan without feedback or --fresh', () => {
483
+ expect(content).toContain('Add a ## Feedback section to revise');
484
+ });
485
+
486
+ it('prepends Changes From Previous Plan section', () => {
487
+ expect(content).toContain('### Changes From Previous Plan');
488
+ });
489
+
490
+ it('passes feedback to generation step as additional context', () => {
491
+ expect(content).toContain('Pass this feedback to the plan generation');
492
+ });
493
+
494
+ it('plan footer mentions ## Feedback for revision', () => {
495
+ expect(content).toContain('add a ## Feedback section');
496
+ });
497
+
498
+ it('--fresh takes precedence over feedback', () => {
499
+ expect(content).toContain('takes precedence over feedback');
500
+ });
501
+
502
+ it('handles multiple ## Feedback sections by concatenating', () => {
503
+ expect(content).toContain('multiple `## Feedback` sections');
504
+ expect(content).toContain('concatenate all sections');
505
+ });
506
+
507
+ it('matches ## Feedback as line-anchored heading not in code fences', () => {
508
+ expect(content).toContain('start of a line');
509
+ expect(content).toContain('not inside code fences');
510
+ });
511
+
512
+ it('feedback is not carried forward to revised plan', () => {
513
+ expect(content).toContain('NOT carried forward');
514
+ });
515
+
516
+ it('specifies revision procedure for Step 4 sub-steps', () => {
517
+ expect(content).toContain('Skip Step 4a');
518
+ expect(content).toContain('reuse the existing exploration');
519
+ });
520
+
521
+ it('--afk multi-row includes rows with feedback', () => {
522
+ expect(content).toContain('(unplanned rows) ∪ (rows with feedback)');
523
+ });
524
+
525
+ it('default row selection prefers rows with feedback first', () => {
526
+ expect(content).toContain(
527
+ 'first row with feedback if any, otherwise first unplanned row',
528
+ );
529
+ });
530
+
531
+ it('revised local plans use LOCAL_REVISED log entry', () => {
532
+ expect(content).toContain('LOCAL_REVISED');
533
+ });
534
+
535
+ it('Changes From Previous Plan is positioned for local template', () => {
536
+ expect(content).toContain('after the local header block');
537
+ });
538
+ });
539
+
540
+ // ---------------------------------------------------------------------------
541
+ // --list flag: plan inventory
542
+ // ---------------------------------------------------------------------------
543
+
544
+ describe('--list flag handling', () => {
545
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
546
+
547
+ it('documents --list flag in Step 2 arg list', () => {
548
+ expect(content).toContain('**`--list`:** display the plan inventory');
549
+ });
550
+
551
+ it('--list short-circuits Step 1 preflight', () => {
552
+ expect(content).toContain('### 0. Short-circuit on `--list`');
553
+ expect(content).toContain(
554
+ 'skip the rest of Step 1 entirely (no installation detection, no `git fetch`',
555
+ );
556
+ expect(content).toContain('jump straight to Step 8');
557
+ });
558
+
559
+ it('--list takes precedence over other flags and arguments', () => {
560
+ expect(content).toContain('`--list` always wins over other flags');
561
+ expect(content).toContain(
562
+ 'the inventory is displayed and the other flags are ignored',
563
+ );
564
+ });
565
+
566
+ it('standalone board-ticket guard skips when --list is present', () => {
567
+ expect(content).toContain(
568
+ 'Skip this guard entirely if `--list` was passed',
569
+ );
570
+ });
571
+ });
572
+
573
+ describe('plan inventory step', () => {
574
+ const content = readFileSync(new URL('plan.md', import.meta.url), 'utf8');
575
+
576
+ it('defines a Plan inventory step at Step 8', () => {
577
+ expect(content).toContain('## Step 8 — Plan inventory (`--list`)');
578
+ });
579
+
580
+ it('scans .clancy/plans/ for markdown files', () => {
581
+ expect(content).toContain('Scan `.clancy/plans/` for all `.md` files');
582
+ });
583
+
584
+ it('parses plan headers written by Step 5a', () => {
585
+ // Each header field gets its own line item describing how it is parsed
586
+ // (the `**Foo:**` strings exist elsewhere in the file too — these tests
587
+ // assert the Step 8 parsing intent, not just substring presence).
588
+ expect(content).toContain('value of the `**Brief:**` line');
589
+ expect(content).toContain('value of the `**Row:**` line');
590
+ expect(content).toContain('value of the `**Source:**` line');
591
+ expect(content).toContain('value of the `**Planned:**` line');
592
+ });
593
+
594
+ it('Plan ID is the filename minus .md, not the brief slug', () => {
595
+ expect(content).toContain(
596
+ '**Plan ID** — the plan filename minus the `.md` extension',
597
+ );
598
+ expect(content).toContain('first column in the listing is the Plan ID');
599
+ });
600
+
601
+ it('reserves a Status column for the future approve-plan PR', () => {
602
+ expect(content).toContain('**Status**');
603
+ expect(content).toContain('always `Planned`');
604
+ expect(content).toContain('`.approved` marker');
605
+ });
606
+
607
+ it('sort is deterministic with explicit tie-breakers', () => {
608
+ expect(content).toContain('newest first');
609
+ expect(content).toContain(
610
+ 'Tie-break on same date by Plan ID, alphabetical ascending',
611
+ );
612
+ expect(content).toContain(
613
+ 'Files with a missing or unparseable date sort last',
614
+ );
615
+ expect(content).toContain('deterministic across runs');
616
+ });
617
+
618
+ it('handles empty or missing .clancy/plans/ directory', () => {
619
+ expect(content).toContain(
620
+ 'If `.clancy/plans/` does not exist or contains no `.md` files',
621
+ );
622
+ expect(content).toContain('No plans found');
623
+ });
624
+
625
+ it('describes how missing header fields are rendered', () => {
626
+ expect(content).toContain(
627
+ 'Display the literal `?` if the line is absent or empty',
628
+ );
629
+ });
630
+
631
+ it('inventory step is filesystem-only (no API/board access)', () => {
632
+ expect(content).toContain(
633
+ 'filesystem-only — no API calls, no board access, no `.clancy/.env` required',
634
+ );
635
+ });
636
+
637
+ it('--list never writes to progress.txt or any other file', () => {
638
+ expect(content).toContain(
639
+ 'never logs to `.clancy/progress.txt` and never modifies any file',
640
+ );
641
+ });
642
+ });