@chief-clancy/plan 0.3.0 → 0.4.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 +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/workflows.test.ts +299 -0
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
|
|
@@ -640,3 +640,302 @@ describe('plan inventory step', () => {
|
|
|
640
640
|
);
|
|
641
641
|
});
|
|
642
642
|
});
|
|
643
|
+
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
// approve-plan.md content assertions (PR 7b — standalone adaptation)
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
|
|
648
|
+
describe('approve-plan three-state preflight', () => {
|
|
649
|
+
const content = readFileSync(
|
|
650
|
+
new URL('approve-plan.md', import.meta.url),
|
|
651
|
+
'utf8',
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
it('Step 1 detects all three installation states', () => {
|
|
655
|
+
expect(content).toContain('standalone mode');
|
|
656
|
+
expect(content).toContain('standalone+board mode');
|
|
657
|
+
expect(content).toContain('terminal mode');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('Step 1 checks .clancy/.env presence for state detection', () => {
|
|
661
|
+
expect(content).toContain('.clancy/.env');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('Step 1 checks clancy-implement.js for terminal detection', () => {
|
|
665
|
+
expect(content).toContain('clancy-implement.js');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('Step 1 does not hard-stop on missing .clancy/.env', () => {
|
|
669
|
+
expect(content).not.toContain(
|
|
670
|
+
'.clancy/ not found. Run /clancy:init to set up Clancy first.',
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('CLANCY_ROLES check only runs in terminal mode', () => {
|
|
675
|
+
expect(content).toContain('Terminal-mode preflight');
|
|
676
|
+
expect(content).toContain(
|
|
677
|
+
'skip in standalone mode and standalone+board mode',
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('standalone mode requires .clancy/plans/ to exist', () => {
|
|
682
|
+
expect(content).toContain('.clancy/plans/');
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe('approve-plan dual-mode resolver (Step 2)', () => {
|
|
687
|
+
const content = readFileSync(
|
|
688
|
+
new URL('approve-plan.md', import.meta.url),
|
|
689
|
+
'utf8',
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
it('Step 2 routes between plan-file stem and ticket key based on mode', () => {
|
|
693
|
+
expect(content).toContain('plan-file stem');
|
|
694
|
+
expect(content).toContain('ticket key');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('standalone mode only accepts plan-file stems', () => {
|
|
698
|
+
expect(content).toContain(
|
|
699
|
+
'In standalone mode, the argument must be a plan-file stem',
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('standalone+board and terminal modes try plan-file lookup first', () => {
|
|
704
|
+
expect(content).toContain(
|
|
705
|
+
'Try plan-file lookup first (does `.clancy/plans/{arg}.md` exist?)',
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('plan stem wins over ticket key on collision', () => {
|
|
710
|
+
expect(content).toContain('plan stem wins over ticket key');
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('standalone no-arg auto-selects oldest unapproved plan', () => {
|
|
714
|
+
expect(content).toContain('auto-select the oldest unapproved local plan');
|
|
715
|
+
expect(content).toContain('no sibling marker');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('standalone no-arg filters scan to actual plan files', () => {
|
|
719
|
+
expect(content).toContain('## Clancy Implementation Plan');
|
|
720
|
+
expect(content).toContain('Filter to plan files only');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('standalone no-arg sorts files with missing/unparseable date last', () => {
|
|
724
|
+
expect(content).toContain('missing or unparseable `**Planned:**` date');
|
|
725
|
+
expect(content).toContain('sort **last**');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('preserves existing terminal-mode no-arg progress.txt scan', () => {
|
|
729
|
+
expect(content).toContain('.clancy/progress.txt');
|
|
730
|
+
expect(content).toContain('| PLAN |');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('errors clearly if standalone arg is not a plan-file stem', () => {
|
|
734
|
+
expect(content).toContain('Plan file not found:');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('errors if standalone has no plans and no arg', () => {
|
|
738
|
+
expect(content).toContain('No local plans awaiting approval');
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
describe('approve-plan local marker (Step 4a)', () => {
|
|
743
|
+
const content = readFileSync(
|
|
744
|
+
new URL('approve-plan.md', import.meta.url),
|
|
745
|
+
'utf8',
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
it('defines Step 4a — Write local marker', () => {
|
|
749
|
+
expect(content).toContain('## Step 4a — Write local marker');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('Step 4a runs only when the resolved arg was a plan-file stem', () => {
|
|
753
|
+
expect(content).toContain(
|
|
754
|
+
'Run this step instead of Steps 5, 5b, 6 when the resolved argument was a plan-file stem',
|
|
755
|
+
);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('writes marker to .clancy/plans/{stem}.approved', () => {
|
|
759
|
+
expect(content).toContain('.clancy/plans/{stem}.approved');
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('uses race-safe exclusive create (O_EXCL / wx)', () => {
|
|
763
|
+
expect(content).toContain('exclusive create');
|
|
764
|
+
expect(content).toContain('O_EXCL');
|
|
765
|
+
expect(content).toContain('wx');
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('marker body contains sha256 and approved_at fields', () => {
|
|
769
|
+
expect(content).toContain('sha256=');
|
|
770
|
+
expect(content).toContain('approved_at=');
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('sha256 is computed over the plan file content at approval time', () => {
|
|
774
|
+
expect(content).toContain('SHA-256');
|
|
775
|
+
expect(content).toContain('Order of operations');
|
|
776
|
+
expect(content).toContain(
|
|
777
|
+
'Read the plan file at `.clancy/plans/{stem}.md` from disk',
|
|
778
|
+
);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('SHA hash is never computed over the .approved marker itself', () => {
|
|
782
|
+
expect(content).toContain('`.approved` file is **never** included');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('approved_at is ISO 8601 UTC', () => {
|
|
786
|
+
expect(content).toContain('ISO 8601');
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('handles EEXIST as already-approved', () => {
|
|
790
|
+
expect(content).toContain('EEXIST');
|
|
791
|
+
expect(content).toContain('already approved');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('explains the marker is the gate for /clancy:implement-from', () => {
|
|
795
|
+
expect(content).toContain('/clancy:implement-from');
|
|
796
|
+
expect(content).toContain('gate');
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('after writing the marker, Step 4a jumps to Step 7 (log) and skips board flow', () => {
|
|
800
|
+
expect(content).toContain('jump to Step 7');
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
describe('approve-plan brief-marker update (Step 4b)', () => {
|
|
805
|
+
const content = readFileSync(
|
|
806
|
+
new URL('approve-plan.md', import.meta.url),
|
|
807
|
+
'utf8',
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
it('defines Step 4b — Update brief marker', () => {
|
|
811
|
+
expect(content).toContain('## Step 4b — Update brief marker');
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('resolves brief filename from the plan **Brief:** header', () => {
|
|
815
|
+
expect(content).toContain('**Brief:**');
|
|
816
|
+
expect(content).toContain('extract the `**Brief:**` header line');
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('resolves row number from the plan **Row:** header', () => {
|
|
820
|
+
expect(content).toContain('**Row:**');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('uses a tolerant line-anchored regex with optional approved prefix', () => {
|
|
824
|
+
expect(content).toContain(
|
|
825
|
+
'^<!--\\s*(?:approved:([\\d,]*)\\s+)?planned:([\\d,]+)\\s*-->\\s*$',
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('canonical ordering puts approved before planned', () => {
|
|
830
|
+
expect(content).toContain('approved:` first, `planned:` second');
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('handles existing planned-only marker (PR 6b state)', () => {
|
|
834
|
+
expect(content).toContain('<!-- planned:1,2,3 -->');
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('handles existing approved+planned marker', () => {
|
|
838
|
+
expect(content).toContain('<!-- approved:1 planned:1,2,3 -->');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('handles unspaced marker variant', () => {
|
|
842
|
+
expect(content).toContain('<!--planned:1,2,3-->');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('best-effort: failure does not roll back the .approved marker', () => {
|
|
846
|
+
expect(content).toContain('does NOT roll back');
|
|
847
|
+
expect(content).toContain('local marker is the source of truth');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('warns and skips when **Brief:** header is absent', () => {
|
|
851
|
+
expect(content).toContain('cannot update brief marker');
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('documents concurrency-not-safe nature of the read-modify-write', () => {
|
|
855
|
+
expect(content).toContain('not concurrency-safe');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('documents reversed-order brief markers fall through to warn-and-skip', () => {
|
|
859
|
+
expect(content).toContain('Reversed-order markers');
|
|
860
|
+
expect(content).toContain('do NOT match this regex');
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('documents code-fence false-positive risk', () => {
|
|
864
|
+
expect(content).toContain('Code-fence false positives');
|
|
865
|
+
expect(content).toContain('first match wins');
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
describe('approve-plan local-mode log + summary (Step 7)', () => {
|
|
870
|
+
const content = readFileSync(
|
|
871
|
+
new URL('approve-plan.md', import.meta.url),
|
|
872
|
+
'utf8',
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
it('uses LOCAL_APPROVE_PLAN log token for plan-file stem mode', () => {
|
|
876
|
+
expect(content).toContain('LOCAL_APPROVE_PLAN');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('local log entry includes the sha256 prefix for audit', () => {
|
|
880
|
+
expect(content).toContain('sha256={first 12 hex}');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('local success summary points to /clancy:implement-from', () => {
|
|
884
|
+
expect(content).toContain(
|
|
885
|
+
'Next: /clancy:implement-from .clancy/plans/{stem}.md',
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('preserves board-mode APPROVE_PLAN log entry', () => {
|
|
890
|
+
expect(content).toContain('| {KEY} | APPROVE_PLAN | —');
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it('Step 7 has an explicit mode gate so local and board branches do not double-render', () => {
|
|
894
|
+
expect(content).toContain('Mode gate (read first)');
|
|
895
|
+
expect(content).toContain(
|
|
896
|
+
'Do NOT render both — exactly one branch executes per approval',
|
|
897
|
+
);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('plan-file-not-found error hints at the row-number convention', () => {
|
|
901
|
+
expect(content).toContain('Plan stems include the row number');
|
|
902
|
+
expect(content).toContain('Run /clancy:plan --list');
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('EEXIST advice points at manual deletion, not a non-existent --fresh flag', () => {
|
|
906
|
+
expect(content).toContain('Delete .clancy/plans/{stem}.approved manually');
|
|
907
|
+
expect(content).not.toContain('/clancy:approve-plan --fresh');
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
describe('approve-plan board mode preserved unchanged', () => {
|
|
912
|
+
const content = readFileSync(
|
|
913
|
+
new URL('approve-plan.md', import.meta.url),
|
|
914
|
+
'utf8',
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
it('keeps Jira ADF construction', () => {
|
|
918
|
+
expect(content).toContain('ADF');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('keeps GitHub PATCH /issues for description update', () => {
|
|
922
|
+
expect(content).toContain('PATCH');
|
|
923
|
+
expect(content).toContain('/issues/');
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('keeps Linear issueUpdate mutation', () => {
|
|
927
|
+
expect(content).toContain('issueUpdate');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('keeps Azure DevOps work item update', () => {
|
|
931
|
+
expect(content).toContain('Azure DevOps');
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('keeps Shortcut PUT /stories', () => {
|
|
935
|
+
expect(content).toContain('Shortcut');
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('keeps Notion description update', () => {
|
|
939
|
+
expect(content).toContain('Notion');
|
|
940
|
+
});
|
|
941
|
+
});
|