@erclx/canon 5.24.0 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +19 -6
- package/claude/skills/canvas/REQUIREMENT.md +49 -0
- package/claude/skills/canvas/SKILL.md +106 -0
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +8 -6
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/design-extract/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +4 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +21 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/draft-doc/REQUIREMENT.md +1 -1
- package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
- package/claude/skills/draft-figure/SKILL.md +24 -51
- package/claude/skills/draft-figure/references/architecture-set.md +35 -0
- package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
- package/claude/skills/draft-figure/references/mermaid.md +55 -0
- package/claude/skills/draft-identity/SKILL.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +9 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +31 -20
- package/claude/skills/git-followup/SKILL.md +17 -6
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/references/evidence.md +12 -2
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-split/SKILL.md +19 -8
- package/claude/skills/index-lookup/SKILL.md +2 -2
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/review-address/SKILL.md +6 -13
- package/claude/skills/review-address/references/rest-reads.md +43 -0
- package/claude/skills/review-craft/references/ui.md +7 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -0
- package/claude/skills/review-pr/SKILL.md +9 -3
- package/claude/skills/review-pr/references/close-out.md +2 -2
- package/claude/skills/review-pr/references/post.md +17 -5
- package/claude/skills/review-pr/references/scope.md +10 -8
- package/claude/skills/review-ui/SKILL.md +12 -4
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +38 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
- package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
- package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
- package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +3 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/write-human/SKILL.md +1 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/docs/agents/canvas.md +4 -2
- package/docs/agents/capture.md +1 -1
- package/docs/agents/commands.md +47 -47
- package/docs/agents/context-audit-checks.md +2 -2
- package/docs/agents/index.md +2 -3
- package/docs/agents/key-changes.md +3 -3
- package/docs/agents/pr-evidence.md +11 -2
- package/docs/agents/pr-reads.md +5 -4
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/skills-audit.md +10 -3
- package/docs/agents/slides.md +15 -3
- package/docs/target/projects.md +4 -4
- package/docs/workflow/ai-workflow.md +1 -2
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +3 -36
- package/docs/workflow/visual-design-workflow.md +1 -0
- package/governance/renames.toml +0 -4
- package/governance/rules/standards/markdown.md +1 -0
- package/package.json +1 -2
- package/scripts/core/install-check.sh +1 -1
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/sandbox-hook.sh +99 -0
- package/src/audits/catalog.ts +2 -2
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/color-field.tsx +368 -0
- package/src/canvas/client/inspector/field.tsx +200 -0
- package/src/canvas/client/inspector/fields.css +292 -0
- package/src/canvas/client/inspector/section.tsx +27 -0
- package/src/canvas/client/inspector/values.ts +183 -0
- package/src/canvas/client/inspector.tsx +176 -160
- package/src/canvas/client/shell.css +0 -101
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +1 -1
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/cases/workflow.ts +6 -1
- package/src/claude/seeds.ts +1 -2
- package/src/claude/skills-audit.ts +68 -14
- package/src/client-commands.ts +1 -1
- package/src/commands/claude.ts +27 -2
- package/src/commands/design.ts +2 -64
- package/src/commands/feedback.ts +3 -3
- package/src/commands/pr.ts +219 -202
- package/src/commands/records.ts +88 -6
- package/src/commands/sandbox.ts +50 -10
- package/src/commands/slides.ts +31 -77
- package/src/context/audit.ts +8 -7
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +1 -1
- package/src/gate/measures.ts +6 -4
- package/src/gov/citations.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +2 -2
- package/src/help.ts +2 -2
- package/src/markdown/ceiling.ts +1 -1
- package/src/migrate/skill-names.ts +7 -1
- package/src/pr/evidence.ts +25 -5
- package/src/pr/rest.ts +137 -0
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +1 -0
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +596 -0
- package/src/sandbox/tree.ts +103 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/standards/read.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +112 -0
- package/standards/context.md +2 -1
- package/standards/figures.md +0 -1
- package/standards/index.md +1 -1
- package/standards/markdown.md +1 -0
- package/standards/mermaid.md +1 -7
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +1 -0
- package/tooling/claude/reference.md +0 -5
- package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
- package/claude/skills/draft-diagram/SKILL.md +0 -177
- package/docs/agents/design-board.md +0 -33
- package/governance/rules/standards/diagrams.md +0 -14
- package/governance/rules/standards/mermaid.md +0 -15
- package/scripts/core/check-gallery-exclusion.sh +0 -50
- package/scripts/manage-sandbox.sh +0 -472
- package/src/design/board.ts +0 -576
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- package/standards/diagrams.md +0 -132
- package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
package/src/commands/pr.ts
CHANGED
|
@@ -70,7 +70,18 @@ import {
|
|
|
70
70
|
type RunRow,
|
|
71
71
|
type WorkflowFile,
|
|
72
72
|
} from '@/pr/preview'
|
|
73
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
commentRowOf,
|
|
75
|
+
identityOf,
|
|
76
|
+
type PullIdentity,
|
|
77
|
+
parseJsonLines,
|
|
78
|
+
type RestComment,
|
|
79
|
+
type RestPull,
|
|
80
|
+
type RestReview,
|
|
81
|
+
reviewRowOf,
|
|
82
|
+
selectBranchPull,
|
|
83
|
+
} from '@/pr/rest'
|
|
84
|
+
import { resolveReviewScope } from '@/pr/review-scope'
|
|
74
85
|
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/ui'
|
|
75
86
|
|
|
76
87
|
const GH_TIMEOUT_MS = 30_000
|
|
@@ -78,17 +89,6 @@ const GH_TIMEOUT_MS = 30_000
|
|
|
78
89
|
/** How many unnamed files the frame prints before it names a count instead. */
|
|
79
90
|
const UNNAMED_PRINT_LIMIT = 10
|
|
80
91
|
|
|
81
|
-
/**
|
|
82
|
-
* Where `gh pr view --json files` stops.
|
|
83
|
-
*
|
|
84
|
-
* It pages the underlying query once and returns at most this many rows with
|
|
85
|
-
* nothing on the record saying so, which was measured against `#1250`: the
|
|
86
|
-
* pull request carries 101 files and the view reports 100. A set silently one
|
|
87
|
-
* short is the worst input this comparison can take, since the missing file is
|
|
88
|
-
* exactly what a claim would then be accused of inventing.
|
|
89
|
-
*/
|
|
90
|
-
const GH_VIEW_FILE_CAP = 100
|
|
91
|
-
|
|
92
92
|
interface KeyChangesOptions {
|
|
93
93
|
readonly body?: string
|
|
94
94
|
readonly base?: string
|
|
@@ -113,6 +113,10 @@ interface LocalOptions extends ReadOptions {
|
|
|
113
113
|
readonly note?: string
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** Shared by every verb that resolves a pull request from the checkout's branch. */
|
|
117
|
+
const AMBIGUOUS_PULL =
|
|
118
|
+
'More than one pull request is open on this branch, each against another base. Name the pull request number.'
|
|
119
|
+
|
|
116
120
|
/** What replaces the local address line once the pull request closes. */
|
|
117
121
|
const LOCAL_REMOVED_NOTE =
|
|
118
122
|
'_Local preview removed when the pull request closed._'
|
|
@@ -226,6 +230,7 @@ const PREVIEW_HEAD_NOTES: Record<PreviewHeadReason, string> = {
|
|
|
226
230
|
type PullRefusal =
|
|
227
231
|
| 'gh-missing'
|
|
228
232
|
| 'gh-failed'
|
|
233
|
+
| 'ambiguous-pull'
|
|
229
234
|
| 'no-branch'
|
|
230
235
|
| 'runs-unreadable'
|
|
231
236
|
| 'reviews-unreadable'
|
|
@@ -236,6 +241,7 @@ const PULL_REFUSALS: Record<PullRefusal | HeadRefusal, string> = {
|
|
|
236
241
|
'gh is not on the path, so no pull request could be resolved. Name the branch through a checkout that carries one.',
|
|
237
242
|
'gh-failed':
|
|
238
243
|
'gh could not answer for this branch. Name the pull request number instead.',
|
|
244
|
+
'ambiguous-pull': AMBIGUOUS_PULL,
|
|
239
245
|
'no-branch':
|
|
240
246
|
'The pull request carries no head branch name, so no ref could be read for it.',
|
|
241
247
|
'unresolvable-ref':
|
|
@@ -282,7 +288,7 @@ const EVIDENCE_REFUSALS: Record<EvidenceRefusal, string> = {
|
|
|
282
288
|
type SourceRefusal =
|
|
283
289
|
| 'gh-missing'
|
|
284
290
|
| 'gh-failed'
|
|
285
|
-
| '
|
|
291
|
+
| 'ambiguous-pull'
|
|
286
292
|
| 'unreadable-body'
|
|
287
293
|
| 'unreadable-tree'
|
|
288
294
|
| 'no-base'
|
|
@@ -297,7 +303,7 @@ const REFUSALS: Record<Refusal, string> = {
|
|
|
297
303
|
'gh is not on the path, so no pull request body could be read. Pass --body <path> to read one off disk instead.',
|
|
298
304
|
'gh-failed':
|
|
299
305
|
'gh could not answer for this branch. Name the pull request number, or pass --body <path>.',
|
|
300
|
-
'
|
|
306
|
+
'ambiguous-pull': AMBIGUOUS_PULL,
|
|
301
307
|
'unreadable-body': 'The file named by --body could not be read.',
|
|
302
308
|
'unreadable-tree':
|
|
303
309
|
'git could not list this repository, so no path could be judged whole rather than partial.',
|
|
@@ -817,11 +823,11 @@ type SourceRead =
|
|
|
817
823
|
* Reads the body and the changed set from the pull request the caller named,
|
|
818
824
|
* or from the one open on this branch.
|
|
819
825
|
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
826
|
+
* The body comes off the pull read and the files off the paginated files
|
|
827
|
+
* endpoint, since GraphQL is the only route that returns both in one call and
|
|
828
|
+
* a cloud session's proxy refuses it. A push between the two reads can compare
|
|
829
|
+
* a body against the next commit's files, which the paginated read already
|
|
830
|
+
* risked for any pull request past the old view's cap of 100 files.
|
|
825
831
|
*/
|
|
826
832
|
async function readFromApi(
|
|
827
833
|
cwd: string,
|
|
@@ -831,63 +837,31 @@ async function readFromApi(
|
|
|
831
837
|
return { kind: 'refused', reason: 'gh-missing' }
|
|
832
838
|
}
|
|
833
839
|
|
|
834
|
-
const
|
|
835
|
-
if (
|
|
836
|
-
args.push('--json', 'body,files,headRefOid,number')
|
|
837
|
-
|
|
838
|
-
try {
|
|
839
|
-
// See src/worktrees/reclaim.ts for why gh needs the stripped environment:
|
|
840
|
-
// it resolves its repository through the same variables git does and they
|
|
841
|
-
// beat `cwd`, so a run from inside a hook would answer for another
|
|
842
|
-
// repository and compare this branch's claims against its files.
|
|
843
|
-
const result = await execa('gh', args, {
|
|
844
|
-
cwd,
|
|
845
|
-
timeout: GH_TIMEOUT_MS,
|
|
846
|
-
env: gitEnv(),
|
|
847
|
-
extendEnv: false,
|
|
848
|
-
})
|
|
840
|
+
const pull = await readPull(cwd, number)
|
|
841
|
+
if (pull.kind === 'refused') return pull
|
|
849
842
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
files?: readonly { path: string }[]
|
|
853
|
-
headRefOid?: string
|
|
854
|
-
number?: number
|
|
855
|
-
}
|
|
843
|
+
const { row } = pull
|
|
844
|
+
if (row.number === undefined) return { kind: 'refused', reason: 'gh-failed' }
|
|
856
845
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
viewed.length < GH_VIEW_FILE_CAP || row.number === undefined
|
|
860
|
-
? viewed
|
|
861
|
-
: await listFilesByPage(cwd, row.number)
|
|
846
|
+
const changed = await listFilesByPage(cwd, row.number)
|
|
847
|
+
if (changed === undefined) return { kind: 'refused', reason: 'gh-failed' }
|
|
862
848
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
849
|
+
const body = row.body ?? ''
|
|
850
|
+
const head = row.head?.sha
|
|
851
|
+
const sorted = [...changed].sort()
|
|
852
|
+
const evidence = await resolveApiEvidence(cwd, body, sorted, head, row.number)
|
|
866
853
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
cwd,
|
|
854
|
+
return {
|
|
855
|
+
kind: 'read',
|
|
856
|
+
source: {
|
|
871
857
|
body,
|
|
872
|
-
sorted,
|
|
873
|
-
|
|
874
|
-
row.number,
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
source: {
|
|
880
|
-
body,
|
|
881
|
-
changed: sorted,
|
|
882
|
-
head: row.headRefOid,
|
|
883
|
-
number: row.number,
|
|
884
|
-
renames: evidence.renames,
|
|
885
|
-
ignoreAdditions: evidence.ignoreAdditions,
|
|
886
|
-
evidenceUnread: evidence.unread,
|
|
887
|
-
},
|
|
888
|
-
}
|
|
889
|
-
} catch {
|
|
890
|
-
return { kind: 'refused', reason: 'gh-failed' }
|
|
858
|
+
changed: sorted,
|
|
859
|
+
head,
|
|
860
|
+
number: row.number,
|
|
861
|
+
renames: evidence.renames,
|
|
862
|
+
ignoreAdditions: evidence.ignoreAdditions,
|
|
863
|
+
evidenceUnread: evidence.unread,
|
|
864
|
+
},
|
|
891
865
|
}
|
|
892
866
|
}
|
|
893
867
|
|
|
@@ -896,9 +870,8 @@ async function readFromApi(
|
|
|
896
870
|
* first pass with neither already reports an unmet claim.
|
|
897
871
|
*
|
|
898
872
|
* The probe pays for a repository listing this read takes again a moment
|
|
899
|
-
* later in `runKeyChanges`, and the `gh api …/files` call besides it
|
|
900
|
-
* the pass that already has something to double-check
|
|
901
|
-
* `gh-truncated` pagination fallback makes.
|
|
873
|
+
* later in `runKeyChanges`, and the full-row `gh api …/files` call besides it
|
|
874
|
+
* only on the pass that already has something to double-check.
|
|
902
875
|
*
|
|
903
876
|
* `unread` separates a read that failed from one that succeeded and found
|
|
904
877
|
* neither, which an empty `renames`/`ignoreAdditions` cannot do on its own.
|
|
@@ -983,11 +956,9 @@ async function resolveApiEvidence(
|
|
|
983
956
|
/**
|
|
984
957
|
* Every file a pull request changed, read through the paginated endpoint.
|
|
985
958
|
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
* capped set: a comparison run against a set known to be short would accuse a
|
|
990
|
-
* correct bullet of naming a file nobody changed.
|
|
959
|
+
* Returns undefined when any page fails, which refuses rather than comparing
|
|
960
|
+
* the pages that arrived: a comparison run against a set known to be short
|
|
961
|
+
* would accuse a correct bullet of naming a file nobody changed.
|
|
991
962
|
*/
|
|
992
963
|
async function listFilesByPage(
|
|
993
964
|
cwd: string,
|
|
@@ -1182,14 +1153,6 @@ async function runKeyChanges(
|
|
|
1182
1153
|
return report.unmet.length === 0 ? 0 : 2
|
|
1183
1154
|
}
|
|
1184
1155
|
|
|
1185
|
-
/** The head branch and reported head of the pull request a caller named. */
|
|
1186
|
-
interface PullIdentity {
|
|
1187
|
-
readonly number: number | undefined
|
|
1188
|
-
readonly branch: string
|
|
1189
|
-
readonly head?: string
|
|
1190
|
-
readonly mergeState?: string
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
1156
|
type IdentityRead =
|
|
1194
1157
|
| { readonly kind: 'read'; readonly identity: PullIdentity }
|
|
1195
1158
|
| { readonly kind: 'refused'; readonly reason: PullRefusal }
|
|
@@ -1218,54 +1181,152 @@ async function gh(
|
|
|
1218
1181
|
}
|
|
1219
1182
|
}
|
|
1220
1183
|
|
|
1184
|
+
type PullNumber =
|
|
1185
|
+
| { readonly kind: 'found'; readonly number: string }
|
|
1186
|
+
| {
|
|
1187
|
+
readonly kind: 'refused'
|
|
1188
|
+
readonly reason: 'gh-failed' | 'ambiguous-pull'
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1221
1191
|
/**
|
|
1222
|
-
*
|
|
1192
|
+
* The number the caller named, or the one open pull request on the branch the
|
|
1193
|
+
* checkout at `cwd` holds.
|
|
1223
1194
|
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1195
|
+
* `{owner}` in the head filter is the base repository's owner, which is the
|
|
1196
|
+
* head owner for every pull request opened from a branch of this repository.
|
|
1226
1197
|
*/
|
|
1227
|
-
async function
|
|
1198
|
+
async function resolvePullNumber(
|
|
1228
1199
|
cwd: string,
|
|
1229
1200
|
number: string | undefined,
|
|
1230
|
-
): Promise<
|
|
1231
|
-
if (
|
|
1232
|
-
return { kind: 'refused', reason: 'gh-missing' }
|
|
1233
|
-
}
|
|
1201
|
+
): Promise<PullNumber> {
|
|
1202
|
+
if (number !== undefined) return { kind: 'found', number }
|
|
1234
1203
|
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1204
|
+
const branch = await $`git -C ${cwd} symbolic-ref --short -q HEAD`
|
|
1205
|
+
.env(gitEnv())
|
|
1206
|
+
.quiet()
|
|
1207
|
+
.nothrow()
|
|
1208
|
+
const name = branch.exitCode === 0 ? branch.text().trim() : ''
|
|
1209
|
+
if (name === '') return { kind: 'refused', reason: 'gh-failed' }
|
|
1238
1210
|
|
|
1239
|
-
const stdout = await gh(cwd,
|
|
1211
|
+
const stdout = await gh(cwd, [
|
|
1212
|
+
'api',
|
|
1213
|
+
`repos/{owner}/{repo}/pulls?head={owner}:${encodeURIComponent(name)}&state=open&per_page=100`,
|
|
1214
|
+
])
|
|
1240
1215
|
if (stdout === null) return { kind: 'refused', reason: 'gh-failed' }
|
|
1241
1216
|
|
|
1242
|
-
let
|
|
1243
|
-
number?: number
|
|
1244
|
-
headRefName?: string
|
|
1245
|
-
headRefOid?: string
|
|
1246
|
-
mergeStateStatus?: string
|
|
1247
|
-
}
|
|
1217
|
+
let rows: readonly RestPull[]
|
|
1248
1218
|
try {
|
|
1249
|
-
|
|
1219
|
+
rows = JSON.parse(stdout)
|
|
1250
1220
|
} catch {
|
|
1251
1221
|
return { kind: 'refused', reason: 'gh-failed' }
|
|
1252
1222
|
}
|
|
1223
|
+
const selected = selectBranchPull(rows)
|
|
1224
|
+
return selected.kind === 'found'
|
|
1225
|
+
? { kind: 'found', number: String(selected.number) }
|
|
1226
|
+
: selected
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
type PullRead =
|
|
1230
|
+
| { readonly kind: 'read'; readonly row: RestPull }
|
|
1231
|
+
| {
|
|
1232
|
+
readonly kind: 'refused'
|
|
1233
|
+
readonly reason: 'gh-failed' | 'ambiguous-pull'
|
|
1234
|
+
}
|
|
1253
1235
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1236
|
+
/**
|
|
1237
|
+
* The pull request object off the REST pull endpoint, which a cloud session's
|
|
1238
|
+
* proxy serves where it refuses the GraphQL every `gh pr view` runs on.
|
|
1239
|
+
*/
|
|
1240
|
+
async function readPull(
|
|
1241
|
+
cwd: string,
|
|
1242
|
+
number: string | undefined,
|
|
1243
|
+
): Promise<PullRead> {
|
|
1244
|
+
const resolved = await resolvePullNumber(cwd, number)
|
|
1245
|
+
if (resolved.kind === 'refused') return resolved
|
|
1246
|
+
|
|
1247
|
+
const stdout = await gh(cwd, [
|
|
1248
|
+
'api',
|
|
1249
|
+
`repos/{owner}/{repo}/pulls/${resolved.number}`,
|
|
1250
|
+
])
|
|
1251
|
+
if (stdout === null) return { kind: 'refused', reason: 'gh-failed' }
|
|
1252
|
+
try {
|
|
1253
|
+
return { kind: 'read', row: JSON.parse(stdout) as RestPull }
|
|
1254
|
+
} catch {
|
|
1255
|
+
return { kind: 'refused', reason: 'gh-failed' }
|
|
1256
1256
|
}
|
|
1257
|
+
}
|
|
1257
1258
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1259
|
+
type PagesRead<T> =
|
|
1260
|
+
| { readonly kind: 'read'; readonly rows: readonly T[] }
|
|
1261
|
+
| { readonly kind: 'refused'; readonly reason: 'gh-failed' | 'unparsed' }
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Every row of a paginated listing, or a refusal when any page failed.
|
|
1265
|
+
*
|
|
1266
|
+
* A refusal rather than an empty list, since a refused read that reads as no
|
|
1267
|
+
* reviews routes the next pass to the whole change.
|
|
1268
|
+
*/
|
|
1269
|
+
async function listPages<T>(
|
|
1270
|
+
cwd: string,
|
|
1271
|
+
path: string,
|
|
1272
|
+
fields: string,
|
|
1273
|
+
): Promise<PagesRead<T>> {
|
|
1274
|
+
const stdout = await gh(cwd, [
|
|
1275
|
+
'api',
|
|
1276
|
+
'--paginate',
|
|
1277
|
+
`${path}?per_page=100`,
|
|
1278
|
+
'--jq',
|
|
1279
|
+
`.[] | {${fields}} | @json`,
|
|
1280
|
+
])
|
|
1281
|
+
if (stdout === null) return { kind: 'refused', reason: 'gh-failed' }
|
|
1282
|
+
const rows = parseJsonLines<T>(stdout)
|
|
1283
|
+
return rows === undefined
|
|
1284
|
+
? { kind: 'refused', reason: 'unparsed' }
|
|
1285
|
+
: { kind: 'read', rows }
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
async function listReviews(cwd: string, number: string) {
|
|
1289
|
+
return listPages<RestReview>(
|
|
1290
|
+
cwd,
|
|
1291
|
+
`repos/{owner}/{repo}/pulls/${number}/reviews`,
|
|
1292
|
+
'body, commit_id, submitted_at',
|
|
1293
|
+
)
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** The issue comments on a pull request, or undefined when the thread is unread. */
|
|
1297
|
+
async function listComments(
|
|
1298
|
+
cwd: string,
|
|
1299
|
+
number: string,
|
|
1300
|
+
): Promise<readonly { url?: string; body: string }[] | undefined> {
|
|
1301
|
+
const read = await listPages<RestComment>(
|
|
1302
|
+
cwd,
|
|
1303
|
+
`repos/{owner}/{repo}/issues/${number}/comments`,
|
|
1304
|
+
'html_url, body',
|
|
1305
|
+
)
|
|
1306
|
+
return read.kind === 'read' ? read.rows.map(commentRowOf) : undefined
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Reads the head branch and the head the pull request object reports.
|
|
1311
|
+
*
|
|
1312
|
+
* Both come from one call, so the branch a ref is read for and the head that
|
|
1313
|
+
* ref is compared against describe the same object.
|
|
1314
|
+
*/
|
|
1315
|
+
async function readIdentity(
|
|
1316
|
+
cwd: string,
|
|
1317
|
+
number: string | undefined,
|
|
1318
|
+
): Promise<IdentityRead> {
|
|
1319
|
+
if (Bun.which('gh') === null) {
|
|
1320
|
+
return { kind: 'refused', reason: 'gh-missing' }
|
|
1268
1321
|
}
|
|
1322
|
+
|
|
1323
|
+
const pull = await readPull(cwd, number)
|
|
1324
|
+
if (pull.kind === 'refused') return pull
|
|
1325
|
+
|
|
1326
|
+
const identity: PullIdentity = identityOf(pull.row)
|
|
1327
|
+
if (identity.branch === '') return { kind: 'refused', reason: 'no-branch' }
|
|
1328
|
+
|
|
1329
|
+
return { kind: 'read', identity }
|
|
1269
1330
|
}
|
|
1270
1331
|
|
|
1271
1332
|
/**
|
|
@@ -1476,39 +1537,32 @@ async function runReviewState(
|
|
|
1476
1537
|
return refuseWith('gh-missing', PULL_REFUSALS['gh-missing'], emitJson, root)
|
|
1477
1538
|
}
|
|
1478
1539
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
// stay out of the query, since nothing here parses one and a listing the
|
|
1484
|
-
// caller discards is a payload paid for twice.
|
|
1485
|
-
args.push('--json', 'number,reviews')
|
|
1486
|
-
|
|
1487
|
-
const stdout = await gh(root, args)
|
|
1488
|
-
if (stdout === null) {
|
|
1489
|
-
return refuseWith('gh-failed', PULL_REFUSALS['gh-failed'], emitJson, root)
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
let listing: ReviewListing & { number?: number }
|
|
1493
|
-
try {
|
|
1494
|
-
listing = JSON.parse(stdout)
|
|
1495
|
-
} catch {
|
|
1540
|
+
// The number is resolved up front so the record names the pull request a
|
|
1541
|
+
// caller that passed no argument was answered about.
|
|
1542
|
+
const resolved = await resolvePullNumber(root, number)
|
|
1543
|
+
if (resolved.kind === 'refused') {
|
|
1496
1544
|
return refuseWith(
|
|
1497
|
-
|
|
1498
|
-
PULL_REFUSALS[
|
|
1545
|
+
resolved.reason,
|
|
1546
|
+
PULL_REFUSALS[resolved.reason],
|
|
1499
1547
|
emitJson,
|
|
1500
1548
|
root,
|
|
1501
1549
|
)
|
|
1502
1550
|
}
|
|
1503
1551
|
|
|
1504
|
-
const
|
|
1552
|
+
const reviews = await listReviews(root, resolved.number)
|
|
1553
|
+
if (reviews.kind === 'refused') {
|
|
1554
|
+
const reason =
|
|
1555
|
+
reviews.reason === 'gh-failed' ? 'gh-failed' : 'reviews-unreadable'
|
|
1556
|
+
return refuseWith(reason, PULL_REFUSALS[reason], emitJson, root)
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const listing = { number: Number(resolved.number) }
|
|
1560
|
+
const scope = resolveReviewScope({
|
|
1561
|
+
reviews: reviews.rows.map(reviewRowOf),
|
|
1562
|
+
})
|
|
1505
1563
|
|
|
1506
1564
|
logStep('Scope')
|
|
1507
|
-
logInfo(
|
|
1508
|
-
listing.number === undefined
|
|
1509
|
-
? 'the pull request on this branch'
|
|
1510
|
-
: `#${listing.number}`,
|
|
1511
|
-
)
|
|
1565
|
+
logInfo(`#${listing.number}`)
|
|
1512
1566
|
|
|
1513
1567
|
if (scope.source === 'none') {
|
|
1514
1568
|
logStep('First pass')
|
|
@@ -1534,7 +1588,7 @@ async function runReviewState(
|
|
|
1534
1588
|
process.stdout.write(
|
|
1535
1589
|
`${JSON.stringify({
|
|
1536
1590
|
root,
|
|
1537
|
-
|
|
1591
|
+
number: listing.number,
|
|
1538
1592
|
...scope,
|
|
1539
1593
|
})}\n`,
|
|
1540
1594
|
)
|
|
@@ -1581,22 +1635,13 @@ async function listPullFiles(
|
|
|
1581
1635
|
*/
|
|
1582
1636
|
async function readPullMergeBase(
|
|
1583
1637
|
cwd: string,
|
|
1584
|
-
|
|
1638
|
+
baseRef: string | undefined,
|
|
1585
1639
|
head: string,
|
|
1586
1640
|
): Promise<string | undefined> {
|
|
1587
|
-
|
|
1588
|
-
'pr',
|
|
1589
|
-
'view',
|
|
1590
|
-
String(number),
|
|
1591
|
-
'--json',
|
|
1592
|
-
'baseRefName',
|
|
1593
|
-
'--jq',
|
|
1594
|
-
'.baseRefName',
|
|
1595
|
-
])
|
|
1596
|
-
if (baseRef === null || baseRef.trim() === '') return undefined
|
|
1641
|
+
if (baseRef === undefined || baseRef === '') return undefined
|
|
1597
1642
|
const sha = await gh(cwd, [
|
|
1598
1643
|
'api',
|
|
1599
|
-
`repos/{owner}/{repo}/compare/${baseRef
|
|
1644
|
+
`repos/{owner}/{repo}/compare/${baseRef}...${head}`,
|
|
1600
1645
|
'--jq',
|
|
1601
1646
|
'.merge_base_commit.sha',
|
|
1602
1647
|
])
|
|
@@ -1672,9 +1717,7 @@ async function runEvidence(
|
|
|
1672
1717
|
// a failed one cannot hold a draft lift for a reason the check never had.
|
|
1673
1718
|
const [pullFiles, base] = await Promise.all([
|
|
1674
1719
|
listPullFiles(root, identity.number),
|
|
1675
|
-
isCheck
|
|
1676
|
-
? undefined
|
|
1677
|
-
: readPullMergeBase(root, identity.number, identity.head),
|
|
1720
|
+
isCheck ? undefined : readPullMergeBase(root, identity.base, identity.head),
|
|
1678
1721
|
])
|
|
1679
1722
|
if (pullFiles === undefined) {
|
|
1680
1723
|
return refuseWith(
|
|
@@ -1695,30 +1738,12 @@ async function runEvidence(
|
|
|
1695
1738
|
let carriedPreview: string | undefined
|
|
1696
1739
|
let carriedChecklist: string | undefined
|
|
1697
1740
|
let carriedLocal: string | undefined
|
|
1698
|
-
const commentsRow = await gh(root, [
|
|
1699
|
-
'pr',
|
|
1700
|
-
'view',
|
|
1701
|
-
String(identity.number),
|
|
1702
|
-
'--json',
|
|
1703
|
-
'comments',
|
|
1704
|
-
])
|
|
1705
1741
|
// An unread thread refuses rather than rendering, since a body built
|
|
1706
1742
|
// without it knows neither the comment to edit nor the preview address
|
|
1707
1743
|
// to carry, and posting it would duplicate the comment and drop the link.
|
|
1708
1744
|
// It refuses ahead of no-evidence too, so that record never reports an
|
|
1709
1745
|
// absent field it did not read.
|
|
1710
|
-
|
|
1711
|
-
if (commentsRow !== null) {
|
|
1712
|
-
try {
|
|
1713
|
-
comments = (
|
|
1714
|
-
JSON.parse(commentsRow) as {
|
|
1715
|
-
comments?: readonly { url?: string; body: string }[]
|
|
1716
|
-
}
|
|
1717
|
-
).comments
|
|
1718
|
-
} catch {
|
|
1719
|
-
comments = undefined
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1746
|
+
const comments = await listComments(root, String(identity.number))
|
|
1722
1747
|
if (comments === undefined) {
|
|
1723
1748
|
return refuseWith(
|
|
1724
1749
|
'gh-failed',
|
|
@@ -1803,12 +1828,7 @@ async function runEvidence(
|
|
|
1803
1828
|
return 0
|
|
1804
1829
|
}
|
|
1805
1830
|
|
|
1806
|
-
const
|
|
1807
|
-
const repo =
|
|
1808
|
-
repoRow === null
|
|
1809
|
-
? undefined
|
|
1810
|
-
: (JSON.parse(repoRow) as { nameWithOwner?: string }).nameWithOwner
|
|
1811
|
-
|
|
1831
|
+
const repo = await readRepoName(root)
|
|
1812
1832
|
if (repo === undefined) {
|
|
1813
1833
|
return refuseWith(
|
|
1814
1834
|
'gh-failed',
|
|
@@ -2278,23 +2298,19 @@ async function runLocalRemove(
|
|
|
2278
2298
|
return refuseWith('gh-missing', PULL_REFUSALS['gh-missing'], emitJson, root)
|
|
2279
2299
|
}
|
|
2280
2300
|
|
|
2281
|
-
const
|
|
2282
|
-
if (
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
comments?: readonly { url?: string; body: string }[]
|
|
2293
|
-
}
|
|
2294
|
-
).comments
|
|
2295
|
-
} catch {
|
|
2296
|
-
comments = undefined
|
|
2301
|
+
const resolved = await resolvePullNumber(root, number)
|
|
2302
|
+
if (resolved.kind === 'refused') {
|
|
2303
|
+
const reason = resolved.reason
|
|
2304
|
+
return refuseWith(
|
|
2305
|
+
reason,
|
|
2306
|
+
reason === 'ambiguous-pull'
|
|
2307
|
+
? PULL_REFUSALS[reason]
|
|
2308
|
+
: LOCAL_REFUSALS[reason],
|
|
2309
|
+
emitJson,
|
|
2310
|
+
root,
|
|
2311
|
+
)
|
|
2297
2312
|
}
|
|
2313
|
+
const comments = await listComments(root, resolved.number)
|
|
2298
2314
|
if (comments === undefined) {
|
|
2299
2315
|
return refuseWith('gh-failed', LOCAL_REFUSALS['gh-failed'], emitJson, root)
|
|
2300
2316
|
}
|
|
@@ -2531,11 +2547,12 @@ function framesArgumentRefusal(
|
|
|
2531
2547
|
return undefined
|
|
2532
2548
|
}
|
|
2533
2549
|
|
|
2550
|
+
/** `owner/repo` off the REST repository read, since `gh repo view` runs on GraphQL. */
|
|
2534
2551
|
async function readRepoName(root: string): Promise<string | undefined> {
|
|
2535
|
-
const row = await gh(root, ['
|
|
2552
|
+
const row = await gh(root, ['api', 'repos/{owner}/{repo}'])
|
|
2536
2553
|
if (row === null) return undefined
|
|
2537
2554
|
try {
|
|
2538
|
-
const name = (JSON.parse(row) as {
|
|
2555
|
+
const name = (JSON.parse(row) as { full_name?: unknown }).full_name
|
|
2539
2556
|
return typeof name === 'string' ? name : undefined
|
|
2540
2557
|
} catch {
|
|
2541
2558
|
return undefined
|