@bompus/house-rules 0.14.2 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/rules/core.md +8 -4
- package/rules/modifiers/compression-codecs.md +55 -0
- package/rules/modifiers/squash-landing.md +2 -2
- package/rules/modifiers/upstream-contributions.md +4 -2
- package/rules/references/landing.md +25 -16
- package/rules/references/squash-landing.md +5 -4
- package/skills/hr-handoff/references/reconciliation.md +4 -1
- package/skills/hr-handoff/references/successor-thread.md +3 -1
- package/skills/hr-handoff/scripts/check-reconciliation.mjs +24 -4
- package/skills/hr-pr-followup/references/review-helpers.md +27 -7
- package/skills/hr-pr-followup/scripts/check-landing.py +2 -1
- package/skills/hr-pr-followup/scripts/merge-pr.sh +20 -15
- package/skills/hr-pr-followup/scripts/verify-landed.sh +134 -91
- package/skills/hr-what-next/scripts/check-open-work.mjs +28 -13
package/README.md
CHANGED
|
@@ -236,6 +236,7 @@ in name order, after the modifiers.
|
|
|
236
236
|
| `backlog-outline` | A backlog report adds a Markdown outline of every open item grouped by what it waits on; nothing is rendered, so it reads the same on every host. |
|
|
237
237
|
| `backlog-visual` | A backlog report adds one dark panel of every open item grouped by what it waits on, with a table for waiting items and one row per decision; hosts without a render tool get the same outline in Markdown. |
|
|
238
238
|
| `coded-offers` | Offers use numbered questions and coded options (`1A`, `1B`) so one short reply answers every decision. |
|
|
239
|
+
| `compression-codecs` | Prefer zstd, then brotli, then gzip for data you write, with browser negotiation, gzip exceptions and measured levels. |
|
|
239
240
|
| `cross-session-relay` | Relayed work between sessions keeps its source and authorized scope, and an authorized push that changes inherited guidance or tooling is announced to the affected sessions. |
|
|
240
241
|
| `effort-estimates` | Options that differ in cost, or work that waits on CI, a build or a deploy, carry a wall-clock estimate based on comparable finished work. Estimates from workers, docs or other models are converted the same way or dropped. |
|
|
241
242
|
| `land-when-done` | Authorized repository work is not finished until it is in the remote default branch. |
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -190,6 +190,8 @@ Transfer to another top-level conversation only at the user's direction, under a
|
|
|
190
190
|
|
|
191
191
|
Keep independent repository edits on separate branches and isolated worktrees when a checkout may be shared. Leave other people's and sessions' edits alone.
|
|
192
192
|
|
|
193
|
+
Before creating a branch or worktree, read the default branch of the repository the change will land in (`gh repo view <owner>/<repo> --json defaultBranchRef` or `git ls-remote --symref <remote> HEAD`), fetch it and base the work on its tip. That repository is your fork when the change lands there and upstream when a pull request targets upstream. A branch name shown by a tool or harness is not evidence of the default branch, which need not be `main`. If the repository's own guidance names another base, follow it and say so.
|
|
194
|
+
|
|
193
195
|
Before creating an issue or pull request, search the target repository's open and closed issues and pull requests for the same change. Reuse or coordinate with a match; otherwise link related work and say how yours differs. If you cannot search, say so instead of claiming no matches.
|
|
194
196
|
|
|
195
197
|
Before submitting a pull request to an external upstream project, review the
|
|
@@ -223,9 +225,11 @@ At task completion, unlanded or uncommitted task changes get one landing option,
|
|
|
223
225
|
Before integration, updating a default checkout or deleting a landed branch, read
|
|
224
226
|
[the landing procedure](references/landing.md).
|
|
225
227
|
|
|
226
|
-
|
|
227
|
-
session or job
|
|
228
|
-
|
|
228
|
+
Never reset, stash, switch or merge-commit a checkout to make it match the default.
|
|
229
|
+
A session or job running in it is not a reason to wait or ask. A checkout of the
|
|
230
|
+
default branch that is strictly behind, with no uncommitted changes to tracked files, is fast-forwarded
|
|
231
|
+
with `git merge --ff-only <sha>`. Leave a dirty, diverged or mid-operation one, report its path and reason,
|
|
232
|
+
and do not ask the user.
|
|
229
233
|
|
|
230
234
|
Before the first publish step of a version bump (tag, publish, release, announcement), review unreleased changelog entries and public materials (READMEs, charts, graphics with alt text, listing text, announcement) and fix what is stale first; an urgent release may limit only the materials to what it affects.
|
|
231
235
|
|
|
@@ -236,7 +240,7 @@ Remove a task worktree only when the default branch contains its work, its tree
|
|
|
236
240
|
## Coordination and isolation
|
|
237
241
|
|
|
238
242
|
A coordination roster, where there is one, may omit sessions. Check checkout and
|
|
239
|
-
branch ownership independently before treating them as free.
|
|
243
|
+
branch ownership independently before treating them as free to edit or remove.
|
|
240
244
|
|
|
241
245
|
Stop only processes you started, by the PID you recorded when starting them.
|
|
242
246
|
Never kill by pattern (`pkill -f`, `pgrep | kill`, or a PID found by matching
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prefer zstd, then brotli, then gzip for data you write, with browser negotiation, gzip exceptions and measured levels.
|
|
3
|
+
after: Implementation economy
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Compression codecs
|
|
7
|
+
|
|
8
|
+
When you choose how to compress data a project writes itself (logs, snapshots,
|
|
9
|
+
caches, archives, API payloads), prefer zstd, then brotli, then gzip. Use the
|
|
10
|
+
first codec in that order that every writer and reader of the data supports. A
|
|
11
|
+
preference does not add a codec to a runtime. Node added `zlib.createZstdCompress`
|
|
12
|
+
in 22.15.0 and 23.8.0 and marks it experimental as of Node 26. Check the version
|
|
13
|
+
and the API before relying on it.
|
|
14
|
+
|
|
15
|
+
These figures are single runs on Node. They use three 10.5 MB log files. Zstd at level 9
|
|
16
|
+
compressed 10.0 to 13.2 times, brotli at 4 compressed 9.5 to 12.9 times and gzip at
|
|
17
|
+
6 compressed 4.9 to 9.8 times. Compression took 9 to 14 ms at zstd 3, 24 to 33 ms
|
|
18
|
+
at brotli 4 and 40 to 85 ms at gzip 6. The runs did not time zstd 9 or measure the
|
|
19
|
+
ratio at zstd 3, so they do not show zstd winning on both size and speed at one level.
|
|
20
|
+
|
|
21
|
+
On JSON payloads of 0.1 to 7 MB, also single runs, zstd 3 compressed 1.7 to 2.3
|
|
22
|
+
times faster than brotli 4. Its output was 1% to 4% larger up to 1 MB and 19% larger
|
|
23
|
+
at 7 MB. All of this comes from a few files in one project, not a general claim.
|
|
24
|
+
|
|
25
|
+
Treat these levels as starting points and verify them on your data. Use zstd 3 on hot
|
|
26
|
+
paths and zstd 9 for cold data kept at rest. Use brotli 4 for responses compressed on
|
|
27
|
+
request and brotli 11 for static files compressed ahead of time. The runs above did not
|
|
28
|
+
measure brotli 11.
|
|
29
|
+
|
|
30
|
+
For HTTP responses to browsers, negotiate by `Accept-Encoding` instead of sending
|
|
31
|
+
zstd to every client. Pick the first of zstd, brotli and gzip that the header accepts. A coding
|
|
32
|
+
listed with `q=0` is not acceptable, so skip it. If none is acceptable, send the
|
|
33
|
+
response uncompressed unless the header also rules out `identity`, as `identity;q=0` does.
|
|
34
|
+
Then answer 406 Not Acceptable. Zstd content encoding needs Chrome 123, Edge 123
|
|
35
|
+
or Firefox 126, or later. Safari takes its support from the operating system. It has
|
|
36
|
+
zstd from Safari 26.3 on the 26.3 releases of iOS, iPadOS, visionOS and macOS Tahoe.
|
|
37
|
+
Safari on older releases lacks it, and Can I use shows macOS Safari as only partial. Smaller output matters more on high-latency or slow links, where
|
|
38
|
+
brotli can win despite slower compression.
|
|
39
|
+
|
|
40
|
+
Keep gzip where another tool must read the file: `zcat`, tarballs, npm and release
|
|
41
|
+
assets. WebSocket compression stays on `permessage-deflate`, the only extension
|
|
42
|
+
browsers offer; IANA registers no zstd or brotli extension.
|
|
43
|
+
|
|
44
|
+
Before choosing between codecs or changing a working one, measure on the real data.
|
|
45
|
+
Use `hr-benchmarking`'s defaults: 3 runs per codec, and 2 more when the gap is within
|
|
46
|
+
the run-to-run variation. The gain is output size for cold data and compression time
|
|
47
|
+
for hot paths. The other measure is a constraint, and it must not get worse outside the
|
|
48
|
+
run-to-run variation. Change a working codec only when the gain lies outside that
|
|
49
|
+
variation. Converting existing data is outside a task unless the task asks
|
|
50
|
+
for it.
|
|
51
|
+
|
|
52
|
+
References: [Node zlib](https://nodejs.org/api/zlib.html),
|
|
53
|
+
[MDN Content-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding),
|
|
54
|
+
[WebKit features for Safari 26.3](https://webkit.org/blog/17798/webkit-features-for-safari-26-3/) and
|
|
55
|
+
[Can I use zstd](https://caniuse.com/zstd).
|
|
@@ -13,10 +13,10 @@ When a review bot reviews pull requests, merge only after it finishes; fix or
|
|
|
13
13
|
answer all findings, resolve threads, and read the full review body.
|
|
14
14
|
|
|
15
15
|
During integration, never switch, reset, force-move or remove the session's
|
|
16
|
-
checkout. Default-branch updates must meet the fast-forward
|
|
16
|
+
checkout. Default-branch updates must meet the fast-forward
|
|
17
17
|
conditions in § Landing. After the merge, fetch first; move off a landed branch
|
|
18
18
|
only when its content is in the base and the tree is clean. When the base is
|
|
19
19
|
checked out elsewhere, detach at the fetched base. Never merge, reset or stash
|
|
20
20
|
to move the checkout. Leave app-managed checkouts for the app to retire.
|
|
21
|
-
If the tree is dirty
|
|
21
|
+
If the tree is dirty, leave the checkout where it is and record the merged commit, its path and the blocker; the landing stands.
|
|
22
22
|
Update the task record and follow § Cleanup after each landing.
|
|
@@ -19,8 +19,10 @@ issues invite, such as performance fixes or a reported defect.
|
|
|
19
19
|
request. A behavior change, or a project that asks for prior approval, starts
|
|
20
20
|
with a comment or discussion, not a pull request. Count the underlying
|
|
21
21
|
problems and split the submission when the guide asks for one per request.
|
|
22
|
-
3. Meet § Repository work first
|
|
23
|
-
|
|
22
|
+
3. Meet § Repository work first. Base the work on the default branch of the
|
|
23
|
+
repository the pull request targets. Search for existing reports, review the
|
|
24
|
+
diff, run the relevant checks, and follow the project's template and
|
|
25
|
+
disclosure ask.
|
|
24
26
|
4. Show the user the exact text and channel once before posting. Posting is
|
|
25
27
|
public and cannot be undone; this rule makes submitting the expected next
|
|
26
28
|
step, not a waiver of that look. Only a user instruction naming the kind of
|
|
@@ -9,25 +9,34 @@ recommended option includes it, is that direction.
|
|
|
9
9
|
|
|
10
10
|
At task completion, unlanded or uncommitted task changes get one landing option, `Land PR #<n>` or `Commit and land <change>`, never a bare done; only an explicit user deferral (recorded with branch, commits and reason) leaves it out. The option covers this section and § Cleanup, merge included; name only departures from that. Land through the repository's required path without bypassing checks. Landing is done when a fresh fetch shows the change in the default branch.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
session or job
|
|
14
|
-
|
|
12
|
+
Never reset, stash, switch or merge-commit a checkout to make it match the default.
|
|
13
|
+
A session or job running in it is not a reason to wait or ask. A checkout of the
|
|
14
|
+
default branch that is strictly behind, with no uncommitted changes to tracked files, is fast-forwarded
|
|
15
|
+
with `git merge --ff-only <sha>`. Leave a dirty, diverged or mid-operation one, report its path and reason,
|
|
16
|
+
and do not ask the user.
|
|
17
|
+
Landing never waits on a local checkout, and a roster entry in one is not an input either: the
|
|
18
|
+
checkout's own state decides whether it can be fast-forwarded (below). Removing a worktree is
|
|
19
|
+
different; § Cleanup still waits on use.
|
|
15
20
|
|
|
16
21
|
After integration:
|
|
17
22
|
|
|
18
|
-
- Fetch, then bring the local default branch current
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
- Fetch, then bring the local default branch current, whoever else has it open.
|
|
24
|
+
When no checkout has it, fast-forward the branch ref. When one checkout has it
|
|
25
|
+
(your own, the primary or another linked worktree), run `git merge --ff-only <sha>`
|
|
26
|
+
there with the remote head you just fetched and verified, provided that checkout
|
|
27
|
+
is on the default branch, has no uncommitted changes to tracked files and no
|
|
28
|
+
unfinished Git operation (merge, rebase, cherry-pick, revert, bisect), and is
|
|
29
|
+
strictly behind. Read that state right before the merge. Untracked files may
|
|
30
|
+
stay, and Git refuses a merge that would overwrite one. Avoid `git pull` there: a fetch by another process can rewrite the shared
|
|
31
|
+
`FETCH_HEAD` mid-pull and fail it with "Cannot fast-forward to multiple
|
|
32
|
+
branches". Confirm the local and fetched remote heads match.
|
|
33
|
+
- If that checkout is dirty, diverged, mid-operation or no longer on the default
|
|
34
|
+
branch, or the merge refuses because local edits would be overwritten, leave it
|
|
35
|
+
and write down its path and the reason as a pending local sync. The landing
|
|
36
|
+
stands, so finish the task and do not ask the user to confirm or wait for the
|
|
37
|
+
checkout to go idle. Never merge, reset, stash or switch branches to make it
|
|
38
|
+
match, and never touch a separate clone or a worktree that does not hold the
|
|
39
|
+
default branch.
|
|
31
40
|
- Delete the merged remote branch when it is this task's own branch
|
|
32
41
|
(`git push origin --delete <branch>` when the merge did not); § Cleanup
|
|
33
42
|
covers branches others may rely on. Before merging a pull request that another open pull
|
|
@@ -26,8 +26,9 @@ During integration, never switch, reset or remove the session's own checkout: no
|
|
|
26
26
|
`checkout` or `switch` to another ref, no `reset` or branch force-move, and no
|
|
27
27
|
`worktree remove` on it. Updating the default branch it has checked out
|
|
28
28
|
(`merge --squash`, or `merge --ff-only` to the fetched remote head after a remote merge)
|
|
29
|
-
under the fast-forward conditions in § Landing is expected
|
|
30
|
-
|
|
29
|
+
under the fast-forward conditions in § Landing is expected, and so is fast-forwarding
|
|
30
|
+
the one other checkout that holds the default branch. Report a checkout that cannot be
|
|
31
|
+
fast-forwarded with its path and reason.
|
|
31
32
|
|
|
32
33
|
After the merge, do not leave the session's own checkout on a landed branch.
|
|
33
34
|
Fetch first, then:
|
|
@@ -41,8 +42,8 @@ Fetch first, then:
|
|
|
41
42
|
commits.
|
|
42
43
|
- When an app manages the checkout, leave it for the app to retire. Report it;
|
|
43
44
|
it is not a blocker.
|
|
44
|
-
- When the tree is dirty
|
|
45
|
-
|
|
45
|
+
- When the tree is dirty, leave the checkout where it is and record the merged
|
|
46
|
+
commit, the checkout path and the blocker. The landing stands.
|
|
46
47
|
|
|
47
48
|
After each landing, update the task list and any active handoff with the
|
|
48
49
|
commit, the verification result and the remaining work, then retire the task
|
|
@@ -23,7 +23,10 @@ A predecessor plan needs no conversion. Name its `plan.md` as the source
|
|
|
23
23
|
`file`; the checker reads each item line of its `## Ledger` section (`- [ ]`,
|
|
24
24
|
`- [~]`, `- [x]`, `- [-]`) as a record named `L<line>` (`L12`). Other text in
|
|
25
25
|
the section is not read, so turn any follow-up written there into an item line
|
|
26
|
-
before the handoff.
|
|
26
|
+
before the handoff. Item lines elsewhere in the file are not read either. The
|
|
27
|
+
checker counts them and reports a gap that names the first five lines (the open
|
|
28
|
+
or deferred ones, when there are any), so the receipt is partial and cannot claim complete
|
|
29
|
+
coverage until they move into the ledger section. Take a copy of the plan as it stood at the handoff,
|
|
27
30
|
because later edits renumber the lines. Each open or deferred line must map to
|
|
28
31
|
an item; a context-only reason such as "the rest, reopen only if asked" is
|
|
29
32
|
rejected. When one item absorbs several open or deferred lines, give the item
|
|
@@ -57,7 +57,9 @@ registry row, the project and the thread id.
|
|
|
57
57
|
ledger or source record still shows it open. An offer answered, completed,
|
|
58
58
|
deferred or superseded since the handoff was written authorizes nothing. A
|
|
59
59
|
bare code or `go` in the successor selects an offer only when one listed offer
|
|
60
|
-
is open. With several, it asks which one, as § Offers requires.
|
|
60
|
+
is open. With several, it asks which one, as § Offers requires. A thread that
|
|
61
|
+
holds no offer, such as a project Orchestrator, writes the list as "none"; an
|
|
62
|
+
empty list is not left out.
|
|
61
63
|
- The inventory and read-backs from the skill's thread-move step. The
|
|
62
64
|
receiving thread re-points to itself each item the host lets it bind:
|
|
63
65
|
registry rows, scheduled tasks, pull request watches and the messaging name
|
|
@@ -56,18 +56,36 @@ function unique(entries, label) {
|
|
|
56
56
|
// A plan file (`.md`) is read as its `## Ledger` item lines, one record per item, named `L<line>`;
|
|
57
57
|
// other text in the section, wrapped continuation lines included, is not read.
|
|
58
58
|
// Open (`- [ ]`) and deferred (`- [~]`) lines are remembered: they must carry over as items.
|
|
59
|
+
// Item lines elsewhere in the file are not records either, but they are counted and reported as a gap.
|
|
59
60
|
function ledgerRecords(raw) {
|
|
60
61
|
const lines = raw.split(/\r?\n/);
|
|
61
62
|
const start = lines.findIndex((line) => /^#{1,6}\s+Ledger\s*$/i.test(line));
|
|
62
63
|
if (start < 0) return null;
|
|
63
64
|
const level = lines[start].match(/^#+/)[0].length;
|
|
64
65
|
const records = [];
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const outside = [];
|
|
67
|
+
let inside = false;
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
if (i === start) inside = true;
|
|
70
|
+
else if (inside && new RegExp(`^#{1,${level}}\\s`).test(lines[i])) inside = false;
|
|
67
71
|
const match = lines[i].match(/^\s*[-*]\s+\[([ x~-])\]\s+(.*)$/i);
|
|
68
|
-
if (match)
|
|
72
|
+
if (!match) continue;
|
|
73
|
+
if (inside) records.push({ id: `L${i + 1}`, text: match[2], mark: match[1] });
|
|
74
|
+
else outside.push({ id: `L${i + 1}`, mark: match[1] });
|
|
69
75
|
}
|
|
70
|
-
return { records };
|
|
76
|
+
return { records, outside };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function outsideGap(label, outside) {
|
|
80
|
+
const open = outside.filter((line) => /[ ~]/.test(line.mark));
|
|
81
|
+
const named = open.length > 0 ? open : outside;
|
|
82
|
+
const shown = named
|
|
83
|
+
.slice(0, 5)
|
|
84
|
+
.map((line) => line.id)
|
|
85
|
+
.join(", ");
|
|
86
|
+
const more = named.length > 5 ? ` and ${named.length - 5} more` : "";
|
|
87
|
+
const count = open.length > 0 ? `, ${open.length} of them open or deferred` : "";
|
|
88
|
+
return `${label}: ${outside.length} item line(s) outside the \`## Ledger\` section were not read${count} (${shown}${more}); move them into the ledger section`;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
function check(receipt, directory) {
|
|
@@ -125,6 +143,8 @@ function check(receipt, directory) {
|
|
|
125
143
|
errors.push(`${label}: snapshot must be an object`);
|
|
126
144
|
continue;
|
|
127
145
|
}
|
|
146
|
+
if (source.file.endsWith(".md") && data.outside.length > 0)
|
|
147
|
+
gaps.push(outsideGap(label, data.outside));
|
|
128
148
|
const rows = unique(entries(data.records, `${label}.records`), `${label} record`);
|
|
129
149
|
if (Number.isSafeInteger(source.expectedRecords) && source.expectedRecords !== rows.size) {
|
|
130
150
|
gaps.push(`${label}: expected ${source.expectedRecords} records, read ${rows.size}`);
|
|
@@ -108,18 +108,38 @@ It does not clear that gate's review-body findings or change the landing workflo
|
|
|
108
108
|
Three scripts in `scripts/` implement the landing steps § Landing describes. They never
|
|
109
109
|
reset, switch or delete a checkout.
|
|
110
110
|
|
|
111
|
-
- `merge-pr.sh <pr> [--merge] [--remote <name>]
|
|
111
|
+
- `merge-pr.sh <pr> [--merge] [--remote <name>]` squash-merges
|
|
112
112
|
(or merge-commits) an open pull request and deletes its remote head without
|
|
113
113
|
`gh pr merge --delete-branch`, which removes other sessions' linked worktrees. It
|
|
114
114
|
refuses while review feedback is open (`check-pr-review.ts`), while another open pull
|
|
115
115
|
request targets the head, and for a repository listed in `HR_PUSH_LANDED`
|
|
116
116
|
(space-separated `owner/repo`; those land by fast-forward push, which it prints). The
|
|
117
117
|
merge passes `--match-head-commit`. It lists worktrees still on the merged branch so
|
|
118
|
-
their owners clean up.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
their owners clean up. The merge never waits on a local checkout; the local default is
|
|
119
|
+
synchronized afterwards as `verify-landed.sh` describes.
|
|
120
|
+
- `verify-landed.sh --commit <sha> [--ff]` fetches, checks that the default branch
|
|
121
|
+
contains the commit, and checks the local default. `--ff` brings the local default
|
|
122
|
+
current: it moves the branch ref when no checkout has it, and runs
|
|
123
|
+
`merge --ff-only <sha>` in the one checkout that has it when that checkout is on the
|
|
124
|
+
default branch, free of uncommitted changes to tracked files and of an unfinished Git
|
|
125
|
+
operation, and strictly behind. Untracked files may stay; Git refuses a fast-forward
|
|
126
|
+
that would overwrite one. Sessions,
|
|
127
|
+
processes and roster entries are never inputs. Any other checkout is left alone and
|
|
128
|
+
reported on one line, `sync skipped at <path>: <reason>`, with a reason of
|
|
129
|
+
`working tree dirty` (tracked changes), `diverged from <remote>/<default>`, `on branch
|
|
130
|
+
<name>, not <default>`, `unfinished Git operation: <name>`, `fast-forward refused: <why>`,
|
|
131
|
+
`cannot inspect Git state` or `cannot inspect working tree`.
|
|
132
|
+
Exit 0 means landed and current; 1 is a failure; 2 is usage; 3 means landed with a local
|
|
133
|
+
sync pending, which is not a failed landing and needs no question to the user: record
|
|
134
|
+
the path and reason in the task plan. Both scripts use these exit codes. After a
|
|
135
|
+
failure, rerun this script, not `merge-pr.sh`. Both scripts accept the old
|
|
136
|
+
`--idle-primary <path>` flag and ignore it with a note. It takes the next argument as its path unless that
|
|
137
|
+
argument is missing or starts with `-`. `verify-landed.sh` also accepts `--preflight`. Without
|
|
138
|
+
`--commit` and `--branch`, it prints a note and `preflight skipped: it no longer gates landing`, checks
|
|
139
|
+
nothing and exits 0. With either one it exits 2. `merge-pr.sh` rejects `--preflight` with exit 2. Both
|
|
140
|
+
flags will be removed.
|
|
123
141
|
- `check-landing.py [--path <checkout>] [--base <ref>]` reports whether a checkout's
|
|
124
142
|
work is in the base branch by content, so squash-merged branches count as landed, and
|
|
125
|
-
lists live processes whose working directory is inside the checkout (Linux only).
|
|
143
|
+
lists live processes whose working directory is inside the checkout (Linux only). That
|
|
144
|
+
list is a blocker for removing the worktree only. Landing and syncing the default
|
|
145
|
+
checkout never read it.
|
|
@@ -8,7 +8,8 @@ the base. Read-only; never fetches — the caller fetches first.
|
|
|
8
8
|
|
|
9
9
|
It also lists live processes whose working directory is inside the checkout
|
|
10
10
|
(Linux /proc only), excluding the checker's ancestors and its pipeline. Each listed process
|
|
11
|
-
blocks worktree removal unless its PID is one you recorded when you started it.
|
|
11
|
+
blocks worktree removal unless its PID is one you recorded when you started it. The list says nothing
|
|
12
|
+
about landing or about fast-forwarding a default checkout: neither reads it. `own_session`
|
|
12
13
|
is true only for a process that descends from the nearest agent CLI above the checker (a
|
|
13
14
|
process whose comm is one of AGENT_COMMS); a process under another session of the same agent
|
|
14
15
|
host's server, or any process when the checker has no agent ancestor, is false. A process
|
|
@@ -15,25 +15,28 @@
|
|
|
15
15
|
# empty by default): its own guidance lands by fast-forwarding the base to the head, because a GitHub
|
|
16
16
|
# merge writes the account's profile name into public history. It prints that push instead.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# If verification
|
|
25
|
-
# fails after "merged", rerun verify-landed.sh with the printed commit, not this script.
|
|
18
|
+
# Landing never waits on a local checkout. After the merge it fast-forwards the checkout that holds
|
|
19
|
+
# the default branch when that checkout is on the default branch, clean, free of an unfinished Git
|
|
20
|
+
# operation and strictly behind (verify-landed.sh), whoever else has it open. Otherwise it leaves the
|
|
21
|
+
# checkout alone, prints `sync skipped at <path>: <reason>` and exits 3: landed, local sync pending.
|
|
22
|
+
# Record the path and reason in the task plan; nothing needs the user. If verification fails after
|
|
23
|
+
# "merged" (exit 1), rerun verify-landed.sh with the printed commit, not this script.
|
|
26
24
|
#
|
|
27
25
|
# --merge lands with a merge commit instead of a squash. Use it only for a PR whose second parent
|
|
28
26
|
# must stay in history, such as an `-s ours` merge that records an upstream reconcile; a squash
|
|
29
27
|
# would drop that parent. Every other PR is squashed.
|
|
30
28
|
#
|
|
31
|
-
#
|
|
29
|
+
# Exit status: 0 landed and every local default current; 1 refused or failed; 2 usage; 3 landed, local sync pending.
|
|
30
|
+
# --idle-primary <path> is deprecated: accepted and ignored with a note on stderr, removed in a later release.
|
|
31
|
+
# It takes the next argument as its path unless that argument is missing or starts with "-".
|
|
32
|
+
# --preflight is not accepted here (unknown argument, exit 2); only verify-landed.sh still takes it.
|
|
33
|
+
#
|
|
34
|
+
# Usage: bash scripts/merge-pr.sh <pr> [--merge] [--remote <name>]
|
|
32
35
|
set -euo pipefail
|
|
33
36
|
|
|
34
37
|
PUSH_LANDED=" ${HR_PUSH_LANDED:-} "
|
|
35
38
|
|
|
36
|
-
PR="${1:?usage: merge-pr.sh <pr> [--merge] [--remote <name>]
|
|
39
|
+
PR="${1:?usage: merge-pr.sh <pr> [--merge] [--remote <name>]}"
|
|
37
40
|
shift
|
|
38
41
|
REMOTE=origin
|
|
39
42
|
STRATEGY=--squash
|
|
@@ -42,7 +45,9 @@ while [ $# -gt 0 ]; do
|
|
|
42
45
|
case "$1" in
|
|
43
46
|
--merge) STRATEGY=--merge; shift;;
|
|
44
47
|
--remote) REMOTE="${2:?}"; shift 2;;
|
|
45
|
-
--idle-primary)
|
|
48
|
+
--idle-primary)
|
|
49
|
+
echo "note: --idle-primary is ignored; the checkout holding the default branch is fast-forwarded when it is clean and strictly behind. The flag will be removed in a later release." >&2
|
|
50
|
+
shift; case "${1:-}" in ''|-*) ;; *) shift;; esac;;
|
|
46
51
|
*) echo "unknown argument: $1" >&2; exit 2;;
|
|
47
52
|
esac
|
|
48
53
|
done
|
|
@@ -67,9 +72,6 @@ if [[ "$PUSH_LANDED" == *" $REPO "* ]]; then
|
|
|
67
72
|
exit 1
|
|
68
73
|
fi
|
|
69
74
|
|
|
70
|
-
bash "$(dirname "${BASH_SOURCE[0]}")/verify-landed.sh" --preflight \
|
|
71
|
-
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}"
|
|
72
|
-
|
|
73
75
|
gh pr merge "$PR" "$STRATEGY" --match-head-commit "$OID"
|
|
74
76
|
SHA="$(gh pr view "$PR" --json mergeCommit --jq '.mergeCommit.oid // empty')"
|
|
75
77
|
if ! [[ "$SHA" =~ ^([0-9a-f]{40}|[0-9a-f]{64})$ ]]; then
|
|
@@ -91,10 +93,13 @@ if [ "$FORK" != true ]; then
|
|
|
91
93
|
fi
|
|
92
94
|
fi
|
|
93
95
|
|
|
96
|
+
# Exit 3 (local sync pending) is not a failure: finish the worktree report, then pass it on.
|
|
97
|
+
STATUS=0
|
|
94
98
|
bash "$(dirname "${BASH_SOURCE[0]}")/verify-landed.sh" --commit "$SHA" --branch "$HEAD" \
|
|
95
|
-
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}"
|
|
99
|
+
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}" || STATUS=$?
|
|
96
100
|
|
|
97
101
|
git worktree list --porcelain | awk -v want="branch refs/heads/$HEAD" '
|
|
98
102
|
/^worktree / { path = substr($0, 10) }
|
|
99
103
|
$0 == want { print "still on " substr(want, 19) ": " path " (its owner retires it)" }
|
|
100
104
|
'
|
|
105
|
+
exit "$STATUS"
|
|
@@ -2,25 +2,38 @@
|
|
|
2
2
|
# Post-merge landing checks: containment, local-default state, leftover remote branches.
|
|
3
3
|
# Fetches and verifies both remote containment and local-default equality. A stale local default
|
|
4
4
|
# fails even when the remote contains the commit. Never resets, switches or deletes.
|
|
5
|
-
#
|
|
6
|
-
# --
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
#
|
|
6
|
+
# With --ff it brings the local default branch current, whoever else has that checkout open. Which
|
|
7
|
+
# processes, sessions or roster entries sit in a checkout is never an input. The branch moves when
|
|
8
|
+
# it is checked out nowhere (a ref update), or when the one checkout that holds it is on the default
|
|
9
|
+
# branch, free of uncommitted changes to tracked files and of an unfinished Git operation, and
|
|
10
|
+
# strictly behind: then
|
|
11
|
+
# `merge --ff-only <sha>` runs there. It uses the verified remote commit rather than `pull`, because
|
|
11
12
|
# `pull` re-fetches into the shared FETCH_HEAD, which a concurrent fetch in the same checkout can
|
|
12
|
-
# overwrite ("Cannot fast-forward to multiple branches").
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
13
|
+
# overwrite ("Cannot fast-forward to multiple branches"). Untracked files may stay: Git keeps them
|
|
14
|
+
# and refuses a fast-forward that would overwrite one.
|
|
15
|
+
#
|
|
16
|
+
# A checkout that fails any of those conditions is left alone and reported on one line:
|
|
17
|
+
# sync skipped at <path>: <reason>
|
|
18
|
+
# with a reason of "working tree dirty" (tracked changes), "diverged from <remote>/<default>", "on
|
|
19
|
+
# branch <name>, not <default>", "unfinished Git operation: <name>", "fast-forward refused: <why>",
|
|
20
|
+
# "cannot inspect Git state" or "cannot inspect working tree". A diverged default that no checkout
|
|
21
|
+
# holds is reported at refs/heads/<default>. The landing itself stands; record the path and reason
|
|
22
|
+
# where you track the task and do not ask the user.
|
|
23
|
+
#
|
|
24
|
+
# Exit status: 0 landed and every local default current; 1 a real failure (commit not contained,
|
|
25
|
+
# stale default with nothing skipped, leftover remote branch, fetch failure); 2 usage; 3 landed and
|
|
26
|
+
# verified, but a local checkout was skipped (local sync pending). After a failure, rerun this
|
|
27
|
+
# script, not merge-pr.sh: the PR is already merged.
|
|
28
|
+
#
|
|
29
|
+
# Deprecated, removed in a later release; landing no longer waits on any local checkout.
|
|
30
|
+
# --idle-primary <path> is accepted and ignored with a note on stderr. It takes the next argument as its
|
|
31
|
+
# path unless that argument is missing or starts with "-".
|
|
32
|
+
# --preflight prints a note and "preflight skipped: it no longer gates landing" and exits 0 without
|
|
33
|
+
# checking anything; with --commit or --branch it exits 2.
|
|
19
34
|
# Usage:
|
|
20
35
|
# bash scripts/verify-landed.sh --commit <sha> [--default <branch>]
|
|
21
|
-
# [--branch <merged-head>] [--remote <name>] [--ff
|
|
22
|
-
# bash scripts/verify-landed.sh --preflight --ff [--default <branch>]
|
|
23
|
-
# [--remote <name>] [--idle-primary <path>]
|
|
36
|
+
# [--branch <merged-head>] [--remote <name>] [--ff]
|
|
24
37
|
set -euo pipefail
|
|
25
38
|
|
|
26
39
|
REMOTE=origin
|
|
@@ -28,8 +41,7 @@ DEFAULT=""
|
|
|
28
41
|
COMMIT=""
|
|
29
42
|
BRANCH=""
|
|
30
43
|
FF=0
|
|
31
|
-
|
|
32
|
-
PREFLIGHT=0
|
|
44
|
+
LEGACY_PREFLIGHT=0
|
|
33
45
|
|
|
34
46
|
while [ $# -gt 0 ]; do
|
|
35
47
|
case "$1" in
|
|
@@ -38,24 +50,22 @@ while [ $# -gt 0 ]; do
|
|
|
38
50
|
--branch) BRANCH="${2:?}"; shift 2;;
|
|
39
51
|
--remote) REMOTE="${2:?}"; shift 2;;
|
|
40
52
|
--ff) FF=1; shift;;
|
|
41
|
-
--idle-primary)
|
|
42
|
-
|
|
53
|
+
--idle-primary)
|
|
54
|
+
echo "note: --idle-primary is ignored; the checkout holding the default branch is fast-forwarded when it is clean and strictly behind. The flag will be removed in a later release." >&2
|
|
55
|
+
shift; case "${1:-}" in ''|-*) ;; *) shift;; esac;;
|
|
56
|
+
--preflight)
|
|
57
|
+
LEGACY_PREFLIGHT=1
|
|
58
|
+
echo "note: --preflight is ignored; landing no longer waits on local checkouts. The flag will be removed in a later release." >&2
|
|
59
|
+
shift;;
|
|
43
60
|
*) echo "unknown argument: $1" >&2; exit 2;;
|
|
44
61
|
esac
|
|
45
62
|
done
|
|
46
|
-
if [ "$
|
|
47
|
-
[ "$
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
[ -n "$COMMIT" ] || { echo "missing --commit <sha>" >&2; exit 2; }
|
|
51
|
-
fi
|
|
52
|
-
if [ -n "$IDLE_PRIMARY" ]; then
|
|
53
|
-
[ "$FF" = 1 ] || { echo "--idle-primary requires --ff" >&2; exit 2; }
|
|
54
|
-
PRIMARY="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
|
55
|
-
IDLE_PRIMARY="$(git -C "$IDLE_PRIMARY" rev-parse --show-toplevel)"
|
|
56
|
-
[ "$IDLE_PRIMARY" = "$PRIMARY" ] \
|
|
57
|
-
|| { echo "--idle-primary is not this repository's primary checkout: $IDLE_PRIMARY" >&2; exit 2; }
|
|
63
|
+
if [ "$LEGACY_PREFLIGHT" = 1 ]; then
|
|
64
|
+
[ -z "$COMMIT$BRANCH" ] || { echo "--preflight excludes --commit/--branch" >&2; exit 2; }
|
|
65
|
+
echo "preflight skipped: it no longer gates landing"
|
|
66
|
+
exit 0
|
|
58
67
|
fi
|
|
68
|
+
[ -n "$COMMIT" ] || { echo "missing --commit <sha>" >&2; exit 2; }
|
|
59
69
|
|
|
60
70
|
if [ -z "$DEFAULT" ]; then
|
|
61
71
|
# The remote's own answer wins; the cached origin/HEAD goes stale when the default branch changes.
|
|
@@ -76,48 +86,64 @@ HOLDER="$(printf '%s\n' "$WTLS" | awk -v want="branch refs/heads/$DEFAULT" '
|
|
|
76
86
|
')"
|
|
77
87
|
HERE="$(git rev-parse --show-toplevel)"
|
|
78
88
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
# A rebase or bisect detaches HEAD while still reserving its original branch.
|
|
90
|
+
if [ -z "$HOLDER" ]; then
|
|
91
|
+
while IFS= read -r RECORD; do
|
|
92
|
+
case "$RECORD" in worktree\ *) CHECKOUT="${RECORD#worktree }";; *) continue;; esac
|
|
93
|
+
[ -d "$CHECKOUT" ] || continue
|
|
94
|
+
for ASSOCIATION in rebase-merge/head-name rebase-apply/head-name BISECT_START; do
|
|
95
|
+
# A checkout whose state cannot be read counts as holding the branch: holder_blocker then skips it.
|
|
96
|
+
ASSOCIATION_PATH="$(git -C "$CHECKOUT" rev-parse --path-format=absolute --git-path "$ASSOCIATION" 2>/dev/null)" || {
|
|
97
|
+
HOLDER="$CHECKOUT"
|
|
98
|
+
break 2
|
|
99
|
+
}
|
|
100
|
+
if [ -f "$ASSOCIATION_PATH" ]; then
|
|
101
|
+
ASSOCIATED_BRANCH=""
|
|
102
|
+
if ! { IFS= read -r ASSOCIATED_BRANCH < "$ASSOCIATION_PATH" || [ -n "$ASSOCIATED_BRANCH" ]; } 2>/dev/null; then
|
|
103
|
+
HOLDER="$CHECKOUT"
|
|
104
|
+
break 2
|
|
93
105
|
fi
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
echo "sync blocked at $HOLDER: that checkout has the default branch. If nothing is using it, rerun with --idle-primary <primary-path>" >&2
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
STATUS="$(git -C "$HOLDER" status --porcelain)" \
|
|
103
|
-
|| { echo "sync blocked at $HOLDER: cannot inspect working tree" >&2; exit 1; }
|
|
104
|
-
[ -z "$STATUS" ] \
|
|
105
|
-
|| { echo "sync blocked at $HOLDER: working tree dirty; leave it for its owner" >&2; exit 1; }
|
|
106
|
-
for OPERATION in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-merge rebase-apply sequencer BISECT_START; do
|
|
107
|
-
OPERATION_PATH="$(git -C "$HOLDER" rev-parse --path-format=absolute --git-path "$OPERATION")"
|
|
108
|
-
[ ! -e "$OPERATION_PATH" ] \
|
|
109
|
-
|| { echo "sync blocked at $HOLDER: unfinished Git operation: $OPERATION" >&2; exit 1; }
|
|
106
|
+
if [ "$ASSOCIATED_BRANCH" = "refs/heads/$DEFAULT" ] || [ "$ASSOCIATED_BRANCH" = "$DEFAULT" ]; then
|
|
107
|
+
HOLDER="$CHECKOUT"
|
|
108
|
+
break 2
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
110
111
|
done
|
|
111
|
-
|
|
112
|
-
if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
113
|
-
git merge-base --is-ancestor "$DEFAULT" "$REMOTE/$DEFAULT" \
|
|
114
|
-
|| { echo "sync blocked: $DEFAULT cannot fast-forward to $REMOTE/$DEFAULT" >&2; exit 1; }
|
|
115
|
-
fi
|
|
116
|
-
echo "synchronization preflight passed: $DEFAULT (${HOLDER:-not checked out})"
|
|
117
|
-
exit 0
|
|
112
|
+
done <<< "$WTLS"
|
|
118
113
|
fi
|
|
119
114
|
|
|
120
115
|
FAIL=0
|
|
116
|
+
SKIPPED=0
|
|
117
|
+
|
|
118
|
+
skip() {
|
|
119
|
+
echo "sync skipped at $1: $2"
|
|
120
|
+
SKIPPED=1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Prints why a checkout cannot be fast-forwarded now, or nothing when it can.
|
|
124
|
+
holder_blocker() {
|
|
125
|
+
local path="$1" operation operation_path branch status
|
|
126
|
+
for operation in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-merge rebase-apply sequencer BISECT_START; do
|
|
127
|
+
operation_path="$(git -C "$path" rev-parse --path-format=absolute --git-path "$operation" 2>/dev/null)" \
|
|
128
|
+
|| { echo "cannot inspect Git state"; return; }
|
|
129
|
+
if [ -e "$operation_path" ]; then
|
|
130
|
+
echo "unfinished Git operation: $operation"
|
|
131
|
+
return
|
|
132
|
+
fi
|
|
133
|
+
done
|
|
134
|
+
branch="$(git -C "$path" symbolic-ref --quiet HEAD 2>/dev/null || true)"
|
|
135
|
+
if [ "$branch" != "refs/heads/$DEFAULT" ]; then
|
|
136
|
+
if [ -n "$branch" ]; then
|
|
137
|
+
echo "on branch ${branch#refs/heads/}, not $DEFAULT"
|
|
138
|
+
else
|
|
139
|
+
echo "on branch (detached HEAD), not $DEFAULT"
|
|
140
|
+
fi
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
# Untracked files may stay: Git itself refuses a fast-forward that would overwrite one.
|
|
144
|
+
status="$(git -C "$path" status --porcelain --untracked-files=no 2>/dev/null)" || { echo "cannot inspect working tree"; return; }
|
|
145
|
+
[ -z "$status" ] || echo "working tree dirty"
|
|
146
|
+
}
|
|
121
147
|
|
|
122
148
|
if git merge-base --is-ancestor "$COMMIT" "$REMOTE/$DEFAULT" 2>/dev/null; then
|
|
123
149
|
echo "contained: $COMMIT in $REMOTE/$DEFAULT"
|
|
@@ -133,35 +159,45 @@ if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
|
133
159
|
echo "local $DEFAULT: current ($LOCAL)"
|
|
134
160
|
elif git merge-base --is-ancestor "$LOCAL" "$UPSTREAM" 2>/dev/null; then
|
|
135
161
|
BEHIND="$(git rev-list --count "$LOCAL..$UPSTREAM")"
|
|
136
|
-
if [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
if [ "$FF" != 1 ] || [ "$FAIL" != 0 ]; then
|
|
163
|
+
WHY="not updated without --ff"
|
|
164
|
+
[ "$FF" != 1 ] || WHY="not updated: the landing check failed"
|
|
165
|
+
if [ -z "$HOLDER" ]; then
|
|
166
|
+
if [ "$FF" != 1 ]; then
|
|
167
|
+
echo "local $DEFAULT: $BEHIND behind, not checked out anywhere. Safe: git fetch $REMOTE $DEFAULT:$DEFAULT"
|
|
168
|
+
else
|
|
169
|
+
echo "local $DEFAULT: $BEHIND behind, not checked out anywhere; $WHY"
|
|
170
|
+
fi
|
|
140
171
|
else
|
|
141
|
-
echo "local $DEFAULT: $BEHIND behind,
|
|
172
|
+
echo "local $DEFAULT: $BEHIND behind, checked out at $HOLDER; $WHY"
|
|
142
173
|
fi
|
|
143
|
-
elif [ "$HOLDER"
|
|
144
|
-
if
|
|
145
|
-
|
|
146
|
-
|| { echo "primary checkout changed branches; refusing update" >&2; exit 1; }
|
|
147
|
-
git -C "$HOLDER" merge --quiet --ff-only "$UPSTREAM"
|
|
148
|
-
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) in confirmed idle primary $HOLDER"
|
|
174
|
+
elif [ -z "$HOLDER" ]; then
|
|
175
|
+
if REF_ERR="$(git update-ref -m "verify-landed: fast-forward $DEFAULT" "refs/heads/$DEFAULT" "$UPSTREAM" "$LOCAL" 2>&1)"; then
|
|
176
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (not checked out anywhere)"
|
|
149
177
|
else
|
|
150
|
-
|
|
151
|
-
echo "If nothing is using that checkout, rerun verify-landed.sh with --ff --idle-primary <primary-path>." >&2
|
|
178
|
+
skip "refs/heads/$DEFAULT" "fast-forward refused: $(printf '%s' "$REF_ERR" | head -n 1)"
|
|
152
179
|
fi
|
|
153
|
-
elif [ -n "$(git status --porcelain)" ]; then
|
|
154
|
-
echo "local $DEFAULT: $BEHIND behind but working tree dirty — report, do not touch"
|
|
155
|
-
elif [ "$FF" = 1 ] && [ "$FAIL" = 0 ]; then
|
|
156
|
-
git merge --quiet --ff-only "$UPSTREAM"
|
|
157
|
-
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (checked out here)"
|
|
158
180
|
else
|
|
159
|
-
|
|
181
|
+
# The state is read right before the merge; Git refuses it if edits would be overwritten.
|
|
182
|
+
REASON="$(holder_blocker "$HOLDER")"
|
|
183
|
+
if [ -n "$REASON" ]; then
|
|
184
|
+
skip "$HOLDER" "$REASON"
|
|
185
|
+
elif MERGE_ERR="$(git -C "$HOLDER" merge --quiet --ff-only "$UPSTREAM" 2>&1)"; then
|
|
186
|
+
if [ "$HOLDER" = "$HERE" ]; then
|
|
187
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (checked out here)"
|
|
188
|
+
else
|
|
189
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM in $HOLDER"
|
|
190
|
+
fi
|
|
191
|
+
elif printf '%s' "$MERGE_ERR" | grep -q "would be overwritten"; then
|
|
192
|
+
skip "$HOLDER" "fast-forward refused: local edits would be overwritten"
|
|
193
|
+
else
|
|
194
|
+
skip "$HOLDER" "fast-forward refused: $(printf '%s' "$MERGE_ERR" | head -n 1)"
|
|
195
|
+
fi
|
|
160
196
|
fi
|
|
161
197
|
else
|
|
162
|
-
|
|
198
|
+
skip "${HOLDER:-refs/heads/$DEFAULT}" "diverged from $REMOTE/$DEFAULT"
|
|
163
199
|
fi
|
|
164
|
-
if [ "$(git rev-parse "$DEFAULT")" != "$(git rev-parse "$REMOTE/$DEFAULT")" ]; then
|
|
200
|
+
if [ "$(git rev-parse "$DEFAULT")" != "$(git rev-parse "$REMOTE/$DEFAULT")" ] && [ "$SKIPPED" = 0 ]; then
|
|
165
201
|
echo "LOCAL DEFAULT NOT CURRENT: $DEFAULT differs from $REMOTE/$DEFAULT; landing incomplete" >&2
|
|
166
202
|
FAIL=1
|
|
167
203
|
fi
|
|
@@ -181,4 +217,11 @@ if [ -n "$BRANCH" ]; then
|
|
|
181
217
|
fi
|
|
182
218
|
fi
|
|
183
219
|
|
|
184
|
-
|
|
220
|
+
if [ "$FAIL" != 0 ]; then
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
if [ "$SKIPPED" = 1 ]; then
|
|
224
|
+
echo "landed; local sync pending (exit 3): record the path and reason above in the task plan"
|
|
225
|
+
exit 3
|
|
226
|
+
fi
|
|
227
|
+
exit 0
|
|
@@ -209,7 +209,11 @@ function parsePlan(text) {
|
|
|
209
209
|
i > 0 &&
|
|
210
210
|
(NARRATIVE.test(line) || PROSE_MARKER.test(line) || /^\s*[-*]\s+\[[ ~]\]/.test(line))
|
|
211
211
|
)
|
|
212
|
-
narrative.push({
|
|
212
|
+
narrative.push({
|
|
213
|
+
line: i + 1,
|
|
214
|
+
text: line.trim(),
|
|
215
|
+
box: /^\s*[-*]\s+\[[ ~]\]/.test(line),
|
|
216
|
+
});
|
|
213
217
|
});
|
|
214
218
|
return { status, owner, purpose, project, items, errors, narrative };
|
|
215
219
|
}
|
|
@@ -580,7 +584,15 @@ async function run(options) {
|
|
|
580
584
|
};
|
|
581
585
|
const open = plan.items.filter((item) => item.mark === " " || item.mark === "~");
|
|
582
586
|
report.open = open.map(row);
|
|
583
|
-
|
|
587
|
+
// A plan that fails its lint still gets every check that reads only its text. The checks that read the
|
|
588
|
+
// host (scratch, checkouts, the audit record, GitHub) wait until the plan is valid.
|
|
589
|
+
const invalid = report.errors.length > 0;
|
|
590
|
+
if (invalid) {
|
|
591
|
+
report.invalidPlan = true;
|
|
592
|
+
report.skipped.push(
|
|
593
|
+
"scratch, checkout, audit record and GitHub checks: they wait until the plan passes its lint",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
584
596
|
report.awaitingUser = open.filter(awaitsUser).map(row);
|
|
585
597
|
if (TERMINAL_STATUS.test(plan.status) && open.length)
|
|
586
598
|
report.findings.push(
|
|
@@ -588,7 +600,7 @@ async function run(options) {
|
|
|
588
600
|
);
|
|
589
601
|
const roots = notesRoot(planPath);
|
|
590
602
|
const scratch = options.scratch ?? (roots ? join(roots.notes, "tmp") : null);
|
|
591
|
-
if (scratch) {
|
|
603
|
+
if (scratch && !invalid) {
|
|
592
604
|
const { owned, unreadable } = ownedScratch(scratch, plan.owner);
|
|
593
605
|
for (const manifest of unreadable)
|
|
594
606
|
report.errors.push(
|
|
@@ -596,7 +608,7 @@ async function run(options) {
|
|
|
596
608
|
);
|
|
597
609
|
for (const name of owned) accountFor(`scratch ${name}`, [name], plan.items, report.findings);
|
|
598
610
|
}
|
|
599
|
-
for (const path of options.checkouts) {
|
|
611
|
+
for (const path of invalid ? [] : options.checkouts) {
|
|
600
612
|
const state = checkoutState(path);
|
|
601
613
|
if (state.error) report.errors.push(state.error);
|
|
602
614
|
else if (state.dirty || state.unpushed)
|
|
@@ -608,7 +620,7 @@ async function run(options) {
|
|
|
608
620
|
);
|
|
609
621
|
}
|
|
610
622
|
// An app retires its own checkouts, so their state is reported but is not a finding.
|
|
611
|
-
for (const path of options.appManaged) {
|
|
623
|
+
for (const path of invalid ? [] : options.appManaged) {
|
|
612
624
|
const state = checkoutState(path);
|
|
613
625
|
if (state.error) report.errors.push(state.error);
|
|
614
626
|
else if (state.dirty || state.unpushed)
|
|
@@ -620,20 +632,23 @@ async function run(options) {
|
|
|
620
632
|
report.warnings.push(
|
|
621
633
|
"no `Purpose:` line in the first 40 lines (the session's purpose, one sentence)",
|
|
622
634
|
);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
635
|
+
// Every unchecked box outside the ledger is a possible lost item, so each is listed. Prose labels such
|
|
636
|
+
// as "Deferred:" are listed up to five, with a count of the rest. Lines stay in file order.
|
|
637
|
+
let prose = 0;
|
|
638
|
+
for (const entry of plan.narrative)
|
|
639
|
+
if (entry.box || ++prose <= 5)
|
|
640
|
+
report.warnings.push(`L${entry.line} outside the ledger: ${clip(entry.text, 70)}`);
|
|
641
|
+
if (prose > 5) report.warnings.push(`${prose - 5} more narrative marker line(s)`);
|
|
627
642
|
offerWarnings(plan.items, report.warnings);
|
|
628
643
|
report.unansweredOffers = unansweredOffers(plan.items);
|
|
629
644
|
if (report.unansweredOffers.length >= OFFER_LIMIT)
|
|
630
645
|
report.warnings.push(
|
|
631
646
|
`${report.unansweredOffers.length} offers are open and unanswered; the offers rule says to present no further offer at ${OFFER_LIMIT} or more. Queue the new decision in a walk-through (hr-what-next, Walk-through) instead`,
|
|
632
647
|
);
|
|
633
|
-
const audit = plan.project && auditWarning(plan.project, roots, options.today);
|
|
648
|
+
const audit = !invalid && plan.project && auditWarning(plan.project, roots, options.today);
|
|
634
649
|
if (audit) report.warnings.push(audit);
|
|
635
650
|
report.recheckDue = rechecks(open, options.today, report.warnings);
|
|
636
|
-
report.stale = await possiblyFinished(open, options, report);
|
|
651
|
+
if (!invalid) report.stale = await possiblyFinished(open, options, report);
|
|
637
652
|
if (options.archive && !report.errors.length) {
|
|
638
653
|
if (!COMPLETED_STATUS.test(plan.status))
|
|
639
654
|
report.findings.push(`archiving refused: Status is "${plan.status}", not completed`);
|
|
@@ -678,7 +693,7 @@ async function main() {
|
|
|
678
693
|
console.log(
|
|
679
694
|
`plan: ${report.plan}\nstatus: ${report.status ?? "?"} | owner: ${report.owner ?? "?"}`,
|
|
680
695
|
);
|
|
681
|
-
|
|
696
|
+
console.log(`unanswered offers: ${report.unansweredOffers.length}`);
|
|
682
697
|
for (const [label, rows] of [
|
|
683
698
|
["error", report.errors],
|
|
684
699
|
["open", report.open],
|
|
@@ -696,7 +711,7 @@ async function main() {
|
|
|
696
711
|
code === 0
|
|
697
712
|
? "result: nothing open"
|
|
698
713
|
: code === 1
|
|
699
|
-
?
|
|
714
|
+
? `result: invalid input${report.invalidPlan ? " (the checks that read only the plan text still ran)" : ""}`
|
|
700
715
|
: `result: ${report.open.length} open item(s) (${report.awaitingUser.length} awaiting your yes/no), ${report.findings.length} finding(s)`,
|
|
701
716
|
);
|
|
702
717
|
}
|