@jaybeeuu/agent-cortex 2.10.0 → 2.11.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/CHANGELOG.md +36 -0
- package/agents/guppi/agent.md +2 -1
- package/package.json +1 -1
- package/skills/workflow/prioritise-work/REFERENCE.md +34 -0
- package/skills/workflow/prioritise-work/SKILL.md +22 -22
- package/skills/workflow/prioritise-work/scripts/strategy.mjs +194 -11
- package/skills/workflow/ralph/REFERENCE.md +5 -3
- package/skills/workflow/ralph/scripts/prune-worktrees.mjs +89 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0a98d7b: Open a `prioritise-work` run with a summary of the committed strategy instead of a question. A new
|
|
8
|
+
`strategy.mjs summary` subcommand validates the file first, then prints a handful of lines — the
|
|
9
|
+
path it read, the version, the newest change-log entry, the themes, the two admissibility lists as
|
|
10
|
+
up to two bullets each, and the deadline factors when the file states them — so the digest cannot
|
|
11
|
+
describe a strategy that is not on disk. The skill shows it and asks "what should change?" before
|
|
12
|
+
the interview; with no strategy file it exits `3` and the skill goes straight to the first-run
|
|
13
|
+
interview with no digest; with an invalid or partly missing one it exits `1` and names what is wrong
|
|
14
|
+
rather than presenting it as current. The change requires no new section of the strategy file, so
|
|
15
|
+
every strategy already committed still validates: `## What counts as value` and `## Explicitly out
|
|
16
|
+
of scope` are summarised as the admissibility lists they are, never as a ranking order, and
|
|
17
|
+
`## Deadline tiers` is summarised narrowest first, the order the queue applies the factors, whatever
|
|
18
|
+
order the file holds the rows in.
|
|
19
|
+
|
|
20
|
+
The script contract moved to the skill's `REFERENCE.md` so `SKILL.md` stays within its length target.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 5a6a14a: Let the content proof decide a closed bead's worktree, whatever its upstream is.
|
|
25
|
+
`prune-worktrees.mjs` still refused every worktree ralph created, because ralph
|
|
26
|
+
cuts its branch from the remote base (`git worktree add -b feature/<id> …
|
|
27
|
+
origin/main`), `branch.autoSetupMerge` makes that base the branch's upstream, and
|
|
28
|
+
the squash merge that lands the work rewrites its commits — so the branch stays
|
|
29
|
+
ahead of its own base for ever and the unpushed-commit check, which ran first,
|
|
30
|
+
always fired. A closed bead is now decided by content alone: the base must hold
|
|
31
|
+
the branch's own version of every path the branch changed since their merge base,
|
|
32
|
+
and the refusal names that content when it does not. An open bead keeps the
|
|
33
|
+
upstream refusal, and with no upstream it is refused whatever the content says.
|
|
34
|
+
The base guard now tests the ref instead of the tip: a base that names the
|
|
35
|
+
worktree's own branch (local or remote-tracking) is a self-comparison and
|
|
36
|
+
refuses, while a branch cut from the base and never committed to holds the base's
|
|
37
|
+
own commit and is removed — the case every fresh ralph worktree used to reach.
|
|
38
|
+
|
|
3
39
|
## 2.10.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/agents/guppi/agent.md
CHANGED
|
@@ -92,7 +92,8 @@ A wish for a different order is a `prioritise-work` change, never a boundary one
|
|
|
92
92
|
- **Never merges.** A pull request is a human gate. guppi may read one and hand it to a reviewer.
|
|
93
93
|
- **Never pushes.** No `git push`, no release, no version bump of this package.
|
|
94
94
|
- **Never overrides a priority silently.** "This should be higher" means the strategy is wrong,
|
|
95
|
-
stale, or has shifted.
|
|
95
|
+
stale, or has shifted. Run `prioritise-work`, which opens with a digest of the current strategy,
|
|
96
|
+
record the change, then re-rank.
|
|
96
97
|
- **Never invents a strategy.** If the strategy file is missing or invalid, say so plainly and offer
|
|
97
98
|
the interview. Do not guess priorities.
|
|
98
99
|
- **Propose-only and resident.** guppi is a standing collaborator in one session. It is not a task
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# The strategy scripts
|
|
2
|
+
|
|
3
|
+
The contract behind [SKILL.md](SKILL.md): what each subcommand prints, and the exit codes the
|
|
4
|
+
skill routes on. The workflow lives in `SKILL.md`; this file is the reference for one run.
|
|
5
|
+
|
|
6
|
+
Two scripts, five subcommands, zero dependencies, no tracker access. `strategy.mjs` reads and
|
|
7
|
+
writes the strategy file; `challenge.mjs` reads it and prints a trail entry. No run creates, tags
|
|
8
|
+
or closes a bead.
|
|
9
|
+
|
|
10
|
+
| Command | Behaviour | Exit |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `init` | Writes the template, stamped with today's date; refuses an existing file | 0 written, 1 refused |
|
|
13
|
+
| `validate` | Checks the version integer, the date, the change log, the deadline tiers and every required section; prints the file to stdout when valid | 0 valid, 1 invalid |
|
|
14
|
+
| `bump` | Refuses an invalid file or an empty `--reason`, raises the version by one, records today, appends the change-log line | 0 bumped, 1 invalid file, 2 no or empty `--reason` |
|
|
15
|
+
| `summary` | Validates first, then prints the digest a run opens with: the path it read, the version, the newest change-log entry, the themes and the two admissibility lists as up to two bullets each, and the deadline factors when the file states a table | 0 summarised, 1 invalid, 3 no file |
|
|
16
|
+
| `record` (`challenge.mjs`) | Refuses an order or a resolution the strategy does not support; prints one bead-trail entry | 0 recorded, 1 refused |
|
|
17
|
+
|
|
18
|
+
`summary` is the one command that separates "absent" from "wrong". It exits `3` only when the file
|
|
19
|
+
is not there, because the skill's next move is the first-run interview and no digest; a file that is
|
|
20
|
+
present but fails the shape check exits `1` with the validator's own lines, never a digest. Both
|
|
21
|
+
paths print nothing to stdout, so a caller can show whatever arrives there without deciding what to
|
|
22
|
+
hide.
|
|
23
|
+
|
|
24
|
+
`validate` splits its output: status and errors go to stderr, the rubric goes to stdout.
|
|
25
|
+
`validate > rubric.md` captures the rubric alone, which is how a scorer loads it. A failing run names
|
|
26
|
+
the file and lists one line per defect, so one fix round clears the whole file.
|
|
27
|
+
|
|
28
|
+
The date is checked as a **calendar** date, not as a shape. `2026-02-31` and `2026-04-31` fail in
|
|
29
|
+
both `validate` and `--today`, because V8's ISO parser rolls them into the next month and accepts
|
|
30
|
+
them; `2024-02-29` passes and `2026-02-29` does not. A rolled date records a change on a day it did
|
|
31
|
+
not happen.
|
|
32
|
+
|
|
33
|
+
Exit code 2 means a malformed call — an unknown subcommand, a bad `--today`, or a missing or empty
|
|
34
|
+
`--reason`. `--today YYYY-MM-DD` exists so a test pins the recorded date; a person accepts the default.
|
|
@@ -47,17 +47,29 @@ Use that rather than `git add -f`: an ignored file disappears from `git status`,
|
|
|
47
47
|
|
|
48
48
|
**One strategy, one workspace.** A rubric answers the question "what is worth doing here", so each bd workspace carries its own. The HITL queue reads several scopes at once; the **personal** scope's strategy is therefore also where the daily line lives, and the queue quotes it above the ranked list to say which scope the day belongs to. Write it under `## Current themes`, name the scopes first, then an em dash and the reason. `validate` does not require it: a strategy with no daily line is valid, and the queue says the note is missing rather than inventing one.
|
|
49
49
|
|
|
50
|
-
##
|
|
51
|
-
|
|
52
|
-
### First run — no strategy file
|
|
50
|
+
## Step one — summarise what is already there
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
A run never opens with a question. Run the summary first: it reads the committed file, so it cannot describe a strategy that is not on disk, and it shows the owner what they are about to edit.
|
|
55
53
|
|
|
56
54
|
```bash
|
|
57
|
-
node <skill-scripts>/strategy.mjs
|
|
55
|
+
node <skill-scripts>/strategy.mjs summary
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
The digest is all it prints: the path it read, the version, the newest change-log entry, the themes, the two admissibility lists as up to two bullets each, and — when the file states a table — the deadline factors in the order the queue applies them. It is not a re-print — open the file for what a cut line left out.
|
|
59
|
+
|
|
60
|
+
| Exit | The file | What to do |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| 0 | valid | Show the digest, then ask "what should change?" |
|
|
63
|
+
| 3 | absent | Say there is no strategy yet and take the first-run interview below. Print no digest. |
|
|
64
|
+
| 1 | invalid or partly missing | Never present it as current. Say what it named — the empty or missing section, the field it could not read — and fix the file or re-interview before anything is scored against it. |
|
|
65
|
+
|
|
66
|
+
**The file gains no new section.** An admissibility heading would fail every strategy already committed, because `validate` gates them all. The file says what is worth doing and what is not: `## Current themes` is what the month is for, `## What counts as value` is the test a bead must pass, and `## Explicitly out of scope` is the work the month rules out — the last two are the digest's **admissibility** lines. What orders the beads that pass them is the **scoring rubric**, RICE with its multipliers, never this file. The one ordered rule set the file states is `## Deadline tiers`, and the digest prints its rows narrowest first whatever order the file holds them in; with no table the documented default applies and the digest says nothing about deadlines.
|
|
67
|
+
|
|
68
|
+
## Workflow
|
|
69
|
+
|
|
70
|
+
### First run — no strategy file
|
|
71
|
+
|
|
72
|
+
1. The summary exited `3`: the file is absent. Do not search the repository for an alternative — one strategy, one location, or two scorers rank against two rubrics.
|
|
61
73
|
|
|
62
74
|
2. Write the shipped template to that path. This is the copy step — the script resolves its own template, so it works from either install layout:
|
|
63
75
|
|
|
@@ -79,7 +91,7 @@ node <skill-scripts>/strategy.mjs validate
|
|
|
79
91
|
|
|
80
92
|
### Later runs — the file exists
|
|
81
93
|
|
|
82
|
-
6.
|
|
94
|
+
6. The summary is step one above, so the version and the last change are already in front of the owner. A last change from an earlier month is stale: re-run the interview before scoring anything against it.
|
|
83
95
|
|
|
84
96
|
7. Ask the four questions again, plus the confidence question, and edit the sections that changed. Edit the deadline-tier table too when a due date should move a rank further or sooner.
|
|
85
97
|
|
|
@@ -109,20 +121,7 @@ The confidence question matters as much as the other four: effort and impact est
|
|
|
109
121
|
|
|
110
122
|
## The scripts
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
| Command | Behaviour | Exit |
|
|
115
|
-
|---|---|---|
|
|
116
|
-
| `init` | Writes the template, stamped with today's date; refuses an existing file | 0 written, 1 refused |
|
|
117
|
-
| `validate` | Checks the version integer, the date, the change log, the deadline tiers and every required section; prints the file to stdout when valid | 0 valid, 1 invalid |
|
|
118
|
-
| `bump` | Refuses an invalid file or an empty `--reason`, raises the version by one, records today, appends the change-log line | 0 bumped, 1 refused |
|
|
119
|
-
| `record` (`challenge.mjs`) | Refuses an order or a resolution the strategy does not support; prints one bead-trail entry | 0 recorded, 1 refused |
|
|
120
|
-
|
|
121
|
-
`validate` splits its output: status and errors go to stderr, the rubric goes to stdout. `validate > rubric.md` captures the rubric alone, which is how a scorer loads it. A failing run names the file and lists one line per defect, so one fix round clears the whole file.
|
|
122
|
-
|
|
123
|
-
The date is checked as a **calendar** date, not as a shape. `2026-02-31` and `2026-04-31` fail in both `validate` and `--today`, because V8's ISO parser rolls them into the next month and accepts them; `2024-02-29` passes and `2026-02-29` does not. A rolled date records a change on a day it did not happen.
|
|
124
|
-
|
|
125
|
-
Exit code 2 means a malformed call — an unknown subcommand or a bad `--today`. `--today YYYY-MM-DD` exists so a test pins the recorded date; a person accepts the default.
|
|
124
|
+
Four subcommands of `strategy.mjs` and one of `challenge.mjs`, all zero dependencies with no tracker access: [REFERENCE.md](REFERENCE.md) lists what each one prints and the exit codes the steps above route on.
|
|
126
125
|
|
|
127
126
|
## Hand edits and forgotten bumps
|
|
128
127
|
|
|
@@ -137,7 +136,7 @@ A hand edit that changes a section but forgets `bump` still passes `validate`: t
|
|
|
137
136
|
|
|
138
137
|
## Verification checklist
|
|
139
138
|
|
|
140
|
-
- [ ] `node <skill-scripts>/strategy.mjs validate` exits 0
|
|
139
|
+
- [ ] The run opened with the digest — file, version, last change, themes, admissibility, deadline factors — before it asked anything, and `node <skill-scripts>/strategy.mjs validate` exits 0 with the rubric printed.
|
|
141
140
|
- [ ] The version is one higher than the previous file for an update, or `1` for a new one, and the change log names it.
|
|
142
141
|
- [ ] A challenged order moved only after a bump, and the record names what moved.
|
|
143
142
|
- [ ] No bead was created, tagged or closed by this skill; `git ls-files --error-unmatch .agent-cortex/working-docs/strategy.md` succeeds.
|
|
@@ -145,6 +144,7 @@ A hand edit that changes a section but forgets `bump` still passes `validate`: t
|
|
|
145
144
|
## See Also
|
|
146
145
|
|
|
147
146
|
- [CHALLENGE.md](CHALLENGE.md) — the sequence, the refusals and the no-change path when the order is challenged.
|
|
147
|
+
- [REFERENCE.md](REFERENCE.md) — every subcommand, what it prints, and the exit codes that tell a first run from an invalid strategy.
|
|
148
148
|
- [plan](../../workflow/plan/SKILL.md) — shapes one feature into beads; this skill orders the beads that exist.
|
|
149
149
|
- [bd-tool](../../productivity/bd-tool/SKILL.md) — where operational state lives, and why a durable document does not belong there.
|
|
150
150
|
- [score-bead](../../workflow/score-bead/SKILL.md) — turns this rubric into a RICE record on a bead; the queue's daily line is read by [hitl-queue](../hitl-queue/SCOPES.md).
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// and never a scratch file: operational state belongs in the bead trail, and durable
|
|
7
7
|
// documents that are reviewed, diffed and versioned belong in git.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// Four subcommands, and nothing else:
|
|
10
10
|
//
|
|
11
11
|
// node strategy.mjs init [--file <path>] write the template
|
|
12
12
|
// node strategy.mjs validate [--file <path>] check the shape, then print it
|
|
13
13
|
// node strategy.mjs bump [--file <path>] [--today d] raise the version, set the date
|
|
14
|
+
// node strategy.mjs summary [--file <path>] the digest a run opens with
|
|
14
15
|
//
|
|
15
16
|
// `validate` prints the rubric to stdout only after it passes, so a scorer can never
|
|
16
17
|
// read a stale or malformed strategy: the file and the proof of its shape arrive
|
|
@@ -22,8 +23,14 @@
|
|
|
22
23
|
// Zero runtime dependencies, async fs only. `skills/*/scripts` sits outside the
|
|
23
24
|
// check-no-sync scan, and this script stays async anyway so the rule holds everywhere.
|
|
24
25
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
26
|
+
// `summary` is what the skill prints before it asks anything, and it exists so the digest
|
|
27
|
+
// is read off the file rather than re-derived by an agent: it validates first, then reports
|
|
28
|
+
// the version, the newest change-log entry, the themes, the two admissibility lists and the
|
|
29
|
+
// deadline factors — each one read from the file, none restated from elsewhere.
|
|
30
|
+
//
|
|
31
|
+
// Exit codes: 0 the file is valid or summarised, 1 the file is invalid or the operation
|
|
32
|
+
// refused, 2 the caller asked for something malformed, 3 `summary` found no strategy file —
|
|
33
|
+
// a first run, which the skill answers with the interview and no digest.
|
|
27
34
|
|
|
28
35
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
29
36
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -143,6 +150,7 @@ const MS_PER_MINUTE = 60_000;
|
|
|
143
150
|
* @property {boolean} ok
|
|
144
151
|
* @property {DeadlineTier[]} tiers The parsed tiers, or the documented default when absent.
|
|
145
152
|
* @property {string[]} errors One line per defect in the table.
|
|
153
|
+
* @property {'file' | 'default'} source Whether the tiers were read from the file.
|
|
146
154
|
*/
|
|
147
155
|
|
|
148
156
|
/** A usage mistake: the caller asked for something malformed. Exit code 2. */
|
|
@@ -268,7 +276,7 @@ function parseTierRow(when, factorText) {
|
|
|
268
276
|
*/
|
|
269
277
|
export function parseDeadlineTiers(text) {
|
|
270
278
|
const body = sectionBody(text.split('\n'), DEADLINE_TIERS_SECTION);
|
|
271
|
-
if (body === null) return { ok: true, tiers: [...DEFAULT_DEADLINE_TIERS], errors: [] };
|
|
279
|
+
if (body === null) return { ok: true, tiers: [...DEFAULT_DEADLINE_TIERS], errors: [], source: 'default' };
|
|
272
280
|
|
|
273
281
|
/** @type {DeadlineTier[]} */
|
|
274
282
|
const rows = [];
|
|
@@ -294,8 +302,10 @@ export function parseDeadlineTiers(text) {
|
|
|
294
302
|
rows.push(parsed.tier);
|
|
295
303
|
}
|
|
296
304
|
|
|
305
|
+
// A heading with no readable row states no tiers, so the default applies and the file is
|
|
306
|
+
// not credited with a table it does not carry.
|
|
297
307
|
if (rows.length === 0 && errors.length === 0) {
|
|
298
|
-
return { ok: true, tiers: [...DEFAULT_DEADLINE_TIERS], errors: [] };
|
|
308
|
+
return { ok: true, tiers: [...DEFAULT_DEADLINE_TIERS], errors: [], source: 'default' };
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
if (errors.length === 0) {
|
|
@@ -305,14 +315,14 @@ export function parseDeadlineTiers(text) {
|
|
|
305
315
|
if (count('within') === 0) errors.push(`${DEADLINE_TIERS_SECTION}: needs at least one "within <time>" row`);
|
|
306
316
|
}
|
|
307
317
|
|
|
308
|
-
if (errors.length > 0) return { ok: false, tiers: [...DEFAULT_DEADLINE_TIERS], errors };
|
|
318
|
+
if (errors.length > 0) return { ok: false, tiers: [...DEFAULT_DEADLINE_TIERS], errors, source: 'file' };
|
|
309
319
|
|
|
310
320
|
const within = rows
|
|
311
321
|
.filter((tier) => tier.kind === 'within')
|
|
312
322
|
.sort((a, b) => (a.kind === 'within' && b.kind === 'within' ? a.minutes - b.minutes : 0));
|
|
313
323
|
const beyond = rows.filter((tier) => tier.kind === 'beyond');
|
|
314
324
|
const overdue = rows.filter((tier) => tier.kind === 'overdue');
|
|
315
|
-
return { ok: true, tiers: [...within, ...beyond, ...overdue], errors: [] };
|
|
325
|
+
return { ok: true, tiers: [...within, ...beyond, ...overdue], errors: [], source: 'file' };
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
/**
|
|
@@ -524,16 +534,161 @@ export function bumpStrategy(text, { today, reason }) {
|
|
|
524
534
|
return { from, to, updated: today, reason: cleanReason, line, text: appendChangeLog(bumped, line) };
|
|
525
535
|
}
|
|
526
536
|
|
|
537
|
+
// ─── Summary ──────────────────────────────────────────────────────────────────
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* How many bullets the digest quotes from each admissibility list. Two keeps the digest a
|
|
541
|
+
* handful of lines; the count of what it left out points the reader back at the file.
|
|
542
|
+
*/
|
|
543
|
+
const SUMMARY_ITEMS = 2;
|
|
544
|
+
|
|
545
|
+
/** The longest one bullet may run in the digest, so one answer cannot re-print a section. */
|
|
546
|
+
const SUMMARY_ITEM_LENGTH = 100;
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The items under one heading: each bullet with the continuation lines that belong to it,
|
|
550
|
+
* and the template's HTML-comment guidance skipped. A bullet that wraps over several lines
|
|
551
|
+
* is one item, so a quoting rule is read whole rather than sliced at its first line.
|
|
552
|
+
* @param {string[]} lines @param {string} title @returns {string[]}
|
|
553
|
+
*/
|
|
554
|
+
function sectionItems(lines, title) {
|
|
555
|
+
const body = sectionBody(lines, title);
|
|
556
|
+
if (body === null) return [];
|
|
557
|
+
|
|
558
|
+
/** @type {string[]} */
|
|
559
|
+
const items = [];
|
|
560
|
+
let inComment = false;
|
|
561
|
+
for (const line of body) {
|
|
562
|
+
const trimmed = line.trim();
|
|
563
|
+
if (inComment) {
|
|
564
|
+
if (trimmed.includes('-->')) inComment = false;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (trimmed.startsWith('<!--')) {
|
|
568
|
+
if (!trimmed.includes('-->')) inComment = true;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const bullet = /^[-*][ \t]+(.+)$/.exec(trimmed);
|
|
572
|
+
if (bullet !== null) {
|
|
573
|
+
items.push(bullet[1].trim());
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (trimmed === '' || items.length === 0) continue;
|
|
577
|
+
items[items.length - 1] = `${items[items.length - 1]} ${trimmed}`;
|
|
578
|
+
}
|
|
579
|
+
return items;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* One item as the digest shows it, cut when it is long. A cut item says so, because a
|
|
584
|
+
* silently shortened rule reads as the whole rule.
|
|
585
|
+
* @param {string} item @returns {string}
|
|
586
|
+
*/
|
|
587
|
+
function shorten(item) {
|
|
588
|
+
return item.length <= SUMMARY_ITEM_LENGTH ? item : `${item.slice(0, SUMMARY_ITEM_LENGTH).trimEnd()}…`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* One section as a single digest line: at most `SUMMARY_ITEMS` bullets, then how many the
|
|
593
|
+
* digest left out, so the reader knows the file still says more.
|
|
594
|
+
* @param {string[]} items @returns {string}
|
|
595
|
+
*/
|
|
596
|
+
function digestLine(items) {
|
|
597
|
+
const shown = items.slice(0, SUMMARY_ITEMS).map(shorten);
|
|
598
|
+
if (shown.length === 0) return '(none written)';
|
|
599
|
+
const hidden = items.length - shown.length;
|
|
600
|
+
return hidden === 0 ? shown.join(' · ') : `${shown.join(' · ')} · (+${hidden} more in the file)`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* A tier's distance in the largest unit that divides it, so the digest repeats the spelling
|
|
605
|
+
* the table uses: 1440 minutes reads `1d`, and 90 reads `90m`.
|
|
606
|
+
* @param {number} minutes @returns {string}
|
|
607
|
+
*/
|
|
608
|
+
function spanText(minutes) {
|
|
609
|
+
if (minutes % TIER_UNIT_MINUTES.d === 0) return `${minutes / TIER_UNIT_MINUTES.d}d`;
|
|
610
|
+
if (minutes % TIER_UNIT_MINUTES.h === 0) return `${minutes / TIER_UNIT_MINUTES.h}h`;
|
|
611
|
+
return `${minutes}m`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* One tier as the digest shows it: the phrase the table's row uses, then the factor.
|
|
616
|
+
* @param {DeadlineTier} tier @returns {string}
|
|
617
|
+
*/
|
|
618
|
+
function tierText(tier) {
|
|
619
|
+
if (tier.kind === 'overdue') return `overdue ×${tier.factor}`;
|
|
620
|
+
return `${tier.kind === 'within' ? 'within' : 'more than'} ${spanText(tier.minutes)} ×${tier.factor}`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* The deadline factors as one digest line, narrowest row first. The order is part of the
|
|
625
|
+
* rule — the queue multiplies by the narrowest row the remaining time fits inside — so the
|
|
626
|
+
* whole table prints: a truncated order would summarise a different rule.
|
|
627
|
+
* @param {DeadlineTier[]} tiers @returns {string}
|
|
628
|
+
*/
|
|
629
|
+
function deadlineLine(tiers) {
|
|
630
|
+
return tiers.map(tierText).join(' · ');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* When the rubric last changed and why: the newest change-log entry, because the log is
|
|
635
|
+
* written oldest first and `validate` already proves it names the version the file carries.
|
|
636
|
+
* A file with no entry says so rather than borrowing the header date as a reason.
|
|
637
|
+
* @param {string[]} lines @param {string} updated @returns {string}
|
|
638
|
+
*/
|
|
639
|
+
function lastChange(lines, updated) {
|
|
640
|
+
const entries = (sectionBody(lines, CHANGE_LOG_SECTION) ?? [])
|
|
641
|
+
.map((line) => CHANGE_LOG_ENTRY.exec(line))
|
|
642
|
+
.filter((match) => match !== null);
|
|
643
|
+
const newest = entries[entries.length - 1];
|
|
644
|
+
if (newest === null || newest === undefined) return `${updated} — no change-log entry, so no reason is on file`;
|
|
645
|
+
return `${newest[1]} v${newest[2]}: ${newest[3]}`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* The digest the skill opens with: the file it read, the version, the newest change-log
|
|
650
|
+
* entry, the themes, the two admissibility lists, and the deadline factors when the file
|
|
651
|
+
* states them — a handful of lines, never a re-print.
|
|
652
|
+
*
|
|
653
|
+
* Nothing here is a ranking order, and the labels must not suggest one. `## What counts as
|
|
654
|
+
* value` and `## Explicitly out of scope` are admissibility: the test a bead passes to earn
|
|
655
|
+
* work, and the work the month rules out. What orders the beads that pass them is the
|
|
656
|
+
* scoring rubric — RICE with its multipliers — not this file. The one ordered rule set the
|
|
657
|
+
* file does state is `## Deadline tiers`, printed in its own order and omitted entirely when
|
|
658
|
+
* the section is absent, because the documented default then applies and the file is silent.
|
|
659
|
+
*
|
|
660
|
+
* None of these is a section of its own and none must become one. `validate` gates every
|
|
661
|
+
* strategy already committed, so a new required section would fail all of them.
|
|
662
|
+
* @param {string} text @param {string} file @param {StrategyReport} report A passing report.
|
|
663
|
+
* @returns {string}
|
|
664
|
+
*/
|
|
665
|
+
export function summariseStrategy(text, file, report) {
|
|
666
|
+
const lines = text.split('\n');
|
|
667
|
+
const digest = [
|
|
668
|
+
`${file} — strategy v${report.version}`,
|
|
669
|
+
` last change: ${lastChange(lines, report.updated ?? 'no date recorded')}`,
|
|
670
|
+
` themes: ${digestLine(sectionItems(lines, 'Current themes'))}`,
|
|
671
|
+
` admissibility — what counts as value: ${digestLine(sectionItems(lines, 'What counts as value'))}`,
|
|
672
|
+
` admissibility — out of scope: ${digestLine(sectionItems(lines, 'Explicitly out of scope'))}`,
|
|
673
|
+
];
|
|
674
|
+
const tiers = parseDeadlineTiers(text);
|
|
675
|
+
if (tiers.source === 'file') digest.push(` deadline factors: ${deadlineLine(tiers.tiers)}`);
|
|
676
|
+
return `${digest.join('\n')}\n`;
|
|
677
|
+
}
|
|
678
|
+
|
|
527
679
|
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
528
680
|
|
|
529
681
|
/** @type {readonly string[]} */
|
|
530
|
-
export const COMMANDS = /** @type {const} */ (['init', 'validate', 'bump']);
|
|
682
|
+
export const COMMANDS = /** @type {const} */ (['init', 'validate', 'bump', 'summary']);
|
|
531
683
|
|
|
532
684
|
const USAGE = `Usage: node strategy.mjs <${COMMANDS.join('|')}> [options]
|
|
533
685
|
|
|
534
686
|
init Write the shipped template to the strategy path. Refuses to overwrite.
|
|
535
687
|
validate Check the shape and print the file to stdout only when it passes.
|
|
536
688
|
bump Raise the version by one, record today's date and append the reason.
|
|
689
|
+
summary Print the digest to open a run with: path, version, last change, admissibility,
|
|
690
|
+
deadline factors. Exits 3 when no strategy file exists, so a first run is told
|
|
691
|
+
apart from a strategy that is present but invalid.
|
|
537
692
|
|
|
538
693
|
Options:
|
|
539
694
|
--file <path> Strategy file (default: ${DEFAULT_FILE}, relative to the repository root)
|
|
@@ -637,6 +792,15 @@ export async function resolveStrategyPath({ file = DEFAULT_FILE, cwd = process.c
|
|
|
637
792
|
return resolve(base, file);
|
|
638
793
|
}
|
|
639
794
|
|
|
795
|
+
/**
|
|
796
|
+
* The one line every subcommand prints when the strategy file is not there yet. Reused so
|
|
797
|
+
* `summary` and `validate` cannot drift into two descriptions of the same absence.
|
|
798
|
+
* @param {string} file @returns {string}
|
|
799
|
+
*/
|
|
800
|
+
function missingMessage(file) {
|
|
801
|
+
return `${file} does not exist — run "init" and interview the owner, or pass --file <path>`;
|
|
802
|
+
}
|
|
803
|
+
|
|
640
804
|
/**
|
|
641
805
|
* Build the report text for one failed run.
|
|
642
806
|
* @param {string} file @param {string[]} errors @returns {string}
|
|
@@ -706,14 +870,33 @@ export async function runCli({
|
|
|
706
870
|
return 0;
|
|
707
871
|
}
|
|
708
872
|
|
|
873
|
+
// `summary` separates the two states the skill answers differently: no file is a first
|
|
874
|
+
// run, which takes the interview and no digest; a file that fails the shape check is
|
|
875
|
+
// reported as what it is, never as the current strategy.
|
|
876
|
+
if (options.command === 'summary') {
|
|
877
|
+
/** @type {string} */
|
|
878
|
+
let current;
|
|
879
|
+
try {
|
|
880
|
+
current = await readFile(file, 'utf8');
|
|
881
|
+
} catch {
|
|
882
|
+
writeError(`${missingMessage(file)}\n`);
|
|
883
|
+
return 3;
|
|
884
|
+
}
|
|
885
|
+
const currentReport = validateStrategy(current);
|
|
886
|
+
if (!currentReport.ok) {
|
|
887
|
+
writeError(formatErrors(file, currentReport.errors));
|
|
888
|
+
return 1;
|
|
889
|
+
}
|
|
890
|
+
write(summariseStrategy(current, file, currentReport));
|
|
891
|
+
return 0;
|
|
892
|
+
}
|
|
893
|
+
|
|
709
894
|
/** @type {string} */
|
|
710
895
|
let text;
|
|
711
896
|
try {
|
|
712
897
|
text = await readFile(file, 'utf8');
|
|
713
898
|
} catch {
|
|
714
|
-
throw new StrategyError(
|
|
715
|
-
`${file} does not exist — run "init" and interview the owner, or pass --file <path>`,
|
|
716
|
-
);
|
|
899
|
+
throw new StrategyError(missingMessage(file));
|
|
717
900
|
}
|
|
718
901
|
|
|
719
902
|
if (options.command === 'bump') {
|
|
@@ -236,11 +236,13 @@ node skills/workflow/ralph/scripts/prune-worktrees.mjs --base <ref> # prove
|
|
|
236
236
|
|
|
237
237
|
**Candidacy comes from bead state, never from git ancestry.** The repo squash-merges, so every landed branch still reports as unmerged. A directory named after a bead that `bd show` reports `closed` is the only proof that admits removal; an open bead, a missing bead, or a bead `bd` could not answer for is kept unless `--remove-orphans` asks otherwise. A bead `bd` could not read is never removed, whatever the flags say.
|
|
238
238
|
|
|
239
|
-
**A candidate that fails a guard is refused, and the reason is printed.** The guards are: dirty working tree, `locked` worktree, detached HEAD, directory not registered with git or unreadable,
|
|
239
|
+
**A candidate that fails a guard is refused, and the reason is printed.** The guards are: dirty working tree, `locked` worktree, detached HEAD, directory not registered with git or unreadable, and one proof guard that follows the bead's state — unpushed commits on an open bead, unproven content on a closed one.
|
|
240
240
|
|
|
241
|
-
**
|
|
241
|
+
**A closed bead is decided by content alone, whatever its upstream is.** Ralph cuts its branch from the remote base (`git worktree add -b feature/<id> … origin/main`), so `branch.autoSetupMerge` makes that base the branch's upstream, and a squash merge rewrites the commits: the branch stays ahead of its own upstream for ever. Counting commits against that upstream refuses every landed ralph worktree. So the guard is content, upstream or not: the base must hold the branch's own version of every path the branch changed since their merge base. No upstream is not a refusal by itself — a merge that deletes the branch also deletes its upstream, so no remote-tracking branch is the normal end of landed work. That compares content, never history, so removal discards every commit the base does not contain, an empty commit or a change the branch added and reverted included, together with its message. The test is one-sided, so a base that moved on after the branch was cut still proves the branch landed.
|
|
242
242
|
|
|
243
|
-
**
|
|
243
|
+
**An open bead is decided by its upstream.** With no upstream it is refused on that alone, whatever its content says; with one, every commit must be pushed to it.
|
|
244
|
+
|
|
245
|
+
**The base is the remote's default branch,** read from `origin/HEAD` so that no branch name is hardcoded. `--base <ref>` names another, such as a release branch. A base that cannot be resolved, or that names the worktree's own branch, keeps the worktree and refuses.
|
|
244
246
|
|
|
245
247
|
**Removal is three commands.** `git worktree remove <path>`, then `git branch -D <branch>`, then `git push origin --delete <branch>`. The remote branch name is the worktree's own checked-out branch, so ralph's `feature/<parent-id>` deletes `origin/feature/<parent-id>`. A remote ref that git already reports as gone is not a failure.
|
|
246
248
|
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
// branch was cut holds paths the branch never had, and demanding tree equality
|
|
15
15
|
// would keep every branch but the last one landed.
|
|
16
16
|
//
|
|
17
|
+
// A closed bead's upstream is never the arbiter. Ralph cuts its branch from the
|
|
18
|
+
// remote base (`git worktree add -b feature/<id> … origin/<base>`), and
|
|
19
|
+
// `branch.autoSetupMerge` then makes that base the branch's upstream. A squash
|
|
20
|
+
// merge rewrites the commits, so the branch stays ahead of its own upstream for
|
|
21
|
+
// ever: counting commits against it refuses every landed ralph worktree, which is
|
|
22
|
+
// the one case the content test exists to decide. A closed bead is therefore
|
|
23
|
+
// decided by content alone, whatever its upstream is. An open bead keeps the
|
|
24
|
+
// upstream refusal, because nothing about an open bead is proven.
|
|
25
|
+
//
|
|
17
26
|
// The test proves content, never history. A branch removed here is discarded
|
|
18
27
|
// together with every commit the base does not contain — a reverted change and a
|
|
19
28
|
// run of empty commits both go, and so do their messages. The base is the remote's
|
|
@@ -200,10 +209,45 @@ function splitNulPaths(stdout) {
|
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* the
|
|
206
|
-
*
|
|
212
|
+
* The opening clause of a content-proof refusal. Two situations reach the content
|
|
213
|
+
* check — a closed bead whose branch has lost its upstream, and a closed bead whose
|
|
214
|
+
* branch still tracks the base it was cut from — and both are refused for the same
|
|
215
|
+
* reason: the base does not hold the branch's content. The clause states which
|
|
216
|
+
* situation applies, so a reader can tell an unpublished branch from one whose
|
|
217
|
+
* upstream cannot prove it.
|
|
218
|
+
* @param {string} branch
|
|
219
|
+
* @param {string | null} upstream the branch's remote-tracking ref, or null when it has none.
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function contentLead(branch, upstream) {
|
|
223
|
+
return upstream === null
|
|
224
|
+
? `branch ${branch} has no remote-tracking branch`
|
|
225
|
+
: `branch ${branch} is not proven by ${upstream}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* True when `ref` names the worktree's own branch, locally or as its
|
|
230
|
+
* remote-tracking ref. Comparing such a ref with the branch is a self-comparison:
|
|
231
|
+
* the diff is empty whatever the branch holds, so it proves nothing.
|
|
232
|
+
* @param {string} path
|
|
233
|
+
* @param {string} ref
|
|
234
|
+
* @param {string} branch
|
|
235
|
+
* @param {(cwd: string, args: string[]) => Promise<{ ok: true, stdout: string } | { ok: false, stderr: string }>} git
|
|
236
|
+
* @returns {Promise<boolean>}
|
|
237
|
+
*/
|
|
238
|
+
async function namesOwnBranch(path, ref, branch, git) {
|
|
239
|
+
const resolved = await git(path, ['rev-parse', '--symbolic-full-name', ref]);
|
|
240
|
+
if (!resolved.ok) return false;
|
|
241
|
+
const name = resolved.stdout.trim();
|
|
242
|
+
if (name === `refs/heads/${branch}`) return true;
|
|
243
|
+
// `refs/remotes/<remote>/<branch>`, whatever the remote is called.
|
|
244
|
+
return name.startsWith('refs/remotes/') && name.endsWith(`/${branch}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Decide whether a closed bead's branch still holds work. Removing such a branch is
|
|
249
|
+
* only safe when the base holds the branch's own version of every path the branch
|
|
250
|
+
* changed — the squash-merge case. Anything unprovable keeps the worktree.
|
|
207
251
|
*
|
|
208
252
|
* One-sided containment, in three steps: take the merge base of the base and the
|
|
209
253
|
* branch, list the paths the branch changed since that merge base, then require the
|
|
@@ -227,46 +271,51 @@ function splitNulPaths(stdout) {
|
|
|
227
271
|
* read as pathspec magic instead of a name. Neither alone is sufficient to fix,
|
|
228
272
|
* so the first diff reads with `-z` and the second is run with
|
|
229
273
|
* `--literal-pathspecs`.
|
|
230
|
-
* @param {{ path: string, branch: string, beadClosed: boolean }} target
|
|
274
|
+
* @param {{ path: string, branch: string, beadClosed: boolean, upstream: string | null }} target
|
|
275
|
+
* `upstream` is the branch's remote-tracking ref, or null when it has none.
|
|
231
276
|
* @param {string | null} baseRef
|
|
232
277
|
* @param {(cwd: string, args: string[]) => Promise<{ ok: true, stdout: string } | { ok: false, stderr: string }>} git
|
|
233
278
|
* @returns {Promise<string | null>} a refusal reason, or null when nothing is lost.
|
|
234
279
|
*/
|
|
235
|
-
async function findLostWork({ path, branch, beadClosed }, baseRef, git) {
|
|
280
|
+
async function findLostWork({ path, branch, beadClosed, upstream }, baseRef, git) {
|
|
236
281
|
if (!beadClosed) {
|
|
237
282
|
return `branch ${branch} has no remote-tracking branch and its bead is not closed`;
|
|
238
283
|
}
|
|
284
|
+
/** @param {string} clause @returns {string} */
|
|
285
|
+
const refuse = (clause) => `${contentLead(branch, upstream)}: ${clause}`;
|
|
239
286
|
if (baseRef === null) {
|
|
240
|
-
return
|
|
287
|
+
return refuse('the base branch cannot be resolved, so its content cannot be proven landed');
|
|
241
288
|
}
|
|
242
289
|
const tip = await git(path, ['rev-parse', '--verify', 'HEAD']);
|
|
243
290
|
if (!tip.ok) {
|
|
244
291
|
const detail = tip.stderr.trim();
|
|
245
292
|
const suffix = detail === '' ? '' : `: ${detail}`;
|
|
246
|
-
return `
|
|
293
|
+
return refuse(`its tip cannot be read${suffix}`);
|
|
247
294
|
}
|
|
248
295
|
const baseTip = await git(path, ['rev-parse', '--verify', '--quiet', `${baseRef}^{commit}`]);
|
|
249
296
|
if (!baseTip.ok) {
|
|
250
297
|
const detail = baseTip.stderr.trim();
|
|
251
298
|
const suffix = detail === '' ? '' : `: ${detail}`;
|
|
252
|
-
return
|
|
299
|
+
return refuse(`${baseRef} cannot be compared with it${suffix}`);
|
|
253
300
|
}
|
|
254
|
-
// A base that
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
301
|
+
// A base that names the branch being deleted compares the branch with itself,
|
|
302
|
+
// whatever the branch holds. The test is on the ref, not on the tip: a branch cut
|
|
303
|
+
// from the base and never committed to holds the base's own commit, and that is a
|
|
304
|
+
// real proof — it has no content of its own to lose.
|
|
305
|
+
if (await namesOwnBranch(path, baseRef, branch, git)) {
|
|
306
|
+
return refuse(`${baseRef} names this branch, so the comparison would prove nothing`);
|
|
258
307
|
}
|
|
259
308
|
const mergeBase = await git(path, ['merge-base', baseRef, 'HEAD']);
|
|
260
309
|
if (!mergeBase.ok) {
|
|
261
310
|
const detail = mergeBase.stderr.trim();
|
|
262
311
|
const suffix = detail === '' ? '' : `: ${detail}`;
|
|
263
|
-
return
|
|
312
|
+
return refuse(`${baseRef} cannot be compared with it${suffix}`);
|
|
264
313
|
}
|
|
265
314
|
const touched = await git(path, ['diff', '--name-only', '-z', '--no-renames', mergeBase.stdout.trim(), 'HEAD', '--']);
|
|
266
315
|
if (!touched.ok) {
|
|
267
316
|
const detail = touched.stderr.trim();
|
|
268
317
|
const suffix = detail === '' ? '' : `: ${detail}`;
|
|
269
|
-
return
|
|
318
|
+
return refuse(`${baseRef} cannot be compared with it${suffix}`);
|
|
270
319
|
}
|
|
271
320
|
const changedPaths = splitNulPaths(touched.stdout);
|
|
272
321
|
// Nothing but empty commits: no content to prove, and no content to lose.
|
|
@@ -277,11 +326,11 @@ async function findLostWork({ path, branch, beadClosed }, baseRef, git) {
|
|
|
277
326
|
if (!unproven.ok) {
|
|
278
327
|
const detail = unproven.stderr.trim();
|
|
279
328
|
const suffix = detail === '' ? '' : `: ${detail}`;
|
|
280
|
-
return
|
|
329
|
+
return refuse(`${baseRef} cannot be compared with it${suffix}`);
|
|
281
330
|
}
|
|
282
331
|
const unprovenPaths = splitNulPaths(unproven.stdout).length;
|
|
283
332
|
if (unprovenPaths > 0) {
|
|
284
|
-
return
|
|
333
|
+
return refuse(`${baseRef} does not hold this branch's version of ${unprovenPaths} path(s), so its content is not landed`);
|
|
285
334
|
}
|
|
286
335
|
return null;
|
|
287
336
|
}
|
|
@@ -315,12 +364,18 @@ async function findRefusals({ path, registered, beadClosed }, git, baseRef) {
|
|
|
315
364
|
if (dirtyPaths > 0) reasons.push(`worktree is dirty: ${dirtyPaths} uncommitted path(s)`);
|
|
316
365
|
|
|
317
366
|
const upstream = await git(path, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
318
|
-
|
|
319
|
-
|
|
367
|
+
const trackingRef = upstream.ok ? upstream.stdout.trim() : null;
|
|
368
|
+
|
|
369
|
+
// A closed bead is decided by content alone, never by its upstream: a ralph
|
|
370
|
+
// branch is cut from the remote base, so it tracks that base and stays ahead of
|
|
371
|
+
// it after a squash merge rewrites its commits. Counting commits against such an
|
|
372
|
+
// upstream refuses every landed ralph worktree. An open bead with no upstream has
|
|
373
|
+
// nothing to prove, so it lands here too and refuses below.
|
|
374
|
+
if (beadClosed || trackingRef === null) {
|
|
375
|
+
const lost = await findLostWork({ path, branch: registered.branch, beadClosed, upstream: trackingRef }, baseRef, git);
|
|
320
376
|
if (lost !== null) reasons.push(lost);
|
|
321
377
|
return { branch: registered.branch, reasons };
|
|
322
378
|
}
|
|
323
|
-
const trackingRef = upstream.stdout.trim();
|
|
324
379
|
const unpushed = await git(path, ['rev-list', '--count', `${trackingRef}..HEAD`]);
|
|
325
380
|
if (!unpushed.ok) return { branch: registered.branch, reasons: [...reasons, `branch ${registered.branch} cannot be compared with ${trackingRef}: ${unpushed.stderr.trim()}`] };
|
|
326
381
|
const unpushedCommits = Number.parseInt(unpushed.stdout.trim(), 10);
|
|
@@ -422,15 +477,20 @@ reports the rest. A worktree whose bead is open or missing is an orphan: it is
|
|
|
422
477
|
only removed when --remove-orphans is passed. Run from anywhere inside the
|
|
423
478
|
repository.
|
|
424
479
|
|
|
425
|
-
A closed bead's branch
|
|
426
|
-
methods delete the remote branch when the work lands
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
commit
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
worktree
|
|
480
|
+
A closed bead's branch is proven by content alone, whatever its upstream is: many
|
|
481
|
+
merge methods delete the remote branch when the work lands, and a branch cut from
|
|
482
|
+
the remote base tracks that base, which a squash merge leaves behind. Such a branch
|
|
483
|
+
is removed only when the base holds the branch's own version of every path the
|
|
484
|
+
branch changed since their merge base. That proves content and never history:
|
|
485
|
+
removal discards every commit the base does not contain, such as a reverted change
|
|
486
|
+
or an empty commit, together with its message. The base defaults to the remote's
|
|
487
|
+
default branch (origin/HEAD); --base names another ref, such as a release branch. A
|
|
488
|
+
base that names the worktree's own branch compares the branch with itself, so it
|
|
489
|
+
proves nothing: the worktree is kept and refused, as it is when no base resolves.
|
|
490
|
+
|
|
491
|
+
An open bead is proven by its upstream instead, and only with --remove-orphans: its
|
|
492
|
+
branch must carry an upstream and no commit unpushed to it. With no upstream it is
|
|
493
|
+
refused whatever its content says.
|
|
434
494
|
`;
|
|
435
495
|
|
|
436
496
|
/** Every flag the CLI accepts. */
|