@chief-clancy/plan 0.3.0 → 0.4.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 +37 -1
- package/bin/plan.js +5 -6
- package/dist/installer/install.d.ts.map +1 -1
- package/dist/installer/install.js +8 -12
- package/dist/installer/install.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/approve-plan.md +14 -2
- package/src/workflows/approve-plan.md +269 -40
- package/src/workflows/plan.md +30 -19
- package/src/workflows/workflows.test.ts +365 -4
package/README.md
CHANGED
|
@@ -80,7 +80,43 @@ The `--from` flag lets you go from idea to implementation plan without ever touc
|
|
|
80
80
|
|
|
81
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
82
|
|
|
83
|
-
|
|
83
|
+
## Approving plans
|
|
84
|
+
|
|
85
|
+
The plan package ships `/clancy:approve-plan` so the approval gate works without the full pipeline. The behaviour depends on the install mode.
|
|
86
|
+
|
|
87
|
+
### Standalone (no board)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Approve a specific local plan
|
|
91
|
+
/clancy:approve-plan add-dark-mode-2
|
|
92
|
+
|
|
93
|
+
# Or auto-select the oldest unapproved local plan
|
|
94
|
+
/clancy:approve-plan
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The command writes a `.clancy/plans/{stem}.approved` marker file containing the full lowercase hex SHA-256 of the plan file and an ISO 8601 UTC approval timestamp:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
sha256=d2c9f3a1b4e6c8f09123456789abcdef0123456789abcdef0123456789abcd
|
|
101
|
+
approved_at=2026-04-08T22:30:00Z
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The full 64-character hex hash is what `.approved` actually stores — `/clancy:implement-from` (PR 8) reads the marker, hashes the current plan file the same way, and blocks implementation on any mismatch.
|
|
105
|
+
|
|
106
|
+
The marker is the gate `/clancy:implement-from` (shipping in the next PR) checks before applying changes — if the plan file is edited after approval, the SHA mismatch blocks implementation until you re-approve. Clancy also tries to update the brief file's `<!-- planned:1,2 -->` marker to `<!-- approved:1 planned:1,2 -->` so `/clancy:plan --list` knows which rows are approved, but that brief-marker update is best-effort and may warn-and-skip if the expected brief metadata or matching marker is missing.
|
|
107
|
+
|
|
108
|
+
### Standalone+board (board credentials but no full pipeline)
|
|
109
|
+
|
|
110
|
+
The argument decides which path runs:
|
|
111
|
+
|
|
112
|
+
- **Plan-file stem** (e.g. `add-dark-mode-2`): writes the local marker. The board push offer for local plan stems lands in a future PR
|
|
113
|
+
- **Board ticket key** (e.g. `PROJ-123`): runs the full board comment-to-description transport flow — fetches the plan comment, appends it to the ticket description, edits the plan comment with an approval note, swaps the ticket labels (`CLANCY_LABEL_PLAN` → `CLANCY_LABEL_BUILD`, both with sensible defaults), and — only if `CLANCY_STATUS_PLANNED` is configured — transitions the ticket status. Label swaps are mandatory; status transitions are opt-in via the `CLANCY_STATUS_PLANNED` env var
|
|
114
|
+
|
|
115
|
+
The plan-file lookup runs first, so plan stems win on collision (`PROJ-123.md` exists locally AND `PROJ-123` is a valid ticket key → the local plan wins).
|
|
116
|
+
|
|
117
|
+
### Terminal mode (full pipeline)
|
|
118
|
+
|
|
119
|
+
Existing behaviour, unchanged. Board ticket transport, queue transitions, and the implementation handoff all work as they did before.
|
|
84
120
|
|
|
85
121
|
## Board ticket mode
|
|
86
122
|
|
package/bin/plan.js
CHANGED
|
@@ -83,12 +83,8 @@ const ask = (label) => new Promise((resolve) => rl.question(label, resolve));
|
|
|
83
83
|
// File lists (keep in sync with install.ts)
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// workflow content is currently board-only (byte-identical to terminal's
|
|
89
|
-
// previous version). PR 7b makes it standalone-safe and adds it here.
|
|
90
|
-
const COMMAND_FILES = ['board-setup.md', 'plan.md'];
|
|
91
|
-
const WORKFLOW_FILES = ['board-setup.md', 'plan.md'];
|
|
86
|
+
const COMMAND_FILES = ['approve-plan.md', 'board-setup.md', 'plan.md'];
|
|
87
|
+
const WORKFLOW_FILES = ['approve-plan.md', 'board-setup.md', 'plan.md'];
|
|
92
88
|
|
|
93
89
|
// ---------------------------------------------------------------------------
|
|
94
90
|
// Installer
|
|
@@ -214,6 +210,9 @@ async function main() {
|
|
|
214
210
|
console.log(
|
|
215
211
|
` ${cyan('/clancy:plan')} ${dim('Generate an implementation plan')}`,
|
|
216
212
|
);
|
|
213
|
+
console.log(
|
|
214
|
+
` ${cyan('/clancy:approve-plan')} ${dim('Approve a plan (run /clancy:plan first)')}`,
|
|
215
|
+
);
|
|
217
216
|
console.log(
|
|
218
217
|
` ${cyan('/clancy:board-setup')} ${dim('Configure board credentials (optional)')}`,
|
|
219
218
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/installer/install.ts"],"names":[],"mappings":"AAaA,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjD,8DAA8D;AAC9D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,iDAAiD;AACjD,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;;GAIG;AACH,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CAC/C,CAAC;AAEF,0CAA0C;AAC1C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,eAAe,CAAC;CAC9B,CAAC;
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/installer/install.ts"],"names":[],"mappings":"AAaA,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjD,8DAA8D;AAC9D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,iDAAiD;AACjD,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;;GAIG;AACH,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CAC/C,CAAC;AAEF,0CAA0C;AAC1C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,eAAe,CAAC;CAC9B,CAAC;AAuBF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,MAAM,SAAS,MAAM,EAAE,KACtB,eAAe,GAAG,IAKpB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAClC,MAAM,eAAe,EACrB,SAAS,MAAM,EACf,KAAK,MAAM,KACV,gBAQF,CAAC;AA4EF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,qBAAqB,KAAG,IAuB/D,CAAC"}
|
|
@@ -9,18 +9,14 @@ import { join } from 'node:path';
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Constants
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
const COMMAND_FILES = ['board-setup.md', 'plan.md'];
|
|
22
|
-
/** Workflow files shipped with the plan package. See COMMAND_FILES. */
|
|
23
|
-
const WORKFLOW_FILES = ['board-setup.md', 'plan.md'];
|
|
12
|
+
/** Command files shipped with the plan package. */
|
|
13
|
+
const COMMAND_FILES = ['approve-plan.md', 'board-setup.md', 'plan.md'];
|
|
14
|
+
/** Workflow files shipped with the plan package. */
|
|
15
|
+
const WORKFLOW_FILES = [
|
|
16
|
+
'approve-plan.md',
|
|
17
|
+
'board-setup.md',
|
|
18
|
+
'plan.md',
|
|
19
|
+
];
|
|
24
20
|
/** Matches `@.claude/clancy/workflows/<filename>.md` on its own line. */
|
|
25
21
|
const WORKFLOW_REF = /^@\.claude\/clancy\/workflows\/([^/\\]+\.md)\r?$/gm;
|
|
26
22
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/installer/install.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AA6CjC,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/installer/install.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# /clancy:approve-plan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Approve a Clancy implementation plan. Behaviour depends on the install context and the argument:
|
|
4
|
+
|
|
5
|
+
- **Local plan file:** `/clancy:approve-plan add-dark-mode-2` — write a `.clancy/plans/{stem}.approved` marker (with the plan's SHA-256 + approval timestamp) and update the source brief's row marker. The marker is the gate `/clancy:implement-from` checks before applying changes
|
|
6
|
+
- **Board ticket:** `/clancy:approve-plan PROJ-123` — promote an approved plan from a ticket comment to the ticket description, edit the plan comment with an approval note, swap the ticket labels (`CLANCY_LABEL_PLAN` → `CLANCY_LABEL_BUILD`, both with sensible defaults), and — only if `CLANCY_STATUS_PLANNED` is configured — transition the ticket status. Requires board credentials. Runs in both standalone+board and terminal modes (the full pipeline is not required for the board transport flow itself; it is only required for downstream `/clancy:implement` to consume the result)
|
|
7
|
+
- **No argument:** auto-select the oldest unapproved plan. In standalone mode this scans `.clancy/plans/`; in standalone+board / terminal mode it scans `.clancy/progress.txt` for board tickets
|
|
8
|
+
|
|
9
|
+
The argument is **mode-aware**: in standalone+board / terminal modes, a plan-file lookup runs first (so `add-dark-mode-2` matches the local plan even if a board ticket happens to share the name). Plan stems win on collision.
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
|
|
13
|
+
- `/clancy:approve-plan add-dark-mode-2` — approve a local plan from `/clancy:plan --from`
|
|
14
|
+
- `/clancy:approve-plan PROJ-123` — promote a board plan to the ticket description
|
|
15
|
+
- `/clancy:approve-plan` — auto-select the oldest pending approval
|
|
4
16
|
|
|
5
17
|
Optional flags:
|
|
6
18
|
|
|
@@ -8,4 +20,4 @@ Optional flags:
|
|
|
8
20
|
|
|
9
21
|
@.claude/clancy/workflows/approve-plan.md
|
|
10
22
|
|
|
11
|
-
Follow the approve-plan workflow above.
|
|
23
|
+
Follow the approve-plan workflow above. Detect the install context, resolve the argument (plan-file stem or ticket key), and either write the local marker (Step 4a/4b) or run the existing board transport flow (Steps 5/5b/6). Do not implement anything — approval only.
|
|
@@ -2,57 +2,102 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Approve a Clancy implementation plan. Behaviour depends on the install context:
|
|
6
|
+
|
|
7
|
+
- **Standalone mode** (no `.clancy/.env`): write a local `.clancy/plans/{stem}.approved` marker file with the plan's SHA-256 and approval timestamp. The marker is the gate `/clancy:implement-from` checks before applying changes
|
|
8
|
+
- **Standalone+board mode** (`.clancy/.env` present, no full pipeline): with a board ticket key, run the existing comment-to-description transport flow; with a plan-file stem, write the local marker (board push lands in PR 9)
|
|
9
|
+
- **Terminal mode** (full pipeline installed): existing behaviour — promote an approved plan from a ticket comment to the ticket description and transition the ticket to the implementation queue
|
|
6
10
|
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
## Step 1 — Preflight checks
|
|
10
14
|
|
|
11
|
-
1.
|
|
15
|
+
### 1. Detect installation context
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
Check for `.clancy/.env`:
|
|
18
|
+
|
|
19
|
+
- **Absent** → **standalone mode**. No board credentials. Board ticket arguments are blocked; only plan-file stems are accepted
|
|
20
|
+
- **Present** → continue to `.clancy/clancy-implement.js` check below
|
|
21
|
+
|
|
22
|
+
If `.clancy/.env` is present, check for `.clancy/clancy-implement.js`:
|
|
23
|
+
|
|
24
|
+
- **Present** → **terminal mode**. Full Clancy pipeline installed
|
|
25
|
+
- **Absent** → **standalone+board mode**. Board credentials available via `/clancy:board-setup`. Board ticket arguments work via the existing transport flow. Plan-file stems write the local marker
|
|
26
|
+
|
|
27
|
+
### 2. Terminal-mode preflight (skip in standalone mode and standalone+board mode)
|
|
28
|
+
|
|
29
|
+
If in **terminal mode** (`.clancy/.env` present AND `.clancy/clancy-implement.js` present):
|
|
30
|
+
|
|
31
|
+
a. Source `.clancy/.env` and check board credentials are present.
|
|
32
|
+
|
|
33
|
+
b. Check `CLANCY_ROLES` includes `planner` (or env var is unset, which indicates a global install where all roles are available). If `CLANCY_ROLES` is set but does not include `planner`:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
The Planner role is not enabled. Add "planner" to CLANCY_ROLES in .clancy/.env or run /clancy:settings.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Stop.
|
|
40
|
+
|
|
41
|
+
### 3. Standalone-mode preflight (only in standalone mode)
|
|
42
|
+
|
|
43
|
+
If in **standalone mode** (no `.clancy/.env`), check that `.clancy/plans/` exists. If not:
|
|
16
44
|
|
|
17
|
-
|
|
45
|
+
```
|
|
46
|
+
No local plans found. Run /clancy:plan --from .clancy/briefs/<brief>.md first.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Stop.
|
|
50
|
+
|
|
51
|
+
### 4. Standalone+board preflight (only in standalone+board mode)
|
|
18
52
|
|
|
19
|
-
|
|
53
|
+
If in **standalone+board mode**, source `.clancy/.env` for board credentials. Both plan-file stems and board ticket arguments are valid in this mode — Step 2 routes between them.
|
|
20
54
|
|
|
21
55
|
---
|
|
22
56
|
|
|
23
|
-
## Step 2 —
|
|
57
|
+
## Step 2 — Resolve target
|
|
24
58
|
|
|
25
|
-
|
|
59
|
+
The argument can be either a **plan-file stem** (e.g. `add-dark-mode-2`, matching a file at `.clancy/plans/{stem}.md`) or a **board ticket key** (e.g. `PROJ-123`, `#42`). Resolution depends on the installation mode detected in Step 1.
|
|
26
60
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
To resolve the title, fetch the ticket from the board:
|
|
39
|
-
- **GitHub:** `GET /repos/$GITHUB_REPO/issues/$ISSUE_NUMBER` → use `.title`
|
|
40
|
-
- **Jira:** `GET $JIRA_BASE_URL/rest/api/3/issue/$KEY?fields=summary` → use `.fields.summary`
|
|
41
|
-
- **Linear:** `issues(filter: { identifier: { eq: "$KEY" } }) { nodes { title } }` → use `nodes[0].title`
|
|
42
|
-
- **Azure DevOps:** `GET https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$ID?fields=System.Title&api-version=7.1` → use `.fields["System.Title"]`
|
|
43
|
-
- **Shortcut:** `GET https://api.app.shortcut.com/api/v3/stories/$STORY_ID` → use `.name`
|
|
44
|
-
- **Notion:** `GET https://api.notion.com/v1/pages/$PAGE_ID` → extract title from the `title` type property in `properties`
|
|
45
|
-
If fetching fails, show the key without a title: `Auto-selected [{KEY}] (planned {date}). Promote? [Y/n]`
|
|
46
|
-
5. If user declines:
|
|
47
|
-
```
|
|
48
|
-
Cancelled.
|
|
49
|
-
```
|
|
50
|
-
Stop.
|
|
51
|
-
6. Note that the user has already confirmed — set a flag to skip the Step 4 confirmation.
|
|
61
|
+
### Standalone mode
|
|
62
|
+
|
|
63
|
+
In standalone mode, the argument must be a plan-file stem. Board ticket keys are not valid here because there are no board credentials.
|
|
64
|
+
|
|
65
|
+
**With argument:** look up `.clancy/plans/{arg}.md`. If the file does not exist:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Plan file not found: .clancy/plans/{arg}.md. Plan stems include the row number (e.g. `add-dark-mode-2`). Run /clancy:plan --list to see available plans.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Stop. Do not attempt to interpret the argument as a ticket key in standalone mode.
|
|
52
72
|
|
|
53
|
-
|
|
73
|
+
**No argument:** auto-select the oldest unapproved local plan.
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
1. Scan `.clancy/plans/` for `.md` files
|
|
76
|
+
2. **Filter to plan files only**: a file qualifies as a plan if it contains the literal heading `## Clancy Implementation Plan` (the marker written by Step 4f / 5a of `plan.md`). Files without this heading are scratch / notes / drafts and are silently skipped — they are not approvable
|
|
77
|
+
3. For each remaining file, check whether a sibling `.approved` marker exists at the same path with the `.approved` suffix. The unapproved set is qualifying files with no sibling marker
|
|
78
|
+
4. Sort the unapproved set by the `**Planned:**` header date (ascending). Tie-break by Plan ID (alphabetical ascending). Files with a missing or unparseable `**Planned:**` date sort **last** (after all dated plans), then by Plan ID alphabetically among themselves. Mirrors `plan.md` Step 8 inventory's deterministic ordering
|
|
79
|
+
|
|
80
|
+
If the unapproved set is empty:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
No local plans awaiting approval. Run /clancy:plan --from .clancy/briefs/<brief>.md first, or all existing plans are already approved.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Stop. If non-empty, auto-select the first entry. Confirm with the user (skipped in `--afk` mode):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Auto-selected {stem} (planned {date}). Approve this plan? [Y/n]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If declined: `Cancelled.` Stop.
|
|
93
|
+
|
|
94
|
+
### Standalone+board and terminal modes
|
|
95
|
+
|
|
96
|
+
In these modes the argument may be either a plan-file stem or a board ticket key. **Try plan-file lookup first (does `.clancy/plans/{arg}.md` exist?)**, then fall back to ticket-key validation. The plan stem wins over ticket key on collision (e.g. if `PROJ-123.md` exists in `.clancy/plans/` AND `PROJ-123` is a valid ticket key, the plan stem wins). Document the collision rule explicitly so users are not surprised.
|
|
97
|
+
|
|
98
|
+
**With argument that resolves to a plan file:** continue to Step 4 (Confirm), then Step 4a (Write local marker). The board push offer for plan-file-stem mode is deferred to a future PR — for now the local marker is the only side effect.
|
|
99
|
+
|
|
100
|
+
**With argument that does not resolve to a plan file:** validate as a ticket key per the board configured in `.clancy/.env` (case-insensitive):
|
|
56
101
|
|
|
57
102
|
- **GitHub:** `#\d+` or bare number
|
|
58
103
|
- **Jira:** `[A-Za-z][A-Za-z0-9]+-\d+` (e.g. `PROJ-123` or `proj-123`)
|
|
@@ -69,7 +114,34 @@ Invalid ticket key: {input}. Expected format: {board-specific example}.
|
|
|
69
114
|
|
|
70
115
|
Stop.
|
|
71
116
|
|
|
72
|
-
|
|
117
|
+
If valid: proceed with that key. The board transport flow runs (Steps 3-7 below) — this is the existing behaviour, unchanged from before PR 7b.
|
|
118
|
+
|
|
119
|
+
**No argument:**
|
|
120
|
+
|
|
121
|
+
- **Standalone+board and terminal:** scan `.clancy/progress.txt` for entries matching `| PLAN |` or `| REVISED |` that have no subsequent `| APPROVE_PLAN |` for the same key. Sort by timestamp ascending (oldest first).
|
|
122
|
+
- If 0 found:
|
|
123
|
+
```
|
|
124
|
+
No planned tickets awaiting approval. Run /clancy:plan first.
|
|
125
|
+
```
|
|
126
|
+
Stop.
|
|
127
|
+
- If 1+ found, auto-select the oldest. Show:
|
|
128
|
+
```
|
|
129
|
+
Auto-selected [{KEY}] {Title} (planned {date}). Promote this plan? [Y/n]
|
|
130
|
+
```
|
|
131
|
+
To resolve the title, fetch the ticket from the board:
|
|
132
|
+
- **GitHub:** `GET /repos/$GITHUB_REPO/issues/$ISSUE_NUMBER` → use `.title`
|
|
133
|
+
- **Jira:** `GET $JIRA_BASE_URL/rest/api/3/issue/$KEY?fields=summary` → use `.fields.summary`
|
|
134
|
+
- **Linear:** `issues(filter: { identifier: { eq: "$KEY" } }) { nodes { title } }` → use `nodes[0].title`
|
|
135
|
+
- **Azure DevOps:** `GET https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$ID?fields=System.Title&api-version=7.1` → use `.fields["System.Title"]`
|
|
136
|
+
- **Shortcut:** `GET https://api.app.shortcut.com/api/v3/stories/$STORY_ID` → use `.name`
|
|
137
|
+
- **Notion:** `GET https://api.notion.com/v1/pages/$PAGE_ID` → extract title from the `title` type property in `properties`
|
|
138
|
+
If fetching fails, show the key without a title: `Auto-selected [{KEY}] (planned {date}). Promote? [Y/n]`
|
|
139
|
+
- If user declines:
|
|
140
|
+
```
|
|
141
|
+
Cancelled.
|
|
142
|
+
```
|
|
143
|
+
Stop.
|
|
144
|
+
- Note that the user has already confirmed — set a flag to skip the Step 4 confirmation.
|
|
73
145
|
|
|
74
146
|
---
|
|
75
147
|
|
|
@@ -270,6 +342,129 @@ If the user declines (interactive only), stop:
|
|
|
270
342
|
Cancelled. No changes made.
|
|
271
343
|
```
|
|
272
344
|
|
|
345
|
+
For **plan-file stem mode** (Step 2 resolved the argument to a local plan file), the summary shows the plan stem instead of `[{KEY}] {Title}`:
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
Clancy — Approve Plan (local)
|
|
349
|
+
|
|
350
|
+
{stem}
|
|
351
|
+
Size: {S/M/L} | {N} affected files
|
|
352
|
+
Planned: {date from plan}
|
|
353
|
+
|
|
354
|
+
Approve this plan? [Y/n]
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
After confirmation in plan-file stem mode, jump to Step 4a (local marker write). For board ticket key mode, continue to Step 5 below.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Step 4a — Write local marker
|
|
362
|
+
|
|
363
|
+
Run this step instead of Steps 5, 5b, 6 when the resolved argument was a plan-file stem (standalone mode, or standalone+board / terminal mode where Step 2 found a matching plan file). Write a `.clancy/plans/{stem}.approved` marker that gates `/clancy:implement-from`.
|
|
364
|
+
|
|
365
|
+
### Compute the SHA-256
|
|
366
|
+
|
|
367
|
+
**Order of operations** (do these in order, exactly):
|
|
368
|
+
|
|
369
|
+
1. Read the plan file at `.clancy/plans/{stem}.md` from disk into memory as bytes.
|
|
370
|
+
2. Compute the SHA-256 hash of those bytes — no normalisation (no line-ending fix, no trailing-whitespace strip, no BOM removal). Hex-encode lowercase.
|
|
371
|
+
3. **Then** (only after the hash is computed) open the `.approved` marker for exclusive create as described below.
|
|
372
|
+
|
|
373
|
+
The `.approved` file is **never** included in the hash — only `.clancy/plans/{stem}.md` is hashed, and only its on-disk byte content at the moment of step 1. PR 8's `/clancy:implement-from` re-reads the same plan file, hashes it the same way, and compares to the `sha256=` value stored in the marker. Any divergence (re-edit, line-ending change, trailing whitespace tweak) blocks implementation until re-approval.
|
|
374
|
+
|
|
375
|
+
### Write the marker file with O_EXCL
|
|
376
|
+
|
|
377
|
+
Open `.clancy/plans/{stem}.approved` for **exclusive create** (Node `fs.openSync(path, 'wx')`, equivalent to `open(2)` with `O_EXCL`). Write the marker body as plain text:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
sha256={hex sha256 of the plan file at approval time}
|
|
381
|
+
approved_at={ISO 8601 UTC timestamp, e.g. 2026-04-08T22:30:00Z}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Two `key=value` lines, each terminated with `\n`. No JSON, no extra whitespace, no comments. PR 8 parses this with a tolerant `^(sha256|approved_at)=(.+)$` regex per line.
|
|
385
|
+
|
|
386
|
+
### Handle EEXIST (already-approved)
|
|
387
|
+
|
|
388
|
+
If the exclusive create fails with `EEXIST`, the marker already exists — the plan was previously approved. Stop with:
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
Plan already approved: {stem}
|
|
392
|
+
Marker: .clancy/plans/{stem}.approved
|
|
393
|
+
|
|
394
|
+
To re-approve (e.g. after revising the plan):
|
|
395
|
+
Delete .clancy/plans/{stem}.approved manually, then re-run /clancy:approve-plan {stem}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
A `--fresh` flag for `/clancy:approve-plan` is not implemented in this release. Manual deletion is the supported re-approval path.
|
|
399
|
+
|
|
400
|
+
### Marker is the gate for /clancy:implement-from
|
|
401
|
+
|
|
402
|
+
The `.approved` marker is the gate `/clancy:implement-from` checks before applying changes. PR 8 reads the marker, hashes the current plan file, and compares to the stored `sha256`. Match → proceed; mismatch → block with a "plan changed since approval" error. This is why the SHA must be computed over the plan file content (not just touched as an empty file).
|
|
403
|
+
|
|
404
|
+
### After writing the marker
|
|
405
|
+
|
|
406
|
+
After the marker is written successfully, update the source brief file's marker comment (Step 4b below), then jump to Step 7 (Confirm and log). Steps 5, 5b, and 6 (board transport) are skipped entirely in plan-file stem mode.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Step 4b — Update brief marker (best-effort)
|
|
411
|
+
|
|
412
|
+
After Step 4a writes the local plan marker, update the source brief file's planned-rows marker so `/clancy:plan --list` and the brief's display logic know which rows have been approved. This step is best-effort: any failure here logs a warning but does NOT roll back the `.clancy/plans/{stem}.approved` marker.
|
|
413
|
+
|
|
414
|
+
### Resolve the source brief filename
|
|
415
|
+
|
|
416
|
+
Read the plan file at `.clancy/plans/{stem}.md` and extract the `**Brief:**` header line (e.g. `**Brief:** 2026-04-01-add-dark-mode.md`). The value is the brief filename relative to `.clancy/briefs/`. If the line is absent or empty, warn and skip the rest of Step 4b:
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
⚠ Plan {stem} has no **Brief:** header — cannot update brief marker. Continuing without brief update.
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Resolve the row number
|
|
423
|
+
|
|
424
|
+
Extract the row number from the plan file's `**Row:**` header line (e.g. `**Row:** #2 — Add toggle component`). The number after `#` and before the em-dash (U+2014, U+2013, or hyphen) is the row. If absent, warn and skip Step 4b.
|
|
425
|
+
|
|
426
|
+
### Find and update the marker
|
|
427
|
+
|
|
428
|
+
Open `.clancy/briefs/{brief-filename}` and find the marker comment matching this tolerant regex (line-anchored, allows missing-or-present `approved:` prefix, allows arbitrary whitespace):
|
|
429
|
+
|
|
430
|
+
```
|
|
431
|
+
^<!--\s*(?:approved:([\d,]*)\s+)?planned:([\d,]+)\s*-->\s*$
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
This matches all of:
|
|
435
|
+
|
|
436
|
+
- `<!-- planned:1,2,3 -->` (no approved prefix yet — current state from PR 6b)
|
|
437
|
+
- `<!-- approved:1 planned:1,2,3 -->` (PR 7b adds the approved prefix)
|
|
438
|
+
- `<!-- approved: planned:1,2,3 -->` (empty approved list — should not happen but handle gracefully)
|
|
439
|
+
- `<!--planned:1,2,3-->` (no surrounding spaces — hand-edited)
|
|
440
|
+
|
|
441
|
+
If no marker line matches, warn and skip — do not synthesise a marker. The brief should already have one written by `/clancy:plan --from`.
|
|
442
|
+
|
|
443
|
+
**Reversed-order markers** (`<!-- planned:1,2 approved:1 -->` — `planned:` first, `approved:` second) do NOT match this regex and fall through to the warn-and-skip branch. The canonical ordering is enforced on every write, so a brief that drifts into reversed order requires manual correction. If you see the warning "no marker found" but the brief clearly has a marker, check the order — `approved:` must come before `planned:`.
|
|
444
|
+
|
|
445
|
+
**Code-fence false positives:** the regex is line-anchored but does NOT track fenced-code-block context. If a brief file embeds an example marker inside a triple-backtick block (e.g. documentation about how markers work), the regex may match the example. The first match wins, so authors should keep the real marker as the first marker line in the file. The `## Feedback` detector in `plan.md` Step 3a uses code-fence-aware parsing for the same reason — apply that pattern manually if a brief becomes complex enough to need it.
|
|
446
|
+
|
|
447
|
+
If a marker matches, parse the existing `approved:` and `planned:` row lists. Add the current row number to the `approved:` list (deduped, sorted ascending). Reconstruct the marker with the canonical ordering: `approved:` first, `planned:` second, single space between fields and inside the comment. Example:
|
|
448
|
+
|
|
449
|
+
- Before: `<!-- planned:1,2,3 -->`, current row = `2`
|
|
450
|
+
- After: `<!-- approved:2 planned:1,2,3 -->`
|
|
451
|
+
- Before: `<!-- approved:1 planned:1,2,3 -->`, current row = `2`
|
|
452
|
+
- After: `<!-- approved:1,2 planned:1,2,3 -->`
|
|
453
|
+
|
|
454
|
+
Write the updated brief file back. The read-modify-write is not concurrency-safe — running multiple `/clancy:approve-plan` commands against the same brief in parallel may produce duplicate or missing entries. Single-user local flow is assumed (mirrors the concurrency note in [`plan.md` Step 3a](./plan.md)).
|
|
455
|
+
|
|
456
|
+
### Best-effort failure handling
|
|
457
|
+
|
|
458
|
+
If any step in 4b fails (file not found, marker not found, write error, regex mismatch), log a warning but do NOT roll back the `.clancy/plans/{stem}.approved` marker. The local marker is the source of truth — the brief marker is metadata for display and `/clancy:plan --list`. Example warning:
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
⚠ Failed to update brief marker for {stem}: {reason}
|
|
462
|
+
The plan is still approved. The .clancy/plans/{stem}.approved marker is in place.
|
|
463
|
+
You can manually update .clancy/briefs/{brief}.md if needed.
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
After Step 4b completes (successfully or with a warning), jump to Step 7 (Confirm and log). Skip Steps 5, 5b, and 6 entirely.
|
|
467
|
+
|
|
273
468
|
---
|
|
274
469
|
|
|
275
470
|
## Step 5 — Update ticket description
|
|
@@ -870,7 +1065,9 @@ Could not transition ticket. Move it manually to your implementation queue.
|
|
|
870
1065
|
|
|
871
1066
|
## Step 7 — Confirm and log
|
|
872
1067
|
|
|
873
|
-
|
|
1068
|
+
**Mode gate (read first):** if Steps 4a/4b ran (the resolved argument was a plan-file stem), skip the entire "board-specific success message" and "board-mode progress.txt entry" blocks below and jump straight to the **Local mode (Step 4a / 4b path)** subsection further down. The board-success-message text only applies when Steps 5/5b/6 ran for a board ticket key. Do NOT render both — exactly one branch executes per approval.
|
|
1069
|
+
|
|
1070
|
+
On success in **board ticket mode**, display a board-specific message:
|
|
874
1071
|
|
|
875
1072
|
**GitHub:**
|
|
876
1073
|
|
|
@@ -944,18 +1141,50 @@ Plan promoted to page content. Move the page to your implementation queue for /c
|
|
|
944
1141
|
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
945
1142
|
```
|
|
946
1143
|
|
|
947
|
-
Append to `.clancy/progress.txt
|
|
1144
|
+
Append to `.clancy/progress.txt` for **board mode**:
|
|
948
1145
|
|
|
949
1146
|
```
|
|
950
1147
|
YYYY-MM-DD HH:MM | {KEY} | APPROVE_PLAN | —
|
|
951
1148
|
```
|
|
952
1149
|
|
|
953
|
-
On failure:
|
|
1150
|
+
On board failure:
|
|
954
1151
|
|
|
955
1152
|
```
|
|
956
1153
|
Failed to update description for [{KEY}]. Check your board permissions.
|
|
957
1154
|
```
|
|
958
1155
|
|
|
1156
|
+
### Local mode (Step 4a / 4b path)
|
|
1157
|
+
|
|
1158
|
+
For **plan-file stem mode** (Step 4a wrote a `.approved` marker), display:
|
|
1159
|
+
|
|
1160
|
+
```
|
|
1161
|
+
Clancy — Approve Plan (local)
|
|
1162
|
+
|
|
1163
|
+
✅ Approved {stem}
|
|
1164
|
+
Marker: .clancy/plans/{stem}.approved
|
|
1165
|
+
sha256: {first 12 hex chars}…
|
|
1166
|
+
|
|
1167
|
+
Next: /clancy:implement-from .clancy/plans/{stem}.md
|
|
1168
|
+
|
|
1169
|
+
"Book 'em, Lou."
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
**Conditional Brief line:** if Step 4b actually resolved AND updated the brief marker (i.e. the `**Brief:**`/`**Row:**` headers were present, the marker regex matched, and the write succeeded), insert this line between `sha256:` and the blank line above `Next:`:
|
|
1173
|
+
|
|
1174
|
+
```
|
|
1175
|
+
Brief: .clancy/briefs/{brief}.md (row #{N} marked approved)
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
Do NOT print the Brief line when Step 4b warned and skipped (missing headers, no matching marker, or write error). In that case, also print the warning that Step 4b emitted under the success block but do not change the exit status — the plan IS approved regardless of whether the brief marker was updated.
|
|
1179
|
+
|
|
1180
|
+
Append to `.clancy/progress.txt`:
|
|
1181
|
+
|
|
1182
|
+
```
|
|
1183
|
+
YYYY-MM-DD HH:MM | {stem} | LOCAL_APPROVE_PLAN | sha256={first 12 hex}
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
The `LOCAL_APPROVE_PLAN` token mirrors the `LOCAL_PLAN` / `LOCAL_REVISED` convention used by `/clancy:plan --from` (see [`plan.md` Step 6](./plan.md)). PR 8's `/clancy:implement-from` does NOT scan progress.txt for approval state — it reads the `.clancy/plans/{stem}.approved` marker directly. The log entry is for human audit only.
|
|
1187
|
+
|
|
959
1188
|
---
|
|
960
1189
|
|
|
961
1190
|
## Notes
|
package/src/workflows/plan.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
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.
|
|
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. Use `/clancy:approve-plan` to approve plans in any install mode — it ships with this package, writes a local `.approved` marker for plan-file stems, and runs the board transport flow for ticket keys only when board credentials are available.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ Check for `.clancy/.env`:
|
|
|
24
24
|
If `.clancy/.env` is present, check for `.clancy/clancy-implement.js`:
|
|
25
25
|
|
|
26
26
|
- **Present** → **terminal mode**. Full Clancy pipeline installed.
|
|
27
|
-
- **Absent** → **standalone+board mode**. Board credentials available via `/clancy:board-setup`. Board ticket mode works. Step 5 works.
|
|
27
|
+
- **Absent** → **standalone+board mode**. Board credentials available via `/clancy:board-setup`. Board ticket mode works. Step 5 works. `/clancy:approve-plan` also works in this mode — it ships with the plan package and runs the board transport flow for ticket keys or writes a local `.approved` marker for plan-file stems.
|
|
28
28
|
|
|
29
29
|
### 2. Terminal-mode preflight (skip in standalone mode and standalone+board mode)
|
|
30
30
|
|
|
@@ -822,7 +822,7 @@ Write the plan in this exact template:
|
|
|
822
822
|
|
|
823
823
|
---
|
|
824
824
|
|
|
825
|
-
_Generated by [Clancy](https://github.com/Pushedskydiver/chief-clancy). To request changes: comment on this ticket, then re-run `/clancy:plan` to revise. To start over: `/clancy:plan --fresh`. To approve: `/clancy:approve-plan {KEY}
|
|
825
|
+
_Generated by [Clancy](https://github.com/Pushedskydiver/chief-clancy). To request changes: comment on this ticket, then re-run `/clancy:plan` to revise. To start over: `/clancy:plan --fresh`. To approve: `/clancy:approve-plan {KEY}`._
|
|
826
826
|
```
|
|
827
827
|
|
|
828
828
|
**If re-planning with feedback**, prepend a section before Summary:
|
|
@@ -885,7 +885,7 @@ Replace the `**Ticket:** [{KEY}] {Title}` line from the board template with `**S
|
|
|
885
885
|
**Local plan footer:** Replace the board-specific footer with:
|
|
886
886
|
|
|
887
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:
|
|
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: `/clancy:approve-plan {plan-id}`._
|
|
889
889
|
```
|
|
890
890
|
|
|
891
891
|
**Re-planning:** If `--fresh` was used, the existing plan file is overwritten (same slug + row number = same filename).
|
|
@@ -1052,8 +1052,7 @@ Planned {N} ticket(s):
|
|
|
1052
1052
|
⏭️ [{KEY4}] {Title} — not a codebase change
|
|
1053
1053
|
|
|
1054
1054
|
Plans written to your board. After review:
|
|
1055
|
-
|
|
1056
|
-
Standalone: install the full pipeline — npx chief-clancy
|
|
1055
|
+
/clancy:approve-plan {KEY}
|
|
1057
1056
|
|
|
1058
1057
|
"Let me dust this for prints..."
|
|
1059
1058
|
```
|
|
@@ -1068,7 +1067,7 @@ Single row:
|
|
|
1068
1067
|
|
|
1069
1068
|
✅ Saved to .clancy/plans/{slug}-{row-number}.md
|
|
1070
1069
|
|
|
1071
|
-
To approve:
|
|
1070
|
+
To approve: /clancy:approve-plan {slug}-{row-number}
|
|
1072
1071
|
|
|
1073
1072
|
"Let me dust this for prints..."
|
|
1074
1073
|
```
|
|
@@ -1083,7 +1082,7 @@ Multi-row (`--afk`):
|
|
|
1083
1082
|
✅ Row 2: {title} — Saved to .clancy/plans/{slug}-2.md
|
|
1084
1083
|
⏭️ Row 3: {title} — already planned
|
|
1085
1084
|
|
|
1086
|
-
To approve:
|
|
1085
|
+
To approve: /clancy:approve-plan {slug}-{row-number} (or use /clancy:plan --list to see all plans)
|
|
1087
1086
|
|
|
1088
1087
|
"Let me dust this for prints..."
|
|
1089
1088
|
```
|
|
@@ -1101,35 +1100,47 @@ Scan `.clancy/plans/` for all `.md` files. For each file, parse the local plan h
|
|
|
1101
1100
|
- **Row** — value of the `**Row:**` line (e.g. `#2 — Add toggle component`). Display `?` if absent or empty.
|
|
1102
1101
|
- **Source** — value of the `**Source:**` line (the brief's Source field). Display `?` if absent or empty.
|
|
1103
1102
|
- **Planned** — value of the `**Planned:**` line (the YYYY-MM-DD planned date). Display `?` if absent or unparseable as a date.
|
|
1104
|
-
- **Status** —
|
|
1103
|
+
- **Status** — derived live from the sibling `.approved` marker written by `/clancy:approve-plan` (PR 7b). The reader is filesystem-only — no `.clancy/.env` or board access required. For each plan file, follow this procedure (every numbered step either branches to a verdict or feeds the next step):
|
|
1104
|
+
1. Check whether `.clancy/plans/{plan-id}.approved` exists. If marker absent → `Planned` (verdict)
|
|
1105
|
+
2. Marker exists. Read and validate its `sha256=` line (the marker body is two `key=value` lines: `sha256={hex}` and `approved_at={ISO 8601}`) AND hash the current plan file's bytes the same way `/clancy:approve-plan` Step 4a does (lowercase hex SHA-256, no normalisation, no line-ending fix)
|
|
1106
|
+
3. If the marker exists but is malformed, missing its `sha256=` line, has a non-hex or wrong-length `sha256` value, or otherwise cannot be parsed deterministically → `Stale (re-approve)` (verdict). Print a hint after the table: `Marker .clancy/plans/{plan-id}.approved is malformed. Delete it and re-run /clancy:approve-plan {plan-id} to recreate.` Folding this into `Stale` (rather than inventing a new state) keeps the inventory deterministic — the user's remediation is the same as for a hash drift: delete the marker and re-approve
|
|
1107
|
+
4. If the marker's valid `sha256` matches the current plan file's hash → `Approved` (verdict)
|
|
1108
|
+
5. If the marker exists and its valid `sha256` differs from the current hash → `Stale (re-approve)` (verdict) — the plan file was edited after approval. `/clancy:implement-from` (PR 8) will refuse to run against a stale plan until it is re-approved
|
|
1109
|
+
|
|
1110
|
+
A future `Implemented` state — derived from `LOCAL_IMPLEMENT` entries in `.clancy/progress.txt` — will be added by PR 8. Today the inventory shows three states (`Planned`, `Approved`, `Stale (re-approve)`), with malformed `.approved` markers folded into `Stale (re-approve)`; the table format is stable so PR 8's addition will be a one-line extension.
|
|
1105
1111
|
|
|
1106
1112
|
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
1113
|
|
|
1108
1114
|
**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
1115
|
|
|
1110
|
-
|
|
1116
|
+
**Summary line:** after the table, print one line in the format `{N} local plan(s). {A} approved, {S} stale, {P} planned.` where the counts match the rows above. Omit zero-count states for brevity (e.g. `3 local plan(s). 2 approved, 1 planned.` if no plans are stale). A `Stale (re-approve)` row means the plan file was edited after approval — re-running `/clancy:approve-plan {plan-id}` (after deleting the existing marker) will refresh the SHA.
|
|
1117
|
+
|
|
1118
|
+
Display the inventory as a pipe-delimited table so each Status value is unambiguous to scan and parse:
|
|
1111
1119
|
|
|
1112
1120
|
```
|
|
1113
1121
|
Clancy — Plans
|
|
1114
1122
|
================================================================
|
|
1115
1123
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1124
|
+
| # | Plan ID | Planned | Status | Row | Brief | Source |
|
|
1125
|
+
|---|--------------------|------------|---------------------|----------------------------------|------------------------------------|-----------|
|
|
1126
|
+
| 1 | add-dark-mode-2 | 2026-04-08 | Approved | #2 — Add toggle component | 2026-04-01-add-dark-mode.md | #50 |
|
|
1127
|
+
| 2 | add-dark-mode-1 | 2026-04-07 | Stale (re-approve) | #1 — Wire theme context | 2026-04-01-add-dark-mode.md | #50 |
|
|
1128
|
+
| 3 | customer-portal-3 | 2026-04-05 | Planned | #3 — Billing page | 2026-03-28-customer-portal.md | PROJ-200 |
|
|
1119
1129
|
|
|
1120
|
-
3 local plan(s).
|
|
1130
|
+
3 local plan(s). 1 approved, 1 stale, 1 planned.
|
|
1121
1131
|
|
|
1122
|
-
To
|
|
1123
|
-
To
|
|
1124
|
-
To
|
|
1132
|
+
To approve a plan: /clancy:approve-plan {plan-id}
|
|
1133
|
+
To re-approve a stale plan: delete .clancy/plans/{plan-id}.approved, then /clancy:approve-plan {plan-id}
|
|
1134
|
+
To revise: add a `## Feedback` section to the plan file, then re-run /clancy:plan --from {brief}
|
|
1135
|
+
To start over: /clancy:plan --fresh --from {brief}
|
|
1125
1136
|
```
|
|
1126
1137
|
|
|
1127
|
-
The first column
|
|
1138
|
+
The first column (after `#`) is the Plan ID (filename without `.md`), not the brief slug. Pipe-table format is intentional — Status values like `Stale (re-approve)` contain spaces, and a column-aligned space-delimited layout would make them ambiguous to read.
|
|
1128
1139
|
|
|
1129
1140
|
If `.clancy/plans/` does not exist or contains no `.md` files:
|
|
1130
1141
|
|
|
1131
1142
|
```
|
|
1132
|
-
No plans found. Run /clancy:plan --from .clancy/briefs
|
|
1143
|
+
No plans found. Run /clancy:plan --from .clancy/briefs/{brief}.md to create one.
|
|
1133
1144
|
```
|
|
1134
1145
|
|
|
1135
1146
|
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.
|
|
@@ -595,13 +595,75 @@ describe('plan inventory step', () => {
|
|
|
595
595
|
expect(content).toContain(
|
|
596
596
|
'**Plan ID** — the plan filename minus the `.md` extension',
|
|
597
597
|
);
|
|
598
|
-
expect(content).toContain('first column
|
|
598
|
+
expect(content).toContain('first column (after `#`) is the Plan ID');
|
|
599
599
|
});
|
|
600
600
|
|
|
601
|
-
it('
|
|
601
|
+
it('Status column reads sibling .approved marker for live state', () => {
|
|
602
602
|
expect(content).toContain('**Status**');
|
|
603
|
-
expect(content).toContain('
|
|
604
|
-
|
|
603
|
+
expect(content).toContain('sibling `.approved` marker');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('Status is Planned when no marker exists', () => {
|
|
607
|
+
expect(content).toContain('marker absent → `Planned`');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('Status is Approved when marker sha256 matches the current plan file', () => {
|
|
611
|
+
expect(content).toContain('sha256` matches the current plan file');
|
|
612
|
+
expect(content).toContain('→ `Approved`');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('Status is Stale (re-approve) when marker sha256 drifts from the plan file', () => {
|
|
616
|
+
expect(content).toContain('Stale (re-approve)');
|
|
617
|
+
expect(content).toContain('sha256` differs');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('inventory example shows at least one Approved row', () => {
|
|
621
|
+
// Match a pipe-delimited table cell on both sides (literal `|`s with
|
|
622
|
+
// tolerant whitespace) so the test proves the example output is in the
|
|
623
|
+
// pipe-delimited table format, not just that the word `Approved` happens
|
|
624
|
+
// to appear in nearby prose.
|
|
625
|
+
expect(content).toMatch(/\|\s*Approved\s*\|/);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('inventory example shows at least one Stale row', () => {
|
|
629
|
+
expect(content).toMatch(/\|\s*Stale \(re-approve\)\s*\|/);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('inventory example still shows at least one Planned row', () => {
|
|
633
|
+
expect(content).toMatch(/\|\s*Planned\s*\|/);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('inventory documents the summary line format and zero-count omission', () => {
|
|
637
|
+
expect(content).toContain('Summary line');
|
|
638
|
+
expect(content).toContain(
|
|
639
|
+
'{N} local plan(s). {A} approved, {S} stale, {P} planned.',
|
|
640
|
+
);
|
|
641
|
+
expect(content).toContain('Omit zero-count states');
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('explains that Stale means the plan was edited after approval', () => {
|
|
645
|
+
expect(content).toContain('plan file was edited after approval');
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('reserves an Implemented state for PR 8 without claiming it exists today', () => {
|
|
649
|
+
expect(content).toContain('Implemented');
|
|
650
|
+
expect(content).toContain('PR 8');
|
|
651
|
+
expect(content).toContain('shows three states');
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('folds malformed .approved markers into Stale (re-approve)', () => {
|
|
655
|
+
expect(content).toContain('marker exists but is malformed');
|
|
656
|
+
expect(content).toContain('non-hex or wrong-length');
|
|
657
|
+
expect(content).toContain('cannot be parsed deterministically');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('uses {plan-id} placeholder consistently in the footer (no <plan-id>)', () => {
|
|
661
|
+
expect(content).not.toContain('<plan-id>');
|
|
662
|
+
expect(content).toContain('/clancy:approve-plan {plan-id}');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('inventory footer hint points at /clancy:approve-plan', () => {
|
|
666
|
+
expect(content).toContain('/clancy:approve-plan');
|
|
605
667
|
});
|
|
606
668
|
|
|
607
669
|
it('sort is deterministic with explicit tie-breakers', () => {
|
|
@@ -640,3 +702,302 @@ describe('plan inventory step', () => {
|
|
|
640
702
|
);
|
|
641
703
|
});
|
|
642
704
|
});
|
|
705
|
+
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
// approve-plan.md content assertions (PR 7b — standalone adaptation)
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
|
|
710
|
+
describe('approve-plan three-state preflight', () => {
|
|
711
|
+
const content = readFileSync(
|
|
712
|
+
new URL('approve-plan.md', import.meta.url),
|
|
713
|
+
'utf8',
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
it('Step 1 detects all three installation states', () => {
|
|
717
|
+
expect(content).toContain('standalone mode');
|
|
718
|
+
expect(content).toContain('standalone+board mode');
|
|
719
|
+
expect(content).toContain('terminal mode');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('Step 1 checks .clancy/.env presence for state detection', () => {
|
|
723
|
+
expect(content).toContain('.clancy/.env');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('Step 1 checks clancy-implement.js for terminal detection', () => {
|
|
727
|
+
expect(content).toContain('clancy-implement.js');
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('Step 1 does not hard-stop on missing .clancy/.env', () => {
|
|
731
|
+
expect(content).not.toContain(
|
|
732
|
+
'.clancy/ not found. Run /clancy:init to set up Clancy first.',
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('CLANCY_ROLES check only runs in terminal mode', () => {
|
|
737
|
+
expect(content).toContain('Terminal-mode preflight');
|
|
738
|
+
expect(content).toContain(
|
|
739
|
+
'skip in standalone mode and standalone+board mode',
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('standalone mode requires .clancy/plans/ to exist', () => {
|
|
744
|
+
expect(content).toContain('.clancy/plans/');
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
describe('approve-plan dual-mode resolver (Step 2)', () => {
|
|
749
|
+
const content = readFileSync(
|
|
750
|
+
new URL('approve-plan.md', import.meta.url),
|
|
751
|
+
'utf8',
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
it('Step 2 routes between plan-file stem and ticket key based on mode', () => {
|
|
755
|
+
expect(content).toContain('plan-file stem');
|
|
756
|
+
expect(content).toContain('ticket key');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('standalone mode only accepts plan-file stems', () => {
|
|
760
|
+
expect(content).toContain(
|
|
761
|
+
'In standalone mode, the argument must be a plan-file stem',
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('standalone+board and terminal modes try plan-file lookup first', () => {
|
|
766
|
+
expect(content).toContain(
|
|
767
|
+
'Try plan-file lookup first (does `.clancy/plans/{arg}.md` exist?)',
|
|
768
|
+
);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('plan stem wins over ticket key on collision', () => {
|
|
772
|
+
expect(content).toContain('plan stem wins over ticket key');
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('standalone no-arg auto-selects oldest unapproved plan', () => {
|
|
776
|
+
expect(content).toContain('auto-select the oldest unapproved local plan');
|
|
777
|
+
expect(content).toContain('no sibling marker');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('standalone no-arg filters scan to actual plan files', () => {
|
|
781
|
+
expect(content).toContain('## Clancy Implementation Plan');
|
|
782
|
+
expect(content).toContain('Filter to plan files only');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('standalone no-arg sorts files with missing/unparseable date last', () => {
|
|
786
|
+
expect(content).toContain('missing or unparseable `**Planned:**` date');
|
|
787
|
+
expect(content).toContain('sort **last**');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('preserves existing terminal-mode no-arg progress.txt scan', () => {
|
|
791
|
+
expect(content).toContain('.clancy/progress.txt');
|
|
792
|
+
expect(content).toContain('| PLAN |');
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('errors clearly if standalone arg is not a plan-file stem', () => {
|
|
796
|
+
expect(content).toContain('Plan file not found:');
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('errors if standalone has no plans and no arg', () => {
|
|
800
|
+
expect(content).toContain('No local plans awaiting approval');
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
describe('approve-plan local marker (Step 4a)', () => {
|
|
805
|
+
const content = readFileSync(
|
|
806
|
+
new URL('approve-plan.md', import.meta.url),
|
|
807
|
+
'utf8',
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
it('defines Step 4a — Write local marker', () => {
|
|
811
|
+
expect(content).toContain('## Step 4a — Write local marker');
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('Step 4a runs only when the resolved arg was a plan-file stem', () => {
|
|
815
|
+
expect(content).toContain(
|
|
816
|
+
'Run this step instead of Steps 5, 5b, 6 when the resolved argument was a plan-file stem',
|
|
817
|
+
);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('writes marker to .clancy/plans/{stem}.approved', () => {
|
|
821
|
+
expect(content).toContain('.clancy/plans/{stem}.approved');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('uses race-safe exclusive create (O_EXCL / wx)', () => {
|
|
825
|
+
expect(content).toContain('exclusive create');
|
|
826
|
+
expect(content).toContain('O_EXCL');
|
|
827
|
+
expect(content).toContain('wx');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('marker body contains sha256 and approved_at fields', () => {
|
|
831
|
+
expect(content).toContain('sha256=');
|
|
832
|
+
expect(content).toContain('approved_at=');
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('sha256 is computed over the plan file content at approval time', () => {
|
|
836
|
+
expect(content).toContain('SHA-256');
|
|
837
|
+
expect(content).toContain('Order of operations');
|
|
838
|
+
expect(content).toContain(
|
|
839
|
+
'Read the plan file at `.clancy/plans/{stem}.md` from disk',
|
|
840
|
+
);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('SHA hash is never computed over the .approved marker itself', () => {
|
|
844
|
+
expect(content).toContain('`.approved` file is **never** included');
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('approved_at is ISO 8601 UTC', () => {
|
|
848
|
+
expect(content).toContain('ISO 8601');
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('handles EEXIST as already-approved', () => {
|
|
852
|
+
expect(content).toContain('EEXIST');
|
|
853
|
+
expect(content).toContain('already approved');
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('explains the marker is the gate for /clancy:implement-from', () => {
|
|
857
|
+
expect(content).toContain('/clancy:implement-from');
|
|
858
|
+
expect(content).toContain('gate');
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('after writing the marker, Step 4a jumps to Step 7 (log) and skips board flow', () => {
|
|
862
|
+
expect(content).toContain('jump to Step 7');
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe('approve-plan brief-marker update (Step 4b)', () => {
|
|
867
|
+
const content = readFileSync(
|
|
868
|
+
new URL('approve-plan.md', import.meta.url),
|
|
869
|
+
'utf8',
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
it('defines Step 4b — Update brief marker', () => {
|
|
873
|
+
expect(content).toContain('## Step 4b — Update brief marker');
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('resolves brief filename from the plan **Brief:** header', () => {
|
|
877
|
+
expect(content).toContain('**Brief:**');
|
|
878
|
+
expect(content).toContain('extract the `**Brief:**` header line');
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('resolves row number from the plan **Row:** header', () => {
|
|
882
|
+
expect(content).toContain('**Row:**');
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('uses a tolerant line-anchored regex with optional approved prefix', () => {
|
|
886
|
+
expect(content).toContain(
|
|
887
|
+
'^<!--\\s*(?:approved:([\\d,]*)\\s+)?planned:([\\d,]+)\\s*-->\\s*$',
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('canonical ordering puts approved before planned', () => {
|
|
892
|
+
expect(content).toContain('approved:` first, `planned:` second');
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('handles existing planned-only marker (PR 6b state)', () => {
|
|
896
|
+
expect(content).toContain('<!-- planned:1,2,3 -->');
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('handles existing approved+planned marker', () => {
|
|
900
|
+
expect(content).toContain('<!-- approved:1 planned:1,2,3 -->');
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('handles unspaced marker variant', () => {
|
|
904
|
+
expect(content).toContain('<!--planned:1,2,3-->');
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('best-effort: failure does not roll back the .approved marker', () => {
|
|
908
|
+
expect(content).toContain('does NOT roll back');
|
|
909
|
+
expect(content).toContain('local marker is the source of truth');
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it('warns and skips when **Brief:** header is absent', () => {
|
|
913
|
+
expect(content).toContain('cannot update brief marker');
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('documents concurrency-not-safe nature of the read-modify-write', () => {
|
|
917
|
+
expect(content).toContain('not concurrency-safe');
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('documents reversed-order brief markers fall through to warn-and-skip', () => {
|
|
921
|
+
expect(content).toContain('Reversed-order markers');
|
|
922
|
+
expect(content).toContain('do NOT match this regex');
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('documents code-fence false-positive risk', () => {
|
|
926
|
+
expect(content).toContain('Code-fence false positives');
|
|
927
|
+
expect(content).toContain('first match wins');
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
describe('approve-plan local-mode log + summary (Step 7)', () => {
|
|
932
|
+
const content = readFileSync(
|
|
933
|
+
new URL('approve-plan.md', import.meta.url),
|
|
934
|
+
'utf8',
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
it('uses LOCAL_APPROVE_PLAN log token for plan-file stem mode', () => {
|
|
938
|
+
expect(content).toContain('LOCAL_APPROVE_PLAN');
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it('local log entry includes the sha256 prefix for audit', () => {
|
|
942
|
+
expect(content).toContain('sha256={first 12 hex}');
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('local success summary points to /clancy:implement-from', () => {
|
|
946
|
+
expect(content).toContain(
|
|
947
|
+
'Next: /clancy:implement-from .clancy/plans/{stem}.md',
|
|
948
|
+
);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('preserves board-mode APPROVE_PLAN log entry', () => {
|
|
952
|
+
expect(content).toContain('| {KEY} | APPROVE_PLAN | —');
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it('Step 7 has an explicit mode gate so local and board branches do not double-render', () => {
|
|
956
|
+
expect(content).toContain('Mode gate (read first)');
|
|
957
|
+
expect(content).toContain(
|
|
958
|
+
'Do NOT render both — exactly one branch executes per approval',
|
|
959
|
+
);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('plan-file-not-found error hints at the row-number convention', () => {
|
|
963
|
+
expect(content).toContain('Plan stems include the row number');
|
|
964
|
+
expect(content).toContain('Run /clancy:plan --list');
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('EEXIST advice points at manual deletion, not a non-existent --fresh flag', () => {
|
|
968
|
+
expect(content).toContain('Delete .clancy/plans/{stem}.approved manually');
|
|
969
|
+
expect(content).not.toContain('/clancy:approve-plan --fresh');
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
describe('approve-plan board mode preserved unchanged', () => {
|
|
974
|
+
const content = readFileSync(
|
|
975
|
+
new URL('approve-plan.md', import.meta.url),
|
|
976
|
+
'utf8',
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
it('keeps Jira ADF construction', () => {
|
|
980
|
+
expect(content).toContain('ADF');
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it('keeps GitHub PATCH /issues for description update', () => {
|
|
984
|
+
expect(content).toContain('PATCH');
|
|
985
|
+
expect(content).toContain('/issues/');
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('keeps Linear issueUpdate mutation', () => {
|
|
989
|
+
expect(content).toContain('issueUpdate');
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('keeps Azure DevOps work item update', () => {
|
|
993
|
+
expect(content).toContain('Azure DevOps');
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('keeps Shortcut PUT /stories', () => {
|
|
997
|
+
expect(content).toContain('Shortcut');
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('keeps Notion description update', () => {
|
|
1001
|
+
expect(content).toContain('Notion');
|
|
1002
|
+
});
|
|
1003
|
+
});
|