@erclx/canon 5.65.0 → 5.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +35 -0
- package/claude/skills/app-drive/SKILL.md +112 -0
- package/claude/skills/app-drive/references/config.md +38 -0
- package/claude/skills/app-drive/references/drive-record.md +70 -0
- package/claude/skills/app-drive/references/isolate.md +46 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/create-skill/references/skill.md +1 -1
- package/claude/skills/git-pr/references/labels.md +6 -2
- package/claude/skills/git-pr-close/REQUIREMENT.md +29 -0
- package/claude/skills/git-pr-close/SKILL.md +55 -0
- package/claude/skills/git-pr-close/references/close.md +54 -0
- package/claude/skills/plan-feature/references/plan.md +1 -1
- package/claude/skills/plan-intake/references/intake.md +12 -2
- package/claude/skills/pr-evidence/SKILL.md +2 -2
- package/claude/skills/pr-evidence/references/evidence.md +3 -1
- package/claude/skills/review-ui/SKILL.md +5 -4
- package/claude/skills/review-ui/references/drive-copy.md +4 -0
- package/claude/skills/review-ui/references/driving.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +15 -2
- package/claude/skills/role-planner/SKILL.md +2 -2
- package/claude/skills/role-worker/SKILL.md +6 -8
- package/claude/skills/session-map/REQUIREMENT.md +1 -1
- package/claude/skills/session-map/SKILL.md +8 -1
- package/claude/skills/session-map/references/session.md +19 -5
- package/claude/skills/ui-record/REQUIREMENT.md +34 -0
- package/claude/skills/ui-record/SKILL.md +103 -0
- package/claude/skills/ui-record/references/config.md +40 -0
- package/claude/skills/ui-record/references/handoff.md +48 -0
- package/claude/skills/ui-record/references/take-craft.md +92 -0
- package/docs/agents/records.md +1 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +1 -0
- package/docs/workflow/skill-map.md +2 -0
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/package.json +1 -1
- package/src/audits/catalog.ts +5 -1
- package/src/browser/agent-cli.ts +68 -0
- package/src/claude/cases/git.ts +4 -0
- package/src/claude/cases/workflow.ts +20 -0
- package/src/cli/help.ts +3 -0
- package/src/cli.ts +4 -0
- package/src/commands/browser.ts +102 -0
- package/src/commands/intake.ts +181 -1
- package/src/commands/labels.ts +152 -4
- package/src/commands/pr.ts +278 -8
- package/src/commands/records.ts +18 -1
- package/src/commands/take.ts +266 -0
- package/src/context/requirements.ts +1 -11
- package/src/git/status.ts +73 -0
- package/src/intake/drive-check.ts +355 -0
- package/src/intake/folder.ts +1 -1
- package/src/labels/apply.ts +10 -5
- package/src/labels/sync.ts +17 -0
- package/src/markdown/scan.ts +10 -0
- package/src/pr/close.ts +152 -0
- package/src/pr/drive-copy.ts +97 -84
- package/src/pr/health.ts +56 -0
- package/src/pr/local.ts +49 -8
- package/src/pr/rest.ts +10 -2
- package/src/records/session-map.ts +75 -0
- package/src/records/validate.ts +64 -8
- package/src/sandbox/replay/run.ts +30 -5
- package/src/take/camera.ts +309 -0
- package/src/take/encode.ts +170 -0
- package/src/take/film.ts +206 -0
- package/src/take/fixtures/fade.html +49 -0
- package/src/take/fixtures/fetch.html +38 -0
- package/src/take/log.ts +109 -0
- package/src/take/perform.ts +200 -0
- package/src/take/plan.ts +193 -0
- package/src/take/screencast.ts +94 -0
- package/src/take/shell.ts +287 -0
- package/src/take/stepper.ts +252 -0
- package/src/take/zoom/NOTICE.md +45 -0
- package/src/take/zoom/math-utils.ts +69 -0
- package/src/take/zoom/region-utils.ts +55 -0
- package/src/take/zoom/spring.ts +133 -0
- package/src/tasks/archive.ts +1 -3
- package/src/tooling/verify-stack.ts +4 -5
- package/tooling/base/reference.md +1 -0
- package/tooling/base/seeds/canon/config/pr-labels.toml +41 -0
- package/claude/skills/review-ui/scripts/pw.sh +0 -60
package/src/commands/pr.ts
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
readBaseStanding,
|
|
75
75
|
readChangedPaths,
|
|
76
76
|
} from '@/pr/lift'
|
|
77
|
+
import { close, type CloseApi, type CloseRefusal } from '@/pr/close'
|
|
77
78
|
import {
|
|
78
79
|
merge,
|
|
79
80
|
type MergeApi,
|
|
@@ -82,16 +83,15 @@ import {
|
|
|
82
83
|
type MergeRequest,
|
|
83
84
|
} from '@/pr/merge'
|
|
84
85
|
import { listWorktrees } from '@/git/worktree'
|
|
86
|
+
import { branchSlug, buildDriveCopy, loadPreviewConfig } from '@/pr/drive-copy'
|
|
85
87
|
import {
|
|
86
|
-
branchSlug,
|
|
87
|
-
buildDriveCopy,
|
|
88
88
|
changedSince,
|
|
89
|
-
loadPreviewConfig,
|
|
90
89
|
readWorktreeStatus,
|
|
91
90
|
type StatusEntry,
|
|
92
|
-
} from '@/
|
|
91
|
+
} from '@/git/status'
|
|
93
92
|
import { machineListenerReader } from '@/listeners/machine'
|
|
94
93
|
import { loadPaintedPatterns } from '@/pr/painted'
|
|
94
|
+
import { LOCAL_CONFIG_REL, readHealthPaths } from '@/pr/health'
|
|
95
95
|
import {
|
|
96
96
|
dropAddressLine,
|
|
97
97
|
maskLocalAddress,
|
|
@@ -174,6 +174,12 @@ interface MergeCommandOptions extends ReadOptions {
|
|
|
174
174
|
readonly dryRun?: boolean
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
interface CloseCommandOptions extends ReadOptions {
|
|
178
|
+
readonly reasonFile?: string
|
|
179
|
+
readonly head?: string
|
|
180
|
+
readonly keepBranch?: boolean
|
|
181
|
+
}
|
|
182
|
+
|
|
177
183
|
interface ReviewStateOptions extends ReadOptions {
|
|
178
184
|
readonly wait?: string
|
|
179
185
|
}
|
|
@@ -244,9 +250,15 @@ const AMBIGUOUS_PULL =
|
|
|
244
250
|
/** How long the probe waits on a listener before reading it as not a server. */
|
|
245
251
|
const LOCAL_PROBE_TIMEOUT_MS = 2_000
|
|
246
252
|
|
|
247
|
-
const LOCAL_REFUSALS: Record<
|
|
253
|
+
const LOCAL_REFUSALS: Record<
|
|
254
|
+
LocalRefusal | 'invalid-config' | 'gh-failed',
|
|
255
|
+
string
|
|
256
|
+
> = {
|
|
248
257
|
'no-server':
|
|
249
258
|
'Nothing listening inside this worktree served an HTML page. Start the dev server here and re-run to get a link.',
|
|
259
|
+
unhealthy:
|
|
260
|
+
'A page answered inside this worktree, but a health path it declares failed. Start what the page reaches and re-run to get a link.',
|
|
261
|
+
'invalid-config': `${LOCAL_CONFIG_REL} cannot be read, so no health path was checked and no link is given. Fix or delete it and re-run.`,
|
|
250
262
|
'no-listener-reader':
|
|
251
263
|
'This machine offers neither lsof nor /proc, so no listening socket could be read. Nothing is posted.',
|
|
252
264
|
'gh-failed':
|
|
@@ -991,6 +1003,60 @@ export function register(program: Command): void {
|
|
|
991
1003
|
process.exitCode = await runMerge(number, opts)
|
|
992
1004
|
})
|
|
993
1005
|
|
|
1006
|
+
pr.command('close')
|
|
1007
|
+
.description(
|
|
1008
|
+
'Close a pull request unmerged, posting the reason first and deleting its head branch',
|
|
1009
|
+
)
|
|
1010
|
+
.argument('<number>', 'Pull request to close')
|
|
1011
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
1012
|
+
.option('--reason-file <path>', 'Closing comment to post, required')
|
|
1013
|
+
.option(
|
|
1014
|
+
'--head <branch>',
|
|
1015
|
+
'Head branch the pull request must carry, defaulting to the current branch',
|
|
1016
|
+
)
|
|
1017
|
+
.option('--keep-branch', 'Leave the head branch in place')
|
|
1018
|
+
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
1019
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
1020
|
+
.addHelpText(
|
|
1021
|
+
'after',
|
|
1022
|
+
[
|
|
1023
|
+
'',
|
|
1024
|
+
'The reason is required, so a pull request this verb closes always says',
|
|
1025
|
+
'why. It is checked before anything is read: banned characters, phase',
|
|
1026
|
+
'labels, board references, session links, and density measured on the',
|
|
1027
|
+
'reason itself. A finding refuses reason-unpublishable with the findings',
|
|
1028
|
+
'in the record.',
|
|
1029
|
+
'',
|
|
1030
|
+
'Then the pull request is read and must be open with the head --head',
|
|
1031
|
+
'names. The comment posts first, then the close, then the branch',
|
|
1032
|
+
'delete, so a failure part way leaves a comment on an open pull request',
|
|
1033
|
+
'rather than a close with no reason. A failed delete never fails the',
|
|
1034
|
+
'close, and a head in another repository is never deleted.',
|
|
1035
|
+
'',
|
|
1036
|
+
'Record: number, commentUrl, branch (deleted, kept, fork, delete-failed),',
|
|
1037
|
+
'findings on a reason-unpublishable refusal',
|
|
1038
|
+
'',
|
|
1039
|
+
'Reasons:',
|
|
1040
|
+
' closed',
|
|
1041
|
+
' no-reason, reason-unreadable, reason-unpublishable, no-head,',
|
|
1042
|
+
' gh-failed, pull-closed, head-mismatch, comment-failed, close-failed',
|
|
1043
|
+
'',
|
|
1044
|
+
'Every call is REST, never `gh pr close`, which runs on GraphQL.',
|
|
1045
|
+
'',
|
|
1046
|
+
'Exit codes:',
|
|
1047
|
+
' 0 closed',
|
|
1048
|
+
' 1 refused, with the reason in the JSON record',
|
|
1049
|
+
'',
|
|
1050
|
+
'Examples:',
|
|
1051
|
+
' canon pr close 1341 --reason-file reason.md --json',
|
|
1052
|
+
' canon pr close 1341 --reason-file reason.md --head feat/old --keep-branch',
|
|
1053
|
+
'',
|
|
1054
|
+
].join('\n'),
|
|
1055
|
+
)
|
|
1056
|
+
.action(async (number: string, opts: CloseCommandOptions) => {
|
|
1057
|
+
process.exitCode = await runClose(number, opts)
|
|
1058
|
+
})
|
|
1059
|
+
|
|
994
1060
|
pr.command('preview')
|
|
995
1061
|
.description(
|
|
996
1062
|
"Deploy the pull request's branch to a Cloudflare Pages preview and report its address",
|
|
@@ -1090,6 +1156,13 @@ export function register(program: Command): void {
|
|
|
1090
1156
|
'port serving an HTML page is reported, which skips a test runner or a',
|
|
1091
1157
|
'reload socket answering in the same tree.',
|
|
1092
1158
|
'',
|
|
1159
|
+
'A project can declare paths the page must also serve, in',
|
|
1160
|
+
'canon/config/local.toml as health = ["/api/health"]. Each is requested',
|
|
1161
|
+
"on the page's own port, redirects followed, and only a final 2xx",
|
|
1162
|
+
'passes. A page failing one is skipped for the next page port, and the',
|
|
1163
|
+
"refusal names the lowest page port's first failing path. An absent",
|
|
1164
|
+
'file or an empty list checks the page alone.',
|
|
1165
|
+
'',
|
|
1093
1166
|
'A process in a sibling worktree, in the main checkout seen from a',
|
|
1094
1167
|
'linked worktree, or in a linked worktree seen from the main checkout',
|
|
1095
1168
|
'is never reported, since its server shows another branch. Sockets are',
|
|
@@ -1112,6 +1185,11 @@ export function register(program: Command): void {
|
|
|
1112
1185
|
' ok a page answered, with its address in `url`',
|
|
1113
1186
|
' no-server nothing inside this worktree served a page',
|
|
1114
1187
|
' no-listener-reader neither lsof nor /proc is available',
|
|
1188
|
+
' unhealthy a page answered but a declared health path did',
|
|
1189
|
+
' not, with `port`, `path`, and `status`, which',
|
|
1190
|
+
' is null on a timeout',
|
|
1191
|
+
' invalid-config canon/config/local.toml is not valid TOML, or',
|
|
1192
|
+
' health is not a list of paths starting with /',
|
|
1115
1193
|
' removed --remove dropped the line',
|
|
1116
1194
|
' no-comment --remove found no marked comment, a no-op',
|
|
1117
1195
|
' no-line --remove found no address line, a no-op',
|
|
@@ -1226,18 +1304,28 @@ export function register(program: Command): void {
|
|
|
1226
1304
|
[
|
|
1227
1305
|
'',
|
|
1228
1306
|
'A project names what its preview writes in',
|
|
1229
|
-
'`canon/config/preview.json` as
|
|
1307
|
+
'`canon/config/preview.json` as',
|
|
1308
|
+
'`{ "writes": [path], "keep": [path], "env": "<NAME>" }`.',
|
|
1230
1309
|
'Each `writes` path is relative to the worktree, and `env`, defaulting',
|
|
1231
1310
|
'to `CANON_PREVIEW_ROOT`, is the variable the server reads for the copy',
|
|
1232
1311
|
'root. The server resolves each declared path under that root.',
|
|
1233
1312
|
'',
|
|
1313
|
+
'Each `keep` path sits below a `writes` path, never equal to one, and',
|
|
1314
|
+
'names state the running server owns there, such as a run file. A',
|
|
1315
|
+
"rebuild leaves it in the copy and never copies the worktree's own, so",
|
|
1316
|
+
'list it for a server that writes under a declared path. The key is',
|
|
1317
|
+
'optional.',
|
|
1318
|
+
'',
|
|
1234
1319
|
'The copy lives at `.canon/tmp/pr/review-ui/<slug>/board/` at the main',
|
|
1235
1320
|
'worktree root, where `<slug>` is the branch with a leading type',
|
|
1236
1321
|
'segment dropped and each `/` made `-`. With a number the branch is the',
|
|
1237
1322
|
"pull request's head, and without one it is the checkout's, so the",
|
|
1238
1323
|
'worker starting its server before a pull request exists and the',
|
|
1239
1324
|
'reviewer naming the number land on one copy. Every run but --check',
|
|
1240
|
-
"
|
|
1325
|
+
"resets each declared path from the worktree, dropping the last pass's",
|
|
1326
|
+
'drives, and leaves the rest of the copy root and each `keep` path as',
|
|
1327
|
+
'they are. A page added only to the copy, inside a declared path, is',
|
|
1328
|
+
'wiped.',
|
|
1241
1329
|
'',
|
|
1242
1330
|
'--baseline records `git status` for the worktree under',
|
|
1243
1331
|
'`.canon/tmp/pr/review-ui/<number>/`, and --check reports each path',
|
|
@@ -3005,6 +3093,19 @@ function machineLocalRunner(): LocalRunner {
|
|
|
3005
3093
|
return false
|
|
3006
3094
|
}
|
|
3007
3095
|
},
|
|
3096
|
+
// The final status after redirects, so a login bounce ending on a 3xx or
|
|
3097
|
+
// a 4xx fails rather than reading as the backend answering.
|
|
3098
|
+
async check(port, path) {
|
|
3099
|
+
try {
|
|
3100
|
+
const response = await fetch(`http://localhost:${port}${path}`, {
|
|
3101
|
+
signal: AbortSignal.timeout(LOCAL_PROBE_TIMEOUT_MS),
|
|
3102
|
+
})
|
|
3103
|
+
await response.body?.cancel()
|
|
3104
|
+
return response.status
|
|
3105
|
+
} catch {
|
|
3106
|
+
return null
|
|
3107
|
+
}
|
|
3108
|
+
},
|
|
3008
3109
|
}
|
|
3009
3110
|
}
|
|
3010
3111
|
|
|
@@ -3025,7 +3126,35 @@ async function runLocal(opts: LocalOptions): Promise<number> {
|
|
|
3025
3126
|
intro('canon pr local')
|
|
3026
3127
|
|
|
3027
3128
|
const worktree = await resolveWorktreeRoot(root)
|
|
3028
|
-
const
|
|
3129
|
+
const declaration = readHealthPaths(worktree)
|
|
3130
|
+
if (declaration.kind === 'invalid') {
|
|
3131
|
+
return refuseWith(
|
|
3132
|
+
'invalid-config',
|
|
3133
|
+
`${declaration.reason}. ${LOCAL_REFUSALS['invalid-config']}`,
|
|
3134
|
+
emitJson,
|
|
3135
|
+
root,
|
|
3136
|
+
)
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
const result = await findLocalServer(
|
|
3140
|
+
worktree,
|
|
3141
|
+
machineLocalRunner(),
|
|
3142
|
+
declaration.kind === 'paths' ? declaration.paths : [],
|
|
3143
|
+
)
|
|
3144
|
+
if (result.kind === 'refused' && result.reason === 'unhealthy') {
|
|
3145
|
+
const { port, path, status } = result
|
|
3146
|
+
const answer =
|
|
3147
|
+
status === null
|
|
3148
|
+
? `gave no answer within ${LOCAL_PROBE_TIMEOUT_MS / 1000} seconds`
|
|
3149
|
+
: `answered ${status}`
|
|
3150
|
+
return refuseWith(
|
|
3151
|
+
'unhealthy',
|
|
3152
|
+
`${LOCAL_REFUSALS.unhealthy} ${path} on http://localhost:${port} ${answer}.`,
|
|
3153
|
+
emitJson,
|
|
3154
|
+
root,
|
|
3155
|
+
{ port, path, status },
|
|
3156
|
+
)
|
|
3157
|
+
}
|
|
3029
3158
|
if (result.kind === 'refused') {
|
|
3030
3159
|
return refuseWith(
|
|
3031
3160
|
result.reason,
|
|
@@ -4364,6 +4493,7 @@ async function runDriveCopy(
|
|
|
4364
4493
|
target.worktree,
|
|
4365
4494
|
copyRoot,
|
|
4366
4495
|
config.config.writes,
|
|
4496
|
+
config.config.keep,
|
|
4367
4497
|
)
|
|
4368
4498
|
logStep('Copied')
|
|
4369
4499
|
logInfo(`${config.config.env}=${copyRoot}`)
|
|
@@ -4394,6 +4524,146 @@ async function runDriveCopy(
|
|
|
4394
4524
|
return 0
|
|
4395
4525
|
}
|
|
4396
4526
|
|
|
4527
|
+
type CloseCommandRefusal = 'reason-unreadable' | 'no-head'
|
|
4528
|
+
|
|
4529
|
+
const CLOSE_REFUSALS: Record<CloseRefusal | CloseCommandRefusal, string> = {
|
|
4530
|
+
'no-reason':
|
|
4531
|
+
'No closing reason was given. Write it to a file and pass --reason-file.',
|
|
4532
|
+
'reason-unreadable': 'The --reason-file could not be read.',
|
|
4533
|
+
'reason-unpublishable':
|
|
4534
|
+
'The reason carries text a reader on the remote cannot use or that breaks the density limits. The findings are in the record.',
|
|
4535
|
+
'no-head':
|
|
4536
|
+
'This checkout is on no branch, so pass --head with the branch the pull request carries.',
|
|
4537
|
+
'gh-failed': 'gh could not read the pull request, so nothing was written.',
|
|
4538
|
+
'pull-closed': 'The pull request is already closed.',
|
|
4539
|
+
'head-mismatch':
|
|
4540
|
+
'The pull request carries another head branch than the one expected. Nothing was written to it.',
|
|
4541
|
+
'comment-failed':
|
|
4542
|
+
'The closing comment did not post, so the pull request was left open.',
|
|
4543
|
+
'close-failed':
|
|
4544
|
+
'The comment posted but the close was refused, so the pull request is still open. Close it by hand or re-run.',
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
function closeApi(root: string, number: string): CloseApi {
|
|
4548
|
+
return {
|
|
4549
|
+
readPull: async () => {
|
|
4550
|
+
const stdout = await gh(root, [
|
|
4551
|
+
'api',
|
|
4552
|
+
`repos/{owner}/{repo}/pulls/${number}`,
|
|
4553
|
+
])
|
|
4554
|
+
if (stdout === null) return undefined
|
|
4555
|
+
try {
|
|
4556
|
+
const row = JSON.parse(stdout) as RestPull
|
|
4557
|
+
if (typeof row.head?.ref !== 'string') return undefined
|
|
4558
|
+
const headRepo = row.head.repo?.full_name
|
|
4559
|
+
return {
|
|
4560
|
+
isOpen: row.state === 'open',
|
|
4561
|
+
branch: row.head.ref,
|
|
4562
|
+
isFork:
|
|
4563
|
+
headRepo === undefined || headRepo !== row.base?.repo?.full_name,
|
|
4564
|
+
}
|
|
4565
|
+
} catch {
|
|
4566
|
+
return undefined
|
|
4567
|
+
}
|
|
4568
|
+
},
|
|
4569
|
+
postComment: async (body) => {
|
|
4570
|
+
const url = await gh(root, [
|
|
4571
|
+
'api',
|
|
4572
|
+
'-X',
|
|
4573
|
+
'POST',
|
|
4574
|
+
`repos/{owner}/{repo}/issues/${number}/comments`,
|
|
4575
|
+
'-f',
|
|
4576
|
+
`body=${body}`,
|
|
4577
|
+
'--jq',
|
|
4578
|
+
'.html_url',
|
|
4579
|
+
])
|
|
4580
|
+
return url === null || url.trim() === '' ? undefined : url.trim()
|
|
4581
|
+
},
|
|
4582
|
+
patchClosed: async () =>
|
|
4583
|
+
(await gh(root, [
|
|
4584
|
+
'api',
|
|
4585
|
+
'-X',
|
|
4586
|
+
'PATCH',
|
|
4587
|
+
`repos/{owner}/{repo}/pulls/${number}`,
|
|
4588
|
+
'-f',
|
|
4589
|
+
'state=closed',
|
|
4590
|
+
'--silent',
|
|
4591
|
+
])) !== null,
|
|
4592
|
+
deleteBranch: async (branch) =>
|
|
4593
|
+
(await gh(root, [
|
|
4594
|
+
'api',
|
|
4595
|
+
'-X',
|
|
4596
|
+
'DELETE',
|
|
4597
|
+
`repos/{owner}/{repo}/git/refs/heads/${encodeURIComponent(branch)}`,
|
|
4598
|
+
'--silent',
|
|
4599
|
+
])) !== null,
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
async function runClose(
|
|
4604
|
+
number: string,
|
|
4605
|
+
opts: CloseCommandOptions,
|
|
4606
|
+
): Promise<number> {
|
|
4607
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
4608
|
+
const emitJson = opts.json ?? false
|
|
4609
|
+
|
|
4610
|
+
intro('canon pr close')
|
|
4611
|
+
|
|
4612
|
+
const refuseClose = (
|
|
4613
|
+
reason: CloseRefusal | CloseCommandRefusal,
|
|
4614
|
+
extra: Record<string, unknown> = {},
|
|
4615
|
+
): number =>
|
|
4616
|
+
refuseWith(reason, CLOSE_REFUSALS[reason], emitJson, root, {
|
|
4617
|
+
number: Number(number),
|
|
4618
|
+
...extra,
|
|
4619
|
+
})
|
|
4620
|
+
|
|
4621
|
+
if (opts.reasonFile === undefined) return refuseClose('no-reason')
|
|
4622
|
+
let reason: string
|
|
4623
|
+
try {
|
|
4624
|
+
reason = await readFile(resolve(root, opts.reasonFile), 'utf8')
|
|
4625
|
+
} catch {
|
|
4626
|
+
logWarn(`Could not read ${opts.reasonFile}`)
|
|
4627
|
+
return refuseClose('reason-unreadable')
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
let head = opts.head
|
|
4631
|
+
if (head === undefined) {
|
|
4632
|
+
const branch = await $`git -C ${root} symbolic-ref --short -q HEAD`
|
|
4633
|
+
.env(gitEnv())
|
|
4634
|
+
.quiet()
|
|
4635
|
+
.nothrow()
|
|
4636
|
+
head = branch.exitCode === 0 ? branch.text().trim() : ''
|
|
4637
|
+
if (head === '') return refuseClose('no-head')
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
const record = await close(closeApi(root, number), {
|
|
4641
|
+
number: Number(number),
|
|
4642
|
+
reason,
|
|
4643
|
+
head,
|
|
4644
|
+
isKeepBranch: opts.keepBranch ?? false,
|
|
4645
|
+
})
|
|
4646
|
+
|
|
4647
|
+
if (!record.isOk) {
|
|
4648
|
+
return refuseClose(record.reason as CloseRefusal, {
|
|
4649
|
+
...(record.commentUrl !== undefined && { commentUrl: record.commentUrl }),
|
|
4650
|
+
...(record.findings !== undefined && { findings: record.findings }),
|
|
4651
|
+
})
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
logStep('Closed')
|
|
4655
|
+
logInfo(`#${record.number} closed, branch ${record.branch}`)
|
|
4656
|
+
if (record.branch === 'delete-failed') {
|
|
4657
|
+
logWarn(`The branch ${head} could not be deleted. Delete it by hand.`)
|
|
4658
|
+
}
|
|
4659
|
+
outro()
|
|
4660
|
+
|
|
4661
|
+
if (emitJson) {
|
|
4662
|
+
process.stdout.write(`${JSON.stringify({ root, ...record })}\n`)
|
|
4663
|
+
}
|
|
4664
|
+
return 0
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4397
4667
|
function refuseWith(
|
|
4398
4668
|
reason: string,
|
|
4399
4669
|
message: string,
|
package/src/commands/records.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type StructureFinding,
|
|
35
35
|
validateRecords,
|
|
36
36
|
} from '@/records/validate'
|
|
37
|
+
import type { SessionMapLength } from '@/records/session-map'
|
|
37
38
|
import {
|
|
38
39
|
intro,
|
|
39
40
|
logError,
|
|
@@ -92,7 +93,7 @@ export function register(program: Command): void {
|
|
|
92
93
|
.argument('<kind>', `Record folder: ${RECORD_KINDS.join(', ')}`)
|
|
93
94
|
.argument(
|
|
94
95
|
'[record]',
|
|
95
|
-
'One plan stem
|
|
96
|
+
'One plan stem, folder slug, or session map stem, narrowing the report to it',
|
|
96
97
|
)
|
|
97
98
|
.helpOption('-h, --help', 'Show this help message')
|
|
98
99
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
@@ -105,6 +106,7 @@ export function register(program: Command): void {
|
|
|
105
106
|
' plans filename, required sections, and the suggested-and-answer contract',
|
|
106
107
|
' groundwork README and current-state files, numbering, dating, and a half-closed track',
|
|
107
108
|
' intake overview file, numbering, dating, and the four bullets every item carries',
|
|
109
|
+
" sessions each session map's words against its cap, under `length` and never in the exit",
|
|
108
110
|
'',
|
|
109
111
|
'Structure, under `structure` and never in the exit:',
|
|
110
112
|
" S1 to S5 from the plan standard, read on plans, a track's README and",
|
|
@@ -128,6 +130,7 @@ export function register(program: Command): void {
|
|
|
128
130
|
' canon records validate plans',
|
|
129
131
|
' canon records validate plans feature-records-by-heading',
|
|
130
132
|
' canon records validate intake --json',
|
|
133
|
+
' canon records validate sessions session-orchestrator',
|
|
131
134
|
'',
|
|
132
135
|
].join('\n'),
|
|
133
136
|
)
|
|
@@ -828,6 +831,7 @@ function report(
|
|
|
828
831
|
records: outcome.records,
|
|
829
832
|
findings: outcome.findings,
|
|
830
833
|
structure: outcome.structure,
|
|
834
|
+
...(outcome.length && { length: outcome.length }),
|
|
831
835
|
})}\n`,
|
|
832
836
|
)
|
|
833
837
|
} else {
|
|
@@ -846,6 +850,14 @@ function report(
|
|
|
846
850
|
logStep('Structure, reported and never in the exit')
|
|
847
851
|
for (const entry of outcome.structure) logInfo(describeStructure(entry))
|
|
848
852
|
}
|
|
853
|
+
|
|
854
|
+
if (outcome.length) {
|
|
855
|
+
logStep('Length, reported and never in the exit')
|
|
856
|
+
for (const entry of outcome.length) {
|
|
857
|
+
const log = entry.over ? logWarn : logInfo
|
|
858
|
+
log(describeLength(entry))
|
|
859
|
+
}
|
|
860
|
+
}
|
|
849
861
|
outro()
|
|
850
862
|
}
|
|
851
863
|
|
|
@@ -857,6 +869,11 @@ function describe(found: Finding): string {
|
|
|
857
869
|
return `${scope}${found.subject} ${found.message}`
|
|
858
870
|
}
|
|
859
871
|
|
|
872
|
+
function describeLength(entry: SessionMapLength): string {
|
|
873
|
+
const verdict = entry.over ? 'over the cap' : 'within the cap'
|
|
874
|
+
return `${entry.record}: ${entry.words} of ${entry.cap} words, ${verdict}`
|
|
875
|
+
}
|
|
876
|
+
|
|
860
877
|
function describeStructure(entry: StructureFinding): string {
|
|
861
878
|
const scope = entry.record === entry.subject ? '' : `${entry.record}/`
|
|
862
879
|
return `${entry.criterion} ${scope}${entry.subject}:${entry.line} ${entry.message}`
|