@erclx/canon 5.46.0 → 5.47.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/git-followup/SKILL.md +1 -0
- package/claude/skills/git-pr/REQUIREMENT.md +2 -2
- package/claude/skills/git-pr/SKILL.md +2 -1
- package/claude/skills/git-pr/references/pr.md +58 -7
- package/claude/skills/review-address/SKILL.md +18 -6
- package/claude/skills/review-address/references/post-review-findings.md +4 -1
- package/claude/skills/review-address/references/rebase-only.md +14 -2
- package/claude/skills/review-pr/SKILL.md +9 -2
- package/claude/skills/review-pr/references/body-variants.md +29 -8
- package/claude/skills/review-pr/references/post.md +29 -10
- package/claude/skills/review-ui/SKILL.md +1 -0
- package/claude/skills/review-ui/references/post.md +2 -0
- package/claude/skills/review-ui/references/tick.md +3 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +3 -0
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +1 -0
- package/claude/skills/task-board/references/decline.md +2 -2
- package/claude/skills/task-complete/references/archive.md +3 -2
- package/claude/skills/ui-checklist/SKILL.md +3 -3
- package/docs/workflow/zshrc-aliases.md +3 -0
- package/package.json +1 -1
- package/scripts/core/bootstrap.sh +1 -0
- package/src/commands/labels.ts +66 -1
- package/src/commands/pr.ts +113 -4
- package/src/commands/sessions.ts +25 -11
- package/src/commands/tasks.ts +2 -0
- package/src/labels/density.ts +363 -0
- package/src/pr/evidence.ts +1 -1
- package/src/pr/rest.ts +2 -0
- package/src/pr/tick.ts +89 -0
- package/src/sessions/live.ts +26 -0
- package/src/sessions/resolve.ts +62 -11
- package/src/tasks/archive.ts +15 -4
- package/tooling/base/seeds/.github/pull_request_template.md +1 -1
|
@@ -17,6 +17,7 @@ A hand-written block without markers is detected through its `alias clp=` line,
|
|
|
17
17
|
```zsh
|
|
18
18
|
# >>> canon aliases >>>
|
|
19
19
|
TOOLKIT="/path/to/toolkit"
|
|
20
|
+
export CLAUDE_CODE_ENABLE_TODO_TOOLS=1
|
|
20
21
|
|
|
21
22
|
alias cl='claude'
|
|
22
23
|
alias clr='cl -r'
|
|
@@ -32,6 +33,8 @@ alias clpac='clpa --cwd .'
|
|
|
32
33
|
# <<< canon aliases <<<
|
|
33
34
|
```
|
|
34
35
|
|
|
36
|
+
The export gives a session started from your terminal the task tools, which Claude Code reads before it starts, so workers and reviewers can show their checklist on `Ctrl+T`. A background session has them without it.
|
|
37
|
+
|
|
35
38
|
To opt out, delete the block and run `bun install` and `bun link` yourself. Bash takes the block unchanged in `~/.bashrc`, pasted by hand. Bootstrap writes `~/.zshrc` alone.
|
|
36
39
|
|
|
37
40
|
## What each one does
|
package/package.json
CHANGED
package/src/commands/labels.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { gitEnv } from '@/git/env'
|
|
|
6
6
|
import { diffLabels } from '@/labels/apply'
|
|
7
7
|
import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
|
|
8
8
|
import { checkClosing } from '@/labels/closing'
|
|
9
|
+
import {
|
|
10
|
+
bodyDensity,
|
|
11
|
+
isPost,
|
|
12
|
+
POST_HEADINGS,
|
|
13
|
+
postDensity,
|
|
14
|
+
} from '@/labels/density'
|
|
9
15
|
import { resolveScanInput } from '@/labels/event'
|
|
10
16
|
import { checkTitleFormat, type TitleFormatIssue } from '@/labels/format'
|
|
11
17
|
import { MAP_REL } from '@/labels/map'
|
|
@@ -241,7 +247,27 @@ export function register(program: Command): void {
|
|
|
241
247
|
'It grades the body of an ordinary pull request against the closed',
|
|
242
248
|
'section list: a level-two heading outside it, or a missing',
|
|
243
249
|
'## Summary or ## Key changes, fails. A review, a release-please body,',
|
|
244
|
-
'and a scan carrying no title skip this check.
|
|
250
|
+
'and a scan carrying no title skip this check.',
|
|
251
|
+
'',
|
|
252
|
+
'It reports the density of that same body per section under `density`,',
|
|
253
|
+
'without failing on it: a bullet past 200 characters, more than 5 plain',
|
|
254
|
+
'bullets or 2 paragraphs in a row, a paragraph past 350 characters, any',
|
|
255
|
+
'H3 or deeper heading, and Technical context mixing bullets with',
|
|
256
|
+
'paragraphs. A bold label alone on a line splits a Key changes run, a',
|
|
257
|
+
'bold-led paragraph starts a new Technical context run, and Testing',
|
|
258
|
+
'takes the heading limit alone. Each finding carries its section, kind,',
|
|
259
|
+
'line in the whole body, value, and limit.',
|
|
260
|
+
'',
|
|
261
|
+
'A body the section check skips is graded as a post under the same',
|
|
262
|
+
'`density` key when its first level-two heading is one of',
|
|
263
|
+
`${POST_HEADINGS.map((heading) => `## ${heading}`).join(', ')}.`,
|
|
264
|
+
'A post takes the same length and run limits, allows an H3, and',
|
|
265
|
+
'measures a nested item as a bullet of its own. The footer, the',
|
|
266
|
+
'marker, and a table never report, a block inside <details> takes the',
|
|
267
|
+
'length limits alone, and a checklist box under ## What to look at',
|
|
268
|
+
'takes no length limit. Post density never fails the scan.',
|
|
269
|
+
'',
|
|
270
|
+
'It also fails a banned',
|
|
245
271
|
'character, an em dash or a semicolon, in the title or body of a pull',
|
|
246
272
|
'request or a review, outside a code span or a link.',
|
|
247
273
|
'',
|
|
@@ -692,6 +718,44 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
692
718
|
}
|
|
693
719
|
}
|
|
694
720
|
|
|
721
|
+
// Reported and never gated yet. New bodies come back clean under the
|
|
722
|
+
// shape in pr.md first, so an older open pull request fails no edit event.
|
|
723
|
+
// A post is never gated, since CI reads one only after it is published.
|
|
724
|
+
const post = !sectionsChecked && isPost(resolved.body)
|
|
725
|
+
const density = sectionsChecked
|
|
726
|
+
? bodyDensity(resolved.body)
|
|
727
|
+
: post
|
|
728
|
+
? postDensity(resolved.body)
|
|
729
|
+
: []
|
|
730
|
+
|
|
731
|
+
logStep(
|
|
732
|
+
!sectionsChecked && !post
|
|
733
|
+
? 'Density not checked'
|
|
734
|
+
: density.length === 0
|
|
735
|
+
? 'Clean'
|
|
736
|
+
: 'Density finding found',
|
|
737
|
+
)
|
|
738
|
+
if (!sectionsChecked && !post) {
|
|
739
|
+
logInfo(
|
|
740
|
+
'only an ordinary pull request body or a post opening on a post heading is graded for density',
|
|
741
|
+
)
|
|
742
|
+
} else if (density.length === 0) {
|
|
743
|
+
logInfo('every section is within its length, run, and heading limits')
|
|
744
|
+
} else {
|
|
745
|
+
logWarn(
|
|
746
|
+
`${plural(density.length, 'density finding')} in the ${post ? 'post' : 'body'}. Regroup the section under bold labels or split the long item rather than cutting a path.`,
|
|
747
|
+
)
|
|
748
|
+
for (const finding of density) {
|
|
749
|
+
const measured =
|
|
750
|
+
finding.value === null
|
|
751
|
+
? ''
|
|
752
|
+
: ` (${finding.value} over ${finding.limit})`
|
|
753
|
+
logWarn(
|
|
754
|
+
`line ${finding.line} in ${finding.section ?? 'the opening'}: ${finding.kind}${measured}`,
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
695
759
|
const banFindings = [
|
|
696
760
|
...scanBans(
|
|
697
761
|
[{ number: 1, text: resolved.title, fenced: false }],
|
|
@@ -781,6 +845,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
781
845
|
sessionLinks: result.sessionLinks,
|
|
782
846
|
titleFormatIssues,
|
|
783
847
|
sections: { checked: sectionsChecked, ...sections },
|
|
848
|
+
density,
|
|
784
849
|
bannedCharacters,
|
|
785
850
|
missingClosures: closing.missing,
|
|
786
851
|
strayClosures: closing.stray,
|
package/src/commands/pr.ts
CHANGED
|
@@ -93,7 +93,13 @@ import {
|
|
|
93
93
|
servesChange,
|
|
94
94
|
type WorkflowFile,
|
|
95
95
|
} from '@/pr/preview'
|
|
96
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
readUiPassHead,
|
|
98
|
+
type TickRefusal,
|
|
99
|
+
tickBoxes,
|
|
100
|
+
type UiPassSummary,
|
|
101
|
+
writeUiPassLine,
|
|
102
|
+
} from '@/pr/tick'
|
|
97
103
|
import {
|
|
98
104
|
commentRowOf,
|
|
99
105
|
identityOf,
|
|
@@ -157,9 +163,16 @@ interface TickOptions extends ReadOptions {
|
|
|
157
163
|
readonly head?: string
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
type TickReason =
|
|
166
|
+
type TickReason =
|
|
167
|
+
| TickRefusal
|
|
168
|
+
| 'stale-head'
|
|
169
|
+
| 'bad-boxes'
|
|
170
|
+
| 'no-comment'
|
|
171
|
+
| 'body-refused'
|
|
161
172
|
|
|
162
173
|
const TICK_REFUSALS: Record<TickReason | 'gh-failed', string> = {
|
|
174
|
+
'body-refused':
|
|
175
|
+
'The boxes are ticked on the evidence comment, but gh could not edit the pull request body, so its UI pass line still shows the earlier result. Re-run the tick to retry the body.',
|
|
163
176
|
'stale-head':
|
|
164
177
|
'The head named is not the branch tip on the remote, so a tick would claim a pass on a commit that is no longer the pull request. Re-drive the checklist at the tip.',
|
|
165
178
|
'bad-boxes':
|
|
@@ -736,6 +749,11 @@ export function register(program: Command): void {
|
|
|
736
749
|
'comment. Together with --checklist the link and the checklist land in',
|
|
737
750
|
'one comment.',
|
|
738
751
|
'',
|
|
752
|
+
"A render at a new head also resets the pull request body's UI pass",
|
|
753
|
+
'line, the one `canon pr tick` writes under ## Testing, to unchecked',
|
|
754
|
+
'naming that head. It touches no other body text, leaves a line already',
|
|
755
|
+
'naming this head alone, and reports `uiPass` as reset or reset-refused.',
|
|
756
|
+
'',
|
|
739
757
|
'Both reasons carry what the marked comment already posted, each field',
|
|
740
758
|
'present only when that comment holds it:',
|
|
741
759
|
' preview the hosted **Preview:** address the comment carries',
|
|
@@ -1024,8 +1042,17 @@ export function register(program: Command): void {
|
|
|
1024
1042
|
'it. A later render at another head clears a tick whose stamp names',
|
|
1025
1043
|
'a different commit.',
|
|
1026
1044
|
'',
|
|
1045
|
+
"It also keeps one marked line in the pull request body's ## Testing,",
|
|
1046
|
+
'such as `- [x] UI pass, 17 of 17 boxes passed at <short sha>, 1 taste',
|
|
1047
|
+
'box left to the operator`. The line is ticked only when every box a',
|
|
1048
|
+
'driver can run is ticked, counts taste boxes apart, and is the only',
|
|
1049
|
+
'body text this verb writes, so hand edits elsewhere survive.',
|
|
1050
|
+
'',
|
|
1027
1051
|
'Read `reason` on the JSON record:',
|
|
1028
|
-
' ticked the boxes were ticked
|
|
1052
|
+
' ticked the boxes were ticked and the body line written, with the',
|
|
1053
|
+
' tip in `head`',
|
|
1054
|
+
' body-refused the comment is ticked but gh could not edit the body,',
|
|
1055
|
+
' exit 1 with `commentTicked`. Re-run to retry the body.',
|
|
1029
1056
|
' stale-head --head is not the remote branch tip, nothing written',
|
|
1030
1057
|
' taste-box a named box ends in (taste), nothing written',
|
|
1031
1058
|
' no-box a named number is past the checklist, nothing written',
|
|
@@ -2437,10 +2464,30 @@ async function runEvidence(
|
|
|
2437
2464
|
return 1
|
|
2438
2465
|
}
|
|
2439
2466
|
|
|
2467
|
+
// A UI pass line stamped at an earlier head describes a tree that is gone,
|
|
2468
|
+
// so a render at a new head puts it back to unchecked. A line already
|
|
2469
|
+
// naming this head is left alone, since each body edit reruns the label gate.
|
|
2470
|
+
const priorBody = identity.body ?? ''
|
|
2471
|
+
const stampedHead = readUiPassHead(priorBody)
|
|
2472
|
+
let uiPass: 'reset' | 'reset-refused' | undefined
|
|
2473
|
+
if (stampedHead !== undefined && !identity.head.startsWith(stampedHead)) {
|
|
2474
|
+
const reset = writeUiPassLine(priorBody, {
|
|
2475
|
+
kind: 'reset',
|
|
2476
|
+
head: identity.head,
|
|
2477
|
+
})
|
|
2478
|
+
uiPass = (await patchPullBody(root, identity.number, reset))
|
|
2479
|
+
? 'reset'
|
|
2480
|
+
: 'reset-refused'
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2440
2483
|
logStep('Scope')
|
|
2441
2484
|
logInfo(
|
|
2442
2485
|
`${plural(caseCount, 'case')} across ${plural(states.length, 'state')}`,
|
|
2443
2486
|
)
|
|
2487
|
+
if (uiPass === 'reset') logInfo('UI pass line reset to the new head')
|
|
2488
|
+
if (uiPass === 'reset-refused') {
|
|
2489
|
+
logWarn('gh could not edit the body, so its UI pass line names an old head')
|
|
2490
|
+
}
|
|
2444
2491
|
if (sized.unread.length > 0) {
|
|
2445
2492
|
logWarn(
|
|
2446
2493
|
`width unread for ${plural(sized.unread.length, 'image')}, rendered at 1000: ${sized.unread.join(', ')}`,
|
|
@@ -2469,6 +2516,7 @@ async function runEvidence(
|
|
|
2469
2516
|
body,
|
|
2470
2517
|
states,
|
|
2471
2518
|
...(sized.unread.length > 0 && { widthUnread: sized.unread }),
|
|
2519
|
+
...(uiPass !== undefined && { uiPass }),
|
|
2472
2520
|
...(commentId !== undefined && { commentId }),
|
|
2473
2521
|
...carried,
|
|
2474
2522
|
})}\n`,
|
|
@@ -2941,9 +2989,53 @@ async function runTick(
|
|
|
2941
2989
|
if (patched === null) return refuseTick('gh-failed')
|
|
2942
2990
|
}
|
|
2943
2991
|
|
|
2992
|
+
const boxes = readChecklistBoxes(
|
|
2993
|
+
findEvidenceChecklist([{ body: result.body }]) ?? '',
|
|
2994
|
+
)
|
|
2995
|
+
const driven = boxes.filter((box) => !box.isTaste)
|
|
2996
|
+
const summary: UiPassSummary = {
|
|
2997
|
+
kind: 'passed',
|
|
2998
|
+
// A tick counts as this pass's only when stamped at the tip, since an
|
|
2999
|
+
// unstamped one is the author's claim and an older stamp names another head.
|
|
3000
|
+
passed: driven.filter(
|
|
3001
|
+
(box) =>
|
|
3002
|
+
box.isTicked &&
|
|
3003
|
+
box.stamp !== undefined &&
|
|
3004
|
+
reading.tip.startsWith(box.stamp),
|
|
3005
|
+
).length,
|
|
3006
|
+
total: driven.length,
|
|
3007
|
+
taste: boxes.length - driven.length,
|
|
3008
|
+
head: reading.tip,
|
|
3009
|
+
}
|
|
3010
|
+
const priorBody = identity.body ?? ''
|
|
3011
|
+
const nextBody = writeUiPassLine(priorBody, summary)
|
|
3012
|
+
if (
|
|
3013
|
+
nextBody !== priorBody &&
|
|
3014
|
+
!(await patchPullBody(root, identity.number, nextBody))
|
|
3015
|
+
) {
|
|
3016
|
+
logStep('Partly ticked')
|
|
3017
|
+
logWarn(TICK_REFUSALS['body-refused'])
|
|
3018
|
+
outro()
|
|
3019
|
+
if (emitJson) {
|
|
3020
|
+
process.stdout.write(
|
|
3021
|
+
`${JSON.stringify({
|
|
3022
|
+
root,
|
|
3023
|
+
number: identity.number,
|
|
3024
|
+
reason: 'body-refused',
|
|
3025
|
+
message: TICK_REFUSALS['body-refused'],
|
|
3026
|
+
commentTicked: true,
|
|
3027
|
+
head: reading.tip,
|
|
3028
|
+
boxes: numbers.map(Number),
|
|
3029
|
+
commentId,
|
|
3030
|
+
})}\n`,
|
|
3031
|
+
)
|
|
3032
|
+
}
|
|
3033
|
+
return 1
|
|
3034
|
+
}
|
|
3035
|
+
|
|
2944
3036
|
logStep('Ticked')
|
|
2945
3037
|
logInfo(
|
|
2946
|
-
`${plural(numbers.length, 'box')} at ${reading.tip.slice(0, 7)} on comment ${commentId}`,
|
|
3038
|
+
`${plural(numbers.length, 'box')} at ${reading.tip.slice(0, 7)} on comment ${commentId}, UI pass line on the body`,
|
|
2947
3039
|
)
|
|
2948
3040
|
outro()
|
|
2949
3041
|
if (emitJson) {
|
|
@@ -2961,6 +3053,23 @@ async function runTick(
|
|
|
2961
3053
|
return 0
|
|
2962
3054
|
}
|
|
2963
3055
|
|
|
3056
|
+
/** Replaces a pull request's body, which `gh` takes through the REST pulls endpoint a cloud proxy serves. */
|
|
3057
|
+
async function patchPullBody(
|
|
3058
|
+
root: string,
|
|
3059
|
+
number: number,
|
|
3060
|
+
body: string,
|
|
3061
|
+
): Promise<boolean> {
|
|
3062
|
+
const patched = await gh(root, [
|
|
3063
|
+
'api',
|
|
3064
|
+
'-X',
|
|
3065
|
+
'PATCH',
|
|
3066
|
+
`repos/{owner}/{repo}/pulls/${number}`,
|
|
3067
|
+
'-f',
|
|
3068
|
+
`body=${body}`,
|
|
3069
|
+
])
|
|
3070
|
+
return patched !== null
|
|
3071
|
+
}
|
|
3072
|
+
|
|
2964
3073
|
/** One `gh api` call, keeping the HTTP status of a failure so a refused write can be told from a missing ref. */
|
|
2965
3074
|
type ApiCall =
|
|
2966
3075
|
| { readonly ok: true; readonly data: unknown }
|
package/src/commands/sessions.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Command } from 'commander'
|
|
|
3
3
|
import { checkClaim, type ClaimReport } from '@/sessions/claim'
|
|
4
4
|
import {
|
|
5
5
|
callerIdentity,
|
|
6
|
+
confidenceOf,
|
|
6
7
|
repositoryOf,
|
|
7
8
|
type ResolvedSession,
|
|
8
9
|
resolveSessions,
|
|
@@ -134,15 +135,21 @@ export function register(program: Command): void {
|
|
|
134
135
|
'',
|
|
135
136
|
'The confidence field says how the liveness of a row was decided.',
|
|
136
137
|
'"confirmed" matched the running process against the start time the',
|
|
137
|
-
'record stamped. "unverified" means only that the
|
|
138
|
-
'cannot rule out a pid handed to an unrelated
|
|
139
|
-
'reported that way is a candidate list rather than
|
|
138
|
+
'record stamped, and reads "via": "pid". "unverified" means only that the',
|
|
139
|
+
'pid answers, which cannot rule out a pid handed to an unrelated',
|
|
140
|
+
'process, so a roster reported that way is a candidate list rather than',
|
|
141
|
+
'an identity. Where the start time cannot be read, a row whose messaging',
|
|
142
|
+
'socket accepts a connection is confirmed instead and reads "via":',
|
|
143
|
+
'"listener". The probe connects and closes without writing. Each row',
|
|
144
|
+
'carries its own "confidence", and the top-level field is the weakest of',
|
|
145
|
+
'the rows listed, so --branch is not weakened by a row it dropped.',
|
|
140
146
|
'',
|
|
141
147
|
'A reader in another pid namespace, such as a sandboxed shell, cannot',
|
|
142
148
|
'see the pid of a record stamped elsewhere. That row is judged on its',
|
|
143
149
|
'messaging socket instead: it stays on the roster as "unverified" with',
|
|
144
150
|
'"via": "socket", since a crashed session leaves its socket behind. A',
|
|
145
|
-
'foreign record whose socket
|
|
151
|
+
'foreign record whose socket accepts a connection is confirmed as',
|
|
152
|
+
'"listener". One whose socket is gone is left off the roster and',
|
|
146
153
|
'counted in "foreignDead", so an empty roster beside a count above zero',
|
|
147
154
|
'is one this reader could not check, not one whose sessions ended.',
|
|
148
155
|
'',
|
|
@@ -471,7 +478,7 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
471
478
|
: null
|
|
472
479
|
|
|
473
480
|
intro('canon sessions list')
|
|
474
|
-
reportConfidence(report)
|
|
481
|
+
reportConfidence(shown, report)
|
|
475
482
|
reportSessions(
|
|
476
483
|
shown,
|
|
477
484
|
opts.branch,
|
|
@@ -486,7 +493,8 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
486
493
|
process.stdout.write(
|
|
487
494
|
`${JSON.stringify({
|
|
488
495
|
dir: report.dir,
|
|
489
|
-
|
|
496
|
+
// Over the rows shown, so an unrelated row --branch dropped cannot weaken it.
|
|
497
|
+
confidence: confidenceOf(shown),
|
|
490
498
|
foreignDead: report.foreignDead,
|
|
491
499
|
branch: opts.branch ?? null,
|
|
492
500
|
repository,
|
|
@@ -528,6 +536,7 @@ function selfRefusal(own: Extract<SelfReport, { kind: 'unresolved' }>): string {
|
|
|
528
536
|
* addresses a session directly and the second opens by asking to be corrected.
|
|
529
537
|
*/
|
|
530
538
|
function reportConfidence(
|
|
539
|
+
shown: readonly ResolvedSession[],
|
|
531
540
|
report: Extract<SessionReport, { kind: 'resolved' }>,
|
|
532
541
|
): void {
|
|
533
542
|
logStep('Liveness')
|
|
@@ -535,23 +544,28 @@ function reportConfidence(
|
|
|
535
544
|
// A roster of none decided nothing, so the confirmed line would claim a check
|
|
536
545
|
// over rows that do not exist. The registry is never pruned and holds a record
|
|
537
546
|
// per session ever run, which is what makes the empty result worth stating.
|
|
538
|
-
if (
|
|
547
|
+
if (shown.length === 0) {
|
|
539
548
|
logInfo(
|
|
540
|
-
report.foreignDead > 0
|
|
549
|
+
report.foreignDead > 0 || report.sessions.length > 0
|
|
541
550
|
? 'No row to decide.'
|
|
542
551
|
: 'No row to decide. Every record belongs to a session that has ended.',
|
|
543
552
|
)
|
|
544
553
|
return
|
|
545
554
|
}
|
|
546
555
|
|
|
547
|
-
if (
|
|
556
|
+
if (confidenceOf(shown) === 'confirmed') {
|
|
557
|
+
const listened = shown.filter(
|
|
558
|
+
(session) => session.via === 'listener',
|
|
559
|
+
).length
|
|
548
560
|
logInfo(
|
|
549
|
-
|
|
561
|
+
listened === 0
|
|
562
|
+
? 'Every row matched a running process against the start time its record stamped.'
|
|
563
|
+
: `Every row is confirmed, ${listened} of them because their messaging socket accepted a connection and the rest by start time.`,
|
|
550
564
|
)
|
|
551
565
|
return
|
|
552
566
|
}
|
|
553
567
|
|
|
554
|
-
if (
|
|
568
|
+
if (shown.some((session) => session.via === 'socket')) {
|
|
555
569
|
logWarn(
|
|
556
570
|
'This reader sits in another pid namespace than some records, so those rows rest on a messaging socket existing alone. A crashed session leaves its socket behind, so treat the mapping as inferred and open by asking to be corrected.',
|
|
557
571
|
)
|
package/src/commands/tasks.ts
CHANGED
|
@@ -2195,6 +2195,7 @@ function report(
|
|
|
2195
2195
|
logInfo('retargeted the Ready: line')
|
|
2196
2196
|
}
|
|
2197
2197
|
if (outcome.priorityRowRemoved) logInfo('cleared the ordering row')
|
|
2198
|
+
if (outcome.backlogRowRemoved) logInfo('cleared the backlog row')
|
|
2198
2199
|
logRelinked(outcome.relinked, outcome.relinkFailed, root)
|
|
2199
2200
|
if (outcome.indexRegenerated) logInfo('regenerated index.md')
|
|
2200
2201
|
if (outcome.cut > 0) logInfo(`${outcome.cut} outcome(s) cut`)
|
|
@@ -2277,6 +2278,7 @@ function recordFor(
|
|
|
2277
2278
|
from: relative(root, outcome.from),
|
|
2278
2279
|
to: relative(root, outcome.to),
|
|
2279
2280
|
priorityRowRemoved: outcome.priorityRowRemoved,
|
|
2281
|
+
backlogRowRemoved: outcome.backlogRowRemoved,
|
|
2280
2282
|
indexRegenerated: outcome.indexRegenerated,
|
|
2281
2283
|
plans: planMovesFor(outcome.plans, root),
|
|
2282
2284
|
ready: outcome.ready
|