@erclx/canon 5.10.0 → 5.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/docs/agents/index.md +2 -2
- package/docs/agents/pr-evidence.md +48 -1
- package/docs/agents/worktrees.md +16 -1
- package/docs/workflow/skill-map.md +1 -1
- package/package.json +1 -1
- package/src/commands/gate.ts +3 -2
- package/src/commands/pr.ts +77 -5
- package/src/commands/worktrees.ts +37 -0
- package/src/gate/sequencer.ts +0 -20
- package/src/gate/stages.ts +0 -1
- package/src/pr/evidence.ts +35 -0
- package/src/worktrees/bare-flag.ts +83 -0
- package/scripts/core/repair-bare-flag.sh +0 -19
- package/scripts/lib/worktree.sh +0 -20
|
@@ -63,7 +63,7 @@ Report the state of play in the invocation block, and open every later sweep rep
|
|
|
63
63
|
4. Hand off. Read `${CLAUDE_SKILL_DIR}/references/orchestrator-dispatch.md` and follow it: check the branch is unclaimed, check the row's file set against every track in flight, then dispatch a background worker with `claude --bg`. Fall back to the human-launch line it replaces when the check refuses, the sets overlap, or a stated reason serializes the row behind something already out.
|
|
64
64
|
5. Review the PR. When a worker opens a PR, its first pass runs through `review-pr` in a reviewer dispatched under `role-reviewer` when `### The review dispatch` below fires, and in this session otherwise. Either is the deep, independent pass. The worker's autoship self-review was only the green gate.
|
|
65
65
|
- Learning that a PR moved is the mechanical half, so read `${CLAUDE_SKILL_DIR}/references/orchestrator-poll.md` and run the poll under the condition it states rather than checking the board by hand. That runbook holds the routing and the trigger, and a summary of it here is a second source that drifts from it.
|
|
66
|
-
6. Dispatch the handback. A pass posting anything owed, a finding at any severity or a testing question, tells the session holding that branch to run `review-address`, rather than waiting for a person to relay it. Re-review when the worker's own message says the address pass finished, per the channel `role-worker` states, rather than polling for an answer nothing else marks as landed, and send the re-review back to the dispatched reviewer when one took the first pass. Once a pass posts `## Review closed`,
|
|
66
|
+
6. Dispatch the handback. A pass posting anything owed, a finding at any severity or a testing question, tells the session holding that branch to run `review-address`, rather than waiting for a person to relay it. Re-review when the worker's own message says the address pass finished, per the channel `role-worker` states, rather than polling for an answer nothing else marks as landed, and send the re-review back to the dispatched reviewer when one took the first pass. Once a pass posts `## Review closed`, a UI close names the current head wherever the PR carries a checklist, and `--check` reports `settled`, lift the pull request's draft mark yourself, per Boundaries below. Then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
|
|
67
67
|
- Read `${CLAUDE_SKILL_DIR}/references/orchestrator-handback.md` on reaching this step. It holds how the message is addressed and worded, what to do when no live session holds the branch, and where a worker's reply goes.
|
|
68
68
|
|
|
69
69
|
A plan written here is written against a tree several branches are already changing, so it names the file set of every track in flight as a constraint, one set per track, read from the Touches column of that track's row. State for each set which of the two acts it forbids, per Constraints in `${CLAUDE_SKILL_DIR}/../../standards/plan.md`. A bare path list leaves the worker guessing, which is how a plan ends up forbidding the repair of a citation the change broke.
|
|
@@ -75,7 +75,7 @@ Stamp the block with the commit this session read the tree at, which the same se
|
|
|
75
75
|
- Run one orchestrator at a time. The board is gitignored, so a second session sees none of this one's writes: two task files land minutes apart under different labels for the same work, one session archives a task mid-sweep in the other, and each archives a plan the other had retargeted. An Owner column does not fix this, since neither session can read the other's rows.
|
|
76
76
|
- Do not implement features in this session. Hand the plan to a worker.
|
|
77
77
|
- Do not merge. Recommend merge or changes. The human merges.
|
|
78
|
-
- Lift a pull request's draft mark once its review closes, whether this session or a reviewer it dispatched closed it, acting directly on the pull request rather than dispatching a worker
|
|
78
|
+
- Lift a pull request's draft mark once its review closes, whether this session or a reviewer it dispatched closed it, acting directly on the pull request rather than dispatching a worker. Hold it on any `canon pr evidence <number> --check --json` reason but `settled`, reporting that reason, and where that record carries a `checklist`, until a UI close names the current head. `role-worker` and `role-reviewer` state the mirroring refusal: neither can verify who is asking or whether review actually closed, so the act stays with this session.
|
|
79
79
|
- Do not spawn a worker with the Agent tool. An in-process subagent shares this session's context and cannot be steered or reached independently, which breaks the property this boundary protects rather than the mechanism it names. The launch in `orchestrator-launch.md` is a separate `claude --bg` process with its own worktree and its own PR, so it preserves that property instead.
|
|
80
80
|
- Dispatch a background worker only once the collision check in `orchestrator-dispatch.md` clears and the row's file set is disjoint from every track in flight. Colliding with an existing worktree or session is what the check exists to catch rather than a judgment call this session makes case by case. No fixed count binds how many tracks run at once, and Parallelism below states what does.
|
|
81
81
|
- Do not edit tracked files from this session, at any size. The boundary offers no proportionality exception and nothing enforces it.
|
package/docs/agents/index.md
CHANGED
|
@@ -35,7 +35,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
35
35
|
- [Migrate](migrate.md): What each canon migrate verb moves and rewrites, what it refuses, the markers that keep an old spelling, and the idempotence check a second run gives
|
|
36
36
|
- [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
|
|
37
37
|
- [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
|
|
38
|
-
- [The pull request evidence comparison](pr-evidence.md): What canon pr evidence compares, the marker that lets it edit its own comment rather than duplicate it, the refusal reasons it names, and why the comparison anchors at the merge base rather than the previous push
|
|
38
|
+
- [The pull request evidence comparison](pr-evidence.md): What canon pr evidence compares, the marker that lets it edit its own comment rather than duplicate it, the refusal reasons it names, what the --check read reports as owed, and why the comparison anchors at the merge base rather than the previous push
|
|
39
39
|
- [The pull request local preview](pr-local.md): How canon pr local finds the server a worktree is running, why a server in another worktree is never reported, the refusal reasons it names, and how a close workflow removes the link with --remove
|
|
40
40
|
- [The pull request preview deploy](pr-preview.md): How canon pr preview finds a fenced Cloudflare Pages deploy, dispatches it on a pull request's branch, reads the alias the run prints, the refusal reasons it names, and why an unfenced deploy is refused before anything runs
|
|
41
41
|
- [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
|
|
@@ -64,4 +64,4 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
64
64
|
- [Teach authoring](teach-authoring.md): Resolving what the next lesson needs before it is written, rendering a lesson body's blocks to HTML, and rewriting the root listing, a contents page, and each lesson's chrome and quiz stepper from its marker regions
|
|
65
65
|
- [Teach](teach.md): Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, viewing one with its chrome refreshed, where the lesson authoring verbs are described, the refusal reasons, and why every write here runs through a verb
|
|
66
66
|
- [Test order](test-order.md): Reading where an implementation reached history ahead of its test, how a pair is decided, the three verdicts, the coverage the pairing cannot reach, and why the check reports rather than gates
|
|
67
|
-
- [Worktrees](worktrees.md): Reporting which worktrees are reclaimable, removing the ones that are, the record a hook reads back, why the reading keys on the pull request rather than on git ancestry, the refusals it names, and the two removal shapes
|
|
67
|
+
- [Worktrees](worktrees.md): Reporting which worktrees are reclaimable, removing the ones that are, the hidden verb that clears a stranded bare flag, the record a hook reads back, why the reading keys on the pull request rather than on git ancestry, the refusals it names, and the two removal shapes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: The pull request evidence comparison
|
|
3
|
-
description: What canon pr evidence compares, the marker that lets it edit its own comment rather than duplicate it, the refusal reasons it names, and why the comparison anchors at the merge base rather than the previous push
|
|
3
|
+
description: What canon pr evidence compares, the marker that lets it edit its own comment rather than duplicate it, the refusal reasons it names, what the --check read reports as owed, and why the comparison anchors at the merge base rather than the previous push
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# The pull request evidence comparison
|
|
@@ -58,6 +58,7 @@ image or marks the case new.
|
|
|
58
58
|
| `no-base` | GitHub reported no merge base for the pull request. |
|
|
59
59
|
| `unreadable-changes` | GitHub could not list what the pull request changed. |
|
|
60
60
|
| `would-empty` | The render holds no cases and the marked comment holds some. No body. |
|
|
61
|
+
| `check-writes` | `--check` was passed with `--preview`, `--local`, or `--checklist`. |
|
|
61
62
|
|
|
62
63
|
`no-evidence` is not a refusal a caller reports. A project on a stack that
|
|
63
64
|
carries no evidence path, such as `base` or `python`, hits this reason on
|
|
@@ -179,6 +180,52 @@ the checklist land in one comment rather than the checklist going out alone.
|
|
|
179
180
|
`canon docs pr-local` covers where the address comes from and how the line is
|
|
180
181
|
removed at close.
|
|
181
182
|
|
|
183
|
+
## What a pull request is still owed
|
|
184
|
+
|
|
185
|
+
`--check` reads the same changed set and the same thread, renders no body, and
|
|
186
|
+
reports which steps of the ship chain that run after the pull request opens
|
|
187
|
+
never landed on it. It reads the pull request rather than any session's account
|
|
188
|
+
of what it ran, so a session that stopped after opening the pull request leaves
|
|
189
|
+
a branch the check can tell apart from one that needed neither step.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
canon pr evidence 1341 --check --json
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| Reason | What it means |
|
|
196
|
+
| --------- | ------------------------------------------------------------------ |
|
|
197
|
+
| `settled` | Nothing is owed. `owed` is empty. |
|
|
198
|
+
| `owed` | `owed` lists `evidence`, `preview`, or both, always in that order. |
|
|
199
|
+
|
|
200
|
+
- `evidence` is owed when the changed set carries an evidence image and no
|
|
201
|
+
comment carries the marker.
|
|
202
|
+
- `preview` is owed when the pull request has a marked comment or an evidence
|
|
203
|
+
change, a deploy workflow resolves in the checkout the verb runs from, and
|
|
204
|
+
the marked comment's first line is no `**Preview:**` line. A
|
|
205
|
+
`**Local preview:**` line does not satisfy it. A project deploying nothing is
|
|
206
|
+
never owed a preview.
|
|
207
|
+
|
|
208
|
+
The record carries `owed`, `commentId` when a marked comment exists, and the
|
|
209
|
+
same `preview`, `local`, and `checklist` fields the other reasons carry, so a
|
|
210
|
+
caller reads one shape. It never carries `body`, so a writer branching on `ok`
|
|
211
|
+
and posting `body` cannot act on a check record. Passing `--preview`, `--local`,
|
|
212
|
+
or `--checklist` with it refuses as `check-writes` before any `gh` call, since
|
|
213
|
+
those ask a read to write.
|
|
214
|
+
|
|
215
|
+
The thread cannot separate a skipped preview step from a failed one. A pull
|
|
216
|
+
request whose deploy run failed or timed out reads as owing a preview, and the
|
|
217
|
+
session that ran it holds the cause. A checklist posted on its own carries no
|
|
218
|
+
marker, so a branch with a checklist and no evidence image never reads as
|
|
219
|
+
owing a preview, though `git-pr` runs the preview step for it. The deploy
|
|
220
|
+
workflow is read from the checkout rather than the pull request's head, so a
|
|
221
|
+
pull request that adds or removes its own deploy workflow reads against trunk's.
|
|
222
|
+
|
|
223
|
+
An unreadable thread refuses as `gh-failed` rather than reporting `settled` off
|
|
224
|
+
a thread it never read. The check skips the merge base read, which only the
|
|
225
|
+
render needs, so it never refuses `no-base`. The orchestrator's draft lift is
|
|
226
|
+
the caller, and lifts only on `settled`, so a refusal holds it the same as
|
|
227
|
+
`owed` does.
|
|
228
|
+
|
|
182
229
|
## What a collapsed comment still leaves to GitHub
|
|
183
230
|
|
|
184
231
|
GitHub already draws its own before-and-after comparison on the Files Changed
|
package/docs/agents/worktrees.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Worktrees
|
|
3
|
-
description: Reporting which worktrees are reclaimable, removing the ones that are, the record a hook reads back, why the reading keys on the pull request rather than on git ancestry, the refusals it names, and the two removal shapes
|
|
3
|
+
description: Reporting which worktrees are reclaimable, removing the ones that are, the hidden verb that clears a stranded bare flag, the record a hook reads back, why the reading keys on the pull request rather than on git ancestry, the refusals it names, and the two removal shapes
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Worktrees
|
|
@@ -24,6 +24,21 @@ Exit codes: `0` every worktree was read, `1` refused. The refusal carries a `rea
|
|
|
24
24
|
|
|
25
25
|
An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `reason` rather than the exit when a skill consumes this.
|
|
26
26
|
|
|
27
|
+
## Repair the bare flag
|
|
28
|
+
|
|
29
|
+
`canon worktrees repair-bare-flag` clears a `core.bare` flag that Claude Code's worktree entry left set in the shared config. It is hidden, so no help listing shows it, because its one caller is a hook rather than a person.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
canon worktrees repair-bare-flag --root <path> --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Behavior |
|
|
36
|
+
| --------------- | ------------------------------------- |
|
|
37
|
+
| `--root <path>` | Repository to repair, default the cwd |
|
|
38
|
+
| `--json` | Print the record on stdout |
|
|
39
|
+
|
|
40
|
+
`--json` writes `{repaired, reason, message}` on stdout, once, whether or not anything was written. `reason` is `flag-unset`, `common-dir-unreadable`, `genuinely-bare`, or `write-failed` when `repaired` is false. `message` carries text when the flag is repaired and when the write failed, and is null on the three quiet skips. A `write-failed` record means the flag is still set, which a lock file or a read-only config causes. A repository whose common dir is not named `.git` keeps its flag. The exit code is always `0`, so a hook never fails a tool call. Without `--json`, a record carrying a message prints it on stderr and any other prints nothing.
|
|
41
|
+
|
|
27
42
|
## Reclaim
|
|
28
43
|
|
|
29
44
|
`canon worktrees reclaim` removes every worktree the reading above called reclaimable, and the branch behind each one.
|
|
@@ -8,7 +8,7 @@ category: Workflow
|
|
|
8
8
|
|
|
9
9
|
Groups run in the order a project meets them, so a reader at a known point scans to that group and reads across. The set reconciles the scenarios in [AI workflow](ai-workflow.md) with the lifecycle [target projects](../target/projects.md) describes, rather than inventing a third vocabulary beside those two, so a group name matches neither source exactly and every moment either one names has a group. Each row says when to reach for the skill. What it does is the skill's own description.
|
|
10
10
|
|
|
11
|
-
This page is the corpus the coverage claim is measured against: every name `canon claude skills list --names` reports takes exactly one row here. A skill serving two moments sits at the earlier one, and a mention on any other page is prose rather than routing.
|
|
11
|
+
This page is the corpus the coverage claim is measured against: every name `canon claude skills list --names` reports takes exactly one row here. A skill serving two moments sits at the earlier one, and a mention on any other page is prose rather than routing. The landing page's skills field renders from this page, so its build refuses a skill with no row and a row naming no skill.
|
|
12
12
|
|
|
13
13
|
## Set up a project
|
|
14
14
|
|
package/package.json
CHANGED
package/src/commands/gate.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
commandRunner,
|
|
6
6
|
exitCodeFor,
|
|
7
7
|
type GateContext,
|
|
8
|
-
repairBareFlag,
|
|
9
8
|
type StageResult,
|
|
10
9
|
type Summary,
|
|
11
10
|
runStages,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
pipeOutput,
|
|
25
24
|
plural,
|
|
26
25
|
} from '@/ui'
|
|
26
|
+
import { repairBareFlag } from '@/worktrees/bare-flag'
|
|
27
27
|
|
|
28
28
|
interface RunCommandOptions {
|
|
29
29
|
readonly all?: boolean
|
|
@@ -97,7 +97,8 @@ async function runGate(opts: RunCommandOptions): Promise<number> {
|
|
|
97
97
|
|
|
98
98
|
if (!emitJson && !nested) intro('canon gate run')
|
|
99
99
|
|
|
100
|
-
await repairBareFlag(root)
|
|
100
|
+
const repair = await repairBareFlag(root)
|
|
101
|
+
if (repair.message !== null) logWarn(repair.message)
|
|
101
102
|
|
|
102
103
|
const changed = opts.all
|
|
103
104
|
? { scoped: false, files: [] }
|
package/src/commands/pr.ts
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
findEvidenceLocal,
|
|
33
33
|
findEvidencePreview,
|
|
34
34
|
groupEvidence,
|
|
35
|
+
hasMarkedEvidenceComment,
|
|
36
|
+
readOwed,
|
|
35
37
|
renderEvidenceBody,
|
|
36
38
|
} from '@/pr/evidence'
|
|
37
39
|
import { type HeadRefusal, resolveHead, resolveTip } from '@/pr/head'
|
|
@@ -88,6 +90,7 @@ interface EvidenceOptions extends ReadOptions {
|
|
|
88
90
|
readonly preview?: string
|
|
89
91
|
readonly checklist?: string
|
|
90
92
|
readonly local?: string
|
|
93
|
+
readonly check?: boolean
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
interface LocalOptions extends ReadOptions {
|
|
@@ -172,6 +175,7 @@ const PULL_REFUSALS: Record<PullRefusal | HeadRefusal, string> = {
|
|
|
172
175
|
* one `identity.head` owns (`no-object-head`).
|
|
173
176
|
*/
|
|
174
177
|
type EvidenceRefusal =
|
|
178
|
+
| 'check-writes'
|
|
175
179
|
| 'gh-failed'
|
|
176
180
|
| 'no-base'
|
|
177
181
|
| 'unreadable-changes'
|
|
@@ -179,6 +183,8 @@ type EvidenceRefusal =
|
|
|
179
183
|
| 'would-empty'
|
|
180
184
|
|
|
181
185
|
const EVIDENCE_REFUSALS: Record<EvidenceRefusal, string> = {
|
|
186
|
+
'check-writes':
|
|
187
|
+
'--check is a read and renders no body, so it takes none of --preview, --local, or --checklist. Run the check and the render as two calls.',
|
|
182
188
|
'gh-failed':
|
|
183
189
|
'gh could not answer for this repository. Name the pull request number.',
|
|
184
190
|
'unreadable-checklist':
|
|
@@ -425,6 +431,10 @@ export function register(program: Command): void {
|
|
|
425
431
|
'--local <url>',
|
|
426
432
|
"Add this worktree's running server under any preview line, from canon pr local",
|
|
427
433
|
)
|
|
434
|
+
.option(
|
|
435
|
+
'--check',
|
|
436
|
+
'Report which post-pull-request steps the thread is still owed, rendering nothing',
|
|
437
|
+
)
|
|
428
438
|
.addHelpText(
|
|
429
439
|
'after',
|
|
430
440
|
[
|
|
@@ -478,8 +488,19 @@ export function register(program: Command): void {
|
|
|
478
488
|
'record reports none of the three for it. An unread thread refuses as',
|
|
479
489
|
'gh-failed on both reasons rather than reporting the fields absent.',
|
|
480
490
|
'',
|
|
491
|
+
'--check reads the same changed set and thread, renders no body, and',
|
|
492
|
+
'reports what the ship chain still owes the pull request:',
|
|
493
|
+
' settled nothing is owed',
|
|
494
|
+
' owed `owed` lists evidence, preview, or both, in that order',
|
|
495
|
+
'evidence is owed when an evidence image changed and no comment carries',
|
|
496
|
+
'the marker. preview is owed when either holds, a deploy workflow in',
|
|
497
|
+
"this checkout resolves, and the marked comment's first line is no",
|
|
498
|
+
'**Preview:** line. A failed deploy reads the same as a skipped one.',
|
|
499
|
+
'The record carries `owed` and the same marked fields. --check refuses',
|
|
500
|
+
'with --preview, --local, or --checklist as check-writes.',
|
|
501
|
+
'',
|
|
481
502
|
'Exit codes:',
|
|
482
|
-
' 0 read, whether it produced a body
|
|
503
|
+
' 0 read, whether it produced a body, reported no-evidence, or checked',
|
|
483
504
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
484
505
|
'',
|
|
485
506
|
'Examples:',
|
|
@@ -488,6 +509,7 @@ export function register(program: Command): void {
|
|
|
488
509
|
' canon pr evidence 1341 --preview https://feat-x.site.pages.dev --json',
|
|
489
510
|
' canon pr evidence 1341 --checklist .canon/tmp/handoff/ui-checklist/x.md --json',
|
|
490
511
|
' canon pr evidence 1341 --local http://localhost:5173 --json',
|
|
512
|
+
' canon pr evidence 1341 --check --json',
|
|
491
513
|
'',
|
|
492
514
|
].join('\n'),
|
|
493
515
|
)
|
|
@@ -1417,9 +1439,24 @@ async function runEvidence(
|
|
|
1417
1439
|
): Promise<number> {
|
|
1418
1440
|
const root = resolve(opts.root ?? process.cwd())
|
|
1419
1441
|
const emitJson = opts.json ?? false
|
|
1442
|
+
const isCheck = opts.check ?? false
|
|
1420
1443
|
|
|
1421
1444
|
intro('canon pr evidence')
|
|
1422
1445
|
|
|
1446
|
+
if (
|
|
1447
|
+
isCheck &&
|
|
1448
|
+
(opts.preview !== undefined ||
|
|
1449
|
+
opts.local !== undefined ||
|
|
1450
|
+
opts.checklist !== undefined)
|
|
1451
|
+
) {
|
|
1452
|
+
return refuseWith(
|
|
1453
|
+
'check-writes',
|
|
1454
|
+
EVIDENCE_REFUSALS['check-writes'],
|
|
1455
|
+
emitJson,
|
|
1456
|
+
root,
|
|
1457
|
+
)
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1423
1460
|
// Read ahead of anything else, so a bad path refuses before a `gh` round
|
|
1424
1461
|
// trip rather than after one.
|
|
1425
1462
|
let suppliedChecklist: string | undefined
|
|
@@ -1461,9 +1498,13 @@ async function runEvidence(
|
|
|
1461
1498
|
)
|
|
1462
1499
|
}
|
|
1463
1500
|
|
|
1501
|
+
// The check never reads the merge base, so it skips the compare call and
|
|
1502
|
+
// a failed one cannot hold a draft lift for a reason the check never had.
|
|
1464
1503
|
const [pullFiles, base] = await Promise.all([
|
|
1465
1504
|
listPullFiles(root, identity.number),
|
|
1466
|
-
|
|
1505
|
+
isCheck
|
|
1506
|
+
? undefined
|
|
1507
|
+
: readPullMergeBase(root, identity.number, identity.head),
|
|
1467
1508
|
])
|
|
1468
1509
|
if (pullFiles === undefined) {
|
|
1469
1510
|
return refuseWith(
|
|
@@ -1473,9 +1514,6 @@ async function runEvidence(
|
|
|
1473
1514
|
root,
|
|
1474
1515
|
)
|
|
1475
1516
|
}
|
|
1476
|
-
if (base === undefined) {
|
|
1477
|
-
return refuseWith('no-base', EVIDENCE_REFUSALS['no-base'], emitJson, root)
|
|
1478
|
-
}
|
|
1479
1517
|
|
|
1480
1518
|
const grouped = await groupEvidence(
|
|
1481
1519
|
[...pullFiles.keys()],
|
|
@@ -1533,6 +1571,40 @@ async function runEvidence(
|
|
|
1533
1571
|
...(carriedChecklist !== undefined && { checklist: carriedChecklist }),
|
|
1534
1572
|
}
|
|
1535
1573
|
|
|
1574
|
+
if (isCheck) {
|
|
1575
|
+
const owed = readOwed({
|
|
1576
|
+
hasEvidenceChange: grouped.kind === 'read',
|
|
1577
|
+
hasMarkedComment: hasMarkedEvidenceComment(comments),
|
|
1578
|
+
carriedPreview: carriedPreview !== undefined,
|
|
1579
|
+
deployWorkflowFound:
|
|
1580
|
+
findDeployWorkflow(await readWorkflows(root)).kind === 'found',
|
|
1581
|
+
})
|
|
1582
|
+
logStep(owed.length === 0 ? 'Settled' : 'Owed')
|
|
1583
|
+
logInfo(
|
|
1584
|
+
owed.length === 0
|
|
1585
|
+
? 'Nothing the ship chain posts after the pull request is missing.'
|
|
1586
|
+
: `missing ${owed.join(', ')}`,
|
|
1587
|
+
)
|
|
1588
|
+
outro()
|
|
1589
|
+
if (emitJson) {
|
|
1590
|
+
process.stdout.write(
|
|
1591
|
+
`${JSON.stringify({
|
|
1592
|
+
root,
|
|
1593
|
+
number: identity.number,
|
|
1594
|
+
reason: owed.length === 0 ? 'settled' : 'owed',
|
|
1595
|
+
owed,
|
|
1596
|
+
...(commentId !== undefined && { commentId }),
|
|
1597
|
+
...carried,
|
|
1598
|
+
})}\n`,
|
|
1599
|
+
)
|
|
1600
|
+
}
|
|
1601
|
+
return 0
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (base === undefined) {
|
|
1605
|
+
return refuseWith('no-base', EVIDENCE_REFUSALS['no-base'], emitJson, root)
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1536
1608
|
// A local address rides on a checklist or a hosted preview and never opens
|
|
1537
1609
|
// a comment alone, so a docs-only branch with a server up posts nothing.
|
|
1538
1610
|
const hasChecklistAndLocal =
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type WorktreeVerdict,
|
|
18
18
|
} from '@/worktrees/reclaim'
|
|
19
19
|
import { mainWorktreeRoot } from '@/worktree'
|
|
20
|
+
import { repairBareFlag } from '@/worktrees/bare-flag'
|
|
20
21
|
import {
|
|
21
22
|
removeReclaimable,
|
|
22
23
|
type RemovalOutcome,
|
|
@@ -27,6 +28,11 @@ interface ListCommandOptions {
|
|
|
27
28
|
readonly json?: boolean
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
interface RepairBareFlagOptions {
|
|
32
|
+
readonly root?: string
|
|
33
|
+
readonly json?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
interface ReclaimCommandOptions {
|
|
31
37
|
readonly dryRun?: boolean
|
|
32
38
|
readonly json?: boolean
|
|
@@ -208,6 +214,37 @@ export function register(program: Command): void {
|
|
|
208
214
|
.action(async (opts: ReclaimCommandOptions) => {
|
|
209
215
|
process.exitCode = await runReclaim(opts)
|
|
210
216
|
})
|
|
217
|
+
|
|
218
|
+
worktrees
|
|
219
|
+
.command('repair-bare-flag', { hidden: true })
|
|
220
|
+
.description('Clear a core.bare flag that worktree entry left set')
|
|
221
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
222
|
+
.option('--root <path>', 'Repository to repair', '.')
|
|
223
|
+
.option('--json', 'Print the record on stdout')
|
|
224
|
+
.addHelpText(
|
|
225
|
+
'after',
|
|
226
|
+
[
|
|
227
|
+
'',
|
|
228
|
+
'Exit codes:',
|
|
229
|
+
' 0 always, since the caller is a hook that must never fail a tool call',
|
|
230
|
+
'',
|
|
231
|
+
'Called by .claude/hooks/bare-flag-repair.sh, the one caller that is not',
|
|
232
|
+
'a person, so no help listing shows it. A repository whose common dir is',
|
|
233
|
+
'not named .git keeps its flag, since that is a genuinely bare one.',
|
|
234
|
+
'',
|
|
235
|
+
'Examples:',
|
|
236
|
+
' canon worktrees repair-bare-flag --root . --json',
|
|
237
|
+
'',
|
|
238
|
+
].join('\n'),
|
|
239
|
+
)
|
|
240
|
+
.action(async (opts: RepairBareFlagOptions) => {
|
|
241
|
+
const record = await repairBareFlag(opts.root ?? '.')
|
|
242
|
+
if (opts.json === true) {
|
|
243
|
+
process.stdout.write(`${JSON.stringify(record)}\n`)
|
|
244
|
+
} else if (record.message !== null) {
|
|
245
|
+
logWarn(record.message)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
211
248
|
}
|
|
212
249
|
|
|
213
250
|
/**
|
package/src/gate/sequencer.ts
CHANGED
|
@@ -377,23 +377,3 @@ async function spawn(
|
|
|
377
377
|
spawnError: code === 'ENOENT' ? `${file} is not on PATH` : undefined,
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Repairs `core.bare`, which Claude Code's worktree entry leaves set in the
|
|
383
|
-
* shared config and nothing restores.
|
|
384
|
-
*
|
|
385
|
-
* It runs ahead of every stage rather than as one of them, because the flag
|
|
386
|
-
* breaks the git reads that scope the run. The rule itself stays in
|
|
387
|
-
* `scripts/lib/worktree.sh`, which is the one bash function under test, so this
|
|
388
|
-
* calls it rather than restating the guard that spares a genuinely bare
|
|
389
|
-
* repository.
|
|
390
|
-
*/
|
|
391
|
-
export async function repairBareFlag(root: string): Promise<void> {
|
|
392
|
-
await execa('bash', [join(root, 'scripts/core/repair-bare-flag.sh')], {
|
|
393
|
-
cwd: root,
|
|
394
|
-
reject: false,
|
|
395
|
-
stdio: 'inherit',
|
|
396
|
-
env: { ...gitEnv(), PROJECT_ROOT: root },
|
|
397
|
-
extendEnv: false,
|
|
398
|
-
})
|
|
399
|
-
}
|
package/src/gate/stages.ts
CHANGED
package/src/pr/evidence.ts
CHANGED
|
@@ -215,6 +215,41 @@ function hasEvidenceMarker(body: string): boolean {
|
|
|
215
215
|
return (lines[index] ?? '').trim().startsWith(MARKER_PREFIX)
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/** Whether any comment carries the evidence marker, url or not. */
|
|
219
|
+
export function hasMarkedEvidenceComment(
|
|
220
|
+
comments: readonly EvidenceComment[],
|
|
221
|
+
): boolean {
|
|
222
|
+
return comments.some((comment) => hasEvidenceMarker(comment.body))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A post-pull-request step of the ship chain that never landed on the thread. */
|
|
226
|
+
export type OwedStep = 'evidence' | 'preview'
|
|
227
|
+
|
|
228
|
+
export interface OwedInput {
|
|
229
|
+
readonly hasEvidenceChange: boolean
|
|
230
|
+
readonly hasMarkedComment: boolean
|
|
231
|
+
readonly carriedPreview: boolean
|
|
232
|
+
readonly deployWorkflowFound: boolean
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The steps a pull request is still owed, read off the pull request rather
|
|
237
|
+
* than off any session's account of what it ran. The thread cannot tell a
|
|
238
|
+
* skipped preview from one whose deploy failed, so both read as owed.
|
|
239
|
+
*/
|
|
240
|
+
export function readOwed(input: OwedInput): OwedStep[] {
|
|
241
|
+
const owed: OwedStep[] = []
|
|
242
|
+
if (input.hasEvidenceChange && !input.hasMarkedComment) owed.push('evidence')
|
|
243
|
+
if (
|
|
244
|
+
(input.hasMarkedComment || input.hasEvidenceChange) &&
|
|
245
|
+
input.deployWorkflowFound &&
|
|
246
|
+
!input.carriedPreview
|
|
247
|
+
) {
|
|
248
|
+
owed.push('preview')
|
|
249
|
+
}
|
|
250
|
+
return owed
|
|
251
|
+
}
|
|
252
|
+
|
|
218
253
|
/**
|
|
219
254
|
* The REST id of the comment this module already posted, read off its `url`
|
|
220
255
|
* field, since `gh pr view --json comments` reports only a GraphQL id there
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import { gitEnv } from '@/git-env'
|
|
4
|
+
|
|
5
|
+
/** Why a run wrote nothing. The three are different states a caller can tell apart. */
|
|
6
|
+
export type BareFlagSkip =
|
|
7
|
+
| 'flag-unset'
|
|
8
|
+
| 'common-dir-unreadable'
|
|
9
|
+
| 'genuinely-bare'
|
|
10
|
+
| 'write-failed'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What the repair did, or the reason it left the configuration alone.
|
|
14
|
+
*
|
|
15
|
+
* `message` is non-null whenever the flag is still set and a caller should say
|
|
16
|
+
* so, which is a repair and a failed write. The three quiet skips carry none.
|
|
17
|
+
*/
|
|
18
|
+
export type BareFlagRecord =
|
|
19
|
+
| { readonly repaired: true; readonly reason: null; readonly message: string }
|
|
20
|
+
| {
|
|
21
|
+
readonly repaired: false
|
|
22
|
+
readonly reason: BareFlagSkip
|
|
23
|
+
readonly message: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const REPAIRED_MESSAGE =
|
|
27
|
+
"Repaired core.bare, which worktree entry left set. Recovery is 'git config core.bare false'."
|
|
28
|
+
|
|
29
|
+
export const WRITE_FAILED_MESSAGE =
|
|
30
|
+
"core.bare is set and the repair could not write the config, which a lock file or a read-only config causes. Recovery is 'git config core.bare false'."
|
|
31
|
+
|
|
32
|
+
const skipped = (reason: BareFlagSkip): BareFlagRecord => ({
|
|
33
|
+
repaired: false,
|
|
34
|
+
reason,
|
|
35
|
+
message: null,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Repairs `core.bare`, which Claude Code's worktree entry leaves set in the
|
|
40
|
+
* shared config and nothing restores.
|
|
41
|
+
*
|
|
42
|
+
* The flag strands the main worktree and breaks the git reads that scope a
|
|
43
|
+
* verification run, so the gate runs this ahead of every stage. Every git call
|
|
44
|
+
* drops the repository-resolution variables a git hook exports, so the root
|
|
45
|
+
* the caller names is the repository that gets read and written.
|
|
46
|
+
*
|
|
47
|
+
* A genuinely bare repository keeps its objects at the root and has no `.git`
|
|
48
|
+
* directory, so its common dir fails the basename test and keeps its flag. That
|
|
49
|
+
* test is the one case where an unguarded repair does damage.
|
|
50
|
+
*/
|
|
51
|
+
export async function repairBareFlag(root: string): Promise<BareFlagRecord> {
|
|
52
|
+
const git = (...args: string[]) =>
|
|
53
|
+
execa('git', ['-C', root, ...args], {
|
|
54
|
+
env: gitEnv(),
|
|
55
|
+
extendEnv: false,
|
|
56
|
+
reject: false,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const flag = await git('config', '--get', 'core.bare')
|
|
60
|
+
if (flag.exitCode !== 0 || flag.stdout.trim() !== 'true') {
|
|
61
|
+
return skipped('flag-unset')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const common = await git(
|
|
65
|
+
'rev-parse',
|
|
66
|
+
'--path-format=absolute',
|
|
67
|
+
'--git-common-dir',
|
|
68
|
+
)
|
|
69
|
+
if (common.exitCode !== 0) return skipped('common-dir-unreadable')
|
|
70
|
+
if (basename(common.stdout.trim()) !== '.git')
|
|
71
|
+
return skipped('genuinely-bare')
|
|
72
|
+
|
|
73
|
+
const write = await git('config', 'core.bare', 'false')
|
|
74
|
+
if (write.exitCode !== 0) {
|
|
75
|
+
return {
|
|
76
|
+
repaired: false,
|
|
77
|
+
reason: 'write-failed',
|
|
78
|
+
message: WRITE_FAILED_MESSAGE,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { repaired: true, reason: null, message: REPAIRED_MESSAGE }
|
|
83
|
+
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Repairs `core.bare`, which Claude Code's worktree entry leaves set in the
|
|
3
|
-
# shared config with nothing restoring it. The flag strands the main worktree
|
|
4
|
-
# and breaks the git reads that scope a verification run, so this runs ahead of
|
|
5
|
-
# every stage rather than as one of them.
|
|
6
|
-
#
|
|
7
|
-
# The rule itself lives in `scripts/lib/worktree.sh`, the one bash function in
|
|
8
|
-
# this repository under test, so this file is the invocation and never a second
|
|
9
|
-
# copy of the guard that spares a genuinely bare repository.
|
|
10
|
-
set -e
|
|
11
|
-
set -o pipefail
|
|
12
|
-
|
|
13
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
15
|
-
|
|
16
|
-
source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
17
|
-
source "$PROJECT_ROOT/scripts/lib/worktree.sh"
|
|
18
|
-
|
|
19
|
-
repair_bare_flag "$PROJECT_ROOT"
|
package/scripts/lib/worktree.sh
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
# Claude Code's worktree entry writes core.bare into the parent repository's
|
|
4
|
-
# shared config and its exit never restores it, stranding every later command in
|
|
5
|
-
# the main worktree. Sourced by verify.sh, which runs the repair before any stage
|
|
6
|
-
# because the flag breaks the git reads that scope the run.
|
|
7
|
-
|
|
8
|
-
repair_bare_flag() {
|
|
9
|
-
local root="${1:-$PROJECT_ROOT}"
|
|
10
|
-
local common_dir
|
|
11
|
-
[ "$(git -C "$root" config --get core.bare 2>/dev/null || echo false)" = true ] || return 0
|
|
12
|
-
|
|
13
|
-
common_dir=$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 0
|
|
14
|
-
# A genuinely bare repository keeps its objects at the root and has no .git
|
|
15
|
-
# directory, so it fails this test and keeps its flag.
|
|
16
|
-
[ "$(basename "$common_dir")" = ".git" ] || return 0
|
|
17
|
-
|
|
18
|
-
git -C "$root" config core.bare false
|
|
19
|
-
log_warn "Repaired core.bare, which worktree entry left set. Recovery is 'git config core.bare false'."
|
|
20
|
-
}
|