@erclx/canon 5.41.0 → 5.43.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/api-design/references/adopted.md +34 -14
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
- package/claude/skills/auto-ship/SKILL.md +24 -4
- package/claude/skills/auto-ship/references/draft-mark.md +32 -6
- package/claude/skills/auto-ship/references/failure-recovery.md +15 -13
- package/claude/skills/auto-ship/references/tier-failures.md +31 -10
- package/claude/skills/auto-ship/references/ui-checklist.md +9 -4
- package/claude/skills/auto-ship/references/worktree-name.md +9 -3
- package/claude/skills/build-in-slices/references/adopted.md +35 -21
- package/claude/skills/canvas-board/references/canvas.md +17 -6
- package/claude/skills/canvas-board/references/design.md +26 -7
- package/claude/skills/ci-workflow/REQUIREMENT.md +2 -1
- package/claude/skills/ci-workflow/SKILL.md +12 -2
- package/claude/skills/ci-workflow/references/workflows.md +12 -9
- package/claude/skills/code-craft/references/adopted.md +58 -20
- package/claude/skills/codebase-layout/references/adopted.md +49 -16
- package/claude/skills/codebase-layout/references/frontend.md +11 -4
- package/claude/skills/codebase-layout/references/tests.md +4 -2
- package/claude/skills/context-fold/references/classify.md +10 -15
- package/claude/skills/context-fold/references/completion-report.md +11 -9
- package/claude/skills/context-fold/references/context-refresh.md +26 -9
- package/claude/skills/context-fold/references/unusable-baseline.md +5 -3
- package/claude/skills/create-rule/REQUIREMENT.md +5 -0
- package/claude/skills/create-rule/SKILL.md +10 -1
- package/claude/skills/create-rule/references/checklist.md +26 -0
- package/claude/skills/create-rule/references/rule.md +98 -20
- package/claude/skills/create-skill/SKILL.md +1 -0
- package/claude/skills/create-skill/references/skill-disclosure.md +10 -2
- package/claude/skills/create-skill/references/skill-frontmatter.md +43 -0
- package/claude/skills/create-skill/references/skill-paths.md +7 -4
- package/claude/skills/create-skill/references/skill-practice.md +6 -3
- package/claude/skills/create-skill/references/skill-requirement.md +4 -2
- package/claude/skills/create-skill/references/skill.md +67 -44
- package/claude/skills/decision-escalate/SKILL.md +1 -2
- package/claude/skills/deploy-app/references/vercel.md +2 -1
- package/claude/skills/deprecation-migration/references/adopted.md +44 -25
- package/claude/skills/design-taste/references/adopted.md +9 -1
- package/claude/skills/design-taste/references/craft.md +3 -1
- package/claude/skills/design-taste/references/kinds.md +8 -7
- package/claude/skills/design-taste/references/preflight.md +12 -0
- package/claude/skills/design-taste/references/systems.md +7 -6
- package/claude/skills/design-taste/references/tells.md +24 -25
- package/claude/skills/design-taste/references/vocabulary.md +7 -5
- package/claude/skills/document-health/references/axes.md +55 -30
- package/claude/skills/draft-and-pick/references/live-arms.md +2 -2
- package/claude/skills/draft-doc/references/context.md +4 -3
- package/claude/skills/draft-doc/references/docs.md +6 -3
- package/claude/skills/draft-doc/references/readme/agent-facing.md +6 -0
- package/claude/skills/draft-doc/references/readme/application.md +8 -2
- package/claude/skills/draft-doc/references/readme/cli.md +5 -1
- package/claude/skills/draft-doc/references/readme/library.md +4 -0
- package/claude/skills/draft-doc/references/readme/plugin.md +9 -1
- package/claude/skills/draft-doc/references/readme.md +30 -13
- package/claude/skills/draft-doc/references/requirements.md +17 -4
- package/claude/skills/draft-doc/references/wiki-kind.md +20 -10
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/references/architecture-set.md +7 -3
- package/claude/skills/draft-figure/references/figures.md +8 -6
- package/claude/skills/draft-figure/references/hand-drawn.md +3 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +19 -9
- package/claude/skills/draft-figure/references/mermaid.md +10 -5
- package/claude/skills/draft-ready/references/ready.md +22 -15
- package/claude/skills/git-branch/references/branch.md +11 -1
- package/claude/skills/git-branch/references/slug.md +17 -5
- package/claude/skills/git-commit/references/commit.md +9 -5
- package/claude/skills/git-commit/references/versioning.md +23 -17
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +5 -6
- package/claude/skills/git-issue/references/issue.md +29 -16
- package/claude/skills/git-pr/REQUIREMENT.md +1 -5
- package/claude/skills/git-pr/SKILL.md +11 -22
- package/claude/skills/git-pr/references/final-command.md +21 -8
- package/claude/skills/git-pr/references/labels.md +45 -28
- package/claude/skills/git-pr/references/pr.md +29 -9
- package/claude/skills/git-pr/references/publish.md +42 -28
- package/claude/skills/git-pr/references/task-number.md +20 -8
- package/claude/skills/git-ship/SKILL.md +1 -1
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/plan-feature/references/plan-lifecycle.md +10 -3
- package/claude/skills/plan-feature/references/plan.md +58 -21
- package/claude/skills/plan-groundwork/references/groundwork-spikes.md +4 -1
- package/claude/skills/plan-groundwork/references/groundwork.md +14 -16
- package/claude/skills/plan-intake/references/intake.md +29 -24
- package/claude/skills/pr-evidence/REQUIREMENT.md +40 -0
- package/claude/skills/pr-evidence/SKILL.md +53 -0
- package/claude/skills/pr-evidence/references/evidence.md +150 -0
- package/claude/skills/review-address/references/post-review-findings.md +10 -3
- package/claude/skills/review-address/references/rebase-conflicts.md +15 -7
- package/claude/skills/review-address/references/rest-reads.md +3 -1
- package/claude/skills/review-craft/references/adopted.md +35 -18
- package/claude/skills/review-craft/references/security.md +42 -7
- package/claude/skills/review-craft/references/ui.md +23 -8
- package/claude/skills/review-pr/SKILL.md +6 -6
- package/claude/skills/review-pr/references/body-variants.md +29 -9
- package/claude/skills/review-pr/references/close-out.md +20 -5
- package/claude/skills/review-pr/references/key-changes.md +27 -3
- package/claude/skills/review-pr/references/post.md +83 -31
- package/claude/skills/review-pr/references/scope.md +54 -19
- package/claude/skills/review-ui/references/close-out.md +7 -5
- package/claude/skills/review-ui/references/driving.md +24 -4
- package/claude/skills/review-ui/references/post.md +43 -15
- package/claude/skills/role-orchestrator/SKILL.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +67 -17
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +80 -40
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +52 -15
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +48 -26
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +12 -13
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +72 -36
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +16 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +64 -24
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +80 -26
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +41 -18
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +19 -7
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +22 -12
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +15 -6
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +25 -7
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +34 -8
- package/claude/skills/role-orchestrator/scripts/poll.ts +16 -24
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +3 -3
- package/claude/skills/role-worker/REQUIREMENT.md +3 -0
- package/claude/skills/role-worker/SKILL.md +34 -2
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +2 -2
- package/claude/skills/search-craft/references/adopted.md +18 -6
- package/claude/skills/session-map/references/session.md +26 -7
- package/claude/skills/session-worktree/references/main-root-writes.md +25 -8
- package/claude/skills/systematic-debugging/references/adopted.md +30 -16
- package/claude/skills/target-check/references/domains.md +39 -11
- package/claude/skills/target-setup/references/detect.md +80 -65
- package/claude/skills/target-setup/references/indexes.md +8 -6
- package/claude/skills/task-board/references/board.md +86 -30
- package/claude/skills/task-board/references/decline.md +29 -13
- package/claude/skills/task-board/references/tasks-decline.md +12 -5
- package/claude/skills/task-board/references/tasks.md +65 -33
- package/claude/skills/task-complete/references/archive.md +41 -17
- package/claude/skills/task-complete/references/receipt-sweep.md +23 -9
- package/claude/skills/task-complete/references/unusable-baseline.md +10 -3
- package/claude/skills/teach-workspace/references/glossary.md +6 -2
- package/claude/skills/teach-workspace/references/lesson-build.md +44 -16
- package/claude/skills/teach-workspace/references/lesson-craft.md +12 -7
- package/claude/skills/teach-workspace/references/pedagogy.md +6 -3
- package/claude/skills/teach-workspace/references/promotion-fold.md +17 -11
- package/claude/skills/teach-workspace/references/promotion.md +31 -8
- package/claude/skills/teach-workspace/references/revisit.md +8 -2
- package/claude/skills/teach-workspace/references/teach.md +17 -9
- package/claude/skills/test-craft/references/adopted.md +48 -14
- package/claude/skills/test-craft/references/component.md +7 -4
- package/claude/skills/test-craft/references/e2e.md +25 -7
- package/claude/skills/test-craft/references/unit.md +3 -1
- package/claude/skills/test-first/references/adopted.md +12 -4
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/video-craft/references/adopted.md +62 -18
- package/claude/skills/write-human/references/adopted.md +21 -8
- package/docs/agents/audits.md +1 -0
- package/docs/agents/migrate.md +11 -8
- package/docs/target/migrations.md +11 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map.md +1 -0
- package/governance/detect.toml +4 -0
- package/governance/rules/canon/context.md +2 -1
- package/governance/rules/canon/plan.md +3 -5
- package/governance/rules/canon/scratch.md +3 -1
- package/governance/rules/canon/task-scope.md +2 -1
- package/governance/rules/canon/tasks.md +2 -1
- package/governance/rules/canon/worktrees.md +5 -2
- package/governance/rules/claude/output.md +7 -4
- package/governance/rules/claude/skill.md +6 -4
- package/governance/rules/tooling/ci-workflow.md +6 -39
- package/governance/rules/ui/design-reference.md +37 -0
- package/governance/rules/writing/markdown.md +2 -1
- package/package.json +6 -6
- package/src/audits/catalog.ts +42 -0
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/settings.ts +8 -4
- package/src/commands/claude.ts +11 -1
- package/src/commands/gov.ts +198 -0
- package/src/commands/migrate.ts +121 -2
- package/src/commands/pr.ts +146 -105
- package/src/commands/tooling.ts +3 -3
- package/src/commands/worktrees.ts +49 -0
- package/src/gate/stages.ts +15 -0
- package/src/gov/audit.ts +440 -0
- package/src/listeners/belongs.ts +21 -0
- package/src/listeners/machine.ts +93 -0
- package/src/listeners/parse.ts +41 -0
- package/src/markdown/scope.ts +144 -0
- package/src/migrate/session-defaults.ts +138 -0
- package/src/pr/lift.ts +6 -3
- package/src/pr/local.ts +4 -69
- package/src/pr/review-state.ts +182 -0
- package/src/sync/check.ts +15 -0
- package/src/tooling/stamp.ts +56 -12
- package/src/worktrees/stop.ts +92 -0
- package/tooling/base/configs/.github/workflows/phase-label-gate.yml +6 -2
- package/tooling/base/configs/commitlint.config.js +0 -20
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/workflows/verify.yml +4 -1
- package/tooling/claude/reference.md +13 -7
- package/tooling/claude/seeds/.claude/settings.json +4 -0
- package/tooling/claude/user/settings.template.json +0 -4
- package/tooling/cloudflare/configs/.github/workflows/deploy.yml +5 -5
- package/tooling/public/configs/.github/dependabot.yml +12 -0
- package/tooling/public/configs/.github/workflows/release-please.yml +30 -0
- package/tooling/public/manifest.toml +8 -0
- package/tooling/public/reference.md +53 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/bug.yml +48 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/feature.yml +16 -0
- package/tooling/public/seeds/.release-please-manifest.json +3 -0
- package/tooling/public/seeds/CONTRIBUTING.md +50 -0
- package/tooling/public/seeds/LICENSE +21 -0
- package/tooling/public/seeds/SECURITY.md +13 -0
- package/tooling/public/seeds/release-please-config.json +12 -0
- package/tooling/shell/configs/.github/workflows/shell.yml +4 -1
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +6 -3
- package/tooling/web/configs/.github/workflows/verify.yml +15 -12
- package/claude/skills/git-pr/references/evidence.md +0 -124
package/src/commands/pr.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import {
|
|
2
|
+
import { readFile, realpath } from 'node:fs/promises'
|
|
3
3
|
import { basename, join, resolve } from 'node:path'
|
|
4
4
|
import { $ } from 'bun'
|
|
5
5
|
import type { Command } from 'commander'
|
|
@@ -72,15 +72,13 @@ import {
|
|
|
72
72
|
readBaseStanding,
|
|
73
73
|
readChangedPaths,
|
|
74
74
|
} from '@/pr/lift'
|
|
75
|
+
import { machineListenerReader } from '@/listeners/machine'
|
|
75
76
|
import { loadPaintedPatterns } from '@/pr/painted'
|
|
76
77
|
import {
|
|
77
78
|
dropAddressLine,
|
|
78
79
|
findLocalServer,
|
|
79
|
-
type Listener,
|
|
80
80
|
type LocalRefusal,
|
|
81
81
|
type LocalRunner,
|
|
82
|
-
parseLsofListeners,
|
|
83
|
-
parseProcNetTcp,
|
|
84
82
|
} from '@/pr/local'
|
|
85
83
|
import { KEY_CHANGES } from '@/pr/paths'
|
|
86
84
|
import {
|
|
@@ -107,7 +105,12 @@ import {
|
|
|
107
105
|
reviewRowOf,
|
|
108
106
|
selectBranchPull,
|
|
109
107
|
} from '@/pr/rest'
|
|
110
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
readReviewState,
|
|
110
|
+
type ReviewStateApi,
|
|
111
|
+
type ReviewStateRecord,
|
|
112
|
+
waitForReviews,
|
|
113
|
+
} from '@/pr/review-state'
|
|
111
114
|
import { PROJECT_ROOT } from '@/roots/project'
|
|
112
115
|
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
|
|
113
116
|
|
|
@@ -140,6 +143,10 @@ interface LiftCommandOptions extends ReadOptions {
|
|
|
140
143
|
readonly wait?: string
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
interface ReviewStateOptions extends ReadOptions {
|
|
147
|
+
readonly wait?: string
|
|
148
|
+
}
|
|
149
|
+
|
|
143
150
|
interface LocalOptions extends ReadOptions {
|
|
144
151
|
readonly remove?: boolean
|
|
145
152
|
}
|
|
@@ -561,11 +568,17 @@ export function register(program: Command): void {
|
|
|
561
568
|
})
|
|
562
569
|
|
|
563
570
|
pr.command('review-state')
|
|
564
|
-
.description(
|
|
571
|
+
.description(
|
|
572
|
+
'Report what the last code review and UI review passes covered at the tip',
|
|
573
|
+
)
|
|
565
574
|
.argument('[number]', 'Pull request to read, defaulting to this branch')
|
|
566
575
|
.helpOption('-h, --help', 'Show this help message')
|
|
567
576
|
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
568
577
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
578
|
+
.option(
|
|
579
|
+
'--wait <seconds>',
|
|
580
|
+
`Hold until a pass covers the tip in every bound family, default ${DEFAULT_WAIT_SECONDS}`,
|
|
581
|
+
)
|
|
569
582
|
.addHelpText(
|
|
570
583
|
'after',
|
|
571
584
|
[
|
|
@@ -587,6 +600,31 @@ export function register(program: Command): void {
|
|
|
587
600
|
' fallback a pass posted before the marker shipped, off GitHub stamps',
|
|
588
601
|
' none the thread carries no pass, so the next one is a first pass',
|
|
589
602
|
'',
|
|
603
|
+
'Those code review fields stay at the top level of the record. Beside',
|
|
604
|
+
'them, `head` is the branch tip read from the remote, and `review` and',
|
|
605
|
+
'`ui` carry each family at that tip:',
|
|
606
|
+
' reason the refusal `canon pr lift` gives the family, absent when',
|
|
607
|
+
' its gate passes, read through the same gate function',
|
|
608
|
+
' isCovered a pass covers the tip: the gate passes, or the newest pass,',
|
|
609
|
+
' open or unmarked, names the tip',
|
|
610
|
+
'',
|
|
611
|
+
'`ui` also carries the newest `## UI review` pass (`heading`, `state`,',
|
|
612
|
+
'`commit`, `source`), `isBound`, which is whether the evidence comment',
|
|
613
|
+
'carries a checklist, and `carriedFrom` or `paintedPaths` when the lift',
|
|
614
|
+
'carried a close forward or refused to. An unbound family reads covered.',
|
|
615
|
+
'',
|
|
616
|
+
'Both families post as pull request reviews, so this reads',
|
|
617
|
+
'`pulls/<n>/reviews` alone, as the lift does. Issue comments are not',
|
|
618
|
+
'read: neither review skill posts there, and a heading found only in a',
|
|
619
|
+
'comment would let a worker move on a pass the lift then refuses.',
|
|
620
|
+
'',
|
|
621
|
+
'`--wait <seconds>` holds until `isCovered` reads true in both families',
|
|
622
|
+
'or the deadline passes, re-reading the tip every 30 seconds so a push',
|
|
623
|
+
'moves what a pass must cover. The evidence is read once at the start.',
|
|
624
|
+
'The record then adds `reason`, `covered` or `timeout`, and',
|
|
625
|
+
'`waitedSeconds`. A wait past 600 seconds outlasts a foreground shell',
|
|
626
|
+
'call, so run it in the background.',
|
|
627
|
+
'',
|
|
590
628
|
'Refusals, read off `reason`:',
|
|
591
629
|
' ambiguous-pull two open pull requests share the branch against',
|
|
592
630
|
' different bases, so name a number',
|
|
@@ -599,18 +637,22 @@ export function register(program: Command): void {
|
|
|
599
637
|
' gh-missing `gh` is not on the path, so no pull request resolved',
|
|
600
638
|
' gh-failed `gh` could not answer for this branch, so name a number',
|
|
601
639
|
' no-object-head the pull request object reported no head commit',
|
|
640
|
+
' bad-wait --wait takes a whole number of seconds',
|
|
602
641
|
'',
|
|
603
642
|
'Exit codes:',
|
|
604
|
-
' 0 the thread was read, whether it carries a pass or not',
|
|
605
|
-
'
|
|
643
|
+
' 0 the thread was read, whether it carries a pass or not, or the',
|
|
644
|
+
' wait ended covered',
|
|
645
|
+
' 1 refused, with the reason on stderr or in the JSON record, or the',
|
|
646
|
+
' wait reached its deadline as `timeout`',
|
|
606
647
|
'',
|
|
607
648
|
'Examples:',
|
|
608
649
|
' canon pr review-state',
|
|
609
650
|
' canon pr review-state 1341 --json',
|
|
651
|
+
' canon pr review-state 1341 --wait 1200 --json',
|
|
610
652
|
'',
|
|
611
653
|
].join('\n'),
|
|
612
654
|
)
|
|
613
|
-
.action(async (number: string | undefined, opts:
|
|
655
|
+
.action(async (number: string | undefined, opts: ReviewStateOptions) => {
|
|
614
656
|
process.exitCode = await runReviewState(number, opts)
|
|
615
657
|
})
|
|
616
658
|
|
|
@@ -1818,13 +1860,21 @@ async function runChecks(
|
|
|
1818
1860
|
|
|
1819
1861
|
async function runReviewState(
|
|
1820
1862
|
number: string | undefined,
|
|
1821
|
-
opts:
|
|
1863
|
+
opts: ReviewStateOptions,
|
|
1822
1864
|
): Promise<number> {
|
|
1823
1865
|
const root = resolve(opts.root ?? process.cwd())
|
|
1824
1866
|
const emitJson = opts.json ?? false
|
|
1825
1867
|
|
|
1826
1868
|
intro('canon pr review-state')
|
|
1827
1869
|
|
|
1870
|
+
let waitSeconds: number | undefined
|
|
1871
|
+
if (opts.wait !== undefined) {
|
|
1872
|
+
if (!/^\d+$/.test(opts.wait)) {
|
|
1873
|
+
return refuseWith('bad-wait', LIFT_REFUSALS['bad-wait'], emitJson, root)
|
|
1874
|
+
}
|
|
1875
|
+
waitSeconds = Number(opts.wait)
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1828
1878
|
if (Bun.which('gh') === null) {
|
|
1829
1879
|
return refuseWith('gh-missing', PULL_REFUSALS['gh-missing'], emitJson, root)
|
|
1830
1880
|
}
|
|
@@ -1841,20 +1891,84 @@ async function runReviewState(
|
|
|
1841
1891
|
)
|
|
1842
1892
|
}
|
|
1843
1893
|
|
|
1844
|
-
const
|
|
1845
|
-
if (
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1894
|
+
const read = await readIdentity(root, resolved.number)
|
|
1895
|
+
if (read.kind === 'refused') {
|
|
1896
|
+
return refuseWith(read.reason, PULL_REFUSALS[read.reason], emitJson, root)
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// The module refuses every failed read as `gh-failed`. The api keeps each
|
|
1900
|
+
// read's own reason, so a caller still tells an unreadable thread from an
|
|
1901
|
+
// unreadable remote.
|
|
1902
|
+
let failure: PullRefusal | HeadRefusal = 'gh-failed'
|
|
1903
|
+
const git = gitRunner(root)
|
|
1904
|
+
const api: ReviewStateApi = {
|
|
1905
|
+
readTip: async () => {
|
|
1906
|
+
const tip = await resolveTip(read.identity.branch, refReader(root))
|
|
1907
|
+
if (tip.kind === 'read') return tip.tip
|
|
1908
|
+
failure = tip.reason
|
|
1909
|
+
return undefined
|
|
1910
|
+
},
|
|
1911
|
+
readReviews: async () => {
|
|
1912
|
+
const reviews = await listReviews(root, resolved.number)
|
|
1913
|
+
if (reviews.kind === 'read') return reviews.rows.map(reviewRowOf)
|
|
1914
|
+
failure =
|
|
1915
|
+
reviews.reason === 'gh-failed' ? 'gh-failed' : 'reviews-unreadable'
|
|
1916
|
+
return undefined
|
|
1917
|
+
},
|
|
1918
|
+
readEvidence: () => readEvidenceGate(root, resolved.number),
|
|
1919
|
+
readChanged: (from, to) => readChangedPaths(git, from, to),
|
|
1920
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
1921
|
+
now: () => Date.now(),
|
|
1849
1922
|
}
|
|
1850
1923
|
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1853
|
-
|
|
1854
|
-
|
|
1924
|
+
const painted = loadPaintedPatterns(root)
|
|
1925
|
+
const wait =
|
|
1926
|
+
waitSeconds === undefined
|
|
1927
|
+
? undefined
|
|
1928
|
+
: await waitForReviews(api, { waitSeconds, painted })
|
|
1929
|
+
const state = wait ?? (await readReviewState(api, painted))
|
|
1930
|
+
if (state.kind === 'refused') {
|
|
1931
|
+
return refuseWith(failure, PULL_REFUSALS[failure], emitJson, root)
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
const { record } = state
|
|
1935
|
+
const pull = Number(resolved.number)
|
|
1936
|
+
logReviewState(pull, record)
|
|
1937
|
+
const waited =
|
|
1938
|
+
wait?.kind === 'read'
|
|
1939
|
+
? { reason: wait.reason, waitedSeconds: wait.waitedSeconds }
|
|
1940
|
+
: undefined
|
|
1941
|
+
if (waited !== undefined) {
|
|
1942
|
+
logStep(waited.reason === 'covered' ? 'Covered' : 'Timeout')
|
|
1943
|
+
logInfo(
|
|
1944
|
+
waited.reason === 'covered'
|
|
1945
|
+
? `a pass covers ${record.head.slice(0, 8)} in every bound family after ${waited.waitedSeconds}s`
|
|
1946
|
+
: `no pass covered ${record.head.slice(0, 8)} in every bound family within ${waited.waitedSeconds}s`,
|
|
1947
|
+
)
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
outro()
|
|
1951
|
+
|
|
1952
|
+
if (emitJson) {
|
|
1953
|
+
const { scope, ...families } = record
|
|
1954
|
+
process.stdout.write(
|
|
1955
|
+
`${JSON.stringify({
|
|
1956
|
+
root,
|
|
1957
|
+
number: pull,
|
|
1958
|
+
...scope,
|
|
1959
|
+
...families,
|
|
1960
|
+
...waited,
|
|
1961
|
+
})}\n`,
|
|
1962
|
+
)
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
return waited?.reason === 'timeout' ? 1 : 0
|
|
1966
|
+
}
|
|
1855
1967
|
|
|
1968
|
+
function logReviewState(number: number, record: ReviewStateRecord): void {
|
|
1969
|
+
const { scope } = record
|
|
1856
1970
|
logStep('Scope')
|
|
1857
|
-
logInfo(`#${
|
|
1971
|
+
logInfo(`#${number} at ${record.head.slice(0, 8)}`)
|
|
1858
1972
|
|
|
1859
1973
|
if (scope.source === 'none') {
|
|
1860
1974
|
logStep('First pass')
|
|
@@ -1874,19 +1988,17 @@ async function runReviewState(
|
|
|
1874
1988
|
)
|
|
1875
1989
|
}
|
|
1876
1990
|
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
)
|
|
1991
|
+
for (const [name, family] of [
|
|
1992
|
+
['review', record.review],
|
|
1993
|
+
['ui', record.ui],
|
|
1994
|
+
] as const) {
|
|
1995
|
+
const line = `${name}: ${family.reason ?? 'clear'}, ${family.isCovered ? 'a pass covers the tip' : 'no pass covers the tip'}`
|
|
1996
|
+
if (family.isCovered) logInfo(line)
|
|
1997
|
+
else logWarn(line)
|
|
1998
|
+
}
|
|
1999
|
+
if (!record.ui.isBound) {
|
|
2000
|
+
logInfo('ui: the evidence carries no checklist, so the family is unbound')
|
|
1887
2001
|
}
|
|
1888
|
-
|
|
1889
|
-
return 0
|
|
1890
2002
|
}
|
|
1891
2003
|
|
|
1892
2004
|
interface PullFiles {
|
|
@@ -2616,81 +2728,10 @@ async function runPreviewCheck(
|
|
|
2616
2728
|
return reading.reason === 'fresh' ? 0 : 1
|
|
2617
2729
|
}
|
|
2618
2730
|
|
|
2619
|
-
|
|
2620
|
-
return readFile(path, 'utf8').catch(() => undefined)
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
/**
|
|
2624
|
-
* Every listening socket's owning pid, read from `/proc`. Only a process this
|
|
2625
|
-
* user owns exposes its `fd` table, which is also the only process whose
|
|
2626
|
-
* server this user's worktree could have started.
|
|
2627
|
-
*/
|
|
2628
|
-
async function readProcListeners(): Promise<Listener[] | undefined> {
|
|
2629
|
-
const tables = await Promise.all(
|
|
2630
|
-
['/proc/net/tcp', '/proc/net/tcp6'].map(readText),
|
|
2631
|
-
)
|
|
2632
|
-
if (tables.every((text) => text === undefined)) return undefined
|
|
2633
|
-
const byInode = new Map(
|
|
2634
|
-
tables.flatMap((text) =>
|
|
2635
|
-
text === undefined
|
|
2636
|
-
? []
|
|
2637
|
-
: parseProcNetTcp(text).map((s) => [s.inode, s.port] as const),
|
|
2638
|
-
),
|
|
2639
|
-
)
|
|
2640
|
-
|
|
2641
|
-
const processIds = (await readdir('/proc').catch(() => [])).filter((name) =>
|
|
2642
|
-
/^\d+$/.test(name),
|
|
2643
|
-
)
|
|
2644
|
-
const listeners: Listener[] = []
|
|
2645
|
-
for (const pid of processIds) {
|
|
2646
|
-
const fds = await readdir(`/proc/${pid}/fd`).catch(() => [])
|
|
2647
|
-
for (const fd of fds) {
|
|
2648
|
-
const target = await readlink(`/proc/${pid}/fd/${fd}`).catch(() => '')
|
|
2649
|
-
const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1]
|
|
2650
|
-
const port = inode === undefined ? undefined : byInode.get(inode)
|
|
2651
|
-
if (port !== undefined) listeners.push({ pid: Number(pid), port })
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
return listeners
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
/**
|
|
2658
|
-
* The machine half of a detection: `/proc` wherever it exists, lsof on a
|
|
2659
|
-
* machine without it, and no reader anywhere else.
|
|
2660
|
-
*/
|
|
2731
|
+
/** The listener reads come from the machine, and the HTML probe from here. */
|
|
2661
2732
|
function machineLocalRunner(): LocalRunner {
|
|
2662
|
-
const hasLsof = Bun.which('lsof') !== null
|
|
2663
2733
|
return {
|
|
2664
|
-
|
|
2665
|
-
// lsof 4.95 drops every process whose kernel-truncated name holds an
|
|
2666
|
-
// unmatched `(`, which Next's `next-server (vX.Y.Z)` title cuts down to,
|
|
2667
|
-
// so `/proc` goes first despite costing several times what lsof does.
|
|
2668
|
-
if (existsSync('/proc/net/tcp')) {
|
|
2669
|
-
const listeners = await readProcListeners()
|
|
2670
|
-
if (listeners !== undefined) return listeners
|
|
2671
|
-
}
|
|
2672
|
-
if (hasLsof) {
|
|
2673
|
-
const result = await $`lsof -nP -iTCP -sTCP:LISTEN -Fpn`
|
|
2674
|
-
.quiet()
|
|
2675
|
-
.nothrow()
|
|
2676
|
-
// lsof exits 1 both when it matched nothing and when it could not
|
|
2677
|
-
// read some process, and the rows it did print are real either way.
|
|
2678
|
-
if (result.exitCode <= 1) return parseLsofListeners(result.text())
|
|
2679
|
-
}
|
|
2680
|
-
return undefined
|
|
2681
|
-
},
|
|
2682
|
-
async cwdOf(pid) {
|
|
2683
|
-
if (existsSync(`/proc/${pid}/cwd`)) {
|
|
2684
|
-
return readlink(`/proc/${pid}/cwd`).catch(() => undefined)
|
|
2685
|
-
}
|
|
2686
|
-
if (!hasLsof) return undefined
|
|
2687
|
-
const result = await $`lsof -a -p ${pid} -d cwd -Fn`.quiet().nothrow()
|
|
2688
|
-
const line = result
|
|
2689
|
-
.text()
|
|
2690
|
-
.split('\n')
|
|
2691
|
-
.find((row) => row.startsWith('n'))
|
|
2692
|
-
return line?.slice(1)
|
|
2693
|
-
},
|
|
2734
|
+
...machineListenerReader(),
|
|
2694
2735
|
// An HTML page rather than any answer, since a test runner's server or a
|
|
2695
2736
|
// reload socket in the same tree answers too, and a reviewer opening one
|
|
2696
2737
|
// gets a bare 404 instead of the branch.
|
package/src/commands/tooling.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from '@/tooling/manifest'
|
|
19
19
|
import { readReference, resolveReference } from '@/tooling/read'
|
|
20
20
|
import { scan, type ScanResult } from '@/tooling/scan'
|
|
21
|
-
import {
|
|
21
|
+
import { recordToolingChain } from '@/tooling/stamp'
|
|
22
22
|
import { subfolderPath } from '@/tooling/subfolder'
|
|
23
23
|
import { verifyStack } from '@/tooling/verify-stack'
|
|
24
24
|
import {
|
|
@@ -513,8 +513,8 @@ async function stampChain(chain: Manifest[], target: string): Promise<void> {
|
|
|
513
513
|
new Date(),
|
|
514
514
|
)
|
|
515
515
|
|
|
516
|
-
if (recorded) {
|
|
517
|
-
logInfo(`Recorded chain: ${
|
|
516
|
+
if (recorded !== undefined) {
|
|
517
|
+
logInfo(`Recorded chain: ${recorded.join(' < ')}`)
|
|
518
518
|
return
|
|
519
519
|
}
|
|
520
520
|
|
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
type WorktreeVerdict,
|
|
18
18
|
} from '@/worktrees/reclaim'
|
|
19
19
|
import { mainWorktreeRoot } from '@/git/worktree'
|
|
20
|
+
import { machineListenerReader } from '@/listeners/machine'
|
|
20
21
|
import { repairBareFlag } from '@/worktrees/bare-flag'
|
|
22
|
+
import { type StopReport, stopListeners } from '@/worktrees/stop'
|
|
21
23
|
import {
|
|
22
24
|
removeReclaimable,
|
|
23
25
|
type RemovalOutcome,
|
|
@@ -57,6 +59,12 @@ export interface ReclaimRecord {
|
|
|
57
59
|
readonly removed: number
|
|
58
60
|
readonly failed: number
|
|
59
61
|
readonly pruned: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Processes signaled, or that a dry run would signal, for listening inside a
|
|
64
|
+
* reclaimed worktree. Null when the machine offered no way to read
|
|
65
|
+
* listeners, which says nothing about what is still running.
|
|
66
|
+
*/
|
|
67
|
+
readonly stopped: number | null
|
|
60
68
|
readonly outcomes: readonly RemovalOutcome[]
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -79,6 +87,8 @@ export type ReclaimSource =
|
|
|
79
87
|
* from a run that was asked not to act.
|
|
80
88
|
*/
|
|
81
89
|
readonly removal: RemovalReport | null
|
|
90
|
+
/** Null when no stop ran, which is a reading with nothing reclaimable. */
|
|
91
|
+
readonly stop?: StopReport | null
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
const REFUSALS: Record<Refusal, string> = {
|
|
@@ -198,6 +208,14 @@ export function register(program: Command): void {
|
|
|
198
208
|
'deletes its branch, and a run carrying a directory that is already',
|
|
199
209
|
'gone sweeps stale registrations once before the branch deletes.',
|
|
200
210
|
'',
|
|
211
|
+
'Ahead of each removal it sends SIGTERM to every process listening from',
|
|
212
|
+
'inside a reclaimable worktree, so no dev server outlives the tree it',
|
|
213
|
+
'serves. A listener in the main checkout, in a refused worktree, or in',
|
|
214
|
+
'a worktree nested under another is left alone. A dry run signals',
|
|
215
|
+
'nothing and still reports what it would stop. "stopped" in the record',
|
|
216
|
+
'counts those processes, and reads null on a machine with neither lsof',
|
|
217
|
+
'nor /proc, where no listener could be read.',
|
|
218
|
+
'',
|
|
201
219
|
'It deletes only what a reading called reclaimable. A worktree a live',
|
|
202
220
|
'session holds is refused rather than removed here. `claude rm <id>`',
|
|
203
221
|
'is meant to remove the session and its worktree together, but its',
|
|
@@ -324,11 +342,13 @@ export function reclaimRecord(source: ReclaimSource): ReclaimRecord {
|
|
|
324
342
|
removed: 0,
|
|
325
343
|
failed: 0,
|
|
326
344
|
pruned: false,
|
|
345
|
+
stopped: 0,
|
|
327
346
|
outcomes: [],
|
|
328
347
|
}
|
|
329
348
|
}
|
|
330
349
|
|
|
331
350
|
const outcomes = source.removal?.outcomes ?? []
|
|
351
|
+
const stop = source.stop ?? null
|
|
332
352
|
|
|
333
353
|
return {
|
|
334
354
|
reason: null,
|
|
@@ -338,10 +358,30 @@ export function reclaimRecord(source: ReclaimSource): ReclaimRecord {
|
|
|
338
358
|
removed: outcomes.filter((outcome) => outcome.removed).length,
|
|
339
359
|
failed: outcomes.filter((outcome) => !outcome.removed).length,
|
|
340
360
|
pruned: source.removal?.pruned ?? false,
|
|
361
|
+
stopped: stop === null ? 0 : countStopped(stop),
|
|
341
362
|
outcomes,
|
|
342
363
|
}
|
|
343
364
|
}
|
|
344
365
|
|
|
366
|
+
function countStopped(stop: StopReport): number | null {
|
|
367
|
+
if (stop.kind === 'unread') return null
|
|
368
|
+
return stop.stopped.reduce((sum, server) => sum + server.pids.length, 0)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Names each server a stop took, or would take on a dry run, and says when none could be read. */
|
|
372
|
+
function reportStop(stop: StopReport, dryRun: boolean): void {
|
|
373
|
+
if (stop.kind === 'unread') {
|
|
374
|
+
logWarn(
|
|
375
|
+
'This machine offers neither lsof nor /proc, so listening servers were not read and none was stopped.',
|
|
376
|
+
)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
const verb = dryRun ? 'Would stop' : 'Stopped'
|
|
380
|
+
for (const { path, ports } of stop.stopped) {
|
|
381
|
+
logInfo(`${verb} the server on ${ports.join(', ')} in ${path}`)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
345
385
|
function emitReclaim(opts: ReclaimCommandOptions, source: ReclaimSource): void {
|
|
346
386
|
if (opts.json !== true) return
|
|
347
387
|
process.stdout.write(`${JSON.stringify(reclaimRecord(source))}\n`)
|
|
@@ -381,8 +421,14 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
|
|
|
381
421
|
logInfo(plural(reclaimable.length, 'worktree'))
|
|
382
422
|
pipeOutput(reclaimable.map(describe).join('\n'))
|
|
383
423
|
|
|
424
|
+
const stop = await stopListeners(
|
|
425
|
+
reclaimable.map((entry) => entry.path),
|
|
426
|
+
{ reader: machineListenerReader(), dryRun },
|
|
427
|
+
)
|
|
428
|
+
|
|
384
429
|
if (dryRun) {
|
|
385
430
|
logStep('Dry run')
|
|
431
|
+
reportStop(stop, dryRun)
|
|
386
432
|
logWarn('Nothing was removed. Run this again without --dry-run to remove.')
|
|
387
433
|
outro()
|
|
388
434
|
emitReclaim(opts, {
|
|
@@ -390,6 +436,7 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
|
|
|
390
436
|
dryRun,
|
|
391
437
|
reclaimable: reclaimable.length,
|
|
392
438
|
removal: null,
|
|
439
|
+
stop,
|
|
393
440
|
})
|
|
394
441
|
return 0
|
|
395
442
|
}
|
|
@@ -403,6 +450,7 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
|
|
|
403
450
|
})
|
|
404
451
|
|
|
405
452
|
logStep('Removed')
|
|
453
|
+
reportStop(stop, dryRun)
|
|
406
454
|
for (const outcome of removal.outcomes) reportOutcome(outcome)
|
|
407
455
|
if (removal.pruned) {
|
|
408
456
|
logInfo(
|
|
@@ -423,6 +471,7 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
|
|
|
423
471
|
dryRun,
|
|
424
472
|
reclaimable: reclaimable.length,
|
|
425
473
|
removal,
|
|
474
|
+
stop,
|
|
426
475
|
})
|
|
427
476
|
return failed.length > 0 ? 1 : 0
|
|
428
477
|
}
|
package/src/gate/stages.ts
CHANGED
|
@@ -380,6 +380,21 @@ export const STAGES: readonly Stage[] = [
|
|
|
380
380
|
],
|
|
381
381
|
success: 'Rule citations resolve',
|
|
382
382
|
},
|
|
383
|
+
{
|
|
384
|
+
// Reads the whole rule corpus and fails only on a folder the verb lists as
|
|
385
|
+
// gated, so the finding count prints on every push while the rewrite moves
|
|
386
|
+
// folder by folder toward zero.
|
|
387
|
+
id: 'rule-shape',
|
|
388
|
+
label: 'Rule shape',
|
|
389
|
+
checks: [
|
|
390
|
+
{
|
|
391
|
+
kind: 'cli',
|
|
392
|
+
argv: ['gov', 'audit', '--gate'],
|
|
393
|
+
failure:
|
|
394
|
+
'A rule in a gated folder is past a shape limit or short of its ledger. Run bun src/cli.ts gov audit.',
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
383
398
|
{
|
|
384
399
|
// A citation naming the old record root resolves to nothing once the
|
|
385
400
|
// folders have moved, and the sweep that would repoint it only runs when a
|